SPB Git

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%
7.3 KB · 183 lines swift
Raw Blame History
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