import { useState, type ReactNode } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { ROLES, type AgentKey, type Role } from '@wellformed/shared'; import { api } from '../api/client'; import { useAgentKeys } from '../api/hooks'; import { useI18n } from '../i18n'; import { useSession } from '../state/session'; import { useToasts } from '../state/toasts'; import { absolute } from '../state/route'; import { Modal } from './Modal'; /** * The keys an AI assistant works through. * * The thing worth being clear about on this screen is that a key is *yours*. * It acts as you, with your role, and everything it does lands in the audit * trail under your name with the key's label beside it. So the label is not * decoration — it is how someone reading the trail in three months knows which * assistant did what. */ export function AgentKeysDialog({ onClose }: { onClose(): void }): ReactNode { const session = useSession(); const toasts = useToasts(); const queryClient = useQueryClient(); const { t } = useI18n(); const isAdmin = session.can('admin'); const myRole = session.workspace?.role ?? 'viewer'; const keys = useAgentKeys(true); const [creating, setCreating] = useState(false); const [label, setLabel] = useState(''); const [ceiling, setCeiling] = useState('viewer'); const [busy, setBusy] = useState(false); /** Shown until this dialog closes, because there is no second chance to read it. */ const [issued, setIssued] = useState(null); const refresh = () => queryClient.invalidateQueries({ queryKey: ['agentKeys'] }); /** A ceiling above your own role would be inert, so it is not offered. */ const available = ROLES.filter((r) => rank(r) <= rank(myRole)); async function create() { setBusy(true); try { const { key } = await api.createAgentKey({ label: label.trim(), ceiling }); await refresh(); setIssued(key); setCreating(false); setLabel(''); await copy(key.secret!); toasts.push(t('keys.copied'), 'success'); } catch (error) { toasts.fail(error); } finally { setBusy(false); } } async function revoke(key: AgentKey) { const ok = window.confirm(t('keys.revokeConfirm', key.label)); if (!ok) return; try { await api.revokeAgentKey(key.id); if (issued?.id === key.id) setIssued(null); await refresh(); } catch (error) { toasts.fail(error); } } const rows = keys.data?.keys ?? []; return (
{!creating && ( )} } >

{t('keys.intro')}

{keys.isLoading &&
{t('common.loading')}
} {!keys.isLoading && rows.length === 0 && !creating && (
{t('keys.noneYet')}
)} {rows.length > 0 && ( {isAdmin && } {rows.map((key) => ( {isAdmin && ( )} ))}
{t('keys.label')} {t('keys.can')}{t('keys.heldBy')}{t('keys.lastUsed')}
{key.label} {t(`role.${key.ceiling}`)} {key.accountId === session.account?.id ? t('keys.you') : key.accountName} {key.lastUsedAt ? key.lastUsedAt.replace(' ', ' · ') : t('keys.never')}
)} {issued?.secret && (
{t('keys.copyNow')}
e.target.select()} />
{t('keys.pointBefore')} {absolute(`/api/w/${session.workspace?.slug ?? ''}/ai/manifest`)} {t('keys.pointMiddle')} Authorization: Bearer <key> {t('keys.pointAfter')}
)} {creating && (
{t('keys.ceilingHint', t(`role.${myRole}`))}
)}
); } function rank(role: Role): number { return ROLES.indexOf(role); } async function copy(text: string): Promise { try { await navigator.clipboard.writeText(text); } catch { // Clipboard access can be refused; the key is on screen either way. } }