From c0d462b5e5d864f1abce95cb03d7a2d5e8e6678c Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 28 Jul 2025 05:40:26 +0000 Subject: [PATCH] fix: add dialog recovery mechanism to prevent grey screen issue - Added 30-second timeout to automatically close stuck dialogs - Added DialogErrorBoundary component to catch errors in dialog content - Added Escape key handler for manual dialog recovery - Wrapped all dialog components with error boundaries This prevents the grey screen issue when dialogs get stuck in an open state without their content rendering properly. Fixes #6283 --- webview-ui/src/App.tsx | 122 +++++++++++++----- .../src/components/ui/DialogErrorBoundary.tsx | 36 ++++++ 2 files changed, 126 insertions(+), 32 deletions(-) create mode 100644 webview-ui/src/components/ui/DialogErrorBoundary.tsx diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 3782242707..3e2188343f 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -21,6 +21,7 @@ import ModesView from "./components/modes/ModesView" import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog" import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog" import ErrorBoundary from "./components/ErrorBoundary" +import { DialogErrorBoundary } from "./components/ui/DialogErrorBoundary" import { AccountView } from "./components/account/AccountView" import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick" import { TooltipProvider } from "./components/ui/tooltip" @@ -207,6 +208,57 @@ const App = () => { console.debug("App initialized with source map support") }, []) + // Dialog recovery mechanism - detect and close stuck dialogs + useEffect(() => { + // Set up a timeout to check for stuck dialogs after 30 seconds + const timeoutId = setTimeout(() => { + // Check if any dialog is open but the app seems unresponsive + const hasStuckDialog = + humanRelayDialogState.isOpen || deleteMessageDialogState.isOpen || editMessageDialogState.isOpen + + if (hasStuckDialog) { + console.warn("Detected potentially stuck dialog, attempting recovery") + + // Reset all dialog states + setHumanRelayDialogState({ isOpen: false, requestId: "", promptText: "" }) + setDeleteMessageDialogState({ isOpen: false, messageTs: 0 }) + setEditMessageDialogState({ isOpen: false, messageTs: 0, text: "", images: [] }) + + // Log telemetry for debugging + telemetryClient.capture("dialog_recovery_triggered", { + humanRelayOpen: humanRelayDialogState.isOpen, + deleteMessageOpen: deleteMessageDialogState.isOpen, + editMessageOpen: editMessageDialogState.isOpen, + }) + } + }, 30000) // 30 seconds timeout + + return () => clearTimeout(timeoutId) + }, [humanRelayDialogState.isOpen, deleteMessageDialogState.isOpen, editMessageDialogState.isOpen]) + + // Add keyboard shortcut for manual dialog recovery (Escape key) + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Check if Escape key is pressed and any dialog is open + if (e.key === "Escape") { + const hasOpenDialog = + humanRelayDialogState.isOpen || deleteMessageDialogState.isOpen || editMessageDialogState.isOpen + + if (hasOpenDialog) { + console.log("Manual dialog recovery triggered via Escape key") + + // Close all dialogs + setHumanRelayDialogState({ isOpen: false, requestId: "", promptText: "" }) + setDeleteMessageDialogState({ isOpen: false, messageTs: 0 }) + setEditMessageDialogState({ isOpen: false, messageTs: 0, text: "", images: [] }) + } + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [humanRelayDialogState.isOpen, deleteMessageDialogState.isOpen, editMessageDialogState.isOpen]) + // Focus the WebView when non-interactive content is clicked (only in editor/tab mode) useAddNonInteractiveClickListener( useCallback(() => { @@ -260,38 +312,44 @@ const App = () => { showAnnouncement={showAnnouncement} hideAnnouncement={() => setShowAnnouncement(false)} /> - setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))} - onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })} - onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })} - /> - setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: open }))} - onConfirm={() => { - vscode.postMessage({ - type: "deleteMessageConfirm", - messageTs: deleteMessageDialogState.messageTs, - }) - setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: false })) - }} - /> - setEditMessageDialogState((prev) => ({ ...prev, isOpen: open }))} - onConfirm={() => { - vscode.postMessage({ - type: "editMessageConfirm", - messageTs: editMessageDialogState.messageTs, - text: editMessageDialogState.text, - images: editMessageDialogState.images, - }) - setEditMessageDialogState((prev) => ({ ...prev, isOpen: false })) - }} - /> + setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}> + setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))} + onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })} + onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })} + /> + + setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: false }))}> + setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: open }))} + onConfirm={() => { + vscode.postMessage({ + type: "deleteMessageConfirm", + messageTs: deleteMessageDialogState.messageTs, + }) + setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: false })) + }} + /> + + setEditMessageDialogState((prev) => ({ ...prev, isOpen: false }))}> + setEditMessageDialogState((prev) => ({ ...prev, isOpen: open }))} + onConfirm={() => { + vscode.postMessage({ + type: "editMessageConfirm", + messageTs: editMessageDialogState.messageTs, + text: editMessageDialogState.text, + images: editMessageDialogState.images, + }) + setEditMessageDialogState((prev) => ({ ...prev, isOpen: false })) + }} + /> + ) } diff --git a/webview-ui/src/components/ui/DialogErrorBoundary.tsx b/webview-ui/src/components/ui/DialogErrorBoundary.tsx new file mode 100644 index 0000000000..7335231455 --- /dev/null +++ b/webview-ui/src/components/ui/DialogErrorBoundary.tsx @@ -0,0 +1,36 @@ +import React, { Component, ReactNode } from "react" + +interface Props { + children: ReactNode + onError?: () => void +} + +interface State { + hasError: boolean +} + +export class DialogErrorBoundary extends Component { + constructor(props: Props) { + super(props) + this.state = { hasError: false } + } + + static getDerivedStateFromError(): State { + return { hasError: true } + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error("Dialog error:", error, errorInfo) + // Call the onError callback if provided + this.props.onError?.() + } + + render() { + if (this.state.hasError) { + // Return null to close the dialog content and prevent grey screen + return null + } + + return this.props.children + } +}