/** * ApiKeysPanel — grid of saved API keys with add button and empty state. */ import { useEffect, useMemo, useState } from 'react'; import { Button } from '@renderer/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { useStore } from '@renderer/store'; import { AlertTriangle, Info, Key, Plus } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; import { ApiKeyCard } from './ApiKeyCard'; import { ApiKeyFormDialog } from './ApiKeyFormDialog'; import type { ApiKeyEntry } from '@shared/types/extensions'; interface ApiKeysPanelProps { projectPath: string | null; projectLabel: string | null; } export const ApiKeysPanel = ({ projectPath, projectLabel, }: ApiKeysPanelProps): React.JSX.Element => { const { apiKeys, apiKeysLoading, apiKeysError, storageStatus, fetchStorageStatus, cliStatus } = useStore( useShallow((s) => ({ apiKeys: s.apiKeys, apiKeysLoading: s.apiKeysLoading, apiKeysError: s.apiKeysError, storageStatus: s.apiKeyStorageStatus, fetchStorageStatus: s.fetchApiKeyStorageStatus, cliStatus: s.cliStatus, })) ); const [dialogOpen, setDialogOpen] = useState(false); const [editingKey, setEditingKey] = useState(null); useEffect(() => { void fetchStorageStatus(); }, [fetchStorageStatus]); const handleAdd = () => { setEditingKey(null); setDialogOpen(true); }; const handleEdit = (key: ApiKeyEntry) => { setEditingKey(key); setDialogOpen(true); }; const handleDialogClose = () => { setDialogOpen(false); setEditingKey(null); }; const isOsKeychain = storageStatus?.encryptionMethod === 'os-keychain'; const providerKeyCards = useMemo(() => { if (!cliStatus?.providers?.length) { return []; } return ( [ { providerId: 'anthropic', label: 'Anthropic runtime', envVar: 'ANTHROPIC_API_KEY', }, { providerId: 'codex', label: 'Codex runtime', envVar: 'OPENAI_API_KEY', }, ] as const ).flatMap((item) => { const provider = cliStatus.providers.find((entry) => entry.providerId === item.providerId); if (!provider) { return []; } return [ { ...item, authenticated: provider.authenticated, apiKeyConfigured: provider.connection?.apiKeyConfigured ?? false, sourceLabel: provider.connection?.apiKeySourceLabel ?? null, statusMessage: provider.statusMessage ?? null, }, ]; }); }, [cliStatus]); return (
{providerKeyCards.length > 0 && (
{providerKeyCards.map((provider) => (

{provider.label}

{provider.envVar}

{provider.authenticated ? 'Connected' : provider.apiKeyConfigured ? 'Key configured' : 'Key missing'}

{provider.sourceLabel ? `Current source: ${provider.sourceLabel}.` : 'No stored or environment key detected for this provider.'} {provider.statusMessage ? ` ${provider.statusMessage}` : ''}

))}
)} {/* Header row */}

Securely store API keys for auto-filling when installing MCP servers. {storageStatus && ( {isOsKeychain ? ( ) : ( )} {isOsKeychain ? (

Keys are encrypted via {storageStatus.backend} and stored with restricted file permissions (owner-only).

) : (

OS keychain unavailable — keys are encrypted locally with AES-256. For stronger protection, install a keyring service (gnome-keyring, kwallet).

)} )}

{/* Error */} {apiKeysError && (
{apiKeysError}
)} {/* Skeleton loading */} {apiKeysLoading && apiKeys.length === 0 && (
{Array.from({ length: 3 }, (_, i) => (
))}
)} {/* Empty state */} {!apiKeysLoading && apiKeys.length === 0 && (

No API keys saved

Add keys to auto-fill environment variables when installing MCP servers.

)} {/* Key cards grid */} {apiKeys.length > 0 && (
{apiKeys.map((key) => ( ))}
)} {/* Form dialog */}
); };