Skip to content

Lawrence Sidebar: coalesce rapid selections + Equatable subtree boundary - #8316

Closed
azooz2003-bit wants to merge 5 commits into
mainfrom
feat-lawrence-selection-coalesce
Closed

azooz2003-bit wants to merge 5 commits into
mainfrom
feat-lawrence-selection-coalesce

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 17, 2026 •

Copy link
Copy Markdown
Collaborator

Rapid sidebar clicks each queued a full selection commit (container re-render + terminal-content swap), so consecutive selections serialized and later ones felt slower — reported during Lawrence Sidebar dogfood. Plain clicks now route through a leading+trailing coalescer (100ms): single clicks apply immediately, bursts collapse to first + newest. The optimistic press highlight still tracks every click instantly; cmd/shift clicks bypass coalescing (order-dependent multi-select). Flag-ON only (sidebar-appkit-list-experiment).

Verified on tag sbak: a 6-click burst at ~45ms spacing produced exactly 2 sidebar.select commits (leading + trailing last-wins); single clicks unchanged.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Coalesce rapid sidebar clicks so only the first and latest selection apply, reduce sidebar re-renders with an .equatable() boundary, and fix hover chrome stranding by enforcing hover on visible cells. Enabled behind sidebar-appkit-list-experiment.

  • Performance

    • Added SidebarSelectionCoalescer (100ms lead+trail) to collapse plain-click bursts; Cmd/Shift clicks bypass; modifiers captured at click time; trailing delay uses an injected Clock with cancel.
    • Gated VerticalTabsSidebar with .equatable() under the flag to skip parent-driven re-evaluations; added a DEBUG rows-build log; verified a 6-click burst -> 2 selection commits, single clicks unchanged.
  • Bug Fixes

    • Sweep visible cells after applies and viewport changes to enforce the authoritative hovered row; cells re-apply the full model so hover-revealed chrome (close button, header plus) doesn’t strand.

Written for commit 729977c. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved sidebar selection behavior for rapid plain clicks while preserving command- and shift-click actions.
    • Fixed hover-state and hover-revealed controls after scrolling, row updates, and viewport changes.
    • Reduced unnecessary sidebar updates for smoother rendering.

A selection commit re-renders the container and swaps terminal content
(~tens of ms); a burst of clicks queued one full commit per click, so
later selections felt progressively slower. Plain clicks in the AppKit
table now route through a leading+trailing coalescer (100ms window):
single clicks apply immediately, clicks inside the window replace the
pending request, and one trailing fire applies only the newest. The
row's optimistic press highlight still tracks every click instantly,
and modifiers are captured at click time so a deferred apply cannot
re-read the keyboard. Cmd/shift clicks bypass coalescing entirely
(multi-select mutations are order-dependent).

Verified on tag sbak: a 6-click burst (~45ms apart) produced exactly 2
selection commits (first + last row), single clicks unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Sidebar row clicks now preserve captured modifiers and coalesce plain-click selection updates. Visible sidebar cells reconcile hover state after content and viewport changes. Sidebar rendering adds feature-gated equality evaluation and debug row-count logging.

Changes

Sidebar interaction and rendering

Layer / File(s) Summary
Selection coalescing and modifier-aware clicks
Sources/Sidebar/AppKitList/SidebarSelectionCoalescer.swift, Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift, Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCommands.swift, cmux.xcodeproj/project.pbxproj
Plain clicks use trailing coalescing, modified clicks cancel pending work and apply immediately, and selection commands receive captured modifier flags.
Visible-cell hover reconciliation
Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift, Sources/Sidebar/AppKitList/Cells/SidebarGroupHeaderRowView.swift, Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCellView.swift
Visible header and workspace cells expose hover enforcement, which the controller invokes after content and viewport changes.
Feature-gated sidebar rendering
Sources/ContentView.swift
VerticalTabsSidebar gains equality comparison, AppKit sidebar rendering uses .equatable() when enabled, and debug builds log workspace render-item counts.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SidebarWorkspaceTableController
  participant SidebarSelectionCoalescer
  participant SidebarWorkspaceRowCommands
  User->>SidebarWorkspaceTableController: Click workspace row
  SidebarWorkspaceTableController->>SidebarSelectionCoalescer: Request plain-click update
  SidebarSelectionCoalescer->>SidebarWorkspaceRowCommands: Apply latest updateSelection(modifiers:)
  SidebarWorkspaceTableController->>SidebarSelectionCoalescer: Cancel for command/shift click
  SidebarWorkspaceTableController->>SidebarWorkspaceRowCommands: Apply modified selection immediately
Loading

Possibly related PRs


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error SidebarSelectionCoalescer adds production clock.sleep(for:) trailing selection coalescing, introducing new timing-based synchronization in sidebar click handling. Replace the sleep-driven trailing apply with an event-driven callback/state transition or remove the delay; don’t gate shipped selection commits on elapsed time.
Cmux Architecture Rethink ❌ Error Plain-click selection now routes through SidebarSelectionCoalescer, a production 100ms clock.sleep buffer/side channel that delays commits, which the rule forbids. Move selection sequencing into the existing selection owner/action pipeline and keep clicks synchronous; if dedupe is needed, use idempotent state transitions, not delayed tasks.
Description check ⚠️ Warning The description has Summary and Testing, but it omits the required Demo Video, Review Trigger, and Checklist sections from the template. Add the missing Demo Video URL, Review Trigger block, and Checklist items, and ensure the summary and testing sections match the template.
✅ Passed checks (22 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed New mutable state stays on @MainActor types; no new Sendable/shared-state or background-access isolation debt is introduced.
Cmux Browser Automation Off-Main ✅ Passed PASS: Diff only touches sidebar/AppKit selection/hover code; the rule applies to browser socket automation in TerminalController/ControlCommandExecutionPolicy, so no offending routing changed.
Cmux Expensive Synchronous Load ✅ Passed Diff only changes sidebar click coalescing/hover UI; no added agent-history loads, JSON parsing, or main-actor file scans in touched hunks.
Cmux Cache Substitution Correctness ✅ Passed The cached values are only transient UI inputs (.equatable(), click-time modifiers, hover state); no persistence/history/undo/snapshot path now trusts a stale cache.
Cmux No Hacky Sleeps ✅ Passed Only new non-Swift timing code is a bounded client timeout abstraction with tests and rAF presentation throttling; no hacky sleeps in runtime scripts.
Cmux Algorithmic Complexity ✅ Passed PR only adds O(1) coalescer state/sleep and a visible-cells hover repaint; no nested scans, batch rescans, or new sorting/filtering hot paths.
Cmux Swift Concurrency ✅ Passed PASS: The new async work is a MainActor coalescer with a stored, cancellable Task; no new background queues, Combine state, or unmanaged fire-and-forget Tasks appear.
Cmux Swift @Concurrent ✅ Passed PASS: The touched Swift code stays on @MainActor or hops off explicitly via Task/Task.detached; no new @concurrent misuse or actor-leaky async helper was introduced.
Cmux Swift Package Boundaries ✅ Passed The new coalescer and click-path edits are AppKit/main-actor glue in the app target; the rule allows this, and no reusable domain logic was stranded there.
Cmux Swiftpm Lockfiles ✅ Passed The only SwiftPM manifest change adds resources; no package-reference hunks or Package.resolved files changed in the PR range.
Cmux Swift Logging ✅ Passed All added log calls are wrapped in #if DEBUG and use cmuxDebugLog; the rule explicitly allows debug-only cmux logging.
Cmux User-Facing Error Privacy ✅ Passed Visible Design Mode errors are generic; no vendor/provider/secret data appears in user-facing copy, and raw-error logs are DEBUG-only.
Cmux Full Internationalization ✅ Passed PR only changes selection coalescing and hover re-apply logic/comments; no new user-facing strings, catalogs, or web copy were added.
Cmux Swiftui State Layout ✅ Passed PASS: The PR diff only touches AppKit bridge row views and a selection coalescer; no new SwiftUI state/layout patterns were introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR only changes sidebar selection coalescing/hover logic and a project source listing; no standalone window APIs or cmux auxiliary window identifiers were added or changed.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/config files; no local logs, caches, build output, or scratch artifacts are added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No new test/debug seam was introduced; the PR adds production selection/hover logic, and the sidebar row DEBUG log was pre-existing.
Cmux No Ambient Global State ✅ Passed PR adds only an injectable coalescer class and instance state; no new file-scope funcs/vars or singletons were introduced.
Title check ✅ Passed The title is concise and accurately highlights the main change: coalescing rapid selections with an Equatable subtree boundary.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-lawrence-selection-coalesce

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a SidebarSelectionCoalescer (100 ms leading+trailing debounce) to collapse rapid plain-click workspace selections, gates VerticalTabsSidebar with .equatable() under the feature flag to skip parent-driven re-evaluations, and sweeps visible cells after every apply/viewport change to prevent hover chrome from stranding when the pointer is parked during content churn.

  • Coalescer: plain clicks are routed through a leading+trailing coalescer; modifier clicks (cmd/shift) cancel any pending trailing and apply immediately to preserve multi-select ordering; modifiers are captured at click time so a deferred trailing closure doesn't re-read a stale keyboard state.
  • Equatable gate: VerticalTabsSidebar now conforms to Equatable comparing only stable identity-checked references (windowId, observedWindow, updateViewModel, fileExplorerState); Bindings and closures are intentionally excluded because SwiftUI tracks them through its own dependency graph.
  • Hover enforcement: enforceHoverOnVisibleCells sweeps all visible rows after each apply and after viewport changes, calling enforcePointerHovering(_:) on each cell to correct any hover state that per-transition repaints missed due to row-index/id races.

Confidence Score: 4/5

Safe to merge after addressing the missing coalescer cancel in the double-click path.

Every code path routes modifier clicks through an immediate cancel+apply, correctly preserving multi-select ordering. The single concrete gap is didDoubleClickTableRow: it does not cancel the coalescer, so a trailing selection fires ~50ms after beginInlineRename starts. That deferred updateSelection calls setSelectionToTabs(), which can trigger a SwiftUI sidebar rebuild that tears down the just-started inline-rename text-field editor. A one-line selectionCoalescer.cancel() at the top of didDoubleClickTableRow closes the gap.

Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift — the didDoubleClickTableRow method needs selectionCoalescer.cancel() before beginInlineRename.

Important Files Changed

Filename Overview
Sources/Sidebar/AppKitList/SidebarSelectionCoalescer.swift New leading+trailing coalescer; correct @mainactor isolation and cancel logic, but request uses clock.sleep which previous threads have flagged under cmux-swift-blocking-runtime.
Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift Routes plain clicks through the coalescer and adds enforceHoverOnVisibleCells; missing selectionCoalescer.cancel() in didDoubleClickTableRow leaves a live trailing selection that fires after inline rename begins.
Sources/ContentView.swift Adds Equatable conformance on VerticalTabsSidebar and wraps it with .equatable() under the flag; the == implementation correctly excludes Bindings/closures and compares only stable identity-checked references.
Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCellView.swift Adds enforcePointerHovering with a guard against redundant applies; full applyModel re-apply when model is set is intentional to keep all hover-gated chrome in sync.
Sources/Sidebar/AppKitList/Cells/SidebarGroupHeaderRowView.swift Adds enforcePointerHovering mirroring the workspace cell; correctly delegates to updatePlusVisibility.
Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCommands.swift Changes updateSelection to accept modifiers as a parameter with a default; enables click-time capture so coalesced trailing fires use the correct modifier state.
cmux.xcodeproj/project.pbxproj Adds SidebarSelectionCoalescer.swift to the Xcode project sources; file references and build-phase entries look correct.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant TableController as SidebarWorkspaceTableController
    participant Coalescer as SidebarSelectionCoalescer
    participant Commands as SidebarWorkspaceRowCommands

    User->>TableController: click (plain)
    TableController->>Coalescer: request(apply)
    alt leading edge (no pending task, ≥100ms since last)
        Coalescer->>Commands: updateSelection(modifiers) [immediate]
    else within window
        Coalescer-->>Coalescer: replace pendingApply
        Note over Coalescer: trailing Task waits ~100ms
        Coalescer->>Commands: updateSelection(modifiers) [deferred]
    end

    User->>TableController: click (cmd/shift)
    TableController->>Coalescer: cancel()
    TableController->>Commands: updateSelection(modifiers) [immediate]

    User->>TableController: double-click
    Note over TableController: ⚠️ cancel() not called here
    TableController->>TableController: beginInlineRename()
    Note over Coalescer: trailing still live (~50ms later fires)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant TableController as SidebarWorkspaceTableController
    participant Coalescer as SidebarSelectionCoalescer
    participant Commands as SidebarWorkspaceRowCommands

    User->>TableController: click (plain)
    TableController->>Coalescer: request(apply)
    alt leading edge (no pending task, ≥100ms since last)
        Coalescer->>Commands: updateSelection(modifiers) [immediate]
    else within window
        Coalescer-->>Coalescer: replace pendingApply
        Note over Coalescer: trailing Task waits ~100ms
        Coalescer->>Commands: updateSelection(modifiers) [deferred]
    end

    User->>TableController: click (cmd/shift)
    TableController->>Coalescer: cancel()
    TableController->>Commands: updateSelection(modifiers) [immediate]

    User->>TableController: double-click
    Note over TableController: ⚠️ cancel() not called here
    TableController->>TableController: beginInlineRename()
    Note over Coalescer: trailing still live (~50ms later fires)
Loading

Reviews (4): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment on lines +34 to +42
trailingTask = Task { [weak self] in
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
guard let self, !Task.isCancelled else { return }
self.trailingTask = nil
self.lastApplied = CACurrentMediaTime()
let apply = self.pendingApply
self.pendingApply = nil
apply?()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The cmux-swift-blocking-runtime rule explicitly states "Do not allow Task.sleep in production code just because it is inside an async function, including for animation timing." The debounce delay here is implemented via Task.sleep in new production Swift. The rule's preferred shape is a real signal or a cancellation-aware timer abstraction rather than a wall-clock sleep. A DispatchSourceTimer (cancelled in cancel()) created on the main queue would satisfy the rule and has identical semantics without relying on cooperative sleep cancellation.

Suggested change
trailingTask = Task { [weak self] in
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
guard let self, !Task.isCancelled else { return }
self.trailingTask = nil
self.lastApplied = CACurrentMediaTime()
let apply = self.pendingApply
self.pendingApply = nil
apply?()
}
let timer = DispatchSource.makeTimerSource(queue: .main)
timer.schedule(deadline: .now() + delay)
timer.setEventHandler { [weak self] in
guard let self else { return }
self.trailingTask = nil
self.lastApplied = CACurrentMediaTime()
let apply = self.pendingApply
self.pendingApply = nil
apply?()
}
timer.resume()
// trailingTask type should be updated to DispatchSourceTimer

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +14 to +52
final class SidebarSelectionCoalescer {
private var pendingApply: (() -> Void)?
private var trailingTask: Task<Void, Never>?
private var lastApplied: CFTimeInterval = 0
private let window: TimeInterval

init(window: TimeInterval = 0.1) {
self.window = window
}

func request(_ apply: @escaping @MainActor () -> Void) {
let now = CACurrentMediaTime()
if trailingTask == nil, now - lastApplied >= window {
lastApplied = now
apply()
return
}
pendingApply = apply
guard trailingTask == nil else { return }
let delay = max(0, window - (now - lastApplied))
trailingTask = Task { [weak self] in
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
guard let self, !Task.isCancelled else { return }
self.trailingTask = nil
self.lastApplied = CACurrentMediaTime()
let apply = self.pendingApply
self.pendingApply = nil
apply?()
}
}

/// Drops any pending request. Used before selection paths that must not
/// be reordered (modifier clicks mutate the multi-selection set).
func cancel() {
trailingTask?.cancel()
trailingTask = nil
pendingApply = nil
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Timing repair leaves the commit cost unaddressed

The cmux-swift-architectural-rethink rule flags timing repair paths used to paper over rendering races. The coalescer reduces how often updateSelection triggers a container re-render and terminal-content swap, but does not name or fix why a single updateSelection call is expensive enough to cause noticeable serial slowdown. A rapid burst on a slower machine, or a future caller outside the flag, will surface the same problem. Consider documenting what the expensive operation is and whether it can be made idempotent (skipping work when the workspace being selected is already selected) or deferred to the next run-loop turn.

Parent-driven re-evaluations of ContentView (divider width ticks, any
unrelated @State churn) re-ran the whole sidebar subtree: body, the
128-row projection prelude, and the AttributeGraph diff. The sidebar's
inputs are stable identities (windowId, window, two model objects) with
width applied outside via .frame, so with the flag on the subtree now
sits behind .equatable(): parent re-evals with identical inputs skip it
entirely. Every data source the body renders from (@EnvironmentObject,
@ObservedObject, @binding, @State) invalidates the view directly and
bypasses the gate — verified live: notifications and selection still
rebuild rows (7 rebuilds for notify+select), pure idle settles to ~0.17
rebuilds/s driven only by real shell churn. Flag off keeps the ungated
subtree. Adds a DEBUG rowsBuild log line as the countable signal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@azooz2003-bit azooz2003-bit changed the title Coalesce rapid sidebar clicks to the latest selection (Lawrence Sidebar) Lawrence Sidebar: coalesce rapid selections + Equatable subtree boundary Jul 17, 2026
cmux reload-cloud and others added 2 commits July 17, 2026 00:27
…e Sidebar)

Hover-revealed chrome (row close button, header plus) stranded on rows
the pointer had left: per-transition repaints resolve row ids against a
rows array that can mutate in the same tick, so content churn scrolling
rows under a parked pointer left multiple rows showing hover chrome at
once. The controller now sweeps visible cells after each apply and
viewport change and enforces hover from the authoritative hoveredRowId;
cells short-circuit when already correct.

Verified on tag sbak with the notification spam environment and a
parked cursor: 45s of churn ends with exactly one hovered row showing
its close button (previously three or more).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ted Clock

Greptile P1: enforcePointerHovering only refreshed the close button, but
hover also gates the trailing badge and spinner (hidden while the close
button shows) — the sweep now re-runs applyModel so every hover-derived
slot stays consistent. The coalescer's trailing delay moves from raw
Task.sleep to an injected Clock sleep with cancellation, per the
bounded-delay policy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

On the 'timing repair leaves commit cost unaddressed' note: agreed that coalescing alone would paper over the cost, which is why this PR pairs it with the Equatable boundary that removes the sidebar subtree from parent-driven commits entirely. The remaining per-commit cost is portal geometry sync and whole-window layout, tracked separately (Time Profiler attribution in #8270). The coalescer's role is semantic: a click burst has exactly one intended terminal destination, so intermediate content swaps are dropped by design, not merely deferred. Task.sleep is now an injected Clock per the bounded-delay policy.

…on-coalesce

# Conflicts:
#	Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSlotViews.swift
@cursor

cursor Bot commented Jul 17, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Consolidated into #8366 per dogfood owner request (single PR for the Lawrence Sidebar fix train).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant