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// AgentSettingsTab.swift3// Zyquo Agent4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Settings › Agent — the engine tunables persisted to AgentSettings.json9// and applied to every new run (max steps, token/time budgets, per-command10// timeout, parallel tool calls, compaction threshold), plus the Personas11// CRUD (system-prompt addition + preferred model + safety default).12//1314import SwiftUI1516struct AgentSettingsTab: View {17 @EnvironmentObject private var settings: AgentSettingsStore18 @EnvironmentObject private var personas: PersonaStore19 @EnvironmentObject private var catalog: ModelCatalog20 @State private var editingPersona: Persona?21 @State private var creatingPersona = false2223 var body: some View {24 Form {25 Section("Run budgets (LoopGuard)") {26 Stepper(value: $settings.settings.maxSteps, in: 5...200, step: 5) {27 LabeledContent("Max steps per run", value: "\(settings.settings.maxSteps)")28 }29 Stepper(value: $settings.settings.tokenBudget, in: 50_000...5_000_000, step: 50_000) {30 LabeledContent("Token budget per run", value: "\(settings.settings.tokenBudget / 1_000)K")31 }32 Stepper(value: $settings.settings.timeBudgetMinutes, in: 5...240, step: 5) {33 LabeledContent("Time budget per run", value: "\(settings.settings.timeBudgetMinutes) min")34 }35 Text("When a budget trips, the run pauses and asks — it never aborts silently.")36 .font(ZyquoFont.caption)37 .foregroundStyle(ZyquoColor.textTertiary)38 }3940 Section("Execution") {41 Stepper(value: $settings.settings.perCommandTimeoutSeconds, in: 10...3600, step: 10) {42 LabeledContent("Per-command timeout", value: "\(settings.settings.perCommandTimeoutSeconds) s")43 }44 Toggle("Parallel tool calls", isOn: $settings.settings.parallelToolCalls)45 Text("When on, a turn's tool calls run concurrently — but only when every call is read-only under the active policy. Mutating calls always run sequentially.")46 .font(ZyquoFont.caption)47 .foregroundStyle(ZyquoColor.textTertiary)48 }4950 Section("Memory") {51 VStack(alignment: .leading, spacing: ZyquoSpacing.xxs) {52 LabeledContent("Compaction threshold") {53 Text("\(Int(settings.settings.compactionThreshold * 100))% of context window")54 .font(ZyquoFont.caption)55 .foregroundStyle(ZyquoColor.textSecondary)56 .monospacedDigit()57 }58 Slider(value: $settings.settings.compactionThreshold, in: 0.5...0.95, step: 0.05)59 }60 Text("Older completed steps are summarized when estimated context usage crosses this fraction; the plan, MEMORY.md, and recent steps always survive verbatim.")61 .font(ZyquoFont.caption)62 .foregroundStyle(ZyquoColor.textTertiary)63 }6465 Section("Personas") {66 if personas.personas.isEmpty {67 Text("Personas add a system-prompt section to every run of a task, and can pin a preferred model and safety default.")68 .font(ZyquoFont.body(size: 12))69 .foregroundStyle(ZyquoColor.textTertiary)70 }71 ForEach(personas.personas) { persona in72 personaRow(persona)73 }74 Button {75 creatingPersona = true76 } label: {77 Label("New Persona", systemImage: "plus")78 }79 .controlSize(.small)80 }81 }82 .formStyle(.grouped)83 .sheet(item: $editingPersona) { persona in84 PersonaEditorSheet(persona: persona) { updated in85 personas.update(updated)86 }87 }88 .sheet(isPresented: $creatingPersona) {89 PersonaEditorSheet(persona: Persona(name: "", systemPrompt: "")) { created in90 personas.add(created)91 }92 }93 }9495 private func personaRow(_ persona: Persona) -> some View {96 HStack(spacing: ZyquoSpacing.xs) {97 Image(systemName: persona.symbolName)98 .font(.system(size: 12))99 .foregroundStyle(ZyquoColor.accent)100 .frame(width: 18)101 VStack(alignment: .leading, spacing: 1) {102 Text(persona.name)103 .font(ZyquoFont.bodyEmphasis(size: 12.5))104 .foregroundStyle(ZyquoColor.textPrimary)105 Text(persona.systemPrompt)106 .font(ZyquoFont.caption)107 .foregroundStyle(ZyquoColor.textTertiary)108 .lineLimit(1)109 }110 Spacer()111 if let modelID = persona.modelID {112 ZyquoBadge(text: modelID.split(separator: "/").last.map(String.init) ?? modelID)113 }114 if let mode = persona.safetyMode {115 ZyquoBadge(text: mode.displayName, color: ZyquoColor.textSecondary)116 }117 Button {118 editingPersona = persona119 } label: {120 Image(systemName: "pencil")121 .font(.system(size: 11))122 .foregroundStyle(ZyquoColor.textSecondary)123 }124 .buttonStyle(.plain)125 .help("Edit persona")126 Button {127 personas.delete(persona.id)128 } label: {129 Image(systemName: "trash")130 .font(.system(size: 11))131 .foregroundStyle(ZyquoColor.danger)132 }133 .buttonStyle(.plain)134 .help("Delete persona")135 }136 }137}138139// MARK: - Persona editor140141struct PersonaEditorSheet: View {142 @State var persona: Persona143 var onSave: (Persona) -> Void144145 @EnvironmentObject private var catalog: ModelCatalog146 @Environment(\.dismiss) private var dismiss147148 /// Sentinel tag for "no preferred model" in the picker.149 private static let noModelTag = ""150151 var body: some View {152 VStack(alignment: .leading, spacing: ZyquoSpacing.sm) {153 Text(persona.name.isEmpty ? "New Persona" : "Edit Persona")154 .font(ZyquoFont.title)155 .foregroundStyle(ZyquoColor.textPrimary)156 Form {157 TextField("Name", text: $persona.name)158 VStack(alignment: .leading, spacing: ZyquoSpacing.xxs) {159 Text("System-prompt addition")160 .font(ZyquoFont.caption)161 .foregroundStyle(ZyquoColor.textSecondary)162 TextEditor(text: $persona.systemPrompt)163 .font(ZyquoFont.body(size: 12.5))164 .frame(height: 110)165 .padding(ZyquoSpacing.xxs)166 .background(167 RoundedRectangle(cornerRadius: ZyquoRadius.small, style: .continuous)168 .strokeBorder(ZyquoColor.border, lineWidth: ZyquoMetrics.hairline)169 )170 }171 Picker("Preferred agent model", selection: modelSelection) {172 Text("None (use default)").tag(Self.noModelTag)173 ForEach(catalog.agentCapableModels) { model in174 Text("\(model.displayName) — \(model.provider.displayName)")175 .tag("\(model.provider.rawValue)|\(model.id)")176 }177 }178 Picker("Default safety mode", selection: safetySelection) {179 Text("App default").tag(Self.noModelTag)180 ForEach(SafetyMode.allCases) { mode in181 Text(mode.displayName).tag(mode.rawValue)182 }183 }184 }185 HStack {186 Spacer()187 Button("Cancel") { dismiss() }188 Button("Save") {189 onSave(persona)190 dismiss()191 }192 .buttonStyle(.borderedProminent)193 .keyboardShortcut(.defaultAction)194 .disabled(persona.name.trimmingCharacters(in: .whitespaces).isEmpty)195 }196 }197 .padding(ZyquoSpacing.xl)198 .frame(width: 460)199 .background(ZyquoColor.surface)200 }201202 private var modelSelection: Binding<String> {203 Binding(204 get: {205 guard let provider = persona.provider, let id = persona.modelID else { return Self.noModelTag }206 return "\(provider.rawValue)|\(id)"207 },208 set: { key in209 let parts = key.split(separator: "|", maxSplits: 1)210 if parts.count == 2, let provider = ProviderID(rawValue: String(parts[0])) {211 persona.provider = provider212 persona.modelID = String(parts[1])213 } else {214 persona.provider = nil215 persona.modelID = nil216 }217 }218 )219 }220221 private var safetySelection: Binding<String> {222 Binding(223 get: { persona.safetyMode?.rawValue ?? Self.noModelTag },224 set: { persona.safetyMode = SafetyMode(rawValue: $0) }225 )226 }227}228