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
standalone pages use. The live terminal page keeps its own header. */
@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
component's own shadow root (adoptedStyleSheets), not a global
stylesheet — nothing to @import here any more. */
@ -304,16 +300,16 @@ h2, h3 {
background: color-mix(in srgb, var(--purple) 6%, transparent);
}
/* 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 — inbox / loose-ends triggers. Compact, count-prominent. */
.header-pill {
background: transparent;
border-color: var(--purple-dim);
border: 1px solid var(--purple-dim);
color: var(--fg);
font-family: inherit;
font-size: 0.85em;
letter-spacing: 0.04em;
border-radius: 999px;
padding: 0.25em 0.7em;
display: inline-flex;
align-items: center;
gap: 0.4em;
@ -606,11 +602,14 @@ pre.diff {
font-size: 0.8em;
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,
.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); }
.effort-chip { color: var(--amber); }
@ -619,15 +618,28 @@ pre.diff {
breakdown on hover. Sized/coloured like a peer of model-chip so
the state row reads as one row of chrome. */
.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);
font-size: 0.78em;
letter-spacing: 0.04em;
cursor: default;
white-space: pre-line;
}
/* Harness reachability badge. Markup carries `hive-pill` alongside
`status-badge` (shared pill.css supplies the shape); colour
communicates the actual reachability state. */
/* Harness reachability badge. Same chip shape + sizing as
`.state-badge` / `.model-chip` so the state row stays visually
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-online { color: var(--green); border-color: var(--green);
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
`:disabled` opacity treatment lives on the shared
`.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 {
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,
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 age = fmtAge(Date.now() - stateSince);
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;
const cancelBtn = $('cancel-btn');
if (cancelBtn) cancelBtn.hidden = stateName !== 'thinking';
@ -1057,7 +1057,7 @@ window.marked = marked;
if (!el_) return;
const def = ALIVE_LABELS[status] || ALIVE_LABELS.loading;
el_.textContent = def.glyph + ' ' + def.text;
el_.className = 'hive-pill status-badge ' + def.cls;
el_.className = 'status-badge ' + def.cls;
}
function renderModelChip(model) {

View file

@ -35,12 +35,12 @@
</div>
<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="state-badge" class="hive-pill state-badge state-loading">… booting</span>
<span id="model-chip" class="hive-pill-sm model-chip" hidden></span>
<span id="effort-chip" class="hive-pill-sm 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="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="alive-badge" class="status-badge status-loading" title="harness reachability">…</span>
<span id="state-badge" class="state-badge state-loading">… booting</span>
<span id="model-chip" class="model-chip" hidden></span>
<span id="effort-chip" class="effort-chip" hidden></span>
<span id="ctx-badge" class="ctx-badge" hidden title="tokens used in the current context window"></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>
<button type="button" id="cancel-btn" class="btn-cancel-turn" hidden>■ cancel turn</button>
</div>
@ -52,13 +52,13 @@
it — see docs/web-ui.md::Per-agent page (Overflow button)
for the rare-destructive-extra-click rationale. -->
<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">
<span class="header-pill-icon" aria-hidden="true">📬</span>
<span class="header-pill-label">inbox</span>
<span class="header-pill-count" id="inbox-count">0</span>
</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">
<span class="header-pill-icon" aria-hidden="true">📋</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 live = !h.status;
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 age = h.finished_at ? fmtTs(h.finished_at) : (live ? '' : fmtTs(h.started_at));
const runtime = h.runtime_secs != null
@ -398,7 +398,7 @@ async function fetchBuild() {
);
if (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);
streamEs = openBuildLogStream(h.id, pre, {
@ -408,7 +408,7 @@ async function fetchBuild() {
const elapsed = Math.floor(Date.now() / 1000) - h.started_at;
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;
streamEs = null;
detail.dataset.loaded = '1';
@ -416,7 +416,7 @@ async function fetchBuild() {
onError: () => {
if (durTimer) { clearInterval(durTimer); durTimer = null; }
badge.textContent = 'stream error';
badge.className = 'hive-pill-sm badge-fail';
badge.className = 'badge badge-fail';
streamEs = null;
},
});

View file

@ -4,10 +4,6 @@
@import "@hive/shared/terminal.css";
@import "@hive/shared/tabs.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 ─────────────────────────────────────────────
Element-level rules shared across all three pages (index, flow,
@ -57,11 +53,17 @@ code {
.empty { color: var(--muted); font-style: italic; }
/* ─── status badges ─────────────────────────────────────────────────
Semantic colour variants only — shape comes from `hive-pill-sm`
(shared with the agent UI's pill/chip family, @hive/shared/pill.css),
carried directly in the markup alongside each of these classnames
(swarm.js/core.js/builds.js). Used on container rows and build-log
rows. */
.badge base + semantic colour variants. Used on container rows
(tabs.js) and build-log rows (logs.js). */
.badge {
display: inline-block;
padding: 0.05em 0.5em;
border: 1px solid;
border-radius: 2px;
font-size: 0.75em;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.badge-warn {
color: var(--amber); border-color: var(--amber);
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),
// Was `destroyed`, which the backend cannot actually know — see the
// 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' },
`${fmtBytes(t.state_bytes)} · ${fmtAgeDays(t.last_seen)}`));
li.append(head);
@ -117,7 +117,7 @@ function renderStalePerms(root, ghosts) {
for (const name of ghosts) {
const li = el('li', { class: 'tombstones-stale-row' });
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', {
type: 'button',
class: 'btn btn-destroy',
@ -398,7 +398,7 @@ function renderInfraContainers(rows) {
const head = el('div', { class: 'head' });
head.append(
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'),
);
li.append(head);

View file

@ -448,7 +448,7 @@ function buildContainerLi(c, node, opts) {
// already shown (pending / not-running already handled sync).
if (ds.rate_limited) {
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'));
}
// ctx-window badge
@ -464,7 +464,7 @@ function buildContainerLi(c, node, opts) {
? `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: `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) ---------
if (ds.status_text) {
@ -516,32 +516,32 @@ function buildContainerLi(c, node, opts) {
} else if (!c.running && c.failed) {
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 '
+ 'on its own, not on operator request; start it to bring the harness back up',
},
'✖ gave up'));
} else if (!c.running) {
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'));
} else if (c.needs_login) {
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 →'));
}
if (c.paused) {
// 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.
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.`,
{}, { noRefresh: true },
));
}
if (c.needs_update) {
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.',
{}, { noRefresh: true },
));
@ -549,7 +549,7 @@ function buildContainerLi(c, node, opts) {
if (c.active_model) {
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));
}
@ -563,7 +563,7 @@ function buildContainerLi(c, node, opts) {
if (targetCount > 0) parts.push(`${targetCount} to answer`);
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`,
},
`❓ ${agentQCount}`));

View file

@ -16,7 +16,6 @@
"./base.css": "./src/base.css",
"./terminal.css": "./src/terminal/terminal.css",
"./chrome.css": "./src/chrome.css",
"./pill.css": "./src/pill.css",
"./forms.js": "./src/forms.js",
"./prefs.js": "./src/prefs.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;
}