-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Fix Cmd+N crash from workspace creation config snapshots #2178
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 |
|---|---|---|
|
|
@@ -816,7 +816,7 @@ class TabManager: ObservableObject { | |
| let selectedTabId: UUID? | ||
| let selectedTabWasPinned: Bool | ||
| let preferredWorkingDirectory: String? | ||
| let inheritedTerminalConfig: ghostty_surface_config_s? | ||
| let inheritedTerminalFontPoints: Float? | ||
| } | ||
| private var agentPIDSweepTimer: DispatchSourceTimer? | ||
| private var workspaceGitMetadataPollTimer: DispatchSourceTimer? | ||
|
|
@@ -1218,7 +1218,9 @@ class TabManager: ObservableObject { | |
| sentryBreadcrumb("workspace.create", data: ["tabCount": nextTabCount]) | ||
| let explicitWorkingDirectory = normalizedWorkingDirectory(overrideWorkingDirectory) | ||
| let workingDirectory = explicitWorkingDirectory ?? snapshot.preferredWorkingDirectory | ||
| let inheritedConfig = snapshot.inheritedTerminalConfig | ||
| let inheritedConfig = workspaceCreationConfigTemplate( | ||
| inheritedTerminalFontPoints: snapshot.inheritedTerminalFontPoints | ||
| ) | ||
| // Resolve placement against the pre-creation snapshot before Workspace init | ||
| // boots terminal state. The ssh/new-workspace path can otherwise crash while | ||
| // reading @Published placement state from existing workspaces mid-creation. | ||
|
|
@@ -2191,7 +2193,7 @@ class TabManager: ObservableObject { | |
| selectedTabId: currentSelectedTabId, | ||
| selectedTabWasPinned: selectedTabSnapshot?.isPinned ?? false, | ||
| preferredWorkingDirectory: preferredWorkingDirectoryForNewTab(workspace: selectedWorkspace), | ||
| inheritedTerminalConfig: inheritedTerminalConfigForNewWorkspace(workspace: selectedWorkspace) | ||
| inheritedTerminalFontPoints: inheritedTerminalFontPointsForNewWorkspace(workspace: selectedWorkspace) | ||
| ) | ||
| } | ||
|
|
||
|
|
@@ -2251,7 +2253,7 @@ class TabManager: ObservableObject { | |
| inheritedTerminalConfigForNewWorkspace(workspace: selectedWorkspace) | ||
| } | ||
|
|
||
| private func inheritedTerminalConfigForNewWorkspace( | ||
| func inheritedTerminalConfigForNewWorkspace( | ||
| workspace: Workspace? | ||
| ) -> ghostty_surface_config_s? { | ||
| if let panel = terminalPanelForWorkspaceConfigInheritanceSource(workspace: workspace), | ||
|
|
@@ -2271,6 +2273,30 @@ class TabManager: ObservableObject { | |
| return nil | ||
| } | ||
|
|
||
| private func inheritedTerminalFontPointsForNewWorkspace( | ||
| workspace: Workspace? | ||
| ) -> Float? { | ||
| guard let inheritedConfig = inheritedTerminalConfigForNewWorkspace(workspace: workspace), | ||
| inheritedConfig.font_size > 0 else { | ||
| return nil | ||
| } | ||
| return inheritedConfig.font_size | ||
| } | ||
|
|
||
| private func workspaceCreationConfigTemplate( | ||
| inheritedTerminalFontPoints: Float? | ||
| ) -> ghostty_surface_config_s? { | ||
| guard let inheritedTerminalFontPoints, inheritedTerminalFontPoints > 0 else { | ||
| return nil | ||
| } | ||
| // ghostty_surface_config_s can carry raw C pointers owned by the source surface. | ||
| // New workspace creation only needs the inherited zoom level, so rebuild a clean | ||
| // config instead of snapshotting pointer-backed fields across workspace creation. | ||
| var config = ghostty_surface_config_new() | ||
| config.font_size = inheritedTerminalFontPoints | ||
| return config | ||
| } | ||
|
Comment on lines
+2286
to
+2298
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. 🧩 Analysis chain🏁 Script executed: # Check if TabManager.swift exists and read the specified lines
if [ -f "Sources/TabManager.swift" ]; then
wc -l Sources/TabManager.swift
echo "---"
sed -n '2276,2298p' Sources/TabManager.swift
else
# Try to locate TabManager.swift
find . -name "TabManager.swift" -type f
fiRepository: manaflow-ai/cmux Length of output: 1086 🏁 Script executed: # Search for ghostty_surface_config definition and usage
rg "ghostty_surface_config" -A 3 -B 1 --max-count 20Repository: manaflow-ai/cmux Length of output: 9223 🏁 Script executed: # Look for the Ghostty header file in the repo
find . -name "ghostty.h" -o -name "*ghostty*.h" | head -20Repository: manaflow-ai/cmux Length of output: 71 🏁 Script executed: # Read the ghostty.h header to understand the struct and constructor
cat -n ghostty.h | grep -A 40 "ghostty_surface_config_s" | head -60Repository: manaflow-ai/cmux Length of output: 2266 🏁 Script executed: # Look for the implementation or documentation of ghostty_surface_config_new()
# Check if there's a build directory or any implementation hints
rg "ghostty_surface_config_new" -B 3 -A 10Repository: manaflow-ai/cmux Length of output: 6034 🏁 Script executed: # Check how the config is actually used across the codebase to understand the risk
rg "ghostty_surface_config_new\(\)" -A 5 | head -80Repository: manaflow-ai/cmux Length of output: 2722 🏁 Script executed: # Search for any comments or documentation about field initialization
rg "zero.init|zero-init|zero.initialize" -i -C 2Repository: manaflow-ai/cmux Length of output: 1237 🏁 Script executed: # Get the exact struct definition to see all pointer fields
sed -n '400,453p' ghostty.h | cat -nRepository: manaflow-ai/cmux Length of output: 1575 Explicitly clear uninitialized pointer fields before returning. The Ghostty header doesn't document that Suggested hardening var config = ghostty_surface_config_new()
+ config.working_directory = nil
+ config.command = nil
+ config.env_vars = nil
+ config.env_var_count = 0
+ config.initial_input = nil
config.font_size = inheritedTerminalFontPoints
return config🤖 Prompt for AI Agents |
||
|
|
||
| private func normalizedWorkingDirectory(_ directory: String?) -> String? { | ||
| guard let directory else { return nil } | ||
| let normalized = normalizeDirectory(directory) | ||
|
|
||
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.
privatewidened tointernalfor test subclassinginheritedTerminalConfigForNewWorkspace(workspace:)is nowinternal(no explicit access modifier) soUnsafeConfigSnapshotTabManagerin the test target can override it. This is a pragmatic and functional approach given the test harness, but it permanently exposes this method to all callers within the module, not just the test.An alternative that keeps the method
privateand avoids a permanent API surface increase is injecting the config source as a closure or a small protocol:The test then just swaps out
manager.inheritedConfigProviderinstead of subclassing. Not a blocker — the current approach is correct and testable — but worth considering if the codebase discourages wideningprivatetointernalsolely for tests.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!