From 0010e10bf5051285e19e9f1cf9f976da22428415 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu, 19 Mar 2026 01:05:12 -0700 Subject: [PATCH] Stabilize sidebar directory ordering when split focus changes (#1798) * Add sidebar directory ordering regression test * Stabilize sidebar directory ordering --------- Co-authored-by: Lawrence Chen --- Sources/ContentView.swift | 11 +---- Sources/Workspace.swift | 67 ++++++++++++++++++++++++++++-- cmuxTests/WorkspaceUnitTests.swift | 53 +++++++++++++++++++++++ 3 files changed, 119 insertions(+), 12 deletions(-) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 1d862346..c5f7193d 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -11731,17 +11731,10 @@ private struct TabItemView: View, Equatable { } private func directorySummaryText(orderedPanelIds: [UUID]) -> String? { - guard !tab.panels.isEmpty else { return nil } let home = SidebarPathFormatter.homeDirectoryPath - var seen: Set = [] - var entries: [String] = [] - for panelId in orderedPanelIds { - let directory = tab.panelDirectories[panelId] ?? tab.currentDirectory + let entries = tab.sidebarDirectoriesInDisplayOrder(orderedPanelIds: orderedPanelIds).compactMap { directory in let shortened = SidebarPathFormatter.shortenedPath(directory, homeDirectoryPath: home) - guard !shortened.isEmpty else { continue } - if seen.insert(shortened).inserted { - entries.append(shortened) - } + return shortened.isEmpty ? nil : shortened } return entries.isEmpty ? nil : entries.joined(separator: " | ") } diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 4557f122..a6fadd0e 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -4739,7 +4739,7 @@ enum SidebarBranchOrdering { for panelId in orderedPanelIds { let panelBranch = normalized(panelBranches[panelId]?.branch) let branch = panelBranch ?? defaultBranchForPanels - let directory = normalized(panelDirectories[panelId] ?? defaultDirectory) + let directory = normalized(panelDirectories[panelId]) guard branch != nil || directory != nil else { continue } let panelDirty = panelBranch != nil @@ -5933,6 +5933,67 @@ final class Workspace: Identifiable, ObservableObject { ) } + private func normalizedSidebarDirectory(_ directory: String?) -> String? { + guard let directory else { return nil } + let trimmed = directory.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + } + + private func canonicalSidebarDirectoryKey(_ directory: String?) -> String? { + guard let directory = normalizedSidebarDirectory(directory) else { return nil } + let expanded = NSString(string: directory).expandingTildeInPath + let standardized = NSString(string: expanded).standardizingPath + let cleaned = standardized.trimmingCharacters(in: .whitespacesAndNewlines) + return cleaned.isEmpty ? nil : cleaned + } + + private func sidebarResolvedDirectory(for panelId: UUID) -> String? { + if let directory = normalizedSidebarDirectory(panelDirectories[panelId]) { + return directory + } + if let requestedDirectory = normalizedSidebarDirectory( + terminalPanel(for: panelId)?.requestedWorkingDirectory + ) { + return requestedDirectory + } + guard panelId == focusedPanelId else { return nil } + return normalizedSidebarDirectory(currentDirectory) + } + + private func sidebarResolvedPanelDirectories(orderedPanelIds: [UUID]) -> [UUID: String] { + var resolved: [UUID: String] = [:] + for panelId in orderedPanelIds { + if let directory = sidebarResolvedDirectory(for: panelId) { + resolved[panelId] = directory + } + } + return resolved + } + + func sidebarDirectoriesInDisplayOrder(orderedPanelIds: [UUID]) -> [String] { + let resolvedDirectories = sidebarResolvedPanelDirectories(orderedPanelIds: orderedPanelIds) + var ordered: [String] = [] + var seen: Set = [] + + for panelId in orderedPanelIds { + guard let directory = resolvedDirectories[panelId], + let key = canonicalSidebarDirectoryKey(directory) else { continue } + if seen.insert(key).inserted { + ordered.append(directory) + } + } + + if ordered.isEmpty, let fallbackDirectory = normalizedSidebarDirectory(currentDirectory) { + return [fallbackDirectory] + } + + return ordered + } + + func sidebarDirectoriesInDisplayOrder() -> [String] { + sidebarDirectoriesInDisplayOrder(orderedPanelIds: sidebarOrderedPanelIds()) + } + func sidebarGitBranchesInDisplayOrder(orderedPanelIds: [UUID]) -> [SidebarGitBranchState] { SidebarBranchOrdering .orderedUniqueBranches( @@ -5953,8 +6014,8 @@ final class Workspace: Identifiable, ObservableObject { SidebarBranchOrdering.orderedUniqueBranchDirectoryEntries( orderedPanelIds: orderedPanelIds, panelBranches: panelGitBranches, - panelDirectories: panelDirectories, - defaultDirectory: currentDirectory, + panelDirectories: sidebarResolvedPanelDirectories(orderedPanelIds: orderedPanelIds), + defaultDirectory: normalizedSidebarDirectory(currentDirectory), fallbackBranch: gitBranch ) } diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index 719baa43..8031a0c4 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -1629,6 +1629,59 @@ final class WorkspacePanelGitBranchTests: XCTestCase { XCTAssertEqual(branches.map(\.isDirty), [true, false, false]) } + func testSidebarBranchDirectoryEntriesStayStableAcrossFocusedSplitChanges() { + let workspace = Workspace() + let leftLiveDirectory = "/repo/left/live" + let rightFocusedDirectory = "/repo/right/focused" + let leftFocusedDirectory = "/repo/left/focused" + let rightRequestedDirectory = "/repo/right/requested" + + guard let leftPanelId = workspace.focusedPanelId else { + XCTFail("Expected initial focused panel") + return + } + + workspace.updatePanelDirectory(panelId: leftPanelId, directory: leftLiveDirectory) + + guard let rightSplitPanel = workspace.newTerminalSplit( + from: leftPanelId, + orientation: .horizontal, + focus: false + ), + let rightPaneId = workspace.paneId(forPanelId: rightSplitPanel.id), + let rightRequestedPanel = workspace.newTerminalSurface( + inPane: rightPaneId, + focus: false, + workingDirectory: rightRequestedDirectory + ) else { + XCTFail("Expected right split panes for sidebar directory ordering test") + return + } + + let orderedPanelIds = workspace.sidebarOrderedPanelIds() + XCTAssertEqual(orderedPanelIds, [leftPanelId, rightSplitPanel.id, rightRequestedPanel.id]) + + workspace.currentDirectory = rightFocusedDirectory + let entriesWhenRightLooksFocused = workspace.sidebarBranchDirectoryEntriesInDisplayOrder( + orderedPanelIds: orderedPanelIds + ) + + workspace.currentDirectory = leftFocusedDirectory + let entriesWhenLeftLooksFocused = workspace.sidebarBranchDirectoryEntriesInDisplayOrder( + orderedPanelIds: orderedPanelIds + ) + + XCTAssertEqual( + entriesWhenRightLooksFocused, + entriesWhenLeftLooksFocused, + "Expected sidebar directory ordering to ignore focused-workspace cwd churn when panel-specific directories are available" + ) + XCTAssertEqual( + entriesWhenRightLooksFocused.map(\.directory), + [leftLiveDirectory, rightRequestedDirectory] + ) + } + func testSidebarDerivedCollectionsMatchWhenUsingPrecomputedPanelOrder() { let workspace = Workspace() guard let leftFirstPanelId = workspace.focusedPanelId,