Merge nucleic/mellow-ember-shrew into dev

This commit is contained in:
2026-07-13 00:49:58 -07:00
parent 6a28d3e1d9
commit 80aca315e7
3 changed files with 50 additions and 9 deletions
@@ -116,11 +116,13 @@ struct SettingsView: View {
Section { Section {
Toggle("Show lock events", isOn: $showLockEvents) Toggle("Show lock events", isOn: $showLockEvents)
Toggle("Show raw events", isOn: $showRaw) Toggle("Show advanced detail", isOn: $showRaw)
} header: { } header: {
Text("Transcript") Text("Transcript")
} footer: { } footer: {
Text("") Text("Advanced detail surfaces per-turn token counts, rate-limit notices, turn "
+ "markers, and raw events. Hidden by default so consecutive tool calls "
+ "read as one group.")
} }
Section { Section {
@@ -265,17 +265,31 @@ final class IncrementalTranscriptProjection {
spans[result.toolCallID]?.resultSeen = true spans[result.toolCallID]?.resultSeen = true
case .toolCallInputDelta, .fileChange, .approvalResolved, .codexUsage: case .toolCallInputDelta, .fileChange, .approvalResolved, .codexUsage:
break // refs (if any) tracked above; creates nothing break // refs (if any) tracked above; creates nothing
case .turnCompleted, .runFinished: case .runFinished:
creations[r] = .separator creations[r] = .separator
lastBoundary = r lastBoundary = r
case .sessionStarted, .usage, .rateLimit, .approvalRequested, .error: case .turnCompleted:
// The turn marker is a debug row now (held aside from tool runs by
// `rendersNothing`, then filtered from display), so it's transparent to the seam —
// like a droppable lock note, a seam can't rest on it. It still closes the ids of
// its turn, so keep it as a boundary.
creations[r] = .transparent
lastBoundary = r
case .sessionStarted, .approvalRequested, .error:
creations[r] = .separator creations[r] = .separator
case .usage, .rateLimit:
// Debug rows (token tallies, rate-limit notices): held aside from tool runs and
// filtered from display, so — like a droppable lock note — they're transparent to
// the seam rather than a hard separator it can rest on.
creations[r] = .transparent
case .note(let note): case .note(let note):
if note.lockEvent && !showLockEvents { break } if note.lockEvent && !showLockEvents { break }
if let lock = note.lock, !lock.paths.isEmpty { creations[r] = .transparent } if let lock = note.lock, !lock.paths.isEmpty { creations[r] = .transparent }
else { creations[r] = .separator } else { creations[r] = .separator }
case .raw: case .raw:
if showRaw { creations[r] = .separator } // Shown only with advanced detail, and even then a debug row held aside from tool
// runs — so transparent to the seam, not a separator.
if showRaw { creations[r] = .transparent }
} }
} }
@@ -60,9 +60,25 @@ struct TranscriptItem: Identifiable, Equatable {
return false return false
} }
/// Items that draw no visible row. They must not break a run of tool calls when coalescing: /// Diagnostic, debug-level rows: per-turn token tallies, rate-limit notices, turn markers, and
/// an invisible row sitting between two calls would otherwise split them into two cards. /// unrecognized raw passthrough events. Hidden by default to keep the transcript clean — they're
var rendersNothing: Bool { isEmptyThinking } /// the "token counts and other lines between tool calls" that fragmented a run of cards — and
/// surfaced only when "advanced detail" (`showRaw`) is on. Prose, thinking, tool calls,
/// session-start, approvals, run outcome, and errors are never debug. Mirrors the Mac's
/// `TranscriptItem.isDebug`.
var isDebug: Bool {
switch kind {
case .usage, .rateLimit, .turnBoundary, .raw: return true
default: return false
}
}
/// Items that draw no visible row (once debug rows are filtered out for display). They must not
/// break a run of tool calls when coalescing: a debug/invisible row sitting between two calls
/// would otherwise split them into two cards with a gap. Such rows are held aside during
/// coalescing and re-emitted after the block, so consecutive tool cards stay one contiguous
/// group. Mirrors the Mac's `rendersNothing` (`isDebug || isEmptyThinking`).
var rendersNothing: Bool { isDebug || isEmptyThinking }
} }
/// One tool call's coalesced lifecycle: start → input deltas → complete → result → file changes. /// One tool call's coalesced lifecycle: start → input deltas → complete → result → file changes.
@@ -169,10 +185,19 @@ enum TranscriptProjection {
var patches: [(toolCallID: String, lock: NoteLock)] = [] 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, priorEdits: priorEdits, priorLockPatches: &patches) worktreeRoot: root, priorEdits: priorEdits, priorLockPatches: &patches)
let items = coalesceToolRuns(flat).map { let coalesced = coalesceToolRuns(flat).map {
attachSubagentChildren($0, byParent: byParent, depth: 0, attachSubagentChildren($0, byParent: byParent, depth: 0,
showRaw: showRaw, showLockEvents: showLockEvents) showRaw: showRaw, showLockEvents: showLockEvents)
} }
// Drop the diagnostic rows (token tallies, rate-limit notices, turn markers, raw
// passthrough) unless "advanced detail" is on. `coalesceToolRuns` already held them aside
// from any tool run (they're `rendersNothing`), so removing them here leaves consecutive
// tool cards as one contiguous block instead of a stack fragmented by token-count lines.
// Filtering this shared choke point — rather than the view — keeps the incremental
// projector's `fold(prefix) ++ fold(tail)` byte-identical to `fold(whole)`: a debug row is
// never a tool edit or lock target, so the filter distributes cleanly over the seam and the
// per-segment `edits`/`priorLockPatches` bookkeeping below is unaffected.
let items = showRaw ? coalesced : coalesced.filter { !$0.isDebug }
let edits = flat.compactMap { item -> PriorEdit? in let edits = flat.compactMap { item -> PriorEdit? in
guard case .tool(let group) = item.kind else { return nil } guard case .tool(let group) = item.kind else { return nil }
let paths = editedPaths(toolName: group.name, input: group.input) let paths = editedPaths(toolName: group.name, input: group.input)