Skip to content

feat: persist workspace sessions by directory - #4300

Open
Fewmanism wants to merge 8 commits into
manaflow-ai:mainfrom
Fewmanism:feat/workspace-session-persistence
Open

Fewmanism wants to merge 8 commits into
manaflow-ai:mainfrom
Fewmanism:feat/workspace-session-persistence

Conversation

@Fewmanism

@Fewmanism Fewmanism commented May 18, 2026 •

Copy link
Copy Markdown

Summary

  • Add per-workspace session snapshot persistence keyed by canonical working directory.
  • Restore a saved workspace session when opening a workspace for an explicit directory.
  • Keep config/layout-driven workspace creation fresh by opting out of workspace session restore.

Test Plan

  • git diff --check
  • swiftc -parse Sources/SessionPersistence.swift Sources/TabManager.swift Sources/AppDelegate.swift Sources/CmuxConfigExecutor.swift Sources/TerminalController.swift cmuxTests/SessionPersistenceTests.swift cmuxTests/TabManagerSessionSnapshotTests.swift
  • Targeted xcodebuild test could not run locally because the active developer directory is CommandLineTools, not a full Xcode install.

Notes

  • Workspace session snapshots are stored in Application Support by hashed canonical working directory, not inside the project directory.

Summary by cubic

Persist per-directory workspace sessions and auto-restore them when opening that directory. Keys are stable across subdirectory changes; restore runs async with safety checks.

  • New Features

    • Save/load workspace snapshots in Application Support, keyed by SHA-256 of the canonical directory; identical data skips rewrites.
    • Stabilized snapshot keys via workspaceSessionRootDirectory stored in snapshots; batch saves prefer this root over the live currentDirectory.
    • Auto-restore when creating a workspace with an explicit workingDirectory and no initial command/input/env; runs off the main thread, verifies the workspace didn’t change (title/cwd/panels), preserves explicit titles, updates the window title, refreshes Git metadata, and falls back to Welcome if nothing to restore.
    • Opt out via restoreWorkspaceSession: false (used for config/layout flows).
    • App-level session saves now cascade per-workspace snapshot writes after a successful or unchanged app snapshot write and when closing a window; save results now distinguish unchanged vs failed writes, and unchanged counts as success.
  • Migration

    • TabManager.init/addWorkspace add: workspaceSessionAppSupportDirectory, workspaceSessionBundleIdentifier, restoreWorkspaceSession (default true).
    • TerminalController restores only for non-layout workspaces; CmuxConfigExecutor disables restore for config-driven workspaces.
    • No user action required.

Written for commit c20f276. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • New Features

    • Workspace-level session persistence: per-workspace state (custom title, current directory, panels) is saved and restorable.
    • Optional automatic restore when creating workspaces—only when using an explicit working directory and enabled.
  • Behavior

    • Workspace snapshots stored separately with canonicalized directory keys; writes skip unchanged data and may run sync or async.
    • Restore is applied asynchronously, validated before applying, and won’t overwrite recent user changes.
  • Tests

    • Added tests covering save/load, canonicalization, idempotent writes, and restore scenarios.

Review Change Stack

@vercel

vercel Bot commented May 18, 2026

Copy link
Copy Markdown

@Fewmanism 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 May 18, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds per-workspace, versioned, canonicalized session snapshot persistence and atomic storage; integrates async workspace-session restoration into TabManager with opt-in flags; wires creation sites to enable/disable restore; updates AppDelegate to coordinate primary and per-workspace saves; and adds tests for persistence and restore behaviors.

Changes

Workspace Session Persistence

Layer / File(s) Summary
Workspace snapshot persistence APIs
Sources/SessionPersistence.swift
Introduced WorkspaceSessionSnapshotEnvelope and SessionSnapshotWriteResult; added loadWorkspaceSnapshot, saveWorkspaceSnapshot, saveWorkspaceSnapshots, helpers for canonicalizing directories, computing SHA-256 digest, deriving per-workspace session.json under Application Support, and JSON encoding with .sortedKeys. Imported CryptoKit.
TabManager workspace restoration
Sources/TabManager.swift
Added restoration parameters to initializer and addWorkspace, gating logic to request restoration only when no explicit overrides exist, and implemented async scheduleWorkspaceSessionRestore(...) that validates the workspace and applies restoreSessionSnapshot, suppressing auto-welcome when restored.
AppDelegate persistence coordination
Sources/AppDelegate.swift
Refactored session snapshot building (extracted sessionWindowSnapshot(...)), introduced writePrimarySnapshotBlock/writeAllSnapshotsBlock, added persistWorkspaceSessionSnapshots(...), and changed window-unregister persistence to persist workspace snapshots without a full primary save.
Creation-site restore flags
Sources/CmuxConfigExecutor.swift, Sources/TerminalController.swift
Propagated restoreWorkspaceSession into workspace-creation calls; config-created workspaces explicitly disable restore and v2 workspace creation enables restore only for non-custom layouts.
Workspace model plumbing
Sources/Workspace.swift
Added workspaceSessionRootDirectory stored property, included it in sessionSnapshot(...), and restored it when non-empty.
Workspace persistence and restoration tests
cmuxTests/SessionPersistenceTests.swift, cmuxTests/TabManagerSessionSnapshotTests.swift
Added tests for saveResult semantics, workspace snapshot round-trip keyed by workingDirectory, canonicalization/preserved live directory behavior, deduplication of identical writes, and TabManager restore behaviors (apply persisted metadata, preserve explicit title, avoid overwriting user mutations, and disable restore option).

Sequence Diagram

sequenceDiagram
  participant TabManager
  participant SessionPersistenceStore
  participant FileSystem
  participant Workspace

  TabManager->>SessionPersistenceStore: saveWorkspaceSnapshot(snapshot, workingDirectory)
  SessionPersistenceStore->>SessionPersistenceStore: canonicalize directory & compute SHA-256 digest
  SessionPersistenceStore->>FileSystem: write envelope JSON to per-workspace session.json
  TabManager->>SessionPersistenceStore: loadWorkspaceSnapshot(workingDirectory) [async]
  SessionPersistenceStore->>FileSystem: read per-workspace session.json
  SessionPersistenceStore->>SessionPersistenceStore: validate version & match canonical workingDirectory
  SessionPersistenceStore->>TabManager: return SessionWorkspaceSnapshot?
  TabManager->>Workspace: restoreSessionSnapshot(snapshot) — apply title/currentDirectory/panels
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#4130: Also modifies the session snapshot schema (adds unread-related optional fields) and may intersect with snapshot decoding/compatibility.

Poem

🐰 I hashed the paths where your projects hide,
I wrapped each workspace safe and snug inside.
When tabs awaken, their old titles hop home,
Quiet restores so you need never roam.
A tiny rabbit keeps your sessions known.


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (2 errors, 1 warning, 2 inconclusive)

Check name Status Explanation Resolution
Cmux Swift Concurrency ❌ Error Fire-and-forget Task without storage in TabManager.scheduleWorkspaceSessionRestore; new DispatchQueue.sync blocking call in AppDelegate.persistSessionSnapshot Store Task for lifecycle. Use async/await instead of DispatchQueue.sync.
Cmux Architecture Rethink ❌ Error Async restore missing window title update and workspaceSessionRootDirectory clear. Both were flagged in review with explicit fixes ignored, leaving lifecycle state inconsistent. Apply: (1) Workspace.restoreSessionSnapshot add else clause clearing workspaceSessionRootDirectory; (2) TabManager.scheduleWorkspaceSessionRestore call updateWindowTitle after restore.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Swift Actor Isolation ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
Description check ❓ Inconclusive PR description includes summary of changes and test plan, but lacks explicit testing results, demo video, bot review trigger request, and comprehensive checklist completion. Clarify which tests were run successfully, provide a demo video if UI changed, request bot reviews using the trigger block, and confirm all checklist items are completed.
✅ Passed checks (11 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: persist workspace sessions by directory' accurately summarizes the main change: adding per-workspace session snapshot persistence keyed by working directory.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Blocking Runtime ✅ Passed No blocking patterns in production code. sync() calls limited to background queue at app termination. Task.detached used for I/O. Test polling is acceptable.
Cmux No Hacky Sleeps ✅ Passed This check applies only to non-Swift production code (TypeScript, JavaScript, shell, build/runtime scripts). All PR changes are Swift files, which are explicitly covered by a separate check.
Cmux Swift @Concurrent ✅ Passed No violations of swift-concurrent-annotation.md rules. File I/O is properly hopped via Task.detached or sessionPersistenceQueue. No invalid @concurrent or nonisolated annotations present.
Cmux Swift File And Package Boundaries ✅ Passed SessionPersistence +217 lines (541→762, under 800). AppDelegate +73, TabManager +86, Workspace +7, TerminalController +2 below threshold. Responsibilities properly separated without mixing concerns.
Cmux Swift Logging ✅ Passed New runtime code passes Swift logging check: no print/NSLog/debugPrint/dump found in SessionPersistence, TabManager, AppDelegate, or Workspace changes. All new functions are clean.
Cmux User-Facing Error Privacy ✅ Passed No user-facing errors, alerts, or localized strings added. New internal types have no user-visible strings. Error handling is silent internally.
Cmux Swiftui State Layout ✅ Passed No SwiftUI state violations found. PR adds data types (Codable/Sendable enums/structs), a plain String? property, and state mutations only in proper lifecycle callbacks—not in render/body contexts.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR does not add or materially change any standalone cmux-owned windows. Changes are session persistence/restoration logic and data structures only.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 May 18, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds per-directory workspace session persistence: snapshots are keyed by SHA-256 of the canonicalized working directory and stored in Application Support, restored asynchronously when a workspace is opened for an explicit directory, and skipped for config/layout-driven workspaces.

  • SessionPersistence.swift gains WorkspaceSessionSnapshotEnvelope (disk format with stable createdAt), SessionSnapshotWriteResult (distinguishes written/unchanged/failed for conditional cascade), and the full loadWorkspaceSnapshot/saveWorkspaceSnapshot/saveWorkspaceSnapshots surface.
  • TabManager.addWorkspace schedules an async restore via a @MainActor + Task.detached pattern, with triple-guard validation (title, cwd, panel IDs) before applying; CmuxConfigExecutor and TerminalController opt layout-driven workspaces out via restoreWorkspaceSession: false.
  • AppDelegate cascades per-workspace snapshot writes after a successful (or unchanged) app snapshot write and on window close, using workspaceSessionRootDirectory as a stable key that survives cd navigation.

Confidence Score: 5/5

Safe to merge; all new code paths carry adequate guards and the existing test suite exercises the main restore, opt-out, mutation-guard, and idempotent-write paths.

The core persistence logic is well-isolated: canonicalization, key stability, and idempotent writes are all tested. The async restore path is correctly @MainActor-anchored with Task.detached for I/O and a triple-guard before applying state. The two open items are declaration-level and lifecycle hygiene concerns that do not affect runtime correctness today.

Sources/SessionPersistence.swift (SessionSnapshotWriteResult isolation annotation) and Sources/TabManager.swift (restore Task lifecycle) have minor cleanup left but no blocking correctness issue.

Important Files Changed

Filename Overview
Sources/SessionPersistence.swift Adds WorkspaceSessionSnapshotEnvelope, SessionSnapshotWriteResult, and full workspace snapshot save/load/key logic; createdAt is correctly preserved across identical saves; SessionSnapshotWriteResult lacks nonisolated marking for strict-concurrency safety.
Sources/TabManager.swift Adds scheduleWorkspaceSessionRestore as @mainactor with Task.detached I/O; actor isolation and concurrency shape are improved over the prior GCD version; outer Task is fire-and-forget without a stored cancellation handle.
Sources/AppDelegate.swift Adds persistWorkspaceSessionSnapshots and cascades workspace snapshot writes after app/window save; closingWorkspaceSnapshot is captured before context unregister, preserving the correct workspace state on window close.
Sources/Workspace.swift Adds workspaceSessionRootDirectory as a non-@published stable key field, set at init and round-tripped through session snapshots; correctly cleared when snapshot field is absent.
Sources/CmuxConfigExecutor.swift Opts config-driven workspace creation out of session restore via restoreWorkspaceSession: false; minimal and correct change.
Sources/TerminalController.swift Opts layout-driven workspaces out of session restore (restoreWorkspaceSession: layoutNode == nil); minimal and correct.
cmuxTests/SessionPersistenceTests.swift Adds round-trip, canonicalization, stable-key, and idempotent-write tests for workspace snapshot persistence; thorough coverage of the new save/load surface.
cmuxTests/TabManagerSessionSnapshotTests.swift Adds integration tests for async restore, explicit-title preservation, mutation guard, opt-out, and root-directory clearing; polling helper uses RunLoop-driven spin which is acceptable in test scaffolding.

Sequence Diagram

sequenceDiagram
    participant AC as AppDelegate (MainActor)
    participant TM as TabManager (MainActor)
    participant WS as Workspace (MainActor)
    participant SP as SessionPersistenceStore
    participant FS as FileSystem (App Support)

    Note over AC,FS: Workspace open (explicit directory)
    AC->>TM: addWorkspace(workingDirectory:restoreWorkspaceSession:true)
    TM->>WS: "create Workspace(workspaceSessionRootDirectory = dir)"
    TM->>TM: scheduleWorkspaceSessionRestore(workspaceId, expectedState)
    TM-->>SP: Task.detached(priority:.utility) loadWorkspaceSnapshot(dir)
    SP->>FS: canonicalize dir to SHA256 key, read session.json
    SP-->>TM: SessionWorkspaceSnapshot?
    TM->>TM: guard workspace still matches expectedState
    alt snapshot found
        TM->>WS: restoreSessionSnapshot(snapshot)
        TM->>TM: clearWorkspaceGitProbes / scheduleGitRefresh
    else no snapshot
        TM->>AC: sendWelcomeCommandWhenReady (if autoWelcome)
    end

    Note over AC,FS: Autosave or window close
    AC-->>SP: sessionPersistenceQueue.async saveResult(appSnapshot)
    SP->>FS: write app session.json (skip if unchanged)
    alt write succeeded or unchanged
        SP->>SP: saveWorkspaceSnapshots(from: appSnapshot)
        loop each workspace
            SP->>FS: canonicalize workspaceSessionRootDirectory or currentDirectory
            SP->>FS: read existing envelope (preserve createdAt)
            SP->>FS: write workspace SHA256(dir)/session.json (skip if identical)
        end
    end
Loading

Reviews (8): Last reviewed commit: "fix: address remaining workspace restore..." | Re-trigger Greptile

Comment thread Sources/TabManager.swift Outdated
Comment thread Sources/SessionPersistence.swift
Comment thread Sources/SessionPersistence.swift

@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 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/SessionPersistence.swift">

<violation number="1" location="Sources/SessionPersistence.swift:503">
P2: The `createdAt` timestamp defeats the deduplication optimization in `saveWorkspaceSnapshot`, causing unnecessary repeated writes.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread Sources/SessionPersistence.swift Outdated
@Fewmanism

Copy link
Copy Markdown
Author

Addressed the review feedback in 4bd8a1035.

Changes:

  • Moved workspace snapshot restore file I/O off the TabManager.addWorkspace main-actor creation path and onto a utility queue.
  • Added a pristine-workspace guard before applying async restore so immediate post-creation mutations are not clobbered.
  • Preserved explicit workspace titles after async restore, matching the previous synchronous restore semantics.
  • Kept synchronous app-session persistence writes on sessionPersistenceQueue instead of executing the write block directly on the main actor.
  • Marked WorkspaceSessionSnapshotEnvelope as nonisolated.
  • Reused an existing workspace snapshot envelope createdAt when version/directory match, so identical payloads hit the existing-data early return instead of rewriting every autosave.
  • Added regression coverage for workspace dedup, async restore, explicit-title preservation, and mutation-before-restore skip behavior.

Verification:

  • git diff --check
  • swiftc -parse Sources/SessionPersistence.swift Sources/TabManager.swift Sources/AppDelegate.swift Sources/CmuxConfigExecutor.swift Sources/TerminalController.swift cmuxTests/SessionPersistenceTests.swift cmuxTests/TabManagerSessionSnapshotTests.swift
  • Independent reviewer pass: safe_to_commit true

Local blocker:

  • Full xcodebuild test still cannot run here because the active developer directory is CommandLineTools rather than a full Xcode install.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the PR diff for review
gh pr view 4300 --repo manaflow-ai/cmux --json number,title,body,commits,files

# Get the diff
gh pr diff 4300 --repo manaflow-ai/cmux

Length of output: 39423


@Fewmanism: Great work on the follow-up commit — the major concerns from the initial review are all addressed. Here are the remaining observations:


🔴 Nit – nonisolated on a struct type

Sources/SessionPersistence.swift

nonisolated struct WorkspaceSessionSnapshotEnvelope: Codable, Sendable {

nonisolated is a member-level modifier in Swift (for actor members / stored properties to opt out of isolation). Applying it directly to a struct declaration is non-standard and will likely generate a compiler warning under strict concurrency checking — "nonisolated cannot be applied to struct; structs are implicitly nonisolated". Since the type already conforms to Sendable, the modifier is redundant. Drop it:

struct WorkspaceSessionSnapshotEnvelope: Codable, Sendable {

🟡 Stale snapshot accumulation in Application Support

Sources/SessionPersistence.swift – saveWorkspaceSnapshots

Snapshot files (one per SHA-256 hashed directory) are written but never removed when a working directory is deleted or renamed. Over time, Application Support will accumulate orphaned session.json files indefinitely. Worth a follow-up issue to prune snapshots whose canonical path no longer resolves — perhaps during app launch or when the session count exceeds a threshold.


🟡 canonicalWorkspaceDirectoryPath silently drops virtual/remote paths

The helper requires fileManager.fileExists(atPath:, isDirectory:) to succeed, which means any path that doesn't exist on the local filesystem (e.g., a remote SSH working directory) will return nil and skip persistence. This is the correct safe default, but it means remote-CWD workspaces in saveWorkspaceSnapshots will silently skip every autosave cycle. A debug-level os_log on the return nil path would help diagnose unexpected skips.


🟢 Test helper suggestion – waitForWorkspaceSessionRestore

cmuxTests/TabManagerSessionSnapshotTests.swift

The busy-wait approach works, but XCTestExpectation / fulfillment(of:timeout:) is the idiomatic XCTest pattern for async conditions and surfaces better failure messages (timeout reason vs. silent false). Consider for a later cleanup:

let exp = expectation(description: "workspace session restore")
// … poll or use KVO / NotificationCenter to fulfill …
wait(for: [exp], timeout: 2.0)

🟢 Test name accuracy

testInitialWorkspaceRestoreCanBeDisabledForExplicitLayouts tests restoreWorkspaceSession: false via the init flag directly, without actually constructing a layout. The name is slightly misleading; something like testInitialWorkspaceRestoreCanBeDisabledViaFlag would match the assertion more precisely. Minor.


✅ Everything else looks good

Concern from prior review Status
File I/O off main actor ✅ workspaceSessionRestoreQueue
Pristine-workspace guard ✅ customTitle + currentDirectory + panelIds
Explicit title preservation ✅ explicitTitle override after restore
Sync writes on background queue ✅ sessionPersistenceQueue.sync
createdAt reuse / dedup early-return ✅ existingEnvelope check
Regression coverage ✅ 7 new test cases across 2 files
Config/layout opt-out ✅ restoreWorkspaceSession: false in CmuxConfigExecutor and TerminalController

The nonisolated struct syntax is the only item that could produce a real compiler diagnostic — the rest are polish. Once that's resolved the implementation looks solid.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Comment thread Sources/TabManager.swift Outdated
Comment thread Sources/AppDelegate.swift Outdated

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 3735-3737: The per-workspace writes must only run when the primary
session save succeeds: capture and check the result of
SessionPersistenceStore.save(snapshot) inside the if let snapshot block and only
call SessionPersistenceStore.saveWorkspaceSnapshots(from: snapshot) when that
save returned success; if the save fails, avoid invoking saveWorkspaceSnapshots
(and emit/log an error or handle the failure path) so workspace restore files
can't diverge from a failed main session write.

In `@Sources/TabManager.swift`:
- Around line 2247-2259: addWorkspace schedules git/PR probing for the bootstrap
terminal before the async restore runs, but after
workspace.restoreSessionSnapshot(...) replaces the panel graph the restored
terminal panels never get re-probed; update the restore path initiated by
scheduleWorkspaceSessionRestore to, once the async restore completes, explicitly
re-register the git/PR probing for the restored panels by invoking the same
probing helper used in the full-manager restore path (i.e. the helper that
probes git/PR state for terminal panels), ensuring the call occurs after
workspace.restoreSessionSnapshot(...) so restored terminals receive branch/PR
sidebar state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f6ec2fb3-6494-441b-973c-109b62379594

📥 Commits

Reviewing files that changed from the base of the PR and between 51306ab and 4bd8a10.

📒 Files selected for processing (5)
  • Sources/AppDelegate.swift
  • Sources/SessionPersistence.swift
  • Sources/TabManager.swift
  • cmuxTests/SessionPersistenceTests.swift
  • cmuxTests/TabManagerSessionSnapshotTests.swift

Comment thread Sources/AppDelegate.swift Outdated
Comment thread Sources/TabManager.swift
@Fewmanism

Copy link
Copy Markdown
Author

Follow-up fixes pushed in 359619c09 for the latest review comments.

Additional changes:

  • AppDelegate.persistSessionSnapshot now gates per-workspace snapshot writes on the primary SessionPersistenceStore.save(snapshot) succeeding.
  • The synchronous termination flush no longer performs or schedules the O(workspaces) derived workspace snapshot writes; it only flushes the primary session snapshot.
  • TabManager.scheduleWorkspaceSessionRestore now uses Swift concurrency (Task + Task.detached(priority: .utility)) instead of a new GCD queue / DispatchQueue.main.async callback.
  • After async workspace restore applies, restored terminal panels are re-registered for git metadata probing with reason: "workspace-session-restore".

Verification:

  • git diff --check
  • swiftc -parse Sources/SessionPersistence.swift Sources/TabManager.swift Sources/AppDelegate.swift Sources/CmuxConfigExecutor.swift Sources/TerminalController.swift cmuxTests/SessionPersistenceTests.swift cmuxTests/TabManagerSessionSnapshotTests.swift

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the latest PR diff
gh pr diff 4300 --repo manaflow-ai/cmux

Length of output: 35142


@Fewmanism: The Swift concurrency migration (Task.detached) and the AppDelegate gating logic look clean. One prior item still needs attention:


🔴 nonisolated on struct — still present

Sources/SessionPersistence.swift

nonisolated struct WorkspaceSessionSnapshotEnvelope: Codable, Sendable {

This modifier was flagged in the previous round and is still in the diff. nonisolated is an actor-member keyword; it has no meaning on a nominal type declaration and triggers a Swift compiler warning under strict concurrency checking ("'nonisolated' is not supported on struct declarations"). Since the type already conforms to Sendable, it is redundant. The fix is a one-character change:

struct WorkspaceSessionSnapshotEnvelope: Codable, Sendable {

✅ Changes from 359619c09 look good

Concern from prior round Status
File I/O off main actor ✅ Task.detached(priority: .utility)
Pristine-workspace guard ✅ customTitle + currentDirectory + panelIds check
Sync termination flush skips workspace I/O ✅ writePrimarySnapshotBlock returns Bool; sync path discards it
Workspace snapshots gated on primary save ✅ if writePrimarySnapshotBlock(), let snapshot
Git probe re-registration after restore ✅ clearWorkspaceGitProbes + scheduleInitialWorkspaceGitMetadataRefreshIfPossible
Explicit title preservation after restore ✅ workspace.setCustomTitle(explicitTitle) override

The nonisolated struct is the only remaining compiler-visible issue. Once that is removed, the implementation is ready.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 3743-3749: The synchronous branch currently runs
writePrimarySnapshotBlock on sessionPersistenceQueue but does not call
SessionPersistenceStore.saveWorkspaceSnapshots, leaving per-workspace snapshots
stale; update the synchronously == true path (where sessionPersistenceQueue.sync
executes writePrimarySnapshotBlock) to detect the returned snapshot (from
writePrimarySnapshotBlock) and call
SessionPersistenceStore.saveWorkspaceSnapshots(from:) just like the async
closure does so per-workspace snapshot files are written on both sync and async
flows.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4d62c69d-561b-4e61-9b1d-ffccf80169e6

📥 Commits

Reviewing files that changed from the base of the PR and between 4bd8a10 and 359619c.

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

Comment thread Sources/AppDelegate.swift Outdated
Comment thread Sources/TabManager.swift Outdated
@Fewmanism

Copy link
Copy Markdown
Author

Second follow-up pushed in 0e4b1a62d.

Changes:

  • Restored synchronous per-workspace snapshot saving, but now through a single writeAllSnapshotsBlock that only runs workspace snapshot persistence if the primary session save succeeds.
  • Marked scheduleWorkspaceSessionRestore as @MainActor so the outer Task inherits main-actor isolation while the file I/O remains isolated in Task.detached(priority: .utility).

Verification:

  • git diff --check
  • swiftc -parse Sources/SessionPersistence.swift Sources/TabManager.swift Sources/AppDelegate.swift Sources/CmuxConfigExecutor.swift Sources/TerminalController.swift cmuxTests/SessionPersistenceTests.swift cmuxTests/TabManagerSessionSnapshotTests.swift

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh pr diff 4300 --repo manaflow-ai/cmux

Length of output: 35203


@Fewmanism: The AppDelegate consolidation (writeAllSnapshotsBlock) and the @MainActor + Task.detached migration in scheduleWorkspaceSessionRestore both look correct. One thing still needs fixing before this is mergeable:


🔴 nonisolated on struct — still present (third round)

Sources/SessionPersistence.swift

nonisolated struct WorkspaceSessionSnapshotEnvelope: Codable, Sendable {

This modifier was flagged in the two previous review rounds and is still in the diff unchanged. nonisolated is an actor-member keyword — it is not valid on a nominal type declaration and produces a Swift compiler warning under strict concurrency checking: "'nonisolated' is not supported on struct declarations". The type is already Sendable, so the modifier is entirely redundant. One-word fix:

struct WorkspaceSessionSnapshotEnvelope: Codable, Sendable {

✅ Changes from 0e4b1a62d look good

Concern Status
writeAllSnapshotsBlock gates workspace I/O on primary save ✅ if writePrimarySnapshotBlock(), let snapshot
Sync termination flush now includes workspace snapshots ✅ both paths dispatch writeAllSnapshotsBlock via sessionPersistenceQueue
scheduleWorkspaceSessionRestore main-actor isolation ✅ @MainActor outer + Task.detached(priority: .utility) for I/O
Explicit title preservation after restore ✅ workspace.setCustomTitle(explicitTitle) override

The nonisolated struct is the sole remaining blocker. Everything else is in good shape.

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 3743-3746: The current logic inside writeAllSnapshotsBlock
incorrectly skips calling SessionPersistenceStore.saveWorkspaceSnapshots(from:)
when writePrimarySnapshotBlock() returns false; change the flow so that if let
snapshot { SessionPersistenceStore.saveWorkspaceSnapshots(from: snapshot) } is
executed regardless of the boolean result of writePrimarySnapshotBlock(), and
only treat false as a hard failure when SessionPersistenceStore.save(_:)
actually indicates a failure (consider updating SessionPersistenceStore.save(_:)
to return a richer enum like .written / .unchanged / .failed and adjust
writePrimarySnapshotBlock and its callers to handle the new status if needed).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: eb8496cd-661f-47e0-b99e-46acaef9f0c3

📥 Commits

Reviewing files that changed from the base of the PR and between 359619c and 0e4b1a6.

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

Comment thread Sources/AppDelegate.swift
@Fewmanism

Copy link
Copy Markdown
Author

Follow-up for the latest CodeRabbit comment pushed in fb9d91b85.

Changes:

  • Added SessionSnapshotWriteResult with .written, .unchanged, and .failed so an unchanged primary session file is no longer conflated with a failed save.
  • Kept the existing SessionPersistenceStore.save(...) -> Bool API compatible: .written and .unchanged are success, .failed is false.
  • Updated AppDelegate.persistSessionSnapshot to save per-workspace snapshots when the primary session save result is .written or .unchanged, and skip only .failed.
  • Added regression coverage for .written then .unchanged save results.

Verification:

  • git diff --check
  • swiftc -parse Sources/SessionPersistence.swift Sources/TabManager.swift Sources/AppDelegate.swift Sources/CmuxConfigExecutor.swift Sources/TerminalController.swift cmuxTests/SessionPersistenceTests.swift cmuxTests/TabManagerSessionSnapshotTests.swift
  • Independent reviewer pass: safe_to_commit true

Local blocker remains unchanged:

  • Full xcodebuild test cannot run here because the active developer directory is CommandLineTools rather than full Xcode.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh pr diff 4300 --repo manaflow-ai/cmux

Length of output: 38061


@Fewmanism: The SessionSnapshotWriteResult enum and the AppDelegate gating logic are clean additions — good call distinguishing .unchanged from .failed for the workspace snapshot follow-on writes.

One item still needs to be fixed before merge:


🔴 nonisolated on nominal types — now on two declarations

Sources/SessionPersistence.swift

nonisolated struct WorkspaceSessionSnapshotEnvelope: Codable, Sendable {
nonisolated enum SessionSnapshotWriteResult: Equatable, Sendable {

nonisolated was carried forward on WorkspaceSessionSnapshotEnvelope (flagged in the three prior rounds) and is now also applied to the new SessionSnapshotWriteResult enum. The modifier is an actor-member keyword; Swift does not accept it on nominal type declarations and will emit "'nonisolated' is not supported on struct/enum declarations" under strict concurrency. Both types already conform to Sendable, so the keyword is entirely redundant. Drop it from both:

struct WorkspaceSessionSnapshotEnvelope: Codable, Sendable {
enum SessionSnapshotWriteResult: Equatable, Sendable {

✅ Everything else in fb9d91b85 looks good

Item Status
SessionSnapshotWriteResult .written/.unchanged/.failed semantics ✅
save(_:) -> Bool backward-compat via saveResult(...).succeeded ✅
writeAllSnapshotsBlock gates workspace writes on != .failed ✅
Sync path uses sessionPersistenceQueue.sync ✅
scheduleWorkspaceSessionRestore @MainActor isolation ✅
testSaveResultDistinguishesUnchangedFromFailedWrites regression ✅

The two nonisolated removals are the only remaining change needed. Would you like to open a follow-up issue for the stale snapshot pruning strategy mentioned in earlier rounds?

Comment thread Sources/SessionPersistence.swift

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 3743-3749: The fast path in writeAllSnapshotsBlock returns when
snapshot is nil and thus skips
SessionPersistenceStore.saveWorkspaceSnapshots(from:), causing lost
per-workspace state for the last-closed window; change the branch in
writeAllSnapshotsBlock so you persist per-workspace snapshots from the
pre-removal snapshot before taking the fast exit (i.e., call
SessionPersistenceStore.saveWorkspaceSnapshots(from: snapshot) prior to or
immediately after invoking writePrimarySnapshotBlock() in the snapshot == nil
branch), or alternatively move the saveSessionSnapshot call in
unregisterMainWindow(_:) earlier so the closing workspaces are still present
when writeAllSnapshotsBlock runs; refer to writeAllSnapshotsBlock,
writePrimarySnapshotBlock(),
SessionPersistenceStore.saveWorkspaceSnapshots(from:), and
unregisterMainWindow(_:) when making the change.

In `@Sources/SessionPersistence.swift`:
- Around line 380-387: The file declares nominal types with invalid
`nonisolated` modifiers; remove the `nonisolated` keyword from the type
declarations for WorkspaceSessionSnapshotEnvelope and SessionSnapshotWriteResult
so they are declared as normal `struct WorkspaceSessionSnapshotEnvelope:
Codable, Sendable { ... }` and `enum SessionSnapshotWriteResult: Equatable,
Sendable { ... }`, preserving their existing conformances and members; do not
change any nested method/ property `nonisolated` uses (if present) — only delete
the `nonisolated` before the `struct` and `enum` keywords.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d6de7020-7894-4566-8174-ba8098477640

📥 Commits

Reviewing files that changed from the base of the PR and between 0e4b1a6 and fb9d91b.

📒 Files selected for processing (3)
  • Sources/AppDelegate.swift
  • Sources/SessionPersistence.swift
  • cmuxTests/SessionPersistenceTests.swift

Comment thread Sources/AppDelegate.swift
Comment thread Sources/SessionPersistence.swift Outdated
@Fewmanism

Copy link
Copy Markdown
Author

Follow-up pushed in 9a0dc2bd9.

Changes:

  • Removed the invalid nonisolated modifiers from the new nominal struct/enum declarations while keeping Codable/Sendable conformances.
  • Added a stable workspaceSessionRootDirectory to workspace snapshots so batch per-workspace saves key by the original workspace root instead of the terminal's live currentDirectory after cd.
  • Preserved the live currentDirectory inside the saved workspace snapshot, so the stable lookup key and restored cwd no longer fight each other.
  • Captured and flushed the closing window's workspace snapshot before unregisterMainWindowContext(for:) removes the last context, so the last-window close path no longer loses the final per-workspace state.
  • Added regression coverage for stable-root batch saves and preserving live directory while canonicalizing the lookup key.

Verification:

  • git diff --check
  • swiftc -parse Sources/SessionPersistence.swift Sources/Workspace.swift Sources/TabManager.swift Sources/AppDelegate.swift Sources/CmuxConfigExecutor.swift Sources/TerminalController.swift cmuxTests/SessionPersistenceTests.swift cmuxTests/TabManagerSessionSnapshotTests.swift cmuxTests/PiVaultAgentPersistenceTests.swift
  • Independent reviewer pass: safe_to_commit: true

Local blocker remains unchanged:

  • Full xcodebuild test cannot run here because this machine's active developer directory is CommandLineTools rather than full Xcode.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh pr diff 4300 --repo manaflow-ai/cmux

Length of output: 48023


@Fewmanism: Excellent — this commit is clean. Here's the full assessment:


✅ nonisolated on nominal types — fixed

Both declarations are now correct:

struct WorkspaceSessionSnapshotEnvelope: Codable, Sendable { … }
enum SessionSnapshotWriteResult: Equatable, Sendable { … }

The four-round blocker is resolved.


✅ Changes from 9a0dc2bd9 look good

Item Status
nonisolated removed from WorkspaceSessionSnapshotEnvelope ✅
nonisolated removed from SessionSnapshotWriteResult ✅
Stable workspaceSessionRootDirectory lookup key ✅ Set in Workspace.init, stamped canonical in saveWorkspaceSnapshot, used by saveWorkspaceSnapshots
Live currentDirectory preserved in snapshot ✅ Key and payload no longer conflict
Closing window snapshot captured before unregisterMainWindowContext ✅ Last-window-close data loss is fixed
persistWorkspaceSessionSnapshots helper extracted ✅ Async path correct for non-termination closes
sessionWindowSnapshot(for:...) helper extracted ✅ Removes duplicate inline construction
New regression tests ✅ Batch stable-root save + live-directory preservation

🟡 Minor: double canonicalization in saveWorkspaceSnapshot

Not a bug, but worth noting: saveWorkspaceSnapshot calls canonicalWorkspaceDirectoryPath(workingDirectory:) to produce canonicalDirectory, then passes canonicalDirectory to workspaceSnapshotFileURL(workingDirectory:...) which calls canonicalWorkspaceDirectoryPath again internally. The second call is a no-op on an already-canonical path, but it is redundant work on every save. Low priority, but an easy cleanup if the function signature ever gains an "already-canonical" fast path.


🟡 Reminder: two previously-raised follow-up items (not blockers)

  1. Stale snapshot accumulation — per-directory session.json files are never pruned when directories are renamed/deleted. Opening a follow-up issue is the suggested path.
  2. Silent skip for remote/virtual paths — canonicalWorkspaceDirectoryPath returns nil for non-local paths; a debug-level os_log on that return would help diagnose unexpected persistence skips.

Would you like me to open a GitHub issue to track the stale snapshot pruning work?


✅ Summary table (all rounds)

Concern Status
nonisolated on struct/enum declarations ✅ Resolved
File I/O off main actor ✅ Task.detached(priority: .utility)
Pristine-workspace guard ✅ title + cwd + panelIds
Explicit title preservation after restore ✅ setCustomTitle(explicitTitle)
Sync writes on sessionPersistenceQueue ✅ writeAllSnapshotsBlock
createdAt reuse / dedup ✅ existingEnvelope check
Config/layout opt-out ✅ restoreWorkspaceSession: false
Workspace snapshots gated on primary save ✅ .failed guard
Git probe re-registration after restore ✅ clearWorkspaceGitProbes + refresh
Last-window-close data loss ✅ Snapshot captured before unregisterMainWindowContext
Stable root key vs live currentDirectory ✅ workspaceSessionRootDirectory field
Regression coverage ✅ 9 new test cases across 2 files

The implementation is ready for merge. 🎉

(∩^ᗜ^)⊃━ ✦ ✦ ✦


✏️ Learnings added
Learnt from: Fewmanism
URL: https://github.com/manaflow-ai/cmux/pull/4300

Timestamp: 2026-05-18T11:34:34.448Z
Learning: In the cmux repository (Swift/macOS), `nonisolated` is an actor-member modifier and cannot be applied to nominal type declarations (`struct`, `enum`, `class`). Structs and enums conforming to `Sendable` are already implicitly nonisolated. Applying `nonisolated` to a struct or enum declaration produces a Swift compiler warning under strict concurrency checking and should be removed.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@Fewmanism

Copy link
Copy Markdown
Author

@coderabbitai resolve

All latest CodeRabbit inline comments have been addressed in 9a0dc2bd9, current CodeRabbit check is passing/skipped, and all non-outdated review threads are now resolved.

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# List all review threads on the PR to find CodeRabbit comments to resolve
gh api graphql -f query='
{
  repository(owner: "manaflow-ai", name: "cmux") {
    pullRequest(number: 4300) {
      reviewThreads(first: 50) {
        nodes {
          id
          isResolved
          comments(first: 1) {
            nodes {
              author {
                login
              }
              body
            }
          }
        }
      }
    }
  }
}'

Length of output: 26821


@Fewmanism: All CodeRabbit (coderabbitai) review threads are already marked as resolved — nothing further to action on my side. ✅

One note: there are 5 unresolved threads in the PR from greptile-apps (a separate review bot) that I cannot resolve on their behalf:

Thread Topic
PRRT_kwDORDHQWM6Cylmz Blocking file I/O on main thread during workspace creation
PRRT_kwDORDHQWM6Cylox nonisolated on WorkspaceSessionSnapshotEnvelope struct
PRRT_kwDORDHQWM6CzD77 Legacy GCD dispatch pattern in scheduleWorkspaceSessionRestore
PRRT_kwDORDHQWM6CzD9H Synchronous termination flush performing O(workspaces) I/O
PRRT_kwDORDHQWM6CzUT5 scheduleWorkspaceSessionRestore missing @MainActor annotation

The last three of those were directly addressed in your follow-up commits (Task.detached migration, @MainActor annotation, writeAllSnapshotsBlock gating). You may want to resolve those threads yourself or ask greptile-apps to close them if you consider them addressed. The nonisolated one (PRRT_kwDORDHQWM6Cylox) maps to the same issue that CodeRabbit flagged and is resolved in 9a0dc2bd9.

(´。• ᵕ •。`) ♡

[resolve]

@Fewmanism

Copy link
Copy Markdown
Author

@coderabbitai full review

Please re-review the latest head 9a0dc2bd9; all previous actionable comments have been addressed and all current review threads are resolved.

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh pr diff 4300 --repo manaflow-ai/cmux

Length of output: 48023


@Fewmanism: Full review of head 9a0dc2bd9 — no new blockers found. Here's the complete assessment:


✅ All previously raised actionable items are resolved

Concern Status
nonisolated on struct WorkspaceSessionSnapshotEnvelope ✅ Removed
nonisolated on enum SessionSnapshotWriteResult ✅ Removed
File I/O off main actor ✅ Task.detached(priority: .utility)
Pristine-workspace guard ✅ customTitle + currentDirectory + Set(panelIds)
Explicit title preservation after restore ✅ workspace.setCustomTitle(explicitTitle)
Sync writes on sessionPersistenceQueue ✅ writeAllSnapshotsBlock dispatched via queue
Workspace snapshots gated on primary save success ✅ writePrimarySnapshotBlock() != .failed guard
createdAt reuse / dedup ✅ existingEnvelope check
Config/layout opt-out ✅ restoreWorkspaceSession: false in CmuxConfigExecutor and TerminalController
Git probe re-registration after restore ✅ clearWorkspaceGitProbes + scheduleInitialWorkspaceGitMetadataRefreshIfPossible
Last-window-close data loss ✅ closingWorkspaceSnapshot captured before unregisterMainWindowContext
Stable root key vs live currentDirectory ✅ workspaceSessionRootDirectory field + saveWorkspaceSnapshots uses it as key
SessionSnapshotWriteResult .unchanged vs .failed distinction ✅
Regression coverage ✅ 9 new test cases across 2 files

🟡 Minor carry-over items (not blockers)

Double canonicalization in saveWorkspaceSnapshot (Sources/SessionPersistence.swift)

saveWorkspaceSnapshot computes canonicalDirectory via canonicalWorkspaceDirectoryPath, then passes it to workspaceSnapshotFileURL which calls canonicalWorkspaceDirectoryPath a second time internally. On an already-canonical path the second call is a no-op, but it's redundant work on every save. Easy cleanup if the helper ever gains an "already-canonical" fast-path overload.

Stale snapshot accumulation — per-directory session.json files under Application Support are never pruned when directories are renamed or deleted. Worth a follow-up issue (previously offered to open one — happy to do so if you'd like).

Silent skip for remote/virtual paths — canonicalWorkspaceDirectoryPath returns nil for paths that don't exist on the local filesystem (remote SSH CWDs, etc.) and there's no log on that branch. A os_log(.debug, ...) on the return nil path would help diagnose unexpected persistence skips.


🟢 Polish (same as prior rounds)

  • testInitialWorkspaceRestoreCanBeDisabledForExplicitLayouts tests the restoreWorkspaceSession: false flag directly without a layout; a name like testInitialWorkspaceRestoreCanBeDisabledViaFlag would match the assertion more precisely.
  • waitForWorkspaceSessionRestore busy-polls with a 10 ms RunLoop step; XCTestExpectation / fulfillment(of:timeout:) would give cleaner failure messages — good candidate for a future test-infrastructure pass.

✅ Implementation is ready for merge

All correctness concerns have been addressed. The minor items above are polish/follow-up material with no correctness impact.

Would you like me to open a GitHub issue to track the stale snapshot pruning strategy?

✅ Actions performed

Full review triggered.

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/TabManagerSessionSnapshotTests.swift`:
- Around line 193-197: The assertions that "no restore applied" run after only a
short run loop slice and can miss a late async restore; change the test to wait
the full configured restore timeout before asserting by waiting for the actual
timeout used by the restore logic (or use an XCTestExpectation and XCTWaiter
with that timeout) so the test observes that no restore arrived; update the
occurrences around the RunLoop.current.run(...) call and the checks of
workspace.customTitle, workspace.customDescription, workspace.currentDirectory
(also apply same fix to the other occurrence at lines 233-235) to use the full
timeout-based wait/expectation instead of the 0.2s run loop slice.

In `@Sources/TabManager.swift`:
- Around line 2294-2307: After calling
workspace.restoreSessionSnapshot(restoredSnapshot) (and after
workspace.setCustomTitle if present), explicitly refresh the window title so
async restore doesn't leave the bootstrap title displayed; invoke the existing
window-title update path for this workspace (i.e. re-run whatever logic that the
selectedTabId side effects trigger—call the function that recomputes/applies the
window title for the workspace, e.g. updateWindowTitle(forWorkspaceId:
workspace.id) or the equivalent method used elsewhere) immediately after the
restore and custom title handling; keep the clearWorkspaceGitProbes(...) and
scheduleInitialWorkspaceGitMetadataRefreshIfPossible(...) calls intact.

In `@Sources/Workspace.swift`:
- Around line 268-271: The restore logic ignores nil/blank values and therefore
never clears the stored workspaceSessionRootDirectory; update the handling of
snapshot.workspaceSessionRootDirectory so that when the snapshot value is nil or
trims to an empty string you explicitly clear workspaceSessionRootDirectory (set
it to nil or the empty-state the property uses), and when it contains non-empty
trimmed text assign that trimmed value to workspaceSessionRootDirectory (use
snapshot.workspaceSessionRootDirectory, normalizedSessionRootDirectory, and
workspaceSessionRootDirectory to locate and adjust the code).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b57c1e2a-2bfa-4ea1-a60f-62a8906c3bf4

📥 Commits

Reviewing files that changed from the base of the PR and between 11218fe and 9a0dc2b.

📒 Files selected for processing (8)
  • Sources/AppDelegate.swift
  • Sources/CmuxConfigExecutor.swift
  • Sources/SessionPersistence.swift
  • Sources/TabManager.swift
  • Sources/TerminalController.swift
  • Sources/Workspace.swift
  • cmuxTests/SessionPersistenceTests.swift
  • cmuxTests/TabManagerSessionSnapshotTests.swift

Comment thread cmuxTests/TabManagerSessionSnapshotTests.swift Outdated
Comment thread Sources/TabManager.swift
Comment thread Sources/Workspace.swift Outdated
@Fewmanism

Copy link
Copy Markdown
Author

Follow-up pushed in c20f276bb for the latest bot review items.

Changes:

  • Resolved the remaining greptile-apps review threads after verifying the current code addresses them.
  • Explicitly refresh the selected window title after async workspace-session restore applies, including the explicit-title path.
  • Allow Workspace.restoreSessionSnapshot(...) to clear workspaceSessionRootDirectory when the snapshot has nil/blank root state.
  • Strengthened the negative async-restore tests to wait through a full timeout instead of a short run-loop slice.
  • Added regression coverage for clearing a missing workspace session root directory.

Verification:

  • git diff --check
  • xcrun swiftc -parse Sources/Workspace.swift Sources/TabManager.swift Sources/SessionPersistence.swift Sources/AppDelegate.swift cmuxTests/TabManagerSessionSnapshotTests.swift
  • Independent reviewer: safe_to_commit: true

Note: xcodebuild cannot run in this environment because the active developer directory is CommandLineTools, not full Xcode.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh pr diff 4300 --repo manaflow-ai/cmux

Length of output: 49219


@Fewmanism: Commit c20f276bb looks clean — no new blockers. Here's the full assessment:


✅ All changes in c20f276bb look good

Item Status
Window title refresh after async restore (updateWindowTitle) ✅ Correctly guarded on selectedTabId == workspace.id
restoreSessionSnapshot clears workspaceSessionRootDirectory for nil/blank ✅ Explicit else { workspaceSessionRootDirectory = nil } branch
closingWorkspaceSnapshot captured before unregisterMainWindowContext ✅ Correct ordering
persistWorkspaceSessionSnapshots(from:synchronously:) helper ✅ Clean dispatch to sessionPersistenceQueue
sessionWindowSnapshot(for:...) extraction removes inline duplication ✅
Negative async-restore tests strengthened (poll full timeout) ✅ deadline-based loop + final predicate check
testWorkspaceRestoreClearsMissingSessionRootDirectory ✅ Tests restoreSessionSnapshot in isolation, no async dependency needed
nonisolated on struct/enum declarations ✅ Already resolved in 9a0dc2bd9, not re-introduced here

🟡 Carry-over minor items (not blockers, unchanged from prior rounds)

  1. Double canonicalization in saveWorkspaceSnapshot → workspaceSnapshotFileURL (redundant call, no correctness impact).
  2. Stale snapshot accumulation — per-directory session.json files in Application Support are never pruned.
  3. Silent skip for remote/virtual paths — canonicalWorkspaceDirectoryPath returns nil without logging.
  4. Test name testInitialWorkspaceRestoreCanBeDisabledForExplicitLayouts — tests the restoreWorkspaceSession: false flag, not a layout path directly. Minor naming inconsistency.
  5. Busy-polling in waitForWorkspaceSessionRestore — XCTestExpectation would give cleaner failure messages; low priority.

✅ Summary (all rounds)

Concern Status
nonisolated on nominal type declarations ✅ Resolved
File I/O off main actor ✅ Task.detached(priority: .utility)
Pristine-workspace guard ✅ title + cwd + panelIds
Explicit title preservation after restore ✅ setCustomTitle(explicitTitle)
Window title UI refresh after restore ✅ updateWindowTitle(for:)
Sync writes on sessionPersistenceQueue ✅ writeAllSnapshotsBlock
createdAt reuse / dedup ✅ existingEnvelope check
Config/layout opt-out ✅ restoreWorkspaceSession: false
Workspace snapshots gated on primary save ✅ != .failed guard
Git probe re-registration after restore ✅ clearWorkspaceGitProbes + refresh
Last-window-close data loss ✅ closingWorkspaceSnapshot before unregisterMainWindowContext
Stable root key vs live currentDirectory ✅ workspaceSessionRootDirectory field
workspaceSessionRootDirectory cleared on nil/blank restore ✅
Regression coverage ✅ 10 new test cases across 2 files

The implementation is ready for merge. Would you like me to open a GitHub issue to track the stale snapshot pruning work?

@Fewmanism

Copy link
Copy Markdown
Author

@coderabbitai resolve

Latest head c20f276bb has no active unresolved review threads. Your latest full review comment says the commit looks clean and has no new blockers. Please clear/dismiss the stale formal CHANGES_REQUESTED review state if supported.

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown

@Fewmanism: Understood — the latest full review confirmed c20f276bb is clean with no new blockers, and all previously raised actionable items are resolved. Resolving all CodeRabbit review threads now.

Regarding the formal CHANGES_REQUESTED review state: I can resolve individual review threads (done below), but dismissing a submitted pull request review that is in CHANGES_REQUESTED state requires a repository maintainer to dismiss it via the GitHub UI (Dismiss review on the PR's Reviews panel) or via the REST API — that action is scoped to maintainers and is outside what I can perform directly.

(ノ◕ヮ◕)ノ*:・゚✧

✅ Actions performed

Comments resolved and changes approved.

@teamleaderleo teamleaderleo added S2: major A crash, hang, lost state, broken connection, or a regression on a path people use area: workspaces Workspaces, sessions, restore after relaunch, worktrees labels Sep 30, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: workspaces Workspaces, sessions, restore after relaunch, worktrees S2: major A crash, hang, lost state, broken connection, or a regression on a path people use

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants