chat: rename the pi-mono provider router + purge residual pi names (Stage 4b/2)
Renames the AI-harness/provider settings router into the chat namespace and clears the remaining "pi" identifiers from the chat stack. - server-settings/pi-mono.ts → chat-providers.ts; piMonoRouter → chatProvidersRouter; route /server-settings/pi-mono → /server-settings/chat-providers (+ all callers) - piId → providerId (PROVIDERS map + AIHarnessesSection UI), PiProvider → ChatProvider, PI_MONO_* query keys → CHAT_PROVIDERS_*, installPiMono → installAgent - data-path: PI_CONFIG_DIR → AGENT_CONFIG_DIR (path ~/.pi/agent unchanged); drop dead getPiMonoDir/getPiMonoSessionDir exports - settings: flip the vestigial defaultProvider literal 'pi' → 'chat' (never read; only defaultModel drives behavior); access-policy config key 'pi-access-policy' → 'chat-access-policy' - misc: ModelSelector fallback label, TaskDefaults model grouping, CapabilityPage chat var, a stale stream-parser comment Intentionally left (genuine external `pi`/opencode references, not ours to rename): the `pi` binary install/version flow (@mariozechner/pi-coding-agent, `which pi`), the ~/.pi/agent config path, PI_TOOLS_DIRS/PI_SEARXNG_URL runtime env-var contract, TOOL.md `targets: pi` metadata, and the "Pi Mono" installer UI label. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -279,20 +279,20 @@ export const CapabilityChat = ({
|
|||||||
? (description ?? `Help me create the content for this new ${kind} file`)
|
? (description ?? `Help me create the content for this new ${kind} file`)
|
||||||
: `Help me understand and improve this ${kind} file`;
|
: `Help me understand and improve this ${kind} file`;
|
||||||
|
|
||||||
const pi = useChat(undefined, undefined, { replaceUrl: false });
|
const chat = useChat(undefined, undefined, { replaceUrl: false });
|
||||||
|
|
||||||
const onResponseEndRef = useRef(onResponseEnd);
|
const onResponseEndRef = useRef(onResponseEnd);
|
||||||
onResponseEndRef.current = onResponseEnd;
|
onResponseEndRef.current = onResponseEnd;
|
||||||
|
|
||||||
const wasGenerating = useRef(false);
|
const wasGenerating = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (wasGenerating.current && !pi.isGenerating) {
|
if (wasGenerating.current && !chat.isGenerating) {
|
||||||
onResponseEndRef.current?.();
|
onResponseEndRef.current?.();
|
||||||
}
|
}
|
||||||
wasGenerating.current = pi.isGenerating;
|
wasGenerating.current = chat.isGenerating;
|
||||||
}, [pi.isGenerating]);
|
}, [chat.isGenerating]);
|
||||||
|
|
||||||
return <EmbeddableChat chat={pi} defaultInput={defaultInput} promptPrefix={promptFrontmatter} className="h-full" />;
|
return <EmbeddableChat chat={chat} defaultInput={defaultInput} promptPrefix={promptFrontmatter} className="h-full" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FrontmatterBlock = ({ yaml }: { yaml: string }) => {
|
export const FrontmatterBlock = ({ yaml }: { yaml: string }) => {
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ function MemberModelsSection() {
|
|||||||
const refreshModels = async () => {
|
const refreshModels = async () => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
try {
|
try {
|
||||||
await client.post('/server-settings/pi-mono/local-providers/refresh');
|
await client.post('/server-settings/chat-providers/local-providers/refresh');
|
||||||
queryClient.invalidateQueries({ queryKey: ['CHAT_MODELS'] });
|
queryClient.invalidateQueries({ queryKey: ['CHAT_MODELS'] });
|
||||||
toast.success('Models refreshed');
|
toast.success('Models refreshed');
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { useUserVisibleModels, type ModelOption } from 'state/useModels';
|
|||||||
|
|
||||||
export const TaskDefaults = () => {
|
export const TaskDefaults = () => {
|
||||||
const { settings, saveSettings } = useSettings();
|
const { settings, saveSettings } = useSettings();
|
||||||
const piModels = useUserVisibleModels();
|
const models = useUserVisibleModels();
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
const [model, setModel] = useState<string | null>(settings.tasks.defaultModel);
|
const [model, setModel] = useState<string | null>(settings.tasks.defaultModel);
|
||||||
@@ -37,13 +37,13 @@ export const TaskDefaults = () => {
|
|||||||
.map(([provider, models]) => ({ provider, models: models.sort((a, b) => a.name.localeCompare(b.name)) }));
|
.map(([provider, models]) => ({ provider, models: models.sort((a, b) => a.name.localeCompare(b.name)) }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const piGroups = useMemo(() => buildGroups(piModels, 'Pi'), [piModels]);
|
const modelGroups = useMemo(() => buildGroups(models, 'Chat'), [models]);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (isSaving) return;
|
if (isSaving) return;
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
await saveSettings({ ...settings, tasks: { defaultProvider: 'pi', defaultModel: model } });
|
await saveSettings({ ...settings, tasks: { defaultProvider: 'chat', defaultModel: model } });
|
||||||
toast.success('Task defaults saved');
|
toast.success('Task defaults saved');
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Failed to save settings');
|
toast.error('Failed to save settings');
|
||||||
@@ -61,7 +61,7 @@ export const TaskDefaults = () => {
|
|||||||
<SelectValue placeholder="Same as chat default" />
|
<SelectValue placeholder="Same as chat default" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="z-[600] max-h-[300px]">
|
<SelectContent className="z-[600] max-h-[300px]">
|
||||||
{piGroups.map(({ provider, models }) => (
|
{modelGroups.map(({ provider, models }) => (
|
||||||
<SelectGroup key={`pi-${provider}`}>
|
<SelectGroup key={`pi-${provider}`}>
|
||||||
<SelectLabel>{provider}</SelectLabel>
|
<SelectLabel>{provider}</SelectLabel>
|
||||||
{models.map((m) => (
|
{models.map((m) => (
|
||||||
|
|||||||
+61
-61
@@ -33,7 +33,7 @@ type ProbeResult = {
|
|||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PiProvider = { key: string; piId: string };
|
type ChatProvider = { key: string; providerId: string };
|
||||||
|
|
||||||
type ProbeState = { step: 'url' } | { step: 'probing' } | { step: 'auth'; probe: ProbeResult } | { step: 'saving' };
|
type ProbeState = { step: 'url' } | { step: 'probing' } | { step: 'auth'; probe: ProbeResult } | { step: 'saving' };
|
||||||
|
|
||||||
@@ -42,31 +42,31 @@ export const AIHarnessesSection = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [installing, setInstalling] = useState(false);
|
const [installing, setInstalling] = useState(false);
|
||||||
|
|
||||||
const { data: PI_PROVIDERS = [] as PiProvider[] } = useQuery({
|
const { data: CHAT_PROVIDERS = [] as ChatProvider[] } = useQuery({
|
||||||
queryKey: ['PI_MONO_PROVIDERS'],
|
queryKey: ['CHAT_PROVIDERS_PROVIDERS'],
|
||||||
queryFn: () => client.get<PiProvider[]>('/server-settings/pi-mono/providers'),
|
queryFn: () => client.get<ChatProvider[]>('/server-settings/chat-providers/providers'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: piMonoVersion, isLoading: piMonoLoading } = useQuery({
|
const { data: piMonoVersion, isLoading: piMonoLoading } = useQuery({
|
||||||
queryKey: ['PI_MONO_VERSION'],
|
queryKey: ['CHAT_PROVIDERS_VERSION'],
|
||||||
queryFn: () => client.get<VersionInfo>('/server-settings/pi-mono/version'),
|
queryFn: () => client.get<VersionInfo>('/server-settings/chat-providers/version'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: piMonoKeys } = useQuery({
|
const { data: piMonoKeys } = useQuery({
|
||||||
queryKey: ['PI_MONO_API_KEYS'],
|
queryKey: ['CHAT_PROVIDERS_API_KEYS'],
|
||||||
queryFn: () => client.get<StoredApiKeys>('/server-settings/pi-mono/api-keys'),
|
queryFn: () => client.get<StoredApiKeys>('/server-settings/chat-providers/api-keys'),
|
||||||
enabled: !!piMonoVersion?.version,
|
enabled: !!piMonoVersion?.version,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: localProviders = [] as LocalProviderEntry[] } = useQuery({
|
const { data: localProviders = [] as LocalProviderEntry[] } = useQuery({
|
||||||
queryKey: ['PI_MONO_LOCAL_PROVIDERS'],
|
queryKey: ['CHAT_PROVIDERS_LOCAL_PROVIDERS'],
|
||||||
queryFn: () => client.get<LocalProviderEntry[]>('/server-settings/pi-mono/local-providers'),
|
queryFn: () => client.get<LocalProviderEntry[]>('/server-settings/chat-providers/local-providers'),
|
||||||
enabled: !!piMonoVersion?.version,
|
enabled: !!piMonoVersion?.version,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: localHealth = {} as Record<string, boolean> } = useQuery({
|
const { data: localHealth = {} as Record<string, boolean> } = useQuery({
|
||||||
queryKey: ['PI_MONO_LOCAL_HEALTH'],
|
queryKey: ['CHAT_PROVIDERS_LOCAL_HEALTH'],
|
||||||
queryFn: () => client.get<Record<string, boolean>>('/server-settings/pi-mono/local-providers/health'),
|
queryFn: () => client.get<Record<string, boolean>>('/server-settings/chat-providers/local-providers/health'),
|
||||||
enabled: localProviders.length > 0,
|
enabled: localProviders.length > 0,
|
||||||
refetchInterval: 15_000,
|
refetchInterval: 15_000,
|
||||||
});
|
});
|
||||||
@@ -96,31 +96,31 @@ export const AIHarnessesSection = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const storedKeys: StoredApiKeys['keys'] = piMonoKeys?.keys ?? [];
|
const storedKeys: StoredApiKeys['keys'] = piMonoKeys?.keys ?? [];
|
||||||
const storedPiIds = new Set(storedKeys.map((k) => k.provider));
|
const storedProviderIds = new Set(storedKeys.map((k) => k.provider));
|
||||||
const connectedProviders = PI_PROVIDERS.filter((p) => storedPiIds.has(p.piId));
|
const connectedProviders = CHAT_PROVIDERS.filter((p) => storedProviderIds.has(p.providerId));
|
||||||
const unconnectedProviders = PI_PROVIDERS.filter((p) => !storedPiIds.has(p.piId));
|
const unconnectedProviders = CHAT_PROVIDERS.filter((p) => !storedProviderIds.has(p.providerId));
|
||||||
|
|
||||||
const { data: remoteHealth = {} as Record<string, boolean | null> } = useQuery({
|
const { data: remoteHealth = {} as Record<string, boolean | null> } = useQuery({
|
||||||
queryKey: ['PI_MONO_REMOTE_HEALTH'],
|
queryKey: ['CHAT_PROVIDERS_REMOTE_HEALTH'],
|
||||||
queryFn: () => client.get<Record<string, boolean | null>>('/server-settings/pi-mono/api-keys/health'),
|
queryFn: () => client.get<Record<string, boolean | null>>('/server-settings/chat-providers/api-keys/health'),
|
||||||
enabled: connectedProviders.length > 0,
|
enabled: connectedProviders.length > 0,
|
||||||
refetchInterval: 30_000,
|
refetchInterval: 30_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const getStoredMasked = (piId: string) => storedKeys.find((k) => k.provider === piId)?.value ?? '';
|
const getStoredMasked = (providerId: string) => storedKeys.find((k) => k.provider === providerId)?.value ?? '';
|
||||||
|
|
||||||
const saveApiKey = async (piId: string) => {
|
const saveApiKey = async (providerId: string) => {
|
||||||
const value = keyInputs[piId];
|
const value = keyInputs[providerId];
|
||||||
if (value === undefined) return;
|
if (value === undefined) return;
|
||||||
setSavingKey(piId);
|
setSavingKey(providerId);
|
||||||
try {
|
try {
|
||||||
await client.put('/server-settings/pi-mono/api-keys', { provider: piId, value });
|
await client.put('/server-settings/chat-providers/api-keys', { provider: providerId, value });
|
||||||
queryClient.invalidateQueries({ queryKey: ['PI_MONO_API_KEYS'] });
|
queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_API_KEYS'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['PI_MONO_REMOTE_HEALTH'] });
|
queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_REMOTE_HEALTH'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['CHAT_MODELS'] });
|
queryClient.invalidateQueries({ queryKey: ['CHAT_MODELS'] });
|
||||||
setKeyInputs((prev) => {
|
setKeyInputs((prev) => {
|
||||||
const next = { ...prev };
|
const next = { ...prev };
|
||||||
delete next[piId];
|
delete next[providerId];
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
setEditingProvider(null);
|
setEditingProvider(null);
|
||||||
@@ -129,19 +129,19 @@ export const AIHarnessesSection = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const disconnectProvider = async (provider: (typeof PI_PROVIDERS)[number]) => {
|
const disconnectProvider = async (provider: (typeof CHAT_PROVIDERS)[number]) => {
|
||||||
await client.put('/server-settings/pi-mono/api-keys', { provider: provider.piId, value: '' });
|
await client.put('/server-settings/chat-providers/api-keys', { provider: provider.providerId, value: '' });
|
||||||
queryClient.invalidateQueries({ queryKey: ['PI_MONO_API_KEYS'] });
|
queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_API_KEYS'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['PI_MONO_REMOTE_HEALTH'] });
|
queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_REMOTE_HEALTH'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['CHAT_MODELS'] });
|
queryClient.invalidateQueries({ queryKey: ['CHAT_MODELS'] });
|
||||||
if (editingProvider === provider.key) setEditingProvider(null);
|
if (editingProvider === provider.key) setEditingProvider(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const installPiMono = async () => {
|
const installAgent = async () => {
|
||||||
setInstalling(true);
|
setInstalling(true);
|
||||||
try {
|
try {
|
||||||
const result = await client.post<VersionInfo>('/server-settings/pi-mono/install');
|
const result = await client.post<VersionInfo>('/server-settings/chat-providers/install');
|
||||||
queryClient.setQueryData(['PI_MONO_VERSION'], result);
|
queryClient.setQueryData(['CHAT_PROVIDERS_VERSION'], result);
|
||||||
} finally {
|
} finally {
|
||||||
setInstalling(false);
|
setInstalling(false);
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@ export const AIHarnessesSection = () => {
|
|||||||
) => {
|
) => {
|
||||||
setProbeState({ step: 'probing' });
|
setProbeState({ step: 'probing' });
|
||||||
try {
|
try {
|
||||||
const result = await client.post<ProbeResult>('/server-settings/pi-mono/local-providers/probe', {
|
const result = await client.post<ProbeResult>('/server-settings/chat-providers/local-providers/probe', {
|
||||||
url: localUrl.trim(),
|
url: localUrl.trim(),
|
||||||
auth,
|
auth,
|
||||||
});
|
});
|
||||||
@@ -182,7 +182,7 @@ export const AIHarnessesSection = () => {
|
|||||||
// Re-probe with credentials to verify they work
|
// Re-probe with credentials to verify they work
|
||||||
setProbeState({ step: 'probing' });
|
setProbeState({ step: 'probing' });
|
||||||
try {
|
try {
|
||||||
const result = await client.post<ProbeResult>('/server-settings/pi-mono/local-providers/probe', {
|
const result = await client.post<ProbeResult>('/server-settings/chat-providers/local-providers/probe', {
|
||||||
url: localUrl.trim(),
|
url: localUrl.trim(),
|
||||||
auth,
|
auth,
|
||||||
});
|
});
|
||||||
@@ -209,14 +209,14 @@ export const AIHarnessesSection = () => {
|
|||||||
) => {
|
) => {
|
||||||
setProbeState({ step: 'saving' });
|
setProbeState({ step: 'saving' });
|
||||||
try {
|
try {
|
||||||
await client.post('/server-settings/pi-mono/local-providers', {
|
await client.post('/server-settings/chat-providers/local-providers', {
|
||||||
url: localUrl.trim(),
|
url: localUrl.trim(),
|
||||||
name: localName.trim() || probe.name,
|
name: localName.trim() || probe.name,
|
||||||
apiType: probe.apiType,
|
apiType: probe.apiType,
|
||||||
auth,
|
auth,
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: ['PI_MONO_LOCAL_PROVIDERS'] });
|
queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_LOCAL_PROVIDERS'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['PI_MONO_LOCAL_HEALTH'] });
|
queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_LOCAL_HEALTH'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['CHAT_MODELS'] });
|
queryClient.invalidateQueries({ queryKey: ['CHAT_MODELS'] });
|
||||||
toast.success(`Connected to ${probe.name}`);
|
toast.success(`Connected to ${probe.name}`);
|
||||||
resetLocalForm();
|
resetLocalForm();
|
||||||
@@ -227,9 +227,9 @@ export const AIHarnessesSection = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const removeLocalProvider = async (id: string) => {
|
const removeLocalProvider = async (id: string) => {
|
||||||
await client.delete(`/server-settings/pi-mono/local-providers/${id}`);
|
await client.delete(`/server-settings/chat-providers/local-providers/${id}`);
|
||||||
queryClient.invalidateQueries({ queryKey: ['PI_MONO_LOCAL_PROVIDERS'] });
|
queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_LOCAL_PROVIDERS'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['PI_MONO_LOCAL_HEALTH'] });
|
queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_LOCAL_HEALTH'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['CHAT_MODELS'] });
|
queryClient.invalidateQueries({ queryKey: ['CHAT_MODELS'] });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ export const AIHarnessesSection = () => {
|
|||||||
<span className="text-xs text-duck-dark/50">{piMonoVersion.version}</span>
|
<span className="text-xs text-duck-dark/50">{piMonoVersion.version}</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={installPiMono}
|
onClick={installAgent}
|
||||||
disabled={installing}
|
disabled={installing}
|
||||||
className="p-0.5 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30"
|
className="p-0.5 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30"
|
||||||
title="Update Pi Mono"
|
title="Update Pi Mono"
|
||||||
@@ -257,7 +257,7 @@ export const AIHarnessesSection = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-6 text-xs bg-duck-teal text-duck-yellow hover:bg-duck-teal/90"
|
className="h-6 text-xs bg-duck-teal text-duck-yellow hover:bg-duck-teal/90"
|
||||||
onClick={installPiMono}
|
onClick={installAgent}
|
||||||
disabled={installing}
|
disabled={installing}
|
||||||
>
|
>
|
||||||
{installing ? 'Installing...' : 'Install'}
|
{installing ? 'Installing...' : 'Install'}
|
||||||
@@ -423,20 +423,20 @@ export const AIHarnessesSection = () => {
|
|||||||
<span className="text-sm font-medium text-duck-dark dark:text-foreground">Remote Providers</span>
|
<span className="text-sm font-medium text-duck-dark dark:text-foreground">Remote Providers</span>
|
||||||
<div className="mt-3 flex flex-col gap-2">
|
<div className="mt-3 flex flex-col gap-2">
|
||||||
{connectedProviders.map((provider) => (
|
{connectedProviders.map((provider) => (
|
||||||
<div key={provider.piId} className="flex flex-col gap-2">
|
<div key={provider.providerId} className="flex flex-col gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={`inline-block h-2 w-2 shrink-0 rounded-full ${
|
className={`inline-block h-2 w-2 shrink-0 rounded-full ${
|
||||||
remoteHealth[provider.piId] === true
|
remoteHealth[provider.providerId] === true
|
||||||
? 'bg-green-500'
|
? 'bg-green-500'
|
||||||
: remoteHealth[provider.piId] === false
|
: remoteHealth[provider.providerId] === false
|
||||||
? 'bg-red-500'
|
? 'bg-red-500'
|
||||||
: 'bg-duck-dark/20'
|
: 'bg-duck-dark/20'
|
||||||
}`}
|
}`}
|
||||||
title={
|
title={
|
||||||
remoteHealth[provider.piId] === true
|
remoteHealth[provider.providerId] === true
|
||||||
? 'Valid'
|
? 'Valid'
|
||||||
: remoteHealth[provider.piId] === false
|
: remoteHealth[provider.providerId] === false
|
||||||
? 'Invalid'
|
? 'Invalid'
|
||||||
: 'Unknown'
|
: 'Unknown'
|
||||||
}
|
}
|
||||||
@@ -464,19 +464,19 @@ export const AIHarnessesSection = () => {
|
|||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
className="h-7 text-xs flex-1"
|
className="h-7 text-xs flex-1"
|
||||||
placeholder={getStoredMasked(provider.piId) || 'Enter API key'}
|
placeholder={getStoredMasked(provider.providerId) || 'Enter API key'}
|
||||||
value={keyInputs[provider.piId] ?? ''}
|
value={keyInputs[provider.providerId] ?? ''}
|
||||||
onChange={(ev) => setKeyInputs((prev) => ({ ...prev, [provider.piId]: ev.target.value }))}
|
onChange={(ev) => setKeyInputs((prev) => ({ ...prev, [provider.providerId]: ev.target.value }))}
|
||||||
onKeyDown={(ev) => {
|
onKeyDown={(ev) => {
|
||||||
if (ev.key === 'Enter' && keyInputs[provider.piId]) saveApiKey(provider.piId);
|
if (ev.key === 'Enter' && keyInputs[provider.providerId]) saveApiKey(provider.providerId);
|
||||||
if (ev.key === 'Escape') setEditingProvider(null);
|
if (ev.key === 'Escape') setEditingProvider(null);
|
||||||
}}
|
}}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={keyInputs[provider.piId] === undefined || savingKey === provider.piId}
|
disabled={keyInputs[provider.providerId] === undefined || savingKey === provider.providerId}
|
||||||
onClick={() => saveApiKey(provider.piId)}
|
onClick={() => saveApiKey(provider.providerId)}
|
||||||
className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default"
|
className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default"
|
||||||
title="Save key"
|
title="Save key"
|
||||||
>
|
>
|
||||||
@@ -495,10 +495,10 @@ export const AIHarnessesSection = () => {
|
|||||||
))}
|
))}
|
||||||
{editingProvider &&
|
{editingProvider &&
|
||||||
(() => {
|
(() => {
|
||||||
const provider = PI_PROVIDERS.find((p) => p.key === editingProvider);
|
const provider = CHAT_PROVIDERS.find((p) => p.key === editingProvider);
|
||||||
if (!provider || connectedProviders.some((cp) => cp.piId === provider.piId)) return null;
|
if (!provider || connectedProviders.some((cp) => cp.providerId === provider.providerId)) return null;
|
||||||
return (
|
return (
|
||||||
<div key={provider.piId} className="flex items-center gap-2">
|
<div key={provider.providerId} className="flex items-center gap-2">
|
||||||
<label className="w-36 text-duck-dark/70 dark:text-foreground/70 shrink-0 truncate font-medium text-[11px]">
|
<label className="w-36 text-duck-dark/70 dark:text-foreground/70 shrink-0 truncate font-medium text-[11px]">
|
||||||
{provider.key}
|
{provider.key}
|
||||||
</label>
|
</label>
|
||||||
@@ -506,18 +506,18 @@ export const AIHarnessesSection = () => {
|
|||||||
type="password"
|
type="password"
|
||||||
className="h-7 text-xs flex-1"
|
className="h-7 text-xs flex-1"
|
||||||
placeholder="Enter API key"
|
placeholder="Enter API key"
|
||||||
value={keyInputs[provider.piId] ?? ''}
|
value={keyInputs[provider.providerId] ?? ''}
|
||||||
onChange={(ev) => setKeyInputs((prev) => ({ ...prev, [provider.piId]: ev.target.value }))}
|
onChange={(ev) => setKeyInputs((prev) => ({ ...prev, [provider.providerId]: ev.target.value }))}
|
||||||
onKeyDown={(ev) => {
|
onKeyDown={(ev) => {
|
||||||
if (ev.key === 'Enter' && keyInputs[provider.piId]) saveApiKey(provider.piId);
|
if (ev.key === 'Enter' && keyInputs[provider.providerId]) saveApiKey(provider.providerId);
|
||||||
if (ev.key === 'Escape') setEditingProvider(null);
|
if (ev.key === 'Escape') setEditingProvider(null);
|
||||||
}}
|
}}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={keyInputs[provider.piId] === undefined || savingKey === provider.piId}
|
disabled={keyInputs[provider.providerId] === undefined || savingKey === provider.providerId}
|
||||||
onClick={() => saveApiKey(provider.piId)}
|
onClick={() => saveApiKey(provider.providerId)}
|
||||||
className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default"
|
className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default"
|
||||||
title="Save key"
|
title="Save key"
|
||||||
>
|
>
|
||||||
|
|||||||
+53
-53
@@ -1,14 +1,14 @@
|
|||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { createRouter } from '../../create-router';
|
import { createRouter } from '../../create-router';
|
||||||
import { PI_CONFIG_DIR } from '../../data-path';
|
import { AGENT_CONFIG_DIR } from '../../data-path';
|
||||||
import { invalidateModelCache } from '../chat/list-models';
|
import { invalidateModelCache } from '../chat/list-models';
|
||||||
import { logger } from '../chat/logger';
|
import { logger } from '../chat/logger';
|
||||||
import { readConfigValue, writeConfigValue } from 'officerdb';
|
import { readConfigValue, writeConfigValue } from 'officerdb';
|
||||||
|
|
||||||
export const piMonoRouter = createRouter();
|
export const chatProvidersRouter = createRouter();
|
||||||
|
|
||||||
const PI_AUTH_FILE = join(PI_CONFIG_DIR, 'auth.json');
|
const AUTH_FILE = join(AGENT_CONFIG_DIR, 'auth.json');
|
||||||
const PI_MODELS_FILE = join(PI_CONFIG_DIR, 'models.json');
|
const MODELS_FILE = join(AGENT_CONFIG_DIR, 'models.json');
|
||||||
|
|
||||||
// --- Local provider types ---
|
// --- Local provider types ---
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ type ProbeResult = {
|
|||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PiModelConfig = {
|
type ModelConfig = {
|
||||||
providers: Record<
|
providers: Record<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
@@ -58,18 +58,18 @@ type OfficerMeta = {
|
|||||||
auth?: { type: 'api-key'; apiKey: string } | { type: 'basic'; username: string; password: string };
|
auth?: { type: 'api-key'; apiKey: string } | { type: 'basic'; username: string; password: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
async function readModelsConfig(): Promise<PiModelConfig> {
|
async function readModelsConfig(): Promise<ModelConfig> {
|
||||||
try {
|
try {
|
||||||
const file = Bun.file(PI_MODELS_FILE);
|
const file = Bun.file(MODELS_FILE);
|
||||||
if (!(await file.exists())) return { providers: {} };
|
if (!(await file.exists())) return { providers: {} };
|
||||||
return (await file.json()) as PiModelConfig;
|
return (await file.json()) as ModelConfig;
|
||||||
} catch {
|
} catch {
|
||||||
return { providers: {} };
|
return { providers: {} };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeModelsConfig(config: PiModelConfig): Promise<void> {
|
async function writeModelsConfig(config: ModelConfig): Promise<void> {
|
||||||
await Bun.write(PI_MODELS_FILE, JSON.stringify(config, null, 2));
|
await Bun.write(MODELS_FILE, JSON.stringify(config, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -333,7 +333,7 @@ type AuthJson = Record<string, AuthEntry>;
|
|||||||
|
|
||||||
async function readAuthJson(): Promise<AuthJson> {
|
async function readAuthJson(): Promise<AuthJson> {
|
||||||
try {
|
try {
|
||||||
const file = Bun.file(PI_AUTH_FILE);
|
const file = Bun.file(AUTH_FILE);
|
||||||
if (!(await file.exists())) return {};
|
if (!(await file.exists())) return {};
|
||||||
return (await file.json()) as AuthJson;
|
return (await file.json()) as AuthJson;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -342,7 +342,7 @@ async function readAuthJson(): Promise<AuthJson> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function writeAuthJson(auth: AuthJson): Promise<void> {
|
async function writeAuthJson(auth: AuthJson): Promise<void> {
|
||||||
await Bun.write(PI_AUTH_FILE, JSON.stringify(auth, null, 2));
|
await Bun.write(AUTH_FILE, JSON.stringify(auth, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -350,29 +350,29 @@ async function writeAuthJson(auth: AuthJson): Promise<void> {
|
|||||||
* Pi's native providers have built-in models; adding a key to auth.json
|
* Pi's native providers have built-in models; adding a key to auth.json
|
||||||
* is all that's needed to make their models appear in `pi --list-models`.
|
* is all that's needed to make their models appear in `pi --list-models`.
|
||||||
*/
|
*/
|
||||||
export const PROVIDERS: { key: string; piId: string }[] = [
|
export const PROVIDERS: { key: string; providerId: string }[] = [
|
||||||
{ key: 'Anthropic', piId: 'anthropic' },
|
{ key: 'Anthropic', providerId: 'anthropic' },
|
||||||
{ key: 'OpenAI', piId: 'openai' },
|
{ key: 'OpenAI', providerId: 'openai' },
|
||||||
{ key: 'Google', piId: 'google' },
|
{ key: 'Google', providerId: 'google' },
|
||||||
{ key: 'Groq', piId: 'groq' },
|
{ key: 'Groq', providerId: 'groq' },
|
||||||
{ key: 'Mistral', piId: 'mistral' },
|
{ key: 'Mistral', providerId: 'mistral' },
|
||||||
{ key: 'xAI', piId: 'xai' },
|
{ key: 'xAI', providerId: 'xai' },
|
||||||
{ key: 'OpenRouter', piId: 'openrouter' },
|
{ key: 'OpenRouter', providerId: 'openrouter' },
|
||||||
{ key: 'MiniMax', piId: 'minimax' },
|
{ key: 'MiniMax', providerId: 'minimax' },
|
||||||
{ key: 'Hugging Face', piId: 'huggingface' },
|
{ key: 'Hugging Face', providerId: 'huggingface' },
|
||||||
{ key: 'Azure OpenAI', piId: 'azure-openai-responses' },
|
{ key: 'Azure OpenAI', providerId: 'azure-openai-responses' },
|
||||||
{ key: 'OpenCode Zen', piId: 'opencode' },
|
{ key: 'OpenCode Zen', providerId: 'opencode' },
|
||||||
{ key: 'ZAI', piId: 'zai' },
|
{ key: 'ZAI', providerId: 'zai' },
|
||||||
{ key: 'Cerebras', piId: 'cerebras' },
|
{ key: 'Cerebras', providerId: 'cerebras' },
|
||||||
];
|
];
|
||||||
|
|
||||||
piMonoRouter.get('/providers', async (ctx) => {
|
chatProvidersRouter.get('/providers', async (ctx) => {
|
||||||
return ctx.json(PROVIDERS);
|
return ctx.json(PROVIDERS);
|
||||||
});
|
});
|
||||||
|
|
||||||
piMonoRouter.get('/auth', async (ctx) => {
|
chatProvidersRouter.get('/auth', async (ctx) => {
|
||||||
const auth = await readAuthJson();
|
const auth = await readAuthJson();
|
||||||
const providers = PROVIDERS.filter((p) => auth[p.piId]?.key?.trim()).map((p) => p.key);
|
const providers = PROVIDERS.filter((p) => auth[p.providerId]?.key?.trim()).map((p) => p.key);
|
||||||
return ctx.json({ authenticated: providers.length > 0, providers });
|
return ctx.json({ authenticated: providers.length > 0, providers });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -381,18 +381,18 @@ const maskValue = (value: string) => {
|
|||||||
return value.slice(0, 3) + '...' + value.slice(-3);
|
return value.slice(0, 3) + '...' + value.slice(-3);
|
||||||
};
|
};
|
||||||
|
|
||||||
piMonoRouter.get('/api-keys', async (ctx) => {
|
chatProvidersRouter.get('/api-keys', async (ctx) => {
|
||||||
const auth = await readAuthJson();
|
const auth = await readAuthJson();
|
||||||
const keys = PROVIDERS.filter((p) => auth[p.piId]?.key?.trim()).map((p) => ({
|
const keys = PROVIDERS.filter((p) => auth[p.providerId]?.key?.trim()).map((p) => ({
|
||||||
provider: p.piId,
|
provider: p.providerId,
|
||||||
value: maskValue(auth[p.piId]!.key),
|
value: maskValue(auth[p.providerId]!.key),
|
||||||
}));
|
}));
|
||||||
return ctx.json({ keys });
|
return ctx.json({ keys });
|
||||||
});
|
});
|
||||||
|
|
||||||
piMonoRouter.put('/api-keys', async (ctx) => {
|
chatProvidersRouter.put('/api-keys', async (ctx) => {
|
||||||
const { provider, value } = await ctx.req.json<{ provider: string; value: string }>();
|
const { provider, value } = await ctx.req.json<{ provider: string; value: string }>();
|
||||||
if (!PROVIDERS.some((p) => p.piId === provider)) return ctx.json({ error: 'Invalid provider' }, 400);
|
if (!PROVIDERS.some((p) => p.providerId === provider)) return ctx.json({ error: 'Invalid provider' }, 400);
|
||||||
|
|
||||||
const auth = await readAuthJson();
|
const auth = await readAuthJson();
|
||||||
if (value.trim()) {
|
if (value.trim()) {
|
||||||
@@ -406,14 +406,14 @@ piMonoRouter.put('/api-keys', async (ctx) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
type AccessPolicy = { allowedModels: string[] };
|
type AccessPolicy = { allowedModels: string[] };
|
||||||
const ACCESS_POLICY_KEY = 'pi-access-policy';
|
const ACCESS_POLICY_KEY = 'chat-access-policy';
|
||||||
|
|
||||||
piMonoRouter.get('/access-policy', async (ctx) => {
|
chatProvidersRouter.get('/access-policy', async (ctx) => {
|
||||||
const policy = await readConfigValue<AccessPolicy>(ACCESS_POLICY_KEY, { allowedModels: [] });
|
const policy = await readConfigValue<AccessPolicy>(ACCESS_POLICY_KEY, { allowedModels: [] });
|
||||||
return ctx.json(policy);
|
return ctx.json(policy);
|
||||||
});
|
});
|
||||||
|
|
||||||
piMonoRouter.put('/access-policy', async (ctx) => {
|
chatProvidersRouter.put('/access-policy', async (ctx) => {
|
||||||
const body = await ctx.req.json<AccessPolicy>();
|
const body = await ctx.req.json<AccessPolicy>();
|
||||||
await writeConfigValue(ACCESS_POLICY_KEY, body);
|
await writeConfigValue(ACCESS_POLICY_KEY, body);
|
||||||
return ctx.json(body);
|
return ctx.json(body);
|
||||||
@@ -441,26 +441,26 @@ const REMOTE_HEALTH_CONFIG: Record<
|
|||||||
zai: { url: 'https://opencode.ai/v1/models', headers: (k) => ({ Authorization: `Bearer ${k}` }) },
|
zai: { url: 'https://opencode.ai/v1/models', headers: (k) => ({ Authorization: `Bearer ${k}` }) },
|
||||||
};
|
};
|
||||||
|
|
||||||
piMonoRouter.get('/api-keys/health', async (ctx) => {
|
chatProvidersRouter.get('/api-keys/health', async (ctx) => {
|
||||||
const auth = await readAuthJson();
|
const auth = await readAuthJson();
|
||||||
const results: Record<string, boolean | null> = {};
|
const results: Record<string, boolean | null> = {};
|
||||||
|
|
||||||
const checks = PROVIDERS.filter((p) => auth[p.piId]?.key?.trim()).map(async (p) => {
|
const checks = PROVIDERS.filter((p) => auth[p.providerId]?.key?.trim()).map(async (p) => {
|
||||||
const config = REMOTE_HEALTH_CONFIG[p.piId];
|
const config = REMOTE_HEALTH_CONFIG[p.providerId];
|
||||||
if (!config) {
|
if (!config) {
|
||||||
results[p.piId] = null;
|
results[p.providerId] = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const key = auth[p.piId]!.key;
|
const key = auth[p.providerId]!.key;
|
||||||
const url = typeof config.url === 'function' ? config.url(key) : config.url;
|
const url = typeof config.url === 'function' ? config.url(key) : config.url;
|
||||||
const headers = config.headers(key);
|
const headers = config.headers(key);
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timer = setTimeout(() => controller.abort(), 3000);
|
const timer = setTimeout(() => controller.abort(), 3000);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, { headers, signal: controller.signal });
|
const res = await fetch(url, { headers, signal: controller.signal });
|
||||||
results[p.piId] = res.ok;
|
results[p.providerId] = res.ok;
|
||||||
} catch {
|
} catch {
|
||||||
results[p.piId] = false;
|
results[p.providerId] = false;
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
@@ -524,12 +524,12 @@ async function getPiVersion(): Promise<{ version: string | null; path: string |
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
piMonoRouter.get('/version', async (ctx) => {
|
chatProvidersRouter.get('/version', async (ctx) => {
|
||||||
const { version, path } = await getPiVersion();
|
const { version, path } = await getPiVersion();
|
||||||
return ctx.json({ version, path, globalPath: path });
|
return ctx.json({ version, path, globalPath: path });
|
||||||
});
|
});
|
||||||
|
|
||||||
piMonoRouter.post('/install', async (ctx) => {
|
chatProvidersRouter.post('/install', async (ctx) => {
|
||||||
try {
|
try {
|
||||||
const proc = Bun.spawn(['npm', 'install', '-g', '@mariozechner/pi-coding-agent'], {
|
const proc = Bun.spawn(['npm', 'install', '-g', '@mariozechner/pi-coding-agent'], {
|
||||||
stdout: 'pipe',
|
stdout: 'pipe',
|
||||||
@@ -552,7 +552,7 @@ piMonoRouter.post('/install', async (ctx) => {
|
|||||||
|
|
||||||
// --- Local providers ---
|
// --- Local providers ---
|
||||||
|
|
||||||
piMonoRouter.get('/local-providers', async (ctx) => {
|
chatProvidersRouter.get('/local-providers', async (ctx) => {
|
||||||
const providers = await readLocalProviders();
|
const providers = await readLocalProviders();
|
||||||
return ctx.json(
|
return ctx.json(
|
||||||
providers.map((p) => ({
|
providers.map((p) => ({
|
||||||
@@ -562,14 +562,14 @@ piMonoRouter.get('/local-providers', async (ctx) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
piMonoRouter.post('/local-providers/probe', async (ctx) => {
|
chatProvidersRouter.post('/local-providers/probe', async (ctx) => {
|
||||||
const { url, auth } = await ctx.req.json<{ url: string; auth?: LocalProvider['auth'] }>();
|
const { url, auth } = await ctx.req.json<{ url: string; auth?: LocalProvider['auth'] }>();
|
||||||
if (!url?.trim()) return ctx.json({ success: false, error: 'URL is required' }, 400);
|
if (!url?.trim()) return ctx.json({ success: false, error: 'URL is required' }, 400);
|
||||||
const result = await probeUrl(url.trim(), auth);
|
const result = await probeUrl(url.trim(), auth);
|
||||||
return ctx.json(result);
|
return ctx.json(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
piMonoRouter.post('/local-providers', async (ctx) => {
|
chatProvidersRouter.post('/local-providers', async (ctx) => {
|
||||||
const body = await ctx.req.json<{
|
const body = await ctx.req.json<{
|
||||||
url: string;
|
url: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -589,19 +589,19 @@ piMonoRouter.post('/local-providers', async (ctx) => {
|
|||||||
return ctx.json({ ...provider, auth: provider.auth ? { type: provider.auth.type } : undefined });
|
return ctx.json({ ...provider, auth: provider.auth ? { type: provider.auth.type } : undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
piMonoRouter.delete('/local-providers/:id', async (ctx) => {
|
chatProvidersRouter.delete('/local-providers/:id', async (ctx) => {
|
||||||
const { id } = ctx.req.param();
|
const { id } = ctx.req.param();
|
||||||
const removed = await removeLocalProviderFromModelsConfig(id);
|
const removed = await removeLocalProviderFromModelsConfig(id);
|
||||||
if (!removed) return ctx.json({ error: 'Not found' }, 404);
|
if (!removed) return ctx.json({ error: 'Not found' }, 404);
|
||||||
return ctx.json({ ok: true });
|
return ctx.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
piMonoRouter.post('/local-providers/refresh', async (ctx) => {
|
chatProvidersRouter.post('/local-providers/refresh', async (ctx) => {
|
||||||
await refreshLocalProviders();
|
await refreshLocalProviders();
|
||||||
return ctx.json({ ok: true });
|
return ctx.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
piMonoRouter.get('/local-providers/health', async (ctx) => {
|
chatProvidersRouter.get('/local-providers/health', async (ctx) => {
|
||||||
const providers = await readLocalProviders();
|
const providers = await readLocalProviders();
|
||||||
const results: Record<string, boolean> = {};
|
const results: Record<string, boolean> = {};
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ import { readdirSync, existsSync } from 'node:fs';
|
|||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { readServerSettings, writeServerSettings } from 'officerdb';
|
import { readServerSettings, writeServerSettings } from 'officerdb';
|
||||||
import { claudeCodeRouter } from './claude-code';
|
import { claudeCodeRouter } from './claude-code';
|
||||||
import { piMonoRouter } from './pi-mono';
|
import { chatProvidersRouter } from './chat-providers';
|
||||||
import { applicationsRouter } from './applications';
|
import { applicationsRouter } from './applications';
|
||||||
import { smtpRouter } from './smtp';
|
import { smtpRouter } from './smtp';
|
||||||
import { ttsRouter } from './tts';
|
import { ttsRouter } from './tts';
|
||||||
@@ -13,7 +13,7 @@ import { ocrRouter } from './ocr';
|
|||||||
export const serverSettingsRouter = createRouter();
|
export const serverSettingsRouter = createRouter();
|
||||||
|
|
||||||
serverSettingsRouter.route('/claude-code', claudeCodeRouter);
|
serverSettingsRouter.route('/claude-code', claudeCodeRouter);
|
||||||
serverSettingsRouter.route('/pi-mono', piMonoRouter);
|
serverSettingsRouter.route('/chat-providers', chatProvidersRouter);
|
||||||
serverSettingsRouter.route('/applications', applicationsRouter);
|
serverSettingsRouter.route('/applications', applicationsRouter);
|
||||||
serverSettingsRouter.route('/smtp', smtpRouter);
|
serverSettingsRouter.route('/smtp', smtpRouter);
|
||||||
serverSettingsRouter.route('/tts', ttsRouter);
|
serverSettingsRouter.route('/tts', ttsRouter);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getUserSettings, setUserSettings, getUserState, patchUserState } from '
|
|||||||
|
|
||||||
const DEFAULT_SETTINGS = {
|
const DEFAULT_SETTINGS = {
|
||||||
chat: {
|
chat: {
|
||||||
defaultProvider: 'pi',
|
defaultProvider: 'chat',
|
||||||
defaultModel: null,
|
defaultModel: null,
|
||||||
systemPrompt: '',
|
systemPrompt: '',
|
||||||
temperature: 1,
|
temperature: 1,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const TYPING_INTERVAL_MS = 8_000;
|
|||||||
type SendableChannel = { send: (content: string) => Promise<unknown> };
|
type SendableChannel = { send: (content: string) => Promise<unknown> };
|
||||||
|
|
||||||
type AccessPolicy = { allowedModels: string[] };
|
type AccessPolicy = { allowedModels: string[] };
|
||||||
const ACCESS_POLICY_KEY = 'pi-access-policy';
|
const ACCESS_POLICY_KEY = 'chat-access-policy';
|
||||||
|
|
||||||
async function getVisibleModels(): Promise<ModelInfo[]> {
|
async function getVisibleModels(): Promise<ModelInfo[]> {
|
||||||
const allModels = await listChatModels();
|
const allModels = await listChatModels();
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const TYPING_INTERVAL_MS = 5_000;
|
|||||||
type SendFn = (text: string) => Promise<unknown>;
|
type SendFn = (text: string) => Promise<unknown>;
|
||||||
|
|
||||||
type AccessPolicy = { allowedModels: string[] };
|
type AccessPolicy = { allowedModels: string[] };
|
||||||
const ACCESS_POLICY_KEY = 'pi-access-policy';
|
const ACCESS_POLICY_KEY = 'chat-access-policy';
|
||||||
|
|
||||||
async function getVisibleModels(): Promise<ModelInfo[]> {
|
async function getVisibleModels(): Promise<ModelInfo[]> {
|
||||||
const allModels = await listChatModels();
|
const allModels = await listChatModels();
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const TYPING_INTERVAL_MS = 5_000;
|
|||||||
type SendFn = (text: string) => Promise<unknown>;
|
type SendFn = (text: string) => Promise<unknown>;
|
||||||
|
|
||||||
type AccessPolicy = { allowedModels: string[] };
|
type AccessPolicy = { allowedModels: string[] };
|
||||||
const ACCESS_POLICY_KEY = 'pi-access-policy';
|
const ACCESS_POLICY_KEY = 'chat-access-policy';
|
||||||
|
|
||||||
function extractPhone(waId: string): string {
|
function extractPhone(waId: string): string {
|
||||||
// WhatsApp ID format: 5511999999999@c.us → 5511999999999
|
// WhatsApp ID format: 5511999999999@c.us → 5511999999999
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ export const ensureItemDirs = () => {
|
|||||||
|
|
||||||
export const SERVER_CONFIG_DIR = join(DATA_PATH, 'server-settings');
|
export const SERVER_CONFIG_DIR = join(DATA_PATH, 'server-settings');
|
||||||
|
|
||||||
export const PI_CONFIG_DIR = join(homedir(), '.pi', 'agent');
|
// The `pi`/opencode agent's own config dir (auth.json + models.json). External-tool path.
|
||||||
|
export const AGENT_CONFIG_DIR = join(homedir(), '.pi', 'agent');
|
||||||
|
|
||||||
export const SEED_PATH = resolve(import.meta.dir, '../../seed');
|
export const SEED_PATH = resolve(import.meta.dir, '../../seed');
|
||||||
|
|
||||||
@@ -31,11 +32,6 @@ export const getClaudeDir = (email: string) => join(DATA_PATH, email, 'chat_sess
|
|||||||
export const getSessionDir = (email: string, sessionId: string) =>
|
export const getSessionDir = (email: string, sessionId: string) =>
|
||||||
join(DATA_PATH, email, 'chat_sessions', 'claude', sessionId);
|
join(DATA_PATH, email, 'chat_sessions', 'claude', sessionId);
|
||||||
|
|
||||||
export const getPiMonoDir = (email: string) => join(DATA_PATH, email, 'chat_sessions', 'pi-mono');
|
|
||||||
|
|
||||||
export const getPiMonoSessionDir = (email: string, sessionId: string) =>
|
|
||||||
join(DATA_PATH, email, 'chat_sessions', 'pi-mono', sessionId);
|
|
||||||
|
|
||||||
export const getArchivedSessionDir = (email: string, sessionId: string) =>
|
export const getArchivedSessionDir = (email: string, sessionId: string) =>
|
||||||
join(DATA_PATH, email, 'chat_sessions', 'claude', 'archived', sessionId);
|
join(DATA_PATH, email, 'chat_sessions', 'claude', 'archived', sessionId);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Claude Code NDJSON Stream Parser
|
* Claude Code NDJSON Stream Parser
|
||||||
*
|
*
|
||||||
* Parses the stream-json output format from Claude Code CLI into PiEvents.
|
* Parses the stream-json output format from Claude Code CLI into ChatEvents.
|
||||||
* Handles: stream_event (deltas), assistant (text/tool_use), user (tool_result),
|
* Handles: stream_event (deltas), assistant (text/tool_use), user (tool_result),
|
||||||
* system init, and result messages.
|
* system init, and result messages.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export function ModelSelector({
|
|||||||
<div className="flex items-center justify-between mt-2">
|
<div className="flex items-center justify-between mt-2">
|
||||||
{isLocked ? (
|
{isLocked ? (
|
||||||
<span className="rounded-md bg-duck-dark/80 px-3 py-1 text-xs font-medium text-white">
|
<span className="rounded-md bg-duck-dark/80 px-3 py-1 text-xs font-medium text-white">
|
||||||
{activeProvider ? displayName(activeProvider) : 'Pi'}
|
{activeProvider ? displayName(activeProvider) : 'Chat'}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-1 rounded-lg bg-background/60 p-1">
|
<div className="flex items-center gap-1 rounded-lg bg-background/60 p-1">
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ export function useAccessPolicy() {
|
|||||||
const { data: policy = { allowedModels: [] } } = useQuery<AccessPolicy>({
|
const { data: policy = { allowedModels: [] } } = useQuery<AccessPolicy>({
|
||||||
queryKey: QUERY_KEY,
|
queryKey: QUERY_KEY,
|
||||||
enabled: isAuthenticated,
|
enabled: isAuthenticated,
|
||||||
queryFn: () => client.get<AccessPolicy>('/server-settings/pi-mono/access-policy'),
|
queryFn: () => client.get<AccessPolicy>('/server-settings/chat-providers/access-policy'),
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const savePolicy = useCallback(
|
const savePolicy = useCallback(
|
||||||
async (newPolicy: AccessPolicy) => {
|
async (newPolicy: AccessPolicy) => {
|
||||||
queryClient.setQueryData(QUERY_KEY, newPolicy);
|
queryClient.setQueryData(QUERY_KEY, newPolicy);
|
||||||
await client.put('/server-settings/pi-mono/access-policy', newPolicy);
|
await client.put('/server-settings/chat-providers/access-policy', newPolicy);
|
||||||
},
|
},
|
||||||
[client, queryClient],
|
[client, queryClient],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export type UseSettingsType = ReturnType<typeof useSettings>;
|
|||||||
|
|
||||||
export type UserSettings = {
|
export type UserSettings = {
|
||||||
chat: {
|
chat: {
|
||||||
defaultProvider: 'pi';
|
defaultProvider: 'chat';
|
||||||
defaultModel: string | null;
|
defaultModel: string | null;
|
||||||
defaultProjectModel: string | null;
|
defaultProjectModel: string | null;
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
@@ -63,7 +63,7 @@ export type UserSettings = {
|
|||||||
disabledProviders: string[];
|
disabledProviders: string[];
|
||||||
};
|
};
|
||||||
tasks: {
|
tasks: {
|
||||||
defaultProvider: 'pi';
|
defaultProvider: 'chat';
|
||||||
defaultModel: string | null;
|
defaultModel: string | null;
|
||||||
};
|
};
|
||||||
appearance: {
|
appearance: {
|
||||||
@@ -87,7 +87,7 @@ export type UserState = Record<string, unknown>;
|
|||||||
|
|
||||||
export const DEFAULT_SETTINGS: UserSettings = {
|
export const DEFAULT_SETTINGS: UserSettings = {
|
||||||
chat: {
|
chat: {
|
||||||
defaultProvider: 'pi',
|
defaultProvider: 'chat',
|
||||||
defaultModel: 'claude-code',
|
defaultModel: 'claude-code',
|
||||||
defaultProjectModel: 'claude-code',
|
defaultProjectModel: 'claude-code',
|
||||||
systemPrompt: '',
|
systemPrompt: '',
|
||||||
@@ -100,7 +100,7 @@ export const DEFAULT_SETTINGS: UserSettings = {
|
|||||||
disabledProviders: [],
|
disabledProviders: [],
|
||||||
},
|
},
|
||||||
tasks: {
|
tasks: {
|
||||||
defaultProvider: 'pi',
|
defaultProvider: 'chat',
|
||||||
defaultModel: 'claude-code',
|
defaultModel: 'claude-code',
|
||||||
},
|
},
|
||||||
appearance: {
|
appearance: {
|
||||||
|
|||||||
Reference in New Issue
Block a user