diff --git a/hive-ag3nt/assets/agent.css b/hive-ag3nt/assets/agent.css index ca912c2..249daa5 100644 --- a/hive-ag3nt/assets/agent.css +++ b/hive-ag3nt/assets/agent.css @@ -124,6 +124,42 @@ pre.diff { word-break: break-all; max-height: 30em; } +#state-row { + margin: 0.4em 0 0.2em; +} +.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; +} +.state-badge.state-loading { + color: var(--muted); border-color: var(--purple-dim); +} +.state-badge.state-offline { + color: var(--muted); border-color: var(--muted); +} +.state-badge.state-idle { + color: var(--cyan); border-color: var(--cyan); + text-shadow: 0 0 6px rgba(137, 220, 235, 0.55); +} +.state-badge.state-thinking { + color: var(--amber); border-color: var(--amber); + text-shadow: 0 0 6px rgba(250, 179, 135, 0.65); + animation: badge-pulse 1.8s ease-in-out infinite; +} +.state-badge.state-just-changed { + animation: state-flash 600ms ease-out; +} +@keyframes state-flash { + 0% { box-shadow: 0 0 0 0 currentColor, 0 0 0 0 currentColor; } + 60% { box-shadow: 0 0 18px -4px currentColor, 0 0 4px 0 currentColor; } + 100% { box-shadow: 0 0 0 0 currentColor, 0 0 0 0 currentColor; } +} /* Terminal-ish wrapper holding the live output + prompt input as one unit. Crust as bg (almost-black), slightly inset, mauve phosphor glow. Frosted-glass backdrop blur: the page bg behind the wrap gets softened, diff --git a/hive-ag3nt/assets/app.js b/hive-ag3nt/assets/app.js index 763526c..6183e93 100644 --- a/hive-ag3nt/assets/app.js +++ b/hive-ag3nt/assets/app.js @@ -254,6 +254,54 @@ if (ta) ta.disabled = !online; } + // Granular state badge: idle / thinking / offline. Driven from SSE + // turn_start/turn_end. Age timer ticks client-side; badge re-renders + // each second so the "· 12s" suffix stays current. State changes + // trigger a short flash animation via .state-just-changed. + const STATE_LABELS = { + loading: { glyph: '…', text: 'booting' }, + offline: { glyph: '○', text: 'offline' }, + idle: { glyph: '💤', text: 'idle' }, + thinking: { glyph: '🧠', text: 'thinking' }, + }; + let stateName = 'loading'; + let stateSince = Date.now(); + let stateTickTimer = null; + function fmtAge(ms) { + const s = Math.floor(ms / 1000); + if (s < 60) return s + 's'; + const m = Math.floor(s / 60); + if (m < 60) return m + 'm ' + (s % 60) + 's'; + const h = Math.floor(m / 60); + return h + 'h ' + (m % 60) + 'm'; + } + function renderStateBadge() { + const badge = $('state-badge'); + if (!badge) return; + const def = STATE_LABELS[stateName] || STATE_LABELS.loading; + const age = fmtAge(Date.now() - stateSince); + badge.textContent = def.glyph + ' ' + def.text + ' · ' + age; + badge.className = 'state-badge state-' + stateName; + } + function setState(next) { + if (next === stateName) return; + stateName = next; + stateSince = Date.now(); + const badge = $('state-badge'); + if (badge) { + // Re-add the flash class so the animation replays. + badge.classList.remove('state-just-changed'); + void badge.offsetWidth; + badge.classList.add('state-just-changed'); + } + renderStateBadge(); + } + function startStateTicker() { + if (stateTickTimer) return; + stateTickTimer = setInterval(renderStateBadge, 1000); + } + startStateTicker(); + // 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. @@ -277,6 +325,11 @@ const s = await resp.json(); if (!headerSet) { setHeader(s.label, s.dashboard_port); headerSet = true; } renderTermInput(s.label, s.status === 'online'); + // Drive the state badge from the harness status. Live SSE events + // override to 'thinking' / 'idle' as turns start/end; this only + // kicks in for the not-online (offline) case and the initial seed. + if (s.status !== 'online') setState('offline'); + else if (stateName === 'loading' || stateName === 'offline') setState('idle'); // Skip the re-render if nothing structurally changed. The most // common case is `online` polling itself — without this guard, the // operator's gets clobbered every cycle. @@ -474,7 +527,7 @@ function handle(ev, opts) { const fromHistory = !!(opts && opts.fromHistory); if (ev.kind === 'turn_start') { - if (!fromHistory) setBannerActive(true); + if (!fromHistory) { setBannerActive(true); setState('thinking'); } const block = row('turn-start', '◆ TURN ← ' + ev.from); if (ev.unread > 0) { const badge = document.createElement('span'); @@ -489,7 +542,7 @@ return; } if (ev.kind === 'turn_end') { - if (!fromHistory) setBannerActive(false); + if (!fromHistory) { setBannerActive(false); setState('idle'); } const cls = ev.ok ? 'turn-end-ok' : 'turn-end-fail'; row(cls, (ev.ok ? '✓' : '✗') + ' turn ' + (ev.ok ? 'ok' : 'fail') + (ev.note ? ' — ' + ev.note : '')); // Login may have just landed (or session re-enters Online). Pull @@ -528,6 +581,7 @@ } currentNoAnim = false; for (let i = 0; i < openTurns; i++) setBannerActive(true); + if (openTurns > 0) setState('thinking'); if (events.length) row('note', '─── live (older above) ───'); else setPlaceholder('(connected — waiting for events)'); } catch (err) { diff --git a/hive-ag3nt/assets/index.html b/hive-ag3nt/assets/index.html index 7e3613e..219f8d9 100644 --- a/hive-ag3nt/assets/index.html +++ b/hive-ag3nt/assets/index.html @@ -13,6 +13,10 @@
+