mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: show loading view with retry instead of grey screen when state not hydrated
Replace `return null` with a LoadingView component when the webview has not yet received its initial state from the extension host. This prevents users from seeing a blank grey panel (#11931). The LoadingView shows a spinner while waiting, automatically retries the webviewDidLaunch message up to 3 times (every 5 seconds), and displays a manual "Retry Connection" button after all retries are exhausted.
This commit is contained in:
parent
ad25634905
commit
b6e94366df
4 changed files with 179 additions and 2 deletions
|
|
@ -19,6 +19,7 @@ import { MarketplaceView } from "./components/marketplace/MarketplaceView"
|
|||
import { CheckpointRestoreDialog } from "./components/chat/CheckpointRestoreDialog"
|
||||
import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog"
|
||||
import ErrorBoundary from "./components/ErrorBoundary"
|
||||
import LoadingView from "./components/LoadingView"
|
||||
import { CloudView } from "./components/cloud/CloudView"
|
||||
import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick"
|
||||
import { TooltipProvider } from "./components/ui/tooltip"
|
||||
|
|
@ -217,7 +218,7 @@ const App = () => {
|
|||
}, [tab])
|
||||
|
||||
if (!didHydrateState) {
|
||||
return null
|
||||
return <LoadingView />
|
||||
}
|
||||
|
||||
// Do not conditionally load ChatView, it's expensive and there's state we
|
||||
|
|
|
|||
73
webview-ui/src/components/LoadingView.tsx
Normal file
73
webview-ui/src/components/LoadingView.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import React, { useEffect, useState, useCallback } from "react"
|
||||
import { useAppTranslation } from "../i18n/TranslationContext"
|
||||
import { vscode } from "../utils/vscode"
|
||||
|
||||
const RETRY_INTERVAL_MS = 5_000
|
||||
const MAX_RETRIES = 3
|
||||
|
||||
/**
|
||||
* LoadingView is displayed while the webview waits for the extension host to
|
||||
* send the initial state hydration message. It replaces the previous
|
||||
* `return null` which left users staring at a blank grey panel (see #11931).
|
||||
*
|
||||
* If the state message does not arrive within {@link RETRY_INTERVAL_MS} the
|
||||
* component automatically re-sends the `webviewDidLaunch` message up to
|
||||
* {@link MAX_RETRIES} times, after which a manual "Retry" button is shown.
|
||||
*/
|
||||
export default function LoadingView() {
|
||||
const { t } = useAppTranslation()
|
||||
const [retryCount, setRetryCount] = useState(0)
|
||||
const [showRetryButton, setShowRetryButton] = useState(false)
|
||||
|
||||
const retry = useCallback(() => {
|
||||
vscode.postMessage({ type: "webviewDidLaunch" })
|
||||
setRetryCount((prev) => prev + 1)
|
||||
}, [])
|
||||
|
||||
// Automatic retries on a timer
|
||||
useEffect(() => {
|
||||
if (showRetryButton) {
|
||||
return // Stop auto-retrying once we're showing the manual button.
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (retryCount < MAX_RETRIES) {
|
||||
retry()
|
||||
} else {
|
||||
setShowRetryButton(true)
|
||||
}
|
||||
}, RETRY_INTERVAL_MS)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [retryCount, showRetryButton, retry])
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 flex flex-col bg-vscode-editor-background text-vscode-foreground">
|
||||
<div className="flex-1 flex items-center justify-center px-6">
|
||||
<div className="flex flex-col items-center gap-5 text-center">
|
||||
{!showRetryButton ? (
|
||||
<div className="flex items-center gap-2 text-sm text-vscode-descriptionForeground">
|
||||
<span className="codicon codicon-loading codicon-modifier-spin text-base" />
|
||||
<span>{t("common:ui.initializing")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<p className="text-sm text-vscode-descriptionForeground">
|
||||
{t("common:ui.connection_failed")}
|
||||
</p>
|
||||
<button
|
||||
className="px-4 py-1.5 rounded text-sm font-medium bg-vscode-button-background text-vscode-button-foreground hover:bg-vscode-button-hoverBackground"
|
||||
onClick={() => {
|
||||
setRetryCount(0)
|
||||
setShowRetryButton(false)
|
||||
retry()
|
||||
}}>
|
||||
{t("common:ui.retry_connection")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
100
webview-ui/src/components/__tests__/LoadingView.spec.tsx
Normal file
100
webview-ui/src/components/__tests__/LoadingView.spec.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// npx vitest run src/components/__tests__/LoadingView.spec.tsx
|
||||
|
||||
import React from "react"
|
||||
import { render, screen, act } from "@testing-library/react"
|
||||
import LoadingView from "../LoadingView"
|
||||
|
||||
vi.mock("@src/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@src/i18n/TranslationContext", () => ({
|
||||
useAppTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"common:ui.initializing": "Initializing...",
|
||||
"common:ui.retry_connection": "Retry Connection",
|
||||
"common:ui.connection_failed":
|
||||
"Unable to connect to the extension host. Click the button below to retry.",
|
||||
}
|
||||
return translations[key] ?? key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
describe("LoadingView", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it("renders a spinner and initializing text", () => {
|
||||
render(<LoadingView />)
|
||||
expect(screen.getByText("Initializing...")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not show retry button initially", () => {
|
||||
render(<LoadingView />)
|
||||
expect(screen.queryByText("Retry Connection")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("retries webviewDidLaunch after timeout", async () => {
|
||||
const { vscode } = await import("@src/utils/vscode")
|
||||
render(<LoadingView />)
|
||||
|
||||
// Advance past the first retry interval (5s)
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(5_000)
|
||||
})
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({ type: "webviewDidLaunch" })
|
||||
})
|
||||
|
||||
it("shows retry button after max retries", async () => {
|
||||
render(<LoadingView />)
|
||||
|
||||
// Advance through all 3 retries (5s each) + one more to trigger the button
|
||||
for (let i = 0; i < 4; i++) {
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(5_000)
|
||||
})
|
||||
}
|
||||
|
||||
expect(screen.getByText("Retry Connection")).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText("Unable to connect to the extension host. Click the button below to retry."),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("allows manual retry when button is clicked", async () => {
|
||||
const { vscode } = await import("@src/utils/vscode")
|
||||
render(<LoadingView />)
|
||||
|
||||
// Advance through all retries to show the button
|
||||
for (let i = 0; i < 4; i++) {
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(5_000)
|
||||
})
|
||||
}
|
||||
|
||||
const calls = (vscode.postMessage as ReturnType<typeof vi.fn>).mock.calls.length
|
||||
|
||||
const retryButton = screen.getByText("Retry Connection")
|
||||
act(() => {
|
||||
retryButton.click()
|
||||
})
|
||||
|
||||
// Should have sent another webviewDidLaunch
|
||||
expect((vscode.postMessage as ReturnType<typeof vi.fn>).mock.calls.length).toBeGreaterThan(calls)
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({ type: "webviewDidLaunch" })
|
||||
|
||||
// Should go back to showing the spinner
|
||||
expect(screen.getByText("Initializing...")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -21,7 +21,10 @@
|
|||
},
|
||||
"ui": {
|
||||
"search_placeholder": "Search...",
|
||||
"no_results": "No results found"
|
||||
"no_results": "No results found",
|
||||
"initializing": "Initializing...",
|
||||
"retry_connection": "Retry Connection",
|
||||
"connection_failed": "Unable to connect to the extension host. Click the button below to retry."
|
||||
},
|
||||
"mermaid": {
|
||||
"loading": "Generating mermaid diagram...",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue