Repository navigation
Fix sidebar row-ID layout preference feedback #5637
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 |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| import AppKit | ||
| import CmuxUpdater | ||
| import SwiftUI | ||
| import XCTest | ||
|
|
||
| #if canImport(cmux_DEV) | ||
| @testable import cmux_DEV | ||
| #elseif canImport(cmux) | ||
| @testable import cmux | ||
| #endif | ||
|
|
||
| final class AAASidebarWorkspaceRowIdPreferenceRegressionTests: XCTestCase { | ||
| func testAAARowIdPreferenceKeyDoesNotAggregatePerRowIds() { | ||
| let rowIds = (0..<32).map { _ in UUID() } | ||
| var reducedRowIds: Set<UUID> = [] | ||
|
|
||
| for rowId in rowIds { | ||
| SidebarWorkspaceRowIdsPreferenceKey.reduce(value: &reducedRowIds) { | ||
| Set([rowId]) | ||
| } | ||
| } | ||
|
|
||
| XCTAssertTrue( | ||
| reducedRowIds.isEmpty, | ||
| "The legacy row-ID preference key must not aggregate per-row IDs. Aggregating this key performs Set.formUnion across the lazy sidebar rows in the SwiftUI/AttributeGraph layout loop reported in https://github.com/manaflow-ai/cmux/issues/5570 and https://github.com/manaflow-ai/cmux/issues/2586." | ||
| ) | ||
| } | ||
|
|
||
| @MainActor | ||
| func testDefaultWorkspaceSidebarDoesNotPublishRowIdLayoutPreferences() { | ||
| _ = NSApplication.shared | ||
|
|
||
| let defaults = UserDefaults.standard | ||
| let previousProviderId = defaults.object(forKey: CmuxExtensionSidebarSelection.defaultsKey) | ||
| CmuxExtensionSidebarSelection.setProviderId(CmuxExtensionSidebarSelection.defaultProviderId) | ||
| defer { | ||
| if let previousProviderId { | ||
| defaults.set(previousProviderId, forKey: CmuxExtensionSidebarSelection.defaultsKey) | ||
| } else { | ||
| defaults.removeObject(forKey: CmuxExtensionSidebarSelection.defaultsKey) | ||
| } | ||
| } | ||
|
|
||
| let tabManager = TabManager( | ||
| initialWorkspaceTitle: "Workspace 0", | ||
| autoWelcomeIfNeeded: false | ||
| ) | ||
| for index in 1..<32 { | ||
| tabManager.addWorkspace( | ||
| title: "Workspace \(index)", | ||
| select: false, | ||
| eagerLoadTerminal: false, | ||
| autoWelcomeIfNeeded: false, | ||
| autoRefreshMetadata: false | ||
| ) | ||
| } | ||
|
|
||
| var observedRowIdPreferences: [Set<UUID>] = [] | ||
| let root = SidebarWorkspaceRowIdPreferenceProbe( | ||
| tabManager: tabManager, | ||
| onRowIdsPreference: { rowIds in | ||
| observedRowIdPreferences.append(rowIds) | ||
| } | ||
| ) | ||
|
|
||
| let window = NSWindow( | ||
| contentRect: NSRect(x: 0, y: 0, width: 260, height: 520), | ||
| styleMask: [.titled, .closable, .resizable], | ||
| backing: .buffered, | ||
| defer: false | ||
| ) | ||
| let hostingView = MainWindowHostingView(rootView: root) | ||
| hostingView.frame = window.contentRect(forFrameRect: window.frame) | ||
| window.contentView = hostingView | ||
| defer { | ||
| window.contentView = nil | ||
| window.orderOut(nil) | ||
| } | ||
|
|
||
| window.makeKeyAndOrderFront(nil) | ||
| window.displayIfNeeded() | ||
| hostingView.layoutSubtreeIfNeeded() | ||
| for _ in 0..<5 { | ||
| RunLoop.current.run(mode: .default, before: Date().addingTimeInterval(0.01)) | ||
| window.displayIfNeeded() | ||
| hostingView.layoutSubtreeIfNeeded() | ||
| } | ||
|
|
||
| let nonEmptyRowIdPreferences = observedRowIdPreferences.filter { !$0.isEmpty } | ||
| XCTAssertTrue( | ||
| nonEmptyRowIdPreferences.isEmpty, | ||
| "The default workspace sidebar must not publish non-empty row-ID layout preferences in steady state. A row-wide layout preference feeds the SwiftUI/AttributeGraph layout loop reported in https://github.com/manaflow-ai/cmux/issues/5570 and https://github.com/manaflow-ai/cmux/issues/2586." | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private struct SidebarWorkspaceRowIdPreferenceProbe: View { | ||
| @State private var selection: SidebarSelection = .tabs | ||
| @State private var selectedTabIds: Set<UUID> = [] | ||
| @State private var lastSidebarSelectionIndex: Int? | ||
|
|
||
| let tabManager: TabManager | ||
| let onRowIdsPreference: (Set<UUID>) -> Void | ||
| private let updateViewModel = UpdateStateModel() | ||
| private let fileExplorerState = FileExplorerState() | ||
| private let cmuxConfigStore = CmuxConfigStore() | ||
| private let windowId = UUID() | ||
|
|
||
| var body: some View { | ||
| VerticalTabsSidebar( | ||
| updateViewModel: updateViewModel, | ||
| fileExplorerState: fileExplorerState, | ||
| windowId: windowId, | ||
| onSendFeedback: {}, | ||
| onToggleSidebar: {}, | ||
| onNewTab: {}, | ||
| observedWindow: nil, | ||
| selection: $selection, | ||
| selectedTabIds: $selectedTabIds, | ||
| lastSidebarSelectionIndex: $lastSidebarSelectionIndex | ||
| ) | ||
| .environmentObject(tabManager) | ||
| .environmentObject(TerminalNotificationStore.shared) | ||
| .environmentObject(cmuxConfigStore) | ||
| .frame(width: 260, height: 520) | ||
| .onPreferenceChange(SidebarWorkspaceRowIdsPreferenceKey.self) { rowIds in | ||
| onRowIdsPreference(rowIds) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import AppKit | ||
| import CmuxUpdater | ||
|
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: #!/bin/bash
# Description: Check if tests compile without the CmuxUpdater import
# Remove the import and try to build the tests
cd "$(git rev-parse --show-toplevel)"
# Create a temporary file without the import
sed '2d' cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift > /tmp/test_without_import.swift
# Try to compile just this test file
xcodebuild -project cmux.xcodeproj \
-scheme cmux \
-configuration Debug \
-destination 'platform=macOS' \
-derivedDataPath /tmp/cmux-verify-import \
build-for-testing \
2>&1 | grep -i "SidebarWorkspaceSnapshotRefreshPolicyTests"Repository: manaflow-ai/cmux Length of output: 42 🏁 Script executed: #!/bin/bash
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
echo "== command availability =="
command -v xcodebuild || true
echo
echo "== file excerpt =="
nl -ba cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift | head -n 200
echo
echo "== direct CmuxUpdater symbol usage in file =="
rg -n "CmuxUpdater|CmuxUpdater\." cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift || true
echo
echo "== quick compile check: build-for-testing baseline =="
# Note: this will likely fail in this sandbox if Xcode isn't available.
xcodebuild -project cmux.xcodeproj \
-scheme cmux \
-configuration Debug \
-destination 'platform=macOS' \
-derivedDataPath /tmp/cmux-verify-import-baseline \
build-for-testing 2>&1 | head -n 200 || trueRepository: manaflow-ai/cmux Length of output: 169 🏁 Script executed: #!/bin/bash
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
echo "== tools =="
command -v rg || true
command -v cat || true
command -v sed || true
command -v head || true
command -v xcodebuild || true
echo
echo "== imports in SidebarWorkspaceSnapshotRefreshPolicyTests.swift =="
rg -n "^import " cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift || true
echo
echo "== CmuxUpdater symbol usage in file =="
rg -n "CmuxUpdater(\.|\\b)" cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift || true
echo
echo "== first ~120 lines with numbers (cat -n) =="
cat -n cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift | head -n 160
echo
echo "== whether file imports testable import cmux =="
rg -n "testable import cmux|`@testable` import cmux" cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift || true
echo
echo "== sample: other *Sidebar* test files import CmuxUpdater? =="
rg -n "^import CmuxUpdater" cmuxTests | head -n 50 || trueRepository: manaflow-ai/cmux Length of output: 7964 🏁 Script executed: #!/bin/bash
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
echo "== tools =="
command -v rg || true
command -v cat || true
command -v sed || true
command -v head || true
command -v xcodebuild || true
echo
echo "== imports in SidebarWorkspaceSnapshotRefreshPolicyTests.swift =="
rg -n "^import " cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift || true
echo
echo "== CmuxUpdater symbol usage in file =="
rg -n "CmuxUpdater(\.|\\b)" cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift || true
echo
echo "== first ~160 lines with numbers (cat -n) =="
cat -n cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift | head -n 200
echo
echo "== whether file imports testable import cmux =="
rg -n "testable import cmux|`@testable` import cmux" cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift || true
echo
echo "== sample: other tests import CmuxUpdater? =="
rg -n "^import CmuxUpdater" cmuxTests | head -n 50 || trueRepository: manaflow-ai/cmux Length of output: 9598 Remove the unused
🤖 Prompt for AI Agents |
||
| import SwiftUI | ||
| import XCTest | ||
|
|
||
|
|
||
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.
shouldScrollSelectedWorkspaceAfterWorkspaceOrderChangereturningtruecausesscrollSelectedWorkspaceIfNeededto callproxy.scrollTosynchronously duringonReceive, before SwiftUI processes the updatedtabManager.tabsinto a new render. At that pointrenderContext.workspaceIdsstill carries the pre-move ordering and theLazyVStackstill places the row at its old offset, so the scroll view jumps to the old position. After the subsequent render the row physically moves to its new slot, leaving the scroll position stale. The old preference-based flush path fired post-layout (viaonPreferenceChange), so it always scrolled to the settled position. For visible-row drag reorders the effect is imperceptible, but programmatic moves that shift the selected workspace far in the list can leave scroll state stale.