refactor(#1292): remove agent-owned fields from ContainerView

Now that the dashboard fetches agent-owned state directly from
GET /api/dashboard-state (via gateway), hive-c0re no longer needs
to read those fields from disk on the agent's behalf.

Removed from ContainerView:
  ctx_tokens, context_window_tokens, rate_limited,
  extra_links, status_text, status_set_at

Removed from container_view.rs:
  DashboardLink struct, build_nav_links, read_dashboard_links,
  read_status, is_rate_limited, read_last_turn, resolve_ctx_window
  (and the resolve_ctx_window unit tests)

Removed from dashboard.rs:
  GET /api/agent/{name}/links route + get_agent_links handler

dashboard JS (tabs.js):
  Merged rate_limited, ctx-window badge, and status-text rendering
  into the async dashboard-state fetch block. c0re still provides
  needs_login (auth sentinel on host), needs_update, pending_reminders,
  running, deployed_sha, parent — all genuinely host-side fields.
This commit is contained in:
iris 2026-06-04 19:39:06 +02:00
commit e0ea22f3ee
3 changed files with 76 additions and 361 deletions

View file

@ -744,38 +744,79 @@ window.marked = marked;
: `http://${hostname}:${c.port}`;
if (c.running) {
// Fetch the lean dashboard-state snapshot from the agent directly.
// Fails gracefully (empty strip) when the agent is starting up
// or the gateway is not yet routing to it.
// Populates: nav strip links (including the screen link that
// c0re's disk-based build cannot detect), rate_limited badge,
// ctx-window badge, and self-reported status text.
// Fails gracefully when the agent is starting up or the gateway
// is not yet routing to it — badges simply don't appear.
fetch(`${containerBase}/api/dashboard-state`)
.then((r) => (r.ok ? r.json() : null))
.then((ds) => {
if (!ds || !Array.isArray(ds.links)) return;
for (const lnk of ds.links) {
const href = lnk.kind === 'forge' ? forgeBase + (lnk.url || '')
: lnk.kind === 'external' ? (lnk.url || '')
: /* container */ containerBase + '/' + (lnk.url || '');
const a = el('a', {
class: 'nav-link',
href,
target: '_blank',
rel: 'noopener',
title: lnk.label || '',
});
// Plain text — agent-controlled strings stay out of innerHTML.
a.textContent = lnk.icon || lnk.label || '';
navStrip.append(a);
if (!ds) return;
// ── nav strip ───────────────────────────────────────────
if (Array.isArray(ds.links)) {
for (const lnk of ds.links) {
const href = lnk.kind === 'forge' ? forgeBase + (lnk.url || '')
: lnk.kind === 'external' ? (lnk.url || '')
: /* container */ containerBase + '/' + (lnk.url || '');
const a = el('a', {
class: 'nav-link',
href,
target: '_blank',
rel: 'noopener',
title: lnk.label || '',
});
// Plain text — agent-controlled strings stay out of innerHTML.
a.textContent = lnk.icon || lnk.label || '';
navStrip.append(a);
}
}
// ── agent-owned status badges ────────────────────────────
// rate_limited: only show when no other critical badge is
// already shown (pending / not-running already handled sync).
if (ds.rate_limited) {
head.append(el('span',
{ class: 'badge badge-rate-limited', title: 'API rate-limited — harness is parked, will retry automatically' },
'⊘ rate limited'));
}
// ctx-window badge
if (ds.ctx_tokens != null) {
const k = Math.round(ds.ctx_tokens / 1000);
const win = ds.context_window_tokens;
const warn = win != null ? win * CTX_WARN_FRACTION : CTX_WARN_TOKENS;
const caution = win != null ? win * CTX_CAUTION_FRACTION : CTX_CAUTION_TOKENS;
const ctxClass = ds.ctx_tokens >= warn ? 'badge-ctx-warn'
: ds.ctx_tokens >= caution ? 'badge-ctx-caution'
: 'badge-ctx-ok';
const title = win != null
? `last turn context: ${ds.ctx_tokens.toLocaleString()} / ${win.toLocaleString()} `
+ `tokens (${Math.round((ds.ctx_tokens / win) * 100)}% of the window)`
: `last turn context size: ${ds.ctx_tokens.toLocaleString()} tokens`;
head.append(el('span', { class: `badge ${ctxClass}`, title }, `ctx·${k}k`));
}
// ── agent status text (self-reported via set_status) ─────
if (ds.status_text) {
const nowUnix = Math.floor(Date.now() / 1000);
const ageStr = ds.status_set_at != null
? ` (set ${fmtAgeSecs(nowUnix - ds.status_set_at)} ago)` : '';
body.append(el('div', {
class: 'agent-status',
title: `agent self-reported status${ageStr}`,
},
el('span', { class: 'status-icon' }, '◈ '),
ds.status_text,
el('span', { class: 'status-age' }, ageStr),
));
}
})
.catch(() => { /* graceful: agent starting / gateway miss → no strip */ });
.catch(() => { /* graceful: agent starting / gateway miss → no data */ });
}
// Status / runtime badges. Pending transients always win
// (start / stop / restart / rebuild is in progress). Otherwise,
// when the container is stopped, surface a single `■ not
// running` badge; the backend has already cleared rate_limited /
// needs_login / ctx_tokens / status_text in that case (see
// docs/web-ui.md::Container row) so the rest of the chain is a
// no-op for stopped containers — but we still want SOME badge
// there so the row doesn't look empty.
// when the container is stopped, surface a single `■ not running`
// badge. `needs_login` is still c0re-owned (reads auth sentinel
// files on the host). rate_limited / ctx / status_text are
// agent-owned and rendered by the async dashboard-state fetch above.
if (pending) {
head.append(el('span', { class: 'pending-state' },
el('span', { class: 'spinner' }, '◐'), ' ', pending + '…'));
@ -783,10 +824,6 @@ window.marked = marked;
head.append(el('span',
{ class: 'badge badge-muted', title: 'container is shut down — start it to bring the harness back up' },
'■ not running'));
} else if (c.rate_limited) {
head.append(el('span',
{ class: 'badge badge-rate-limited', title: 'API rate-limited — harness is parked, will retry automatically' },
'⊘ rate limited'));
} else if (c.needs_login) {
head.append(el('a',
{ class: 'badge badge-warn', href: url, target: '_blank', rel: 'noopener' },
@ -800,7 +837,6 @@ window.marked = marked;
));
}
if (c.pending_reminders && c.pending_reminders > 0) {
head.append(el('span',
{
@ -829,46 +865,8 @@ window.marked = marked;
},
`${agentQCount}`));
}
if (c.ctx_tokens != null) {
const k = Math.round(c.ctx_tokens / 1000);
// Thresholds track the model's real context window when the
// backend supplies it; otherwise fall back to fixed constants.
const win = c.context_window_tokens;
const warn = win != null ? win * CTX_WARN_FRACTION : CTX_WARN_TOKENS;
const caution = win != null ? win * CTX_CAUTION_FRACTION : CTX_CAUTION_TOKENS;
const ctxClass = c.ctx_tokens >= warn ? 'badge-ctx-warn'
: c.ctx_tokens >= caution ? 'badge-ctx-caution'
: 'badge-ctx-ok';
const title = win != null
? `last turn context: ${c.ctx_tokens.toLocaleString()} / ${win.toLocaleString()} `
+ `tokens (${Math.round((c.ctx_tokens / win) * 100)}% of the window)`
: `last turn context size: ${c.ctx_tokens.toLocaleString()} tokens`;
head.append(el('span',
{ class: `badge ${ctxClass}`, title },
`ctx·${k}k`));
}
body.append(head);
// ── agent status text ─────────────────────────────────────────
// Self-reported status (via set_status MCP tool) — only fresh
// while the harness is up. The backend already clears
// `status_text` on stopped containers (docs/web-ui.md::Container
// row) so we can render unconditionally here: a stopped
// container simply has no `status_text` and skips naturally.
if (c.status_text) {
const nowUnix = Math.floor(Date.now() / 1000);
const ageStr = c.status_set_at != null
? ` (set ${fmtAgeSecs(nowUnix - c.status_set_at)} ago)` : '';
body.append(el('div', {
class: 'agent-status',
title: `agent self-reported status${ageStr}`,
},
el('span', { class: 'status-icon' }, '◈ '),
c.status_text,
el('span', { class: 'status-age' }, ageStr),
));
}
// Per-card action buttons (R3ST4RT / ST0P / ST4RT / R3BU1LD /
// DESTR0Y / PURG3) moved to the selection bar — see
// docs/web-ui.md::Selection bar. The contextual `needs update ↻`