From 620b6ab7e85a57eaba158e0575d97cb973389b23 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 29 May 2026 22:41:30 -0700 Subject: [PATCH] fix: clear close dialog debounce timers (#3478) --- src/renderer/src/components/Terminal.tsx | 55 ++++++++++++++++-------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/src/renderer/src/components/Terminal.tsx b/src/renderer/src/components/Terminal.tsx index 4a5867506b5..6d15a533d63 100644 --- a/src/renderer/src/components/Terminal.tsx +++ b/src/renderer/src/components/Terminal.tsx @@ -227,6 +227,24 @@ function Terminal(): React.JSX.Element | null { // so the stray click from the previous dialog is absorbed while a genuine // new click on the next dialog still works. const isClosingRef = useRef(false) + const closeDialogDebounceTimersRef = useRef>(new Set()) + const releaseCloseDialogGuardAfterDebounce = useCallback(() => { + const timer = window.setTimeout(() => { + closeDialogDebounceTimersRef.current.delete(timer) + isClosingRef.current = false + }, CLOSE_DIALOG_DEBOUNCE_MS) + closeDialogDebounceTimersRef.current.add(timer) + }, []) + + useEffect(() => { + const timers = closeDialogDebounceTimersRef.current + return () => { + for (const timer of timers) { + window.clearTimeout(timer) + } + timers.clear() + } + }, []) // Window close confirmation dialog — shown for local terminals with running // child processes. SSH terminals detach/persist through the relay lifecycle. @@ -399,9 +417,7 @@ function Terminal(): React.JSX.Element | null { (id) => id !== fileId ) advanceEditorCloseQueue() - setTimeout(() => { - isClosingRef.current = false - }, CLOSE_DIALOG_DEBOUNCE_MS) + releaseCloseDialogGuardAfterDebounce() return } @@ -433,9 +449,7 @@ function Terminal(): React.JSX.Element | null { (id) => id !== fileId ) advanceEditorCloseQueue() - setTimeout(() => { - isClosingRef.current = false - }, CLOSE_DIALOG_DEBOUNCE_MS) + releaseCloseDialogGuardAfterDebounce() return } toast.error('Save timed out or failed. Fix errors before closing.') @@ -450,10 +464,13 @@ function Terminal(): React.JSX.Element | null { (id) => id !== fileId ) advanceEditorCloseQueue() - setTimeout(() => { - isClosingRef.current = false - }, CLOSE_DIALOG_DEBOUNCE_MS) - }, [advanceEditorCloseQueue, saveDialogFileId, waitForFileClosed]) + releaseCloseDialogGuardAfterDebounce() + }, [ + advanceEditorCloseQueue, + releaseCloseDialogGuardAfterDebounce, + saveDialogFileId, + waitForFileClosed + ]) const handleSaveDialogDiscard = useCallback(async () => { if (isClosingRef.current) { @@ -488,10 +505,14 @@ function Terminal(): React.JSX.Element | null { (id) => id !== fileId ) advanceEditorCloseQueue() - setTimeout(() => { - isClosingRef.current = false - }, CLOSE_DIALOG_DEBOUNCE_MS) - }, [advanceEditorCloseQueue, closeFile, markFileDirty, saveDialogFileId]) + releaseCloseDialogGuardAfterDebounce() + }, [ + advanceEditorCloseQueue, + closeFile, + markFileDirty, + releaseCloseDialogGuardAfterDebounce, + saveDialogFileId + ]) const handleSaveDialogCancel = useCallback(() => { if (isClosingRef.current) { @@ -501,10 +522,8 @@ function Terminal(): React.JSX.Element | null { pendingEditorCloseQueueRef.current = [] windowCloseAfterDirtyRef.current = null setSaveDialogFileId(null) - setTimeout(() => { - isClosingRef.current = false - }, CLOSE_DIALOG_DEBOUNCE_MS) - }, []) + releaseCloseDialogGuardAfterDebounce() + }, [releaseCloseDialogGuardAfterDebounce]) useEffect(() => { const onRequestEditorClose = (event: Event): void => {