From ecf9ff4d807573a19f98c6ee2b2afe6e04aa2615 Mon Sep 17 00:00:00 2001 From: iris Date: Tue, 11 Aug 2026 17:59:27 +0200 Subject: [PATCH] web-ui: remove dead ask->operator inline-answer binding from the per-agent terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hyperhive#2922: the inline-answer slot (pendingAskBinds/reconcileAskBinds/ buildAnswerForm) depended on a since-removed /api/loose-ends endpoint and had been silently non-functional the whole time β€” nothing ever reassigned lastLooseEnds, so the reconciler always no-op'd. Per mara's call on the issue (ask/answer is deprecated per #2850, only the main dashboard UI needs to work, remove the broken per-agent inline view): ripped out the dead JS (binding + form + CSS) rather than restoring the endpoint. An ask tool call now renders like any other tool call, no inline answer affordance; the operator answers via the dashboard's own question surfacing. Also fixed adjacent doc staleness this same removal made newly contradictory (docs/web-ui/agent.md, docs/terminal-rendering.md): the doc already described the loose-ends/bash-tasks flyouts and the ask-binding as recently dead, but the endpoint reference table and a header-pills bullet list still described them as live. --- docs/terminal-rendering.md | 32 +++---- docs/web-ui/agent.md | 71 +++++----------- frontend/packages/agent/src/agent.css | 103 +---------------------- frontend/packages/agent/src/app.js | 117 ++------------------------ 4 files changed, 36 insertions(+), 287 deletions(-) diff --git a/docs/terminal-rendering.md b/docs/terminal-rendering.md index 14a2e22e..5ff08259 100644 --- a/docs/terminal-rendering.md +++ b/docs/terminal-rendering.md @@ -59,7 +59,6 @@ parent's negative pull. | `.tool-use` (flat) | ` Name args…` | cyan | tool_use w/o rich renderer; `` from `toolIcon(name)`: πŸ“€ send Β· πŸ“₯ recv Β· ❓ ask Β· ⏰ remind Β· 🏷️ set_status Β· πŸͺ’ loose-ends Β· βœ‚οΈ cancel_loose_end Β· ℹ️ get_agent_meta Β· βœ… ack_until Β· πŸ“œ get_logs/get_host_journal Β· ↻ restart Β· ⏹️ kill Β· ▢️ start Β· πŸ”„ update Β· πŸ“‹ list_containers/list_rooms/list_room_members/list_invites Β· πŸ“– read_room/Read Β· πŸ‘οΈ mark_read Β· πŸ›‘ bash kill Β· πŸ–₯️ bash other Β· πŸ’¬ matrix send/reply/dm Β· πŸ“¦ request_* Β· ⏱️ schedule Β· πŸ”§ default | stream-json | | `.tool-use` `
` | `πŸ’Ύ/✏️ Write/Edit Β· +N` (no `β†’`) | cyan, body is +/- diff | `renderRichToolUse` Write/Edit | stream-json | | `.tool-use` `
` | `πŸ“€ send β†’ to Β· NL`, `❓ ask β†’ to`, `✍️ answer #id` | cyan, body is markdown | rich renderer for send / ask / answer | stream-json | -| `.tool-use .ask-answer-inline-slot` | (sub-block under `ask β†’ operator`) | inherits row | inline answer form bound by `reconcileAskBinds` to the loose-end | rich renderer | | `.tool-result` (flat) | `← ` | muted | short `tool_result` (≀120c, non-recv) | stream-json | | `.tool-result-block` `
` | `Nl Β· headline` | muted, body is text | long generic `tool_result` | stream-json | | `.tool-result-block` `
` | `recv ← ` | muted, body is markdown | `tool_result` correlated to a prior `recv` tool_use via id | stream-json | @@ -211,28 +210,17 @@ This keeps less-frequent tools that don't have a specific `fmtToolUse` case from dumping raw JSON. Common matrix and hyperhive tools have their own cases and skip this path. -## Inline ask-operator answer +## Inline ask-operator answer (removed) -When an agent calls `mcp__hyperhive__ask` with `to == "operator"` -(default), the rich tool-use renderer mounts an empty -`
` inside the row's expanded -body, then enqueues a loose-ends refresh. `reconcileAskBinds()` -runs on every loose-ends refresh, matches each waiting slot -against pending operator-bound questions by question text, and -injects an inline `.answer-form` (textarea + send button bound to -`/answer-question/` on the host dashboard) into the matching -slot. When a question subsequently leaves the pending list (answered, -cancelled by asker, or TTL-expired), the same reconciler -replaces the form with a struck-through `[resolved]` tag so the -scrollback reflects the closed state. The label is neutral -because `/api/loose-ends` only carries pending state β€” full -resolution detail is visible via the question's history in the -side panel. - -Lets the operator answer mid-flow without context-switching to the -loose-ends side panel or the dashboard tab. Side panel + dashboard -forms remain β€” they're the same `buildAnswerForm` factory, three -mount points for the same POST. +An `mcp__hyperhive__ask(to: "operator", ...)` row used to mount an +inline answer form (`.ask-answer-inline-slot`, `reconcileAskBinds()`) +directly in the terminal scrollback. It depended on a since-removed +`/api/loose-ends` endpoint and had been silently dead since that +removal (hyperhive#2922) β€” ripped out rather than rebuilt (mara: only +the main dashboard's own question surfacing needs to work). An `ask` +tool call now renders like any other tool call, with no inline answer +affordance; the operator answers via the dashboard +(`dashboard/src/swarm.js` + `call.js`). ## Dashboard side (not covered here) diff --git a/docs/web-ui/agent.md b/docs/web-ui/agent.md index e681625e..084575c1 100644 --- a/docs/web-ui/agent.md +++ b/docs/web-ui/agent.md @@ -62,15 +62,9 @@ through. Three flex columns: - **Right cluster** (`.agent-header-pills`): flyout pills + overflow. - **Inbox pill** (`πŸ“¬ inbox Β· N`): hidden when empty; click opens the inbox flyout in the side panel. - - **Loose-ends pill** (`πŸͺ’ loose ends Β· N`): hidden when empty; - click opens the loose-ends flyout. - - **Tasks pill** (`βš™ tasks Β· N`): hidden when empty; click opens the - running-bash-tasks flyout (`GET /api/bash-tasks` β€” the in-flight - Pending/Running tasks from the in-container `bash-tasks/` dir). Unlike - loose-ends (refreshed on turn_end), tasks start + finish asynchronously - between turns and `/api/state` isn't polled while online, so the pill - polls the endpoint on a light interval (β‰ˆ4s). Snapshot only (no SSE push - yet). + - **Todos pill** (`πŸ“‹ todos Β· N`): hidden when empty; click opens + the todos flyout β€” see the todos-flyout paragraph below + (`GET /api/todos`, refreshed on cold load + every `turn_end`). - **Overflow button** (`β‹―`): always visible. Opens a frosted popover (`#overflow-menu`, positioned outside the header to escape any stacking context) with four management rows followed by a model @@ -176,39 +170,17 @@ 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 (`
`) -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 `POST /api/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. +exist β€” todos superseded both. A third casualty of that migration β€” +an "ask β†’ operator" inline-answer form that used to mount under an +`mcp__hyperhive__ask(to: "operator", ...)` row in the terminal +scrollback β€” depended on the same removed `/api/loose-ends` endpoint +and was found dead (hyperhive#2922: the binding never fired, so the +slot never mounted a form). Per mara's call on that issue, it was +removed rather than rebuilt β€” the main dashboard's own question +surfacing (`dashboard/src/swarm.js` + `call.js`) is the one supported +path for answering a pending question as the operator; this agent's +own terminal just shows the `ask` tool call like any other tool call, +with no inline answer affordance. ## Live view @@ -302,8 +274,8 @@ Successful POSTs return 200 (no 303 redirects). Error responses use semantic status codes: **400** for missing/invalid input (`body` required, unknown model name, invalid effort level), **409** for retryable state conflicts (turn in flight when -`/compact` is called, hive-c0re busy on `GET /api/loose-ends`), -**500** only for genuine server/transport failures. The matching +`/compact` is called), **500** only for genuine server/transport +failures. The matching mutations fire `LiveEvent` variants on the per-agent bus, so the client doesn't refetch `/api/state` on submit β€” the SSE stream delivers the new state faster anyway. Only the login flow still @@ -371,13 +343,10 @@ shaped). rate_limited, links }`. Only called when the container is running; skipped (muted badges) when stopped. Also accessible via the gateway at `/agent//api/dashboard-state`. -- `GET /api/loose-ends` β€” loose-ends snapshot consumed by the inbox - flyout (`renderLooseEnds`). Returns pending questions the agent asked - or owes, plus pending reminders. Also calls `reconcileAskBinds()` to - wire inline answer forms to open `question_asked` events. -- `GET /api/bash-tasks` β€” in-flight bash-task snapshot; returns only - `Pending` and `Running` tasks (completed/killed tasks are excluded). - Consumed by the running-bash-tasks flyout. +- `GET /api/todos` β€” harness-local todos snapshot (loose-ends v2) + consumed by the todos flyout (`refreshTodos` / `buildTodosList`). + See the todos-flyout paragraph above for the producer/subsystem + model and the bulk mark-done row. - `GET /api/stats?window=24h|7d|30d|all` β€” time-bucketed turn analytics `Snapshot` consumed by the `/stats` page. `all` ranges from the earliest recorded turn with an adaptive bucket width. diff --git a/frontend/packages/agent/src/agent.css b/frontend/packages/agent/src/agent.css index 79f32625..411ed31a 100644 --- a/frontend/packages/agent/src/agent.css +++ b/frontend/packages/agent/src/agent.css @@ -560,112 +560,11 @@ pre.diff { } .agent-inbox .inbox-reply-tag { color: var(--muted); font-size: 0.85em; } -.agent-inbox .answer-form { - /* Block-level under the new layout β€” `grid-column: 1 / -1` was - for the legacy grid; under block layout the form naturally - starts on its own row. */ - display: flex; - gap: 0.4em; - align-items: flex-start; - margin-top: 0.5em; - padding-left: 0.8em; -} -.agent-inbox .answer-form textarea { - flex: 1; - font-family: inherit; - font-size: inherit; - background: var(--bg); - color: var(--fg); - border: 1px solid var(--border); - border-radius: 3px; - padding: 0.3em; - resize: vertical; -} -.agent-inbox .answer-form button { - font-family: inherit; - font-size: inherit; - background: var(--bg-elev); - color: var(--fg); - border: 1px solid var(--border); - border-radius: 3px; - padding: 0.3em 0.7em; - cursor: pointer; - white-space: nowrap; -} -.agent-inbox .answer-form button:hover:not(:disabled) { - border-color: var(--purple); - color: var(--purple); -} -.agent-inbox .answer-form button:disabled { opacity: 0.5; cursor: default; } -.agent-inbox .answer-status { color: var(--muted); align-self: center; } - -/* Inline answer slot mounted under each `ask β†’ operator` row in - the live terminal stream. Mirrors the side-panel `.answer-form` - look-and-feel β€” same textarea, same send button β€” so the operator - doesn't have to context-switch between "answering in the panel" - and "answering inline". Empty slot collapses to nothing (no - margin) so pre-bind rows stay tidy; populated slot gets a thin - top divider to separate the question body from the form. A - `[resolved]` tag (neutral, covers answered / cancelled / - TTL-expired uniformly) replaces the form once the question - leaves the pending list. */ -.live .ask-answer-inline-slot:empty { display: none; } -.live .ask-answer-inline-slot { - margin-top: 0.5em; - padding-top: 0.5em; - border-top: 1px dashed var(--border); -} -.live .ask-answer-inline-slot .answer-form { - display: flex; - gap: 0.4em; - align-items: flex-start; -} -.live .ask-answer-inline-slot .answer-form textarea { - flex: 1; - font-family: inherit; - font-size: inherit; - background: var(--bg); - color: var(--fg); - border: 1px solid var(--border); - border-radius: 3px; - padding: 0.3em; - resize: vertical; -} -.live .ask-answer-inline-slot .answer-form button { - font-family: inherit; - font-size: inherit; - background: var(--bg-elev); - color: var(--fg); - border: 1px solid var(--border); - border-radius: 3px; - padding: 0.3em 0.7em; - cursor: pointer; - white-space: nowrap; -} -.live .ask-answer-inline-slot .answer-form button:hover:not(:disabled) { - border-color: var(--purple); - color: var(--purple); -} -.live .ask-answer-inline-slot .answer-form button:disabled { - opacity: 0.5; - cursor: default; -} -.live .ask-answer-inline-slot .answer-status { - color: var(--muted); - align-self: center; -} -.live .ask-answer-inline-slot .ask-resolved-tag { - color: var(--muted); - font-style: italic; - text-decoration: line-through; - text-decoration-color: var(--muted); -} - /* Bulk-action header row: "mark all read" above the recent-messages list in the inbox flyout, and "select all / select none / mark done" above the list in the todos flyout β€” same classes, shared look (mauve hover, bg-elev background) so both read as part of - the same affordance family as the answer-form button. */ + the same affordance family. */ .agent-inbox .inbox-mark-all-row { display: flex; gap: 0.6em; diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js index c064a404..26192801 100644 --- a/frontend/packages/agent/src/app.js +++ b/frontend/packages/agent/src/app.js @@ -54,7 +54,7 @@ window.marked = marked; }); })(); - // ─── side panel (singleton drawer for inbox + loose-ends flyouts) ────── + // ─── side panel (singleton drawer for inbox + todos flyouts) ─────────── // The shared `` element (see @hive/shared/side-panel.js // for the chrome/behavior it owns), created once, eagerly, when this // module's IIFE runs (ES modules execute after the document is @@ -69,7 +69,7 @@ window.marked = marked; // Wire the header pills to open the side panel. Pre-built (vs // re-building per-click) so the freshest snapshot already lives - // in `lastInbox` / `lastLooseEnds` when the pill is clicked β€” even + // in `lastInbox` / `lastTodos` when the pill is clicked β€” even // if it fires during a turn the render is the same. (function bindHeaderPills() { const inboxPill = $('inbox-pill'); @@ -140,7 +140,7 @@ window.marked = marked; // ↑ dashboard β€” host dashboard back-link. The dashboard SPA lives at // `dashboard.html` (the `/` root now serves the H0M3 menu hub), so the // link targets that file; `dashUrl` itself stays the API origin used - // for the rebuild / answer-question / mark-all-read POSTs below. + // for the rebuild / mark-all-read POSTs below. menu.append(el('a', { class: 'overflow-item overflow-item-dashboard', href: dashUrl + 'dashboard.html', @@ -582,10 +582,6 @@ window.marked = marked; return true; case '/clear': termAPI.clear(); - // Detached `ask β†’ operator` rows no longer have a live - // mount-point in the DOM β€” drop their slots so subsequent - // loose-ends reconciliation doesn't walk dead references. - pendingAskBinds.length = 0; termAPI.row('note', 'Β· terminal cleared (local view only β€” server history kept)'); return true; case '/cancel': @@ -805,17 +801,8 @@ window.marked = marked; /** Latest snapshot kept in module state so the pill click handler * has fresh data to render into the panel without re-fetching. */ let lastTodos = []; - let lastLooseEnds = []; let lastInbox = []; - /** 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 β€” - * 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 = []; - /** 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 @@ -922,83 +909,8 @@ window.marked = marked; buildTodosList(todos)); } - /** Walk `pendingAskBinds` against the latest `lastLooseEnds` - * snapshot, pair unbound slots with the first unclaimed pending - * operator-bound question whose text matches, and flip already- - * bound slots to `[resolved]` when their question disappears. - * See docs/web-ui.md::Per-agent page (Ask β†’ operator inline-answer - * binding) for the full design + edge cases. */ - function reconcileAskBinds() { - if (!pendingAskBinds.length) return; - const pending = lastLooseEnds.filter( - (t) => t.kind === 'question' && (!t.target || t.target === 'operator'), - ); - // Defensive prune: drop slots whose DOM node has been removed. - for (let i = pendingAskBinds.length - 1; i >= 0; i--) { - if (!pendingAskBinds[i].isConnected) pendingAskBinds.splice(i, 1); - } - const claimed = new Set(); - for (const slot of pendingAskBinds) { - if (slot._boundId) claimed.add(slot._boundId); - } - for (const slot of pendingAskBinds) { - if (slot._boundId) { - const stillPending = pending.some((q) => q.id === slot._boundId); - if (!stillPending && !slot._resolvedShown) { - slot.replaceChildren(); - slot.appendChild(el('span', { class: 'ask-resolved-tag' }, '[resolved]')); - slot._resolvedShown = true; - } - continue; - } - const q = pending.find((p) => p.question === slot._askQuestion && !claimed.has(p.id)); - if (!q) continue; - claimed.add(q.id); - slot._boundId = q.id; - slot.replaceChildren(); - slot.appendChild(buildAnswerForm(q.id)); - } - } - - // 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. - // 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…' }); - const btn = el('button', { type: 'button' }, 'send answer'); - const status = el('span', { class: 'answer-status' }); - btn.addEventListener('click', () => { - const answer = ta.value.trim(); - if (!answer) { status.textContent = 'answer required'; return; } - if (!dashboardBase) { status.textContent = 'dashboard url unknown'; return; } - status.textContent = 'sending…'; - asyncBtn(btn, async () => { - try { - const resp = await fetch(dashboardBase + 'api/answer-question/' + id, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: 'answer=' + encodeURIComponent(answer), - }); - if (resp.ok) { - status.textContent = 'answered βœ“'; - } else { - status.textContent = 'failed: ' + (await resp.text()); - } - } catch (err) { - status.textContent = 'failed: ' + err; - } - }); - }); - wrap.append(ta, btn, status); - return wrap; - } - /** "mark all read" affordance for the agent's inbox flyout β€” - * see docs/web-ui.md::Per-agent page (Loose-ends flyout) for the + * see docs/web-ui.md::Per-agent page for the inbox flyout's * cross-origin POST + count rendering. Returns a DOM row * containing a button + an inline status pill; re-runs * `onCleared` on success so the caller can refresh its own state. */ @@ -1388,7 +1300,6 @@ window.marked = marked; // mcp__hyperhive__send: collapsed
, full body text inside. function renderRichToolUse(c, api) { const name = c.name || ''; - const input = c.input || {}; const icon = c._icon || 'πŸ”§'; // Message-bearing tools render default-open with a markdown body so // the operator sees the content without an extra click. send / ask @@ -1402,25 +1313,7 @@ window.marked = marked; return api.detailsDiff('tool-use', summary, c._body, icon); } if (c._body_type === 'markdown') { - const d = detailsOpenMd(api, 'tool-use', summary, c._body, icon); - // For ask β†’ operator: mount an inline answer slot in the live terminal - // β€” see docs/web-ui.md::Per-agent page (Ask β†’ operator inline-answer - // binding) for the slot registry + reconciler + [resolved] semantics. - // Body text and recipient are both available from backend fields. - if (name === 'mcp__hyperhive__ask') { - const to = (input.to || 'operator'); - if (to === 'operator') { - const slot = el('div', { class: 'ask-answer-inline-slot' }); - // Stash the question text on the slot so the reconciler - // can match against `lastLooseEnds` entries without - // walking the row's text content. - slot._askQuestion = c._body; - d.appendChild(slot); - pendingAskBinds.push(slot); - if (api.fromHistory) reconcileAskBinds(); - } - } - return d; + return detailsOpenMd(api, 'tool-use', summary, c._body, icon); } return api.details('tool-use', summary, c._body, icon); }