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// TemplateBrowserView.swift3// Zyquo Agent4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// The template browser sheet (empty state "Browse templates", ⌘K, menu):9// built-in library + user templates grouped by category, searchable, with10// user-template CRUD. Using a template with {{variables}} opens the fill-in11// sheet; the rendered prompt lands in a new task's input bar (pendingDraft).12//1314import SwiftUI1516struct TemplateBrowserView: View {17 /// Called with the rendered prompt and the template's suggested mode.18 var onUse: (String, SafetyMode) -> Void1920 @EnvironmentObject private var templates: TemplateStore21 @Environment(\.dismiss) private var dismiss22 @State private var query = ""23 @State private var fillingTemplate: TaskTemplate?24 @State private var editingTemplate: TaskTemplate?25 @State private var creatingTemplate = false2627 var body: some View {28 VStack(spacing: 0) {29 header30 ZyquoHairline()31 ScrollView {32 LazyVStack(alignment: .leading, spacing: 1) {33 ForEach(TemplateCategory.allCases) { category in34 let matching = filtered(in: category)35 if !matching.isEmpty {36 sectionHeader(category)37 ForEach(matching) { template in38 templateRow(template)39 }40 }41 }42 }43 .padding(ZyquoSpacing.xs)44 }45 ZyquoHairline()46 footer47 }48 .frame(width: 560, height: 480)49 .background(ZyquoColor.surface)50 .sheet(item: $fillingTemplate) { template in51 TemplateFillSheet(template: template) { prompt in52 use(prompt: prompt, template: template)53 }54 }55 .sheet(item: $editingTemplate) { template in56 TemplateEditorSheet(template: template, isNew: false)57 }58 .sheet(isPresented: $creatingTemplate) {59 TemplateEditorSheet(60 template: TaskTemplate(title: "", category: .filesAndFolders, prompt: ""),61 isNew: true62 )63 }64 }6566 private var header: some View {67 HStack(spacing: ZyquoSpacing.xs) {68 Image(systemName: "square.grid.2x2")69 .font(.system(size: 14))70 .foregroundStyle(ZyquoColor.accent)71 Text("Task Templates")72 .font(ZyquoFont.bodyEmphasis(size: 14))73 .foregroundStyle(ZyquoColor.textPrimary)74 Spacer()75 HStack(spacing: ZyquoSpacing.xxs) {76 Image(systemName: "magnifyingglass")77 .font(.system(size: 11))78 .foregroundStyle(ZyquoColor.textTertiary)79 TextField("Search templates", text: $query)80 .textFieldStyle(.plain)81 .font(ZyquoFont.body(size: 12.5))82 .frame(width: 160)83 }84 .padding(.horizontal, ZyquoSpacing.xs)85 .padding(.vertical, 4)86 .background(87 RoundedRectangle(cornerRadius: ZyquoRadius.small, style: .continuous)88 .fill(ZyquoColor.surfaceSecondary)89 )90 }91 .padding(ZyquoSpacing.sm)92 }9394 private var footer: some View {95 HStack {96 Button {97 creatingTemplate = true98 } label: {99 Label("New Template", systemImage: "plus")100 }101 .controlSize(.small)102 Spacer()103 Button("Close") { dismiss() }104 .controlSize(.small)105 .keyboardShortcut(.cancelAction)106 }107 .padding(ZyquoSpacing.sm)108 }109110 private func sectionHeader(_ category: TemplateCategory) -> some View {111 HStack(spacing: ZyquoSpacing.xxs) {112 Image(systemName: category.symbolName)113 .font(.system(size: 10))114 Text(category.displayName)115 .font(ZyquoFont.caption)116 }117 .foregroundStyle(ZyquoColor.textTertiary)118 .padding(.horizontal, ZyquoSpacing.xs)119 .padding(.top, ZyquoSpacing.sm)120 .padding(.bottom, 2)121 }122123 private func templateRow(_ template: TaskTemplate) -> some View {124 Button {125 if template.variables.isEmpty {126 use(prompt: template.prompt, template: template)127 } else {128 fillingTemplate = template129 }130 } label: {131 HStack(spacing: ZyquoSpacing.xs) {132 Image(systemName: template.displaySymbol)133 .font(.system(size: 12))134 .foregroundStyle(ZyquoColor.accent)135 .frame(width: 18)136 VStack(alignment: .leading, spacing: 1) {137 HStack(spacing: ZyquoSpacing.xxs) {138 Text(template.title)139 .font(ZyquoFont.body(size: 13))140 .foregroundStyle(ZyquoColor.textPrimary)141 if !template.isBuiltIn {142 ZyquoBadge(text: "yours", color: ZyquoColor.success)143 }144 if !template.variables.isEmpty {145 ZyquoBadge(text: "\(template.variables.count) variable\(template.variables.count == 1 ? "" : "s")")146 }147 }148 Text(template.prompt)149 .font(ZyquoFont.caption)150 .foregroundStyle(ZyquoColor.textTertiary)151 .lineLimit(1)152 }153 Spacer(minLength: 0)154 ZyquoBadge(text: template.suggestedSafetyMode.displayName, color: ZyquoColor.textSecondary)155 if !template.isBuiltIn {156 Button {157 editingTemplate = template158 } label: {159 Image(systemName: "pencil")160 .font(.system(size: 10))161 .foregroundStyle(ZyquoColor.textSecondary)162 }163 .buttonStyle(.plain)164 .help("Edit template")165 }166 }167 .padding(.horizontal, ZyquoSpacing.xs)168 .padding(.vertical, 5)169 .contentShape(Rectangle())170 }171 .buttonStyle(.plain)172 .zyquoHoverHighlight()173 }174175 private func filtered(in category: TemplateCategory) -> [TaskTemplate] {176 let all = templates.templates(in: category)177 let trimmed = query.trimmingCharacters(in: .whitespaces)178 guard !trimmed.isEmpty else { return all }179 return all.filter {180 $0.title.localizedCaseInsensitiveContains(trimmed)181 || $0.prompt.localizedCaseInsensitiveContains(trimmed)182 }183 }184185 private func use(prompt: String, template: TaskTemplate) {186 dismiss()187 onUse(prompt, template.suggestedSafetyMode)188 }189}190191// MARK: - Variable fill-in192193struct TemplateFillSheet: View {194 let template: TaskTemplate195 var onSubmit: (String) -> Void196197 @Environment(\.dismiss) private var dismiss198 @State private var values: [String: String] = [:]199200 var body: some View {201 VStack(alignment: .leading, spacing: ZyquoSpacing.sm) {202 HStack(spacing: ZyquoSpacing.xs) {203 Image(systemName: template.displaySymbol)204 .font(.system(size: 14))205 .foregroundStyle(ZyquoColor.accent)206 Text(template.title)207 .font(ZyquoFont.title)208 .foregroundStyle(ZyquoColor.textPrimary)209 }210 Form {211 ForEach(template.variables, id: \.self) { name in212 TextField(name.capitalized, text: Binding(213 get: { values[name] ?? "" },214 set: { values[name] = $0 }215 ))216 }217 }218 Text(preview)219 .font(ZyquoFont.body(size: 12))220 .foregroundStyle(ZyquoColor.textSecondary)221 .lineLimit(5)222 .padding(ZyquoSpacing.xs)223 .frame(maxWidth: .infinity, alignment: .leading)224 .background(225 RoundedRectangle(cornerRadius: ZyquoRadius.small, style: .continuous)226 .fill(ZyquoColor.surfaceSecondary)227 )228 HStack {229 Spacer()230 Button("Cancel") { dismiss() }231 Button("Use Template") {232 dismiss()233 onSubmit(template.renderedPrompt(values: values))234 }235 .buttonStyle(.borderedProminent)236 .keyboardShortcut(.defaultAction)237 .disabled(!allFilled)238 }239 }240 .padding(ZyquoSpacing.xl)241 .frame(width: 440)242 .background(ZyquoColor.surface)243 }244245 private var allFilled: Bool {246 template.variables.allSatisfy {247 !(values[$0] ?? "").trimmingCharacters(in: .whitespaces).isEmpty248 }249 }250251 private var preview: String {252 template.renderedPrompt(values: values)253 }254}255256// MARK: - User-template editor257258struct TemplateEditorSheet: View {259 @State var template: TaskTemplate260 let isNew: Bool261262 @EnvironmentObject private var templates: TemplateStore263 @Environment(\.dismiss) private var dismiss264265 var body: some View {266 VStack(alignment: .leading, spacing: ZyquoSpacing.sm) {267 Text(isNew ? "New Template" : "Edit Template")268 .font(ZyquoFont.title)269 .foregroundStyle(ZyquoColor.textPrimary)270 Form {271 TextField("Title", text: $template.title)272 Picker("Category", selection: $template.category) {273 ForEach(TemplateCategory.allCases) { category in274 Text(category.displayName).tag(category)275 }276 }277 Picker("Suggested safety mode", selection: $template.suggestedSafetyMode) {278 ForEach(SafetyMode.allCases) { mode in279 Text(mode.displayName).tag(mode)280 }281 }282 VStack(alignment: .leading, spacing: ZyquoSpacing.xxs) {283 Text("Prompt — use {{variable}} for fill-in placeholders")284 .font(ZyquoFont.caption)285 .foregroundStyle(ZyquoColor.textSecondary)286 TextEditor(text: $template.prompt)287 .font(ZyquoFont.body(size: 12.5))288 .frame(height: 120)289 .padding(ZyquoSpacing.xxs)290 .background(291 RoundedRectangle(cornerRadius: ZyquoRadius.small, style: .continuous)292 .strokeBorder(ZyquoColor.border, lineWidth: ZyquoMetrics.hairline)293 )294 }295 }296 HStack {297 if !isNew {298 Button("Delete", role: .destructive) {299 templates.delete(template.id)300 dismiss()301 }302 }303 Spacer()304 Button("Cancel") { dismiss() }305 Button("Save") {306 if isNew {307 templates.add(template)308 } else {309 templates.update(template)310 }311 dismiss()312 }313 .buttonStyle(.borderedProminent)314 .keyboardShortcut(.defaultAction)315 .disabled(template.title.trimmingCharacters(in: .whitespaces).isEmpty316 || template.prompt.trimmingCharacters(in: .whitespaces).isEmpty)317 }318 }319 .padding(ZyquoSpacing.xl)320 .frame(width: 480)321 .background(ZyquoColor.surface)322 }323}324