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// EmptyStateView.swift3// Zyquo Agent4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// The "What should I do on your Mac?" hero: brand glyph, four example task9// cards (clicking fills the input), the safety-mode selector, and the model10// chip — powerful and trustworthy, never blank.11//1213import SwiftUI1415struct AgentEmptyStateView: View {16 let model: AIModel?17 let safetyMode: SafetyMode18 /// Name of the task's active persona (nil = none / no task yet).19 var personaName: String?20 var onSelectModel: (AIModel) -> Void21 var onSelectSafetyMode: (SafetyMode) -> Void22 /// Called with the chosen persona (nil = none).23 var onSelectPersona: ((Persona?) -> Void)?24 var onBrowseTemplates: (() -> Void)?25 var onSuggestion: (String) -> Void2627 @EnvironmentObject private var personas: PersonaStore2829 private static let suggestions: [(symbol: String, title: String, prompt: String)] = [30 ("folder.badge.gearshape", "Organize my Downloads folder",31 "Look at my Downloads folder, group the files by type into subfolders (Images, Documents, Archives, Installers…), and show me a summary of what you moved."),32 ("textformat.abc.dottedunderline", "Batch-rename these files",33 "Rename all the files in the workspace to a consistent kebab-case pattern with a numeric suffix, and list the before → after mapping."),34 ("chevron.left.forwardslash.chevron.right", "Set up a Python project and run the tests",35 "Create a small Python project in the workspace with a src/ layout, one example module with two functions, pytest tests for them, then run the tests and report the results."),36 ("note.text", "Export my Notes to Markdown",37 "Use AppleScript to read my Apple Notes and export each note in the default folder as a Markdown file in the workspace, named after its title."),38 ]3940 var body: some View {41 VStack(spacing: ZyquoSpacing.lg) {42 Spacer()43 AgentZGlyph(size: 88)44 VStack(spacing: ZyquoSpacing.xxs) {45 Text("What should I do on your Mac?")46 .font(ZyquoFont.title)47 .foregroundStyle(ZyquoColor.textPrimary)48 Text("Zyquo Agent plans, runs commands, and works until it's done — with your approval on anything risky.")49 .font(ZyquoFont.body())50 .foregroundStyle(ZyquoColor.textSecondary)51 .multilineTextAlignment(.center)52 }53 HStack(spacing: ZyquoSpacing.sm) {54 ModelChipView(model: model, onSelect: onSelectModel)55 SafetyModePicker(mode: safetyMode, onSelect: onSelectSafetyMode)56 if let onSelectPersona, !personas.personas.isEmpty {57 personaMenu(onSelectPersona)58 }59 }60 if let onBrowseTemplates {61 Button {62 onBrowseTemplates()63 } label: {64 HStack(spacing: ZyquoSpacing.xxs) {65 Image(systemName: "square.grid.2x2")66 .font(.system(size: 11))67 Text("Browse templates")68 .font(ZyquoFont.body(size: 12.5))69 }70 .foregroundStyle(ZyquoColor.accent)71 }72 .buttonStyle(.plain)73 .help("25+ ready-made agent tasks (⌘K)")74 }75 LazyVGrid(76 columns: [GridItem(.flexible()), GridItem(.flexible())],77 spacing: ZyquoSpacing.sm78 ) {79 ForEach(Self.suggestions, id: \.title) { suggestion in80 Button {81 onSuggestion(suggestion.prompt)82 } label: {83 HStack(spacing: ZyquoSpacing.xs) {84 Image(systemName: suggestion.symbol)85 .font(.system(size: 14))86 .foregroundStyle(ZyquoColor.accent)87 .frame(width: 20)88 Text(suggestion.title)89 .font(ZyquoFont.body(size: 13))90 .foregroundStyle(ZyquoColor.textPrimary)91 .multilineTextAlignment(.leading)92 Spacer(minLength: 0)93 }94 .padding(ZyquoSpacing.sm)95 .background(96 RoundedRectangle(cornerRadius: ZyquoRadius.medium, style: .continuous)97 .fill(ZyquoColor.surface)98 .overlay(99 RoundedRectangle(cornerRadius: ZyquoRadius.medium, style: .continuous)100 .strokeBorder(ZyquoColor.border, lineWidth: ZyquoMetrics.hairline)101 )102 )103 .contentShape(Rectangle())104 }105 .buttonStyle(PressableButtonStyle())106 }107 }108 .frame(maxWidth: ZyquoMetrics.emptyStateGridWidth)109 Spacer()110 Spacer()111 }112 .frame(maxWidth: .infinity, maxHeight: .infinity)113 .background(ZyquoColor.background)114 }115116 /// Persona chip + menu (None + every stored persona).117 private func personaMenu(_ onSelect: @escaping (Persona?) -> Void) -> some View {118 Menu {119 Button("None") { onSelect(nil) }120 Divider()121 ForEach(personas.personas) { persona in122 Button {123 onSelect(persona)124 } label: {125 Label(persona.name, systemImage: persona.symbolName)126 }127 }128 } label: {129 HStack(spacing: ZyquoSpacing.xxs) {130 Image(systemName: "person.crop.circle")131 .font(.system(size: 11, weight: .medium))132 .foregroundStyle(ZyquoColor.accent)133 Text(personaName ?? "Persona")134 .font(ZyquoFont.bodyEmphasis(size: 12.5))135 .foregroundStyle(personaName == nil ? ZyquoColor.textSecondary : ZyquoColor.textPrimary)136 .lineLimit(1)137 }138 .padding(.horizontal, ZyquoSpacing.xs)139 .padding(.vertical, 4)140 .background(141 RoundedRectangle(cornerRadius: ZyquoRadius.small, style: .continuous)142 .fill(ZyquoColor.surfaceSecondary)143 )144 }145 .menuStyle(.borderlessButton)146 .fixedSize()147 .help("Persona — adds a system-prompt section and can pin a model and safety default")148 }149}150151/// The Manual / Guarded / Autonomous segmented control (header + empty state).152struct SafetyModePicker: View {153 let mode: SafetyMode154 var onSelect: (SafetyMode) -> Void155156 var body: some View {157 Picker("Safety mode", selection: Binding(158 get: { mode },159 set: { onSelect($0) }160 )) {161 ForEach(SafetyMode.allCases) { candidate in162 Text(candidate.displayName).tag(candidate)163 }164 }165 .pickerStyle(.segmented)166 .labelsHidden()167 .controlSize(.small)168 .fixedSize()169 .help(helpText)170 }171172 private var helpText: String {173 switch mode {174 case .manual: return "Manual — approve every action"175 case .guarded: return "Guarded — safe actions run automatically, anything mutating asks"176 case .autonomous: return "Autonomous — runs freely within budget; destructive actions still ask"177 }178 }179}180