Fix Tauri tab close confirm reentry
This commit is contained in:
@@ -658,6 +658,7 @@ function WorkspaceScreenContent({
|
|||||||
(state) => state.clearWorkspaceTabActionRequest
|
(state) => state.clearWorkspaceTabActionRequest
|
||||||
);
|
);
|
||||||
const consumedOpenIntentsRef = useRef(new Set<string>());
|
const consumedOpenIntentsRef = useRef(new Set<string>());
|
||||||
|
const pendingCloseTabIdsRef = useRef(new Set<string>());
|
||||||
const [resolvedOpenIntentKey, setResolvedOpenIntentKey] = useState<string | null>(null);
|
const [resolvedOpenIntentKey, setResolvedOpenIntentKey] = useState<string | null>(null);
|
||||||
const currentOpenIntentKey = useMemo(
|
const currentOpenIntentKey = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -1035,29 +1036,48 @@ function WorkspaceScreenContent({
|
|||||||
[handleCreateDraftTab]
|
[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(
|
const handleCloseTerminalTab = useCallback(
|
||||||
async (input: { tabId: string; terminalId: string }) => {
|
async (input: { tabId: string; terminalId: string }) => {
|
||||||
const { tabId, terminalId } = input;
|
const { tabId, terminalId } = input;
|
||||||
if (
|
await runCloseFlowForTab({
|
||||||
killTerminalMutation.isPending &&
|
tabId,
|
||||||
killTerminalMutation.variables === terminalId
|
action: async () => {
|
||||||
) {
|
if (
|
||||||
return;
|
killTerminalMutation.isPending &&
|
||||||
}
|
killTerminalMutation.variables === terminalId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const confirmed = await confirmDialog({
|
const confirmed = await confirmDialog({
|
||||||
title: "Close terminal?",
|
title: "Close terminal?",
|
||||||
message: "Any running process in this terminal will be stopped immediately.",
|
message: "Any running process in this terminal will be stopped immediately.",
|
||||||
confirmLabel: "Close",
|
confirmLabel: "Close",
|
||||||
cancelLabel: "Cancel",
|
cancelLabel: "Cancel",
|
||||||
destructive: true,
|
destructive: true,
|
||||||
});
|
});
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
killTerminalMutation.mutate(terminalId, {
|
await killTerminalMutation.mutateAsync(terminalId);
|
||||||
onSuccess: () => {
|
|
||||||
setHoveredTabKey((current) => (current === tabId ? null : current));
|
setHoveredTabKey((current) => (current === tabId ? null : current));
|
||||||
setHoveredCloseTabKey((current) => (current === tabId ? null : current));
|
setHoveredCloseTabKey((current) => (current === tabId ? null : current));
|
||||||
|
|
||||||
@@ -1090,6 +1110,7 @@ function WorkspaceScreenContent({
|
|||||||
normalizedServerId,
|
normalizedServerId,
|
||||||
normalizedWorkspaceId,
|
normalizedWorkspaceId,
|
||||||
queryClient,
|
queryClient,
|
||||||
|
runCloseFlowForTab,
|
||||||
terminalsQueryKey,
|
terminalsQueryKey,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -1097,31 +1118,36 @@ function WorkspaceScreenContent({
|
|||||||
const handleCloseAgentTab = useCallback(
|
const handleCloseAgentTab = useCallback(
|
||||||
async (input: { tabId: string; agentId: string }) => {
|
async (input: { tabId: string; agentId: string }) => {
|
||||||
const { tabId, agentId } = input;
|
const { tabId, agentId } = input;
|
||||||
if (
|
await runCloseFlowForTab({
|
||||||
!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,
|
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,
|
isArchivingAgent,
|
||||||
normalizedServerId,
|
normalizedServerId,
|
||||||
normalizedWorkspaceId,
|
normalizedWorkspaceId,
|
||||||
|
runCloseFlowForTab,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ describe("confirmDialog", () => {
|
|||||||
|
|
||||||
it("uses Tauri dialog.ask on web when available", async () => {
|
it("uses Tauri dialog.ask on web when available", async () => {
|
||||||
const askMock = vi.fn(async () => true);
|
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__ = {
|
(globalThis as { __TAURI__?: unknown }).__TAURI__ = {
|
||||||
dialog: { ask: askMock },
|
dialog: { ask: askMock },
|
||||||
};
|
};
|
||||||
@@ -54,6 +58,7 @@ describe("confirmDialog", () => {
|
|||||||
|
|
||||||
expect(confirmed).toBe(true);
|
expect(confirmed).toBe(true);
|
||||||
expect(alertMock).not.toHaveBeenCalled();
|
expect(alertMock).not.toHaveBeenCalled();
|
||||||
|
expect(blurMock).toHaveBeenCalledTimes(1);
|
||||||
expect(askMock).toHaveBeenCalledWith("This will restart the daemon.", {
|
expect(askMock).toHaveBeenCalledWith("This will restart the daemon.", {
|
||||||
title: "Restart host",
|
title: "Restart host",
|
||||||
okLabel: "Restart",
|
okLabel: "Restart",
|
||||||
@@ -64,6 +69,10 @@ describe("confirmDialog", () => {
|
|||||||
|
|
||||||
it("falls back to browser confirm on web when Tauri APIs are unavailable", async () => {
|
it("falls back to browser confirm on web when Tauri APIs are unavailable", async () => {
|
||||||
const browserConfirm = vi.fn(() => true);
|
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;
|
(globalThis as { confirm?: unknown }).confirm = browserConfirm;
|
||||||
|
|
||||||
const { confirmDialog } = await loadModuleForPlatform("web");
|
const { confirmDialog } = await loadModuleForPlatform("web");
|
||||||
@@ -73,6 +82,7 @@ describe("confirmDialog", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(confirmed).toBe(true);
|
expect(confirmed).toBe(true);
|
||||||
|
expect(blurMock).toHaveBeenCalledTimes(1);
|
||||||
expect(browserConfirm).toHaveBeenCalledWith(
|
expect(browserConfirm).toHaveBeenCalledWith(
|
||||||
"Restart host\n\nThis will restart the daemon."
|
"Restart host\n\nThis will restart the daemon."
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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> {
|
async function showTauriConfirmDialog(input: ConfirmDialogInput): Promise<boolean | null> {
|
||||||
const tauriApi = getTauriApi();
|
const tauriApi = getTauriApi();
|
||||||
if (!tauriApi) {
|
if (!tauriApi) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blurActiveWebElement();
|
||||||
const options = buildTauriAskOptions(input);
|
const options = buildTauriAskOptions(input);
|
||||||
const tauriAsk = tauriApi.dialog?.ask;
|
const tauriAsk = tauriApi.dialog?.ask;
|
||||||
|
|
||||||
@@ -105,6 +114,7 @@ function showWebConfirmDialog(input: ConfirmDialogInput): boolean {
|
|||||||
throw new Error("[ConfirmDialog] No web confirmation backend is available.");
|
throw new Error("[ConfirmDialog] No web confirmation backend is available.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blurActiveWebElement();
|
||||||
const promptMessage = `${input.title}\n\n${input.message}`;
|
const promptMessage = `${input.title}\n\n${input.message}`;
|
||||||
return browserConfirm(promptMessage);
|
return browserConfirm(promptMessage);
|
||||||
}
|
}
|
||||||
@@ -121,3 +131,8 @@ export async function confirmDialog(input: ConfirmDialogInput): Promise<boolean>
|
|||||||
|
|
||||||
return showWebConfirmDialog(input);
|
return showWebConfirmDialog(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const __private__ = {
|
||||||
|
blurActiveWebElement,
|
||||||
|
buildTauriAskOptions,
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user