// // ProvidersSettingsTab.swift // Zyquo Agent // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // Settings › Providers & Keys — ported from Zyquo Cloud's vault UI: one row // per provider (glyph, name, status dot, masked key field, Test, delete), // extended with the environment-variable fallback indicator (keys resolved // from the environment take precedence over the vault at run time). // import SwiftUI struct ProvidersSettingsTab: View { @EnvironmentObject private var vault: KeyVaultStore @EnvironmentObject private var catalog: ModelCatalog @State private var draftKeys: [ProviderID: String] = [:] var body: some View { ScrollView { VStack(spacing: ZyquoSpacing.xs) { 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.") .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.textTertiary) .frame(maxWidth: .infinity, alignment: .leading) ForEach(ProviderID.builtIn) { provider in providerRow(provider) if provider != ProviderID.builtIn.last { ZyquoHairline() } } } .padding(ZyquoMetrics.contentInset) } .background(ZyquoColor.background) } private func providerRow(_ provider: ProviderID) -> some View { HStack(spacing: ZyquoSpacing.sm) { Image(systemName: provider.symbolName) .font(.system(size: 14)) .foregroundStyle(ZyquoColor.accent) .frame(width: 22) VStack(alignment: .leading, spacing: 1) { HStack(spacing: ZyquoSpacing.xxs) { Text(provider.displayName) .font(ZyquoFont.bodyEmphasis(size: 13)) .foregroundStyle(ZyquoColor.textPrimary) statusIndicator(provider) if let envName = activeEnvironmentKey(provider) { ZyquoBadge(text: "env: \(envName)", color: ZyquoColor.success) .help("An environment variable provides this key; it takes precedence over the vault.") } } statusDetail(provider) } Spacer() keyField(provider) testButton(provider) if vault.hasKey(for: provider) { Button { vault.deleteKey(for: provider) } label: { Image(systemName: "trash") .font(.system(size: 11)) .foregroundStyle(ZyquoColor.danger) } .buttonStyle(.plain) .help("Delete key from the vault") } } .padding(.vertical, ZyquoSpacing.xxs) } /// The first set environment variable providing a key for this provider. private func activeEnvironmentKey(_ provider: ProviderID) -> String? { let environment = ProcessInfo.processInfo.environment return AgentCLI.environmentKeyNames(for: provider).first { !(environment[$0] ?? "").isEmpty } } @ViewBuilder private func statusIndicator(_ provider: ProviderID) -> some View { switch vault.statuses[provider] ?? .unset { case .unset: StatusDot(status: .unset) case .saved: StatusDot(status: .unset).overlay(Circle().strokeBorder(ZyquoColor.textSecondary, lineWidth: 1)) case .testing: ProgressView().controlSize(.mini) case .verified: StatusDot(status: .verified) case .failed: StatusDot(status: .failed) } } @ViewBuilder private func statusDetail(_ provider: ProviderID) -> some View { switch vault.statuses[provider] ?? .unset { case .verified(let latency): Text(String(format: "Verified · %.0f ms", latency * 1000)) .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.success) case .failed(let message): Text(message) .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.danger) .lineLimit(1) .help(message) case .saved: Text(vault.redactedKeys[provider] ?? "") .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.textTertiary) default: Text("No key") .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.textTertiary) } } private func keyField(_ provider: ProviderID) -> some View { SecureField( vault.hasKey(for: provider) ? (vault.redactedKeys[provider] ?? "") : "API key", text: Binding( get: { draftKeys[provider] ?? "" }, set: { draftKeys[provider] = $0 } ) ) .textFieldStyle(.roundedBorder) .font(ZyquoFont.code(size: 11)) .frame(width: 200) .onSubmit { saveDraft(provider) } } private func testButton(_ provider: ProviderID) -> some View { Button("Test") { saveDraft(provider) Task { await vault.testKey(for: provider, catalog: catalog) } } .controlSize(.small) .disabled(!vault.hasKey(for: provider) && (draftKeys[provider] ?? "").isEmpty) } private func saveDraft(_ provider: ProviderID) { if let draft = draftKeys[provider], !draft.trimmingCharacters(in: .whitespaces).isEmpty { vault.setKey(draft, for: provider) draftKeys[provider] = "" } } }