Skip to content

Fix sidebar row-ID layout preference feedback - #5637

Closed
austinywang wants to merge 2 commits into
mainfrom
issue-5570-cpu-layout-loop
Closed

austinywang wants to merge 2 commits into
mainfrom
issue-5570-cpu-layout-loop

Conversation

@austinywang

@austinywang austinywang commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #5570.

Summary:

  • remove the default sidebar row-ID preference emission and layout-state observer
  • scroll selected workspaces from model IDs instead of row layout preferences
  • add an early cmuxTests regression for the legacy row-ID preference key and hosted default sidebar

Evidence:

Local tests were not run per repo policy.

@vercel

vercel Bot commented Jun 8, 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 Jun 8, 2026 9:02am
cmux-staging Ready Ready Preview, Comment Jun 8, 2026 9:02am

@coderabbitai

coderabbitai Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR eliminates a sustained CPU loop in the SwiftUI layout engine by removing workspace row-ID preference accumulation, replacing deferred preference-driven scrolling with direct conditional calls, and adding regression tests with CI detection to prevent reintroduction.

Changes

Sidebar Workspace Scrolling: Preference Loop Fix

Layer / File(s) Summary
Preference Key Behavior Redesign
Sources/ContentView.swift, Sources/VerticalTabsSidebar+WorkspaceGroups.swift
SidebarWorkspaceRowIdsPreferenceKey.reduce now resets to an empty set instead of unioning accumulated values, and per-row preference emissions are removed from sidebar rows and group headers. Comments note the key is retained for regression coverage.
Direct Scrolling Helpers and Integration
Sources/ContentView.swift
New scrollSelectedWorkspaceIfNeeded and shouldScrollSelectedWorkspaceAfterWorkspaceOrderChange helpers implement immediate conditional scrolling. Old deferred scroll state fields are removed, and new helpers are wired into onAppear, onChange, and notification handlers to scroll directly when row layout is available.
Regression Test Suite
cmuxTests/AAASidebarWorkspaceRowIdPreferenceRegressionTests.swift, cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift
Unit tests verify that SidebarWorkspaceRowIdsPreferenceKey never aggregates row IDs across reductions and that the sidebar does not emit non-empty preference sets in steady state. A SwiftUI probe view captures live preference updates. Test dependencies import CmuxUpdater.
CI Regression Gate and Build Configuration
cmux.xcodeproj/project.pbxproj, .github/workflows/ci.yml
The regression test file is wired into the Xcode project build. The CI job scans test output for sidebar row-ID preference regression warnings and fails early if either message is detected.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • The changes directly address the SwiftUI/AttributeGraph layout loop reported in issue #5570 by removing the preference key accumulation mechanism that caused sustained CPU consumption.

Possibly related PRs

  • manaflow-ai/cmux#5401: Both PRs modify sidebarWorkspaceGroupHeader(...) in Sources/VerticalTabsSidebar+WorkspaceGroups.swift—this PR removes the row-ID preference emission while the linked PR threads settings.sidebarFontScale into SidebarWorkspaceGroupHeaderView—so the changes are related at the same call site.

Poem

🐰 Hopping past the loops and bounds,
Where preferences once spun around,
Now scrolling straight with guarded care,
The layout breathes—fresh, light, and fair!
No more the endless AttributeGraph flight. ✨

🚥 Pre-merge checks | ✅ 17 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description addresses the key requirements: it summarizes what changed and why, provides CI evidence, and references the issue. However, it lacks information on manual testing, includes no demo video, and the checklist is missing. Add details about manual testing verification, include a demo video if there are UI/behavior changes, and complete the checklist to confirm testing and review bot requests were performed.
✅ Passed checks (17 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix sidebar row-ID layout preference feedback' clearly and specifically describes the main change—removing row-ID preference feedback that caused the CPU loop.
Linked Issues check ✅ Passed All code changes directly address issue #5570: removal of row-ID preference emission, layout-state observer, and addition of regression tests to prevent feedback loop.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the row-ID preference feedback loop; no unrelated modifications detected.
Cmux Swift Actor Isolation ✅ Passed All production changes are internal helper methods in SwiftUI View types (implicitly MainActor) using only value types; no new isolation issues introduced.
Cmux Swift Blocking Runtime ✅ Passed Production code changes eliminate blocking preference loops without introducing new blocking primitives. Test RunLoop is allowed for deterministic test scaffolding.
Cmux No Hacky Sleeps ✅ Passed PR contains no production non-Swift script changes; Swift changes covered by separate rule; CI workflow sleeps intentionally out-of-scope; test uses proper RunLoop event-driven approach.
Cmux Algorithmic Complexity ✅ Passed PR replaces 100% CPU layout loop with O(n) scrolling check; single O(n) operation per event is acceptable for ~1000 workspaces under the rule.
Cmux Swift Concurrency ✅ Passed Scrolling functions use synchronous calls only; preference key clears values without aggregation; no DispatchQueue, Task, async/await, or completion handlers introduced in new code.
Cmux Swift @Concurrent ✅ Passed New test file correctly uses @MainActor for intentionally UI-bound work; no missing @concurrent on nonisolated async, no invalid @concurrent usage, and no unguarded CPU-heavy async patterns detected.
Cmux Swift File And Package Boundaries ✅ Passed PR reduces ContentView.swift by 15 lines despite file being oversized; new 130-line test file is below threshold; focused bug fix with no new responsibility mixing.
Cmux Swift Logging ✅ Passed The PR's only logging statement (NSLog in Sources/ContentView.swift) is properly guarded by #if DEBUG, which is an allowed case. No other app/runtime logging violations detected.
Cmux User-Facing Error Privacy ✅ Passed All new user-facing text is in test code and CI workflow, not production code. The production changes remove preference machinery without adding error messages, alerts, or user-visible output.
Cmux Full Internationalization ✅ Passed No production user-facing text was added. PR only modified test files and CI code, both exempt from internationalization requirements per the rules.
Cmux Swiftui State Layout ✅ Passed PR fixes issue #5570 layout loop by removing preference emission. Adds direct scrolling in lifecycle handlers. Introduces no new ObservableObject, @Published, state mutations, or problematic patterns.
Cmux Architecture Rethink ✅ Passed Removes preference feedback loop causing CPU issue, replaces with direct model-driven scrolling. Clear owner, no timing/observer debts, regression tests prevent recurrence.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The NSWindow created in the regression test is a test-only fixture allowed by the rule; no production windows are added or materially changed by this PR.
Cmux Source Artifacts ✅ Passed All changed files are legitimate: hand-written source code, tests, configurations, or required build-system files (pbxproj). No artifact patterns, tool output, or generated logs present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 issue-5570-cpu-layout-loop

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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 Jun 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a SwiftUI/AttributeGraph non-converging layout loop (issue #5570) caused by SidebarWorkspaceRowIdsPreferenceKey being emitted by every lazy sidebar row, forcing Set.formUnion aggregation across the full lazy list on every layout transaction. The fix removes the .preference(key:value:) modifier from both workspace rows and group-header rows, changes the reduce function to always return [] as a defensive guard, and replaces the preference-gated two-step scroll mechanism with a direct proxy.scrollTo call.

  • Layout loop fix: Both emission sites (workspaceRow and sidebarWorkspaceGroupHeader) drop the preference modifier; the reduce body now sets value = [] without calling nextValue(), preventing accumulation even if a modifier is accidentally re-added.
  • Scroll simplification: requestSelectedWorkspaceScroll / flushPendingSelectedWorkspaceScroll and the laidOutWorkspaceRowIds / pendingSelectedWorkspaceScrollId state are replaced by a single scrollSelectedWorkspaceIfNeeded call; scroll-on-order-change is refactored to a pure predicate + immediate call pattern.
  • Regression coverage: AAASidebarWorkspaceRowIdPreferenceRegressionTests adds a unit test for the reduce contract and an integration test that renders a 32-workspace sidebar and asserts no non-empty preference values bubble up; CI gains a hard-fail guard so these tests cannot be silently absorbed by the expected-failures mechanism.

Confidence Score: 4/5

Safe to merge; the layout-loop root cause is cleanly removed and the regression tests lock it in place.

The workspace-order-change handler now calls proxy.scrollTo synchronously before the LazyVStack re-renders the new row positions, so the scroll can land on the workspace's old offset rather than its settled new one. This is a narrow edge case limited to programmatic reorders of the selected workspace while it is off-screen, and it does not affect the core layout-loop fix or any data integrity.

Sources/ContentView.swift — specifically the onReceive(.workspaceOrderDidChange) closure and the timing of scrollSelectedWorkspaceIfNeeded relative to the subsequent render pass.

Important Files Changed

Filename Overview
Sources/ContentView.swift Removes SidebarWorkspaceRowIdsPreferenceKey emission from lazy rows, replacing the two-step preference-gate scroll with a direct proxy.scrollTo call; the onReceive(.workspaceOrderDidChange) path now scrolls before the re-render, which can leave scroll position stale for programmatic moves.
Sources/VerticalTabsSidebar+WorkspaceGroups.swift Removes the SidebarWorkspaceRowIdsPreferenceKey emission from group-header rows, completing the cleanup alongside ContentView.swift.
cmuxTests/AAASidebarWorkspaceRowIdPreferenceRegressionTests.swift New regression suite: one pure-unit test verifying the reduce function never accumulates row IDs, and one integration test checking the full sidebar emits no non-empty preference values after RunLoop settling.
.github/workflows/ci.yml Adds a hard-fail guard that exits non-zero when either regression test failure message appears in test output, preventing the expected-failures mechanism from silently absorbing these tests.
cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift Adds missing CmuxUpdater import; no functional change to tests.
cmux.xcodeproj/project.pbxproj Adds the new AAASidebarWorkspaceRowIdPreferenceRegressionTests.swift file reference and build-phase entry to the cmuxTests target.

Sequence Diagram

sequenceDiagram
    participant TM as TabManager
    participant SV as VerticalTabsSidebar
    participant LV as LazyVStack rows
    participant SP as ScrollViewProxy

    note over SV,LV: OLD (layout-loop path)
    LV->>SV: .preference(SidebarWorkspaceRowIdsPreferenceKey, Set([tab.id]))
    SV->>SV: "onPreferenceChange → laidOutWorkspaceRowIds += id"
    SV->>SP: flushPendingSelectedWorkspaceScroll → proxy.scrollTo
    note over LV,SP: Set.formUnion per row → layout loop (issue 5570)

    note over SV,LV: NEW (fixed path)
    TM->>SV: selectedTabId changes / workspaceOrderDidChange
    SV->>SP: scrollSelectedWorkspaceIfNeeded → proxy.scrollTo(selectedId)
    note over LV,SP: No preference emission, no aggregation
Loading

Reviews (1): Last reviewed commit: "Remove sidebar row-id layout preference ..." | Re-trigger Greptile

Comment thread Sources/ContentView.swift
Comment on lines +10887 to +10893
private func shouldScrollSelectedWorkspaceAfterWorkspaceOrderChange(_ notification: Notification) -> Bool {
guard let manager = notification.object as? TabManager, manager === tabManager else {
return
return false
}
guard let selectedWorkspaceId = tabManager.selectedTabId else { return }
guard let selectedWorkspaceId = tabManager.selectedTabId else { return false }
let movedWorkspaceIds = notification.userInfo?[WorkspaceOrderChangeNotificationKey.movedWorkspaceIds] as? [UUID] ?? []
guard movedWorkspaceIds.contains(selectedWorkspaceId) else { return }
pendingSelectedWorkspaceScrollId = selectedWorkspaceId
return movedWorkspaceIds.contains(selectedWorkspaceId)

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 Scroll fires before re-render on order change

shouldScrollSelectedWorkspaceAfterWorkspaceOrderChange returning true causes scrollSelectedWorkspaceIfNeeded to call proxy.scrollTo synchronously during onReceive, before SwiftUI processes the updated tabManager.tabs into a new render. At that point renderContext.workspaceIds still carries the pre-move ordering and the LazyVStack still places the row at its old offset, so the scroll view jumps to the old position. After the subsequent render the row physically moves to its new slot, leaving the scroll position stale. The old preference-based flush path fired post-layout (via onPreferenceChange), so it always scrolled to the settled position. For visible-row drag reorders the effect is imperceptible, but programmatic moves that shift the selected workspace far in the list can leave scroll state stale.

@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 `@cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift`:
- Line 2: Remove the unused top-level import by deleting the line "import
CmuxUpdater" from SidebarWorkspaceSnapshotRefreshPolicyTests.swift; all test
types are already available via "`@testable` import cmux", so removing this
redundant import (the lone occurrence of CmuxUpdater in the file) will eliminate
unnecessary module wiring.
🪄 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: 5423ebfb-c3da-44b7-93a2-ded3e554d2b0

📥 Commits

Reviewing files that changed from the base of the PR and between ed1744e and 3a31498.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • Sources/ContentView.swift
  • Sources/VerticalTabsSidebar+WorkspaceGroups.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/AAASidebarWorkspaceRowIdPreferenceRegressionTests.swift
  • cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift
💤 Files with no reviewable changes (1)
  • Sources/VerticalTabsSidebar+WorkspaceGroups.swift

@@ -1,4 +1,5 @@
import AppKit
import CmuxUpdater

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if tests compile without the CmuxUpdater import

# Remove the import and try to build the tests
cd "$(git rev-parse --show-toplevel)"

# Create a temporary file without the import
sed '2d' cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift > /tmp/test_without_import.swift

# Try to compile just this test file
xcodebuild -project cmux.xcodeproj \
  -scheme cmux \
  -configuration Debug \
  -destination 'platform=macOS' \
  -derivedDataPath /tmp/cmux-verify-import \
  build-for-testing \
  2>&1 | grep -i "SidebarWorkspaceSnapshotRefreshPolicyTests"

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cd "$(git rev-parse --show-toplevel)"

echo "== command availability =="
command -v xcodebuild || true

echo
echo "== file excerpt =="
nl -ba cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift | head -n 200

echo
echo "== direct CmuxUpdater symbol usage in file =="
rg -n "CmuxUpdater|CmuxUpdater\." cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift || true

echo
echo "== quick compile check: build-for-testing baseline =="
# Note: this will likely fail in this sandbox if Xcode isn't available.
xcodebuild -project cmux.xcodeproj \
  -scheme cmux \
  -configuration Debug \
  -destination 'platform=macOS' \
  -derivedDataPath /tmp/cmux-verify-import-baseline \
  build-for-testing 2>&1 | head -n 200 || true

Repository: manaflow-ai/cmux

Length of output: 169


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cd "$(git rev-parse --show-toplevel)"

echo "== tools =="
command -v rg || true
command -v cat || true
command -v sed || true
command -v head || true
command -v xcodebuild || true

echo
echo "== imports in SidebarWorkspaceSnapshotRefreshPolicyTests.swift =="
rg -n "^import " cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift || true

echo
echo "== CmuxUpdater symbol usage in file =="
rg -n "CmuxUpdater(\.|\\b)" cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift || true

echo
echo "== first ~120 lines with numbers (cat -n) =="
cat -n cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift | head -n 160

echo
echo "== whether file imports testable import cmux =="
rg -n "testable import cmux|`@testable` import cmux" cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift || true

echo
echo "== sample: other *Sidebar* test files import CmuxUpdater? =="
rg -n "^import CmuxUpdater" cmuxTests | head -n 50 || true

Repository: manaflow-ai/cmux

Length of output: 7964


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cd "$(git rev-parse --show-toplevel)"

echo "== tools =="
command -v rg || true
command -v cat || true
command -v sed || true
command -v head || true
command -v xcodebuild || true

echo
echo "== imports in SidebarWorkspaceSnapshotRefreshPolicyTests.swift =="
rg -n "^import " cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift || true

echo
echo "== CmuxUpdater symbol usage in file =="
rg -n "CmuxUpdater(\.|\\b)" cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift || true

echo
echo "== first ~160 lines with numbers (cat -n) =="
cat -n cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift | head -n 200

echo
echo "== whether file imports testable import cmux =="
rg -n "testable import cmux|`@testable` import cmux" cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift || true

echo
echo "== sample: other tests import CmuxUpdater? =="
rg -n "^import CmuxUpdater" cmuxTests | head -n 50 || true

Repository: manaflow-ai/cmux

Length of output: 9598


Remove the unused CmuxUpdater import from cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift.

import CmuxUpdater is the only occurrence of CmuxUpdater in this file; all referenced test types come via the existing @testable import cmux. Remove the import to reduce redundant module wiring.

🤖 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 `@cmuxTests/SidebarWorkspaceSnapshotRefreshPolicyTests.swift` at line 2, Remove
the unused top-level import by deleting the line "import CmuxUpdater" from
SidebarWorkspaceSnapshotRefreshPolicyTests.swift; all test types are already
available via "`@testable` import cmux", so removing this redundant import (the
lone occurrence of CmuxUpdater in the file) will eliminate unnecessary module
wiring.

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 3a31498f Deployed Jun 8, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cmux 0.64.14 sustained ~100% CPU in SwiftUI/AttributeGraph layout loop

3 participants