/** * — renders the registered secrets from the framework * secrets registry. Configured keys stay compact; adding or editing one * progressively discloses its controls. */ import { Picker, TextField } from "@agent-native/toolkit/design-system"; import { Button as ToolkitButton, ButtonBase as ToolkitButtonBase, } from "@agent-native/toolkit/ui/button"; import { IconCheck, IconChevronRight, IconExternalLink, IconLoader2, IconPlugConnected, IconPlus, IconTrash, IconRefresh, } from "@tabler/icons-react"; import React, { useEffect, useMemo, useState, useCallback } from "react"; import { agentNativePath } from "../api-path.js"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "../components/ui/dropdown-menu.js"; import { Tooltip, TooltipContent, TooltipTrigger, } from "../components/ui/tooltip.js"; import { useT } from "../i18n.js"; import { cn } from "../utils.js"; const Button = React.forwardRef< HTMLButtonElement, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); Button.displayName = "SecretsPrimitiveButton"; interface SecretStatus { key: string; label: string; description?: string; docsUrl?: string; scope: "user" | "workspace"; kind: "api-key" | "oauth"; required: boolean; status: "set" | "unset" | "invalid"; last4?: string; updatedAt?: number; oauthProvider?: string; oauthConnectUrl?: string; error?: string; } const ENDPOINT = agentNativePath("/_agent-native/secrets"); function notifySecretsChanged() { if (typeof window === "undefined") return; window.dispatchEvent( new CustomEvent("agent-engine:configured-changed", { detail: { source: "secrets" }, }), ); } export interface SecretsSectionProps { /** Optional hash fragment to focus a specific secret (e.g. "secrets:OPENAI_API_KEY"). */ focusKey?: string; } export function SecretsSection({ focusKey }: SecretsSectionProps) { const [secrets, setSecrets] = useState(null); const [error, setError] = useState(null); const [reloadToken, setReloadToken] = useState(0); const [openSecretKey, setOpenSecretKey] = useState( focusKey ?? null, ); const [customKeyOpen, setCustomKeyOpen] = useState(false); useEffect(() => { let cancelled = false; fetch(ENDPOINT) .then(async (r) => { if (!r.ok) { throw new Error(`Failed to load secrets (${r.status})`); } return (await r.json()) as SecretStatus[]; }) .then((data) => { if (!cancelled) setSecrets(data); }) .catch((err) => { if (!cancelled) setError(err?.message ?? "Failed to load"); }); return () => { cancelled = true; }; }, [reloadToken]); const reload = useCallback(() => setReloadToken((t) => t + 1), []); useEffect(() => { if (focusKey) { setCustomKeyOpen(false); setOpenSecretKey(focusKey); } }, [focusKey]); if (error) { return (

Failed to load secrets: {error}

); } if (secrets === null) { return (
Loading…
); } if (secrets.length === 0) { return (
setCustomKeyOpen(true)} />
); } const visibleSecrets = secrets.filter( (secret) => secret.status !== "unset" || secret.key === openSecretKey, ); const availableSecrets = secrets.filter( (secret) => secret.status === "unset" && secret.key !== openSecretKey, ); return (
{ setCustomKeyOpen(false); setOpenSecretKey(key); }} onCustomKey={() => { setOpenSecretKey(null); setCustomKeyOpen(true); }} /> {visibleSecrets.length > 0 && (
{visibleSecrets.map((secret) => ( { if (open) setCustomKeyOpen(false); setOpenSecretKey(open ? secret.key : null); }} focusInput={openSecretKey === secret.key} /> ))}
)}
); } function KeysHeader({ availableSecrets = [], onSecret, onCustomKey, }: { availableSecrets?: SecretStatus[]; onSecret?: (key: string) => void; onCustomKey: () => void; }) { return (

Keys

New {availableSecrets.length > 0 && ( <> Choose a key {availableSecrets.map((secret) => ( onSecret?.(secret.key)} className="flex items-center justify-between gap-3" > {secret.label} {secret.required && ( Required )} ))} )} Custom
); } interface SecretCardProps { secret: SecretStatus; onChanged: () => void; open: boolean; onOpenChange: (open: boolean) => void; focusInput?: boolean; } function SecretCard({ secret, onChanged, open, onOpenChange, focusInput, }: SecretCardProps) { const [value, setValue] = useState(""); const [busy, setBusy] = useState(null); const [confirmDelete, setConfirmDelete] = useState(false); const [toast, setToast] = useState<{ kind: "ok" | "err"; text: string; } | null>(null); const inputRef = React.useRef(null); useEffect(() => { if (open && focusInput && inputRef.current) { inputRef.current.focus(); } }, [focusInput, open]); const setToastAndClear = (kind: "ok" | "err", text: string, ms = 2500) => { setToast({ kind, text }); setTimeout(() => setToast(null), ms); }; const handleSave = async () => { if (!value.trim() || busy) return; setBusy("save"); try { const res = await fetch(`${ENDPOINT}/${encodeURIComponent(secret.key)}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ value: value.trim() }), }); if (!res.ok) { const err = await res .json() .then((j: { error?: string }) => j.error) .catch(() => null); setToastAndClear("err", err ?? `Save failed (${res.status})`); return; } setValue(""); setConfirmDelete(false); setToastAndClear("ok", "Saved"); notifySecretsChanged(); onChanged(); } finally { setBusy(null); } }; const handleDelete = async () => { if (busy) return; setBusy("delete"); try { const res = await fetch(`${ENDPOINT}/${encodeURIComponent(secret.key)}`, { method: "DELETE", headers: { "Content-Type": "application/json" }, }); if (!res.ok) { const err = await res .json() .then((j: { error?: string }) => j.error) .catch(() => null); setToastAndClear("err", err ?? `Delete failed (${res.status})`); return; } setToastAndClear("ok", "Removed"); setConfirmDelete(false); notifySecretsChanged(); onChanged(); } finally { setBusy(null); } }; const handleTest = async () => { if (busy) return; setBusy("test"); try { const res = await fetch( `${ENDPOINT}/${encodeURIComponent(secret.key)}/test`, { method: "POST", }, ); const body = (await res.json().catch(() => ({}))) as { ok?: boolean; error?: string; }; if (res.ok && body.ok) { setToastAndClear("ok", "Working"); } else { setToastAndClear( "err", body.error ?? (body.ok === false ? "Invalid" : `Test failed`), ); } } finally { setBusy(null); } }; const pill = useMemo(() => { if (secret.status === "set") { return ( Set ); } if (secret.required) { return ( Required ); } return ( Optional ); }, [secret.status, secret.required]); const isOAuth = secret.kind === "oauth"; return (
{open && (
{secret.description && (

{secret.description}

)} {isOAuth ? (
{secret.oauthConnectUrl && ( {secret.status === "set" ? "Reconnect" : "Connect"} )} {secret.docsUrl && ( Docs )}
) : (
{secret.status === "set" && (
Stored value ending in {secret.last4}
)}
{ if (event.key === "Enter") handleSave(); }} placeholder={ secret.status === "set" ? "Enter new value to rotate" : "Paste key" } className="flex-1 text-[11px]" />
{secret.status === "set" && ( <> )} {secret.docsUrl && ( Get key )}
{confirmDelete && (
Remove this saved value?
)}
)} {toast && (

{toast.text}

)}
)}
); } // ─── Ad-hoc Keys Section ────────────────────────────────────────────────── interface AdHocKey { name: string; scope: "user" | "workspace"; scopeId: string; description: string | null; last4: string; createdAt: number; updatedAt: number; } const ADHOC_ENDPOINT = agentNativePath("/_agent-native/secrets/adhoc"); function AdHocKeysSection({ showForm, onShowFormChange, showEmptyState, }: { showForm: boolean; onShowFormChange: (show: boolean) => void; showEmptyState: boolean; }) { const t = useT(); const [keys, setKeys] = useState([]); const [loading, setLoading] = useState(true); const [reloadToken, setReloadToken] = useState(0); const [formName, setFormName] = useState(""); const [formValue, setFormValue] = useState(""); const [formDescription, setFormDescription] = useState(""); const [formScope, setFormScope] = useState<"user" | "workspace">("user"); const [formBusy, setFormBusy] = useState(false); const [formError, setFormError] = useState(null); const [confirmDeleteName, setConfirmDeleteName] = useState( null, ); const [deletingName, setDeletingName] = useState(null); const [toast, setToast] = useState<{ kind: "ok" | "err"; text: string; } | null>(null); const showToast = useCallback( (kind: "ok" | "err", text: string, ms = 2500) => { setToast({ kind, text }); setTimeout(() => setToast(null), ms); }, [], ); const reload = useCallback(() => setReloadToken((t) => t + 1), []); useEffect(() => { let cancelled = false; setLoading(true); fetch(ADHOC_ENDPOINT) .then(async (r) => { if (!r.ok) throw new Error(`Failed to load (${r.status})`); return (await r.json()) as AdHocKey[]; }) .then((data) => { if (!cancelled) { setKeys(data); setLoading(false); } }) .catch(() => { if (!cancelled) { setKeys([]); setLoading(false); } }); return () => { cancelled = true; }; }, [reloadToken]); const resetForm = useCallback(() => { onShowFormChange(false); setFormName(""); setFormValue(""); setFormDescription(""); setFormScope("user"); setFormError(null); }, [onShowFormChange]); const handleAdd = useCallback(async () => { const name = formName.trim(); const value = formValue.trim(); if (!name || !value || formBusy) return; setFormBusy(true); setFormError(null); try { const res = await fetch(ADHOC_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, value, description: formDescription.trim() || undefined, scope: formScope, }), }); if (!res.ok) { const body = await res .json() .then((j: { error?: string }) => j.error) .catch(() => null); setFormError(body ?? `Save failed (${res.status})`); return; } resetForm(); showToast("ok", "Key saved"); notifySecretsChanged(); reload(); } catch (err: any) { setFormError(err?.message ?? "Failed to save"); } finally { setFormBusy(false); } }, [ formName, formValue, formDescription, formScope, formBusy, resetForm, showToast, reload, ]); const handleDelete = useCallback( async (name: string) => { setDeletingName(name); try { const res = await fetch( `${ADHOC_ENDPOINT}/${encodeURIComponent(name)}`, { method: "DELETE", headers: { "Content-Type": "application/json" }, }, ); if (!res.ok) { showToast("err", "Failed to delete key"); return; } showToast("ok", "Key deleted"); setConfirmDeleteName(null); notifySecretsChanged(); reload(); } finally { setDeletingName(null); } }, [showToast, reload], ); return (
{showForm && (
setFormName(value.toUpperCase().replace(/[^A-Z0-9_-]/g, "")) } className="w-full text-[11px]" aria-label="Key name" placeholder="KEY_NAME" /> { if (value === "user" || value === "workspace") { setFormScope(value); } }} aria-label={t("secrets.scopeLabel")} description={t( formScope === "user" ? "secrets.scopePersonalDescription" : "secrets.scopeWorkspaceDescription", )} className="text-[11px]" />
{formError &&

{formError}

}
)} {loading ? (
Loading...
) : keys.length === 0 && !showForm && showEmptyState ? (

No keys added yet.

) : keys.length > 0 ? (
{keys.map((key) => (
{key.name} {key.scope === "workspace" ? "workspace" : "personal"}
{key.description && (

{key.description}

)}
Ending in{" "} {key.last4}
{confirmDeleteName === key.name ? (
) : ( Delete )}
))}
) : null} {toast && (

{toast.text}

)}
); }