From eaabaad3d3c9e63141b813a23fbf9b01283377b3 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Wed, 25 Feb 2026 03:54:51 -0800 Subject: [PATCH] Add Cmd+Option+W to close other pane tabs with confirmation (#475) * Add Cmd+Option+W close-other-tabs confirmation * Match close-other-tabs shortcut to Cmd+Option+T --- Sources/AppDelegate.swift | 20 +++ Sources/TabManager.swift | 69 +++++++++ Sources/cmuxApp.swift | 10 ++ ...t_cmd_option_t_close_other_tabs_in_pane.py | 140 ++++++++++++++++++ 4 files changed, 239 insertions(+) create mode 100644 tests/test_cmd_option_t_close_other_tabs_in_pane.py diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index b4568427..70d3e4af 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -4301,6 +4301,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent guard panel.isVisible, let root = panel.contentView else { return false } return findStaticText(in: root, equals: "Close workspace?") || findStaticText(in: root, equals: "Close tab?") + || findStaticText(in: root, equals: "Close other tabs?") } if let closeConfirmationPanel { // Special-case: Cmd+D should confirm destructive close on alerts. @@ -4563,6 +4564,25 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return true } + if normalizedFlags == [.command, .option], (chars == "t" || event.keyCode == 17) { + if let targetWindow = event.window ?? NSApp.keyWindow ?? NSApp.mainWindow, + targetWindow.identifier?.rawValue == "cmux.settings" { + targetWindow.performClose(nil) + } else { + let responder = event.window?.firstResponder + ?? NSApp.keyWindow?.firstResponder + ?? NSApp.mainWindow?.firstResponder + if let ghosttyView = cmuxOwningGhosttyView(for: responder), + let workspaceId = ghosttyView.tabId, + let manager = tabManagerFor(tabId: workspaceId) ?? tabManager { + manager.closeOtherTabsInFocusedPaneWithConfirmation() + } else { + tabManager?.closeOtherTabsInFocusedPaneWithConfirmation() + } + } + return true + } + // Cmd+W must close the focused panel even if first-responder momentarily lags on a // browser NSTextView during split focus transitions. if normalizedFlags == [.command], (chars == "w" || event.keyCode == 13) { diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index b88f8cc9..e3d7efaa 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -1050,6 +1050,27 @@ class TabManager: ObservableObject { closePanelWithConfirmation(tab: tab, panelId: focusedPanelId) } + func canCloseOtherTabsInFocusedPane() -> Bool { + closeOtherTabsInFocusedPanePlan() != nil + } + + func closeOtherTabsInFocusedPaneWithConfirmation() { + guard let plan = closeOtherTabsInFocusedPanePlan() else { return } + + let count = plan.panelIds.count + let titleLines = plan.titles.map { "• \($0)" }.joined(separator: "\n") + let message = "This is about to close \(count) tab\(count == 1 ? "" : "s") in this pane:\n\(titleLines)" + guard confirmClose( + title: "Close other tabs?", + message: message, + acceptCmdD: false + ) else { return } + + for panelId in plan.panelIds { + _ = plan.workspace.closePanel(panelId, force: true) + } + } + func closeCurrentWorkspaceWithConfirmation() { #if DEBUG UITestRecorder.incrementInt("closeTabInvocations") @@ -1097,6 +1118,54 @@ class TabManager: ObservableObject { return alert.runModal() == .alertFirstButtonReturn } + private struct CloseOtherTabsInFocusedPanePlan { + let workspace: Workspace + let panelIds: [UUID] + let titles: [String] + } + + private func closeOtherTabsInFocusedPanePlan() -> CloseOtherTabsInFocusedPanePlan? { + guard let workspace = selectedWorkspace else { return nil } + guard let paneId = workspace.bonsplitController.focusedPaneId ?? workspace.bonsplitController.allPaneIds.first else { + return nil + } + + let tabsInPane = workspace.bonsplitController.tabs(inPane: paneId) + guard !tabsInPane.isEmpty else { return nil } + guard let selectedTabId = workspace.bonsplitController.selectedTab(inPane: paneId)?.id ?? tabsInPane.first?.id else { + return nil + } + + var targetPanelIds: [UUID] = [] + var targetTitles: [String] = [] + for tab in tabsInPane where tab.id != selectedTabId { + guard let panelId = workspace.panelIdFromSurfaceId(tab.id) else { continue } + if workspace.isPanelPinned(panelId) { + continue + } + targetPanelIds.append(panelId) + targetTitles.append(closeOtherTabsDisplayTitle(workspace.panelTitle(panelId: panelId))) + } + + guard !targetPanelIds.isEmpty else { return nil } + return CloseOtherTabsInFocusedPanePlan( + workspace: workspace, + panelIds: targetPanelIds, + titles: targetTitles + ) + } + + private func closeOtherTabsDisplayTitle(_ title: String?) -> String { + let collapsed = title? + .replacingOccurrences(of: "\n", with: " ") + .replacingOccurrences(of: "\r", with: " ") + .trimmingCharacters(in: .whitespacesAndNewlines) + if let collapsed, !collapsed.isEmpty { + return collapsed + } + return "Untitled Tab" + } + private func closeWorkspaceIfRunningProcess(_ workspace: Workspace) { let willCloseWindow = tabs.count <= 1 if workspaceNeedsConfirmClose(workspace), diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index 2cad848f..9a151237 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -409,6 +409,12 @@ struct cmuxApp: App { } .keyboardShortcut("w", modifiers: .command) + Button("Close Other Tabs in Pane") { + closeOtherTabsInFocusedPane() + } + .keyboardShortcut("t", modifiers: [.command, .option]) + .disabled(!activeTabManager.canCloseOtherTabsInFocusedPane()) + // Cmd+Shift+W closes the current workspace (with confirmation if needed). If this // is the last workspace, it closes the window. splitCommandButton(title: "Close Workspace", shortcut: closeWorkspaceMenuShortcut) { @@ -784,6 +790,10 @@ struct cmuxApp: App { activeTabManager.closeCurrentPanelWithConfirmation() } + private func closeOtherTabsInFocusedPane() { + activeTabManager.closeOtherTabsInFocusedPaneWithConfirmation() + } + private func closeTabOrWindow() { activeTabManager.closeCurrentTabWithConfirmation() } diff --git a/tests/test_cmd_option_t_close_other_tabs_in_pane.py b/tests/test_cmd_option_t_close_other_tabs_in_pane.py new file mode 100644 index 00000000..778900a2 --- /dev/null +++ b/tests/test_cmd_option_t_close_other_tabs_in_pane.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +""" +Regression test: Cmd+Option+T closes all other tabs in the focused pane +after an explicit confirmation. + +Run this against an app launched with CMUX_SOCKET_MODE=allowAll. +""" + +import os +import subprocess +import sys +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) +from cmux import cmux, cmuxError + + +SOCKET_PATH = os.environ.get("CMUX_SOCKET", "/tmp/cmux-debug.sock") + + +def _wait_until(predicate, timeout_s: float = 5.0, interval_s: float = 0.05) -> bool: + start = time.time() + while time.time() - start < timeout_s: + if predicate(): + return True + time.sleep(interval_s) + return False + + +def _pane_state(client: cmux) -> list[dict]: + rows: list[dict] = [] + for index, panel_id, title, selected in client.list_pane_surfaces(): + rows.append( + { + "index": index, + "panel_id": panel_id, + "title": title, + "selected": selected, + } + ) + return rows + + +def _send_shortcut_via_system_events(key: str, modifiers: str) -> None: + script = f'tell application "System Events" to keystroke "{key}" using {{{modifiers}}}' + try: + subprocess.run(["osascript", "-e", script], check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as exc: + stderr = (exc.stderr or "").strip() + raise cmuxError( + "Failed to send keyboard shortcut via System Events. " + f"Ensure macOS Accessibility automation is enabled. stderr={stderr}" + ) from exc + + +def main() -> int: + with cmux(SOCKET_PATH) as client: + if not client.ping(): + raise cmuxError( + f"Socket ping failed on {SOCKET_PATH}. " + "Launch Debug app with CMUX_SOCKET_MODE=allowAll for this test." + ) + + workspace_id = client.new_workspace() + try: + client.select_workspace(workspace_id) + time.sleep(0.25) + client.activate_app() + time.sleep(0.15) + + # Create two additional tabs in the current focused pane. + client.new_surface() + client.new_surface() + time.sleep(0.25) + + before = _pane_state(client) + if len(before) < 3: + raise cmuxError(f"Expected >=3 tabs before shortcut, got {before}") + + selected_rows = [row for row in before if row["selected"]] + if len(selected_rows) != 1: + raise cmuxError(f"Expected exactly one selected tab before shortcut, got {before}") + selected_panel_id = selected_rows[0]["panel_id"] + + expected_to_close = [row for row in before if row["panel_id"] != selected_panel_id] + if len(expected_to_close) < 2: + raise cmuxError( + f"Expected at least two non-selected tabs before shortcut, got {before}" + ) + + # Trigger shortcut via real OS key event; this should open the confirmation dialog. + _send_shortcut_via_system_events("t", "command down, option down") + time.sleep(0.25) + after_trigger = _pane_state(client) + if len(after_trigger) != len(before): + raise cmuxError( + "Cmd+Option+T should require confirmation before closing.\n" + f"before={before}\n" + f"after_trigger={after_trigger}" + ) + + # Confirm the dialog with Cmd+D (wired to click the destructive "Close" button). + _send_shortcut_via_system_events("d", "command down") + closed = _wait_until(lambda: len(_pane_state(client)) == 1, timeout_s=5.0, interval_s=0.05) + if not closed: + raise cmuxError( + "Timed out waiting for tabs to close after confirming Cmd+Option+T.\n" + f"before={before}\n" + f"after_trigger={after_trigger}\n" + f"after_confirm={_pane_state(client)}" + ) + + after_confirm = _pane_state(client) + if len(after_confirm) != 1: + raise cmuxError( + f"Expected one remaining tab after confirmation, got {after_confirm}" + ) + remaining = after_confirm[0] + if remaining["panel_id"] != selected_panel_id: + raise cmuxError( + "Expected selected tab to remain after closing others.\n" + f"expected_selected={selected_panel_id}\n" + f"remaining={remaining}\n" + f"before={before}" + ) + + print("PASS: Cmd+Option+T closed all other tabs in focused pane.") + print(f"workspace={workspace_id}") + print(f"selected_panel={selected_panel_id}") + return 0 + finally: + try: + client.close_workspace(workspace_id) + except Exception: + pass + + +if __name__ == "__main__": + raise SystemExit(main())