Skip to content

Commit c0d462b

Browse files
committed
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
1 parent 342ee70 commit c0d462b

File tree

2 files changed

+126
-32
lines changed

2 files changed

+126
-32
lines changed

webview-ui/src/App.tsx

Lines changed: 90 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import ModesView from "./components/modes/ModesView"
2121
import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
2222
import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog"
2323
import ErrorBoundary from "./components/ErrorBoundary"
24+
import { DialogErrorBoundary } from "./components/ui/DialogErrorBoundary"
2425
import { AccountView } from "./components/account/AccountView"
2526
import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick"
2627
import { TooltipProvider } from "./components/ui/tooltip"
@@ -207,6 +208,57 @@ const App = () => {
207208
console.debug("App initialized with source map support")
208209
}, [])
209210

211+
// Dialog recovery mechanism - detect and close stuck dialogs
212+
useEffect(() => {
213+
// Set up a timeout to check for stuck dialogs after 30 seconds
214+
const timeoutId = setTimeout(() => {
215+
// Check if any dialog is open but the app seems unresponsive
216+
const hasStuckDialog =
217+
humanRelayDialogState.isOpen || deleteMessageDialogState.isOpen || editMessageDialogState.isOpen
218+
219+
if (hasStuckDialog) {
220+
console.warn("Detected potentially stuck dialog, attempting recovery")
221+
222+
// Reset all dialog states
223+
setHumanRelayDialogState({ isOpen: false, requestId: "", promptText: "" })
224+
setDeleteMessageDialogState({ isOpen: false, messageTs: 0 })
225+
setEditMessageDialogState({ isOpen: false, messageTs: 0, text: "", images: [] })
226+
227+
// Log telemetry for debugging
228+
telemetryClient.capture("dialog_recovery_triggered", {
229+
humanRelayOpen: humanRelayDialogState.isOpen,
230+
deleteMessageOpen: deleteMessageDialogState.isOpen,
231+
editMessageOpen: editMessageDialogState.isOpen,
232+
})
233+
}
234+
}, 30000) // 30 seconds timeout
235+
236+
return () => clearTimeout(timeoutId)
237+
}, [humanRelayDialogState.isOpen, deleteMessageDialogState.isOpen, editMessageDialogState.isOpen])
238+
239+
// Add keyboard shortcut for manual dialog recovery (Escape key)
240+
useEffect(() => {
241+
const handleKeyDown = (e: KeyboardEvent) => {
242+
// Check if Escape key is pressed and any dialog is open
243+
if (e.key === "Escape") {
244+
const hasOpenDialog =
245+
humanRelayDialogState.isOpen || deleteMessageDialogState.isOpen || editMessageDialogState.isOpen
246+
247+
if (hasOpenDialog) {
248+
console.log("Manual dialog recovery triggered via Escape key")
249+
250+
// Close all dialogs
251+
setHumanRelayDialogState({ isOpen: false, requestId: "", promptText: "" })
252+
setDeleteMessageDialogState({ isOpen: false, messageTs: 0 })
253+
setEditMessageDialogState({ isOpen: false, messageTs: 0, text: "", images: [] })
254+
}
255+
}
256+
}
257+
258+
window.addEventListener("keydown", handleKeyDown)
259+
return () => window.removeEventListener("keydown", handleKeyDown)
260+
}, [humanRelayDialogState.isOpen, deleteMessageDialogState.isOpen, editMessageDialogState.isOpen])
261+
210262
// Focus the WebView when non-interactive content is clicked (only in editor/tab mode)
211263
useAddNonInteractiveClickListener(
212264
useCallback(() => {
@@ -260,38 +312,44 @@ const App = () => {
260312
showAnnouncement={showAnnouncement}
261313
hideAnnouncement={() => setShowAnnouncement(false)}
262314
/>
263-
<MemoizedHumanRelayDialog
264-
isOpen={humanRelayDialogState.isOpen}
265-
requestId={humanRelayDialogState.requestId}
266-
promptText={humanRelayDialogState.promptText}
267-
onClose={() => setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}
268-
onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })}
269-
onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })}
270-
/>
271-
<MemoizedDeleteMessageDialog
272-
open={deleteMessageDialogState.isOpen}
273-
onOpenChange={(open) => setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: open }))}
274-
onConfirm={() => {
275-
vscode.postMessage({
276-
type: "deleteMessageConfirm",
277-
messageTs: deleteMessageDialogState.messageTs,
278-
})
279-
setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: false }))
280-
}}
281-
/>
282-
<MemoizedEditMessageDialog
283-
open={editMessageDialogState.isOpen}
284-
onOpenChange={(open) => setEditMessageDialogState((prev) => ({ ...prev, isOpen: open }))}
285-
onConfirm={() => {
286-
vscode.postMessage({
287-
type: "editMessageConfirm",
288-
messageTs: editMessageDialogState.messageTs,
289-
text: editMessageDialogState.text,
290-
images: editMessageDialogState.images,
291-
})
292-
setEditMessageDialogState((prev) => ({ ...prev, isOpen: false }))
293-
}}
294-
/>
315+
<DialogErrorBoundary onError={() => setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}>
316+
<MemoizedHumanRelayDialog
317+
isOpen={humanRelayDialogState.isOpen}
318+
requestId={humanRelayDialogState.requestId}
319+
promptText={humanRelayDialogState.promptText}
320+
onClose={() => setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}
321+
onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })}
322+
onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })}
323+
/>
324+
</DialogErrorBoundary>
325+
<DialogErrorBoundary onError={() => setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: false }))}>
326+
<MemoizedDeleteMessageDialog
327+
open={deleteMessageDialogState.isOpen}
328+
onOpenChange={(open) => setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: open }))}
329+
onConfirm={() => {
330+
vscode.postMessage({
331+
type: "deleteMessageConfirm",
332+
messageTs: deleteMessageDialogState.messageTs,
333+
})
334+
setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: false }))
335+
}}
336+
/>
337+
</DialogErrorBoundary>
338+
<DialogErrorBoundary onError={() => setEditMessageDialogState((prev) => ({ ...prev, isOpen: false }))}>
339+
<MemoizedEditMessageDialog
340+
open={editMessageDialogState.isOpen}
341+
onOpenChange={(open) => setEditMessageDialogState((prev) => ({ ...prev, isOpen: open }))}
342+
onConfirm={() => {
343+
vscode.postMessage({
344+
type: "editMessageConfirm",
345+
messageTs: editMessageDialogState.messageTs,
346+
text: editMessageDialogState.text,
347+
images: editMessageDialogState.images,
348+
})
349+
setEditMessageDialogState((prev) => ({ ...prev, isOpen: false }))
350+
}}
351+
/>
352+
</DialogErrorBoundary>
295353
</>
296354
)
297355
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React, { Component, ReactNode } from "react"
2+
3+
interface Props {
4+
children: ReactNode
5+
onError?: () => void
6+
}
7+
8+
interface State {
9+
hasError: boolean
10+
}
11+
12+
export class DialogErrorBoundary extends Component<Props, State> {
13+
constructor(props: Props) {
14+
super(props)
15+
this.state = { hasError: false }
16+
}
17+
18+
static getDerivedStateFromError(): State {
19+
return { hasError: true }
20+
}
21+
22+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
23+
console.error("Dialog error:", error, errorInfo)
24+
// Call the onError callback if provided
25+
this.props.onError?.()
26+
}
27+
28+
render() {
29+
if (this.state.hasError) {
30+
// Return null to close the dialog content and prevent grey screen
31+
return null
32+
}
33+
34+
return this.props.children
35+
}
36+
}

0 commit comments

Comments
 (0)