perf(chat): apply .fixedWidth() directly in centeredChatColumn (follow-up to #29231)#29232
Conversation
…th banner explicitly
Apply .fixedWidth(width) directly to content in centeredChatColumn,
mirroring MessageListView's pattern. Wrap CompactionCircuitOpenBanner
(the only natural-width caller) in HStack { Spacer; banner; Spacer }
at its call site so it stays centered. The other five callers are
horizontally flexible and fill the column on their own.
Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
There was a problem hiding this comment.
✦ APPROVE
Value: Tightens the fix from #29231 — eliminates the inner HStack { Spacer; content; Spacer } in favour of applying .fixedWidth(width) directly to content(), matching the MessageListView precedent. Cleaner and semantically unambiguous.
The argument against #29231's inner-Spacer pattern holds:
Inside a .fixedWidth(width) block, an HStack containing two Spacer(minLength: 0) siblings alongside flexible content has order-dependent distribution. The "inner spacers collapse to zero" claim is not a documented invariant. Valid concern.
New pattern is correct:
content().fixedWidth(width) passes exactly width as the proposal to the single child. FixedWidthLayout.sizeThatFits returns (width, childHeight). placeSubviews anchors at .topLeading — no guide query. Flexible content fills width; natural-width content sits at top-leading. Outer HStack { Spacer; ...; Spacer } handles horizontal centering on the page. Exact match to MessageListView.swift:147-182.
CompactionCircuitOpenBanner natural-width claim verified:
Read the source. HStack(spacing: VSpacing.sm) { VIconView; Text } — no Spacer, no .frame(maxWidth: .infinity). Genuinely natural-width; the explicit centering wrap at its call site is correct and necessary.
All checks green. Devin not yet reviewed — Vex + CI sufficient here given this is a one-function cleanup on a single-file diff authored by Boss.
What
Drops the inner
HStack { Spacer; content; Spacer }wrapper fromcenteredChatColumn, applying.fixedWidth(width)directly tocontent()— matching the established pattern inMessageListView.swift:147-182. Adds an explicit centering wrap atCompactionCircuitOpenBanner's call site, the only natural-width caller.Why
Follow-up to #29231. The previous shape was:
Inside the inner
.fixedWidth(width)HStack, content with.frame(maxWidth: .infinity)or an internalSpaceris layout-flexible.Spaceris also flexible. SwiftUI's HStack distributes the proposedwidthamong children sorted by flexibility; with twoSpacer(minLength: 0)siblings sharing the proposal alongside flexible content, the result is order-dependent and not a documented invariant. The previous claim — "inner spacers collapse to zero and content fills the full column" — relies on undocumented HStack distribution behavior.MessageListView.swift:147-182already solved the same problem differently: apply.fixedWidth(W)directly to the content (scrollViewContent.fixedWidth(widths.chatColumnWidth)), and rely onFixedWidthLayout's contract —sizeThatFitsreturns exactlyW, andplaceSubviewsproposes(W, height)to the single child. The child either fillsW(flexible) or sits at top-leading (natural-width). No competition with sibling Spacers.This PR adopts the MessageListView pattern in
centeredChatColumn. Of the six callers, five are already horizontally flexible and fill the column on their own:CreditsExhaustedBanner— inner VStack.frame(maxWidth: .infinity, alignment: .leading)(ChatErrorToastView.swift:223)DiskPressureBanner—Spacer(minLength: VSpacing.lg)between content and buttons (ChatErrorToastView.swift:263)MissingApiKeyBanner—VButton.frame(maxWidth: .infinity)(ChatErrorToastView.swift:405)RecoveryModeBanner— inner VStack.frame(maxWidth: .infinity, alignment: .leading)(RecoveryModeBanner.swift:55)ComposerSection—ComposerView.frame(maxWidth: .infinity)HStack { Spacer; Icon; Text; Spacer }(already self-centers)CompactionCircuitOpenBanneris the lone natural-width caller — a tight pill with icon + text and no horizontal flex (ChatErrorToastView.swift:339). The previous.frame(width:)semantics centered it implicitly; underFixedWidthLayout'stopLeadingplacement it would render flush-left within the column. The fix wraps it explicitly at its call site so it stays centered — a more honest expression of the intent.Benefits
MessageListViewandcenteredChatColumnnow use the same shape —Spacer + content.fixedWidth(W) + Spacerfor page-level centering, with internal positioning handled at each call site as needed.FixedWidthLayoutproposes exactlywidthto its single child; the result is determined by the child's own size policy.Safety
widthto content that fills horizontally.CompactionCircuitOpenBanner: now wrapped at call site withHStack { Spacer; banner; Spacer }. Banner is non-flexible, Spacers split surplus, banner stays centered. Visually identical to pre-perf(chat): use FixedWidthLayout for centeredChatColumn (LUM-1276) #29231 behavior._FrameLayoutor_FlexFrameLayoutintroduced.Alternatives considered
FixedWidthLayoutaccept a placement anchor parameter. Adds API surface to a primitive that intentionally exposes only width. Doesn't help: natural-width content centered inside a wider region would still need spacers somewhere. Rejected..frame(maxWidth: .infinity)toCompactionCircuitOpenBanner's outer HStack. Creates_FlexFrameLayout— the exact anti-pattern this layout family is replacing perclients/macos/AGENTS.mdlines 304-309. Rejected.CompactionCircuitOpenBanneritself. Would change its visual from "tight pill at center" to "bar that spans the column with content leading-aligned." Different design intent. Rejected.References
Layout.placeSubviews— single-child placement contract.Layout.explicitAlignment— returningnilopts out of alignment-guide cascade.clients/shared/DesignSystem/Modifiers/FixedWidthLayout.swift— reference implementation.clients/macos/vellum-assistant/Features/Chat/MessageListView.swift:147-182— established pattern.Root cause analysis
.frame(width:)with.fixedWidth(...)insidecenteredChatColumn, then added an innerHStack { Spacer; content; Spacer }to preserve the implicit.centeralignment that.frame(width:)provided. The chosen shape works for fixed-size content but introduces an order-dependent behavior for flexible content..frame(width:)'s alignment-as-default as needing direct reproduction inside the helper, instead of pushing it to the one caller that actually needed it. The existingMessageListViewpattern (direct.fixedWidth()) was visible but not followed.MessageListView's pattern as the canonical shape for chat-column wrappers. When introducing a Layout primitive, keep its contract narrow (one input, one output) and push positioning concerns to call sites.clients/macos/AGENTS.mdlines 304-309 already prescribe.fixedWidth()over.frame(width:). The fix is to apply that rule consistently, not add more text.Prompt / plan
Follow-up to PR #29231 addressing review feedback flagging the inner-Spacer competition with flexible content.
Test plan
centeredChatColumncall sites (5 flexible banners + composer + sentinel + natural-widthCompactionCircuitOpenBanner).Link to Devin session: https://app.devin.ai/sessions/9a108e31a87d4ebcb19aeefaff7f529a
Requested by: @ashleeradka