import SwiftUI struct Row: Identifiable { let id: Int64 let kind: String var text: String var tools: [String] = [] } func buildRows(_ events: [ByreEvent]) -> [Row] { var rows: [Row] = [] for e in events { switch e.kind { case "user.message", "agent.message", "agent.thinking": rows.append(Row(id: e.seq, kind: e.kind, text: e.payload.value["text"] as? String ?? "")) case "agent.tool_call": let name = e.payload.value["name"] as? String ?? "?" let input = e.payload.value["input"] as? [String: (any Sendable)?] let detail = input?["file_path"] as? String ?? input?["command"] as? String ?? input?["pattern"] as? String ?? "" let line = "\(name) \(detail)" if rows.last?.kind == "tools" { rows[rows.count - 1].tools.append(line) } else { rows.append(Row(id: e.seq, kind: "tools", text: "", tools: [line])) } default: continue } } return rows } func toolSummary(_ tools: [String]) -> String { var order: [String] = [] var counts: [String: Int] = [:] for t in tools { let name = String(t.split(separator: " ").first ?? "") if counts[name] == nil { order.append(name) } counts[name, default: 0] += 1 } return order.map { counts[$0]! > 1 ? "\($0) ×\(counts[$0]!)" : $0 }.joined(separator: ", ") } struct RowView: View { let row: Row @ViewState private var open = false var body: some View { switch row.kind { case "tools": VStack(alignment: .leading, spacing: 2) { Button((open ? "▾ " : "▸ ") + toolSummary(row.tools)) { open.toggle() } .buttonStyle(.plain) .foregroundStyle(.secondary) if open { ForEach(Array(row.tools.enumerated()), id: \.offset) { _, t in Text(t) .foregroundStyle(.secondary) .textSelection(.enabled) } } } case "user.message": Text(markdown(row.text)) .textSelection(.enabled) .padding(8) .frame(maxWidth: .infinity, alignment: .leading) .background(Color.accentColor.opacity(0.12)) case "agent.thinking": Text(row.text) .italic() .foregroundStyle(.secondary) .textSelection(.enabled) default: Text(markdown(row.text)) .textSelection(.enabled) .frame(maxWidth: .infinity, alignment: .leading) } } private func markdown(_ s: String) -> AttributedString { (try? AttributedString(markdown: s, options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace))) ?? AttributedString(s) } } enum Container: String, CaseIterable { case lazyVStack = "ScrollView + LazyVStack" case list = "List" } struct TranscriptSpike: View { @Environment(Store.self) private var store @ViewState private var all: [Row] = [] @ViewState private var shown = 0 @ViewState private var container = Container.lazyVStack @ViewState private var playing = false @ViewState private var streaming = false @ViewState private var pinned = true @ViewState private var distance: CGFloat = 0 @ViewState private var tick = 0 @ViewState private var position = ScrollPosition(edge: .bottom) @ViewState private var events = 0 @ViewState private var loadTime = "" private var rows: ArraySlice { all.prefix(shown) } var body: some View { VStack(alignment: .leading) { HStack { Picker("", selection: $container) { ForEach(Container.allCases, id: \.self) { c in Text(c.rawValue).tag(c) } } .pickerStyle(.segmented) .frame(width: 320) Toggle("Play held-back rows", isOn: $playing) Toggle("Stream a message", isOn: $streaming) Text("\(events) events, \(shown)/\(all.count) rows \(loadTime) · \(pinned ? "stuck to bottom" : "scrolled up") · \(Int(distance)) pt from bottom") .foregroundStyle(.secondary) } switch container { case .lazyVStack: ScrollView { LazyVStack(alignment: .leading, spacing: 10) { ForEach(rows) { r in RowView(row: r) } } .scrollTargetLayout() .padding(.horizontal, 8) } .scrollPosition($position) .defaultScrollAnchor(.bottom) .onScrollGeometryChange(for: CGFloat.self) { g in g.contentSize.height - g.visibleRect.maxY } action: { _, d in distance = d pinned = d <= 80 } .onChange(of: tick) { if pinned { position.scrollTo(edge: .bottom) } } case .list: ScrollViewReader { proxy in List(rows) { r in RowView(row: r) } .onScrollGeometryChange(for: CGFloat.self) { g in g.contentSize.height - g.visibleRect.maxY } action: { _, d in distance = d pinned = d <= 80 } .onChange(of: tick) { if pinned, let last = rows.last { proxy.scrollTo(last.id, anchor: .bottom) } } .onAppear { if let last = rows.last { proxy.scrollTo(last.id, anchor: .bottom) } } } } } .onChange(of: store.transcript.count, initial: true) { load() } .task(id: playing) { while playing && shown < all.count { shown += 1 tick += 1 try? await Task.sleep(for: .milliseconds(150)) } } .task(id: streaming) { await stream() } } private func load() { guard let feed = store.selectedFeed, feed.ready else { return } let start = Date() let got = feed.events events = got.count all = buildRows(got) shown = all.count > 200 ? all.count - 100 : all.count loadTime = String(format: "in %.2fs", Date().timeIntervalSince(start)) tick += 1 } private func stream() async { guard streaming, let source = all.filter({ $0.kind == "agent.message" }).max(by: { $0.text.count < $1.text.count }) else { return } let id = (all.last?.id ?? 0) + 1_000_000 all.insert(Row(id: id, kind: "agent.message", text: ""), at: shown) shown += 1 let index = shown - 1 for word in source.text.split(separator: " ", omittingEmptySubsequences: false) { if !streaming { break } all[index].text += (all[index].text.isEmpty ? "" : " ") + word tick += 1 try? await Task.sleep(for: .milliseconds(30)) } streaming = false } }