Repository navigation
Fix session restore: preserve workspace titles and prevent duplicate windows #2454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -240,6 +240,12 @@ extension Workspace { | |
| setCustomColor(snapshot.customColor) | ||
| isPinned = snapshot.isPinned | ||
|
|
||
| // 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 | ||
| } | ||
|
|
||
| // Status entries and agent PIDs are ephemeral runtime state tied to running | ||
| // processes (e.g. claude_code "Running"). Don't restore them across app | ||
| // restarts because the processes that set them are gone. | ||
|
|
@@ -5104,6 +5110,13 @@ final class Workspace: Identifiable, ObservableObject { | |
|
|
||
| private var processTitle: String | ||
|
|
||
| /// When non-nil, the workspace title was restored from a session snapshot. | ||
| /// The first shell process title update is suppressed to prevent the new | ||
| /// shell's generic title (e.g. "zsh") from overwriting the meaningful | ||
| /// restored title. Cleared once a *different* process title arrives, | ||
| /// indicating the user has started a new command. | ||
| private var restoredSessionProcessTitle: String? | ||
|
|
||
| private enum SurfaceKind { | ||
| static let terminal = "terminal" | ||
| static let browser = "browser" | ||
|
|
@@ -5808,6 +5821,7 @@ final class Workspace: Identifiable, ObservableObject { | |
|
|
||
| func setCustomTitle(_ title: String?) { | ||
| let trimmed = title?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" | ||
| restoredSessionProcessTitle = nil | ||
| if trimmed.isEmpty { | ||
| customTitle = nil | ||
| self.title = processTitle | ||
|
|
@@ -6018,14 +6032,33 @@ final class Workspace: Identifiable, ObservableObject { | |
| ) | ||
| } | ||
|
|
||
| // If this is the only panel and no custom title, update workspace title | ||
| // 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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| restoredSessionProcessTitle = nil | ||
| if self.title != trimmed { | ||
| self.title = trimmed | ||
| didMutate = true | ||
| } | ||
| if processTitle != trimmed { | ||
| processTitle = trimmed | ||
| } | ||
| } | ||
|
Comment on lines
+6041
to
+6053
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The guard clears (and immediately allows the new title through) when Per the PR description the intent is to suppress the initial generic shell title (e.g. 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. |
||
| } else { | ||
| if self.title != trimmed { | ||
| self.title = trimmed | ||
| didMutate = true | ||
| } | ||
| if processTitle != trimmed { | ||
| processTitle = trimmed | ||
| } | ||
|
Comment on lines
+6035
to
+6061
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
restoredSessionProcessTitleis assigned unconditionally here for any workspace whose snapshot hascustomTitle == niland a non-emptyprocessTitle, regardless of how many panels the workspace had. However, inupdatePanelTitlethe guard is only examined whenpanels.count == 1. For a workspace restored with multiple panels, the guard is set but will never be evaluated or cleared through the normalupdatePanelTitlepath. 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: