66 lines
2.6 KiB
Swift
66 lines
2.6 KiB
Swift
import NucleicCore
|
|
import SwiftUI
|
|
|
|
/// A dedicated reading surface for the latest plan in the open chat. It follows the live pending
|
|
/// approval first, then falls back to the newest plan tool call in the transcript, so the plan
|
|
/// remains available after Accept / Revise / Deny removes the composer popup.
|
|
struct PlanPanel: View {
|
|
@Environment(AppStore.self) private var store
|
|
let session: Session?
|
|
|
|
private var plan: PlanReview.Plan? {
|
|
guard session?.id == store.openSessionID else { return nil }
|
|
return Self.latestPlan(events: store.openTranscript, approvals: store.openApprovals)
|
|
}
|
|
|
|
var body: some View {
|
|
Group {
|
|
if let plan {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 14) {
|
|
MarkdownText(markdown: plan.markdown, bodySize: 13)
|
|
.textSelection(.enabled)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
if let filePath = plan.filePath {
|
|
Label(filePath, systemImage: "doc.text")
|
|
.font(.caption.monospaced())
|
|
.foregroundStyle(.secondary)
|
|
.textSelection(.enabled)
|
|
}
|
|
}
|
|
.padding(16)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
} else {
|
|
ContentUnavailableView(
|
|
"No Plan Yet",
|
|
systemImage: "list.bullet.clipboard",
|
|
description: Text(
|
|
"When an agent presents a plan for review, it will remain available here."))
|
|
}
|
|
}
|
|
.background(DetailSurface(layer: .inner))
|
|
}
|
|
|
|
/// Pure selection helper kept internal for the pane header and focused tests.
|
|
static func latestPlan(
|
|
events: [AgentEvent], approvals: [ApprovalRequest]
|
|
) -> PlanReview.Plan? {
|
|
if let current = approvals.first(where: { PlanReview.isPlanTool($0.toolName) }),
|
|
let plan = PlanReview.plan(from: current.input) {
|
|
return plan
|
|
}
|
|
for event in events.reversed() {
|
|
let call: ToolCall? = switch event.kind {
|
|
case .toolCallStarted(let call), .toolCallCompleted(let call): call
|
|
default: nil
|
|
}
|
|
if let call, PlanReview.isPlanTool(call.name),
|
|
let plan = PlanReview.plan(from: call.input) {
|
|
return plan
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|