Merge nucleic/calm-coral-lynx-nlsx into dev
This commit is contained in:
@@ -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)))
|
||||||
|
|||||||
Reference in New Issue
Block a user