Commit graph hyperhive/frontend/packages/agent/src/hooks
Author SHA1 Message Date
iris
d49a9df479 useAgentState: re-arm the poll loop from refresh() instead of killing it
argus's review on the original fix: refresh() did a parallel one-off
fetch instead of calling the mount effect's self-rescheduling poll(),
so it never re-armed timerRef after firing. Wiring refresh() to run on
every visibilitychange-to-visible meant the very first tab-switch back
into focus would kill periodic polling for the rest of the session --
reproducing the exact staleness bug this branch set out to fix, just
delayed by one tab switch instead of immediate.

Fix: hoist poll into a ref set by the mount effect so refresh() invokes
the same self-rescheduling function rather than a parallel fetch that
drops the loop. Also gave the visibilitychange effect an empty
dependency array per the review's second note -- it only closes over
stable refs/setters, and now that Root's 1s ticker re-renders the
calling component every second, a deps-less effect would tear down and
reattach the listener that often for no reason.
2026-08-31 19:02:57 +02:00
iris
9c72a4ae3e agent web UI: keep the turn-state badge live instead of freezing until reload
The status badge's elapsed-time text ('thinking Xm Ys') was computed
from Date.now() inside the render function, so it only ever advanced
when the component actually re-rendered. Two gaps let it go stale:

- useAgentState's poll loop is a chained setTimeout, which browsers
  throttle (or suspend outright) once the tab is backgrounded, so
  polling could stall for a long time with no way back to a live
  reading short of a full page reload.
- even under healthy polling, the age text only advanced once every
  ~4s (the poll interval) instead of counting up smoothly.

Fix: resync immediately on visibilitychange (so returning to a
backgrounded tab doesn't leave a stale reading), and drive the age
text off its own 1s interval independent of the poll cadence.
2026-08-31 19:02:57 +02:00
iris
5930efc29b Trim negative-space comments per mara's review
Don't state what a function/module doesn't do and where that
happens instead — just describe what it does. Cut the "not
something this function decides" / "not affected by this" /
"not a placeholder for a later commit" asides from the doc
comments touched in the last two commits.
2026-08-30 21:31:33 +02:00
iris
907567ef76 Fix stale default-open doc comments per argus's review
ClassifyCtx's tool_use-id correlation gates markdown-vs-plain body
format for a recv result, not open/collapsed state — that's always
the operator's uniform preference now. 5 backend comments still
described it as controlling "default-open" rendering, contradicting
the actual render path and this PR's own rewritten docs.

Also fixed useAgentState.ts's stale comment promising an
SSE-triggered refresh model "in a later commit" — that's permanently
off the table now that the terminal stream's kind tag is gone by
design (argus flagged this as a drive-by, not blocking, but it's a
one-line cause-and-effect of this same PR so fixing it here).
2026-08-30 21:28:34 +02:00
iris
cebf3c6ced Drop classifyEvent.ts, render TermMsg directly
Per review: StreamRow was meant to match what the server sends in
TermMsg, not be a separate model needing a translation step.

- classifyEvent.ts and streamRow.ts deleted; termMsg.ts holds the wire
  types (TermMsg/TermEnvelope) plus TermRow, a TermMsg with just the
  key/fromHistory bookkeeping Preact needs for list rendering.
- Row.tsx renders a TermRow directly: level -> CSS class, empty
  summary + markdown body -> flat row, everything else with a body ->
  expandable details gated by the operator's preference. No separate
  classification step.
- useLiveStream.ts drops ClassifyCtx (a single incrementing key
  counter didn't need a whole context object) and maps envelopes to
  rows inline.
- docs/terminal-rendering.md trimmed substantially — was documenting
  more implementation detail than useful; points at stream_enrich.rs
  for the per-tool specifics instead of duplicating them in prose.
2026-08-30 21:23:28 +02:00
iris
5eefaa951d Simplify terminal message shape to a uniform TermMsg
Move terminal-row classification server-side into a new
hive-agent/src/term_msg.rs, replacing the old JSON-mutation
enrich()/stamped-field approach in stream_enrich.rs with one
uniform wire shape: {icon?, level: debug|info|warn|error, summary,
body?, body_format?: markdown|diff, coalesce_key?}. No more per-row
`kind` tag or raw claude-JSON passthrough — every row is the same
shape, with structural identity carried by icon + summary text
instead of a CSS class per row kind.

hive-agent/src/web_ui/stream.rs's history + SSE endpoints now both
call term_msg::classify() and serve TermEnvelope{ts, seq?, msgs}
frames; events that classify to zero rows (agent-state changes,
drop-noise) never reach the wire.

Frontend: classifyEvent.ts collapses from a large per-tool dispatch
tree to a thin TermMsg -> StreamRow adapter. streamRow.ts/Row.tsx
drop the now-dead meta/childText fields. terminal.css switches from
a dozen-odd per-row-kind classes to four level-based color rules.
Expand/collapse of a bodied row is now a uniform client-side
decision (the operator's preference), no server-side per-tool
override.

docs/terminal-rendering.md rewritten to match.
2026-08-30 21:11:25 +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
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