Skip to main content
A minimal, first-class React UI for a workflow, shipped with the Gateway in one file. It demonstrates every piece a real UI needs (see the breakdown below). Drop it at .smithers/ui/<your-workflow>.tsx, declare it from the workflow with <UI entry="../ui/<your-workflow>.tsx" />, and open it with bunx smithers-orchestrator ui. For the full conceptual walkthrough, see Custom Workflow UIs.

Hijacked pipeline chat

For a runnable multi-step version, see examples/hijacked-chat-pipeline: three sequential agent tasks declared with hijack and onHijackExit="complete". The workflow UI follows the active hijack candidate, opens the Gateway’s PTY WebSocket, and presents the native agent session through the shared ChatTranscript, ChatMessage, and ChatComposer components from smithers-orchestrator/ui. Each session is still a real, resumable Codex session: the browser only replaces the terminal presentation, writing chat input as PTY bytes and rendering the streamed PTY screen in the assistant bubble. Ending a stage returns control to Smithers, which resumes the run and advances the Sequence to the next hijacked task.

The bundle

Declaring the UI

In your workflow file:
Then:
The Gateway discovers the <UI> declaration when it registers the workflow’s entryFile, and serves the bundle at /workflows/demo-react-ui. Its HTML shell injects __SMITHERS_GATEWAY_UI__ and ?runId= before your script runs; createGatewayReactRoot picks up both automatically.

Embedding it in apps/smithers

The PWA discovers workflows that advertise hasUi: true and renders this exact bundle in an iframe at /gw/demo-react-ui/<runId>. The same-origin Vite proxy (dev) and Worker proxy (Cloud) let the iframe’s RPC and WebSocket reach the real Gateway: no CORS, no token shuttling. See Same-origin proxy patterns.

What this example demonstrates

  • createGatewayReactRoot boot: one call, reads the boot config, mounts the provider, renders the tree.
  • ?runId= parsing with a safe fallback to the most recent run via useGatewayRuns.
  • Live subscription with useGatewayRunEvents: resilient reconnect, pushed updates, gap resync, and metrics.
  • Node output reads with useGatewayNodeOutput: handles produced / pending / failed.
  • Approvals strip with useGatewayApprovals + useGatewayActions().submitApproval.
  • Lifecycle action with useGatewayActions().cancelRun.
  • Stale-data-free transitions: every read and the event stream clears synchronously when runId changes, so a late response from the previous run can never repopulate state. See Stale-data-free update model.