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:
parent
f5a4e380c9
commit
93b34ebb9b
11 changed files with 334 additions and 3 deletions
146
frontend/packages/agent/src/components/StatusChips.tsx
Normal file
146
frontend/packages/agent/src/components/StatusChips.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue