From 4e39c3847802115fda0e8272a0f9c1e5c55be6ec Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 5 Aug 2026 21:27:19 -0700 Subject: [PATCH 1/2] Add failing test: parked reveal-time sidebar click must request an apply A click landing while row actions are detached is deferred (#9225) but the replay only runs from the next authoritative apply, and nothing requests one: the park mutates no SwiftUI-tracked state, the sidebar body is Equatable-gated, and an idle app never re-arms the rows. The click stays parked until unrelated invalidation, historically an app deactivate/ reactivate cycle. The test drives the reveal gap (suspend + reveal without an apply), sends the row click, and asserts the controller fires the new onDeferredRowClickAwaitingApply seam exactly once; the seam is inert in this commit so CI shows the test red. Refs https://github.com/manaflow-ai/cmux/issues/9690 Co-Authored-By: Claude Fable 5 --- .../SidebarWorkspaceTableController.swift | 8 ++ ...SidebarWorkspaceTableSuspensionTests.swift | 88 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift b/Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift index ce8f67a8cdd6..bef20b201f43 100644 --- a/Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift +++ b/Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift @@ -24,6 +24,14 @@ final class SidebarWorkspaceTableController: NSObject, NSTableViewDataSource, NS private var rows: [SidebarWorkspaceTableRowConfiguration] = [] private var actions: SidebarWorkspaceTableActions? private var deferredRowClick: DeferredRowClick? + /// SwiftUI-side wake-up for a parked click. A deferred click only lands + /// through the next authoritative apply, and applies only happen when + /// the deliberately Equatable-gated sidebar body re-evaluates. The park + /// itself mutates no SwiftUI-tracked state, so without requesting an + /// apply an idle app never re-arms the rows and the click waits on + /// unrelated invalidation — historically an app deactivate/reactivate + /// (issue #9690). + var onDeferredRowClickAwaitingApply: (() -> Void)? private var hoveredRowId: SidebarWorkspaceRenderItemID? private var contextMenuRowId: SidebarWorkspaceRenderItemID? private var workspaceIds: [UUID] = [] diff --git a/cmuxTests/SidebarWorkspaceTableSuspensionTests.swift b/cmuxTests/SidebarWorkspaceTableSuspensionTests.swift index 00cdcd295961..36199b41e848 100644 --- a/cmuxTests/SidebarWorkspaceTableSuspensionTests.swift +++ b/cmuxTests/SidebarWorkspaceTableSuspensionTests.swift @@ -181,6 +181,94 @@ struct SidebarWorkspaceTableSuspensionTests { ) } + @Test + func visibleRowClickWhileRevealApplyIsPendingRequestsAuthoritativeApply() async throws { + let tabManager = TabManager(autoWelcomeIfNeeded: false) + let initiallySelectedWorkspace = try #require(tabManager.selectedWorkspace) + let clickedWorkspace = tabManager.addWorkspace( + select: false, + autoWelcomeIfNeeded: false, + autoRefreshMetadata: false + ) + let model = SidebarWorkspaceRowSuspensionTests.makeModel( + workspaceId: clickedWorkspace.id + ) + let row = SidebarWorkspaceTableRowConfiguration( + workspaceRowModel: model, + actions: SidebarWorkspaceRowSuspensionTests.makeActions( + model: model, + workspace: clickedWorkspace, + tabManager: tabManager + ), + groupId: nil, + isPinned: false, + environment: SidebarWorkspaceTableEnvironmentSnapshot( + colorScheme: .light, + globalFontMagnificationPercent: 100, + lazyContractProbe: SidebarLazyContractProbe() + ) + ) + let controller = SidebarWorkspaceTableController() + let container = controller.makeContainerView() + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 320, height: 240), + styleMask: [.borderless], + backing: .buffered, + defer: false + ) + window.contentView = container + defer { + window.contentView = nil + window.close() + } + var applyRequests = 0 + controller.onDeferredRowClickAwaitingApply = { applyRequests += 1 } + + controller.apply( + rows: [row], + actions: makeTableActions(), + workspaceIds: [clickedWorkspace.id], + selectedWorkspaceId: initiallySelectedWorkspace.id, + selectedScrollTargetWorkspaceId: nil + ) + await flushStagedTableMutations() + container.layoutSubtreeIfNeeded() + container.tableView.layoutSubtreeIfNeeded() + + controller.setPresentationActive(false, workspaceIds: [clickedWorkspace.id]) + controller.setPresentationActive(true, workspaceIds: [clickedWorkspace.id]) + + let table = container.tableView + let action = try #require(table.action) + let target = try #require(table.target) + table.setValue(0, forKey: "clickedRow") + defer { table.setValue(-1, forKey: "clickedRow") } + #expect(table.sendAction(action, to: target)) + + #expect( + applyRequests == 1, + """ + A click parked on a reveal-time row must request an authoritative \ + apply: the park mutates no SwiftUI-tracked state, so nothing else \ + re-evaluates the Equatable-gated sidebar and the click stays \ + parked until unrelated invalidation (issue #9690: taps only \ + landed after an app focus cycle). + """ + ) + + // Respond to the request the way production SwiftUI does — with a + // fresh authoritative apply — and confirm the parked click lands. + controller.apply( + rows: [row], + actions: makeTableActions(), + workspaceIds: [clickedWorkspace.id], + selectedWorkspaceId: initiallySelectedWorkspace.id, + selectedScrollTargetWorkspaceId: nil + ) + await flushStagedTableMutations() + #expect(tabManager.selectedTabId == clickedWorkspace.id) + } + @Test func hidingRetiresNativeReorderSession() async { let controller = SidebarWorkspaceTableController() From df21c4d44cb0f3fe257a4f9dae16d79d0d681fa7 Mon Sep 17 00:00:00 2001 From: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com> Date: Wed, 5 Aug 2026 21:28:40 -0700 Subject: [PATCH 2/2] Request an authoritative apply when a sidebar click parks awaiting actions Fixes https://github.com/manaflow-ai/cmux/issues/9690: infrequently, sidebar row taps did nothing until an app deactivate/reactivate cycle. A click landing on a presentation-snapshot row (live action captures released) is deferred and replayed from the next authoritative apply (#9225). But the replay was passive: applies only happen when the Equatable-gated sidebar body re-evaluates, and the park itself mutates no SwiftUI-tracked state, so an idle app never re-armed the rows. The parked click waited for unrelated invalidation, which an app focus cycle eventually provided via window-key row repaints. The controller now fires onDeferredRowClickAwaitingApply when it parks a click; SidebarWorkspaceTableView forwards it to VerticalTabsSidebar, which bumps a @State token read by appKitWorkspaceScrollArea (same pattern as appKitPostResizeRefreshToken). The body re-evaluates, updateNSView re-applies fresh action-carrying rows, and the parked click replays immediately. The request fires only from a physical click, never from a replay re-park, so a request per click is the ceiling and it cannot loop. Co-Authored-By: Claude Fable 5 --- Sources/ContentView.swift | 11 ++++++++++- .../AppKitList/SidebarWorkspaceTableController.swift | 7 +++++++ .../AppKitList/SidebarWorkspaceTableView.swift | 4 ++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 6a2ea2b84027..6a4797feead7 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -10612,6 +10612,13 @@ struct VerticalTabsSidebar: View, Equatable { /// notifications) would otherwise stay unrendered until the next /// unrelated sidebar change. The bump forces one fresh rebuild. @State private var appKitPostResizeRefreshToken: UInt64 = 0 + // Bumped when a completed row click parks in the table controller + // awaiting live actions. The park mutates no other tracked state and + // this view is Equatable-gated, so without this token nothing would + // re-evaluate the body, no authoritative apply would re-arm the rows, + // and the parked click would wait on unrelated invalidation + // (issue #9690: taps only landed after an app focus cycle). + @State private var appKitTableApplyRequestToken: UInt64 = 0 @State private var workspaceScrollContentMinHeight: CGFloat = 0 @State private var checklistPopoverWorkspaceId: UUID? // Pending keyed refresh ids are intentionally non-observed. Workspace @@ -11444,6 +11451,7 @@ struct VerticalTabsSidebar: View, Equatable { private func appKitWorkspaceScrollArea(renderContext: WorkspaceListRenderContext) -> some View { let _ = anchorCwdRevision let _ = appKitPostResizeRefreshToken + let _ = appKitTableApplyRequestToken let tableRows: [SidebarWorkspaceTableRowConfiguration] let isDividerDragActive = isPresented && TerminalWindowPortalRegistry.isInteractiveGeometryResizeActive(in: observedWindow) @@ -11476,7 +11484,8 @@ struct VerticalTabsSidebar: View, Equatable { selectedWorkspaceId: selectedWorkspaceId, selectedScrollTargetWorkspaceId: selectedScrollTargetWorkspaceId, isPresented: isPresented, - unreadSource: sidebarUnread + unreadSource: sidebarUnread, + onDeferredClickAwaitingApply: { appKitTableApplyRequestToken &+= 1 } ) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .mask( diff --git a/Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift b/Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift index bef20b201f43..0913ffefe317 100644 --- a/Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift +++ b/Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift @@ -573,6 +573,13 @@ final class SidebarWorkspaceTableController: NSObject, NSTableViewDataSource, NS // so preserve the completed click by stable row identity. previewSelection(row: row, modifiers: modifiers, hitView: nil) deferredRowClick = click + // Request the apply the replay depends on. Fired only from a + // physical click (never from a replay re-park), so a request per + // click is the ceiling and a pathological apply cannot loop. +#if DEBUG + cmuxDebugLog("sidebar.table.applyRequest row=\(row)") +#endif + onDeferredRowClickAwaitingApply?() case .invalid: deferredRowClick = nil } diff --git a/Sources/Sidebar/AppKitList/SidebarWorkspaceTableView.swift b/Sources/Sidebar/AppKitList/SidebarWorkspaceTableView.swift index 821c1a3e153b..97f18be842d8 100644 --- a/Sources/Sidebar/AppKitList/SidebarWorkspaceTableView.swift +++ b/Sources/Sidebar/AppKitList/SidebarWorkspaceTableView.swift @@ -10,6 +10,9 @@ struct SidebarWorkspaceTableView: NSViewRepresentable { let selectedScrollTargetWorkspaceId: UUID? let isPresented: Bool let unreadSource: SidebarUnreadModel + /// Invoked when a completed row click parks awaiting live actions; the + /// owner must invalidate itself so this view re-applies (issue #9690). + let onDeferredClickAwaitingApply: () -> Void #if DEBUG @Environment(\.sidebarLazyContractProbe) private var sidebarLazyContractProbe @@ -28,6 +31,7 @@ struct SidebarWorkspaceTableView: NSViewRepresentable { context.coordinator.reconfigurationProbe = sidebarLazyContractProbe.tableRootViewReconfigure #endif context.coordinator.setUnreadSource(unreadSource) + context.coordinator.onDeferredRowClickAwaitingApply = onDeferredClickAwaitingApply context.coordinator.setPresentationActive(isPresented, workspaceIds: workspaceIds) guard isPresented else { return } context.coordinator.apply(