- {filtered.length === 0 && (
+ {sessions.length === 0 && (
No sessions yet. Start a new chat!
)}
- {filtered.map((session) => {
+ {sessions.map((session) => {
const isSelected = selected?.id === session.id && selected?.provider === session.provider;
return (
{
{session.title}
+
+ ({session.id.slice(0, 8)})
+
{new Date(session.createdAt).toLocaleDateString(undefined, {
@@ -117,21 +91,12 @@ export const SessionList = () => {
hour: '2-digit',
minute: '2-digit',
})}
-
- {session.provider === 'claude' ? 'Claude' : session.provider === 'opencode' ? 'OpenCode' : 'Pi'}
-
-
- {session.id.slice(0, 8)}
-
+ {session.model && (
+
+ {session.model}
+
+ )}
{/* Chat */}
- {provider === 'claude' ? (
-
- ) : provider === 'opencode' ? (
-
- ) : (
-
- )}
+
diff --git a/src/apps/officer-web/Screens/Dashboard/Home/ChatLauncher.tsx b/src/apps/officer-web/Screens/Dashboard/Home/ChatLauncher.tsx
index 430b74a3..652d332b 100644
--- a/src/apps/officer-web/Screens/Dashboard/Home/ChatLauncher.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Home/ChatLauncher.tsx
@@ -1,4 +1,4 @@
-import { useState, useRef, useEffect, type KeyboardEvent } from 'react';
+import { useState, useMemo, useRef, useEffect, type KeyboardEvent } from 'react';
import { useNavigate } from 'react-router';
import {
Send,
@@ -23,18 +23,32 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useSettings } from '@/state/useSettings';
-import { useVisibleClaudeModels, useVisibleOpenCodeModels, useVisiblePiMonoModels } from '@/state/useModels';
+import { useVisiblePiMonoModels } from '@/state/useModels';
import type { Attachment } from '@/Screens/Dashboard/Chat/EmbeddableChat';
+const PROVIDER_DISPLAY: Record
= {
+ anthropic: 'Anthropic',
+ openai: 'OpenAI',
+ opencode: 'OpenCode Zen',
+ google: 'Google',
+ groq: 'Groq',
+ mistral: 'Mistral',
+ xai: 'xAI',
+ openrouter: 'OpenRouter',
+ huggingface: 'Hugging Face',
+ 'github-copilot': 'GitHub Copilot',
+ minimax: 'MiniMax',
+ bedrock: 'Amazon Bedrock',
+ 'google-vertex': 'Google Vertex AI',
+ 'azure-openai': 'Azure OpenAI',
+};
+
export const ChatLauncher = () => {
const navigate = useNavigate();
const { settings } = useSettings();
- const claudeModels = useVisibleClaudeModels();
- const openCodeModels = useVisibleOpenCodeModels();
const piMonoModels = useVisiblePiMonoModels();
const client = useClient();
- const [provider, setProvider] = useState<'claude' | 'opencode' | 'pi-mono'>(settings.chat.defaultProvider);
const [model, setModel] = useState(settings.chat.defaultModel);
const [input, setInput] = useState('');
const [attachments, setAttachments] = useState([]);
@@ -44,11 +58,23 @@ export const ChatLauncher = () => {
const imageInputRef = useRef(null);
useEffect(() => {
- setProvider(settings.chat.defaultProvider);
setModel(settings.chat.defaultModel);
- }, [settings.chat.defaultProvider, settings.chat.defaultModel]);
+ }, [settings.chat.defaultModel]);
- const models = provider === 'claude' ? claudeModels : provider === 'opencode' ? openCodeModels : piMonoModels;
+ const providers = useMemo(
+ () => [...new Set(piMonoModels.map((m) => m.provider).filter(Boolean))] as string[],
+ [piMonoModels],
+ );
+
+ const activeProvider = piMonoModels.find((m) => m.id === model)?.provider ?? providers[0];
+ const providerModels = piMonoModels.filter((m) => m.provider === activeProvider);
+
+ const displayName = (provider: string) => PROVIDER_DISPLAY[provider] ?? provider;
+
+ const handleProviderClick = (provider: string) => {
+ const firstModel = piMonoModels.find((m) => m.provider === provider);
+ if (firstModel) setModel(firstModel.id);
+ };
const handleAttachWebpage = async (url: string) => {
const idx = attachments.length;
@@ -60,7 +86,7 @@ export const ChatLauncher = () => {
try {
const res = await client.post<{ url: string; title: string; content: string; attachmentId: string }>('/scrape', {
url,
- provider,
+ provider: 'pi-mono',
});
setAttachments((prev) =>
prev.map((a, i) =>
@@ -85,7 +111,7 @@ export const ChatLauncher = () => {
try {
const formData = new FormData();
formData.append('file', file);
- formData.append('provider', provider);
+ formData.append('provider', 'pi-mono');
const res = await client.post<{ filename: string; dataUrl: string; attachmentId: string }>('/upload', formData);
setAttachments((prev) =>
@@ -125,9 +151,7 @@ export const ChatLauncher = () => {
attachmentIds.push(a.attachmentId);
}
- const route =
- provider === 'claude' ? '/chat/new' : provider === 'opencode' ? '/chat/opencode/new' : '/chat/pi-mono/new';
- navigate(route, {
+ navigate('/chat/new', {
state: {
initialMessage: prompt,
model,
@@ -259,40 +283,36 @@ export const ChatLauncher = () => {
- {(['claude', 'opencode', 'pi-mono'] as const).map((value) => (
+ {providers.map((provider) => (
{
- setProvider(value);
- setModel(null);
- }}
+ key={provider}
+ onClick={() => handleProviderClick(provider)}
className={`rounded-md px-3 py-1 text-xs font-medium transition-colors cursor-pointer ${
- provider === value
+ activeProvider === provider
? 'bg-background text-duck-dark shadow-sm'
: 'text-duck-dark/50 hover:text-duck-dark/70'
}`}
>
- {value === 'claude' ? 'Claude' : value === 'opencode' ? 'OpenCode' : 'Pi'}
+ {displayName(provider)}
))}
- {models.length > 0 && (
+ {providerModels.length > 0 && (
- {models.find((m) => m.id === (model ?? models[0]?.id))?.name ?? models[0]?.name}
+ {providerModels.find((m) => m.id === (model ?? providerModels[0]?.id))?.name ?? providerModels[0]?.name}
- {models.map((m) => (
+ {providerModels.map((m) => (
setModel(m.id)} className="cursor-pointer">
{m.name}
- {m.provider && ({m.provider})}
))}
diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx
index 68730c60..c099a7b7 100644
--- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx
@@ -1,321 +1,534 @@
import { useState } from 'react';
-import { Copy, Check, Play } from 'lucide-react';
+import { Plus, Save, Trash2, RefreshCw, Loader2, X } from 'lucide-react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
-import { Checkbox } from '@/components/ui/checkbox';
+import { Input } from '@/components/ui/input';
import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from '@/components/ui/alert-dialog';
+ CommandDialog,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from '@/components/ui/command';
import { useClient } from 'hooks/useClient';
-import { usePanelChannel } from 'hooks/usePanelChannel';
-import { useServerSettings } from '@/state/useServerSettings';
-import { RUN_COMMAND_CHANNEL, type RunCommandState } from './run-command-channel';
type VersionInfo = { version: string | null; path: string | null; globalPath: string | null };
-type ClaudeAuthInfo = { authenticated: boolean; loggedIn?: boolean; subscriptionType?: string };
-type OpencodeAuthInfo = { authenticated: boolean; providers: string[] };
+type StoredApiKeys = { keys: { env: string; value: string }[] };
+type LocalProviderEntry = {
+ id: string;
+ name: string;
+ url: string;
+ apiType: 'ollama' | 'openai-compatible' | 'lmstudio';
+ auth?: { type: 'api-key' | 'basic' };
+};
+type ProbeResult = {
+ success: boolean;
+ apiType?: LocalProviderEntry['apiType'];
+ name?: string;
+ needsAuth?: boolean;
+ authType?: 'api-key' | 'basic' | 'unknown';
+ models?: string[];
+ error?: string;
+};
+
+const PI_PROVIDERS: { key: string; env: string[] }[] = [
+ { key: 'OpenAI', env: ['OPENAI_API_KEY'] },
+ { key: 'Google', env: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'] },
+ { key: 'OpenCode Zen', env: ['OPENCODE_API_KEY'] },
+ { key: 'MiniMax', env: ['MINIMAX_API_KEY'] },
+ { key: 'Groq', env: ['GROQ_API_KEY'] },
+ { key: 'Mistral', env: ['MISTRAL_API_KEY'] },
+ { key: 'xAI', env: ['XAI_API_KEY'] },
+ { key: 'OpenRouter', env: ['OPENROUTER_API_KEY'] },
+ { key: 'Hugging Face', env: ['HF_TOKEN'] },
+ { key: 'GitHub Copilot', env: ['COPILOT_GITHUB_TOKEN'] },
+ { key: 'Amazon Bedrock', env: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION'] },
+ { key: 'Google Vertex AI', env: ['GOOGLE_APPLICATION_CREDENTIALS', 'GOOGLE_CLOUD_PROJECT', 'GOOGLE_CLOUD_LOCATION'] },
+ { key: 'Azure OpenAI', env: ['AZURE_OPENAI_API_KEY', 'AZURE_OPENAI_BASE_URL'] },
+ { key: 'Anthropic', env: ['ANTHROPIC_API_KEY'] },
+];
+
+const TEXT_FIELDS = new Set([
+ 'AWS_REGION',
+ 'GOOGLE_APPLICATION_CREDENTIALS',
+ 'GOOGLE_CLOUD_PROJECT',
+ 'GOOGLE_CLOUD_LOCATION',
+ 'AZURE_OPENAI_BASE_URL',
+]);
+
+type ProbeState =
+ | { step: 'url' }
+ | { step: 'probing' }
+ | { step: 'auth'; probe: ProbeResult }
+ | { step: 'saving' };
export const AIHarnessesSection = () => {
const client = useClient();
const queryClient = useQueryClient();
- const { aiHarnesses, saveSettings } = useServerSettings();
- const [installing, setInstalling] = useState<{ claudeCode: boolean; opencode: boolean; piMono: boolean }>({
- claudeCode: false,
- opencode: false,
- piMono: false,
- });
- const [copied, setCopied] = useState(null);
-
- const { data: opencodeVersion, isLoading: opencodeLoading } = useQuery({
- queryKey: ['OPENCODE_VERSION'],
- queryFn: () => client.get('/server-settings/opencode/version'),
- enabled: !!aiHarnesses?.opencode,
- refetchInterval: (query) => {
- const data = query.state.data;
- return data?.version && !data?.globalPath ? 1000 : false;
- },
- });
-
- const { data: claudeVersion, isLoading: claudeLoading } = useQuery({
- queryKey: ['CLAUDE_CODE_VERSION'],
- queryFn: () => client.get('/server-settings/claude-code/version'),
- enabled: !!aiHarnesses?.claudeCode,
- refetchInterval: (query) => {
- const data = query.state.data;
- return data?.version && !data?.globalPath ? 1000 : false;
- },
- });
+ const [installing, setInstalling] = useState(false);
const { data: piMonoVersion, isLoading: piMonoLoading } = useQuery({
queryKey: ['PI_MONO_VERSION'],
queryFn: () => client.get('/server-settings/pi-mono/version'),
- enabled: !!aiHarnesses?.piMono,
- refetchInterval: (query) => {
- const data = query.state.data;
- return data?.version && !data?.globalPath ? 1000 : false;
- },
});
- const { data: opencodeAuth } = useQuery({
- queryKey: ['OPENCODE_AUTH'],
- queryFn: () => client.get('/server-settings/opencode/auth'),
- enabled: !!opencodeVersion?.version,
- refetchInterval: (query) => (!query.state.data?.authenticated ? 2000 : false),
+ const { data: piMonoKeys } = useQuery({
+ queryKey: ['PI_MONO_API_KEYS'],
+ queryFn: () => client.get('/server-settings/pi-mono/api-keys'),
+ enabled: !!piMonoVersion?.version,
});
- const { data: claudeAuth } = useQuery({
- queryKey: ['CLAUDE_CODE_AUTH'],
- queryFn: () => client.get('/server-settings/claude-code/auth'),
- enabled: !!claudeVersion?.version,
- refetchInterval: (query) => (!query.state.data?.authenticated ? 2000 : false),
+ const { data: localProviders = [] as LocalProviderEntry[] } = useQuery({
+ queryKey: ['PI_MONO_LOCAL_PROVIDERS'],
+ queryFn: () => client.get('/server-settings/pi-mono/local-providers'),
+ enabled: !!piMonoVersion?.version,
});
- const toggleHarness = (key: 'claudeCode' | 'opencode' | 'piMono', checked: boolean) => {
- const updated = { ...aiHarnesses, [key]: checked };
- saveSettings({ aiHarnesses: updated });
+ const { data: localHealth = {} as Record } = useQuery({
+ queryKey: ['PI_MONO_LOCAL_HEALTH'],
+ queryFn: () => client.get>('/server-settings/pi-mono/local-providers/health'),
+ enabled: localProviders.length > 0,
+ refetchInterval: 15_000,
+ });
+
+ const [keyInputs, setKeyInputs] = useState>({});
+ const [savingKey, setSavingKey] = useState(null);
+ const [commandOpen, setCommandOpen] = useState(false);
+ const [editingProvider, setEditingProvider] = useState(null);
+
+ // Local provider connection flow
+ const [addingLocal, setAddingLocal] = useState(false);
+ const [localName, setLocalName] = useState('');
+ const [localUrl, setLocalUrl] = useState('');
+ const [probeState, setProbeState] = useState({ step: 'url' });
+ const [authApiKey, setAuthApiKey] = useState('');
+ const [authUsername, setAuthUsername] = useState('');
+ const [authPassword, setAuthPassword] = useState('');
+
+ const resetLocalForm = () => {
+ setAddingLocal(false);
+ setLocalName('');
+ setLocalUrl('');
+ setProbeState({ step: 'url' });
+ setAuthApiKey('');
+ setAuthUsername('');
+ setAuthPassword('');
};
- const installClaude = async () => {
- setInstalling((prev) => ({ ...prev, claudeCode: true }));
+ const storedEnvs = new Set(piMonoKeys?.keys.map((k: { env: string }) => k.env) ?? []);
+ const connectedProviders = PI_PROVIDERS.filter((p) => p.env.some((e) => storedEnvs.has(e)));
+ const unconnectedProviders = PI_PROVIDERS.filter((p) => !p.env.some((e) => storedEnvs.has(e)));
+
+ const getStoredMasked = (env: string) =>
+ piMonoKeys?.keys.find((k: { env: string; value: string }) => k.env === env)?.value ?? '';
+
+ const saveApiKey = async (env: string) => {
+ const value = keyInputs[env];
+ if (value === undefined) return;
+ setSavingKey(env);
try {
- const result = await client.post('/server-settings/claude-code/install');
- queryClient.setQueryData(['CLAUDE_CODE_VERSION'], result);
+ await client.put('/server-settings/pi-mono/api-keys', { key: env, value });
+ queryClient.invalidateQueries({ queryKey: ['PI_MONO_API_KEYS'] });
+ setKeyInputs((prev) => {
+ const next = { ...prev };
+ delete next[env];
+ return next;
+ });
} finally {
- setInstalling((prev) => ({ ...prev, claudeCode: false }));
+ setSavingKey(null);
}
};
- const installOpencode = async () => {
- setInstalling((prev) => ({ ...prev, opencode: true }));
- try {
- const result = await client.post('/server-settings/opencode/install');
- queryClient.setQueryData(['OPENCODE_VERSION'], result);
- } finally {
- setInstalling((prev) => ({ ...prev, opencode: false }));
+ const disconnectProvider = async (provider: typeof PI_PROVIDERS[number]) => {
+ for (const env of provider.env) {
+ await client.put('/server-settings/pi-mono/api-keys', { key: env, value: '' });
}
+ queryClient.invalidateQueries({ queryKey: ['PI_MONO_API_KEYS'] });
+ if (editingProvider === provider.key) setEditingProvider(null);
};
const installPiMono = async () => {
- setInstalling((prev) => ({ ...prev, piMono: true }));
+ setInstalling(true);
try {
const result = await client.post('/server-settings/pi-mono/install');
queryClient.setQueryData(['PI_MONO_VERSION'], result);
} finally {
- setInstalling((prev) => ({ ...prev, piMono: false }));
+ setInstalling(false);
}
};
- const copyToClipboard = (text: string) => {
- navigator.clipboard.writeText(text);
- setCopied(text);
- setTimeout(() => setCopied(null), 1500);
+ const handleProbe = async (auth?: { type: 'api-key'; apiKey: string } | { type: 'basic'; username: string; password: string }) => {
+ setProbeState({ step: 'probing' });
+ try {
+ const result = await client.post('/server-settings/pi-mono/local-providers/probe', {
+ url: localUrl.trim(),
+ auth,
+ });
+ if (!result.success) {
+ toast.error(result.error ?? 'Could not detect API type');
+ setProbeState({ step: 'url' });
+ return;
+ }
+ if (result.needsAuth) {
+ setProbeState({ step: 'auth', probe: result });
+ return;
+ }
+ // No auth needed — save directly
+ await saveLocalProvider(result, auth);
+ } catch {
+ toast.error('Failed to connect');
+ setProbeState({ step: 'url' });
+ }
};
- const [, setRunCommand] = usePanelChannel(RUN_COMMAND_CHANNEL, null);
+ const handleAuthSubmit = async (probe: ProbeResult) => {
+ const auth = probe.authType === 'basic'
+ ? { type: 'basic' as const, username: authUsername, password: authPassword }
+ : { type: 'api-key' as const, apiKey: authApiKey };
- const [confirmCommand, setConfirmCommand] = useState<{ command: string; refetchKeys: string[] } | null>(null);
+ // Re-probe with credentials to verify they work
+ setProbeState({ step: 'probing' });
+ try {
+ const result = await client.post('/server-settings/pi-mono/local-providers/probe', {
+ url: localUrl.trim(),
+ auth,
+ });
+ if (!result.success) {
+ toast.error(result.error ?? 'Could not connect with provided credentials');
+ setProbeState({ step: 'auth', probe });
+ return;
+ }
+ if (result.needsAuth) {
+ toast.error('Authentication failed');
+ setProbeState({ step: 'auth', probe });
+ return;
+ }
+ await saveLocalProvider(result, auth);
+ } catch {
+ toast.error('Failed to connect');
+ setProbeState({ step: 'auth', probe });
+ }
+ };
- const CopyCommand = ({ command, refetchKeys }: { command: string; refetchKeys: string[] }) => (
-
- Not globally accessible. Run:
-
-
{command}
-
setConfirmCommand({ command, refetchKeys })}
- className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors"
- title="Run in terminal"
- >
-
-
-
copyToClipboard(command)}
- className="shrink-0 p-1 rounded hover:bg-duck-dark/10 cursor-pointer transition-colors"
- title="Copy command"
- >
- {copied === command ? (
-
- ) : (
-
- )}
-
-
-
- );
+ const saveLocalProvider = async (
+ probe: ProbeResult,
+ auth?: { type: 'api-key'; apiKey: string } | { type: 'basic'; username: string; password: string },
+ ) => {
+ setProbeState({ step: 'saving' });
+ try {
+ await client.post('/server-settings/pi-mono/local-providers', {
+ url: localUrl.trim(),
+ name: localName.trim() || probe.name,
+ apiType: probe.apiType,
+ auth,
+ });
+ queryClient.invalidateQueries({ queryKey: ['PI_MONO_LOCAL_PROVIDERS'] });
+ toast.success(`Connected to ${probe.name}`);
+ resetLocalForm();
+ } catch {
+ toast.error('Failed to save provider');
+ setProbeState({ step: 'url' });
+ }
+ };
+
+ const removeLocalProvider = async (id: string) => {
+ await client.delete(`/server-settings/pi-mono/local-providers/${id}`);
+ queryClient.invalidateQueries({ queryKey: ['PI_MONO_LOCAL_PROVIDERS'] });
+ };
return (
- <>
-
- {aiHarnesses?.opencode && (
-
- {opencodeLoading ? (
- 'Checking version...'
- ) : opencodeVersion?.version ? (
- <>
-
{opencodeVersion.version}
-
{opencodeVersion.path}
- {opencodeAuth && (
-
- {opencodeAuth.authenticated ? (
- `Logged in (${opencodeAuth.providers.join(', ')})`
- ) : (
-
- Not logged in
- client.post('/server-settings/opencode/auth/login')}
- >
- Login
-
-
- )}
-
- )}
- {!opencodeVersion.globalPath && opencodeVersion.path && (
-
- )}
- >
- ) : (
-
- {installing.opencode ? 'Installing...' : 'Install'}
-
- )}
-
- )}
-
-
-
-
- {aiHarnesses?.claudeCode && (
-
- {claudeLoading ? (
- 'Checking version...'
- ) : claudeVersion?.version ? (
- <>
-
{claudeVersion.version}
-
{claudeVersion.path}
- {claudeAuth && (
-
- {claudeAuth.authenticated ? (
- `Logged in (${claudeAuth.subscriptionType ?? 'unknown plan'})`
- ) : (
-
- Not logged in
- client.post('/server-settings/claude-code/auth/login')}
- >
- Login
-
-
- )}
-
- )}
- {!claudeVersion.globalPath && claudeVersion.path && (
-
- )}
- >
- ) : (
-
- {installing.claudeCode ? 'Installing...' : 'Install'}
-
- )}
-
- )}
-
-
-
-
- {aiHarnesses?.piMono && (
-
- {piMonoLoading ? (
- 'Checking version...'
- ) : piMonoVersion?.version ? (
- <>
-
{piMonoVersion.version}
-
{piMonoVersion.path}
- {!piMonoVersion.globalPath && piMonoVersion.path && (
-
- )}
- >
- ) : (
-
+ Pi Mono
+ {piMonoLoading ? (
+ Checking...
+ ) : piMonoVersion?.version ? (
+
+ {piMonoVersion.version}
+
- {installing.piMono ? 'Installing...' : 'Install'}
-
- )}
+
+
+
+ ) : (
+
+ {installing ? 'Installing...' : 'Install'}
+
+ )}
+
+ {piMonoVersion?.version && (
+ <>
+ {/* Local Providers */}
+
+
Local Providers
+
+ {localProviders.map((lp: LocalProviderEntry) => (
+
+
+ {lp.name}
+ {lp.url}
+ removeLocalProvider(lp.id)}
+ className="shrink-0 p-0.5 rounded hover:bg-red-500/10 cursor-pointer transition-colors"
+ title="Remove provider"
+ >
+
+
+
+ ))}
+ {addingLocal ? (
+
+ ) : (
+
setAddingLocal(true)}
+ >
+
+ Connect Local Provider
+
+ )}
+
+
+ {/* Remote Providers */}
+
+
Remote Providers
+
+ {connectedProviders.map((provider) => (
+
+
+ {provider.key}
+ disconnectProvider(provider)}
+ className="p-0.5 rounded hover:bg-red-500/10 cursor-pointer transition-colors"
+ title="Disconnect provider"
+ >
+
+
+
+ {provider.env.map((env) => (
+
+
+ setKeyInputs((prev) => ({ ...prev, [env]: ev.target.value }))}
+ />
+ saveApiKey(env)}
+ className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default"
+ title="Save key"
+ >
+
+
+
+ ))}
+
+ ))}
+ {editingProvider && (() => {
+ const provider = PI_PROVIDERS.find((p) => p.key === editingProvider);
+ if (!provider || connectedProviders.includes(provider)) return null;
+ return (
+
+
{provider.key}
+ {provider.env.map((env) => (
+
+
+ setKeyInputs((prev) => ({ ...prev, [env]: ev.target.value }))}
+ autoFocus={env === provider.env[0]}
+ />
+ saveApiKey(env)}
+ className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default"
+ title="Save key"
+ >
+
+
+
+ ))}
+
+ );
+ })()}
+ {unconnectedProviders.length > 0 && (
+
setCommandOpen(true)}
+ >
+
+ Connect Provider
+
+ )}
+
+
+
+
+ No providers found.
+
+ {unconnectedProviders.map((provider) => (
+ {
+ setEditingProvider(provider.key);
+ setCommandOpen(false);
+ }}
+ >
+ {provider.key}
+
+ ))}
+
+
+
+
+ >
)}
-
- !open && setConfirmCommand(null)}>
-
-
- Run with elevated privileges
-
- You are about to run a command with elevated privileges (sudo). Are you sure?
-
-
- {confirmCommand?.command}
-
- Cancel
- {
- if (confirmCommand) setRunCommand(confirmCommand);
- setConfirmCommand(null);
- }}
- >
- Run
-
-
-
-
- >
);
};
diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/SettingsPanel.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/SettingsPanel.tsx
index 20878be6..aa9c9474 100644
--- a/src/apps/officer-web/Screens/Dashboard/Settings/SettingsPanel.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Settings/SettingsPanel.tsx
@@ -131,7 +131,7 @@ export const SettingsContent = ({ globalKey, sections }: SettingsContentProps) =
{section.title}
{section.description}
- {section.content}
+ {section.content}
);
};
diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/SystemSettings.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/SystemSettings.tsx
index 9dc63f8a..f61997fe 100644
--- a/src/apps/officer-web/Screens/Dashboard/Settings/SystemSettings.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Settings/SystemSettings.tsx
@@ -1,14 +1,11 @@
-import { useState, useEffect, useMemo } from 'react';
+import { useState, useEffect, useMemo, useCallback, type DragEvent } from 'react';
import { toast } from 'sonner';
-import { Terminal, Eye, Trash2, Bot, Server, Puzzle, Settings, X } from 'lucide-react';
+import { Terminal, Eye, Bot, Settings, X, Plus } from 'lucide-react';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Slider } from '@/components/ui/slider';
-import { Switch } from '@/components/ui/switch';
-import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
-import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useQueryClient } from '@tanstack/react-query';
import type { LayoutNode, PanelComponents } from '@/components/Workspace';
@@ -19,34 +16,19 @@ import { appRegistry } from '../Workspaces/app-registry';
import { createSettingsPanelComponents, type SettingsSectionGroup } from './SettingsPanel';
import { useSettings } from '@/state/useSettings';
import { useUserState } from '@/state/useUserState';
-import {
- useClaudeModels,
- useOpenCodeModels,
- usePiMonoModels,
- useVisibleClaudeModels,
- useVisibleOpenCodeModels,
- useVisiblePiMonoModels,
-} from '@/state/useModels';
+import { usePiMonoModels, useVisiblePiMonoModels } from '@/state/useModels';
import type { UserSettings } from '@/state/types/user-settings';
import { AIHarnessesSection } from './ServerSettings/AIHarnessesSection';
-import { PluginsSection } from './ServerSettings/PluginsSection';
import { RUN_COMMAND_CHANNEL, type RunCommandState } from './ServerSettings/run-command-channel';
const groups: SettingsSectionGroup[] = [
- {
- label: 'Server',
- icon: Server,
- sections: [
- { key: 'ai-harnesses', icon: Terminal, title: 'AI Harnesses', description: 'AI coding tools setup', content: