Skip to content

Flag-gated AppKit sidebar list (NSTableView rows + headers) - #8270

Merged
azooz2003-bit merged 14 commits into
mainfrom
feat-sidebar-appkit
Jul 17, 2026
Merged

azooz2003-bit merged 14 commits into
mainfrom
feat-sidebar-appkit

Conversation

@azooz2003-bit

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

Copy link
Copy Markdown
Collaborator

Rewrites the workspace sidebar list in AppKit behind the feature flag sidebar-appkit-list-experiment (default OFF; PostHog-overridable, local override via Internal Flags).

Flag OFF (default): the existing SwiftUI LazyVStack sidebar and DragGesture resizer, unchanged.

Flag ON: pure-AppKit NSTableView list. Virtualized rows with a measured-once height cache (fixes the scrollbar stutter from LazyVStack height re-estimation and the seconds-long tap latency after flinging a 100-workspace sidebar). Native cells render every row slot without NSHostingView: title wrap + inline rename, unread badges, GPU spinner, media glyphs, close button, description/notification preview, remote SSH line + reconnect, metadata entries + markdown blocks, log line, progress bar, three branch/directory layouts with width-adaptive candidates, custom-vector PR icons, ports, checklist section (respecting the popover setting), shortcut hint pills, drop indicators. Context menus are NSMenu with item-for-item SwiftUI parity (groups, status lanes, colors with swatches, move-to-window, notifications submenu). Hover/middle-click/drag are table-owned; rows rebuild from the container-observed SidebarWorkspaceRowInput projections so workspace churn repaints single cells without container re-renders.

Flag ON also replaces the sidebar resizer's SwiftUI DragGesture with a native tracking view (SidebarDividerTrackingView) using NSSplitView's technique: a synchronous nextEvent(matching:) loop in .eventTracking mode that applies the width, spins the runloop once so SwiftUI/CA commit, and presents via displayIfNeeded inside the same mouse event. It accepts first mouse and owns its cursor rect. Selection paints optimistically on mouse-down and reconciles from the authoritative configure pass.

Verification (all on tag sbak, LG HDR 4K):

  • Synthetic HID divider drag (+80px one-way): sidebar hairline moved exactly 239.5pt -> 319.5pt, app responsive after, cursor released.
  • Fling storm (25x400px wheel) + click, 3 trials per flag state, measured click epoch -> sidebar.select debug-log timestamp: flag ON 30/32/38 ms (source=appKitRow), flag OFF 25/27/27 ms. Click latency is at parity; the AppKit win is scroll/render cost (no LazyVStack height re-estimation livelock), not click dispatch.
  • scripts/check-sidebar-lazy-layout.py and SidebarLazyLayoutScaleTests remain main's originals and pass (legacy path untouched).
  • Localization: en/ja entries for the two new flag strings in Resources/Localizable.xcstrings; no other user-visible strings added (AppKit cells reuse existing localized keys, verified item-for-item against the SwiftUI menus).

Branch is merged with current main (post-#8211/#8240 row-projection and resize-coalescing refactors adopted).

🤖 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

Adds a feature-flagged AppKit sidebar list that replaces the SwiftUI list with an NSTableView for smoother scrolling and lower tap latency with many workspaces. With the flag on, divider drags use a native tracker that sticks to the cursor and now coalesce portal updates to cut drag cost; flag-off behavior is unchanged. Off by default; enable sidebar-appkit-list-experiment in Internal Flags.

  • New Features

    • New NSTableView sidebar behind sidebar-appkit-list-experiment (off by default). Virtualized rows with measured-once height caching replace LazyVStack re-measure churn.
    • Native AppKit cells for workspace rows and group headers with full UI parity and NSMenu; table-owned hover/middle-click/drag. Selection via table target/action; double-click to inline rename.
    • Integrated with the projected row API (SidebarWorkspaceRowInput): container-observed projections replace the prior per-row Combine pump and snapshot memo; AppKit row actions renamed to avoid type conflicts.
    • Divider resize (flag ON): native tracking view flushes layout/display inside each event; portal fan-out is coalesced to one deferred pass per tick, and rows freeze during drags to avoid rebuilds. Flushes are paced to the screen’s refresh; failsafe clears a stuck resize cursor. Selection paints optimistically on mouse-down.
    • Localized flag strings (en/ja); Settings title is “Lawrence Sidebar.”
  • Bug Fixes

    • Reduced divider-drag overhead: anchor callbacks sync only their hosted view; interactive-resize session ends from the cursor failsafe; drag layout logging avoids pasteboard XPC unless an app-owned drag is active.
    • Interaction/UI polish: group headers are excluded from middle-click close and row drags; context menus no longer disappear without a notification store (only that section hides); closed-PR icon tint renders correctly; port controls wrap at the trailing edge; O(1) group lookup in row building; bulk mark read/unread respects per-workspace filters.

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

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added an optional AppKit-based sidebar list experience behind a feature flag, including enhanced workspace/group rows (inline rename, rich status/details, checklists, shortcuts, and improved context menus).
    • Added English and Japanese localized labels for the new sidebar option.
  • Bug Fixes

    • Improved divider resizing reliability with detection of canceled drag events to prevent stuck resize behavior.
    • Stabilized sidebar workspace scrolling, hover/selection behavior, row height rendering, and drag-and-drop target handling.

cmux reload-cloud added 3 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.
@coderabbitai

coderabbitai Bot commented Jul 16, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7b5d8905-fc2b-4d90-b52f-d05a8475e194

📥 Commits

Reviewing files that changed from the base of the PR and between 1896ebe and fab9097.

📒 Files selected for processing (12)
  • Resources/Localizable.xcstrings
  • Sources/ContentView.swift
  • Sources/FeatureFlags.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCellView.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCommands.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSlotViews.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableRowConfiguration.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableViewImpl.swift
  • Sources/Sidebar/SidebarDividerTrackingView.swift
  • Sources/TerminalWindowPortal.swift

📝 Walkthrough

Walkthrough

Adds a feature-flagged AppKit NSTableView sidebar with reusable group and workspace rows, manual layout, cached heights, drag/drop geometry, context menus, SwiftUI bridging, debug probes, localization, and focused tests.

Changes

AppKit sidebar workspace list

Layer / File(s) Summary
Feature flag and sidebar integration
Sources/FeatureFlags.swift, Sources/ContentView.swift, Sources/Sidebar/SidebarDividerTrackingView.swift, Sources/TerminalWindowPortal.swift, Resources/Localizable.xcstrings
Adds the AppKit sidebar feature flag, localized metadata, AppKit divider tracking, drag cancellation cleanup, environment propagation, feature-flagged routing, and deferred visible-terminal reconciliation.
AppKit row models and renderers
Sources/Sidebar/AppKitList/Cells/*, Sources/SidebarWorkspaceGroupHeaderView.swift, Sources/VerticalTabsSidebar+WorkspaceGroups.swift
Adds pure-AppKit group and workspace row models, manually laid-out cells, status/checklist/link views, inline rename handling, row commands, context menus, and group-header wiring.
Reusable table lifecycle and layout
Sources/Sidebar/AppKitList/SidebarWorkspaceTable*.swift
Adds the SwiftUI/AppKit cell bridge, table controller, row configuration, hover tracking, reusable cells, deterministic height estimation, environment snapshots, and exact height caching.
Drag/drop orchestration and project wiring
Sources/Sidebar/AppKitList/SidebarWorkspaceTableActions.swift, Sources/Sidebar/AppKitList/SidebarWorkspaceTableDropTargetGeometryGate.swift, cmux.xcodeproj/project.pbxproj
Connects workspace lifecycle, selection, Bonsplit, reorder, drop indicators, and new sidebar sources into application and test targets.
Contract and behavior validation
Sources/Debug/SidebarLazyContractProbe.swift, cmuxTests/SidebarWorkspaceTableTests.swift
Extends debug reconfiguration probing and tests table layout, height caching, cell reuse, hover resolution, and drag-target lifecycle.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

  • manaflow-ai/cmux#8224 — Tracks the AppKit NSTableView virtualization and sidebar replacement implemented here.
  • manaflow-ai/cmux-dev-artifacts#4207 — Concerns the AppKit sidebar table and lazy-layout mechanisms added here.
  • manaflow-ai/cmux-dev-artifacts#4212 — Directly concerns hosted cell roots and SidebarLazyContractProbe behavior.
  • manaflow-ai/cmux-dev-artifacts#4209 — Covers the new SidebarWorkspaceTableTests suite.

Possibly related PRs


Important

Pre-merge checks failed

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

❌ Failed checks (8 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Swift Actor Isolation ❌ Error scheduleWidthRemeasure() starts a plain Task, sleeps, then touches @MainActor controller/cache state off-actor; that’s a new UI-bound background access and a Swift 6 isolation bug. Move the delayed remeasure onto MainActor (or better, trigger it from divider-tracking end) and remove Task.sleep; keep all width/cache mutations on the controller’s main-actor path.
Cmux Swift Blocking Runtime ❌ Error New production code adds Task.sleep(120ms) remeasure delay and a blocking nextEvent(...until:.distantFuture) divider loop on the main actor. Replace timing-based retries with explicit resize-completion signals, and avoid blocking event loops on the UI thread; use nonblocking callbacks/actor-owned state.
Cmux Algorithmic Complexity ❌ Error ContentView:10785 and SidebarWorkspaceRowCommands:234/247 introduce nested scans on hot sidebar paths with no benchmark or bound. Replace the group lookup with workspaceGroupById[groupId] and prebuild a Set(contextMenuWorkspaceIds) before filtering tabs; both are O(n) fixes for ~1000-workspace paths.
Cmux Swift Concurrency ❌ Error Diff adds Task.sleep-delayed remeasure in SidebarWorkspaceTableController and DispatchQueue.main.async deferrals in SidebarWorkspaceRowCommands. Replace the delayed Task with explicit resize-completion invalidation from the divider/end callback, and remove the deferred NSAlert first-responder dispatches.
Cmux Full Internationalization ❌ Error The new featureFlags.appKitSidebarList xcstrings entries only include en and ja, but the catalog supports 20 locales. Add translated values for ar, bs, da, de, es, fr, it, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, and zh-Hant to both new entries in Resources/Localizable.xcstrings.
Cmux Swiftui State Layout ❌ Error VerticalTabsSidebar adds @State appKitRowSnapshotCache and prunes it inside appKitWorkspaceScrollArea, a body helper, causing render-time state mutation. Move cache pruning out of body construction—e.g. update/prune the cache in explicit lifecycle or row-apply callbacks, not while building the view.
Cmux Architecture Rethink ❌ Error SidebarWorkspaceTableController.scheduleWidthRemeasure uses Task.sleep(120ms), a timing repair path that defers row-height reconciliation and leaves stale height state representable. Move remeasurement to one resize-completion callback owned by the divider tracker, and remove the delayed task/polling path.
Cmux No Test Or Debug Seam In Production Source ❌ Error Production Sources add DEBUG-only probes/accessors (reconfigurationProbe, computationProbe, hostingViewIdentity) and wire sidebarLazyContractProbe through the table bridge. Move observation into Tests/ via @testable import, or isolate any real debug-only facility in a dedicated debug-only file/folder.
Description check ⚠️ Warning The description covers summary and testing, but it omits required Demo Video, Review Trigger, and Checklist sections. Add the missing Demo Video URL, the review-trigger block, and the checklist items, and ensure all template sections are present.
Docstring Coverage ⚠️ Warning Docstring coverage is 13.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (15 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: a flag-gated AppKit sidebar list with NSTableView rows and headers.
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 Browser Automation Off-Main ✅ Passed PR only changes sidebar/AppKit list files; no browser automation routing, worker-policy, or WebKit/cookie-wait code was modified.
Cmux Expensive Synchronous Load ✅ Passed PASS: The touched files add AppKit sidebar/table code and a resize timer, but no new RestorableAgentSessionIndex.load() or agent-history JSON/JSONL loads on interactive paths.
Cmux Cache Substitution Correctness ✅ Passed The new caches are UI-only and cold-safe: row snapshots fall back to a fresh rebuild and are refreshed via event-driven observers; row heights also fallback when missing.
Cmux No Hacky Sleeps ✅ Passed Diff only touches Swift sources and Xcode project metadata; no TS/JS/shell/build runtime scripts or fixed waits were added, so the rule isn’t implicated.
Cmux Swift @Concurrent ✅ Passed No new @concurrent/nonisolated async misuse found; the new async work is @MainActor UI-bound remeasure and an explicit Task.detached file-load hop.
Cmux Swift Package Boundaries ✅ Passed The diff is feature-flagged AppKit/UI glue (views, controller, layout/cache, menus); I found no reusable domain logic that should be extracted to a SwiftPM package.
Cmux Swiftpm Lockfiles ✅ Passed PASS: The PR only changes source-file refs in project.pbxproj; no Package.resolved or .gitignore files changed, and no SwiftPM package references were added.
Cmux Swift Logging ✅ Passed The changed Swift files add no print/debugPrint/dump/NSLog/os_log calls and no file-scoped Logger changes.
Cmux User-Facing Error Privacy ✅ Passed New alerts/menu labels are generic; no added user-facing text exposes provider/internal names or other forbidden details.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The commit adds a divider tracking NSView and resizer logic only; no NSWindow/NSPanel/WindowGroup changes or cmuxAuxiliaryWindowIdentifiers updates appear.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/config/localization/test files; no artifacts, logs, caches, build output, or scratch dirs were added.
Cmux No Ambient Global State ✅ Passed PASS: the new AppKit sidebar code uses injectable instance types; I found no new file-scope funcs/vars or singleton/runtime globals in the touched production Swift files.
✨ 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-sidebar-appkit

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.

cmux reload-cloud and others added 8 commits July 16, 2026 13:30
…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>
@azooz2003-bit
azooz2003-bit marked this pull request as ready for review July 17, 2026 02:27
@greptile-apps

greptile-apps Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR rewrites the workspace sidebar list behind the sidebar-appkit-list-experiment feature flag (off by default). When the flag is enabled, an NSTableView with a measured-once height cache replaces the SwiftUI LazyVStack, and a synchronous AppKit event-tracking loop replaces the SwiftUI DragGesture for the divider resizer. The flag-off path is byte-for-byte identical to the current code.

  • SidebarWorkspaceTableController owns the full NSTableView lifecycle: virtualized row height caching, hover/middle-click/drag delegation, and a container-observed apply(rows:...) path that limits AppKit mutations to changed cells.
  • SidebarDividerTrackingView runs a synchronous nextEvent(matching:) loop (NSSplitView technique) with an explicit 1 ms RunLoop pass to commit SwiftUI/CA layout inside each mouse event, keeping the divider glued to the cursor without async jitter.
  • SidebarWorkspaceRowCellView renders every workspace row slot natively (title, badges, branch/PR lines, checklist, etc.) with a per-cell Combine pump for sub-apply workspace change repaints.

Confidence Score: 4/5

Flag-off path is byte-for-byte identical to main; the AppKit path is safe to land behind the disabled-by-default flag while two open concerns from prior review passes are addressed.

The implementation is well-structured and the feature flag gate fully protects the default user experience. Two concerns surfaced in earlier review passes remain open in the code: the Task.sleep debounce in scheduleWidthRemeasure uses a fixed wall-clock wait for layout-width settlement rather than a real signal, and the #if DEBUG test-observability seams in production Sources/ files have no production callers and should be reached via @testable import instead. Neither is a flag-off regression, but both remain unaddressed.

Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift — Task.sleep debounce and #if DEBUG production seams; Sources/Sidebar/AppKitList/SidebarWorkspaceTableCellView.swift — additional #if DEBUG identity-exposure seams.

Important Files Changed

Filename Overview
Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift Core table controller managing the NSTableView lifecycle; contains the Task.sleep debounce and #if DEBUG test seams flagged in prior review threads, plus a minor pump-reinstall on hover reconfigures.
Sources/Sidebar/SidebarDividerTrackingView.swift New NSView running the NSSplitView-style synchronous event-tracking loop; the 1 ms RunLoop pass is intentional for CA commit, and the #if DEBUG diagnostic block gates real debug behavior rather than exposing test state.
Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCellView.swift Pure-AppKit workspace row cell with full slot parity; Combine pump correctly scoped to visible cells, optimistic selection/deselection logic is well-isolated.
Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCommands.swift Action surface and NSMenu builder; uses String(localized:defaultValue:) throughout and mirrors TabItemView dispatch logic cleanly.
Sources/FeatureFlags.swift Adds the sidebar-appkit-list-experiment flag at the correct array index (6); follows the established indexed-accessor pattern used by all existing flags.
Sources/ContentView.swift Refactors sidebarResizerHandleOverlay into a flag-gated base+overlay split; adds a 16 ms timer failsafe to recover stranded isResizerDragging for the SwiftUI path; both branches apply the accessibility modifier correctly.
Sources/Sidebar/AppKitList/SidebarWorkspaceTableRowHeightCache.swift Measured-once height cache with content-equivalence keying; correctly skips NSHostingView measurement for pure-AppKit rows and returns IndexSet of changed rows to avoid unnecessary noteHeightOfRows calls.
Sources/Sidebar/AppKitList/SidebarWorkspaceTableCellView.swift SwiftUI-hosting cell wrapping the legacy row path; contains #if DEBUG test-observability seams (hostingViewIdentity, hostedRootIdentity) in production Sources/ that follow the same pattern already flagged in SidebarWorkspaceTableController.
Resources/Localizable.xcstrings Adds en/ja entries for the two new feature-flag strings; consistent with the existing pattern where all feature-flag strings carry only en/ja translations.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[VerticalTabsSidebar.workspaceScrollArea] --> B{isAppKitSidebarListEnabled?}
    B -- "OFF (default)" --> C[legacyWorkspaceScrollArea\nSwiftUI LazyVStack + ScrollView]
    B -- "ON" --> D[appKitWorkspaceScrollArea\nSidebarWorkspaceTableContainerView]
    D --> E[SidebarWorkspaceTableController\nNSTableView delegate + datasource]
    E --> F[apply rows]
    F --> G[SidebarWorkspaceTableRowHeightCache\nmeasured-once heights]
    F --> H{Row type}
    H --> I[SidebarWorkspaceRowTableCellView\npure-AppKit workspace row]
    H --> J[SidebarGroupHeaderTableCellView\npure-AppKit group header]
    H --> K[SidebarWorkspaceTableCellView\nNSHostingView legacy row]
    I --> L[installPump\nCombine per-cell observer]
    A2[ContentView.sidebarResizerHandleOverlay] --> B2{isAppKitSidebarListEnabled?}
    B2 -- "OFF" --> C2[SwiftUI DragGesture]
    B2 -- "ON" --> D2[SidebarDividerTracker\nnextEvent synchronous loop]
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"}}}%%
flowchart TD
    A[VerticalTabsSidebar.workspaceScrollArea] --> B{isAppKitSidebarListEnabled?}
    B -- "OFF (default)" --> C[legacyWorkspaceScrollArea\nSwiftUI LazyVStack + ScrollView]
    B -- "ON" --> D[appKitWorkspaceScrollArea\nSidebarWorkspaceTableContainerView]
    D --> E[SidebarWorkspaceTableController\nNSTableView delegate + datasource]
    E --> F[apply rows]
    F --> G[SidebarWorkspaceTableRowHeightCache\nmeasured-once heights]
    F --> H{Row type}
    H --> I[SidebarWorkspaceRowTableCellView\npure-AppKit workspace row]
    H --> J[SidebarGroupHeaderTableCellView\npure-AppKit group header]
    H --> K[SidebarWorkspaceTableCellView\nNSHostingView legacy row]
    I --> L[installPump\nCombine per-cell observer]
    A2[ContentView.sidebarResizerHandleOverlay] --> B2{isAppKitSidebarListEnabled?}
    B2 -- "OFF" --> C2[SwiftUI DragGesture]
    B2 -- "ON" --> D2[SidebarDividerTracker\nnextEvent synchronous loop]
Loading

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

Comment on lines +355 to +370
private func scheduleWidthRemeasure() {
widthRemeasureTask?.cancel()
widthRemeasureTask = Task { [weak self] in
try? await Task.sleep(nanoseconds: 120_000_000)
guard let self, !Task.isCancelled else { return }
self.widthRemeasureTask = nil
let width = self.currentColumnWidth()
guard width > 0 else { return }
let changed = self.rowHeightCache.prepareHostedRows(self.rows, columnWidth: width)
self.lastMeasuredWidth = width
self.pumpHeightOverrides.removeAll(keepingCapacity: true)
if !changed.isEmpty {
self.containerView?.tableView.noteHeightOfRows(withIndexesChanged: changed)
}
}
}

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.

P1 Task.sleep used as a debounce timer in the render path

scheduleWidthRemeasure uses Task.sleep(nanoseconds: 120_000_000) as a 120ms wall-clock debounce before re-measuring row heights after a sidebar resize. The cmux blocking-runtime rule explicitly flags Task.sleep in production Swift as a timing-based synchronization primitive. While the task is cancellation-aware (try? + .isCancelled guard), the 120ms delay is a fixed wall-clock wait — if the resize settle signal fires sooner or later the height update is still wrong for those frames. A DispatchSourceTimer on the main queue (cancel + reschedule on each resize event) or observing the NSScrollView delegate's scrollViewDidEndLiveScroll / column-width-settled notification would provide a real signal without a sleep.

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

Comment on lines +24 to +30
#if DEBUG
var reconfigurationProbe: (() -> Void)?
var dropTargetComputationProbe: (() -> Void)? {
get { dropTargetGeometry.computationProbe }
set { dropTargetGeometry.computationProbe = newValue }
}
#endif

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 New #if DEBUG test-observability seams in production source

reconfigurationProbe and dropTargetComputationProbe are #if DEBUG-guarded vars with no production caller — they exist solely so SidebarLazyLayoutScaleTests can observe cell reconfiguration events. Per the no-test-debug-seam-in-production-source rule these should not live in a production Sources/ file. The same contract check can be satisfied by widening reconfigurationProbe to internal and reading it from the test target via @testable import, which is the canonical fix pattern (PR #6452). The dropTargetComputationProbe chain similarly belongs in test infrastructure, not the shipped controller.

Rule Used: Do not add new test/debug seams (ForTesting-styl... (source)

Comment on lines +166 to +169
DispatchQueue.main.async {
alertWindow.makeFirstResponder(input)
input.selectText(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 Legacy DispatchQueue.main.async inside @MainActor-isolated function

promptRename (and promptCustomColor at line 204) use DispatchQueue.main.async for first-responder setup inside an @MainActor-isolated struct — a legacy Dispatch pattern. Note: because alert.runModal() spins a nested AppKit event loop, a bare Task { @MainActor in ... } might not process until after the modal closes. The safer fix is RunLoop.main.perform { ... } (fires inside any active run loop including the modal's) or confirming that initialFirstResponder alone is sufficient and dropping the deferred dispatch.

Rule Used: Flag new legacy async patterns in cmux-owned Swift... (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!

@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: 20

🤖 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/ContentView.swift`:
- Line 12999: Remove the redundant second call that ends the same signpost in
the surrounding profiling flow: keep the new defer-based
SidebarProfilingSignposts.end(signpost) and delete the later explicit end at the
corresponding cleanup point.
- Around line 1414-1425: The cancellation branch in the resizer gesture
lifecycle must use the same teardown path as normal completion. Remove the 16 ms
mouse-state polling and its direct state cleanup, add an explicit cancellation
callback, and route it through the existing resize-session end logic so
TerminalWindowPortalRegistry.endInteractiveGeometryResize(owner:) is invoked and
final geometry synchronization occurs.
- Around line 10782-10802: Update the .groupHeader branch in the tableRows
construction to retrieve the group from the existing
renderContext.workspaceGroupById index using groupId, instead of calling
renderContext.workspaceGroups.first(where:). Preserve the existing nil handling
and sidebarWorkspaceGroupTableConfiguration arguments.

In `@Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCellView.swift`:
- Around line 91-99: Update showOptimisticSelectionHighlight() to synchronously
update the authoritative selection model instead of directly painting
speculative background and text colors. Then invoke the normal model-driven
rendering/configuration path so selection UI has a single source of truth and
automatically preserves rollback behavior when selection fails.
- Around line 844-855: Update the visible port layout logic around visiblePorts,
portX, and lineHeight to keep each port within the available leading-to-trailing
width by wrapping controls onto additional lines when needed. Advance y and
include every wrapped line’s height and spacing in the returned row height,
while preserving frame placement for visible controls.
- Around line 420-426: Update the pluralized tooltip handling near agentCount to
use the explicit sidebar.agentActivity.tooltip.other localization key instead of
sidebar.agentActivity.tooltip.many, and rename the corresponding entry in the
string catalog for every supported locale while preserving the existing .one key
and formatting behavior.

In `@Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCommands.swift`:
- Around line 164-169: Remove the DispatchQueue.main.async focus-repair block in
the alert setup within SidebarWorkspaceRowCommands, including the
makeFirstResponder and selectText calls. Keep alertWindow.initialFirstResponder
= input as the sole initial focus configuration, and apply the same cleanup to
the corresponding second alert setup.
- Around line 253-259: Update the workspace-move logic in the
orderedWorkspaceIds handling so the final workspace is excluded from the
non-focused loop and moved exactly once with focus enabled. Preserve the
existing behavior for preceding workspaces and avoid invoking
app.moveWorkspaceToWindow twice for the finalWorkspaceId.
- Around line 295-315: The menu-building flow should not return early when
notificationStore is unavailable. Update the guard around tabManager and
notificationStore to require only tabManager, then conditionally call
addNotificationItems and its separator when notificationStore exists, while
preserving all unrelated actions such as rename, move, close, and copy.
- Around line 697-721: Filter the bulk mutations in addNotificationItems so each
selected workspace is changed only when its current notification state requires
it: mark read only unread workspaces and mark unread only read workspaces. Reuse
the notificationStore state/query APIs, preserving the existing menu enablement
and iteration behavior while preventing redundant markUnread calls from setting
a manual unread flag.

In `@Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowModel.swift`:
- Around line 61-86: Remove the duplicate SidebarRowSnapshotCache ownership from
SidebarWorkspaceRowModel.swift, or transfer sole ownership to the table
controller with explicit invalidation tied to model/lifecycle events. In
Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowModel.swift lines 61-86,
eliminate the redundant mutable snapshot cache or centralize its ownership; in
Sources/ContentView.swift line 10803, remove render-time pruning and update
cache state only through model/lifecycle handling.

In `@Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSlotViews.swift`:
- Around line 84-101: Update the closed-status rendering in the
SidebarWorkspaceRowSlotViews drawing block to draw the prepared tinted image
rather than the original image, and use source-over compositing so it remains
visible on the transparent view. Preserve the existing icon sizing and centered
rect while applying the intended tint through the image’s mask or palette.

In `@Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSupportViews.swift`:
- Around line 690-744: The SidebarRowChecklistPopoverController’s item lines
capture stale model and item state, so mutations leave the popover outdated and
repeated toggles submit the same transition. Update loadView and the
onToggle/onRemove handlers to refresh from the authoritative row snapshot
through the existing SidebarAppKitRowActions path, or dismiss the popover
immediately after each mutation; avoid retaining duplicated item state in the
closures.

In `@Sources/Sidebar/AppKitList/SidebarWorkspaceTableCellView.swift`:
- Around line 12-20: Remove the test/debug-only accessors around
representedRowId, reconfigurationProbe, hostingViewIdentity, and
hostedRootIdentity in
Sources/Sidebar/AppKitList/SidebarWorkspaceTableCellView.swift; remove the
corresponding forwarding probes in
Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift:24-30 and the
computation callback in
Sources/Sidebar/AppKitList/SidebarWorkspaceTableDropTargetGeometryGate.swift:14-16.
Update cmuxTests/SidebarWorkspaceTableTests.swift:150-243 to verify observable
behavior or use test-target instrumentation instead of these production seams.

In `@Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift`:
- Around line 353-369: Replace the delayed Task.sleep-based implementation in
scheduleWidthRemeasure with a shared immediate remeasurement action, and have
the divider/live-resize owner invoke that action when tracking ends. Preserve
the existing width validation, cache preparation, override clearing, and table
row-height notification behavior, while removing the fixed-delay task
coordination.
- Around line 119-128: Eliminate full-list work from targeted row updates: in
Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift lines 119-128,
pass changed IDs/indexes and perform structural scans only when structural
changes occur; in
Sources/Sidebar/AppKitList/SidebarWorkspaceTableRowHeightCache.swift lines
61-84, update affected cache entries in place rather than rebuilding the
complete dictionary. Preserve existing content-change and height-measurement
behavior.
- Around line 241-251: Update both the drag path in
tableView(_:pasteboardWriterForRow:) and the middle-click close path to reject
group-header and fallback rows before accessing their workspaceId or performing
any action. Validate that the selected row represents a real workspace, while
preserving existing bounds and behavior for valid workspace rows.

In `@Sources/Sidebar/SidebarDividerTrackingView.swift`:
- Around line 48-73: Replace the blocking event loop in
SidebarDividerTrackingView.mouseDown(with:) with non-blocking mouseDragged,
mouseUp, and cancellation handling, or an equivalent gesture recognizer
coordinated on the MainActor. Preserve the existing onBegan, onChanged, onEnded,
cursor cleanup, and horizontal delta behavior without using nextEvent, RunLoop
spinning, or manual window presentation.

In `@Sources/VerticalTabsSidebar`+WorkspaceGroups.swift:
- Around line 95-191: Extract the command closures currently assembled in the
SidebarGroupHeaderRowActions initializer into a shared group-header action
owner, including focus, creation, rename, notification, grouping, deletion, and
navigation actions. Update both SwiftUI and AppKit renderers, including
sidebarWorkspaceGroupRow, to consume that shared owner instead of wiring
equivalent behavior separately, preserving the live membership and
notification-state checks used by onMarkAllRead and onMarkAllUnread.
- Line 90: Update the isFirstRow calculation in the group-header rendering flow
to derive the first-row state from the visible render order rather than
sidebarReorderIds, which is empty outside active drag operations. Ensure the
first visible group is marked first during normal rendering while preserving the
existing reorder behavior when a drag is active.
🪄 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: fcc15abb-ad4c-4567-b94b-9d6896d4eb17

📥 Commits

Reviewing files that changed from the base of the PR and between c2fbf8d and 1896ebe.

📒 Files selected for processing (34)
  • Resources/Localizable.xcstrings
  • Sources/App/WorkspaceRuntimeSettings.swift
  • Sources/ContentView.swift
  • Sources/Debug/SidebarLazyContractProbe.swift
  • Sources/FeatureFlags.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarGroupHeaderRowModel.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarGroupHeaderRowView.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCellView.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCommands.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowModel.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSlotViews.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSupportViews.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableActions.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableCellModel.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableCellRootView.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableCellState.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableCellView.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableClipView.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableContainerView.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableController.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableDropTargetGeometryGate.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableEmptyDropIndicatorView.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableEnvironmentSnapshot.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableHoverResolver.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableRowConfiguration.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableRowHeightCache.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableRowHeightCalculator.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableView.swift
  • Sources/Sidebar/AppKitList/SidebarWorkspaceTableViewImpl.swift
  • Sources/Sidebar/SidebarDividerTrackingView.swift
  • Sources/SidebarWorkspaceGroupHeaderView.swift
  • Sources/VerticalTabsSidebar+WorkspaceGroups.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/SidebarWorkspaceTableTests.swift
💤 Files with no reviewable changes (1)
  • Sources/App/WorkspaceRuntimeSettings.swift

Comment thread Sources/ContentView.swift
Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/ContentView.swift
Comment on lines +91 to +99
/// Paints the selected-row background instantly on press; the next
/// authoritative configure() repaints the full selected treatment (or
/// reverts if the selection did not land).
func showOptimisticSelectionHighlight() {
guard let model, !model.isActive else { return }
let palette = SidebarRowPalette(model: model)
backgroundView.layer?.backgroundColor = palette.selectedBackground.cgColor
titleView.textColor = palette.selectedForeground(1.0)
}

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 | 🟠 Major | 🏗️ Heavy lift

Render selection only from the authoritative selection model.

This directly paints a speculative selected state outside model.isActive and has no local rollback if selection fails to trigger reconfiguration. Update the authoritative selection synchronously, then render through the normal model path.

As per path instructions, correctness-critical selection UI must use one reliable source of truth rather than a best-effort fallback.

🤖 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/Cells/SidebarWorkspaceRowCellView.swift` around
lines 91 - 99, Update showOptimisticSelectionHighlight() to synchronously update
the authoritative selection model instead of directly painting speculative
background and text colors. Then invoke the normal model-driven
rendering/configuration path so selection UI has a single source of truth and
automatically preserves rollback behavior when selection fails.

Sources: Coding guidelines, Path instructions

Comment on lines +420 to +426
let agentCount = model.snapshot.activeCodingAgentCount
let tooltip = agentCount == 1
? String(localized: "sidebar.agentActivity.tooltip.one", defaultValue: "1 agent running")
: String.localizedStringWithFormat(
String(localized: "sidebar.agentActivity.tooltip.many", defaultValue: "%lld agents running"),
agentCount
)

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

Use the .other plural localization key.

Replace sidebar.agentActivity.tooltip.many with an .other key and update every supported locale in the string catalog.

Based on learnings, pluralized strings in this repository use explicit .one and .other keys.

Proposed key change
-                String(localized: "sidebar.agentActivity.tooltip.many", defaultValue: "%lld agents running"),
+                String(localized: "sidebar.agentActivity.tooltip.other", defaultValue: "%lld agents running"),
📝 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
let agentCount = model.snapshot.activeCodingAgentCount
let tooltip = agentCount == 1
? String(localized: "sidebar.agentActivity.tooltip.one", defaultValue: "1 agent running")
: String.localizedStringWithFormat(
String(localized: "sidebar.agentActivity.tooltip.many", defaultValue: "%lld agents running"),
agentCount
)
let agentCount = model.snapshot.activeCodingAgentCount
let tooltip = agentCount == 1
? String(localized: "sidebar.agentActivity.tooltip.one", defaultValue: "1 agent running")
: String.localizedStringWithFormat(
String(localized: "sidebar.agentActivity.tooltip.other", defaultValue: "%lld agents running"),
agentCount
)
🤖 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/Cells/SidebarWorkspaceRowCellView.swift` around
lines 420 - 426, Update the pluralized tooltip handling near agentCount to use
the explicit sidebar.agentActivity.tooltip.other localization key instead of
sidebar.agentActivity.tooltip.many, and rename the corresponding entry in the
string catalog for every supported locale while preserving the existing .one key
and formatting behavior.

Sources: Coding guidelines, Learnings

Comment on lines +241 to +251
func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> (any NSPasteboardWriting)? {
guard rows.indices.contains(row), let actions else { return nil }
let workspaceId = rows[row].workspaceId
actions.beginWorkspaceDrag(workspaceId)
workspaceDragSessionDidBegin()
let item = NSPasteboardItem()
item.setString(
"\(SidebarTabDragPayload.prefix)\(workspaceId.uuidString)",
forType: NSPasteboard.PasteboardType(SidebarTabDragPayload.typeIdentifier)
)
return item

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 | 🟠 Major | ⚡ Quick win

Reject non-workspace rows before dragging or closing.

Both paths accept group headers and fallback rows, so middle-clicking a header can close its associated workspace and dragging it can start an unintended workspace reorder.

Proposed fix
 func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int)
     -> (any NSPasteboardWriting)? {
-    guard rows.indices.contains(row), let actions else { return nil }
+    guard rows.indices.contains(row),
+          rows[row].appKitWorkspaceRowModel != nil,
+          let actions else { return nil }
     let workspaceId = rows[row].workspaceId
 func middleClick(row: Int) {
-    guard rows.indices.contains(row) else { return }
+    guard rows.indices.contains(row),
+          rows[row].appKitWorkspaceRowModel != nil else { return }
     actions?.closeWorkspace(rows[row].workspaceId)
 }

Also applies to: 285-288

🤖 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 241 - 251, Update both the drag path in
tableView(_:pasteboardWriterForRow:) and the middle-click close path to reject
group-header and fallback rows before accessing their workspaceId or performing
any action. Validate that the selected row represents a real workspace, while
preserving existing bounds and behavior for valid workspace rows.

Comment on lines +353 to +369
/// One trailing re-measure ~120ms after the sidebar width stops moving;
/// per-pixel divider drags otherwise re-measure every row every frame.
private func scheduleWidthRemeasure() {
widthRemeasureTask?.cancel()
widthRemeasureTask = Task { [weak self] in
try? await Task.sleep(nanoseconds: 120_000_000)
guard let self, !Task.isCancelled else { return }
self.widthRemeasureTask = nil
let width = self.currentColumnWidth()
guard width > 0 else { return }
let changed = self.rowHeightCache.prepareHostedRows(self.rows, columnWidth: width)
self.lastMeasuredWidth = width
self.pumpHeightOverrides.removeAll(keepingCapacity: true)
if !changed.isEmpty {
self.containerView?.tableView.noteHeightOfRows(withIndexesChanged: changed)
}
}

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 | 🟠 Major | 🏗️ Heavy lift

Replace the 120 ms timer with an explicit resize-completion signal.

The fixed delay makes row-height correctness timing-dependent. Have the divider/live-resize owner invoke one shared remeasurement action when tracking ends.

As per coding guidelines, production Swift must not use Task.sleep for delayed coordination or readiness synchronization.

🤖 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 353 - 369, Replace the delayed Task.sleep-based implementation in
scheduleWidthRemeasure with a shared immediate remeasurement action, and have
the divider/live-resize owner invoke that action when tracking ends. Preserve
the existing width validation, cache preparation, override clearing, and table
row-height notification behavior, while removing the fixed-delay task
coordination.

Source: Coding guidelines

Comment on lines +48 to +73
override func mouseDown(with event: NSEvent) {
guard let window else { return }
onBegan?()
let startX = event.locationInWindow.x
NSCursor.resizeLeftRight.push()
defer {
NSCursor.pop()
onEnded?()
}
while true {
guard let next = window.nextEvent(
matching: [.leftMouseDragged, .leftMouseUp],
until: .distantFuture,
inMode: .eventTracking,
dequeue: true
) else { continue }
if next.type == .leftMouseUp {
break
}
onChanged?(next.locationInWindow.x - startX)
// Drain the SwiftUI/CA commit scheduled by the width write, then
// present — all inside this event, like NSSplitView's own loop.
RunLoop.current.run(mode: .eventTracking, before: Date())
window.contentView?.layoutSubtreeIfNeeded()
window.displayIfNeeded()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Do not block the MainActor in a custom mouse-event loop.

nextEvent(... until: .distantFuture) owns the UI thread until mouse-up while manually spinning and presenting the window. Use normal mouseDragged/mouseUp/cancellation callbacks or a gesture recognizer under one MainActor coordinator instead.

As per coding guidelines, do not introduce blocking repair paths for lifecycle or rendering races.

🤖 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/SidebarDividerTrackingView.swift` around lines 48 - 73,
Replace the blocking event loop in SidebarDividerTrackingView.mouseDown(with:)
with non-blocking mouseDragged, mouseUp, and cancellation handling, or an
equivalent gesture recognizer coordinated on the MainActor. Preserve the
existing onBegan, onChanged, onEnded, cursor cleanup, and horizontal delta
behavior without using nextEvent, RunLoop spinning, or manual window
presentation.

Source: Coding guidelines

globalFontMagnificationPercent: renderContext.environment.globalFontMagnificationPercent,
cwdContextMenuItems: cwdContextMenuItems,
rowSpacing: tabRowSpacing,
isFirstRow: renderContext.sidebarReorderIds.first == group.anchorWorkspaceId,

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

Derive first-row state from visible render order.

sidebarReorderIds is empty whenever no drag is active, so the first group header is never marked first during normal rendering.

Proposed fix
-            isFirstRow: renderContext.sidebarReorderIds.first == group.anchorWorkspaceId,
+            isFirstRow: renderContext.visibleWorkspaceRowIds.first == group.anchorWorkspaceId,
📝 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
isFirstRow: renderContext.sidebarReorderIds.first == group.anchorWorkspaceId,
isFirstRow: renderContext.visibleWorkspaceRowIds.first == group.anchorWorkspaceId,
🤖 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/VerticalTabsSidebar`+WorkspaceGroups.swift at line 90, Update the
isFirstRow calculation in the group-header rendering flow to derive the
first-row state from the visible render order rather than sidebarReorderIds,
which is empty outside active drag operations. Ensure the first visible group is
marked first during normal rendering while preserving the existing reorder
behavior when a drag is active.

Comment on lines +95 to +191
let actions = SidebarGroupHeaderRowActions(
onToggleCollapsed: { [weak tabManager, groupId = group.id] in
tabManager?.toggleWorkspaceGroupCollapsed(groupId: groupId)
},
onFocusAnchor: { [weak tabManager, anchorId = group.anchorWorkspaceId, selectedTabIds = $selectedTabIds, lastSidebarSelectionIndex = $lastSidebarSelectionIndex] in
guard let tabManager else { return }
guard let anchorTab = tabManager.tabs.first(where: { $0.id == anchorId }) else { return }
tabManager.selectWorkspace(anchorTab)
if selectedTabIds.wrappedValue != [anchorId] {
selectedTabIds.wrappedValue = [anchorId]
}
if let anchorIndex = tabManager.tabs.firstIndex(where: { $0.id == anchorId }) {
lastSidebarSelectionIndex.wrappedValue = anchorIndex
}
},
onTapPlus: { [weak tabManager, groupId = group.id, placement = newWorkspacePlacement] in
guard let tabManager else { return }
let resolved = placement
?? UserDefaultsSettingsClient(defaults: .standard).value(for: SettingCatalog().workspaceGroups.newWorkspacePlacement)
_ = tabManager.createWorkspaceInGroup(groupId: groupId, placement: resolved)
},
onRunResolvedItem: { [weak tabManager, groupId = group.id] item in
guard let tabManager else { return }
SidebarWorkspaceGroupContextMenuRunner.run(
item: item,
tabManager: tabManager,
groupId: groupId
)
},
onRename: { [weak tabManager, groupId = group.id, currentName = group.name] in
guard let tabManager else { return }
presentSidebarWorkspaceGroupRenamePrompt(
tabManager: tabManager,
groupId: groupId,
currentName: currentName
)
},
onTogglePinned: { [weak tabManager, groupId = group.id] in
tabManager?.toggleWorkspaceGroupPinned(groupId: groupId)
},
onMarkRead: { [weak notificationStore, anchorId = group.anchorWorkspaceId] in
notificationStore?.markRead(forTabId: anchorId)
},
onMarkUnread: { [weak notificationStore, anchorId = group.anchorWorkspaceId] in
notificationStore?.markUnread(forTabId: anchorId)
},
onClearLatestNotifications: { [weak notificationStore, anchorId = group.anchorWorkspaceId] in
notificationStore?.clearLatestNotification(forTabId: anchorId)
},
onMarkAllRead: { [weak tabManager, weak notificationStore, groupId = group.id, anchorId = group.anchorWorkspaceId] in
guard let tabManager, let notificationStore else { return }
// Resolve members live at action time: closures are excluded
// from model equality, so a captured ID list could go stale
// across a same-count membership swap.
let ids = tabManager.tabs.compactMap { $0.groupId == groupId && $0.id != anchorId ? $0.id : nil }
// Only touch members that are actually unread, so we never run
// notification teardown on already-read workspaces.
for id in ids where notificationStore.canMarkWorkspaceRead(forTabIds: [id]) {
notificationStore.markRead(forTabId: id)
}
},
onMarkAllUnread: { [weak tabManager, weak notificationStore, groupId = group.id, anchorId = group.anchorWorkspaceId] in
guard let tabManager, let notificationStore else { return }
let ids = tabManager.tabs.compactMap { $0.groupId == groupId && $0.id != anchorId ? $0.id : nil }
// Only mark members that are not already unread. Calling
// markUnread on an already-unread member would set its manual
// unread flag, which a later notification dismissal cannot
// clear, leaving the workspace stuck unread.
for id in ids where notificationStore.canMarkWorkspaceUnread(forTabIds: [id]) {
notificationStore.markUnread(forTabId: id)
}
},
onUngroup: { [weak tabManager, groupId = group.id] in
tabManager?.ungroupWorkspaceGroup(groupId: groupId)
},
onDelete: { [weak tabManager, groupId = group.id] in
guard let tabManager,
let confirmation = tabManager.workspaceGrouping.deletionConfirmation(
groupId: groupId,
fallbackGroupName: group.name,
fallbackAnchorWorkspaceId: group.anchorWorkspaceId
) else { return }
if confirmation.containedWorkspaceCount > 0 {
guard confirmDeleteWorkspaceGroup(
groupName: confirmation.groupName,
memberCount: confirmation.containedWorkspaceCount
) else { return }
}
tabManager.workspaceGrouping.deleteWorkspaceGroup(confirmed: confirmation)
},
onEditConfig: {
SidebarWorkspaceGroupConfigOpener.openCmuxConfigInEditor()
},
onOpenDocs: {
SidebarWorkspaceGroupConfigOpener.openWorkspaceGroupsDocs()
}
)

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 | 🟠 Major | 🏗️ Heavy lift

Use one group-header action path for SwiftUI and AppKit.

This repeats the existing sidebarWorkspaceGroupRow command wiring and has already diverged—for example, the SwiftUI mark-read actions revalidate state while these do not. Extract a shared command/action owner and adapt both renderers to it.

As per coding guidelines, do not wire the same behavior separately through multiple surfaces.

🤖 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/VerticalTabsSidebar`+WorkspaceGroups.swift around lines 95 - 191,
Extract the command closures currently assembled in the
SidebarGroupHeaderRowActions initializer into a shared group-header action
owner, including focus, creation, rename, notification, grouping, deletion, and
navigation actions. Update both SwiftUI and AppKit renderers, including
sidebarWorkspaceGroupRow, to consume that shared owner instead of wiring
equivalent behavior separately, preserving the live membership and
notification-state checks used by onMarkAllRead and onMarkAllUnread.

Source: Coding guidelines

cmux reload-cloud and others added 3 commits July 16, 2026 21:04
…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
@azooz2003-bit
azooz2003-bit merged commit b7bd901 into main Jul 17, 2026
5 of 6 checks passed
azooz2003-bit pushed a commit that referenced this pull request Jul 17, 2026
NSImage's escaping drawing handler referenced the view's color property
without explicit capture semantics, which is a hard compile error; main
merged #8270 with PR CI off, so
tagged app builds from current main fail. Capture the color by value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
azooz2003-bit pushed a commit that referenced this pull request Jul 17, 2026
Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSlotViews.swift from
main (#8270) fails to compile under Xcode 26.6, which marks the
NSImage(size:flipped:) drawing closure so property access requires
explicit self. Main-side fix needed too.

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