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:
parent
77e30dd1bc
commit
94fb42f2b6
2 changed files with 42 additions and 96 deletions
|
|
@ -147,34 +147,42 @@ newlines, Tab-completes slash commands (see "Terminal-embedded
|
|||
prompt" below).
|
||||
|
||||
**Side panel** (slide-in from right): singleton shared with the
|
||||
dashboard's side panel shape. Carries inbox and loose-ends flyouts
|
||||
(opened via the header pills) as well as long content (file previews,
|
||||
diffs, journald logs). Inbox flyout: unread messages addressed to
|
||||
this agent (`acked_at IS NULL`, newest-first, up to 30); reply
|
||||
messages indented with `↳ reply ·` in amber. A `✓ mark all read`
|
||||
button appears in the flyout header when the inbox is non-empty;
|
||||
clicking it confirms then POSTs cross-origin to the core
|
||||
dashboard's `POST /api/agent/{name}/mark-all-read` — all pending
|
||||
messages for this agent are acked, the harness won't receive
|
||||
wake-prompts for them. A `{ marked: N }` pill surfaces the count.
|
||||
After the drain the inbox list empties on reload (the filter is
|
||||
`acked_at IS NULL`, so drained messages disappear). Loose-ends flyout: questions,
|
||||
approvals, reminders, pending inbox messages, and unread matrix
|
||||
notifications pending against this agent (`GET /api/loose-ends`);
|
||||
question rows carry an inline answer form that POSTs cross-origin to
|
||||
the core dashboard's `POST /api/answer-question/{id}` so the operator answers
|
||||
*as operator* (see `docs/boundary.md`). Pending inbox messages (`✉ inbox
|
||||
— N pending message(s)`) and unread matrix rooms appear informational
|
||||
only (not cancellable from the flyout — drain with `recv` / `mark_read`
|
||||
in-turn). Tasks flyout: in-flight bash
|
||||
tasks (`GET /api/bash-tasks`); each row shows status (`▶ running` /
|
||||
`◷ queued`), the task id, elapsed time, and a truncated one-line
|
||||
command preview. Read-only — kill/inspect lives in the harness, not
|
||||
the page.
|
||||
dashboard's side panel shape. Carries inbox and todos flyouts (opened
|
||||
via the header pills) as well as long content (file previews, diffs,
|
||||
journald logs). Inbox flyout: unread messages addressed to this agent
|
||||
(`acked_at IS NULL`, newest-first, up to 30); reply messages indented
|
||||
with `↳ reply ·` in amber. A `✓ mark all read` button appears in the
|
||||
flyout header when the inbox is non-empty; clicking it confirms then
|
||||
POSTs cross-origin to the core dashboard's `POST
|
||||
/api/agent/{name}/mark-all-read` — all pending messages for this agent
|
||||
are acked, the harness won't receive wake-prompts for them. A `{
|
||||
marked: N }` pill surfaces the count. After the drain the inbox list
|
||||
empties on reload (the filter is `acked_at IS NULL`, so drained
|
||||
messages disappear). Todos flyout ("loose-ends v2"): the harness-local
|
||||
todos other subsystems (matrix, forge, bash) push at this agent
|
||||
(`GET /api/todos`) — each row shows the producing subsystem, an
|
||||
optional source label, a summary, and age. A checkbox per row plus a
|
||||
select-all / select-none / `✓ mark done` bulk row above the list POSTs
|
||||
the checked ids to `POST /api/todos/mark-done`, which dismisses them
|
||||
from the harness-local store (same effect as `cancel_loose_end(kind:
|
||||
"todo")`, just from the web UI instead of the agent's own tool calls).
|
||||
|
||||
**Ask → operator inline-answer binding.** When the agent emits
|
||||
`mcp__hyperhive__ask(to: "operator", ...)`, the tool_use renderer
|
||||
mounts an empty slot (`<div class="ask-answer-inline-slot">`)
|
||||
Older per-agent flyouts this doc used to describe (a "loose-ends"
|
||||
list of questions/approvals/reminders backed by a since-removed
|
||||
`GET /api/loose-ends`, and a read-only "tasks" list of in-flight bash
|
||||
tasks backed by a since-removed `GET /api/bash-tasks`) no longer
|
||||
exist — todos superseded both. One open gap from that migration:
|
||||
the "Ask → operator inline-answer binding" described below relies on
|
||||
that removed `/api/loose-ends` endpoint and is currently non-functional
|
||||
(hyperhive#2922) — the operator can still answer a pending question via
|
||||
the main dashboard's own question surfacing, just not inline in this
|
||||
agent's terminal.
|
||||
|
||||
**Ask → operator inline-answer binding** (currently non-functional,
|
||||
see the todos-flyout paragraph above + hyperhive#2922 — described here
|
||||
for the intended design, which is what a fix would restore). When the
|
||||
agent emits `mcp__hyperhive__ask(to: "operator", ...)`, the tool_use
|
||||
renderer mounts an empty slot (`<div class="ask-answer-inline-slot">`)
|
||||
right under the `↳ ask → operator` row in the terminal scrollback
|
||||
and pushes a reference into `pendingAskBinds`. The broker assigns
|
||||
the question id asynchronously, so the slot waits — and the next
|
||||
|
|
|
|||
|
|
@ -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…' });
|
||||
|
|
|
|||
Loading…
Reference in a new issue