diff --git a/NucleicRemote/NucleicRemote/Views/ApprovalCardView.swift b/NucleicRemote/NucleicRemote/Views/ApprovalCardView.swift index 049805c..f63cee1 100644 --- a/NucleicRemote/NucleicRemote/Views/ApprovalCardView.swift +++ b/NucleicRemote/NucleicRemote/Views/ApprovalCardView.swift @@ -170,13 +170,22 @@ struct ApprovalCardView: View { struct ExitPlanModeApprovalCardView: View { @EnvironmentObject var store: RemoteStore let approval: ApprovalRequest - let plan: ExitPlanMode.Plan? + let plan: PlanReview.Plan? var availableHeight: CGFloat = 0 @State private var revising = false @State private var feedback = "" private var canRespond: Bool { store.connectivity.isLive } + private var isLegacyExitPlanMode: Bool { approval.toolName == ExitPlanMode.toolName } + private var rejectionReason: String { + isLegacyExitPlanMode ? ExitPlanMode.rejectionReason : PlanReview.rejectionReason + } + private func revisionReason(_ feedback: String) -> String? { + isLegacyExitPlanMode + ? ExitPlanMode.revisionReason(feedback: feedback) + : PlanReview.revisionReason(feedback: feedback) + } var body: some View { VStack(alignment: .leading, spacing: 10) { @@ -193,13 +202,13 @@ struct ExitPlanModeApprovalCardView: View { // Claude versions that don't include the plan Markdown in the permission payload // still need the three-way review. State that plainly instead of degrading to the // generic permission card (which would incorrectly offer Allow Always). - Text("Claude is ready to leave plan mode and begin implementation.") + Text("The agent is ready to begin implementation and is waiting for plan review.") .font(.footnote).foregroundStyle(.secondary) } if revising { VStack(alignment: .leading, spacing: 6) { - Text("What should Claude change?") + Text("What should the agent change?") .font(.footnote.weight(.semibold)) TextField( "Describe the changes you want in the plan…", @@ -219,13 +228,13 @@ struct ExitPlanModeApprovalCardView: View { Button("Send Revision") { submitRevision() } .frame(maxWidth: .infinity) .buttonStyle(.borderedProminent) - .disabled(!canRespond || ExitPlanMode.revisionReason(feedback: feedback) == nil) + .disabled(!canRespond || revisionReason(feedback) == nil) .keyboardShortcut(.defaultAction) } } else { HStack(spacing: 8) { Button(role: .destructive) { - store.respond(approval, .deny(reason: ExitPlanMode.rejectionReason)) + store.respond(approval, .deny(reason: rejectionReason)) } label: { Text("Deny").frame(maxWidth: .infinity) } @@ -263,7 +272,7 @@ struct ExitPlanModeApprovalCardView: View { } private func submitRevision() { - guard let reason = ExitPlanMode.revisionReason(feedback: feedback) else { return } + guard let reason = revisionReason(feedback) else { return } store.respond(approval, .deny(reason: reason)) } } diff --git a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift index 5617f14..6a30816 100644 --- a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift +++ b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift @@ -638,10 +638,10 @@ struct SessionDetailView: View { { AskUserQuestionCardView( approval: approval, questions: questions, availableHeight: availableHeight) - } else if approval.toolName == ExitPlanMode.toolName { + } else if PlanReview.isPlanTool(approval.toolName) { ExitPlanModeApprovalCardView( approval: approval, - plan: ExitPlanMode.plan(from: approval.input), + plan: PlanReview.plan(from: approval.input), availableHeight: availableHeight) } else { ApprovalCardView(approval: approval, availableHeight: availableHeight) @@ -860,7 +860,7 @@ struct SessionDetailView: View { if let approval = store.openApprovals.first { switch approval.toolName { case AskUserQuestion.toolName: return "Waiting for answers…" - case ExitPlanMode.toolName: return "Waiting for plan review…" + case ExitPlanMode.toolName, PlanReview.toolName: return "Waiting for plan review…" default: return "Waiting for approval…" } } diff --git a/NucleicRemote/NucleicRemote/Views/Transcript/ToolGroupRow.swift b/NucleicRemote/NucleicRemote/Views/Transcript/ToolGroupRow.swift index 6244354..d87121d 100644 --- a/NucleicRemote/NucleicRemote/Views/Transcript/ToolGroupRow.swift +++ b/NucleicRemote/NucleicRemote/Views/Transcript/ToolGroupRow.swift @@ -582,7 +582,8 @@ 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 ExitPlanMode.toolName, PlanReview.toolName, PlanReview.qualifiedToolName: + return "list.bullet.clipboard" case HostCommandSummary.hostExecToolName: return "desktopcomputer" // The VM/container tools each get their own glyph — a screen for computer-use, a box for // a container — so they don't all collapse into the generic wrench. diff --git a/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift b/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift index 54fa145..f71aeed 100644 --- a/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift +++ b/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift @@ -121,7 +121,7 @@ struct ToolGroup: Equatable { name == "Task" || name == "Agent" || name == Self.orchestraSubagentToolName } var isAskUserQuestion: Bool { name == "AskUserQuestion" } - var isExitPlanMode: Bool { name == ExitPlanMode.toolName } + var isExitPlanMode: Bool { PlanReview.isPlanTool(name) } /// 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 1da85a4..7400e52 100644 --- a/NucleicRemote/NucleicRemote/Views/TranscriptRow.swift +++ b/NucleicRemote/NucleicRemote/Views/TranscriptRow.swift @@ -92,7 +92,7 @@ struct TranscriptRow: View { /// 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 } + guard group.isExitPlanMode, let plan = PlanReview.plan(from: group.input) else { return nil } return ExitPlanModeCard(plan: plan) }