Merge nucleic/olive-coral-civet-pvqv into dev

This commit is contained in:
2026-08-05 19:59:22 -07:00
parent 9a15ed85b4
commit 9c6a5a649c
5 changed files with 22 additions and 12 deletions
@@ -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))
}
}
@@ -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…"
}
}
@@ -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.
@@ -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,
@@ -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)
}