agent/app.js: final cookie scrub — 0 cookies (#713 batch 3 final)

All 19 remaining cookies in app.js were attribution refs to
closed issues / PRs / past mara/argus reviews. Substance is
already in docs from earlier batches (PR #780). This batch
collapses the cookie attributions to brief inline notes pointing
at docs where appropriate.

- #360 side-panel duplication note → drop closed-issue framing
- #394 vibec0re header redesign (×5) → drop cookies, prose stays
- #589 hive-qualified label glyphic-title note → drop cookie
- #576/#542/#584 logout chain rationale → drop cookies, prose
  preserved (the WHY + .credentials.json + mcp-needs-auth-
  cache.json detail stays inline)
- #14 relative paths convention → docs/web-ui.md::Per-agent
  relative paths pointer
- #233 marked autolinks new-tab → drop cookie
- #559/#566 mark-all-read flyout (×3) → docs/web-ui.md::Per-agent
  page (Loose-ends flyout) pointer + drop attribution
- #666 ask→operator inline-answer slot (×2) → docs/web-ui.md::
  Per-agent page (Ask → operator inline-answer binding) pointer

app.js: 19 → 0 cookies (100% reduction). Combined with PR #780
+ batch 1 + 2, app.js is fully migrated for #713 scope.

Closes #713 — all agent UI files (app.js, agent.css, screen.html,
index.html, stats.js, stats.html) at 0 issue-ref cookies.
This commit is contained in:
iris 2026-05-31 15:23:08 +02:00 committed by mara
commit 4cee50a3be

View file

@ -79,8 +79,7 @@ window.marked = marked;
// ─── side panel (singleton drawer for inbox + loose-ends flyouts) ──────
// Shared shape with the dashboard's panel. Candidate for extraction
// into @hive/shared in a follow-up — keeping the duplication for
// now to land #360 without simultaneously refactoring the dashboard.
// into @hive/shared once both surfaces stabilise.
const Panel = (() => {
const root = $('side-panel');
const titleEl = $('side-panel-title');
@ -141,13 +140,12 @@ window.marked = marked;
// ─── state rendering ────────────────────────────────────────────────────
function setHeader(label, qualifiedLabel, dashboardPort) {
const title = $('title');
// Title is now just the glowing identity glyph — DASHB04RD,
// R3BU1LD, NEW SESSION all live in the overflow `⋯` menu now
// (#394). Glow + uppercase styling from h2 / .agent-header-title-row.
// The glyphic title stays short (no @hive suffix) — the hive
// qualifier lives on the second row's `qualified` chip + the
// browser tab title so the cinematic header reads cleanly at a
// glance (#589).
// Title is just the glowing identity glyph — DASHB04RD, R3BU1LD,
// NEW SESSION live in the overflow `⋯` menu. Glow + uppercase
// styling from h2 / .agent-header-title-row. The glyphic title
// stays short (no @hive suffix) — the hive qualifier lives on
// the second row's `qualified` chip + the browser tab title so
// the cinematic header reads cleanly at a glance.
title.textContent = `${label}`;
// Document title carries the qualified name so the browser's tab
// bar disambiguates between same-named agents on different hives
@ -160,20 +158,17 @@ window.marked = marked;
populateOverflowMenu(label, dashUrl);
}
// Overflow popover: dashboard back-link + rebuild + new-session.
// Per #394 mara's spec — rebuild + new-session both moved off the
// header strip into the `⋯` menu (rare actions, both destructive
// enough to warrant one extra click; the operator rebuilds from
// the host dashboard normally). Dashboard link also slotted in so
// every "leave this page" action lives in one menu.
// Overflow popover: dashboard back-link + rebuild + new-session +
// logout. Rare + destructive actions live here behind one extra
// click (the operator rebuilds from the host dashboard normally).
// See docs/web-ui.md::Per-agent page (Overflow button).
let overflowMenuPopulated = false;
function populateOverflowMenu(label, dashUrl) {
const menu = $('overflow-menu');
if (!menu) return;
menu.replaceChildren();
// ↑ dashboard — host dashboard back-link (was `.btn-dashlink`
// beside the title pre-#394).
// ↑ dashboard — host dashboard back-link.
menu.append(el('a', {
class: 'overflow-item overflow-item-dashboard',
href: dashUrl,
@ -227,15 +222,14 @@ window.marked = marked;
});
menu.append(newSessBtn);
// 🔓 logout (#576) — SIGINTs claude, wipes the credentials dir,
// flips the harness LoginState to NeedsLogin. The turn loop's
// next iteration parks in wait_for_login; a fresh `claude auth
// login` from the dashboard re-arms it (#542 mtime resumption).
// Operator has to re-paste OAuth creds on the login screen after,
// but the --continue session history is preserved (#584 narrowed
// the backend wipe to just .credentials.json + mcp-needs-auth-
// cache.json) — so the agent picks up where it left off on the
// next turn after re-login.
// 🔓 logout — SIGINTs claude, wipes the credentials dir, flips
// the harness LoginState to NeedsLogin. The turn loop's next
// iteration parks in wait_for_login; a fresh `claude auth login`
// from the dashboard re-arms it. Operator has to re-paste OAuth
// creds on the login screen after, but the --continue session
// history is preserved (the backend wipe is narrowed to just
// .credentials.json + mcp-needs-auth-cache.json) — so the agent
// picks up where it left off on the next turn after re-login.
const logoutBtn = el('button', {
type: 'button',
class: 'overflow-item overflow-item-logout',
@ -464,8 +458,9 @@ window.marked = marked;
if (termAPI) termAPI.row('turn-end-fail', '✗ ' + label + ' failed: ' + err);
}
}
// First arg is the URL path (relative to document base, #14);
// second is the slash-command label rendered in the local note.
// First arg is the URL path (relative to document base — see
// docs/web-ui.md::Per-agent relative paths); second is the
// slash-command label rendered in the local note.
const postCancelTurn = () => postSimple('api/cancel', '/cancel');
const postCompact = () => postSimple('api/compact', '/compact');
const postNewSession = () => postSimple('api/new-session', '/new-session');
@ -485,7 +480,7 @@ window.marked = marked;
return true;
case '/clear':
termAPI.clear();
// #666: detached `ask → operator` rows no longer have a live
// 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;
@ -837,12 +832,11 @@ window.marked = marked;
return wrap;
}
/** #559: "mark all read" affordance for the agent's inbox flyout.
* Returns a DOM row containing a button + an inline status pill.
* POSTs to the host dashboard's `/api/agent/{name}/mark-all-read`
* (damocles PR #566) and surfaces the `{ marked: N }` count back to
* the operator. Re-runs `onCleared` on success so the caller can
* refresh whatever state it owns. */
/** "mark all read" affordance for the agent's inbox flyout
* see docs/web-ui.md::Per-agent page (Loose-ends flyout) for the
* 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. */
function buildInboxMarkAllRow(label, onCleared) {
const status = el('span', { class: 'inbox-mark-status' });
const btn = el('button', {
@ -896,12 +890,12 @@ window.marked = marked;
'inbox empty.'));
return wrap;
}
// #559: "mark all read" header row drains the host broker's
// pending + delivered-unacked rows for this agent (damocles PR
// #566). Visible rows here are the most-recent-N regardless of
// ack state, so the list itself doesn't visually empty on click —
// the status pill confirms the drain count, and the next
// turn_start's "unread" badge will read zero.
// "mark all read" header row drains the host broker's pending +
// delivered-unacked rows for this agent. Visible rows here are
// the most-recent-N regardless of ack state, so the list itself
// doesn't visually empty on click — the status pill confirms
// the drain count, and the next turn_start's "unread" badge
// will read zero.
wrap.append(buildInboxMarkAllRow(currentLabel, () => {
// Refresh state so any UI surface that DOES depend on
// delivery state (eg future per-status filters) picks up
@ -1029,10 +1023,6 @@ window.marked = marked;
});
})();
// (#394) — `↻ new session` button moved into the overflow `⋯`
// menu and wired by `populateOverflowMenu()` above. Previously
// wired here as a static `#new-session-btn` in index.html.
// Track banner activity by reference-counting in-flight turns. A turn
// can begin while the previous turn_end is still in the pipeline (rare
// but happens on tight wake cycles), so we count rather than toggle.
@ -1074,9 +1064,8 @@ window.marked = marked;
rel: 'noopener',
title: lnk.label || '',
});
// Layout gap comes from `.agent-nav { gap }` (#394) — drop
// the legacy per-link inline `marginLeft`. The trailing `→`
// is the "leaves this page" affordance.
// Layout gap comes from `.agent-nav { gap }`. The trailing
// `→` is the "leaves this page" affordance.
a.append(((lnk.icon || '') + ' ' + (lnk.label || '')).trim() + ' →');
metaLinks.append(a);
});
@ -1154,7 +1143,7 @@ window.marked = marked;
marked.setOptions({ breaks: true, gfm: true });
div.innerHTML = marked.parse(src);
// marked autolinks URLs but leaves them same-tab — open them
// externally so a click never unloads the terminal. (issue #233)
// externally so a click never unloads the terminal.
div.querySelectorAll('a[href]').forEach((a) => {
a.target = '_blank';
a.rel = 'noopener noreferrer';
@ -1306,15 +1295,10 @@ window.marked = marked;
const d = detailsOpenMd(api, 'tool-use',
'ask → ' + to + (lines > 1 ? ` · ${lines}L` : ''),
q);
// #666: when the ask targets the operator, mount an inline
// answer slot in the live terminal so the operator doesn't
// need to open the loose-ends side panel (or jump to Y3R C4LL
// on the dashboard) to respond. The slot starts empty and
// gets populated by `reconcileAskBinds()` once the
// loose-ends fetch identifies a matching pending question
// (by asker == this agent + question text). Resolved
// questions render as a struck-through [answered ✓] tag
// instead of a form.
// When the ask targets the 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.
if (to === 'operator') {
const slot = el('div', { class: 'ask-answer-inline-slot' });
// Stash the question text on the slot so the reconciler
@ -1356,12 +1340,13 @@ window.marked = marked;
: (c.content || '');
const sourceName = c.tool_use_id ? toolNameById.get(c.tool_use_id) : null;
const isMessageBearing = sourceName === 'mcp__hyperhive__recv';
// #666: when an ask's tool_result lands the broker has just
// persisted the question with its assigned id. Refresh loose
// ends so `reconcileAskBinds` finds the new entry and mounts
// the inline answer form under the rendered ask row. Skipped
// during history replay (the question's likely long-resolved;
// turn_end refresh on cold-load already covers reconciliation).
// When an ask's tool_result lands the broker has just
// persisted the question with its assigned id. Refresh
// loose-ends so reconcileAskBinds finds the new entry and
// mounts the inline answer form under the rendered ask row.
// Skipped during history replay (the question's likely
// long-resolved; turn_end refresh on cold-load covers
// reconciliation).
if (sourceName === 'mcp__hyperhive__ask' && !api.fromHistory) {
refreshLooseEnds();
}