rename: open_threads → loose_ends + cancel_thread → cancel_loose_end across wire / tools / web ui

This commit is contained in:
damocles 2026-05-18 18:22:49 +02:00
parent b1d0a62cb9
commit 6e23d087d2
16 changed files with 152 additions and 139 deletions

View file

@ -393,43 +393,43 @@
}
renderStateBadge();
}
// Open-threads section: same data the get_open_threads MCP tool
// Loose-ends section: same data the get_loose_ends MCP tool
// returns. Best-effort fetch on cold load + after every turn_end
// (a turn likely answered or asked something). Silent failure
// keeps the section hidden rather than surfacing an empty banner.
let lastOpenThreadsCount = 0;
async function refreshOpenThreads() {
let lastLooseEndsCount = 0;
async function refreshLooseEnds() {
try {
const resp = await fetch('/api/open-threads');
const resp = await fetch('/api/loose-ends');
if (!resp.ok) {
renderOpenThreads([]);
renderLooseEnds([]);
return;
}
const data = await resp.json();
renderOpenThreads(data.threads || []);
renderLooseEnds(data.loose_ends || []);
} catch (err) {
console.warn('open-threads fetch failed', err);
renderOpenThreads([]);
console.warn('loose-ends fetch failed', err);
renderLooseEnds([]);
}
}
function renderOpenThreads(threads) {
const root = $('open-threads-section');
const list = $('open-threads-list');
const summary = $('open-threads-summary');
function renderLooseEnds(threads) {
const root = $('loose-ends-section');
const list = $('loose-ends-list');
const summary = $('loose-ends-summary');
if (!root || !list || !summary) return;
if (!threads.length) {
root.hidden = true;
lastOpenThreadsCount = 0;
lastLooseEndsCount = 0;
return;
}
root.hidden = false;
summary.textContent = 'open threads · ' + threads.length;
summary.textContent = 'loose ends · ' + threads.length;
list.innerHTML = '';
// Auto-expand on first appearance of any open thread so the
// operator notices new loose ends; collapse only on operator
// click (sticky after that).
if (lastOpenThreadsCount === 0) root.open = true;
lastOpenThreadsCount = threads.length;
if (lastLooseEndsCount === 0) root.open = true;
lastLooseEndsCount = threads.length;
const fmtAge = (s) => {
if (s < 60) return s + 's';
if (s < 3600) return Math.floor(s / 60) + 'm';
@ -455,6 +455,18 @@
el('span', { class: 'inbox-ts' }, fmtAge(t.age_seconds || 0) + ' ago'),
el('div', { class: 'inbox-body' }, t.question || ''),
);
} else if (t.kind === 'reminder') {
// due_at is an absolute unix-seconds value; show time-until-fire
// (negative when overdue, fmtAge handles 0/positive case here).
const now = Math.floor(Date.now() / 1000);
const dueIn = (t.due_at || 0) - now;
const dueLabel = dueIn >= 0 ? 'in ' + fmtAge(dueIn) : fmtAge(-dueIn) + ' overdue';
li.append(
el('span', { class: 'inbox-from' }, '⏰ reminder #' + t.id), ' ',
el('span', { class: 'inbox-sep' }, t.owner + ' · due ' + dueLabel), ' ',
el('span', { class: 'inbox-ts' }, 'scheduled ' + fmtAge(t.age_seconds || 0) + ' ago'),
el('div', { class: 'inbox-body' }, t.message || ''),
);
} else {
li.append(el('span', { class: 'inbox-body' }, JSON.stringify(t)));
}
@ -614,7 +626,7 @@
// Open-threads aren't part of /api/state (kept on the broker
// db, fetched via the per-agent socket). Cold-load fetches
// it here; turn_end refreshes it via the renderer below.
refreshOpenThreads();
refreshLooseEnds();
// Skip the re-render if nothing structurally changed. The most
// common case is `online` polling itself — without this guard, the
// operator's <input value> gets clobbered every cycle.
@ -956,7 +968,7 @@
} else {
setBannerActive(false); setState('idle');
// Likely answered/asked/scheduled something — refresh.
refreshOpenThreads();
refreshLooseEnds();
}
const cls = ev.ok ? 'turn-end-ok' : 'turn-end-fail';
api.row(cls,