Skip to content

fix(chat): eliminate _FlexFrameLayout alignment cascade in PinnedLatestTurnSection#29199

Merged
ashleeradka merged 5 commits into
mainfrom
devin/1777673893-fix-flexframe-cascade-lum-1341
May 1, 2026
Merged

fix(chat): eliminate _FlexFrameLayout alignment cascade in PinnedLatestTurnSection#29199
ashleeradka merged 5 commits into
mainfrom
devin/1777673893-fix-flexframe-cascade-lum-1341

Conversation

@ashleeradka
Copy link
Copy Markdown
Contributor

@ashleeradka ashleeradka commented May 1, 2026

Replaces .frame(minHeight:, alignment: .top) on PinnedLatestTurnSection with TopAlignedMinHeightLayout (Layout protocol) to eliminate the O(n × depth) explicitAlignment cascade causing 503+ Sentry hang/crash events (18+ users, including a fatal EXC_BAD_ACCESS). Also extends the CI FlexFrame lint to catch all six flexible frame parameters and updates AGENTS.md accordingly.


Why this change is needed

.frame(minHeight: viewportMinHeight, alignment: .top) at MessageListContentView.swift:498 creates _FlexFrameLayout, whose placeSubviews queries explicitAlignment on every descendant via commonPlacement → ViewDimensions[guide]. Inside PinnedLatestTurnSection, this cascades O(n × depth) through the entire response cluster on every layout pass — triggered by viewport height changes during streaming, composer resizes, and .transition(.move) animations.

Sentry evidence: 503 events across 5 issues (MACOS-63: 143, MACOS-33: 193, MACOS-50: 95, MACOS-F: 71, MACOS-RF: 1 fatal crash), active on v0.6.6.

Benefits

  • Performance: Eliminates the O(n × depth) alignment cascade, replacing it with O(1) placement via LayoutSubview.place(at:anchor:)
  • Prevention: CI lint now catches ALL six flexible frame parameters (minWidth, minHeight, maxWidth, maxHeight, idealWidth, idealHeight), not just max*
  • Clarity: AGENTS.md now correctly identifies the flexible frame overload as the source of _FlexFrameLayout, rather than listing specific parameter names that create a false sense of completeness

Why this is safe

  • Identical sizing: TopAlignedMinHeightLayout.sizeThatFits returns max(childHeight, minHeight) — same as .frame(minHeight:). The section still grows past the viewport for tall responses.
  • Identical placement: Child is pinned to bounds.origin with .topLeading anchor — same visual result as alignment: .top.
  • No alignment guide queries: Custom Layout implementations use place(at:anchor:) which resolves anchor from the child's known size as a UnitPoint — no alignment guide cascade. Returning nil from explicitAlignment tells ancestors "no explicit value; use default positioning."
  • Proven pattern: Mirrors BottomAlignedMinHeightLayout, FixedWidthLayout, and WidthCapLayout — all stable across multiple releases.
  • CI note: No macOS build in CI (no Xcode runner). Must verify locally: short response (spacer fills viewport), tall response (content scrollable), composer resize (no anchor clipping).

Alternatives NOT taken

  • Per-cell alignment barrier (AlignmentBarrierLayout wrapping each cell): Sound per Layout protocol, but unnecessary after removing the FlexFrame source. Previous attempts at barrier layouts between ScrollView and content broke scroll infrastructure — the barrier disrupted SwiftUI's internal scroll measurement. Cell-level barriers remain an option for defense-in-depth but are not needed for this fix.
  • .transition(.opacity) instead of .transition(.move()): Masks the cascade by avoiding uncached layout, but leaves the _FlexFrameLayout in place. AGENTS.md correctly documents this as a workaround, not a fix.
  • containerRelativeFrame(.vertical) directly on VStack: Caps section height at the viewport, making tall responses unreachable by scroll. This was the approach that created the need for .frame(minHeight:) in the first place — the current viewport-probe + minHeight pattern is load-bearing.

Root cause analysis

  1. How did the code get into this state? containerRelativeFrame(.vertical) was introduced to fix one-frame anchor clipping during composer resizes. A review correctly identified it would cap tall responses at viewport height. The follow-up fix switched to .frame(minHeight:, alignment: .top) to allow growth — but this creates _FlexFrameLayout, the same internal type responsible for the original hang events.

  2. What mistakes or decisions led to it? The team's mental model associated "FlexFrame danger" exclusively with maxWidth/maxHeight. AGENTS.md, CI lint, code reviews, and automated review bots all shared this blind spot. The flexible frame overload creates _FlexFrameLayout for ANY of its six parameters, but documentation only warned about two.

  3. Were there warning signs we missed? Yes — MessageListView.swift:187-189 already had a comment explaining why .frame(minHeight:alignment: .bottom) was replaced with BottomAlignedMinHeightLayout for this exact reason. The same pattern was introduced in PinnedLatestTurnSection without connecting the dots.

  4. What can we do to prevent this pattern from recurring? The CI lint regex now catches all six flexible frame parameters. AGENTS.md has been rewritten to warn about the flexible frame overload generically rather than listing specific parameter names, and explains why Layout protocol wrappers are the safe alternative (with Apple doc links for independent verification).

  5. AGENTS.md updates made in this PR: Rewrote the FlexFrame rule (line 304) to identify ALL parameters on the flexible frame overload as creating _FlexFrameLayout. Added TopAlignedMinHeightLayout and BottomAlignedMinHeightLayout to the safe alternatives list. Added an explanation of why Layout protocol wrappers are safe, citing LayoutSubview.place(at:anchor:) and the default explicitAlignment behavior. Updated the CI enforcement description to list all six parameters.

References

Prompt / plan

See root cause analysis above. Investigated as LUM-1341.

Test plan

  • CI FlexFrame lint passes with extended regex and updated allowlist
  • No macOS build in CI — verify locally in Xcode:
    • Short response: spacer fills viewport, anchor pinned to top
    • Tall response: content scrollable past viewport, no clipping
    • Composer resize: no anchor row clipping

… stop alignment cascade

Replace .frame(minHeight:, alignment: .top) on PinnedLatestTurnSection with
TopAlignedMinHeightLayout (Layout protocol). The flexible frame overload
creates _FlexFrameLayout whose placeSubviews queries explicitAlignment on
every descendant — O(n × depth) cascade causing 503+ Sentry hang/crash
events (MACOS-63, MACOS-33, MACOS-50, MACOS-F, MACOS-RF).

TopAlignedMinHeightLayout achieves identical sizing and placement via
place(at:anchor:) and returns nil from explicitAlignment, stopping the
cascade in O(1). Mirrors the proven BottomAlignedMinHeightLayout pattern.

Also extends check-flexframe.sh to catch min(Width|Height) alongside
max(Width|Height), and updates AGENTS.md to warn about ALL flexible frame
parameters — the gap that allowed this regression.

Fixes: LUM-1341
Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@linear
Copy link
Copy Markdown

linear Bot commented May 1, 2026

LUM-1341 App Hanging Regression: LazyVStack FlexFrame cascade (maxHeight/maxWidth) — LUM-835 reopened

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73c4f5bf9d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread clients/scripts/check-flexframe.sh Outdated
# Rust-regex compatible (no lookaround) so it works with ripgrep's default
# engine; we strip comment-only lines in a second pass below.
PATTERN='\.frame\(\s*max(Width|Height)\s*:'
PATTERN='\.frame\(\s*(max|min)(Width|Height)\s*:'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Expand FlexFrame lint to include idealWidth/idealHeight

PATTERN only matches min/maxWidth and min/maxHeight, so new usages like .frame(idealWidth:) or .frame(idealHeight:) in the scanned chat/window paths bypass this guard even though they use the same flexible-frame overload (frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:)) that the updated docs now classify as unsafe. In practice this leaves a hole where a future _FlexFrameLayout regression can land without CI failing.

Useful? React with 👍 / 👎.

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.

Good catch — addressed in f62f147. The regex now catches ideal(Width|Height) alongside min/max variants, closing the gap for all six flexible frame parameters.

All six parameters on the flexible frame overload create _FlexFrameLayout.
The regex now catches ideal* alongside min*/max* to close the remaining gap.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 7 additional findings in Devin Review.

Open in Devin Review

Comment thread clients/macos/AGENTS.md Outdated
**Enforced mechanically in CI**: [`clients/scripts/check-flexframe.sh`](../scripts/check-flexframe.sh) fails the build on new `.frame(maxWidth:)` / `.frame(maxHeight:)` inside `Features/Chat/`, `Features/Home/`, and `Features/MainWindow/`. Known intentional leaves are listed in [`clients/scripts/flexframe-allowlist.txt`](../scripts/flexframe-allowlist.txt) with rationale. The manual audit process missed regressions twice (#25947 wrong call on bounded `maxWidth`, #26220 deferred leaves) before the lint existed — prefer fixing the code over adding allowlist entries; the allowlist is a last resort.
**Why Layout protocol wrappers are safe**: custom [`Layout`](https://developer.apple.com/documentation/swiftui/layout) implementations use [`LayoutSubview.place(at:anchor:proposal:)`](https://developer.apple.com/documentation/swiftui/layoutsubview/place(at:anchor:proposal:)) for positioning, which resolves the anchor from the child's known size as a `UnitPoint` — no alignment guide queries. The [default `explicitAlignment`](https://developer.apple.com/documentation/swiftui/layout/explicitalignment(of:in:proposal:subviews:cache:)-8cl0p) merges all subviews' guides recursively; overriding it to return `nil` tells ancestors "no explicit value; use default positioning", blocking the cascade in O(1).

**Enforced mechanically in CI**: [`clients/scripts/check-flexframe.sh`](../scripts/check-flexframe.sh) fails the build on new `.frame(minWidth:)` / `.frame(minHeight:)` / `.frame(maxWidth:)` / `.frame(maxHeight:)` inside `Features/Chat/`, `Features/Home/`, and `Features/MainWindow/`. Known intentional usages are listed in [`clients/scripts/flexframe-allowlist.txt`](../scripts/flexframe-allowlist.txt). Prefer fixing the code over adding allowlist entries; the allowlist is a last resort.
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.

🟡 AGENTS.md CI enforcement description omits idealWidth/idealHeight despite script catching them

The CI enforcement description at line 318 lists only four flex-frame patterns (.frame(minWidth:) / .frame(minHeight:) / .frame(maxWidth:) / .frame(maxHeight:)) but the actual check-flexframe.sh regex (clients/scripts/check-flexframe.sh:72) is \.frame\(\s*(max|min|ideal)(Width|Height)\s*: which also catches .frame(idealWidth:) and .frame(idealHeight:). The rule description at clients/macos/AGENTS.md:304 correctly lists all six parameters, but the enforcement paragraph omits the two ideal* variants. An agent following the enforcement description would believe idealWidth/idealHeight are not CI-enforced and could be surprised by a lint failure.

Prompt for agents
In clients/macos/AGENTS.md line 318, the 'Enforced mechanically in CI' sentence lists .frame(minWidth:) / .frame(minHeight:) / .frame(maxWidth:) / .frame(maxHeight:) but omits .frame(idealWidth:) and .frame(idealHeight:). The CI script regex at clients/scripts/check-flexframe.sh:72 catches all six flex-frame parameters including ideal*. Add .frame(idealWidth:) and .frame(idealHeight:) to the enforcement description to match the actual CI behavior. The line is truncated in the diff but the full text should list all six patterns consistently with the rule description at line 304.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

Fixed in be267bd — added idealWidth/idealHeight to the CI enforcement description to match the actual regex.

@devin-ai-integration devin-ai-integration Bot changed the title fix(chat): replace _FlexFrameLayout with TopAlignedMinHeightLayout to stop alignment cascade fix(chat): eliminate _FlexFrameLayout alignment cascade in PinnedLatestTurnSection May 1, 2026
Copy link
Copy Markdown
Contributor

@vex-assistant-bot vex-assistant-bot Bot left a comment

Choose a reason for hiding this comment

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

APPROVE

Value: Fixes an O(n × depth) explicitAlignment cascade in PinnedLatestTurnSection that's actively generating 503 Sentry events across 18+ users including one fatal EXC_BAD_ACCESS — replacing the one remaining .frame(minHeight:, alignment: .top) in the pinned turn section with the same Layout-protocol pattern that already eliminated the cascade for bottom-aligned content (PR #28691).

What this does: Introduces TopAlignedMinHeightLayout (mirrors BottomAlignedMinHeightLayout), wires it into PinnedLatestTurnSection via .topAlignedMinHeight(), extends the CI FlexFrame lint to cover all six flexible frame params (not just max*), and updates AGENTS.md + SCROLL_STRATEGY.md.


Implementation is correct — three critical things I verified independently:

  1. Both explicitAlignment overrides return nil (horizontal and vertical). This is the only thing that actually stops the cascade. The default Layout extension merges all subview guides recursively — missing either override would still trigger the O(n × depth) walk. ✅

  2. placeSubviews re-measures with its own proposal parameter, not a stale one from sizeThatFits. The comment says "same proposal that sizeThatFits received" — technically placeSubviews receives its own ProposedViewSize argument, and the code correctly uses that. Using bounds.height would propose the expanded min-height back to the child and cause a size inconsistency SwiftUI detects and re-evaluates every frame. Code path is correct. (Minor comment wording nit, non-blocking.) ✅

  3. Placement via place(at: bounds.origin, anchor: .topLeading) correctly pins content to the top-left — equivalent to alignment: .top in the original .frame() call. ✅

CI lint extension is sound. All six flexible frame params (minWidth, minHeight, maxWidth, maxHeight, idealWidth, idealHeight) belong to the same overload that generates _FlexFrameLayout — the old max*-only pattern was always incomplete. The allowlist entries for ChatErrorToastView and ComposerView are legitimate: those minHeight uses are outside the LazyVStack ancestor chain. FlexFrame Lint CI is green. ✅

All bot-found issues addressed — Codex P2 (regex missing idealWidth/idealHeight), Devin --help truncation, Devin AGENTS.md description gap — all fixed and confirmed resolved by Devin's follow-up commits. ✅

Docs: AGENTS.md is now the most complete it's ever been on this topic — all six params documented, both the sizeThatFits (O(1)) vs placeSubviews (cascade via commonPlacement) distinction for .frame(width:) is explicit, and the "why Layout wrappers are safe" explanation is solid reference material for future Devin briefings.

One non-blocking observation: the no-subviews fallback in sizeThatFits (guard let child = subviews.first else { ... }) returns proposal.replacingUnspecifiedDimensions().width which is 10.0 for nil — but PinnedLatestTurnSection always has a child so this path is dead code in practice. Fine as-is.

@ashleeradka ashleeradka merged commit cd4b63a into main May 1, 2026
8 checks passed
@ashleeradka ashleeradka deleted the devin/1777673893-fix-flexframe-cascade-lum-1341 branch May 1, 2026 23:01
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.

1 participant