agent-ecosystem/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx
Artem Rootman da58917032
perf: offload heavy I/O to worker thread, reduce renderer re-renders
Main process — worker thread for team data:
- New team-data-worker thread handles getTeamData and findLogsForTask,
  isolating heavy file I/O (scanning 300+ subagent JSONL files) from
  Electron's main event loop. getTeamData dropped from ~2000ms on the
  main thread to ~110ms via the worker.
- Worker-side dedup and 10s result cache for findLogsForTask prevents
  redundant scans when the same task is queried multiple times.
- Discovery cache TTL raised from 5s to 30s — avoids re-scanning the
  entire project directory on every call.
- Message cap at 200 in TeamDataService to keep IPC payloads under 1MB
  (was sending 2200+ messages / ~3MB, stalling Chromium IPC serialization).
- IPC handlers fall back to main-thread execution if the worker is
  unavailable (graceful degradation).

Renderer — useShallow and memoization (55 files):
- Added useShallow to store selectors across 55 renderer files. Batched
  individual useStore() calls (e.g. 17 calls in ExtensionStoreView,
  10 in ConnectionSection) into single useShallow selectors, cutting
  unnecessary re-render checks on every store update.
- MemberLogsTab: three 5-second polling intervals now pause when the
  parent tab is hidden (display:none). Previously 5 hidden tabs × 3
  intervals = 15 polling timers firing continuously.
- KanbanColumn wrapped in React.memo to skip re-renders when props
  haven't changed.
- MemberList: memoized activeMembers/removedMembers/colorMap; replaced
  O(n×m) per-member task scan with a pre-computed reviewer map.
- Bounded timer Maps in store initialization to prevent unbounded growth
  of debounce/throttle tracking maps during long sessions.
2026-04-05 16:21:05 +00:00

144 lines
5 KiB
TypeScript

/**
* ApiKeysPanel — grid of saved API keys with add button and empty state.
*/
import { useEffect, 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 { useShallow } from 'zustand/react/shallow';
import { AlertTriangle, Info, Key, Plus } from 'lucide-react';
import { ApiKeyCard } from './ApiKeyCard';
import { ApiKeyFormDialog } from './ApiKeyFormDialog';
import type { ApiKeyEntry } from '@shared/types/extensions';
export const ApiKeysPanel = (): React.JSX.Element => {
const { apiKeys, apiKeysLoading, apiKeysError, storageStatus, fetchStorageStatus } = useStore(
useShallow((s) => ({
apiKeys: s.apiKeys,
apiKeysLoading: s.apiKeysLoading,
apiKeysError: s.apiKeysError,
storageStatus: s.apiKeyStorageStatus,
fetchStorageStatus: s.fetchApiKeyStorageStatus,
}))
);
const [dialogOpen, setDialogOpen] = useState(false);
const [editingKey, setEditingKey] = useState<ApiKeyEntry | null>(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';
return (
<div className="flex flex-col gap-4">
{/* Header row */}
<div className="flex items-center justify-between">
<p className="flex items-center gap-1.5 text-sm text-text-secondary">
Securely store API keys for auto-filling when installing MCP servers.
{storageStatus && (
<Tooltip>
<TooltipTrigger asChild>
{isOsKeychain ? (
<Info className="size-3.5 shrink-0 cursor-help text-text-muted" />
) : (
<AlertTriangle className="size-3.5 shrink-0 cursor-help text-amber-400" />
)}
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
{isOsKeychain ? (
<p>
Keys are encrypted via {storageStatus.backend} and stored with restricted file
permissions (owner-only).
</p>
) : (
<p>
OS keychain unavailable keys are encrypted locally with AES-256. For stronger
protection, install a keyring service (gnome-keyring, kwallet).
</p>
)}
</TooltipContent>
</Tooltip>
)}
</p>
<Button variant="outline" size="sm" onClick={handleAdd} className="gap-1.5">
<Plus className="size-3.5" />
Add API Key
</Button>
</div>
{/* Error */}
{apiKeysError && (
<div className="rounded-md border border-red-500/30 bg-red-500/5 p-4 text-sm text-red-400">
{apiKeysError}
</div>
)}
{/* Skeleton loading */}
{apiKeysLoading && apiKeys.length === 0 && (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
{Array.from({ length: 3 }, (_, i) => (
<div
key={i}
className="skeleton-card flex flex-col gap-2 rounded-lg border border-border p-4"
style={{ animationDelay: `${i * 80}ms` }}
>
<div className="h-4 w-32 rounded bg-surface-raised" />
<div className="h-3 w-24 rounded bg-surface-raised" />
<div className="h-3 w-40 rounded bg-surface-raised" />
</div>
))}
</div>
)}
{/* Empty state */}
{!apiKeysLoading && apiKeys.length === 0 && (
<div className="flex flex-col items-center gap-3 rounded-sm border border-dashed border-border px-8 py-16">
<div className="flex size-10 items-center justify-center rounded-lg border border-border bg-surface-raised">
<Key className="size-5 text-text-muted" />
</div>
<p className="text-sm text-text-secondary">No API keys saved</p>
<p className="text-xs text-text-muted">
Add keys to auto-fill environment variables when installing MCP servers.
</p>
<Button variant="outline" size="sm" onClick={handleAdd} className="mt-2 gap-1.5">
<Plus className="size-3.5" />
Add your first key
</Button>
</div>
)}
{/* Key cards grid */}
{apiKeys.length > 0 && (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
{apiKeys.map((key) => (
<ApiKeyCard key={key.id} apiKey={key} onEdit={handleEdit} />
))}
</div>
)}
{/* Form dialog */}
<ApiKeyFormDialog open={dialogOpen} editingKey={editingKey} onClose={handleDialogClose} />
</div>
);
};