// // ModelsSettingsTab.swift // Zyquo Agent // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // Settings › Models — the full shared Zyquo Cloud catalog grouped by // provider, with the Agent twist: agent-capable models carry an "Agent" // badge, and the star sets the persisted default agent model used by new // tasks. Per-model info: context window, capability flags, pricing. // import SwiftUI struct ModelsSettingsTab: View { @EnvironmentObject private var catalog: ModelCatalog @EnvironmentObject private var vault: KeyVaultStore @EnvironmentObject private var settings: AgentSettingsStore @State private var selectedProvider: ProviderID = AgentModelSupport.defaultModelProvider @State private var refreshing = false @State private var refreshResult: String? var body: some View { VStack(spacing: 0) { HStack { Picker("Provider", selection: $selectedProvider) { ForEach(ProviderID.builtIn) { provider in Text(provider.displayName).tag(provider) } } .frame(width: 240) Spacer() if let result = refreshResult { Text(result) .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.textSecondary) .lineLimit(1) } Button { refreshModels() } label: { if refreshing { ProgressView().controlSize(.small) } else { Label("Refresh from API", systemImage: "arrow.clockwise") } } .controlSize(.small) .disabled(refreshing || !selectedProvider.supportsModelListing || !vault.hasKey(for: selectedProvider)) } .padding(ZyquoSpacing.sm) ZyquoHairline() defaultBanner ZyquoHairline() modelTable } .background(ZyquoColor.background) } private var defaultBanner: some View { HStack(spacing: ZyquoSpacing.xs) { Image(systemName: "star.fill") .font(.system(size: 10)) .foregroundStyle(ZyquoColor.warning) if let model = settings.defaultAgentModel(in: catalog) { Text("Default agent model: \(model.displayName) (\(model.provider.displayName)) — new tasks start with it. Star another agent-capable model to change.") .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.textSecondary) } else { Text("No default agent model — star an agent-capable model below.") .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.textSecondary) } Spacer(minLength: 0) } .padding(.horizontal, ZyquoSpacing.sm) .padding(.vertical, ZyquoSpacing.xxs) } private var modelTable: some View { ScrollView { LazyVStack(spacing: 1) { ForEach(catalog.models(for: selectedProvider)) { model in modelRow(model) } let unknown = catalog.unknownLiveIDs(for: selectedProvider) if !unknown.isEmpty { Text("Live on the API but not in the catalog: \(unknown.joined(separator: ", "))") .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.textTertiary) .frame(maxWidth: .infinity, alignment: .leading) .padding(ZyquoSpacing.sm) } } .padding(ZyquoSpacing.sm) } } private func modelRow(_ model: AIModel) -> some View { let isDefault = isDefaultAgentModel(model) return HStack(spacing: ZyquoSpacing.xs) { Button { settings.setDefaultAgentModel(model) } label: { Image(systemName: isDefault ? "star.fill" : "star") .font(.system(size: 10)) .foregroundStyle(isDefault ? ZyquoColor.warning : ZyquoColor.textTertiary) } .buttonStyle(.plain) .disabled(!model.agentCapable) .help(model.agentCapable ? "Set as the default agent model" : "Not agent-capable — cannot be the default") VStack(alignment: .leading, spacing: 0) { HStack(spacing: ZyquoSpacing.xxs) { Text(model.displayName) .font(ZyquoFont.body(size: 12.5)) .foregroundStyle(model.agentCapable ? ZyquoColor.textPrimary : ZyquoColor.textSecondary) if model.agentCapable { ZyquoBadge(text: "Agent", color: ZyquoColor.accent) .help("Verified for deep agentic, multi-step tool use") } if model.isRecommended { ZyquoBadge(text: "Featured", color: ZyquoColor.success) } if model.isLegacy { ZyquoBadge(text: "Legacy") } } Text(model.id) .font(ZyquoFont.code(size: 10)) .foregroundStyle(ZyquoColor.textTertiary) } Spacer() HStack(spacing: ZyquoSpacing.xxs) { if model.capabilities.tools { ZyquoBadge(text: "tools") } if model.capabilities.vision { ZyquoBadge(text: "vision") } if model.capabilities.reasoning { ZyquoBadge(text: "reasoning") } } Text(model.contextBadge) .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.textSecondary) .frame(width: 64, alignment: .trailing) Text(pricingText(model)) .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.textTertiary) .frame(width: 100, alignment: .trailing) .help("USD per 1M tokens, input / output (estimates)") } .padding(.vertical, 3) .padding(.horizontal, ZyquoSpacing.xs) .zyquoHoverHighlight() } private func isDefaultAgentModel(_ model: AIModel) -> Bool { let current = settings.defaultAgentModel(in: catalog) return current?.id == model.id && current?.provider == model.provider } private func pricingText(_ model: AIModel) -> String { guard let pricing = model.pricing else { return "—" } return String(format: "$%.2f / $%.2f", pricing.inputPerMTok, pricing.outputPerMTok) } private func refreshModels() { refreshing = true refreshResult = nil let provider = selectedProvider Task { defer { refreshing = false } do { let key = try vault.apiKey(for: provider) let ids = try await ProviderRegistry.client(for: provider).listModelIDs(apiKey: key) catalog.applyLiveListing(ids, for: provider) let unknown = catalog.unknownLiveIDs(for: provider).count refreshResult = "\(ids.count) live models · \(unknown) not in catalog" } catch { refreshResult = error.localizedDescription } } } }