docs/agent.md: fix stale 3-flyout description; drop dead buildLooseEndsList

docs/web-ui/agent.md still described a "loose-ends" and "tasks" flyout
that were superseded by the todos flyout when loose-ends-v2 landed —
GET /api/loose-ends and GET /api/bash-tasks are both gone server-side.
Replaced with an accurate description of the todos flyout (including
the mark-done bulk action from #2919), and noted that the ask->operator
inline-answer binding this doc also describes is currently
non-functional (its data source was the same removed endpoint) —
tracked separately as #2922, not fixed here.

buildLooseEndsList in app.js rendered the old loose-ends flyout and had
zero call sites left; removed it. buildAnswerForm stays — reconcileAskBinds
still calls it, even though that path is currently dead per #2922.

Fixes #2920
This commit is contained in:
iris 2026-08-01 21:08:08 +02:00 committed by mara
commit 94fb42f2b6
2 changed files with 42 additions and 96 deletions

View file

@ -792,75 +792,12 @@ window.marked = marked;
/** ask operator inline-answer slot registry. See
* docs/web-ui.md::Per-agent page (Ask operator inline-answer
* binding) for the matching algorithm + resolution semantics. */
* binding) for the matching algorithm + resolution semantics
* currently non-functional (its data source was the old loose-ends
* endpoint, since removed tracked on the forge), kept for when
* that's resolved rather than ripped out speculatively. */
const pendingAskBinds = [];
function buildLooseEndsList(threads) {
// Returns the <div> the side panel renders. The structural shape
// mirrors the legacy <details>-collapsible block — same CSS rules
// apply via `hive-side-panel .agent-inbox`.
const wrap = el('div', { class: 'agent-inbox' });
if (!threads.length) {
wrap.append(el('p', { class: 'side-panel-empty' },
'no loose ends — every question, approval and reminder is resolved.'));
return wrap;
}
const list = el('ul');
const fmtAge = (s) => {
if (s < 60) return s + 's';
if (s < 3600) return Math.floor(s / 60) + 'm';
if (s < 86400) return Math.floor(s / 3600) + 'h';
return Math.floor(s / 86400) + 'd';
};
for (const t of threads) {
const li = el('li');
if (t.kind === 'approval') {
li.append(
el('span', { class: 'inbox-from' }, '◇ approval #' + t.id), ' ',
el('span', { class: 'inbox-sep' }, t.agent + ' @ ' + (t.commit_ref || '').slice(0, 12)), ' ',
el('span', { class: 'inbox-ts' }, fmtAge(t.age_seconds || 0) + ' ago'),
);
if (t.description) {
li.append(el('div', { class: 'inbox-body' }, t.description));
}
} else if (t.kind === 'question') {
const target = t.target || 'operator';
li.append(
el('span', { class: 'inbox-from' }, '? #' + t.id), ' ',
el('span', { class: 'inbox-sep' }, t.asker + ' → ' + target), ' ',
el('span', { class: 'inbox-ts' }, fmtAge(t.age_seconds || 0) + ' ago'),
el('div', { class: 'inbox-body' }, t.question || ''),
buildAnswerForm(t.id),
);
} else if (t.kind === 'reminder') {
// due_at arrives as an ISO 8601 string (DateTime<Utc> on the wire).
// Parse it to unix seconds before arithmetic — doing `t.due_at - now`
// directly yields NaN because a string minus a number is NaN in JS.
const now = Math.floor(Date.now() / 1000);
const dueAtSec = t.due_at ? Math.floor(new Date(t.due_at).getTime() / 1000) : 0;
const dueIn = dueAtSec - 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 if (t.kind === 'pending_messages') {
li.append(
el('span', { class: 'inbox-from' }, '✉ inbox'), ' ',
el('span', { class: 'inbox-sep' }, (t.count || 0) + ' pending message(s)'), ' ',
el('span', { class: 'inbox-ts' }, 'drain with recv'),
);
} else {
li.append(el('span', { class: 'inbox-body' }, JSON.stringify(t)));
}
list.append(li);
}
wrap.append(list);
return wrap;
}
/** Bulk "mark done" row for the todos flyout: select all / select none
* + a mark-done button, disabled until at least one row is checked.
* POSTs the checked ids (comma-joined into one field, same shape as
@ -1008,8 +945,9 @@ window.marked = marked;
// Inline "answer as operator" form for a question loose-end. POSTs to
// the host dashboard (core backend), never this agent's socket — the
// core is the only place that can stamp `operator` as the answerer.
// Used by both the loose-ends side panel (`buildLooseEndsList`) and
// the in-stream ask-row binder (`reconcileAskBinds`).
// Only live call site today is the in-stream ask-row binder
// (`reconcileAskBinds`) — see its doc comment above for why that path
// is currently non-functional.
function buildAnswerForm(id) {
const wrap = el('div', { class: 'answer-form' });
const ta = el('textarea', { rows: '2', placeholder: 'answer as operator…' });