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:
parent
40845c21cd
commit
e0ea22f3ee
3 changed files with 76 additions and 361 deletions
|
|
@ -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 ↻`
|
||||
|
|
|
|||
Loading…
Reference in a new issue