diff options
| author | Adam Malczewski <[email protected]> | 2026-06-21 21:47:24 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-21 21:47:24 +0900 |
| commit | fd81987fcec0178ae2c466800b428e1b1dfc4ab0 (patch) | |
| tree | 646e39ed43c64f763721553ba7a7821d62730df8 /src/adapters | |
| parent | 90ab92626555bb6a764a3c15fc03ac3e36966226 (diff) | |
| download | dispatch-web-fd81987fcec0178ae2c466800b428e1b1dfc4ab0.tar.gz dispatch-web-fd81987fcec0178ae2c466800b428e1b1dfc4ab0.zip | |
feat(ws): handle conversation.open broadcast — open/focus tab from CLI --open
Consume the conversation.open handoff ([email protected], [email protected]).
Re-pinned file: deps + re-mirrored .dispatch/*.reference.md.
- WS adapter (logic.ts + index.ts): parse + route the new top-level
"conversation.open" WsServerMessage to an onConversationOpen handler
- app store: openConversation(id) opens (or focuses) a tab — creates a chat
store, loads history, subscribes to live turns, creates+selects the tab
- conformance guard + WS adapter tests cover the new type
- backend also shipped conversation metadata endpoints (GET /conversations,
GET /conversations/:id/last, GET/PUT /conversations/:id/title) — mirrored
but not yet consumed by the FE
682 tests green.
Diffstat (limited to 'src/adapters')
| -rw-r--r-- | src/adapters/ws/index.test.ts | 24 | ||||
| -rw-r--r-- | src/adapters/ws/index.ts | 5 | ||||
| -rw-r--r-- | src/adapters/ws/logic.test.ts | 16 | ||||
| -rw-r--r-- | src/adapters/ws/logic.ts | 10 |
4 files changed, 55 insertions, 0 deletions
diff --git a/src/adapters/ws/index.test.ts b/src/adapters/ws/index.test.ts index 961f919..e13f123 100644 --- a/src/adapters/ws/index.test.ts +++ b/src/adapters/ws/index.test.ts @@ -269,6 +269,30 @@ describe("createSurfaceSocket", () => { expect(onMessage).not.toHaveBeenCalled(); }); + it("routes conversation.open to onConversationOpen", () => { + const ws = fakeSocket(); + const onMessage = vi.fn(); + const onChat = vi.fn(); + const onConversationOpen = vi.fn(); + createSurfaceSocket({ + url: "ws://test", + onMessage, + onChat, + onConversationOpen, + socketFactory: () => ws, + }); + + ws.resolveOpen(); + ws.invokeMessage(JSON.stringify({ type: "conversation.open", conversationId: "c1" })); + expect(onConversationOpen).toHaveBeenCalledOnce(); + expect(onConversationOpen).toHaveBeenCalledWith({ + type: "conversation.open", + conversationId: "c1", + }); + expect(onMessage).not.toHaveBeenCalled(); + expect(onChat).not.toHaveBeenCalled(); + }); + it("still routes surface catalog/surface to onMessage", () => { const ws = fakeSocket(); const onMessage = vi.fn(); diff --git a/src/adapters/ws/index.ts b/src/adapters/ws/index.ts index 54a501c..18ebdf7 100644 --- a/src/adapters/ws/index.ts +++ b/src/adapters/ws/index.ts @@ -1,6 +1,7 @@ import type { ChatDeltaMessage, ChatErrorMessage, + ConversationOpenMessage, WsClientMessage, } from "@dispatch/transport-contract"; import type { SurfaceServerMessage } from "@dispatch/ui-contract"; @@ -18,6 +19,8 @@ export interface SurfaceSocketOptions { url: string; onMessage: (msg: SurfaceServerMessage) => void; onChat?: (msg: ChatDeltaMessage | ChatErrorMessage) => void; + /** Broadcast when a conversation is "opened" (e.g. CLI `--open` flag). */ + onConversationOpen?: (msg: ConversationOpenMessage) => void; onReopen?: () => void; socketFactory?: (url: string) => WebSocketLike; } @@ -60,6 +63,8 @@ export function createSurfaceSocket(opts: SurfaceSocketOptions): SurfaceSocketHa if (msg !== null) { if (msg.type === "chat.delta" || msg.type === "chat.error") { opts.onChat?.(msg as ChatDeltaMessage | ChatErrorMessage); + } else if (msg.type === "conversation.open") { + opts.onConversationOpen?.(msg as ConversationOpenMessage); } else { opts.onMessage(msg as SurfaceServerMessage); } diff --git a/src/adapters/ws/logic.test.ts b/src/adapters/ws/logic.test.ts index 2784295..ca129c0 100644 --- a/src/adapters/ws/logic.test.ts +++ b/src/adapters/ws/logic.test.ts @@ -217,6 +217,22 @@ describe("parseServerMessage", () => { ), ).toBeNull(); }); + + it("parses a conversation.open message", () => { + const data = JSON.stringify({ type: "conversation.open", conversationId: "c1" }); + const result = parseServerMessage(data); + expect(result).toEqual({ type: "conversation.open", conversationId: "c1" }); + }); + + it("returns null for conversation.open with missing conversationId", () => { + expect(parseServerMessage(JSON.stringify({ type: "conversation.open" }))).toBeNull(); + }); + + it("returns null for conversation.open with non-string conversationId", () => { + expect( + parseServerMessage(JSON.stringify({ type: "conversation.open", conversationId: 42 })), + ).toBeNull(); + }); }); describe("round-trip: parseServerMessage(serialize(...))", () => { diff --git a/src/adapters/ws/logic.ts b/src/adapters/ws/logic.ts index 17e3951..a9b70ff 100644 --- a/src/adapters/ws/logic.ts +++ b/src/adapters/ws/logic.ts @@ -1,6 +1,7 @@ import type { ChatDeltaMessage, ChatErrorMessage, + ConversationOpenMessage, WsClientMessage, WsServerMessage, } from "@dispatch/transport-contract"; @@ -18,6 +19,7 @@ const VALID_SERVER_TYPES = new Set([ "error", "chat.delta", "chat.error", + "conversation.open", ]); /** Serialize a client message to a JSON string for the wire. */ @@ -107,6 +109,14 @@ export function parseServerMessage(data: string): WsServerMessage | null { : { type: "chat.error", message: parsed.message }; return msg; } + case "conversation.open": { + if (typeof parsed.conversationId !== "string") return null; + const msg: ConversationOpenMessage = { + type: "conversation.open", + conversationId: parsed.conversationId, + }; + return msg; + } default: return null; } |
