Repository navigation
Guard workspace sidebar LazyVStack against layout re-livelock (#6384) - #6870
Conversation
The workspace-sidebar rows render as a LazyVStack inside a vertical ScrollView. Keeping that stack lazy at *measure time* is load-bearing: the sidebar is re-diffed on every workspace/telemetry update, so any code that forces SwiftUI to realize and measure the whole row list on each layout pass turns a routine update into a multi-second GraphHost.flushTransactions() main-thread livelock once enough workspaces/surfaces are open. That is exactly the ~1s beachball reported in #6384. The root cause -- SidebarRowsFillLayout, a custom Layout that called subviews.first?.sizeThatFits(ProposedViewSize(width:, height: nil)) on the LazyVStack every pass -- was removed in #6188 (#6210), and the rows are lazy again in main. But this same class of bug has now regressed four times (#2586, #5764, #5845, #6033 -> #6210/#6384) and is defended only by inline comments, which CI cannot enforce. Add a source-scan regression guard so the contract fails CI on re-introduction: - scripts/check-sidebar-lazy-layout.py neutralizes comments/string literals (the guarded functions deliberately *name* the forbidden anti-patterns in explanatory comments), extracts the bodies of workspaceScrollContent and workspaceRows from Sources/ContentView.swift, and fails if either reintroduces a whole-list measurement signature (GeometryReader, ProposedViewSize(..., nil), .sizeThatFits(, or SidebarRowsFillLayout) or drops a lazy-fill primitive the fix relies on (LazyVStack( in workspaceRows, .frame(minHeight:) in workspaceScrollContent). A renamed/removed guarded function fails loudly rather than silently skipping. - tests/test_ci_sidebar_lazy_layout_guard.py proves the guard catches the bug: it passes the real repo and a clean fixture whose comments/strings name every forbidden token, and fails synthetic fixtures for each regression mode (force-measure, reintroduced custom Layout, GeometryReader, eager VStack, missing minHeight, renamed function). - Wire the self-test into the workflow-guard-tests CI job. The drag-only drop-target reader (rowsWithGatedDropTargetReader) is not scanned: it intentionally uses a GeometryReader to resolve per-row drop anchors and is gated behind an active drag (#5325), so it never runs during the steady-state layout this guard protects. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
More reviews will be available in 1 minute and 46 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the 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 credits. 🚦 How do rate 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 see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis CI-only PR adds a regression guard to keep the workspace sidebar's
Confidence Score: 5/5CI-only additions with no runtime changes; the guard logic is correct for all current and historically regressed patterns. All three files are pure build tooling — no Swift sources, no runtime paths, no user-facing behavior is touched. The guard's core state machine (comment/string neutralization, brace-matched function extraction, forbidden-pattern scanning) is well-tested across 9 positive and negative cases. The two gaps identified (generic-constraint false positive in CUSTOM_LAYOUT_DECL and unhandled raw string literals) would, if triggered, produce visible CI noise rather than silent regressions, and neither is plausible in the guarded functions today. No files require special attention for merge safety; the two noted edge cases in scripts/check-sidebar-lazy-layout.py are low-probability and would manifest as obvious CI false positives rather than missed real bugs. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A([check-sidebar-lazy-layout.py]) --> B[Read Sources/ContentView.swift]
A --> C[Scan Sources/ + Packages/ for Layout-conforming types]
B --> D[neutralize_swift: strip comments, strings, multi-line strings]
C --> D2[neutralize_swift each file]
D2 --> E[CUSTOM_LAYOUT_DECL regex → custom_layout_names set]
D --> F[extract_function_body: workspaceScrollContent]
D --> G[extract_function_body: workspaceRows]
F --> F1{function found?}
G --> G1{function found?}
F1 -- No --> FAIL([FAIL: rename detected])
G1 -- No --> FAIL
F1 -- Yes --> H[Check FORBIDDEN_PATTERNS in body]
G1 -- Yes --> H
E --> H
H --> H1{GeometryReader / sizeThatFits / ProposedViewSize nil / SidebarRowsFillLayout / any custom Layout name?}
H1 -- Found --> FAIL
H1 -- Clean --> I[Check REQUIRED_PRIMITIVES]
I --> I1{LazyVStack in workspaceRows?}
I1 -- Missing --> FAIL
I1 -- Present --> I2{.frame minHeight in workspaceScrollContent?}
I2 -- Missing --> FAIL
I2 -- Present --> PASS([PASS: ok])
%%{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([check-sidebar-lazy-layout.py]) --> B[Read Sources/ContentView.swift]
A --> C[Scan Sources/ + Packages/ for Layout-conforming types]
B --> D[neutralize_swift: strip comments, strings, multi-line strings]
C --> D2[neutralize_swift each file]
D2 --> E[CUSTOM_LAYOUT_DECL regex → custom_layout_names set]
D --> F[extract_function_body: workspaceScrollContent]
D --> G[extract_function_body: workspaceRows]
F --> F1{function found?}
G --> G1{function found?}
F1 -- No --> FAIL([FAIL: rename detected])
G1 -- No --> FAIL
F1 -- Yes --> H[Check FORBIDDEN_PATTERNS in body]
G1 -- Yes --> H
E --> H
H --> H1{GeometryReader / sizeThatFits / ProposedViewSize nil / SidebarRowsFillLayout / any custom Layout name?}
H1 -- Found --> FAIL
H1 -- Clean --> I[Check REQUIRED_PRIMITIVES]
I --> I1{LazyVStack in workspaceRows?}
I1 -- Missing --> FAIL
I1 -- Present --> I2{.frame minHeight in workspaceScrollContent?}
I2 -- Missing --> FAIL
I2 -- Present --> PASS([PASS: ok])
Reviews (4): Last reviewed commit: "Guard: discover custom Layouts across Pa..." | Re-trigger Greptile |
This comment has been minimized.
This comment has been minimized.
Greptile P2 (#6870 review): neutralize_swift treated every `"` as a regular string boundary, so a Swift multi-line string `"""..."""` parsed as two empty strings plus an unclosed string. A bare `"` inside such a literal (e.g. `"""... he said "GeometryReader" ..."""`) would close the outer string early and expose the remaining content -- including a forbidden token named in prose -- as apparent code, tripping the guard with a false positive and a spurious CI failure. Add a MULTILINE_STRING tokenizer state: `"""` opens it, only a closing `"""` ends it, and a lone `"` inside is neutralized like any other string content. Add self-test case (b2) with a multi-line string containing a bare quote plus GeometryReader / sizeThatFits(ProposedViewSize(height: nil)) / SidebarRowsFillLayout, asserting the guard still passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @greptile — addressed the P2 in 73903a2: |
|
The fix is solid. The LGTM — this addresses the false-positive concern completely. |
… name Codex autoreview P2 (#6870): the guard only banned the literal deleted type name `SidebarRowsFillLayout`, so a future regression could wrap `workspaceRows(...)` in a differently named custom `Layout` (with the `subview.sizeThatFits(ProposedViewSize(... height: nil ...))` body living outside the two scanned functions) and CI would pass -- the exact #6033 shape under a new name. Generalize the guard: discover every type conforming to SwiftUI's `Layout` protocol across the whole Sources/ tree (comment/string- neutralized, pre-filtered to files that mention `Layout`), then fail if ANY of those type names is applied within `workspaceScrollContent` / `workspaceRows`. A custom Layout wrapping the LazyVStack measures it on every pass regardless of the type's name; rows must be sized by `.frame(minHeight:)` instead. The literal-name and direct force-measure token bans are kept as belt-and-suspenders. Add self-test case (d2): a `struct RowsFillLayout: Layout` (NOT the old name) whose force-measure lives in the layout type, applied to the rows in `workspaceScrollContent`; the guard must fail it. Without the generalization this is a false negative. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codex autoreview P2 (#6870): custom-Layout discovery scanned only Sources/, but cmux migrates app code into Packages/. A force-measuring sidebar layout defined in a repo-owned package and applied in workspaceScrollContent would not be discovered, leaving the guard bypassable exactly where code is moving. - Replace the Sources-only glob with repo_owned_swift_files(), which walks both Sources/ and Packages/ and prunes build/VCS/vendored dirs (.build, .git, DerivedData, Vendor, Pods, Carthage, node_modules, ...). External-dependency Layouts remain out of scope by design. - Add self-test case (i): repo_owned_swift_files() covers Sources/ and Packages/, discovers their Layout types, and excludes a .build/checkouts vendored Layout. - Document the guard's scope boundary: it protects the rows layout as expressed in workspaceScrollContent/workspaceRows and does not chase a force-measure relocated into an arbitrary transitively-called helper (fragile to track in a lint; such an extraction should re-review this guard). Custom Layout types are the exception chased across files, since a renamed force-measuring layout is the concrete #6033 regression. Real-repo scan stays ~2s (Layout-substring pre-filter). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
…-view guard (#7221) * Extend sidebar lazy-layout guard to the row views (TabItemView, group header) The source-scan guard from #6870 protected only the two container functions, but four of the five historical livelock regressions entered through the row views: the #2586/#6556 GeometryReader -> @State rowHeight probes lived in TabItemView and SidebarWorkspaceGroupHeaderView (removed by #6111, reintroduced by #4385, removed again by #7117) and shipped in stable v0.64.17, which livelocked in the wild on 2026-07-02 with exactly that signature (#2586 (comment)). Scan the TabItemView region of ContentView.swift and the whole group header file for per-row geometry feedback: GeometryReader, onGeometryChange, manual sizeThatFits, ProposedViewSize(nil), per-row anchorPreference/overlayPreferenceValue, and any discovered custom Layout. Rows must stay measurement-free; the only sanctioned geometry path is the container's drag-gated reader. Missing row types fail loudly so a rename cannot rot the guard into a no-op. Verified the extended guard retroactively flags both v0.64.17 row views. New self-test cases (j)-(m) cover clean-pass, the #6556 probe shape, the #5323 anchorPreference shape, and rename protection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add behavioral scale gate for the sidebar lazy-layout contract The lazy-layout contract (sidebar layout/diff work stays O(visible rows), never O(all workspaces)) has regressed five times through five different mechanisms (#5323 anchorPreference aggregation, #5764 String ids, #5845 animated height interpolation, #6210 force-measuring custom Layout, #6556 GeometryReader -> @State feedback), each shipping to stable before detection because nothing exercises the sidebar at the 100+ workspace scale where O(N) per pass livelocks the main thread (#2586). SidebarLazyLayoutScaleTests mounts the real VerticalTabsSidebar with 300 workspaces in an NSHostingView and counts actual row body evaluations through a DEBUG-only environment probe (SidebarLazyContractProbe, same pattern as MinimalModeInvalidationProbe): - mount must realize only viewport rows (catches any virtualization defeat, present or future, regardless of mechanism) - a 40-burst unread-model storm (the sidebar's highest-frequency whole-body invalidation path) must stay row-scoped and go quiet when the burst stops (catches feedback loops the way #6556 manifested) - a harness canary reproduces the GeometryReader -> @State shape in divergent form and asserts the harness detects it, so the gate cannot silently rot This is the mechanism-independent backstop behind the source-shape scan in scripts/check-sidebar-lazy-layout.py. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix scale-test autorelease avalanche that hung/crashed the app host Creating 300 workspaces inside one main-actor job accumulated every autoreleased object from the O(N)-per-add snapshot work into a single autorelease pool; the closing objc_autoreleasePoolPop then crashed CI (Signal 11 in AutoreleasePoolPage::releaseUntil, masked as a green run, see #5641) and hung for hours when reproduced on an AWS M4 Pro (sampled: main thread pinned in releaseUntil). The app never does this; real workspace creation happens one per event-loop turn with AppKit popping the pool between turns. Make the harness match real cadence: per-iteration autoreleasepool around addWorkspace and a run-loop turn every 20 creations. Also hoist the RunLoop.run call into a synchronous helper (fixes the Swift 6 unavailable-from-async warning) wrapped in its own pool. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix harness NSWindow double-release that killed the app host NSZombies named the corpse: "-[NSKVONotifying_NSWindow release]: message sent to deallocated instance". The harness windows used the NSWindow default isReleasedWhenClosed=true, so tearDown's close() performed AppKit's own release on top of ARC's; the double-release SEGV'd the host at the next autorelease-pool pop, before the pass was recorded, and CI masked the crash as a green run (#5641 (comment)). With zombies absorbing the over-release, all assertions pass in under a second, isolating the crash entirely to window teardown. Set isReleasedWhenClosed = false on both harness windows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Guard --file mode: require container functions only when one is present Addresses Greptile P2 on the PR: --file against a row-view source (no workspaceScrollContent/workspaceRows) emitted false could-not-locate violations that masked real row findings. In --file mode the container checks now apply only when at least one guarded function exists in the source, so ad-hoc row-view scans are clean while a fixture that renamed one function still fails loudly. Self-test cases added for both sides. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Split probe env key + extension into their own files (Aziz policy) One major type per Swift file, matching the MinimalModeInvalidationProbe three-file layout exactly. No content changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Cover the group-header row wrapper (guard target + grouped scale fixture) Codex review found the blind spot: the group-header row is assembled by sidebarWorkspaceGroupHeader(...) in VerticalTabsSidebar+WorkspaceGroups.swift, where modifiers wrap the header before it enters the LazyVStack — a GeometryReader or anchorPreference added there defeats laziness exactly like one inside the row view (the #4385 regression entered through the header path). The guard now scans that whole file for the row-forbidden shapes with a rename-protected marker, and the scale fixture groups the first 20 workspaces into 5 groups so group-header realization and convergence are asserted by the behavioral backstop (bounds on groupHeaderBodies at mount and in the quiet check). Verified on the AWS M4 Pro runner: 3/3 pass in 2.7s, no host restarts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Make the scale harness hermetic against persisted sidebar provider Codex review: VerticalTabsSidebar selects between the workspace list and extension/built-in sidebars via @AppStorage(CmuxExtensionSidebarSelection.defaultsKey), so a host with a persisted non-default provider would mount the wrong sidebar and the probes would never fire. Use a scratch UserDefaults suite pinned to the default provider via .defaultAppStorage, cleaned in tearDown — the WorkspaceContentViewVisibilityTests pattern. Verified on the AWS M4 Pro runner: 3/3 pass in 2.7s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…-view guard (manaflow-ai#7221) * Extend sidebar lazy-layout guard to the row views (TabItemView, group header) The source-scan guard from manaflow-ai#6870 protected only the two container functions, but four of the five historical livelock regressions entered through the row views: the manaflow-ai#2586/manaflow-ai#6556 GeometryReader -> @State rowHeight probes lived in TabItemView and SidebarWorkspaceGroupHeaderView (removed by manaflow-ai#6111, reintroduced by livelocked in the wild on 2026-07-02 with exactly that signature (manaflow-ai#2586 (comment)). Scan the TabItemView region of ContentView.swift and the whole group header file for per-row geometry feedback: GeometryReader, onGeometryChange, manual sizeThatFits, ProposedViewSize(nil), per-row anchorPreference/overlayPreferenceValue, and any discovered custom Layout. Rows must stay measurement-free; the only sanctioned geometry path is the container's drag-gated reader. Missing row types fail loudly so a rename cannot rot the guard into a no-op. Verified the extended guard retroactively flags both v0.64.17 row views. New self-test cases (j)-(m) cover clean-pass, the manaflow-ai#6556 probe shape, the manaflow-ai#5323 anchorPreference shape, and rename protection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add behavioral scale gate for the sidebar lazy-layout contract The lazy-layout contract (sidebar layout/diff work stays O(visible rows), never O(all workspaces)) has regressed five times through five different mechanisms (manaflow-ai#5323 anchorPreference aggregation, manaflow-ai#5764 String ids, manaflow-ai#5845 animated height interpolation, manaflow-ai#6210 force-measuring custom Layout, manaflow-ai#6556 GeometryReader -> @State feedback), each shipping to stable before detection because nothing exercises the sidebar at the 100+ workspace scale where O(N) per pass livelocks the main thread (manaflow-ai#2586). SidebarLazyLayoutScaleTests mounts the real VerticalTabsSidebar with 300 workspaces in an NSHostingView and counts actual row body evaluations through a DEBUG-only environment probe (SidebarLazyContractProbe, same pattern as MinimalModeInvalidationProbe): - mount must realize only viewport rows (catches any virtualization defeat, present or future, regardless of mechanism) - a 40-burst unread-model storm (the sidebar's highest-frequency whole-body invalidation path) must stay row-scoped and go quiet when the burst stops (catches feedback loops the way manaflow-ai#6556 manifested) - a harness canary reproduces the GeometryReader -> @State shape in divergent form and asserts the harness detects it, so the gate cannot silently rot This is the mechanism-independent backstop behind the source-shape scan in scripts/check-sidebar-lazy-layout.py. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix scale-test autorelease avalanche that hung/crashed the app host Creating 300 workspaces inside one main-actor job accumulated every autoreleased object from the O(N)-per-add snapshot work into a single autorelease pool; the closing objc_autoreleasePoolPop then crashed CI (Signal 11 in AutoreleasePoolPage::releaseUntil, masked as a green run, see manaflow-ai#5641) and hung for hours when reproduced on an AWS M4 Pro (sampled: main thread pinned in releaseUntil). The app never does this; real workspace creation happens one per event-loop turn with AppKit popping the pool between turns. Make the harness match real cadence: per-iteration autoreleasepool around addWorkspace and a run-loop turn every 20 creations. Also hoist the RunLoop.run call into a synchronous helper (fixes the Swift 6 unavailable-from-async warning) wrapped in its own pool. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix harness NSWindow double-release that killed the app host NSZombies named the corpse: "-[NSKVONotifying_NSWindow release]: message sent to deallocated instance". The harness windows used the NSWindow default isReleasedWhenClosed=true, so tearDown's close() performed AppKit's own release on top of ARC's; the double-release SEGV'd the host at the next autorelease-pool pop, before the pass was recorded, and CI masked the crash as a green run (manaflow-ai#5641 (comment)). With zombies absorbing the over-release, all assertions pass in under a second, isolating the crash entirely to window teardown. Set isReleasedWhenClosed = false on both harness windows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Guard --file mode: require container functions only when one is present Addresses Greptile P2 on the PR: --file against a row-view source (no workspaceScrollContent/workspaceRows) emitted false could-not-locate violations that masked real row findings. In --file mode the container checks now apply only when at least one guarded function exists in the source, so ad-hoc row-view scans are clean while a fixture that renamed one function still fails loudly. Self-test cases added for both sides. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Split probe env key + extension into their own files (Aziz policy) One major type per Swift file, matching the MinimalModeInvalidationProbe three-file layout exactly. No content changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Cover the group-header row wrapper (guard target + grouped scale fixture) Codex review found the blind spot: the group-header row is assembled by sidebarWorkspaceGroupHeader(...) in VerticalTabsSidebar+WorkspaceGroups.swift, where modifiers wrap the header before it enters the LazyVStack — a GeometryReader or anchorPreference added there defeats laziness exactly like one inside the row view (the manaflow-ai#4385 regression entered through the header path). The guard now scans that whole file for the row-forbidden shapes with a rename-protected marker, and the scale fixture groups the first 20 workspaces into 5 groups so group-header realization and convergence are asserted by the behavioral backstop (bounds on groupHeaderBodies at mount and in the quiet check). Verified on the AWS M4 Pro runner: 3/3 pass in 2.7s, no host restarts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Make the scale harness hermetic against persisted sidebar provider Codex review: VerticalTabsSidebar selects between the workspace list and extension/built-in sidebars via @AppStorage(CmuxExtensionSidebarSelection.defaultsKey), so a host with a persisted non-default provider would mount the wrong sidebar and the probes would never fire. Use a scratch UserDefaults suite pinned to the default provider via .defaultAppStorage, cleaned in tearDown — the WorkspaceContentViewVisibilityTests pattern. Verified on the AWS M4 Pro runner: 3/3 pass in 2.7s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 2071529)
Fixes #6384
Investigation
#6384 reports a ~1s main-thread beachball with many surfaces open: the main thread is 100% on-CPU (not blocked) inside
GraphHost.flushTransactions(), recomputing layout for aScrollView+LazyVStack+ForEachtree — the workspace sidebar.The authoritative follow-up comment (from a second reporter on 0.64.16) pinpoints the cmux-owned frames as
SidebarRowsFillLayout.placeSubviews/.sizeThatFitsdrivingLazyStack → ForEachList.applyNodes → measureEstimates, and notes this is the force-measure root-caused in #6210 and removed in #6188 (merged 2026-06-16, ~14h after v0.64.16 was tagged — so the stable build both reporters hit still shipped it).I confirmed on current
main:313a2d2(Sidebar: remove rows measurement that re-livelocks layout at scale #6188) is an ancestor ofHEAD;SidebarRowsFillLayoutis deleted (only a historical comment remains), and there is noProposedViewSize(… nil)/sizeThatFits(… nil)force-measure anywhere in the repo. The workspace sidebar (workspaceScrollContent/workspaceRows) is lazy again:LazyVStack+ a measurement-free.frame(minHeight:)fed by aGeometryReaderthat sits outside theScrollViewand only supplies a concrete viewport-derivedminHeightconstant.SidebarWorkspaceRenderItemIDenum, cmux.app 100% CPU hang: non-converging LazyHVStack layout loop over sidebar workspace list (SidebarWorkspaceRowIdsPreferenceKey feedback) — stable 0.64.14 #5764), a snapshot refresh policy that throttles re-renders, and a drag-only drop-anchorGeometryReadergated behind an active drag (Fix sidebar workspace frame collection virtualization #5325).Because this is a layout/timing livelock that only manifests at agent-heavy scale on the reporter's stable build, it isn't cleanly reproducible locally or unit-testable as a SwiftUI layout pass; the structural fix already landed in #6188.
What this PR adds
This exact class of bug has regressed four times — #2586 → #5764 → #5845 → #6033/#6210/#6384 — and the lazy-at-measure-time contract is currently defended only by inline comments, which CI cannot enforce. This PR adds the missing CI regression guard so a re-introduction fails the build instead of shipping another beachball:
scripts/check-sidebar-lazy-layout.py— neutralizes comments and string literals (the guarded functions deliberately name the forbidden anti-patterns in their explanatory comments), extracts the bodies ofworkspaceScrollContentandworkspaceRowsfromSources/ContentView.swift, and fails if either:GeometryReader,ProposedViewSize(…, nil),.sizeThatFits(, orSidebarRowsFillLayout; orLazyVStack(inworkspaceRows,.frame(minHeight:)inworkspaceScrollContent.tests/test_ci_sidebar_lazy_layout_guard.py— proves the guard catches the bug: passes the real repo and a clean fixture whose comments/strings name every forbidden token, and fails synthetic fixtures for each regression mode (force-measure, reintroduced customLayout,GeometryReader, eagerVStack, missingminHeight, renamed function).workflow-guard-testsCI job, matching the repo's existing guard-script convention (lint-pbxproj-test-wiring,check-workspace-package-groups, etc.).The drag-only
rowsWithGatedDropTargetReaderis intentionally not scanned — itsGeometryReaderresolves per-row drop anchors and only runs during an active drag (#5325), never in the steady-state layout this guard protects.Note on the two-commit regression policy
The red/green two-commit structure is for adding a new code fix. Here the fix already shipped in #6188, so the guard passes on
mainprecisely becausemainis correct. The proof that the guard catches the bug is instead permanent and in-CI: the negative cases intest_ci_sidebar_lazy_layout_guard.pyreintroduce each anti-pattern and assert the guard fails.Validation
No app/runtime behavior changes (CI-only). No user-facing strings, settings, menus, shortcuts, docs, or web messages touched — localization audit: N/A.
🤖 Generated with Claude Code
Summary by cubic
Adds a CI guard to keep the workspace sidebar
LazyVStacklazy at measure time and prevent reintroducing whole‑list measurement behind #6384. Now scans repo-ownedSources/andPackages/for any customLayoutused with the rows, handles Swift multi-line strings, and remains CI-only.scripts/check-sidebar-lazy-layout.pyto scanworkspaceScrollContentandworkspaceRowsinSources/ContentView.swift; fails onGeometryReader,.sizeThatFits(,ProposedViewSize(..., nil), or anyLayout-conforming type applied to the rows (discovered across repo-ownedSources/andPackages/, excluding.build,.git, and vendor dirs); requiresLazyVStack(and.frame(minHeight:); neutralizes comments, strings, and"""..."""; fails loudly if the guarded functions are renamed.tests/test_ci_sidebar_lazy_layout_guard.pywith pass/fail fixtures, including a renamed customLayout, a multi-line string with a bare", and a discovery check that includesPackages/and excludes vendored build trees; wired into CI via.github/workflows/ci.yml.Written for commit 3cb88e6. Summary will update on new commits.