SPB Git

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%
11.9 KB · 324 lines swift
Raw Blame History
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