diff --git a/clients/macos/AGENTS.md b/clients/macos/AGENTS.md index 7d9c5efe75f..ef0a203f0d6 100644 --- a/clients/macos/AGENTS.md +++ b/clients/macos/AGENTS.md @@ -252,6 +252,14 @@ All design system types use the `V` prefix (VButton, VColor, VFont, etc.). Alway - **Background**: Use a single `.background { }` with a `ZStack` inside instead of chaining multiple `.background()` calls. - **No-op backgrounds**: Never add invisible backgrounds like `.background(Capsule().fill(Color.clear))` — they create layout wrappers with zero visual effect. - **No animated insertions in chat `LazyVStack`**: ANY animated insertion/removal in a `LazyVStack` triggers `motionVectors` — an O(n) `sizeThatFits` measurement over ALL children that defeats lazy loading and causes multi-minute hangs. The chat message list uses `.transaction { $0.animation = nil }` to suppress all insertion animations. Do NOT remove that modifier or wrap content mutations in `withAnimation` that flows into the `LazyVStack`. See [`.transaction` docs](https://developer.apple.com/documentation/swiftui/view/transaction(_:)) and [WWDC23: Demystify SwiftUI performance](https://developer.apple.com/videos/play/wwdc2023/10160/). +- **No `.frame(maxWidth:, alignment:)` inside LazyVStack cell hierarchy**: `.frame(maxWidth:)` creates [`_FlexFrameLayout`](https://developer.apple.com/documentation/swiftui/view/frame(minwidth:idealwidth:maxwidth:minheight:idealheight:maxheight:alignment:)) whose `placement()` queries each child's explicit alignment via [`ViewDimensions.subscript`](https://developer.apple.com/documentation/swiftui/viewdimensions). Nested FlexFrames recurse O(depth × children) per layout pass. **This includes `.frame(maxWidth: X)` with no explicit alignment** — it defaults to `.center`, still triggering the query. See [WWDC23: Demystify SwiftUI performance](https://developer.apple.com/videos/play/wwdc2023/10160/). Safe alternatives: + - `.frame(width: exactWidth)` — [`_FrameLayout`](https://developer.apple.com/documentation/swiftui/view/frame(width:height:alignment:)), no alignment query. + - `HStack { content; Spacer(minLength: 0) }` — leading alignment without queries. + - `HStack { Spacer(minLength: 0); content }` — trailing alignment without queries. + - [`.containerRelativeFrame(.horizontal)`](https://developer.apple.com/documentation/swiftui/view/containerrelativeframe(_:alignment:)) — width constraint without FlexFrame. + + Never trade `HStack+Spacer` for `.frame(alignment:)` in lazy containers — fewer layout nodes is not worth O(n) recursive alignment queries per node. +- **`AlignmentBarrierLayout` for LazyVStack protection**: Wrap content inside a `ScrollView` that contains a `LazyVStack` with `AlignmentBarrierLayout { ... }` to block `explicitAlignment` queries from cascading into the lazy container. The barrier returns `nil` for all alignment queries while passing through sizing and placement unchanged. This is a defense-in-depth measure — even if `.frame(maxWidth:, alignment:)` modifiers exist above the barrier, the alignment cascade stops at the barrier and never reaches the `LazyVStack`. See [`Layout.explicitAlignment`](https://developer.apple.com/documentation/swiftui/layout/explicitalignment(of:in:proposal:subviews:cache:)-3iqmu) and `clients/shared/DesignSystem/Layout/AlignmentBarrierLayout.swift`. - **Gallery**: When adding or modifying a design system primitive/component, update the corresponding Gallery section file (`Gallery/Sections/`) so the visual catalog stays current. - **Accessibility**: See `clients/AGENTS.md` § [Accessibility](../AGENTS.md#accessibility) for the full checklist (labels, hidden elements, custom interactions, AppKit panels). All rules there apply to macOS components. diff --git a/clients/macos/vellum-assistant/Features/Chat/AssistantProgressView.swift b/clients/macos/vellum-assistant/Features/Chat/AssistantProgressView.swift index 13919be4819..de98771f7c4 100644 --- a/clients/macos/vellum-assistant/Features/Chat/AssistantProgressView.swift +++ b/clients/macos/vellum-assistant/Features/Chat/AssistantProgressView.swift @@ -399,7 +399,6 @@ struct AssistantProgressView: View { .buttonStyle(.plain) .environment(\.isEnabled, true) .padding(EdgeInsets(top: VSpacing.xs, leading: VSpacing.sm, bottom: VSpacing.xs, trailing: VSpacing.sm)) - .frame(maxWidth: .infinity, alignment: .leading) } @ViewBuilder @@ -882,9 +881,12 @@ private struct StepDetailRow: View { isError: Bool = false ) -> some View { ZStack(alignment: .topTrailing) { - outputTextView(text: text, attributedText: attributedText, isError: isError) + // ⚠️ No .frame(maxWidth:) in LazyVStack cells — see AGENTS.md. + HStack(spacing: 0) { + outputTextView(text: text, attributedText: attributedText, isError: isError) + Spacer(minLength: 0) + } .padding(EdgeInsets(top: VSpacing.sm, leading: VSpacing.sm, bottom: VSpacing.sm, trailing: VSpacing.sm + VSpacing.xl)) - .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: VRadius.sm) .fill(VColor.surfaceOverlay.opacity(0.6)) @@ -923,17 +925,22 @@ private struct StepDetailRow: View { attributedText: AttributedString?, isError: Bool = false ) -> some View { + // ⚠️ No .frame(maxWidth:) in LazyVStack cells — see AGENTS.md. if let attrText = attributedText { - Text(attrText) - .font(VFont.bodySmallDefault) - .frame(maxWidth: .infinity, alignment: .leading) - .fixedSize(horizontal: false, vertical: true) + HStack(spacing: 0) { + Text(attrText) + .font(VFont.bodySmallDefault) + .fixedSize(horizontal: false, vertical: true) + Spacer(minLength: 0) + } } else if let plainText = text { - Text(plainText) - .font(VFont.bodySmallDefault) - .foregroundStyle(isError ? VColor.systemNegativeStrong : VColor.contentSecondary) - .frame(maxWidth: .infinity, alignment: .leading) - .fixedSize(horizontal: false, vertical: true) + HStack(spacing: 0) { + Text(plainText) + .font(VFont.bodySmallDefault) + .foregroundStyle(isError ? VColor.systemNegativeStrong : VColor.contentSecondary) + .fixedSize(horizontal: false, vertical: true) + Spacer(minLength: 0) + } } } diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatBubble.swift b/clients/macos/vellum-assistant/Features/Chat/ChatBubble.swift index 6378ac2de3f..e9dff3f7480 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatBubble.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatBubble.swift @@ -266,22 +266,21 @@ struct ChatBubble: View, Equatable { func bubbleChrome(@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) .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) } } @@ -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() } diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatBubbleInterleavedContent.swift b/clients/macos/vellum-assistant/Features/Chat/ChatBubbleInterleavedContent.swift index 14cee512dc7..10f4a436bc2 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatBubbleInterleavedContent.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatBubbleInterleavedContent.swift @@ -326,25 +326,27 @@ extension ChatBubble { return result }() - AssistantProgressView( - toolCalls: groupedToolCalls, - isStreaming: isLatestGroup ? message.isStreaming : false, - hasText: hasTrailingText, - isProcessing: isLatestGroup && isProcessingAfterTools, - processingStatusText: isLatestGroup && isProcessingAfterTools ? processingStatusText : nil, - streamingCodePreview: isLatestGroup ? message.streamingCodePreview : nil, - streamingCodeToolName: isLatestGroup ? message.streamingCodeToolName : nil, - decidedConfirmations: groupConfirmations, - onRehydrate: onRehydrate, - onConfirmationAllow: onConfirmationAllow, - onConfirmationDeny: onConfirmationDeny, - onAlwaysAllow: onAlwaysAllow, - onTemporaryAllow: onTemporaryAllow, - activeConfirmationRequestId: activeConfirmationRequestId, - progressUIState: $progressUIState - ) - .frame(maxWidth: VSpacing.chatBubbleMaxWidth, alignment: .leading) - + // ⚠️ No .frame(maxWidth:) in LazyVStack cells — see AGENTS.md. + HStack(spacing: 0) { + AssistantProgressView( + toolCalls: groupedToolCalls, + isStreaming: isLatestGroup ? message.isStreaming : false, + hasText: hasTrailingText, + isProcessing: isLatestGroup && isProcessingAfterTools, + processingStatusText: isLatestGroup && isProcessingAfterTools ? processingStatusText : nil, + streamingCodePreview: isLatestGroup ? message.streamingCodePreview : nil, + streamingCodeToolName: isLatestGroup ? message.streamingCodeToolName : nil, + decidedConfirmations: groupConfirmations, + onRehydrate: onRehydrate, + onConfirmationAllow: onConfirmationAllow, + onConfirmationDeny: onConfirmationDeny, + onAlwaysAllow: onAlwaysAllow, + onTemporaryAllow: onTemporaryAllow, + activeConfirmationRequestId: activeConfirmationRequestId, + progressUIState: $progressUIState + ) + Spacer(minLength: 0) + } } } diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatBubbleToolStatusView.swift b/clients/macos/vellum-assistant/Features/Chat/ChatBubbleToolStatusView.swift index 216b7e2c770..418dc0625c1 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatBubbleToolStatusView.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatBubbleToolStatusView.swift @@ -35,35 +35,39 @@ extension ChatBubble { if hasToolCalls || hasStreamingCode || shouldShowProcessing { // Unified progress view handles all tool/streaming/processing states - AssistantProgressView( - toolCalls: message.toolCalls, - isStreaming: message.isStreaming, - hasText: hasText, - isProcessing: shouldShowProcessing, - processingStatusText: shouldShowProcessing ? processingStatusText : nil, - streamingCodePreview: message.streamingCodePreview, - streamingCodeToolName: message.streamingCodeToolName, - decidedConfirmations: effectiveConfirmations, - onRehydrate: onRehydrate, - onConfirmationAllow: onConfirmationAllow, - onConfirmationDeny: onConfirmationDeny, - onAlwaysAllow: onAlwaysAllow, - onTemporaryAllow: onTemporaryAllow, - activeConfirmationRequestId: activeConfirmationRequestId, - progressUIState: $progressUIState - ) - .frame(maxWidth: VSpacing.chatBubbleMaxWidth, alignment: .leading) + // ⚠️ No .frame(maxWidth:) in LazyVStack cells — see AGENTS.md. + HStack(spacing: 0) { + AssistantProgressView( + toolCalls: message.toolCalls, + isStreaming: message.isStreaming, + hasText: hasText, + isProcessing: shouldShowProcessing, + processingStatusText: shouldShowProcessing ? processingStatusText : nil, + streamingCodePreview: message.streamingCodePreview, + streamingCodeToolName: message.streamingCodeToolName, + decidedConfirmations: effectiveConfirmations, + onRehydrate: onRehydrate, + onConfirmationAllow: onConfirmationAllow, + onConfirmationDeny: onConfirmationDeny, + onAlwaysAllow: onAlwaysAllow, + onTemporaryAllow: onTemporaryAllow, + activeConfirmationRequestId: activeConfirmationRequestId, + progressUIState: $progressUIState + ) + Spacer(minLength: 0) + } // Inline image previews from completed tool calls (e.g. image generation) inlineToolCallImages(from: message.toolCalls) } else if !effectiveConfirmations.isEmpty, !inlineToolProgressRenderedInContent { // No tool display needed — only show permission chips. + // ⚠️ No .frame(maxWidth:) in LazyVStack cells — see AGENTS.md. HStack(alignment: .center, spacing: VSpacing.sm) { ForEach(Array(effectiveConfirmations.enumerated()), id: \.offset) { _, confirmation in compactPermissionChip(confirmation) } + Spacer(minLength: 0) } - .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, VSpacing.xxs) } } diff --git a/clients/macos/vellum-assistant/Features/Chat/ChatMarkdownParser.swift b/clients/macos/vellum-assistant/Features/Chat/ChatMarkdownParser.swift index 4d3a8fa3e05..71401f51f63 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ChatMarkdownParser.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ChatMarkdownParser.swift @@ -355,13 +355,14 @@ struct MarkdownTableView: View { // Header row HStack(spacing: 0) { ForEach(Array(headers.enumerated()), id: \.offset) { _, header in - Text(header) - .font(VFont.labelDefault) - .foregroundStyle(VColor.contentSecondary) - .textSelection(.enabled) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, VSpacing.sm) - .padding(.vertical, VSpacing.sm) + HStack(spacing: 0) { + Text(header) + .font(VFont.labelDefault) + .foregroundStyle(VColor.contentSecondary) + .textSelection(.enabled) + Spacer(minLength: 0) + } + .padding(VSpacing.sm) } } @@ -371,10 +372,11 @@ struct MarkdownTableView: View { ForEach(Array(rows.enumerated()), id: \.offset) { rowIdx, row in HStack(spacing: 0) { ForEach(Array(row.enumerated()), id: \.offset) { _, cell in - inlineMarkdownCell(cell) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, VSpacing.sm) - .padding(.vertical, VSpacing.sm) + HStack(spacing: 0) { + inlineMarkdownCell(cell) + Spacer(minLength: 0) + } + .padding(VSpacing.sm) } } if rowIdx < rows.count - 1 { @@ -388,7 +390,8 @@ struct MarkdownTableView: View { RoundedRectangle(cornerRadius: VRadius.md) .stroke(VColor.borderBase, lineWidth: 0.5) ) - .frame(maxWidth: maxWidth, alignment: .leading) + // ⚠️ No .frame(maxWidth:) in LazyVStack cells — see AGENTS.md. + .frame(width: maxWidth.isFinite ? maxWidth : nil, alignment: .leading) } private func inlineMarkdownCell(_ text: String) -> some View { diff --git a/clients/macos/vellum-assistant/Features/Chat/MarkdownSegmentView.swift b/clients/macos/vellum-assistant/Features/Chat/MarkdownSegmentView.swift index 98c62bb9dcf..ee6850108a6 100644 --- a/clients/macos/vellum-assistant/Features/Chat/MarkdownSegmentView.swift +++ b/clients/macos/vellum-assistant/Features/Chat/MarkdownSegmentView.swift @@ -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) } else { self } diff --git a/clients/macos/vellum-assistant/Features/Chat/MessageCellView.swift b/clients/macos/vellum-assistant/Features/Chat/MessageCellView.swift index 7cf2bba0ada..5781700e5ff 100644 --- a/clients/macos/vellum-assistant/Features/Chat/MessageCellView.swift +++ b/clients/macos/vellum-assistant/Features/Chat/MessageCellView.swift @@ -138,11 +138,14 @@ struct MessageCellView: View, Equatable { @ViewBuilder private func thinkingIndicatorRow() -> some View { - RunningIndicator( - label: anchoredThinkingLabel, - showIcon: false - ) - .frame(maxWidth: VSpacing.chatBubbleMaxWidth, alignment: .leading) + // ⚠️ No .frame(maxWidth:) in LazyVStack cells — see AGENTS.md. + HStack(spacing: 0) { + RunningIndicator( + label: anchoredThinkingLabel, + showIcon: false + ) + Spacer(minLength: 0) + } .id("thinking-indicator") } @@ -231,14 +234,16 @@ struct MessageCellView: View, Equatable { } ForEach(subagentsByParent[message.id] ?? []) { subagent in - SubagentEventsReader( - store: subagentDetailStore, - subagent: subagent, - onAbort: { onAbortSubagent?(subagent.id) }, - onTap: { onSubagentTap?(subagent.id) } - ) - .frame(maxWidth: VSpacing.chatBubbleMaxWidth, alignment: .leading) - .id("subagent-\(subagent.id)") + HStack(spacing: 0) { + SubagentEventsReader( + store: subagentDetailStore, + subagent: subagent, + onAbort: { onAbortSubagent?(subagent.id) }, + onTap: { onSubagentTap?(subagent.id) } + ) + Spacer(minLength: 0) + } + .id("subagent-\(subagent.id)") } if showAnchoredThinkingIndicator { diff --git a/clients/macos/vellum-assistant/Features/Chat/MessageListContentView.swift b/clients/macos/vellum-assistant/Features/Chat/MessageListContentView.swift index 8938680c70f..b7a3583fcde 100644 --- a/clients/macos/vellum-assistant/Features/Chat/MessageListContentView.swift +++ b/clients/macos/vellum-assistant/Features/Chat/MessageListContentView.swift @@ -107,7 +107,6 @@ struct MessageListContentView: View, Equatable { } Spacer() } - .frame(maxWidth: VSpacing.chatBubbleMaxWidth, alignment: .leading) .id("thinking-indicator") .transition(.opacity) } @@ -118,7 +117,6 @@ struct MessageListContentView: View, Equatable { label: "Compacting context\u{2026}", showIcon: false ) - .frame(maxWidth: VSpacing.chatBubbleMaxWidth, alignment: .leading) .id("compacting-indicator") .transition(.opacity) } @@ -225,13 +223,16 @@ struct MessageListContentView: View, Equatable { } ForEach(state.orphanSubagents) { subagent in - SubagentEventsReader( - store: subagentDetailStore, - subagent: subagent, - onAbort: { onAbortSubagent?(subagent.id) }, - onTap: { onSubagentTap?(subagent.id) } - ) - .frame(maxWidth: VSpacing.chatBubbleMaxWidth, alignment: .leading) + // ⚠️ No .frame(maxWidth:) in LazyVStack cells — see AGENTS.md. + HStack(spacing: 0) { + SubagentEventsReader( + store: subagentDetailStore, + subagent: subagent, + onAbort: { onAbortSubagent?(subagent.id) }, + onTap: { onSubagentTap?(subagent.id) } + ) + Spacer(minLength: 0) + } .id("subagent-\(subagent.id)") .transition(.opacity) } @@ -248,7 +249,6 @@ struct MessageListContentView: View, Equatable { TypingIndicatorView() Spacer() } - .frame(maxWidth: VSpacing.chatBubbleMaxWidth, alignment: .leading) .id("streaming-without-text-indicator") .transition(.opacity) } else if isCompacting && !state.shouldShowThinkingIndicator && !state.canInlineProcessing { diff --git a/clients/macos/vellum-assistant/Features/Chat/MessageListView.swift b/clients/macos/vellum-assistant/Features/Chat/MessageListView.swift index 76ac331419a..8d91dd0cdcb 100644 --- a/clients/macos/vellum-assistant/Features/Chat/MessageListView.swift +++ b/clients/macos/vellum-assistant/Features/Chat/MessageListView.swift @@ -111,12 +111,17 @@ struct MessageListView: View { let _ = os_signpost(.event, log: PerfSignposts.log, name: "MessageListView.body") #endif ScrollView { - scrollViewContent - .background( - MessageListScrollObserver { newState in - enqueueScrollGeometryUpdate(newState) - } - ) + // AlignmentBarrierLayout stops explicitAlignment queries from + // cascading into the LazyVStack — see AGENTS.md and + // AlignmentBarrierLayout.swift for details. + AlignmentBarrierLayout { + scrollViewContent + } + .background( + MessageListScrollObserver { newState in + enqueueScrollGeometryUpdate(newState) + } + ) } .id(conversationId) .scrollContentBackground(.hidden) diff --git a/clients/macos/vellum-assistant/Features/Chat/ThinkingBlockView.swift b/clients/macos/vellum-assistant/Features/Chat/ThinkingBlockView.swift index 8ec7dadd118..9aa145fd2f9 100644 --- a/clients/macos/vellum-assistant/Features/Chat/ThinkingBlockView.swift +++ b/clients/macos/vellum-assistant/Features/Chat/ThinkingBlockView.swift @@ -37,6 +37,7 @@ struct ThinkingBlockView: View { Divider() .padding(.horizontal, VSpacing.sm) + // ⚠️ No .frame(maxWidth:) in LazyVStack cells — see AGENTS.md. MarkdownSegmentView( segments: cachedSegments, isStreaming: isStreaming, @@ -49,7 +50,6 @@ struct ThinkingBlockView: View { codeTextColor: VColor.contentDefault, codeBackgroundColor: VColor.surfaceBase ) - .frame(maxWidth: .infinity, alignment: .leading) .padding(VSpacing.sm) .transition(.opacity) } diff --git a/clients/shared/DesignSystem/Layout/AlignmentBarrierLayout.swift b/clients/shared/DesignSystem/Layout/AlignmentBarrierLayout.swift new file mode 100644 index 00000000000..115e813ee94 --- /dev/null +++ b/clients/shared/DesignSystem/Layout/AlignmentBarrierLayout.swift @@ -0,0 +1,51 @@ +import SwiftUI + +/// Blocks [`explicitAlignment`](https://developer.apple.com/documentation/swiftui/layout/explicitalignment(of:in:proposal:subviews:cache:)-3iqmu) +/// queries from cascading into its subtree. Returns `nil` for both horizontal +/// and vertical alignment guides while passing sizing and placement through unchanged. +/// +/// Place between any `.frame(maxWidth:, alignment:)` and a `LazyVStack` to +/// prevent O(depth × children) recursive alignment measurement. +/// +/// - SeeAlso: [WWDC23 – Demystify SwiftUI performance](https://developer.apple.com/videos/play/wwdc2023/10160/) +/// - SeeAlso: [`ViewDimensions`](https://developer.apple.com/documentation/swiftui/viewdimensions) +public struct AlignmentBarrierLayout: Layout { + public init() {} + + public func sizeThatFits( + proposal: ProposedViewSize, + subviews: Subviews, + cache: inout () + ) -> CGSize { + subviews.first?.sizeThatFits(proposal) ?? .zero + } + + public func placeSubviews( + in bounds: CGRect, + proposal: ProposedViewSize, + subviews: Subviews, + cache: inout () + ) { + subviews.first?.place(at: bounds.origin, proposal: proposal) + } + + public func explicitAlignment( + of guide: HorizontalAlignment, + in bounds: CGRect, + proposal: ProposedViewSize, + subviews: Subviews, + cache: inout () + ) -> CGFloat? { + nil + } + + public func explicitAlignment( + of guide: VerticalAlignment, + in bounds: CGRect, + proposal: ProposedViewSize, + subviews: Subviews, + cache: inout () + ) -> CGFloat? { + nil + } +}