diff options
Diffstat (limited to 'src/adapters')
| -rw-r--r-- | src/adapters/ws/index.ts | 5 | ||||
| -rw-r--r-- | src/adapters/ws/logic.test.ts | 60 | ||||
| -rw-r--r-- | src/adapters/ws/logic.ts | 21 |
3 files changed, 86 insertions, 0 deletions
diff --git a/src/adapters/ws/index.ts b/src/adapters/ws/index.ts index 4dc0104..76d9e75 100644 --- a/src/adapters/ws/index.ts +++ b/src/adapters/ws/index.ts @@ -4,6 +4,7 @@ import type { ConversationCompactedMessage, ConversationOpenMessage, ConversationStatusChangedMessage, + ConversationTabGroupChangedMessage, ExtensionsChangedMessage, LayoutChangedMessage, TileLayoutChangedMessage, @@ -30,6 +31,8 @@ export interface SurfaceSocketOptions { onConversationStatusChanged?: (msg: ConversationStatusChangedMessage) => void; /** Broadcast when a conversation's history has been compacted (reload needed). */ onConversationCompacted?: (msg: ConversationCompactedMessage) => void; + /** Broadcast when a conversation's (visual) tab group parent changes. */ + onConversationTabGroupChanged?: (msg: ConversationTabGroupChangedMessage) => void; /** * Broadcast after every plugin-manager mutation (plan 05 §11): every * connected frontend refetches its extension list (the server re-pushes the @@ -93,6 +96,8 @@ export function createSurfaceSocket(opts: SurfaceSocketOptions): SurfaceSocketHa opts.onConversationOpen?.(msg as ConversationOpenMessage); } else if (msg.type === "conversation.statusChanged") { opts.onConversationStatusChanged?.(msg as ConversationStatusChangedMessage); + } else if (msg.type === "conversation.tabGroupChanged") { + opts.onConversationTabGroupChanged?.(msg as ConversationTabGroupChangedMessage); } else if (msg.type === "conversation.compacted") { opts.onConversationCompacted?.(msg as ConversationCompactedMessage); } else if (msg.type === "extensions-changed") { diff --git a/src/adapters/ws/logic.test.ts b/src/adapters/ws/logic.test.ts index 8fe2b5f..2179ad0 100644 --- a/src/adapters/ws/logic.test.ts +++ b/src/adapters/ws/logic.test.ts @@ -502,3 +502,63 @@ describe("parseServerMessage — layout (plan 02)", () => { expect(parseServerMessage(JSON.stringify({ type: "layout.unknown" }))).toBeNull(); }); }); + +describe("parseServerMessage — subagent tab groups", () => { + it("forwards the effective tabGroupParentId on conversation.open", () => { + const result = parseServerMessage( + JSON.stringify({ + type: "conversation.open", + conversationId: "child", + workspaceId: "w1", + tabGroupParentId: "parent", + }), + ); + expect(result).toEqual({ + type: "conversation.open", + conversationId: "child", + workspaceId: "w1", + tabGroupParentId: "parent", + }); + }); + + it("omits a non-string/empty tabGroupParentId on conversation.open", () => { + for (const tabGroupParentId of ["", 42, null]) { + const result = parseServerMessage( + JSON.stringify({ + type: "conversation.open", + conversationId: "c1", + workspaceId: "w1", + tabGroupParentId, + }), + ); + expect(result).toEqual({ + type: "conversation.open", + conversationId: "c1", + workspaceId: "w1", + }); + } + }); + + it("parses conversation.tabGroupChanged with a parent id and with null (ungrouped)", () => { + for (const tabGroupParentId of ["parent", null]) { + const msg = { + type: "conversation.tabGroupChanged", + conversationId: "c1", + workspaceId: "w1", + tabGroupParentId, + }; + expect(parseServerMessage(JSON.stringify(msg))).toEqual(msg); + } + }); + + it("rejects a malformed conversation.tabGroupChanged", () => { + const base = { type: "conversation.tabGroupChanged", conversationId: "c1", workspaceId: "w1" }; + expect(parseServerMessage(JSON.stringify(base))).toBeNull(); // missing (undefined) parent + expect(parseServerMessage(JSON.stringify({ ...base, tabGroupParentId: "" }))).toBeNull(); + expect(parseServerMessage(JSON.stringify({ ...base, tabGroupParentId: 1 }))).toBeNull(); + const { workspaceId: _w, ...noWorkspace } = base; + expect( + parseServerMessage(JSON.stringify({ ...noWorkspace, tabGroupParentId: "p" })), + ).toBeNull(); + }); +}); diff --git a/src/adapters/ws/logic.ts b/src/adapters/ws/logic.ts index 4dbd173..cecc66c 100644 --- a/src/adapters/ws/logic.ts +++ b/src/adapters/ws/logic.ts @@ -4,6 +4,7 @@ import type { ConversationCompactedMessage, ConversationOpenMessage, ConversationStatusChangedMessage, + ConversationTabGroupChangedMessage, ExtensionsChangedMessage, LayoutChangedMessage, TileLayoutChangedMessage, @@ -27,6 +28,7 @@ const VALID_SERVER_TYPES = new Set([ "conversation.open", "conversation.statusChanged", "conversation.compacted", + "conversation.tabGroupChanged", "extensions-changed", "layout", "tile-layout", @@ -126,11 +128,30 @@ export function parseServerMessage(data: string): WsServerMessage | null { // it, so a client can stamp the opened tab with the conversation's ACTUAL // model (fix-toolcall-duplicate-keys plan Part 4). const model = parsed.model; + // The EFFECTIVE tab group parent, so a summoned subagent's tab lands in + // its summoner's group immediately. + const tabGroupParentId = parsed.tabGroupParentId; const msg: ConversationOpenMessage = { type: "conversation.open", conversationId: parsed.conversationId, workspaceId: parsed.workspaceId, ...(typeof model === "string" && model.length > 0 ? { model } : {}), + ...(typeof tabGroupParentId === "string" && tabGroupParentId.length > 0 + ? { tabGroupParentId } + : {}), + }; + return msg; + } + case "conversation.tabGroupChanged": { + if (typeof parsed.conversationId !== "string") return null; + if (typeof parsed.workspaceId !== "string") return null; + const parent = parsed.tabGroupParentId; + if (parent !== null && (typeof parent !== "string" || parent.length === 0)) return null; + const msg: ConversationTabGroupChangedMessage = { + type: "conversation.tabGroupChanged", + conversationId: parsed.conversationId, + workspaceId: parsed.workspaceId, + tabGroupParentId: parent, }; return msg; } |
