Skip to content

Fix Cmd+N workspace creation snapshot crashes#2173

Merged
austinywang merged 2 commits intomainfrom
issue-2169-cmd-n-snapshot-null-v2
Mar 25, 2026
Merged

Fix Cmd+N workspace creation snapshot crashes#2173
austinywang merged 2 commits intomainfrom
issue-2169-cmd-n-snapshot-null-v2

Conversation

@austinywang
Copy link
Copy Markdown
Contributor

@austinywang austinywang commented Mar 25, 2026

Summary

Testing

  • Ran ./scripts/reload.sh --tag issue2169-cmdn-v2 --launch.
  • Verified the tagged Debug app built successfully and launched.
  • Did not run unit/UI tests locally because this repo’s policy is to run them in CI/VM.

Demo Video

For UI or behavior changes, include a short demo video (GitHub upload, Loom, or other direct link).

  • Video URL or attachment: N/A

Review Trigger (Copy/Paste as PR comment)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I tested the change locally
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed
  • I requested bot reviews after my latest commit (copy/paste block above or equivalent)
  • All code review bot comments are resolved
  • All human review comments are resolved

Summary by cubic

Fixes crashes when creating a workspace with Cmd+N by taking a stable snapshot instead of walking live focus state; preserves pin state and correct insertion during rapid changes (fixes #2169).

  • Bug Fixes
    • Build the snapshot from cached tab/panel state, including selected tab pin, preferred working directory, and inherited terminal config; no live Bonsplit focus walk.
    • Compute insert position against the current live tab order using snapshot tab values and preserve pin state; includes a safe fallback if re-entrant adds create tabs not in the snapshot.
    • Add regression tests for pin-state mutation after snapshot and for live reordering during Cmd+N.

Written for commit 23ed170. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved workspace tab snapshot handling for more accurate pinned state tracking
    • Enhanced terminal panel selection logic for consistent workspace configuration inheritance
    • Refined working directory assignment for newly created tabs
  • Tests

    • Added test coverage for workspace placement scenarios with tab state mutations

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 25, 2026

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 25, 2026 11:51pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 344b9b48-94ff-4f43-96f0-6e24e4dc0af4

📥 Commits

Reviewing files that changed from the base of the PR and between 9f2adce and 23ed170.

📒 Files selected for processing (2)
  • Sources/TabManager.swift
  • cmuxTests/WorkspaceUnitTests.swift

📝 Walkthrough

Walkthrough

TabManager.swift refactored workspace snapshot creation and consumption to avoid runtime null pointer crashes during rapid Cmd+N presses. Changes precompute tab snapshots upfront, reorder them against live tabs, and eliminate costly live state traversals (focus chains, panel lookups) in favor of snapshot-derived values.

Changes

Cohort / File(s) Summary
Workspace Snapshot Refactoring
Sources/TabManager.swift
Updated workspaceCreationSnapshot() to precompute tab snapshots and derive selectedTabWasPinned from snapshot. Added orderedLiveWorkspaceCreationTabs(from:) helper to reorder snapshots against live tabs using id mapping (returns nil if tabs missing). Modified newTabInsertIndex() to compute insertion via snapshot-derived tabs when available, falling back to snapshot. Eliminated live focus state walks in terminalPanelForWorkspaceConfigInheritanceSource() by aggregating/deduplicating cached candidates. Replaced live focused panel directory traversal in preferredWorkingDirectoryForNewTab() with cached workspace directory preference.
Snapshot-Based Placement Tests
cmuxTests/WorkspaceUnitTests.swift
Added two WorkspaceCreationPlacementTests cases: one validating snapshot-pinned ordering is used when pin/unpin mutations occur post-snapshot, and another validating live workspace reorder is reflected via snapshot-derived tab values during .afterCurrent insertion.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 Snapshots captured, no null to dread,
Tab ids mapped, reordered with care,
No live-chain walks through focus thread,
Safe insertion awaits everywhere!
Cmd+N no longer makes crashes spread!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-2169-cmd-n-snapshot-null-v2

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.

@austinywang austinywang merged commit cbf0845 into main Mar 25, 2026
14 of 15 checks passed
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 25, 2026

Greptile Summary

This PR fixes snapshot-time races that caused crashes during rapid Cmd+N workspace creation by snapshotting stable value data (pin state, working directory, terminal config) before any Bonsplit focus traversal can race with the operation. It also introduces orderedLiveWorkspaceCreationTabs, which recomputes the insertion position against the current live tab order while preserving snapshot pin state — with a safe nil-fallback when a re-entrant add introduces a tab that wasn't in the snapshot.

Key changes:

  • workspaceCreationSnapshot(): selectedTabWasPinned now reads from the freshly-built tabSnapshots array instead of the live Workspace.isPinned property, preventing post-snapshot pin mutations from affecting insertion order.
  • orderedLiveWorkspaceCreationTabs(from:): New helper that remaps snapshot tab values onto the current live tab ordering by ID, so post-snapshot tab reorders are respected without re-walking live published state; returns nil (triggering a graceful snapshot.tabs fallback) when the live array contains a tab not present in the snapshot (re-entrant add).
  • terminalPanelForWorkspaceConfigInheritanceSource: Replaced live Bonsplit focus traversal (focusedTerminalPanel, bonsplitController.focusedPaneId) with a candidate list built from lastRememberedTerminalPanelForConfigInheritance() and cached workspace.panels, filtered for live surfaces.
  • preferredWorkingDirectoryForNewTab: Replaced focusedPanelId lookup with workspace.currentDirectorypanelDirectories.values fallback to avoid live focus reads.
  • Tests: Two new regression tests exercise the pin-state-frozen-at-snapshot and live-reorder-respecting behaviors; commit structure follows the two-commit regression policy (CLAUDE.md).

Confidence Score: 4/5

  • Safe to merge; the crash-fix logic and snapshot semantics are correct with two minor P2 follow-ups around fallback determinism.
  • The core fixes (snapshot pin state, orderedLiveWorkspaceCreationTabs, avoiding live Bonsplit traversal) are logically sound and validated by correctly-structured regression tests following CLAUDE.md policy. Two P2 style concerns remain: the UUID-string sort in terminalPanelForWorkspaceConfigInheritanceSource is an arbitrary tiebreaker for multi-panel workspaces, and panelDirectories.values.lazy.first has non-deterministic iteration order for directory fallback. Neither is a crash or data-loss risk — they're quality tradeoffs in edge-case fallback paths already defended by the safety-net design.
  • Sources/TabManager.swift — specifically the panel-selection sort order (line ~2238) and directory fallback iteration order (line ~2332).

Important Files Changed

Filename Overview
Sources/TabManager.swift Core crash-fix changes are correct: snapshot now captures pin state from snapshot objects instead of live workspace, and orderedLiveWorkspaceCreationTabs correctly remaps snapshot values against live tab order with a safe nil-fallback for re-entrant adds. Two minor P2 concerns: the UUID-string sort in terminalPanelForWorkspaceConfigInheritanceSource is arbitrary for multi-panel workspaces, and panelDirectories.values.lazy.first has non-deterministic iteration order for the directory fallback.
cmuxTests/WorkspaceUnitTests.swift Two well-constructed regression tests added. testAddWorkspaceAfterCurrentUsesSnapshotPinnedStateWhenPinningMutatesAfterSnapshot validates pin state is frozen at snapshot time; testAddWorkspaceAfterCurrentFollowsLiveReorderUsingSnapshotTabValues validates live tab reorder is respected while snapshot values are preserved. Both test assertions trace correctly through the updated insertion-index logic. Commit structure (tests-only commit, then fix commit) follows the CLAUDE.md two-commit regression policy.

Sequence Diagram

sequenceDiagram
    participant User as User (Cmd+N)
    participant TM as TabManager
    participant SS as Snapshot
    participant OL as orderedLiveTabs()

    User->>TM: addWorkspace(placementOverride)
    TM->>SS: workspaceCreationSnapshot()
    Note over SS: Capture tabs[], selectedTabId,<br/>selectedTabWasPinned (from snapshot),<br/>preferredWorkingDirectory (cached),<br/>inheritedTerminalConfig (cached panels)
    SS-->>TM: WorkspaceCreationSnapshot

    TM->>TM: didCaptureWorkspaceCreationSnapshot()
    Note over TM: (test seam — live mutations<br/>may happen here in tests)

    TM->>OL: orderedLiveWorkspaceCreationTabs(from: snapshot)
    Note over OL: Map live tab order → snapshot values.<br/>Return nil if live has tab not in snapshot<br/>(re-entrant add fallback)
    OL-->>TM: [WorkspaceCreationTabSnapshot]? (or nil → use snapshot.tabs)

    TM->>TM: newTabInsertIndex(snapshot, placement)
    Note over TM: Uses live-ordered snapshot tabs<br/>(pin state frozen at snapshot time)

    TM->>TM: Insert new Workspace at insertIndex<br/>into current live tabs array
    TM-->>User: Workspace
Loading

Reviews (1): Last reviewed commit: "Fix Cmd+N workspace creation snapshot ra..." | Re-trigger Greptile

Comment on lines +2238 to +2241
for terminalPanel in panels.values
.compactMap({ $0 as? TerminalPanel })
.sorted(by: { $0.id.uuidString < $1.id.uuidString }) {
appendCandidate(terminalPanel)
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 UUID-string sort is an arbitrary tiebreaker for panel selection

Sorting panels.values by id.uuidString produces a stable, deterministic order across a single run, but because UUIDs are generated randomly the "first" panel in UUID-string order has no semantic relationship to which pane the user was working in. In a workspace with multiple splits, this means config inheritance (font size, environment, etc.) could be drawn from an arbitrary pane rather than the most-recently-used one.

The original code addressed this through a layered semantic priority; the new code collapses all non-remembered panels into a single UUID-sorted bucket. It might be worth preserving at least a coarser semantic ordering — e.g., prefer remembered panel, then any panel with a live surface sorted by most-recently-created (if a creation timestamp is available), before falling back to UUID-order — so the regression under rapid Cmd+N is limited to the crash-avoidance path rather than becoming the normal path for all multi-panel workspaces.

If the UUID sort is intentionally kept as a simple, safe fallback and the remembered-panel path is expected to cover the common case, a short comment to that effect here would help future readers.

Comment on lines +2332 to +2334
return workspace.panelDirectories.values.lazy.compactMap { directory in
self.normalizedWorkingDirectory(directory)
}.first
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 Dictionary iteration order is non-deterministic for directory fallback

workspace.panelDirectories.values.lazy.compactMap { ... }.first iterates a Dictionary, whose key order is not guaranteed in Swift. In a workspace with two or more panes open in different directories, the fallback directory for a new Cmd+N workspace will be drawn from a non-deterministic panel rather than the most recently active one.

The original code used focusedPanelId to pick the right directory deterministically. If a stable cached alternative exists (e.g. the most recently assigned directory key, a separate lastRememberedDirectory property, or just the first key in insertion order), anchoring the fallback to that would preserve predictable behavior without reintroducing a live-focus traversal.

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

1 issue found across 2 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="cmuxTests/WorkspaceUnitTests.swift">

<violation number="1" location="cmuxTests/WorkspaceUnitTests.swift:458">
P2: This regression test setup is non-discriminating: unpinning the selected pinned workspace yields the same insertion index for both snapshot and live pin-state logic, so the intended bug can regress without failing this test.</violation>
</file>

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

return
}

manager.setPinned(first, pinned: true)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 25, 2026

Choose a reason for hiding this comment

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

P2: This regression test setup is non-discriminating: unpinning the selected pinned workspace yields the same insertion index for both snapshot and live pin-state logic, so the intended bug can regress without failing this test.

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

<comment>This regression test setup is non-discriminating: unpinning the selected pinned workspace yields the same insertion index for both snapshot and live pin-state logic, so the intended bug can regress without failing this test.</comment>

<file context>
@@ -448,6 +448,58 @@ final class WorkspaceCreationPlacementTests: XCTestCase {
+            return
+        }
+
+        manager.setPinned(first, pinned: true)
+        let second = manager.addWorkspace()
+        let third = manager.addWorkspace()
</file context>
Fix with Cubic

zzdif pushed a commit to zzdif/cmux that referenced this pull request Mar 26, 2026
* Add regression tests for Cmd+N workspace snapshot mutations

* Fix Cmd+N workspace creation snapshot races
Jesssullivan added a commit to Jesssullivan/cmux that referenced this pull request Mar 26, 2026
Ingests all upstream fixes since 2026-03-22 including:
- Fix Cmd+N crash: retain snapshot workspaces (manaflow-ai#2183, manaflow-ai#2181, manaflow-ai#2178, manaflow-ai#2173)
- Fix browser pane restore after reopen (manaflow-ai#2141)
- Fix Ghostty resize_split keybind (manaflow-ai#1899)
- Reduce shell integration prompt latency (manaflow-ai#2109)
- Fix command palette focus after terminal find (manaflow-ai#2089)
- Add Codex CLI hooks (manaflow-ai#2103)
- Add cmux.json custom commands (manaflow-ai#2011)
- Fix window position restore on relaunch (manaflow-ai#2129)

Conflict resolution:
- BrowserPanel.swift: accepted upstream configureWebViewConfiguration()
  refactor (already includes our forMainFrameOnly:true CAPTCHA fix from PR manaflow-ai#1877)

Fork-specific files preserved:
- Sources/Panels/WebAuthn{Coordinator,BridgeJavaScript}.swift
- Sources/FIDO2/module.modulemap
- vendor/ctap2 submodule
- cmux.entitlements (with camera/audio-input removed)
- cmux.embedded.entitlements
- .github/workflows/fork-{ci,release}.yml
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.

Cmd+N crash: null pointer in TabManager.workspaceCreationSnapshot()

1 participant