Transcript Incremental Projection

Nucleic-Session: 0FFD007B-0696-4517-9429-129C7B0FD5AC
Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
2026-07-06 18:49:55 -07:00
co-authored by Nucleic
parent 4bd4e81778
commit f5423f981e
@@ -123,17 +123,37 @@ struct SessionDetailView: View {
set: { store.setSessionEffort(sessionID, $0) }) set: { store.setSessionEffort(sessionID, $0) })
} }
/// Memo for the context-occupancy scan below. The backward scan usually stops at the last
/// turn's usage event, but a stream with sparse (or no) usage reporting walks the whole
/// transcript — and `body` re-evaluates on every store change and keystroke, so an O(N) scan
/// per evaluation quietly compounds on long sessions. `(count, lastSeq)` pins the stream, as
/// in the projection cache; a class held in `@State` so updating it from `body` doesn't
/// itself invalidate the view.
private final class ContextScanCache {
var count = -1
var lastSeq: UInt64 = 0
var used: Int?
}
@State private var contextScanCache = ContextScanCache()
/// Live context-window occupancy (newest turn's input tokens ÷ the model's window), read /// Live context-window occupancy (newest turn's input tokens ÷ the model's window), read
/// from the transcript exactly as the Mac header does. /// from the transcript exactly as the Mac header does. Only the token scan is memoized —
/// the window division stays live, so a model switch reflects immediately.
private var contextPercent: Int? { private var contextPercent: Int? {
let used = store.openEvents.reversed().lazy.compactMap { event -> Int? in let events = store.openEvents
switch event.kind { let cache = contextScanCache
case .turnCompleted(let turn): return turn.usage?.contextInputTokens if cache.count != events.count || cache.lastSeq != events.last?.seq ?? 0 {
case .usage(let usage): return usage.contextInputTokens cache.count = events.count
default: return nil cache.lastSeq = events.last?.seq ?? 0
} cache.used = events.reversed().lazy.compactMap { event -> Int? in
}.first { $0 > 0 } switch event.kind {
guard let used else { return nil } case .turnCompleted(let turn): return turn.usage?.contextInputTokens
case .usage(let usage): return usage.contextInputTokens
default: return nil
}
}.first { $0 > 0 }
}
guard let used = cache.used else { return nil }
let window = store.modelCatalog.contextWindow(summary?.model) let window = store.modelCatalog.contextWindow(summary?.model)
guard window > 0 else { return nil } guard window > 0 else { return nil }
return min(100, Int((Double(used) / Double(window)) * 100)) return min(100, Int((Double(used) / Double(window)) * 100))