Commit graph hyperhive/frontend/packages/agent/src/lib
Author SHA1 Message Date
iris
9720bdfad0 settings: move the theme/motion panel into @hive/shared, add motion to agent
Review feedback on this PR (mara): "i think the component should be
shared. motion setting is missing." Both addressed:

- `settings-storage.ts` (generic localStorage hook), `theme-apply.ts`,
  `motion-apply.ts`, and `SettingsMenu.tsx`/`.css` all move from
  swarm-ui's `lib/`/`shell/` into `@hive/shared/src/settings/` —
  agent's previous local copies are deleted outright rather than kept
  as a second implementation. One component, `Badge` trigger
  everywhere (already used elsewhere in swarm-ui, so not a new visual
  language there either) — storage keys stay caller-owned (`themeKey`/
  `motionKey` props + matching `useApplyThemeOverride`/
  `useApplyMotionOverride` calls at each package's single mount point)
  so agent and swarm-ui keep fully independent, non-colliding
  persisted settings.
- Agent's settings menu now includes the motion row, matching
  swarm-ui's. No animation in the agent package is gated behind
  `data-motion` yet — same as when swarm-ui first built this plumbing
  ahead of having a consumer — so it's currently inert there, ready for
  whenever agent grows a motion-guarded animation.
- swarm-ui's own theme default flips to `'dark'` as part of this move
  (`theme-apply.ts`'s new default), superseding PR #3715 — that PR
  becomes redundant once this lands and will be closed rather than
  merged, to avoid the two colliding on the same file.

Verified end-to-end with real screenshots on both pages: shared
component renders identically (Badge trigger, theme+motion rows, dark
default) on agent's mock server and a static rebuild of swarm-ui's
dist.
2026-08-29 11:11:39 +02:00
iris
b28e8f1c4e agent: add a settings menu with a theme override, defaulted to dark
Ports swarm-ui's `SettingsMenu` (mara: "agent terminal page should get
the settings panel from swarm ui as well") — same shape as `MetaNav`
already in this header: a `Badge` icon trigger ("⚙"), popover, close on
outside-click/Escape.

`theme-apply.ts` + `settings-storage.ts` are near-verbatim ports of
swarm-ui's own (duplicated rather than moved into `@hive/shared` for
this pass — lower risk than reworking swarm-ui's imports in the same
change). Defaults the stored override to 'dark', not 'system', for the
same reason as swarm-ui's own default flip: `prefers-color-scheme` has
no real "unset" value, so 'system' silently reads as light for anyone
who's never touched an OS dark-mode toggle.

Motion NOT ported — agent has no animation gated behind `data-motion`
yet, so that plumbing would have nothing to control.

Verified end-to-end: default dark on a fresh load, and an explicit
localStorage override to 'light' correctly re-themes the whole page via
the existing `colors.css` `:root[data-theme='light']` block (already
shipped, previously only reachable from swarm-ui).
2026-08-29 11:00:11 +02:00
iris
668ccc2278 agent: remove rebuild button, move dashboard link into the links menu
mara (#3704): 'remove rebuild button, move link to dashboards into
links menu.'

The overflow (⋯) menu existed for exactly two items: the dashboard
back-link and a rebuild-container action. Rebuild is gone outright —
the dashboard's own R3BU1LD button already covers it, this was just a
rarely-used shortcut not worth its own menu. The dashboard link moves
into MetaNav's links popover (now the first item, above stats/forge/
config/extras) instead. With both gone, OverflowMenu had nothing left
to justify existing as a separate component — deleted along with its
CSS and the now-unused rebuildAction.ts (only consumer).

MetaNav gained a dashboardBase prop (Root.tsx already computes this
via resolveDashboardBase for InboxPanel/pause — reused, not
duplicated) and renders the dashboard link as a real <a>, same
treatment as every other item in that popover — no dangling
window.open()-only affordance.

Updated docs/web-ui/agent.md's Header section and the couple of
now-stale OverflowMenu references in index.html's/MetaNav.css's own
comments.

Verified: header now shows a single trailing icon-badge (was two),
popover opens with dashboard first then the agent_links() set.
tsc --noEmit clean, build clean, both pre-push lints clean.
2026-08-28 23:40:56 +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
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