fix: Pi harness - dynamic models, correct RPC protocol, proper event handling

Backend:
- /api/pi/models now calls 'pi --list-models' with stored API keys
- pi-bridge.ts: callback-based event handling (matches pi-monorepo)
- pi-bridge.ts: correct RPC format (type: 'prompt' not jsonrpc)
- pi-bridge.ts: pass API keys to Pi process env
- websocket.ts: event handler runs in background, no blocking
- rest.ts: fix user home path (getHomeDir instead of hardcoded)

Frontend:
- Fix /api/ double prefix in useChatSessions, useChatGroups, useModels
- Add PROVIDER_DISPLAY mapping in SystemSettings.tsx
- Provider tabs show friendly names (e.g., 'OpenCode Zen')

UI (from previous session):
- Grouped session list with collapsible folders
- CreateGroupDialog, GroupContextMenu, SessionContextMenu components
This commit is contained in:
2026-02-20 22:55:00 +00:00
parent ca497f9eff
commit 68c7973281
11 changed files with 978 additions and 457 deletions
@@ -0,0 +1,138 @@
import { useState } from 'react';
import { X } from 'lucide-react';
import { useChatGroups } from '@/state/useChatGroups';
type CreateGroupDialogProps = {
onClose: () => void;
};
function toSlug(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
}
export function CreateGroupDialog({ onClose }: CreateGroupDialogProps) {
const { createGroup } = useChatGroups();
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!name.trim()) {
setError('Name is required');
return;
}
const slug = toSlug(name);
if (!slug) {
setError('Name must contain at least one alphanumeric character');
return;
}
setIsSubmitting(true);
setError(null);
try {
await createGroup(name.trim(), slug, description.trim() || undefined);
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create group');
setIsSubmitting(false);
}
}
function handleBackdropClick(e: React.MouseEvent) {
if (e.target === e.currentTarget) {
onClose();
}
}
return (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
onClick={handleBackdropClick}
>
<div className="bg-background border border-duck-dark/10 dark:border-foreground/10 rounded-lg shadow-xl w-full max-w-md mx-4">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-duck-dark/10 dark:border-foreground/10">
<h2 className="text-lg font-semibold text-duck-dark dark:text-foreground">Create Group</h2>
<button
onClick={onClose}
className="p-1 rounded-md hover:bg-duck-dark/10 dark:hover:bg-foreground/10 transition-colors"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="p-6 space-y-4">
<div>
<label htmlFor="group-name" className="block text-sm font-medium text-duck-dark/70 dark:text-foreground/70 mb-1.5">
Name <span className="text-red-500">*</span>
</label>
<input
id="group-name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g., Work Projects"
className="w-full px-3 py-2 rounded-md border border-duck-dark/20 dark:border-foreground/20 bg-background text-duck-dark dark:text-foreground placeholder:text-duck-dark/40 dark:placeholder:text-foreground/40 focus:outline-none focus:ring-2 focus:ring-duck-teal/50"
autoFocus
disabled={isSubmitting}
/>
{name && (
<p className="mt-1 text-xs text-duck-dark/40 dark:text-foreground/40">
Slug: <span className="font-mono">{toSlug(name) || '(invalid)'}</span>
</p>
)}
</div>
<div>
<label htmlFor="group-description" className="block text-sm font-medium text-duck-dark/70 dark:text-foreground/70 mb-1.5">
Description <span className="text-duck-dark/40 dark:text-foreground/40">(optional)</span>
</label>
<textarea
id="group-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Add a description for this group..."
className="w-full px-3 py-2 rounded-md border border-duck-dark/20 dark:border-foreground/20 bg-background text-duck-dark dark:text-foreground placeholder:text-duck-dark/40 dark:placeholder:text-foreground/40 focus:outline-none focus:ring-2 focus:ring-duck-teal/50 resize-none"
rows={3}
disabled={isSubmitting}
/>
</div>
{error && (
<div className="p-3 rounded-md bg-red-500/10 border border-red-500/20 text-red-600 dark:text-red-400 text-sm">
{error}
</div>
)}
{/* Actions */}
<div className="flex justify-end gap-3 pt-2">
<button
type="button"
onClick={onClose}
disabled={isSubmitting}
className="px-4 py-2 text-sm font-medium text-duck-dark/70 dark:text-foreground/70 hover:bg-duck-dark/10 dark:hover:bg-foreground/10 rounded-md transition-colors disabled:opacity-50"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting || !name.trim()}
className="px-4 py-2 text-sm font-medium bg-duck-teal hover:bg-duck-teal/90 text-duck-yellow rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? 'Creating...' : 'Create Group'}
</button>
</div>
</form>
</div>
</div>
);
}
@@ -0,0 +1,122 @@
import { useState } from 'react';
import { MoreVertical, Edit2, Trash2 } from 'lucide-react';
import { useChatGroups } from '@/state/useChatGroups';
import type { GroupEntry } from 'apps/Chat';
type GroupContextMenuProps = {
group: GroupEntry;
};
export function GroupContextMenu({ group }: GroupContextMenuProps) {
const { updateGroup, deleteGroup } = useChatGroups();
const [isOpen, setIsOpen] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [editName, setEditName] = useState(group.name);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
async function handleRename() {
if (editName.trim() && editName !== group.name) {
await updateGroup(group.slug, { name: editName.trim() });
}
setIsEditing(false);
setIsOpen(false);
}
async function handleDelete() {
await deleteGroup(group.slug);
setShowDeleteConfirm(false);
setIsOpen(false);
}
function handleButtonClick(e: React.MouseEvent) {
e.stopPropagation();
setIsOpen(!isOpen);
}
function handleBackdropClick() {
setIsOpen(false);
setShowDeleteConfirm(false);
}
if (isEditing) {
return (
<div className="flex items-center gap-2 ml-auto" onClick={(e) => e.stopPropagation()}>
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleRename();
if (e.key === 'Escape') {
setEditName(group.name);
setIsEditing(false);
}
}}
className="px-2 py-1 text-sm border border-duck-dark/20 dark:border-foreground/20 rounded bg-background focus:outline-none focus:ring-2 focus:ring-duck-teal/50"
autoFocus
onBlur={handleRename}
/>
</div>
);
}
return (
<div className="relative ml-auto" onClick={(e) => e.stopPropagation()}>
<button
onClick={handleButtonClick}
className="p-1 rounded-md hover:bg-duck-dark/10 dark:hover:bg-foreground/10 transition-colors"
>
<MoreVertical className="h-4 w-4" />
</button>
{isOpen && (
<>
<div className="fixed inset-0 z-40" onClick={handleBackdropClick} />
<div className="absolute right-0 top-full mt-1 z-50 min-w-[160px] bg-background border border-duck-dark/10 dark:border-foreground/10 rounded-md shadow-lg overflow-hidden">
{!showDeleteConfirm ? (
<>
<button
onClick={() => {
setIsEditing(true);
setIsOpen(false);
}}
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-duck-dark/5 dark:hover:bg-foreground/5 transition-colors text-left"
>
<Edit2 className="h-4 w-4" />
Rename
</button>
<button
onClick={() => setShowDeleteConfirm(true)}
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-red-500/10 text-red-600 dark:text-red-400 transition-colors text-left"
>
<Trash2 className="h-4 w-4" />
Delete
</button>
</>
) : (
<div className="p-3 space-y-2">
<p className="text-sm text-duck-dark/70 dark:text-foreground/70">
Delete "{group.name}"? Sessions will be ungrouped.
</p>
<div className="flex gap-2">
<button
onClick={() => setShowDeleteConfirm(false)}
className="flex-1 px-3 py-1.5 text-xs rounded-md bg-duck-dark/10 dark:bg-foreground/10 hover:bg-duck-dark/20 dark:hover:bg-foreground/20 transition-colors"
>
Cancel
</button>
<button
onClick={handleDelete}
className="flex-1 px-3 py-1.5 text-xs rounded-md bg-red-500 hover:bg-red-600 text-white transition-colors"
>
Delete
</button>
</div>
</div>
)}
</div>
</>
)}
</div>
);
}
@@ -1,12 +1,20 @@
import { useEffect, useRef, useCallback } from 'react';
import { Plus, MessageSquare, Trash2 } from 'lucide-react';
import { useEffect, useRef, useCallback, useState } from 'react';
import { Plus, MessageSquare, Folder, ChevronRight, FolderPlus } from 'lucide-react';
import { usePanelChannel } from 'hooks/usePanelChannel';
import { useChatSessions } from '@/state/useChatSessions';
import { useChatGroups } from '@/state/useChatGroups';
import type { SelectedSession } from './ChatDetailPanel';
import { CreateGroupDialog } from './CreateGroupDialog';
import { GroupContextMenu } from './GroupContextMenu';
import { SessionContextMenu } from './SessionContextMenu';
export const SessionList = () => {
const { sessions, deleteSession } = useChatSessions();
const { groups } = useChatGroups();
const [selected, setSelected] = usePanelChannel<SelectedSession>('chat:selected-session', null);
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
const [showCreateGroup, setShowCreateGroup] = useState(false);
const scrolledRef = useRef(false);
const selectedRef = useCallback(
(node: HTMLDivElement | null) => {
@@ -22,6 +30,20 @@ export const SessionList = () => {
scrolledRef.current = false;
}, [selected?.id]);
const toggleGroup = (slug: string) => {
setCollapsed(prev => {
const next = new Set(prev);
next.has(slug) ? next.delete(slug) : next.add(slug);
return next;
});
};
const ungrouped = sessions.filter(s => !s.groupSlug);
const grouped = groups.map(g => ({
...g,
sessions: sessions.filter(s => s.groupSlug === g.slug)
}));
const handleSelect = (session: (typeof sessions)[number]) => {
setSelected({ id: session.id, model: session.model ?? null });
window.history.replaceState(null, '', `/chat/${session.id}`);
@@ -35,80 +57,117 @@ export const SessionList = () => {
await deleteSession(id);
};
const renderSession = (session: (typeof sessions)[number]) => {
const isSelected = selected?.id === session.id;
return (
<div
key={session.id}
ref={isSelected ? selectedRef : undefined}
className={`group flex items-center rounded-lg border transition-colors cursor-pointer ${
isSelected
? 'border-duck-teal/30 bg-duck-teal/5 dark:bg-duck-teal/10'
: 'border-duck-dark/10 dark:border-foreground/10 bg-background/80 hover:bg-background/90'
}`}
>
<button
onClick={() => handleSelect(session)}
className="flex-1 flex items-center gap-3 px-4 py-3 min-w-0 text-left cursor-pointer"
>
<MessageSquare className="h-4 w-4 shrink-0 text-duck-teal/60" />
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-duck-dark/80 dark:text-foreground/80 truncate">
{session.title}
<span className="ml-1.5 font-mono text-xs font-normal text-duck-dark/25 dark:text-foreground/25">
({session.id.slice(0, 8)})
</span>
</div>
<div className="text-xs text-duck-dark/40 dark:text-foreground/40">
{new Date(session.createdAt).toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</div>
{session.model && (
<div className="text-xs text-duck-teal/70 dark:text-duck-teal/60 truncate">
{session.model.includes('/') ? session.model.replace('/', ' - ') : session.model}
</div>
)}
</div>
</button>
<div className="shrink-0 mr-2">
<SessionContextMenu session={session} onDelete={handleDelete} />
</div>
</div>
);
};
return (
<div className="flex flex-col h-full overflow-hidden">
{/* Header */}
<div className="shrink-0 flex items-center justify-between px-4 py-2 border-b border-duck-dark/10 dark:border-foreground/10 bg-background/60">
<h2 className="text-sm font-medium text-duck-dark/70 dark:text-foreground/70">Sessions</h2>
<button
onClick={() => {
setSelected({ id: 'new' });
window.history.replaceState(null, '', '/chat/new');
}}
className="flex items-center gap-1.5 rounded-md bg-duck-teal hover:bg-duck-teal/90 text-duck-yellow cursor-pointer h-7 px-3 text-xs font-medium"
>
<Plus className="h-3.5 w-3.5" />
New Chat
</button>
<div className="flex gap-2">
<button
onClick={() => setShowCreateGroup(true)}
className="flex items-center gap-1.5 rounded-md bg-duck-dark/10 hover:bg-duck-dark/20 dark:bg-foreground/10 dark:hover:bg-foreground/20 text-duck-dark dark:text-foreground cursor-pointer h-7 px-3 text-xs font-medium"
>
<FolderPlus className="h-3.5 w-3.5" />
New Group
</button>
<button
onClick={() => {
setSelected({ id: 'new' });
window.history.replaceState(null, '', '/chat/new');
}}
className="flex items-center gap-1.5 rounded-md bg-duck-teal hover:bg-duck-teal/90 text-duck-yellow cursor-pointer h-7 px-3 text-xs font-medium"
>
<Plus className="h-3.5 w-3.5" />
New Chat
</button>
</div>
</div>
{/* Session list */}
<div className="flex-1 min-h-0 overflow-y-auto p-3 space-y-1.5">
<div className="flex-1 min-h-0 overflow-y-auto p-3 space-y-3">
{sessions.length === 0 && (
<div className="text-center py-16 text-duck-dark/30 dark:text-foreground/30 text-sm">
No sessions yet. Start a new chat!
</div>
)}
{sessions.map((session) => {
const isSelected = selected?.id === session.id;
return (
<div
key={session.id}
ref={isSelected ? selectedRef : undefined}
className={`group flex items-center rounded-lg border transition-colors cursor-pointer ${
isSelected
? 'border-duck-teal/30 bg-duck-teal/5 dark:bg-duck-teal/10'
: 'border-duck-dark/10 dark:border-foreground/10 bg-background/80 hover:bg-background/90'
}`}
>
<button
onClick={() => handleSelect(session)}
className="flex-1 flex items-center gap-3 px-4 py-3 min-w-0 text-left cursor-pointer"
>
<MessageSquare className="h-4 w-4 shrink-0 text-duck-teal/60" />
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-duck-dark/80 dark:text-foreground/80 truncate">
{session.title}
<span className="ml-1.5 font-mono text-xs font-normal text-duck-dark/25 dark:text-foreground/25">
({session.id.slice(0, 8)})
</span>
</div>
<div className="text-xs text-duck-dark/40 dark:text-foreground/40">
{new Date(session.createdAt).toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</div>
{session.model && (
<div className="text-xs text-duck-teal/70 dark:text-duck-teal/60 truncate">
{session.model.includes('/') ? session.model.replace('/', ' - ') : session.model}
</div>
)}
</div>
</button>
<button
onClick={() => handleDelete(session.id)}
className="shrink-0 p-2 mr-2 text-duck-dark/20 dark:text-foreground/20 hover:text-red-500 md:opacity-0 md:group-hover:opacity-100 transition-opacity cursor-pointer"
>
<Trash2 className="h-4 w-4" />
</button>
{/* Ungrouped sessions */}
{ungrouped.length > 0 && (
<div className="space-y-1.5">
{ungrouped.map(renderSession)}
</div>
)}
{/* Grouped sessions */}
{grouped.map((group) => (
<div key={group.slug} className="space-y-1.5">
<div className="flex items-center gap-2 px-3 py-2 cursor-pointer rounded-md hover:bg-duck-dark/5 dark:hover:bg-foreground/5 transition-colors">
<div onClick={() => toggleGroup(group.slug)} className="flex items-center gap-2 flex-1 min-w-0">
<ChevronRight
className={`h-4 w-4 transition-transform ${!collapsed.has(group.slug) ? 'rotate-90' : ''}`}
/>
<Folder className="h-4 w-4 text-duck-teal" />
<span className="font-medium text-sm text-duck-dark/70 dark:text-foreground/70">{group.name}</span>
<span className="text-xs text-duck-dark/40 dark:text-foreground/40">({group.sessionCount})</span>
</div>
<GroupContextMenu group={group} />
</div>
);
})}
{!collapsed.has(group.slug) && (
<div className="ml-6 space-y-1.5">
{group.sessions.map(renderSession)}
</div>
)}
</div>
))}
</div>
{showCreateGroup && <CreateGroupDialog onClose={() => setShowCreateGroup(false)} />}
</div>
);
};
@@ -0,0 +1,141 @@
import { useState } from 'react';
import { MoreVertical, FolderInput, Edit2, Trash2 } from 'lucide-react';
import { useChatGroups } from '@/state/useChatGroups';
import { useChatSessions } from '@/state/useChatSessions';
import type { SessionEntry } from 'apps/Chat';
type SessionContextMenuProps = {
session: SessionEntry;
onDelete: (id: string) => void;
};
export function SessionContextMenu({ session, onDelete }: SessionContextMenuProps) {
const { groups, moveSession } = useChatGroups();
const { renameSession } = useChatSessions();
const [isOpen, setIsOpen] = useState(false);
const [showMoveMenu, setShowMoveMenu] = useState(false);
const [isRenaming, setIsRenaming] = useState(false);
const [editTitle, setEditTitle] = useState(session.title);
async function handleMove(groupSlug: string | null) {
await moveSession(session.id, groupSlug);
setIsOpen(false);
setShowMoveMenu(false);
}
async function handleRename() {
if (editTitle.trim() && editTitle !== session.title) {
await renameSession(session.id, editTitle.trim());
}
setIsRenaming(false);
setIsOpen(false);
}
function handleButtonClick(e: React.MouseEvent) {
e.stopPropagation();
setIsOpen(!isOpen);
}
function handleBackdropClick() {
setIsOpen(false);
setShowMoveMenu(false);
}
if (isRenaming) {
return (
<div className="flex items-center gap-2 ml-2" onClick={(e) => e.stopPropagation()}>
<input
type="text"
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleRename();
if (e.key === 'Escape') {
setEditTitle(session.title);
setIsRenaming(false);
}
}}
className="px-2 py-1 text-sm border border-duck-dark/20 dark:border-foreground/20 rounded bg-background focus:outline-none focus:ring-2 focus:ring-duck-teal/50"
autoFocus
onBlur={handleRename}
/>
</div>
);
}
return (
<div className="relative" onClick={(e) => e.stopPropagation()}>
<button
onClick={handleButtonClick}
className="p-1 rounded-md hover:bg-duck-dark/10 dark:hover:bg-foreground/10 transition-colors opacity-0 group-hover:opacity-100"
>
<MoreVertical className="h-4 w-4" />
</button>
{isOpen && (
<>
<div className="fixed inset-0 z-40" onClick={handleBackdropClick} />
<div className="absolute right-0 top-full mt-1 z-50 min-w-[160px] bg-background border border-duck-dark/10 dark:border-foreground/10 rounded-md shadow-lg overflow-hidden">
{!showMoveMenu ? (
<>
<button
onClick={() => setShowMoveMenu(true)}
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-duck-dark/5 dark:hover:bg-foreground/5 transition-colors text-left"
>
<FolderInput className="h-4 w-4" />
Move to group
</button>
<button
onClick={() => {
setIsRenaming(true);
setIsOpen(false);
}}
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-duck-dark/5 dark:hover:bg-foreground/5 transition-colors text-left"
>
<Edit2 className="h-4 w-4" />
Rename
</button>
<button
onClick={() => onDelete(session.id)}
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-red-500/10 text-red-600 dark:text-red-400 transition-colors text-left"
>
<Trash2 className="h-4 w-4" />
Delete
</button>
</>
) : (
<>
<div className="px-3 py-2 text-xs font-medium text-duck-dark/50 dark:text-foreground/50 border-b border-duck-dark/10 dark:border-foreground/10">
Move to:
</div>
<button
onClick={() => handleMove(null)}
className="w-full px-3 py-2 text-sm hover:bg-duck-dark/5 dark:hover:bg-foreground/5 transition-colors text-left"
>
Ungrouped
</button>
{groups.map((group) => (
<button
key={group.slug}
onClick={() => handleMove(group.slug)}
className="w-full px-3 py-2 text-sm hover:bg-duck-dark/5 dark:hover:bg-foreground/5 transition-colors text-left"
>
{group.name}
</button>
))}
<div className="border-t border-duck-dark/10 dark:border-foreground/10">
<button
onClick={() => setShowMoveMenu(false)}
className="w-full px-3 py-2 text-xs text-duck-dark/50 dark:text-foreground/50 hover:bg-duck-dark/5 dark:hover:bg-foreground/5 transition-colors text-left"
>
Back
</button>
</div>
</>
)}
</div>
</>
)}
</div>
);
}
@@ -21,6 +21,27 @@ import type { UserSettings } from '@/state/types/user-settings';
import { AIHarnessesSection } from './ServerSettings/AIHarnessesSection';
import { RUN_COMMAND_CHANNEL, type RunCommandState } from './ServerSettings/run-command-channel';
const PROVIDER_DISPLAY: Record<string, string> = {
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',
};
function displayProviderName(provider: string): string {
return PROVIDER_DISPLAY[provider] ?? provider;
}
const groups: SettingsSectionGroup[] = [
{
label: 'AI',
@@ -381,7 +402,7 @@ function ModelVisibilitySection() {
: 'text-duck-dark/50 dark:text-foreground/50 hover:text-duck-dark dark:hover:text-foreground hover:bg-duck-dark/5 dark:hover:bg-foreground/5'
}`}
>
{p}
{displayProviderName(p)}
</button>
))}
</div>
+5 -5
View File
@@ -11,11 +11,11 @@ export function useChatGroups() {
const { data: groups = [] } = useQuery<GroupEntry[]>({
queryKey: ['PI_GROUPS'],
enabled: isAuthenticated,
queryFn: () => client.get<{ groups: GroupEntry[] }>('/api/pi/groups').then((r) => r.groups),
queryFn: () => client.get<{ groups: GroupEntry[] }>('/pi/groups').then((r) => r.groups),
});
async function createGroup(name: string, slug: string, description?: string, sessionIds?: string[]) {
const result = await client.post<{ group: GroupEntry }>('/api/pi/groups', {
const result = await client.post<{ group: GroupEntry }>('/pi/groups', {
name,
slug,
description,
@@ -27,18 +27,18 @@ export function useChatGroups() {
}
async function updateGroup(slug: string, updates: { name?: string; description?: string }) {
await client.patch(`/api/pi/groups/${slug}`, updates);
await client.patch(`/pi/groups/${slug}`, updates);
queryClient.invalidateQueries({ queryKey: ['PI_GROUPS'] });
}
async function deleteGroup(slug: string) {
await client.delete(`/api/pi/groups/${slug}`);
await client.delete(`/pi/groups/${slug}`);
queryClient.invalidateQueries({ queryKey: ['PI_GROUPS'] });
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS'] });
}
async function moveSession(sessionId: string, groupSlug: string | null) {
await client.post(`/api/pi/sessions/${sessionId}/move`, { groupSlug });
await client.post(`/pi/sessions/${sessionId}/move`, { groupSlug });
queryClient.invalidateQueries({ queryKey: ['PI_GROUPS'] });
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS'] });
}
@@ -28,24 +28,24 @@ export function useChatSessions() {
const { data: sessions = [] } = useQuery<SessionEntry[]>({
queryKey: ['PI_SESSIONS'],
enabled: isAuthenticated,
queryFn: () => client.post<{ sessions: SessionEntry[] }>('/api/pi/sessions').then((r) => r.sessions),
queryFn: () => client.post<{ sessions: SessionEntry[] }>('/pi/sessions').then((r) => r.sessions),
});
function getSession(sessionId: string) {
return client.get<{ session: SessionWithMessages }>(`/api/pi/sessions/${sessionId}`);
return client.get<{ session: SessionWithMessages }>(`/pi/sessions/${sessionId}`);
}
function saveMessages(sessionId: string, messages: ChatMessage[]) {
return client.put(`/api/pi/sessions/${sessionId}/messages`, messages);
return client.put(`/pi/sessions/${sessionId}/messages`, messages);
}
async function renameSession(sessionId: string, title: string) {
await client.patch(`/api/pi/sessions/${sessionId}`, { title });
await client.patch(`/pi/sessions/${sessionId}`, { title });
queryClient.invalidateQueries({ queryKey: ['PI_SESSIONS'] });
}
async function deleteSession(sessionId: string) {
await client.delete(`/api/pi/sessions/${sessionId}`);
await client.delete(`/pi/sessions/${sessionId}`);
queryClient.setQueryData<SessionEntry[]>(
['PI_SESSIONS'],
(prev) => prev?.filter((s) => s.id !== sessionId) ?? [],
@@ -53,7 +53,7 @@ export function useChatSessions() {
}
function searchSessions(query: string) {
return client.get<{ results: SessionEntry[] }>(`/api/pi/sessions/search?q=${encodeURIComponent(query)}`);
return client.get<{ results: SessionEntry[] }>(`/pi/sessions/search?q=${encodeURIComponent(query)}`);
}
return {
+1 -1
View File
@@ -18,7 +18,7 @@ export function usePiModels() {
queryKey: ['PI_MODELS'],
enabled: isAuthenticated,
queryFn: async () => {
const data = await client.get<{ models: ModelOption[] }>('/api/pi/models');
const data = await client.get<{ models: ModelOption[] }>('/pi/models');
return data.models;
},
staleTime: 5 * 60 * 1000,