Campaign workspace UI
The campaign workspace from adi/campaign-workspace-ui (source commit
8b74c0e8c) is ported onto the client package architecture in main at
3a48021f3. The source branch remains available for the original design history.
Preserved behavior
The default browser page is the recorded campaign preview: objective brief and objective details, performance trajectories and measurement scrubbing, current workstreams, workstream and agent activity timelines, searchable and sortable history, and workstream/agent detail sheets. Wide screens keep current work, performance, and selected measurement side by side; narrow screens reflow. Timeline zoom and pan do not change the selected measurement. Detail navigation preserves the replay position and keyboard focus.
The Connect action uses the current gateway connection feature. Live runs keep
main's event bootstrap, reconnect, stream/control banners, iteration navigation,
run commands, and server-selected themes. A Conversation action adds the branch's
Ask and Steer modes. Ask uses query.chat; returned events fold into the existing
run store. Steer uses command.steer. Preview pages omit this action because they
have no live run. Drafts and pending requests belong to their run session.
Ownership
clients/features/campaignowns the campaign parser, frame fold, replay and streaming state machines, dashboard, timeline, and presentation styles.CampaignViewModelremains the presentation boundary.clients/features/conversationowns the run conversation machine and view.clients/libs/run-sessionowns the typed chat operation on the existing run connection; it does not create a second control transport or transcript fold.clients/apps/webcomposes these features with the current gateway and run features. Its platform backend owns HTTP and SSE. The Vite plugin owns the development preview's SSE producer.
Features have public state and renderer entries. Apps compose peers through
these interfaces; neither feature imports another feature. Data folds are pure;
XState owns control modes, cancellation, and playback timers. The app injects
fixture fetching and the campaign stream.
The branch's focused Radix components and Tailwind styles remain confined to the campaign feature. Existing shared UI components serve the gateway and conversation.
Live backend wiring
Live (clients/apps/web/src/app/compose.tsx) renders a run/campaign toggle over the same
session a run page already attaches to, for both the browser's and the desktop's live page
(createLiveApp, unchanged). The campaign view's store is createLiveCampaignStore fed by
webSessionCampaignStream(session) (clients/features/campaign/src/state/live-campaign-stream.ts),
which reads session.store's already-folded CoreState and projects it into the same closed frame
union the preview stream emits (campaign-init, agent-upsert, workstream-upsert, measurement,
tokens, status). It subscribes to session.store, not the run connection's own transport,
because that transport is documented single-subscriber; session.store already supports the run
page's own subscription plus this one.
This is a client-local projection, not a new backend contract: webSessionCampaignStream folds
CoreState.workstreams (itself folded from the new WORKSTREAM_FINISHED and
TURN_USAGE_RECORDED wire events) into campaign frames in the browser. The frame types and the
fold that applies them (campaign-fold.ts's contiguous-sequence, absorbing-completed contract)
are unchanged from the preview; only the producer is new. The projection is honest about what it
cannot derive from the run protocol today: objective, benchmarkVersions, and trajectories are
empty, firstSequence is the constant 1, and a workstream's running cost is captured internally
but not surfaced on any frame field. See live-campaign-streaming.md
for why this remains a preview contract rather than #1761's backend-owned one.
Compatibility decisions
The old clients/web, backend-client, and core-state packages are not restored.
Their behavior uses the current app, contracts, run-state, transport, and run-session
packages. Generated protocol artifacts are unchanged.
The branch's experimental async_operation_lifecycle backend patch is not ported:
main removed that lifecycle event. The old thread-based WebSocket helper is also
superseded by main's owned blocking-request implementation and shutdown tests.
The campaign's preview frames are separate from the production run protocol; see
live-campaign-streaming.md.