diff --git a/packages/ui/src/lib/openchamberEvents.test.ts b/packages/ui/src/lib/openchamberEvents.test.ts index e24e4b5e..0667eb89 100644 --- a/packages/ui/src/lib/openchamberEvents.test.ts +++ b/packages/ui/src/lib/openchamberEvents.test.ts @@ -1,12 +1,4 @@ -import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'; - -mock.module('./runtime-url', () => ({ - getRuntimeUrlResolver: () => ({ sse: (path: string) => `http://runtime.test${path}` }), -})); - -mock.module('./runtime-switch', () => ({ - subscribeRuntimeEndpointChanged: () => () => undefined, -})); +import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; class MockEventSource { static CLOSED = 2; @@ -29,20 +21,37 @@ class MockEventSource { describe('openchamber events', () => { beforeEach(() => { MockEventSource.instances = []; - globalThis.window = {} as Window & typeof globalThis; - globalThis.EventSource = MockEventSource as unknown as typeof EventSource; + Object.defineProperty(globalThis, 'window', { + value: Object.assign(new EventTarget(), { location: new URL('http://runtime.test') }), + configurable: true, + writable: true, + }); + Object.defineProperty(globalThis, 'EventSource', { value: MockEventSource, configurable: true, writable: true }); }); afterEach(() => { - delete (globalThis as { window?: unknown }).window; - delete (globalThis as { EventSource?: unknown }).EventSource; + Reflect.deleteProperty(globalThis, 'window'); + Reflect.deleteProperty(globalThis, 'EventSource'); + }); + + test('does not open the server-only event stream in VS Code', async () => { + Object.defineProperty(window, '__VSCODE_CONFIG__', { + value: { workspaceFolder: 'C:/repo', workspaceFolders: [] }, + configurable: true, + }); + const { subscribeOpenchamberEvents } = await import('./openchamberEvents'); + const unsubscribe = subscribeOpenchamberEvents(() => undefined); + try { + expect(MockEventSource.instances).toHaveLength(0); + } finally { + unsubscribe(); + } }); test('dispatches externally created session events', async () => { const { subscribeOpenchamberEvents } = await import('./openchamberEvents'); const events: unknown[] = []; - const listener = (event: unknown) => events.push(event); - const unsubscribe = subscribeOpenchamberEvents(listener); + const unsubscribe = subscribeOpenchamberEvents((event) => events.push(event)); const source = MockEventSource.instances[0]; source.onmessage?.({ diff --git a/packages/ui/src/lib/openchamberEvents.ts b/packages/ui/src/lib/openchamberEvents.ts index 9baa6020..60650884 100644 --- a/packages/ui/src/lib/openchamberEvents.ts +++ b/packages/ui/src/lib/openchamberEvents.ts @@ -1,5 +1,6 @@ import { getRuntimeUrlResolver } from './runtime-url'; import { subscribeRuntimeEndpointChanged } from './runtime-switch'; +import { isVSCodeRuntime } from './desktop'; type ScheduledTaskRanEvent = { type: 'scheduled-task-ran'; @@ -284,6 +285,10 @@ const cleanupRuntimeChangeSubscription = () => { }; export const subscribeOpenchamberEvents = (listener: Listener): (() => void) => { + // VS Code runs OpenCode through its bridge, not the OpenChamber server that + // owns this stream. Opening it here retries against vscode-webview:// forever. + if (isVSCodeRuntime()) return () => undefined; + listeners.add(listener); ensureRuntimeChangeSubscription(); connect(); diff --git a/packages/vscode/src/DOCUMENTATION.md b/packages/vscode/src/DOCUMENTATION.md index 89bfa666..b02d1977 100644 --- a/packages/vscode/src/DOCUMENTATION.md +++ b/packages/vscode/src/DOCUMENTATION.md @@ -103,6 +103,13 @@ The webview build emits each worker as one self-contained file. VS Code webviews Message and part ordering is owned by [`packages/ui/src/sync/DOCUMENTATION.md`](../../ui/src/sync/DOCUMENTATION.md#session-message-loading). The VS Code webview consumes that shared sync implementation; bridge and proxy runtimes pass OpenCode records through without adding runtime-specific ordering. +The OpenChamber control stream (`/api/openchamber/events`) requires the +OpenChamber server, which the extension does not run. `subscribeOpenchamberEvents` +therefore returns a no-op subscription in VS Code before resolving URLs or +opening a connection. Session sync still uses the OpenCode SSE bridge and +global session polling. Sending the control stream to the webview origin caused +repeated `403` responses and URL-token requests to `/auth/url-token`. + ## Extension guideline When adding new bridge route families: