Merge nucleic/calm-coral-lynx-nlsx into dev

This commit is contained in:
2026-07-31 20:51:24 -07:00
parent b67f88adfa
commit 37b594b529
+166 -9
View File
@@ -1805,6 +1805,7 @@ struct SessionDetailView: View {
TranscriptRenderSegmentSlot( TranscriptRenderSegmentSlot(
seed: segment.id, seed: segment.id,
length: segment.length, length: segment.length,
placeholderItems: projection.items[segment.range],
fontSize: transcriptFontSize, fontSize: transcriptFontSize,
reservedHeight: reservedHeight, reservedHeight: reservedHeight,
mounted: mountedTranscriptSegmentIDs.contains(segment.id), mounted: mountedTranscriptSegmentIDs.contains(segment.id),
@@ -2672,11 +2673,11 @@ struct SessionDetailView: View {
} }
} }
/// A deliberately cheap, deterministic stand-in for prose that has not mounted yet. /// A deliberately cheap, deterministic stand-in for transcript content that has not mounted yet.
/// ///
/// The characters look random but are derived from the session/segment identity, so unrelated /// The preview preserves each chunk's prose wrapping and lightweight row silhouette (notes, user
/// SwiftUI updates cannot make the placeholder flicker. A small fixed number of two-line prose /// bubbles, and tool cards), but obscures its text and avoids constructing the real Markdown/tool
/// groups is substantially cheaper than constructing the Markdown/tool hierarchy it represents. /// hierarchy. Its session/segment-derived glyphs remain stable across unrelated SwiftUI updates.
private struct TranscriptLoadingGlyphs: View { private struct TranscriptLoadingGlyphs: View {
private struct SweepToken: Hashable { private struct SweepToken: Hashable {
let seed: Int let seed: Int
@@ -2685,9 +2686,11 @@ private struct TranscriptLoadingGlyphs: View {
} }
@Environment(\.accessibilityReduceMotion) private var reduceMotion @Environment(\.accessibilityReduceMotion) private var reduceMotion
@Environment(\.appPalette) private var palette
let seed: Int let seed: Int
let rowCount: Int let rowCount: Int
var items: ArraySlice<TranscriptItem>? = nil
let fontSize: CGFloat let fontSize: CGFloat
let active: Bool let active: Bool
@@ -2738,14 +2741,162 @@ private struct TranscriptLoadingGlyphs: View {
} }
} }
/// Preserve word lengths, whitespace, punctuation, and explicit newlines while replacing
/// readable characters. That reproduces the real prose wrapping without parsing Markdown or
/// exposing half-loaded transcript content.
private static func obscured(_ source: String, seed: Int, limit: Int = 3_000) -> String {
let letters = Array("abcdefghijklmnopqrstuvwxyz")
let digits = Array("0123456789")
var state = UInt64(bitPattern: Int64(truncatingIfNeeded: seed))
^ 0xD1B5_4A32_D192_ED03
if state == 0 { state = 0x94D0_49BB_1331_11EB }
func next() -> UInt64 {
state ^= state >> 12
state ^= state << 25
state ^= state >> 27
return state &* 2_685_821_657_736_338_717
}
var output = ""
output.reserveCapacity(min(source.utf8.count, limit))
for character in source.prefix(limit) {
if character.isLetter {
output.append(letters[Int(next() % UInt64(letters.count))])
} else if character.isNumber {
output.append(digits[Int(next() % UInt64(digits.count))])
} else {
output.append(character)
}
}
return output
}
private func prose(_ text: String, rowSeed: Int) -> some View {
// Fenced code is the one Markdown feature whose chrome changes the chunk silhouette
// dramatically. Recognize only that cheap boundary; the full renderer remains unmounted.
let blocks = String(text.prefix(3_000)).components(separatedBy: "```")
return VStack(alignment: .leading, spacing: 8) {
ForEach(blocks.indices, id: \.self) { index in
let text = Self.obscured(blocks[index], seed: rowSeed &+ index &* 1_009)
if !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
Text(text)
.font(.system(
size: fontSize,
design: index.isMultiple(of: 2) ? .default : .monospaced))
.frame(maxWidth: .infinity, alignment: .leading)
.padding(index.isMultiple(of: 2) ? 0 : 8)
.background {
if !index.isMultiple(of: 2) {
RoundedRectangle(cornerRadius: 6)
.fill(Color.primary.opacity(0.075))
}
}
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private func statusRow(_ text: String, rowSeed: Int) -> some View {
HStack(alignment: .firstTextBaseline, spacing: 6) {
Image(systemName: "cube").font(.caption).frame(width: 16)
Text(Self.obscured(text, seed: rowSeed, limit: 220))
.font(.callout)
.lineLimit(3)
}
.foregroundStyle(Color.secondary)
.padding(.vertical, 8)
.frame(maxWidth: .infinity, alignment: .leading)
}
private func userRow(_ text: String, rowSeed: Int) -> some View {
Text(Self.obscured(text, seed: rowSeed, limit: 800))
.font(.system(size: fontSize))
.padding(.horizontal, 12)
.padding(.vertical, 10)
.background(Color.primary.opacity(0.075), in: .rect(cornerRadius: 12))
.padding(.vertical, 12)
.frame(maxWidth: .infinity, alignment: .trailing)
}
private func toolCard(rowSeed: Int, lines: Int) -> some View {
VStack(alignment: .leading, spacing: 0) {
ForEach(0..<max(1, min(lines, 6)), id: \.self) { index in
if index > 0 { Divider().overlay(palette.accent.opacity(0.12)) }
HStack(spacing: 9) {
Image(systemName: "terminal")
.font(.caption)
.foregroundStyle(palette.accent)
.frame(width: 16)
Text(Self.makeRows(seed: rowSeed &+ index &* 977, count: 1)[0]
.replacingOccurrences(of: "\n", with: " "))
.font(.callout.weight(index == 0 ? .medium : .regular))
.lineLimit(2)
Spacer(minLength: 8)
Image(systemName: "chevron.right")
.font(.caption2)
.foregroundStyle(.secondary)
}
.padding(.horizontal, 10)
.padding(.vertical, 9)
}
}
.foregroundStyle(AppTheme.primaryText)
.background(palette.accent.opacity(0.06))
.overlay(RoundedRectangle(cornerRadius: 8)
.strokeBorder(palette.accent.opacity(0.14), lineWidth: 1))
.clipShape(.rect(cornerRadius: 8))
.padding(.vertical, 8)
.frame(maxWidth: .infinity, alignment: .leading)
}
@ViewBuilder
private func pendingRow(_ item: TranscriptItem, index: Int) -> some View {
let rowSeed = seed &+ index &* 7_919
switch item {
case .assistant(_, let text, _):
prose(text, rowSeed: rowSeed)
case .thinking(_, let text, _):
statusRow(text, rowSeed: rowSeed).italic()
case .tool(_, _, _, _, let children):
toolCard(rowSeed: rowSeed, lines: children.isEmpty ? 1 : min(3, children.count + 1))
case .toolGroup(_, _, let calls):
toolCard(rowSeed: rowSeed, lines: calls.count)
case .thinkingTokens:
statusRow("Thinking through the next step", rowSeed: rowSeed)
case .event(let event):
switch event.kind {
case .userText(let chunk):
userRow(chunk.text, rowSeed: rowSeed)
case .assistantText(let chunk):
prose(chunk.text, rowSeed: rowSeed)
case .thinking(let chunk):
statusRow(chunk.text, rowSeed: rowSeed).italic()
case .fileChange:
toolCard(rowSeed: rowSeed, lines: 1)
case .note(let note):
statusRow(note.text, rowSeed: rowSeed)
case .error(let error):
statusRow(error.message, rowSeed: rowSeed)
default:
statusRow("Transcript activity is loading", rowSeed: rowSeed)
}
}
}
@ViewBuilder @ViewBuilder
private func glyphStack(_ rows: [String]) -> some View { private func glyphStack(_ rows: [String]) -> some View {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
ForEach(rows.indices, id: \.self) { index in if let items {
Text(rows[index]) ForEach(items.indices, id: \.self) { index in
.font(.system(size: fontSize)) pendingRow(items[index], index: index - items.startIndex)
.lineLimit(2) }
.truncationMode(.tail) } else {
ForEach(rows.indices, id: \.self) { index in
Text(rows[index])
.font(.system(size: fontSize))
.lineLimit(2)
.truncationMode(.tail)
}
} }
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
@@ -2780,6 +2931,7 @@ private struct TranscriptLoadingGlyphs: View {
.blur(radius: 2.4) .blur(radius: 2.4)
.opacity(0.82) .opacity(0.82)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.clipped()
.textSelection(.disabled) .textSelection(.disabled)
.accessibilityHidden(true) .accessibilityHidden(true)
.allowsHitTesting(false) .allowsHitTesting(false)
@@ -2809,6 +2961,7 @@ private struct TranscriptLoadingGlyphs: View {
private struct TranscriptRenderSegmentSlot<Content: View>: View { private struct TranscriptRenderSegmentSlot<Content: View>: View {
let seed: Int let seed: Int
let length: Int let length: Int
let placeholderItems: ArraySlice<TranscriptItem>
let fontSize: CGFloat let fontSize: CGFloat
let reservedHeight: CGFloat let reservedHeight: CGFloat
let mounted: Bool let mounted: Bool
@@ -2822,6 +2975,7 @@ private struct TranscriptRenderSegmentSlot<Content: View>: View {
init( init(
seed: Int, seed: Int,
length: Int, length: Int,
placeholderItems: ArraySlice<TranscriptItem>,
fontSize: CGFloat, fontSize: CGFloat,
reservedHeight: CGFloat, reservedHeight: CGFloat,
mounted: Bool, mounted: Bool,
@@ -2832,6 +2986,7 @@ private struct TranscriptRenderSegmentSlot<Content: View>: View {
) { ) {
self.seed = seed self.seed = seed
self.length = length self.length = length
self.placeholderItems = placeholderItems
self.fontSize = fontSize self.fontSize = fontSize
self.reservedHeight = reservedHeight self.reservedHeight = reservedHeight
self.mounted = mounted self.mounted = mounted
@@ -2862,6 +3017,7 @@ private struct TranscriptRenderSegmentSlot<Content: View>: View {
TranscriptLoadingGlyphs( TranscriptLoadingGlyphs(
seed: seed, seed: seed,
rowCount: length, rowCount: length,
items: placeholderItems,
fontSize: fontSize, fontSize: fontSize,
active: !visible) active: !visible)
.frame( .frame(
@@ -2881,6 +3037,7 @@ private struct TranscriptRenderSegmentSlot<Content: View>: View {
TranscriptLoadingGlyphs( TranscriptLoadingGlyphs(
seed: seed, seed: seed,
rowCount: length, rowCount: length,
items: placeholderItems,
fontSize: fontSize, fontSize: fontSize,
active: true) active: true)
.frame(height: max(reservedHeight, CGFloat(length))) .frame(height: max(reservedHeight, CGFloat(length)))