Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions clients/shared/DesignSystem/Modifiers/WidthCapLayout.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import SwiftUI

/// Caps proposed width at a maximum value using the Layout protocol (O(1)).
/// Drop-in replacement for `.frame(maxWidth: N)` that avoids
/// `_FlexFrameLayout` and its O(n × depth) `explicitAlignment` cascade
/// inside LazyVStack cells.
///
/// Reference: [Layout.explicitAlignment](https://developer.apple.com/documentation/swiftui/layout/explicitalignment(of:in:proposal:subviews:cache:)-8ofeu)
struct WidthCapLayout: Layout {
let cap: CGFloat

func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let available = proposal.replacingUnspecifiedDimensions().width
let width = min(cap, available)
guard let child = subviews.first else { return CGSize(width: width, height: 0) }
let childSize = child.sizeThatFits(ProposedViewSize(width: width, height: proposal.height))
return CGSize(width: width, height: childSize.height)
}

func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
guard let child = subviews.first else { return }
child.place(
at: bounds.origin,
anchor: .topLeading,
proposal: ProposedViewSize(width: bounds.width, height: bounds.height)
)
}
}

extension View {
/// Caps width at `cap` without creating `_FlexFrameLayout`.
/// When `cap` is nil, no constraint is applied.
@ViewBuilder
func widthCap(_ cap: CGFloat?) -> some View {
if let cap {
WidthCapLayout(cap: cap) { self }
} else {
self
}
}
}
2 changes: 1 addition & 1 deletion clients/shared/Features/Chat/CommandListBubble.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public struct CommandListBubble: View {
RoundedRectangle(cornerRadius: VRadius.lg)
.stroke(VColor.borderBase, lineWidth: 1)
)
.frame(maxWidth: 400)
.widthCap(400)
}

private static func parseEntry(from rawLine: Substring) -> CommandEntry? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ struct InlineAppCreatedCard: View {
Image(nsImage: nsImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: .infinity)
.frame(height: 140)
.clipShape(RoundedRectangle(cornerRadius: VRadius.md))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,64 +15,66 @@ public struct InlineDynamicPagePreview: View {
Button {
onViewOutput()
} label: {
VStack(alignment: .leading, spacing: VSpacing.xl) {
// Icon + title row
HStack(spacing: VSpacing.sm) {
if let icon = preview.icon {
if let url = URL(string: icon), url.scheme == "https" || url.scheme == "http" {
AsyncImage(url: url) { phase in
switch phase {
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fit)
case .failure:
RoundedRectangle(cornerRadius: VRadius.sm)
.fill(VColor.surfaceOverlay)
default:
RoundedRectangle(cornerRadius: VRadius.sm)
.fill(VColor.surfaceOverlay)
HStack(spacing: 0) {
VStack(alignment: .leading, spacing: VSpacing.xl) {
// Icon + title row
HStack(spacing: VSpacing.sm) {
if let icon = preview.icon {
if let url = URL(string: icon), url.scheme == "https" || url.scheme == "http" {
AsyncImage(url: url) { phase in
switch phase {
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fit)
case .failure:
RoundedRectangle(cornerRadius: VRadius.sm)
.fill(VColor.surfaceOverlay)
default:
RoundedRectangle(cornerRadius: VRadius.sm)
.fill(VColor.surfaceOverlay)
}
}
.frame(width: 32, height: 32)
.clipShape(RoundedRectangle(cornerRadius: VRadius.sm))
} else {
Text(icon)
.font(.system(size: 28))
}
.frame(width: 32, height: 32)
.clipShape(RoundedRectangle(cornerRadius: VRadius.sm))
} else {
Text(icon)
.font(.system(size: 28))
}
}

VStack(alignment: .leading, spacing: VSpacing.xxs) {
Text(preview.title)
.font(VFont.bodyMediumEmphasised)
.foregroundStyle(VColor.contentDefault)
.lineLimit(2)
VStack(alignment: .leading, spacing: VSpacing.xxs) {
Text(preview.title)
.font(VFont.bodyMediumEmphasised)
.foregroundStyle(VColor.contentDefault)
.lineLimit(2)

if let subtitle = preview.subtitle {
Text(subtitle)
.font(VFont.labelDefault)
.foregroundStyle(VColor.contentTertiary)
.lineLimit(1)
if let subtitle = preview.subtitle {
Text(subtitle)
.font(VFont.labelDefault)
.foregroundStyle(VColor.contentTertiary)
.lineLimit(1)
}
}
}
}

if let description = preview.description, !description.isEmpty {
Text(description)
.font(VFont.labelDefault)
.foregroundStyle(VColor.contentSecondary)
.lineLimit(3)
}
if let description = preview.description, !description.isEmpty {
Text(description)
.font(VFont.labelDefault)
.foregroundStyle(VColor.contentSecondary)
.lineLimit(3)
}

if let metrics = preview.metrics, !metrics.isEmpty {
HStack(spacing: VSpacing.sm) {
ForEach(Array(metrics.prefix(3).enumerated()), id: \.offset) { _, metric in
metricPill(label: metric.label, value: metric.value)
if let metrics = preview.metrics, !metrics.isEmpty {
HStack(spacing: VSpacing.sm) {
ForEach(Array(metrics.prefix(3).enumerated()), id: \.offset) { _, metric in
metricPill(label: metric.label, value: metric.value)
}
}
}
}
Spacer(minLength: 0)
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
Comment thread
ashleeradka marked this conversation as resolved.
}
.buttonStyle(.plain)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,6 @@ private let log = Logger(
category: "InlineSurfaceRouter"
)

/// Caps proposed width at a maximum value using the Layout protocol (O(1)).
/// Drop-in replacement for `.frame(maxWidth:, alignment: .leading)` that
/// avoids `_FlexFrameLayout` and its O(n × depth) alignment cascade.
private struct WidthCapLayout: Layout {
let cap: CGFloat

func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let available = proposal.replacingUnspecifiedDimensions().width
let width = min(cap, available)
guard let child = subviews.first else { return CGSize(width: width, height: 0) }
let childSize = child.sizeThatFits(ProposedViewSize(width: width, height: proposal.height))
return CGSize(width: width, height: childSize.height)
}

func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
guard let child = subviews.first else { return }
child.place(
at: bounds.origin,
anchor: .topLeading,
proposal: ProposedViewSize(width: bounds.width, height: bounds.height)
)
}
}

extension View {
/// Caps width at `cap` without creating `_FlexFrameLayout`.
/// When `cap` is nil, no constraint is applied.
@ViewBuilder
fileprivate func widthCap(_ cap: CGFloat?) -> some View {
if let cap {
WidthCapLayout(cap: cap) { self }
} else {
self
}
}
}

/// Routes an `InlineSurfaceData` to the correct inline widget view.
public struct InlineSurfaceRouter: View {
public let surface: InlineSurfaceData
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ public struct InlineTableWidget: View {
endResize(for: columnIndex)
}
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.frame(width: resizeHandleWidth)
}
.contentShape(Rectangle())
#else
Expand Down
2 changes: 1 addition & 1 deletion clients/shared/Features/Chat/ModelListBubble.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,6 @@ public struct ModelListBubble: View {
RoundedRectangle(cornerRadius: VRadius.lg)
.stroke(VColor.borderBase, lineWidth: 1)
)
.frame(maxWidth: 480)
.widthCap(480)
}
}
58 changes: 33 additions & 25 deletions clients/shared/Features/Chat/ToolCallChip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,31 +155,37 @@ public struct ToolCallChip: View {
let canOpenImage = !toolCall.inputRawValue.isEmpty
&& FileManager.default.fileExists(atPath: toolCall.inputRawValue)
if canOpenImage {
Image(nsImage: cachedImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity)
.clipShape(RoundedRectangle(cornerRadius: VRadius.sm))
.padding(.horizontal, VSpacing.sm)
.onTapGesture(count: 2) {
NSWorkspace.shared.open(URL(fileURLWithPath: toolCall.inputRawValue))
}
.pointerCursor()
HStack(spacing: 0) {
Image(nsImage: cachedImage)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: VRadius.sm))
.onTapGesture(count: 2) {
NSWorkspace.shared.open(URL(fileURLWithPath: toolCall.inputRawValue))
}
.pointerCursor()
Spacer(minLength: 0)
}
.padding(.horizontal, VSpacing.sm)
} else {
Image(nsImage: cachedImage)
HStack(spacing: 0) {
Image(nsImage: cachedImage)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: VRadius.sm))
Spacer(minLength: 0)
}
.padding(.horizontal, VSpacing.sm)
}
#elseif os(iOS)
HStack(spacing: 0) {
Image(uiImage: cachedImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity)
.clipShape(RoundedRectangle(cornerRadius: VRadius.sm))
.padding(.horizontal, VSpacing.sm)
Spacer(minLength: 0)
}
#elseif os(iOS)
Image(uiImage: cachedImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity)
.clipShape(RoundedRectangle(cornerRadius: VRadius.sm))
.padding(.horizontal, VSpacing.sm)
.padding(.horizontal, VSpacing.sm)
#endif
}

Expand Down Expand Up @@ -231,11 +237,13 @@ public struct ToolCallChip: View {
VDiffView(result, maxHeight: lineCount > 500 ? 400 : nil)
} else {
ScrollView {
Text(result)
.font(VFont.bodySmallDefault)
.foregroundStyle(VColor.contentSecondary)
.frame(maxWidth: .infinity, alignment: .leading)
.textSelection(.enabled)
HStack(spacing: 0) {
Text(result)
.font(VFont.bodySmallDefault)
.foregroundStyle(VColor.contentSecondary)
.textSelection(.enabled)
Spacer(minLength: 0)
}
}
.adaptiveScrollFrame(for: result, maxHeight: 400, lineCount: lineCount)
}
Expand Down
37 changes: 21 additions & 16 deletions clients/shared/Features/Chat/ToolCallProgressBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ public struct ToolCallProgressBar: View {
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, VSpacing.md)

// Expanded details (shown when a step is clicked)
Expand Down Expand Up @@ -192,17 +191,21 @@ public struct ToolCallProgressBar: View {
.foregroundStyle(VColor.contentTertiary)

#if os(macOS)
Image(nsImage: cachedImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity)
.clipShape(RoundedRectangle(cornerRadius: VRadius.sm))
HStack(spacing: 0) {
Image(nsImage: cachedImage)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: VRadius.sm))
Spacer(minLength: 0)
}
#elseif os(iOS)
Image(uiImage: cachedImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity)
.clipShape(RoundedRectangle(cornerRadius: VRadius.sm))
HStack(spacing: 0) {
Image(uiImage: cachedImage)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: VRadius.sm))
Spacer(minLength: 0)
}
#endif
}
}
Expand Down Expand Up @@ -248,11 +251,13 @@ public struct ToolCallProgressBar: View {
}
} else {
ScrollView {
Text(result)
.font(VFont.bodySmallDefault)
.foregroundStyle(VColor.contentSecondary)
.frame(maxWidth: .infinity, alignment: .leading)
.textSelection(.enabled)
HStack(spacing: 0) {
Text(result)
.font(VFont.bodySmallDefault)
.foregroundStyle(VColor.contentSecondary)
.textSelection(.enabled)
Spacer(minLength: 0)
}
}
.adaptiveScrollFrame(for: result, maxHeight: 200, lineThreshold: 12, lineCount: cachedResultLineCount)
}
Expand Down