fix: resolve webview loading screen hang issue (#5593)

- Add 10-second timeout to force hydration if state message is never received
- Improve error handling in webview message handler with try-catch blocks
- Add fallback state posting to prevent infinite loading scenarios
- Add comprehensive test for timeout functionality

Fixes #5593
This commit is contained in:
Roo Code 2025-07-11 13:50:07 +00:00
parent b03d03d860
commit 082b827fa1
3 changed files with 61 additions and 7 deletions

View file

@ -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.

View file

@ -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,

View file

@ -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 <div data-testid="hydration-state">{didHydrateState.toString()}</div>
}
render(
<ExtensionStateContextProvider>
<TestTimeoutComponent />
</ExtensionStateContextProvider>,
)
// 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()
})
})