summaryrefslogtreecommitdiffhomepage
path: root/src/adapters
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-21 21:47:24 +0900
committerAdam Malczewski <[email protected]>2026-06-21 21:47:24 +0900
commitfd81987fcec0178ae2c466800b428e1b1dfc4ab0 (patch)
tree646e39ed43c64f763721553ba7a7821d62730df8 /src/adapters
parent90ab92626555bb6a764a3c15fc03ac3e36966226 (diff)
downloaddispatch-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.ts24
-rw-r--r--src/adapters/ws/index.ts5
-rw-r--r--src/adapters/ws/logic.test.ts16
-rw-r--r--src/adapters/ws/logic.ts10
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;
}