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%
5.7 KB · 148 lines swift
Raw Blame History
1//2//  ProvidersSettingsTab.swift3//  Zyquo Agent4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  Settings › Providers & Keys — ported from Zyquo Cloud's vault UI: one row9//  per provider (glyph, name, status dot, masked key field, Test, delete),10//  extended with the environment-variable fallback indicator (keys resolved11//  from the environment take precedence over the vault at run time).12//1314import SwiftUI1516struct ProvidersSettingsTab: View {17    @EnvironmentObject private var vault: KeyVaultStore18    @EnvironmentObject private var catalog: ModelCatalog19    @State private var draftKeys: [ProviderID: String] = [:]2021    var body: some View {22        ScrollView {23            VStack(spacing: ZyquoSpacing.xs) {24                Text("Keys are stored in the encrypted vault shared with Zyquo Cloud (AES-256-GCM, bound to this Mac — never the Keychain). Environment variables, when set, take precedence at run time.")25                    .font(ZyquoFont.caption)26                    .foregroundStyle(ZyquoColor.textTertiary)27                    .frame(maxWidth: .infinity, alignment: .leading)28                ForEach(ProviderID.builtIn) { provider in29                    providerRow(provider)30                    if provider != ProviderID.builtIn.last { ZyquoHairline() }31                }32            }33            .padding(ZyquoMetrics.contentInset)34        }35        .background(ZyquoColor.background)36    }3738    private func providerRow(_ provider: ProviderID) -> some View {39        HStack(spacing: ZyquoSpacing.sm) {40            Image(systemName: provider.symbolName)41                .font(.system(size: 14))42                .foregroundStyle(ZyquoColor.accent)43                .frame(width: 22)44            VStack(alignment: .leading, spacing: 1) {45                HStack(spacing: ZyquoSpacing.xxs) {46                    Text(provider.displayName)47                        .font(ZyquoFont.bodyEmphasis(size: 13))48                        .foregroundStyle(ZyquoColor.textPrimary)49                    statusIndicator(provider)50                    if let envName = activeEnvironmentKey(provider) {51                        ZyquoBadge(text: "env: \(envName)", color: ZyquoColor.success)52                            .help("An environment variable provides this key; it takes precedence over the vault.")53                    }54                }55                statusDetail(provider)56            }57            Spacer()58            keyField(provider)59            testButton(provider)60            if vault.hasKey(for: provider) {61                Button {62                    vault.deleteKey(for: provider)63                } label: {64                    Image(systemName: "trash")65                        .font(.system(size: 11))66                        .foregroundStyle(ZyquoColor.danger)67                }68                .buttonStyle(.plain)69                .help("Delete key from the vault")70            }71        }72        .padding(.vertical, ZyquoSpacing.xxs)73    }7475    /// The first set environment variable providing a key for this provider.76    private func activeEnvironmentKey(_ provider: ProviderID) -> String? {77        let environment = ProcessInfo.processInfo.environment78        return AgentCLI.environmentKeyNames(for: provider).first {79            !(environment[$0] ?? "").isEmpty80        }81    }8283    @ViewBuilder84    private func statusIndicator(_ provider: ProviderID) -> some View {85        switch vault.statuses[provider] ?? .unset {86        case .unset: StatusDot(status: .unset)87        case .saved: StatusDot(status: .unset).overlay(Circle().strokeBorder(ZyquoColor.textSecondary, lineWidth: 1))88        case .testing: ProgressView().controlSize(.mini)89        case .verified: StatusDot(status: .verified)90        case .failed: StatusDot(status: .failed)91        }92    }9394    @ViewBuilder95    private func statusDetail(_ provider: ProviderID) -> some View {96        switch vault.statuses[provider] ?? .unset {97        case .verified(let latency):98            Text(String(format: "Verified · %.0f ms", latency * 1000))99                .font(ZyquoFont.caption)100                .foregroundStyle(ZyquoColor.success)101        case .failed(let message):102            Text(message)103                .font(ZyquoFont.caption)104                .foregroundStyle(ZyquoColor.danger)105                .lineLimit(1)106                .help(message)107        case .saved:108            Text(vault.redactedKeys[provider] ?? "")109                .font(ZyquoFont.caption)110                .foregroundStyle(ZyquoColor.textTertiary)111        default:112            Text("No key")113                .font(ZyquoFont.caption)114                .foregroundStyle(ZyquoColor.textTertiary)115        }116    }117118    private func keyField(_ provider: ProviderID) -> some View {119        SecureField(120            vault.hasKey(for: provider) ? (vault.redactedKeys[provider] ?? "") : "API key",121            text: Binding(122                get: { draftKeys[provider] ?? "" },123                set: { draftKeys[provider] = $0 }124            )125        )126        .textFieldStyle(.roundedBorder)127        .font(ZyquoFont.code(size: 11))128        .frame(width: 200)129        .onSubmit { saveDraft(provider) }130    }131132    private func testButton(_ provider: ProviderID) -> some View {133        Button("Test") {134            saveDraft(provider)135            Task { await vault.testKey(for: provider, catalog: catalog) }136        }137        .controlSize(.small)138        .disabled(!vault.hasKey(for: provider) && (draftKeys[provider] ?? "").isEmpty)139    }140141    private func saveDraft(_ provider: ProviderID) {142        if let draft = draftKeys[provider], !draft.trimmingCharacters(in: .whitespaces).isEmpty {143            vault.setKey(draft, for: provider)144            draftKeys[provider] = ""145        }146    }147}148