diff --git a/TODO.md b/TODO.md index 5492d727..6f5c5e68 100644 --- a/TODO.md +++ b/TODO.md @@ -21,67 +21,16 @@ Pick anything from here when relevant. Cross-cutting design notes live in - **Model override.** Hard-coded to `haiku` in the turn loop right now. Surface as a per-agent override: operator via dashboard, manager via `request_apply_commit` setting an attr on the agent's flake (most natural - place since the flake already carries per-agent env/identity). Pair with - a **model status** indicator on the agent page (active / queued / last - switched) once the override is in place. + place since the flake already carries per-agent env/identity). ## UI / UX - **Per-agent UI substance.** Show last N inbox messages, last turn timing, link back to dashboard. -- **Delivered events history persistence.** The `events::Bus` ring - buffer (500 events, in-memory) backfills the terminal on page load - but dies on harness restart, and only ever holds the most recent - turn or two. Persist to sqlite (`events(agent, id, ts, kind, - payload_json)`) so the operator can scroll back through prior - turns, and so `/events/history` survives restart. Cap rows per - agent or auto-vacuum on age, same trade-off as the bounded broker - entry below. -- **Granular agent state badge** above the terminal: `idle 💤 / thinking 🧠 / - compacting 📦` with an age timer (`thinking · 12s`). Drives a state - channel from the harness: idle when waiting on the inbox, thinking - while claude's stream is open, compacting when `/compact` is in - flight. Replaces the binary "harness alive — turn loop running" line. -- **Terminal: slash commands + tab-completion.** Operator-facing - in-terminal commands: `/help`, `/model`, `/compact`, `/clear`. Tab - completes command names + model names (cf. bitburner-agent's pattern). -- **Terminal: multi-line input.** Replace the single-line `` with - an auto-growing textarea; Enter sends, Shift+Enter newlines. -- **Terminal: cancel-current-turn button.** Explicit "kill claude - process for this turn" control. Harness needs to track the - in-flight claude child PID and offer a `/cancel` endpoint that sends - SIGTERM; UI surfaces a button while the state badge is `thinking`. - Slash-command equivalent: `/cancel`. -- **`/compact` trigger.** Operator-initiated compaction of the current - claude session — `claude --print --continue` with `/compact` over the - same session id. Surfaces as a slash command in the terminal + a - toolbar button while the state badge is `idle`. Sets state to - `compacting` during the run. -- **Visuals.** Frosted-glass backdrop blur on the terminal wrap, - per-event fade-in slide-up animation on new rows, badge pulse - animation on state-badge transitions. - **xterm.js terminal** embedded per-agent, attached to a PTY exposed by the harness. Pairs well with the unprivileged-container work — would let the operator drop into the container without `nixos-container root-login`. -## Telemetry - -- **Harness stats per agent in sqlite, charted on the agent page.** - bitburner-agent samples 18 series; for hyperhive the generally-applicable - ones are: - - turns/min, tool calls/turn, turn duration p50/p95 - - claude exit code distribution (ok vs `--compact`-retry vs failure) - - inbox depth (current + max-over-window) - - messages sent/received per turn (split by recipient: peer / operator / - manager / system) - - approval queue length (across all agents — dashboard-level) - - per-tool usage counts (Read/Edit/Bash/send/recv/…) - - time-since-last-turn (helps spot stuck agents) - - notes file size growth (cues compaction) - Backend: a `stats` table with `(agent, ts, key, value)` written from - the harness on `TurnEnd`; `GET /api/stats?since=…` returns the - series; agent page renders with a small chart lib (uPlot is light). - ## Manager → operator question channel - **TTL / cancel on `ask_operator`.** Questions today block forever; the @@ -93,14 +42,6 @@ Pick anything from here when relevant. Cross-cutting design notes live in ## Loop substance -- **`nap` tool.** Agent-side MCP tool `mcp__hyperhive__nap(seconds)` that - parks the turn loop for a short while before next-message processing. - Use cases: agent decides it has nothing useful to do, or wants to - throttle itself between rapid wake events. Implementation: harness - records a "wake-not-before" timestamp; `recv_blocking` skips the long - poll until that ts; the state badge reads `napping · MM:SS` during. - Operator can cancel via the same `/cancel` slash command or a - dashboard button. - **Notes compaction.** `/state/` is bind-mounted persistently and agents are told (in the system prompt) to keep `/state/notes.md` for durable knowledge — but we don't currently nudge them to compact when notes diff --git a/hive-ag3nt/assets/agent.css b/hive-ag3nt/assets/agent.css index 15ea4777..9cc0eb07 100644 --- a/hive-ag3nt/assets/agent.css +++ b/hive-ag3nt/assets/agent.css @@ -125,14 +125,9 @@ pre.diff { max-height: 30em; } /* 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, - so anything that bleeds through (page banner glow, scroll position) - reads as out-of-focus depth instead of sharp competing detail. */ + unit. Crust as bg (almost-black), slightly inset, mauve phosphor glow. */ .terminal-wrap { - background: rgba(17, 17, 27, 0.78); - -webkit-backdrop-filter: blur(8px) saturate(120%); - backdrop-filter: blur(8px) saturate(120%); + background: #11111b; border: 1px solid var(--purple-dim); box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.7); border-radius: 4px; @@ -156,21 +151,18 @@ pre.diff { .term-input { padding: 0.4em 1em 0.8em; } .term-input .sendform-term { display: flex; - align-items: flex-start; + align-items: center; gap: 0.5em; border-top: 1px dashed var(--purple-dim); padding-top: 0.5em; } -.term-input .prompt, .term-input .submit-hint { - padding-top: 0.25em; -} .term-input .prompt { color: var(--green); text-shadow: 0 0 6px rgba(166, 227, 161, 0.6); user-select: none; flex: 0 0 auto; } -.term-input textarea { +.term-input input { flex: 1; background: transparent; border: 0; @@ -180,15 +172,11 @@ pre.diff { font-size: 1em; padding: 0.2em 0; caret-color: var(--green); - resize: none; - overflow-y: auto; - line-height: 1.4; - min-height: 1.4em; } -.term-input textarea::placeholder { color: var(--muted); } +.term-input input::placeholder { color: var(--muted); } .term-input .submit-hint { color: var(--muted); font-size: 0.8em; flex: 0 0 auto; } .term-input.disabled .prompt { color: var(--muted); text-shadow: none; } -.term-input.disabled textarea { color: var(--muted); } +.term-input.disabled input { color: var(--muted); } .live { background: rgba(255, 255, 255, 0.02); border: 1px solid var(--purple-dim); @@ -203,26 +191,6 @@ pre.diff { margin-left: 0.6em; font-size: 0.85em; text-shadow: 0 0 6px rgba(250, 179, 135, 0.55); - animation: badge-pulse 1.4s ease-in-out infinite; -} -@keyframes badge-pulse { - 0%, 100% { opacity: 1; text-shadow: 0 0 6px rgba(250, 179, 135, 0.55); } - 50% { opacity: 0.7; text-shadow: 0 0 14px rgba(250, 179, 135, 0.95); } -} -/* Per-event fade-in slide-up. Applied to every row the live panel - appends; the `.no-anim` modifier lets history-backfill skip the - animation (we don't want 100 rows fading in at once on page load). */ -.live .row, -.live details.row { - animation: row-fade-in 220ms ease-out both; -} -.live .row.no-anim, -.live details.row.no-anim { - animation: none; -} -@keyframes row-fade-in { - from { opacity: 0; transform: translateY(4px); } - to { opacity: 1; transform: translateY(0); } } details.row { white-space: normal; diff --git a/hive-ag3nt/assets/app.js b/hive-ag3nt/assets/app.js index 45c1bdc1..cb89cbda 100644 --- a/hive-ag3nt/assets/app.js +++ b/hive-ag3nt/assets/app.js @@ -47,7 +47,7 @@ return; } // Clear text inputs the operator typed into (the form value was sent). - f.querySelectorAll('input[type="text"], input:not([type]), textarea').forEach((i) => { i.value = ''; }); + f.querySelectorAll('input[type="text"], input:not([type])').forEach((i) => { i.value = ''; }); // Re-enable the button — refreshState() often skips re-rendering the // form (status unchanged), so without this the spinner sticks and // the operator can't submit again. @@ -155,47 +155,6 @@ let lastOutputLen = -1; let pollTimer = null; let termInputRendered = false; - // Filled in by the live-event IIFE below. Used by the slash-command - // dispatcher to print local-only rows ('help', errors) and to clear - // the terminal on `/clear`. - let termAPI = null; - - const SLASH_COMMANDS = [ - { name: '/help', desc: 'list slash commands' }, - { name: '/clear', desc: 'wipe the terminal panel (local-only)' }, - ]; - - function handleSlashCommand(line) { - if (!termAPI) return false; - const trimmed = line.trim(); - if (!trimmed.startsWith('/')) return false; - const [cmd] = trimmed.split(/\s+/); - switch (cmd) { - case '/help': - termAPI.row('note', '· /help'); - for (const c of SLASH_COMMANDS) { - termAPI.row('note', ' ' + c.name.padEnd(10) + ' — ' + c.desc); - } - return true; - case '/clear': - termAPI.clear(); - termAPI.row('note', '· terminal cleared (local view only — server history kept)'); - return true; - default: - termAPI.row('turn-end-fail', '✗ unknown slash command: ' + cmd + ' — try /help'); - return true; - } - } - - // Cycle through commands when operator hits Tab on a `/…` prefix. - function completeSlash(prefix) { - const matches = SLASH_COMMANDS.filter((c) => c.name.startsWith(prefix)); - if (!matches.length) return null; - // Cycle: when the current prefix already equals a command name, - // advance to the next match. - const idx = matches.findIndex((c) => c.name === prefix); - return matches[(idx + 1) % matches.length].name; - } function renderTermInput(label, online) { const slot = $('term-input'); @@ -206,52 +165,20 @@ action: '/send', method: 'POST', class: 'sendform-term', 'data-async': '', }); - const ta = el('textarea', { - name: 'body', placeholder: 'message ' + label + '…', - required: '', autocomplete: 'off', rows: '1', - }); - // Enter submits, Shift+Enter inserts a newline. Auto-grow up to - // ~8 rows of content, then scroll inside the textarea. - const MAX_PX = 12 * 16; // ~8 lines @ 1.5 line-height, 1em base - const grow = () => { - ta.style.height = 'auto'; - ta.style.height = Math.min(ta.scrollHeight, MAX_PX) + 'px'; - }; - ta.addEventListener('input', grow); - ta.addEventListener('keydown', (e) => { - // Tab-complete slash commands when the buffer starts with `/`. - if (e.key === 'Tab' && ta.value.startsWith('/') && !ta.value.includes(' ')) { - const next = completeSlash(ta.value); - if (next) { e.preventDefault(); ta.value = next; return; } - } - if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { - e.preventDefault(); - const line = ta.value; - if (!line.trim()) return; - // Intercept slash commands locally; never send them to the agent. - if (line.trim().startsWith('/')) { - if (handleSlashCommand(line)) { - ta.value = ''; - grow(); - return; - } - } - form.requestSubmit(); - } - }); - // Reset height after async submit clears the value. - form.addEventListener('submit', () => setTimeout(grow, 0)); form.append( el('span', { class: 'prompt' }, 'operator@' + label + ' ▸'), - ta, - el('span', { class: 'submit-hint' }, '↵ send · ⇧↵ newline · /help'), + el('input', { + name: 'body', placeholder: 'message ' + label + '…', + required: '', autocomplete: 'off', + }), + el('span', { class: 'submit-hint' }, 'enter ↵'), ); slot.append(form); termInputRendered = true; } slot.classList.toggle('disabled', !online); - const ta = slot.querySelector('textarea'); - if (ta) ta.disabled = !online; + const input = slot.querySelector('input'); + if (input) input.disabled = !online; } // Track banner activity by reference-counting in-flight turns. A turn @@ -323,18 +250,10 @@ function clearPlaceholder() { if (placeholder) { log.innerHTML = ''; placeholder = null; } } - // Backfill replays mark rows .no-anim so we don't stagger 100 fade-ins - // on page load. Set via `currentNoAnim` before the row helpers fire. - let currentNoAnim = false; - // Expose the panel API for slash commands (`/help`, `/clear`). - termAPI = { - row: (cls, text) => row(cls, text), - clear: () => { log.innerHTML = ''; placeholder = null; }, - }; function row(cls, text) { clearPlaceholder(); const e = document.createElement('div'); - e.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : ''); + e.className = 'row ' + (cls || ''); e.textContent = text; log.appendChild(e); log.scrollTop = log.scrollHeight; @@ -343,7 +262,7 @@ function details(cls, summary, body) { clearPlaceholder(); const d = document.createElement('details'); - d.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : ''); + d.className = 'row ' + (cls || ''); const s = document.createElement('summary'); s.textContent = summary; d.appendChild(s); @@ -475,13 +394,11 @@ if (!resp.ok) return; const events = await resp.json(); let openTurns = 0; - currentNoAnim = true; for (const ev of events) { handle(ev, { fromHistory: true }); if (ev.kind === 'turn_start') openTurns += 1; else if (ev.kind === 'turn_end') openTurns = Math.max(0, openTurns - 1); } - currentNoAnim = false; for (let i = 0; i < openTurns; i++) setBannerActive(true); if (events.length) row('note', '─── live (older above) ───'); else setPlaceholder('(connected — waiting for events)'); diff --git a/hive-c0re/assets/dashboard.css b/hive-c0re/assets/dashboard.css index 10b86ac1..3036c917 100644 --- a/hive-c0re/assets/dashboard.css +++ b/hive-c0re/assets/dashboard.css @@ -215,11 +215,6 @@ summary:hover { color: var(--purple); } border: 1px solid var(--amber); box-shadow: 0 0 12px -4px var(--amber); padding: 0.6em 0.9em; - animation: questions-pulse 2.4s ease-in-out infinite; -} -@keyframes questions-pulse { - 0%, 100% { box-shadow: 0 0 12px -4px rgba(250, 179, 135, 0.55); } - 50% { box-shadow: 0 0 22px -2px rgba(250, 179, 135, 0.95); } } .questions li.question { padding: 0.4em 0; @@ -266,9 +261,7 @@ summary:hover { color: var(--purple); } .inbox .msg-sep { color: var(--muted); } .inbox .msg-body { color: var(--fg); white-space: pre-wrap; word-break: break-word; } .msgflow { - background: rgba(24, 24, 37, 0.78); - -webkit-backdrop-filter: blur(8px) saturate(120%); - backdrop-filter: blur(8px) saturate(120%); + background: var(--bg-elev); border: 1px solid var(--border); padding: 0.8em; font-size: 0.85em; @@ -276,13 +269,6 @@ summary:hover { color: var(--purple); } max-height: 32em; overflow-y: auto; } -.msgflow .msgrow { - animation: row-fade-in 220ms ease-out both; -} -@keyframes row-fade-in { - from { opacity: 0; transform: translateY(4px); } - to { opacity: 1; transform: translateY(0); } -} .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); }