import { useCallback, useEffect, useMemo, useState } from 'react' import { PageLoader } from '@/components/page-loader' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { deleteEnvVar, getToolsetConfig, revealEnvVar, selectToolsetProvider, setEnvVar } from '@/hermes' import { Check, Loader2, Save } from '@/lib/icons' import { cn } from '@/lib/utils' import { notify, notifyError } from '@/store/notifications' import type { ToolEnvVar, ToolProvider, ToolsetConfig } from '@/types/hermes' import { EnvVarActionsMenu, EnvVarActionsTrigger } from './env-var-actions-menu' import { Pill } from './primitives' interface ToolsetConfigPanelProps { toolset: string /** Called after a key is saved/cleared or a provider chosen, so the parent * can refresh the "Configured / Needs keys" pill. */ onConfiguredChange?: () => void } function providerConfigured(provider: ToolProvider, envState: Record): boolean { if (provider.env_vars.length === 0) { return true } return provider.env_vars.every(ev => envState[ev.key]) } interface EnvVarFieldProps { envVar: ToolEnvVar isSet: boolean onSaved: (key: string) => void onCleared: (key: string) => void } function EnvVarField({ envVar, isSet, onSaved, onCleared }: EnvVarFieldProps) { const [editing, setEditing] = useState(false) const [value, setValue] = useState('') const [revealed, setRevealed] = useState(null) const [busy, setBusy] = useState(false) async function handleSave() { if (!value) { return } setBusy(true) try { await setEnvVar(envVar.key, value) setEditing(false) setValue('') onSaved(envVar.key) notify({ kind: 'success', title: 'Credential saved', message: `${envVar.key} updated.` }) } catch (err) { notifyError(err, `Failed to save ${envVar.key}`) } finally { setBusy(false) } } async function handleClear() { if (!window.confirm(`Remove ${envVar.key} from .env?`)) { return } setBusy(true) try { await deleteEnvVar(envVar.key) setRevealed(null) onCleared(envVar.key) notify({ kind: 'success', title: 'Credential removed', message: `${envVar.key} removed.` }) } catch (err) { notifyError(err, `Failed to remove ${envVar.key}`) } finally { setBusy(false) } } async function handleReveal() { if (revealed !== null) { setRevealed(null) return } try { const result = await revealEnvVar(envVar.key) setRevealed(result.value) } catch (err) { notifyError(err, `Failed to reveal ${envVar.key}`) } } return (
{envVar.key} {isSet && } {isSet ? 'Set' : 'Not set'}
{envVar.prompt && envVar.prompt !== envVar.key && (

{envVar.prompt}

)}
{!editing && ( void handleClear()} onEdit={() => setEditing(true)} onReveal={() => void handleReveal()} > event.stopPropagation()} /> )}
{isSet && revealed !== null && (
{revealed || '---'}
)} {editing && (
setValue(e.target.value)} placeholder={envVar.prompt || envVar.key} type={envVar.default ? 'text' : 'password'} value={value} />
)}
) } export function ToolsetConfigPanel({ toolset, onConfiguredChange }: ToolsetConfigPanelProps) { const [cfg, setCfg] = useState(null) const [loading, setLoading] = useState(true) const [selecting, setSelecting] = useState(null) const [activeProvider, setActiveProvider] = useState(null) // Live per-key set/unset state, seeded from the endpoint then patched locally. const [envState, setEnvState] = useState>({}) const refresh = useCallback(async () => { setLoading(true) try { const next = await getToolsetConfig(toolset) setCfg(next) const seeded: Record = {} for (const provider of next.providers) { for (const ev of provider.env_vars) { seeded[ev.key] = ev.is_set } } setEnvState(seeded) } catch (err) { notifyError(err, 'Tool configuration failed to load') } finally { setLoading(false) } }, [toolset]) useEffect(() => { void refresh() }, [refresh]) const providers = useMemo(() => cfg?.providers ?? [], [cfg]) // Default the expanded provider to the one actually active in config // (`is_active` / `cfg.active_provider`, mirroring the CLI picker), then the // first fully-configured provider, else the first provider. Without this the // panel highlighted the first keyless provider (e.g. Nous Portal) even when // the user had already selected another (e.g. DuckDuckGo). useEffect(() => { if (activeProvider || providers.length === 0) { return } const selected = providers.find(p => p.is_active) ?? (cfg?.active_provider ? providers.find(p => p.name === cfg.active_provider) : undefined) ?? providers.find(p => providerConfigured(p, envState)) ?? providers[0] setActiveProvider(selected.name) }, [activeProvider, providers, envState, cfg]) async function handleSelect(provider: ToolProvider) { setActiveProvider(provider.name) setSelecting(provider.name) try { await selectToolsetProvider(toolset, provider.name) notify({ kind: 'success', title: 'Provider selected', message: `${provider.name} is now active.` }) onConfiguredChange?.() } catch (err) { notifyError(err, `Failed to select ${provider.name}`) } finally { setSelecting(null) } } function patchEnv(key: string, isSet: boolean) { setEnvState(c => ({ ...c, [key]: isSet })) onConfiguredChange?.() } const emptyMessage = useMemo(() => { if (loading || !cfg) { return null } if (!cfg.has_category) { return 'This toolset has no provider options — enable it and it works with your current setup.' } if (providers.length === 0) { return 'No providers are available for this toolset right now.' } return null }, [cfg, loading, providers.length]) if (loading) { return } if (emptyMessage) { return

{emptyMessage}

} return (
{providers.map(provider => { const isActive = activeProvider === provider.name const configured = providerConfigured(provider, envState) return (
{isActive && (
{provider.tag &&

{provider.tag}

} {provider.requires_nous_auth && (

Included with a Nous subscription — sign in to Nous Portal to activate.

)} {provider.env_vars.length === 0 ? (

No API key required.

) : ( provider.env_vars.map(ev => ( patchEnv(key, false)} onSaved={key => patchEnv(key, true)} /> )) )} {provider.post_setup && (

This provider needs an extra setup step ({provider.post_setup}). Run it from the CLI with{' '} hermes tools for now.

)}
)}
) })}
) }