Commit graph hyperhive/frontend/packages/agent/src/app.js
Author SHA1 Message Date
iris
b62652c01b feat(#2196): stamp _body on mcp__bash__run, drop tool-specific JS branch
Backend now stamps `_body` (full `$ <cmd>`) alongside `_summary` (first
line) and `_category: "rich"` for mcp__bash__run tool_use entries. The
frontend drops the mcp__bash__run-specific branch in renderRichToolUse
and uses a generic `if (c._body)` path instead — api.details() with the
backend-computed summary and body, no JS knowledge of the tool name.
2026-07-19 18:16:46 +02:00
iris
0f5367f948 frontend/agent: drop JS dispatch tables, read backend _icon/_summary/_category
Phase 2 of hyperhive#2196. The backend now pre-computes enrichment fields
on every SSE event (stream_enrich.rs); the frontend reads them directly
instead of running its own dispatch logic.

Removed (~330 lines of JS):
- fmtArgsGeneric / TOOL_ICONS / toolIcon / fmtRoom / fmtUser / fmtToolUse

renderStream changes:
- system events: dispatch on v._category (drop/thinking_tok/note/details)
  + v._summary / v._body instead of per-subtype if-chains; status tick
  still overrides label client-side when stateName === 'compacting' since
  elapsed time is a wall-clock value the backend cannot know at emit time
- tool_use: use c._category === 'rich' for rich-renderer routing,
  c._icon / c._summary for flat rows

renderRichToolUse: toolIcon(name) -> c._icon (from backend enrichment)

stream_enrich.rs: also stamp _category: 'drop' on top-level
type=result / type=rate_limit_event so the frontend can use a single
_category check instead of separate type-based early returns
2026-07-19 17:51:12 +02:00
iris
617eecf94e agent ui: show elapsed seconds on status ticks during compaction
During a compaction pass, bare `status` ticks from claude are the only
live signal (no dedicated progress event exists in the headless stream-json).
Previously these rendered as a coalescing `⚙ status` row — uninformative
while waiting for a long compact to finish.

The harness emits `TurnStateChanged { state: Compacting, since_unix }`
immediately before invoking `session.compact()` for both the turn-end and
idle-session manual-compact paths. The frontend already tracks this in
`stateName` / `stateSince` for the state badge (`📦 compacting · Xs`);
now the terminal's `status` tick renderer reads the same two variables so
consecutive ticks show `⚙ compact · Xs…` (elapsed, in-place updating via
the existing coalescer) instead of the unhelpful `⚙ status`.

Result: what used to look like

    ⚙ status
    ⚙ compact · manual · 37k→2k tokens · 40.1s

now looks like

    ⚙ compact · 3s…   (updating in place)
    ⚙ compact · manual · 37k→2k tokens · 40.1s

Auto-compact (triggered by hive-claude's InfiniteSession policy internally,
no TurnState::Compacting set by the harness) continues to show `⚙ status`
as before.

Addresses #2276 (follow-up to status-tick coalescing in #2335).
2026-07-19 16:53:29 +02:00
damocles
c2bd7db998 refactor(#2416): remove the non-pr config-change flow (request_apply_commit / applycommit) 2026-07-15 21:03:52 +02:00
iris
a99508719e refactor: extract makeCoalescer helper for last-row update-in-place pattern
thinkingTokensRow/statusRow/pluginInstallRow were three copy-pasted
module-level row/text pairs implementing the same 'collapse repeated
ticks into one updating row' pattern. Extracted a small makeCoalescer(cls,
icon) factory returning an update(api, text) closure; each call site is
now a single line instead of the duplicated guard-and-update-or-create
block against api.mutableRow.
2026-07-11 20:30:38 +02:00
iris
82250b8f59 fix(agent-ui): coalesce plugin_install started/completed into one row
claude emits a started tick then a completed tick per plugin install;
previously each got its own row (loading... then done) instead of one
line updating in place. Same coalescing pattern already used for
thinking_tokens and the generic status tick.
2026-07-10 19:15:24 +02:00
iris
8c908651bc fix(agent-ui): collapse repeated generic status ticks into one row
Claude's generic `system/status` subtype carries no detail beyond the
bare label, and previously each tick got its own note row. During a
compaction pass (which emits a burst of these with no other signal
before the completed compact_boundary) this looked like a wall of
identical noise followed by silence, making a routine compaction look
stuck.

Collapse consecutive status ticks into one updating row, same pattern
already used for the thinking_tokens counter.
2026-07-10 14:13:36 +02:00
iris
ccc5e631e2 web-ui: sanitize markdown HTML with DOMPurify to fix XSS
Both mdNode implementations (agent UI app.js, dashboard common.js)
assigned marked.parse() output straight to innerHTML with no
sanitizer. marked v5+ dropped its built-in sanitize option, and there
was no DOMPurify anywhere in frontend/, so markdown containing raw
HTML/script tags rendered live in the browser.

Both sinks receive untrusted input in practice: the agent UI's mdNode
renders recv tool_result bodies, assistant prose, and send/ask/answer
payloads sourced from peer agents and matrix-relayed messages (the
documented prompt-injection adversary); the dashboard's mdNode renders
agent-authored .md files served verbatim by GET /api/state-file
(the endpoint validates path, not content). Since the per-agent UI and
dashboard are same-origin behind the gateway with operator-authority
endpoints (approve/spawn/rebuild/destroy/answer-question), injected
script would run with the operator's session.

Fix: DOMPurify.sanitize() the marked.parse() output at both sinks
before assigning to innerHTML. Added dompurify as a dependency to
both the agent and dashboard npm workspaces, recomputed npmDepsHash
in nix/frontend.nix for the updated lockfile. Also corrected
docs/web-ui/shape.md, which claimed the markdown-rendering path was
XSS-safe by construction the same way the text-node-based linkify
path is — it isn't; it's safe because it's sanitized.

CSP hardening for the dashboard (no unsafe-inline) is a separate,
larger backend change (response headers in hive-c0re) and is left as
a fast-follow rather than folded into this fix.
2026-07-10 11:54:50 +02:00
iris
6528a90233 fix(web_ui): drop stale (max) suffix from xhigh effort label
xhigh and max are distinct reasoning-effort levels (see EFFORT_LEVELS
in events.rs); labelling xhigh as '(max)' implied they were the same
level, per issue #2258.
2026-07-05 12:50:20 +02:00
damocles
788673341c refactor(term): add mutableRow to terminal api, use it for thinking_tokens 2026-07-05 12:39:26 +02:00
damocles
dc63bfcb23 fix(term): store text node ref for thinking_tokens, drop DOM dance 2026-07-05 12:20:37 +02:00
damocles
758f5ae5a8 fix(term): flex layout for details summary, fix thinking brain icon column 2026-07-05 12:10:24 +02:00
iris
40bd868f65 fix(agent-term): guard edit_schedule targets_add/remove against empty arrays
Empty arrays are truthy in JS so 'if (input.targets_add)' would emit
'+0 tgt' for an explicit []. Add .length guard so zero-element lists
are silently skipped — consistent with fmtArgsGeneric's [N] handling.

The MCP schema defaults both fields to null, making this theoretical,
but the guard is cleaner.
2026-07-04 18:22:05 +02:00
iris
f11357537e feat(agent-term): add formatters for scheduling and remaining request_* tools
Scheduling tools and the two remaining request_* tools fell through to
fmtArgsGeneric. They already had appropriate icons (⏱️ / 📦) but the
arg display was multi-field verbose.

New fmtToolUse cases:
- request_init_config: 'request_init_config* iris'
- request_update_meta_inputs: 'request_update_meta_inputs* [nixpkgs, …]'
  (or 'all' when inputs list is empty)
- list_schedules: 'list_schedules*()' (no args, explicit)
- cancel_schedule: 'cancel_schedule* #42 all' or '#42 [iris, dmatrix]'
- fire_schedule_now: 'fire_schedule_now* #42'
- edit_schedule: 'edit_schedule* #42 · body · interval · +2 tgt'
  (lists which fields are being changed, not their values)
- request_schedule_prompt: 'request_schedule_prompt* → iris at 14:00Z +3600s'

Closes #2201
2026-07-04 18:22:05 +02:00
iris
66057a97f7 fix(agent-term): split join_room/open_dm cases to fix open_dm always showing ?
join_room and open_dm shared a fall-through case with
(fmtRoom(input.room) || fmtUser(input.user_id) || '?'). fmtRoom()
returns the string '?' when input.room is undefined — truthy — so
the fmtUser() fallback was never reached and open_dm always displayed
'open_dm* ?'.

Split into two separate cases: join_room reads input.room, open_dm
reads input.user_id. Both degrade to '?' via fmtRoom/fmtUser's own
null guard.
2026-07-04 13:12:17 +02:00
iris
6c56bf76d6 feat(agent-term): matrix-tool icons + formatters, fill remaining fmtToolUse gaps
Add specific icons for matrix tools that had the generic 💬 fallback:
- mcp__matrix__read_room → 📖
- mcp__matrix__mark_read → 👁️
- mcp__matrix__list_rooms / list_room_members / list_invites → 📋
- mcp__bash__kill → 🛑 (was generic 🔧)

Add fmtToolUse cases for high-use tools that fell through to fmtArgsGeneric:
- set_status: 'set_status* "idle"' instead of 'set_status text: "idle"'
- get_loose_ends: 'get_loose_ends* [iris]' or 'get_loose_ends*()'
- get_agent_meta: 'get_agent_meta* iris' or 'get_agent_meta*()'
- cancel_loose_end: 'cancel_loose_end* question #42'
- bash kill: 'kill* abc123 [force]'
- Matrix tools: 'read_room* !abc1234 [50]', 'mark_read* !abc1234',
  'send_message* → !room: "body"', 'send_dm* → @mara: "body"',
  'send_reply*', 'send_reaction*', 'join_room*', 'open_dm*',
  'invite_user*', 'download_file*'

Also extend the short-name shortening to cover mcp__matrix__ prefix
(was only hyperhive + bash), so matrix tool rows show 'read_room*'
instead of 'mcp__matrix__read_room' as the prefix.

Two small helpers added: fmtRoom (truncates !id before ':' for
readability; keeps #alias intact) and fmtUser (@user:server → @user).

Closes #2198. Updates terminal-rendering.md icon list.
2026-07-04 13:12:17 +02:00
iris
94d537b7a9 feat(agent-term): show remind timing and message preview in tool row
`mcp__hyperhive__remind` calls previously rendered as the generic
`fmtArgsGeneric` output — with a multi-field input that showed
`message: "..." · delay_seconds: 300`, burying the message after
a verbose field name.

New format: `remind* +5m "check on PR..."` (or `at HH:MMZ` for
absolute timestamps). The timing renders first so it's scannable
at a glance, followed by the first 60 chars of the message body.
Works for all three input shapes: delay_seconds, at_unix_timestamp,
and file_path-only (shows the path as the preview).
2026-07-04 12:13:44 +02:00
iris
0a9f9a4fa6 feat(agent-term): complete tool-icon and fmtToolUse coverage
Several MCP tools appeared with the generic wrench icon (🔧) making
them hard to identify in the scrollback. fmtToolUse also lacked
specific formatters for some frequently-used tools, falling through
to fmtArgsGeneric.

Icons added:
- ack_until  →   (mark-as-read semantics)
- get_logs   → 📜  (log viewer)
- get_host_journal → 📜  (journal reader)

fmtToolUse cases added:
- ack_until          → "ack_until* ≤N" (message-id bound)
- get_logs           → "get_logs* <agent> [NL]"
- get_host_journal   → "get_host_journal* <container|unit> [/grep/] [NL]"
- restart/start/update → "restart* <name>" etc. (previously used
                          fmtArgsGeneric; now match kill's pattern)

Closes #2188.
2026-07-04 12:07:14 +02:00
iris
1fe6783756 feat(agent-term): show compact_boundary details in per-agent terminal
compact_boundary events currently render as the generic "⚙ compact_boundary"
muted note. The event carries useful metadata — pre/post token counts,
duration and trigger — that are invisible to the operator.

With this change the row reads:
  · ⚙ compact · manual · 772k→6k tokens · 101s

Fields rendered (all guarded — missing fields are silently omitted):
- trigger ("manual" or "auto")
- pre_tokens→post_tokens (formatted with k/M suffixes)
- duration_ms (ms or s)

Closes #2187.
2026-07-04 11:59:37 +02:00
iris
44531820d0 feat(agent-term): show details for plugin_install and commands_changed
Currently both system subtypes fall through to the generic muted
note renderer (⚙ <subtype>), giving the operator no insight into
what is happening.

plugin_install:
  Render status explicitly — "loading…" on started, "✓ done" on
  completed — so a slow plugin boot is visible in the scrollback
  instead of two identical cryptic rows.

commands_changed:
  Render the count of available slash commands in the summary row
  and expand to the full list in a collapsible details block. The
  list is most useful right after a fresh plugin_install so the
  operator can see exactly which /commands are now on offer.

Both handlers sit immediately before the generic catch-all in the
system-subtype dispatch in renderStream, preserving the existing
fall-through for other subtypes (context_window_exceeded etc.).

Closes #2183.
2026-07-04 11:59:37 +02:00
damocles
ebb85e6291 fix(#2121): rebuild POST from agent term also missing api/ prefix (mara audit) 2026-07-01 19:33:25 +02:00
damocles
98b5e976af fix(#2121): add missing api/ prefix to answer-question POST from agent term 2026-07-01 19:33:25 +02:00
iris
5264828091 fix: view-queue link + agent inbox shows unread messages only
fix(dashboard): update 'view queue' link to /builds.html

The build queue moved to its own /builds.html page. The queue-summary
'view queue →' link in tabs.js still pointed at /core.html, so clicking
it landed on the wrong page.

fix(broker): filter agent inbox to unread (acked_at IS NULL)

recent_for was returning all messages regardless of ack state, so the
agent inbox showed everything even after 'mark all read'. Now filters
to acked_at IS NULL — mirroring exactly what mark_all_read drains —
so the inbox empties on reload after the operator drains it.
2026-07-01 18:41:43 +02:00
iris
57f936af63 fix(agent): strip tool_use_error tags and render errors in red
When a tool call fails, claude wraps the result text in
<tool_use_error>...</tool_use_error> XML tags. The terminal was
displaying these raw, producing output like:
  '<tool_use_error>File has not been read yet.</tool_use_error>'

Fix renderToolResult in app.js:
- Check c.is_error on the tool_result content block.
- Strip the <tool_use_error>...</tool_use_error> wrapper from the text.
- Render error results with a '✗' prefix under '.tool-result.error'
  (flat, ≤120c) or '.tool-result-block.error' (<details>, longer text).

Add .live .tool-result.error { color: var(--red); } to terminal.css
so error results are visually distinct (red, same as turn-end-fail).

Update terminal-rendering.md row taxonomy to document the two new
error row classes.

Closes #2104.
2026-07-01 18:34:03 +02:00
damocles
f060456860 feat(#2024): add /effort terminal slash command 2026-06-27 13:35:14 +02:00
damocles
49db9d6e1c feat(#1990): surface pending recv() message count in get_loose_ends 2026-06-26 01:22:20 +02:00
iris
1c47bd4333 agent-ui: real fixed-width icon column for terminal rows
Replaces the first-character-glyph + negative-text-indent trick (which let a
wide emoji or a leading disclosure caret knock the icon out of column) with a
genuine icon cell.

terminal.js: row() / details() / detailsDiff() take an optional `icon` that
goes in a fixed-width `.row-glyph` element (inline-block, 1.4em). Details
summaries wrap their text in a `.summary-text` span; the disclosure caret
moves to `.summary-text::before` so it leads the text, not the icon — keeping
the icon in the shared column. terminal.css carries the cell + caret rules.

app.js passes the per-tool emoji as `icon` for the flat tool-use row and every
expandable tool summary (Write/Edit/send/ask/answer/bash) plus the 💭 thinking
row, instead of string-prefixing it. A details `🖥️` now lines up under a flat
row's `🧠` regardless of emoji width. Doc: terminal-rendering.md layout
contract updated. Closes #1844.
2026-06-22 00:41:24 +02:00
iris
fb58f0e5b9 agent-ui: per-tool emoji icons + thinking/turn-end glyphs in terminal
Following the 🧠 thinking-tokens indicator (operator liked it), give terminal
rows evocative glyphs:
- 💭 thinking prose (pairs with the 🧠 token counter)
- / turn-end ok/fail (was ✓/✗)
- per-tool icons via toolIcon(name): 📤 send · 📥 recv ·  ask · ✍️ answer
  ·  remind · 🏷️ set_status · 🪢 loose-ends · ↻/⏹️/▶️/🔄 lifecycle · 📦
  request_* · ⏱️ schedule · 🖥️ bash · 💬 matrix · 📖 Read · 💾 Write · ✏️
  Edit · 🔍 Glob/Grep · 🔧 default — on both flat rows and the rich
  send/ask/answer/Write/Edit summaries.

Dropped the original 🐝 task-row glyphs: claude's nested-agent Task tool is
intentionally omitted from agents' allow-list (mcp.rs), so those rows never
render for agents. Doc table updated.

Closes the emojify-terminal request.
2026-06-21 23:15:25 +02:00
iris
63a79d76ba agent-ui: collapse streamed thinking_tokens ticks into one live row
claude streams a running 'estimated_tokens' counter as system/thinking_tokens
events — many per turn (thousands in a long turn). renderStream rendered each
as a '⚙ thinking_tokens' note, flooding the terminal scrollback. Coalesce
consecutive ticks into a single '🧠 thinking … ~N tokens' note row that
updates in place; reuse the row only while it's still the last one rendered
(nextElementSibling == null) so any other event starts a fresh one. Closes
#1818.
2026-06-21 22:09:31 +02:00
iris
cd7846b348 bash-tasks page: live interval poll + spawn_blocking the dir scan (review)
Two fixes from review:
- Liveness: /api/state isn't polled while online (only during login), so
  hooking refreshBashTasks to it only populated on cold load. Bash tasks
  start + finish asynchronously between turns, so add a light ~4s interval
  to keep the tasks pill live; the /api/state-time call now just does the
  first-paint populate. Doc note corrected to match.
- Move the blocking dir scan + per-file reads in /api/bash-tasks off the
  async executor via tokio::task::spawn_blocking (damocles nit).
2026-06-21 13:28:37 +02:00
iris
4ce919a19f agent-ui: running bash-tasks pill + flyout on the agent page
Adds a 'tasks' header pill (hidden at zero, like inbox/loose-ends) that
opens a side-panel flyout listing the agent's in-flight bash tasks from
GET /api/bash-tasks. Each row shows status (running/queued), task id,
elapsed time, and a truncated single-line command preview. Polled on the
same /api/state cycle as loose-ends (tasks complete async between turns, so
the count stays live); clicking the pill opens the flyout. Snapshot-only
v1 — SSE live-push is a possible follow-up.
2026-06-21 13:28:37 +02:00
iris
e89ca90956 feat(agent-ui): show turn start/end times + duration on the agent terminal
The per-agent terminal marks turn boundaries (◆ TURN ← / ✓ turn) but
shows no time. Append a wall-clock HH:MM:SS to both the turn-start and
turn-end rows, plus the elapsed duration on turn-end, rendered as dim
metadata so the boundary glyph stays the focus.

This is the frontend half of the feature; it reads a per-event `ts`
(unix seconds) off the turn_start / turn_end events. The read is guarded
on a numeric `ts`, so until the harness surfaces per-event timestamps
the rows render exactly as before (inert-until-capture). Once the
backend attaches `ts` to the history rows + live SSE frame, the times
light up automatically for both live and scrollback, with no further
frontend change.
2026-06-10 14:36:54 +02:00
iris
ceb3e3b01a feat(frontend): per-agent effort-level quick-picker (#1596)
Adds a reasoning-effort quick-picker to the agent page's overflow menu,
a direct sibling of the existing model quick-picker, wired to the backend
seam from #1597/#1600: /api/state carries effort + available_efforts, the
picker POSTs { effort } to /api/effort (operator-only, mirroring
/api/model), and live updates arrive via the effort_changed SSE event.

The available levels are the backend's to declare — the frontend holds no
hard-coded list; availableEfforts is seeded from state.available_efforts
on cold-load and the picker section is omitted until the backend supplies
the set. Mirrors the model picker otherwise: postEffort(),
renderEffortChip() (updates the new amber effort-chip + button active
states), the cold-load seed, and the effort_changed handler. Build green.

Applies on the next claude session (operator uses the existing
clear-session), per the backend contract.
2026-06-10 01:34:21 +02:00
iris
8ddab401f9 feat(dashboard): serve H0M3 at /, relocate dashboard to /dashboard.html (#1464)
Step 2 of the nav restructure: make the H0M3 menu hub the landing page
at / and move the dashboard SPA to /dashboard.html, with every surface
linking back to the hub.

Mechanism (frontend-only, no host-side change — confirmed against the
ServeDir route table): the dashboard's ServeDir fallback serves
index.html at / via append_index_html_on_directories, and there is no
SPA path-routing catch-all to break. The dashboard SPA is served as the
plain file dashboard.html so it never shadows the exact-match
/dashboard/stream + /dashboard/history SSE routes registered before the
fallback.

- Swap the HTML entry files: the H0M3 page becomes index.html (loads
  home.js → served at /), and the dashboard SPA becomes dashboard.html
  (loads tabs.js → served at /dashboard.html). build.mjs copies the new
  set; JS bundle names are unchanged (referenced by absolute /static/
  paths, independent of the HTML filename).
- H0M3 Dashboard tile now points at /dashboard.html.
- The dashboard gains a "← home" back-link in its chrome; flow.html and
  logs.html relabel their back-link from "← dashboard" to "← home"
  (href stays / — which is the hub now). Pages link to the hub, not to
  each other.
- Agent page (app.js + stats.js): the "↑ dashboard" link now targets
  /dashboard.html. The API base (rebuild / answer-question /
  mark-all-read POSTs) stays the origin root, unchanged.
- Comment-only: tabs.js / flow.js / common.js references to the
  dashboard's old index.html filename updated to dashboard.html.

Note for review: git renders the file swap as a deleted home.html + an
added dashboard.html + a heavily-modified index.html, because index.html
exists on both sides with swapped content. It's a content swap, not a
rewrite — the built dist/ is verified (index.html→home.js, dashboard.html
→tabs.js).

Deferred to a follow-up: removing the FL0W / L0GS / M4TR1X "→" page-links
from the dashboard tab strip (touches tabs.js gating/overflow), and the
shared reusable chrome component (a later step).
2026-06-07 21:00:34 +02:00
iris
1f54a07195 feat(agent): read available models from HIVE_AVAILABLE_MODELS env var
web_ui.rs: add available_models() helper that reads HIVE_AVAILABLE_MODELS
(comma-separated, injected by services.hyperhive.availableModels nix option).
Falls back to ["haiku", "sonnet", "opus"] when absent or empty. Field added
to StateSnapshot so the frontend receives the list on cold-load.

app.js: replace hardcoded MODEL_ALIASES array with availableModels module var.
Seeded from state.available_models before setHeader/populateOverflowMenu on
first /api/state load. Well-known aliases (haiku/sonnet/opus) still get their
descriptive labels; operator-declared custom model names show the name itself.

Implements the frontend + web_ui.rs side of issue #1359 (nix option shipped
separately in PR #1360 by atlas).
2026-06-05 13:48:03 +02:00
iris
6c74c6dafa feat(agent): model quick-picker in overflow menu
Adds three one-click model shortcuts (haiku / sonnet / opus) at the
bottom of the agent overflow menu (⋯), below a separator and a small
'model' section label.

- The active model alias is highlighted with .active (purple border +
  tint) so the operator can see at a glance which model is current.
  Matching tolerates both short aliases (haiku) and full API names
  (claude-3-5-haiku-20241022) via an endsWith check.
- Clicking the active model is a no-op (closes menu, no POST).
- Clicking a different alias calls postModel() and closes the menu.
- renderModelChip() keeps the picker buttons in sync on every
  /api/state refresh without rebuilding the menu.

Before this change the operator had to type /model <name> in the
compose box. The overflow menu provides a discoverability path for
operators who don't remember slash commands.
2026-06-05 13:14:17 +02:00
iris
defcb5df0b fix(agent): use gateway-relative dashboard URL when behind hive-gateway
When the agent page is accessed via hive-gateway (path is /agent/<name>/)
the dashboard lives at the same origin's root (/), not at the direct
TCP port. Both app.js (overflow menu / setHeader) and stats.js
(dashboard-link anchor) previously hardcoded http://hostname:port which
is wrong or unreachable behind HTTPS TLS termination.

Detection: if location.pathname starts with '/agent/', use
location.origin + '/' as the dashboard base. Otherwise fall back to the
original direct-TCP construction for non-gateway deploys.
2026-06-05 13:05:01 +02:00
iris
bddeb5478e fix(#1286): pretty-print system events in the agent terminal
The catch-all renderer was dumping raw JSON (in loud orange) for
claude's system events like api_retry — producing noise like:
  ! {"attempt":1,"error":"unknown","subtype":"api_retry",...}

Replace the single 'init' filter with a full system-event block:
- api_retry → muted note: '⚠ api retry 1/10 · unknown · 502ms'
- api_error → amber note:  '✗ api error · <message>'
- other subtypes (context_window_exceeded, etc.) → '⚙ <subtype>'

The loud orange sys catch-all is now reserved for truly unrecognised
top-level event types, not routine operational noise.
2026-06-04 18:54:33 +02:00
damocles
f5351eb59c fix(#1194): update Execution tool names + frontend + docs 2026-06-03 21:20:49 +02:00
iris
7767493428 feat(#1161): show full bash commands in terminal as expandable details 2026-06-03 18:14:52 +02:00
iris
2a62a561c4 chore: frontend cleanup — replaceChildren, missing source badge, http case
- Replace innerHTML = '' with replaceChildren() throughout tabs.js and
  app.js (12 + 7 sites). paintAtomic already used replaceChildren; now
  the direct-clear sites are consistent with it.
- Add missing .rqe-source-startup_sweep CSS rule (startup_sweep is a
  valid QueueSource variant but had no badge style, falling through to
  the base muted appearance with no explicit intent).
- Lowercase the one uppercase 'HTTP ' in the fetchAndRenderToolGroups
  error path to match every other fetch error in the file.
2026-06-01 22:48:21 +02:00
iris
d3ca857659 fix(#888): use gateway forge URL in dashboard links
Dashboard links to the forge were hardcoded as http://<hostname>:3000,
which breaks when the operator accesses the dashboard through
hive-gateway (forge is served at forge.<domain> with no port).

- nix/modules/hive-c0re.nix: inject HIVE_FORGE_PUBLIC_URL when
  forge.behindGateway = true (e.g. http://forge.pr1ma.darkest.space)
- hive-c0re/src/dashboard.rs: expose forge_url: Option<String> in
  StateSnapshot, reading from HIVE_FORGE_PUBLIC_URL
- frontend/packages/dashboard/src/tabs.js: use state.forge_url when
  present; fall back to http://<hostname>:3000 for gateway-off /
  local-dev deploys
2026-05-31 22:04:52 +02:00
iris
e1e5195081 dashboard + agent UI: surface hive_name + swarm_name in chrome (#701)
Damocles's backend (c41bf1b) landed HYPERHIVE_HIVE_NAME / _SWARM_NAME
env vars and identity.rs accessors. This commit wires them into the two
web surfaces:

hive-c0re/dashboard.rs:
  StateSnapshot gains `hive_name` + `swarm_name` (Option<String>),
  populated from the env vars the c0re NixOS module injects.

hive-ag3nt/web_ui.rs:
  Per-agent StateSnapshot gains the same two fields, populated from
  crate::identity::hive_name() / swarm_name().

Dashboard frontend (index.html + tabs.js):
  A `<p class="banner-thin" id="hive-identity">` sits above the tab
  strip in the chrome. refreshState() populates it with "swarm / hive"
  (or just "hive") when the fields are non-null, and updates
  document.title to "<swarm>/<hive> // h1ve-c0re". No change to
  chrome when both fields are null (backward compat).

Per-agent frontend (index.html + app.js + agent.css):
  A hidden `.agent-hive-label` span under the title row shows the
  swarm/hive label once setHeader() receives non-null names.
  document.title is updated to "<label> // <hive_name>" when hive_name
  is set, enabling browser-tab disambiguation when multiple hives are
  open in parallel. Styled as a small uppercase purple-dim sub-label.
2026-05-31 21:09:08 +02:00
iris
4cee50a3be agent/app.js: final cookie scrub — 0 cookies (#713 batch 3 final)
All 19 remaining cookies in app.js were attribution refs to
closed issues / PRs / past mara/argus reviews. Substance is
already in docs from earlier batches (PR #780). This batch
collapses the cookie attributions to brief inline notes pointing
at docs where appropriate.

- #360 side-panel duplication note → drop closed-issue framing
- #394 vibec0re header redesign (×5) → drop cookies, prose stays
- #589 hive-qualified label glyphic-title note → drop cookie
- #576/#542/#584 logout chain rationale → drop cookies, prose
  preserved (the WHY + .credentials.json + mcp-needs-auth-
  cache.json detail stays inline)
- #14 relative paths convention → docs/web-ui.md::Per-agent
  relative paths pointer
- #233 marked autolinks new-tab → drop cookie
- #559/#566 mark-all-read flyout (×3) → docs/web-ui.md::Per-agent
  page (Loose-ends flyout) pointer + drop attribution
- #666 ask→operator inline-answer slot (×2) → docs/web-ui.md::
  Per-agent page (Ask → operator inline-answer binding) pointer

app.js: 19 → 0 cookies (100% reduction). Combined with PR #780
+ batch 1 + 2, app.js is fully migrated for #713 scope.

Closes #713 — all agent UI files (app.js, agent.css, screen.html,
index.html, stats.js, stats.html) at 0 issue-ref cookies.
2026-05-31 15:36:23 +02:00
iris
3f84b6492a agent/app.js + docs: migrate ask→operator inline-answer binding prose (#713 batch 1 cont.)
Moves the #666 ask→operator inline-answer wiring rationale from
app.js into a new docs/web-ui.md::Per-agent page sub-paragraph
**Ask → operator inline-answer binding**. Substantial block —
the slot-registry / reconcileAskBinds / buildAnswerForm trio has
real design rationale (async question id, text-match pairing,
resolved-vs-cancelled-vs-expired neutrality, defensive prune,
no-regression-fallback to side-panel answer) that belongs in
docs, not three JSDoc blocks scattered through app.js.

Moved to docs (~30 lines of substantive prose):
- pendingAskBinds slot-registry rationale
- reconcileAskBinds matching algorithm (text-match, first-unbound
  to first-unclaimed pairing stability)
- defensive prune for disconnected slots
- [resolved] vs ✓ rationale (answered / cancelled / TTL-expired
  ambiguity — neutral label)
- fallback to side-panel answer when slot stays unbound

Collapsed in app.js: 3 JSDoc blocks + inline comments → ~10-line
total pointer. Drops #666 ×3, #559, #668 cookies en passant
(substance now lives in docs).

app.js: 24 → 19 refs in this commit; 27 → 19 across batch 1
(30% reduction so far). Net ~57 lines of substantive prose
migrated from app.js to docs/web-ui.md across both commits in
this PR.
2026-05-31 14:35:09 +02:00
iris
8366c3739b agent/app.js + docs: migrate stacking-context + OAuth + nav-link prose to docs (#713 batch 1)
First batch of #713 (agent per-container UI prose migration).
Moves 3 substantive WHY-explanations from app.js into
docs/web-ui.md::Per-agent page, replaces each in-code mention
with a brief pointer.

Moved to docs:
- **Terminal-wrap pill anchor** (~11 lines): why the `↓ N new`
  pill is anchored in `.agent-main` rather than the default
  `.terminal-wrap` parent — backdrop-filter creates a stacking
  context, anchoring inside it traps the pill's z-index below the
  composer.
- **OAuth code input** (~14 lines across two blocks): masked
  password + reveal toggle + `autocomplete="one-time-code"`
  rationale (shoulder-surfer / screenshot exposure + WHATWG
  semantic value + suppresses browser save-password prompt).
- **NavLink kind resolution + XSS-safe DOM-build** (~8 lines):
  Container → same-origin, Forge → `http://<host>:3000<url>`,
  External → already absolute; agent-declared strings never reach
  innerHTML.

Collapsed in app.js: each block becomes a 3-4 line pointer to the
matching docs section. Drops #262 / #568 / #592 cookies en passant
since their substance now lives in docs. The `#14` cookie on
`historyUrl` / `streamUrl` is also scrubbed — the path-relative
shape is the convention, not an open issue. `#375` (agent.css ID
ref) drops as part of the pill-anchor block collapse since it was
sole-purpose pointing inside that comment.

app.js: 27 → 24 refs. Net ~35 lines of substantive prose moved out
of app.js into docs/web-ui.md::Per-agent page, where it belongs.
2026-05-31 14:35:09 +02:00
iris
37d99ed118 agent UI: relative paths for all assets/api/ws so the page works under any nginx prefix (#14)
Per mara on #14: 'make agent page not assume root path, links / api
calls need to be relative'. atlas's nginx side (#15) will mount the
per-agent UI at a prefix like /agent/<name>/ instead of its own
port; for the page to keep working under that prefix, every
in-page reference needs to resolve document-relative rather than
root-anchored.

Converted in this pass:
- HTML <link>/<script>/<img>/<a> hrefs in index.html, stats.html,
  screen.html: '/icon' → 'icon', '/static/agent.css' →
  'static/agent.css', back links '/' → './'.
- app.js fetch() targets ('/api/state' → 'api/state', /api/cancel,
  /api/loose-ends, etc.), form actions ('/login/start', '/send'),
  EventSource urls ('/events/stream', '/events/history').
- stats.js fetch() targets.
- screen.html WebSocket URL: was hardcoded as
  ws(s)://host/screen/ws; now derived from document.baseURI via
  new URL('screen/ws', document.baseURI) so the gateway prefix
  flows through.

Slash-command labels (/cancel, /compact, …) and the dashboard-port
link (different port, intentionally absolute) intentionally
untouched.

Added a new 'Per-agent relative paths' section to docs/web-ui.md
covering the rationale + the trailing-slash gotcha (sub-pages like
/stats must NOT have a trailing slash, or 'static/app.js' resolves
under /stats/ instead of replacing the segment).

Functional code unchanged; build clean. Damocles + atlas can
proceed with the backend / nginx side without depending on this
landing first, but once both ship the agent page works under the
gateway-prefixed URL without further changes.

refs #14
2026-05-31 12:10:56 +02:00
iris
a9114342e4 agent/terminal: neutral [resolved] label for closed asks (argus #668)
Loose-ends endpoint only carries pending state — a question
disappearing from the list can mean answered, cancelled by asker,
or TTL expired. Previous [answered ✓] glyph implied successful
operator response across all three paths.

Rename .ask-answered-tag → .ask-resolved-tag and use the neutral
[resolved] label. Full resolution detail (who answered with what)
remains visible via the question's history in the side panel.

Addresses argus review note on #668.
2026-05-30 21:45:33 +02:00
iris
e056e82d6d agent/terminal: inline answer form under ask → operator rows (#666)
When an agent calls mcp__hyperhive__ask with to==operator (the
default), the rich tool-use renderer now mounts an empty
ask-answer-inline-slot inside the expanded ask row and enqueues a
loose-ends refresh. A new reconcileAskBinds() walks waiting slots
on every loose-ends update, matches each against pending
operator-bound questions by question text, and injects the same
inline answer form the side panel uses (buildAnswerForm → POST to
the host dashboard answer-question endpoint). When a question
resolves, the form gets replaced by a struck-through [answered ✓]
tag so the scrollback reflects the closed state.

Lets the operator respond to agent questions inline in the live
terminal without context-switching to the loose-ends side panel or
the dashboard Y3R C4LL tab.

Refreshes loose-ends both on tool_use render (best-effort, may
miss the question before the broker persists it) and on the
matching tool_result (the right moment — MCP has just returned the
assigned id). Slots are pruned on /clear and defensively filtered
for isConnected on each reconcile.

closes #666
2026-05-30 21:45:33 +02:00
iris
f44bf19707 hive-ag3nt: identity module with hive-qualified label (#589 phase A, first PR)
First chunk of #589 v0 phase A: plumbing the hive-qualified
'name@hive' form through the per-agent surfaces that the harness
itself owns. Broker from/to + dashboard rendering + container_view
follow in subsequent PRs once damocles ships the HYPERHIVE_HIVE_DOMAIN
env var in harness-base.nix.

- new hive_ag3nt::identity module: label() / hive_domain() /
  qualified_label() / qualify(label). Reads HYPERHIVE_HIVE_DOMAIN
  (set by hive-c0re.nix module from hyperhive.domain) — when unset
  or empty, qualified_label degrades to just the short label so
  existing single-hive deployments are unchanged. Six unit tests
  cover the set / unset / empty / arbitrary-label paths.
- prompt::render gains {qualified_label} substitution alongside
  the existing {label}. system.md template uses both: the agent
  intro now reads 'You are hyperhive agent iris (qualified:
  iris@darkest.space) in a multi-agent system. ... When you're
  talking to or about a peer on a different hive, use the
  qualified form (name@hive) so the operator + the manager can
  disambiguate'. Manager flavor gets the same treatment.
- /api/state gains qualified_label: String. Always present, equals
  label when no domain is configured.
- frontend setHeader takes the qualified_label, drives the browser
  tab title (so two  tabs from different hives are
  distinguishable in the tab bar) while the glyphic #title stays
  short for the cinematic header.

Gated on env var presence — no behaviour change for single-hive
deployments. Pairs with damocles's upcoming harness-base.nix
HYPERHIVE_HIVE_DOMAIN ship; safe to land in either order.
2026-05-29 19:09:44 +02:00