core/docs/decisions/ADR-0162-workbench-design-system.md
Shay f2028de0a1 docs(workbench): note the /health liveness signal in ADR-0162 footer spec
StatusFooter now surfaces four signals, not three — add the GET /health
liveness dot to the design-system footer contract to match shipped UI.
2026-06-14 17:18:13 -07:00

25 KiB
Raw Blame History

ADR-0162 — Workbench Design System (v1)

Status: Proposed Date: 2026-05-26 Author: Shay Parent: ADR-0160 — CORE Workbench v1 Companions: ADR-0161 — HITL async queue, ADR-0153 — TurnEvent.trace_hash back-stamp, ADR-0159 — Contemplation-quality eval


Context

ADR-0160 named the Workbench's product doctrine — read-only first, calm by default, infinite depth, replay before persuasion, no accept/reject in v1. W-026 landed the read-only API with typed schemas and closed-set error codes.

What the cascade still lacks is a ratified design substrate. Without one, every downstream branch (W-027 frontend shell, W-028 chat + trace drawer, W-029 proposal queue, W-030 eval center, W-031 replay theater) will hand-roll its own tokens, badge mappings, motion rules, JSON-viewer behavior, and empty/error/loading states. Five branches' worth of silent drift is exactly the failure mode ADR-0160 forbids ("no dashboard soup").

This ADR pins the design substrate before any frontend code exists. Tokens, typography, motion, semantic state mapping, the StableJsonViewer invariants, empty/error/loading contracts, keyboard behavior, and the explicit no-go list are doctrine, not styling. They are the trust surface the visual layer sits on.

The principle behind every call below is the same as ADR-0161's: select the narrowest commitment that still names testable invariants. This ADR ships zero implementation; it commits the contract that W-027 will honor and that every later branch will inherit.


Product north star (one paragraph)

The Workbench should feel like Linear's calm + Raycast's speed + GitHub PR review's auditability + Xcode Instruments' precision — operating a deterministic cognition engine with full traceability. Not a chat clone. Not a SaaS dashboard. Not animated AI theater. The beauty comes from structural truth, hierarchy, responsiveness, and precision; from operational sovereignty, not decoration.

Every meaningful surface must be able to answer six questions: What happened? Why was it allowed? What evidence supports it? Can it be replayed? Did it mutate anything? Who/what has authority to ratify the next step? A panel that cannot answer one of those questions does not ship in v1.


Decision summary

Concern Decision
Token namespace Semantic, not literal. --color-surface-base, not --color-zinc-900. Tokens are CSS custom properties + a typed TS export.
Theme Dark default. Light theme deferred to v2. No system-theme auto-switch in v1.
Typography Inter (UI), JetBrains Mono (hash/JSON/trace). System fallback chain pinned.
Color semantics Bound to ratified EpistemicState (15) and NormativeClearance (4) enums in core/epistemic_state.py. No badge color without an enum binding.
Motion Allowed: drawer/palette/expand/diff/route transitions. Forbidden: "thinking" animations, particle effects, glowing networks, avatar pulses, shimmer-everywhere.
StableJsonViewer Deterministic, lossless, copyable, diffable. Trust surface, not a code block. Six tested invariants.
Empty / error / loading Every route ships all three. Empty includes a next action. Error includes reproducer + mutation status. Loading uses specific labels.
Keyboard Keyboard-first. ⌘K palette, Esc closes overlays, Enter opens selection, focus-visible everywhere, no color-only encoding.
Layout shell Five-region grid: TopBar / LeftNav / MainSurface / RightInspector / StatusFooter. Routes may collapse the rail but not the bar/nav/footer.

Decision detail

1. Token namespace and theme

Tokens live in workbench-ui/src/design/tokens/ as CSS custom properties (tokens.css) plus a typed TypeScript mirror (tokens.ts). The TS export is generated from the CSS to keep one source of truth.

Naming rule. Token names are semantic, not literal. A consumer reads what the value means, not what shade it is:

--color-surface-base
--color-surface-raised
--color-surface-overlay
--color-border-subtle
--color-border-strong
--color-text-primary
--color-text-secondary
--color-text-mono
--radius-sm  --radius-md  --radius-lg
--space-1 .. --space-12
--shadow-panel  --shadow-floating
--motion-fast  --motion-base  --motion-slow
--motion-ease-out  --motion-ease-spring

Forbidden: --color-blue-500, --color-zinc-900. Literal-named tokens leak palette decisions into consumers and prevent palette evolution.

Theme. Dark by default. Light theme is deferred to a follow-up ADR. No system-theme auto-switching in v1 (avoids first-render flash and keeps screenshots stable for the audit trail).

2. Typography

UI: Inter with the system stack as fallback (-apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif).

Mono: JetBrains Mono with fallback ("SF Mono", "Menlo", "Consolas", monospace). Used for every surface that carries audit-significant text: trace_hash, proposal_id, JSON, file paths, CLI commands, digests.

Hierarchy is pinned by token, not by class proliferation:

--font-size-display  --font-size-h1  --font-size-h2
--font-size-body  --font-size-meta  --font-size-mono
--line-height-tight  --line-height-base
--font-weight-regular  --font-weight-medium  --font-weight-semibold

No more than two weight steps per surface. Density without clutter.

3. Color semantics — bound to ratified enums

This is the load-bearing call. Every status badge color must be bound to an enum value that the engine already emits. No "aspirational" badge colors. No two badges may share a color. This keeps the badge layer honest and audit-grade.

3a. EpistemicState (15 values from core/epistemic_state.py)

State Badge label Token Felt meaning
decoded Decoded --color-state-decoded (cool blue-green) Engine reconstructed the answer in canonical form
decoded_unarticulated Decoded (silent) --color-state-decoded (muted) Reconstructed but no surface emitted
verified Verified --color-state-verified (cool green) Cross-checked against reviewed evidence
evidenced Evidenced --color-state-evidenced (teal) Direct evidence supports the surface
evidenced_incomplete Evidenced (partial) --color-state-evidenced (muted) Evidence partial; surface qualified
inferred Inferred --color-state-inferred (indigo) Composed from existing chains, not directly evidenced
unverified_possible Unverified --color-state-unverified (blue-gray) Plausible but unverified
unverified_novel Unverified (novel) --color-state-unverified (warm) New territory, no prior evidence
perceived Perceived --color-state-perceived (cyan) Input registered, not yet grounded
contradicted Contradicted --color-state-contradicted (red) Evidence falsifies the surface
ambiguous Ambiguous --color-state-ambiguous (violet) Multiple coherent readings
undetermined Undetermined --color-state-undetermined (muted gray) Insufficient evidence to choose
scope_boundary Out of scope --color-state-scope (slate) Outside the engine's accepted domain
computationally_bounded Bounded --color-state-bounded (orange) Computation hit a deliberate ceiling
epistemic_state_needed Needs review --color-state-needed (amber) Engine declined to self-classify

Clicking any badge opens a popover explaining the state, citing the ADR that pinned it, and listing the evidence that produced the classification.

3b. NormativeClearance (4 values from core/epistemic_state.py)

State Badge label Token
cleared Cleared --color-clearance-cleared (cool green)
violated Violated --color-clearance-violated (red)
unassessable Unassessable --color-clearance-unassessable (muted gray)
suppressed Suppressed --color-clearance-suppressed (muted red)

3c. ReviewState (4 values from teaching/proposals.py, per ADR-0057 / ADR-0161)

State Badge label Token
pending Pending --color-review-pending (amber)
accepted Accepted --color-review-accepted (cool green)
rejected Rejected --color-review-rejected (muted red)
withdrawn Withdrawn --color-review-withdrawn (muted gray)

3d. Grounding source (6 values from cold-start-grounding lane)

State Badge label Token
teaching Teaching --color-grounding-teaching (cool green)
pack Pack --color-grounding-pack (teal)
vault Vault --color-grounding-vault (indigo)
partial Partial --color-grounding-partial (muted teal)
oov Out of vocab --color-grounding-oov (orange)
none Not grounded --color-grounding-none (slate)

Color-only encoding is forbidden. Every badge carries its label; the color is reinforcement. Accessibility and screenshot-audit fidelity both depend on this.

4. Motion

The motion rule is one line: motion reveals structure, not cognition.

Allowed motion (each with a single token, no per-surface bespoke easing):

Motion Duration Easing
Drawer open/close --motion-base (180ms) --motion-ease-out
Command palette enter/exit --motion-fast (120ms) --motion-ease-out
Trace row expand --motion-base (180ms) --motion-ease-out
Diff highlight pulse --motion-fast (120ms) linear (one cycle, no loop)
Route transition --motion-base (180ms) --motion-ease-out
Skeleton loading shimmer --motion-slow (1200ms) linear, ≤2 cycles before falling back to static "Loading…" label

Forbidden:

  • "thinking…" pulses, dots, or orbs
  • particle effects
  • glowing neural-network animations
  • avatar movement
  • glassmorphism / frosted-glass surfaces
  • shimmer applied to anything that is not a loading skeleton
  • animated graph or chain construction "theater"
  • background gradients that move
  • any animation that loops indefinitely

Reduced-motion (prefers-reduced-motion: reduce) must collapse every allowed motion to instant. No exceptions.

5. StableJsonViewer — trust surface invariants

This is the component that, if done casually, silently erodes the audit invariant. It is doctrine, not styling. Six invariants, each testable:

  1. Deterministic key ordering. Keys render in the order the source serialized them. When the source is a dict, sort lexicographically. When the source is an already-serialized JSON string, preserve appearance order. Never silently re-sort.
  2. Lossless string preservation. No smart quotes. No whitespace stripping. No unicode normalization. No HTML entity coercion. What you see is what is on disk.
  3. No semantic auto-format. Numbers display as the source typed them (1e-60.000001). Booleans, null, and integer/float distinctions are preserved.
  4. Copy paths. Right-click (and a keyboard shortcut) yields the JSON Pointer path to the selection (e.g., /scenes/3/detail/proposed_chain/object). This is the audit-trail handle.
  5. Diff mode. Given two values, render side-by-side with structural alignment. Highlight only the leaf fields that changed. Color-only encoding forbidden — changed leaves carry a glyph (≠ added, ≠ removed, ≠ changed).
  6. Large-document safety. Virtualize rendering above 1,000 leaf nodes. Above 16 MiB raw bytes, refuse to inline-render and surface a "Open in external viewer" affordance with a copy-path button — same ceiling as the W-026 read API.

The viewer must also display the SHA-256 digest of the rendered source as a --font-mono badge. Clicking the digest copies it.

6. Empty / error / loading state contract

Every route ships all three states from day one. This is acceptance criterion #9 from the vision doc, lifted into doctrine here.

Empty

Every empty state must contain:

  • A one-line statement of what is absent.
  • A next action — either a CLI command to copy, a route to navigate to, or a runtime config to inspect.

Example:

No pending proposals.
Run: core demo learning-arc

Never empty-empty. Never just a "—".

Error

Every error state must surface:

  • What failed (one sentence)
  • Whether state was mutated (No corpus mutation occurred. is a load-bearing line per CLAUDE.md doctrine)
  • A reproducer — the CLI command or curl that triggers it
  • Whether retry is safe

Example:

Replay failed before comparison.
No corpus mutation occurred.
Reproduce:
  uv run core demo learning-arc --json
Retry: safe

Loading

Loading labels are specific, never "Thinking…":

Loading trace…
Computing replay…
Reading proposal log…
Running eval lane…
Comparing artifacts…

The skeleton shimmer caps at two cycles (per §4) and then collapses to the static label. Indefinite shimmering is forbidden.

7. Keyboard contract

The Workbench is keyboard-first. Baseline:

  • ⌘K / Ctrl+K opens the command palette from any route
  • Esc closes any overlay (drawer, palette, popover)
  • Enter activates the focused item (open, expand)
  • / traverse lists; / traverse adjacent panels
  • ? opens the keyboard-shortcut cheat sheet
  • Every interactive element has focus-visible styles drawn from --color-focus-ring
  • Tab order matches visual order; explicit tabIndex only where the visual order conflicts with the semantic order
  • Every drawer/dialog announces a role and an aria-label
  • No interactive surface is reachable only by mouse

The command palette must support, at minimum, fuzzy search across:

Run eval lane
Open proposal
Replay trace
Compare run
Inspect pack
Search trace hash
Copy ratification command
Open latest contemplation report

8. Layout shell

The shell is a five-region grid, named consistently across routes:

+---------------------------------------------------------------+
| TopBar             (command palette / context / status)       |
+---------+-------------------------------------------+---------+
|         |                                           |         |
| LeftNav | MainSurface                               | Right   |
|         |                                           | Inspect |
|         |                                           |         |
+---------+-------------------------------------------+---------+
| StatusFooter       (runtime / replay / mutation mode)         |
+---------------------------------------------------------------+

Routes may collapse the RightInspector but not hide the TopBar, LeftNav, or StatusFooter. Removing the persistent surfaces breaks the "always know what runtime you're talking to" affordance that ADR-0160 required.

LeftNav contents are pinned and ordered:

Chat
Trace
Replay
Proposals
Evals
Runs
Packs
Vault / Recall
Audit Log
Settings / Runtime

No giant sidebar. No collapsible sub-trees in v1.

StatusFooter surfaces four signals at all times:

  • status from GET /health (Healthy / Unhealthy / Checking) — a liveness dot polled independently of /runtime/status so a live server still reads healthy when runtime metadata is degraded; any non-ok status fails safe to Unhealthy
  • mutation_mode from GET /runtime/status (read_only or runtime_turn) — color-encoded and labeled
  • git_revision (short SHA, mono)
  • checkpoint_revision (short SHA, mono) — turns amber when revision_warning: true per ADR-0157

9. Component map — v1 must-ship vs follow-up

The vision doc lists ~30 components. The honest v1 scope is narrower. This ADR commits to must-ship for W-027..W-031; everything else is named here so the design system anticipates it, but ships in a follow-up.

Must ship in v1 (W-027..W-031):

WorkbenchShell
TopBar              LeftNav             StatusFooter
CommandPalette

ChatTurnList        ChatTurnCard
ResponseEvidenceStrip
TraceDrawer
EpistemicStateBadge GroundingSourceBadge NormativeClearanceBadge ReviewStateBadge
TraceHashBadge      CopyableHash

ReplayTheater       ReplayComparisonPanel ReplayDiffViewer

ProposalQueue       ProposalCard          ProposalDetailPanel
RatificationCommandPanel

EvalCenter          EvalLaneList          EvalFailureViewer

StableJsonViewer    StableJsonDiffViewer  ArtifactLink

Follow-up after v1 milestone:

TraceStepList         (inline trace step navigator)
ProposalChainViewer   (chain visualization)
SourceProvenancePanel (deep provenance graph)
PackInspector         CorpusInspector      VaultRecallInspector
MetricGateTable       RegressionDiffPanel

Pinning the v1 component set keeps the implementation branches honest.

10. The no-go list

Explicit, because doctrine drift is what produces "AI dashboard" UIs:

  • No chat-clone styling. No avatar bubbles. No "AI is thinking…" affordances.
  • No animated cognition theater (particles, glowing networks, pulsing orbs).
  • No glassmorphism / frosted glass.
  • No purple gradients, no neon accents, no cyberpunk styling.
  • No live "hallucinated" chain or graph construction animations.
  • No node-graph builder in v1.
  • No workflow automation canvas in v1.
  • No "accept proposal" button anywhere in v1 (per ADR-0160 + ADR-0161 §2).
  • No dashboard-soup pages.
  • No vanity green-dashboard eval views.
  • No "active session count" / "total turns" splash metrics on a landing page.
  • No system-tray notifications, no toasts that auto-dismiss audit events.
  • No icon-only buttons without accessible labels.
  • No color-only state encoding.

Implementation plan — Branch 1

Pre-W-027 deliverable. Single PR titled feat(workbench-ui): design system v1 (ADR-0162):

  1. workbench-ui/ directory created at repo root with package.json, vite.config.ts, tsconfig.json. Pin React 18, Vite 5, TS 5.x, Tailwind 3.x, shadcn primitives. No app routes yet.
  2. workbench-ui/src/design/tokens/tokens.css — every token from §1§4.
  3. workbench-ui/src/design/tokens/tokens.ts — typed TS mirror, generated from tokens.css at build time (a scripts/generate-tokens.ts reader).
  4. workbench-ui/src/design/components/StableJsonViewer/ — full implementation honoring all six invariants in §5, with tests.
  5. workbench-ui/src/design/components/EpistemicStateBadge/ and the four other badge primitives — each generated from the enum tables in §3 so the badge set is provably exhaustive.
  6. workbench-ui/src/design/components/EmptyState/, ErrorState/, LoadingState/ — primitives the route screens compose.
  7. workbench-ui/preview/ — a Vite route exposing every primitive on one page so the design baseline is reviewable in a browser. Not shipped to end users; it is the operator's "Storybook lite".
  8. Frontend tests: token presence, badge-set exhaustiveness against the Python enums (parsed from core/epistemic_state.py and teaching/proposals.py at build time), StableJsonViewer invariants 16, reduced-motion collapse, keyboard contract for the palette and drawers.
  9. Docs update: docs/workbench/design-system.md linking the ADR, docs/workbench/README.md runbook addition (cd workbench-ui && pnpm preview).

Crucially: no app shell, no routes, no API client yet. Branch 1 is only the substrate. W-027 (frontend shell) is the first branch that consumes it.


Acceptance criteria

This ADR is ratifiable when:

  1. Every token in §1§4 is a CSS custom property with a matching typed TS export, and a test fails if either drifts.
  2. Every enum value in §3a§3d has exactly one badge component, and a test parses the Python enums and asserts 1:1 coverage. Adding an enum value to the engine without adding a badge fails the test.
  3. StableJsonViewer's six invariants each have a test:
    • byte-identical round-trip on a synthetic source,
    • copy-path returns a valid JSON Pointer,
    • diff highlights only changed leaves and adds the glyph,
    • large-document virtualization triggers above 1,000 leaves,
    • oversize-document refusal triggers above 16 MiB,
    • SHA-256 digest matches the source bytes.
  4. Every primitive component renders an EmptyState / ErrorState / LoadingState snapshot test from a fixture and asserts the "next action" / "reproducer" / "specific label" rules in §6.
  5. prefers-reduced-motion: reduce collapses all motion to instant in a Playwright test.
  6. The command palette is reachable via keyboard from every primitive's preview page (Playwright).
  7. The preview page (pnpm preview) renders every primitive without network access (offline-safe, deterministic).

Out of scope

This ADR is the design substrate. It does not commit to:

  • the app shell or routes (W-027)
  • chat or trace UI (W-028)
  • proposal queue UI (W-029)
  • eval center UI (W-030)
  • replay theater UI (W-031)
  • a backend API beyond what W-026 already exposes
  • light theme / system-theme switching
  • internationalization
  • multi-tenant or remote operation
  • mobile layouts (Workbench is a desktop tool; the mobile path for ratification stays the GitHub mobile app + workflow_dispatch per ADR-0161 §2)
  • a frontend authentication layer (deferred to a separate ADR; v1 is local-only, unauthenticated, per ADR-0160)
  • Storybook / Chromatic — the /preview route is the substitute
  • Figma component mirroring — design tokens are the source of truth, not a Figma file

Consequences

Positive

  • Five downstream branches inherit one substrate. No silent drift between W-027 and W-031.
  • The badge layer is provably exhaustive against the engine's ratified enums — adding a new EpistemicState value cannot ship without a badge.
  • StableJsonViewer becomes a tested trust surface, not a code-styling choice. Replay-equivalence claims gain a UI-side audit handle.
  • The no-go list closes the "AI dashboard drift" failure mode by name, not by hope.
  • Empty / error / loading contracts ship from day one. The "we'll add empty states later" failure mode is forbidden.

Negative

  • Branch 1 is overhead before any user-visible UI exists. ~1 day's work that shows nothing in the chat surface. Worth it.
  • Tying badges to enums means engine refactors that rename or remove enum values cascade into the UI. This is the intended behavior, not a cost — the UI cannot diverge from the engine's self-classification.
  • The 16 MiB StableJsonViewer ceiling matches the W-026 API ceiling. If either ceiling moves, the other must move with it.

Risks

  • Token bikeshedding. Mitigation: tokens are semantic-only. The conversation is about what the surface means, not what shade it is.
  • Inter / JetBrains Mono licensing. Both are OFL-licensed; bundling is permitted. Self-hosted, not fetched at runtime.
  • Component scope creep. The §9 component map names what ships in v1 vs follow-up; deviation requires a separate ADR.

Cross-references

Memory cross-references

  • thesis-decoding-not-generating — the UI must reveal the engine decoding (badges, replay, trace hashes), not staging a generating performance (animation theater). Every motion / animation choice is measured against this.
  • feedback-address-critiques-dont-waive — the vision-doc gaps (StableJsonViewer underdefined, empty-states absent, badge taxonomy outrunning the engine) are addressed here, not deferred.
  • feedback-adr-cross-reference-discipline — every badge color is bound to a ratified enum. No badges without a Python source of truth.
  • user-circumstances — the operator works from a tent / library, on intermittent connectivity. Local-only, offline-safe, no fonts or assets fetched at runtime. The /preview page must render with the network disabled.