diff --git a/NucleicRemote/NucleicRemote/Views/SettingsView.swift b/NucleicRemote/NucleicRemote/Views/SettingsView.swift index 838f2c8..6dd638a 100644 --- a/NucleicRemote/NucleicRemote/Views/SettingsView.swift +++ b/NucleicRemote/NucleicRemote/Views/SettingsView.swift @@ -116,11 +116,13 @@ struct SettingsView: View { Section { Toggle("Show lock events", isOn: $showLockEvents) - Toggle("Show raw events", isOn: $showRaw) + Toggle("Show advanced detail", isOn: $showRaw) } header: { Text("Transcript") } 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 { diff --git a/NucleicRemote/NucleicRemote/Views/Transcript/IncrementalTranscriptProjection.swift b/NucleicRemote/NucleicRemote/Views/Transcript/IncrementalTranscriptProjection.swift index b702694..7eb7905 100644 --- a/NucleicRemote/NucleicRemote/Views/Transcript/IncrementalTranscriptProjection.swift +++ b/NucleicRemote/NucleicRemote/Views/Transcript/IncrementalTranscriptProjection.swift @@ -265,17 +265,31 @@ final class IncrementalTranscriptProjection { spans[result.toolCallID]?.resultSeen = true case .toolCallInputDelta, .fileChange, .approvalResolved, .codexUsage: break // refs (if any) tracked above; creates nothing - case .turnCompleted, .runFinished: + case .runFinished: creations[r] = .separator 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 + 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): 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 } + // 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 } } } diff --git a/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift b/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift index 71f7be0..ee93090 100644 --- a/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift +++ b/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift @@ -60,9 +60,25 @@ struct TranscriptItem: Identifiable, Equatable { return false } - /// Items that draw no visible row. They must not break a run of tool calls when coalescing: - /// an invisible row sitting between two calls would otherwise split them into two cards. - var rendersNothing: Bool { isEmptyThinking } + /// Diagnostic, debug-level rows: per-turn token tallies, rate-limit notices, turn markers, and + /// unrecognized raw passthrough events. Hidden by default to keep the transcript clean — they're + /// 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. @@ -169,10 +185,19 @@ enum TranscriptProjection { var patches: [(toolCallID: String, lock: NoteLock)] = [] let flat = foldLockNotes(flatItems(topLevel, showRaw: showRaw, showLockEvents: showLockEvents), worktreeRoot: root, priorEdits: priorEdits, priorLockPatches: &patches) - let items = coalesceToolRuns(flat).map { + let coalesced = coalesceToolRuns(flat).map { attachSubagentChildren($0, byParent: byParent, depth: 0, 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 guard case .tool(let group) = item.kind else { return nil } let paths = editedPaths(toolName: group.name, input: group.input)