From ccb74d83662beee23ffbc79127dc787c5e171617 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 27 Jul 2026 10:58:33 +0000 Subject: [PATCH 1/2] fix(ui): detect VS Code from bootstrap config in shared runtime helpers d2efa707 fixed projects-store detection via __VSCODE_CONFIG__, but lib/desktop.isVSCodeRuntime (used by useDirectoryStore) still required RuntimeAPIs. At webview startup that left directory init on stale localStorage paths from other windows (#2359). Share bootstrap detection in lib/vscodeBootstrap and use it from both desktop runtime checks and the projects-store helper. Co-authored-by: Serhii Dziupin --- packages/ui/src/lib/desktop.ts | 7 +++ .../ui/src/lib/desktop.vscodeRuntime.test.ts | 46 +++++++++++++++++++ packages/ui/src/lib/vscodeBootstrap.test.ts | 29 ++++++++++++ packages/ui/src/lib/vscodeBootstrap.ts | 20 ++++++++ packages/ui/src/stores/useDirectoryStore.ts | 3 +- .../ui/src/stores/utils/vscodeRuntime.test.ts | 8 ++++ packages/ui/src/stores/utils/vscodeRuntime.ts | 22 ++++----- 7 files changed, 121 insertions(+), 14 deletions(-) create mode 100644 packages/ui/src/lib/desktop.vscodeRuntime.test.ts create mode 100644 packages/ui/src/lib/vscodeBootstrap.test.ts create mode 100644 packages/ui/src/lib/vscodeBootstrap.ts diff --git a/packages/ui/src/lib/desktop.ts b/packages/ui/src/lib/desktop.ts index d8b41983..27ccb18b 100644 --- a/packages/ui/src/lib/desktop.ts +++ b/packages/ui/src/lib/desktop.ts @@ -4,6 +4,7 @@ import type { DraftStarterRef } from '@/lib/draftStarters'; import type { MobileKeyboardMode } from '@/lib/mobileKeyboardMode'; import { getRuntimeApiBaseUrl, getRuntimeKey } from '@/lib/runtime-switch'; import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; +import { isVSCodeBootstrapPresent } from '@/lib/vscodeBootstrap'; type ManagedRemoteTunnelPreset = { id: string; @@ -538,6 +539,12 @@ export const startDesktopWindowDrag = async (): Promise => { }; export const isVSCodeRuntime = (): boolean => { + // Prefer extension-host bootstrap config: it is injected in webview HTML + // before any store module evaluates, so startup does not depend on + // RuntimeAPIs registration order (see #2359). + if (isVSCodeBootstrapPresent()) { + return true; + } const apis = getRegisteredRuntimeAPIs(); return apis?.runtime?.isVSCode === true; }; diff --git a/packages/ui/src/lib/desktop.vscodeRuntime.test.ts b/packages/ui/src/lib/desktop.vscodeRuntime.test.ts new file mode 100644 index 00000000..fb02417f --- /dev/null +++ b/packages/ui/src/lib/desktop.vscodeRuntime.test.ts @@ -0,0 +1,46 @@ +import { afterEach, describe, expect, mock, test } from 'bun:test'; + +type RuntimeApisStub = { runtime?: { isVSCode?: boolean } } | null; + +let registeredRuntimeApis: RuntimeApisStub = null; + +mock.module('@/contexts/runtimeAPIRegistry', () => ({ + getRegisteredRuntimeAPIs: (): RuntimeApisStub => registeredRuntimeApis, +})); + +const { isVSCodeRuntime } = await import('./desktop'); + +describe('desktop isVSCodeRuntime bootstrap detection', () => { + afterEach(() => { + registeredRuntimeApis = null; + delete (globalThis as { window?: unknown }).window; + }); + + test('detects VS Code from bootstrap config before RuntimeAPIs register', () => { + registeredRuntimeApis = null; + (globalThis as { window: unknown }).window = { + __VSCODE_CONFIG__: { + workspaceFolder: '/Users/me/project-a', + workspaceFolders: [{ name: 'project-a', path: '/Users/me/project-a' }], + }, + }; + + expect(isVSCodeRuntime()).toBe(true); + }); + + test('falls back to registered RuntimeAPIs when bootstrap is absent', () => { + registeredRuntimeApis = { + runtime: { isVSCode: true }, + }; + (globalThis as { window: unknown }).window = {}; + + expect(isVSCodeRuntime()).toBe(true); + }); + + test('does not classify an unregistered web runtime as VS Code', () => { + registeredRuntimeApis = null; + (globalThis as { window: unknown }).window = {}; + + expect(isVSCodeRuntime()).toBe(false); + }); +}); diff --git a/packages/ui/src/lib/vscodeBootstrap.test.ts b/packages/ui/src/lib/vscodeBootstrap.test.ts new file mode 100644 index 00000000..3e67c3b8 --- /dev/null +++ b/packages/ui/src/lib/vscodeBootstrap.test.ts @@ -0,0 +1,29 @@ +import { afterEach, describe, expect, test } from 'bun:test'; +import { getVSCodeBootstrapConfig, isVSCodeBootstrapPresent } from './vscodeBootstrap'; + +describe('VS Code bootstrap config', () => { + afterEach(() => { + delete (globalThis as { window?: unknown }).window; + }); + + test('reads extension-host __VSCODE_CONFIG__ before RuntimeAPIs exist', () => { + (globalThis as { window: unknown }).window = { + __VSCODE_CONFIG__: { + workspaceFolder: '/workspace/project-one', + workspaceFolders: [{ name: 'project-one', path: '/workspace/project-one' }], + }, + }; + + expect(getVSCodeBootstrapConfig()).toEqual({ + workspaceFolder: '/workspace/project-one', + workspaceFolders: [{ name: 'project-one', path: '/workspace/project-one' }], + }); + expect(isVSCodeBootstrapPresent()).toBe(true); + }); + + test('treats missing window/bootstrap as not VS Code', () => { + expect(getVSCodeBootstrapConfig()).toBeNull(); + expect(isVSCodeBootstrapPresent()).toBe(false); + expect(isVSCodeBootstrapPresent(null)).toBe(false); + }); +}); diff --git a/packages/ui/src/lib/vscodeBootstrap.ts b/packages/ui/src/lib/vscodeBootstrap.ts new file mode 100644 index 00000000..1c9e7a8b --- /dev/null +++ b/packages/ui/src/lib/vscodeBootstrap.ts @@ -0,0 +1,20 @@ +/** + * Extension-host bootstrap config injected into the VS Code webview HTML + * before any bundled module evaluates. Prefer this over RuntimeAPIs for + * early VS Code detection during store module initialization. + */ +export interface VSCodeBootstrapConfig { + workspaceFolder?: unknown; + workspaceFolders?: unknown; +} + +export const getVSCodeBootstrapConfig = (): VSCodeBootstrapConfig | null => { + if (typeof window === 'undefined') { + return null; + } + return (window as unknown as { __VSCODE_CONFIG__?: VSCodeBootstrapConfig }).__VSCODE_CONFIG__ ?? null; +}; + +export const isVSCodeBootstrapPresent = ( + bootstrapConfig: VSCodeBootstrapConfig | null = getVSCodeBootstrapConfig(), +): boolean => Boolean(bootstrapConfig); diff --git a/packages/ui/src/stores/useDirectoryStore.ts b/packages/ui/src/stores/useDirectoryStore.ts index b0b32af5..865fa97a 100644 --- a/packages/ui/src/stores/useDirectoryStore.ts +++ b/packages/ui/src/stores/useDirectoryStore.ts @@ -2,6 +2,7 @@ import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import { opencodeClient } from '@/lib/opencode/client'; import { getDesktopHomeDirectory, isVSCodeRuntime } from '@/lib/desktop'; +import { getVSCodeBootstrapConfig } from '@/lib/vscodeBootstrap'; import { subscribeRuntimeEndpointChanged } from '@/lib/runtime-switch'; import { updateDesktopSettings } from '@/lib/persistence'; import { useFileSearchStore } from '@/stores/useFileSearchStore'; @@ -227,7 +228,7 @@ const getVsCodeWorkspaceFolder = (): string | null => { if (!isVSCodeRuntime()) { return null; } - const workspaceFolder = (window as unknown as { __VSCODE_CONFIG__?: { workspaceFolder?: unknown } }).__VSCODE_CONFIG__?.workspaceFolder; + const workspaceFolder = getVSCodeBootstrapConfig()?.workspaceFolder; if (typeof workspaceFolder !== 'string' || workspaceFolder.trim().length === 0) { return null; } diff --git a/packages/ui/src/stores/utils/vscodeRuntime.test.ts b/packages/ui/src/stores/utils/vscodeRuntime.test.ts index aebe538c..a1cb518f 100644 --- a/packages/ui/src/stores/utils/vscodeRuntime.test.ts +++ b/packages/ui/src/stores/utils/vscodeRuntime.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'bun:test'; +import type { RuntimeAPIs } from '@/lib/api/types'; import { isVSCodeRuntime } from './vscodeRuntime'; describe('VS Code runtime detection', () => { @@ -9,6 +10,13 @@ describe('VS Code runtime detection', () => { })).toBe(true); }); + test('uses registered runtime APIs when bootstrap is absent', () => { + const runtimeApis = { + runtime: { platform: 'vscode', isDesktop: false, isVSCode: true }, + } as RuntimeAPIs; + expect(isVSCodeRuntime(runtimeApis, null)).toBe(true); + }); + test('does not classify an unregistered web runtime as VS Code', () => { expect(isVSCodeRuntime(null, null)).toBe(false); }); diff --git a/packages/ui/src/stores/utils/vscodeRuntime.ts b/packages/ui/src/stores/utils/vscodeRuntime.ts index 91446ce6..2e7a5d7b 100644 --- a/packages/ui/src/stores/utils/vscodeRuntime.ts +++ b/packages/ui/src/stores/utils/vscodeRuntime.ts @@ -1,18 +1,14 @@ import type { RuntimeAPIs } from '@/lib/api/types'; +import { + getVSCodeBootstrapConfig, + isVSCodeBootstrapPresent, + type VSCodeBootstrapConfig, +} from '@/lib/vscodeBootstrap'; -export interface VSCodeBootstrapConfig { - workspaceFolder?: unknown; - workspaceFolders?: unknown; -} - -export const getVSCodeBootstrapConfig = (): VSCodeBootstrapConfig | null => { - if (typeof window === 'undefined') { - return null; - } - return (window as unknown as { __VSCODE_CONFIG__?: VSCodeBootstrapConfig }).__VSCODE_CONFIG__ ?? null; -}; +export type { VSCodeBootstrapConfig }; +export { getVSCodeBootstrapConfig }; export const isVSCodeRuntime = ( runtimeApis: RuntimeAPIs | null, - bootstrapConfig = getVSCodeBootstrapConfig(), -): boolean => Boolean(bootstrapConfig || runtimeApis?.runtime?.isVSCode); + bootstrapConfig: VSCodeBootstrapConfig | null = getVSCodeBootstrapConfig(), +): boolean => Boolean(isVSCodeBootstrapPresent(bootstrapConfig) || runtimeApis?.runtime?.isVSCode); From 696a381d3af7686c6b83a49270893bfe15154433 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 27 Jul 2026 11:36:48 +0000 Subject: [PATCH 2/2] test(ui): cover VS Code store init before RuntimeAPIs registration Adds focused regression coverage for #2359 bootstrap-only detection used by directory/projects startup when RuntimeAPIs are not registered yet. Co-authored-by: Serhii Dziupin --- .../src/stores/vscodeStoreInit.2359.test.ts | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 packages/ui/src/stores/vscodeStoreInit.2359.test.ts diff --git a/packages/ui/src/stores/vscodeStoreInit.2359.test.ts b/packages/ui/src/stores/vscodeStoreInit.2359.test.ts new file mode 100644 index 00000000..6571c162 --- /dev/null +++ b/packages/ui/src/stores/vscodeStoreInit.2359.test.ts @@ -0,0 +1,86 @@ +import { afterEach, describe, expect, mock, test } from 'bun:test'; + +/** + * Integration-style coverage for #2359: store modules evaluate before + * RuntimeAPIs registration, with only extension-host __VSCODE_CONFIG__ present + * and a stale lastDirectory in storage. + */ + +const WORKSPACE = '/tmp/oc-ws-project-a'; +const STALE = '/tmp/oc-ws-other'; + +const storage = new Map([ + ['lastDirectory', STALE], + ['homeDirectory', STALE], +]); + +const installWindow = () => { + (globalThis as { window: unknown }).window = { + __VSCODE_CONFIG__: { + workspaceFolder: WORKSPACE, + workspaceFolders: [{ name: 'oc-ws-project-a', path: WORKSPACE }], + }, + __OPENCHAMBER_HOME__: WORKSPACE, + localStorage: { + getItem: (key: string) => storage.get(key) ?? null, + setItem: (key: string, value: string) => { + storage.set(key, String(value)); + }, + removeItem: (key: string) => { + storage.delete(key); + }, + }, + matchMedia: () => ({ matches: false, addListener() {}, removeListener() {}, addEventListener() {}, removeEventListener() {} }), + }; + (globalThis as { localStorage: unknown }).localStorage = (globalThis as { window: { localStorage: unknown } }).window.localStorage; +}; + +mock.module('@/contexts/runtimeAPIRegistry', () => ({ + getRegisteredRuntimeAPIs: () => null, +})); + +mock.module('@/lib/opencode/client', () => ({ + opencodeClient: { + setDirectory: () => undefined, + getDirectory: () => WORKSPACE, + getFilesystemHome: async () => WORKSPACE, + getSystemInfo: async () => ({ homeDirectory: WORKSPACE }), + }, +})); + +mock.module('@/lib/persistence', () => ({ + updateDesktopSettings: async () => undefined, +})); + +mock.module('@/lib/runtime-switch', () => ({ + subscribeRuntimeEndpointChanged: () => () => undefined, + getRuntimeApiBaseUrl: () => 'http://127.0.0.1:9', + getRuntimeKey: () => 'test', +})); + +mock.module('@/stores/useFileSearchStore', () => ({ + useFileSearchStore: { + getState: () => ({ clearCache: () => undefined }), + }, +})); + +describe('VS Code store init before RuntimeAPIs (#2359)', () => { + afterEach(() => { + delete (globalThis as { window?: unknown }).window; + delete (globalThis as { localStorage?: unknown }).localStorage; + }); + + test('desktop isVSCodeRuntime prefers bootstrap config', async () => { + installWindow(); + const { isVSCodeRuntime } = await import('@/lib/desktop'); + expect(isVSCodeRuntime()).toBe(true); + }); + + test('projects helper derives workspace projects without RuntimeAPIs', async () => { + installWindow(); + const { getVSCodeBootstrapConfig, isVSCodeRuntime } = await import('@/stores/utils/vscodeRuntime'); + const config = getVSCodeBootstrapConfig(); + expect(isVSCodeRuntime(null, config)).toBe(true); + expect(config?.workspaceFolder).toBe(WORKSPACE); + }); +});