import { useCallback, useEffect, useMemo, useState } from 'react' import { KeyRound, Plug, Plus, Bot, Sparkles, Trash2, Wand2 } from 'lucide-react' import { toast } from 'sonner' import { AppShell } from '@/components/AppShell' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { cn } from '@/lib/utils' import { api } from '@/lib/api' import { useAuth } from '@/store/auth' interface Team { id: string name: string } interface ApiConfig { id: string name: string provider: string model: string endpoint: string | null } interface McpServer { id: string name: string endpoint: string enabled: boolean headerNames: string[] } interface Seat { id: string teamId: string roleName: string state: string agentId?: string | null } interface Skill { skillKey: string name: string roles: string[] status: string } interface Agent { id: string name: string monogram?: string | null autonomy: string apiConfigId: string skillKeys: string[] mcpServerIds: string[] docs: string[] persona?: string | null } interface AgentProfileLite { id: string profileKey: string name: string monogram?: string | null recommendedAutonomy: string skillKeys: string[] } interface AgentProfileDetail { profile: AgentProfileLite body: string } const AUTONOMY = [ { value: 'DraftOnly', label: 'Draft', on: 'bg-slate-600 text-white' }, { value: 'Gated', label: 'Gated', on: 'bg-indigo-600 text-white' }, { value: 'Autonomous', label: 'Auto', on: 'bg-teal-600 text-white' }, ] as const export function SeatsPage() { const organizationId = useAuth((s) => s.organizationId) const [teams, setTeams] = useState([]) const [teamId, setTeamId] = useState(null) const [configs, setConfigs] = useState([]) const [mcpServers, setMcpServers] = useState([]) const [seats, setSeats] = useState([]) const [skills, setSkills] = useState([]) const [cfg, setCfg] = useState({ name: '', provider: 'stub', model: 'gpt-4o-mini', apiKey: '', endpoint: '' }) const [mcp, setMcp] = useState({ name: '', endpoint: '', headerName: 'Authorization', headerValue: '' }) const [newSeat, setNewSeat] = useState('') const [selectedSeat, setSelectedSeat] = useState(null) const [profiles, setProfiles] = useState([]) const [agent, setAgent] = useState({ name: '', monogram: '', autonomy: 'Gated', apiConfigId: '', skillKeys: [] as string[], mcpServerIds: [] as string[], docs: '', persona: '', }) const run = useCallback(async (action: () => Promise) => { try { await action() } catch (err) { toast.error((err as Error).message) } }, []) const loadConfigs = useCallback(async () => { if (!organizationId) return setConfigs(await api.get(`/api/integrations/api-configs?organizationId=${organizationId}`)) }, [organizationId]) const loadMcpServers = useCallback(async () => { if (!organizationId) return setMcpServers(await api.get(`/api/integrations/mcp-servers?organizationId=${organizationId}`)) }, [organizationId]) const loadSeats = useCallback(async (id: string) => { setSeats(await api.get(`/api/orgboard/seats?teamId=${id}`)) }, []) useEffect(() => { if (!organizationId) return void run(async () => { setTeams(await api.get(`/api/orgboard/teams?organizationId=${organizationId}`)) // The org's library = shared builtins + its own authored/installed skills. The API returns // every version (newest first per key); collapse to one selectable entry per key. const lib = await api.get(`/api/skills/?organizationId=${organizationId}`) const byKey = new Map() for (const s of lib) if (!byKey.has(s.skillKey)) byKey.set(s.skillKey, s) setSkills([...byKey.values()]) // Agent profiles (AGENTS.md): one selectable entry per key (the resolvable winner is first). const profileList = await api.get(`/api/orgboard/agent-profiles?organizationId=${organizationId}`) const byProfileKey = new Map() for (const p of profileList) if (!byProfileKey.has(p.profileKey)) byProfileKey.set(p.profileKey, p) setProfiles([...byProfileKey.values()]) await loadConfigs() await loadMcpServers() }) }, [organizationId, loadConfigs, loadMcpServers, run]) useEffect(() => { if (teamId) void run(() => loadSeats(teamId)) }, [teamId, loadSeats, run]) const createConfig = () => run(async () => { await api.post('/api/integrations/api-configs', { organizationId, ...cfg, endpoint: cfg.endpoint.trim() || null }) setCfg({ name: '', provider: 'stub', model: 'gpt-4o-mini', apiKey: '', endpoint: '' }) await loadConfigs() toast.success('API config saved (key encrypted).') }) const testConfig = (id: string) => run(async () => { const result = await api.post<{ success: boolean; error?: string; latencyMs: number }>( `/api/integrations/api-configs/${id}/test`, ) result.success ? toast.success(`Test call succeeded (${result.latencyMs} ms).`) : toast.error(`Test failed: ${result.error}`) }) const createMcpServer = () => run(async () => { const headers = mcp.headerValue.trim() && mcp.headerName.trim() ? { [mcp.headerName.trim()]: mcp.headerValue.trim() } : null await api.post('/api/integrations/mcp-servers', { organizationId, name: mcp.name.trim(), endpoint: mcp.endpoint.trim(), headers, }) setMcp({ name: '', endpoint: '', headerName: 'Authorization', headerValue: '' }) await loadMcpServers() toast.success('MCP server added (auth header encrypted).') }) const testMcpServer = (id: string) => run(async () => { const result = await api.post<{ success: boolean; error?: string; toolCount: number; toolNames: string[] }>( `/api/integrations/mcp-servers/${id}/test`, ) result.success ? toast.success(`Connected — ${result.toolCount} tool(s): ${result.toolNames.slice(0, 6).join(', ') || 'none'}.`) : toast.error(`MCP test failed: ${result.error}`) }) const deleteMcpServer = (id: string) => run(async () => { await api.del(`/api/integrations/mcp-servers/${id}`) await loadMcpServers() }) const toggleMcp = (id: string) => setAgent((a) => ({ ...a, mcpServerIds: a.mcpServerIds.includes(id) ? a.mcpServerIds.filter((x) => x !== id) : [...a.mcpServerIds, id], })) const createSeat = () => run(async () => { if (!teamId) return await api.post('/api/orgboard/seats', { teamId, roleName: newSeat }) setNewSeat('') await loadSeats(teamId) }) const selectSeat = (seat: Seat) => run(async () => { setSelectedSeat(seat.id) const existing = seat.agentId ? await api.get(`/api/orgboard/seats/${seat.id}/agent`).catch(() => null) : null setAgent( existing ? { name: existing.name, monogram: existing.monogram ?? '', autonomy: existing.autonomy, apiConfigId: existing.apiConfigId, skillKeys: existing.skillKeys, mcpServerIds: existing.mcpServerIds ?? [], docs: existing.docs.join(', '), persona: existing.persona ?? '', } : { name: '', monogram: '', autonomy: 'Gated', apiConfigId: configs[0]?.id ?? '', skillKeys: [], mcpServerIds: [], docs: '', persona: '' }, ) }) // Apply an AGENTS.md profile to the seat: prefill identity, autonomy, skills, and the persona // (operating guide). The user can still tweak everything before saving. const applyProfile = (key: string) => run(async () => { const versions = await api.get(`/api/orgboard/agent-profiles/${key}?organizationId=${organizationId}`) const chosen = versions[0] if (!chosen) return const known = new Set(skills.map((s) => s.skillKey)) setAgent((a) => ({ ...a, name: chosen.profile.name, monogram: chosen.profile.monogram ?? '', autonomy: chosen.profile.recommendedAutonomy, skillKeys: chosen.profile.skillKeys.filter((k) => known.has(k)), persona: chosen.body, })) toast.success(`Applied “${chosen.profile.name}”. Review and save.`) }) const saveAgent = () => run(async () => { if (!selectedSeat) return await api.post(`/api/orgboard/seats/${selectedSeat}/agent`, { name: agent.name, monogram: agent.monogram || null, autonomy: agent.autonomy, apiConfigId: agent.apiConfigId, skillKeys: agent.skillKeys, mcpServerIds: agent.mcpServerIds, docs: agent.docs ? agent.docs.split(',').map((d) => d.trim()).filter(Boolean) : [], persona: agent.persona.trim() || null, }) if (teamId) await loadSeats(teamId) toast.success(`${agent.name || 'Agent'} configured — seat is now AI.`) }) const toggleSkill = (key: string) => setAgent((a) => ({ ...a, skillKeys: a.skillKeys.includes(key) ? a.skillKeys.filter((k) => k !== key) : [...a.skillKeys, key], })) const selected = useMemo(() => seats.find((s) => s.id === selectedSeat) ?? null, [seats, selectedSeat]) return (

AI seats

Connect a model (BYOK) and staff a seat with an AI agent.

Model connections (BYOK) Keys are encrypted server-side and never shown again after saving.
setCfg({ ...cfg, name: e.target.value })} className="w-40" placeholder="Vertex-Pro" /> setCfg({ ...cfg, model: e.target.value })} className="w-40" /> setCfg({ ...cfg, apiKey: e.target.value })} className="w-44" placeholder="sk-…" /> setCfg({ ...cfg, endpoint: e.target.value })} className="w-72" placeholder="https://my-gateway.example.com" />
{configs.map((c) => (
{c.name} {c.provider} · {c.model} {c.endpoint ? ` · ${c.endpoint}` : ''}
))} {configs.length === 0 &&

No model connections yet.

}
MCP servers Connect Model Context Protocol servers (Streamable HTTP). Auth headers are encrypted and never shown again. Bind servers to an agent below — their tools are offered to the agent at run time.
setMcp({ ...mcp, name: e.target.value })} className="w-40" placeholder="GitHub MCP" /> setMcp({ ...mcp, endpoint: e.target.value })} className="w-72" placeholder="https://host/mcp" /> setMcp({ ...mcp, headerName: e.target.value })} className="w-36" placeholder="Authorization" /> setMcp({ ...mcp, headerValue: e.target.value })} className="w-48" placeholder="Bearer …" />
{mcpServers.map((s) => (
{s.name} {s.endpoint}{s.headerNames.length > 0 ? ` · auth: ${s.headerNames.join(', ')}` : ''}
))} {mcpServers.length === 0 &&

No MCP servers yet.

}
Team {teamId && (
setNewSeat(e.target.value)} className="w-48" placeholder="Product Owner" />
)}
{teamId && (
Seats Pick a seat to configure its agent. {seats.map((seat) => ( ))} {seats.length === 0 &&

No seats yet.

}
Agent {selected ? `Configure “${selected.roleName}”` : 'Select a seat on the left.'} {selected && ( {profiles.length > 0 && ( )}
setAgent({ ...agent, name: e.target.value })} className="w-40" placeholder="Aria" /> setAgent({ ...agent, monogram: e.target.value })} className="w-20" placeholder="AR" />
{AUTONOMY.map((a) => ( ))}
{selected && ( setAgent({ ...agent, skillKeys: keys })} /> )}
{skills.map((skill) => ( ))} {skills.length === 0 &&

No skills indexed yet.

}
{mcpServers.map((s) => ( ))} {mcpServers.length === 0 &&

No MCP servers connected — add one above.

}
setAgent({ ...agent, docs: e.target.value })} placeholder="product-docs, house-style" />