# FE handoff — conversation lifecycle (tab persistence across devices) Courier this to `../dispatch-web`. All changes are ADDITIVE — nothing existing breaks. ## What shipped (backend) Conversations now have a lifecycle **status** field: `active`, `idle`, or `closed`. This enables tab persistence: when a new browser connects, it fetches all `active` + `idle` conversations and restores the tab bar. - **`active`** — an agent is currently generating (a turn is in-flight). - **`idle`** — conversation exists, not generating. User can send a message to resume. - **`closed`** — user dismissed the tab (hidden from the tab bar, not deleted). Status transitions are driven by the backend: - `idle → active` when a turn starts. - `active → idle` when a turn settles (done/error). - `→ closed` when `POST /conversations/:id/close` is called. ## Bump pinned deps - `@dispatch/wire` → `0.10.0` - `@dispatch/transport-contract` → `0.14.0` ## New types (`@dispatch/wire` + `@dispatch/transport-contract`) ```ts export type ConversationStatus = "active" | "idle" | "closed"; // ConversationMeta now has a status field: export interface ConversationMeta { readonly id: string; readonly createdAt: number; readonly lastActivityAt: number; readonly title: string; readonly status: ConversationStatus; } // New WS message (server → client): export interface ConversationStatusChangedMessage { readonly type: "conversation.statusChanged"; readonly conversationId: string; readonly status: ConversationStatus; } ``` `ConversationStatusChangedMessage` is added to the `WsServerMessage` union. ## `GET /conversations?status=active,idle` — filter by status The existing `GET /conversations` endpoint now accepts an optional `?status=` query param: a comma-separated list of statuses to filter by. - **Default (no param):** returns ALL conversations (all statuses). - `?status=active,idle` → only active + idle (what the FE tab bar wants). - `?status=closed` → only closed conversations (for a history view). - Invalid values are silently dropped. If all values are invalid, no filter is applied (returns all). ## `POST /conversations/:id/close` — marks as closed The existing close endpoint now also sets the conversation's status to `closed` in the store. This persists across server restarts. The response is unchanged (`{ conversationId, abortedTurn }`). ## `conversation.statusChanged` WS message Broadcast to ALL connected WS clients whenever a conversation's status changes. The backend emits this synchronously alongside the existing `turnStarted` / `turnSettled` / `conversationClosed` hooks. ```ts { type: "conversation.statusChanged", conversationId: "conv-1", status: "active" } ``` ## What the FE needs to do 1. **On connect:** call `GET /conversations?status=active,idle` to fetch conversations for the tab bar. Render tabs for each. 2. **`active` tabs:** subscribe to the conversation's live stream (`chat.subscribe` WS op) to receive in-flight events. 3. **`idle` tabs:** load history via `GET /conversations/:id`. No live subscription needed until the user sends a message. 4. **Tab close button:** call `POST /conversations/:id/close` to mark the conversation as `closed`. Remove it from the tab bar. 5. **Handle `conversation.statusChanged` WS messages:** update the tab's status indicator. When a conversation goes `idle → active`, show a loading/generating indicator. When it goes `active → idle`, remove the indicator. When it goes `closed`, remove the tab. 6. **Closed conversations:** accessible from a history view (`GET /conversations?status=closed`). Can be reopened by sending a message (which transitions `closed → active`). ## CLI `dispatch list` now defaults to `active,idle` (excludes closed). New flags: - `--status ` — filter by a single status. - `--all` — include closed (show all statuses).