Skip to content

LUM-799: Replace GeometryReader with .onGeometryChange(for:) in ChatView#24423

Merged
ashleeradka merged 2 commits into
mainfrom
devin/1775679368-replace-geometryreader-ongeometrychange
Apr 8, 2026
Merged

LUM-799: Replace GeometryReader with .onGeometryChange(for:) in ChatView#24423
ashleeradka merged 2 commits into
mainfrom
devin/1775679368-replace-geometryreader-ongeometrychange

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

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

Summary

Replaces the GeometryReader wrapper in ChatView.body with a @State property + .onGeometryChange(for:) modifier to eliminate 48–83 second performance hangs.

GeometryReader as a layout container creates a GeometryReaderLayout node whose child measurement runs during placeSubviews. Any attribute graph invalidation (hover, window focus, animation tick) forces a full O(n × depth) re-measurement cascade through the LazyVStack. .onGeometryChange is a post-layout observer — it reads geometry after layout completes without participating in the measurement chain, and its action only fires when the extracted value actually changes (Apple docs).

The downstream data flow is unchanged: containerWidth still flows through mainContentStackactiveConversationContentMessageListViewbubbleMaxWidth environment key.

Because @State containerWidth initializes to 0 (unlike GeometryReader which provided the real width from the first render), the first .onGeometryChange callback produces a 0→real transition. Without mitigation, this would trigger spurious scroll resize stabilization during initial load. A guard in handleContainerWidthChanged() now absorbs this first measurement (when lastHandledContainerWidth == 0) by recording the width without starting stabilization. Subsequent real resizes still trigger normally.

Review & Testing Checklist for Human

  • Build in Xcode — CI skips all macOS checks; this must be verified locally
  • Test window resizing — resize the window (including very narrow < 808px) and verify chat bubbles reflow correctly, scroll position is maintained, and no hangs occur
  • Hover/focus stress test — hover over messages, switch window focus back and forth rapidly, and confirm the UI stays responsive (this was the original hang trigger)
  • Verify initial scroll position on app launch — open a long conversation from cold start and confirm it scrolls to bottom without a stutter. The new lastHandledContainerWidth > 0 guard skips stabilization on first mount, so verify this doesn't regress the initial-load scroll-to-bottom behavior
  • Conversation switching — switch between conversations and verify scroll position resets correctly. handleConversationSwitched() seeds lastHandledContainerWidth = containerWidth — confirm this still works when the width was previously recorded

Recommended test plan: Open a long conversation, resize the window several times (including very narrow), hover over messages, switch window focus, switch conversations, and verify no hangs or scroll position issues.

Notes

  • .onGeometryChange(for:of:action:) requires macOS 14+; deployment target is macOS 15.0 — safe
  • This reverts the approach from commit bf7d13875 (PR refactor: ChatView uses GeometryReader instead of @State for container width #23855) which introduced the GeometryReader, contradicting AGENTS.md guidance. The @State write concern that motivated that commit is moot because .onGeometryChange deduplicates via Equatable
  • Already used in ~12 other places in the codebase
  • For narrow windows (< 808px), the first frame renders bubbles at the 760px default before .onGeometryChange fires with the real width — verify whether this causes a visible layout jump

Link to Devin session: https://app.devin.ai/sessions/14a69bbf31b145c987e81d2aee0c2612
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

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: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@devin-ai-integration devin-ai-integration Bot force-pushed the devin/1775679368-replace-geometryreader-ongeometrychange branch from cd0cf90 to d63db85 Compare April 8, 2026 20:59
GeometryReader at ChatView.swift:124 wrapped the entire chat view
hierarchy as a layout container, forcing full O(n × depth) measurement
cascades through LazyVStack on every attribute graph dirty (hover,
window focus, animation tick), causing 48-83 second hangs.

Replace with @State containerWidth + .onGeometryChange(for:) modifier,
which observes geometry after layout completes without creating a
layout node that participates in measurement. The action closure only
fires when the extracted width value actually changes (built-in
deduplication via Equatable), so hover/focus events cause zero state
writes.

Data flow is unchanged: containerWidth flows through
mainContentStack(containerWidth:) -> activeConversationContent ->
MessageListView -> bubbleMaxWidth environment key.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@devin-ai-integration devin-ai-integration Bot force-pushed the devin/1775679368-replace-geometryreader-ongeometrychange branch from d63db85 to bfbff69 Compare April 8, 2026 21:04
devin-ai-integration[bot]

This comment was marked as resolved.

ashleeradka
ashleeradka previously approved these changes Apr 8, 2026
Copy link
Copy Markdown
Contributor

@ashleeradka ashleeradka left a comment

Choose a reason for hiding this comment

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

✦ Reviewed ChatView.swift in full context.

GeometryReader removed, replaced with @State containerWidth + .onGeometryChange(for: CGFloat.self). containerWidth=0 initial state handled gracefully — existing ternary in MessageListContentView falls back to VSpacing.chatBubbleMaxWidth until first callback. Same behavior as before.

Clean single-file change, no conflicts with main.

Ship it.

With @State containerWidth starting at 0, the first .onGeometryChange
callback triggers handleContainerWidthChanged() which would start a
spurious 100ms resize stabilization during initial load. This never
happened with GeometryReader because containerWidth was always the
real width from the first render.

Add an early return when lastHandledContainerWidth is 0, recording the
width without triggering stabilization. Subsequent real resizes still
trigger normally.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@ashleeradka ashleeradka merged commit ff9d30c into main Apr 8, 2026
6 checks passed
@ashleeradka ashleeradka deleted the devin/1775679368-replace-geometryreader-ongeometrychange branch April 8, 2026 21:23
devin-ai-integration Bot added a commit that referenced this pull request Apr 9, 2026
Replace .frame(maxWidth:).frame(maxWidth: .infinity) patterns with
.frame(width: min(containerWidth, maxWidth)) in ChatView banners,
ComposerView, and ChatLoadingSkeleton. These FlexFrame modifiers in the
VStack siblings of MessageListView triggered explicitAlignment queries
that cascaded through _FrameLayout into the LazyVStack subtree, causing
77s hangs (Spindump-4, build BE7C2D43).

Changes:
- ChatView: Replace 3 banner .frame(maxWidth:) pairs with .frame(width:)
- ChatView: Replace read-only label .frame(maxWidth: .infinity) with Spacer centering
- ComposerView: Replace .frame(maxWidth:).frame(maxWidth: .infinity) with .frame(width:)
- ComposerSection: Thread containerWidth from ChatView
- ChatLoadingSkeleton: Accept containerWidth and use .frame(width:)

containerWidth is already captured via .onGeometryChange in ChatView
(PR #24423) and threaded through activeConversationContent. This extends
that flow to ComposerSection -> ComposerView and ChatLoadingSkeleton.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
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