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.
This commit is contained in:
iris 2026-08-28 01:54:22 +02:00
commit 93b34ebb9b
11 changed files with 334 additions and 3 deletions

View file

@ -0,0 +1,146 @@
// <StatusChips> — 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<string, string> = {
haiku: 'fast',
sonnet: 'balanced',
opus: 'powerful',
};
const EFFORT_DESCRIPTIONS: Record<string, string> = {
low: '',
medium: 'default',
high: '',
xhigh: '',
max: '',
};
function Picker({
label,
value,
options,
descriptions,
onSelect,
}: {
label: string;
value: string;
options: string[];
descriptions: Record<string, string>;
onSelect: (v: string) => void;
}) {
const [open, setOpen] = useState(false);
const dropdownOptions: DropdownOption[] = options.map((name) => ({
value: name,
label: name,
description: descriptions[name] || undefined,
}));
return (
<div class="status-chip-anchor">
<Badge label={label} value={value} onClick={() => setOpen((o) => !o)} expanded={open} />
<Dropdown
open={open}
options={dropdownOptions}
activeValue={value}
label={`select ${label}`}
onSelect={(v) => {
onSelect(v);
setOpen(false);
}}
onClose={() => setOpen(false)}
/>
</div>
);
}
export function StatusChips({
aliveLabel,
aliveTone,
stateLabel,
stateTone,
stateTooltip,
model,
availableModels,
onSelectModel,
effort,
availableEfforts,
onSelectEffort,
ctxLabel,
costLabel,
paused,
onTogglePause,
lastTurnLabel,
}: StatusChipsProps) {
return (
<div class="status-chips">
<Badge value={aliveLabel} tone={aliveTone} />
<Badge value={stateLabel} tone={stateTone} title={stateTooltip} />
<Picker
label="model"
value={model}
options={availableModels}
descriptions={MODEL_DESCRIPTIONS}
onSelect={onSelectModel}
/>
{availableEfforts.length ? (
<Picker
label="effort"
value={effort}
options={availableEfforts}
descriptions={EFFORT_DESCRIPTIONS}
onSelect={onSelectEffort}
/>
) : null}
{ctxLabel ? (
<Badge
label="ctx"
value={ctxLabel}
title="tokens used in the current context window"
/>
) : null}
{costLabel ? (
<Badge
label="cost"
value={costLabel}
title="cumulative tokens billed across the last turn (sum across every inference; tool-heavy turns rebill the cached prompt per call)"
/>
) : null}
<Badge
value={paused ? '▶ resume' : '⏸ pause'}
tone={paused ? 'warning' : 'neutral'}
onClick={onTogglePause}
/>
{lastTurnLabel ? <span class="status-chips-last-turn">{lastTurnLabel}</span> : null}
</div>
);
}