Commit graph

35 commits

Author SHA1 Message Date
müde
0c62bbf1cd dashboard: render markdown file previews in the side panel
clicking a .md / .markdown path reference now opens a marked-rendered
view in the slide-in panel instead of raw text; other files stay raw
in a <pre>. serves the vendored marked bundle at /static/marked.js and
scopes a .md stylesheet to the panel body.
2026-05-20 11:01:16 +02:00
müde
7ce3da1e21 dashboard: open long content in a slide-in side panel
file previews, approval diffs, journald logs and applied config no
longer expand inline — they open in a drawer that swipes in from the
right, with a title naming what's open and a close button (esc /
backdrop also close). path references in messages become plain inline
links that open the file in the panel; the sibling-<details> dance in
appendLinkified is gone.

also: the question-answer free-text field is now a textarea — enter
submits, shift+enter inserts a newline.
2026-05-20 10:43:23 +02:00
müde
f8f2ccff52 agent terminal: coherence pass
layout
- unified prefix-column for every row kind: padding-left + negative
  text-indent so the glyph (→ ← · ◆ ✓ ✗ ⌁ !) sits in the same column
  whether the row is flat or a <details>. wraps hang under the body,
  not under the glyph.
- expandable rows drop the directional glyph from their summary text;
  the ▸/▾ disclosure marker from CSS sits in the prefix column instead,
  and the row's colour still carries cyan = outbound, muted = inbound.
- turn-start / turn-end de-weighted: bold/margin/tint dropped, the
  coloured left rule alone marks the boundary.

note classification
- stderr lines render orange with a `!` glyph (was muted `·`)
- operator-initiated notes (cancel/compact/model/new-session) render
  mauve italic (was muted `·` indistinguishable from harness chatter)
- catch-all .sys row escalates to orange `!` so unrecognised stream-json
  shapes surface for follow-up instead of hiding in muted noise

message-bearing rows
- send / ask / answer tool_use rich renderers default-open with the
  body inline; new ask + answer renderers (previously fell through to
  the generic JSON dump). recv tool_result also default-open, keyed by
  tracking tool_use_id → name across the stream so we know which
  result came from which tool.
- assistant text rows render markdown.
- bodies use vendored marked v4.0.2 (hive-fr0nt::MARKED_JS); falls
  back to plain text when the asset doesn't load.

extra-mcp tool pretty-print
- generic args formatter replaces the raw JSON dump for unknown tools
  (single-string field → `name k: "v"`; single dict / multi-field →
  trimmed `k: v · k: v …` summary)

dashboard .live .msgrow gets a text-indent: 0 reset so the new
hanging-indent metrics from TERMINAL_CSS don't leak into the flex-grid
broker rows.
2026-05-18 18:13:14 +02:00
müde
978a3cf391 reminders: persist + surface delivery failures
Broker schema gains attempt_count INTEGER + last_error TEXT
columns via idempotent ALTER TABLE migration (pragma-probed so
fresh + existing dbs converge). reminder_scheduler::tick calls
record_reminder_failure on every deliver_reminder error,
bumping the counter + stashing the message. get_due_reminders
filters out rows where attempt_count >= MAX_REMINDER_ATTEMPTS
(5) so the scheduler stops retrying a stuck row until the
operator intervenes.

new POST /retry-reminder/{id} → reset_reminder_failure clears
the counters; next 5s tick re-attempts. cancel-reminder
unchanged (hard-delete).

dashboard renders failed rows with a red left rule, the error
text inline, and a ⚠ N failed badge. ↻ R3TRY button appears
when attempt_count > 0 — sits next to ✗ C4NC3L in a small
actions row below the body.
2026-05-18 00:08:09 +02:00
müde
087a5366fb container row: pending-reminder count chip ( N)
ContainerView gains pending_reminders: u64; computed during
build_all via Broker::count_pending_reminders_for, mapping
manager → MANAGER_AGENT recipient + sub-agents → logical name.
Updates on every rescan (mutation sites + crash_watch's 10s
poll); accept 10s staleness on background remind / scheduler
delivery — live updates on operator cancel via /api/state path.

client renders a small cyan chip on the row when the count > 0;
tooltip points the operator at the reminders section to view
or cancel.
2026-05-17 23:52:56 +02:00
müde
2ad5a94897 dashboard: msgrow flex layout — fix per-char wrap when paths linkify 2026-05-17 23:20:29 +02:00
müde
1db6b8ffed dashboard: queued reminders surface
new 'qu3u3d r3m1nd3rs' section between approvals and operator
inbox. lists every pending reminder with agent, due-relative
timestamp, body, payload path (path-linkified), and a cancel
button. drives off a new /api/reminders endpoint and a
POST /cancel-reminder/{id} that hard-deletes the row.

failure surface (last_error / attempt_count + retry) deferred —
needs a sqlite migration; tracked in TODO.md.
2026-05-17 22:10:02 +02:00
müde
cb71a07300 dashboard: clickable file-path previews
agents constantly emit pointer strings to /agents/<n>/state/foo.md
since broker bodies cap at 1 KiB. now those tokens linkify in the
message flow, question bodies, answer text, and operator inbox;
clicking expands an inline <details> that lazy-fetches via the
new /api/state-file?path=... endpoint.

endpoint allow-list: per-agent state dirs + shared docs, both
in their container-mount form (/agents/<n>/state, /shared) and
host form (/var/lib/hyperhive/...). 1 MiB read cap; canonicalises
before the prefix check so `..` / symlinks can't escape.

legacy bare `/state/...` is deliberately not matched — ambiguous
from the host's perspective (we'd need to know which agent the
message references to translate). agents should use the qualified
form going forward.
2026-05-17 22:08:15 +02:00
müde
a15fafb5de dashboard: surface peer questions + operator override
questions pane now shows both operator-targeted threads
(target IS NULL) and agent-to-agent threads (target = some
agent). filter chips above the list: all / @operator / @peer /
per-participant. peer rows get a mauve left rule + a 0V3RR1D3
button that POSTs the same /answer-question endpoint
(OperatorQuestions::answer already permits the operator as
answerer on any target).

wire changes: OperatorQuestions gains pending_all +
recent_answered_all; QuestionAdded + QuestionResolved events
carry target: Option<String>; emit sites drop their
target.is_none() guard. answered-history rows show the
answerer prefix so override answers are auditable at a glance.
2026-05-17 22:06:53 +02:00
müde
8c186d4fb7 dashboard: msgflow uses shared terminal + backfill via /messages/history 2026-05-17 11:56:29 +02:00
müde
e283e39949 css: route palette + body typography through hive-fr0nt::BASE_CSS 2026-05-17 11:47:45 +02:00
damocles
6ba4241a45 show answered question history on dashboard 2026-05-17 01:41:59 +02:00
damocles
4a8a668348 feat: add optional description to request_apply_commit and request_spawn 2026-05-16 15:18:32 +02:00
müde
fe8fb15f8f Revert "dashboard: 3-column layout — swarm / 0per4t0r 1n / m3ss4g3s"
This reverts commit 74ba8a63e1.
2026-05-16 03:54:02 +02:00
müde
74ba8a63e1 dashboard: 3-column layout — swarm / 0per4t0r 1n / m3ss4g3s
regroups the 7 stacked sections into three semantic columns
backed by a CSS grid (single column under 1400px, 3 columns
above). column headers are sticky so vertical scrolling
inside a column doesn't lose context.

- SW4RM (left, slightly wider): containers + kept-state +
  spawn-agent form + meta-input update form. all
  swarm-mutating operator knobs live here.
- 0PER4T0R 1N (middle): mind-questions + pending approvals.
  the two things waiting on operator action.
- M3SS4G3S (right): operator-inbox + msg-flow tail + the
  @-mention compose box. broker traffic in one place.

spawn form moves out of renderApprovals into static HTML
under sw4rm; renderApprovals no longer injects it.

cosmetic: per-section h2/divider replaced with smaller cyan
sub-heads + a dashed underline so each column reads as one
cohesive unit instead of seven competing banners. body
max-width grows 70em → 110em to actually use the new
horizontal real estate.
2026-05-16 03:47:16 +02:00
müde
266c2c7a77 dashboard: meta flake inputs UI + sequential rebuild loop
new section 'M3T4 1NPUTS' between approvals and message flow:
one row per input in meta/flake.lock (hyperhive first, then
agent-<n> alphabetically). each row shows the input name, the
first 12 chars of the locked sha, a relative timestamp from
locked.lastModified, and the original.url when available.
checkbox per row; submit button is disabled until at least one
box is checked; submitting confirms then POSTs the selected
names to /meta-update.

backend:
- meta::lock_update(inputs: &[String]) — runs 'nix flake update
  <names>' in the meta dir, commits the lock change with a
  combined message ('lock update: hyperhive, agent-coder').
  preserves the existing META_LOCK serialization. existing
  lock_update_for_rebuild / lock_update_hyperhive stay for
  their single-input callers.
- POST /meta-update — comma-separated 'inputs' form field
  (JS joins checkboxes since axum::Form doesn't natively
  decode repeated keys); spawns a background task that runs
  the lock update + per-agent rebuild loop. hyperhive
  selection fans out to all agents; agent-<n> selection only
  rebuilds <n>. each rebuild fires Rebuilt to the manager
  exactly like dashboard / admin-CLI / auto-update.

rebuild loop is sequential — auto_update::run too (was
parallel via tokio::spawn). parallel rebuilds collide on
nix-store's sqlite cache ('sqlite db busy, not using cache')
and the meta META_LOCK contention. nix-daemon serializes the
heavy build steps anyway, so this isn't a throughput loss.
2026-05-16 03:38:07 +02:00
müde
96cb9f84c9 dashboard: approval history tab on P3NDING APPR0VALS
new tabs above the approvals list: 'pending · N' and
'history · M'. active tab persists in localStorage so the
operator can park on history if they prefer. on a fresh
dashboard the default is pending (matches the prior shape).

history view shows the last 30 resolved approvals — newest
first by resolved_at — with one row per approval: status
glyph (✓ approved / ✗ denied / ⚠ failed), id, agent, kind,
short sha, status label, and a relative time chip. when the
row has a note (deny reason or build error), it renders
below in a muted block with line wraps preserved.

backend: Approvals::recent_resolved(limit) queries by
status IN ('approved', 'denied', 'failed') ORDER BY
resolved_at DESC. StateSnapshot gets approval_history (a
lean ApprovalHistoryView without diff_html — rendering 30
git diffs per state poll would be expensive and the operator
already saw the diff at decision time). dashboard's
history_view fn projects the sqlite row.

retires the matching TODO entry.
2026-05-16 03:07:50 +02:00
müde
5208b0112a dashboard: terminal compose with @-mention sticky recipient
new section under MESS4GE FL0W. msgflow already tails only
broker traffic (sent + delivered), which is exactly the
'messages through core' view the operator wants; no
per-agent thinking leaks through. compose box below:

- a prompt span renders the sticky recipient ('@coder>'),
  rendered outside the textarea so it can't be edited
  inadvertently. on submit the recipient gets persisted to
  localStorage so it survives reload.
- start the input with '@name body' to redirect — the parser
  splits at the first whitespace and the new recipient
  becomes sticky.
- typing '@' at the start opens a completion dropdown over
  the textarea pulled from window.__hyperhive_state.containers;
  arrow keys cycle, tab/enter selects, escape closes. clicking
  works too.
- manager swap: agents flagged is_manager are surfaced as
  '@manager' (the broker's recipient string) instead of
  '@hm1nd' (the container name), so the message actually
  routes to the manager's inbox.

backend: new POST /op-send accepts {to, body} and drops a
broker.send({from:'operator', to, body}) — same shape as the
per-agent web UI's OperatorMsg, but lets the operator choose
the recipient explicitly from the main dashboard.
2026-05-16 01:55:00 +02:00
müde
6a2ffd521b surface agent-vs-agent port collisions (manager:8000 can't collide)
manager is fixed at 8000, sub-agents are 8100-8999, so collisions
are strictly between two sub-agents hashing to the same value.
the colliding container's harness restart-loops on AddrInUse —
which the user just hit on :8945. previously the only sign was a
buried journalctl warn line.

now surfaced two ways:

- lifecycle::spawn / rebuild preflight: walks the live container
  list, computes each agent's hashed port, refuses with
  'port N already taken by <other> — rename one of them' if any
  running sub-agent shares the new agent's port. so the operator
  sees an actionable error in the dashboard's transient pill /
  approve-result instead of waiting for the harness to die.

- /api/state grows a port_conflicts: [{port, agents: [...]}]
  array; dashboard renders a pulsing red banner above the
  containers list listing each cluster. matches the questions
  panel pulse so it's hard to miss.
2026-05-15 22:08:19 +02:00
müde
237b215c55 dashboard: browser notifications for operator-bound events
three signals fire OS notifications:
- new approval lands in the queue (per id, via /api/state delta)
- new ask_operator question queued (per id)
- broker message sent to operator (live via SSE)

first /api/state render after page load seeds the 'seen' sets
without firing — only items that arrive while the page is open
count. controls in a row under the banner: 🔔 enable
notifications (calls requestPermission, hides on grant), 🔕 mute /
🔔 unmute toggle (localStorage-backed so operator can silence
without revoking the permission), inline status text when blocked
or unsupported.

notification tag='hyperhive' collapses rapid bursts; onclick
focuses the dashboard tab. requires secure context (HTTPS or
localhost) — on other origins the API is unavailable and the
controls hide themselves.

todo: entry dropped.
2026-05-15 21:10:20 +02:00
müde
0385d96bf3 dashboard: per-container journald viewer
new GET /api/journal/{name}?unit=&lines= shells out journalctl -M
<container> -b --no-pager --output=short-iso --lines=<N> (cap 5000).
optional unit filter, restricted to hive-ag3nt.service /
hive-m1nd.service so the shell-out can't be coerced into reading
unrelated units. validates the container name against the live list
before invoking journalctl.

frontend renders a collapsed '↳ logs · <container>' details block
on each container row. expanding triggers a lazy fetch; refresh
button re-fetches; unit dropdown switches between the harness
service (default) and the full machine journal. output sits in a
24em-tall monospace pre, auto-scrolled to the bottom on fresh
fetch.

hive-c0re's systemd unit already runs as root, so journalctl has
the access it needs.
2026-05-15 20:42:56 +02:00
müde
754db7830e ask_operator: ttl_seconds auto-cancel + remaining-time chip
manager can pass ttl_seconds to ask_operator. on submit, host
stores deadline_at = now + ttl in operator_questions (new column,
migrated via existing pragma_table_info pattern), spawns a tokio
task that sleeps until the deadline then resolves the question with
answer '[expired]' and fires the same OperatorAnswered helper event.
already-resolved races no-op silently.

dashboard renders a ' MM:SS' chip on the question row when
deadline_at is set. format collapses seconds → s, < 1h → m s, ≥ 1h
→ h m. heartbeat refresh (5s) keeps the chip current; the operator
sees it tick down.

manager prompt + mcp tool description updated. journald viewer per
container queued in todo (separate task).
2026-05-15 20:38:02 +02:00
müde
ee5b85716d ask_operator: operator-side ✗ CANC3L on pending questions
new POST /cancel-question/{id} resolves a pending operator question
with the sentinel answer '[cancelled]' and fires the usual
HelperEvent::OperatorAnswered so the manager sees a terminal state
and can fall back. uses the same OperatorQuestions::answer path —
no special handling, the manager already has to deal with arbitrary
answer strings.

dashboard renders the cancel as a separate <form> below the main
qform so the answer-merge submit handler on the main form doesn't
inadvertently fire when the operator clicks cancel. confirm dialog
spells out what the manager will see.

ttl-based auto-cancel is still on the todo (would spawn a tokio task
per submitted question).
2026-05-15 20:25:11 +02:00
müde
5ee65d2f15 dashboard: K3PT ST4T3 section + agent links open in new tab
new section between containers and questions: lists every name with a
state dir under /var/lib/hyperhive/agents/ that doesn't correspond to
a live container. shows state size + last-modified age + whether
claude creds are kept. two actions per row:

- R3V1V3 — queues a spawn approval with the same name (operator
  approves to recreate; spawn flow reuses prior config + claude
  creds, no re-login needed)
- PURG3 — wipes the agent's state + applied dirs (post /purge-tombstone/
  endpoint; refuses if a live container with that name still exists)

dashboard also opens agent links in new tabs now (target=_blank +
rel=noopener) so the operator's overview tab stays put when they
dive into an agent.
2026-05-15 19:55:27 +02:00
müde
8344dd9ab7 ask_operator: multi-select + free-text fallback
ask_operator now accepts a multi: bool. when true and options is
non-empty, the dashboard renders the choices as checkboxes — operator
picks any subset, answer comes back as a ', '-joined string. when
false (default), options are radio buttons.

independent of multi, a free-text input ('or type your own…') is
always rendered alongside options so the operator is never trapped
by an incomplete list. submit merges checked options + free text into
the single 'answer' field.

schema migration: operator_questions grows a multi INTEGER column
with a one-shot ALTER TABLE on open. backward compatible — old rows
default to 0 (not multi).

prompt + mcp tool description updated; existing dashboard css for
.qform was rewritten around the new vertical layout.
2026-05-15 19:52:44 +02:00
müde
c337cc06f8 dashboard: spinners on in-flight lifecycle actions + cleaner row layout
backend:
- TransientKind grows Starting / Stopping / Restarting / Rebuilding /
  Destroying alongside the existing Spawning. each dashboard handler
  (start/restart/kill/rebuild/destroy) wraps the lifecycle call with
  set_transient + clear_transient so the dashboard knows what's in
  flight. transient kind is surfaced inline on ContainerView.pending
  (existing-container actions) — only Spawning (pre-creation) lands
  in the separate transients list.

frontend:
- container row is now two lines: identity + meta on top, action
  buttons below. less cluttered, leaves room for the pending state
  pill. pending rows dim their actions and surface a pulsing
  '◐ spawning… / starting… / stopping… / restarting… / rebuilding…
  / destroying…' indicator next to the name.
- 'needs login' / 'needs update' chips moved into a unified .badge
  styling for consistency.
- auto-refresh kicks in not only on transient spawn but on any
  container with a pending action.
2026-05-15 19:49:43 +02:00
müde
fd39226883 visuals: frosted-glass terminal/msgflow, row fade-in, badge pulses
agent terminal-wrap + dashboard msgflow get a translucent bg with
backdrop-filter blur+saturate so page-bg glow softens behind them.
new rows in the live panel and the dashboard message flow fade in
with a 4px slide-up. unread badge pulses; pending-operator-questions
section pulses its glow. history-backfilled rows skip the animation
(.no-anim) so the page doesn't stagger 100 fade-ins on load.
2026-05-15 19:20:15 +02:00
müde
d943bddd9e agent ui: input lives in terminal section, banner shimmer on activity
agent page restructure:
- send form moves into the terminal panel as a prompt-style row beneath
  the live tail (status line stays above so it still reads as a header).
- live panel + prompt share a single bordered 'terminal-wrap' box.
- harness-alive / login-state status lines drop their decorative ascii
  bookends; just a leading dot/glyph remains.
- banner gradient is now a real css gradient with a shimmer animation
  toggled by an .active class. turn_start adds it, turn_end removes it.
  dashboard side mirrors this: each broker sse event nudges a 4s
  shimmer window.
- dashboard container rows drop their static ▓█▓▒░ / ▒░▒░░ glyph
  prefixes; the role chips already disambiguate m1nd vs ag3nt.
- empty-state placeholders drop the ▓ bookends.

terminal pre-fill: hive-ag3nt::events::Bus grows a 500-event ring
buffer; new GET /events/history endpoint returns it. The agent JS
fetches history before opening the SSE stream so opening the page mid-
turn shows the last N events instead of a blank panel. The replay
walks turn_start/turn_end pairs to seed the banner-active state
correctly if a turn was still open.
2026-05-15 18:54:19 +02:00
müde
2770630f33 ask_operator tool: non-blocking; operator answer arrives as helper event
new mcp tool on the manager surface that queues a question on the
dashboard and returns the question id immediately. operator submits an
answer via /answer-question/<id>; the dashboard fires
HelperEvent::OperatorAnswered { id, question, answer } into the manager
inbox so the next turn picks it up.

also: fix async-form button stuck on spinner after successful submit
(refreshState skipped re-rendering, so the button was never re-enabled).
2026-05-15 18:44:42 +02:00
müde
7be64c5e66 theme: bring back the vibec0re glow on catppuccin mocha 2026-05-15 17:51:36 +02:00
müde
f33fc3dd50 theme: catppuccin mocha across dashboard + agent UI 2026-05-15 17:49:54 +02:00
müde
37c6504462 manager events: Spawned/Rebuilt/Killed/Destroyed + start button 2026-05-15 17:38:41 +02:00
müde
06ea0cf283 operator inbox view on dashboard; agent ui doesn't clobber typing 2026-05-15 17:23:53 +02:00
müde
8428c693e0 dashboard: stop/restart per-container + update-all when any stale 2026-05-15 17:00:56 +02:00
müde
edf42b7e93 extract dashboard + agent CSS/JS to assets/ (include_str!) 2026-05-15 16:32:35 +02:00