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// ModelChipView.swift3// Zyquo Agent4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// The header model chip and its picker popover — the same catalog as Zyquo9// Cloud, with the Agent twist: agent-capable models are emphasized (and the10// recommended tier surfaced first, default agent model starred), while11// models without reliable multi-step tool use are dimmed and labeled12// "limited tool use".13//1415import SwiftUI1617struct ModelChipView: View {18 let model: AIModel?19 var onSelect: (AIModel) -> Void2021 @State private var showingPicker = false2223 var body: some View {24 Button {25 showingPicker.toggle()26 } label: {27 HStack(spacing: ZyquoSpacing.xxs) {28 Image(systemName: model?.provider.symbolName ?? "questionmark.circle")29 .font(.system(size: 11, weight: .medium))30 .foregroundStyle(ZyquoColor.accent)31 Text(model?.displayName ?? "Choose Model")32 .font(ZyquoFont.bodyEmphasis(size: 12.5))33 .foregroundStyle(ZyquoColor.textPrimary)34 .lineLimit(1)35 if let model, !model.agentCapable {36 Image(systemName: "exclamationmark.triangle")37 .font(.system(size: 9))38 .foregroundStyle(ZyquoColor.warning)39 .help("Limited tool use — not recommended for agent tasks")40 }41 Image(systemName: "chevron.down")42 .font(.system(size: 8, weight: .semibold))43 .foregroundStyle(ZyquoColor.textTertiary)44 }45 .padding(.horizontal, ZyquoSpacing.xs)46 .padding(.vertical, 4)47 .background(48 RoundedRectangle(cornerRadius: ZyquoRadius.small, style: .continuous)49 .fill(ZyquoColor.surfaceSecondary)50 )51 }52 .buttonStyle(PressableButtonStyle())53 .popover(isPresented: $showingPicker, arrowEdge: .bottom) {54 ModelPickerView(selected: model) { chosen in55 showingPicker = false56 onSelect(chosen)57 }58 }59 }60}6162/// Picker popover: search + recommended agent tier + provider groups (full63/// shared catalog; non-agent-capable entries dimmed).64struct ModelPickerView: View {65 let selected: AIModel?66 var onSelect: (AIModel) -> Void6768 @EnvironmentObject private var catalog: ModelCatalog69 @EnvironmentObject private var vault: KeyVaultStore70 @EnvironmentObject private var settings: AgentSettingsStore71 @State private var query = ""7273 var body: some View {74 VStack(spacing: 0) {75 HStack(spacing: ZyquoSpacing.xxs) {76 Image(systemName: "magnifyingglass")77 .font(.system(size: 11))78 .foregroundStyle(ZyquoColor.textTertiary)79 TextField("Search models", text: $query)80 .textFieldStyle(.plain)81 .font(ZyquoFont.body(size: 12.5))82 }83 .padding(ZyquoSpacing.xs)84 ZyquoHairline()85 ScrollView {86 LazyVStack(alignment: .leading, spacing: 1) {87 if query.isEmpty, !catalog.recommendedAgentModels.isEmpty {88 sectionHeader("Recommended for agent tasks")89 ForEach(catalog.recommendedAgentModels) { model in90 row(model)91 }92 }93 ForEach(providerGroups, id: \.0) { provider, models in94 sectionHeader(provider.displayName)95 ForEach(models) { model in row(model) }96 }97 }98 .padding(ZyquoSpacing.xxs)99 }100 .frame(width: 340, height: 400)101 }102 .background(ZyquoColor.surface)103 }104105 /// Providers with a saved key first; models filtered by the search text.106 private var providerGroups: [(ProviderID, [AIModel])] {107 let ordered = ProviderID.builtIn.sorted {108 (vault.hasKey(for: $0) ? 0 : 1, $0.displayName) < (vault.hasKey(for: $1) ? 0 : 1, $1.displayName)109 }110 return ordered.compactMap { provider in111 var models = catalog.models(for: provider)112 if !query.isEmpty {113 models = models.filter {114 $0.displayName.localizedCaseInsensitiveContains(query)115 || $0.id.localizedCaseInsensitiveContains(query)116 }117 }118 // Agent-capable first within each provider.119 models = models.sorted { ($0.agentCapable ? 0 : 1) < ($1.agentCapable ? 0 : 1) }120 return models.isEmpty ? nil : (provider, models)121 }122 }123124 private func sectionHeader(_ title: String) -> some View {125 Text(title)126 .font(ZyquoFont.caption)127 .foregroundStyle(ZyquoColor.textTertiary)128 .padding(.horizontal, ZyquoSpacing.xs)129 .padding(.top, ZyquoSpacing.xs)130 .padding(.bottom, 2)131 }132133 private func row(_ model: AIModel) -> some View {134 let defaultModel = settings.defaultAgentModel(in: catalog)135 let isDefault = model.id == defaultModel?.id136 && model.provider == defaultModel?.provider137 let isSelected = model.id == selected?.id && model.provider == selected?.provider138 return Button {139 onSelect(model)140 } label: {141 HStack(spacing: ZyquoSpacing.xs) {142 Image(systemName: model.provider.symbolName)143 .font(.system(size: 11))144 .foregroundStyle(model.agentCapable ? ZyquoColor.accent : ZyquoColor.textTertiary)145 .frame(width: 14)146 Text(model.displayName)147 .font(model.agentCapable ? ZyquoFont.bodyEmphasis(size: 12.5) : ZyquoFont.body(size: 12.5))148 .foregroundStyle(model.agentCapable ? ZyquoColor.textPrimary : ZyquoColor.textTertiary)149 .lineLimit(1)150 if isDefault {151 Image(systemName: "star.fill")152 .font(.system(size: 9))153 .foregroundStyle(ZyquoColor.warning)154 .help("Default agent model")155 }156 Spacer(minLength: ZyquoSpacing.xs)157 if !model.agentCapable {158 Text("limited tool use")159 .font(ZyquoFont.caption)160 .foregroundStyle(ZyquoColor.textTertiary)161 }162 if model.capabilities.reasoning {163 Image(systemName: "brain")164 .font(.system(size: 9))165 .foregroundStyle(ZyquoColor.textTertiary)166 }167 Text(model.contextBadge)168 .font(ZyquoFont.caption)169 .foregroundStyle(ZyquoColor.textTertiary)170 }171 .padding(.horizontal, ZyquoSpacing.xs)172 .padding(.vertical, 4)173 .background(174 RoundedRectangle(cornerRadius: ZyquoRadius.small, style: .continuous)175 .fill(isSelected ? ZyquoColor.accentSubtle : .clear)176 )177 .contentShape(Rectangle())178 }179 .buttonStyle(.plain)180 .zyquoHoverHighlight()181 }182}183