From 2ab6fddb256d0ea7b5b7803dff860738768fb4b8 Mon Sep 17 00:00:00 2001
From: yoziv <59967231+yoziv@users.noreply.github.com>
Date: Mon, 13 Jul 2026 19:52:21 +0300
Subject: [PATCH] feat: confirm before closing a tab/pane session
Add an opt-in "Confirm before closing" setting so an accidental tab-X
click or middle-click no longer kills a live terminal/AI session.
- New confirmOnCloseSession setting (default off) in terminal-store,
persisted via config and toggled from Settings > Tabs.
- closeTerminal gains an opts.skipConfirm; when the setting is on it
shows the existing tmax confirmDialog (danger) naming the session.
- New closeTerminals(ids) so bulk closes (Close All / Close Others /
close group / multi-select) show one aggregate confirm instead of
one dialog per tab.
Covers X-click, middle-click, context-menu close, Ctrl+Shift+W, and
command palette since they all route through closeTerminal.
Backlog: TASK-276
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 928d2772-f9cd-42d4-864a-24d999bd1328
---
...nfirm-before-closing-a-tab-pane-session.md | 31 +++++++++++
src/renderer/components/Settings.tsx | 7 +++
src/renderer/components/TabBar.tsx | 2 +-
src/renderer/components/TabContextMenu.tsx | 6 +--
src/renderer/hooks/useKeybindings.ts | 2 +-
src/renderer/state/terminal-store.ts | 54 ++++++++++++++++++-
6 files changed, 95 insertions(+), 7 deletions(-)
create mode 100644 backlog/tasks/task-276 - Confirm-before-closing-a-tab-pane-session.md
diff --git a/backlog/tasks/task-276 - Confirm-before-closing-a-tab-pane-session.md b/backlog/tasks/task-276 - Confirm-before-closing-a-tab-pane-session.md
new file mode 100644
index 00000000..1d707173
--- /dev/null
+++ b/backlog/tasks/task-276 - Confirm-before-closing-a-tab-pane-session.md
@@ -0,0 +1,31 @@
+---
+id: TASK-276
+title: Confirm before closing a tab/pane session
+status: In Progress
+assignee:
+ - yoziv
+created_date: '2026-07-13 16:18'
+updated_date: '2026-07-13 16:20'
+labels: []
+dependencies: []
+---
+
+## Description
+
+
+User accidentally clicks the tab close X (or middle-clicks) when reaching for a tab to return to it, losing a live terminal/AI session. Add an opt-in confirmation dialog before closing a tab/pane, reusing the existing tmax-styled confirmDialog (AppDialog.tsx, TASK-115). Bulk closes (Close All/Others/group) get a single aggregate confirm, not one per tab.
+
+
+## Acceptance Criteria
+
+- [ ] #1 A new opt-in setting (default off) gates confirm-before-close; when off, closing is unchanged
+- [ ] #2 With the setting on, clicking the tab X or middle-clicking a tab shows a confirm dialog naming the session; Cancel keeps the session, Close removes it
+- [ ] #3 Bulk closes (Close All / Close Others / close group / multi-select close) show a single aggregate confirm, never one dialog per tab
+- [ ] #4 Setting persists across restarts and has a toggle in Settings > Tabs
+
+
+## Implementation Plan
+
+
+1. Add opt-in setting confirmOnCloseSession (default false) in terminal-store: state field, default value, config load, and toggleConfirmOnCloseSession action - mirroring hideTabCloseButtons.
+
diff --git a/src/renderer/components/Settings.tsx b/src/renderer/components/Settings.tsx
index 585245d0..33daf516 100644
--- a/src/renderer/components/Settings.tsx
+++ b/src/renderer/components/Settings.tsx
@@ -775,6 +775,13 @@ const AppearanceSettings: React.FC = () => {
+
+
+
= ({ ver
.filter(([, t]) => t.groupId === groupMenu.groupId)
.map(([id]) => id);
store.deleteTabGroup(groupMenu.groupId);
- (async () => { for (const id of ids) await store.closeTerminal(id); })();
+ store.closeTerminals(ids);
setGroupMenu(null);
}}>
Close All
diff --git a/src/renderer/components/TabContextMenu.tsx b/src/renderer/components/TabContextMenu.tsx
index c91fc916..4cf08fba 100644
--- a/src/renderer/components/TabContextMenu.tsx
+++ b/src/renderer/components/TabContextMenu.tsx
@@ -447,21 +447,21 @@ const TabContextMenu: React.FC = ({ position, selectedAtOpe
: [position.terminalId];
onClose();
useTerminalStore.getState().clearSelection();
- (async () => { for (const id of ids) await useTerminalStore.getState().closeTerminal(id); })();
+ useTerminalStore.getState().closeTerminals(ids);
}}>
Close{targetIds.length > 1 ? ` (${targetIds.length})` : ''} {formatKeyForPlatform('Ctrl+Shift+W')}
diff --git a/src/renderer/hooks/useKeybindings.ts b/src/renderer/hooks/useKeybindings.ts
index 2fbf6672..77fec7f2 100644
--- a/src/renderer/hooks/useKeybindings.ts
+++ b/src/renderer/hooks/useKeybindings.ts
@@ -238,7 +238,7 @@ function dispatchAction(action: string): void {
: (focusedId ? [focusedId] : []);
if (ids.length === 0) break;
if (sel.length > 0) store.clearSelection();
- (async () => { for (const id of ids) await store.closeTerminal(id); })();
+ store.closeTerminals(ids);
break;
}
case 'restoreClosedTerminal':
diff --git a/src/renderer/state/terminal-store.ts b/src/renderer/state/terminal-store.ts
index f7359604..672efce8 100644
--- a/src/renderer/state/terminal-store.ts
+++ b/src/renderer/state/terminal-store.ts
@@ -897,6 +897,10 @@ interface TerminalStore {
tabBarPosition: 'top' | 'bottom' | 'left' | 'right';
hideTabTitles: boolean;
hideTabCloseButtons: boolean;
+ // When true, closing a tab/pane asks for confirmation first so an
+ // accidental X-click or middle-click doesn't kill a live session. Bulk
+ // closes collapse to a single aggregate confirm. Opt-in; default off.
+ confirmOnCloseSession: boolean;
renamingTerminalId: TerminalId | null;
viewMode: 'split' | 'focus' | 'grid';
broadcastMode: boolean; // when true, typing in any pane is sent to all tiled panes
@@ -988,7 +992,10 @@ interface TerminalStore {
// Actions
loadConfig: () => Promise;
createTerminal: (shellProfileId?: string, cwdOverride?: string) => Promise;
- closeTerminal: (id: TerminalId) => Promise;
+ closeTerminal: (id: TerminalId, opts?: { skipConfirm?: boolean }) => Promise;
+ // Close many tabs at once with a single aggregate confirm (instead of
+ // one dialog per tab). Used by Close All / Close Others / close group.
+ closeTerminals: (ids: TerminalId[]) => Promise;
replaceTerminal: (id: TerminalId, shellProfileId?: string) => Promise;
/**
* Browser-style undo close (TASK-112). Pops the most recent entry off
@@ -1034,6 +1041,7 @@ interface TerminalStore {
toggleTabBarPosition: () => void;
toggleHideTabTitles: () => void;
toggleHideTabCloseButtons: () => void;
+ toggleConfirmOnCloseSession: () => void;
setTerminalOpacity: (opacity: number) => void;
startRenaming: (id: TerminalId | null) => void;
toggleViewMode: () => void;
@@ -1376,6 +1384,7 @@ export const useTerminalStore = create((set, get) => ({
tabBarPosition: 'top' as 'top' | 'bottom' | 'left' | 'right',
hideTabTitles: false,
hideTabCloseButtons: false,
+ confirmOnCloseSession: false,
renamingTerminalId: null,
viewMode: 'grid' as 'split' | 'focus' | 'grid',
broadcastMode: false,
@@ -1399,6 +1408,7 @@ export const useTerminalStore = create((set, get) => ({
if (config?.tabBarPosition) updates.tabBarPosition = config.tabBarPosition;
if (typeof (config as any)?.hideTabTitles === 'boolean') updates.hideTabTitles = (config as any).hideTabTitles;
if (typeof (config as any)?.hideTabCloseButtons === 'boolean') updates.hideTabCloseButtons = (config as any).hideTabCloseButtons;
+ if (typeof (config as any)?.confirmOnCloseSession === 'boolean') updates.confirmOnCloseSession = (config as any).confirmOnCloseSession;
if ((config as any)?.terminalOpacity != null) {
updates.terminalOpacity = (config as any).terminalOpacity;
document.documentElement.style.setProperty('--terminal-opacity', String((config as any).terminalOpacity));
@@ -1613,12 +1623,26 @@ export const useTerminalStore = create((set, get) => ({
get().saveSession();
},
- closeTerminal: async (id: TerminalId) => {
+ closeTerminal: async (id: TerminalId, opts?: { skipConfirm?: boolean }) => {
const t0 = performance.now();
const { terminals, layout, focusedTerminalId, closedTerminals, copilotSessions, claudeCodeSessions } = get();
const instance = terminals.get(id);
if (!instance) return;
+ // Guard against an accidental X-click / middle-click killing a live
+ // session. skipConfirm lets bulk closers show one aggregate confirm
+ // instead of one dialog per pane (see closeTerminals).
+ if (get().confirmOnCloseSession && !opts?.skipConfirm) {
+ const label = instance.title?.trim() || 'this session';
+ const ok = await confirmDialog({
+ title: 'Close session?',
+ message: `Close "${label}"? This ends the terminal session.`,
+ confirmText: 'Close',
+ danger: true,
+ });
+ if (!ok) return;
+ }
+
// Snapshot pane identity for browser-style undo close (TASK-112).
// Capture BEFORE the PTY is killed so the metadata is intact even if
// killPty surfaces an error. Cap at 10 - older entries fall off the
@@ -1722,6 +1746,26 @@ export const useTerminalStore = create((set, get) => ({
});
},
+ closeTerminals: async (ids: TerminalId[]) => {
+ if (ids.length === 0) return;
+ // A single tab still names itself; let closeTerminal own that confirm.
+ if (ids.length === 1) {
+ await get().closeTerminal(ids[0]);
+ return;
+ }
+ // Many tabs: one aggregate confirm, then close each without re-asking.
+ if (get().confirmOnCloseSession) {
+ const ok = await confirmDialog({
+ title: 'Close sessions?',
+ message: `Close ${ids.length} sessions? This ends all of them.`,
+ confirmText: `Close ${ids.length}`,
+ danger: true,
+ });
+ if (!ok) return;
+ }
+ for (const id of ids) await get().closeTerminal(id, { skipConfirm: true });
+ },
+
restoreClosedTerminal: async () => {
const { closedTerminals } = get();
if (closedTerminals.length === 0) return;
@@ -2646,6 +2690,12 @@ export const useTerminalStore = create((set, get) => ({
get().updateConfig({ hideTabCloseButtons: val } as any);
},
+ toggleConfirmOnCloseSession: () => {
+ const val = !get().confirmOnCloseSession;
+ set({ confirmOnCloseSession: val });
+ get().updateConfig({ confirmOnCloseSession: val } as any);
+ },
+
setTerminalOpacity: (opacity: number) => {
const clamped = Math.max(0.3, Math.min(1, opacity));
set({ terminalOpacity: clamped });