Fix Tauri tab close confirm reentry

This commit is contained in:
Mohamed Boudra
2026-03-17 16:14:18 +07:00
parent 18533ef52b
commit 4897627943
3 changed files with 94 additions and 42 deletions
@@ -658,6 +658,7 @@ function WorkspaceScreenContent({
(state) => state.clearWorkspaceTabActionRequest
);
const consumedOpenIntentsRef = useRef(new Set<string>());
const pendingCloseTabIdsRef = useRef(new Set<string>());
const [resolvedOpenIntentKey, setResolvedOpenIntentKey] = useState<string | null>(null);
const currentOpenIntentKey = useMemo(
() =>
@@ -1035,29 +1036,48 @@ function WorkspaceScreenContent({
[handleCreateDraftTab]
);
const runCloseFlowForTab = useCallback(
async (input: { tabId: string; action: () => Promise<void> }): Promise<void> => {
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,
]
);
@@ -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."
);
+15
View File
@@ -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<boolean | null> {
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<boolean>
return showWebConfirmDialog(input);
}
export const __private__ = {
blurActiveWebElement,
buildTauriAskOptions,
};