spb/zyquo-agent Public MIT
The autonomous agent that actually operates your Mac — plans, runs real commands, verifies its own work.
Swift 94.7%
Shell 4.1%
Python 0.7%
Makefile 0.5%
1//2// PlanPanelView.swift3// Zyquo Agent4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// The right-hand Plan/Todo panel (~300pt, collapsible): the live checklist9// the agent maintains via `update_plan` — items animate through pending/10// active/done/failed, titles are user-editable — plus a progress bar and the11// LoopGuard meters (steps, tokens, elapsed time vs. budgets).12//1314import Combine15import SwiftUI1617struct PlanPanelView: View {18 @ObservedObject var controller: RunController1920 /// Ticks the elapsed-time meter while a run is live.21 @State private var now = Date()22 private let clock = Timer.publish(every: 1, on: .main, in: .common).autoconnect()2324 var body: some View {25 VStack(alignment: .leading, spacing: 0) {26 header27 ZyquoHairline()28 ScrollView {29 VStack(alignment: .leading, spacing: ZyquoSpacing.sm) {30 if let plan = controller.plan, !plan.items.isEmpty {31 progressBar(plan)32 checklist(plan)33 } else {34 emptyPlan35 }36 }37 .padding(ZyquoSpacing.sm)38 }39 ZyquoHairline()40 meters41 }42 .frame(width: ZyquoMetrics.planPanelWidth)43 .background(ZyquoColor.background)44 .onReceive(clock) { tick in45 if controller.isRunning { now = tick }46 }47 }4849 // MARK: - Header5051 private var header: some View {52 HStack(spacing: ZyquoSpacing.xs) {53 Image(systemName: "checklist")54 .font(.system(size: 11))55 .foregroundStyle(ZyquoColor.accent)56 Text("Plan")57 .font(ZyquoFont.bodyEmphasis(size: 12.5))58 .foregroundStyle(ZyquoColor.textPrimary)59 Spacer(minLength: 0)60 if let plan = controller.plan, !plan.items.isEmpty {61 Text("\(plan.doneCount)/\(plan.items.count)")62 .font(ZyquoFont.caption)63 .foregroundStyle(ZyquoColor.textTertiary)64 }65 }66 .padding(.horizontal, ZyquoSpacing.sm)67 .frame(height: ZyquoSpacing.xxl)68 }6970 // MARK: - Checklist7172 private func progressBar(_ plan: TaskPlan) -> some View {73 ProgressView(value: Double(plan.doneCount), total: Double(max(plan.items.count, 1)))74 .progressViewStyle(.linear)75 .tint(ZyquoColor.accent)76 .animation(ZyquoMotion.appear, value: plan.doneCount)77 }7879 private func checklist(_ plan: TaskPlan) -> some View {80 VStack(alignment: .leading, spacing: ZyquoSpacing.xs) {81 ForEach(plan.items) { item in82 PlanItemRow(item: item) { newTitle in83 controller.renamePlanItem(id: item.id, to: newTitle)84 }85 }86 }87 }8889 private var emptyPlan: some View {90 VStack(alignment: .leading, spacing: ZyquoSpacing.xxs) {91 Text("No plan yet")92 .font(ZyquoFont.body(size: 12.5))93 .foregroundStyle(ZyquoColor.textSecondary)94 Text("The agent drafts a checklist here when a run starts, and checks items off as it works.")95 .font(ZyquoFont.caption)96 .foregroundStyle(ZyquoColor.textTertiary)97 .fixedSize(horizontal: false, vertical: true)98 }99 .padding(.top, ZyquoSpacing.xs)100 }101102 // MARK: - Budget meters103104 private var meters: some View {105 let config = controller.loopGuardConfiguration106 let elapsed = controller.runStartedAt.map { min(now.timeIntervalSince($0), config.wallClockBudget) } ?? 0107 return VStack(alignment: .leading, spacing: ZyquoSpacing.xs) {108 meter(109 label: "Steps",110 value: Double(controller.stepsUsed),111 total: Double(config.maxSteps),112 text: "\(controller.stepsUsed)/\(config.maxSteps)"113 )114 meter(115 label: "Tokens",116 value: Double(controller.tokensUsed),117 total: Double(config.tokenBudget),118 text: tokenText119 )120 meter(121 label: "Time",122 value: elapsed,123 total: config.wallClockBudget,124 text: timeText(elapsed: elapsed, budget: config.wallClockBudget)125 )126 }127 .padding(ZyquoSpacing.sm)128 }129130 private func meter(label: String, value: Double, total: Double, text: String) -> some View {131 VStack(alignment: .leading, spacing: 2) {132 HStack {133 Text(label)134 .font(ZyquoFont.caption)135 .foregroundStyle(ZyquoColor.textSecondary)136 Spacer(minLength: 0)137 Text(text)138 .font(ZyquoFont.caption)139 .foregroundStyle(ZyquoColor.textTertiary)140 }141 ProgressView(value: min(value, total), total: max(total, 1))142 .progressViewStyle(.linear)143 .tint(value / max(total, 1) > 0.85 ? ZyquoColor.warning : ZyquoColor.accent)144 .controlSize(.small)145 }146 }147148 private var tokenText: String {149 let used = controller.tokensUsed150 let budget = controller.loopGuardConfiguration.tokenBudget151 func format(_ count: Int) -> String {152 count >= 1_000 ? String(format: "%.0fK", Double(count) / 1_000) : "\(count)"153 }154 return "\(format(used))/\(format(budget))"155 }156157 private func timeText(elapsed: TimeInterval, budget: TimeInterval) -> String {158 "\(Int(elapsed / 60))m/\(Int(budget / 60))m"159 }160}161162// MARK: - Row163164private struct PlanItemRow: View {165 let item: PlanItem166 var onRename: (String) -> Void167168 @State private var editing = false169 @State private var draft = ""170171 var body: some View {172 HStack(alignment: .firstTextBaseline, spacing: ZyquoSpacing.xs) {173 statusIcon174 .animation(ZyquoMotion.appear, value: item.status)175 VStack(alignment: .leading, spacing: 1) {176 if editing {177 TextField("Step", text: $draft, onCommit: {178 editing = false179 onRename(draft)180 })181 .textFieldStyle(.plain)182 .font(ZyquoFont.body(size: 12.5))183 } else {184 Text(item.title)185 .font(ZyquoFont.body(size: 12.5))186 .foregroundStyle(item.status == .done ? ZyquoColor.textTertiary : ZyquoColor.textPrimary)187 .strikethrough(item.status == .done, color: ZyquoColor.textTertiary)188 .fixedSize(horizontal: false, vertical: true)189 .onTapGesture(count: 2) {190 draft = item.title191 editing = true192 }193 }194 if let note = item.note, !note.isEmpty {195 Text(note)196 .font(ZyquoFont.caption)197 .foregroundStyle(item.status == .failed ? ZyquoColor.danger : ZyquoColor.textTertiary)198 .fixedSize(horizontal: false, vertical: true)199 }200 }201 Spacer(minLength: 0)202 }203 .contentShape(Rectangle())204 .help("Double-click to edit")205 }206207 @ViewBuilder208 private var statusIcon: some View {209 switch item.status {210 case .pending:211 Image(systemName: "circle")212 .font(.system(size: 11))213 .foregroundStyle(ZyquoColor.textTertiary)214 case .active:215 Image(systemName: "circle.dotted.circle")216 .font(.system(size: 11))217 .foregroundStyle(ZyquoColor.accent)218 case .done:219 Image(systemName: "checkmark.circle.fill")220 .font(.system(size: 11))221 .foregroundStyle(ZyquoColor.success)222 .transition(.scale.combined(with: .opacity))223 case .failed:224 Image(systemName: "xmark.circle.fill")225 .font(.system(size: 11))226 .foregroundStyle(ZyquoColor.danger)227 .transition(.scale.combined(with: .opacity))228 }229 }230}231