Skip to content

fix: restore terminal working directories on relaunch - #2171

Closed
jorgitin02 wants to merge 6 commits into
manaflow-ai:mainfrom
jorgitin02:issue-2125-session-restore-cwd
Closed

jorgitin02 wants to merge 6 commits into
manaflow-ai:mainfrom
jorgitin02:issue-2125-session-restore-cwd

Conversation

@jorgitin02

@jorgitin02 jorgitin02 commented Mar 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • restore the focused terminal's startup cwd from the workspace snapshot when panel-specific cwd data is missing during session restore
  • add session persistence regressions covering both cwd snapshot persistence and the focused-panel restore fallback
  • verify with targeted unit tests, the full SessionPersistenceTests suite, a cmux-unit build, and a tagged relaunch/manual restore cycle

Testing

  • xcodebuild -project "/Users/jorgitin/projects/cmux/.worktrees/issue-2125-session-restore-cwd/GhosttyTabs.xcodeproj" -scheme cmux-unit -destination 'platform=macOS' -only-testing:cmuxTests/SessionPersistenceTests/testWorkspaceSessionSnapshotPersistsFocusedTerminalWorkingDirectory -only-testing:cmuxTests/SessionPersistenceTests/testWorkspaceSessionSnapshotRestoresFocusedTerminalWorkingDirectoryWhenPanelDirectoryIsMissing test
  • xcodebuild -project "/Users/jorgitin/projects/cmux/.worktrees/issue-2125-session-restore-cwd/GhosttyTabs.xcodeproj" -scheme cmux-unit -destination 'platform=macOS' -only-testing:cmuxTests/SessionPersistenceTests test
  • xcodebuild -project "/Users/jorgitin/projects/cmux/.worktrees/issue-2125-session-restore-cwd/GhosttyTabs.xcodeproj" -scheme cmux-unit -destination 'platform=macOS' build
  • manual tagged relaunch verification via ./scripts/reload.sh --tag issue-2125-manual restoring two workspaces to distinct directories

Summary by cubic

Restores terminal working directories on relaunch by falling back to the workspace snapshot when panel-specific data is missing. Addresses Linear 2125 and adds Ghostty-compatible split resizing with targeted regression tests.

  • Bug Fixes

    • Restore focused terminal CWD on session relaunch using the workspace snapshot when panel CWD is absent.
    • Add session persistence tests covering both CWD persistence and the focused-panel fallback.
  • New Features

    • Implement split resizing: direction-aware for horizontal/vertical splits, with clamping between 0.1–0.9.
    • Add unit tests for resize behavior and edge cases (no adjacent border, clamping).

Written for commit 0e26e5f. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Implemented directional split-pane resizing with proportional movement and automatic clamping of divider positions.
  • Improvements

    • Session restoration now preserves the focused panel state and restores terminal working directories with correct focused/unfocused precedence.
  • Tests

    • Added tests covering split resizing behavior (directional moves, clamping, negative paths) and session persistence for terminal working directories.

@vercel

vercel Bot commented Mar 25, 2026

Copy link
Copy Markdown

@jorgitin02 is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Mar 25, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Implements split-resize logic in TabManager with tree traversal and divider-position updates, threads focused panel ID through workspace restore to resolve terminal working directories, and adds unit tests for split resizing and session persistence behavior.

Changes

Cohort / File(s) Summary
Split Resize Implementation
Sources/TabManager.swift
Implements resizeSplit(tabId:surfaceId:direction:amount:): validates input, resolves target pane, traverses ExternalTreeNode snapshot to collect candidate splits, filters by orientation/child membership, computes/clamps divider delta, and calls setDividerPosition. Adds ResizeSplitCandidate, ResizeSplitTrace, and resizeSplitCollectCandidates. Extends ResizeDirection with splitOrientation, requiresPaneInFirstChild, and dividerDeltaSign.
Session Restoration Enhancement
Sources/Workspace.swift
Threads snapshot.focusedPanelId into restorePane(...) and createPanel(...). Terminal working-directory resolution prefers currentDirectory only when restoring the workspace-focused panel; otherwise falls back to panel snapshot fields and workspace snapshot values as before.
Split Resize Tests
cmuxTests/TabManagerUnitTests.swift
Adds TabManagerResizeSplitsTests with directional movement, negative-path (no adjacent border), and clamping tests. Moves splitNodes(in:) helper location.
Session Restoration Tests
cmuxTests/SessionPersistenceTests.swift
Adds two tests validating terminal working-directory persistence and restore behavior for focused vs unfocused panels and workspace snapshot currentDirectory.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant TabManager
    participant BonSplit as BonSplit Controller
    participant Tree as ExternalTreeNode

    Client->>TabManager: resizeSplit(tabId, surfaceId, direction, amount)
    TabManager->>TabManager: Validate amount > 0
    TabManager->>TabManager: Resolve target pane for surfaceId
    TabManager->>Tree: treeSnapshot()
    Tree-->>TabManager: tree
    TabManager->>TabManager: resizeSplitCollectCandidates(node, targetPaneId)
    loop traverse tree
        TabManager->>Tree: inspect node bounds & membership
        TabManager->>TabManager: append ResizeSplitCandidate when split found
    end
    TabManager->>TabManager: filter candidates by orientation & child side
    TabManager->>TabManager: compute divider delta (amount / axisPixels) * sign
    TabManager->>TabManager: clamp to [0.1,0.9]
    TabManager->>BonSplit: setDividerPosition(newPos, forSplit, fromExternal: true)
    BonSplit-->>Client: success
Loading
sequenceDiagram
    participant Client
    participant Workspace
    participant Snapshot as Session Snapshot
    participant Creator as Panel Creator

    Client->>Workspace: restoreSession(snapshot)
    Workspace->>Snapshot: read focusedPanelId
    Snapshot-->>Workspace: focusedPanelId
    loop for each pane
        Workspace->>Workspace: restorePane(..., workspaceFocusedPanelId)
        Workspace->>Creator: createPanel(..., workspaceFocusedPanelId)
        alt panel is terminal & focused
            Creator->>Creator: use snapshot.currentDirectory as workingDirectory
        else
            Creator->>Creator: use panel.snapshot.terminal?.workingDirectory or snapshot.directory
        end
        Creator-->>Workspace: panel created
    end
    Workspace-->>Client: restored
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I nudged the split, a gentle hop,
Dividers shift, no edge to stop,
Focus finds the panel's door,
Sessions wake and roam once more,
thump-thump — a rabbit's tiny roar.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the primary change: restoring terminal working directories on session relaunch, which aligns with the main objective of the PR.
Description check ✅ Passed The description covers the summary and testing sections with specific test commands and verification steps, though the demo video section is missing.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch issue-2125-session-restore-cwd

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.

@greptile-apps

greptile-apps Bot commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR addresses session CWD restore for relaunched terminals and simultaneously ships the full resizeSplit implementation that was previously a no-op. The two concerns are bundled together in the same PR.

Session restore CWD change (Workspace.swift)

  • Threads workspaceFocusedPanelId through restorePane → createPanel to express the intent that the focused panel should preferentially inherit currentDirectory from the workspace snapshot.
  • In practice the change is functionally equivalent to the pre-PR code: the added intermediate step ?? (snapshot.id == workspaceFocusedPanelId ? currentDirectory : nil) resolves to nil for non-focused panels, but the trailing ?? currentDirectory catch-all then supplies the same value anyway. Every terminal without a saved per-panel CWD still receives currentDirectory.
  • The regression test testWorkspaceSessionSnapshotRestoresFocusedTerminalWorkingDirectoryWhenPanelDirectoryIsMissing appears to pass against the pre-PR code path (since currentDirectory is already updated to snapshot.currentDirectory before createPanel is called), which may violate the CLAUDE.md two-commit policy.

resizeSplit implementation (TabManager.swift)

  • Clean post-order tree walk collecting ResizeSplitCandidate entries for each split that contains the target pane.
  • ResizeDirection extensions (splitOrientation, requiresPaneInFirstChild, dividerDeltaSign) are correct for all four directions.
  • axisPixels is guarded by max(..., 1) to handle zero-frame unit-test environments.
  • Delta is clamped to [0.1, 0.9] before being applied.
  • Seven new TabManagerResizeSplitsTests cover all directions, the no-valid-border guard, and both clamping extremes.

Confidence Score: 3/5

  • The resizeSplit feature is solid and ready to merge, but the core CWD session-restore fix appears functionally equivalent to the pre-PR code, and the regression test does not demonstrably fail without the fix — both warrant clarification before merging.
  • The resizeSplit implementation and its seven tests are well-crafted and independently mergeable. The Workspace.swift change, however, introduces a logically redundant intermediate step: the ?? (snapshot.id == workspaceFocusedPanelId ? currentDirectory : nil) branch evaluates to the same value as the existing trailing ?? currentDirectory for every code path, leaving observable behaviour unchanged from the pre-PR state. This raises the question of what the actual bug was and whether the fix landed in the right place. The regression test does not go red on the old code (CLAUDE.md policy violation), compounding the uncertainty about correctness. Until the intended semantic distinction is either justified or the fallback chain is corrected, the primary fix cannot be fully trusted.
  • Sources/Workspace.swift (redundant fallback step) and cmuxTests/SessionPersistenceTests.swift (regression test validity)

Important Files Changed

Filename Overview
Sources/Workspace.swift Adds workspaceFocusedPanelId threading into createPanel. The intermediate fallback ?? (snapshot.id == workspaceFocusedPanelId ? currentDirectory : nil) is logically redundant since the final ?? currentDirectory supplies the same value for all panels regardless of focus, leaving the behaviour unchanged from the pre-PR code.
cmuxTests/SessionPersistenceTests.swift Adds two new session-persistence tests. The first (snapshot persistence) looks correct. The second (fallback restore) appears to pass against the pre-PR code path because currentDirectory is already set to restoredDirectory.path before createPanel is called, potentially violating CLAUDE.md's two-commit red/green policy.
Sources/TabManager.swift Replaces the no-op resizeSplit stub with a full tree-walk implementation. Direction mapping, paneInFirstChild semantics, dividerDeltaSign, axisPixels selection, and [0.1, 0.9] clamping all look correct. The max(axisPixels, 1) guard handles zero-sized frames in tests safely.
cmuxTests/TabManagerUnitTests.swift Adds TabManagerResizeSplitsTests covering all four directions, the no-border-in-direction guard, and both clamping bounds. The splitNodes helper is also promoted to file scope. Coverage is thorough and the assertions are directional (greater/less than 0.5) rather than exact, which is appropriate given zero-frame units.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[restoreSessionSnapshot] --> B[Set currentDirectory from snapshot.currentDirectory]
    B --> C[restorePane for each layout leaf]
    C --> D[createPanel from snapshot + workspaceFocusedPanelId]
    D --> E{snapshot.terminal?.workingDirectory?}
    E -- set --> F[use workingDirectory]
    E -- nil --> G{snapshot.directory?}
    G -- set --> F
    G -- nil --> H{snapshot.id == workspaceFocusedPanelId?}
    H -- yes --> I[use currentDirectory]
    H -- no --> J[nil → ?? currentDirectory]
    J --> I
    I --> K[newTerminalSurface with workingDirectory]

    style H fill:#ffe0b2,stroke:#f57c00
    style J fill:#ffe0b2,stroke:#f57c00
Loading

Reviews (1): Last reviewed commit: "fix: restore terminal working directorie..." | Re-trigger Greptile

Comment thread Sources/Workspace.swift Outdated
Comment on lines +547 to +550
let workingDirectory = snapshot.terminal?.workingDirectory
?? snapshot.directory
?? (snapshot.id == workspaceFocusedPanelId ? currentDirectory : nil)
?? currentDirectory

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.

P1 Intermediate fallback step is logically redundant

The ?? (snapshot.id == workspaceFocusedPanelId ? currentDirectory : nil) line evaluates to either currentDirectory or nil. But the immediately following ?? currentDirectory supplies currentDirectory for the nil branch as well, meaning every panel — focused or not — ends up with currentDirectory when neither snapshot.terminal?.workingDirectory nor snapshot.directory is set. The conditional branch buys nothing over the old two-fallback chain.

If the intent is to prevent non-focused panels from inheriting the workspace's (focused panel's) directory when their own CWD was never saved, the final catch-all must be replaced with a different fallback (e.g. the workspace's startup requestedDirectory). As written, the semantics are identical to the pre-PR code:

// current (effectively equivalent to the old code for all panels)
let workingDirectory = snapshot.terminal?.workingDirectory
    ?? snapshot.directory
    ?? (snapshot.id == workspaceFocusedPanelId ? currentDirectory : nil)
    ?? currentDirectory

Either simplify back to the two-fallback form, or replace the trailing ?? currentDirectory with a distinct value so the workspaceFocusedPanelId guard actually differentiates the two cases.

Comment on lines +73 to +115
let requestedDirectory = root.appendingPathComponent("requested", isDirectory: true)
let restoredDirectory = root.appendingPathComponent("restored", isDirectory: true)
try FileManager.default.createDirectory(at: requestedDirectory, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: restoredDirectory, withIntermediateDirectories: true)

var snapshot = makeSnapshot(version: SessionSnapshotSchema.currentVersion).windows[0].tabManager.workspaces[0]
let panelId = UUID()
snapshot.focusedPanelId = panelId
snapshot.currentDirectory = restoredDirectory.path
snapshot.layout = .pane(SessionPaneLayoutSnapshot(panelIds: [panelId], selectedPanelId: panelId))
snapshot.panels = [
SessionPanelSnapshot(
id: panelId,
type: .terminal,
title: "Terminal",
customTitle: nil,
directory: nil,
isPinned: false,
isManuallyUnread: false,
gitBranch: nil,
listeningPorts: [],
ttyName: nil,
terminal: SessionTerminalPanelSnapshot(
workingDirectory: nil,
scrollback: nil
),
browser: nil,
markdown: nil
)
]

let restored = Workspace(title: "Terminal", workingDirectory: requestedDirectory.path, portOrdinal: 0)
restored.restoreSessionSnapshot(snapshot)

let restoredFocusedPanelId = try XCTUnwrap(restored.focusedPanelId)
let restoredTerminal = try XCTUnwrap(restored.terminalPanel(for: restoredFocusedPanelId))
XCTAssertEqual(restored.currentDirectory, restoredDirectory.path)
XCTAssertEqual(
restoredTerminal.requestedWorkingDirectory,
restoredDirectory.path,
"Expected restored terminals to use the workspace snapshot cwd when no panel-specific cwd was persisted"
)
}

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.

P1 Regression test likely passes without the fix (violates two-commit policy)

testWorkspaceSessionSnapshotRestoresFocusedTerminalWorkingDirectoryWhenPanelDirectoryIsMissing builds a snapshot where terminal.workingDirectory == nil and directory == nil, then asserts that restoredTerminal.requestedWorkingDirectory == restoredDirectory.path.

In restoreSessionSnapshot, currentDirectory is updated to snapshot.currentDirectory (restoredDirectory.path) before createPanel is called. The original pre-PR fallback chain was:

let workingDirectory = snapshot.terminal?.workingDirectory ?? snapshot.directory ?? currentDirectory

With both optional fields nil, this already resolves to currentDirectory — which is already restoredDirectory.path at that point. The assertion in the test would therefore pass against the unmodified code, meaning this test does not go red on the first commit as CLAUDE.md's regression-test policy requires.

Either the test is covering the wrong scenario (it should simulate the case where currentDirectory was not updated, or where snapshot.currentDirectory is empty), or the actual bug lies elsewhere and the fix should target that path. Consider revisiting the test setup so it reliably fails on the old code.

@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

🧹 Nitpick comments (1)
cmuxTests/SessionPersistenceTests.swift (1)

60-63: Tighten the focused-panel assertions to prevent false positives.

The snapshot test resolves snapshot.panels.first, and the restore test uses a single terminal. A regression that applies snapshot.currentDirectory to every terminal, or a panel-ordering change, would still pass. Resolve the persisted panel by snapshot.focusedPanelId and add a non-focused terminal with workingDirectory == nil, then assert only the focused terminal picks up restoredDirectory.path.

Also applies to: 78-114

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/SessionPersistenceTests.swift` around lines 60 - 63, The test
currently inspects snapshot.panels.first which can mask regressions; instead
find the persisted panel by matching snapshot.focusedPanelId (use
snapshot.panels.first { $0.id == snapshot.focusedPanelId }) and assert that its
terminal's workingDirectory equals restoredDirectory.path, while adding a second
(non-focused) terminal in the test setup with workingDirectory == nil and
asserting that that non-focused terminal's workingDirectory remains nil; apply
the same change pattern to the other assertions in the 78-114 block so only the
focused panel is expected to receive snapshot.currentDirectory.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/Workspace.swift`:
- Around line 547-550: The fallback to currentDirectory is currently applied
unconditionally, nullifying the workspaceFocusedPanelId check; change the
expression for workingDirectory so the workspace cwd (currentDirectory) is only
used when snapshot.id == workspaceFocusedPanelId by removing the final
unconditional "?? currentDirectory" and making the ternary the last fallback,
e.g. keep snapshot.terminal?.workingDirectory ?? snapshot.directory ??
(snapshot.id == workspaceFocusedPanelId ? currentDirectory : nil);
alternatively, if the global fallback is intended, remove
workspaceFocusedPanelId plumbing (references to workspaceFocusedPanelId)
instead.

---

Nitpick comments:
In `@cmuxTests/SessionPersistenceTests.swift`:
- Around line 60-63: The test currently inspects snapshot.panels.first which can
mask regressions; instead find the persisted panel by matching
snapshot.focusedPanelId (use snapshot.panels.first { $0.id ==
snapshot.focusedPanelId }) and assert that its terminal's workingDirectory
equals restoredDirectory.path, while adding a second (non-focused) terminal in
the test setup with workingDirectory == nil and asserting that that non-focused
terminal's workingDirectory remains nil; apply the same change pattern to the
other assertions in the 78-114 block so only the focused panel is expected to
receive snapshot.currentDirectory.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ba0956f1-3d31-4396-ae84-ca7a7011759a

📥 Commits

Reviewing files that changed from the base of the PR and between 99ca3c9 and 0e26e5f.

📒 Files selected for processing (4)
  • Sources/TabManager.swift
  • Sources/Workspace.swift
  • cmuxTests/SessionPersistenceTests.swift
  • cmuxTests/TabManagerUnitTests.swift

Comment thread Sources/Workspace.swift Outdated

@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: 0e26e5f155

ℹ️ 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/Workspace.swift Outdated
Comment on lines +547 to +550
let workingDirectory = snapshot.terminal?.workingDirectory
?? snapshot.directory
?? (snapshot.id == workspaceFocusedPanelId ? currentDirectory : nil)
?? currentDirectory

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 Make focused-panel cwd fallback actually conditional

This fallback chain is effectively unconditional because it ends with ?? currentDirectory, so non-focused terminals still restore to the workspace cwd whenever both terminal.workingDirectory and directory are missing. In practice, snapshots with multiple panels missing per-panel cwd will still assign the focused panel’s directory to other panels, so the new workspaceFocusedPanelId check does not change behavior as intended.

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.

1 issue found across 4 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/Workspace.swift">

<violation number="1" location="Sources/Workspace.swift:550">
P2: The new focused-panel fallback is ineffective because the unconditional `?? currentDirectory` makes the branch a no-op. This leaves restore behavior unchanged and the new parameter unused in practice.</violation>
</file>

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

Comment thread Sources/Workspace.swift Outdated
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@jorgitin02

Copy link
Copy Markdown
Contributor Author

Pushed follow-up in to address the restore-path issue and tighten the regression tests.\n\nChanges:\n- scoped workspace cwd fallback to the focused restored terminal only\n- strengthened session-persistence coverage to resolve panels by \n- added a second restored terminal assertion so non-focused panels keep their own persisted directory\n\nVerification:\n-

@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: 1852c3c6cb

ℹ️ 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".

restored.restoreSessionSnapshot(snapshot)

let restoredFocusedPanel = try XCTUnwrap(restored.terminalPanel(for: try XCTUnwrap(restored.focusedPanelId)))
let restoredUnfocusedPanel = try XCTUnwrap(restored.terminalPanel(for: unfocusedPanelId))

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 Badge Resolve restored panel ID before asserting unfocused cwd

restoreSessionSnapshot creates new panel UUIDs and tracks old→new IDs internally, so unfocusedPanelId from the snapshot is stale after restore. Looking up restored.terminalPanel(for: unfocusedPanelId) will almost always return nil, causing this regression test to fail before it validates the non-focused cwd behavior; the assertion should use a restored panel ID (for example via the restored layout/selection) instead of the pre-restore UUID.

Useful? React with 👍 / 👎.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Workspace.swift (1)

227-233: ⚠️ Potential issue | 🟠 Major

Use an immutable snapshot cwd for restore, not the live currentDirectory.

Restore can still pick the wrong cwd here. createTab(...) selects internally, so earlier tabs recreated in the focused pane can temporarily become focusedPanelId, and applySessionPanelMetadata(...) can overwrite currentDirectory before the real focused snapshot panel is created. If the focused tab is not first in its pane, this fallback can relaunch it in another tab’s directory. Capture the normalized workspace snapshot directory once in restoreSessionSnapshot, thread that immutable value through restorePane/createPanel, and seed panelDirectories from the resolved launch cwd so a subsequent snapshot preserves it.

💡 Proposed fix
     func restoreSessionSnapshot(_ snapshot: SessionWorkspaceSnapshot) {
         restoredTerminalScrollbackByPanelId.removeAll(keepingCapacity: false)

         let normalizedCurrentDirectory = snapshot.currentDirectory.trimmingCharacters(in: .whitespacesAndNewlines)
         if !normalizedCurrentDirectory.isEmpty {
             currentDirectory = normalizedCurrentDirectory
         }
+        let workspaceSnapshotDirectory = currentDirectory

         let panelSnapshotsById = Dictionary(uniqueKeysWithValues: snapshot.panels.map { ($0.id, $0) })
         let leafEntries = restoreSessionLayout(snapshot.layout)
         var oldToNewPanelIds: [UUID: UUID] = [:]

         for entry in leafEntries {
             restorePane(
                 entry.paneId,
                 snapshot: entry.snapshot,
                 panelSnapshotsById: panelSnapshotsById,
                 workspaceFocusedPanelId: snapshot.focusedPanelId,
+                workspaceSnapshotDirectory: workspaceSnapshotDirectory,
                 oldToNewPanelIds: &oldToNewPanelIds
             )
         }
@@
     private func restorePane(
         _ paneId: PaneID,
         snapshot: SessionPaneLayoutSnapshot,
         panelSnapshotsById: [UUID: SessionPanelSnapshot],
         workspaceFocusedPanelId: UUID?,
+        workspaceSnapshotDirectory: String?,
         oldToNewPanelIds: inout [UUID: UUID]
     ) {
@@
             guard let createdPanelId = createPanel(
                 from: panelSnapshot,
                 inPane: paneId,
-                workspaceFocusedPanelId: workspaceFocusedPanelId
+                workspaceFocusedPanelId: workspaceFocusedPanelId,
+                workspaceSnapshotDirectory: workspaceSnapshotDirectory
             ) else { continue }
             createdPanelIds.append(createdPanelId)
             oldToNewPanelIds[oldPanelId] = createdPanelId
         }
@@
     private func createPanel(
         from snapshot: SessionPanelSnapshot,
         inPane paneId: PaneID,
-        workspaceFocusedPanelId: UUID?
+        workspaceFocusedPanelId: UUID?,
+        workspaceSnapshotDirectory: String?
     ) -> UUID? {
         switch snapshot.type {
         case .terminal:
             let workingDirectory = snapshot.terminal?.workingDirectory
                 ?? snapshot.directory
-                ?? (snapshot.id == workspaceFocusedPanelId ? currentDirectory : nil)
+                ?? (snapshot.id == workspaceFocusedPanelId ? workspaceSnapshotDirectory : nil)
             let replayEnvironment = SessionScrollbackReplayStore.replayEnvironment(
                 for: snapshot.terminal?.scrollback
             )
@@
             }
             applySessionPanelMetadata(snapshot, toPanelId: terminalPanel.id)
+            if let workingDirectory = workingDirectory {
+                panelDirectories[terminalPanel.id] = workingDirectory
+            }
             return terminalPanel.id

Also applies to: 492-510, 540-549

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 227 - 233, The restore path is reading
the live currentDirectory and can pick the wrong cwd during recreation; capture
the normalized snapshot cwd once in restoreSessionSnapshot and thread that
immutable value into restorePane and createPanel (instead of reading
Workspace.currentDirectory), seed panelDirectories from that resolved launch
cwd, and ensure applySessionPanelMetadata uses the passed-in immutable cwd
rather than mutating/reading currentDirectory so recreated tabs keep the
intended directory; update calls at the restore sites (including the call
replacing restorePane(entry.paneId, ...)) to pass the captured snapshot cwd
through.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@Sources/Workspace.swift`:
- Around line 227-233: The restore path is reading the live currentDirectory and
can pick the wrong cwd during recreation; capture the normalized snapshot cwd
once in restoreSessionSnapshot and thread that immutable value into restorePane
and createPanel (instead of reading Workspace.currentDirectory), seed
panelDirectories from that resolved launch cwd, and ensure
applySessionPanelMetadata uses the passed-in immutable cwd rather than
mutating/reading currentDirectory so recreated tabs keep the intended directory;
update calls at the restore sites (including the call replacing
restorePane(entry.paneId, ...)) to pass the captured snapshot cwd through.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eacc8002-66c8-4e0f-8455-e7bc38082dcb

📥 Commits

Reviewing files that changed from the base of the PR and between 0e26e5f and 1852c3c.

📒 Files selected for processing (2)
  • Sources/Workspace.swift
  • cmuxTests/SessionPersistenceTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmuxTests/SessionPersistenceTests.swift

@jorgitin02 jorgitin02 closed this May 5, 2026
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