From 93b34ebb9b3e5c79bb91980975c9b4464881a256 Mon Sep 17 00:00:00 2001 From: iris Date: Fri, 28 Aug 2026 01:54:22 +0200 Subject: [PATCH] agent: scaffold Preact rewrite, Header + StatusChips first slice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- frontend/package-lock.json | 6 +- frontend/packages/agent/build.mjs | 19 +++ frontend/packages/agent/package.json | 9 +- frontend/packages/agent/src/Root.tsx | 37 +++++ .../packages/agent/src/components/Header.tsx | 32 ++++ .../agent/src/components/StatusChips.css | 16 ++ .../agent/src/components/StatusChips.tsx | 146 ++++++++++++++++++ frontend/packages/agent/src/css.d.ts | 4 + frontend/packages/agent/src/main.tsx | 16 ++ frontend/packages/agent/src/types.ts | 32 ++++ frontend/packages/agent/tsconfig.json | 20 +++ 11 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 frontend/packages/agent/src/Root.tsx create mode 100644 frontend/packages/agent/src/components/Header.tsx create mode 100644 frontend/packages/agent/src/components/StatusChips.css create mode 100644 frontend/packages/agent/src/components/StatusChips.tsx create mode 100644 frontend/packages/agent/src/css.d.ts create mode 100644 frontend/packages/agent/src/main.tsx create mode 100644 frontend/packages/agent/src/types.ts create mode 100644 frontend/packages/agent/tsconfig.json diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ecfc32e3..8c25b577 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -991,7 +991,11 @@ "@hive/shared": "*", "chart.js": "4.5.1", "dompurify": "^3.2.4", - "marked": "18.0.6" + "marked": "18.0.6", + "preact": "10.29.8" + }, + "devDependencies": { + "typescript": "7.0.2" } }, "packages/dashboard": { diff --git a/frontend/packages/agent/build.mjs b/frontend/packages/agent/build.mjs index be0c3e3a..6e0790a8 100644 --- a/frontend/packages/agent/build.mjs +++ b/frontend/packages/agent/build.mjs @@ -47,6 +47,25 @@ await build({ loader: { '.css': 'text' }, }); +// The Preact rewrite (in progress — see main.tsx's file comment). A +// third, separate bundle rather than folding into the `app.js` build +// above: different loader needs (`.css: 'css'`, real stylesheets a +// component imports, vs. `app.js`'s raw-text shadow-DOM CSS) and JSX +// transpilation, same split swarm-ui's own build already makes. +await build({ + entryPoints: [src('main.tsx')], + outfile: staticDir('main.js'), + bundle: true, + format: 'esm', + platform: 'browser', + target: ['es2022'], + sourcemap: true, + logLevel: 'info', + jsx: 'automatic', + jsxImportSource: 'preact', + loader: { '.css': 'css' }, +}); + // Bundle the CSS. `colors.css` re-exports the standalone base16 palette // (the theme swap contract — its own output file so a swap replaces only // it, no bundle rebuild); `theme.css` is the semantic derivation layer; diff --git a/frontend/packages/agent/package.json b/frontend/packages/agent/package.json index 82da97f3..2a18ce33 100644 --- a/frontend/packages/agent/package.json +++ b/frontend/packages/agent/package.json @@ -5,12 +5,17 @@ "description": "hive-ag3nt per-container web UI. Bundled by esbuild into a static dist; served by the in-container Rust binary at runtime via tower_http::ServeDir. Per-agent additions are layered on top via the hyperhive.frontend.extraFiles nix option.", "type": "module", "scripts": { - "build": "node ./build.mjs" + "build": "node ./build.mjs", + "typecheck": "tsc --noEmit" }, "dependencies": { "@hive/shared": "*", "chart.js": "4.5.1", "dompurify": "^3.2.4", - "marked": "18.0.6" + "marked": "18.0.6", + "preact": "10.29.8" + }, + "devDependencies": { + "typescript": "7.0.2" } } diff --git a/frontend/packages/agent/src/Root.tsx b/frontend/packages/agent/src/Root.tsx new file mode 100644 index 00000000..2e3d8655 --- /dev/null +++ b/frontend/packages/agent/src/Root.tsx @@ -0,0 +1,37 @@ +// — root of the Preact rewrite. Currently just `Header` + +// `StatusChips` wired to local component state as a first slice (real +// `/api/state` polling + the live SSE stream land in follow-up commits +// on this same PR, per mara's "one pr != one commit" note on the +// issue). Sample values below match the shapes in `types.ts`. +import { useState } from 'preact/hooks'; +import { Header } from './components/Header.js'; +import { StatusChips } from './components/StatusChips.js'; + +export function Root() { + const [model, setModel] = useState('sonnet'); + const [effort, setEffort] = useState('high'); + const [paused, setPaused] = useState(false); + + return ( +
+ setPaused((p) => !p)} + lastTurnLabel="last turn 8.4s" + /> +
+ ); +} diff --git a/frontend/packages/agent/src/components/Header.tsx b/frontend/packages/agent/src/components/Header.tsx new file mode 100644 index 00000000..f5ecadfd --- /dev/null +++ b/frontend/packages/agent/src/components/Header.tsx @@ -0,0 +1,32 @@ +//
— the agent page's identity strip: icon, glyphic title, the +// "swarm / hive" label row, and a slot for the status row +// (`` — ../status-chips/, kept separate so this component +// has no opinion on what badges exist). Structurally the same +// three-row shape as the old `#agent-header` markup in index.html +// (icon · main · pills) — see `docs/web-ui.md::Per-agent page` — this +// is a like-for-like layout port, the redesign work is in the children. +// Reuses `agent.css`'s existing `.agent-header*`/`.agent-icon` rules +// (loaded globally by index.html) rather than a component-scoped +// stylesheet — no new visual language needed for the chrome itself. +import type { ComponentChildren } from 'preact'; + +export interface HeaderProps { + label: string; + hiveLabel?: string | null; + children?: ComponentChildren; +} + +export function Header({ label, hiveLabel, children }: HeaderProps) { + return ( +
+ +
+
+

◆ {label} ◆

+
+ {hiveLabel ?
{hiveLabel}
: null} +
{children}
+
+
+ ); +} diff --git a/frontend/packages/agent/src/components/StatusChips.css b/frontend/packages/agent/src/components/StatusChips.css new file mode 100644 index 00000000..cb5c2c5a --- /dev/null +++ b/frontend/packages/agent/src/components/StatusChips.css @@ -0,0 +1,16 @@ +/* layout. Colours all come from Badge/Dropdown's own CSS + (@hive/shared) — nothing here overrides a base16 slot. */ +.status-chips { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5em; +} +.status-chip-anchor { + position: relative; + display: inline-block; +} +.status-chips-last-turn { + font-size: 0.8em; + color: var(--muted); +} diff --git a/frontend/packages/agent/src/components/StatusChips.tsx b/frontend/packages/agent/src/components/StatusChips.tsx new file mode 100644 index 00000000..b4e674b6 --- /dev/null +++ b/frontend/packages/agent/src/components/StatusChips.tsx @@ -0,0 +1,146 @@ +// — the agent page's status row: alive/turn-state, the +// model + effort pickers, context/cost usage, and pause/resume. This is +// the concrete fix for the design guide's own named anti-example (the +// old page's model/effort *pickers* lived in the `⋯` overflow menu while +// the current model/effort only showed as a disconnected chip) — every +// badge here IS the control for what it displays, composed from +// `@hive/shared`'s `Badge`/`Dropdown` (../../shared/src/badge, +// ../../shared/src/dropdown). Presentational only: formatting + +// selection state live in the caller (a `useAgentState` hook lands in a +// later commit), so this component can be demoed and reviewed against +// plain sample data before it's wired to `/api/state` polling. +import { useState } from 'preact/hooks'; +import { Badge, type BadgeTone } from '@hive/shared/badge.js'; +import { Dropdown, type DropdownOption } from '@hive/shared/dropdown.js'; +import './StatusChips.css'; + +export interface StatusChipsProps { + aliveLabel: string; + aliveTone: BadgeTone; + stateLabel: string; + stateTone: BadgeTone; + stateTooltip?: string; + model: string; + availableModels: string[]; + onSelectModel: (name: string) => void; + effort: string; + availableEfforts: string[]; + onSelectEffort: (level: string) => void; + ctxLabel?: string; + costLabel?: string; + paused: boolean; + onTogglePause: () => void; + lastTurnLabel?: string; +} + +const MODEL_DESCRIPTIONS: Record = { + haiku: 'fast', + sonnet: 'balanced', + opus: 'powerful', +}; +const EFFORT_DESCRIPTIONS: Record = { + low: '', + medium: 'default', + high: '', + xhigh: '', + max: '', +}; + +function Picker({ + label, + value, + options, + descriptions, + onSelect, +}: { + label: string; + value: string; + options: string[]; + descriptions: Record; + onSelect: (v: string) => void; +}) { + const [open, setOpen] = useState(false); + const dropdownOptions: DropdownOption[] = options.map((name) => ({ + value: name, + label: name, + description: descriptions[name] || undefined, + })); + return ( +
+ setOpen((o) => !o)} expanded={open} /> + { + onSelect(v); + setOpen(false); + }} + onClose={() => setOpen(false)} + /> +
+ ); +} + +export function StatusChips({ + aliveLabel, + aliveTone, + stateLabel, + stateTone, + stateTooltip, + model, + availableModels, + onSelectModel, + effort, + availableEfforts, + onSelectEffort, + ctxLabel, + costLabel, + paused, + onTogglePause, + lastTurnLabel, +}: StatusChipsProps) { + return ( +
+ + + + {availableEfforts.length ? ( + + ) : null} + {ctxLabel ? ( + + ) : null} + {costLabel ? ( + + ) : null} + + {lastTurnLabel ? {lastTurnLabel} : null} +
+ ); +} diff --git a/frontend/packages/agent/src/css.d.ts b/frontend/packages/agent/src/css.d.ts new file mode 100644 index 00000000..8597bb2b --- /dev/null +++ b/frontend/packages/agent/src/css.d.ts @@ -0,0 +1,4 @@ +// Ambient module for `import './Foo.css'` side-effect imports (esbuild +// resolves these directly, see build.mjs; tsc otherwise has no idea what +// a `.css` specifier is and refuses the whole side-effect import). +declare module '*.css'; diff --git a/frontend/packages/agent/src/main.tsx b/frontend/packages/agent/src/main.tsx new file mode 100644 index 00000000..ad824058 --- /dev/null +++ b/frontend/packages/agent/src/main.tsx @@ -0,0 +1,16 @@ +// Entry point for the Preact rewrite of the per-agent terminal page +// (replacing app.js's imperative DOM). Not wired into index.html yet — +// this lands in small, reviewable slices (component by component) per +// mara's "one pr != one commit" note on the issue; `index.html`'s +// `