Skip to content

fix(desktop): confirm before closing active terminal sessions - #5120

Closed
aronchick wants to merge 1 commit into
ParadevAI:mainfrom
aronchick:fix/confirm-close-active-agents
Closed

aronchick wants to merge 1 commit into
ParadevAI:mainfrom
aronchick:fix/confirm-close-active-agents

Conversation

@aronchick

@aronchick aronchick commented Jun 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • prompts before closing active terminal panes in v2 workspaces
  • applies to tab close and direct pane close paths via existing close guards
  • helps prevent accidental loss of Claude/Codex/OpenCode terminal sessions

Related issues

Test plan

  • bunx @biomejs/biome@2.4.2 check on changed files

Notes

  • Uses the existing alert UI and terminal runtime registry; no new persistence or settings are introduced.

Summary by CodeRabbit

  • Improved Features
    • Added confirmation prompts when closing terminal panes with active connections. Users are now prompted before closing terminals or tabs containing active terminal sessions, preventing accidental loss of work.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c1953c1f-58f4-406e-9ca8-8098e65345f8

📥 Commits

Reviewing files that changed from the base of the PR and between 1087491 and 8e94c8c.

📒 Files selected for processing (3)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/confirmTerminalClose.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDirtyTabCloseGuard/useDirtyTabCloseGuard.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDirtyTabCloseGuard/useDirtyTabCloseGuard.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx

📝 Walkthrough

Walkthrough

Adds a helper that finds terminal panes with open connections and an exported confirmation function that prompts the user. The confirmation is invoked from the tab-close guard and the terminal pane onBeforeClose handler to prevent closing active terminals without explicit consent.

Changes

Terminal Pane Close Confirmation

Layer / File(s) Summary
Terminal close confirmation module
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/confirmTerminalClose.ts
Adds a private getActiveTerminalPanes helper that materializes and filters panes to terminal panes with a truthy terminalId whose connection state is "open". Exports confirmCloseActiveTerminalPanes(panes) which returns true when no active terminals exist, otherwise shows a count-aware window.confirm dialog and returns the user's boolean choice.
Tab close guard integration
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDirtyTabCloseGuard/useDirtyTabCloseGuard.ts
Imports confirmCloseActiveTerminalPanes and makes the tab close callback async. It awaits confirmation for all tab panes via confirmCloseActiveTerminalPanes(Object.values(tab.panes)) and returns false immediately when confirmation disallows closing, before continuing the dirty-file flow.
Pane registry integration
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx
Imports confirmCloseActiveTerminalPanes and updates terminal.onBeforeClose to call confirmCloseActiveTerminalPanes([pane]), delegating close confirmation for individual terminal panes to the new function.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A rabbit nods by the glowing shell,
“Don’t close the run — listen to the bell.”
Count the sessions, ask once more,
Let users choose before you roar.
twitches whiskers 🐇

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The pull request description covers key aspects (summary, related issues, test plan, notes) but deviates significantly from the provided template structure with missing sections like 'Type of Change', 'Testing', and 'Screenshots'. Add the missing template sections (Type of Change, Testing, Screenshots if applicable) to ensure consistency with repository standards, even if some sections are marked as N/A.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(desktop): confirm before closing active terminal sessions' directly and clearly describes the primary change: adding confirmation prompts before closing active terminal sessions in the desktop application.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ghost

ghost commented Jun 4, 2026 •

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 3 individual chapters for you:

Title
1 Implement terminal session confirmation logic
2 Integrate confirmation prompt into tab closing
3 Integrate confirmation prompt into pane closing
Open in Stage

Chapters generated by Stage for commit 8e94c8c on Jun 5, 2026 12:35am UTC.

@aronchick
aronchick marked this pull request as ready for review June 5, 2026 00:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/confirmTerminalClose.ts (1)

6-13: 💤 Low value

Type-cast safety: Validate TerminalPaneData shape.

Line 9 casts pane.data to TerminalPaneData after checking kind === "terminal", but the kind check doesn't guarantee the data shape matches the type. If the pane kind is set incorrectly or the data is malformed, accessing terminalId could fail at runtime.

Consider adding a runtime guard or using a type predicate to safely validate the cast:

🛡️ Suggested type-safe approach
 function getActiveTerminalPanes(panes: Iterable<Pane<PaneViewerData>>) {
 	return Array.from(panes).filter((pane) => {
 		if (pane.kind !== "terminal") return false;
-		const { terminalId } = pane.data as TerminalPaneData;
-		if (!terminalId) return false;
+		const data = pane.data as TerminalPaneData;
+		const terminalId = data?.terminalId;
+		if (!terminalId) return false;
 		return terminalRuntimeRegistry.has(terminalId);
 	});
 }

Alternatively, define a type guard function to validate the structure more explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/confirmTerminalClose.ts
around lines 6 - 13, getActiveTerminalPanes currently casts pane.data to
TerminalPaneData after only checking pane.kind === "terminal", which can still
allow malformed data to cause runtime errors; add a runtime guard/type predicate
(e.g., isTerminalPaneData) and use it in getActiveTerminalPanes to verify
pane.data has the expected shape (presence and type of terminalId) before
accessing terminalId and consulting terminalRuntimeRegistry; update the function
to accept Pane<PaneViewerData> and return only when the guard passes and
terminalRuntimeRegistry.has(terminalId).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/confirmTerminalClose.ts:
- Around line 6-13: getActiveTerminalPanes currently casts pane.data to
TerminalPaneData after only checking pane.kind === "terminal", which can still
allow malformed data to cause runtime errors; add a runtime guard/type predicate
(e.g., isTerminalPaneData) and use it in getActiveTerminalPanes to verify
pane.data has the expected shape (presence and type of terminalId) before
accessing terminalId and consulting terminalRuntimeRegistry; update the function
to accept Pane<PaneViewerData> and return only when the guard passes and
terminalRuntimeRegistry.has(terminalId).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ca77d9ae-c016-46de-a645-8f1ff97a8ba6

📥 Commits

Reviewing files that changed from the base of the PR and between 30ff989 and 1087491.

📒 Files selected for processing (3)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/confirmTerminalClose.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDirtyTabCloseGuard/useDirtyTabCloseGuard.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

Architecture diagram
sequenceDiagram
    participant User as User
    participant TabGuard as Tab Close Guard (useDirtyTabCloseGuard)
    participant PaneGuard as Pane Close Guard (onBeforeClose)
    participant TerminalConfirm as confirmCloseActiveTerminalPanes
    participant Alert as Alert Dialog

    Note over User,Alert: User attempts to close a tab or a terminal pane

    alt Tab close triggered
        User->>TabGuard: close tab (passes all panes in tab)
        TabGuard->>TerminalConfirm: NEW: confirmCloseActiveTerminalPanes(panes)
    else Pane close triggered (terminal pane)
        User->>PaneGuard: close terminal pane (single pane)
        PaneGuard->>TerminalConfirm: NEW: confirmCloseActiveTerminalPanes([pane])
    end

    Note over TerminalConfirm: Filters panes where kind === 'terminal'<br/>and terminalRuntimeRegistry.has(terminalId)

    alt No active terminal panes
        TerminalConfirm-->>TabGuard: true
        TerminalConfirm-->>PaneGuard: true
        Note over TabGuard: Proceeds to existing dirty file check<br/>(unchanged)
        Note over PaneGuard: Pane close allowed
    else One or more active terminal panes
        TerminalConfirm->>Alert: Show confirmation dialog<br/>("Close active terminal session(s)?")
        alt User clicks "Close"
            User->>Alert: Click "Close" (destructive action)
            Alert-->>TerminalConfirm: resolve(true)
            TerminalConfirm-->>TabGuard: true
            TerminalConfirm-->>PaneGuard: true
            Note over TabGuard: Proceeds to existing dirty file check
            Note over PaneGuard: Pane close allowed
        else User clicks "Cancel"
            User->>Alert: Click "Cancel"
            Alert-->>TerminalConfirm: resolve(false)
            TerminalConfirm-->>TabGuard: false
            TerminalConfirm-->>PaneGuard: false
            Note over TabGuard,PaneGuard: Close aborted – no further checks
        end
    end
Loading

Re-trigger cubic

@greptile-apps

greptile-apps Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a close guard that detects active terminal panes and prompts the user before closing a tab or individual terminal pane, preventing accidental loss of in-progress agent sessions. The implementation introduces a shared helper (confirmCloseActiveTerminalPanes) and hooks it into both the tab-level useDirtyTabCloseGuard and the terminal-pane onBeforeClose registry entry.

  • confirmTerminalClose.ts (new): core helper that filters active terminal panes via terminalRuntimeRegistry.has(), builds a count-aware title/description, and resolves a Promise<boolean> through the existing alert dialog. The Promise never resolves if the dialog is dismissed with Escape or a click-outside, which can permanently stall the close sequence.
  • useDirtyTabCloseGuard.ts: callback converted to async, runs the terminal check first then falls through to the existing dirty-file guard.
  • usePaneRegistry.tsx: adds a one-liner onBeforeClose to the terminal pane config.

Confidence Score: 3/5

The close guard logic is wired correctly into both paths, but the confirmation dialog can reach a state where the underlying Promise never settles, permanently blocking the close operation for any user who dismisses the dialog with Escape.

The new helper follows the same Promise-based alert pattern used elsewhere in the codebase, but the Alert component's dismiss path (Escape key / click-outside) calls handleClose() without resolving the promise. Any caller that awaits confirmCloseActiveTerminalPanes will hang indefinitely after such a dismissal, leaving the tab or pane stuck in a non-closeable state until the page is reloaded. The terminalRuntimeRegistry.has() signal also fires for terminals whose process has already exited, which could surface unnecessary prompts.

confirmTerminalClose.ts deserves the most attention — the Promise lifecycle and the active-terminal detection heuristic both have issues that affect the two callers.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/confirmTerminalClose.ts New helper that detects active terminal panes and shows a confirmation dialog; the alert Promise never resolves on Escape-dismiss, which can permanently stall the close sequence.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDirtyTabCloseGuard/useDirtyTabCloseGuard.ts Tab close guard converted to async to await the terminal confirmation before the existing dirty-file check; integration is correct, though it inherits the unresolved-Promise issue from the new helper.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx Adds onBeforeClose to the terminal pane registry entry, delegating to confirmCloseActiveTerminalPanes; one-liner hook-in is clean and consistent with the file-pane pattern.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant TG as useDirtyTabCloseGuard
    participant CT as confirmCloseActiveTerminalPanes
    participant A as Alert (dialog)
    participant PR as usePaneRegistry (onBeforeClose)

    Note over U,PR: Tab close path
    U->>TG: close tab
    TG->>CT: confirmCloseActiveTerminalPanes(tab.panes)
    CT->>CT: getActiveTerminalPanes() via registry.has()
    alt No active terminals
        CT-->>TG: return true (sync)
    else Has active terminals
        CT->>A: "alert({ title, description, actions })"
        A-->>U: Show confirmation dialog
        alt User clicks Close
            U->>A: onClick resolve(true)
            A-->>CT: Promise resolves true
            CT-->>TG: true
            TG->>TG: Check dirty file panes
        else User clicks Cancel
            U->>A: onClick resolve(false)
            A-->>CT: Promise resolves false
            CT-->>TG: false abort close
        else User presses Escape or clicks outside
            U->>A: dismiss (no onClick)
            Note over A,CT: Promise NEVER resolves — close stalls
        end
    end

    Note over U,PR: Direct pane close path
    U->>PR: close terminal pane
    PR->>CT: confirmCloseActiveTerminalPanes([pane])
    CT->>A: alert (if active)
    A-->>U: Show confirmation dialog
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/confirmTerminalClose.ts:34-51
**Promise hangs forever when dialog is dismissed without a button click**

The `Promise<boolean>` created here only resolves when one of the two `onClick` handlers fires. Inspecting `Alert.tsx`, the `handleClose()` path — triggered by pressing Escape or clicking outside the modal — simply sets `isOpen = false` without ever calling any action's `onClick`. The promise is never resolved or rejected in that path.

When this function is called from `useDirtyTabCloseGuard`, the `await` at line 19 will stall indefinitely after an Escape-dismiss, permanently blocking the tab-close sequence. Consider treating a dialog-dismissed-without-action as a cancel (i.e., resolve with `false`) by adding an `onOpenChange` callback or a dedicated dismiss action that calls `resolve(false)`.

### Issue 2 of 2
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/confirmTerminalClose.ts:11
**`terminalRuntimeRegistry.has()` does not verify the terminal process is still running**

`has(terminalId)` returns `true` as long as an entry exists in `entryKeysByTerminalId` — which is true from the moment the pane is mounted until it is explicitly closed. A terminal whose underlying process has already exited (e.g., an agent run that completed) retains its registry entry until `onAfterClose` fires. This means the confirmation prompt will appear even for finished terminals, potentially confusing users who are closing a pane that was already idle. Consider also checking the connection state (`terminalRuntimeRegistry.getConnectionState(terminalId) === "open"`) to limit prompts to terminals with an active WebSocket connection.

Reviews (1): Last reviewed commit: "fix(desktop): confirm before closing act..." | Re-trigger Greptile

Comment on lines +34 to +51
return new Promise<boolean>((resolve) => {
alert({
title,
description,
actions: [
{
label: "Close",
variant: "destructive",
onClick: () => resolve(true),
},
{
label: "Cancel",
variant: "secondary",
onClick: () => resolve(false),
},
],
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Promise hangs forever when dialog is dismissed without a button click

The Promise<boolean> created here only resolves when one of the two onClick handlers fires. Inspecting Alert.tsx, the handleClose() path — triggered by pressing Escape or clicking outside the modal — simply sets isOpen = false without ever calling any action's onClick. The promise is never resolved or rejected in that path.

When this function is called from useDirtyTabCloseGuard, the await at line 19 will stall indefinitely after an Escape-dismiss, permanently blocking the tab-close sequence. Consider treating a dialog-dismissed-without-action as a cancel (i.e., resolve with false) by adding an onOpenChange callback or a dedicated dismiss action that calls resolve(false).

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/confirmTerminalClose.ts
Line: 34-51

Comment:
**Promise hangs forever when dialog is dismissed without a button click**

The `Promise<boolean>` created here only resolves when one of the two `onClick` handlers fires. Inspecting `Alert.tsx`, the `handleClose()` path — triggered by pressing Escape or clicking outside the modal — simply sets `isOpen = false` without ever calling any action's `onClick`. The promise is never resolved or rejected in that path.

When this function is called from `useDirtyTabCloseGuard`, the `await` at line 19 will stall indefinitely after an Escape-dismiss, permanently blocking the tab-close sequence. Consider treating a dialog-dismissed-without-action as a cancel (i.e., resolve with `false`) by adding an `onOpenChange` callback or a dedicated dismiss action that calls `resolve(false)`.

How can I resolve this? If you propose a fix, please make it concise.

if (pane.kind !== "terminal") return false;
const { terminalId } = pane.data as TerminalPaneData;
if (!terminalId) return false;
return terminalRuntimeRegistry.has(terminalId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 terminalRuntimeRegistry.has() does not verify the terminal process is still running

has(terminalId) returns true as long as an entry exists in entryKeysByTerminalId — which is true from the moment the pane is mounted until it is explicitly closed. A terminal whose underlying process has already exited (e.g., an agent run that completed) retains its registry entry until onAfterClose fires. This means the confirmation prompt will appear even for finished terminals, potentially confusing users who are closing a pane that was already idle. Consider also checking the connection state (terminalRuntimeRegistry.getConnectionState(terminalId) === "open") to limit prompts to terminals with an active WebSocket connection.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/confirmTerminalClose.ts
Line: 11

Comment:
**`terminalRuntimeRegistry.has()` does not verify the terminal process is still running**

`has(terminalId)` returns `true` as long as an entry exists in `entryKeysByTerminalId` — which is true from the moment the pane is mounted until it is explicitly closed. A terminal whose underlying process has already exited (e.g., an agent run that completed) retains its registry entry until `onAfterClose` fires. This means the confirmation prompt will appear even for finished terminals, potentially confusing users who are closing a pane that was already idle. Consider also checking the connection state (`terminalRuntimeRegistry.getConnectionState(terminalId) === "open"`) to limit prompts to terminals with an active WebSocket connection.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@aronchick
aronchick force-pushed the fix/confirm-close-active-agents branch from 1087491 to 8e94c8c Compare June 5, 2026 00:34
@aronchick

Copy link
Copy Markdown
Contributor Author

Updated for review feedback:

  • Removed the promise-based alert path so Escape/outside-dismiss cannot leave close handling hung.
  • Limited prompts to terminals with an open runtime connection instead of any registered terminal runtime.
  • Re-ran focused Biome check on changed files.

@Kitenite

Copy link
Copy Markdown
Member

Closing: main already ships this. e3f23ee (#5517) confirm-close-terminals.ts in v2. Thanks for the PR; if you think the shipped version misses a case, open an issue and link this.

@Kitenite Kitenite closed this Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants