Commit graph hyperhive/frontend/packages/agent
Author SHA1 Message Date
iris
d7e70fd195 agent: badges join the pills cluster, not the other way around
mara, live, correcting the prior round: 'oh i meant move badges to
where the dropdowns are not the other way around. this way the height
does not change even on widescreen.'

Prior commit nested pills INTO a new row inside .agent-header-main
(badges + pills sharing a row under the title). Backwards from what
she wanted: badges now join .agent-header-pills instead — the same
header-level column pills already lived in, which already handles a
variable child count via flex-wrap + justify-content: flex-end.
.agent-header-main goes back to just the title + hive-label rows,
which don't wrap in practice, so its height stays stable regardless of
how many badges/pills are showing.

The ResizeObserver dynamic-height fix from the prior commit is
untouched and still does the real work of keeping .agent-status-
overlay's offset correct if anything does wrap — this is a pure
layout-preference change on top of that, not a new bugfix.

Verified at 500/1024/1400px — badges+pills form one right-aligned
cluster next to the title, no overlap, no ResizeObserver console
error. tsc --noEmit clean, build clean, both pre-push lints clean.
2026-08-28 22:47:49 +02:00
iris
f49e236c74 agent: dynamic header height, badges+pills one row, composer alignment
mara, live: 'login panel still looks like it is behind', 'bottom input
row text not aligned / inconsistent sizes', 'can we put the badges on
the same row as the links button etc? then the header would be more
compact.'

- Real fix for the overlap this time, not another band-aid: the
  header's downstream offset (.agent-status-overlay, .agent-main's
  scroll padding) was reading a STATIC --agent-header-h guess (6em).
  Every prior fix on this PR removed one specific contributor to the
  header wrapping past that guess (meta-nav's inline list, then
  disconnected badge/pills clusters) but the guess itself was always
  the actual defect — any content that can wrap an extra line breaks
  it at whatever width triggers it, and no width is safe to promise.
  Header.tsx now measures its own rendered height via ResizeObserver
  and writes it to a new --agent-header-real-h var; downstream
  consumers prefer that over the static guess. Deliberately a SEPARATE
  var from --agent-header-h, not an overwrite: .agent-header's own
  min-height also reads that var, so overwriting it in place created
  an actual ResizeObserver feedback loop (confirmed live on the first
  pass of this fix, fixed by splitting the var).
- Badges (StatusChips) + pills (inbox/todos/links/overflow) now share
  one row (.agent-state-row) instead of two visually disconnected
  clusters (old app.js markup's 3-column layout, which the new page
  had also carried over) — pills pushed to the row's right edge via a
  margin-left: auto rule scoped to that nesting, so app.js's own
  still-live separate-column usage of the same classes is untouched.
- Composer text alignment: real cause was .prompt (1em) and
  .submit-hint (0.8em) each getting a different-sized default line-box
  under align-items: flex-start, so the same nominal padding-top
  landed at different absolute offsets. align-items: center sidesteps
  the whole line-box-size mismatch. This one's shared with app.js too
  (same markup/classes) — same bug there, now fixed for both.

Verified at 480/500/768/1024/1400px — no overlap, no ResizeObserver
loop, badges+pills share one row, composer aligned. tsc --noEmit
clean, build clean, both pre-push lints clean.
2026-08-28 22:30:35 +02:00
iris
3ce961e5bd agent: meta-nav popout + header height fix + badge row consistency
mara, live review: 'the login card looks like it is behind the header',
'badges and interactive elements on same row have inconsistent size',
'the links should have the same popout as the links in the nav bar of
swarm-ui.' All three, addressed:

- Root cause of the overlap: MetaNav rendered every link inline in the
  header's title row, a variable-width list that can grow the row past
  the fixed --agent-header-h the rest of the page (position: fixed
  header, position: absolute content below it) is offset against, so
  the header visually covers whatever's underneath it once it's
  actually taller than assumed. Fix: MetaNav is now a single
  fixed-size trigger (reuses @hive/shared's Badge, icon-only, same
  shape as the header's existing overflow trigger) in the pills
  cluster, not a variable-width list in the title row. A single badge
  can't grow the row regardless of how many links the backend sends.
  Verified at 1024px/1400px (no overlap, was previously untested below
  1400px) — the deeper fragility (StatusChips' badge row itself can
  still wrap on a genuinely narrow/mobile viewport and hit the same
  class of bug) is real but predates this PR and is out of scope here.
- The popover mara asked for: same visual language as swarm-ui's own
  LinksMenu (Shell/LinksMenu.tsx) and this page's Dropdown/OverflowMenu
  — quiet icon trigger, popover on click. Not built from @hive/shared's
  Dropdown itself (its items are always <button>s for command dispatch,
  which would lose real <a> link semantics — ctrl/middle-click, copy
2026-08-28 22:05:13 +02:00
iris
df78236ce4 agent: meta-nav, favicon fallback, document.title
Ports the 3 remaining gaps argus's lost-functionality audit found beyond
login: the header meta-nav strip (stats/screen/forge/config +
hyperhive.dashboardLinks extras — the only on-page path to those, not
just polish), the header icon's /favicon.svg fallback on a broken
image, and the browser tab title update. All three are straight ports
of app.js's existing logic (refreshState's meta-links loop,
bindHeaderIconFallback, setHeader's document.title block), same
kind -> URL resolution rules, same dataset.fallback loop guard, same
qualified_label/hive_name fallback chain — no new backend fields
needed, hive_agent::web_ui::state::StateSnapshot already serves
links/forge_public_url and useAgentState's whole-payload cast already
threads them to Root.

New <MetaNav> component (reuses agent.css's existing .agent-nav/
.agent-nav-link rules verbatim, no new CSS) renders in Header's title
row via a new nav prop. Favicon fallback lives in Header itself
(onError handler on the .agent-icon <img>). document.title is a
useEffect in Root keyed on label/qualified_label/hive_name.

tsc --noEmit clean, build clean, both pre-push lints clean. Screenshot
verifies meta-nav renders 4 links (stats/forge/config/extra, forge
kind resolving against a mocked forge_public_url) and the favicon
fallback firing against a deliberately-404'd /icon.
2026-08-28 22:05:13 +02:00
iris
b56696d898 agent: LoginFlow — one card, matching Dropdown's popup visual language
mara: "reuse the popup component for the login, the floating separate
panels should be one card" — the old markup's .agent-status-overlay >
* rule boxed every direct-child <p>/<form> separately, a faithful port
of app.js's el().append()-per-line but not what a hand-authored
component should do. Wraps each state's content in one .login-card
div instead, styled to match Dropdown's .ui-dropdown (@hive/shared)
background/border/radius/shadow values exactly, so this reads as the
same popup family as every other floating surface in the app rather
than a one-off. Not the Dropdown component itself — its options.map
render is menu-item-shaped, not a fit for a login form + output pane.

agent.css's .agent-status-overlay > * rule is untouched (app.js's
still-live markup depends on it verbatim until the index.html
cutover) — .login-card coexists with it rather than editing it,
correctly overriding on equal specificity since main.css loads after
agent.css.
2026-08-28 22:05:13 +02:00
iris
ae94fff5af agent: LoginFlow — the needs_login recovery UI app.js's rewrite was missing
Ports app.js's renderNeedsLoginIdle/renderLoginInProgress as real
functional Preact UI, not just a StatusChip label: a start-login
button (POST login/start), the OAuth-URL link once claude emits it,
a code-paste form (POST login/code, masked + reveal toggle) with
cancel (POST login/cancel), and the streamed process output. Without
this, an agent needing re-login (post-/logout, credential rotation)
had no web-UI path back in through the new page — the gap I flagged
before the cutover; mara: "1 - do it now".

Reuses agent.css's existing .agent-status-overlay/.status-needs-login/
.btn-login/.loginform*/.diff rules verbatim (same class names as the
old markup), matching Header.tsx's precedent — no new CSS file needed.
Added SessionView to types.ts (mirrors hive-agent's SessionView struct
exactly: url/output/finished/exit_note) and lib/loginAction.ts (same
{ok,detail} fetch shape as modelEffort.ts — same-origin POSTs, no CORS
concerns like pauseAction.ts's dashboard-origin actions).

Screenshot-verified all 3 states (idle, in-progress-with-url, finished
with error) against a mock server.
2026-08-28 22:05:13 +02:00
iris
48a1745bb5 agent: OverflowMenu — trimmed to rebuild + dashboard link
Deliberately trimmed vs. app.js's `populateOverflowMenu`, which held
dashboard-link/rebuild/new-session/logout/model-picker/effort-picker
all in one flat list — the design guide's own named junk-drawer
anti-example. Model/effort already moved to StatusChips' own
Badge+Dropdown controls; new-session/logout now live as TermInput
slash commands (typed, with their own two-step confirm). What's left —
rebuild + a dashboard back-link — is genuinely menu-shaped (rare, not
tied to any other visible control), so it's still a `⋯` menu, just a
much shorter one.

Reuses `@hive/shared`'s Badge+Dropdown pair (same anchored-popover
shape as StatusChips' model/effort pickers), not a bespoke popover.
Rebuild's confirm is the same "select once to arm, select again to
fire" idea as the slash commands, adapted to a menu click. New
`lib/rebuildAction.ts` — same cross-origin-needs-a-real-form-submit
reasoning as pauseAction.ts (hive-c0re's `/api/rebuild/{name}` has no
CORS header either).

Found + fixed a real layout gap in the shared Dropdown while
screenshot-verifying: its CSS anchors left-edge-to-left-edge, correct
for a picker with room to its right, but the overflow trigger is the
header's right-most element, so the menu ran off-screen. Fixed with a
scoped override in this component's own CSS (`.header-overflow-anchor
.ui-dropdown { left: auto; right: 0; }`) rather than changing the
shared default, which is still correct for every other caller.
2026-08-28 22:05:13 +02:00
iris
d9249a36bf agent: TermInput — composer + slash commands
Ported from app.js's `renderTermInput`/`handleSlashCommand`/
`completeSlash`: prompt + auto-growing textarea, Enter sends (Shift+Enter
newline), Tab cycles slash-command completion. Same command set
(/help, /clear, /cancel, /compact, /model, /effort, /new-session,
/logout), same routes (`api/cancel`, `api/compact`, `api/new-session`,
`api/logout`, `send`) via new `lib/termActions.ts` (same `{ok, detail}`
shape as modelEffort.ts's POST helper).

One deliberate UX change: `/new-session` and `/logout` used to pop the
old shadow-DOM `themedConfirm` modal before firing — this rewrite's
destructive actions all avoid that widget family already (SidePanel,
useConfirmClick), and a modal doesn't fit a text-input flow anyway.
Typing the command once arms it (a local note explains what confirming
does); typing it again fires it — a keyboard-native two-step confirm.

`/help`/`/clear` need to reach into LiveStream's row list (local-only
echo rows, never sent anywhere) without lifting that state up to Root —
`useLiveStream` gained `pushLocalNote`/`clearLocal`, exposed off
`LiveStream` via `forwardRef`+`useImperativeHandle` (preact/compat),
same shape as app.js's old `termAPI` object but scoped as a ref handle
instead of a module-level variable.

Screenshot-verified end to end: typed "/help" + Enter into a real
mounted composer, confirmed the textarea clears and the local note rows
(command list) append to the live pane.
2026-08-28 22:05:13 +02:00
iris
190d810762 agent: TodosPanel/InboxPanel flyouts + header pills
New Preact `SidePanel` drawer — deliberately not the shared
`<hive-side-panel>` shadow-DOM custom element, same rationale as
StatusChips' Badge/Dropdown. Drops that element's drag-to-resize +
localStorage width persistence for this first slice (fixed width via
CSS); reuses agent.css's existing `.agent-inbox`/`.inbox-*`/
`.side-panel-empty` content classes verbatim, only the drawer chrome
itself is new (SidePanel.css). Kept mounted regardless of open/closed
state (toggles the `open` prop) rather than conditionally rendered, so
the slide/fade CSS transitions actually fire on both open and close.

- InboxPanel: renders `state.inbox` (already polled by useAgentState)
  + a "mark all read" action — plain `fetch()` to the dashboard's
  `/api/agent/{name}/mark-all-read` (JSON response, same as app.js;
  unlike pause/resume this one doesn't need a form-submit workaround).
- TodosPanel: new `useTodos` hook (same 4s poll cadence as app.js's
  `refreshTodos`) + bulk select/mark-done via `api/todos/mark-done`.
- Both destructive-but-recoverable actions use a new `useConfirmClick`
  two-click-arm hook instead of the old shadow-DOM `themedConfirm`
  modal — lighter weight, no backdrop/dialog machinery needed for a
  "did you mean to click that" nudge.
- HeaderPill: the inbox/todos count triggers in the header's right
  cluster; Header.tsx gained a `pills` slot for them (mirrors the old
  markup's `.agent-header-pills` third column).
- types.ts: added `InboxRow`/`TodoRow`, mirroring
  `hive_sh4re::inbox::{InboxRow, LooseEnd}` (todo variant only —
  `/api/todos` never returns the others).

Screenshot-verified against a mock server exercising both panels.
2026-08-28 22:05:13 +02:00
iris
99c614cd54 agent: address argus review notes — stale comment + CI typecheck wiring
- StatusChips.tsx: drop the stale 'useAgentState lands in a later
  commit' comment now that Root.tsx already wires it.
- nix/checks.nix: add agent-typecheck, same shape as swarm-ui-typecheck,
  so a real TS error in the agent package fails CI instead of just
  esbuild-stripping silently.
2026-08-28 22:05:13 +02:00
iris
8521c6becb agent: LiveStream/Row/DetailsRow — Preact live event pane
Ports the row taxonomy in docs/terminal-rendering.md (app.js's
renderStream/renderRichToolUse/renderToolResult/renderTaskEvent) to
real Preact components + hooks:

- lib/streamRow.ts: plain-data StreamRow model (one row = one line/
  panel), lib/classifyEvent.ts: raw stream-json event -> StreamRow[],
  almost entirely dispatching on the backend's precomputed _icon/
  _summary/_category/_body/_body_type fields, same as the old client.
- lib/markdown.ts, lib/linkify.tsx: sanitized-markdown + auto-link
  helpers, same behavior as app.js's mdNode/terminal.js's linkify.
- hooks/useLiveStream.ts: backfill + SSE + seq-dedupe + coalescing,
  reduced to a plain StreamRow[] — deliberately has no opinion on
  scroll position, only on what rows exist.
- components/Row.tsx: renders one StreamRow (flat or details).
- components/LiveStream.tsx: owns the scrollable DOM node + a from-
  scratch sticky-bottom implementation — not the old MutationObserver
  + rAF snap-animation + smoothScrollingUntil gate. stickToBottom is
  plain state driven by the scroll handler; snapping is an instant
  scrollTop write in a useLayoutEffect that runs after Preact has
  already committed the new rows, so it always sees the real
  scrollHeight. A load-older prepend is told apart from a normal
  append via an explicit ref set right before calling loadMore(),
  rather than inferred from DOM mutation shape after the fact.

Wired into Root.tsx below Header/StatusChips; turn_start/turn_end
also nudge useAgentState's refresh() for a snappier badge update than
the plain poll interval.

Reuses @hive/shared/terminal.css's existing row-kind classes as-is —
the taxonomy's visual language isn't what changed, the component
model underneath it is.
2026-08-28 22:05:13 +02:00
iris
908f479372 agent: wire Header/StatusChips to real /api/state polling
- useAgentState hook: polls GET /api/state (4s interval for now — see
  its file comment for why this isn't yet the SSE-triggered + login-
  only-timer cadence the old page used; that lands with the live
  stream + term-input commit, which is when clobbering the operator's
  in-progress input actually becomes a risk).
- format.ts: fmtTokens/fmtAge, same output shapes as app.js's.
- modelEffort.ts: POST /api/model + /api/effort (same-origin, plain
  fetch).
- dashboardBase.ts + pauseAction.ts: pause/resume POST to the
  *dashboard's* origin via a real <form> submit, kept unchanged from
  app.js — a cross-origin fetch needs CORS headers hive-c0re doesn't
  send, a form submit sidesteps that same as it already did.
- Root.tsx: wires it all together, including app.js's "any non-online
  status forces the turn-state badge to offline" behavior.

Screenshot-verified against a mock GET /api/state (real fetch, not
hardcoded props) + the real agent.css/theme.css/colors.css.

Builds + tsc --noEmit clean.
2026-08-28 22:05:13 +02:00
iris
93b34ebb9b agent: scaffold Preact rewrite, Header + StatusChips first slice
Adds a new Preact/TSX build alongside the existing app.js (esbuild
entry `main.tsx` → dist/static/main.{js,css}, same jsx/tsconfig shape
swarm-ui already uses) and the first real page slice: Header +
StatusChips, composing the Badge/Dropdown components from the prior
commit's PR. Not wired into index.html yet — app.js keeps rendering
the live page untouched while this fills in component by component
(state polling, the live SSE stream, login flow, inbox/todos, term
input) in follow-up commits on this branch.

StatusChips folds the model/effort pickers and pause into the badge
row itself (each badge IS its own control), replacing the old
overflow-menu-only pickers — the concrete fix the design guide already
names this page as needing. Presentational only for now (props, not
live data) so it's reviewable against sample data before being wired
to /api/state.

Screenshot-verified against the real agent.css/theme.css/colors.css
(headless chromium, sample data) — renders correctly.

Builds + tsc --noEmit clean.
2026-08-28 22:05:13 +02:00
iris
4b71a76b12 agent: drop dead .header-pill-loose CSS
Styled variant with zero HTML/JS reference anywhere in the frontend
tree, found while surveying pill/chip/badge sites for the "extract
pill component" issue. The extraction it was originally paired with
(PR#3197) got closed as not worth the churn for 2 instances, but this
one finding stands on its own — no reason to carry dead CSS forward.

npm run build clean.
2026-08-12 19:33:07 +02:00
iris
8dd6d05d6c frontend: unify pill/chip/badge shape into shared CSS
Per mara's steer on #3053 ("chip/pill/badge is the same if you squint
... theme unification is part of the goal" then "make it common css
instead of component, thats fine. but make them look unified (not as
much per usage css)"): shared CSS, not a JS component.

New @hive/shared/pill.css defines two classes, `.hive-pill` (primary
state chips) and `.hive-pill-sm` (secondary meta chips) — border/
border-radius/padding/font-size/letter-spacing (colour stays per-site,
the meaningful semantic part). Every render call site across dashboard
(swarm.js/core.js/builds.js) and agent (index.html/app.js) now carries
one of the two shared classes directly, alongside its own existing
semantic-colour modifier class.

Second cut of this PR, per argus's approve + mara's follow-up review
comment on the first cut: the first version instead enumerated every
legacy classname (`.badge`, `.status-badge`, `.header-pill`, etc.)
straight into pill.css's own selector groups so no call sites needed
touching. Mara's correction: that just relocates the duplication
rather than removing it, and the shared CSS shouldn't have to keep
naming every consumer. This version does the real rename instead.

Most visible consequence, unchanged from the first cut: dashboard's
`.badge` family moves off its own shape (2px square corners, uppercase,
tighter padding) onto the shared rounded-pill shape + agent's "sm" tier
sizing. `npm run build` clean across all three packages; verified the
compiled bundles carry the new classnames at every call site (dashboard
JS, agent index.html + app.js), not just the source tree.

Fixes #3053
2026-08-12 19:25:45 +02:00
iris
92b9e67327 frontend: unify pill/chip/badge shape into shared CSS
Per mara's steer on #3053 ("chip/pill/badge is the same if you squint
... theme unification is part of the goal ... make it common css
instead of component, thats fine. but make them look unified"):
shared CSS, not a JS component.

New @hive/shared/pill.css defines the shape (border/border-radius/
padding/font-size/letter-spacing — color stays per-site, that's the
meaningful semantic part) in two tiers matching what was already
organically in use: `.hive-pill` (primary state chips) and
`.hive-pill-sm` (secondary meta chips). Every pre-existing classname
that drew its own copy of this shape (dashboard's `.badge` family,
agent's `.status-badge`/`.state-badge`/`.header-pill`/`.ctx-badge`/
`.model-chip`/`.effort-chip`) is folded straight into the same
selector groups, so no markup or JS changes were needed anywhere —
every render call site keeps constructing the exact same classnames
it always did.

Most visible consequence: dashboard's `.badge` family moves from its
own shape (2px square corners, uppercase, tighter padding) onto the
shared rounded-pill shape + agent's "sm" tier sizing, matching the
"look unified" ask directly. `npm run build` clean across all three
packages.
2026-08-12 19:25:45 +02:00
iris
5bd085fbac web-ui: expose per-agent paused status, add pause/resume to the agent page's own overflow menu
hive-agent's own web_ui module never exposed the agent's own paused
status to its own /api/state — the dashboard's cross-container view
knew it, but a per-agent page had no way to know it's paused. Added
StateSnapshot.paused (a direct stat of the same harness-local pause
marker hive-c0re's Coordinator::is_paused checks).

The per-agent page's ⋯ overflow menu now has a pause/resume item that
POSTs to hive-c0re's existing /api/pause/<name> / /api/resume/<name> —
the same endpoints the dashboard's <hive-agent-menu> already uses,
same cross-origin form-submit pattern the existing rebuild-container
item uses. The item's label tracks state.paused on every /api/state
refresh so a pause/resume triggered from the dashboard while this page
is open doesn't leave a stale action showing.
2026-08-11 20:42:53 +02:00
iris
ecf9ff4d80 web-ui: remove dead ask->operator inline-answer binding from the per-agent terminal
hyperhive#2922: the inline-answer slot (pendingAskBinds/reconcileAskBinds/
buildAnswerForm) depended on a since-removed /api/loose-ends endpoint
and had been silently non-functional the whole time — nothing ever
reassigned lastLooseEnds, so the reconciler always no-op'd.

Per mara's call on the issue (ask/answer is deprecated per #2850, only
the main dashboard UI needs to work, remove the broken per-agent inline
view): ripped out the dead JS (binding + form + CSS) rather than
restoring the endpoint. An ask tool call now renders like any other
tool call, no inline answer affordance; the operator answers via the
dashboard's own question surfacing.

Also fixed adjacent doc staleness this same removal made newly
contradictory (docs/web-ui/agent.md, docs/terminal-rendering.md):
the doc already described the loose-ends/bash-tasks flyouts and the
ask-binding as recently dead, but the endpoint reference table and a
header-pills bullet list still described them as live.
2026-08-11 20:21:52 +02:00
iris
e525dcb6d4 agent icon: 404 when unconfigured, client-side fallback
hive_sh4re::assets::branding_svg() resolved a server-side default
icon at runtime from HIVE_ASSETS_DIR — the only consumer was
serve_icon(), which fell back to it whenever the agent had no
`hyperhive.icon` override. Removed both the fallback and the
function: serve_icon() now 404s when /etc/hyperhive/icon.svg is
absent, and the per-agent web UI (app.js) picks up the existing
dashboard swarm.js pattern — swap the <img> src to the
frontend-bundled /favicon.svg on load failure, guarded against
looping if the fallback itself 404s.

Updated the doc/comment claims that said the server always returns
an image (docs/web-ui/agent.md, nix/agent-modules/default.nix, the
hive-c0re/forge/users.rs comment referencing the old shared-asset
set). forge-avatar-sync and the matrix avatar sync are unaffected —
both are gated on hyperhive.icon != null and never depended on the
removed fallback.
2026-08-10 20:59:59 +02:00
damocles
642377c5e0 docs: stop implying matrix/bash/forge is a closed todo-producer list 2026-08-04 00:06:19 +02:00
iris
3512e4b019 dashboard: hide forge links instead of guessing <hostname>:3000
Adds services.hyperhive.forge.publicUrl (defaults to the gateway vhost
URL when behindGateway=true, null otherwise). HIVE_FORGE_PUBLIC_URL is
now sourced from it instead of hardcoding https://${forge.domain}
whenever behindGateway is on.

The 4 frontend call sites that built a forge link from
state.forge_public_url now hide the link when that's absent, rather
than guessing http://<browser-hostname>:3000 — a guess that's only
correct by accident once the operator isn't on plain localhost. Fixes
the dashboard H0M3 tile, per-agent-row forge links + agent menu, the
approval-queue PR link, and the per-agent page's own meta-nav forge
link (found during this pass, same defect, not in the original
3-site inventory).

Docs + doc-comments updated to match.
2026-08-03 01:21:11 +02:00
iris
da3fc9bf95 move terminal-verbosity toggle from agent overflow menu to /settings.html
Per mara: 'i wanted you to put this in .../settings.html' — the toggle
belongs with the other operator-local browser preferences, not buried
in each agent's own overflow menu.

Extracted the get/set + localStorage key into @hive/shared/prefs.js so
settings.html (writer) and every per-agent app.js (reader, via
HiveTerminal.create's expandDetails option) agree on the exact same key
without two independently-typed copies that could drift. Removed the
now-unused overflow-menu toggle + its agent.css rules from the agent
page. Docs moved from docs/web-ui/agent.md's overflow-button section to
docs/web-ui/dashboard.md's S3TT1NGS section, next to the existing
browser-notifications preference.
2026-08-02 18:34:40 +02:00
iris
3b299375e3 fix: move settings section after effort picker, matching docs
argus caught the settings block appending before the effort picker's
conditional despite the PR description and docs both saying it lands
after — DOM order is visual order here (no CSS order: override), so
actual layout was model -> settings -> effort. Moved the block after
the effort picker's if-block; layout now matches what both already
claimed.
2026-08-02 18:29:28 +02:00
iris
198db326b3 agent web UI: add terminal verbosity setting (expand tool output by default)
Adds a browser-local (localStorage only, no backend field) toggle in
the per-agent overflow menu's new settings section: whether otherwise-
collapsed <details> rows in the live terminal (long tool-results,
Write/Edit diffs, ...) default open. Message-bearing rows that already
default open (send/ask/answer/recv) are unaffected either way.

The shared terminal factory (frontend/packages/shared/src/terminal/terminal.js)
gains an optional expandDetails option (boolean or zero-arg function),
read live on every details()/detailsDiff() call rather than captured
once, so flipping the toggle mid-session applies to the next rendered
row without a reload. Unused by the dashboard's own terminal pane, so
its default-closed behaviour is unchanged.

Closes #2961.
2026-08-02 18:25:45 +02:00
iris
94fb42f2b6 docs/agent.md: fix stale 3-flyout description; drop dead buildLooseEndsList
docs/web-ui/agent.md still described a "loose-ends" and "tasks" flyout
that were superseded by the todos flyout when loose-ends-v2 landed —
GET /api/loose-ends and GET /api/bash-tasks are both gone server-side.
Replaced with an accurate description of the todos flyout (including
the mark-done bulk action from #2919), and noted that the ask->operator
inline-answer binding this doc also describes is currently
non-functional (its data source was the same removed endpoint) —
tracked separately as #2922, not fixed here.

buildLooseEndsList in app.js rendered the old loose-ends flyout and had
zero call sites left; removed it. buildAnswerForm stays — reconcileAskBinds
still calls it, even though that path is currently dead per #2922.

Fixes #2920
2026-08-02 02:40:01 +02:00
iris
41c1b1a3fb agent web UI: bulk mark-done for the todos flyout
The per-agent web UI todos flyout (loose-ends v2) had no mark-done
affordance at all — dismissing a todo was only possible via the
cancel_loose_end MCP tool, one id at a time. Add a checkbox per row,
a select-all/select-none/mark-done bulk row, and a new
POST /api/todos/mark-done handler that loops the existing single-id
MarkTodoDone request over the in-agent socket (no new wire request
type needed — the todos list is small, so N same-host round-trips is
cheap).

Fixes #2917
2026-08-01 20:49:07 +02:00
iris
399a837e17 frontend: drop the Panel forwarding object and the side-panel-body compat class
Per mara's review: '2 and maybe 1, but 3 also sounds reasonable on first
glance' (against 3 options I posted). Doing 2 and 1, leaving open()/
openNamed() as-is (option 3, tentative only).

Both dashboard/common.js and agent/app.js now export/use the
<hive-side-panel> element instance directly (sidePanel) instead of a
thin Panel = { open, openNamed, refresh, close } object that existed
purely to keep the old call-site shape unchanged. All 6 real call sites
updated to call the element's own methods directly.

The .side-panel-body class each wrapper stamped onto its own instance,
purely so common.css/agent.css's pre-existing content-styling selectors
kept matching, is gone too -- those selectors now use the element's own
tag name as the root (hive-side-panel .md, hive-side-panel .agent-inbox),
which already uniquely identifies the light-DOM instance without a
compatibility class. Verified via headless Chromium/CDP that the
tag-name selectors resolve correctly with no class needed.

Drive-by: removed an unrelated dead Panel import in call.js.
2026-08-01 00:58:53 +02:00
iris
996899fcad frontend: create the side-panel instance eagerly, not lazily behind a per-call guard
Per mara's review on the side-panel PR: create the shared <hive-side-panel>
instance once at module-evaluation time instead of lazily on first call
via an ensurePanel() guard every wrapper method had to remember to call.
ES modules execute after the document is parsed (same timing as a defer
script), so document.body is already available when this code runs --
lazy init bought nothing here and left a footgun for any future method
added to either wrapper.
2026-08-01 00:58:53 +02:00
iris
c5610b075a frontend: unify dashboard + agent side panel into shared hive-side-panel
The dashboard's Panel singleton and the per-agent UI's own inline Panel
IIFE each had their own near-identical implementation of the right-side
slide-in drawer used for file previews, diffs, logs, and inbox/todo
lists. Both are now thin wrappers around a new <hive-side-panel>
shadow-DOM custom element in @hive/shared, following the same house
pattern as <hive-menu>: the element owns and builds all its structural
chrome itself (backdrop, drawer, resize handle, header, title, close
button) in connectedCallback, and only the caller's opaque content node
is projected in via a default <slot> so each package's own
content-type-specific CSS keeps reaching it.

Public API is the union of both originals: open(title, content),
openNamed(name, title, content), refresh(name, title, content),
close(), and currentOwner(). Drag-to-resize + localStorage width
persistence (ported verbatim from the dashboard's original
implementation, the only one of the two that had it) is now available
to both consumers by default — a deliberate behavior widening for the
agent UI, which didn't have resize before. Along the way, fixed a
latent bug in the ported CSS: the resize handle was setting a
--side-panel-w custom property that no width rule ever consumed, so
dragging never actually resized the drawer even though it looked wired
up; the new shared stylesheet's width rule reads it properly.

Each package's own global stylesheet keeps its content-specific rules
(common.css's .side-panel-body .md, agent.css's .side-panel-body
.agent-inbox) exactly where they were — those can never be reached from
the shared element's shadow tree, same architectural floor as
<hive-menu>'s item-row styling. Each wrapper applies a plain
'side-panel-body' compatibility class to its own <hive-side-panel>
instance so those existing selectors keep matching by ordinary
light-DOM descendant matching, with the shared element itself having no
knowledge of what that class name means.

Panel.bind() is gone from both packages' public API — the shared
element wires its own listeners in connectedCallback, so there's no
bind step left to call. tabs.js's one call site (the only bind() caller
in either package) was updated to drop it.

The two original chrome CSS blocks disagreed on several purely visual
details beyond the resize-handle rules (z-index, backdrop color, drawer
border/box-shadow, title typography) — the dashboard's values (the more
feature-complete of the two) were kept as canonical, which is a small
visible style change for the agent UI's panel chrome (thinner border,
no box-shadow, no bold purple title). Flagged for visibility since nothing
in the original two implementations called this out explicitly.

Verified with a real headless-Chromium/CDP harness (bundled the actual
component + built page CSS, served statically, drove via raw CDP) for
both usage shapes: open/close, backdrop-click dismiss, Escape dismiss,
refresh() owner-matching (no-op on wrong owner, applies on matching
owner), and drag-to-resize (drawer width updates live during drag and
persists to localStorage on release).
2026-08-01 00:58:53 +02:00
iris
bce666a9b2 frontend: move shadow-DOM component CSS to real .css files, not JS strings
mara: 'i dont like sharing css via js, thats not how it should be done.'

Replaced the DIALOG_CSS/TOAST_CSS template-string constants in modal.js
and the inline CSS text in component-styles.js with three real .css
files (hive-dialog.css, hive-toast.css, component-common.css),
imported as raw text via esbuild's 'text' loader and turned into
CSSStyleSheet objects at runtime (same replaceSync() call as before --
only where the CSS text comes from changed). Both packages' build.mjs
gained a '.css': 'text' loader entry on their JS-bundling step; this
doesn't collide with the separate page-stylesheet bundling ('css'
loader), which is a different esbuild invocation over different entry
points.

No behavior change -- same adoptedStyleSheets wiring, same rules,
same output. Verified with a full frontend build (grepped the bundled
JS to confirm the CSS text inlines correctly); nix fmt clean.
2026-07-29 12:55:54 +02:00
iris
d79df3883d frontend: shadow-DOM upgrade for hive-dialog/hive-toast, drop global modal.css
Follow-up to the light-DOM custom-elements pilot (mara: 'that one landed
and works. i dont really like the css still being shared - id like that
to be split by component, with common stuff via @include').

<hive-dialog> and <hive-toast> now attach a shadow root and adopt a
component-scoped CSSStyleSheet built from a template-string constant in
modal.js (DIALOG_CSS / TOAST_CSS), plus a new shared/src/component-
styles.js sheet (currently just .btn) adopted alongside it via
adoptedStyleSheets -- the native equivalent of a Sass @include, no
preprocessor added. Theme vars keep resolving through the shadow
boundary since CSS custom properties inherit across it; only plain
class rules needed the explicit move.

Deleted the global shared/src/modal.css entirely and dropped its
@import from both dashboard/common.css and agent/agent.css -- nothing
outside modal.js renders the old .tc-* classes any more. The <hive-
dialog> element is now the backdrop itself (:host carries the fixed-
position/centering rules that used to be .tc-backdrop on a light-DOM
div); box/title/message/content/actions all render inside its shadow
root. <hive-toast> similarly styles :host directly instead of a light-
DOM div, with the message text placed straight into the shadow root
(no <slot> needed since there's no external light-DOM content to
project).

Public API (openDialog/themedConfirm/themedPrompt/themedToast)
unchanged -- no call-site changes needed anywhere in dashboard/agent.

Verified with a full frontend build; nix fmt clean.
2026-07-29 12:55:54 +02:00
iris
7110a25cf6 frontend: extract themed dialogs + async-form handler to shared, wire agent UI
The dashboard has a themed modal/dialog system (modal.js: themedToast/
themedConfirm/themedPrompt) and a data-async form submit interceptor
(bindAsyncForms) that every dashboard action routes through. The
per-agent UI never adopted either — it had its own more primitive
data-async handler using native window.confirm()/alert() (8 call
sites) and a duplicated el() DOM helper.

- Moved el() out of dashboard/common.js into shared/src/dom.js.
- Moved modal.js + modal.css from dashboard/src/ to shared/src/,
  updating its internal el import.
- Moved bindAsyncForms from dashboard/common.js into shared/forms.js,
  alongside the asyncBtn primitive it's built on.
- Updated every dashboard file's imports to the new shared locations
  (no re-export shims).
- agent.css now @imports shared/modal.css so the dialogs render
  themed there too.
- agent/app.js: dropped its local el()/data-async duplicate, wired
  bindAsyncForms(), and replaced all 8 window.confirm() sites with
  themedConfirm (async, wrapped in a fire-and-forget IIFE where the
  call site needs a synchronous boolean return, e.g. the slash-command
  dispatcher).

Closes hyperhive#2791. Verified with a full frontend build
(npm run build) — both dashboard and agent bundles compile clean and
agent.css picks up the .tc-* dialog styles it previously lacked.
2026-07-27 18:55:17 +02:00
damocles
c4fcf7fbf1 feat(#2659): serve hive-bash-mcp over persistent streamable-http, drop stdio bridge 2026-07-23 18:01:20 +02:00
iris
88f17320b6 fix(#2632): remove /api/loose-ends endpoint; drop refreshLooseEnds (address argus/mara review)
Both old endpoints removed. refreshLooseEnds() call sites cleaned up;
lastLooseEnds stays as empty [] for reconcileAskBinds (no-op now that
the loose-ends source is gone).
2026-07-22 18:02:01 +02:00
iris
4905b688be feat(#2632): switch agent page loose-ends pill to todos; remove bash-tasks pill
- Add GET /api/todos backend endpoint (connects to HIVE_AGENT_SOCKET,
  calls ListTodos, returns { todos: [...] } — LooseEnd::Todo items).
- Register route in web_ui/mod.rs.
- index.html: replace loose-ends pill (🪢) + bash-tasks pill (⚙)
  with a single todos pill (📋, id=todos-pill).
- agent.css: add .header-pill-todos count colour (green, same as old tasks).
- app.js:
  - refreshTodos() fetches /api/todos, drives renderTodos/buildTodosList.
  - refreshLooseEnds() becomes background-only (no pill); still drives
    reconcileAskBinds for inline ask-form wiring.
  - Remove refreshBashTasks / buildBashTasksList / renderBashTasks.
  - Cold-load and turn_end both call refreshTodos; 4s interval replaces
    the old bash-tasks interval.
2026-07-22 18:02:01 +02:00
iris
4b45c5cd3d feat(#2443): asyncBtn — shared reusable component, replace ad-hoc disable/spinner patterns
add `asyncBtn(btn, fn)` to `@hive/shared/forms.js` as the single
reusable component for async button actions:
  1. double-click guard: returns immediately if btn is already disabled
  2. saves btn.innerHTML, replaces with spinner while in-flight
  3. restores btn on resolve or reject via finally

wire it into all ad-hoc disable/spinner/restore patterns:
  - common.js: bindAsyncForms uses asyncBtn internally
  - core.js: 'clear perms' button
  - permissions.js: clearStaleAgent
  - schedules.js: saveSchedule submit, editSchedule submit
  - app.js: buildAnswerForm, buildInboxMarkAllRow

fireScheduleNow in schedules.js is left with its existing childNode
save/restore because it shows a custom result flash on the button
content after a successful fire-now (the auto-restore of asyncBtn
would overwrite it); the surrounding themedConfirm dialog already
acts as a natural double-click barrier before the fetch.

saveAll in permissions.js is also left as-is: it uses a custom
'queued ✓' success label + a 900ms delay before re-fetch; the
btn.dataset.busy flag is its own double-submit guard.
2026-07-20 19:51:03 +02:00
iris
4c51a00c9f fix(#2612): parse ISO due_at string before arithmetic in loose-ends panel
The Reminder loose-end variant's due_at field is serialized as an ISO 8601
string (DateTime<Utc> on the wire), but the JS was doing:

  const dueIn = (t.due_at || 0) - now;

A string minus a number is NaN in JS, so fmtAge(NaN) returned 'NaNd',
producing the 'due NaNd overdue' label seen in the screenshot.

Fix: parse the ISO string to unix seconds with new Date(...).getTime() / 1000
before the subtraction.
2026-07-20 19:27:50 +02:00
iris
35611e9f0d feat(#2608): show session count in agent stats window
Adds a 'sessions' chip to the per-agent stats summary panel showing how
many fresh claude sessions started within the selected time window.

Backend (hive-agent/src/stats.rs):
- New optional field `session_count: Option<u64>` on `Snapshot`
  (skip_serializing_if = None — inert-until-data, same pattern as
  first_turn_ctx). Counts rows in the `sessions` table whose
  started_at falls within the window; returns None when the table
  doesn't exist on an older db.
- New `read_session_count(conn, from)` helper (rusqlite::Result so the
  caller maps Err to None).
- Extracted per-row accumulation loop into `TurnAccum` struct +
  `push()` method to keep `snapshot()` under the too_many_lines limit.

Frontend (frontend/packages/agent/src/stats.js):
- New 'sessions' chip added to renderSummary, guarded by
  `typeof s.session_count === 'number'`, placed before the
  existing first-turn-ctx chip.
2026-07-20 19:14:32 +02:00
iris
b25c44f7cb feat(#2196): move send/ask/answer body to backend _body field
The three message-bearing hyperhive tools (send, ask, answer) previously
had named JS branches in renderRichToolUse that each:
- computed a summary string (recipient / line count)
- rendered the body text via detailsOpenMd (marked + DOMPurify)

This commit moves the body text and summary string to the backend,
reducing the JS dispatch table to a single generic markdown path.

Backend (stream_enrich.rs):
- rich_tool_body: new 'markdown' body_type for send/ask/answer —
  stamps _body with input.body / input.question / input.answer
- fmt_hyperhive_message_tool: new helper formats _summary as
  'send* → to' / 'ask* → to' / 'answer* #id' with ' · NL' when
  the body spans multiple lines; extracted out of fmt_hyperhive_tool
  to keep it under the too_many_lines limit
- doc: updated rich_tool_body docstring to list the new 'markdown' type

Frontend (app.js):
- Remove the three named branches (send/ask/answer) from renderRichToolUse
- Extend the generic _body path: 'markdown' type calls detailsOpenMd
- The ask-form slot logic (operator inline-answer binding) is preserved
  within the markdown branch, now reading the question from c._body
  instead of input.question — DOM mounting remains client-side
2026-07-20 19:13:43 +02:00
iris
0e4b69dd20 feat(#2196): move Write/Edit diff body to backend, drop JS branch
- fmt_builtin_tool: Edit gets its own arm computing `-N +M` line counts
  in _summary (was shared with Read/Write as bare file-path).
- is_rich_tool: drop Write (content is huge/one-sided; flat _summary row
  is correct); Edit stays rich since it has an old/new diff.
- rich_tool_body: now returns Option<(String, &'static str)> where the
  second field is the body type ('diff' or 'plain'). Edit arm builds the
  '-'/'+ ' prefixed diff body; mcp__bash__run gets type 'plain'.
- enrich_tool_use_entry: stamps both _body and _body_type.

Frontend (app.js):
- Remove the Write/Edit branch from renderRichToolUse (~25 lines).
- Generic _body path now dispatches on _body_type: 'diff' ->
  api.detailsDiff (colour-coded spans), default -> api.details.
  No tool-specific JS remains for file diff rendering.
2026-07-19 18:53:51 +02:00
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
iris
9edd37501a chore(frontend): update npm dependencies
- dompurify 3.2.4 -> 3.4.12 (fixes a moderate-severity XSS advisory
  chain flagged by npm audit; dompurify sanitizes untrusted markdown
  output in both the dashboard and agent UIs)
- marked 18.0.4 -> 18.0.6
- esbuild 0.28.0 -> 0.28.1

npm audit now reports 0 vulnerabilities. Verified with npm run build
(dashboard + agent) and nix fmt --fail-on-change (no changes).
2026-07-15 23:30:25 +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