From 432f3bb9dfaa2ba1f75f8d0b842de154efd6559c Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 9 Sep 2026 17:04:02 +0300 Subject: [PATCH] fix(vscode): avoid navigation reload on chunk load failure Browser-style chunk recovery used location.reload inside VS Code webviews, where navigation reload is unsupported. Preserve import retries and let the original failure reach the error boundary instead. Validated with focused regression tests, UI type-check and lint, and the VS Code webview build. The reported gray-screen crash has not been reproduced. --- packages/ui/src/lib/chunkLoadRecovery.test.ts | 35 +++++++++++++++++++ packages/ui/src/lib/chunkLoadRecovery.ts | 4 +++ packages/vscode/src/DOCUMENTATION.md | 4 +++ 3 files changed, 43 insertions(+) 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: