Skip to content

Lawrence Sidebar: consolidated dogfood fixes (badge centering, selection coalescing, Equatable boundary, hover enforcement, highlight width) - #8366

Merged
azooz2003-bit merged 30 commits into
mainfrom
lawrence-sidebar-fixes
Jul 17, 2026
Merged

azooz2003-bit merged 30 commits into
mainfrom
lawrence-sidebar-fixes

Conversation

@azooz2003-bit

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

Copy link
Copy Markdown
Collaborator

Consolidates the overnight Lawrence Sidebar fix train into one PR, per dogfood owner request. All flag-ON only (sidebar-appkit-list-experiment, default OFF); flag-off paths untouched.

Group-header badge centering — the header's unread badge rendered its digit off the circle's optical center (NSTextField intrinsic insets). Now uses the shared direct-draw SidebarRowUnreadBadgeView (cap-height optical centering), one implementation for header and row badges.

Selection coalescing — rapid sidebar clicks each queued a full selection commit (container re-render + terminal swap), so burst clicks felt progressively slower. Plain clicks route through a leading+trailing 100ms coalescer: single clicks apply immediately, bursts collapse to first + newest; press highlight still tracks every click; cmd/shift bypass (order-dependent); modifiers captured at click time; injected-Clock delay. Verified: 6-click burst at ~45ms spacing → exactly 2 selection commits.

Equatable boundary on VerticalTabsSidebar — divider width ticks and unrelated ContentView churn re-ran the whole sidebar subtree (body + 128-row prelude + graph diff). The subtree now sits behind .equatable() (inputs are stable identities; width applies outside via .frame); all rendered data flows through tracked dependencies that bypass the gate. Verified: notifications/selection still rebuild rows; pure idle ~0.17 rebuilds/s.

Hover enforcement — hover-revealed chrome (close button, header plus) stranded on rows the pointer left (id/index races during churn). The controller sweeps visible cells after each apply/viewport change, enforcing hover from the authoritative hoveredRowId; the sweep re-runs the full model apply so badge/spinner slots stay consistent. Verified: 45s parked-cursor churn → exactly one hovered row shows its close button.

Highlight width parity — the selection background (and all row content) sat 6pt inboard of legacy: NSTableView .fullWidth still insets cell frames ~6pt/side, stacking with the cell's 6pt outer padding. .plain hands cells the full row width. Verified: left inset now exactly 6.0pt, matching legacy.

Supersedes #8312, #8316, #8323, #8364.

🤖 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

Consolidates Lawrence Sidebar polish behind sidebar-appkit-list-experiment (default OFF). Selection is snappier with full color flip on press, hover chrome stays correct, badges are centered, and the highlight matches legacy width.

  • Bug Fixes

    • Centered group-header unread badges via shared SidebarRowUnreadBadgeView.
    • Applied the full selection treatment optimistically on press (and deselect) so background, text, previews, and badges update instantly.
    • Enforced hover on visible rows/headers so close/plus don’t stick; sweep re-applies the full model.
    • Switched NSTableView to .plain so rows use full width and the highlight inset matches legacy.
  • Refactors

    • Added SidebarSelectionCoalescer (100ms leading+trailing) to collapse rapid plain-click selections; captures modifiers at click time; cmd/shift bypass.
    • Wrapped VerticalTabsSidebar with .equatable() to skip sidebar subtree rebuilds on unrelated parent updates (tracked data still updates normally).

Written for commit f9917aa. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Performance

    • Reduced sidebar re-rendering via conditional sidebar caching.
    • Coalesced rapid sidebar selection updates to lower render/commit churn.
  • Bug Fixes

    • Improved hover-state consistency across visible workspace rows, including close/add controls.
    • Fixed optimistic selection visuals so selected treatment updates immediately.
    • Improved command-/shift-click selection behavior.
    • Aligned workspace row and selection geometry with the existing sidebar padding model.

cmux reload-cloud and others added 29 commits July 15, 2026 21:06
Port of PR 8068 (issue-8004-appkit-list @ ebea0a0) resolved by hand onto
856a186. Reference commit before re-integration onto post-8211 main.
sidebar-appkit-list-experiment (default OFF) switches the workspace sidebar
between the existing SwiftUI LazyVStack list (default) and a new pure-AppKit
NSTableView implementation: virtualized rows, measured-once height cache,
per-row Combine churn pump, native cells for workspace rows and group headers
(badges, spinners, media glyphs, branch/dir candidates, PR vector icons,
ports, checklist, hint pills, drop indicators), NSMenu context menus with
full SwiftUI parity, and table-owned hover/middle-click/drag.

Fixes the LazyVStack scrollbar stutter and scroll-edge tap latency at 100+
workspaces by construction when the flag is enabled.
…list popover style, header truncation, divider drag cost

- Row selection via NSTableView target/action (cell gesture recognizers lose
  to the table's mouse tracking loop); double-click routes to inline rename.
- Unread badge digit drawn directly with optical cap-height centering.
- Checklist honors the popover style setting: never inline; summary opens an
  NSPopover with the same AppKit item lines.
- Group header name field owns all remaining width; badge tracks measured
  text, so names no longer truncate early.
- Divider resize: heights pinned to last stable width during drags with one
  trailing re-measure; per-workspace snapshot memo so container re-renders do
  dictionary lookups instead of full snapshot rebuilds.
A cancelled divider DragGesture never fires onEnded, stranding
isResizerDragging; the 16ms cursor stabilizer then re-asserts the resize
cursor indefinitely. The stabilizer now reconciles against the physical
button state and force-releases the cursor when the button is up.
…lush

Divider drags previously committed width to SwiftUI state and waited for the
next runloop turn + vsync to lay out and present, so the divider trailed the
cursor while the main thread sat idle (profiled). Each drag event now flushes
layoutSubtreeIfNeeded + displayIfNeeded in the same mouse event, riding the
terminal portal's immediate anchor-sync path during interactive resizes.
Per-event synchronous flushing did redundant layout+draw passes when mouse
events outpaced the display (high-polling mice). The flush now runs at most
once per refresh interval (screen maximumFramesPerSecond, min 60); events in
between only update state and ride the next eligible flush.
…ard restore

- Close/plus hover reveals fade with 120ms ease-out (hit-testing follows
  target state immediately so fading buttons never swallow clicks).
- en/ja catalog entries for the sidebar-appkit-list feature flag strings.
- Restore main's check-sidebar-lazy-layout.py + self-test + scale tests:
  the flag-off default path preserves the original LazyVStack contract
  byte-faithfully, so the original guard passes unchanged; the AppKit table
  path is additive.
Merges origin/main (a5a70ff) including the sidebar row-layer refactor
(SidebarWorkspaceRowInput projections, container-owned snapshots and
observation, selectWorkspaceRow) plus #8211 and the #8240 terminal-resize
coalescing. The AppKit table now consumes the same projection as the SwiftUI
list: workspaceTableRowConfiguration builds its row model from
SidebarWorkspaceRowInput and the shared context-menu aggregates, container
observation replaces the per-cell pump and snapshot memo, and the AppKit
actions bundle is renamed SidebarAppKitRowActions to coexist with upstream's
type. Verified rendering and interaction on both flag states.
The clicked workspace cell paints its selected background and title color
the instant the press lands, before NSTableView's click tracking and the
container round trip confirm the selection; the authoritative configure
reconciles (or reverts) immediately after. Selection now reads as
instantaneous instead of tracking the async model path.
With the flag on, the resizer handle hosts an NSView that runs
NSSplitView's tracking technique: pull mouse events with
nextEvent(matching:) in .eventTracking mode, apply the width per event,
spin the runloop once so SwiftUI/CA commit, then present via
displayIfNeeded — all inside the same mouse event. This removes the
async runloop hop between HID drag events and on-screen width changes
that made SwiftUI DragGesture resizes feel detached from the cursor.
The view accepts first mouse and owns its cursor rect, matching native
split-divider behavior.

With the flag off, the original DragGesture path is unchanged. The
per-event flush throttle (flushSidebarResizeToDisplay/MutableTimeBox)
is removed; it ran after SwiftUI had already deferred the commit past
the event, so it presented stale layout and had no user-visible effect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ar rows (flag ON)

Time Profiler attribution of a real drag (68ms per width commit, ~11
updates/s): 57% was the portal's per-anchor failsafe fan-out (every
anchor callback re-synced all hosted views and re-reconciled every
visible surface), 14% the sidebar row-projection prelude, 5% a DEBUG
log helper doing a synchronous pasteboard XPC per pane layout.

With the AppKit sidebar flag on, the anchor callback now syncs only its
own hosted view and coalesces the failsafe fan-out to one deferred pass
per main-queue turn (still within the same drag tick — the tracking
loop spins the runloop per event). Flag off keeps the per-callback
fan-out. Row building freezes against the last-built rows while the
portal registry reports an interactive resize; rows cannot change while
the resizer owns the mouse. The drag-layout debug log now gates on
app-owned drag state before touching NSPasteboard. The tracking loop
gains per-phase timing (write/commit/layout/display/flush) and a
hit-test routing diagnostic, and its runloop pass uses a real 1ms
deadline so SwiftUI/CA commits run inside the event.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CodeRabbit/Greptile round: end the registry's interactive-resize
session from the cursor failsafe (portals stayed latched in drag mode);
O(1) group lookup in table row building; remove double signpost end;
wrap port controls at the row's trailing edge; drop the deferred
modal-focus repair in rename/color prompts (initialFirstResponder
suffices); menu no longer disappears without a notification store (only
its notification section does); per-workspace filters on bulk mark
read/unread (legacy parity); group headers rejected for middle-click
close and row drags (they carry the anchor's workspaceId); closed-PR
icon tint drawn inside the image (.sourceAtop against transparent
backing drew nothing); agent tooltip defaultValues aligned with the
canonical translations. Settings title for the flag is now 'Lawrence
Sidebar' (en/ja; key unchanged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	Resources/Localizable.xcstrings
#	Sources/FeatureFlags.swift
Same defect class as the workspace-row badge: NSTextField's intrinsic
insets shift a single digit off the circle's optical center. The header
now uses the shared direct-draw SidebarRowUnreadBadgeView, which centers
on the digit's cap-height band; badge width tracks the measured text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
…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>
…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>
…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>
…on-coalesce

# Conflicts:
#	Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSlotViews.swift
…arity

# Conflicts:
#	Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSlotViews.swift
…adge

# Conflicts:
#	Sources/Sidebar/AppKitList/Cells/SidebarGroupHeaderRowView.swift
#	Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSlotViews.swift
…e Sidebar)

The AppKit sidebar's selection background (and all row content) sat 6pt
inboard of the legacy sidebar's geometry: NSTableView's .fullWidth style
still insets cell frames by ~6pt per side, which stacked with the
cell's own 6pt outer padding for 12pt total vs legacy's 6pt. With
.plain the table hands cells the full row width and the cell's padding
is the only inset. Measured on tag sbak: selection left inset now
exactly 6.0pt, matching the legacy sidebar.

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

The PR adds feature-flagged sidebar render gating, coalesced selection updates, click-time modifier handling, optimistic selection painting, authoritative hover synchronization, plain table geometry, and direct-drawn unread badges for the AppKit sidebar.

Changes

AppKit sidebar behavior

Layer / File(s) Summary
Sidebar render gating and diagnostics
Sources/ContentView.swift
VerticalTabsSidebar conforms to Equatable; the sidebar is conditionally wrapped with .equatable(), and debug builds log workspace row counts.
Selection coalescing, optimistic rendering, and hover synchronization
Sources/Sidebar/AppKitList/SidebarSelectionCoalescer.swift, Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift, Sources/Sidebar/AppKitList/Cells/*, Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCommands.swift, cmux.xcodeproj/project.pbxproj
Plain-click selection requests are coalesced with captured modifiers, command/shift selections apply immediately, optimistic selection states reapply row models, and visible cells receive enforced hover state. The new coalescer is added to the Xcode target.
Sidebar cell rendering and table geometry
Sources/Sidebar/AppKitList/Cells/SidebarGroupHeaderRowView.swift, Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift
The table uses .plain styling, and unread badges use SidebarRowUnreadBadgeView with font-based measurement and redraw handling.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SidebarWorkspaceTableController
  participant SidebarSelectionCoalescer
  participant SidebarWorkspaceRowCommands
  User->>SidebarWorkspaceTableController: Click row with modifier flags
  SidebarWorkspaceTableController->>SidebarSelectionCoalescer: Queue plain-click selection
  SidebarSelectionCoalescer->>SidebarWorkspaceRowCommands: Apply latest selection
  SidebarWorkspaceTableController->>SidebarWorkspaceRowCommands: Apply command/shift selection immediately
Loading

Possibly related issues

Possibly related PRs

  • manaflow-ai/cmux#8316: Directly overlaps the selection coalescer, modifier handling, hover enforcement, and equatable sidebar changes.
  • manaflow-ai/cmux#8323: Directly overlaps visible-cell hover enforcement and cell hover-state methods.
  • manaflow-ai/cmux#8364: Directly overlaps the .plain NSTableView style change.

Important

Pre-merge checks failed

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

❌ Failed checks (2 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error Production sidebar click handling now defers commits via SidebarSelectionCoalescer using clock.sleep in a Task, which is timing-based synchronization. Replace the sleep-based coalescer with an explicit signal/actor-owned queue (or immediate commit + cancellation) so click ordering isn’t governed by wall-clock delays.
Cmux Architecture Rethink ❌ Error Plain-click selection now goes through a 100ms sleeping coalescer, adding a delayed-dispatch side channel to hide render/terminal churn instead of a single synchronous owner. Keep selection synchronous in one owner (controller/TabManager). If burst suppression is needed, use one transactional action path or test-only scheduling, not a production sleep-based coalescer.
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description explains the changes, but it does not follow the required template sections for Testing, Demo Video, Review Trigger, or Checklist. Add the missing template sections with a clear Summary, Testing/verification details, a demo video link if applicable, the review trigger block, and checklist items.
✅ Passed checks (21 passed)
Check name Status Explanation
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 PASS: all new/changed sidebar code stays on MainActor/AppKit paths; the row model only gained mutable value fields for optimistic painting, with no new background UI access.
Cmux Browser Automation Off-Main ✅ Passed PASS: The PR only changes sidebar/AppKit list files; no browser.* routing, WebKit waits, or socket-worker policy code was touched.
Cmux Expensive Synchronous Load ✅ Passed Diff only changes optimistic selection painting in SidebarWorkspaceRowCellView/Model; no agent-history load, JSON parse, transcript, or other heavy sync loader was added to an interactive path.
Cmux Cache Substitution Correctness ✅ Passed Only transient UI/render caches changed; snapshot consumers still read fresh via snapshotProvider, and optimistic model state reconciles on next configure.
Cmux No Hacky Sleeps ✅ Passed Pass: the diff only changes Swift files, and this rule is scoped to non-Swift app/runtime scripts; no added waits/timers appear in scope.
Cmux Algorithmic Complexity ✅ Passed New scans are bounded to visible rows or mirror existing ContentView selection logic; no new nested/full-collection hot-path rescans were added.
Cmux Swift Concurrency ✅ Passed The new async work is MainActor-scoped and lifecycle-managed (SidebarSelectionCoalescer/width remeasure tasks are stored and cancellable); no new background queues, Combine state, or completion-h...
Cmux Swift @Concurrent ✅ Passed All new async work is main-actor-bound debounce/sleep logic, and the heavy config load uses Task.detached; no missing or misused @concurrent.
Cmux Swift Package Boundaries ✅ Passed The new code is sidebar AppKit/SwiftUI glue (controller, views, equatable gate), not reusable domain logic that needs a package boundary.
Cmux Swiftpm Lockfiles ✅ Passed PR diff only changes two Swift source files; no Package.resolved, .gitignore, workflow, or Xcode project dependency files were modified.
Cmux Swift Logging ✅ Passed Added logging is a #if DEBUG cmuxDebugLog call in ContentView; no print/debugPrint/NSLog/Logger changes in the diff, and cmuxDebugLog is the existing debug-event path.
Cmux User-Facing Error Privacy ✅ Passed PASS: Touched user-facing copy stays generic (Rename/Color/Invalid Color), and the only new output is a #if DEBUG rowsBuild log, so no sensitive leakage.
Cmux Full Internationalization ✅ Passed PR only changes selection/hover/layout logic plus debug logs/comments; no new user-facing Swift copy or locale/catalog files changed.
Cmux Swiftui State Layout ✅ Passed Touched code adds Equatable gating, AppKit cell updates, and a coalescer; no new ObservableObject/@Published/GeometryReader/lazy-row store refs, and mutations stay in AppKit/event handlers.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: The diff only changes sidebar row cell/model selection rendering; no NSWindow/NSPanel/WindowGroup code or cmux.* window identifiers were added or changed.
Cmux Source Artifacts ✅ Passed Changed paths are hand-written Swift source and an Xcode project file; no artifact/log/cache/build/temp paths appear in the PR diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed PASS: The only new DEBUG-gated code is a cmuxDebugLog render-count line in ContentView; no new debug*/ForTesting/TestHook API or test-observability seam was added.
Cmux No Ambient Global State ✅ Passed The changed Swift code stays within existing scoped types; no new file-scope API, singleton, or ambient global state was introduced.
Title check ✅ Passed The title is concise and accurately summarizes the consolidated Lawrence Sidebar fixes.
✨ 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 lawrence-sidebar-fixes

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Sidebar/AppKitList/SidebarSelectionCoalescer.swift`:
- Around line 14-47: Make SidebarSelectionCoalescer generic over the injected
Clock type, storing lastApplied as C.Instant instead of CFTimeInterval. Replace
CACurrentMediaTime() calculations with clock.now and use the Clock duration APIs
for elapsed-time and delay computation, preserving immediate versus trailing
application behavior while allowing virtual clocks to control all timing.

In `@Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift`:
- Around line 180-182: Update the modifier capture near the click-handling logic
to read modifierFlags from NSApp.currentEvent instead of the global
NSEvent.modifierFlags state. Preserve the existing captured modifiers value used
by the trailing/coalesced apply so it reflects the exact click-time event.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1c70c751-a06c-48b4-97bc-1e553fcffbd1

📥 Commits

Reviewing files that changed from the base of the PR and between b984c9d and 2e5b02a.

📒 Files selected for processing (7)
  • Sources/ContentView.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarGroupHeaderRowView.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCellView.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCommands.swift
  • Sources/Sidebar/AppKitList/SidebarSelectionCoalescer.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift
  • cmux.xcodeproj/project.pbxproj

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

init(window: TimeInterval = 0.1, clock: any Clock<Duration> = ContinuousClock()) {
self.window = window
self.clock = clock
}

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))
// Injected-Clock sleep with cancellation wired to `cancel()`, per the
// bounded-delay policy (no raw Task.sleep in production paths).
trailingTask = Task { [weak self, clock] in
try? await clock.sleep(for: .seconds(delay))
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

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Support virtual time tests by making the coalescer generic over the injected Clock.

lastApplied is checked using CACurrentMediaTime(), but the task delay relies on the injected clock.sleep(). If a test injects a virtual clock (e.g., TestClock from Swift Async Algorithms), CACurrentMediaTime() will not advance alongside virtual time. This causes the condition now - lastApplied >= window to fall back to wall-clock time, defeating the purpose of the injected clock for deterministic testing.

Consider making the class generic over <C: Clock> so it can store lastApplied: C.Instant and fully rely on clock.now for all timing logic.

🧰 Tools
🪛 SwiftLint (0.65.0)

[Warning] 14-14: Classes should have an explicit deinit method

(required_deinit)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Sidebar/AppKitList/SidebarSelectionCoalescer.swift` around lines 14 -
47, Make SidebarSelectionCoalescer generic over the injected Clock type, storing
lastApplied as C.Instant instead of CFTimeInterval. Replace CACurrentMediaTime()
calculations with clock.now and use the Clock duration APIs for elapsed-time and
delay computation, preserving immediate versus trailing application behavior
while allowing virtual clocks to control all timing.

Comment on lines +180 to +182
// Capture modifiers at click time: a coalesced (trailing) apply
// must not re-read the keyboard ~100ms later.
let modifiers = NSEvent.modifierFlags

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Prefer NSApp.currentEvent?.modifierFlags for exact click-time state.

As per path instructions for correctness-critical UI state, ensure there is a single authoritative source of truth, such as the system-provided event state captured at click time. NSEvent.modifierFlags reads the global keyboard state, which might have changed if the user releases a modifier key very rapidly after clicking. Reading from NSApp.currentEvent ensures you evaluate the exact modifiers active when the click occurred.

💡 Proposed fix
             // Capture modifiers at click time: a coalesced (trailing) apply
             // must not re-read the keyboard ~100ms later.
-            let modifiers = NSEvent.modifierFlags
+            let modifiers = NSApp.currentEvent?.modifierFlags ?? NSEvent.modifierFlags
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Capture modifiers at click time: a coalesced (trailing) apply
// must not re-read the keyboard ~100ms later.
let modifiers = NSEvent.modifierFlags
// Capture modifiers at click time: a coalesced (trailing) apply
// must not re-read the keyboard ~100ms later.
let modifiers = NSApp.currentEvent?.modifierFlags ?? NSEvent.modifierFlags
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift` around
lines 180 - 182, Update the modifier capture near the click-handling logic to
read modifierFlags from NSApp.currentEvent instead of the global
NSEvent.modifierFlags state. Preserve the existing captured modifiers value used
by the trailing/coalesced apply so it reflects the exact click-time event.

Source: Path instructions

@greptile-apps

greptile-apps Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR consolidates five overnight dogfood fixes for the sidebar-appkit-list-experiment (flag-OFF by default): badge centering (replacing NSTextField-based rendering with the shared direct-draw SidebarRowUnreadBadgeView), selection coalescing (new SidebarSelectionCoalescer with injected-clock delay), an .equatable() boundary on VerticalTabsSidebar, hover-state enforcement via a sweep over visible cells, and a NSTableView.style change from .fullWidth to .plain for highlight width parity.

  • Selection coalescing adds a 100ms leading+trailing coalescer that captures modifier keys at click time, bypasses cmd/shift clicks, and uses an injected clock for testability; optimistic highlight now flips the full model (not just background color).
  • Equatable boundary implements a custom == on VerticalTabsSidebar that compares only stable identity references (window, update-view-model, file-explorer-state), relying on SwiftUI's tracked-dependency graph to invalidate the subtree when selection/unread data changes.
  • Hover enforcement sweeps visible cells after each apply/viewportDidChange call, correcting stranded hover-chrome via an early-out guard so scroll-path overhead stays O(visible rows) with no re-apply in steady state.

Confidence Score: 5/5

All changes are flag-gated (sidebar-appkit-list-experiment, default OFF); flag-off paths are untouched and the fixes are well-scoped to the AppKit sidebar layer.

The five fixes address real dogfood regressions with clear invariants, verified behavior, and appropriate guards (early-out on hover sweep, modifier bypass in coalescer, .equatable() only under the flag). Two of the subtle design decisions (timing-based coalescer, per-layout NSString allocation) were already surfaced and discussed in prior threads on this PR. No new correctness or isolation issues were found in this pass.

No files require special attention beyond what the prior threads already cover on SidebarSelectionCoalescer.swift and SidebarGroupHeaderRowView.swift.

Important Files Changed

Filename Overview
Sources/Sidebar/AppKitList/SidebarSelectionCoalescer.swift New class implementing 100ms leading+trailing selection coalescing with injected clock and proper cancellation; timing-based design was already discussed in a prior thread.
Sources/ContentView.swift Adds .equatable() wrapper gated by feature flag; custom == only checks 4 stable identity refs, relying on tracked dependencies to bypass the gate for selection/notification changes.
Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift Adds selectionCoalescer, enforceHoverOnVisibleCells, and the modifier-capture logic; both new methods have appropriate early-out guards and correct @MainActor isolation.
Sources/Sidebar/AppKitList/Cells/SidebarGroupHeaderRowView.swift Replaces NSTextField-based badge with shared SidebarRowUnreadBadgeView; per-layout NSString allocation in layout() was flagged in a prior thread.
Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCellView.swift Optimistic highlight/deselect now apply the full model via a mutable copy rather than partial color patches; enforcePointerHovering added with guard to avoid unnecessary re-applies.
Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowModel.swift isActive and isMultiSelected promoted to var for the mutable-copy optimistic-paint pattern; only used on local copies, authoritative stored model still set only via configure().
Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCommands.swift updateSelection() now accepts pre-captured modifier flags to avoid stale re-read inside a coalesced trailing closure; clean change.
cmux.xcodeproj/project.pbxproj Adds SidebarSelectionCoalescer.swift to both the file reference and build phase lists; no package dependency changes so no Package.resolved drift.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant NSTableView
    participant Controller as SidebarWorkspaceTableController
    participant Coalescer as SidebarSelectionCoalescer
    participant Cell as SidebarWorkspaceRowTableCellView
    participant Commands as SidebarWorkspaceRowCommands

    User->>NSTableView: mouseDown (press)
    NSTableView->>Cell: showOptimisticSelectionHighlight()
    Note over Cell: applies mutable copy with isActive=true

    User->>NSTableView: mouseUp (click)
    NSTableView->>Controller: didClickTableRow()
    Controller->>Controller: capture NSEvent.modifierFlags

    alt cmd / shift held
        Controller->>Coalescer: cancel()
        Controller->>Commands: updateSelection(modifiers:)
        Commands-->>Controller: selection committed immediately
    else plain click
        Controller->>Coalescer: request closure
        alt leading edge
            Coalescer->>Commands: updateSelection immediately
        else burst window
            Note over Coalescer: trailing Task sleeps ~100ms
            Coalescer-->>Commands: updateSelection fires once
        end
    end

    Controller->>Controller: enforceHoverOnVisibleCells()
    loop each visible row
        Controller->>Cell: enforcePointerHovering(hovering)
        alt hover state changed
            Cell->>Cell: applyModel + needsLayout
        else no change
            Note over Cell: guard short-circuits
        end
    end
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 NSTableView
    participant Controller as SidebarWorkspaceTableController
    participant Coalescer as SidebarSelectionCoalescer
    participant Cell as SidebarWorkspaceRowTableCellView
    participant Commands as SidebarWorkspaceRowCommands

    User->>NSTableView: mouseDown (press)
    NSTableView->>Cell: showOptimisticSelectionHighlight()
    Note over Cell: applies mutable copy with isActive=true

    User->>NSTableView: mouseUp (click)
    NSTableView->>Controller: didClickTableRow()
    Controller->>Controller: capture NSEvent.modifierFlags

    alt cmd / shift held
        Controller->>Coalescer: cancel()
        Controller->>Commands: updateSelection(modifiers:)
        Commands-->>Controller: selection committed immediately
    else plain click
        Controller->>Coalescer: request closure
        alt leading edge
            Coalescer->>Commands: updateSelection immediately
        else burst window
            Note over Coalescer: trailing Task sleeps ~100ms
            Coalescer-->>Commands: updateSelection fires once
        end
    end

    Controller->>Controller: enforceHoverOnVisibleCells()
    loop each visible row
        Controller->>Cell: enforcePointerHovering(hovering)
        alt hover state changed
            Cell->>Cell: applyModel + needsLayout
        else no change
            Note over Cell: guard short-circuits
        end
    end
Loading

Reviews (2): Last reviewed commit: "Apply the full selection treatment optim..." | Re-trigger Greptile

Comment on lines +38 to +46
trailingTask = Task { [weak self, clock] in
try? await clock.sleep(for: .seconds(delay))
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 Timing-based debounce in production path

clock.sleep is a timing-based delay in a @MainActor production path. The cmux no-sleep and architectural-rethink rules flag timing repairs, even when the clock is injected and cancellation is wired. Concretely: if cancel() is called between the sleep completing and the task resuming on the main actor (another event queued at that suspension point), Task.isCancelled will be true and the guard bails — but the window between sleep completion and the guard check is an observable staleness window where the abandoned pendingApply still holds a captured closure.

The injected-clock and bounded-delay design mitigate this for testing, and the 100 ms cap is reasonable. But worth explicitly verifying that this coalescer's existence is not a symptom of an upstream cost that could be reduced further (e.g., making the selection-commit path cheaper so no debounce is needed at all, or moving the coalescing to the SwiftUI state layer where it can be expressed without timing).

Rule Used: Flag new blocking or timing-based synchronization ... (source)

Comment on lines +267 to +268
let textSize = NSString(string: "\(model.anchorUnreadCount)")
.size(withAttributes: [.font: unreadBadgeFont])

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 Per-layout NSString allocation on a sidebar row path

NSString(string: "\(model.anchorUnreadCount)") bridges a Swift string through NSString on every layout() pass. layout() runs during every resize, scroll bounce, and content-driven height change. Because this is gated on !unreadBadgeView.isHidden, the allocation only occurs when the badge is visible — but that is exactly the case when sidebar churn (notifications, unread count ticks) is most frequent.

The previous unreadBadgeLabel.intrinsicContentSize call had a similar per-call cost. A simple improvement: cache the measured badgeSize alongside unreadBadgeFont and invalidate it only when the font or count changes, avoiding the allocation on re-layout passes where neither changed.

Rule Used: Flag per-call allocating formatting on hot or conc... (source)

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!

…Sidebar)

Dogfood video showed a two-phase selection: the press painted only the
background and title instantly, while selection-derived colors on the
notification preview, secondary text, and badges trailed in with the
terminal swap. The optimistic press/deselect paths now apply a
selection-flipped copy of the row model through the normal applyModel
pass, so every selection-derived color flips with the highlight. The
stored model stays authoritative and the next configure reconciles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@azooz2003-bit
azooz2003-bit merged commit 74409a0 into main Jul 17, 2026
6 checks passed
azooz2003-bit added a commit that referenced this pull request Jul 23, 2026
* Sidebar: move workspace reorder drops to native NSTableView validateDrop/acceptDrop

The AppKit sidebar table previously received workspace reorder drops
through a transparent SidebarWorkspaceReorderDropView overlay stacked
above the scroll view, with a geometry gate feeding it visible-row
frames and a pending-drop state machine covering the async gap between
drop arrival and target collection. The table now registers for the
reorder pasteboard type itself and resolves drops in the data source's
validateDrop/acceptDrop, building visible-row targets synchronously in
table coordinates and passing them to the same shared
SidebarWorkspaceReorderDropResolver. draggingDestinationFeedbackStyle
is .none, so the row-painted indicator bars and the container's
empty-indicator overlay keep owning all drop visuals unchanged.

Behavior improvement: the controller stores the accepted drag point in
window space and re-plans it on every viewport change, so during edge
autoscroll the drop target and indicator track the rows sliding under a
stationary pointer; previously the indicator was only geometrically
repositioned and the plan froze until the pointer moved. Drag exit and
session end (including Escape cancel over the table) now clear the
indicator through explicit draggingExited/draggingEnded overrides.

The geometry gate is now bonsplit-only, the reorder overlay and its
pending-drop machinery are no longer instantiated by the AppKit
container (the legacy SwiftUI sidebar still uses that overlay class,
unchanged), and the reorder side of SidebarWorkspaceTableActions drops
its target-collection closure. Also fixes a stale .fullWidth style
assertion in SidebarWorkspaceTableTests that predates the .plain
switch in #8366 and slipped through while PR CI was disabled.

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

* Fix main build: drop convenience from actor initializer

#8521 added public convenience
init() to the GitHubPullRequestRequestCoordinator actor. Actor
initializers delegate without convenience (SE-0327) and current Swift
toolchains reject the keyword as a hard error, so every fleet
reload-cloud build and CI compile of main fails with 'initializers in
actors are not marked with convenience'. It landed silently because PR
CI is currently disabled. Dropping the keyword keeps the same public
surface and delegation.

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

* Sidebar: controller-owned reorder indicator painting

Dogfood video on a 100-row sidebar showed three drag defects: the drop
line lagged the pointer by hundreds of pixels, transitions briefly drew
two lines at once, and a line was baked into the drag ghost for the
whole drag.

Root cause for the first two: the reorder plan was written into the
SwiftUI drag state, so every gap change rebuilt every sidebar row and
repainted the line one staged apply later. The table's actions now
return the plan to the controller (SidebarWorkspaceTableReorderDropUpdate)
and never touch dragState; the controller maps the indicator onto the
two affected cells with SidebarWorkspaceTableReorderIndicatorPainter,
which wraps the same SidebarTabDropIndicatorPredicate and scope-filtered
row ids the SwiftUI sidebar uses, so gap semantics are unchanged while
transitions become two direct view mutations.

Root cause for the ghost: AppKit snapshots the dragged row lazily, after
the first validateDrop painted the indicator at the grabbed row's own
edge. The painter suppresses both lines on the dragged row, so the
snapshot is always clean.

Recycled or reconfigured cells re-apply the controller paint after their
model reset it, retire paths (drop, exit, session end) sweep the lines
clear, and the apply-time indicator sync prefers the live reorder
painter so dragState (bonsplit-only now) cannot clear the past-the-end
overlay mid-drag.

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

* Fix tuple labels lost through nil-coalescing in indicator sweep

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

* Sidebar drag: stop bottom-edge autoscroll oscillation, re-arm cleared drags

Second dogfood round on the 100-row sidebar surfaced two failures.

Autoscroll parked at the very bottom stuttered continuously: the
60Hz timer's native NSClipView.autoscroll(with:) path has no content
clamp, so each tick overshot into the elastic region and rubber-banded
back. The tick now computes the plan first and stops the timer when
constrainBoundsRect proves the clip view cannot advance in the planned
direction; the next drag update restarts it. The manual fallback
already clamped.

A long drag went indicator-less and its drop silently no-oped: the app
briefly resigned active mid-drag (busy multi-agent desktop), the
app_resign_active failsafe cleared dragState AND the process-wide drag
registry, and activateSidebarWorkspaceDragIfNeeded could only re-arm
foreign drags, so every later validateDrop was rejected while the
native session stayed alive. The drag pasteboard id (parsed once per
validateDrop and handed through the table actions) now re-arms local
drags too: a still-delivering drop callback is proof the session is
real. Escape-cancel cannot resurrect because AppKit ends the session
and no further callbacks arrive.

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

* Sidebar drag: robust payload parse for re-arm plus activation probe

string(forType:) can be nil for item-provider-promised drag data; fall
back to a UTF-8 decode of data(forType:). DEBUG logs record the parsed
payload and activation rejections so silent no-op drops are attributable
from the tagged log.

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

* Sidebar drag: single autoscroll driver, inset-aware clamp

Making the table a native drag destination silently added AppKit's
built-in drag autoscroll on top of cmux's SidebarDragAutoScrollController.
Two drivers with different engagement bands, speeds, and boundary
behavior fought during drags: scrolling continued after the pointer
left the cmux edge zone (AppKit's band is much taller than 44pt), and
edge behavior was erratic. The table now declines AppKit autoscroll
(autoscroll(with:) -> false); the cmux controller is the only driver.

The controller is also planner-only now. Its autoscroll(with:) fast
path read NSApp.currentEvent inside a timer tick — a stale position
basis — and had no content clamp. The manual path's [0, contentHeight]
clamp ignored the scroll view's content insets, which is why upward
autoscroll stopped one top-inset short of the actual top. Ticks now
scroll by the planner's ramped step and clamp through
constrainBoundsRect, which honors insets and cannot enter the elastic
region.

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

* Sidebar drag: scope AppKit-autoscroll refusal to active drop sessions

The unconditional autoscroll(with:) -> false override killed row-drag
initiation: NSTableView's mouseDown tracking loop also calls autoscroll
while deciding whether a press becomes a drag, and refusing it there
made every drag die at birth (pasteboardWriterForRow ran, then the
mouse-up landed as a plain click; log shows dragState.sidebar set
followed by sidebar.table.click with no session). The table now
declines AppKit's built-in drag autoscroll only while the controller
reports a live reorder drop session hovering, which is the only window
where the second scroll driver exists.

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

* Failing test: autoscroll planner measures edge distance in document coordinates

Pointer positions converted into the clip view are document coordinates
whose origin is the scroll offset. The planner treated them as
viewport-relative, so once the list is scrolled more than one viewport
height deep, every pointer position measures as past the bottom edge
and plans max-speed downward scrolling from anywhere: the runaway
scroll-down and the up-scroll stutter (down-plan fighting upward input)
from dogfood. Test-only commit plus the visibility change it needs;
the fix lands in the next commit so CI shows red then green.

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

* Fix autoscroll planner coordinate space: viewport-relative edge distances

Subtract the clip view's bounds origin before measuring distance to the
viewport edges. Scroll-down now stops the moment the pointer leaves the
44pt bottom zone regardless of scroll depth, and upward edge scrolling
no longer fights a phantom max-speed downward plan.

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

* Sidebar drag: drop commits the painted plan, not a release-time re-resolve

Dogfood on rapid consecutive drags: the row occasionally landed one gap
away from the last indicator shown. Two races made the drop diverge
from the paint: the pointer can drift a few pixels between the final
draggingUpdated and the release (AppKit sends no update for those), and
an autoscroll tick can shift rows after the last coalesced repaint but
before acceptDrop. Both re-resolved a fresh plan at release time that
could cross a row-midpoint boundary the indicator never showed.

The accepted update now carries the resolver's full drop plan; the
controller stores it alongside the painted indicator and acceptDrop
commits that stored plan verbatim through a new commitWorkspaceDropPlan
action (falling back to point resolution only when no hover plan
exists). Indicator and outcome are now the same object by construction.

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

* Sidebar drag: keep the viewport at the release position after a drop

Half of drops visibly yanked the sidebar away from the drop spot,
often toward the dragged row's old position. Cause: the apply after a
drop shifts the selected workspace's index, which
SidebarSelectedWorkspaceScrollPolicy cannot distinguish from an
external change, so scrollSelectedRowToVisibleIfNeeded jumped the
viewport to the selected row whenever it was offscreen. It only looked
correct when the selected row's index happened not to change or was
already visible. A successful drop now arms a one-shot suppression
consumed by the next apply; external selection changes still scroll.

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

* Re-mint painter pbxproj UUIDs after collision with main

The hand-minted B804x02A build-file/file-reference pair for
SidebarWorkspaceTableReorderIndicatorPainter.swift was independently
allocated on main for SidebarWorkspaceRowTodoViews.swift. After merging
main, both files claimed the same UUIDs; Xcode keeps one definition and
silently drops the other file from the build, which broke compilation
with 'cannot find SidebarRowClosureMenuItem in scope'. The painter now
uses a unique pair outside the sequential B804 series.

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

---------

Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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