web-ui: remove dead ask->operator inline-answer binding from the per-agent terminal
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.
This commit is contained in:
parent
98d895cf9e
commit
ecf9ff4d80
4 changed files with 37 additions and 288 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 `<hive-side-panel>` 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 <details>, 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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue