Merge nucleic/vivid-spruce-robin-bacs into dev
This commit is contained in:
@@ -144,9 +144,9 @@ struct SettingsView: View {
|
||||
} header: {
|
||||
Text("Transcript")
|
||||
} footer: {
|
||||
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.")
|
||||
Text("Advanced detail surfaces model thought traces, per-turn token counts, "
|
||||
+ "rate-limit notices, turn markers, and raw events. Hidden by default "
|
||||
+ "so conversations stay focused and consecutive tool calls read as one group.")
|
||||
}
|
||||
|
||||
Section {
|
||||
|
||||
@@ -25,8 +25,8 @@ import NucleicProtocol
|
||||
/// 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.)
|
||||
/// future tool call can't merge across. (Thought traces and other advanced-detail 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
|
||||
@@ -205,11 +205,8 @@ final class IncrementalTranscriptProjection {
|
||||
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.
|
||||
/// A thought trace or other row held aside from the conversation, or a lock note that
|
||||
/// may fold away. It counts as nothing so the seam waits for the next hard separator.
|
||||
case transparent
|
||||
}
|
||||
|
||||
@@ -232,9 +229,7 @@ final class IncrementalTranscriptProjection {
|
||||
|
||||
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
|
||||
@@ -256,9 +251,7 @@ final class IncrementalTranscriptProjection {
|
||||
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) }
|
||||
creations[r] = .transparent
|
||||
case .toolCallStarted(let c), .toolCallCompleted(let c):
|
||||
if seenToolItem.insert(c.toolCallID).inserted { creations[r] = .tool }
|
||||
case .toolResult(let result):
|
||||
@@ -310,20 +303,13 @@ final class IncrementalTranscriptProjection {
|
||||
}
|
||||
|
||||
// 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`).
|
||||
// only while the last solid (visible, surviving) item is a hard separator.
|
||||
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
|
||||
|
||||
@@ -64,15 +64,14 @@ struct TranscriptItem: Identifiable, Equatable {
|
||||
return false
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
/// Advanced-detail rows: model thought traces, per-turn token tallies, rate-limit notices,
|
||||
/// turn markers, and unrecognized raw passthrough events. Hidden by default to keep the
|
||||
/// transcript focused on the conversation, and surfaced only when "advanced detail"
|
||||
/// (`showRaw`) is on. Prose, 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
|
||||
case .thinking, .usage, .rateLimit, .turnBoundary, .raw: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
@@ -416,8 +415,9 @@ enum TranscriptProjection {
|
||||
showRaw: Bool, showLockEvents: Bool
|
||||
) -> [TranscriptItem] {
|
||||
let items = coalesceToolRuns(flatItems(events, showRaw: showRaw, showLockEvents: showLockEvents))
|
||||
guard depth < maxSubagentDepth else { return items }
|
||||
return items.map {
|
||||
let visible = showRaw ? items : items.filter { !$0.isDebug }
|
||||
guard depth < maxSubagentDepth else { return visible }
|
||||
return visible.map {
|
||||
attachSubagentChildren($0, byParent: byParent, depth: depth,
|
||||
showRaw: showRaw, showLockEvents: showLockEvents)
|
||||
}
|
||||
@@ -462,8 +462,9 @@ enum TranscriptProjection {
|
||||
/// followed by the agent's own `Bash` calls yields a fan-out card *and* a tool block, not one
|
||||
/// generic block titled by the first `Task` that would swallow the fan-out.
|
||||
///
|
||||
/// `flatItems` already drops the only kind the phone has (empty redacted thinking), so the
|
||||
/// hold path is a guard for a caller passing unfiltered items — not a live case.
|
||||
/// Advanced-detail rows, including non-empty thought traces, take the hold path in the live
|
||||
/// projection. They are re-emitted after the block when detail is enabled and filtered out
|
||||
/// otherwise.
|
||||
static func coalesceToolRuns(_ flat: [TranscriptItem]) -> [TranscriptItem] {
|
||||
var out: [TranscriptItem] = []
|
||||
var run: [TranscriptItem] = [] // consecutive `.tool` items
|
||||
|
||||
Reference in New Issue
Block a user