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// ModelsSettingsTab.swift3// Zyquo Agent4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Settings › Models — the full shared Zyquo Cloud catalog grouped by9// provider, with the Agent twist: agent-capable models carry an "Agent"10// badge, and the star sets the persisted default agent model used by new11// tasks. Per-model info: context window, capability flags, pricing.12//1314import SwiftUI1516struct ModelsSettingsTab: View {17 @EnvironmentObject private var catalog: ModelCatalog18 @EnvironmentObject private var vault: KeyVaultStore19 @EnvironmentObject private var settings: AgentSettingsStore20 @State private var selectedProvider: ProviderID = AgentModelSupport.defaultModelProvider21 @State private var refreshing = false22 @State private var refreshResult: String?2324 var body: some View {25 VStack(spacing: 0) {26 HStack {27 Picker("Provider", selection: $selectedProvider) {28 ForEach(ProviderID.builtIn) { provider in29 Text(provider.displayName).tag(provider)30 }31 }32 .frame(width: 240)33 Spacer()34 if let result = refreshResult {35 Text(result)36 .font(ZyquoFont.caption)37 .foregroundStyle(ZyquoColor.textSecondary)38 .lineLimit(1)39 }40 Button {41 refreshModels()42 } label: {43 if refreshing {44 ProgressView().controlSize(.small)45 } else {46 Label("Refresh from API", systemImage: "arrow.clockwise")47 }48 }49 .controlSize(.small)50 .disabled(refreshing || !selectedProvider.supportsModelListing || !vault.hasKey(for: selectedProvider))51 }52 .padding(ZyquoSpacing.sm)53 ZyquoHairline()54 defaultBanner55 ZyquoHairline()56 modelTable57 }58 .background(ZyquoColor.background)59 }6061 private var defaultBanner: some View {62 HStack(spacing: ZyquoSpacing.xs) {63 Image(systemName: "star.fill")64 .font(.system(size: 10))65 .foregroundStyle(ZyquoColor.warning)66 if let model = settings.defaultAgentModel(in: catalog) {67 Text("Default agent model: \(model.displayName) (\(model.provider.displayName)) — new tasks start with it. Star another agent-capable model to change.")68 .font(ZyquoFont.caption)69 .foregroundStyle(ZyquoColor.textSecondary)70 } else {71 Text("No default agent model — star an agent-capable model below.")72 .font(ZyquoFont.caption)73 .foregroundStyle(ZyquoColor.textSecondary)74 }75 Spacer(minLength: 0)76 }77 .padding(.horizontal, ZyquoSpacing.sm)78 .padding(.vertical, ZyquoSpacing.xxs)79 }8081 private var modelTable: some View {82 ScrollView {83 LazyVStack(spacing: 1) {84 ForEach(catalog.models(for: selectedProvider)) { model in85 modelRow(model)86 }87 let unknown = catalog.unknownLiveIDs(for: selectedProvider)88 if !unknown.isEmpty {89 Text("Live on the API but not in the catalog: \(unknown.joined(separator: ", "))")90 .font(ZyquoFont.caption)91 .foregroundStyle(ZyquoColor.textTertiary)92 .frame(maxWidth: .infinity, alignment: .leading)93 .padding(ZyquoSpacing.sm)94 }95 }96 .padding(ZyquoSpacing.sm)97 }98 }99100 private func modelRow(_ model: AIModel) -> some View {101 let isDefault = isDefaultAgentModel(model)102 return HStack(spacing: ZyquoSpacing.xs) {103 Button {104 settings.setDefaultAgentModel(model)105 } label: {106 Image(systemName: isDefault ? "star.fill" : "star")107 .font(.system(size: 10))108 .foregroundStyle(isDefault ? ZyquoColor.warning : ZyquoColor.textTertiary)109 }110 .buttonStyle(.plain)111 .disabled(!model.agentCapable)112 .help(model.agentCapable ? "Set as the default agent model" : "Not agent-capable — cannot be the default")113 VStack(alignment: .leading, spacing: 0) {114 HStack(spacing: ZyquoSpacing.xxs) {115 Text(model.displayName)116 .font(ZyquoFont.body(size: 12.5))117 .foregroundStyle(model.agentCapable ? ZyquoColor.textPrimary : ZyquoColor.textSecondary)118 if model.agentCapable {119 ZyquoBadge(text: "Agent", color: ZyquoColor.accent)120 .help("Verified for deep agentic, multi-step tool use")121 }122 if model.isRecommended { ZyquoBadge(text: "Featured", color: ZyquoColor.success) }123 if model.isLegacy { ZyquoBadge(text: "Legacy") }124 }125 Text(model.id)126 .font(ZyquoFont.code(size: 10))127 .foregroundStyle(ZyquoColor.textTertiary)128 }129 Spacer()130 HStack(spacing: ZyquoSpacing.xxs) {131 if model.capabilities.tools { ZyquoBadge(text: "tools") }132 if model.capabilities.vision { ZyquoBadge(text: "vision") }133 if model.capabilities.reasoning { ZyquoBadge(text: "reasoning") }134 }135 Text(model.contextBadge)136 .font(ZyquoFont.caption)137 .foregroundStyle(ZyquoColor.textSecondary)138 .frame(width: 64, alignment: .trailing)139 Text(pricingText(model))140 .font(ZyquoFont.caption)141 .foregroundStyle(ZyquoColor.textTertiary)142 .frame(width: 100, alignment: .trailing)143 .help("USD per 1M tokens, input / output (estimates)")144 }145 .padding(.vertical, 3)146 .padding(.horizontal, ZyquoSpacing.xs)147 .zyquoHoverHighlight()148 }149150 private func isDefaultAgentModel(_ model: AIModel) -> Bool {151 let current = settings.defaultAgentModel(in: catalog)152 return current?.id == model.id && current?.provider == model.provider153 }154155 private func pricingText(_ model: AIModel) -> String {156 guard let pricing = model.pricing else { return "—" }157 return String(format: "$%.2f / $%.2f", pricing.inputPerMTok, pricing.outputPerMTok)158 }159160 private func refreshModels() {161 refreshing = true162 refreshResult = nil163 let provider = selectedProvider164 Task {165 defer { refreshing = false }166 do {167 let key = try vault.apiKey(for: provider)168 let ids = try await ProviderRegistry.client(for: provider).listModelIDs(apiKey: key)169 catalog.applyLiveListing(ids, for: provider)170 let unknown = catalog.unknownLiveIDs(for: provider).count171 refreshResult = "\(ids.count) live models · \(unknown) not in catalog"172 } catch {173 refreshResult = error.localizedDescription174 }175 }176 }177}178