From 483966a05b1df9996089545be4304998641192ad Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 18 Feb 2026 11:13:48 +0700 Subject: [PATCH] fix(app): confirm terminal close for running shell commands (#52) --- packages/app/e2e/terminal-pane.spec.ts | 80 +++++++++++++++++++ packages/app/src/components/terminal-pane.tsx | 18 ++++- 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/packages/app/e2e/terminal-pane.spec.ts b/packages/app/e2e/terminal-pane.spec.ts index 0c9ad866a..c573d51bd 100644 --- a/packages/app/e2e/terminal-pane.spec.ts +++ b/packages/app/e2e/terminal-pane.spec.ts @@ -395,6 +395,86 @@ test("terminal tab is removed when shell exits", async ({ page }) => { } }); +test("closing terminal with running command asks for confirmation", async ({ page }) => { + const repo = await createTempGitRepo("paseo-e2e-terminal-close-confirm-"); + + try { + await openNewAgentDraft(page); + await setWorkingDirectory(page, repo.path); + await ensureHostSelected(page); + await createAgent(page, "Terminal close confirmation"); + + await openTerminalsPanel(page); + + const tabTestId = await getFirstTerminalTabTestId(page); + const terminalId = tabTestId.replace("terminal-tab-", ""); + const tab = page.getByTestId(tabTestId).first(); + await expect(tab).toBeVisible({ timeout: 30000 }); + + const runningMarker = `terminal-close-running-${Date.now()}`; + await runTerminalCommand( + page, + `echo ${runningMarker} && sleep 30`, + runningMarker + ); + + await tab.hover(); + const dialogPromise = page.waitForEvent("dialog", { timeout: 30000 }).then( + async (dialog) => { + expect(dialog.type()).toBe("confirm"); + await dialog.dismiss(); + } + ); + await page.getByTestId(`terminal-close-${terminalId}`).first().click(); + await dialogPromise; + + await expect(tab).toBeVisible({ timeout: 30000 }); + } finally { + await repo.cleanup(); + } +}); + +test("confirming terminal close with running command removes the tab", async ({ page }) => { + const repo = await createTempGitRepo("paseo-e2e-terminal-close-confirm-accept-"); + + try { + await openNewAgentDraft(page); + await setWorkingDirectory(page, repo.path); + await ensureHostSelected(page); + await createAgent(page, "Terminal close confirmation accept"); + + await openTerminalsPanel(page); + + const tabTestId = await getFirstTerminalTabTestId(page); + const terminalId = tabTestId.replace("terminal-tab-", ""); + const tab = page.getByTestId(tabTestId).first(); + await expect(tab).toBeVisible({ timeout: 30000 }); + + const runningMarker = `terminal-close-running-accept-${Date.now()}`; + await runTerminalCommand( + page, + `echo ${runningMarker} && sleep 30`, + runningMarker + ); + + await tab.hover(); + const dialogPromise = page.waitForEvent("dialog", { timeout: 30000 }).then( + async (dialog) => { + expect(dialog.type()).toBe("confirm"); + await dialog.accept(); + } + ); + await page.getByTestId(`terminal-close-${terminalId}`).first().click(); + await dialogPromise; + + await expect(page.getByTestId(tabTestId)).toHaveCount(0, { + timeout: 30000, + }); + } finally { + await repo.cleanup(); + } +}); + test("terminals are shared by agents on the same cwd", async ({ page }) => { const repo = await createTempGitRepo("paseo-e2e-terminal-share-"); diff --git a/packages/app/src/components/terminal-pane.tsx b/packages/app/src/components/terminal-pane.tsx index 1634931f4..2886cec74 100644 --- a/packages/app/src/components/terminal-pane.tsx +++ b/packages/app/src/components/terminal-pane.tsx @@ -24,6 +24,7 @@ import { resolvePendingModifierDataInput, } from "@/utils/terminal-keys"; import { upsertTerminalListEntry } from "@/utils/terminal-list"; +import { confirmDialog } from "@/utils/confirm-dialog"; import { TerminalOutputPump, type TerminalOutputChunk, @@ -588,13 +589,26 @@ export function TerminalPane({ serverId, cwd }: TerminalPaneProps) { }, [activeStreamId, flushPendingTerminalInput]); const handleCloseTerminal = useCallback( - (terminalId: string) => { + async (terminalId: string) => { 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; + } + killTerminalMutation.mutate(terminalId); }, [killTerminalMutation] @@ -882,7 +896,7 @@ export function TerminalPane({ serverId, cwd }: TerminalPaneProps) { onHoverOut={() => handleTerminalCloseHoverOut(terminal.id)} onPress={(event) => { event.stopPropagation(); - handleCloseTerminal(terminal.id); + void handleCloseTerminal(terminal.id); }} style={({ hovered, pressed }) => [ styles.terminalTabCloseButton,