nvrsion: Add, fix, refactor, and introduce Usage Tab in Settings showing usage (mirroring home page) with placeholders for Codex limits support (backend not implemented); adjust Live Activity Icon Color to a purple atom icon; add a warning to the VM tab in Settings about 64GB free disk space requirement; implement and address performance issues in TRANSCRIPT_INCREMENTAL_PROJECTION system.

Nucleic-Promote: 1
Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
2026-07-06 18:41:03 -07:00
co-authored by Nucleic
parent 17e904eff1
commit 8fcce22bb8
6 changed files with 565 additions and 71 deletions
@@ -494,43 +494,29 @@ struct TranscriptList: View {
/// settling — a `.task(id:)` debounces it to detect when the opening layout has come to rest. /// settling — a `.task(id:)` debounces it to detect when the opening layout has come to rest.
@State private var transcriptContentHeight: CGFloat = 0 @State private var transcriptContentHeight: CGFloat = 0
/// Memoized transcript projection. `TranscriptProjection.build` folds the entire raw event /// Incremental transcript projection (docs/TRANSCRIPT_INCREMENTAL_PROJECTION.md). Two layers:
/// stream into rows on every read, but `body` re-evaluates far more often than `events` /// a read memo that collapses the redundant `body` re-evaluations (settling layout bumps
/// actually changes: while the chat is opening, `onScrollGeometryChange` bumps /// `transcriptContentHeight` every frame while opening, and each bump re-runs `body` against
/// `transcriptContentHeight` on every settling-layout frame, and each bump re-runs `body` — /// an unchanged stream), and — when the stream *has* grown — a stable-prefix fold that seals
/// so the untouched event stream was re-folded dozens of times during the exact window the /// everything before the live turn once and re-folds only the tail, so a streaming delta
/// tab bar is sliding away, starving that animation of the main thread and making the /// costs O(live-tail) instead of re-folding all N events (which made long sessions cost
/// load-in jitter. Reuse the last fold whenever the stream is unchanged. A class held in /// O(N²) over their lifetime). A class held in `@State` so reading/updating it from `body`
/// `@State` so reading/updating it from `body` doesn't itself invalidate the view (SwiftUI /// doesn't itself invalidate the view (SwiftUI stores the reference, never diffs interior).
/// stores the reference and never diffs its interior). @State private var projectionCache = IncrementalTranscriptProjection()
private final class ProjectionCache {
/// The event stream is append-only with strictly increasing `seq`, so `(count, lastSeq)`
/// uniquely pins it; `firstSeq` guards the open reset (stream cleared to `[]`, then
/// refilled). The two display toggles are inputs to the fold, so they key it too.
private struct Key: Equatable {
var count: Int; var firstSeq: UInt64; var lastSeq: UInt64
var showRaw: Bool; var showLockEvents: Bool
}
private var key: Key?
private var value: [TranscriptItem] = []
func items(for events: [AgentEvent], showRaw: Bool, showLockEvents: Bool) -> [TranscriptItem] {
let k = Key(count: events.count, firstSeq: events.first?.seq ?? 0,
lastSeq: events.last?.seq ?? 0, showRaw: showRaw, showLockEvents: showLockEvents)
if key == k { return value }
let built = TranscriptProjection.build(events, showRaw: showRaw, showLockEvents: showLockEvents)
key = k
value = built
return built
}
}
@State private var projectionCache = ProjectionCache()
/// The in-flight background Markdown pre-warm (below), cancelled when a newer one supersedes /// The in-flight background Markdown pre-warm (below), cancelled when a newer one supersedes
/// it or the transcript goes away — so a long warm can't outlive the view or stack up behind /// it or the transcript goes away — so a long warm can't outlive the view or stack up behind
/// row churn. /// row churn.
@State private var prewarmTask: Task<Void, Never>? @State private var prewarmTask: Task<Void, Never>?
/// 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] { private var items: [TranscriptItem] {
projectionCache.items(for: events, showRaw: showRaw, showLockEvents: showLockEvents) projectionCache.items(for: events, showRaw: showRaw, showLockEvents: showLockEvents)
} }
@@ -541,14 +527,23 @@ struct TranscriptList: View {
/// away, jittering the load-in. Snapshot the message bodies here on the main actor (a cheap read /// 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 /// 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 /// 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() { 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 } if case .message(_, let text) = $0.kind { return text } else { return nil }
} }
prewarmProgress.count = current.count
guard !bodies.isEmpty else { return } guard !bodies.isEmpty else { return }
prewarmTask?.cancel() // Chain batches instead of cancelling the in-flight one: each batch covers *new* rows
prewarmTask = Task.detached(priority: .utility) { // only, so cancelling a predecessor (say, the big open batch, superseded by the first
// streamed row) would permanently drop its coverage. `onDisappear` cancels the head of
// the chain; a predecessor mid-parse just finishes its bounded batch into shared caches.
prewarmTask = Task.detached(priority: .utility) { [previous = prewarmTask] in
await previous?.value
for body in bodies { for body in bodies {
if Task.isCancelled { return } if Task.isCancelled { return }
MarkdownText.prewarm(body) MarkdownText.prewarm(body)
@@ -0,0 +1,419 @@
import Foundation
import NucleicProtocol
/// Stable-prefix incremental projector (docs/TRANSCRIPT_INCREMENTAL_PROJECTION.md).
///
/// `TranscriptProjection.build` folds the whole stream on every read, so a streaming turn of K
/// deltas over an N-event transcript costs O(N·K) ≈ O(N²) per session. But the stream is
/// immutable except at the tail: everything before the live turn is frozen — its `messageID`s and
/// `toolCallID`s never recur. So this projector **seals** the longest provably-stable prefix of
/// the folded item list once, and re-folds only the unstable suffix per delta: O(live-tail)
/// instead of O(N).
///
/// The seal point (an index into the raw event stream) is chosen so that
/// `fold(prefix) ++ fold(tail)` is byte-identical to `fold(whole)`:
///
/// 1. **No coalescing key crosses the seam.** Text/thinking/tool items coalesce on
/// `messageID`/`toolCallID` (and a subagent's events name their parent Task). An exact
/// interval check over the current stream forbids any seam inside an id's first→last
/// reference span, so no item can straddle it.
/// 2. **No id can recur after the seam.** Future arrivals are fenced by closure rules read off
/// the stream itself: a tool is closed once its result arrived; a message/thinking block once
/// a different message has started in its scope; everything, once its turn completed. These
/// are the premises of the design doc ("the stream is immutable except at the tail"); if one
/// is ever violated — a tail event referencing a sealed id — it is *detected* and the
/// projector resets and re-folds from scratch, so correctness never rests on them.
/// 3. **No tool run is split.** `coalesceToolRuns` merges adjacent `.tool` items, so the seam
/// only falls where the last folded item is a hard separator — a visible non-tool row that a
/// future tool call can't merge across. (Empty redacted-thinking rows are transparent to runs
/// and therefore to this rule too.)
/// 4. **Lock notes fold exactly, even across the seam.** A lock's `released` note lands when the
/// file lands in the parent — potentially many turns after the edit it brackets — so sealed
/// edit cards stay reachable through a registry (`TranscriptProjection.PriorEdit`): a tail
/// note that path-matches a sealed edit patches that card, exactly where a whole-stream fold
/// would put it. No lag heuristic, no divergence.
/// 5. **`worktreeRoot` is a carried constant.** Lock-path normalization needs the seq-0
/// `sessionStarted` cwd; nothing seals until it is known, and it never changes once found.
///
/// The equivalence test (`IncrementalProjectionEquivalenceTests`) replays streams and asserts
/// `incremental(prefix) == build(prefix)` for **every** prefix — the whole correctness argument,
/// checked mechanically.
///
/// A class held in `@State` so reads/updates from `body` don't invalidate the view; not
/// thread-safe (main-actor use only, like the `ProjectionCache` it replaces).
final class IncrementalTranscriptProjection {
// MARK: - Sealed state
/// Folded output of the stable prefix — appended to at each seal, never re-walked.
private var sealedItems: [TranscriptItem] = []
/// Watermark into the raw stream: `events[0..<sealedEventCount]` produced `sealedItems`.
private var sealedEventCount = 0
/// `events[sealedEventCount - 1].seq` at seal time — detects a rewritten/merged prefix
/// (a reconnect backfill slotting events in by seq) that shifts history under the watermark.
private var sealedLastSeq: UInt64 = 0
/// The stream's first seq — detects a session switch / stream reset.
private var streamFirstSeq: UInt64?
/// Carried constant from the first non-empty `sessionStarted.cwd`. Nothing seals while nil
/// (a root arriving later would retroactively change sealed lock folds).
private var worktreeRoot: String?
/// Every `messageID`/`toolCallID` referenced by a sealed event. A later event referencing one
/// would mutate sealed output — detected here, answered with a full reset (self-healing).
private var sealedIDs: Set<String> = []
/// Sealed edit-class calls in item order, for cross-seam lock-note folding (rule 4).
private var sealedEdits: [TranscriptProjection.PriorEdit] = []
/// Where each sealed edit's card sits in `sealedItems` (it may live inside a `.toolBlock`).
private var sealedEditIndex: [String: Int] = [:]
/// Display toggles are fold inputs; flipping either resets.
private var showRaw = false
private var showLockEvents = true
// MARK: - Read memo
/// `body` re-evaluates far more often than the stream changes (settling layout, scroll
/// geometry); this collapses those redundant reads to a cached return, as the previous
/// `ProjectionCache` did. The stream is append-only with strictly increasing seq, so
/// `(count, firstSeq, lastSeq)` pins it.
private struct MemoKey: Equatable {
var count: Int
var firstSeq: UInt64
var lastSeq: UInt64
var showRaw: Bool
var showLockEvents: Bool
}
private var memoKey: MemoKey?
private var memoValue: [TranscriptItem] = []
/// Hold-back from the stream edge: never seal into the newest few events. Decoders emit
/// tightly-coupled events in one batch (a `toolResult` and its inferred `fileChange`; a
/// whole-message's final chunks) that sync may deliver one at a time — holding the edge back
/// keeps a mid-batch read from sealing an entity whose trailing batch-mates are still in
/// flight. Cheap insurance on top of the closure rules; violations would only cost a reset.
private let edgeLag = 4
/// Test hook: how far the watermark has advanced (the equivalence suite also asserts sealing
/// actually happens, so a regression to "never seal" can't pass silently).
var sealedEventCountForTesting: Int { sealedEventCount }
// MARK: - Read
func items(for events: [AgentEvent], showRaw: Bool, showLockEvents: Bool) -> [TranscriptItem] {
let key = MemoKey(count: events.count, firstSeq: events.first?.seq ?? 0,
lastSeq: events.last?.seq ?? 0, showRaw: showRaw, showLockEvents: showLockEvents)
if memoKey == key { return memoValue }
if needsReset(events, showRaw: showRaw, showLockEvents: showLockEvents) { reset() }
self.showRaw = showRaw
self.showLockEvents = showLockEvents
streamFirstSeq = events.first?.seq
if worktreeRoot == nil {
// Only the unsealed region needs scanning: sealing requires the root, so a sealed
// region can only exist after it was found.
worktreeRoot = TranscriptProjection.worktreeRoot(in: events[sealedEventCount...])
}
var watermark = chooseWatermark(events)
if watermark == nil {
// A tail event referenced a sealed id — a closure premise was violated (late file
// change, resumed message, post-result subagent child). Refold from scratch; with no
// sealed ids the second pass cannot be violated.
reset()
streamFirstSeq = events.first?.seq
worktreeRoot = TranscriptProjection.worktreeRoot(in: events[...])
watermark = chooseWatermark(events)
}
seal(events, upTo: watermark ?? sealedEventCount)
let result = render(events)
memoKey = key
memoValue = result
return result
}
// MARK: - Reset / identity
private func needsReset(_ events: [AgentEvent], showRaw: Bool, showLockEvents: Bool) -> Bool {
if showRaw != self.showRaw || showLockEvents != self.showLockEvents { return true }
if events.count < sealedEventCount { return true }
if sealedEventCount > 0 {
if events.first?.seq != streamFirstSeq { return true }
if events[sealedEventCount - 1].seq != sealedLastSeq { return true }
} else if streamFirstSeq != nil, events.first?.seq != streamFirstSeq {
return true // nothing sealed, but the carried worktreeRoot belongs to the old stream
}
return false
}
private func reset() {
sealedItems = []
sealedEventCount = 0
sealedLastSeq = 0
streamFirstSeq = nil
worktreeRoot = nil
sealedIDs = []
sealedEdits = []
sealedEditIndex = [:]
memoKey = nil
memoValue = []
}
// MARK: - Watermark selection
/// The coalescing ids an event mentions: its `messageID` or `toolCallID`, plus the parent
/// Task id for subagent-owned events. Two events sharing an id must land on the same side of
/// the seam; the parent link chains a subagent's whole scope (and, transitively, deeper
/// descendants) to its spawn.
private static func refs(of event: AgentEvent) -> [String] {
switch event.kind {
case .userText(let c), .assistantText(let c), .thinking(let c):
if let parent = c.parentToolCallID { return [c.messageID, parent] }
return [c.messageID]
case .toolCallStarted(let c), .toolCallCompleted(let c):
if let parent = c.parentToolCallID { return [c.toolCallID, parent] }
return [c.toolCallID]
case .toolCallInputDelta(let d): return [d.toolCallID]
case .toolResult(let r): return [r.toolCallID]
case .fileChange(let f): return f.toolCallID.map { [$0] } ?? []
default: return []
}
}
/// One id's life within the unsealed region.
private struct IDSpan {
var firstRef: Int
var lastRef: Int
/// Result arrived → the tool (or Task, with its children) is done.
var resultSeen = false
/// A later chunk with a different messageID in the same scope → this message is done
/// (its authoritative non-partial text can only arrive before the next message starts).
var closedByChunk = false
/// For message/thinking ids: the owning subagent scope ("" = top level). The owner
/// Task's result closes everything inside it.
var chunkScope: String?
}
/// What an event *creates* in the folded item list, for the run-split rule (3).
private enum Creation {
/// A visible, never-dropped, non-tool row — a safe last-item for a seam.
case separator
/// A `.tool` row a future adjacent call could merge with.
case tool
/// A thinking row: a separator iff its final text is non-empty (an empty redacted block
/// is transparent to run coalescing, so it must be transparent to the seam rule too).
case thinking(String)
/// A lock note that may fold away (dropping it can fuse the runs around it), so it
/// counts as nothing — the seam just waits for the next hard separator.
case transparent
}
/// The furthest event index the stream can be sealed to right now, or nil when a region event
/// references an already-sealed id (premise violation → caller resets).
private func chooseWatermark(_ events: [AgentEvent]) -> Int? {
let start = sealedEventCount
let n = events.count - start
// Everything below needs the worktree root (rule 5); without it, just verify no sealed-id
// violation … but nothing is sealed if no root was ever found, so there is nothing to do.
guard worktreeRoot != nil else { return start }
guard n > edgeLag else {
// Too little unsealed to advance, but tail refs must still be validated against
// sealed ids so a violation triggers the reset path.
for r in 0..<n where Self.refs(of: events[start + r]).contains(where: sealedIDs.contains) {
return nil
}
return start
}
var spans: [String: IDSpan] = [:]
var creations: [Creation?] = Array(repeating: nil, count: n)
var thinkingText: [String: String] = [:]
var seenMessageItem = Set<String>()
var seenThinkingItem = Set<String>()
var seenToolItem = Set<String>()
var lastChunkInScope: [String: String] = [:]
var lastBoundary = -1 // region index of the latest turnCompleted/runFinished
for r in 0..<n {
let event = events[start + r]
for id in Self.refs(of: event) {
if sealedIDs.contains(id) { return nil }
if var span = spans[id] {
span.lastRef = r
spans[id] = span
} else {
spans[id] = IDSpan(firstRef: r, lastRef: r)
}
}
switch event.kind {
case .userText(let c), .assistantText(let c):
trackChunk(c, in: &spans, lastChunkInScope: &lastChunkInScope)
if seenMessageItem.insert(c.messageID).inserted { creations[r] = .separator }
case .thinking(let c):
trackChunk(c, in: &spans, lastChunkInScope: &lastChunkInScope)
let existing = thinkingText[c.messageID] ?? ""
thinkingText[c.messageID] = c.isPartial ? existing + c.text : c.text
if seenThinkingItem.insert(c.messageID).inserted { creations[r] = .thinking(c.messageID) }
case .toolCallStarted(let c), .toolCallCompleted(let c):
if seenToolItem.insert(c.toolCallID).inserted { creations[r] = .tool }
case .toolResult(let result):
spans[result.toolCallID]?.resultSeen = true
case .toolCallInputDelta, .fileChange, .approvalResolved:
break // refs (if any) tracked above; creates nothing
case .turnCompleted, .runFinished:
creations[r] = .separator
lastBoundary = r
case .sessionStarted, .usage, .rateLimit, .approvalRequested, .error:
creations[r] = .separator
case .note(let note):
if note.lockEvent && !showLockEvents { break }
if let lock = note.lock, !lock.paths.isEmpty { creations[r] = .transparent }
else { creations[r] = .separator }
case .raw:
if showRaw { creations[r] = .separator }
}
}
// Future-proofing (rule 2): an id wholly before the seam must be *closed* — provably done
// taking new events. An open id caps the seam at its first reference (it stays whole in
// the tail).
var cap = n - edgeLag
for (_, span) in spans where !isClosed(span, spans: spans, lastBoundary: lastBoundary) {
cap = min(cap, span.firstRef)
}
// Interval isolation (rule 1): no seam inside any id's [firstRef, lastRef] span.
// maxLastFromBefore[w] = the furthest lastRef among ids first referenced before w; a seam
// at w is isolation-safe iff that never reaches w.
var maxLastAtFirst = [Int](repeating: -1, count: n)
for span in spans.values {
maxLastAtFirst[span.firstRef] = max(maxLastAtFirst[span.firstRef], span.lastRef)
}
// Run-split rule (3): replay creations in item order; a seam is placeable after event r
// only while the last solid (visible, surviving) item is a hard separator. The replay
// resolves each thinking row against its *final* region text, which is exactly what the
// sealed fold will contain (open ids were already excluded by `cap`).
var separatorOK = [Bool](repeating: false, count: n)
var lastSolidIsSeparator = true // sealed prefix is empty or ends with a separator (invariant)
for r in 0..<n {
switch creations[r] {
case .separator: lastSolidIsSeparator = true
case .tool: lastSolidIsSeparator = false
case .thinking(let id):
let text = thinkingText[id] ?? ""
if !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
lastSolidIsSeparator = true
}
case .transparent, nil: break
}
separatorOK[r] = lastSolidIsSeparator
}
var runningMaxLast = -1
var best = 0
if cap >= 1 {
for w in 1...cap {
runningMaxLast = max(runningMaxLast, maxLastAtFirst[w - 1])
if separatorOK[w - 1] && runningMaxLast < w { best = w }
}
}
return start + best
}
/// Track a text/thinking chunk for message-closure: a new messageID in a scope closes the
/// previous one (chunks of one message never resume after the next begins — decoder order).
private func trackChunk(
_ chunk: TextChunk, in spans: inout [String: IDSpan], lastChunkInScope: inout [String: String]
) {
let scope = chunk.parentToolCallID ?? ""
spans[chunk.messageID]?.chunkScope = scope
if let previous = lastChunkInScope[scope], previous != chunk.messageID {
spans[previous]?.closedByChunk = true
}
lastChunkInScope[scope] = chunk.messageID
}
private func isClosed(_ span: IDSpan, spans: [String: IDSpan], lastBoundary: Int) -> Bool {
if span.lastRef < lastBoundary { return true } // its turn completed; ids don't cross turns
if span.resultSeen { return true }
if span.closedByChunk { return true }
if let scope = span.chunkScope, !scope.isEmpty, spans[scope]?.resultSeen == true {
return true // the owning subagent returned; its inner stream is done
}
return false
}
// MARK: - Sealing
private func seal(_ events: [AgentEvent], upTo watermark: Int) {
guard watermark > sealedEventCount else { return }
let slice = events[sealedEventCount..<watermark]
let segment = TranscriptProjection.buildSegment(
slice, worktreeRoot: worktreeRoot, priorEdits: sealedEdits,
showRaw: showRaw, showLockEvents: showLockEvents)
// Lock notes in this segment that folded onto edits sealed earlier: bake them in — the
// note is now sealed too, so the fold is final.
for patch in segment.priorLockPatches {
Self.applyLock(patch.lock, to: patch.toolCallID, at: sealedEditIndex, in: &sealedItems)
}
let editIDs = Set(segment.edits.map(\.toolCallID))
for item in segment.items {
let index = sealedItems.count
switch item.kind {
case .tool(let group) where editIDs.contains(group.toolCallID):
sealedEditIndex[group.toolCallID] = index
case .toolBlock(let groups):
for group in groups where editIDs.contains(group.toolCallID) {
sealedEditIndex[group.toolCallID] = index
}
default:
break
}
sealedItems.append(item)
}
sealedEdits.append(contentsOf: segment.edits)
for event in slice {
for id in Self.refs(of: event) { sealedIDs.insert(id) }
}
sealedEventCount = watermark
sealedLastSeq = events[watermark - 1].seq
}
// MARK: - Rendering
private func render(_ events: [AgentEvent]) -> [TranscriptItem] {
let tailSlice = events[sealedEventCount...]
guard !tailSlice.isEmpty else { return sealedItems }
let tail = TranscriptProjection.buildSegment(
tailSlice, worktreeRoot: worktreeRoot, priorEdits: sealedEdits,
showRaw: showRaw, showLockEvents: showLockEvents)
if tail.priorLockPatches.isEmpty { return sealedItems + tail.items }
// A live (unsealed) lock note folded onto a sealed edit card: patch a copy per read —
// the note may still be re-evaluated until it seals, so the base stays unpatched.
var patched = sealedItems
for patch in tail.priorLockPatches {
Self.applyLock(patch.lock, to: patch.toolCallID, at: sealedEditIndex, in: &patched)
}
return patched + tail.items
}
/// Append a folded lock line to a sealed edit's card, whether it renders alone or inside a
/// coalesced `.toolBlock`.
private static func applyLock(
_ lock: NoteLock, to toolCallID: String, at index: [String: Int],
in items: inout [TranscriptItem]
) {
guard let i = index[toolCallID] else { return }
switch items[i].kind {
case .tool(var group) where group.toolCallID == toolCallID:
group.lockLines.append(lock)
items[i].kind = .tool(group)
case .toolBlock(var groups):
guard let k = groups.firstIndex(where: { $0.toolCallID == toolCallID }) else { return }
groups[k].lockLines.append(lock)
items[i].kind = .toolBlock(groups)
default:
break
}
}
}
@@ -40,14 +40,19 @@ struct MarkdownText: View {
/// (scrolling, a sibling row streaming) and on every chat reopen, yet structural parsing /// (scrolling, a sibling row streaming) and on every chat reopen, yet structural parsing
/// is independent of `bodySize` — so the source string is a complete key. Bounded; /// is independent of `bodySize` — so the source string is a complete key. Bounded;
/// `NSCache` also evicts under memory pressure. /// `NSCache` also evicts under memory pressure.
///
/// The whole parse pipeline (both caches and the statics below) is `nonisolated`: `prewarm`
/// runs it from a detached background task by design, and as `View` statics they'd otherwise
/// be implicitly MainActor (a Swift 6 error for that call). `(unsafe)` on the caches is
/// sound because `NSCache` is thread-safe and the boxed values are immutable.
private final class ParsedBlocks { let blocks: [Block]; init(_ b: [Block]) { self.blocks = b } } private final class ParsedBlocks { let blocks: [Block]; init(_ b: [Block]) { self.blocks = b } }
private static let blockCache: NSCache<NSString, ParsedBlocks> = { nonisolated(unsafe) private static let blockCache: NSCache<NSString, ParsedBlocks> = {
let cache = NSCache<NSString, ParsedBlocks>() let cache = NSCache<NSString, ParsedBlocks>()
cache.countLimit = 2048 cache.countLimit = 2048
return cache return cache
}() }()
private static func parse(_ markdown: String) -> [Block] { nonisolated private static func parse(_ markdown: String) -> [Block] {
let key = markdown as NSString let key = markdown as NSString
if let hit = blockCache.object(forKey: key) { return hit.blocks } if let hit = blockCache.object(forKey: key) { return hit.blocks }
let blocks = parseUncached(markdown) let blocks = parseUncached(markdown)
@@ -55,7 +60,7 @@ struct MarkdownText: View {
return blocks return blocks
} }
private static func parseUncached(_ markdown: String) -> [Block] { nonisolated private static func parseUncached(_ markdown: String) -> [Block] {
var blocks: [Block] = [] var blocks: [Block] = []
var textBuffer: [String] = [] var textBuffer: [String] = []
func flush() { func flush() {
@@ -100,12 +105,12 @@ struct MarkdownText: View {
/// A GitHub-style table: a `|`-bearing header line immediately followed by a /// A GitHub-style table: a `|`-bearing header line immediately followed by a
/// `|---|:--:|` separator line. /// `|---|:--:|` separator line.
private static func isTableStart(_ lines: [String], _ index: Int) -> Bool { nonisolated private static func isTableStart(_ lines: [String], _ index: Int) -> Bool {
guard lines[index].contains("|"), index + 1 < lines.count else { return false } guard lines[index].contains("|"), index + 1 < lines.count else { return false }
return isSeparatorRow(lines[index + 1]) return isSeparatorRow(lines[index + 1])
} }
private static func isSeparatorRow(_ line: String) -> Bool { nonisolated private static func isSeparatorRow(_ line: String) -> Bool {
let cells = tableCells(line) let cells = tableCells(line)
guard !cells.isEmpty else { return false } guard !cells.isEmpty else { return false }
return cells.allSatisfy { cell in return cells.allSatisfy { cell in
@@ -115,7 +120,7 @@ struct MarkdownText: View {
/// Split a table row into trimmed cells, dropping the empties created by the /// Split a table row into trimmed cells, dropping the empties created by the
/// leading/trailing pipes. /// leading/trailing pipes.
private static func tableCells(_ line: String) -> [String] { nonisolated private static func tableCells(_ line: String) -> [String] {
var trimmed = line.trimmingCharacters(in: .whitespaces) var trimmed = line.trimmingCharacters(in: .whitespaces)
if trimmed.hasPrefix("|") { trimmed.removeFirst() } if trimmed.hasPrefix("|") { trimmed.removeFirst() }
if trimmed.hasSuffix("|") { trimmed.removeLast() } if trimmed.hasSuffix("|") { trimmed.removeLast() }
@@ -189,7 +194,7 @@ struct MarkdownText: View {
/// Classify one raw prose line. Headings are checked before bullets (a heading marker wins), /// Classify one raw prose line. Headings are checked before bullets (a heading marker wins),
/// and the plain case keeps the *raw* line (not the trimmed one) exactly as the old cascade /// and the plain case keeps the *raw* line (not the trimmed one) exactly as the old cascade
/// did — the inline parser preserves leading whitespace under `.inlineOnlyPreservingWhitespace`. /// did — the inline parser preserves leading whitespace under `.inlineOnlyPreservingWhitespace`.
private static func classify(_ raw: String) -> LineStyle { nonisolated private static func classify(_ raw: String) -> LineStyle {
let trimmed = raw.trimmingCharacters(in: .whitespaces) let trimmed = raw.trimmingCharacters(in: .whitespaces)
if trimmed.isEmpty { return .blank } if trimmed.isEmpty { return .blank }
// Headings scale relative to the base prose size so the hierarchy holds at any base and // Headings scale relative to the base prose size so the hierarchy holds at any base and
@@ -219,7 +224,7 @@ struct MarkdownText: View {
} }
/// Returns the content after a `- `, `* `, `+ ` or `N. ` list marker, else nil. /// Returns the content after a `- `, `* `, `+ ` or `N. ` list marker, else nil.
private static func bulletContent(_ trimmed: String) -> String? { nonisolated private static func bulletContent(_ trimmed: String) -> String? {
for marker in ["- ", "* ", "+ "] where trimmed.hasPrefix(marker) { for marker in ["- ", "* ", "+ "] where trimmed.hasPrefix(marker) {
return String(trimmed.dropFirst(marker.count)) return String(trimmed.dropFirst(marker.count))
} }
@@ -241,13 +246,13 @@ struct MarkdownText: View {
/// recur across re-renders and reopens, so memoize the parsed result. Independent of /// recur across re-renders and reopens, so memoize the parsed result. Independent of
/// `bodySize` (callers apply the font), so the source string is a complete key. /// `bodySize` (callers apply the font), so the source string is a complete key.
private final class InlineBox { let value: AttributedString; init(_ v: AttributedString) { self.value = v } } private final class InlineBox { let value: AttributedString; init(_ v: AttributedString) { self.value = v } }
private static let inlineCache: NSCache<NSString, InlineBox> = { nonisolated(unsafe) private static let inlineCache: NSCache<NSString, InlineBox> = {
let cache = NSCache<NSString, InlineBox>() let cache = NSCache<NSString, InlineBox>()
cache.countLimit = 16384 cache.countLimit = 16384
return cache return cache
}() }()
private static func attributedInline(_ string: String) -> AttributedString { nonisolated private static func attributedInline(_ string: String) -> AttributedString {
let key = string as NSString let key = string as NSString
if let hit = inlineCache.object(forKey: key) { return hit.value } if let hit = inlineCache.object(forKey: key) { return hit.value }
let options = AttributedString.MarkdownParsingOptions( let options = AttributedString.MarkdownParsingOptions(
@@ -272,7 +277,7 @@ struct MarkdownText: View {
/// keyed only by the source string (independent of `bodySize`), so a warm value equals what /// keyed only by the source string (independent of `bodySize`), so a warm value equals what
/// the main thread would compute, and a repeat call is a cheap cache hit. Cooperatively /// the main thread would compute, and a repeat call is a cheap cache hit. Cooperatively
/// cancellable — a huge transcript's warm loop bails the moment its owning task is cancelled. /// cancellable — a huge transcript's warm loop bails the moment its owning task is cancelled.
static func prewarm(_ markdown: String) { nonisolated static func prewarm(_ markdown: String) {
for block in parse(markdown) { // also warms the block cache for block in parse(markdown) { // also warms the block cache
if Task.isCancelled { return } if Task.isCancelled { return }
switch block { switch block {
@@ -102,26 +102,71 @@ enum TranscriptProjection {
/// owns them, project the main agent's own events at the top level, and recursively project /// owns them, project the main agent's own events at the top level, and recursively project
/// each subagent's events into the `children` of its spawn — so a subagent's inner work nests /// each subagent's events into the `children` of its spawn — so a subagent's inner work nests
/// under its card instead of leaking (and interleaving) into the main transcript. /// under its card instead of leaking (and interleaving) into the main transcript.
///
/// One-shot form: folds the whole stream in one pass. The live transcript uses
/// `IncrementalTranscriptProjection`, which folds the stream as seam-delimited segments via
/// `buildSegment` — this wrapper is the `priorEdits: []` whole-stream case of that.
static func build(_ events: [AgentEvent], showRaw: Bool, showLockEvents: Bool) -> [TranscriptItem] { static func build(_ events: [AgentEvent], showRaw: Bool, showLockEvents: Bool) -> [TranscriptItem] {
buildSegment(events[...], worktreeRoot: worktreeRoot(in: events[...]), priorEdits: [],
showRaw: showRaw, showLockEvents: showLockEvents).items
}
/// An edit-class tool call folded in an *earlier* segment, carried forward so a later
/// segment's lock notes can still fold onto it across the seam (a lock's `released` note
/// lands when the file *lands* in the parent — potentially many turns after the edit).
struct PriorEdit: Equatable {
let toolCallID: String
/// Repo-relative, normalized paths this call writes (`editedPaths` ∘ `normalizeForLock`).
let paths: [String]
}
/// The folded output of one contiguous slice of the stream.
struct SegmentFold {
var items: [TranscriptItem]
/// Lock lines whose backward path-match crossed the seam onto a `priorEdits` entry, in
/// note order. The caller owns those already-folded items and must attach these to them —
/// that's what keeps `fold(prefix) ++ fold(tail)` byte-identical to `fold(whole)` even
/// for a lock note released turns after its edit.
var priorLockPatches: [(toolCallID: String, lock: NoteLock)]
/// This segment's own edit-class calls (in item order), for the caller's registry.
var edits: [PriorEdit]
}
/// Fold one contiguous slice of the stream with an injected worktree root (a mid-stream
/// slice lacks the seq-0 `sessionStarted` that `build` rescans for). The slice must be
/// seam-delimited — no coalescing key, subagent scope, or open tool run straddling either
/// end — which is exactly what `IncrementalTranscriptProjection` guarantees before calling.
static func buildSegment(
_ events: ArraySlice<AgentEvent>, worktreeRoot root: String?, priorEdits: [PriorEdit],
showRaw: Bool, showLockEvents: Bool
) -> SegmentFold {
let (topLevel, byParent) = partition(events) let (topLevel, byParent) = partition(events)
// Fold lock-lifecycle notes onto the edit cards they bracket, exactly as the Mac's // Fold lock-lifecycle notes onto the edit cards they bracket, exactly as the Mac's
// `items(_:worktreeRoot:)` does — matched against the session's working directory so an // `items(_:worktreeRoot:)` does — matched against the session's working directory so an
// edit's absolute `file_path` compares against the note's repo-relative paths. Folding // edit's absolute `file_path` compares against the note's repo-relative paths. Folding
// happens only at the top level (a subagent's inner edits are literal, unlocked); the // happens only at the top level (a subagent's inner edits are literal, unlocked); the
// subagent recursion below stays plain, matching the desktop projection. // subagent recursion below stays plain, matching the desktop projection.
let root = worktreeRoot(in: topLevel) var patches: [(toolCallID: String, lock: NoteLock)] = []
let flat = foldLockNotes(flatItems(topLevel, showRaw: showRaw, showLockEvents: showLockEvents), let flat = foldLockNotes(flatItems(topLevel, showRaw: showRaw, showLockEvents: showLockEvents),
worktreeRoot: root) worktreeRoot: root, priorEdits: priorEdits, priorLockPatches: &patches)
return coalesceToolRuns(flat).map { let items = coalesceToolRuns(flat).map {
attachSubagentChildren($0, byParent: byParent, depth: 0, attachSubagentChildren($0, byParent: byParent, depth: 0,
showRaw: showRaw, showLockEvents: showLockEvents) showRaw: showRaw, showLockEvents: showLockEvents)
} }
let edits = flat.compactMap { item -> PriorEdit? in
guard case .tool(let group) = item.kind else { return nil }
let paths = editedPaths(toolName: group.name, input: group.input)
.map { normalizeForLock($0, worktreeRoot: root) }
.filter { !$0.isEmpty }
return paths.isEmpty ? nil : PriorEdit(toolCallID: group.toolCallID, paths: paths)
}
return SegmentFold(items: items, priorLockPatches: patches, edits: edits)
} }
/// The session's working directory, read from its `sessionStarted` event, so an edit's /// The session's working directory, read from its `sessionStarted` event, so an edit's
/// absolute `file_path` can be made repo-relative to compare against a lock note's /// absolute `file_path` can be made repo-relative to compare against a lock note's
/// repo-relative paths. `nil` before the start event is seen (nothing to fold against yet). /// repo-relative paths. `nil` before the start event is seen (nothing to fold against yet).
private static func worktreeRoot(in events: [AgentEvent]) -> String? { static func worktreeRoot(in events: ArraySlice<AgentEvent>) -> String? {
for event in events { for event in events {
if case .sessionStarted(let started) = event.kind, !started.cwd.isEmpty { if case .sessionStarted(let started) = event.kind, !started.cwd.isEmpty {
return started.cwd return started.cwd
@@ -138,7 +183,15 @@ enum TranscriptProjection {
/// `file_path` is stripped of `worktreeRoot` and normalized so it compares against the note's /// `file_path` is stripped of `worktreeRoot` and normalized so it compares against the note's
/// repo-relative paths. A note overlapping no preceding edit is left in place (renders /// repo-relative paths. A note overlapping no preceding edit is left in place (renders
/// standalone), matching the Mac's `foldLockNotes`. /// standalone), matching the Mac's `foldLockNotes`.
private static func foldLockNotes(_ flat: [TranscriptItem], worktreeRoot: String?) -> [TranscriptItem] { ///
/// The backward scan continues past the front of `flat` into `priorEdits` — the edits of
/// already-folded earlier segments, oldest first — so a note whose edit was sealed in a prior
/// segment (a lock released turns later) still folds exactly where a whole-stream fold would
/// put it. Those hits are reported via `priorLockPatches` for the caller to attach.
private static func foldLockNotes(
_ flat: [TranscriptItem], worktreeRoot: String?, priorEdits: [PriorEdit],
priorLockPatches: inout [(toolCallID: String, lock: NoteLock)]
) -> [TranscriptItem] {
// Each tool item's repo-relative edited paths (only edit-class tools have any), by index. // Each tool item's repo-relative edited paths (only edit-class tools have any), by index.
var editsByIndex: [Int: (id: String, paths: [String])] = [:] var editsByIndex: [Int: (id: String, paths: [String])] = [:]
for (i, item) in flat.enumerated() { for (i, item) in flat.enumerated() {
@@ -148,7 +201,8 @@ enum TranscriptProjection {
.filter { !$0.isEmpty } .filter { !$0.isEmpty }
if !paths.isEmpty { editsByIndex[i] = (group.toolCallID, paths) } if !paths.isEmpty { editsByIndex[i] = (group.toolCallID, paths) }
} }
guard !editsByIndex.isEmpty else { return flat } guard !editsByIndex.isEmpty || !priorEdits.isEmpty else { return flat }
let priorIDs = Set(priorEdits.map(\.toolCallID))
var locksByCall: [String: [NoteLock]] = [:] var locksByCall: [String: [NoteLock]] = [:]
var folded = Set<Int>() var folded = Set<Int>()
@@ -167,14 +221,27 @@ enum TranscriptProjection {
guard let edit = editsByIndex[j] else { continue } guard let edit = editsByIndex[j] else { continue }
if edit.paths.contains(where: { pathsOverlap($0, path) }) { hitID = edit.id; break } if edit.paths.contains(where: { pathsOverlap($0, path) }) { hitID = edit.id; break }
} }
if hitID == nil {
// Nothing in this segment — keep scanning backward across the seam, newest
// prior edit first, exactly where a whole-stream scan would look next.
for prior in priorEdits.reversed()
where prior.paths.contains(where: { pathsOverlap($0, path) }) {
hitID = prior.toolCallID
break
}
}
guard let hitID else { matchedAll = false; break } guard let hitID else { matchedAll = false; break }
if let k = indexByID[hitID] { perCard[k].paths.append(path) } if let k = indexByID[hitID] { perCard[k].paths.append(path) }
else { indexByID[hitID] = perCard.count; perCard.append((hitID, [path])) } else { indexByID[hitID] = perCard.count; perCard.append((hitID, [path])) }
} }
guard matchedAll, !perCard.isEmpty else { continue } guard matchedAll, !perCard.isEmpty else { continue }
for card in perCard { for card in perCard {
if priorIDs.contains(card.id) {
priorLockPatches.append((card.id, NoteLock(state: lock.state, paths: card.paths)))
} else {
locksByCall[card.id, default: []].append(NoteLock(state: lock.state, paths: card.paths)) locksByCall[card.id, default: []].append(NoteLock(state: lock.state, paths: card.paths))
} }
}
folded.insert(i) folded.insert(i)
} }
guard !folded.isEmpty else { return flat } guard !folded.isEmpty else { return flat }
@@ -235,7 +302,7 @@ enum TranscriptProjection {
/// Split a scope's events into the main agent's own (`topLevel`) and each subagent's, keyed by /// Split a scope's events into the main agent's own (`topLevel`) and each subagent's, keyed by
/// the spawning `Task`'s id. /// the spawning `Task`'s id.
private static func partition(_ events: [AgentEvent]) private static func partition(_ events: ArraySlice<AgentEvent>)
-> (topLevel: [AgentEvent], byParent: [String: [AgentEvent]]) -> (topLevel: [AgentEvent], byParent: [String: [AgentEvent]])
{ {
let parentOf = toolParentMap(events) let parentOf = toolParentMap(events)
@@ -254,7 +321,7 @@ enum TranscriptProjection {
/// Maps each tool-call id to its parent subagent's id, built from the call start/complete /// Maps each tool-call id to its parent subagent's id, built from the call start/complete
/// events. A tool *result* or *file change* names only a tool id, so it inherits its subagent /// events. A tool *result* or *file change* names only a tool id, so it inherits its subagent
/// scope from the call it belongs to via this map. /// scope from the call it belongs to via this map.
private static func toolParentMap(_ events: [AgentEvent]) -> [String: String] { private static func toolParentMap(_ events: ArraySlice<AgentEvent>) -> [String: String] {
var map: [String: String] = [:] var map: [String: String] = [:]
for event in events { for event in events {
switch event.kind { switch event.kind {
@@ -512,4 +579,21 @@ extension JSONValue {
default: return false default: return false
} }
} }
/// A one-line gist of a tool input/result for the collapsed row. Lives here (not with the
/// row views) because the projection folds it into `.raw` bodies, and this file also builds
/// standalone as the host-testable `NucleicRemoteProjection` SwiftPM target.
var compactSummary: String {
switch self {
case .string(let s): return s
case .object(let o):
if let cmd = o["command"]?.stringValue { return cmd }
if let path = o["file_path"]?.stringValue ?? o["path"]?.stringValue { return path }
return o.keys.sorted().joined(separator: ", ")
case .array(let a): return "[\(a.count) items]"
case .number(let n): return String(n)
case .bool(let b): return String(b)
case .null: return "null"
}
}
} }
@@ -250,20 +250,8 @@ extension JSONValue {
return canonical == "null" ? "" : canonical return canonical == "null" ? "" : canonical
} }
/// A one-line gist of a tool input/result for the collapsed row. // (`compactSummary` — the one-line gist — lives in TranscriptProjection.swift, which builds
var compactSummary: String { // standalone as the host-testable NucleicRemoteProjection SwiftPM target and needs it there.)
switch self {
case .string(let s): return s
case .object(let o):
if let cmd = o["command"]?.stringValue { return cmd }
if let path = o["file_path"]?.stringValue ?? o["path"]?.stringValue { return path }
return o.keys.sorted().joined(separator: ", ")
case .array(let a): return "[\(a.count) items]"
case .number(let n): return String(n)
case .bool(let b): return String(b)
case .null: return "null"
}
}
/// The full, **untruncated** content of a tool input for the approval card — /// The full, **untruncated** content of a tool input for the approval card —
/// the user must see exactly what they are granting before allowing. Unlike /// the user must see exactly what they are granting before allowing. Unlike
@@ -50,10 +50,12 @@ struct SessionLiveActivity: Widget {
} }
} }
} compactLeading: { } compactLeading: {
// Calm state shows the Nucleic Control brand mark (the purple atom); when the
// user is needed it switches to the amber approval glyph.
Image(systemName: state.needsAttention Image(systemName: state.needsAttention
? ActivityPalette.glyph(.approval) : ActivityPalette.glyph(.running)) ? ActivityPalette.glyph(.approval) : "atom")
.foregroundStyle(state.needsAttention .foregroundStyle(state.needsAttention
? ActivityPalette.attention : ActivityPalette.active) ? ActivityPalette.attention : ActivityPalette.brand)
} compactTrailing: { } compactTrailing: {
Text("\(state.needsAttention ? max(state.approvalCount, state.needsYouCount) : state.runningCount)") Text("\(state.needsAttention ? max(state.approvalCount, state.needsYouCount) : state.runningCount)")
.font(.caption2.bold()) .font(.caption2.bold())
@@ -280,6 +282,7 @@ private enum ActivityPalette {
static let success = Color(red: 0.30, green: 0.78, blue: 0.45) // done green static let success = Color(red: 0.30, green: 0.78, blue: 0.45) // done green
static let danger = Color(red: 0.92, green: 0.34, blue: 0.34) // error red static let danger = Color(red: 0.92, green: 0.34, blue: 0.34) // error red
static let neutral = Color.secondary static let neutral = Color.secondary
static let brand = Color(red: 0.62, green: 0.51, blue: 0.93) // Nucleic Control purple
static let added = Color(red: 0.30, green: 0.78, blue: 0.45) static let added = Color(red: 0.30, green: 0.78, blue: 0.45)
static let removed = Color(red: 0.92, green: 0.34, blue: 0.34) static let removed = Color(red: 0.92, green: 0.34, blue: 0.34)