Merge nucleic/jolly-quartz-toad-vbsw into dev
This commit is contained in:
@@ -162,6 +162,110 @@ struct ApprovalCardView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Claude's plan-mode exit is a review, not an ordinary permission gate. It always pauses even
|
||||
/// when Auto is enabled and offers the same three outcomes as Claude's first-party surface:
|
||||
/// accept the plan, deny it and remain in plan mode, or return concrete revision feedback.
|
||||
struct ExitPlanModeApprovalCardView: View {
|
||||
@EnvironmentObject var store: RemoteStore
|
||||
let approval: ApprovalRequest
|
||||
let plan: ExitPlanMode.Plan?
|
||||
var availableHeight: CGFloat = 0
|
||||
|
||||
@State private var revising = false
|
||||
@State private var feedback = ""
|
||||
|
||||
private var canRespond: Bool { store.connectivity.isLive }
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack(spacing: 7) {
|
||||
Image(systemName: "list.bullet.clipboard").foregroundStyle(Palette.accent)
|
||||
Text("Review plan").font(.subheadline.weight(.semibold))
|
||||
}
|
||||
|
||||
if let plan {
|
||||
BoundedCardScroll(maxHeight: middleMaxHeight) {
|
||||
ExitPlanModeCard(plan: plan)
|
||||
}
|
||||
} else {
|
||||
// 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.")
|
||||
.font(.footnote).foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if revising {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("What should Claude change?")
|
||||
.font(.footnote.weight(.semibold))
|
||||
TextField(
|
||||
"Describe the changes you want in the plan…",
|
||||
text: $feedback,
|
||||
axis: .vertical)
|
||||
.lineLimit(2...5)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
}
|
||||
HStack(spacing: 10) {
|
||||
Button("Cancel") {
|
||||
feedback = ""
|
||||
revising = false
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
Button("Send Revision") { submitRevision() }
|
||||
.frame(maxWidth: .infinity)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(!canRespond || ExitPlanMode.revisionReason(feedback: feedback) == nil)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
}
|
||||
} else {
|
||||
HStack(spacing: 8) {
|
||||
Button(role: .destructive) {
|
||||
store.respond(approval, .deny(reason: ExitPlanMode.rejectionReason))
|
||||
} label: {
|
||||
Text("Deny").frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(!canRespond)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
|
||||
Button("Revise") { revising = true }
|
||||
.frame(maxWidth: .infinity)
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
Button {
|
||||
store.respond(approval, .allow())
|
||||
} label: {
|
||||
Text("Accept").frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(!canRespond)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.glassSurface(cornerRadius: 20)
|
||||
.frame(maxHeight: availableHeight > 0 ? availableHeight - 12 : nil, alignment: .bottom)
|
||||
.onChange(of: approval.id) { _, _ in
|
||||
revising = false
|
||||
feedback = ""
|
||||
}
|
||||
}
|
||||
|
||||
private var middleMaxHeight: CGFloat {
|
||||
let base = availableHeight > 0 ? availableHeight - 12 : 640
|
||||
return max(140, base - (revising ? 300 : 210))
|
||||
}
|
||||
|
||||
private func submitRevision() {
|
||||
guard let reason = ExitPlanMode.revisionReason(feedback: feedback) else { return }
|
||||
store.respond(approval, .deny(reason: reason))
|
||||
}
|
||||
}
|
||||
|
||||
/// The full content of an approval, wrapped and selectable. It no longer scrolls on its own — the
|
||||
/// enclosing ``BoundedCardScroll`` owns the scrolling for the whole card — so it simply sizes to
|
||||
/// its (already length-capped) text. The text wraps; it never scrolls horizontally.
|
||||
|
||||
@@ -440,6 +440,11 @@ struct SessionDetailView: View {
|
||||
{
|
||||
AskUserQuestionCardView(
|
||||
approval: approval, questions: questions, availableHeight: availableHeight)
|
||||
} else if approval.toolName == ExitPlanMode.toolName {
|
||||
ExitPlanModeApprovalCardView(
|
||||
approval: approval,
|
||||
plan: ExitPlanMode.plan(from: approval.input),
|
||||
availableHeight: availableHeight)
|
||||
} else {
|
||||
ApprovalCardView(approval: approval, availableHeight: availableHeight)
|
||||
// So a host_exec approval's working-directory line abbreviates the "cd"
|
||||
@@ -648,9 +653,11 @@ struct SessionDetailView: View {
|
||||
// approval card in that state, but a resolved-but-not-yet-cleared gate can flash through
|
||||
// here — say what's actually being waited on rather than claiming the agent is working.
|
||||
if let approval = store.openApprovals.first {
|
||||
return approval.toolName == AskUserQuestion.toolName
|
||||
? "Waiting for answers…"
|
||||
: "Waiting for approval…"
|
||||
switch approval.toolName {
|
||||
case AskUserQuestion.toolName: return "Waiting for answers…"
|
||||
case ExitPlanMode.toolName: return "Waiting for plan review…"
|
||||
default: return "Waiting for approval…"
|
||||
}
|
||||
}
|
||||
for event in store.openEvents.reversed() {
|
||||
// Extended-thinking progress pings (`system/thinking_tokens`) arrive as raw events
|
||||
|
||||
Reference in New Issue
Block a user