Skip to content

UI architecture — Evidence Ledger

Scope: agentic-workflows-v2/ui/ Current: 2026-07-14 Decision: ADR-054

The dashboard is the evidence and control surface for workflow execution. It uses a light-first editorial system: warm paper, ink typography, hairline structure, restrained orange emphasis, and semantic green/red states. The former terminal vocabulary remains only where it communicates genuine CLI, log, identifier, or machine-output semantics.

Stack and boundaries

Concern Implementation Boundary
Application React 19 + TypeScript 6 Components never execute runtime work directly
Build Vite 8 + route-level dynamic imports Every page is an independent production chunk
Navigation React Router 7 Shareable view state uses paths/query parameters
Server state TanStack Query 5 No duplicate application-wide server cache
UI primitives Owned shadcn/ui source + Radix Customized to Evidence Ledger, not a stock theme
Workflow graph React Flow 12 Specialized canvas with semantic list equivalents
Styling Tailwind 3 + semantic CSS variables Canonical el-* tokens; temporary b-* aliases
Live execution WebSocket reducer Bounded retry/history and terminal-state handoff
Playground Fetch streaming over SSE Abortable text and image input/output
Verification Vitest + Playwright Contract tests live with the Python API

React Flow is intentionally specialized: spatial DAG interaction cannot be replaced by a generic card or table without losing capability. Existing raw workflow-schema controls and virtualized/log views also remain specialized where native browser semantics or performance are stronger. Shared buttons, inputs, text areas, tabs, tables, menus, dialogs, sheets, switches, alerts, tooltips, skeletons, scroll areas, and notifications use owned primitives in src/components/ui/.

Bootstrap and shell

main.tsx creates one query client and renders:

StrictMode
└── QueryClientProvider
    └── BrowserRouter
        └── AppErrorBoundary
            └── App
                ├── skip link
                ├── Sidebar / mobile bottom navigation
                ├── ConsoleHeader
                ├── lazy route boundary
                ├── CLI status strip
                └── Toaster

The desktop shell has a 216 px evidence index. Below the md breakpoint it becomes a five-item bottom navigation and reserves room above the CLI strip. The main landmark is focusable and every routed page has a Suspense fallback. Warm paper is the default theme; warm charcoal remains an optional operator preference.

Routing

All page modules are loaded with React.lazy in App.tsx.

Route Product area
/ Overview
/runs, /runs/:filename Run ledger and permanent evidence
/live/:runId, /live/latest Live execution
/workflows, /workflows/:name Workflow registry and launch
/workflows/:name/edit Feature-gated visual/YAML editor
/datasets Dataset/sample evidence
/evaluations Evaluated runs, rubric layers, comparison
/models?tab=... Model Router: models, providers, tiers, packs, playground, hardware
/settings Compatibility redirect to /models?tab=providers

The former /prototypes production route was removed. Prototype source files may exist as untracked design evidence, but no production module imports them.

Design system

src/styles/tokens.css is the canonical semantic layer. Representative light values are paper #F2EFE8, surface #F8F5EF, raised #FFFDFA, ink #1B1B18, and accent #EF5A36. Status is never encoded by color alone. Georgia supplies editorial display headings; Geist supplies interface text; monospace is reserved for IDs, versions, commands, JSON/YAML, and telemetry.

The migration follows a strangler-fig pattern. Canonical el-* values already surround every production route. The old b-* names resolve to those values while route-specific controls move onto owned primitives. This compatibility layer prevents two apps or a capability-losing big-bang rewrite and can be removed only after its final consumer is migrated.

See the design-system specification and migration record.

Data and mutation model

  • Queries and invalidation use TanStack Query keys scoped to the resource.
  • Transient form, selection, and stream state stays local.
  • Shareable tab/filter/selection state is URL-backed where implemented.
  • Mutations display success/error notifications and invalidate the narrowest affected keys.
  • Lists and event streams retain existing pagination, disclosure, or bounded history; the UI does not fabricate client pagination over incomplete data.
  • Provider credentials are never accepted as values. Only environment-variable names cross the wire.

Model Router and execution provenance

The Model Router consolidates the previous finder and settings surfaces. It supports provider creation/editing, safe enable/disable, classified probes, tier chains with dry-run routing, immutable model packs, model discovery, hardware fit, and the chat playground.

Model packs are exact {id, version} references. The API resolves them in this order after higher-authority direct/environment pins: run selection, workflow binding, global activation, then existing settings/defaults. Resolution is held in a ContextVar, so concurrent runs cannot mutate each other's routing. Completed run records retain the source, immutable pack snapshot, requested override, and every resolved step model/provider.

Multimodal playground

POST /api/chat accepts either legacy string content or ordered text/image parts. Images must be bounded PNG, JPEG, WebP, or GIF data URLs; SVG and remote input URLs are rejected. The UI supports up to four local previews and five MiB per image. Output uses a typed media SSE event and renders only validated raster data/HTTPS URLs. Streaming remains abortable, model content is rendered as text, and error bodies never expose provider credentials.

Type and contract discipline

Pydantic owns wire contracts. scripts/generate_schemas.py writes checked-in JSON snapshots and the UI generator writes TypeScript contract files. Chat message parts and ChatMediaEvent participate in the ADR-014 drift gate. Hand-authored API view types may narrow generated schemas when the generator's tuple expansion is less useful, but drift tests still prove the wire shape.

Accessibility and performance

  • A skip link targets the focusable main landmark.
  • Owned Radix primitives provide dialog/sheet focus containment and escape behavior.
  • Focus rings use a visible orange semantic token.
  • Forms retain native labels, required state, keyboard operation, and live mutation/stream statuses.
  • The DAG exposes an equivalent list/table path for non-spatial use.
  • Routes are lazy chunks; graph/editor code is not in the entry bundle.
  • Live event lists, run windows, samples, and JSON views keep their existing bounds rather than growing without limit.

Development and verification

npm --prefix agentic-workflows-v2/ui test -- --run
npm --prefix agentic-workflows-v2/ui run build
npm --prefix agentic-workflows-v2/ui run test:e2e
.venv\Scripts\python.exe -m pytest agentic-workflows-v2/tests -q

The visual acceptance viewports are 1440×900, 1280×720, 1024×768, and 390×844. Browser evidence and gate results are recorded in the migration record.