summaryrefslogtreecommitdiffhomepage
path: root/.dispatch
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-22 00:36:31 +0900
committerAdam Malczewski <[email protected]>2026-06-22 00:36:31 +0900
commit54e88b71efd9a6fd9d880b6e90d844a875808662 (patch)
tree7d8292486f845225f4f03801531db2dc6ba8b7b1 /.dispatch
parenta8de5b2b9bec07a5ed5df54b859fa6ff5f98406f (diff)
downloaddispatch-web-54e88b71efd9a6fd9d880b6e90d844a875808662.tar.gz
dispatch-web-54e88b71efd9a6fd9d880b6e90d844a875808662.zip
feat(tabs): cross-device tab sync via conversation lifecycle
Consume the conversation lifecycle handoff ([email protected], [email protected]). Re-pinned file: deps + re-mirrored .dispatch/*.reference.md. - fetchOpenConversations() on connect: GET /conversations?status=active,idle restores the tab bar across devices (merges with localStorage — opens new tabs, removes closed ones, updates titles from backend) - conversation.statusChanged WS handler: closed → removeTabLocally (no re-POST); active → open tab + spinner; idle → update status map - conversation.compacted WS handler: dispose stale store + cache, reload history from server - TabBar shows a spinner on active conversations (statusFor prop) - closeTab refactored to use removeTabLocally (extracted cleanup) - conformance guards + WS adapter tests cover all 3 new WsServerMessage types 686 tests green.
Diffstat (limited to '.dispatch')
-rw-r--r--.dispatch/transport-contract.reference.md42
-rw-r--r--.dispatch/wire.reference.md19
2 files changed, 57 insertions, 4 deletions
diff --git a/.dispatch/transport-contract.reference.md b/.dispatch/transport-contract.reference.md
index d5f22c7..e599eb3 100644
--- a/.dispatch/transport-contract.reference.md
+++ b/.dispatch/transport-contract.reference.md
@@ -5,10 +5,19 @@
> hangs on a permission prompt). Your CODE still imports `@dispatch/transport-contract` normally —
> this file is for READING only.
>
-> **Orchestrator:** SNAPSHOT of `[email protected]` (conversation.open broadcast).
-> Depends on `@dispatch/[email protected]` (see `wire.reference.md`) + `@dispatch/[email protected]` (see
+> **Orchestrator:** SNAPSHOT of `[email protected]` (conversation lifecycle).
+> Depends on `@dispatch/[email protected]` (see `wire.reference.md`) + `@dispatch/[email protected]` (see
> `ui-contract.reference.md`).
>
+> **2026-06-22 delta (conversation lifecycle handoff — package bumped `0.13.0` → `0.14.0`, ADDITIVE):**
+> adds conversation lifecycle **status** (`active`/`idle`/`closed`) for cross-device tab
+> persistence. `ConversationMeta` (re-exported from `[email protected]`) gains a `status` field. New
+> WS message `ConversationStatusChangedMessage` (`{ type: "conversation.statusChanged";
+> conversationId; status }`) is broadcast to ALL clients on every status change. `GET
+> /conversations` gains an optional `?status=active,idle` filter (comma-separated; default = all).
+> `POST /conversations/:id/close` now also sets status to `closed` (persists across restarts).
+> The FE fetches `?status=active,idle` on connect to restore the tab bar across devices.
+>
> **2026-06-21 delta (conversation.open handoff — package bumped `0.12.0` → `0.13.0`, ADDITIVE):**
> adds the `conversation.open` WS broadcast — when the CLI's `--open` flag fires
> (`POST /conversations/:id/open`), the backend broadcasts a `ConversationOpenMessage`
@@ -212,6 +221,7 @@ import type { SurfaceClientMessage, SurfaceServerMessage } from "@dispatch/ui-co
import type {
AgentEvent,
ConversationMeta,
+ ConversationStatus,
QueuedMessage,
ReasoningEffort,
StoredChunk,
@@ -221,6 +231,7 @@ import type {
export type {
AgentEvent,
ConversationMeta,
+ ConversationStatus,
QueuedMessage,
ReasoningEffort,
StepMetrics,
@@ -666,7 +677,9 @@ export type WsServerMessage =
| SurfaceServerMessage
| ChatDeltaMessage
| ChatErrorMessage
- | ConversationOpenMessage;
+ | ConversationOpenMessage
+ | ConversationStatusChangedMessage
+ | ConversationCompactedMessage;
// ─── Conversation list + metadata ────────────────────────────────────────────
@@ -681,6 +694,29 @@ export interface ConversationOpenMessage {
}
/**
+ * Broadcast to all connected WS clients when a conversation's lifecycle status
+ * changes (`active`/`idle`/`closed`). The FE uses this for cross-device tab
+ * sync: `closed` → remove the tab; `active` → show a generating indicator.
+ */
+export interface ConversationStatusChangedMessage {
+ readonly type: "conversation.statusChanged";
+ readonly conversationId: string;
+ readonly status: ConversationStatus;
+}
+
+/**
+ * Broadcast to all connected WS clients when a conversation's history has been
+ * compacted (summarized). The frontend should reload the conversation history
+ * via `GET /conversations/:id` to reflect the compacted state.
+ */
+export interface ConversationCompactedMessage {
+ readonly type: "conversation.compacted";
+ readonly conversationId: string;
+ readonly messagesSummarized: number;
+ readonly messagesKept: number;
+}
+
+/**
* Response for `GET /conversations` — the list of all known conversations,
* sorted by `lastActivityAt` descending (most recent first). Each entry carries
* enough metadata for a conversation picker UI (id, title, timestamps).
diff --git a/.dispatch/wire.reference.md b/.dispatch/wire.reference.md
index e96c353..ead4d9c 100644
--- a/.dispatch/wire.reference.md
+++ b/.dispatch/wire.reference.md
@@ -4,9 +4,17 @@
> types WITHOUT following the `file:` dep symlink out of this repo (which hangs on a permission
> prompt). Your CODE still imports `@dispatch/wire` normally — this file is for READING only.
>
-> **Orchestrator:** SNAPSHOT of `[email protected]` (conversation metadata). Regenerate
+> **Orchestrator:** SNAPSHOT of `[email protected]` (conversation lifecycle status). Regenerate
> whenever `@dispatch/wire` changes.
>
+> **2026-06-22 delta (conversation lifecycle handoff — package bumped `0.9.0` → `0.10.0`, ADDITIVE):**
+> adds `ConversationStatus` (`"active" | "idle" | "closed"`) — the per-conversation lifecycle
+> status. `ConversationMeta` gains a `status` field. `active` = a turn is generating; `idle` =
+> exists, not generating; `closed` = dismissed (hidden from the tab bar). Transitions are
+> backend-owned: `idle → active` on turn start, `active → idle` on turn settle, `→ closed` on
+> `POST /conversations/:id/close`. Pushed to all WS clients via `conversation.statusChanged`
+> (see `[email protected]`).
+>
> **2026-06-21 delta (conversation.open handoff — package bumped `0.8.0` → `0.9.0`, ADDITIVE):**
> adds `ConversationMeta` — metadata for a conversation (id, title, createdAt, lastActivityAt),
> returned by `GET /conversations` (the list endpoint, see `[email protected]`).
@@ -585,6 +593,14 @@ export interface TurnSteeringEvent {
// ─── Conversation metadata ───────────────────────────────────────────────────
/**
+ * The per-conversation lifecycle status. `active` = a turn is generating;
+ * `idle` = exists, not generating; `closed` = dismissed (hidden from the tab
+ * bar, not deleted). Transitions are backend-owned and pushed via the
+ * `conversation.statusChanged` WS message (see `transport-contract`).
+ */
+export type ConversationStatus = "active" | "idle" | "closed";
+
+/**
* Metadata for a conversation, returned by `GET /conversations` (the list
* endpoint). The title defaults to the first user message (truncated) and can
* be set via `PUT /conversations/:id/title`. `createdAt` is set on first write;
@@ -595,5 +611,6 @@ export interface ConversationMeta {
readonly createdAt: number;
readonly lastActivityAt: number;
readonly title: string;
+ readonly status: ConversationStatus;
}
```