diff --git a/packages/ui/src/lib/chunkLoadRecovery.test.ts b/packages/ui/src/lib/chunkLoadRecovery.test.ts index 5dd0eea7..7d79e9dc 100644 --- a/packages/ui/src/lib/chunkLoadRecovery.test.ts +++ b/packages/ui/src/lib/chunkLoadRecovery.test.ts @@ -3,6 +3,41 @@ import { describe, expect, test } from 'bun:test'; import { importWithChunkRecovery } from './chunkLoadRecovery'; describe('importWithChunkRecovery', () => { + test('preserves the import error without navigating the VS Code webview', async () => { + const previousWindow = Object.getOwnPropertyDescriptor(globalThis, 'window'); + let reloadCount = 0; + let markerWrites = 0; + Object.defineProperty(globalThis, 'window', { + configurable: true, + writable: true, + value: { + __VSCODE_CONFIG__: { workspaceFolder: 'C:/repo', workspaceFolders: [] }, + sessionStorage: { + getItem: () => null, + setItem: () => { markerWrites += 1; }, + }, + setTimeout: (callback: () => void) => { callback(); return 0; }, + location: { reload: () => { reloadCount += 1; } }, + }, + }); + + const failure = new Error('Failed to fetch dynamically imported module'); + let attempts = 0; + try { + const caught = await importWithChunkRecovery(async () => { + attempts += 1; + throw failure; + }).catch((error: Error) => error); + expect(caught).toBe(failure); + expect(attempts).toBe(2); + expect(reloadCount).toBe(0); + expect(markerWrites).toBe(0); + } finally { + if (previousWindow) Object.defineProperty(globalThis, 'window', previousWindow); + else Reflect.deleteProperty(globalThis, 'window'); + } + }); + test('schedules recovery reload when stored reload marker is corrupt', async () => { const globalWithWindow = globalThis as unknown as { window?: unknown }; const previousWindow = globalWithWindow.window; diff --git a/packages/ui/src/lib/chunkLoadRecovery.ts b/packages/ui/src/lib/chunkLoadRecovery.ts index 6dd0a480..b948218d 100644 --- a/packages/ui/src/lib/chunkLoadRecovery.ts +++ b/packages/ui/src/lib/chunkLoadRecovery.ts @@ -1,4 +1,5 @@ import { lazy } from 'react'; +import { isVSCodeRuntime } from './desktop'; declare const __APP_VERSION__: string | undefined; @@ -42,6 +43,9 @@ function reloadMarkerSignature(error: unknown): string { function scheduleReloadOnce(error: unknown): void { if (typeof window === 'undefined') return; + // VS Code owns webview navigation. Keep the import failure available to the + // error boundary instead of replacing the app with an unsupported reload. + if (isVSCodeRuntime()) return; const now = Date.now(); const signature = reloadMarkerSignature(error); diff --git a/packages/vscode/src/DOCUMENTATION.md b/packages/vscode/src/DOCUMENTATION.md index b02d1977..21980107 100644 --- a/packages/vscode/src/DOCUMENTATION.md +++ b/packages/vscode/src/DOCUMENTATION.md @@ -110,6 +110,10 @@ 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`. +Shared lazy imports retry a failed chunk load, but skip browser-navigation +recovery in VS Code. `window.location.reload()` is unsupported inside webviews; +the original import error must reach the UI error boundary instead. + ## Extension guideline When adding new bridge route families: