fix(vscode): skip unsupported OpenChamber event stream

Shared session-list subscriptions opened server-only event and URL-token routes against the webview origin, causing repeated 403 responses. Skip the control-stream subscription in VS Code while preserving OpenCode bridge sync and polling.

Validated with regression tests, UI type-check and lint, and the VS Code webview build. The reported gray-screen crash remains unconfirmed pending user logs.
This commit is contained in:
Bohdan Triapitsyn
2026-09-09 17:41:14 +03:00
parent 29b9588a51
commit ec7db447bd
3 changed files with 36 additions and 15 deletions
+24 -15
View File
@@ -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?.({
+5
View File
@@ -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();
+7
View File
@@ -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: