Skip to content

Fix session restore: preserve workspace titles and prevent duplicate windows - #2454

Closed
Apptah wants to merge 1 commit into
manaflow-ai:mainfrom
Apptah:fix/session-restore-titles-and-duplicate-windows
Closed

Apptah wants to merge 1 commit into
manaflow-ai:mainfrom
Apptah:fix/session-restore-titles-and-duplicate-windows

Conversation

@Apptah

@Apptah Apptah commented Apr 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • After session restore, new shell processes would overwrite restored workspace titles with generic names (e.g. "zsh"), making all workspaces indistinguishable. Added a restoredSessionProcessTitle guard in Workspace that suppresses the initial shell title update, preserving the meaningful restored title until a genuinely different process title arrives.
  • Set isRestorable = false on all cmux windows (both createMainWindow NSWindows and the primary SwiftUI WindowGroup window) to prevent macOS native state restoration from creating duplicate windows on relaunch — cmux manages its own session persistence via SessionPersistenceStore.

Test plan

  • Built tagged debug app with CMUX_SKIP_ZIG_BUILD=1
  • Copied production session snapshot (9 workspaces) to debug bundle ID
  • Launched debug app → verified session.restore.start fires and restores 9 workspaces
  • Verified all 9 workspace titles preserved after shell startup (not overwritten to "zsh")
  • Verified only 1 mainWindow.register event (no duplicate windows)
  • Test on production build: quit and relaunch to confirm no duplicate windows
  • Test that setting a custom title still works after restore
  • Test that running a new command updates the workspace title normally

🤖 Generated with Claude Code


Summary by cubic

Fixes session restore to keep workspace titles after relaunch and prevents macOS from opening duplicate windows. This removes confusing “zsh” titles and stops extra main windows from appearing.

  • Bug Fixes
    • Preserve restored titles: add restoredSessionProcessTitle in Workspace to ignore the first shell title update; cleared when a custom title is set or a different process title arrives.
    • Prevent duplicate windows: set isRestorable = false on the main NSWindow and SwiftUI WindowGroup windows; session state is handled by SessionPersistenceStore.

Written for commit 7548a9e. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed potential duplicate window creation after app relaunches by preventing conflicting macOS native state restoration behavior.
    • Improved session restoration to better preserve workspace titles when reopening previously saved sessions.

…windows

After session restore, new shell processes would overwrite restored workspace
titles with generic names (e.g. "zsh"), making all workspaces look identical.
Add a `restoredSessionProcessTitle` guard that suppresses the first shell
title update, preserving the meaningful restored title until a genuinely
different process title arrives.

Also set `isRestorable = false` on all cmux windows to prevent macOS native
state restoration from creating duplicate windows on relaunch — cmux manages
its own session persistence via SessionPersistenceStore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Apr 1, 2026

Copy link
Copy Markdown

@Apptah 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 Apr 1, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The PR prevents macOS native window state restoration from interfering with the application's own session persistence by disabling window restorability at initialization and adding state tracking for process titles during session restoration, preventing duplicate windows and title conflicts on relaunches.

Changes

Cohort / File(s) Summary
Window Restoration Disable
Sources/AppDelegate.swift, Sources/ContentView.swift
Set isRestorable = false on the main NSWindow at initialization to prevent macOS native window state restoration from conflicting with the app's custom session persistence mechanism.
Session State Tracking
Sources/Workspace.swift
Added restoredSessionProcessTitle state field to capture and track process titles during session restoration. Conditional title update logic gates incoming title updates while the restored title is present, preventing shell generic titles from overwriting restored context until the first genuine title change occurs.

Sequence Diagram

sequenceDiagram
    participant User
    participant AppInit as App Initialization
    participant MacOS as macOS System
    participant Workspace as Workspace State
    participant Shell as Shell/Process

    User->>AppInit: Launch app with saved session
    AppInit->>AppInit: Set window.isRestorable = false
    AppInit->>MacOS: Disable native window restoration
    AppInit->>Workspace: restoreSessionSnapshot()
    Workspace->>Workspace: Capture processTitle to restoredSessionProcessTitle
    Note over Workspace: Guard active: only restore title<br/>until first genuine update
    Workspace->>Shell: Restore shell context
    Shell->>Workspace: updatePanelTitle() with generic title
    Workspace->>Workspace: Match check: generic title vs restored?
    alt Title matches restored
        Workspace->>Workspace: Skip update, keep guard active
    else First genuine title differs
        Workspace->>Workspace: Clear guard (restoredSessionProcessTitle = nil)
        Workspace->>Workspace: Apply new title update
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A window's fate, now sealed with care,
No ghosts shall duplicate in air,
With process titles tucked away,
Sessions bloom without delay,
Restored yet fresh—hooray, hooray! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
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.
Description check ❓ Inconclusive The description covers the technical changes and test plan, but is missing required template sections: no Demo Video provided, and the checklist shows incomplete testing (several items unchecked). Complete the pending test items (production build relaunch, custom title setting, and command title updates) and check off corresponding checklist items, or update the checklist to reflect actual completion status.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the two main changes: preserving workspace titles during session restore and preventing duplicate windows.

✏️ 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 fix/session-restore-titles-and-duplicate-windows

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.

@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 3 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:6044">
P2: Restore-title guard only blocks exact matches; first differing startup titles still overwrite restored workspace titles.</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
if let restoredTitle = restoredSessionProcessTitle {
// The new shell's initial title matches or is less informative
// than the restored title — keep the restored one.
if trimmed != restoredTitle {

@cubic-dev-ai cubic-dev-ai Bot Apr 1, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Restore-title guard only blocks exact matches; first differing startup titles still overwrite restored workspace titles.

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

<comment>Restore-title guard only blocks exact matches; first differing startup titles still overwrite restored workspace titles.</comment>

<file context>
@@ -6018,14 +6032,33 @@ final class Workspace: Identifiable, ObservableObject {
+            if let restoredTitle = restoredSessionProcessTitle {
+                // The new shell's initial title matches or is less informative
+                // than the restored title — keep the restored one.
+                if trimmed != restoredTitle {
+                    restoredSessionProcessTitle = nil
+                    if self.title != trimmed {
</file context>
Fix with Cubic

@greptile-apps

greptile-apps Bot commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR addresses two distinct session-restore bugs in cmux: duplicate windows on relaunch (via macOS native state restoration conflicting with cmux's own SessionPersistenceStore) and workspace titles being overwritten with generic shell names after restore. The isRestorable = false changes in AppDelegate.swift and ContentView.swift are correct and clean. The Workspace.swift title-preservation guard, however, has a logic gap that limits its effectiveness to the common "directory-as-title" case.

Key changes:

  • AppDelegate.swift / ContentView.swift: Set isRestorable = false on all cmux-managed NSWindows to prevent macOS from creating duplicate windows on relaunch.
  • Workspace.swift: Introduces restoredSessionProcessTitle to suppress the initial shell title event after session restore — but the guard only suppresses updates where trimmed == restoredTitle. When the restored title was a command name (e.g. \"vim\") and the new shell's first report is a directory or \"zsh\", the guard immediately clears and the restored title is still overwritten. The fix works reliably only when the shell's initial title happens to equal the restored title (the directory-based title case).
  • setCustomTitle correctly clears the guard, but restoredSessionProcessTitle can be left set indefinitely on multi-panel workspaces where the panels.count == 1 condition in updatePanelTitle is never satisfied.

Confidence Score: 4/5

The duplicate-window fix is safe to merge; the title-preservation fix has a logic gap that leaves command-name titles still overwritable by the initial shell title.

The isRestorable = false changes are straightforwardly correct and resolve the duplicate-window regression. The restoredSessionProcessTitle guard introduces a P1 logic issue: it clears on any incoming title that differs from the restored one, meaning command-name workspace titles (e.g. 'vim') would still be overwritten by the shell's initial title ('zsh' or current directory). The fix only reliably covers directory-as-title workspaces. The multi-panel stale-guard concern is P2 but worth addressing.

Sources/Workspace.swift — the restoredSessionProcessTitle guard logic and its initialization for multi-panel workspaces.

Important Files Changed

Filename Overview
Sources/Workspace.swift Introduces restoredSessionProcessTitle guard to preserve workspace titles after session restore. Guard logic has a correctness issue: it only suppresses when the new title exactly matches the restored title, so command-name titles (e.g. "vim") are still overwritten by the shell's generic initial title. Also sets guard for multi-panel workspaces where the single-panel check in updatePanelTitle means it can never be cleared.
Sources/AppDelegate.swift Adds window.isRestorable = false to NSWindow created in createMainWindow to prevent macOS native state restoration from duplicating cmux-managed windows. Change is correct and well-placed.
Sources/ContentView.swift Sets window.isRestorable = false on the SwiftUI WindowGroup's underlying NSWindow via WindowAccessor, preventing macOS from duplicating the primary window on relaunch. Correct and consistent with the AppDelegate change.

Sequence Diagram

sequenceDiagram
    participant App
    participant Workspace
    participant Shell
    participant macOS

    App->>Workspace: applySessionSnapshot(snapshot)
    Workspace->>Workspace: applyProcessTitle(snapshot.processTitle)
    Workspace->>Workspace: setCustomTitle(snapshot.customTitle)
    Note over Workspace: restoredSessionProcessTitle = nil (inside setCustomTitle)
    Workspace->>Workspace: restoredSessionProcessTitle = snapshot.processTitle
    Note over Workspace: Guard active (if customTitle==nil and processTitle non-empty)

    Shell->>Workspace: updatePanelTitle(panelId, "zsh")
    alt trimmed == restoredSessionProcessTitle
        Note over Workspace: Suppress update — restored title preserved
    else trimmed != restoredSessionProcessTitle
        Workspace->>Workspace: restoredSessionProcessTitle = nil
        Workspace->>Workspace: self.title = trimmed (guard cleared, title overwritten)
    end

    Shell->>Workspace: updatePanelTitle(panelId, "git log")
    Note over Workspace: Guard nil — normal update, self.title = "git log"

    App->>macOS: window.isRestorable = false
    Note over macOS: macOS native restore disabled — no duplicate windows
Loading

Reviews (1): Last reviewed commit: "Fix session restore: preserve workspace ..." | Re-trigger Greptile

Comment thread Sources/Workspace.swift
Comment on lines +6041 to +6053
if let restoredTitle = restoredSessionProcessTitle {
// The new shell's initial title matches or is less informative
// than the restored title — keep the restored one.
if trimmed != restoredTitle {
restoredSessionProcessTitle = nil
if self.title != trimmed {
self.title = trimmed
didMutate = true
}
if processTitle != trimmed {
processTitle = trimmed
}
}

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 Guard only suppresses when titles match, not the initial shell title

The guard clears (and immediately allows the new title through) when trimmed != restoredTitle. This means the fix only preserves the restored title when the shell's very first reported title happens to exactly equal the restored title — the "directory as title" case. For workspaces where the restored title was a command name (e.g. "vim" or "git log"), the shell's first title report ("zsh" or the current directory) will be different from the restored title, so trimmed != restoredTitle evaluates to true, the guard is immediately cleared, and the restored title is still overwritten.

Per the PR description the intent is to suppress the initial generic shell title (e.g. "zsh") unconditionally, not only when it happens to match. A more robust approach would be to consume exactly one initial title event regardless of its value:

if let _ = restoredSessionProcessTitle {
    // Consume the initial shell title event unconditionally; the restored
    // title is already reflected in self.title / processTitle.
    restoredSessionProcessTitle = nil
} else {
    if self.title != trimmed {
        self.title = trimmed
        didMutate = true
    }
    if processTitle != trimmed {
        processTitle = trimmed
    }
}

This would cover all restored-title shapes (directory-based, command-based, etc.) while still allowing subsequent title updates through normally.

Comment thread Sources/Workspace.swift
Comment on lines +243 to +247
// Preserve the restored title so new shell process titles don't
// immediately overwrite it (see updatePanelTitle guard).
if snapshot.customTitle == nil, !snapshot.processTitle.isEmpty {
restoredSessionProcessTitle = snapshot.processTitle
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Guard set for multi-panel workspaces but never cleared via normal path

restoredSessionProcessTitle is assigned unconditionally here for any workspace whose snapshot has customTitle == nil and a non-empty processTitle, regardless of how many panels the workspace had. However, in updatePanelTitle the guard is only examined when panels.count == 1. For a workspace restored with multiple panels, the guard is set but will never be evaluated or cleared through the normal updatePanelTitle path. It will persist until the workspace is deallocated or the user reduces panels to 1, at which point it could unexpectedly suppress an unrelated title update.

Consider gating the assignment on single-panel workspaces:

if snapshot.customTitle == nil, !snapshot.processTitle.isEmpty, snapshot.panels.count == 1 {
    restoredSessionProcessTitle = snapshot.processTitle
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@Sources/Workspace.swift`:
- Around line 6035-6061: The guard cleared by comparing trimmed !=
restoredSessionProcessTitle is too weak and will drop the restored title when
the relaunched shell reports a generic initial title (e.g., "zsh"); update the
logic in the panels.count == 1 && customTitle == nil branch to only clear
restoredSessionProcessTitle when there is a stronger signal of new shell
activity: instead of releasing the guard on simple inequality, require either
(a) an explicit shell-activity flag or transition (e.g., a newCommandStarted or
lastCommandTimestamp change you add/observe), or (b) a heuristic check that the
incoming trimmed title is meaningfully more informative than the snapshot (for
example not matching known shell names/prompts and containing whitespace or
command characters, or being longer than restoredSessionProcessTitle and not a
short shell identifier). Apply this change where restoredSessionProcessTitle,
trimmed, title and processTitle are handled so the guard only clears when the
new title indicates real new command activity.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8953adc-bfda-4444-8cba-e7a5c0517412

📥 Commits

Reviewing files that changed from the base of the PR and between 92d4fa6 and 7548a9e.

📒 Files selected for processing (3)
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/Workspace.swift

Comment thread Sources/Workspace.swift
Comment on lines +6035 to +6061
// If this is the only panel and no custom title, update workspace title.
// After session restore, suppress the first title update (the new shell
// reporting a generic title like "zsh") to preserve the restored title.
// Once a genuinely different title arrives, clear the guard and resume
// normal auto-updating.
if panels.count == 1, customTitle == nil {
if self.title != trimmed {
self.title = trimmed
didMutate = true
}
if processTitle != trimmed {
processTitle = trimmed
if let restoredTitle = restoredSessionProcessTitle {
// The new shell's initial title matches or is less informative
// than the restored title — keep the restored one.
if trimmed != restoredTitle {
restoredSessionProcessTitle = nil
if self.title != trimmed {
self.title = trimmed
didMutate = true
}
if processTitle != trimmed {
processTitle = trimmed
}
}
} else {
if self.title != trimmed {
self.title = trimmed
didMutate = true
}
if processTitle != trimmed {
processTitle = trimmed
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

trimmed != restoredTitle releases the guard too early.

Line 6044 treats any title that differs from the snapshot as a real post-restore update. If the snapshot was taken while the workspace title was a running command like npm test, the first prompt title from the relaunched shell (zsh) will clear the guard and overwrite the restored title immediately. That misses the "preserve restored title until a real new command appears" behavior this change is aiming for. Please key the release off a stronger signal than simple inequality, such as shell-activity state or another explicit "new command started" transition.

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

In `@Sources/Workspace.swift` around lines 6035 - 6061, The guard cleared by
comparing trimmed != restoredSessionProcessTitle is too weak and will drop the
restored title when the relaunched shell reports a generic initial title (e.g.,
"zsh"); update the logic in the panels.count == 1 && customTitle == nil branch
to only clear restoredSessionProcessTitle when there is a stronger signal of new
shell activity: instead of releasing the guard on simple inequality, require
either (a) an explicit shell-activity flag or transition (e.g., a
newCommandStarted or lastCommandTimestamp change you add/observe), or (b) a
heuristic check that the incoming trimmed title is meaningfully more informative
than the snapshot (for example not matching known shell names/prompts and
containing whitespace or command characters, or being longer than
restoredSessionProcessTitle and not a short shell identifier). Apply this change
where restoredSessionProcessTitle, trimmed, title and processTitle are handled
so the guard only clears when the new title indicates real new command activity.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Thank you for this! Session restore title handling and duplicate-window prevention are covered on main in ecba57a. :)

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.

2 participants