Skip to content

Fix sidebar row-height layout-feedback livelock (#6556) - #6826

Closed
austinywang wants to merge 6 commits into
mainfrom
issue-6556-sidebarworkspacegroupheaderview-tabitemview-s
Closed

austinywang wants to merge 6 commits into
mainfrom
issue-6556-sidebarworkspacegroupheaderview-tabitemview-s

Conversation

@austinywang

@austinywang austinywang commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #6556

Summary

SidebarWorkspaceGroupHeaderView and ContentView's TabItemView each measured their row height with a GeometryReader whose onAppear/onChange closures wrote @State rowHeight from inside the layout pass. Writing layout-derived @State mid-layout retriggers the SwiftUI AttributeGraph and reproduces the StackLayout.sizeThatFits / ViewLayoutEngine.sizeThatFits re-render livelock from the #6556 diagnostic (52% CPU sustained over 173 s under high workspace counts), the same class as #2586 / #5764 / #5845.

PR #6111 intended to remove this pattern, but #4385 re-introduced it in the group header and the TabItemView wiring was never updated — exactly the still-live paths the issue enumerates.

Why this differs from the reverted #6558

The first attempt (#6558) took the issue's option (1): replace the measured height with a pure arithmetic estimate (SidebarWorkspaceRowDropMetrics.dropTargetHeight). That was reverted (#6625) because the estimate diverged from real rendered content and made rows/tab pills render too wide/tall.

This PR takes the issue's option (2) instead: keep the exact measured height — so drag/drop pointer-edge metrics (edgeForPointer, the top/bottom-half insertion split) and visual layout are byte-for-byte unchanged — but move the @State write out of the layout pass.

Change

  • Add SidebarRowHeightPreferenceKey + a View.sidebarRowHeightProbe() helper, mirroring the existing in-repo BrowserAddressBarHeightPreferenceKey pattern in Sources/Panels/BrowserPanelView.swift.
  • A passive GeometryReader publishes the measured height through the preference; .onPreferenceChange(...) applies it to @State rowHeight after layout settles.
  • No GeometryReader writes @State during layout anymore. rowHeight is still consumed by the drop-delegate factories exactly as before, so pointer-edge drop behavior is preserved.

Net diff is a reduction in ContentView.swift and a small shared helper added to SidebarWorkspaceGroupHeaderView.swift.

Verification

  • python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv → Swift file length budget respected.
  • The AttributeGraph livelock is a SwiftUI layout/timing bug under high workspace counts + concurrent sidebar churn; it is not deterministically reproducible in a unit test (the prior PR's tests only covered the arithmetic seam, which this approach intentionally does not introduce). No fake regression test is added; the structural change (no @State write inside a GeometryReader) is the fix. Per task instruction, local builds/tests were not run — CI is the verification gate.
  • No new user-facing strings; localization audit not applicable.

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


Summary by cubic

Fixes the sidebar row-height layout livelock by moving height measurement out of the layout pass while keeping exact row height and drag/drop behavior unchanged.

  • Bug Fixes

  • Refactors

    • Moved SidebarRowHeightPreferenceKey and View.sidebarRowHeightProbe() into SidebarRowHeightPreferenceKey.swift.
    • Removed an obsolete, duplicate row-height probe.

Written for commit 9c23670. Summary will update on new commits.

Review in cubic

`SidebarWorkspaceGroupHeaderView` and `ContentView`'s `TabItemView` each
measured their row height with a `GeometryReader` whose `onAppear`/`onChange`
closures wrote `@State rowHeight` from inside the layout pass. Writing
layout-derived `@State` mid-layout retriggers the SwiftUI AttributeGraph and
reproduces the `StackLayout.sizeThatFits` / `ViewLayoutEngine.sizeThatFits`
re-render livelock documented in #2586 / #5764 / #6556 (a `cpu_resource.diag`
showed 52% CPU sustained over 173s under high workspace counts).

PR #6111 intended to remove this pattern, but #4385 re-introduced it in the
group header and the `TabItemView` wiring was never updated. A prior attempt
(#6558) replaced the measured height with an arithmetic estimate and was
reverted (#6625) because the estimate diverged from real content and made rows
render too wide/tall.

This redo keeps the exact measured height — so drag/drop pointer-edge metrics
and visual layout are unchanged — but moves the write out of the layout pass
using the established preference-key pattern (matches
`BrowserAddressBarHeightPreferenceKey`): a passive `GeometryReader` publishes
the height through `SidebarRowHeightPreferenceKey`, and `onPreferenceChange`
applies it after layout settles. No `@State` is written from inside a
`GeometryReader` anymore.

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

vercel Bot commented Jun 26, 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 Jul 4, 2026 11:10pm
cmux-staging Building Building Preview, Comment Jul 4, 2026 11:10pm

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@austinywang, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b090a152-a037-4a23-ab77-a80ee004c01c

📥 Commits

Reviewing files that changed from the base of the PR and between eeded15 and e783cbf.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (4)
  • Sources/ContentView.swift
  • Sources/SidebarRowHeightPreferenceKey.swift
  • Sources/SidebarWorkspaceGroupHeaderView.swift
  • cmux.xcodeproj/project.pbxproj
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-6556-sidebarworkspacegroupheaderview-tabitemview-s

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

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a SwiftUI layout-feedback livelock (#6556) in which SidebarWorkspaceGroupHeaderView and TabItemView wrote @State rowHeight directly from inside a GeometryReader during the layout pass, continuously retriggering the SwiftUI AttributeGraph and causing 52% sustained CPU under high workspace counts.

  • Root-cause fix: Replaces direct @State mutation inside GeometryReader.onAppear/onChange (mid-layout) with the standard SwiftUI preference-key pattern. A passive GeometryReader in the view's background publishes height via SidebarRowHeightPreferenceKey; .onPreferenceChange writes rowHeight only after the layout pass settles.
  • New shared helper: SidebarRowHeightPreferenceKey.swift (41 lines) introduces SidebarRowHeightPreferenceKey: PreferenceKey and View.sidebarRowHeightProbe(), mirroring the existing BrowserAddressBarHeightPreferenceKey pattern in BrowserPanelView.swift; wired into the Xcode project correctly.
  • No behavior change: Exact measured height is preserved (not estimated), so drag/drop pointer-edge split logic (edgeForPointer) and visual row sizing are byte-for-byte identical to the pre-livelock state.

Confidence Score: 5/5

Safe to merge — the change is a focused structural fix that eliminates mid-layout @State writes without altering any observable row height values or drag/drop behavior.

The fix correctly replaces the problematic GeometryReader→@State write pattern with the standard SwiftUI preference-key approach, mirroring an established pattern already in the codebase. The new helper file is small and well-documented, actor isolation is unaffected (onPreferenceChange always fires on the main actor), and the Xcode project wiring is correct. No existing behavior is changed.

No files require special attention.

Important Files Changed

Filename Overview
Sources/SidebarRowHeightPreferenceKey.swift New 41-line file introducing the shared PreferenceKey type and View helper; clean, well-documented, correctly modelled on the existing BrowserAddressBarHeightPreferenceKey pattern.
Sources/ContentView.swift TabItemView gains .sidebarRowHeightProbe() + .onPreferenceChange to replace any prior mid-layout @State write; the four added lines are placed after all layout-affecting modifiers and before contentShape/gesture modifiers.
Sources/SidebarWorkspaceGroupHeaderView.swift Group header view similarly updated with .sidebarRowHeightProbe() + .onPreferenceChange after padding; the placement is correct and the existing rowHeight consumer (drop-delegate factories) is unchanged.
.github/swift-file-length-budget.tsv Budget for ContentView.swift bumped by 4 lines (16427 → 16431) to reflect the four lines added to TabItemView; accurate and expected.
cmux.xcodeproj/project.pbxproj SidebarRowHeightPreferenceKey.swift correctly added to both the file reference list and the compile sources build phase; no unrelated project changes.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Row as TabItemView/GroupHeaderView
    participant GR as GeometryReader background
    participant PK as SidebarRowHeightPreferenceKey
    participant OPC as onPreferenceChange
    participant State as rowHeight State

    Note over Row,State: OLD path (livelock)
    Row->>GR: layout pass
    GR-->>State: onAppear/onChange writes rowHeight mid-layout
    State-->>Row: State change retriggers AttributeGraph
    Row->>GR: layout pass again

    Note over Row,State: NEW path (this PR)
    Row->>GR: layout pass (GR in background, passive)
    GR->>PK: preference set with proxy.size.height
    PK-->>OPC: preference bubbles up after layout settles
    OPC-->>State: "rowHeight = max(height, 1) post-layout"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Row as TabItemView/GroupHeaderView
    participant GR as GeometryReader background
    participant PK as SidebarRowHeightPreferenceKey
    participant OPC as onPreferenceChange
    participant State as rowHeight State

    Note over Row,State: OLD path (livelock)
    Row->>GR: layout pass
    GR-->>State: onAppear/onChange writes rowHeight mid-layout
    State-->>Row: State change retriggers AttributeGraph
    Row->>GR: layout pass again

    Note over Row,State: NEW path (this PR)
    Row->>GR: layout pass (GR in background, passive)
    GR->>PK: preference set with proxy.size.height
    PK-->>OPC: preference bubbles up after layout settles
    OPC-->>State: "rowHeight = max(height, 1) post-layout"
Loading

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

Comment on lines +377 to +389
/// Publishes a sidebar row's measured height up to an ancestor so the value can
/// be consumed in `onPreferenceChange` — *after* the layout pass — instead of
/// being written into `@State` from inside a `GeometryReader` during layout.
///
/// Writing layout-derived `@State` mid-layout retriggers the SwiftUI
/// AttributeGraph and reproduces the `StackLayout.sizeThatFits` /
/// `ViewLayoutEngine.sizeThatFits` re-render livelock documented in
/// https://github.com/manaflow-ai/cmux/issues/2586 and
/// https://github.com/manaflow-ai/cmux/issues/6556. The measured height only
/// feeds drag/drop hit metrics, so reporting it through a preference keeps the
/// exact pointer-edge behavior while keeping the LazyVStack row free of
/// layout-pass state writes.
struct SidebarRowHeightPreferenceKey: PreferenceKey {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The utility types SidebarRowHeightPreferenceKey and sidebarRowHeightProbe() are defined at the bottom of SidebarWorkspaceGroupHeaderView.swift but consumed by both this file and ContentView.swift. Keeping shared infrastructure in a view-named file makes the dependency direction implicit: ContentView.swift silently depends on a utility defined inside another view's file. The analogous BrowserAddressBarHeightPreferenceKey is private and lives entirely within its own file. A SidebarRowHeightHelpers.swift (or folding these into a SidebarMetrics/SidebarShared file that already exists) would make the ownership explicit and keep SidebarWorkspaceGroupHeaderView.swift focused on its view.

Suggested change
/// Publishes a sidebar row's measured height up to an ancestor so the value can
/// be consumed in `onPreferenceChange` — *after* the layout pass — instead of
/// being written into `@State` from inside a `GeometryReader` during layout.
///
/// Writing layout-derived `@State` mid-layout retriggers the SwiftUI
/// AttributeGraph and reproduces the `StackLayout.sizeThatFits` /
/// `ViewLayoutEngine.sizeThatFits` re-render livelock documented in
/// https://github.com/manaflow-ai/cmux/issues/2586 and
/// https://github.com/manaflow-ai/cmux/issues/6556. The measured height only
/// feeds drag/drop hit metrics, so reporting it through a preference keeps the
/// exact pointer-edge behavior while keeping the LazyVStack row free of
/// layout-pass state writes.
struct SidebarRowHeightPreferenceKey: PreferenceKey {
/// Publishes a sidebar row's measured height up to an ancestor so the value can
/// be consumed in `onPreferenceChange` — *after* the layout pass — instead of
/// being written into `@State` from inside a `GeometryReader` during layout.
///
/// Writing layout-derived `@State` mid-layout retriggers the SwiftUI
/// AttributeGraph and reproduces the `StackLayout.sizeThatFits` /
/// `ViewLayoutEngine.sizeThatFits` re-render livelock documented in
/// https://github.com/manaflow-ai/cmux/issues/2586 and
/// https://github.com/manaflow-ai/cmux/issues/6556. The measured height only
/// feeds drag/drop hit metrics, so reporting it through a preference keeps the
/// exact pointer-edge behavior while keeping the LazyVStack row free of
/// layout-pass state writes.
// TODO: Move to a shared sidebar utilities file (e.g. SidebarRowHeightHelpers.swift)
// so ContentView.swift's dependency on this type is explicit.
struct SidebarRowHeightPreferenceKey: PreferenceKey {

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!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in b605f9f — moved SidebarRowHeightPreferenceKey + sidebarRowHeightProbe() into a dedicated Sources/SidebarRowHeightPreferenceKey.swift (wired into the Xcode project) so the shared dependency from both SidebarWorkspaceGroupHeaderView and ContentView is explicit. Kept the two tightly-coupled members co-located in the type-named file rather than a generic Helpers file.

— Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed by removing the obsolete SidebarRowHeightPreferenceKey/sidebarRowHeightProbe diff after syncing with main. The branch diff against current origin/main is now empty, so this helper no longer exists in the PR.

— Claude Code

@blacksmith-sh

This comment has been minimized.

Addresses the cmux Aziz file-organization policy and Greptile P2 review:
`SidebarRowHeightPreferenceKey` + `View.sidebarRowHeightProbe()` are consumed
by both `SidebarWorkspaceGroupHeaderView` and `ContentView`'s `TabItemView`, so
housing them at the bottom of a view file left the cross-file dependency
implicit. Move them into a dedicated `SidebarRowHeightPreferenceKey.swift`
(named for the major type, with its tightly-coupled `View` helper co-located)
and wire the file into the Xcode project. No behavior change.

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

Copy link
Copy Markdown
Contributor

This PR is now obsolete: #7117 (merged Jun 30) deleted the rowHeightProbe GeometryReader paths in both TabItemView and SidebarWorkspaceGroupHeaderView that this patch rewires, so the diff no longer applies to main. The #6556 concern itself is resolved on main by that removal. Note stable v0.64.17 shipped without #7117 and hung with exactly this signature today (spindump in #2586 (comment)), so the remaining exposure is release lag, not this patch. A behavioral scale gate for the whole livelock class is coming in a separate PR.

…kspacegroupheaderview-tabitemview-s

# Conflicts:
#	Sources/ContentView.swift
#	Sources/SidebarWorkspaceGroupHeaderView.swift
#	cmux.xcodeproj/project.pbxproj
@austinywang

Copy link
Copy Markdown
Contributor Author

@lawrencecchen agreed. I removed the obsolete row-height probe diff after syncing with main; the branch now has an empty diff against current origin/main, so it no longer reintroduces the paths removed by #7117. The remaining exposure is release lag rather than this PR.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

cmux-reconcile: close-candidate

Proposed action: Close this empty PR without merging; preserve the branch.

Evidence checked September 18, 2026: GitHub reports 0 changed files, 0 additions, and 0 deletions. I independently fetched the PR diff and it is empty. Head: 9c23670e06874e16ccf0e6f776b006402b226f66. Target branch: main.

There is no remaining patch in this PR against its target branch. This does not establish that the original feature shipped to main or that its idea should be discarded. If this is an intentional placeholder for future work, keep the underlying request and record that purpose; otherwise it is ready for queue cleanup.

Recheck the head/diff before acting in case new work arrives. Search "cmux-reconcile: close-candidate" in this repository for this cleanup batch.

@github-actions

Copy link
Copy Markdown
Contributor


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document v2.2 and I hereby sign the CLA


Warning

1 commit in this PR was authored by an email address that is not linked to any GitHub user, so we cannot tell whether the author has signed the CLA.

Unlinked author:

To unblock this PR, do one of the following:

  1. Link the email to your GitHub account (recommended). Add each address above at github.com/settings/emails, then push another commit (or comment recheck) so this check re-runs. See why commits are not linked to a user for details.

  2. Rewrite the commits to use an email that is already linked to your GitHub account:

    # Set the correct email locally (one-off, for this repo):
    git config user.email you@example.com
    # Rewrite every commit on this branch with the corrected identity:
    git rebase -i --root --exec 'git commit --amend --reset-author --no-edit'
    git push --force-with-lease

    After the push, comment recheck on this PR (or just re-push) to re-run the check.

    You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@lawrencecchen

Copy link
Copy Markdown
Contributor

Fleet instruction update for head 9c23670e06874e16ccf0e6f776b006402b226f66: this PR is classified other. No macOS build tag is claimed. The current controller app recipe does not establish iOS/test readiness; that requires the appropriate validated recipe. Use cmux-ci for supported jobs, retain the returned ID and receipt, and wait on the same ID after any timeout. Do not use retired maclease allocation or post credentials. Exact-head tags will be posted only after the applicable build succeeds.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Closing as already on main: merging this branch into main at 8421357 produces main's own tree, so there's nothing left to land. The branch is kept; reopen if something here is still missing. Part of the backlog cleanup in manaflow-ai/cmuxterm-hq#563.

This branch was successfully deployed

1 active deployment
Preview – cmux — 9c23670e Deployed Jul 4, 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.

SidebarWorkspaceGroupHeaderView + TabItemView still have live @State rowHeight + GeometryReader layout-feedback path after v0.64.16

3 participants