diff options
Diffstat (limited to 'src/adapters')
| -rw-r--r-- | src/adapters/ws/index.ts | 9 | ||||
| -rw-r--r-- | src/adapters/ws/logic.test.ts | 33 | ||||
| -rw-r--r-- | src/adapters/ws/logic.ts | 18 |
3 files changed, 60 insertions, 0 deletions
diff --git a/src/adapters/ws/index.ts b/src/adapters/ws/index.ts index 1309db0..3a6a97d 100644 --- a/src/adapters/ws/index.ts +++ b/src/adapters/ws/index.ts @@ -4,6 +4,7 @@ import type { ConversationCompactedMessage, ConversationOpenMessage, ConversationStatusChangedMessage, + ExtensionsChangedMessage, WsClientMessage, } from "@dispatch/transport-contract"; import type { SurfaceServerMessage } from "@dispatch/ui-contract"; @@ -27,6 +28,12 @@ export interface SurfaceSocketOptions { onConversationStatusChanged?: (msg: ConversationStatusChangedMessage) => void; /** Broadcast when a conversation's history has been compacted (reload needed). */ onConversationCompacted?: (msg: ConversationCompactedMessage) => void; + /** + * Broadcast after every plugin-manager mutation (plan 05 §11): every + * connected frontend refetches its extension list (the server re-pushes the + * workspace-filtered surface catalog itself). + */ + onExtensionsChanged?: (msg: ExtensionsChangedMessage) => void; onReopen?: () => void; socketFactory?: (url: string) => WebSocketLike; } @@ -75,6 +82,8 @@ export function createSurfaceSocket(opts: SurfaceSocketOptions): SurfaceSocketHa opts.onConversationStatusChanged?.(msg as ConversationStatusChangedMessage); } else if (msg.type === "conversation.compacted") { opts.onConversationCompacted?.(msg as ConversationCompactedMessage); + } else if (msg.type === "extensions-changed") { + opts.onExtensionsChanged?.(msg as ExtensionsChangedMessage); } else { opts.onMessage(msg as SurfaceServerMessage); } diff --git a/src/adapters/ws/logic.test.ts b/src/adapters/ws/logic.test.ts index 43cf8ff..c4ec7e3 100644 --- a/src/adapters/ws/logic.test.ts +++ b/src/adapters/ws/logic.test.ts @@ -1,6 +1,13 @@ import { describe, expect, it } from "vitest"; import { nextBackoffMs, parseServerMessage, serialize } from "./logic"; +describe("serialize — workspace.set (plan 06 §6.4)", () => { + it("serializes a workspace.set client message", () => { + const msg = { type: "workspace.set" as const, workspaceId: "ws-1" }; + expect(JSON.parse(serialize(msg))).toEqual(msg); + }); +}); + describe("serialize", () => { it("serializes a subscribe message", () => { const msg = { type: "subscribe" as const, surfaceId: "s1" }; @@ -430,3 +437,29 @@ describe("nextBackoffMs", () => { } }); }); + +describe("parseServerMessage — extensions-changed (plan 05)", () => { + it("parses the broadcast with a pluginId", () => { + const result = parseServerMessage( + JSON.stringify({ type: "extensions-changed", action: "unload", pluginId: "mcp" }), + ); + expect(result).toEqual({ type: "extensions-changed", action: "unload", pluginId: "mcp" }); + }); + + it("parses a scan broadcast (no pluginId)", () => { + const result = parseServerMessage( + JSON.stringify({ type: "extensions-changed", action: "scan" }), + ); + expect(result).toEqual({ type: "extensions-changed", action: "scan" }); + }); + + it("rejects an unknown action", () => { + expect( + parseServerMessage(JSON.stringify({ type: "extensions-changed", action: "explode" })), + ).toBeNull(); + }); + + it("rejects a missing action", () => { + expect(parseServerMessage(JSON.stringify({ type: "extensions-changed" }))).toBeNull(); + }); +}); diff --git a/src/adapters/ws/logic.ts b/src/adapters/ws/logic.ts index 72a6338..696655b 100644 --- a/src/adapters/ws/logic.ts +++ b/src/adapters/ws/logic.ts @@ -4,6 +4,7 @@ import type { ConversationCompactedMessage, ConversationOpenMessage, ConversationStatusChangedMessage, + ExtensionsChangedMessage, WsClientMessage, WsServerMessage, } from "@dispatch/transport-contract"; @@ -24,6 +25,7 @@ const VALID_SERVER_TYPES = new Set([ "conversation.open", "conversation.statusChanged", "conversation.compacted", + "extensions-changed", ]); /** Serialize a client message to a JSON string for the wire. */ @@ -162,6 +164,22 @@ export function parseServerMessage(data: string): WsServerMessage | null { }; return msg; } + case "extensions-changed": { + // Plugin-manager broadcast (plan 05): the payload is intentionally + // minimal — clients refetch GET /plugins + the (server re-pushed) + // surface catalog rather than diffing here. + const action = parsed.action; + if (action !== "load" && action !== "unload" && action !== "reload") { + if (action !== "scan" && action !== "forget") return null; + } + const pluginId = parsed.pluginId; + const msg: ExtensionsChangedMessage = { + type: "extensions-changed", + action, + ...(pluginId !== undefined && typeof pluginId === "string" ? { pluginId } : {}), + }; + return msg; + } default: return null; } |
