updating transcript row changes'

This commit is contained in:
2026-07-18 01:15:20 -07:00
parent 87d9a09803
commit 677551a82b
@@ -111,15 +111,7 @@ struct MessageBubble: View {
var body: some View { var body: some View {
if role == .user { if role == .user {
HStack { UserMessageBubble(text: text)
Spacer(minLength: 48)
MarkdownText(markdown: text)
.textSelection(.enabled)
.padding(.horizontal, 14)
.padding(.vertical, 9)
.background(Palette.accent.opacity(0.15),
in: RoundedRectangle(cornerRadius: 18, style: .continuous))
}
} else { } else {
MarkdownText(markdown: text) MarkdownText(markdown: text)
.textSelection(.enabled) .textSelection(.enabled)
@@ -136,6 +128,93 @@ struct MessageBubble: View {
} }
} }
/// The user's own message: a right-aligned accent bubble that collapses a long message to a
/// capped height with a "Show more"/"Show less" toggle — the phone echo of the Mac's
/// `UserMessageBubble`. The Mac clamps a plain `Text` by line count; here the bubble renders
/// through `MarkdownText` (a stack of per-line/-block `Text`s that `.lineLimit` can't reach), so
/// we clamp by height instead. A hidden full-height copy of the bubble — laid out at the visible
/// bubble's width but with no height limit — measures the natural content height, so the toggle
/// appears only when collapsing actually hides something. The last visible line fades when
/// collapsed so the cut reads as "more below" rather than a glyph sliced in half.
private struct UserMessageBubble: View {
let text: String
@State private var expanded = false
@State private var fullHeight: CGFloat = 0
/// Collapsed bubbles show at most this much prose (~12 wrapped lines at the base size, matching
/// the Mac's 12-line clamp) before a "Show more". A `@ScaledMetric` so it grows with the user's
/// text-size setting alongside `MarkdownText`'s own scaled prose size.
@ScaledMetric(relativeTo: .body) private var collapsedMaxHeight: CGFloat = 288
/// Collapsing only hides something — and the toggle is only meaningful — when the full,
/// unconstrained content is taller than the collapsed cap. The +1 absorbs sub-point rounding.
private var isTruncated: Bool { fullHeight > collapsedMaxHeight + 1 }
private var collapsed: Bool { isTruncated && !expanded }
var body: some View {
VStack(alignment: .trailing, spacing: 4) {
HStack {
Spacer(minLength: 48)
MarkdownText(markdown: text)
.textSelection(.enabled)
.padding(.horizontal, 14)
.padding(.vertical, 9)
// Cap the height whenever collapsed; harmless for a short message (the cap sits
// above its natural height, so nothing clips). `.clipped` cuts the overflow.
.frame(maxHeight: expanded ? nil : collapsedMaxHeight, alignment: .top)
.clipped()
.mask {
if collapsed { collapseFade } else { Color.black }
}
// The bubble fill is added after the mask, so only the prose fades — not the
// accent behind it, which stays a solid rounded rect.
.background(alignment: .top) { heightProbe }
.background(Palette.accent.opacity(0.15),
in: RoundedRectangle(cornerRadius: 18, style: .continuous))
}
if isTruncated {
Button { expanded.toggle() } label: {
Text(expanded ? "Show less" : "Show more")
.font(.caption.weight(.medium))
.foregroundStyle(Palette.accent)
}
.buttonStyle(.plain)
.padding(.trailing, 4)
}
}
}
/// A hidden copy of the bubble laid out at the visible bubble's width but with no height limit,
/// so its measured height is the full content height regardless of the collapse cap. Attached as
/// a background so it inherits the visible bubble's width and wraps identically.
private var heightProbe: some View {
MarkdownText(markdown: text)
.padding(.horizontal, 14)
.padding(.vertical, 9)
.fixedSize(horizontal: false, vertical: true)
.background(GeometryReader { geo in
Color.clear.preference(key: FullHeightKey.self, value: geo.size.height)
})
.hidden()
.onPreferenceChange(FullHeightKey.self) { fullHeight = $0 }
}
/// Solid for all but the bottom sliver, then fades to clear — so a collapsed bubble's last
/// visible line dissolves instead of being clipped mid-glyph.
private var collapseFade: some View {
LinearGradient(
stops: [
.init(color: .black, location: 0),
.init(color: .black, location: 0.86),
.init(color: .clear, location: 1),
],
startPoint: .top, endPoint: .bottom)
}
}
private struct FullHeightKey: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = max(value, nextValue()) }
}
/// The agent's reasoning, set apart from its prose by a quiet left rule and a `sparkles` glyph, /// The agent's reasoning, set apart from its prose by a quiet left rule and a `sparkles` glyph,
/// italic and muted — the phone echo of the Mac's `thinkingRow`. Renders nothing for an /// italic and muted — the phone echo of the Mac's `thinkingRow`. Renders nothing for an
/// empty/redacted thinking block (matching the desktop), so a placeholder row never shows. /// empty/redacted thinking block (matching the desktop), so a placeholder row never shows.