From 8f67ed6dd1e6de9612fb0680ebfb1775db21e941 Mon Sep 17 00:00:00 2001 From: Nucleic Date: Sat, 18 Jul 2026 00:33:08 -0700 Subject: [PATCH] Merge nucleic/calm-yarn-robin-cmec into dev --- .../Views/Transcript/ExitPlanModeCard.swift | 33 +++++++++++++++++++ .../Views/Transcript/ToolGroupRow.swift | 1 + .../Transcript/TranscriptProjection.swift | 1 + .../NucleicRemote/Views/TranscriptRow.swift | 11 +++++++ 4 files changed, 46 insertions(+) create mode 100644 NucleicRemote/NucleicRemote/Views/Transcript/ExitPlanModeCard.swift diff --git a/NucleicRemote/NucleicRemote/Views/Transcript/ExitPlanModeCard.swift b/NucleicRemote/NucleicRemote/Views/Transcript/ExitPlanModeCard.swift new file mode 100644 index 0000000..1744e03 --- /dev/null +++ b/NucleicRemote/NucleicRemote/Views/Transcript/ExitPlanModeCard.swift @@ -0,0 +1,33 @@ +import SwiftUI +import NucleicProtocol + +/// Pretty rendering of an `ExitPlanMode` tool call — the phone echo of the Mac's `ExitPlanModeCard`. +/// The agent calls `ExitPlanMode` to present the plan it wrote on its way out of plan mode; the raw +/// tool card reads as escaped JSON with an unrendered Markdown blob, so instead we render the plan +/// body as formatted Markdown under a "Plan" header. The plan is on the call input, so the card +/// shows the moment the call arrives — before, and regardless of, the user's approval. Mirrors +/// `AskUserQuestionResultCard`'s accent-tinted card so a presented plan reads as a first-class moment. +struct ExitPlanModeCard: View { + let plan: ExitPlanMode.Plan + + private var accent: Color { Palette.accent } + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 8) { + Image(systemName: "list.bullet.clipboard") + .font(.caption).foregroundStyle(accent).frame(width: 15) + Text("Plan") + .font(.callout.weight(.medium)) + Spacer(minLength: 0) + } + MarkdownText(markdown: plan.markdown) + .textSelection(.enabled) + } + .padding(.horizontal, 12).padding(.vertical, 10) + .frame(maxWidth: .infinity, alignment: .leading) + .background(accent.opacity(0.06)) + .overlay(RoundedRectangle(cornerRadius: 8).strokeBorder(accent.opacity(0.16), lineWidth: 1)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } +} diff --git a/NucleicRemote/NucleicRemote/Views/Transcript/ToolGroupRow.swift b/NucleicRemote/NucleicRemote/Views/Transcript/ToolGroupRow.swift index 6b24d46..78a776d 100644 --- a/NucleicRemote/NucleicRemote/Views/Transcript/ToolGroupRow.swift +++ b/NucleicRemote/NucleicRemote/Views/Transcript/ToolGroupRow.swift @@ -581,6 +581,7 @@ enum ToolGlyph { case "Task", "Agent", ToolGroup.orchestraSubagentToolName: return "person.2" case "TodoWrite": return "checklist" case "AskUserQuestion": return "questionmark.bubble" + case ExitPlanMode.toolName: return "list.bullet.clipboard" case HostCommandSummary.hostExecToolName: return "desktopcomputer" default: return "wrench.and.screwdriver" } diff --git a/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift b/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift index 9cb5faa..f75ebdb 100644 --- a/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift +++ b/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift @@ -115,6 +115,7 @@ struct ToolGroup: Equatable { name == "Task" || name == "Agent" || name == Self.orchestraSubagentToolName } var isAskUserQuestion: Bool { name == "AskUserQuestion" } + var isExitPlanMode: Bool { name == ExitPlanMode.toolName } /// The literal shell command a `Bash` call ran, if any (nil for every other tool). Used to /// detect a git-commit pipeline that should read as a structured commit card at the row level, diff --git a/NucleicRemote/NucleicRemote/Views/TranscriptRow.swift b/NucleicRemote/NucleicRemote/Views/TranscriptRow.swift index 907ceca..4fb2551 100644 --- a/NucleicRemote/NucleicRemote/Views/TranscriptRow.swift +++ b/NucleicRemote/NucleicRemote/Views/TranscriptRow.swift @@ -24,6 +24,8 @@ struct TranscriptRow: View { SubagentCard(group: group) } else if let card = answeredQuestionCard(group) { card + } else if let plan = planCard(group) { + plan } else if let card = gitCommitRowCard(group) { card } else if group.isHostExec { @@ -78,6 +80,15 @@ struct TranscriptRow: View { return AskUserQuestionResultCard(questions: questions, answers: answers) } + /// The plan an `ExitPlanMode` call presents, rendered as formatted Markdown rather than the + /// escaped-JSON blob the generic tool card would show — the phone echo of the Mac's `planCard`. + /// Nil for any other tool (which keeps the normal collapsible card) and for a call with no plan + /// text. The plan is on the call input, so it shows whether or not the user has approved it yet. + private func planCard(_ group: ToolGroup) -> ExitPlanModeCard? { + guard group.isExitPlanMode, let plan = ExitPlanMode.plan(from: group.input) else { return nil } + return ExitPlanModeCard(plan: plan) + } + /// A git-commit `Bash` pipeline rendered as the structured commit card the Mac shows at the row /// level (subject + Markdown body + the new hash + "Show command"), or nil for any non-commit /// call — which keeps the normal collapsible tool card. Mirrors the Mac's `gitCommitCard`.