From 4897627943d01fa165b6601b36c0a8643244f400 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 17 Mar 2026 16:14:18 +0700 Subject: [PATCH] Fix Tauri tab close confirm reentry --- .../screens/workspace/workspace-screen.tsx | 111 +++++++++++------- packages/app/src/utils/confirm-dialog.test.ts | 10 ++ packages/app/src/utils/confirm-dialog.ts | 15 +++ 3 files changed, 94 insertions(+), 42 deletions(-) diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index 19136f72f..1ab584f1b 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -658,6 +658,7 @@ function WorkspaceScreenContent({ (state) => state.clearWorkspaceTabActionRequest ); const consumedOpenIntentsRef = useRef(new Set()); + const pendingCloseTabIdsRef = useRef(new Set()); const [resolvedOpenIntentKey, setResolvedOpenIntentKey] = useState(null); const currentOpenIntentKey = useMemo( () => @@ -1035,29 +1036,48 @@ function WorkspaceScreenContent({ [handleCreateDraftTab] ); + const runCloseFlowForTab = useCallback( + async (input: { tabId: string; action: () => Promise }): Promise => { + const normalizedTabId = input.tabId.trim(); + if (!normalizedTabId || pendingCloseTabIdsRef.current.has(normalizedTabId)) { + return; + } + + pendingCloseTabIdsRef.current.add(normalizedTabId); + try { + await input.action(); + } finally { + pendingCloseTabIdsRef.current.delete(normalizedTabId); + } + }, + [] + ); + const handleCloseTerminalTab = useCallback( async (input: { tabId: string; terminalId: string }) => { const { tabId, terminalId } = input; - if ( - killTerminalMutation.isPending && - killTerminalMutation.variables === terminalId - ) { - return; - } + await runCloseFlowForTab({ + tabId, + action: async () => { + if ( + killTerminalMutation.isPending && + killTerminalMutation.variables === terminalId + ) { + return; + } - const confirmed = await confirmDialog({ - title: "Close terminal?", - message: "Any running process in this terminal will be stopped immediately.", - confirmLabel: "Close", - cancelLabel: "Cancel", - destructive: true, - }); - if (!confirmed) { - return; - } + const confirmed = await confirmDialog({ + title: "Close terminal?", + message: "Any running process in this terminal will be stopped immediately.", + confirmLabel: "Close", + cancelLabel: "Cancel", + destructive: true, + }); + if (!confirmed) { + return; + } - killTerminalMutation.mutate(terminalId, { - onSuccess: () => { + await killTerminalMutation.mutateAsync(terminalId); setHoveredTabKey((current) => (current === tabId ? null : current)); setHoveredCloseTabKey((current) => (current === tabId ? null : current)); @@ -1090,6 +1110,7 @@ function WorkspaceScreenContent({ normalizedServerId, normalizedWorkspaceId, queryClient, + runCloseFlowForTab, terminalsQueryKey, ] ); @@ -1097,31 +1118,36 @@ function WorkspaceScreenContent({ const handleCloseAgentTab = useCallback( async (input: { tabId: string; agentId: string }) => { const { tabId, agentId } = input; - if ( - !normalizedServerId || - isArchivingAgent({ serverId: normalizedServerId, agentId }) - ) { - return; - } - - const confirmed = await confirmDialog({ - title: "Archive agent?", - message: "This closes the tab and archives the agent.", - confirmLabel: "Archive", - cancelLabel: "Cancel", - destructive: true, - }); - if (!confirmed) { - return; - } - - await archiveAgent({ serverId: normalizedServerId, agentId }); - setHoveredTabKey((current) => (current === tabId ? null : current)); - setHoveredCloseTabKey((current) => (current === tabId ? null : current)); - closeWorkspaceTab({ - serverId: normalizedServerId, - workspaceId: normalizedWorkspaceId, + await runCloseFlowForTab({ tabId, + action: async () => { + if ( + !normalizedServerId || + isArchivingAgent({ serverId: normalizedServerId, agentId }) + ) { + return; + } + + const confirmed = await confirmDialog({ + title: "Archive agent?", + message: "This closes the tab and archives the agent.", + confirmLabel: "Archive", + cancelLabel: "Cancel", + destructive: true, + }); + if (!confirmed) { + return; + } + + await archiveAgent({ serverId: normalizedServerId, agentId }); + setHoveredTabKey((current) => (current === tabId ? null : current)); + setHoveredCloseTabKey((current) => (current === tabId ? null : current)); + closeWorkspaceTab({ + serverId: normalizedServerId, + workspaceId: normalizedWorkspaceId, + tabId, + }); + }, }); }, [ @@ -1130,6 +1156,7 @@ function WorkspaceScreenContent({ isArchivingAgent, normalizedServerId, normalizedWorkspaceId, + runCloseFlowForTab, ] ); diff --git a/packages/app/src/utils/confirm-dialog.test.ts b/packages/app/src/utils/confirm-dialog.test.ts index df739d40d..8876611b1 100644 --- a/packages/app/src/utils/confirm-dialog.test.ts +++ b/packages/app/src/utils/confirm-dialog.test.ts @@ -39,6 +39,10 @@ describe("confirmDialog", () => { it("uses Tauri dialog.ask on web when available", async () => { const askMock = vi.fn(async () => true); + const blurMock = vi.fn(); + (globalThis as { document?: unknown }).document = { + activeElement: { blur: blurMock }, + } as unknown as Document; (globalThis as { __TAURI__?: unknown }).__TAURI__ = { dialog: { ask: askMock }, }; @@ -54,6 +58,7 @@ describe("confirmDialog", () => { expect(confirmed).toBe(true); expect(alertMock).not.toHaveBeenCalled(); + expect(blurMock).toHaveBeenCalledTimes(1); expect(askMock).toHaveBeenCalledWith("This will restart the daemon.", { title: "Restart host", okLabel: "Restart", @@ -64,6 +69,10 @@ describe("confirmDialog", () => { it("falls back to browser confirm on web when Tauri APIs are unavailable", async () => { const browserConfirm = vi.fn(() => true); + const blurMock = vi.fn(); + (globalThis as { document?: unknown }).document = { + activeElement: { blur: blurMock }, + } as unknown as Document; (globalThis as { confirm?: unknown }).confirm = browserConfirm; const { confirmDialog } = await loadModuleForPlatform("web"); @@ -73,6 +82,7 @@ describe("confirmDialog", () => { }); expect(confirmed).toBe(true); + expect(blurMock).toHaveBeenCalledTimes(1); expect(browserConfirm).toHaveBeenCalledWith( "Restart host\n\nThis will restart the daemon." ); diff --git a/packages/app/src/utils/confirm-dialog.ts b/packages/app/src/utils/confirm-dialog.ts index 15a3515a4..4297b6574 100644 --- a/packages/app/src/utils/confirm-dialog.ts +++ b/packages/app/src/utils/confirm-dialog.ts @@ -66,12 +66,21 @@ function buildTauriAskOptions(input: ConfirmDialogInput): TauriDialogAskOptions }; } +function blurActiveWebElement(): void { + if (Platform.OS !== "web") { + return; + } + const activeElement = (globalThis as { document?: Document }).document?.activeElement; + (activeElement as HTMLElement | null)?.blur?.(); +} + async function showTauriConfirmDialog(input: ConfirmDialogInput): Promise { const tauriApi = getTauriApi(); if (!tauriApi) { return null; } + blurActiveWebElement(); const options = buildTauriAskOptions(input); const tauriAsk = tauriApi.dialog?.ask; @@ -105,6 +114,7 @@ function showWebConfirmDialog(input: ConfirmDialogInput): boolean { throw new Error("[ConfirmDialog] No web confirmation backend is available."); } + blurActiveWebElement(); const promptMessage = `${input.title}\n\n${input.message}`; return browserConfirm(promptMessage); } @@ -121,3 +131,8 @@ export async function confirmDialog(input: ConfirmDialogInput): Promise return showWebConfirmDialog(input); } + +export const __private__ = { + blurActiveWebElement, + buildTauriAskOptions, +};