Skip to content

Sidebar: remove rows measurement that re-livelocks layout at scale - #6188

Merged
lawrencecchen merged 6 commits into
mainfrom
fix-sidebar-fill-no-measure
Jun 16, 2026
Merged

lawrencecchen merged 6 commits into
mainfrom
fix-sidebar-fill-no-measure

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 15, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A fresh hang capture on stable nightly 0.64.16 (build 96) shows the main thread pegged ~122% CPU, RSS 1.25 GB (footprint peak 3.9 GB), debug socket refusing connections, with the entire ~4s sample inside one NSHostingView.beginTransaction → GraphHost.flushTransactions → LazySubviewPlacements.placeSubviews → LazyStack.place → ForEachList.applyNodes. The app-owned frames are all the workspace sidebar: SidebarWorkspaceRenderItem.id.getter, SidebarWorkspaceRenderItemID.hash(into:), and Layout.sizeThatFits ... in conformance SidebarRowsFillLayout.

This is the same livelock class as #2586, #5764, #5845.

Root cause

#6033 correctly removed the old GeometryReader + PreferenceKey → @State feedback loop, but replaced it with SidebarRowsFillLayout, a custom Layout that splits the rows vs the empty drop area by calling:

subviews.first?.sizeThatFits(ProposedViewSize(width: width, height: nil))

in both sizeThatFits and placeSubviews. Asking a LazyVStack for its natural height (height: nil) forces SwiftUI to realize and measure every row synchronously on every layout pass, defeating laziness at measure time. With many workspaces under continuous agent-driven updates, a single layout transaction takes seconds and the main thread livelocks.

#6033 was dogfooded at 12 workspaces, where that O(N) measure is cheap (its sample showed zero flushTransactions frames), so the cost only appears at agent-heavy scale.

Fix

Remove the custom Layout. The .frame(minHeight: minHeight, alignment: .top) that already wrapped it is the correct primitive: it clamps the content height to max(rowsHeight, viewport) without forcing realization, so the rows stay lazy.

  • Rows render lazily, pinned to the top.
  • The empty drop/tap area moves from a measured, placed sibling to a full-size .background that fills the same minHeight frame (drops over rows still hit the per-row delegates; the blank region below hits the empty-area delegate, as before).
  • The end-of-list drop indicator anchors to the rows' own bottom edge (.overlay(alignment: .bottom)) instead of the empty area's top.

Fill semantics are unchanged and now computed natively by SwiftUI: rows fit → content fills the viewport, no phantom scrollbar (#3241); rows overflow → document scrolls.

Deletes SidebarRowsFillLayout and the now-unused emptyAreaFillHeight helpers + their unit tests. Keeps contentMinHeight and its #3241 pixel-rounding tests (still the production min-height value).

Verification

  • Tagged build sbfill compiles and launches.
  • Manual: sidebar renders; empty area below the last row stays a drop/tap target; no phantom scrollbar when rows fit; scrolls when they overflow.

Review asks (@azooz2003-bit, you own this area)

  1. Confirm there's no reason you needed the explicit rows measurement in SidebarRowsFillLayout that .frame(minHeight:) doesn't cover.
  2. The one behavior delta to sanity-check: with the empty area now a full-size background, a drop landing in the small inter-row spacing gap routes to the empty-area delegate rather than a between-row insert. Per-row drops (tabDropDelegateFactory(rowHeight)) are unchanged. Acceptable?
  3. Best validated with the sidebar drag profile at ~150 workspaces (profile-pr) vs current main.

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


Summary by cubic

Removes the sidebar’s custom rows measurement and uses .frame(minHeight:) to keep rows lazy and stop main-thread livelocks at scale. Preserves fill (no phantom scrollbar) and routes only the real blank space below the last row to the empty-area delegate.

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

Review in cubic


Note

Medium Risk
Touches sidebar scroll layout, drag/drop routing, and empty-area gestures; behavior may differ slightly for drops in inter-row gaps, though per-row drops are unchanged.

Overview
Fixes a main-thread layout livelock in the workspace sidebar by dropping the custom SidebarRowsFillLayout that called sizeThatFits(height: nil) on the LazyVStack every layout pass, which realized all rows at scale (regression from #6033; same class as #2586 / #5764 / #5845).

workspaceScrollContent now lays out lazy rows with .frame(minHeight:) only (viewport fill / scroll without measuring list height), puts the end-of-list drop indicator on a bottom overlay on the rows, mounts SidebarEmptyArea as a full-height top-aligned background, and adds a rows-sized clear background that swallows reorder/Bonsplit drops and double-tap over the list so only the blank below the last row stays an empty-area target.

Deletes SidebarRowsFillLayout.swift, emptyAreaFillHeight helpers in WindowChromeMetrics, their unit tests, and Xcode project references. contentMinHeight and its #3241 rounding tests are unchanged.

Reviewed by Cursor Bugbot for commit f9e29b8. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Refactor
    • Simplified the sidebar workspace scrolling layout by removing the custom rows fill behavior and rendering rows directly.
    • Updated the drop indicator to draw as a bottom overlay, and ensured the empty-area region fills via the scroll view’s minimum-height behavior.
    • Refined end-of-list interactions so only the true blank space below the last row remains interactable.
  • Tests
    • Removed sidebar workspace scrolling tests covering the previous empty-area sizing behavior.

Closes #6210

SidebarRowsFillLayout split the rows vs the empty drop area by calling
`subviews.first?.sizeThatFits(width:, height: nil)` in both sizeThatFits
and placeSubviews. Asking a LazyVStack for its natural height realizes
every row synchronously on every layout pass, so at high workspace counts
one `GraphHost.flushTransactions` pegs the main thread (a fresh capture on
0.64.16 sat ~4s inside a single transaction under SidebarRowsFillLayout).

This is the same livelock class as #2586 / #5764 / #5845. #6033 removed the
old GeometryReader+PreferenceKey @State feedback loop but replaced it with
this custom Layout, which still force-measures the whole list; it was only
dogfooded at 12 workspaces where the O(N) measure is cheap.

Remove the custom Layout. The `.frame(minHeight:)` that already wrapped it
is the correct primitive: it clamps the content to max(rowsHeight, viewport)
without forcing realization. The rows stay lazy and pin to the top; the
empty drop/tap area becomes a full-size background filling the same frame;
the end-of-list drop indicator anchors to the rows' own bottom edge. Fill
semantics (rows fit -> fill viewport, no phantom scrollbar #3241; rows
overflow -> scroll) are preserved, computed natively by SwiftUI.

Deletes SidebarRowsFillLayout and the now-unused emptyAreaFillHeight
helpers + their unit tests; keeps contentMinHeight and its #3241 tests.
@coderabbitai

coderabbitai Bot commented Jun 15, 2026 •

Copy link
Copy Markdown

Review Change Stack

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

Removes the custom SidebarRowsFillLayout SwiftUI Layout and both SidebarWorkspaceScrollLayout.emptyAreaFillHeight overloads. workspaceScrollContent is rewritten to compose workspaceRows directly with a conditional drop-indicator overlay and SidebarEmptyArea in a .background(alignment: .top) using frame(minHeight:), eliminating row-height measurement. Project references and associated tests are deleted.

Changes

Sidebar workspace layout simplification

Layer / File(s) Summary
New workspaceScrollContent composition and deleted layout helpers
Sources/ContentView.swift, Sources/WindowChromeMetrics.swift
workspaceScrollContent drops SidebarRowsFillLayout(viewportHeight:) and instead renders workspaceRows directly, adds a conditional bottom overlay for the top drop indicator driven by emptyAreaTopDropIndicatorVisible(), and hosts SidebarEmptyArea (topDropIndicatorVisible: false, expandsVertically: true) in .background(alignment: .top) within a .frame(minHeight:, alignment: .top). A Color.clear background with explicit double-tap and drop handlers neutralizes the end-of-list empty-area interaction surface. Both emptyAreaFillHeight overloads are removed from SidebarWorkspaceScrollLayout. The workspaceRows inline comment is updated to describe the new frame(minHeight:)-based fill strategy rather than layout measurement.
Xcode project removal and test deletion
cmux.xcodeproj/project.pbxproj, cmuxTests/SidebarWorkspaceScrollLayoutTests.swift
SidebarRowsFillLayout.swift is removed from all four Xcode project sections (PBXBuildFile, PBXFileReference, PBXGroup, PBXSourcesBuildPhase). The @Test cases covering emptyAreaFillHeight container-based and viewport-based scenarios are deleted from the test suite.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • manaflow-ai/cmux#6210: This PR directly implements the fix proposed in that issue by removing SidebarRowsFillLayout custom Layout that was force-measuring the entire LazyVStack on every layout pass and replacing it with .frame(minHeight:) to avoid measurement-induced livelocks.

Possibly related PRs

  • manaflow-ai/cmux#6033: This PR directly reverses the sidebar sizing strategy by removing SidebarRowsFillLayout and emptyAreaFillHeight helpers that were introduced there.
  • manaflow-ai/cmux#4767: That PR added SidebarWorkspaceScrollLayout.emptyAreaFillHeight overloads and configured empty-area height wiring, which this PR removes.
  • manaflow-ai/cmux#4736: Both PRs modify ContentView.swift around SidebarEmptyArea and topDropIndicatorVisible wiring in the workspace sidebar.

Poem

🐇 Hop hop, the layout's shed its weight,
No more measuring rows—how great!
minHeight frames the empty space,
.background fills it right in place.
The custom Layout? Gone for good,
Clean and simple, as it should! 🌿

🚥 Pre-merge checks | ✅ 20 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: removing the rows measurement mechanism from SidebarRowsFillLayout that was causing layout livelocks at scale.
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 PR removes SidebarRowsFillLayout (a struct Layout lacking @MainActor isolation) and replaces with frame modifiers. All remaining code maintains proper actor isolation: SidebarDragState is @MainActo...
Cmux Swift Blocking Runtime ✅ Passed PR removes blocking measurement logic (SidebarRowsFillLayout) and uses standard SwiftUI primitives (.frame, .overlay, .background); no blocking/timing synchronization introduced.
Cmux Expensive Synchronous Load ✅ Passed PR removes expensive synchronous measurement in SidebarRowsFillLayout and adds zero new expensive loaders. The workspaceScrollContent refactoring contains only view composition, no RestorableAgentS...
Cmux Cache Substitution Correctness ✅ Passed This PR does not involve cache substitution. It removes a custom Layout that forced synchronous row measurement and replaces it with native SwiftUI layout primitives (.frame, .overlay, .background)...
Cmux No Hacky Sleeps ✅ Passed PR only modifies Swift source/test files and Xcode project files. The rule explicitly exempts Swift code (covered by swift-blocking-runtime.md), applying only to TypeScript, JavaScript, shell, and...
Cmux Algorithmic Complexity ✅ Passed PR removes O(n) hot-path measurement loop (SidebarRowsFillLayout.sizeThatFits/placeSubviews called sizeThatFits(height: nil) forcing full row realization on every layout pass) and replaces with O(1...
Cmux Swift Concurrency ✅ Passed PR removes livelock-causing Layout measurement code and replaces it with synchronous SwiftUI modifiers; no new DispatchQueue, Combine, Tasks, or legacy async patterns introduced.
Cmux Swift @Concurrent ✅ Passed PR contains only synchronous SwiftUI view modifications. No new async functions, no missing @concurrent annotations, no @concurrent on synchronous/isolated functions, no CPU-heavy async from UI iso...
Cmux Swift File And Package Boundaries ✅ Passed PR deletes problematic SidebarRowsFillLayout (69 lines), reduces WindowChromeMetrics (48 lines), and adds only +18 net lines to ContentView.swift—well below 250-line threshold for oversized files....
Cmux Swift Logging ✅ Passed No logging violations found. PR only refactors SwiftUI layout (removes SidebarRowsFillLayout, changes to .frame(minHeight:)), deletes unused helpers, and removes associated tests—all structural cha...
Cmux User-Facing Error Privacy ✅ Passed PR contains only technical UI layout refactoring code with no new user-facing error messages, alerts, or sensitive data violations of user-facing-errors.md rules.
Cmux Full Internationalization ✅ Passed PR removes SidebarRowsFillLayout and emptyAreaFillHeight helpers (code refactoring). No new user-facing strings added, no string catalogs/Info.plist modifications, no localization changes needed.
Cmux Swiftui State Layout ✅ Passed PR removes problematic SidebarRowsFillLayout custom Layout that measured every row via sizeThatFits(height: nil), replacing it with measurement-free .frame(minHeight:). No new @Observable/@publishe...
Cmux Architecture Rethink ✅ Passed PR eliminates SidebarRowsFillLayout that forced row realization, replacing with native .frame(minHeight:). No new timing, state, lifecycle splits, or symptom patches; clears invariant (rows sta...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR does not add or materially change any user-visible NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup code. Changes are exclusively sidebar layout refactoring (removing Sideba...
Cmux Source Artifacts ✅ Passed All five changed files are hand-written Swift source, test code, or Xcode config; none match artifact patterns (DerivedData, .swiftpm, temp folders, caches, logs). One file deletion is cleanup.
Description check ✅ Passed The PR description provides a comprehensive explanation of the problem, root cause, fix, and verification steps, with clear sections addressing all required template elements including summary, testing approach, and checklists.

✏️ 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 fix-sidebar-fill-no-measure

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.

@vercel

vercel Bot commented Jun 15, 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 16, 2026 2:56am
cmux-staging Ready Ready Preview, Comment Jun 16, 2026 2:56am

@greptile-apps

greptile-apps Bot commented Jun 15, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Removes SidebarRowsFillLayout, the custom Layout added in #6033 that called sizeThatFits(height: nil) on the LazyVStack in both sizeThatFits and placeSubviews, realizing every row on every layout pass and causing the documented main-thread livelock at scale. The new approach uses .frame(minHeight: minHeight, alignment: .top) for viewport fill, a rows-sized Color.clear background to absorb event passthrough, and SidebarEmptyArea as a full-height top-aligned .background — all measurement-free.

  • Removes SidebarRowsFillLayout.swift and the emptyAreaFillHeight overloads in WindowChromeMetrics.swift, along with their unit tests; keeps the contentMinHeight rounding tests for issue sidebar scrollbar always visible — should only appear when content overflows #3241.
  • Relocates the end-of-list drop indicator to a .overlay(alignment: .bottom) on workspaceRows, and mounts SidebarEmptyArea(expandsVertically: true) as a top-aligned background on the minHeight frame so only the genuine blank area below the last row receives empty-area actions.
  • Acknowledged behavioral delta: drops and double-taps that land in inter-row spacing gaps now consume (return false) via the clear absorber rather than routing to a between-row insert, because measuring which gap was hit requires the realized row heights this fix explicitly avoids. The author has flagged this for reviewer sanity-check.

Confidence Score: 5/5

Safe to merge — removes a measurement-based livelock with a native SwiftUI primitive, with no state-management or concurrency changes.

The change is a targeted deletion of a custom Layout whose sizeThatFits(height: nil) call on the LazyVStack was the documented root cause of the livelock. The replacement — .frame(minHeight:) plus a layered hit-target absorber — is a well-understood SwiftUI pattern that requires no new measurement. Fill semantics, per-row drop delegates, and the empty-area double-tap path are all preserved. The only acknowledged behavioral delta (inter-row gap drops consuming via the absorber instead of routing to a between-row insert) is explicitly called out in the PR description for reviewer confirmation.

No files require special attention. The Xcode project file, budget TSV, and test deletions all cleanly match the Swift source changes.

Important Files Changed

Filename Overview
Sources/ContentView.swift Core layout change: replaces SidebarRowsFillLayout with .frame(minHeight:) + rows-sized clear absorber + SidebarEmptyArea as top-aligned background. Logic is sound; drop indicator, per-row delegates, and empty-area interactions all remain correct.
Sources/SidebarRowsFillLayout.swift Deleted in full — the Layout that caused the livelock by calling sizeThatFits(height: nil) on the LazyVStack every pass.
Sources/WindowChromeMetrics.swift Removes emptyAreaFillHeight overloads — both dead code now that SidebarRowsFillLayout is gone. contentMinHeight and its tests are unchanged.
cmuxTests/SidebarWorkspaceScrollLayoutTests.swift Removes 87 lines of tests for the deleted emptyAreaFillHeight helpers. contentMinHeight pixel-rounding tests are kept intact.
.github/swift-file-length-budget.tsv ContentView.swift budget bumped 16696 → 16714 (+18 lines); TerminalComposerView.swift entry re-sorted. Both consistent with the diff.
cmux.xcodeproj/project.pbxproj PBXBuildFile and PBXFileReference entries for SidebarRowsFillLayout.swift removed cleanly with no dangling references.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[workspaceRows
LazyVStack — lazy, no height measure] --> B[.overlay alignment:.bottom
Drop indicator at last-row edge]
    B --> C[.background
Color.clear absorber — rows-sized
swallows double-tap + drop over rows]
    C --> D[.frame minHeight: minHeight
alignment: .top
viewport fill without measuring rows]
    D --> E[.background alignment:.top
SidebarEmptyArea expandsVertically:true
fills full frame]

    subgraph Hit-test order over rows area
        R1[Per-row delegates — frontmost]
        R2[Clear absorber — returns false for
SidebarTab + Bonsplit drops]
        R3[SidebarEmptyArea — blocked by absorber]
        R1 --> R2 --> R3
    end

    subgraph Hit-test order over empty area below rows
        E1[Nothing from rows]
        E2[SidebarEmptyArea — receives
double-tap + drops]
        E1 --> E2
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[workspaceRows
LazyVStack — lazy, no height measure] --> B[.overlay alignment:.bottom
Drop indicator at last-row edge]
    B --> C[.background
Color.clear absorber — rows-sized
swallows double-tap + drop over rows]
    C --> D[.frame minHeight: minHeight
alignment: .top
viewport fill without measuring rows]
    D --> E[.background alignment:.top
SidebarEmptyArea expandsVertically:true
fills full frame]

    subgraph Hit-test order over rows area
        R1[Per-row delegates — frontmost]
        R2[Clear absorber — returns false for
SidebarTab + Bonsplit drops]
        R3[SidebarEmptyArea — blocked by absorber]
        R1 --> R2 --> R3
    end

    subgraph Hit-test order over empty area below rows
        E1[Nothing from rows]
        E2[SidebarEmptyArea — receives
double-tap + drops]
        E1 --> E2
    end
Loading

Reviews (4): Last reviewed commit: "Merge origin/main into fix-sidebar-fill-..." | Re-trigger Greptile

Comment thread Sources/ContentView.swift
Autoreview flagged that mounting SidebarEmptyArea as the background of the
framed rows spread its end-of-list drop delegate (targetTabId: nil) behind the
entire row block: workspace drops in the 2pt inter-row gaps and row padding —
and the whole list when rows overflow and there is no blank area at all — could
resolve to the end target instead of the hovered/adjacent row.

Add a rows-sized background that claims SidebarTabDragPayload drops over the
rows block and no-ops them, so only the genuine blank area below the last row
routes to SidebarEmptyArea (and nothing does when rows overflow). Per-row
delegates render in front and still win over their own rows. Measurement-free:
the blocker is sized to the lazy rows, not the filled frame.

Bumps the ContentView file-length budget by the net fix lines.
Follow-up to the drop-routing fix: the rows-sized blocker now also claims the
double-tap-to-create gesture and Bonsplit new-workspace drops, not just
workspace-reorder drops. Otherwise a double-tap or Bonsplit drop landing in the
2pt inter-row gaps, row padding, or anywhere over an overflowing list could
still reach SidebarEmptyArea behind and create/route a workspace where the old
SidebarRowsFillLayout placed a zero-height empty area.

Physically placing the empty area below the rows requires measuring the
LazyVStack height every layout pass (the realized-all-rows livelock this PR
removes), so neutralizing every empty-area interaction over the rows block is
the measurement-free equivalent.
@lawrencecchen
lawrencecchen merged commit 313a2d2 into main Jun 16, 2026
19 of 21 checks passed
austinywang added a commit that referenced this pull request Jun 29, 2026
…#6870)

* Guard sidebar LazyVStack against re-livelock at scale (#6384)

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>

* Guard: handle Swift multi-line string literals in neutralize_swift

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>

* Guard: ban any custom Layout applied to the sidebar rows, not just by 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>

* Guard: discover custom Layouts across Packages/ too, document scope

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>

---------

Co-authored-by: cmux <cmux@cmuxs-Mac-mini.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — f9e29b87 Deployed Jun 16, 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

3 participants