spb/zyquo-cloud Public MIT
Native macOS AI chat client for 12 cloud providers — your keys, every cloud model, one beautiful chat.
Swift 97.4%
Shell 1.7%
Makefile 1%
1//2// CommandPaletteView.swift3// Zyquo Cloud4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// ⌘K palette: one search box over models, prompt templates, and personas.9// Models switch the current conversation; templates insert into the draft;10// personas apply their system prompt + preferences.11//1213import SwiftUI1415struct CommandPaletteView: View {16 let currentModel: AIModel?17 var onSelectModel: (AIModel) -> Void18 var onInsertTemplate: (PromptTemplate) -> Void19 var onApplyPersona: (Persona) -> Void2021 @EnvironmentObject private var catalog: ModelCatalog22 @StateObject private var library = PromptLibraryStore()23 @Environment(\.dismiss) private var dismiss24 @State private var query = ""25 @FocusState private var focused: Bool2627 var body: some View {28 VStack(spacing: 0) {29 HStack(spacing: ZyquoSpacing.xs) {30 Image(systemName: "command")31 .font(.system(size: 13))32 .foregroundStyle(ZyquoColor.textTertiary)33 TextField("Switch model, insert template, apply persona…", text: $query)34 .textFieldStyle(.plain)35 .font(ZyquoFont.body(size: 15))36 .focused($focused)37 }38 .padding(ZyquoSpacing.md)39 ZyquoHairline()40 ScrollView {41 LazyVStack(alignment: .leading, spacing: 1) {42 if !matchingModels.isEmpty {43 section("Models")44 ForEach(matchingModels.prefix(8)) { model in45 paletteRow(46 symbol: model.provider.symbolName,47 title: model.displayName,48 subtitle: "\(model.provider.displayName) · \(model.contextBadge)"49 ) {50 onSelectModel(model)51 dismiss()52 }53 }54 }55 if !matchingPersonas.isEmpty {56 section("Personas")57 ForEach(matchingPersonas.prefix(6)) { persona in58 paletteRow(59 symbol: persona.symbolName,60 title: persona.name,61 subtitle: String(persona.systemPrompt.prefix(70))62 ) {63 onApplyPersona(persona)64 dismiss()65 }66 }67 }68 if !matchingTemplates.isEmpty {69 section("Prompt Library")70 ForEach(matchingTemplates.prefix(10)) { template in71 paletteRow(72 symbol: "text.badge.plus",73 title: template.title,74 subtitle: template.category75 ) {76 onInsertTemplate(template)77 dismiss()78 }79 }80 }81 }82 .padding(ZyquoSpacing.xxs)83 }84 .frame(height: 380)85 }86 .frame(width: 560)87 .background(ZyquoColor.surface)88 .onAppear { focused = true }89 .onExitCommand { dismiss() }90 }9192 private var matchingModels: [AIModel] {93 query.isEmpty94 ? catalog.all.filter(\.isRecommended)95 : catalog.all.filter {96 $0.displayName.localizedCaseInsensitiveContains(query)97 || $0.id.localizedCaseInsensitiveContains(query)98 }99 }100101 private var matchingTemplates: [PromptTemplate] {102 query.isEmpty103 ? Array(library.allTemplates.prefix(10))104 : library.allTemplates.filter {105 $0.title.localizedCaseInsensitiveContains(query)106 || $0.category.localizedCaseInsensitiveContains(query)107 }108 }109110 private var matchingPersonas: [Persona] {111 query.isEmpty112 ? library.allPersonas113 : library.allPersonas.filter { $0.name.localizedCaseInsensitiveContains(query) }114 }115116 private func section(_ title: String) -> some View {117 Text(title)118 .font(ZyquoFont.caption)119 .foregroundStyle(ZyquoColor.textTertiary)120 .padding(.horizontal, ZyquoSpacing.sm)121 .padding(.top, ZyquoSpacing.xs)122 .padding(.bottom, 2)123 }124125 private func paletteRow(126 symbol: String, title: String, subtitle: String, action: @escaping () -> Void127 ) -> some View {128 Button(action: action) {129 HStack(spacing: ZyquoSpacing.sm) {130 Image(systemName: symbol)131 .font(.system(size: 13))132 .foregroundStyle(ZyquoColor.accent)133 .frame(width: 18)134 VStack(alignment: .leading, spacing: 0) {135 Text(title)136 .font(ZyquoFont.body(size: 13))137 .foregroundStyle(ZyquoColor.textPrimary)138 Text(subtitle)139 .font(ZyquoFont.caption)140 .foregroundStyle(ZyquoColor.textTertiary)141 .lineLimit(1)142 }143 Spacer()144 }145 .padding(.horizontal, ZyquoSpacing.sm)146 .padding(.vertical, 5)147 .contentShape(Rectangle())148 }149 .buttonStyle(.plain)150 .zyquoHoverHighlight()151 }152}153