Skip to content

Commit 17e8bb1

Browse files
committed
Fix Cmd+N crash from stale workspace creation snapshots
1 parent ad2c65e commit 17e8bb1

1 file changed

Lines changed: 70 additions & 47 deletions

File tree

Sources/TabManager.swift

Lines changed: 70 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -800,15 +800,24 @@ class TabManager: ObservableObject {
800800
private var pendingWorkspaceUnfocusTarget: (tabId: UUID, panelId: UUID)?
801801
private var sidebarSelectedWorkspaceIds: Set<UUID> = []
802802
var confirmCloseHandler: ((String, String, Bool) -> Bool)?
803-
private struct WorkspaceCreationSnapshot {
804-
let tabs: [Workspace]
805-
let selectedTabId: UUID?
803+
private struct WorkspaceCreationTabSnapshot {
804+
let id: UUID
805+
let isPinned: Bool
806806

807-
var selectedWorkspace: Workspace? {
808-
guard let selectedTabId else { return nil }
809-
return tabs.first(where: { $0.id == selectedTabId })
807+
@MainActor
808+
init(workspace: Workspace) {
809+
self.id = workspace.id
810+
self.isPinned = workspace.isPinned
810811
}
811812
}
813+
814+
private struct WorkspaceCreationSnapshot {
815+
let tabs: [WorkspaceCreationTabSnapshot]
816+
let selectedTabId: UUID?
817+
let selectedTabWasPinned: Bool
818+
let preferredWorkingDirectory: String?
819+
let inheritedTerminalConfig: ghostty_surface_config_s?
820+
}
812821
private var agentPIDSweepTimer: DispatchSourceTimer?
813822
private var workspaceGitMetadataPollTimer: DispatchSourceTimer?
814823
#if DEBUG
@@ -1176,14 +1185,15 @@ class TabManager: ObservableObject {
11761185
}()
11771186
guard isEnabled,
11781187
let selectedTabId = snapshot.selectedTabId,
1179-
let target = snapshot.tabs.first(where: { $0.id != selectedTabId }) else {
1188+
let targetId = snapshot.tabs.lazy.map(\.id).first(where: { $0 != selectedTabId }),
1189+
tabs.contains(where: { $0.id == targetId }) else {
11801190
return
11811191
}
11821192
dlog(
11831193
"workspace.create.devSelectionMutation from=\(selectedTabId.uuidString.prefix(5)) " +
1184-
"to=\(target.id.uuidString.prefix(5))"
1194+
"to=\(targetId.uuidString.prefix(5))"
11851195
)
1186-
self.selectedTabId = target.id
1196+
self.selectedTabId = targetId
11871197
}
11881198
#endif
11891199

@@ -1207,8 +1217,8 @@ class TabManager: ObservableObject {
12071217
let nextTabCount = snapshot.tabs.count + 1
12081218
sentryBreadcrumb("workspace.create", data: ["tabCount": nextTabCount])
12091219
let explicitWorkingDirectory = normalizedWorkingDirectory(overrideWorkingDirectory)
1210-
let workingDirectory = explicitWorkingDirectory ?? preferredWorkingDirectoryForNewTab(snapshot: snapshot)
1211-
let inheritedConfig = inheritedTerminalConfigForNewWorkspace(snapshot: snapshot)
1220+
let workingDirectory = explicitWorkingDirectory ?? snapshot.preferredWorkingDirectory
1221+
let inheritedConfig = snapshot.inheritedTerminalConfig
12121222
// Resolve placement against the pre-creation snapshot before Workspace init
12131223
// boots terminal state. The ssh/new-workspace path can otherwise crash while
12141224
// reading @Published placement state from existing workspaces mid-creation.
@@ -1228,7 +1238,9 @@ class TabManager: ObservableObject {
12281238
if eagerLoadTerminal && !select {
12291239
requestBackgroundWorkspaceLoad(for: newWorkspace.id)
12301240
}
1231-
var updatedTabs = snapshot.tabs
1241+
// Apply insertion to the current live array so post-snapshot closes/reorders
1242+
// are preserved instead of reintroducing stale workspace instances.
1243+
var updatedTabs = tabs
12321244
if insertIndex >= 0 && insertIndex <= updatedTabs.count {
12331245
updatedTabs.insert(newWorkspace, at: insertIndex)
12341246
} else {
@@ -2160,20 +2172,29 @@ class TabManager: ObservableObject {
21602172
}
21612173

21622174
func terminalPanelForWorkspaceConfigInheritanceSource() -> TerminalPanel? {
2163-
terminalPanelForWorkspaceConfigInheritanceSource(snapshot: workspaceCreationSnapshot())
2175+
terminalPanelForWorkspaceConfigInheritanceSource(workspace: selectedWorkspace)
21642176
}
21652177

21662178
private func workspaceCreationSnapshot() -> WorkspaceCreationSnapshot {
2167-
WorkspaceCreationSnapshot(
2168-
tabs: tabs,
2169-
selectedTabId: selectedTabId
2179+
let currentTabs = tabs
2180+
let currentSelectedTabId = selectedTabId
2181+
let selectedWorkspace = currentSelectedTabId.flatMap { selectedTabId in
2182+
currentTabs.first(where: { $0.id == selectedTabId })
2183+
}
2184+
2185+
return WorkspaceCreationSnapshot(
2186+
tabs: currentTabs.map { WorkspaceCreationTabSnapshot(workspace: $0) },
2187+
selectedTabId: currentSelectedTabId,
2188+
selectedTabWasPinned: selectedWorkspace?.isPinned ?? false,
2189+
preferredWorkingDirectory: preferredWorkingDirectoryForNewTab(workspace: selectedWorkspace),
2190+
inheritedTerminalConfig: inheritedTerminalConfigForNewWorkspace(workspace: selectedWorkspace)
21702191
)
21712192
}
21722193

21732194
private func terminalPanelForWorkspaceConfigInheritanceSource(
2174-
snapshot: WorkspaceCreationSnapshot
2195+
workspace: Workspace?
21752196
) -> TerminalPanel? {
2176-
guard let workspace = snapshot.selectedWorkspace else { return nil }
2197+
guard let workspace else { return nil }
21772198
if let focusedTerminal = workspace.focusedTerminalPanel {
21782199
return focusedTerminal
21792200
}
@@ -2188,21 +2209,21 @@ class TabManager: ObservableObject {
21882209
}
21892210

21902211
private func inheritedTerminalConfigForNewWorkspace() -> ghostty_surface_config_s? {
2191-
inheritedTerminalConfigForNewWorkspace(snapshot: workspaceCreationSnapshot())
2212+
inheritedTerminalConfigForNewWorkspace(workspace: selectedWorkspace)
21922213
}
21932214

21942215
private func inheritedTerminalConfigForNewWorkspace(
2195-
snapshot: WorkspaceCreationSnapshot
2216+
workspace: Workspace?
21962217
) -> ghostty_surface_config_s? {
2197-
if let panel = terminalPanelForWorkspaceConfigInheritanceSource(snapshot: snapshot),
2218+
if let panel = terminalPanelForWorkspaceConfigInheritanceSource(workspace: workspace),
21982219
panel.surface.hasLiveSurface,
21992220
let sourceSurface = panel.surface.surface {
22002221
return cmuxInheritedSurfaceConfig(
22012222
sourceSurface: sourceSurface,
22022223
context: GHOSTTY_SURFACE_CONTEXT_TAB
22032224
)
22042225
}
2205-
if let fallbackFontPoints = snapshot.selectedWorkspace?.lastRememberedTerminalFontPointsForConfigInheritance() {
2226+
if let fallbackFontPoints = workspace?.lastRememberedTerminalFontPointsForConfigInheritance() {
22062227
var config = ghostty_surface_config_new()
22072228
config.font_size = fallbackFontPoints
22082229
return config
@@ -2226,44 +2247,46 @@ class TabManager: ObservableObject {
22262247
placementOverride: NewWorkspacePlacement? = nil
22272248
) -> Int {
22282249
let placement = placementOverride ?? WorkspacePlacementSettings.current()
2229-
let tabs = snapshot.tabs
2230-
var pinnedCount = 0
2231-
var selectedIndex: Int?
2232-
var selectedIsPinned = false
2233-
let selectedTabId = snapshot.selectedTabId
2234-
2235-
for (index, tab) in tabs.enumerated() {
2250+
let liveTabs = tabs.map { WorkspaceCreationTabSnapshot(workspace: $0) }
2251+
let pinnedCount = liveTabs.reduce(into: 0) { partial, tab in
22362252
if tab.isPinned {
2237-
pinnedCount += 1
2238-
}
2239-
if selectedIndex == nil, tab.id == selectedTabId {
2240-
selectedIndex = index
2241-
selectedIsPinned = tab.isPinned
2253+
partial += 1
22422254
}
22432255
}
22442256

2245-
return WorkspacePlacementSettings.insertionIndex(
2246-
placement: placement,
2247-
selectedIndex: selectedIndex,
2248-
selectedIsPinned: selectedIsPinned,
2249-
pinnedCount: pinnedCount,
2250-
totalCount: tabs.count
2251-
)
2257+
switch placement {
2258+
case .top:
2259+
return pinnedCount
2260+
case .end:
2261+
return liveTabs.count
2262+
case .afterCurrent:
2263+
if let selectedTabId = snapshot.selectedTabId,
2264+
let selectedIndex = liveTabs.firstIndex(where: { $0.id == selectedTabId }) {
2265+
return WorkspacePlacementSettings.insertionIndex(
2266+
placement: placement,
2267+
selectedIndex: selectedIndex,
2268+
selectedIsPinned: snapshot.selectedTabWasPinned,
2269+
pinnedCount: pinnedCount,
2270+
totalCount: liveTabs.count
2271+
)
2272+
}
2273+
return snapshot.selectedTabWasPinned ? pinnedCount : liveTabs.count
2274+
}
22522275
}
22532276

22542277
private func preferredWorkingDirectoryForNewTab() -> String? {
2255-
preferredWorkingDirectoryForNewTab(snapshot: workspaceCreationSnapshot())
2278+
preferredWorkingDirectoryForNewTab(workspace: selectedWorkspace)
22562279
}
22572280

22582281
private func preferredWorkingDirectoryForNewTab(
2259-
snapshot: WorkspaceCreationSnapshot
2282+
workspace: Workspace?
22602283
) -> String? {
2261-
guard let tab = snapshot.selectedWorkspace else {
2284+
guard let workspace else {
22622285
return nil
22632286
}
2264-
let focusedDirectory = tab.focusedPanelId
2265-
.flatMap { tab.panelDirectories[$0] }
2266-
let candidate = focusedDirectory ?? tab.currentDirectory
2287+
let focusedDirectory = workspace.focusedPanelId
2288+
.flatMap { workspace.panelDirectories[$0] }
2289+
let candidate = focusedDirectory ?? workspace.currentDirectory
22672290
let normalized = normalizeDirectory(candidate)
22682291
let trimmed = normalized.trimmingCharacters(in: .whitespacesAndNewlines)
22692292
return trimmed.isEmpty ? nil : normalized

0 commit comments

Comments
 (0)