Skip to content

Summarize multi-workspace close confirmation - #1329

Merged
lawrencecchen merged 4 commits into
mainfrom
task-multi-workspace-close-confirmation
Mar 13, 2026
Merged

lawrencecchen merged 4 commits into
mainfrom
task-multi-workspace-close-confirmation

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Mar 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add a shared multi-workspace close path that shows one confirmation dialog with a summary of what will be closed
  • route command palette, workspace menu, and sidebar multi-select close actions through the shared confirmation flow
  • add regression coverage for the command-palette multi-close dialog and the shared close logic

Testing

  • ./scripts/reload.sh --tag task-multi-workspace-close-confirmation (build succeeded)
  • Not run locally: UI and unit tests are left for CI per repo policy

Issues

  • Task: closing multiple workspaces from the command palette, workspace menu, or multi-select sidebar should show one summary confirmation dialog

Summary by cubic

Show one confirmation dialog when closing multiple workspaces, with a clear summary of what will be closed across the command palette, workspace menu, and sidebar multi-select. Meets the Linear task requirement and reduces repeated prompts, including the “Close window?” case when all workspaces are selected.

  • New Features
    • Added shared flow via TabManager.closeWorkspacesWithConfirmation(...) that aggregates workspace titles and localizes messages (en/ja).
    • Uses “Close window?” when all workspaces in the window are being closed; preserves Cmd+D acceptance for window-close.
    • Cmd+Shift+W now honors sidebar multi-select and shows the same single summary dialog; command palette and workspace menu paths are routed through it.
    • Synced sidebar selection to TabManager for ordered multi-close, and updated key-equivalent suppression to include “Close workspaces?”.
    • Added unit and UI tests for multi-close (including Cmd+Shift+W and command palette paths).

Written for commit c8770d1. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Close multiple workspaces with a single confirmation prompt (batch close) and preserved pinned handling
    • Sidebar multi-selection support for multi-workspace close flows
  • Documentation

    • Expanded multilingual/localized strings for workspace, tab, update, and control UI (dialogs, prompts, status messages, developer tool labels)
  • Tests

    • New unit and UI tests covering aggregated close-confirmation behavior and related end-to-end flows

@vercel

vercel Bot commented Mar 13, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 13, 2026 10:18am

@greptile-apps greptile-apps 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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented Mar 13, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a batch workspace-closing flow: a public TabManager API to close multiple workspaces with a single confirmation, UI integration and selection-sync helpers in ContentView, AppDelegate title recognition for the new dialog, localized strings, and unit + UI tests covering the aggregated confirmation behavior.

Changes

Cohort / File(s) Summary
Localization
Resources/Localizable.xcstrings
Added ~51 localized strings for workspace/tab/window close dialogs, update messages, developer tools, and control-mode labels.
App close-dialog titles
Sources/AppDelegate.swift
Extended recognized close-dialog titles to include the "Close workspaces?" prompt.
UI / Selection sync
Sources/ContentView.swift
Added sidebar-selection sync and UI-test hooks, guarded state for one-time test application, and replaced per-workspace close calls with batch API invocation.
App launch wiring
Sources/cmuxApp.swift
Replaced iterative close loop with a single call to TabManager.closeWorkspacesWithConfirmation(...).
Batch close API & internals
Sources/TabManager.swift
Introduced closeWorkspacesWithConfirmation(_:allowPinned:), confirmCloseHandler, sidebar-selected tracking, CloseWorkspacesPlan and ordering helpers, and adjusted per-workspace close to support skip-confirmation for batch flow.
Unit tests
cmuxTests/CmuxWebViewKeyEquivalentTests.swift
Added TabManagerCloseWorkspacesWithConfirmationTests with tests validating single-prompt behavior, cancellation retention, and sidebar multi-selection usage.
UI tests
cmuxUITests/CloseWorkspacesConfirmDialogUITests.swift
Added end-to-end UI tests using a control socket, workspace polling, command invocation, and aggregated-close alert detection/cancellation utilities.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as ContentView
    participant TM as TabManager
    participant Dialog as Confirmation Dialog
    participant Window as App/Window

    User->>UI: Select multiple workspaces & invoke close
    UI->>TM: closeWorkspacesWithConfirmation(workspaceIds, allowPinned)
    TM->>TM: orderedClosableWorkspaces() / build CloseWorkspacesPlan
    TM->>Dialog: request single aggregated confirmation
    alt user accepts
        Dialog->>TM: confirmed
        TM->>TM: closeWorkspaceIfRunningProcess(..., requiresConfirmation: false) for each
        TM->>Window: remove workspace(s)
        Window->>UI: update state/UI
    else user cancels
        Dialog->>TM: cancelled
        TM->>UI: no changes
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I nibbled tabs and counted stacks,
One gentle dialog rests on my tracks.
No repeated asks, just a single cheer —
Close many at once, the burrow's clear! 🥕✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the main changes and testing approach, but is missing demo video and incomplete checklist items required by the template. Add a demo video URL and complete the checklist items, particularly confirming local testing and bot review requests were made.
Docstring Coverage ⚠️ Warning Docstring coverage is 4.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Summarize multi-workspace close confirmation' accurately reflects the main change—introducing a consolidated confirmation dialog for closing multiple workspaces.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch task-multi-workspace-close-confirmation
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmuxUITests/CloseWorkspacesConfirmDialogUITests.swift`:
- Around line 175-181: The element queries in closeWorkspacesDialog and
closeWorkspacesAlert are matching an accessibility identifier but the UI shows
the localized visible text; replace the containing(.staticText, identifier:
"Close workspaces?") calls in both closeWorkspacesDialog and
closeWorkspacesAlert with element(matching: NSPredicate(format: "ANY
staticTexts.label == %@", "Close workspaces?")) so the query matches by visible
label text rather than identifier (keep .firstMatch).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 510a2d35-8e8e-4b41-98ac-e1536a875630

📥 Commits

Reviewing files that changed from the base of the PR and between 01a4797 and f0d15b9.

📒 Files selected for processing (7)
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/TabManager.swift
  • Sources/cmuxApp.swift
  • cmuxTests/CmuxWebViewKeyEquivalentTests.swift
  • cmuxUITests/CloseWorkspacesConfirmDialogUITests.swift

Comment on lines +175 to +181
private func closeWorkspacesDialog(app: XCUIApplication) -> XCUIElement {
app.dialogs.containing(.staticText, identifier: "Close workspaces?").firstMatch
}

private func closeWorkspacesAlert(app: XCUIApplication) -> XCUIElement {
app.alerts.containing(.staticText, identifier: "Close workspaces?").firstMatch
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for how the close-workspaces dialog title is set (localized string key)
rg -n "closeWorkspaces.title\|Close workspaces" --type swift -g '!**/cmuxUITests/**' | head -20

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Read the test file to see context around lines 175-181
cat -n cmuxUITests/CloseWorkspacesConfirmDialogUITests.swift | sed -n '160,190p'

Repository: manaflow-ai/cmux

Length of output: 1027


🏁 Script executed:

# Search for where the close workspaces dialog/alert is implemented in the app code
rg -n "Close workspaces" --type swift | head -30

Repository: manaflow-ai/cmux

Length of output: 873


🏁 Script executed:

# Look for how dialogs/alerts are created in the codebase to understand the pattern
rg -n "\.dialogs\.|\.alerts\." --type swift -B 2 -A 2 | head -40

Repository: manaflow-ai/cmux

Length of output: 3884


Element locator should match by visible text, not accessibility identifier.

The containing(.staticText, identifier:) query parameter matches accessibility identifiers, but "Close workspaces?" is visible text from a localized string (defined in app code as String(localized: "dialog.closeWorkspaces.title", defaultValue: "Close workspaces?")). Line 144 of the same file demonstrates the correct approach: app.staticTexts["Close workspaces?"] directly accesses by text content. Update lines 176 and 180 to use text-based matching with a predicate:

private func closeWorkspacesDialog(app: XCUIApplication) -> XCUIElement {
    app.dialogs.element(matching: NSPredicate(format: "ANY staticTexts.label == %@", "Close workspaces?")).firstMatch
}

private func closeWorkspacesAlert(app: XCUIApplication) -> XCUIElement {
    app.alerts.element(matching: NSPredicate(format: "ANY staticTexts.label == %@", "Close workspaces?")).firstMatch
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxUITests/CloseWorkspacesConfirmDialogUITests.swift` around lines 175 -
181, The element queries in closeWorkspacesDialog and closeWorkspacesAlert are
matching an accessibility identifier but the UI shows the localized visible
text; replace the containing(.staticText, identifier: "Close workspaces?") calls
in both closeWorkspacesDialog and closeWorkspacesAlert with element(matching:
NSPredicate(format: "ANY staticTexts.label == %@", "Close workspaces?")) so the
query matches by visible label text rather than identifier (keep .firstMatch).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0d15b96eb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/AppDelegate.swift
// equivalents working and avoid surprising actions while the confirmation is up.
let closeConfirmationTitles = [
String(localized: "dialog.closeWorkspace.title", defaultValue: "Close workspace?"),
String(localized: "dialog.closeWorkspaces.title", defaultValue: "Close workspaces?"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Respect acceptCmdD for multi-workspace close alerts

Including dialog.closeWorkspaces.title in closeConfirmationTitles causes handleCustomShortcut to route Cmd+D to the alert's Close button unconditionally (the matchShortcut(... "d") branch), even when the alert did not opt into Cmd+D. For non-window multi-workspace closes, closeWorkspacesPlan sets acceptCmdD to false, so pressing Cmd+D on that new summary dialog can unexpectedly confirm a destructive close instead of doing nothing.

Useful? React with 👍 / 👎.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 7 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/AppDelegate.swift">

<violation number="1" location="Sources/AppDelegate.swift:7744">
P1: Gate Cmd+D handling on the dialog’s `acceptCmdD` intent instead of title matching. Including the multi-workspace title here allows Cmd+D to confirm non-window multi-workspace closes even when that confirmation flow explicitly disables Cmd+D.</violation>
</file>

<file name="cmuxTests/CmuxWebViewKeyEquivalentTests.swift">

<violation number="1" location="cmuxTests/CmuxWebViewKeyEquivalentTests.swift:5091">
P3: These tests assume `addWorkspace()` inserts after the current tab, but `addWorkspace` uses the user’s `newWorkspacePlacement` preference to choose the insert index. If the preference is `.top` or `.end`, `tabs[0]` and the bullet list order change, making the expected message flaky. Pass a placement override (or set the default) so the order is deterministic.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/AppDelegate.swift
// equivalents working and avoid surprising actions while the confirmation is up.
let closeConfirmationTitles = [
String(localized: "dialog.closeWorkspace.title", defaultValue: "Close workspace?"),
String(localized: "dialog.closeWorkspaces.title", defaultValue: "Close workspaces?"),

@cubic-dev-ai cubic-dev-ai Bot Mar 13, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Gate Cmd+D handling on the dialog’s acceptCmdD intent instead of title matching. Including the multi-workspace title here allows Cmd+D to confirm non-window multi-workspace closes even when that confirmation flow explicitly disables Cmd+D.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/AppDelegate.swift, line 7744:

<comment>Gate Cmd+D handling on the dialog’s `acceptCmdD` intent instead of title matching. Including the multi-workspace title here allows Cmd+D to confirm non-window multi-workspace closes even when that confirmation flow explicitly disables Cmd+D.</comment>

<file context>
@@ -7741,6 +7741,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
         // equivalents working and avoid surprising actions while the confirmation is up.
         let closeConfirmationTitles = [
             String(localized: "dialog.closeWorkspace.title", defaultValue: "Close workspace?"),
+            String(localized: "dialog.closeWorkspaces.title", defaultValue: "Close workspaces?"),
             String(localized: "dialog.closeTab.title", defaultValue: "Close tab?"),
             String(localized: "dialog.closeOtherTabs.title", defaultValue: "Close other tabs?"),
</file context>
Fix with Cubic

Comment on lines +5091 to +5092
let second = manager.addWorkspace()
let third = manager.addWorkspace()

@cubic-dev-ai cubic-dev-ai Bot Mar 13, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: These tests assume addWorkspace() inserts after the current tab, but addWorkspace uses the user’s newWorkspacePlacement preference to choose the insert index. If the preference is .top or .end, tabs[0] and the bullet list order change, making the expected message flaky. Pass a placement override (or set the default) so the order is deterministic.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmuxTests/CmuxWebViewKeyEquivalentTests.swift, line 5091:

<comment>These tests assume `addWorkspace()` inserts after the current tab, but `addWorkspace` uses the user’s `newWorkspacePlacement` preference to choose the insert index. If the preference is `.top` or `.end`, `tabs[0]` and the bullet list order change, making the expected message flaky. Pass a placement override (or set the default) so the order is deterministic.</comment>

<file context>
@@ -5084,6 +5084,77 @@ final class TabManagerWorkspaceOwnershipTests: XCTestCase {
+final class TabManagerCloseWorkspacesWithConfirmationTests: XCTestCase {
+    func testCloseWorkspacesWithConfirmationPromptsOnceAndClosesAcceptedWorkspaces() {
+        let manager = TabManager()
+        let second = manager.addWorkspace()
+        let third = manager.addWorkspace()
+        manager.setCustomTitle(tabId: manager.tabs[0].id, title: "Alpha")
</file context>
Suggested change
let second = manager.addWorkspace()
let third = manager.addWorkspace()
let second = manager.addWorkspace(placementOverride: .afterCurrent)
let third = manager.addWorkspace(placementOverride: .afterCurrent)
Fix with Cubic

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/ContentView.swift">

<violation number="1" location="Sources/ContentView.swift:6025">
P2: Validate every parsed workspace index before mapping into `tabs` to avoid out-of-bounds crashes in the UI-test hook.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/ContentView.swift

guard let lastIndex = indices.last, !indices.isEmpty, lastIndex < tabs.count else { return }

let selectedIds = Set(indices.map { tabs[$0].id })

@cubic-dev-ai cubic-dev-ai Bot Mar 13, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Validate every parsed workspace index before mapping into tabs to avoid out-of-bounds crashes in the UI-test hook.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/ContentView.swift, line 6025:

<comment>Validate every parsed workspace index before mapping into `tabs` to avoid out-of-bounds crashes in the UI-test hook.</comment>

<file context>
@@ -5985,6 +5997,40 @@ struct ContentView: View {
+
+        guard let lastIndex = indices.last, !indices.isEmpty, lastIndex < tabs.count else { return }
+
+        let selectedIds = Set(indices.map { tabs[$0].id })
+        selectedTabIds = selectedIds
+        lastSidebarSelectionIndex = lastIndex
</file context>
Fix with Cubic

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
cmuxUITests/CloseWorkspacesConfirmDialogUITests.swift (1)

7-12: Consider adding tearDown to clean up socket file.

The socket file created in setUp is not cleaned up after tests complete. While the unique UUID prevents conflicts between test runs, stale files may accumulate in /tmp.

♻️ Suggested tearDown implementation
     override func setUp() {
         super.setUp()
         continueAfterFailure = false
         socketPath = "/tmp/cmux-ui-test-close-workspaces-\(UUID().uuidString).sock"
         try? FileManager.default.removeItem(atPath: socketPath)
     }
+
+    override func tearDown() {
+        try? FileManager.default.removeItem(atPath: socketPath)
+        super.tearDown()
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxUITests/CloseWorkspacesConfirmDialogUITests.swift` around lines 7 - 12,
Add a tearDown method to remove the test socket file created in setUp: implement
override func tearDown() { ... } that calls try?
FileManager.default.removeItem(atPath: socketPath) (or checks existence and
removes) and then calls super.tearDown() so the temporary socketPath created in
setUp is cleaned up after each test run.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmuxTests/CmuxWebViewKeyEquivalentTests.swift`:
- Around line 5090-5191: The tests use manager.tabs[0] to refer to the first
workspace which is flaky; instead capture and use the explicit workspace IDs
returned by addWorkspace() and the initial workspace creation (e.g., store the
initial id in a variable like firstId = manager.tabs[0].id immediately after
creating the manager or better: let first = manager.tabs[0]; let second =
manager.addWorkspace(); let third = manager.addWorkspace()) and then pass those
explicit ids to setCustomTitle, setSidebarSelectedWorkspaceIds,
closeWorkspacesWithConfirmation, and closeCurrentWorkspaceWithConfirmation so
all references use stable identifiers (refer to addWorkspace, tabs,
setCustomTitle(tabId:), setSidebarSelectedWorkspaceIds(_:),
closeWorkspacesWithConfirmation(_:allowPinned:),
closeCurrentWorkspaceWithConfirmation()).

In `@Sources/ContentView.swift`:
- Around line 6004-6026: In applyUITestSidebarSelectionIfNeeded, the code
currently only validates indices.last before using indices to subscript tabs
which can crash for unsorted values like "3,0"; update the logic to validate
every parsed index (each element of indices) is within 0..<tabs.count (and >= 0)
before mapping to tabs, or alternatively filter out-of-range indices and
deduplicate after validation, then build selectedIds from the safe indices and
assign to selectedTabIds; reference indices, tabs, selectedIds and
selectedTabIds when making the checks and early-return on any invalid input.

---

Nitpick comments:
In `@cmuxUITests/CloseWorkspacesConfirmDialogUITests.swift`:
- Around line 7-12: Add a tearDown method to remove the test socket file created
in setUp: implement override func tearDown() { ... } that calls try?
FileManager.default.removeItem(atPath: socketPath) (or checks existence and
removes) and then calls super.tearDown() so the temporary socketPath created in
setUp is cleaned up after each test run.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 779afc09-1bce-4ccb-9439-2925353a6d5e

📥 Commits

Reviewing files that changed from the base of the PR and between f0d15b9 and c8770d1.

📒 Files selected for processing (4)
  • Sources/ContentView.swift
  • Sources/TabManager.swift
  • cmuxTests/CmuxWebViewKeyEquivalentTests.swift
  • cmuxUITests/CloseWorkspacesConfirmDialogUITests.swift

Comment on lines +5090 to +5191
let manager = TabManager()
let second = manager.addWorkspace()
let third = manager.addWorkspace()
manager.setCustomTitle(tabId: manager.tabs[0].id, title: "Alpha")
manager.setCustomTitle(tabId: second.id, title: "Beta")
manager.setCustomTitle(tabId: third.id, title: "Gamma")

var prompts: [(title: String, message: String, acceptCmdD: Bool)] = []
manager.confirmCloseHandler = { title, message, acceptCmdD in
prompts.append((title, message, acceptCmdD))
return true
}

manager.closeWorkspacesWithConfirmation([manager.tabs[0].id, second.id], allowPinned: true)

let expectedMessage = String(
format: String(
localized: "dialog.closeWorkspaces.message",
defaultValue: "This will close %1$lld workspaces and all of their panels:\n%2$@"
),
locale: .current,
Int64(2),
"• Alpha\n• Beta"
)
XCTAssertEqual(prompts.count, 1, "Expected a single confirmation prompt for multi-close")
XCTAssertEqual(
prompts.first?.title,
String(localized: "dialog.closeWorkspaces.title", defaultValue: "Close workspaces?")
)
XCTAssertEqual(prompts.first?.message, expectedMessage)
XCTAssertEqual(prompts.first?.acceptCmdD, false)
XCTAssertEqual(manager.tabs.map(\.title), ["Gamma"])
}

func testCloseWorkspacesWithConfirmationKeepsWorkspacesWhenCancelled() {
let manager = TabManager()
let second = manager.addWorkspace()
manager.setCustomTitle(tabId: manager.tabs[0].id, title: "Alpha")
manager.setCustomTitle(tabId: second.id, title: "Beta")

var prompts: [(title: String, message: String, acceptCmdD: Bool)] = []
manager.confirmCloseHandler = { title, message, acceptCmdD in
prompts.append((title, message, acceptCmdD))
return false
}

manager.closeWorkspacesWithConfirmation([manager.tabs[0].id, second.id], allowPinned: true)

let expectedMessage = String(
format: String(
localized: "dialog.closeWorkspacesWindow.message",
defaultValue: "This will close the current window, its %1$lld workspaces, and all of their panels:\n%2$@"
),
locale: .current,
Int64(2),
"• Alpha\n• Beta"
)
XCTAssertEqual(prompts.count, 1)
XCTAssertEqual(
prompts.first?.title,
String(localized: "dialog.closeWindow.title", defaultValue: "Close window?")
)
XCTAssertEqual(prompts.first?.message, expectedMessage)
XCTAssertEqual(prompts.first?.acceptCmdD, true)
XCTAssertEqual(manager.tabs.map(\.title), ["Alpha", "Beta"])
}

func testCloseCurrentWorkspaceWithConfirmationUsesSidebarMultiSelection() {
let manager = TabManager()
let second = manager.addWorkspace()
let third = manager.addWorkspace()
manager.setCustomTitle(tabId: manager.tabs[0].id, title: "Alpha")
manager.setCustomTitle(tabId: second.id, title: "Beta")
manager.setCustomTitle(tabId: third.id, title: "Gamma")
manager.selectWorkspace(second)
manager.setSidebarSelectedWorkspaceIds([manager.tabs[0].id, second.id])

var prompts: [(title: String, message: String, acceptCmdD: Bool)] = []
manager.confirmCloseHandler = { title, message, acceptCmdD in
prompts.append((title, message, acceptCmdD))
return false
}

manager.closeCurrentWorkspaceWithConfirmation()

let expectedMessage = String(
format: String(
localized: "dialog.closeWorkspaces.message",
defaultValue: "This will close %1$lld workspaces and all of their panels:\n%2$@"
),
locale: .current,
Int64(2),
"• Alpha\n• Beta"
)
XCTAssertEqual(prompts.count, 1, "Expected Cmd+Shift+W path to reuse the multi-close summary dialog")
XCTAssertEqual(
prompts.first?.title,
String(localized: "dialog.closeWorkspaces.title", defaultValue: "Close workspaces?")
)
XCTAssertEqual(prompts.first?.message, expectedMessage)
XCTAssertEqual(prompts.first?.acceptCmdD, false)
XCTAssertEqual(manager.tabs.map(\.title), ["Alpha", "Beta", "Gamma"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make these tests deterministic with explicit workspace references

These tests rely on manager.tabs[0] after insertions. If workspace insertion order changes (e.g., placement setting), IDs/titles can mismatch and make the tests flaky (including duplicate IDs in close requests).

✅ Suggested fix
 func testCloseWorkspacesWithConfirmationPromptsOnceAndClosesAcceptedWorkspaces() {
     let manager = TabManager()
+    let first = manager.tabs[0]
     let second = manager.addWorkspace()
     let third = manager.addWorkspace()
-    manager.setCustomTitle(tabId: manager.tabs[0].id, title: "Alpha")
+    manager.setCustomTitle(tabId: first.id, title: "Alpha")
     manager.setCustomTitle(tabId: second.id, title: "Beta")
     manager.setCustomTitle(tabId: third.id, title: "Gamma")
@@
-    manager.closeWorkspacesWithConfirmation([manager.tabs[0].id, second.id], allowPinned: true)
+    manager.closeWorkspacesWithConfirmation([first.id, second.id], allowPinned: true)
@@
 func testCloseWorkspacesWithConfirmationKeepsWorkspacesWhenCancelled() {
     let manager = TabManager()
+    let first = manager.tabs[0]
     let second = manager.addWorkspace()
-    manager.setCustomTitle(tabId: manager.tabs[0].id, title: "Alpha")
+    manager.setCustomTitle(tabId: first.id, title: "Alpha")
     manager.setCustomTitle(tabId: second.id, title: "Beta")
@@
-    manager.closeWorkspacesWithConfirmation([manager.tabs[0].id, second.id], allowPinned: true)
+    manager.closeWorkspacesWithConfirmation([first.id, second.id], allowPinned: true)
@@
 func testCloseCurrentWorkspaceWithConfirmationUsesSidebarMultiSelection() {
     let manager = TabManager()
+    let first = manager.tabs[0]
     let second = manager.addWorkspace()
     let third = manager.addWorkspace()
-    manager.setCustomTitle(tabId: manager.tabs[0].id, title: "Alpha")
+    manager.setCustomTitle(tabId: first.id, title: "Alpha")
     manager.setCustomTitle(tabId: second.id, title: "Beta")
     manager.setCustomTitle(tabId: third.id, title: "Gamma")
@@
-    manager.setSidebarSelectedWorkspaceIds([manager.tabs[0].id, second.id])
+    manager.setSidebarSelectedWorkspaceIds([first.id, second.id])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/CmuxWebViewKeyEquivalentTests.swift` around lines 5090 - 5191, The
tests use manager.tabs[0] to refer to the first workspace which is flaky;
instead capture and use the explicit workspace IDs returned by addWorkspace()
and the initial workspace creation (e.g., store the initial id in a variable
like firstId = manager.tabs[0].id immediately after creating the manager or
better: let first = manager.tabs[0]; let second = manager.addWorkspace(); let
third = manager.addWorkspace()) and then pass those explicit ids to
setCustomTitle, setSidebarSelectedWorkspaceIds, closeWorkspacesWithConfirmation,
and closeCurrentWorkspaceWithConfirmation so all references use stable
identifiers (refer to addWorkspace, tabs, setCustomTitle(tabId:),
setSidebarSelectedWorkspaceIds(_:),
closeWorkspacesWithConfirmation(_:allowPinned:),
closeCurrentWorkspaceWithConfirmation()).

Comment thread Sources/ContentView.swift
Comment on lines +6004 to +6026
private func applyUITestSidebarSelectionIfNeeded(tabs: [Workspace]) {
#if DEBUG
guard !didApplyUITestSidebarSelection else { return }
let env = ProcessInfo.processInfo.environment
guard let rawValue = env["CMUX_UI_TEST_SIDEBAR_SELECTED_WORKSPACE_INDICES"]?
.trimmingCharacters(in: .whitespacesAndNewlines),
!rawValue.isEmpty else {
return
}

var indices: [Int] = []
for token in rawValue.split(separator: ",") {
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
guard let index = Int(trimmed), index >= 0 else { return }
if !indices.contains(index) {
indices.append(index)
}
}

guard let lastIndex = indices.last, !indices.isEmpty, lastIndex < tabs.count else { return }

let selectedIds = Set(indices.map { tabs[$0].id })
selectedTabIds = selectedIds

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate all parsed UI-test indices before subscripting tabs.

Line 6023 only validates indices.last, but Line 6025 subscripts every parsed index. An unsorted value like "3,0" can still crash with out-of-bounds access.

💡 Proposed fix
-        guard let lastIndex = indices.last, !indices.isEmpty, lastIndex < tabs.count else { return }
+        guard !indices.isEmpty else { return }
+        guard indices.allSatisfy({ $0 < tabs.count }) else { return }
+        guard let lastIndex = indices.last else { return }

         let selectedIds = Set(indices.map { tabs[$0].id })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ContentView.swift` around lines 6004 - 6026, In
applyUITestSidebarSelectionIfNeeded, the code currently only validates
indices.last before using indices to subscript tabs which can crash for unsorted
values like "3,0"; update the logic to validate every parsed index (each element
of indices) is within 0..<tabs.count (and >= 0) before mapping to tabs, or
alternatively filter out-of-range indices and deduplicate after validation, then
build selectedIds from the safe indices and assign to selectedTabIds; reference
indices, tabs, selectedIds and selectedTabIds when making the checks and
early-return on any invalid input.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8770d106f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/ContentView.swift
Comment on lines +6023 to +6025
guard let lastIndex = indices.last, !indices.isEmpty, lastIndex < tabs.count else { return }

let selectedIds = Set(indices.map { tabs[$0].id })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate all sidebar indices before mapping selected IDs

applyUITestSidebarSelectionIfNeeded only checks indices.last against tabs.count, but then indexes every parsed value (tabs[$0]). A value like CMUX_UI_TEST_SIDEBAR_SELECTED_WORKSPACE_INDICES=0,99,1 passes the current guard when there are 2 tabs and then crashes on tabs[99], which can take down DEBUG/UI-test runs from a malformed env setting. Please validate every parsed index is in range before building selectedIds.

Useful? React with 👍 / 👎.

@lawrencecchen
lawrencecchen merged commit 4b0816f into main Mar 13, 2026
22 checks passed
@lawrencecchen
lawrencecchen deleted the task-multi-workspace-close-confirmation branch March 13, 2026 10:26
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
* Add multi-workspace close UI regression test

* Summarize multi-workspace close confirmation

* Add Cmd+Shift+W multi-close UI regression test

* Honor sidebar multi-select for close workspace

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview — c8770d10 Deployed Mar 13, 2026 by vercel[bot]
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.

1 participant