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.
This commit is contained in:
Bohdan Triapitsyn
2026-09-09 17:41:15 +03:00
parent 8cb426083b
commit 432f3bb9df
3 changed files with 43 additions and 0 deletions
@@ -3,6 +3,41 @@ import { describe, expect, test } from 'bun:test';
import { importWithChunkRecovery } from './chunkLoadRecovery'; import { importWithChunkRecovery } from './chunkLoadRecovery';
describe('importWithChunkRecovery', () => { 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 () => { test('schedules recovery reload when stored reload marker is corrupt', async () => {
const globalWithWindow = globalThis as unknown as { window?: unknown }; const globalWithWindow = globalThis as unknown as { window?: unknown };
const previousWindow = globalWithWindow.window; const previousWindow = globalWithWindow.window;
+4
View File
@@ -1,4 +1,5 @@
import { lazy } from 'react'; import { lazy } from 'react';
import { isVSCodeRuntime } from './desktop';
declare const __APP_VERSION__: string | undefined; declare const __APP_VERSION__: string | undefined;
@@ -42,6 +43,9 @@ function reloadMarkerSignature(error: unknown): string {
function scheduleReloadOnce(error: unknown): void { function scheduleReloadOnce(error: unknown): void {
if (typeof window === 'undefined') return; 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 now = Date.now();
const signature = reloadMarkerSignature(error); const signature = reloadMarkerSignature(error);
+4
View File
@@ -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 global session polling. Sending the control stream to the webview origin caused
repeated `403` responses and URL-token requests to `/auth/url-token`. 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 ## Extension guideline
When adding new bridge route families: When adding new bridge route families: