-
Notifications
You must be signed in to change notification settings - Fork 88
Eliminate explicitAlignment recursion in message list layout (LUM-800) #24446
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c2fd37b
94bc4b1
3860b6d
26efc98
6948107
30d1feb
7cc3fee
ed8efb7
5b6f926
36fee0e
50d7676
ae63d5e
b6229e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -266,22 +266,21 @@ struct ChatBubble: View, Equatable { | |
| func bubbleChrome<Content: View>(@ViewBuilder _ content: () -> Content) -> some View { | ||
| let isPlainAssistant = !isUser && !message.isError | ||
| if message.isError { | ||
| // Error: chrome padding + full-width inner expansion frame. | ||
| // ⚠️ No .frame(maxWidth:) in LazyVStack cells — see AGENTS.md. | ||
| // .containerRelativeFrame resolves against the ScrollView for full-width error background. | ||
| content() | ||
| .padding(EdgeInsets(top: VSpacing.md, leading: VSpacing.lg, | ||
| bottom: VSpacing.md, trailing: VSpacing.lg)) | ||
| .frame(maxWidth: .infinity) | ||
| .containerRelativeFrame(.horizontal) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 The error bubble branch in Layout chain showing the mismatchScrollView (width W) → AlignmentBarrierLayout → LazyVStack.padding(.horizontal, VSpacing.xl) → cell proposed (W − 48pt) → HStack → VStack → bubbleChrome → .containerRelativeFrame(.horizontal) resolves to W (not W − 48pt) Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a known tradeoff already documented in the PR's testing checklist. The alternatives were evaluated:
Option 3 is the least-bad choice. The |
||
| .background { | ||
| bubbleChromeBackground | ||
| } | ||
| .frame(maxWidth: .infinity, alignment: isUser ? .trailing : .leading) | ||
| } else if isPlainAssistant { | ||
| // Plain assistant: no chrome padding, no inner frame. | ||
| content() | ||
| .background { | ||
| bubbleChromeBackground | ||
| } | ||
| .frame(maxWidth: bubbleMaxWidth, alignment: .leading) | ||
| } else { | ||
| // User messages (non-error): chrome padding, no inner frame. | ||
| content() | ||
|
|
@@ -290,7 +289,6 @@ struct ChatBubble: View, Equatable { | |
| .background { | ||
| bubbleChromeBackground | ||
| } | ||
| .frame(maxWidth: bubbleMaxWidth, alignment: .trailing) | ||
| } | ||
|
ashleeradka marked this conversation as resolved.
|
||
| } | ||
|
|
||
|
|
@@ -341,12 +339,15 @@ struct ChatBubble: View, Equatable { | |
| let _ = os_signpost(.event, log: PerfSignposts.log, name: "chatBubbleBody", | ||
| "id=%{public}s streaming=%d", message.id.uuidString, message.isStreaming ? 1 : 0) | ||
| #endif | ||
| // Outer VStack ensures a single resolved subview for the parent | ||
| // LazyVStack, avoiding duplicate .id(message.id) from MessageCellView | ||
| // that caused incorrect width proposals at narrow window sizes (LUM-688). | ||
| // The avatar sits outside the inner .compositingGroup() scope so | ||
| // CAShapeLayer animations (breathing, blink, twitch) are unaffected. | ||
| VStack(alignment: isUser ? .trailing : .leading, spacing: VSpacing.sm) { | ||
| // ⚠️ No .frame(maxWidth:) in LazyVStack cells — see AGENTS.md. | ||
| HStack(spacing: 0) { | ||
| if isUser { Spacer(minLength: 0) } | ||
| // Outer VStack ensures a single resolved subview for the parent | ||
| // LazyVStack, avoiding duplicate .id(message.id) from MessageCellView | ||
| // that caused incorrect width proposals at narrow window sizes (LUM-688). | ||
| // The avatar sits outside the inner .compositingGroup() scope so | ||
| // CAShapeLayer animations (breathing, blink, twitch) are unaffected. | ||
| VStack(alignment: isUser ? .trailing : .leading, spacing: VSpacing.sm) { | ||
| // --- Message content (composited) --- | ||
| VStack(alignment: isUser ? .trailing : .leading, spacing: VSpacing.sm) { | ||
| if !isUser && cachedHasInterleavedContent { | ||
|
|
@@ -425,8 +426,9 @@ struct ChatBubble: View, Equatable { | |
| if isLatestAssistantMessage && !isUser && !hideInlineAvatar { | ||
| inlineAvatar | ||
| } | ||
| } | ||
| if !isUser { Spacer(minLength: 0) } | ||
| } | ||
| .frame(maxWidth: .infinity, alignment: isUser ? .trailing : .leading) | ||
| .contentShape(Rectangle()) | ||
| .onChange(of: message.contentOrder) { _, _ in recomputeInterleavedContentCache() } | ||
| .onChange(of: message.textSegments) { _, _ in recomputeInterleavedContentCache() } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -71,14 +71,17 @@ struct MarkdownSegmentView: View, Equatable { | |
| ) | ||
| #else | ||
| let attributed = buildCombinedAttributedString(from: runSegments) | ||
| Text(attributed) | ||
| .font(chatFont) | ||
| .lineSpacing(4) | ||
| .foregroundStyle(textColor) | ||
| .tint(tintColor) | ||
| .optionalMaxWidth(maxContentWidth) | ||
| .lineLimit(nil) | ||
| .fixedSize(horizontal: false, vertical: true) | ||
| // ⚠️ No .frame(maxWidth:) in LazyVStack cells — see AGENTS.md. | ||
| HStack(spacing: 0) { | ||
| Text(attributed) | ||
| .font(chatFont) | ||
| .lineSpacing(4) | ||
| .foregroundStyle(textColor) | ||
| .tint(tintColor) | ||
| .lineLimit(nil) | ||
| .fixedSize(horizontal: false, vertical: true) | ||
| Spacer(minLength: 0) | ||
| } | ||
| #endif | ||
|
|
||
| case .codeBlock(let language, let code): | ||
|
|
@@ -904,12 +907,13 @@ private extension AttributedString { | |
| // MARK: - Optional Max Width | ||
|
|
||
| private extension View { | ||
| /// Applies `.frame(maxWidth:alignment:)` only when a width is provided. | ||
| /// Applies a definite `.frame(width:)` only when a width is provided. | ||
| /// When `nil`, no frame is applied — the view shrink-wraps to its content. | ||
| /// ⚠️ No `.frame(maxWidth:)` — see AGENTS.md. | ||
| @ViewBuilder | ||
| func optionalMaxWidth(_ width: CGFloat?) -> some View { | ||
| if let width { | ||
| self.frame(maxWidth: width, alignment: .leading) | ||
| self.frame(width: width, alignment: .leading) | ||
|
ashleeradka marked this conversation as resolved.
|
||
| } else { | ||
| self | ||
| } | ||
|
Comment on lines
914
to
919
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 The This affects code blocks (line 869) and horizontal rules (line 110). For code blocks, this means they will always be exactly (Refers to lines 914-920) Was this helpful? React with 👍 or 👎 to provide feedback.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged — the function name A rename to |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚩 Remaining
.frame(maxWidth:)usages in non-modified filesThe PR removes
.frame(maxWidth:)from LazyVStack cell hierarchies in the modified files, but several unmodified files still use the pattern within the same LazyVStack:ChatBubbleAttachmentContent.swift:144,269,408,ChatLoadingSkeleton.swift:37,45,ChatEmptyStateView.swift:124,154,167,400. If the AGENTS.md rule about no.frame(maxWidth:)in LazyVStack cells is to be enforced consistently, these would need follow-up. However, attachment views with fixed max widths (notalignment:parameter) may produce_FlexFrameLayoutwithout the problematic alignment queries, so the performance impact may vary.Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are all outside the scope of this PR and protected by the
AlignmentBarrierLayoutbarrier — alignment queries from the outer.frame(maxWidth:)chain stop at the barrier and never reach the LazyVStack. The remaining sites inChatBubbleAttachmentContent,ChatLoadingSkeleton, andChatEmptyStateViewcan be addressed in a follow-up if spindump shows any residual hits after the barrier is verified.