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
|
/// 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
|
/// 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.
|
/// its (already length-capped) text. The text wraps; it never scrolls horizontally.
|
||||||
|
|||||||
@@ -440,6 +440,11 @@ struct SessionDetailView: View {
|
|||||||
{
|
{
|
||||||
AskUserQuestionCardView(
|
AskUserQuestionCardView(
|
||||||
approval: approval, questions: questions, availableHeight: availableHeight)
|
approval: approval, questions: questions, availableHeight: availableHeight)
|
||||||
|
} else if approval.toolName == ExitPlanMode.toolName {
|
||||||
|
ExitPlanModeApprovalCardView(
|
||||||
|
approval: approval,
|
||||||
|
plan: ExitPlanMode.plan(from: approval.input),
|
||||||
|
availableHeight: availableHeight)
|
||||||
} else {
|
} else {
|
||||||
ApprovalCardView(approval: approval, availableHeight: availableHeight)
|
ApprovalCardView(approval: approval, availableHeight: availableHeight)
|
||||||
// So a host_exec approval's working-directory line abbreviates the "cd"
|
// 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
|
// 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.
|
// here — say what's actually being waited on rather than claiming the agent is working.
|
||||||
if let approval = store.openApprovals.first {
|
if let approval = store.openApprovals.first {
|
||||||
return approval.toolName == AskUserQuestion.toolName
|
switch approval.toolName {
|
||||||
? "Waiting for answers…"
|
case AskUserQuestion.toolName: return "Waiting for answers…"
|
||||||
: "Waiting for approval…"
|
case ExitPlanMode.toolName: return "Waiting for plan review…"
|
||||||
|
default: return "Waiting for approval…"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for event in store.openEvents.reversed() {
|
for event in store.openEvents.reversed() {
|
||||||
// Extended-thinking progress pings (`system/thinking_tokens`) arrive as raw events
|
// Extended-thinking progress pings (`system/thinking_tokens`) arrive as raw events
|
||||||
|
|||||||
Reference in New Issue
Block a user