diff --git a/hive-ag3nt/assets/agent.css b/hive-ag3nt/assets/agent.css new file mode 100644 index 0000000..1943405 --- /dev/null +++ b/hive-ag3nt/assets/agent.css @@ -0,0 +1,140 @@ +:root { + --bg: #0a0014; + --fg: #e0d4ff; + --muted: #6c5c8c; + --purple: #cc66ff; + --purple-dim: #4a1a6a; + --amber: #ffb84d; + --green: #66ff99; +} +body { + background: var(--bg); + color: var(--fg); + font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Source Code Pro", monospace; + max-width: 70em; + margin: 1.5em auto; + padding: 0 1.5em; + line-height: 1.6; +} +.banner { + color: var(--purple); + text-align: center; + margin: 0 0 1em 0; + font-size: 0.95em; + text-shadow: 0 0 6px rgba(204, 102, 255, 0.5); + overflow-x: auto; +} +h2, h3 { + color: var(--purple); + text-transform: uppercase; + letter-spacing: 0.15em; + text-shadow: 0 0 8px rgba(204, 102, 255, 0.4); +} +.divider { + color: var(--purple-dim); + overflow: hidden; + white-space: nowrap; + margin-bottom: 0.5em; +} +.meta { color: var(--muted); font-size: 0.85em; } +.status-online { color: var(--green); text-shadow: 0 0 6px rgba(102, 255, 153, 0.5); } +.status-needs-login { color: var(--amber); text-shadow: 0 0 6px rgba(255, 184, 77, 0.6); } +code { background: rgba(204, 102, 255, 0.1); padding: 0.05em 0.3em; border-radius: 2px; } +a { color: #66e0ff; } +.btn { + font-family: inherit; + font-size: 1em; + background: var(--bg); + border: 1px solid var(--purple); + color: var(--purple); + padding: 0.25em 0.8em; + cursor: pointer; + letter-spacing: 0.1em; +} +.btn:hover { background: rgba(204, 102, 255, 0.1); } +.btn-login { color: var(--amber); border-color: var(--amber); } +.btn-cancel { color: #ff6b6b; border-color: #ff6b6b; font-size: 0.85em; padding: 0.15em 0.6em; } +.btn-rebuild { + color: var(--amber); + border: 1px solid var(--amber); + padding: 0.15em 0.6em; + font-size: 0.55em; + font-family: inherit; + text-decoration: none; + letter-spacing: 0.1em; + margin-left: 0.6em; + vertical-align: middle; + cursor: pointer; +} +.btn-rebuild:hover { background: rgba(255, 184, 77, 0.1); } +.btn-send { color: var(--green); border-color: var(--green); } +.sendform { display: flex; gap: 0.6em; margin-top: 0.5em; } +.sendform input { + font-family: inherit; font-size: 1em; + background: rgba(255, 255, 255, 0.04); + color: var(--fg); + border: 1px solid var(--purple-dim); + padding: 0.4em 0.6em; + flex: 1; +} +.sendform input:focus { outline: 1px solid var(--purple); } +.loginform { display: flex; gap: 0.6em; margin-top: 0.5em; } +.loginform input { + font-family: inherit; font-size: 1em; + background: rgba(255, 255, 255, 0.04); + color: var(--fg); + border: 1px solid var(--purple-dim); + padding: 0.4em 0.6em; + flex: 1; +} +.loginform input:focus { outline: 1px solid var(--purple); } +pre.diff { + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--purple-dim); + padding: 0.6em 0.8em; + overflow-x: auto; + white-space: pre-wrap; + word-break: break-all; + max-height: 30em; +} +.live { + background: rgba(255, 255, 255, 0.02); + border: 1px solid var(--purple-dim); + padding: 0.4em 0.6em; + overflow-y: auto; + max-height: 32em; + font-family: inherit; +} +.live .row { + white-space: pre-wrap; + word-break: break-word; + padding: 0.05em 0; + line-height: 1.45; + border-left: 2px solid transparent; + padding-left: 0.5em; + margin: 0.1em 0; +} +.live .row + .row { border-top: 0; } +.live .turn-start { + color: var(--amber); + font-weight: bold; + margin-top: 1em; + border-left-color: var(--amber); + padding-top: 0.3em; +} +.live .turn-start:first-child { margin-top: 0; } +.live .turn-body { + color: var(--fg); + font-weight: normal; + margin-top: 0.15em; + padding-left: 1.2em; + opacity: 0.85; +} +.live .turn-end-ok { color: #66ff99; border-left-color: #66ff99; margin-bottom: 0.4em; } +.live .turn-end-fail { color: #ff6b6b; border-left-color: #ff6b6b; margin-bottom: 0.4em; } +.live .text { color: var(--fg); padding-left: 1.2em; } +.live .thinking { color: var(--muted); font-style: italic; padding-left: 1.2em; } +.live .tool-use { color: #66e0ff; padding-left: 1.2em; } +.live .tool-result { color: var(--muted); padding-left: 1.2em; } +.live .result { color: var(--green); padding-left: 0.5em; } +.live .sys, .live .note { color: var(--muted); } diff --git a/hive-ag3nt/assets/live.js b/hive-ag3nt/assets/live.js new file mode 100644 index 0000000..28200e4 --- /dev/null +++ b/hive-ag3nt/assets/live.js @@ -0,0 +1,103 @@ +(function() { + const log = document.getElementById('live'); + let placeholder = log.firstChild; + function setPlaceholder(text) { + log.innerHTML = ''; + const span = document.createElement('div'); + span.className = 'meta'; + span.textContent = text; + log.appendChild(span); + placeholder = span; + } + function clearPlaceholder() { + if (placeholder) { log.innerHTML = ''; placeholder = null; } + } + function row(cls, text) { + clearPlaceholder(); + const el = document.createElement('div'); + el.className = 'row ' + (cls || ''); + el.textContent = text; + log.appendChild(el); + log.scrollTop = log.scrollHeight; + return el; + } + function trim(s, n) { + return s.length > n ? s.slice(0, n) + '…' : s; + } + function renderStream(v) { + if (v.type === 'system' && v.subtype === 'init') { + row('sys', '· session init · tools=' + (v.tools||[]).length + ' model=' + (v.model || '?')); + return; + } + if (v.type === 'rate_limit_event') { + const u = Math.round((v.rate_limit_info?.utilization || 0) * 100); + const s = v.rate_limit_info?.status || ''; + row('sys', '· rate-limit util=' + u + '% (' + s + ')'); + return; + } + if (v.type === 'assistant' && v.message && v.message.content) { + for (const c of v.message.content) { + if (c.type === 'text' && c.text && c.text.trim()) + row('text', c.text); + else if (c.type === 'thinking') + row('thinking', '· thinking …'); + else if (c.type === 'tool_use') + row('tool-use', '→ ' + c.name + ' ' + trim(JSON.stringify(c.input || {}), 240)); + } + return; + } + if (v.type === 'user' && v.message && v.message.content) { + for (const c of v.message.content) { + if (c.type === 'tool_result') { + const txt = Array.isArray(c.content) + ? c.content.map(p => p.text || '').join(' ') + : (c.content || ''); + row('tool-result', '← ' + trim(txt, 300)); + } + } + return; + } + if (v.type === 'result') { + row('result', '✓ done · ' + (v.subtype || '') + (v.is_error ? ' [error]' : '')); + return; + } + // Fallback: small one-liner for unknown events; don't spam. + row('sys', '· ' + trim(JSON.stringify(v), 200)); + } + function handle(ev) { + if (ev.kind === 'turn_start') { + const block = row('turn-start', '◆ TURN ← ' + ev.from); + const body = document.createElement('div'); + body.className = 'turn-body'; + body.textContent = ev.body; + block.appendChild(body); + return; + } + if (ev.kind === 'turn_end') { + const cls = ev.ok ? 'turn-end-ok' : 'turn-end-fail'; + const sym = ev.ok ? '✓' : '✗'; + row(cls, sym + ' turn ' + (ev.ok ? 'ok' : 'fail') + (ev.note ? ' — ' + ev.note : '')); + return; + } + if (ev.kind === 'note') { + row('note', '· ' + ev.text); + return; + } + if (ev.kind === 'stream') { + const v = Object.assign({}, ev); delete v.kind; + renderStream(v); + return; + } + row('note', JSON.stringify(ev)); + } + const es = new EventSource('/events/stream'); + es.onopen = function() { setPlaceholder('(connected — waiting for events)'); }; + es.onmessage = function(e) { + try { handle(JSON.parse(e.data)); } + catch (err) { row('note', '[parse err] ' + e.data); } + }; + es.onerror = function() { + if (es.readyState === EventSource.CONNECTING) setPlaceholder('(reconnecting…)'); + else row('note', '[disconnected]'); + }; +})(); diff --git a/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs index d6cd09f..df88324 100644 --- a/hive-ag3nt/src/web_ui.rs +++ b/hive-ag3nt/src/web_ui.rs @@ -132,115 +132,13 @@ fn render_online(label: &str) -> String { /// Live event tail rendered into every `/` response when the agent is online. /// JS opens an `EventSource` on `/events/stream` and appends rows; no full-page /// reload, so the login flow and other forms aren't clobbered. -const LIVE_PANEL: &str = r#" -

live

-
connecting…
- -"#; +const LIVE_PANEL: &str = concat!( + "

live

\n", + "
connecting…
\n", + "", +); fn render_needs_login_idle() -> String { "

▓█▓▒░ NEEDS L0G1N ▓█▓▒░

\n

No Claude session in ~/.claude/. The harness is up but the turn loop is paused until you log in.

\n
\n \n
\n

Spawns claude auth login over plain stdio pipes. The OAuth URL will appear here when claude emits it; paste the resulting code back into the form below.

".into() @@ -394,147 +292,8 @@ fn html_escape(s: &str) -> String { .replace('"', """) } -const STYLE: &str = r#" - -"#; +const STYLE: &str = concat!( + "", +); diff --git a/hive-c0re/assets/async_forms.js b/hive-c0re/assets/async_forms.js new file mode 100644 index 0000000..1fe7834 --- /dev/null +++ b/hive-c0re/assets/async_forms.js @@ -0,0 +1,38 @@ +// Generic async submit + spinner for any `
`. +// Replaces the standard form-POST navigation: button shows a spinner during +// the request, `data-confirm` runs first (skips the action if cancelled), +// page reloads on success so the new state is reflected. +(() => { + document.querySelectorAll('form[data-async]').forEach(form => { + form.addEventListener('submit', async (e) => { + e.preventDefault(); + if (form.dataset.confirm && !confirm(form.dataset.confirm)) return; + const btn = form.querySelector('button[type="submit"], button:not([type]), .btn-inline'); + const original = btn ? btn.innerHTML : ''; + if (btn) { + btn.disabled = true; + btn.innerHTML = ''; + } + try { + const resp = await fetch(form.action, { + method: form.method || 'POST', + body: new FormData(form), + redirect: 'manual', + }); + const ok = resp.ok + || resp.type === 'opaqueredirect' + || (resp.status >= 200 && resp.status < 400); + if (!ok) { + const text = await resp.text().catch(() => ''); + alert('action failed: ' + resp.status + (text ? '\n\n' + text : '')); + if (btn) { btn.disabled = false; btn.innerHTML = original; } + return; + } + window.location.reload(); + } catch (err) { + alert('action failed: ' + err); + if (btn) { btn.disabled = false; btn.innerHTML = original; } + } + }); + }); +})(); diff --git a/hive-c0re/assets/dashboard.css b/hive-c0re/assets/dashboard.css new file mode 100644 index 0000000..f2f1741 --- /dev/null +++ b/hive-c0re/assets/dashboard.css @@ -0,0 +1,209 @@ +:root { + --bg: #0a0014; + --bg-elev: #18002a; + --fg: #e0d4ff; + --muted: #6c5c8c; + --purple: #cc66ff; + --purple-dim: #4a1a6a; + --cyan: #00ffff; + --pink: #ff3399; + --amber: #ffaa00; + --green: #00ff88; + --red: #ff4466; + --border: #2a0a4a; +} +body { + background: var(--bg); + color: var(--fg); + font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Source Code Pro", monospace; + max-width: 70em; + margin: 1.5em auto; + padding: 0 1.5em; + line-height: 1.6; +} +.banner { + color: var(--purple); + text-align: center; + margin: 0 0 1em 0; + font-size: 0.95em; + text-shadow: 0 0 6px rgba(204, 102, 255, 0.5); + overflow-x: auto; +} +h1, h2 { + color: var(--purple); + text-transform: uppercase; + letter-spacing: 0.15em; + margin-top: 2em; + text-shadow: 0 0 8px rgba(204, 102, 255, 0.4); +} +.divider { + color: var(--purple-dim); + overflow: hidden; + white-space: nowrap; + margin-bottom: 0.5em; +} +ul { list-style: none; padding-left: 0; } +li { padding: 0.5em 0; } +.glyph { color: var(--purple); margin-right: 0.5em; } +a { + color: var(--cyan); + text-decoration: none; + text-shadow: 0 0 4px rgba(0, 255, 255, 0.5); + font-weight: bold; +} +a:hover { + color: #fff; + text-shadow: 0 0 12px rgba(0, 255, 255, 0.9); +} +.role { + display: inline-block; + margin-left: 0.4em; + padding: 0.05em 0.5em; + border: 1px solid; + border-radius: 2px; + font-size: 0.8em; + letter-spacing: 0.1em; + text-transform: uppercase; +} +.role-m1nd { color: var(--pink); border-color: var(--pink); background: rgba(255, 51, 153, 0.1); } +.role-ag3nt { color: var(--amber); border-color: var(--amber); background: rgba(255, 170, 0, 0.1); } +.meta { color: var(--muted); font-size: 0.85em; margin-left: 0.4em; } +.id { color: var(--pink); font-weight: bold; margin-right: 0.4em; } +.agent { color: var(--amber); font-weight: bold; margin-right: 0.6em; } +.empty { color: var(--muted); font-style: italic; } +code { + color: var(--amber); + background: var(--bg-elev); + padding: 0.1em 0.4em; + border: 1px solid var(--border); + border-radius: 2px; + font-size: 0.9em; +} +.approvals .row { display: flex; align-items: center; flex-wrap: wrap; gap: 0.4em; } +.approvals form.inline { display: inline; margin-left: 0.4em; } +ul form.inline { display: inline-block; } +.btn { + font-family: inherit; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 0.1em; + background: transparent; + border: 1px solid; + padding: 0.25em 0.8em; + cursor: pointer; + text-shadow: 0 0 4px currentColor; +} +.btn:hover { background: rgba(255,255,255,0.05); text-shadow: 0 0 12px currentColor; } +.btn-approve { color: var(--green); border-color: var(--green); } +.btn-deny { color: var(--red); border-color: var(--red); } +.btn-destroy { color: var(--red); border-color: var(--red); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; } +.btn-rebuild { color: var(--amber); border-color: var(--amber); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; } +.btn-talk { color: var(--cyan); border-color: var(--cyan); } +.btn-spawn { color: var(--amber); border-color: var(--amber); } +.spawnform { display: flex; gap: 0.6em; align-items: stretch; margin: 0.5em 0; } +.spawnform input { + font-family: inherit; + font-size: 1em; + background: var(--bg-elev); + color: var(--fg); + border: 1px solid var(--border); + padding: 0.4em 0.6em; + flex: 1; +} +.spawnform input::placeholder { color: var(--muted); } +.spawnform input:focus { outline: 1px solid var(--purple); } +.role-pending { color: var(--amber); border-color: var(--amber); } +.btn-inline { + font-family: inherit; + background: transparent; + cursor: pointer; + margin-left: 0.4em; +} +.btn-inline:hover { background: rgba(255, 184, 77, 0.1); } +.kind { + display: inline-block; + margin-left: 0.4em; + padding: 0.05em 0.5em; + border: 1px solid var(--purple-dim); + color: var(--purple-dim); + border-radius: 2px; + font-size: 0.75em; + letter-spacing: 0.1em; + text-transform: uppercase; +} +.kind-spawn { color: var(--amber); border-color: var(--amber); } +.spinner { + display: inline-block; + animation: spin 1s linear infinite; + color: var(--amber); +} +@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } +.talkform { + display: flex; + gap: 0.6em; + align-items: stretch; + margin-top: 0.5em; +} +.talkform select, .talkform input { + font-family: inherit; + font-size: 1em; + background: var(--bg-elev); + color: var(--fg); + border: 1px solid var(--border); + padding: 0.4em 0.6em; +} +.talkform select { color: var(--amber); } +.talkform input { flex: 1; } +.talkform input::placeholder { color: var(--muted); } +.talkform input:focus, .talkform select:focus { outline: 1px solid var(--purple); } +details { margin-top: 0.5em; } +summary { + cursor: pointer; + color: var(--muted); + font-size: 0.85em; + text-transform: uppercase; + letter-spacing: 0.1em; +} +summary:hover { color: var(--purple); } +.diff { + background: var(--bg-elev); + border: 1px solid var(--border); + padding: 0.8em; + margin-top: 0.4em; + overflow-x: auto; + font-size: 0.85em; + line-height: 1.4; + color: var(--muted); + white-space: pre; +} +.diff span { display: block; } +.diff .diff-add { color: var(--green); } +.diff .diff-del { color: var(--red); } +.diff .diff-hunk { color: var(--cyan); } +.diff .diff-file { color: var(--purple); font-weight: bold; } +.diff .diff-ctx { color: var(--fg); } +.msgflow { + background: var(--bg-elev); + border: 1px solid var(--border); + padding: 0.8em; + font-size: 0.85em; + line-height: 1.5; + max-height: 32em; + overflow-y: auto; +} +.msgrow { display: grid; grid-template-columns: auto auto auto auto auto 1fr; gap: 0.6em; align-items: baseline; padding: 0.1em 0; } +.msgrow.sent .msg-arrow { color: var(--cyan); } +.msgrow.delivered .msg-arrow { color: var(--green); } +.msg-ts { color: var(--muted); font-size: 0.85em; } +.msg-arrow { font-weight: bold; } +.msg-from { color: var(--amber); } +.msg-sep { color: var(--muted); } +.msg-to { color: var(--pink); } +.msg-body { color: var(--fg); white-space: pre-wrap; word-break: break-word; } +footer { + margin-top: 4em; + text-align: center; + color: var(--muted); + font-size: 0.9em; +} +footer a { color: var(--purple); } diff --git a/hive-c0re/assets/msg_flow.js b/hive-c0re/assets/msg_flow.js new file mode 100644 index 0000000..b793073 --- /dev/null +++ b/hive-c0re/assets/msg_flow.js @@ -0,0 +1,30 @@ +(() => { + const flow = document.getElementById('msgflow'); + if (!flow) return; + flow.innerHTML = ''; + const es = new EventSource('/messages/stream'); + const MAX_ROWS = 200; + const tsFmt = (n) => new Date(n * 1000).toISOString().slice(11, 19); + const esc = (s) => s.replace(/[&<>]/g, (c) => ({'&':'&','<':'<','>':'>'}[c])); + es.onmessage = (e) => { + let m; + try { m = JSON.parse(e.data); } catch { return; } + const row = document.createElement('div'); + row.className = 'msgrow ' + m.kind; + const kind = m.kind === 'sent' ? '→' : '✓'; + row.innerHTML = + '' + tsFmt(m.at) + '' + + '' + kind + '' + + '' + esc(m.from) + '' + + '' + + '' + esc(m.to) + '' + + '' + esc(m.body) + ''; + flow.insertBefore(row, flow.firstChild); + while (flow.childNodes.length > MAX_ROWS) flow.removeChild(flow.lastChild); + }; + es.onerror = () => { + flow.insertBefore(Object.assign(document.createElement('div'), { + className: 'msgrow meta', textContent: '[connection lost — retrying]' + }), flow.firstChild); + }; +})(); diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index 77fb0e4..6492c8e 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -411,296 +411,30 @@ const BANNER: &str = r#"