diff --git a/hive-ag3nt/assets/app.js b/hive-ag3nt/assets/app.js index ea07e6c..54b1e83 100644 --- a/hive-ag3nt/assets/app.js +++ b/hive-ag3nt/assets/app.js @@ -161,27 +161,44 @@ } let headerSet = false; + let lastStatus = null; + let lastOutputLen = -1; + let pollTimer = null; async function refreshState() { try { const resp = await fetch('/api/state'); if (!resp.ok) throw new Error('http ' + resp.status); const s = await resp.json(); if (!headerSet) { setHeader(s.label, s.dashboard_port); headerSet = true; } - const root = $('status'); - root.innerHTML = ''; - if (s.status === 'online') renderOnline(s.label, root); - else if (s.status === 'needs_login_idle') renderNeedsLoginIdle(root); - else if (s.status === 'needs_login_in_progress') renderLoginInProgress(s.session || {}, root); + // 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. + const outLen = s.session?.output?.length ?? -1; + const dirty = + s.status !== lastStatus || + (s.status === 'needs_login_in_progress' && outLen !== lastOutputLen); + if (dirty) { + const root = $('status'); + root.innerHTML = ''; + if (s.status === 'online') renderOnline(s.label, root); + else if (s.status === 'needs_login_idle') renderNeedsLoginIdle(root); + else if (s.status === 'needs_login_in_progress') renderLoginInProgress(s.session || {}, root); + lastStatus = s.status; + lastOutputLen = outLen; + } + // Only poll while a login is in flight — otherwise SSE turn_end + // events trigger a refresh, and the operator can type into the + // send form without it getting cleared every few seconds. + if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; } + if (s.status === 'needs_login_in_progress') { + pollTimer = setTimeout(refreshState, 1500); + } } catch (err) { console.error('refreshState failed', err); + pollTimer = setTimeout(refreshState, 5000); } } refreshState(); - // Mid-login refresh on a short interval so the output buffer updates. - setInterval(() => { - // Cheap; api/state is small. Could subscribe to SSE state events later. - refreshState(); - }, 3000); // ─── live event stream ────────────────────────────────────────────────── (function() { diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index 6af4e96..d236a82 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -165,6 +165,28 @@ root.append(ul); } + function renderInbox(s) { + const root = $('inbox-section'); + root.innerHTML = ''; + if (!s.operator_inbox || !s.operator_inbox.length) { + root.append(el('p', { class: 'empty' }, '▓ no messages ▓')); + return; + } + const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(0, 19); + const ul = el('ul', { class: 'inbox' }); + for (const m of s.operator_inbox) { + const li = el('li'); + li.append( + el('span', { class: 'msg-ts' }, fmt(m.at)), ' ', + el('span', { class: 'msg-from' }, m.from), ' ', + el('span', { class: 'msg-sep' }, '→ '), + el('span', { class: 'msg-body' }, m.body), + ); + ul.append(li); + } + root.append(ul); + } + function renderApprovals(s) { const root = $('approvals-section'); root.innerHTML = ''; @@ -223,6 +245,7 @@ if (!resp.ok) throw new Error('http ' + resp.status); const s = await resp.json(); renderContainers(s); + renderInbox(s); renderApprovals(s); // Auto-refresh while a spawn is in flight; otherwise back off. const next = s.transients.length ? 2000 : 0; @@ -246,6 +269,8 @@ es.onmessage = (e) => { let m; try { m = JSON.parse(e.data); } catch { return; } + // Live-update the inbox when claude sends to operator. + if (m.kind === 'sent' && m.to === 'operator') refreshState(); const row = document.createElement('div'); row.className = 'msgrow ' + m.kind; const kind = m.kind === 'sent' ? '→' : '✓'; diff --git a/hive-c0re/assets/dashboard.css b/hive-c0re/assets/dashboard.css index b5bbaaf..1cc84e3 100644 --- a/hive-c0re/assets/dashboard.css +++ b/hive-c0re/assets/dashboard.css @@ -184,6 +184,26 @@ summary:hover { color: var(--purple); } .diff .diff-hunk { color: var(--cyan); } .diff .diff-file { color: var(--purple); font-weight: bold; } .diff .diff-ctx { color: var(--fg); } +.inbox { + background: var(--bg-elev); + border: 1px solid var(--border); + padding: 0.5em 0.8em; + max-height: 24em; + overflow-y: auto; +} +.inbox li { + padding: 0.25em 0; + border-bottom: 1px solid var(--border); + display: grid; + grid-template-columns: auto auto auto 1fr; + gap: 0.5em; + align-items: baseline; +} +.inbox li:last-child { border-bottom: 0; } +.inbox .msg-ts { color: var(--muted); font-size: 0.85em; } +.inbox .msg-from { color: var(--amber); } +.inbox .msg-sep { color: var(--muted); } +.inbox .msg-body { color: var(--fg); white-space: pre-wrap; word-break: break-word; } .msgflow { background: var(--bg-elev); border: 1px solid var(--border); diff --git a/hive-c0re/assets/index.html b/hive-c0re/assets/index.html index 501117f..2565ec0 100644 --- a/hive-c0re/assets/index.html +++ b/hive-c0re/assets/index.html @@ -16,6 +16,12 @@
+