diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx
index b9765d914..81f72be49 100644
--- a/src/renderer/src/App.tsx
+++ b/src/renderer/src/App.tsx
@@ -207,6 +207,7 @@ import { showTerminalShortcutCaptureNotification } from '@/lib/terminal-shortcut
import { resolveMountedLazyModalIds, type LazyModalId } from './lazy-modal-mount-state'
import { translate } from '@/i18n/i18n'
import PinnedTabCloseDialog from './components/terminal-pane/PinnedTabCloseDialog'
+import RunningTerminalCloseDialog from './components/terminal-pane/RunningTerminalCloseDialog'
import WorktreeBaseFallbackDialog from './components/WorktreeBaseFallbackDialog'
import { useOsc52ClipboardDefaultOnNotice } from './components/terminal-pane/osc52-clipboard-default-on-notice'
import {
@@ -2789,6 +2790,7 @@ function App(): React.JSX.Element {
+
{/* Why: Electron's drag-region hit-test is DOM-order-based (ignores z-index); render last so WindowControls stay clickable. */}
{hasCustomTitleBar && }
diff --git a/src/renderer/src/components/Terminal.tsx b/src/renderer/src/components/Terminal.tsx
index 6eaf0afc7..dc39c3a06 100644
--- a/src/renderer/src/components/Terminal.tsx
+++ b/src/renderer/src/components/Terminal.tsx
@@ -1797,7 +1797,8 @@ function Terminal(): React.JSX.Element | null {
) {
if (unifiedTab.contentType === 'terminal') {
// Why: paired-host bulk close must revoke renderer resume and hook authority, not just remove the host session tab.
- closeTerminalTab(unifiedTab.entityId)
+ // No running-process prompt: "Close Others" over N busy tabs would be a modal storm.
+ closeTerminalTab(unifiedTab.entityId, { skipRunningProcessConfirm: true })
} else {
void closeWebRuntimeSessionTab({
worktreeId: activeWorktreeId,
diff --git a/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts b/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts
index 4bb234b97..f493f2e58 100644
--- a/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts
+++ b/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts
@@ -235,10 +235,12 @@ export function useTabGroupWorkspaceModel({
worktreeId
)
if (item.contentType === 'terminal') {
- closeTerminalTab(item.entityId)
- if (!opts?.skipEmptyCheck) {
- leaveWorktreeIfEmpty()
- }
+ // Why: closeTerminalTab can defer behind a pin / running-process dialog, so the
+ // empty check has to run on the actual close — never on cancel.
+ closeTerminalTab(
+ item.entityId,
+ opts?.skipEmptyCheck ? undefined : { onClosed: leaveWorktreeIfEmpty }
+ )
return
}
if (item.contentType === 'browser') {
@@ -296,7 +298,8 @@ export function useTabGroupWorkspaceModel({
)
if (item.contentType === 'terminal' && isWebRuntimeSessionActive(runtimeEnvironmentId)) {
// Why: revoke local resume + hook authority before the host removes its canonical tab.
- closeTerminalTab(item.entityId)
+ // No running-process prompt: a bulk close of N busy tabs would be a modal storm.
+ closeTerminalTab(item.entityId, { skipRunningProcessConfirm: true })
continue
}
if (item.contentType === 'browser') {
diff --git a/src/renderer/src/components/terminal-pane/CloseTerminalDialog.tsx b/src/renderer/src/components/terminal-pane/CloseTerminalDialog.tsx
index 771d5e15e..c4da52444 100644
--- a/src/renderer/src/components/terminal-pane/CloseTerminalDialog.tsx
+++ b/src/renderer/src/components/terminal-pane/CloseTerminalDialog.tsx
@@ -17,17 +17,26 @@ export type CloseTerminalDialogCopyKind = 'command' | 'agent'
export default function CloseTerminalDialog({
open,
copyKind = 'command',
+ tabLabel,
+ subjectKey,
onCancel,
onConfirm
}: {
open: boolean
copyKind?: CloseTerminalDialogCopyKind
+ /** Names the tab when the prompt can target a tab the user is not looking at
+ * (tab-strip X, middle-click). Omitted for the focused-pane keyboard path. */
+ tabLabel?: string
+ /** Identifies what is being closed, for hosts that reuse one open dialog across a queue
+ * of confirmations. Changing it clears the previous subject's "don't ask again" tick. */
+ subjectKey?: string
onCancel: () => void
onConfirm: (dontAskAgain: boolean) => void
}): React.JSX.Element {
const checkboxId = useId()
const [dontAskAgain, setDontAskAgain] = useState(false)
const [previousOpen, setPreviousOpen] = useState(open)
+ const [previousSubjectKey, setPreviousSubjectKey] = useState(subjectKey)
// Why: each reopen represents a fresh confirmation, so clear the old choice
// during render rather than briefly painting it while the dialog opens.
@@ -38,7 +47,18 @@ export default function CloseTerminalDialog({
}
}
+ // Why: a queued confirmation swaps the subject without ever closing the dialog, so the
+ // reopen reset above never fires. Ignore the swap to undefined as the dialog closes —
+ // clearing the tick mid-exit-animation would be visible for no reason.
+ if (subjectKey !== previousSubjectKey) {
+ setPreviousSubjectKey(subjectKey)
+ if (subjectKey !== undefined) {
+ setDontAskAgain(false)
+ }
+ }
+
const isAgent = copyKind === 'agent'
+ const trimmedTabLabel = tabLabel?.trim()
return (