diff --git a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift index 82482a8..2bbe148 100644 --- a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift +++ b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift @@ -509,6 +509,14 @@ struct TranscriptList: View { /// row churn. @State private var prewarmTask: Task? + /// How many projected rows the pre-warm has already covered, so each later trigger snapshots + /// only the *new* rows instead of re-walking (and re-hashing) the whole transcript on every + /// row that lands — which would quietly re-introduce an O(N) main-thread pass per row. Held + /// in a box (not `@State` value) because updating it from `body`-adjacent code must not + /// invalidate the view. + private final class PrewarmProgress { var count = 0 } + @State private var prewarmProgress = PrewarmProgress() + private var items: [TranscriptItem] { projectionCache.items(for: events, showRaw: showRaw, showLockEvents: showLockEvents) } @@ -519,11 +527,16 @@ struct TranscriptList: View { /// away, jittering the load-in. Snapshot the message bodies here on the main actor (a cheap read /// of the memoized projection), then parse them on a background task so that first layout finds /// the caches already warm. Idempotent and self-cancelling; the parse results are the same - /// whichever thread fills the (thread-safe) caches. + /// whichever thread fills the (thread-safe) caches. Incremental: only rows beyond the last + /// covered count are snapshotted (coalescing can shuffle nearby indices, but a missed body just + /// parses on first layout as before — the warm is an optimization, never a correctness gate). private func prewarmMarkdown() { - let bodies: [String] = items.compactMap { + let current = items + if current.count < prewarmProgress.count { prewarmProgress.count = 0 } // stream reset + let bodies: [String] = current[prewarmProgress.count...].compactMap { if case .message(_, let text) = $0.kind { return text } else { return nil } } + prewarmProgress.count = current.count guard !bodies.isEmpty else { return } prewarmTask?.cancel() prewarmTask = Task.detached(priority: .utility) {