import { useCallback, useEffect, useMemo, useState } from 'react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { deleteEnvVar, getEnvVars, revealEnvVar, setEnvVar } from '@/hermes' import { Check, Eye, EyeOff, Save, Settings2, Trash2, X, Zap } from '@/lib/icons' import { cn } from '@/lib/utils' import { notify, notifyError } from '@/store/notifications' import type { EnvVarInfo } from '@/types/hermes' import { CONTROL_TEXT } from './constants' import { asText, includesQuery, prettyName, providerGroup, providerPriority, redactedValue, withoutKey } from './helpers' import { LoadingState, Pill, SectionHeading, SettingsContent } from './primitives' import type { EnvPatch, EnvRowProps, ProviderGroup, SearchProps } from './types' const SHOW_ADVANCED_STORAGE_KEY = 'desktop.settings.keys.show_advanced' interface EnvActionsProps { varKey: string info: EnvVarInfo saving: string | null onEdit: () => void onClear: (key: string) => void onReveal: (key: string) => void isRevealed: boolean showReveal?: boolean } function EnvActions({ varKey, info, saving, onEdit, onClear, onReveal, isRevealed, showReveal = true }: EnvActionsProps) { return (
{info.url && ( )} {info.is_set && showReveal && ( )} {info.is_set && ( )}
) } function EnvVarRow({ varKey, info, edits, revealed, saving, setEdits, onSave, onClear, onReveal, compact = false }: EnvRowProps) { const isEditing = edits[varKey] !== undefined const isRevealed = revealed[varKey] !== undefined const value = isRevealed ? revealed[varKey] : info.redacted_value const startEdit = () => setEdits(c => ({ ...c, [varKey]: '' })) if (compact && !isEditing) { return (
{varKey}
{info.description}
) } return (
{varKey} {info.is_set && } {info.is_set ? 'Set' : 'Not set'}

{info.description}

{!isEditing && info.is_set && (
{value || '---'}
)} {isEditing && (
setEdits(c => ({ ...c, [varKey]: e.target.value }))} placeholder={info.is_set ? 'Replace current value' : 'Enter value'} type={info.is_password ? 'password' : 'text'} value={edits[varKey]} />
)}
) } function EnvProviderGroup({ group, rowProps }: { group: ProviderGroup rowProps: Omit }) { const [expanded, setExpanded] = useState(false) const setCount = group.entries.filter(([, info]) => info.is_set).length return (
{expanded && (
{group.entries.map(([key, info]) => ( ))}
)}
) } export function KeysSettings({ query }: SearchProps) { const [vars, setVars] = useState | null>(null) const [edits, setEdits] = useState>({}) const [revealed, setRevealed] = useState>({}) const [saving, setSaving] = useState(null) const [showAdvanced, setShowAdvanced] = useState(() => { try { const stored = window.localStorage.getItem(SHOW_ADVANCED_STORAGE_KEY) if (stored === null) { return false } return stored === 'true' } catch { return false } }) useEffect(() => { try { window.localStorage.setItem(SHOW_ADVANCED_STORAGE_KEY, showAdvanced ? 'true' : 'false') } catch { // Ignore persistence failures and keep in-memory preference. } }, [showAdvanced]) useEffect(() => { let cancelled = false void (async () => { try { const next = await getEnvVars() if (!cancelled) { setVars(next) } } catch (err) { notifyError(err, 'API keys failed to load') } })() return () => void (cancelled = true) }, []) const filterEnv = useCallback( (info: EnvVarInfo, key: string, q: string, cat: string, extra?: string) => { if (asText(info.category) !== cat) { return false } if (!showAdvanced && Boolean(info.advanced)) { return false } if (!q) { return true } return ( key.toLowerCase().includes(q) || includesQuery(info.description, q) || Boolean(extra && extra.toLowerCase().includes(q)) ) }, [showAdvanced] ) const providerGroups = useMemo(() => { if (!vars) { return [] } const q = query.trim().toLowerCase() const entries = Object.entries(vars).filter(([key, info]) => filterEnv(info, key, q, 'provider', providerGroup(key)) ) const groups = new Map() for (const entry of entries) { const name = providerGroup(entry[0]) groups.set(name, [...(groups.get(name) ?? []), entry]) } return Array.from(groups, ([name, entries]) => ({ name, priority: providerPriority(name), entries: entries.sort(([a], [b]) => a.localeCompare(b)), hasAnySet: entries.some(([, info]) => info.is_set) })).sort((a, b) => a.priority - b.priority || a.name.localeCompare(b.name)) }, [filterEnv, query, vars]) const otherGroups = useMemo(() => { if (!vars) { return [] } const q = query.trim().toLowerCase() const labels: Record = { tool: 'Tools', messaging: 'Messaging', setting: 'Settings' } return ['tool', 'messaging', 'setting'].flatMap(cat => { const entries = Object.entries(vars) .filter(([key, info]) => filterEnv(info, key, q, cat)) .sort(([a], [b]) => a.localeCompare(b)) return entries.length === 0 ? [] : [{ category: cat, label: labels[cat] ?? prettyName(cat), entries }] }) }, [filterEnv, query, vars]) function patchVar(key: string, patch: EnvPatch) { setVars(c => (c ? { ...c, [key]: { ...c[key], ...patch } } : c)) } function clearLocalState(key: string) { setEdits(c => withoutKey(c, key)) setRevealed(c => withoutKey(c, key)) } async function handleSave(key: string) { const value = edits[key] if (!value) { return } setSaving(key) try { await setEnvVar(key, value) patchVar(key, { is_set: true, redacted_value: redactedValue(value) }) clearLocalState(key) notify({ kind: 'success', title: 'Credential saved', message: `${key} updated.` }) } catch (err) { notifyError(err, `Failed to save ${key}`) } finally { setSaving(null) } } async function handleClear(key: string) { if (!window.confirm(`Remove ${key} from .env?`)) { return } setSaving(key) try { await deleteEnvVar(key) patchVar(key, { is_set: false, redacted_value: null }) clearLocalState(key) notify({ kind: 'success', title: 'Credential removed', message: `${key} removed.` }) } catch (err) { notifyError(err, `Failed to remove ${key}`) } finally { setSaving(null) } } async function handleReveal(key: string) { if (revealed[key]) { setRevealed(c => withoutKey(c, key)) return } try { const result = await revealEnvVar(key) setRevealed(c => ({ ...c, [key]: result.value })) } catch (err) { notifyError(err, `Failed to reveal ${key}`) } } if (!vars) { return } const rowProps = { edits, revealed, saving, setEdits, onSave: handleSave, onClear: handleClear, onReveal: handleReveal } const configuredCount = providerGroups.filter(g => g.hasAnySet).length return (
{providerGroups.map(group => ( ))}
{otherGroups.map(group => (
i.is_set).length} of ${group.entries.length} set`} title={group.label} />
{group.entries.map(([key, info]) => ( ))}
))}
) }