Clean up panel branch state when closing a pane

This commit is contained in:
Lawrence Chen 2026-02-20 22:15:17 -08:00
parent c533ebe5e5
commit e9da15d563
2 changed files with 52 additions and 1 deletions

View file

@ -330,6 +330,9 @@ final class Workspace: Identifiable, ObservableObject {
/// Deterministic tab selection to apply after a tab closes.
/// Keyed by the closing tab ID, value is the tab ID we want to select next.
private var postCloseSelectTabId: [TabID: TabID] = [:]
/// Panel IDs that were in a pane when a pane-close operation was approved.
/// Bonsplit pane-close does not emit per-tab didClose callbacks.
private var pendingPaneClosePanelIds: [UUID: [UUID]] = [:]
private var isApplyingTabSelection = false
private var pendingTabSelection: (tabId: TabID, pane: PaneID)?
private var isReconcilingFocusState = false
@ -1880,7 +1883,36 @@ extension Workspace: BonsplitDelegate {
}
func splitTabBar(_ controller: BonsplitController, didClosePane paneId: PaneID) {
_ = paneId
let closedPanelIds = pendingPaneClosePanelIds.removeValue(forKey: paneId.id) ?? []
if !closedPanelIds.isEmpty {
for panelId in closedPanelIds {
panels[panelId]?.close()
panels.removeValue(forKey: panelId)
panelDirectories.removeValue(forKey: panelId)
panelGitBranches.removeValue(forKey: panelId)
panelTitles.removeValue(forKey: panelId)
panelCustomTitles.removeValue(forKey: panelId)
pinnedPanelIds.remove(panelId)
manualUnreadPanelIds.remove(panelId)
panelSubscriptions.removeValue(forKey: panelId)
surfaceTTYNames.removeValue(forKey: panelId)
surfaceListeningPorts.removeValue(forKey: panelId)
PortScanner.shared.unregisterPanel(workspaceId: id, panelId: panelId)
}
let closedSet = Set(closedPanelIds)
surfaceIdToPanelId = surfaceIdToPanelId.filter { !closedSet.contains($0.value) }
recomputeListeningPorts()
if let focusedPane = bonsplitController.focusedPaneId,
let focusedTabId = bonsplitController.selectedTab(inPane: focusedPane)?.id {
applyTabSelection(tabId: focusedTabId, inPane: focusedPane)
} else {
scheduleFocusReconcile()
}
}
scheduleTerminalGeometryReconcile()
scheduleFocusReconcile()
}
@ -1893,9 +1925,11 @@ extension Workspace: BonsplitDelegate {
if let panelId = panelIdFromSurfaceId(tab.id),
let terminalPanel = terminalPanel(for: panelId),
terminalPanel.needsConfirmClose() {
pendingPaneClosePanelIds.removeValue(forKey: pane.id)
return false
}
}
pendingPaneClosePanelIds[pane.id] = tabs.compactMap { panelIdFromSurfaceId($0.id) }
return true
}

View file

@ -1002,6 +1002,23 @@ final class WorkspacePanelGitBranchTests: XCTestCase {
XCTAssertEqual(branches.map(\.branch), ["main", "feature/left", "feature/right"])
XCTAssertEqual(branches.map(\.isDirty), [true, false, false])
}
func testClosingPaneDropsBranchesFromClosedSide() {
let workspace = Workspace()
guard let leftPanelId = workspace.focusedPanelId,
let leftPaneId = workspace.paneId(forPanelId: leftPanelId),
let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal) else {
XCTFail("Expected left/right split panes")
return
}
workspace.updatePanelGitBranch(panelId: leftPanelId, branch: "branch1", isDirty: false)
workspace.updatePanelGitBranch(panelId: rightPanel.id, branch: "branch2", isDirty: false)
XCTAssertEqual(workspace.sidebarGitBranchesInDisplayOrder().map(\.branch), ["branch1", "branch2"])
XCTAssertTrue(workspace.bonsplitController.closePane(leftPaneId))
XCTAssertEqual(workspace.sidebarGitBranchesInDisplayOrder().map(\.branch), ["branch2"])
}
}
final class SidebarBranchOrderingTests: XCTestCase {