summaryrefslogtreecommitdiffhomepage
path: root/src/adapters
diff options
context:
space:
mode:
Diffstat (limited to 'src/adapters')
-rw-r--r--src/adapters/ws/index.ts9
-rw-r--r--src/adapters/ws/logic.test.ts33
-rw-r--r--src/adapters/ws/logic.ts18
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;
}