diff --git a/packages/kilo-vscode/src/services/cli-backend/sse-client.ts b/packages/kilo-vscode/src/services/cli-backend/sse-client.ts index 69d0a45615d..ed80b4c5977 100644 --- a/packages/kilo-vscode/src/services/cli-backend/sse-client.ts +++ b/packages/kilo-vscode/src/services/cli-backend/sse-client.ts @@ -1,5 +1,6 @@ import EventSource from "eventsource" import type { ServerConfig, SSEEvent } from "./types" +import { unwrapSSEPayload } from "./sse-utils" // Type definitions for handlers export type SSEEventHandler = (event: SSEEvent) => void @@ -67,9 +68,8 @@ export class SSEClient { console.log("[Kilo New] SSE: 📨 Received message event:", messageEvent.data) try { const raw = JSON.parse(messageEvent.data) - // Global endpoint wraps events as { directory, payload: { type, properties } } - const event = (raw.payload ?? raw) as SSEEvent - if (!event.type) { + const event = unwrapSSEPayload(raw) + if (!event) { console.warn("[Kilo New] SSE: ⚠️ Received event without type:", raw) return } diff --git a/packages/kilo-vscode/src/services/cli-backend/sse-utils.ts b/packages/kilo-vscode/src/services/cli-backend/sse-utils.ts new file mode 100644 index 00000000000..0d77a41f995 --- /dev/null +++ b/packages/kilo-vscode/src/services/cli-backend/sse-utils.ts @@ -0,0 +1,16 @@ +import type { SSEEvent } from "./types" + +/** + * Unwrap an SSE message payload. + * The global /global/event endpoint wraps events as { directory, payload: SSEEvent }. + * Direct event endpoints return the SSEEvent directly. + * Returns null if the parsed data has no `type` field (malformed or unknown event). + */ +export function unwrapSSEPayload(raw: unknown): SSEEvent | null { + if (!raw || typeof raw !== "object") return null + const event = ((raw as { payload?: SSEEvent }).payload ?? raw) as SSEEvent + if (!event || typeof event !== "object" || !("type" in event)) { + return null + } + return event +} diff --git a/packages/kilo-vscode/tests/unit/sse-client-utils.test.ts b/packages/kilo-vscode/tests/unit/sse-client-utils.test.ts new file mode 100644 index 00000000000..1670d25e8aa --- /dev/null +++ b/packages/kilo-vscode/tests/unit/sse-client-utils.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from "bun:test" +import { unwrapSSEPayload } from "../../src/services/cli-backend/sse-utils" + +describe("unwrapSSEPayload", () => { + it("unwraps global endpoint payload wrapper", () => { + const raw = { + directory: "/workspace", + payload: { type: "session.created", properties: { info: {} } }, + } + const event = unwrapSSEPayload(raw) + expect(event?.type).toBe("session.created") + }) + + it("returns direct event when no payload wrapper", () => { + const raw = { type: "server.connected", properties: {} } + const event = unwrapSSEPayload(raw) + expect(event?.type).toBe("server.connected") + }) + + it("returns null when no type field in direct event", () => { + const raw = { properties: {} } + expect(unwrapSSEPayload(raw)).toBeNull() + }) + + it("returns null when payload wrapper exists but has no type", () => { + const raw = { directory: "/workspace", payload: { properties: {} } } + expect(unwrapSSEPayload(raw)).toBeNull() + }) + + it("returns null for null input", () => { + expect(unwrapSSEPayload(null)).toBeNull() + }) + + it("returns null for empty object", () => { + expect(unwrapSSEPayload({})).toBeNull() + }) + + it("returns null for non-object input", () => { + expect(unwrapSSEPayload("string")).toBeNull() + expect(unwrapSSEPayload(42)).toBeNull() + }) + + it("uses payload over root when both have type", () => { + const raw = { + type: "root-type", + payload: { type: "payload-type", properties: {} }, + } + const event = unwrapSSEPayload(raw) + expect(event?.type).toBe("payload-type") + }) + + it("handles nested event types correctly", () => { + const raw = { + payload: { + type: "message.updated", + properties: { + info: { id: "m1", sessionID: "s1", role: "assistant", time: { created: 0 } }, + }, + }, + } + const event = unwrapSSEPayload(raw) + expect(event?.type).toBe("message.updated") + }) +})