import { useState, type HTMLAttributes, type ReactNode } from 'react'; import { ChevronLeft, ChevronRight, Code, Cpu, Info, Plus, Server, Workflow } from 'lucide-react'; import { CopilotStatusCard } from '@renderer/components/CopilotStatusCard'; import { PatternEditor } from '@renderer/components/PatternEditor'; import type { SidecarCapabilities } from '@shared/contracts/sidecar'; import type { ModelDefinition } from '@shared/domain/models'; import type { PatternDefinition } from '@shared/domain/pattern'; import { normalizeLspProfileDefinition, normalizeMcpServerDefinition, type LspProfileDefinition, type McpServerDefinition, type WorkspaceToolingSettings, validateLspProfileDefinition, validateMcpServerDefinition, } from '@shared/domain/tooling'; import { nowIso } from '@shared/utils/ids'; interface SettingsPanelProps { availableModels: ReadonlyArray; patterns: PatternDefinition[]; sidecarCapabilities?: SidecarCapabilities; toolingSettings: WorkspaceToolingSettings; isRefreshingCapabilities: boolean; onRefreshCapabilities: () => void; onClose: () => void; onSavePattern: (pattern: PatternDefinition) => Promise; onDeletePattern: (patternId: string) => Promise; onNewPattern: () => PatternDefinition; onSaveMcpServer: (server: McpServerDefinition) => Promise; onDeleteMcpServer: (serverId: string) => Promise; onNewMcpServer: () => McpServerDefinition; onSaveLspProfile: (profile: LspProfileDefinition) => Promise; onDeleteLspProfile: (profileId: string) => Promise; onNewLspProfile: () => LspProfileDefinition; } type SettingsSection = 'connection' | 'patterns' | 'mcp-servers' | 'lsp-profiles'; interface NavItem { id: SettingsSection; label: string; icon: ReactNode; } interface NavGroup { label: string; items: NavItem[]; } const navGroups: NavGroup[] = [ { label: 'AI Provider', items: [ { id: 'connection', label: 'Connection', icon: }, ], }, { label: 'Orchestration', items: [ { id: 'patterns', label: 'Patterns', icon: }, ], }, { label: 'Tooling', items: [ { id: 'mcp-servers', label: 'MCP Servers', icon: }, { id: 'lsp-profiles', label: 'LSP Profiles', icon: }, ], }, ]; function modeBadgeClasses(pattern: PatternDefinition) { if (pattern.availability === 'unavailable') return 'bg-amber-500/10 text-amber-400'; return 'bg-zinc-800 text-zinc-400'; } export function SettingsPanel({ availableModels, patterns, sidecarCapabilities, toolingSettings, isRefreshingCapabilities, onRefreshCapabilities, onClose, onSavePattern, onDeletePattern, onNewPattern, onSaveMcpServer, onDeleteMcpServer, onNewMcpServer, onSaveLspProfile, onDeleteLspProfile, onNewLspProfile, }: SettingsPanelProps) { const [activeSection, setActiveSection] = useState('connection'); const [editingPattern, setEditingPattern] = useState(null); const [editingMcpServer, setEditingMcpServer] = useState(null); const [editingLspProfile, setEditingLspProfile] = useState(null); if (editingPattern) { const isBuiltin = editingPattern.id.startsWith('pattern-'); return (
setEditingPattern(null)} onChange={setEditingPattern} onDelete={ isBuiltin ? undefined : async () => { await onDeletePattern(editingPattern.id); setEditingPattern(null); } } onSave={async () => { await onSavePattern(editingPattern); setEditingPattern(null); }} pattern={editingPattern} />
); } if (editingMcpServer) { const exists = toolingSettings.mcpServers.some((server) => server.id === editingMcpServer.id); return (
setEditingMcpServer(null)} onChange={setEditingMcpServer} onDelete={ exists ? async () => { await onDeleteMcpServer(editingMcpServer.id); setEditingMcpServer(null); } : undefined } onSave={async () => { await onSaveMcpServer(normalizeMcpServerDefinition(editingMcpServer)); setEditingMcpServer(null); }} server={editingMcpServer} />
); } if (editingLspProfile) { const exists = toolingSettings.lspProfiles.some((profile) => profile.id === editingLspProfile.id); return (
setEditingLspProfile(null)} onChange={setEditingLspProfile} onDelete={ exists ? async () => { await onDeleteLspProfile(editingLspProfile.id); setEditingLspProfile(null); } : undefined } onSave={async () => { await onSaveLspProfile(normalizeLspProfileDefinition(editingLspProfile)); setEditingLspProfile(null); }} profile={editingLspProfile} />
); } return (

Settings

{activeSection === 'connection' && ( )} {activeSection === 'patterns' && ( setEditingPattern(structuredClone(pattern))} onNewPattern={() => setEditingPattern(onNewPattern())} patterns={patterns} /> )} {activeSection === 'mcp-servers' && ( setEditingMcpServer(structuredClone(server))} onNewServer={() => setEditingMcpServer(onNewMcpServer())} servers={toolingSettings.mcpServers} /> )} {activeSection === 'lsp-profiles' && ( setEditingLspProfile(structuredClone(profile))} onNewProfile={() => setEditingLspProfile(onNewLspProfile())} profiles={toolingSettings.lspProfiles} /> )}
); } function ConnectionSection({ connection, modelCount, isRefreshing, onRefresh, }: { connection?: SidecarCapabilities['connection']; modelCount: number; isRefreshing: boolean; onRefresh: () => void; }) { return (

GitHub Copilot

Eryx uses your installed GitHub Copilot CLI for AI capabilities

); } function PatternsSection({ patterns, onEditPattern, onNewPattern, }: { patterns: PatternDefinition[]; onEditPattern: (pattern: PatternDefinition) => void; onNewPattern: () => void; }) { return (
{patterns.map((pattern) => ( ))}
); } function McpServersSection({ servers, onEditServer, onNewServer, }: { servers: McpServerDefinition[]; onEditServer: (server: McpServerDefinition) => void; onNewServer: () => void; }) { return (
{servers.length === 0 && ( No MCP servers configured yet. Add one here, then enable it per session from the Activity panel. )} {servers.map((server) => ( onEditServer(server)} /> ))}
); } function LspProfilesSection({ profiles, onEditProfile, onNewProfile, }: { profiles: LspProfileDefinition[]; onEditProfile: (profile: LspProfileDefinition) => void; onNewProfile: () => void; }) { return (
{profiles.length === 0 && ( No LSP profiles configured yet. Add one here, then enable it per session from the Activity panel. )} {profiles.map((profile) => ( onEditProfile(profile)} /> ))}
); } function McpServerEditor({ server, onChange, onBack, onSave, onDelete, }: { server: McpServerDefinition; onChange: (server: McpServerDefinition) => void; onBack: () => void; onSave: () => Promise; onDelete?: () => Promise; }) { const validationError = validateMcpServerDefinition(server); return (
onChange(updateMcpServer(server, { name: value }))} value={server.name} /> onChange(changeMcpTransport(server, value as McpServerDefinition['transport']))} options={[ { value: 'local', label: 'Local process' }, { value: 'http', label: 'HTTP' }, { value: 'sse', label: 'SSE' }, ]} value={server.transport} />
{server.transport === 'local' ? (
onChange(updateMcpServer(server, { command: value }))} placeholder="node" value={server.command} /> onChange(updateMcpServer(server, { args: splitMultiline(value) }))} placeholder="One argument per line" rows={4} value={joinMultiline(server.args)} /> onChange(updateMcpServer(server, { cwd: value || undefined }))} placeholder="Optional" value={server.cwd ?? ''} />
) : ( onChange(updateMcpServer(server, { url: value }))} placeholder="https://example.com/mcp" value={server.url} /> )}
onChange(updateMcpServer(server, { tools: splitTokens(value) }))} placeholder="Use * for all tools, or list one tool per line" rows={4} value={joinMultiline(server.tools)} /> onChange( updateMcpServer(server, { timeoutMs: value.trim() ? Number(value) : undefined, }), ) } placeholder="Optional" value={server.timeoutMs?.toString() ?? ''} />
Keep secrets out of this form. Use commands or endpoints that authenticate through the OS or external tooling.
); } function LspProfileEditor({ profile, onChange, onBack, onSave, onDelete, }: { profile: LspProfileDefinition; onChange: (profile: LspProfileDefinition) => void; onBack: () => void; onSave: () => Promise; onDelete?: () => Promise; }) { const validationError = validateLspProfileDefinition(profile); return (
onChange(updateLspProfile(profile, { name: value }))} value={profile.name} /> onChange(updateLspProfile(profile, { languageId: value }))} placeholder="typescript" value={profile.languageId} />
onChange(updateLspProfile(profile, { command: value }))} placeholder="typescript-language-server" value={profile.command} />
onChange(updateLspProfile(profile, { args: splitMultiline(value) }))} placeholder="One argument per line" rows={4} value={joinMultiline(profile.args)} /> onChange(updateLspProfile(profile, { fileExtensions: splitTokens(value) }))} placeholder={'.ts\n.tsx'} rows={4} value={joinMultiline(profile.fileExtensions)} />
Profiles are global definitions only. Project root resolution still comes from the active session's project.
); } function SectionHeader({ title, description, children, }: { title: string; description: string; children?: ReactNode; }) { return (

{title}

{description}

{children}
); } function SectionAction({ label, onClick }: { label: string; onClick: () => void }) { return ( ); } function ToolingListButton({ label, detail, meta, onClick, }: { label: string; detail: string; meta: string; onClick: () => void; }) { return ( ); } function EmptyState({ children }: { children: ReactNode }) { return (
{children}
); } function ToolingEditorShell({ title, description, error, disableSave, onBack, onSave, onDelete, children, }: { title: string; description: string; error?: string; disableSave: boolean; onBack: () => void; onSave: () => Promise; onDelete?: () => Promise; children: ReactNode; }) { return (

{title}

{description}

{onDelete && ( )}
{error && (
{error}
)} {children}
); } function FormField({ label, required, className, children, }: { label: string; required?: boolean; className?: string; children: ReactNode; }) { return ( ); } function TextInput({ value, onChange, placeholder, inputMode, }: { value: string; onChange: (value: string) => void; placeholder?: string; inputMode?: HTMLAttributes['inputMode']; }) { return ( onChange(event.target.value)} placeholder={placeholder} value={value} /> ); } function TextareaInput({ value, onChange, placeholder, rows, }: { value: string; onChange: (value: string) => void; placeholder?: string; rows: number; }) { return (