Skip to content

Make workspace sidebar lazy with @Observable drag state - #4736

Merged
azooz2003-bit merged 6 commits into
manaflow-ai:mainfrom
azooz2003-bit:aziz/sidebar-lazy
May 26, 2026
Merged

azooz2003-bit merged 6 commits into
manaflow-ai:mainfrom
azooz2003-bit:aziz/sidebar-lazy

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented May 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Reapplies #3039's lazy-sidebar refactor on top of current main (the original PR was 924 commits behind and couldn't merge).

  • Replaces @State draggedTabId/dropIndicator/frozenTabItemPresentation on VerticalTabsSidebar with a single @State dragState = SidebarDragState() (an @Observable @mainactor class).
  • Flips the sidebar's workspace VStack → LazyVStack. The eager VStack was a workaround for a layout-invalidation loop (Nightly freezes: sidebar LazyVStack layout loop pegs main thread at 100% CPU, deadlocks CLI #2586) caused by 60Hz @State mutations during drag invalidating the entire sidebar body. @Observable per-property tracking means only the tiny overlays that actually read those properties re-render.
  • Extracts SidebarTabDropIndicatorOverlay, SidebarTabDragOpacityModifier, and a SidebarEmptyAreaDropIndicatorOverlay so the high-frequency dropIndicator/draggedTabId reads stay inside isolated leaf views, not the 1600-line TabItemView body.
  • Adds draggedTabIdBinding / dropIndicatorBinding adapter computed properties so unmigrated consumers (extension sidebar drop delegates, bonsplit workspace overlays) keep working without further refactor.
  • Removes the now-unused SidebarTabItemPresentationSnapshot + SidebarTabItemPresentationResolutionPolicy types — frozen-presentation visual stability is preserved by main's existing rowInteractionState flow.
  • Extracts rowHeightProbe from TabItemView.body to keep the Swift type checker under budget after the new modifier was added.

Differences from #3039

Test plan

  • Open app with many workspaces (50+); verify sidebar still renders correctly.
  • Drag a workspace; verify the drop indicator follows the pointer and lands at the expected position.
  • Check Activity Monitor during a sustained drag — should be far below the eager-VStack baseline.
  • Scroll the workspace list (with many workspaces) — LazyVStack should virtualize rows.
  • Open context menu on a workspace, dismiss; verify hover state recovers and close button doesn't get stuck.
  • Verify extension sidebar drop reordering still works (uses the adapter bindings).
  • Run the cmux unit-test scheme.

Refs #3039, #2586.

🤖 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

Make the workspace sidebar virtualized and smooth during drag by switching to LazyVStack and centralizing drag/drop in an @Observable SidebarDragState. Use value snapshots and pure predicates, precompute tabIds per render, and restore stable modifier shortcut hints while a context menu is open; this fixes the drag layout loop and reduces CPU on large lists (refs #2586).

  • Refactors

    • Replace @State draggedTabId/dropIndicator with SidebarDragState; add adapter bindings for existing @Binding consumers.
    • Enforce snapshot-boundary: pass isBeingDragged, topDropIndicatorVisible, onDragStart, and drop-delegate closures to rows/empty area; update TabItemView Equatable.
    • Compute indicator visibility in the parent via SidebarTabDropIndicatorPredicate; precompute tabIds once per render; remove SidebarTabDropIndicatorOverlay and SidebarTabDragOpacityModifier.
    • Mark SidebarTabDropDelegate @MainActor; clear dropIndicator on drag-clear; move row-height probe out of TabItemView; delete frozen presentation types and their tests.
    • Reintroduce shortcut-hint stability while a context menu is open using SidebarShortcutHintFreezePolicy; clear the freeze if the row is removed; add unit tests for the predicate, drag state, and freeze policy.
  • Migration

    • No changes needed. Existing extension sidebar drop delegates and overlays continue to work via adapter bindings.

Written for commit 4ecd35b. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • Improvements
    • Centralized sidebar drag state for smoother, more consistent drag-and-drop visuals, more accurate drop-indicator placement, and reduced row re-rendering during tab moves.
    • UI refinements to row height measurement, per-row indicator logic, and empty-area drop handling for a steadier drag experience.
  • Tests
    • Replaced a presentation-resolution test suite with new row interaction tests (close-button visibility and pointer/context-menu lifecycle).
    • Added unit tests validating drop-indicator visibility rules and drag-state behavior.

Review Change Stack

Switches the left sidebar to LazyVStack for O(visible) layout instead of
O(total) workspaces. Previously the stack had to stay eager because drag
mutations (draggedTabId, dropIndicator) were @State on the ancestor, and
the 60fps writes during drag fed back through the lazy layout cache and
pegged the main thread (manaflow-ai#2586).

Drag transient state now lives on a @observable @mainactor SidebarDragState
class. Reads are tracked per property, so high-frequency writes invalidate
only the tiny overlays that depend on them, never the sidebar body or the
LazyVStack layout.

- Add SidebarDragState (@observable) owning draggedTabId + dropIndicator.
- Replace @State draggedTabId/dropIndicator/frozenTabItemPresentation on
  VerticalTabsSidebar with a single @State SidebarDragState.
- Replace the @binding chain through TabItemView, SidebarEmptyArea, and
  both SidebarTabDropDelegate and SidebarBonsplitTabDropDelegate with a
  direct SidebarDragState reference.
- Extract SidebarTabDropIndicatorOverlay and SidebarTabDragOpacityModifier
  so the per-frame dropIndicator/draggedTabId reads stay inside isolated
  views and don't invalidate the 1600-line TabItemView body.
- Delete SidebarTabItemPresentationSnapshot and the frozen-presentation
  workflow (context-menu freeze was a workaround for binding churn that
  @observable's per-property tracking makes unnecessary).
- Flip VStack to LazyVStack inside the sidebar ScrollView.
@vercel

vercel Bot commented May 25, 2026

Copy link
Copy Markdown

@lawrencecchen is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 25, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Centralizes sidebar drag/drop transient state into an @Observable SidebarDragState; VerticalTabsSidebar owns lifecycle and adapter bindings; parents compute per-row snapshot booleans and inject onDragStart and tabDropDelegateFactory; SidebarTabDropDelegate and SidebarEmptyArea are rewritten; tests and project metadata added.

Changes

Sidebar drag-and-drop centralization

Layer / File(s) Summary
SidebarDragState and predicate
Sources/ContentView.swift
Adds centralized SidebarDragState @Observable and SidebarTabDropIndicatorPredicate.topVisible(...) for per-row/empty-area top-indicator visibility.
VerticalTabsSidebar state & adapters
Sources/ContentView.swift
VerticalTabsSidebar stores a single dragState and exposes adapter bindings (draggedTabIdBinding, dropIndicatorBinding), snapshots tabIds, and helpers to compute empty-area visibility and construct delegates.
Lifecycle and drag monitors
Sources/ContentView.swift
Clears dragState on appear/disappear and on drag-end; responds to SidebarDragLifecycleNotification.requestClear to reset dragState fields.
Workspace empty-area & bonsplit wiring
Sources/ContentView.swift
Workspace top-strip/empty-area .onDrop now uses a SidebarTabDropDelegate backed by dragState; SidebarEmptyArea receives computed topDropIndicatorVisible and bonsplitDropIndicator adapter binding.
Extension sidebar & browser-stack rewiring
Sources/ContentView.swift
Rewires extension empty-area and browser-stack tile/row drag/drop to mutate dragState, use adapter bindings for legacy delegates, and render drop indicators from dragState.dropIndicator.
TabItemView snapshot API and row integration
Sources/ContentView.swift
Parent computes isBeingDragged and topDropIndicatorVisible, passes onDragStart and tabDropDelegateFactory; TabItemView inputs and Equatable updated; row-height measurement moved to rowHeightProbe; drag/drop modifiers and context-menu lifecycle adjusted.
SidebarEmptyArea API and body
Sources/ContentView.swift
SidebarEmptyArea now takes topDropIndicatorVisible: Bool, tabDropDelegate: SidebarTabDropDelegate, and bonsplitDropIndicator: Binding<SidebarDropIndicator?>; body uses the injected delegate for .onDrop and conditionally renders the top indicator.
SidebarTabDropDelegate rewrite
Sources/ContentView.swift
SidebarTabDropDelegate now accepts dragState: SidebarDragState, validates using dragState.draggedTabId, updates/clears dragState.dropIndicator/dragState.draggedTabId during drop lifecycle, and computes targets from dragState.dropIndicator.
Tests and project metadata
cmuxTests/*, cmux.xcodeproj/project.pbxproj
Removes SidebarTabItemPresentationResolutionPolicyTests, adds SidebarWorkspaceRowInteractionStateTests, adds SidebarTabDropIndicatorPredicateTests.swift with predicate and drag-state tests, and registers the new test file in the Xcode project.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • manaflow-ai/cmux#4309: Upstream drag/drop refactor and extension browser-stack sidebar work related to the current SidebarDragState and browser-stack wiring.

Poem

A rabbit hops through drag-and-drop states,
I stitch the bindings, clear the trails of crates,
Snapshot booleans whisper where to land,
Delegates and probes all work hand in hand. 🐇✨


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (1 error, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Swift File And Package Boundaries ❌ Error ContentView.swift grows by 249 lines while already 956 over budget (16,912/15,956). Mixes UI/state/drag/parsing across 42+ types. SidebarTabDropIndicatorPredicate is independently testable. Extract SidebarDragState and SidebarTabDropIndicatorPredicate to package; shrink ContentView.swift by 200+ lines.
Docstring Coverage ⚠️ Warning Docstring coverage is 9.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Architecture Rethink ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
✅ Passed checks (14 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: implementing lazy rendering via LazyVStack and centralizing drag state in an @Observable class.
Description check ✅ Passed The description covers the Summary and Testing sections from the template with detailed explanations of changes and test plans, though it lacks a Demo Video link and the Checklist section.
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 SidebarDragState is @MainActor @Observable with clear ownership; SidebarTabDropDelegate is @MainActor; pure predicates avoid state; no off-main-thread access detected.
Cmux Swift Blocking Runtime ✅ Passed PR contains no blocking synchronization primitives. Changes are pure state/UI refactoring with @Observable store and pure predicates. Pre-existing Task.sleep is unmodified.
Cmux No Hacky Sleeps ✅ Passed The check targets TypeScript, JavaScript, shell, and non-Swift runtime scripts. This PR modifies only Swift files and Xcode configuration, which are out of scope.
Cmux Swift Concurrency ✅ Passed PR introduces zero legacy async patterns (no DispatchQueue, Combine, completion-handlers, fire-and-forget Tasks). Uses modern @Observable @MainActor instead.
Cmux Swift @Concurrent ✅ Passed New types (SidebarDragState, SidebarTabDropIndicatorPredicate, SidebarTabDropDelegate) have no async functions and violate no @concurrent annotation rules.
Cmux Swift Logging ✅ Passed PR contains only one NSLog statement in production code (ContentView.swift) which is wrapped in #if DEBUG guard, compliant with logging rules allowing debug-only logs.
Cmux User-Facing Error Privacy ✅ Passed PR introduces no user-facing errors, alerts, or sensitive information exposure. New sidebar drag-drop state management classes contain only UI logic without user-visible text.
Cmux Full Internationalization ✅ Passed PR refactors sidebar drag/drop with no new user-facing strings. All UI text uses String(localized:), tests are exempt, and project config has no user-facing text.
Cmux Swiftui State Layout ✅ Passed SidebarDragState uses @Observable (modern shape), LazyVStack rows get snapshots+closures, rowHeightProbe measures via .background, state mutations in callbacks only.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds only data model classes for sidebar state management, not windows/panels/controllers. No new window identifier assignments or window-related code violations detected.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@greptile-apps

greptile-apps Bot commented May 25, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR reapplies the lazy-sidebar refactor (#3039) on current main, replacing three @State drag vars (draggedTabId, dropIndicator, frozenTabItemPresentation) with a single @Observable @MainActor SidebarDragState class and flipping the workspace VStack to LazyVStack. The showsModifierShortcutHints freeze during context menu display is preserved via the new SidebarShortcutHintFreezePolicy enum, and drop-indicator visibility is lifted into the pure SidebarTabDropIndicatorPredicate to keep TabItemView free of @Observable store references.

  • @Observable + LazyVStack: per-property tracking ensures drag mutations at 60fps invalidate only the dragged row's opacity overlay and the affected drop-indicator overlays, not the entire sidebar body; TabItemView.equatable() skips re-rendering for rows whose isBeingDragged/topDropIndicatorVisible snapshot is unchanged.
  • Snapshot-boundary pattern: rows and SidebarEmptyArea receive precomputed value snapshots and closure bundles rather than a direct SidebarDragState reference; adapter Binding properties (draggedTabIdBinding, dropIndicatorBinding) keep the extension-sidebar and bonsplit drop delegates working without further migration.
  • showsModifierShortcutHints freeze: SidebarShortcutHintFreezePolicy.resolved replaces SidebarTabItemPresentationResolutionPolicy for the freeze field only; showsModifierShortcutHints is retained in TabItemView.== (line 13504), so modifier-key transitions correctly re-render non-frozen rows while the frozen row's badge state is pinned for the duration of the context menu.

Confidence Score: 5/5

Safe to merge: the refactor correctly preserves all existing behaviors (modifier-hint freeze, drop-indicator placement, adapter bindings for extension sidebar) and adds no new isolation or layout hazards.

The showsModifierShortcutHints freeze concern raised in the previous review is fully addressed by SidebarShortcutHintFreezePolicy and its inclusion in TabItemView.== at line 13504. SidebarTabDropDelegate now carries an explicit @mainactor annotation consistent with its @Observable-isolated state mutations. The LazyVStack + equatable + per-property @observable tracking design is internally consistent: only the dragged row and affected indicator overlays invalidate during drag, and off-screen rows are never created. New unit tests cover the pure predicate and policy logic including edge cases.

No files require special attention beyond the already-noted ContentView.swift size, which is a pre-existing concern not introduced by this PR.

Important Files Changed

Filename Overview
Sources/ContentView.swift Core refactor: replaces three @State drag vars with a single @observable SidebarDragState, flips VStack→LazyVStack, adds SidebarShortcutHintFreezePolicy for modifier-hint stability during context menu, and passes value snapshots+closure bundles into rows. showsModifierShortcutHints is correctly retained in TabItemView.== (line 13504). The @mainactor annotation on SidebarTabDropDelegate is explicitly added, addressing a prior review thread. No new isolation or layout bugs found.
cmuxTests/SidebarTabDropIndicatorPredicateTests.swift New test file covering SidebarTabDropIndicatorPredicate (topVisible + emptyAreaTopVisible), SidebarDragState initial/independent/clear behaviors, and SidebarShortcutHintFreezePolicy. Tests are well-structured and cover boundary conditions (first row, stale snapshot, nil list). Replaces the removed SidebarTabItemPresentationResolutionPolicyTests.
cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift Removes SidebarTabItemPresentationResolutionPolicyTests (the deleted type) and retains SidebarWorkspaceRowInteractionStateTests. Clean deletion with no orphaned references.
cmux.xcodeproj/project.pbxproj Adds SidebarTabDropIndicatorPredicateTests.swift to both the file references and the test target Sources phase. No unrelated changes.

Sequence Diagram

sequenceDiagram
    participant User
    participant TabItemView
    participant VerticalTabsSidebar
    participant SidebarDragState
    participant SidebarTabDropDelegate

    User->>TabItemView: begins drag
    TabItemView->>VerticalTabsSidebar: onDragStart()
    VerticalTabsSidebar->>SidebarDragState: "draggedTabId = tabId"
    VerticalTabsSidebar->>SidebarDragState: "dropIndicator = nil"
    SidebarDragState-->>VerticalTabsSidebar: "@Observable invalidates body"
    VerticalTabsSidebar->>TabItemView: "isBeingDragged=true (equatable skips others)"

    User->>SidebarTabDropDelegate: dropUpdated(info)
    SidebarTabDropDelegate->>SidebarDragState: "dropIndicator = nextIndicator"
    SidebarDragState-->>VerticalTabsSidebar: "@Observable invalidates body"
    VerticalTabsSidebar->>TabItemView: "topDropIndicatorVisible=true (equatable skips others)"

    User->>SidebarTabDropDelegate: performDrop(info)
    SidebarTabDropDelegate->>SidebarDragState: "draggedTabId = nil"
    SidebarTabDropDelegate->>SidebarDragState: "dropIndicator = nil"
    SidebarDragState-->>VerticalTabsSidebar: "@Observable invalidates body"
    VerticalTabsSidebar->>TabItemView: "isBeingDragged=false, topDropIndicatorVisible=false"
Loading

Reviews (6): Last reviewed commit: "Freeze showsModifierShortcutHints while ..." | Re-trigger Greptile

Comment thread Sources/ContentView.swift

@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/ContentView.swift`:
- Around line 10812-10819: The LazyVStack currently hands row children an
`@Observable` store (SidebarDragState via dragState) which violates the guideline;
instead, keep ownership of dragState in the parent scope that builds the
LazyVStack and change workspaceRow and any drop-gap children to accept only
immutable value snapshots (e.g., draggedTabId, dropIndicator) plus mutation
closures (e.g., onStartDrag(_:), onUpdateDrop(_:), onCommitDrop()) — remove any
direct SidebarDragState or `@Observable` parameters from workspaceRow and related
views and wire those closures to mutate the parent-held SidebarDragState.
- Around line 9880-9887: The requestClear handler for
SidebarDragLifecycleNotification currently only clears dragState.draggedTabId,
which can leave dragState.dropIndicator stale; update the onReceive block
handling SidebarDragLifecycleNotification.requestClear to also reset/clear
dragState.dropIndicator (e.g., set it to nil or its default empty state)
alongside dragState.draggedTabId so that any render paths keyed on dropIndicator
no longer show a lingering indicator; locate the closure attached to
NotificationCenter.default.publisher(for:
SidebarDragLifecycleNotification.requestClear) and modify it to clear both
dragState.draggedTabId and dragState.dropIndicator.
🪄 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: 31b78bcf-1719-4454-b385-c252a7e01179

📥 Commits

Reviewing files that changed from the base of the PR and between 9f0d739 and 0761080.

📒 Files selected for processing (1)
  • Sources/ContentView.swift

Comment thread Sources/ContentView.swift
Comment thread Sources/ContentView.swift
- TabItemView and SidebarEmptyArea no longer hold `let dragState:
  SidebarDragState`. Per the snapshot-boundary rule in CLAUDE.md, rows
  under a LazyVStack must not hold @observable store references. Replaced
  with value snapshots (`isBeingDragged`, `topDropIndicatorVisible`) and
  closure/delegate bundles that the parent constructs from `dragState`
  ownership. TabItemView's Equatable conformance now compares the new
  snapshot fields so unchanged rows still skip re-render.
- Deleted SidebarTabDropIndicatorOverlay and SidebarTabDragOpacityModifier
  (they also held @observable refs); their work is inlined or moved to a
  SidebarTabDropIndicatorPredicate helper evaluated in the parent.
- Marked SidebarTabDropDelegate @mainactor (Greptile P2): it mutates
  @MainActor-isolated SidebarDragState properties, so documenting the
  isolation prevents accidental removal.
- requestClear notification handler now also clears `dropIndicator`
  (CodeRabbit) so render paths keyed on dropIndicator can't linger.
- Dropped SidebarTabItemPresentationResolutionPolicyTests, which still
  referenced the removed presentation-snapshot types and was failing CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Addressed the three review threads (all resolved):

Greptile P2 (SidebarTabDropDelegate @mainactor): added explicit @MainActor to the struct so the isolation that drives its dragState mutations is documented and can't be accidentally removed.

CodeRabbit Minor (requestClear / dropIndicator): requestClear now also clears dragState.dropIndicator. Guard updated to short-circuit when both are already nil.

CodeRabbit Major (snapshot-boundary rule): this was the real one. TabItemView and SidebarEmptyArea no longer hold let dragState: SidebarDragState. They now receive value snapshots (isBeingDragged, topDropIndicatorVisible) and closure/delegate bundles built by the parent (workspaceRow / VerticalTabsSidebar helpers). TabItemView.== includes the new snapshot fields, so rows whose drag/indicator state didn't change still skip body re-evaluation. Deleted SidebarTabDropIndicatorOverlay and SidebarTabDragOpacityModifier (which also held @Observable references); the indicator predicate moved to SidebarTabDropIndicatorPredicate evaluated at the parent boundary. SidebarTabDropDelegate still holds dragState because it's a DropDelegate, not a View, and so isn't subject to the row-snapshot rule.

Failing tests CI job: dropped SidebarTabItemPresentationResolutionPolicyTests from cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift — it referenced types this PR removes.

Vercel deploy failures: these are the fork-PR auth wall (vercel.com/git/authorize link in the check) — a team member needs to authorize the fork once. Not something the branch can fix on its own.

@vercel

vercel Bot commented May 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 26, 2026 1:18am

…tate

- Refactored `SidebarTabDropIndicatorPredicate.topVisible` to take pure
  values (draggedTabId, dropIndicator, tabIds) instead of a SidebarDragState
  + [Tab] pair, so the per-row indicator predicate is unit-testable without
  any view-state setup. Extracted `emptyAreaTopVisible` to host the
  corresponding logic that used to live inline on SidebarEmptyArea.
- New `cmuxTests/SidebarTabDropIndicatorPredicateTests.swift`:
  * `SidebarTabDropIndicatorPredicateTopVisibleTests` (8 cases): no drag,
    no indicator, indicator on this row's top edge, this row's bottom edge,
    previous row's bottom edge, unrelated row, first-row bottom edge, stray
    row id not in tabIds.
  * `SidebarTabDropIndicatorPredicateEmptyAreaTests` (7 cases): no drag,
    no indicator, end-of-list (tabId nil), last row bottom edge, last row
    top edge, non-last row bottom edge, empty list.
  * `SidebarDragStateTests` (3 cases): initial cleared, independent
    per-property mutation, clearing both yields idle state.

All 18 cases pass against the cmux-unit scheme.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

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

🤖 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`:
- Around line 10959-10963: The code calls renderContext.tabs.map(\.id)
repeatedly inside each row to compute topDropIndicatorVisible via
SidebarTabDropIndicatorPredicate.topVisible(forTabId:draggedTabId:dropIndicator:tabIds:),
causing an O(n) allocation per row; fix by precomputing a single [UUID] snapshot
(e.g., renderedTabIds) once in the parent scope where other row snapshots are
created (before iterating/building row views) and pass renderedTabIds into the
call instead of renderContext.tabs.map(\.id), leaving the call site using
SidebarTabDropIndicatorPredicate.topVisible(forTabId: tab.id, draggedTabId:
dragState.draggedTabId, dropIndicator: dragState.dropIndicator, tabIds:
renderedTabIds).
🪄 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: a787ac0a-7ab2-4b4a-b6de-5d40b10216ca

📥 Commits

Reviewing files that changed from the base of the PR and between 57977ae and 5631c6b.

📒 Files selected for processing (3)
  • Sources/ContentView.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/SidebarTabDropIndicatorPredicateTests.swift

Comment thread Sources/ContentView.swift Outdated
CodeRabbit flagged that `workspaceRow(...)` was calling
`renderContext.tabs.map(\.id)` once per row to feed
`SidebarTabDropIndicatorPredicate.topVisible(...)`, making the per-render
cost O(n²) in workspace count. Store the snapshot once on
`WorkspaceListRenderContext.tabIds` at the parent body level and reuse it
per row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Addressed in 186ff09: WorkspaceListRenderContext now stores tabIds: [UUID] precomputed once in VerticalTabsSidebar.body, and workspaceRow passes renderContext.tabIds into the predicate instead of allocating a fresh array per row. Render cost goes from O(n²) to O(n).

azooz2003-bit and others added 2 commits May 25, 2026 17:53
Removing SidebarTabItemPresentationResolutionPolicy in the lazy-sidebar
refactor dropped the per-row freeze that prevented modifier-key
transitions from flipping shortcut-hint badges on the row sitting behind
an open context menu (Greptile flagged this regression; the PR's claim
that rowInteractionState preserves the freeze only covers the workspace
snapshot, not the modifier hint flag).

Reintroduce the freeze with a small pure policy + per-row closures so
the snapshot boundary still holds: the parent owns
frozenShortcutHintsTabId/Value, rows only see Bool snapshots and call
onContextMenuAppear/Disappear closures. Clear the freeze when the frozen
row is removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — 4ecd35bd Deployed May 26, 2026 by vercel[bot]
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.

2 participants