diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index a6577fb2fb..10aa06778c 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -252,14 +252,29 @@ export const webviewMessageHandler = async ( switch (message.type) { case "webviewDidLaunch": - // Load custom modes first - const customModes = await provider.customModesManager.getCustomModes() - await updateGlobalState("customModes", customModes) + try { + // Load custom modes first + const customModes = await provider.customModesManager.getCustomModes() + await updateGlobalState("customModes", customModes) - provider.postStateToWebview() - provider.workspaceTracker?.initializeFilePaths() // Don't await. + // Ensure state is posted to webview - this is critical for preventing loading screen hang + await provider.postStateToWebview() + provider.workspaceTracker?.initializeFilePaths() // Don't await. - getTheme().then((theme) => provider.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) })) + getTheme().then((theme) => + provider.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) }), + ) + } catch (error) { + provider.log(`Error during webview launch: ${error instanceof Error ? error.message : String(error)}`) + // Even if there's an error, try to post state to prevent infinite loading + try { + await provider.postStateToWebview() + } catch (fallbackError) { + provider.log( + `Fallback state posting also failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`, + ) + } + } // If MCP Hub is already initialized, update the webview with // current server list. diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index bf927211c2..b854f63ff0 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -350,7 +350,17 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode useEffect(() => { vscode.postMessage({ type: "webviewDidLaunch" }) - }, []) + + // Set a timeout to prevent infinite loading if state message is never received + const loadingTimeout = setTimeout(() => { + if (!didHydrateState) { + console.warn("Webview state hydration timeout - forcing hydration to prevent infinite loading") + setDidHydrateState(true) + } + }, 10000) // 10 second timeout + + return () => clearTimeout(loadingTimeout) + }, [didHydrateState]) const contextValue: ExtensionStateContextType = { ...state, diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 1e5867d3fc..3a6180b8da 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -244,4 +244,33 @@ describe("mergeExtensionState", () => { multiFileApplyDiff: true, }) }) + + it("forces hydration after timeout when state message is never received", async () => { + // Mock timers to control timeout behavior + vi.useFakeTimers() + + const TestTimeoutComponent = () => { + const { didHydrateState } = useExtensionState() + return
{didHydrateState.toString()}
+ } + + render( + + + , + ) + + // Initially should not be hydrated + expect(screen.getByTestId("hydration-state").textContent).toBe("false") + + // Fast-forward time by 10 seconds (the timeout duration) + act(() => { + vi.advanceTimersByTime(10000) + }) + + // Should now be hydrated due to timeout + expect(screen.getByTestId("hydration-state").textContent).toBe("true") + + vi.useRealTimers() + }) })