Compare commits

..
9 changed files with 73 additions and 97 deletions

View file

@ -6,10 +6,6 @@
agent SUB-pages (stats, screen) the same back-link nav the dashboard's agent SUB-pages (stats, screen) the same back-link nav the dashboard's
standalone pages use. The live terminal page keeps its own header. */ standalone pages use. The live terminal page keeps its own header. */
@import "@hive/shared/chrome.css"; @import "@hive/shared/chrome.css";
/* Shared pill/chip/badge shape (the "extract pill component" issue,
resolved as shared CSS rather than a JS component) see pill.css
itself for the classnames it covers. */
@import "@hive/shared/pill.css";
/* Themed dialog/toast component CSS (modal.js) now lives in each /* Themed dialog/toast component CSS (modal.js) now lives in each
component's own shadow root (adoptedStyleSheets), not a global component's own shadow root (adoptedStyleSheets), not a global
stylesheet nothing to @import here any more. */ stylesheet nothing to @import here any more. */
@ -304,16 +300,16 @@ h2, h3 {
background: color-mix(in srgb, var(--purple) 6%, transparent); background: color-mix(in srgb, var(--purple) 6%, transparent);
} }
/* Header pill inbox / loose-ends triggers. Compact, count-prominent. /* Header pill — inbox / loose-ends triggers. Compact, count-prominent. */
Markup carries `hive-pill` alongside `header-pill` (shared pill.css
supplies border/radius/padding/font-size/letter-spacing; `display`
is the one thing overridden here, to `inline-flex`, for the icon+
label+count row layout see pill.css's own note on that). */
.header-pill { .header-pill {
background: transparent; background: transparent;
border-color: var(--purple-dim); border: 1px solid var(--purple-dim);
color: var(--fg); color: var(--fg);
font-family: inherit; font-family: inherit;
font-size: 0.85em;
letter-spacing: 0.04em;
border-radius: 999px;
padding: 0.25em 0.7em;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.4em; gap: 0.4em;
@ -606,11 +602,14 @@ pre.diff {
font-size: 0.8em; font-size: 0.8em;
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
/* Markup carries `hive-pill-sm` alongside each of these (shared
pill.css supplies the shape); rules below are colour only. */
.model-chip, .model-chip,
.effort-chip { .effort-chip {
border-color: var(--purple-dim); display: inline-block;
padding: 0.1em 0.6em;
border: 1px solid var(--purple-dim);
border-radius: 999px;
font-size: 0.78em;
letter-spacing: 0.04em;
} }
.model-chip { color: var(--cyan); } .model-chip { color: var(--cyan); }
.effort-chip { color: var(--amber); } .effort-chip { color: var(--amber); }
@ -619,15 +618,28 @@ pre.diff {
breakdown on hover. Sized/coloured like a peer of model-chip so breakdown on hover. Sized/coloured like a peer of model-chip so
the state row reads as one row of chrome. */ the state row reads as one row of chrome. */
.ctx-badge { .ctx-badge {
border-color: var(--purple-dim); display: inline-block;
padding: 0.1em 0.6em;
border: 1px solid var(--purple-dim);
border-radius: 999px;
color: var(--green); color: var(--green);
font-size: 0.78em;
letter-spacing: 0.04em;
cursor: default; cursor: default;
white-space: pre-line; white-space: pre-line;
} }
/* Harness reachability badge. Markup carries `hive-pill` alongside /* Harness reachability badge. Same chip shape + sizing as
`status-badge` (shared pill.css supplies the shape); colour `.state-badge` / `.model-chip` so the state row stays visually
communicates the actual reachability state. */ uniform; colour communicates the actual reachability state. */
.status-badge {
display: inline-block;
padding: 0.25em 0.8em;
border: 1px solid;
border-radius: 999px;
font-size: 0.85em;
letter-spacing: 0.05em;
}
.status-badge.status-loading { color: var(--muted); border-color: var(--purple-dim); } .status-badge.status-loading { color: var(--muted); border-color: var(--purple-dim); }
.status-badge.status-online { color: var(--green); border-color: var(--green); .status-badge.status-online { color: var(--green); border-color: var(--green);
text-shadow: 0 0 6px color-mix(in srgb, var(--green) 55%, transparent); } text-shadow: 0 0 6px color-mix(in srgb, var(--green) 55%, transparent); }
@ -659,10 +671,13 @@ pre.diff {
the overflow menu (`.overflow-item-new-session` covers it; the the overflow menu (`.overflow-item-new-session` covers it; the
`:disabled` opacity treatment lives on the shared `:disabled` opacity treatment lives on the shared
`.overflow-item:disabled` rule). */ `.overflow-item:disabled` rule). */
/* Markup carries `hive-pill` alongside `state-badge` (shared pill.css
supplies the shape); this rule is only the transition + state-
specific colour below. */
.state-badge { .state-badge {
display: inline-block;
padding: 0.25em 0.8em;
border: 1px solid;
border-radius: 999px;
font-size: 0.85em;
letter-spacing: 0.05em;
transition: color 280ms ease, border-color 280ms ease, transition: color 280ms ease, border-color 280ms ease,
box-shadow 280ms ease, background 280ms ease; box-shadow 280ms ease, background 280ms ease;
} }

View file

@ -793,7 +793,7 @@ window.marked = marked;
const def = STATE_LABELS[stateName] || STATE_LABELS.loading; const def = STATE_LABELS[stateName] || STATE_LABELS.loading;
const age = fmtAge(Date.now() - stateSince); const age = fmtAge(Date.now() - stateSince);
badge.textContent = def.glyph + ' ' + def.text + ' · ' + age; badge.textContent = def.glyph + ' ' + def.text + ' · ' + age;
badge.className = 'hive-pill state-badge state-' + stateName; badge.className = 'state-badge state-' + stateName;
badge.title = (STATE_TOOLTIPS[stateName] || '') + '\nin this state for ' + age; badge.title = (STATE_TOOLTIPS[stateName] || '') + '\nin this state for ' + age;
const cancelBtn = $('cancel-btn'); const cancelBtn = $('cancel-btn');
if (cancelBtn) cancelBtn.hidden = stateName !== 'thinking'; if (cancelBtn) cancelBtn.hidden = stateName !== 'thinking';
@ -1057,7 +1057,7 @@ window.marked = marked;
if (!el_) return; if (!el_) return;
const def = ALIVE_LABELS[status] || ALIVE_LABELS.loading; const def = ALIVE_LABELS[status] || ALIVE_LABELS.loading;
el_.textContent = def.glyph + ' ' + def.text; el_.textContent = def.glyph + ' ' + def.text;
el_.className = 'hive-pill status-badge ' + def.cls; el_.className = 'status-badge ' + def.cls;
} }
function renderModelChip(model) { function renderModelChip(model) {

View file

@ -35,12 +35,12 @@
</div> </div>
<div id="state-row" class="agent-state-row agent-header-row"> <div id="state-row" class="agent-state-row agent-header-row">
<span id="alive-badge" class="hive-pill status-badge status-loading" title="harness reachability"></span> <span id="alive-badge" class="status-badge status-loading" title="harness reachability"></span>
<span id="state-badge" class="hive-pill state-badge state-loading">… booting</span> <span id="state-badge" class="state-badge state-loading">… booting</span>
<span id="model-chip" class="hive-pill-sm model-chip" hidden></span> <span id="model-chip" class="model-chip" hidden></span>
<span id="effort-chip" class="hive-pill-sm effort-chip" hidden></span> <span id="effort-chip" class="effort-chip" hidden></span>
<span id="ctx-badge" class="hive-pill-sm ctx-badge" hidden title="tokens used in the current context window"></span> <span id="ctx-badge" class="ctx-badge" hidden title="tokens used in the current context window"></span>
<span id="cost-badge" class="hive-pill-sm ctx-badge" hidden title="cumulative tokens billed across the last turn (sum across every inference; tool-heavy turns rebill the cached prompt per call)"></span> <span id="cost-badge" class="ctx-badge" hidden title="cumulative tokens billed across the last turn (sum across every inference; tool-heavy turns rebill the cached prompt per call)"></span>
<span id="last-turn" class="last-turn" hidden></span> <span id="last-turn" class="last-turn" hidden></span>
<button type="button" id="cancel-btn" class="btn-cancel-turn" hidden>■ cancel turn</button> <button type="button" id="cancel-btn" class="btn-cancel-turn" hidden>■ cancel turn</button>
</div> </div>
@ -52,13 +52,13 @@
it — see docs/web-ui.md::Per-agent page (Overflow button) it — see docs/web-ui.md::Per-agent page (Overflow button)
for the rare-destructive-extra-click rationale. --> for the rare-destructive-extra-click rationale. -->
<div class="agent-header-pills"> <div class="agent-header-pills">
<button type="button" id="inbox-pill" class="hive-pill header-pill header-pill-inbox" hidden <button type="button" id="inbox-pill" class="header-pill header-pill-inbox" hidden
title="open inbox flyout"> title="open inbox flyout">
<span class="header-pill-icon" aria-hidden="true">📬</span> <span class="header-pill-icon" aria-hidden="true">📬</span>
<span class="header-pill-label">inbox</span> <span class="header-pill-label">inbox</span>
<span class="header-pill-count" id="inbox-count">0</span> <span class="header-pill-count" id="inbox-count">0</span>
</button> </button>
<button type="button" id="todos-pill" class="hive-pill header-pill header-pill-todos" hidden <button type="button" id="todos-pill" class="header-pill header-pill-todos" hidden
title="open todos flyout"> title="open todos flyout">
<span class="header-pill-icon" aria-hidden="true">📋</span> <span class="header-pill-icon" aria-hidden="true">📋</span>
<span class="header-pill-label">todos</span> <span class="header-pill-label">todos</span>

View file

@ -344,7 +344,7 @@ async function fetchBuild() {
const li = el('li', { class: 'build-logs-item', 'data-log-id': String(h.id) }); const li = el('li', { class: 'build-logs-item', 'data-log-id': String(h.id) });
const live = !h.status; const live = !h.status;
const ok = h.status === 'ok'; const ok = h.status === 'ok';
const statusClass = live ? 'hive-pill-sm badge-running' : ok ? 'hive-pill-sm badge-ok' : 'hive-pill-sm badge-fail'; const statusClass = live ? 'badge badge-running' : ok ? 'badge badge-ok' : 'badge badge-fail';
const statusLabel = live ? 'live' : ok ? 'ok' : 'fail'; const statusLabel = live ? 'live' : ok ? 'ok' : 'fail';
const age = h.finished_at ? fmtTs(h.finished_at) : (live ? '' : fmtTs(h.started_at)); const age = h.finished_at ? fmtTs(h.finished_at) : (live ? '' : fmtTs(h.started_at));
const runtime = h.runtime_secs != null const runtime = h.runtime_secs != null
@ -398,7 +398,7 @@ async function fetchBuild() {
); );
if (live) { if (live) {
const pre = el('pre', { class: 'build-logs-output build-logs-live' }, ''); const pre = el('pre', { class: 'build-logs-output build-logs-live' }, '');
const badge = el('span', { class: 'build-logs-live-badge hive-pill-sm badge-running' }, 'live'); const badge = el('span', { class: 'build-logs-live-badge badge badge-running' }, 'live');
detail.append(badge, pre); detail.append(badge, pre);
streamEs = openBuildLogStream(h.id, pre, { streamEs = openBuildLogStream(h.id, pre, {
@ -408,7 +408,7 @@ async function fetchBuild() {
const elapsed = Math.floor(Date.now() / 1000) - h.started_at; const elapsed = Math.floor(Date.now() / 1000) - h.started_at;
runtime.textContent = fmtDuration(Math.max(0, elapsed)); runtime.textContent = fmtDuration(Math.max(0, elapsed));
} }
badge.className = status === 'ok' ? 'hive-pill-sm badge-ok' : 'hive-pill-sm badge-fail'; badge.className = status === 'ok' ? 'badge badge-ok' : 'badge badge-fail';
badge.textContent = status; badge.textContent = status;
streamEs = null; streamEs = null;
detail.dataset.loaded = '1'; detail.dataset.loaded = '1';
@ -416,7 +416,7 @@ async function fetchBuild() {
onError: () => { onError: () => {
if (durTimer) { clearInterval(durTimer); durTimer = null; } if (durTimer) { clearInterval(durTimer); durTimer = null; }
badge.textContent = 'stream error'; badge.textContent = 'stream error';
badge.className = 'hive-pill-sm badge-fail'; badge.className = 'badge badge-fail';
streamEs = null; streamEs = null;
}, },
}); });

View file

@ -4,10 +4,6 @@
@import "@hive/shared/terminal.css"; @import "@hive/shared/terminal.css";
@import "@hive/shared/tabs.css"; @import "@hive/shared/tabs.css";
@import "@hive/shared/chrome.css"; @import "@hive/shared/chrome.css";
/* Shared pill/chip/badge shape (the "extract pill component" issue,
resolved as shared CSS rather than a JS component) see pill.css
itself for the classnames it covers. */
@import "@hive/shared/pill.css";
/* global typography /* global typography
Element-level rules shared across all three pages (index, flow, Element-level rules shared across all three pages (index, flow,
@ -57,11 +53,17 @@ code {
.empty { color: var(--muted); font-style: italic; } .empty { color: var(--muted); font-style: italic; }
/* status badges /* status badges
Semantic colour variants only shape comes from `hive-pill-sm` .badge base + semantic colour variants. Used on container rows
(shared with the agent UI's pill/chip family, @hive/shared/pill.css), (tabs.js) and build-log rows (logs.js). */
carried directly in the markup alongside each of these classnames .badge {
(swarm.js/core.js/builds.js). Used on container rows and build-log display: inline-block;
rows. */ padding: 0.05em 0.5em;
border: 1px solid;
border-radius: 2px;
font-size: 0.75em;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.badge-warn { .badge-warn {
color: var(--amber); border-color: var(--amber); color: var(--amber); border-color: var(--amber);
text-shadow: 0 0 6px color-mix(in srgb, var(--amber) 50%, transparent); text-shadow: 0 0 6px color-mix(in srgb, var(--amber) 50%, transparent);

View file

@ -66,9 +66,9 @@ function renderTombstones(s) {
el('span', { class: 'name' }, t.name), el('span', { class: 'name' }, t.name),
// Was `destroyed`, which the backend cannot actually know — see the // Was `destroyed`, which the backend cannot actually know — see the
// caveat above. `offline` is what the absence of a container proves. // caveat above. `offline` is what the absence of a container proves.
el('span', { class: 'hive-pill-sm badge-muted' }, 'offline'), el('span', { class: 'badge badge-muted' }, 'offline'),
); );
if (t.has_creds) head.append(el('span', { class: 'hive-pill-sm badge-muted' }, 'creds kept')); if (t.has_creds) head.append(el('span', { class: 'badge badge-muted' }, 'creds kept'));
head.append(el('span', { class: 'meta' }, head.append(el('span', { class: 'meta' },
`${fmtBytes(t.state_bytes)} · ${fmtAgeDays(t.last_seen)}`)); `${fmtBytes(t.state_bytes)} · ${fmtAgeDays(t.last_seen)}`));
li.append(head); li.append(head);
@ -117,7 +117,7 @@ function renderStalePerms(root, ghosts) {
for (const name of ghosts) { for (const name of ghosts) {
const li = el('li', { class: 'tombstones-stale-row' }); const li = el('li', { class: 'tombstones-stale-row' });
li.append(el('span', { class: 'tombstones-stale-name' }, name)); li.append(el('span', { class: 'tombstones-stale-name' }, name));
li.append(el('span', { class: 'hive-pill-sm badge-muted' }, 'stale perms')); li.append(el('span', { class: 'badge badge-muted' }, 'stale perms'));
const btn = el('button', { const btn = el('button', {
type: 'button', type: 'button',
class: 'btn btn-destroy', class: 'btn btn-destroy',
@ -398,7 +398,7 @@ function renderInfraContainers(rows) {
const head = el('div', { class: 'head' }); const head = el('div', { class: 'head' });
head.append( head.append(
el('span', { class: 'name' }, c.name), el('span', { class: 'name' }, c.name),
el('span', { class: 'hive-pill-sm ' + (c.running ? 'badge-ok' : 'badge-fail') }, el('span', { class: 'badge ' + (c.running ? 'badge-ok' : 'badge-fail') },
c.running ? 'running' : 'stopped'), c.running ? 'running' : 'stopped'),
); );
li.append(head); li.append(head);

View file

@ -448,7 +448,7 @@ function buildContainerLi(c, node, opts) {
// already shown (pending / not-running already handled sync). // already shown (pending / not-running already handled sync).
if (ds.rate_limited) { if (ds.rate_limited) {
head.append(el('span', head.append(el('span',
{ class: 'hive-pill-sm badge-rate-limited', title: 'API rate-limited — harness is parked, will retry automatically' }, { class: 'badge badge-rate-limited', title: 'API rate-limited — harness is parked, will retry automatically' },
'⊘ rate limited')); '⊘ rate limited'));
} }
// ctx-window badge // ctx-window badge
@ -464,7 +464,7 @@ function buildContainerLi(c, node, opts) {
? `last turn context: ${ds.ctx_tokens.toLocaleString()} / ${win.toLocaleString()} ` ? `last turn context: ${ds.ctx_tokens.toLocaleString()} / ${win.toLocaleString()} `
+ `tokens (${Math.round((ds.ctx_tokens / win) * 100)}% of the window)` + `tokens (${Math.round((ds.ctx_tokens / win) * 100)}% of the window)`
: `last turn context size: ${ds.ctx_tokens.toLocaleString()} tokens`; : `last turn context size: ${ds.ctx_tokens.toLocaleString()} tokens`;
head.append(el('span', { class: `hive-pill-sm ${ctxClass}`, title }, `ctx·${k}k`)); head.append(el('span', { class: `badge ${ctxClass}`, title }, `ctx·${k}k`));
} }
// -- agent status text (self-reported via set_status) --------- // -- agent status text (self-reported via set_status) ---------
if (ds.status_text) { if (ds.status_text) {
@ -516,32 +516,32 @@ function buildContainerLi(c, node, opts) {
} else if (!c.running && c.failed) { } else if (!c.running && c.failed) {
head.append(el('span', head.append(el('span',
{ {
class: 'hive-pill-sm badge-fail', class: 'badge badge-fail',
title: 'container gave up — its unit hit systemd\'s bounded restart limit and stopped ' title: 'container gave up — its unit hit systemd\'s bounded restart limit and stopped '
+ 'on its own, not on operator request; start it to bring the harness back up', + 'on its own, not on operator request; start it to bring the harness back up',
}, },
'✖ gave up')); '✖ gave up'));
} else if (!c.running) { } else if (!c.running) {
head.append(el('span', head.append(el('span',
{ class: 'hive-pill-sm badge-muted', title: 'container is shut down — start it to bring the harness back up' }, { class: 'badge badge-muted', title: 'container is shut down — start it to bring the harness back up' },
'■ not running')); '■ not running'));
} else if (c.needs_login) { } else if (c.needs_login) {
head.append(el('a', head.append(el('a',
{ class: 'hive-pill-sm badge-warn', href: url, target: '_blank', rel: 'noopener' }, { class: 'badge badge-warn', href: url, target: '_blank', rel: 'noopener' },
'needs login →')); 'needs login →'));
} }
if (c.paused) { if (c.paused) {
// Paused badge is also a resume button: clicking POSTs /api/resume/{name} // Paused badge is also a resume button: clicking POSTs /api/resume/{name}
// which removes the marker and flips the badge off via the SSE rescan. // which removes the marker and flips the badge off via the SSE rescan.
head.append(form( head.append(form(
'/api/resume/' + c.name, 'hive-pill-sm badge-paused btn-inline', '⏸ paused', '/api/resume/' + c.name, 'badge badge-paused btn-inline', '⏸ paused',
`resume ${c.name}? the turn loop restarts and drains queued messages.`, `resume ${c.name}? the turn loop restarts and drains queued messages.`,
{}, { noRefresh: true }, {}, { noRefresh: true },
)); ));
} }
if (c.needs_update) { if (c.needs_update) {
head.append(form( head.append(form(
'/api/rebuild/' + c.name, 'hive-pill-sm badge-warn btn-inline', 'needs update ↻', '/api/rebuild/' + c.name, 'badge badge-warn btn-inline', 'needs update ↻',
'rebuild ' + c.name + '? hot-reloads the container.', 'rebuild ' + c.name + '? hot-reloads the container.',
{}, { noRefresh: true }, {}, { noRefresh: true },
)); ));
@ -549,7 +549,7 @@ function buildContainerLi(c, node, opts) {
if (c.active_model) { if (c.active_model) {
head.append(el('span', head.append(el('span',
{ class: 'hive-pill-sm badge-model', title: `active claude model: ${c.active_model}` }, { class: 'badge badge-model', title: `active claude model: ${c.active_model}` },
c.active_model)); c.active_model));
} }
@ -563,7 +563,7 @@ function buildContainerLi(c, node, opts) {
if (targetCount > 0) parts.push(`${targetCount} to answer`); if (targetCount > 0) parts.push(`${targetCount} to answer`);
head.append(el('span', head.append(el('span',
{ {
class: 'hive-pill-sm badge-loose-ends', class: 'badge badge-loose-ends',
title: `pending questions: ${parts.join(', ')} — see the Q33R1ES tab`, title: `pending questions: ${parts.join(', ')} — see the Q33R1ES tab`,
}, },
`${agentQCount}`)); `${agentQCount}`));

View file

@ -16,7 +16,6 @@
"./base.css": "./src/base.css", "./base.css": "./src/base.css",
"./terminal.css": "./src/terminal/terminal.css", "./terminal.css": "./src/terminal/terminal.css",
"./chrome.css": "./src/chrome.css", "./chrome.css": "./src/chrome.css",
"./pill.css": "./src/pill.css",
"./forms.js": "./src/forms.js", "./forms.js": "./src/forms.js",
"./prefs.js": "./src/prefs.js", "./prefs.js": "./src/prefs.js",
"./dom.js": "./src/dom.js", "./dom.js": "./src/dom.js",

View file

@ -1,40 +0,0 @@
/* Shared pill/chip/badge shape the "extract pill component" issue,
resolved as shared CSS rather than a JS component. `.hive-pill` and
`.hive-pill-sm` are the ONLY two classnames this file names every
inline status/count chip across the dashboard + per-agent UI carries
one of them (added alongside its own semantic colour modifier class,
e.g. `class="hive-pill-sm badge-ok"`) instead of this file having to
enumerate every call site's own base classname. Per mara's review on
the first cut of this: naming every use here just relocates the
duplication rather than removing it the fix is unifying the
classnames themselves, not fanning shared CSS out across old names.
Two size tiers, matching what was already organically in use:
.hive-pill primary state chips (agent status/state chips)
.hive-pill-sm secondary meta chips (agent model/effort/ctx chips,
the dashboard's whole badge family)
`display: inline-block` is the default here since that covers every
plain-text chip; the one exception (the agent header's icon+label+
count flyout pills) overrides `display` locally to `inline-flex`
layout mode isn't part of the shape these two classes exist to
share, so a single site overriding it is expected, not a gap. */
.hive-pill,
.hive-pill-sm {
display: inline-block;
border: 1px solid;
border-radius: 999px;
}
.hive-pill {
padding: 0.25em 0.8em;
font-size: 0.85em;
letter-spacing: 0.05em;
}
.hive-pill-sm {
padding: 0.1em 0.6em;
font-size: 0.78em;
letter-spacing: 0.04em;
}