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// CommandPaletteView.swift3// Zyquo Agent4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// The ⌘K command palette: one fuzzy-filtered list over app actions9// (New Task, Open Settings, toggle panels, audit log, template browser),10// the task library, and every task template. Enter runs the top hit,11// Esc dismisses. Rendered as an overlay card on the main window.12//1314import SwiftUI1516/// One palette entry.17struct PaletteItem: Identifiable {18 enum Kind {19 case action20 case template21 case task22 }2324 let id: String25 var kind: Kind26 var title: String27 var subtitle: String?28 var symbolName: String29 var perform: () -> Void30}3132struct CommandPaletteView: View {33 var items: [PaletteItem]34 var onDismiss: () -> Void3536 @State private var query = ""37 @FocusState private var focused: Bool3839 private var filtered: [PaletteItem] {40 let trimmed = query.trimmingCharacters(in: .whitespaces)41 guard !trimmed.isEmpty else { return items }42 return items43 .compactMap { item -> (PaletteItem, Int)? in44 guard let score = Self.fuzzyScore(needle: trimmed, haystack: item.title) else {45 if let subtitle = item.subtitle,46 let subScore = Self.fuzzyScore(needle: trimmed, haystack: subtitle) {47 return (item, subScore + 100)48 }49 return nil50 }51 return (item, score)52 }53 .sorted { $0.1 < $1.1 }54 .map(\.0)55 }5657 var body: some View {58 VStack(spacing: 0) {59 HStack(spacing: ZyquoSpacing.xs) {60 Image(systemName: "command")61 .font(.system(size: 13))62 .foregroundStyle(ZyquoColor.accent)63 TextField("Type a command, template, or task…", text: $query)64 .textFieldStyle(.plain)65 .font(ZyquoFont.body(size: 15))66 .focused($focused)67 .onSubmit(runFirst)68 Text("esc")69 .font(ZyquoFont.code(size: 10))70 .foregroundStyle(ZyquoColor.textTertiary)71 .padding(.horizontal, ZyquoSpacing.xxs)72 .padding(.vertical, 1)73 .background(74 RoundedRectangle(cornerRadius: 3, style: .continuous)75 .fill(ZyquoColor.surfaceSecondary)76 )77 }78 .padding(ZyquoSpacing.sm)79 ZyquoHairline()80 ScrollView {81 LazyVStack(spacing: 1) {82 if filtered.isEmpty {83 Text("No matches")84 .font(ZyquoFont.body(size: 12.5))85 .foregroundStyle(ZyquoColor.textTertiary)86 .padding(ZyquoSpacing.sm)87 }88 ForEach(Array(filtered.prefix(40).enumerated()), id: \.element.id) { index, item in89 row(item, isFirst: index == 0)90 }91 }92 .padding(ZyquoSpacing.xxs)93 }94 .frame(maxHeight: 320)95 }96 .frame(width: 560)97 .background(98 RoundedRectangle(cornerRadius: ZyquoRadius.large, style: .continuous)99 .fill(ZyquoColor.surface)100 )101 .overlay(102 RoundedRectangle(cornerRadius: ZyquoRadius.large, style: .continuous)103 .strokeBorder(ZyquoColor.border, lineWidth: ZyquoMetrics.hairline)104 )105 .zyquoSoftShadow()106 .onAppear { focused = true }107 .onExitCommand { onDismiss() }108 }109110 private func row(_ item: PaletteItem, isFirst: Bool) -> some View {111 Button {112 onDismiss()113 item.perform()114 } label: {115 HStack(spacing: ZyquoSpacing.xs) {116 Image(systemName: item.symbolName)117 .font(.system(size: 12))118 .foregroundStyle(ZyquoColor.accent)119 .frame(width: 18)120 Text(item.title)121 .font(ZyquoFont.body(size: 13))122 .foregroundStyle(ZyquoColor.textPrimary)123 .lineLimit(1)124 if let subtitle = item.subtitle {125 Text(subtitle)126 .font(ZyquoFont.caption)127 .foregroundStyle(ZyquoColor.textTertiary)128 .lineLimit(1)129 }130 Spacer(minLength: 0)131 ZyquoBadge(text: kindLabel(item.kind))132 if isFirst && !query.isEmpty {133 Text("↩")134 .font(ZyquoFont.code(size: 10))135 .foregroundStyle(ZyquoColor.textTertiary)136 }137 }138 .padding(.horizontal, ZyquoSpacing.xs)139 .padding(.vertical, 5)140 .background(141 RoundedRectangle(cornerRadius: ZyquoRadius.small, style: .continuous)142 .fill(isFirst && !query.isEmpty ? ZyquoColor.accentSubtle : .clear)143 )144 .contentShape(Rectangle())145 }146 .buttonStyle(.plain)147 .zyquoHoverHighlight()148 }149150 private func kindLabel(_ kind: PaletteItem.Kind) -> String {151 switch kind {152 case .action: return "action"153 case .template: return "template"154 case .task: return "task"155 }156 }157158 private func runFirst() {159 guard let first = filtered.first else { return }160 onDismiss()161 first.perform()162 }163164 /// Case-insensitive subsequence match; lower score = tighter match165 /// (prefix matches beat scattered ones). Nil = no match.166 static func fuzzyScore(needle: String, haystack: String) -> Int? {167 let needleChars = Array(needle.lowercased())168 let haystackChars = Array(haystack.lowercased())169 guard !needleChars.isEmpty else { return 0 }170 var score = 0171 var haystackIndex = 0172 for character in needleChars {173 var found = false174 while haystackIndex < haystackChars.count {175 if haystackChars[haystackIndex] == character {176 found = true177 haystackIndex += 1178 break179 }180 score += 1181 haystackIndex += 1182 }183 if !found { return nil }184 }185 return score186 }187}188