agent/app.js + docs: migrate ask→operator inline-answer binding prose (#713 batch 1 cont.)

Moves the #666 ask→operator inline-answer wiring rationale from
app.js into a new docs/web-ui.md::Per-agent page sub-paragraph
**Ask → operator inline-answer binding**. Substantial block —
the slot-registry / reconcileAskBinds / buildAnswerForm trio has
real design rationale (async question id, text-match pairing,
resolved-vs-cancelled-vs-expired neutrality, defensive prune,
no-regression-fallback to side-panel answer) that belongs in
docs, not three JSDoc blocks scattered through app.js.

Moved to docs (~30 lines of substantive prose):
- pendingAskBinds slot-registry rationale
- reconcileAskBinds matching algorithm (text-match, first-unbound
  to first-unclaimed pairing stability)
- defensive prune for disconnected slots
- [resolved] vs ✓ rationale (answered / cancelled / TTL-expired
  ambiguity — neutral label)
- fallback to side-panel answer when slot stays unbound

Collapsed in app.js: 3 JSDoc blocks + inline comments → ~10-line
total pointer. Drops #666 ×3, #559, #668 cookies en passant
(substance now lives in docs).

app.js: 24 → 19 refs in this commit; 27 → 19 across batch 1
(30% reduction so far). Net ~57 lines of substantive prose
migrated from app.js to docs/web-ui.md across both commits in
this PR.
This commit is contained in:
iris 2026-05-31 14:32:11 +02:00 committed by mara
commit 3f84b6492a
2 changed files with 39 additions and 44 deletions

View file

@ -910,7 +910,7 @@ dashboard's side panel shape. Carries inbox and loose-ends flyouts
diffs, journald logs). Inbox flyout: last 30 messages addressed to
this agent (`AgentRequest::Recent { limit: 30 }`); reply messages
indented with `↳ reply ·` in amber. A `✓ mark all read` button
appears in the flyout header when the inbox is non-empty (#559);
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
@ -923,6 +923,31 @@ question rows carry an inline answer form that POSTs cross-origin to
the core dashboard's `/answer-question/{id}` so the operator answers
*as operator* (see `docs/boundary.md`).
**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">`)
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
`/api/loose-ends` refresh runs `reconcileAskBinds()`, which walks
the slot list and pairs each unbound slot with the first unclaimed
pending operator-bound question whose `question` text matches the
slot's stashed `_askQuestion`. On match the slot mounts the
`buildAnswerForm` (same form shape as the loose-ends flyout —
POSTs to the core's `/answer-question/{id}` cross-origin). Slots
stay in the array after binding so the reconciler can flip them
to a neutral `[resolved]` tag when the question later disappears
from the pending list. Disappearance can mean answered, cancelled
by the asker, or TTL-expired — the neutral label avoids
mis-asserting "✓" on the cancel / expire paths; full resolution
state is visible via the side-panel history. A defensive prune
walks the slot list each tick and drops any whose DOM node has
been removed (e.g. via a future "clear single row" affordance),
so stale references don't accumulate. Slots whose question never
arrives (e.g. the agent cancelled the ask, or the question is
older than the loose-ends retention window) stay empty — the
operator can still answer via the side panel, no regression.
### Live view
Each agent runs an `events::Bus`: a `tokio::sync::broadcast<LiveEvent>`