feat: shared workshop components (ADD form, stats, feed, harness) — TDD

- AddLayerForm: keystone ADD capture — string layers (L2-L5) as textarea,
  object layers (L1) as per-field inputs; writes straight to the store
- StatsTally: live classified-frame counts
- LiveFeed: presentational IMU readout (props-driven)
- HarnessTomlPreview: live harness.toml render
- HarnessProviderSelect: provider toggle group + model field
- ui/textarea primitive

14 new tests; suite 63/63 green, typecheck + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-16 19:07:20 -07:00
co-authored by Claude Opus 4.8
parent 66191217ec
commit 965af7e5c0
11 changed files with 437 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { AddLayerForm } from './AddLayerForm'
import { useSession } from '@/store/session'
describe('AddLayerForm', () => {
beforeEach(() => {
useSession.getState().reset()
sessionStorage.clear()
})
it('writes a string layer (L2) to the store as the user types', async () => {
const user = userEvent.setup()
render(<AddLayerForm layer="L2" title="Layer 2" placeholder="reasoning policy" />)
await user.type(screen.getByLabelText(/layer 2/i), 'escalate on critical')
expect(useSession.getState().add.L2).toBe('escalate on critical')
})
it('hydrates a string layer from existing store state', () => {
useSession.getState().setAddLayer('L3', 'prior text')
render(<AddLayerForm layer="L3" title="Layer 3" />)
expect(screen.getByLabelText(/layer 3/i)).toHaveValue('prior text')
})
it('writes an object layer (L1) field-by-field', async () => {
const user = userEvent.setup()
render(
<AddLayerForm
layer="L1"
title="Layer 1"
fields={[
{ key: 'goal', label: 'Goal' },
{ key: 'perception', label: 'Perception' },
]}
/>,
)
await user.type(screen.getByLabelText(/goal/i), 'keep structure safe')
await user.type(screen.getByLabelText(/perception/i), 'imu + acoustic')
expect(useSession.getState().add.L1).toEqual({
goal: 'keep structure safe',
perception: 'imu + acoustic',
})
})
})
+84
View File
@@ -0,0 +1,84 @@
import { useId } from 'react'
import { Textarea } from '@/components/ui/textarea'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { useSession, type AddLayers } from '@/store/session'
export interface AddLayerFieldSpec {
key: string
label: string
placeholder?: string
multiline?: boolean
}
export interface AddLayerFormProps {
layer: keyof AddLayers
title: string
description?: string
/** when present, the layer is a structured object keyed by these fields (L1) */
fields?: AddLayerFieldSpec[]
/** placeholder for the single-textarea string layers (L2L5) */
placeholder?: string
}
/**
* Keystone ADD capture component. Drives one layer of the 5-layer Agent Design
* Document: string layers (L2L5) render a single textarea; object layers (L1)
* render one input per field. All edits flow straight into the session store.
*/
export function AddLayerForm({ layer, title, description, fields, placeholder }: AddLayerFormProps) {
const value = useSession((s) => s.add[layer])
const setAddLayer = useSession((s) => s.setAddLayer)
const baseId = useId()
return (
<Card>
<CardHeader>
<CardTitle className="text-base">{title}</CardTitle>
{description && <p className="text-xs text-muted-foreground leading-relaxed">{description}</p>}
</CardHeader>
<CardContent className="space-y-4">
{fields ? (
fields.map((f) => {
const obj = (value as Record<string, string> | null) ?? {}
const id = `${baseId}-${f.key}`
return (
<div key={f.key} className="space-y-2">
<label htmlFor={id} className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground">
{f.label}
</label>
{f.multiline ? (
<Textarea
id={id}
placeholder={f.placeholder}
value={obj[f.key] ?? ''}
onChange={(e) => setAddLayer(layer, { ...obj, [f.key]: e.target.value })}
/>
) : (
<Input
id={id}
placeholder={f.placeholder}
value={obj[f.key] ?? ''}
onChange={(e) => setAddLayer(layer, { ...obj, [f.key]: e.target.value })}
/>
)}
</div>
)
})
) : (
<div className="space-y-2">
<label htmlFor={baseId} className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground">
{title}
</label>
<Textarea
id={baseId}
placeholder={placeholder}
value={(value as string) ?? ''}
onChange={(e) => setAddLayer(layer, e.target.value)}
/>
</div>
)}
</CardContent>
</Card>
)
}
@@ -0,0 +1,39 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { HarnessProviderSelect } from './HarnessProviderSelect'
import { useSession } from '@/store/session'
describe('HarnessProviderSelect', () => {
beforeEach(() => {
useSession.getState().reset()
sessionStorage.clear()
})
it('marks the current provider as pressed', () => {
render(<HarnessProviderSelect />)
expect(screen.getByRole('button', { name: /anthropic/i })).toHaveAttribute('aria-pressed', 'true')
})
it('writes the chosen provider to the store', async () => {
const user = userEvent.setup()
render(<HarnessProviderSelect />)
await user.click(screen.getByRole('button', { name: /groq/i }))
expect(useSession.getState().harness.provider).toBe('groq')
})
it('writes the model field to the store', async () => {
const user = userEvent.setup()
render(<HarnessProviderSelect />)
const input = screen.getByLabelText(/model/i)
await user.clear(input)
await user.type(input, 'claude-sonnet-4-6')
expect(useSession.getState().harness.model).toBe('claude-sonnet-4-6')
})
it('disables editing when read-only', () => {
render(<HarnessProviderSelect readOnly />)
expect(screen.getByRole('button', { name: /groq/i })).toBeDisabled()
expect(screen.getByLabelText(/model/i)).toHaveAttribute('readonly')
})
})
+60
View File
@@ -0,0 +1,60 @@
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import { useSession, type Provider } from '@/store/session'
const PROVIDERS: { id: Provider; label: string }[] = [
{ id: 'anthropic', label: 'Anthropic' },
{ id: 'groq', label: 'Groq' },
{ id: 'openai', label: 'OpenAI' },
]
export interface HarnessProviderSelectProps {
/** render the model field read-only (e.g. the Module 2 config summary) */
readOnly?: boolean
}
/** Provider toggle group + model field, bound to the session harness. */
export function HarnessProviderSelect({ readOnly }: HarnessProviderSelectProps) {
const provider = useSession((s) => s.harness.provider)
const model = useSession((s) => s.harness.model)
const setHarness = useSession((s) => s.setHarness)
return (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-2" role="group" aria-label="Reasoning provider">
{PROVIDERS.map((p) => {
const selected = p.id === provider
return (
<button
key={p.id}
type="button"
disabled={readOnly}
aria-pressed={selected}
onClick={() => setHarness({ provider: p.id })}
className={cn(
'font-mono text-xs px-2 py-2 rounded-md border transition disabled:opacity-60',
selected
? 'bg-primary text-primary-foreground border-primary'
: 'bg-card text-muted-foreground border-border hover:border-primary/40',
)}
>
{p.label}
</button>
)
})}
</div>
<div className="space-y-2">
<label htmlFor="harness-model" className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground">
Model
</label>
<Input
id="harness-model"
value={model}
readOnly={readOnly}
onChange={(e) => setHarness({ model: e.target.value })}
className="font-mono"
/>
</div>
</div>
)
}
@@ -0,0 +1,24 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import { HarnessTomlPreview } from './HarnessTomlPreview'
import { useSession } from '@/store/session'
describe('HarnessTomlPreview', () => {
beforeEach(() => {
useSession.getState().reset()
sessionStorage.clear()
})
it('renders the live harness as TOML', () => {
render(<HarnessTomlPreview />)
const pre = screen.getByTestId('harness-toml')
expect(pre).toHaveTextContent('[harness]')
expect(pre).toHaveTextContent('threshold_g = 0.8')
})
it('reflects a tuned threshold', () => {
useSession.getState().setHarness({ thresholdG: 1.1 })
render(<HarnessTomlPreview />)
expect(screen.getByTestId('harness-toml')).toHaveTextContent('threshold_g = 1.1')
})
})
+15
View File
@@ -0,0 +1,15 @@
import { useSession } from '@/store/session'
import { harnessToToml } from '@/lib/harness'
/** Read-only `harness.toml` preview that reflects the live tuned config. */
export function HarnessTomlPreview() {
const harness = useSession((s) => s.harness)
return (
<pre
data-testid="harness-toml"
className="font-mono text-xs bg-card border border-border rounded-md p-4 overflow-x-auto whitespace-pre"
>
{harnessToToml(harness)}
</pre>
)
}
+30
View File
@@ -0,0 +1,30 @@
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { LiveFeed } from './LiveFeed'
describe('LiveFeed', () => {
it('prompts to start when there is no frame', () => {
render(<LiveFeed last={null} frames={[]} connected={false} />)
expect(screen.getByText(/no frames yet/i)).toBeInTheDocument()
expect(screen.getByTestId('live-feed').querySelector('[data-state="idle"]')).toBeTruthy()
})
it('renders the latest frame axes and magnitude when live', () => {
render(
<LiveFeed
last={{ ax: 0.3, ay: 0.4, az: 0, db: 50, t: 1 }}
frames={[{ ax: 0.3, ay: 0.4, az: 0, db: 50, t: 1 }]}
connected
/>,
)
const feed = screen.getByTestId('live-feed')
expect(feed.querySelector('[data-state="live"]')).toBeTruthy()
expect(feed.querySelector('[data-axis="ax"]')).toHaveTextContent('0.30')
expect(feed.querySelector('[data-axis="mag"]')).toHaveTextContent('0.50')
})
it('marks a simulated connection', () => {
render(<LiveFeed last={null} frames={[]} connected mocked />)
expect(screen.getByText(/simulated/i)).toBeInTheDocument()
})
})
+63
View File
@@ -0,0 +1,63 @@
import type { ImuFrame } from '@/lib/serial'
import { cn } from '@/lib/utils'
export interface LiveFeedProps {
last: ImuFrame | null
frames: ImuFrame[]
connected: boolean
mocked?: boolean
}
function mag(f: ImuFrame): number {
return Math.sqrt(f.ax * f.ax + f.ay * f.ay + f.az * f.az)
}
/**
* Presentational live IMU readout. Pages feed it `useSerial()` output; tests
* seed it with props directly.
*/
export function LiveFeed({ last, frames, connected, mocked }: LiveFeedProps) {
return (
<div data-testid="live-feed" className="border border-border rounded-md bg-card p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span
data-state={connected ? 'live' : 'idle'}
className={cn('w-2 h-2 rounded-full', connected ? 'bg-teal animate-pulse' : 'bg-muted-foreground')}
/>
<span className="text-sm font-medium">{connected ? 'Live feed' : 'Feed idle'}</span>
</div>
{mocked && (
<span className="font-mono text-[9px] uppercase tracking-widest text-amber">simulated</span>
)}
</div>
{last ? (
<div className="grid grid-cols-4 gap-2 font-mono text-xs">
{(['ax', 'ay', 'az'] as const).map((k) => (
<div key={k} className="space-y-0.5">
<div className="text-[9px] uppercase tracking-widest text-muted-foreground">{k}</div>
<div className="tabular-nums" data-axis={k}>{last[k].toFixed(2)}</div>
</div>
))}
<div className="space-y-0.5">
<div className="text-[9px] uppercase tracking-widest text-muted-foreground">|g|</div>
<div className="tabular-nums" data-axis="mag">{mag(last).toFixed(2)}</div>
</div>
</div>
) : (
<p className="text-xs text-muted-foreground">No frames yet start the feed.</p>
)}
<div className="flex gap-0.5 h-8 items-end" aria-hidden="true">
{frames.slice(-32).map((f, i) => (
<span
key={i}
className="flex-1 bg-primary/40 rounded-sm"
style={{ height: `${Math.min(100, mag(f) * 50)}%` }}
/>
))}
</div>
</div>
)
}
+27
View File
@@ -0,0 +1,27 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import { StatsTally } from './StatsTally'
import { useSession } from '@/store/session'
describe('StatsTally', () => {
beforeEach(() => {
useSession.getState().reset()
sessionStorage.clear()
})
it('renders zeroed counts initially', () => {
render(<StatsTally />)
expect(screen.getByTestId('stats-tally')).toBeInTheDocument()
expect(screen.getByText('Frames').nextElementSibling).toHaveTextContent('0')
})
it('reflects recorded events from the store', () => {
useSession.getState().recordEvent('nominal')
useSession.getState().recordEvent('critical')
render(<StatsTally />)
const tally = screen.getByTestId('stats-tally')
expect(tally.querySelector('[data-stat="calls"]')).toHaveTextContent('2')
expect(tally.querySelector('[data-stat="nominal"]')).toHaveTextContent('1')
expect(tally.querySelector('[data-stat="critical"]')).toHaveTextContent('1')
})
})
+29
View File
@@ -0,0 +1,29 @@
import { useSession } from '@/store/session'
import { cn } from '@/lib/utils'
const CELLS: { key: 'calls' | 'nominal' | 'anomalous' | 'critical'; label: string; tone: string }[] = [
{ key: 'calls', label: 'Frames', tone: 'text-foreground' },
{ key: 'nominal', label: 'Nominal', tone: 'text-teal' },
{ key: 'anomalous', label: 'Anomalous', tone: 'text-amber' },
{ key: 'critical', label: 'Critical', tone: 'text-rose' },
]
/** Live tally of classified-frame counts from the session store. */
export function StatsTally() {
const stats = useSession((s) => s.stats)
return (
<div
data-testid="stats-tally"
className="grid grid-cols-4 gap-px bg-border rounded-md overflow-hidden text-center"
>
{CELLS.map((c) => (
<div key={c.key} className="bg-background p-3 space-y-1">
<div className="font-mono text-[9px] uppercase tracking-widest text-muted-foreground">{c.label}</div>
<div className={cn('text-lg font-bold tabular-nums', c.tone)} data-stat={c.key}>
{stats[c.key]}
</div>
</div>
))}
</div>
)
}
+21
View File
@@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-20 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }