Files
openagent/dashboard/src/app/settings/page.tsx
Thomas Marchand 36fafe193e feat: implement Claude Code backend and harness-aware UI
- Implement ClaudeCodeClient with subprocess JSON streaming to Claude CLI
- Implement ClaudeCodeBackend with Backend trait for mission execution
- Update mission runner to support both OpenCode and Claude Code backends
- Add harness tabs to Library Configs page (OpenCode/Claude Code)
- Add CLI path configuration for Claude Code in Settings
- Add comprehensive harness system documentation
2026-01-18 14:58:04 +00:00

954 lines
38 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect, useCallback } from 'react';
import useSWR from 'swr';
import { toast } from '@/components/toast';
import {
getHealth,
HealthResponse,
listAIProviders,
listAIProviderTypes,
updateAIProvider,
deleteAIProvider,
authenticateAIProvider,
setDefaultAIProvider,
AIProvider,
AIProviderTypeInfo,
getSettings,
updateLibraryRemote,
listBackends,
getBackendConfig,
updateBackendConfig,
} from '@/lib/api';
import {
Server,
Save,
RefreshCw,
AlertTriangle,
GitBranch,
Cpu,
Plus,
Trash2,
Star,
ExternalLink,
Loader,
Key,
Check,
X,
} from 'lucide-react';
import { readSavedSettings, writeSavedSettings } from '@/lib/settings';
import { cn } from '@/lib/utils';
import { AddProviderModal } from '@/components/ui/add-provider-modal';
import { ServerConnectionCard } from '@/components/server-connection-card';
// Provider icons/colors mapping
const providerConfig: Record<string, { color: string; icon: string }> = {
anthropic: { color: 'bg-orange-500/10 text-orange-400', icon: '🧠' },
openai: { color: 'bg-emerald-500/10 text-emerald-400', icon: '🤖' },
google: { color: 'bg-blue-500/10 text-blue-400', icon: '🔮' },
'amazon-bedrock': { color: 'bg-amber-500/10 text-amber-400', icon: '☁️' },
azure: { color: 'bg-sky-500/10 text-sky-400', icon: '⚡' },
'open-router': { color: 'bg-purple-500/10 text-purple-400', icon: '🔀' },
mistral: { color: 'bg-indigo-500/10 text-indigo-400', icon: '🌪️' },
groq: { color: 'bg-pink-500/10 text-pink-400', icon: '⚡' },
xai: { color: 'bg-slate-500/10 text-slate-400', icon: '𝕏' },
'github-copilot': { color: 'bg-gray-500/10 text-gray-400', icon: '🐙' },
custom: { color: 'bg-white/10 text-white/60', icon: '🔧' },
};
function getProviderConfig(type: string) {
return providerConfig[type] || providerConfig.custom;
}
// Default provider types fallback
const defaultProviderTypes: AIProviderTypeInfo[] = [
{ id: 'anthropic', name: 'Anthropic', uses_oauth: true, env_var: 'ANTHROPIC_API_KEY' },
{ id: 'openai', name: 'OpenAI', uses_oauth: true, env_var: 'OPENAI_API_KEY' },
{ id: 'google', name: 'Google AI', uses_oauth: true, env_var: 'GOOGLE_API_KEY' },
{ id: 'open-router', name: 'OpenRouter', uses_oauth: false, env_var: 'OPENROUTER_API_KEY' },
{ id: 'groq', name: 'Groq', uses_oauth: false, env_var: 'GROQ_API_KEY' },
{ id: 'mistral', name: 'Mistral AI', uses_oauth: false, env_var: 'MISTRAL_API_KEY' },
{ id: 'xai', name: 'xAI', uses_oauth: false, env_var: 'XAI_API_KEY' },
{ id: 'github-copilot', name: 'GitHub Copilot', uses_oauth: true, env_var: null },
];
export default function SettingsPage() {
const [testingConnection, setTestingConnection] = useState(false);
// Form state
const [apiUrl, setApiUrl] = useState(
() => readSavedSettings().apiUrl ?? 'http://127.0.0.1:3000'
);
// Track original values for unsaved changes
const [originalValues, setOriginalValues] = useState({
apiUrl: readSavedSettings().apiUrl ?? 'http://127.0.0.1:3000',
});
// Validation state
const [urlError, setUrlError] = useState<string | null>(null);
// Modal/edit state
const [showAddModal, setShowAddModal] = useState(false);
const [authenticatingProviderId, setAuthenticatingProviderId] = useState<string | null>(null);
const [editingProvider, setEditingProvider] = useState<string | null>(null);
const [editForm, setEditForm] = useState<{
name?: string;
api_key?: string;
base_url?: string;
enabled?: boolean;
}>({});
// Library remote edit state
const [editingLibraryRemote, setEditingLibraryRemote] = useState(false);
const [libraryRemoteValue, setLibraryRemoteValue] = useState('');
const [savingLibraryRemote, setSavingLibraryRemote] = useState(false);
// Backend settings state
const [activeBackendTab, setActiveBackendTab] = useState<'opencode' | 'claudecode'>('opencode');
const [savingBackend, setSavingBackend] = useState(false);
const [opencodeForm, setOpencodeForm] = useState({
base_url: '',
default_agent: '',
permissive: false,
enabled: true,
});
const [claudeForm, setClaudeForm] = useState({
api_key: '',
default_model: '',
cli_path: '',
api_key_configured: false,
enabled: true,
});
// SWR: fetch health status
const { data: health, isLoading: healthLoading, mutate: mutateHealth } = useSWR(
'health',
getHealth,
{ revalidateOnFocus: false }
);
// SWR: fetch AI providers
const { data: providers = [], isLoading: providersLoading, mutate: mutateProviders } = useSWR(
'ai-providers',
listAIProviders,
{ revalidateOnFocus: false }
);
// SWR: fetch provider types (with fallback)
const { data: providerTypes = defaultProviderTypes } = useSWR(
'ai-provider-types',
listAIProviderTypes,
{ revalidateOnFocus: false, fallbackData: defaultProviderTypes }
);
// SWR: fetch server settings
const { data: serverSettings, mutate: mutateSettings } = useSWR(
'settings',
getSettings,
{ revalidateOnFocus: false }
);
// SWR: fetch backends
const { data: backends = [] } = useSWR('backends', listBackends, {
revalidateOnFocus: false,
fallbackData: [
{ id: 'opencode', name: 'OpenCode' },
{ id: 'claudecode', name: 'Claude Code' },
],
});
const { data: opencodeBackendConfig, mutate: mutateOpenCodeBackend } = useSWR(
'backend-opencode-config',
() => getBackendConfig('opencode'),
{ revalidateOnFocus: false }
);
const { data: claudecodeBackendConfig, mutate: mutateClaudeBackend } = useSWR(
'backend-claudecode-config',
() => getBackendConfig('claudecode'),
{ revalidateOnFocus: false }
);
// Check if there are unsaved changes
const hasUnsavedChanges = apiUrl !== originalValues.apiUrl;
// Validate URL
const validateUrl = useCallback((url: string) => {
if (!url.trim()) {
setUrlError('API URL is required');
return false;
}
try {
new URL(url);
setUrlError(null);
return true;
} catch {
setUrlError('Invalid URL format');
return false;
}
}, []);
// Unsaved changes warning
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (hasUnsavedChanges) {
e.preventDefault();
e.returnValue = '';
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [hasUnsavedChanges]);
// Keyboard shortcut to save (Ctrl/Cmd + S)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
e.preventDefault();
handleSave();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [apiUrl]);
useEffect(() => {
if (!opencodeBackendConfig?.settings) return;
const settings = opencodeBackendConfig.settings as Record<string, unknown>;
setOpencodeForm({
base_url: typeof settings.base_url === 'string' ? settings.base_url : '',
default_agent: typeof settings.default_agent === 'string' ? settings.default_agent : '',
permissive: Boolean(settings.permissive),
enabled: opencodeBackendConfig.enabled,
});
}, [opencodeBackendConfig]);
useEffect(() => {
if (!claudecodeBackendConfig?.settings) return;
const settings = claudecodeBackendConfig.settings as Record<string, unknown>;
setClaudeForm((prev) => ({
...prev,
default_model: typeof settings.default_model === 'string' ? settings.default_model : '',
cli_path: typeof settings.cli_path === 'string' ? settings.cli_path : '',
api_key_configured: Boolean(settings.api_key_configured),
enabled: claudecodeBackendConfig.enabled,
}));
}, [claudecodeBackendConfig]);
const handleSave = () => {
if (!validateUrl(apiUrl)) {
toast.error('Please fix validation errors before saving');
return;
}
writeSavedSettings({ apiUrl });
setOriginalValues({ apiUrl });
toast.success('Settings saved!');
};
const testApiConnection = async () => {
if (!validateUrl(apiUrl)) {
toast.error('Please enter a valid API URL');
return;
}
setTestingConnection(true);
try {
const response = await fetch(`${apiUrl}/api/health`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
mutateHealth(data, false); // Update cache without revalidation
toast.success(`Connected to OpenAgent v${data.version}`);
} catch (err) {
mutateHealth(undefined, false); // Clear cache on error
toast.error(
`Connection failed: ${err instanceof Error ? err.message : 'Unknown error'}`
);
} finally {
setTestingConnection(false);
}
};
const handleAuthenticate = async (provider: AIProvider) => {
setAuthenticatingProviderId(provider.id);
try {
const result = await authenticateAIProvider(provider.id);
if (result.success) {
toast.success(result.message);
mutateProviders();
} else {
if (result.auth_url) {
window.open(result.auth_url, '_blank');
toast.info(result.message);
} else {
toast.error(result.message);
}
}
} catch (err) {
toast.error(
`Authentication failed: ${err instanceof Error ? err.message : 'Unknown error'}`
);
} finally {
setAuthenticatingProviderId(null);
}
};
const handleSetDefault = async (id: string) => {
try {
await setDefaultAIProvider(id);
toast.success('Default provider updated');
mutateProviders();
} catch (err) {
toast.error(
`Failed to set default: ${err instanceof Error ? err.message : 'Unknown error'}`
);
}
};
const handleDeleteProvider = async (id: string) => {
try {
await deleteAIProvider(id);
toast.success('Provider removed');
mutateProviders();
} catch (err) {
toast.error(
`Failed to delete: ${err instanceof Error ? err.message : 'Unknown error'}`
);
}
};
const handleSaveOpenCodeBackend = async () => {
setSavingBackend(true);
try {
const result = await updateBackendConfig(
'opencode',
{
base_url: opencodeForm.base_url,
default_agent: opencodeForm.default_agent || null,
permissive: opencodeForm.permissive,
},
{ enabled: opencodeForm.enabled }
);
toast.success(result.message || 'OpenCode settings updated');
mutateOpenCodeBackend();
} catch (err) {
toast.error(
`Failed to update OpenCode settings: ${
err instanceof Error ? err.message : 'Unknown error'
}`
);
} finally {
setSavingBackend(false);
}
};
const handleSaveClaudeBackend = async () => {
setSavingBackend(true);
try {
const settings: Record<string, unknown> = {
default_model: claudeForm.default_model || null,
cli_path: claudeForm.cli_path || null,
};
if (claudeForm.api_key) {
settings.api_key = claudeForm.api_key;
}
const result = await updateBackendConfig('claudecode', settings, {
enabled: claudeForm.enabled,
});
toast.success(result.message || 'Claude Code settings updated');
setClaudeForm((prev) => ({ ...prev, api_key: '' }));
mutateClaudeBackend();
} catch (err) {
toast.error(
`Failed to update Claude Code settings: ${
err instanceof Error ? err.message : 'Unknown error'
}`
);
} finally {
setSavingBackend(false);
}
};
const handleStartEdit = (provider: AIProvider) => {
setEditingProvider(provider.id);
setEditForm({
name: provider.name,
api_key: '',
base_url: provider.base_url || '',
enabled: provider.enabled,
});
};
const handleSaveEdit = async () => {
if (!editingProvider) return;
try {
await updateAIProvider(editingProvider, {
name: editForm.name,
api_key: editForm.api_key || undefined,
base_url: editForm.base_url || undefined,
enabled: editForm.enabled,
});
toast.success('Provider updated');
setEditingProvider(null);
mutateProviders();
} catch (err) {
toast.error(
`Failed to update: ${err instanceof Error ? err.message : 'Unknown error'}`
);
}
};
const handleCancelEdit = () => {
setEditingProvider(null);
setEditForm({});
};
// Library remote handlers
const handleStartEditLibraryRemote = () => {
setLibraryRemoteValue(serverSettings?.library_remote || '');
setEditingLibraryRemote(true);
};
const handleCancelEditLibraryRemote = () => {
setEditingLibraryRemote(false);
setLibraryRemoteValue('');
};
const handleSaveLibraryRemote = async () => {
setSavingLibraryRemote(true);
try {
const trimmed = libraryRemoteValue.trim();
const result = await updateLibraryRemote(trimmed || null);
// Revalidate both settings and health (which also exposes library_remote)
mutateSettings();
mutateHealth();
setEditingLibraryRemote(false);
if (result.library_reinitialized) {
if (result.library_error) {
toast.error(`Library saved but failed to initialize: ${result.library_error}`);
} else if (result.library_remote) {
toast.success('Library remote updated and reinitialized');
} else {
toast.success('Library remote cleared');
}
} else {
toast.success('Library remote saved (no change)');
}
} catch (err) {
toast.error(
`Failed to save: ${err instanceof Error ? err.message : 'Unknown error'}`
);
} finally {
setSavingLibraryRemote(false);
}
};
return (
<div className="min-h-screen flex flex-col items-center p-6">
{/* Add Provider Modal */}
<AddProviderModal
open={showAddModal}
onClose={() => setShowAddModal(false)}
onSuccess={() => mutateProviders()}
providerTypes={providerTypes}
/>
{/* Centered content container */}
<div className="w-full max-w-xl">
{/* Header */}
<div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-xl font-semibold text-white">Settings</h1>
<p className="mt-1 text-sm text-white/50">
Configure your server connection and AI providers
</p>
</div>
<div className="flex items-center gap-3">
{hasUnsavedChanges && (
<div className="flex items-center gap-2 text-amber-400 text-xs">
<AlertTriangle className="h-3.5 w-3.5" />
<span>Unsaved</span>
</div>
)}
<button
onClick={handleSave}
disabled={!!urlError}
className={cn(
'flex items-center gap-2 rounded-lg px-3 py-1.5 text-sm font-medium text-white transition-colors cursor-pointer',
urlError
? 'bg-white/10 cursor-not-allowed opacity-50'
: 'bg-indigo-500 hover:bg-indigo-600'
)}
>
<Save className="h-4 w-4" />
Save
<span className="text-xs text-white/40">S</span>
</button>
</div>
</div>
<div className="space-y-5">
{/* Server Connection & System Components */}
<ServerConnectionCard
apiUrl={apiUrl}
setApiUrl={setApiUrl}
urlError={urlError}
validateUrl={validateUrl}
health={health ?? null}
healthLoading={healthLoading}
testingConnection={testingConnection}
testApiConnection={testApiConnection}
/>
{/* AI Providers */}
<div className="rounded-xl bg-white/[0.02] border border-white/[0.04] p-5">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-violet-500/10">
<Cpu className="h-5 w-5 text-violet-400" />
</div>
<div>
<h2 className="text-sm font-medium text-white">AI Providers</h2>
<p className="text-xs text-white/40">
Configure inference providers for OpenCode
</p>
</div>
</div>
<button
onClick={() => setShowAddModal(true)}
className="flex items-center gap-1.5 rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-1.5 text-xs text-white/70 hover:bg-white/[0.04] transition-colors cursor-pointer"
>
<Plus className="h-3 w-3" />
Add Provider
</button>
</div>
{/* Provider List */}
<div className="space-y-2">
{providersLoading ? (
<div className="flex items-center justify-center py-8">
<Loader className="h-5 w-5 animate-spin text-white/40" />
</div>
) : providers.length === 0 ? (
<div className="text-center py-8">
<div className="flex justify-center mb-3">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-white/[0.04]">
<Cpu className="h-6 w-6 text-white/30" />
</div>
</div>
<p className="text-sm text-white/50 mb-1">No providers configured</p>
<p className="text-xs text-white/30">
Add an AI provider to enable inference capabilities
</p>
</div>
) : (
providers.map((provider) => {
const config = getProviderConfig(provider.provider_type);
const statusColor = provider.status.type === 'connected'
? 'bg-emerald-400'
: provider.status.type === 'needs_auth'
? 'bg-amber-400'
: 'bg-red-400';
return (
<div
key={provider.id}
className="group rounded-lg border border-white/[0.06] bg-white/[0.01] hover:bg-white/[0.02] transition-colors"
>
{editingProvider === provider.id ? (
// Edit mode
<div className="p-3 space-y-3">
<input
type="text"
value={editForm.name ?? ''}
onChange={(e) =>
setEditForm({ ...editForm, name: e.target.value })
}
placeholder="Name"
className="w-full rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-sm text-white focus:outline-none focus:border-indigo-500/50"
/>
<input
type="password"
value={editForm.api_key ?? ''}
onChange={(e) =>
setEditForm({ ...editForm, api_key: e.target.value })
}
placeholder="New API key (leave empty to keep)"
className="w-full rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-sm text-white focus:outline-none focus:border-indigo-500/50"
/>
<div className="flex items-center justify-between pt-1">
<label className="flex items-center gap-2 text-xs text-white/60 cursor-pointer">
<input
type="checkbox"
checked={editForm.enabled ?? true}
onChange={(e) =>
setEditForm({ ...editForm, enabled: e.target.checked })
}
className="rounded border-white/20 cursor-pointer"
/>
Enabled
</label>
<div className="flex items-center gap-2">
<button
onClick={handleCancelEdit}
className="rounded-lg px-3 py-1.5 text-xs text-white/60 hover:text-white/80 transition-colors cursor-pointer"
>
Cancel
</button>
<button
onClick={handleSaveEdit}
className="rounded-lg bg-indigo-500 px-3 py-1.5 text-xs text-white hover:bg-indigo-600 transition-colors cursor-pointer"
>
Save
</button>
</div>
</div>
</div>
) : (
// View mode - minimal single row
<div className={cn(
'flex items-center gap-3 px-3 py-2.5',
!provider.enabled && 'opacity-40'
)}>
{/* Icon + Name */}
<span className="text-base">{config.icon}</span>
<span className="text-sm text-white/80 flex-1 truncate">{provider.name}</span>
{/* Status indicators */}
<div className="flex items-center gap-2">
{provider.is_default && (
<Star className="h-3 w-3 text-indigo-400 fill-indigo-400" />
)}
<span className={cn('h-1.5 w-1.5 rounded-full', statusColor)} />
</div>
{/* Actions on hover */}
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
{provider.status.type === 'needs_auth' && (
<button
onClick={() => handleAuthenticate(provider)}
disabled={authenticatingProviderId === provider.id}
className="p-1.5 rounded-md text-amber-400 hover:bg-white/[0.04] transition-colors cursor-pointer disabled:opacity-50"
title="Connect"
>
{authenticatingProviderId === provider.id ? (
<Loader className="h-3.5 w-3.5 animate-spin" />
) : (
<ExternalLink className="h-3.5 w-3.5" />
)}
</button>
)}
{!provider.is_default && provider.enabled && (
<button
onClick={() => handleSetDefault(provider.id)}
className="p-1.5 rounded-md text-white/30 hover:text-white/60 hover:bg-white/[0.04] transition-colors cursor-pointer"
title="Set as default"
>
<Star className="h-3.5 w-3.5" />
</button>
)}
<button
onClick={() => handleStartEdit(provider)}
className="p-1.5 rounded-md text-white/30 hover:text-white/60 hover:bg-white/[0.04] transition-colors cursor-pointer"
title="Edit"
>
<Key className="h-3.5 w-3.5" />
</button>
<button
onClick={() => handleDeleteProvider(provider.id)}
className="p-1.5 rounded-md text-white/30 hover:text-red-400 hover:bg-white/[0.04] transition-colors cursor-pointer"
title="Delete"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
</div>
)}
</div>
);
})
)}
</div>
</div>
{/* Backends */}
<div className="rounded-xl bg-white/[0.02] border border-white/[0.04] p-5">
<div className="flex items-center gap-3 mb-4">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-500/10">
<Server className="h-5 w-5 text-emerald-400" />
</div>
<div>
<h2 className="text-sm font-medium text-white">Backends</h2>
<p className="text-xs text-white/40">
Configure execution backends and authentication
</p>
</div>
</div>
<div className="flex items-center gap-2 mb-4">
{backends.map((backend) => (
<button
key={backend.id}
onClick={() =>
setActiveBackendTab(
backend.id === 'claudecode' ? 'claudecode' : 'opencode'
)
}
className={cn(
'px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors',
activeBackendTab === backend.id
? 'bg-white/[0.08] border-white/[0.12] text-white'
: 'bg-white/[0.02] border-white/[0.06] text-white/50 hover:text-white/70'
)}
>
{backend.name}
</button>
))}
</div>
{activeBackendTab === 'opencode' ? (
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs text-white/60">Enabled</span>
<label className="flex items-center gap-2 text-xs text-white/60 cursor-pointer">
<input
type="checkbox"
checked={opencodeForm.enabled}
onChange={(e) =>
setOpencodeForm((prev) => ({ ...prev, enabled: e.target.checked }))
}
className="rounded border-white/20 cursor-pointer"
/>
Enabled
</label>
</div>
<div>
<label className="block text-xs text-white/60 mb-1.5">Base URL</label>
<input
type="text"
value={opencodeForm.base_url}
onChange={(e) =>
setOpencodeForm((prev) => ({ ...prev, base_url: e.target.value }))
}
placeholder="http://127.0.0.1:4096"
className="w-full rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-sm text-white focus:outline-none focus:border-indigo-500/50"
/>
</div>
<div>
<label className="block text-xs text-white/60 mb-1.5">Default Agent</label>
<input
type="text"
value={opencodeForm.default_agent}
onChange={(e) =>
setOpencodeForm((prev) => ({ ...prev, default_agent: e.target.value }))
}
placeholder="Sisyphus"
className="w-full rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-sm text-white focus:outline-none focus:border-indigo-500/50"
/>
</div>
<label className="flex items-center gap-2 text-xs text-white/60 cursor-pointer">
<input
type="checkbox"
checked={opencodeForm.permissive}
onChange={(e) =>
setOpencodeForm((prev) => ({ ...prev, permissive: e.target.checked }))
}
className="rounded border-white/20 cursor-pointer"
/>
Permissive mode (auto-allow tool permissions)
</label>
<div className="flex items-center gap-2 pt-1">
<button
onClick={handleSaveOpenCodeBackend}
disabled={savingBackend}
className="flex items-center gap-2 rounded-lg bg-indigo-500 px-3 py-1.5 text-xs text-white hover:bg-indigo-600 transition-colors disabled:opacity-50"
>
{savingBackend ? (
<Loader className="h-3.5 w-3.5 animate-spin" />
) : (
<Save className="h-3.5 w-3.5" />
)}
Save OpenCode
</button>
<span className="text-xs text-white/40">Restart required to apply runtime changes</span>
</div>
</div>
) : (
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs text-white/60">Enabled</span>
<label className="flex items-center gap-2 text-xs text-white/60 cursor-pointer">
<input
type="checkbox"
checked={claudeForm.enabled}
onChange={(e) =>
setClaudeForm((prev) => ({ ...prev, enabled: e.target.checked }))
}
className="rounded border-white/20 cursor-pointer"
/>
Enabled
</label>
</div>
<div className="text-xs text-white/50">
API key status:{' '}
<span className={claudeForm.api_key_configured ? 'text-emerald-400' : 'text-amber-400'}>
{claudeForm.api_key_configured ? 'Configured' : 'Not configured'}
</span>
</div>
<div>
<label className="block text-xs text-white/60 mb-1.5">API Key</label>
<input
type="password"
value={claudeForm.api_key}
onChange={(e) =>
setClaudeForm((prev) => ({ ...prev, api_key: e.target.value }))
}
placeholder="sk-..."
className="w-full rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-sm text-white focus:outline-none focus:border-indigo-500/50"
/>
<p className="mt-1.5 text-xs text-white/30">
Stored securely in the secrets vault; leave blank to keep existing key.
</p>
</div>
<div>
<label className="block text-xs text-white/60 mb-1.5">Default Model</label>
<input
type="text"
value={claudeForm.default_model}
onChange={(e) =>
setClaudeForm((prev) => ({ ...prev, default_model: e.target.value }))
}
placeholder="claude-sonnet-4-20250514"
className="w-full rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-sm text-white focus:outline-none focus:border-indigo-500/50"
/>
</div>
<div>
<label className="block text-xs text-white/60 mb-1.5">CLI Path</label>
<input
type="text"
value={claudeForm.cli_path || ''}
onChange={(e) =>
setClaudeForm((prev) => ({ ...prev, cli_path: e.target.value }))
}
placeholder="claude (uses PATH) or /path/to/claude"
className="w-full rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-sm text-white focus:outline-none focus:border-indigo-500/50"
/>
<p className="mt-1.5 text-xs text-white/30">
Path to the Claude CLI executable. Leave blank to use default from PATH.
</p>
</div>
<div className="flex items-center gap-2 pt-1">
<button
onClick={handleSaveClaudeBackend}
disabled={savingBackend}
className="flex items-center gap-2 rounded-lg bg-indigo-500 px-3 py-1.5 text-xs text-white hover:bg-indigo-600 transition-colors disabled:opacity-50"
>
{savingBackend ? (
<Loader className="h-3.5 w-3.5 animate-spin" />
) : (
<Save className="h-3.5 w-3.5" />
)}
Save Claude Code
</button>
<span className="text-xs text-white/40">Restart required to apply runtime changes</span>
</div>
</div>
)}
</div>
{/* Library Settings */}
<div className="rounded-xl bg-white/[0.02] border border-white/[0.04] p-5">
<div className="flex items-center gap-3 mb-4">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-indigo-500/10">
<GitBranch className="h-5 w-5 text-indigo-400" />
</div>
<div>
<h2 className="text-sm font-medium text-white">Library</h2>
<p className="text-xs text-white/40">
Git-based configuration library for skills, tools, and agents
</p>
</div>
</div>
<div>
<label className="block text-xs font-medium text-white/60 mb-1.5">
Library Remote
</label>
{healthLoading ? (
<div className="flex items-center gap-2 py-2.5">
<Loader className="h-4 w-4 animate-spin text-white/40" />
<span className="text-sm text-white/40">Loading...</span>
</div>
) : editingLibraryRemote ? (
<div className="space-y-2">
<input
type="text"
value={libraryRemoteValue}
onChange={(e) => setLibraryRemoteValue(e.target.value)}
placeholder="git@github.com:your-org/agent-library.git"
className="w-full rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-sm text-white font-mono focus:outline-none focus:border-indigo-500/50"
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter') handleSaveLibraryRemote();
if (e.key === 'Escape') handleCancelEditLibraryRemote();
}}
/>
<div className="flex items-center gap-2">
<button
onClick={handleSaveLibraryRemote}
disabled={savingLibraryRemote}
className="flex items-center gap-1.5 rounded-lg bg-indigo-500 px-3 py-1.5 text-xs text-white hover:bg-indigo-600 transition-colors cursor-pointer disabled:opacity-50"
>
{savingLibraryRemote ? (
<Loader className="h-3 w-3 animate-spin" />
) : (
<Check className="h-3 w-3" />
)}
Save
</button>
<button
onClick={handleCancelEditLibraryRemote}
disabled={savingLibraryRemote}
className="flex items-center gap-1.5 rounded-lg border border-white/[0.06] px-3 py-1.5 text-xs text-white/60 hover:bg-white/[0.04] transition-colors cursor-pointer disabled:opacity-50"
>
<X className="h-3 w-3" />
Cancel
</button>
</div>
</div>
) : (
<div
onClick={handleStartEditLibraryRemote}
className={cn(
'w-full rounded-lg border px-3 py-2.5 text-sm font-mono cursor-pointer transition-colors',
serverSettings?.library_remote
? 'border-white/[0.06] bg-white/[0.01] text-white/70 hover:border-indigo-500/30 hover:bg-white/[0.02]'
: 'border-amber-500/20 bg-amber-500/5 text-amber-400/80 hover:border-amber-500/30 hover:bg-amber-500/10'
)}
title="Click to edit"
>
{serverSettings?.library_remote || 'Not configured'}
</div>
)}
<p className="mt-1.5 text-xs text-white/30">
Git remote URL for skills, tools, agents, and rules. Click to edit.
</p>
</div>
</div>
</div>
</div>
</div>
);
}