From 9af5019bf70fd607e6423e2cc1c96e48ded65002 Mon Sep 17 00:00:00 2001 From: Nucleic Date: Fri, 17 Jul 2026 22:22:13 -0700 Subject: [PATCH] Merge nucleic/upbeat-cedar-weasel-1orz into dev --- .../Views/Transcript/TranscriptProjection.swift | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift b/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift index ee93090..9cb5faa 100644 --- a/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift +++ b/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift @@ -451,6 +451,12 @@ enum TranscriptProjection { /// aside and re-emitted right after the block, so the calls stay one contiguous block. /// Mirrors the desktop projection. /// + /// A subagent spawn (`Task`/`Agent`) only groups with *other* subagent spawns: a run of them + /// is a multi-agent fan-out (the gold orchestration card), which the view gates on the block + /// being all-orchestration. So a subagent-ness switch breaks the run — a wave of `Task`s + /// 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. static func coalesceToolRuns(_ flat: [TranscriptItem]) -> [TranscriptItem] { @@ -474,7 +480,14 @@ enum TranscriptProjection { } for item in flat { - if case .tool = item.kind { + if case .tool(let group) = item.kind { + // A subagent spawn and an ordinary tool call never share a run (see above): + // switching kind flushes the run in progress so each side forms its own card. + // `isOrchestration` mirrors the view's fan-out gate (`groups.allSatisfy(\.isOrchestration)`) + // exactly — worker spawns included — so no mixed block can ever reach it. + if case .tool(let first)? = run.first?.kind, first.isOrchestration != group.isOrchestration { + flush() + } run.append(item) } else if item.rendersNothing, !run.isEmpty { held.append(item)