pi session context persistence, fix new session button, add clear all sessions
- handle pi process death by nulling piProcess ref so next message respawns - replay conversation history on respawn via --session flag - fix user message JSONL format to array for pi compatibility - fix container sessions mount to match storage path - fix ChatPanelWrapper: key on inner component so usePiChat resets on new session - add bulk delete sessions endpoint and clear all button in chat header Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { MessageSquare, History, Plus } from 'lucide-react';
|
||||
import { MessageSquare, History, Plus, Trash2 } from 'lucide-react';
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover';
|
||||
import { useWorkspace } from '../../components/Workspace/WorkspaceContext';
|
||||
import { useChatSessions } from 'state/useChatSessions';
|
||||
@@ -29,7 +29,7 @@ export const ChatHeader = () => {
|
||||
: workspaceId && !workspaceId.startsWith('screens/')
|
||||
? { context: 'workspace' as const, contextId: workspaceId }
|
||||
: undefined;
|
||||
const { sessions } = useChatSessions(contextFilter);
|
||||
const { sessions, clearSessions } = useChatSessions(contextFilter);
|
||||
const [selection, setSelection] = usePanelChannel<ChatSessionSelection | null>('chat:panel-session', null);
|
||||
const [activeSessionId] = usePanelChannel<string | null>('chat:active-session', null);
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -69,14 +69,29 @@ export const ChatHeader = () => {
|
||||
<PopoverContent align="end" className="w-72 p-0 max-h-80 flex flex-col">
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b border-border">
|
||||
<span className="text-xs font-medium">Sessions</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectSession(null)}
|
||||
className="flex items-center gap-1 text-xs text-duck-teal hover:text-duck-teal/80 cursor-pointer"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
New
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
{sessions.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
await clearSessions();
|
||||
selectSession(null);
|
||||
}}
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-destructive cursor-pointer"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectSession(null)}
|
||||
className="flex items-center gap-1 text-xs text-duck-teal hover:text-duck-teal/80 cursor-pointer"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
New
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* Show active session at top if not yet in fetched list */}
|
||||
|
||||
@@ -9,6 +9,39 @@ type ChatSessionSelection = {
|
||||
model?: string | null;
|
||||
};
|
||||
|
||||
type ChatPanelInnerProps = {
|
||||
sessionId?: string;
|
||||
model?: string;
|
||||
scoped: boolean;
|
||||
sandboxed: boolean;
|
||||
cwdParam?: { root?: string; path: string };
|
||||
promptPrefix?: string;
|
||||
chatContext: Record<string, string | undefined>;
|
||||
setActiveSession: (id: string | null) => void;
|
||||
};
|
||||
|
||||
const ChatPanelInner = ({ sessionId, model, scoped, sandboxed, cwdParam, promptPrefix, chatContext, setActiveSession }: ChatPanelInnerProps) => {
|
||||
const chat = usePiChat(sessionId, model, { replaceUrl: false, projectScoped: scoped, ...chatContext });
|
||||
|
||||
useEffect(() => {
|
||||
setActiveSession(chat.sessionId);
|
||||
}, [chat.sessionId]);
|
||||
|
||||
return (
|
||||
<EmbeddableChat
|
||||
className="h-full"
|
||||
chat={chat}
|
||||
sessionId={sessionId}
|
||||
initialModel={model}
|
||||
cwd={cwdParam}
|
||||
sandboxed={sandboxed}
|
||||
replaceUrl={false}
|
||||
promptPrefix={promptPrefix}
|
||||
{...chatContext}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ChatPanelWrapper = () => {
|
||||
const { workspaceId, cwd, root, promptPrefix } = useWorkspace();
|
||||
const scoped = cwd !== '~';
|
||||
@@ -31,24 +64,17 @@ export const ChatPanelWrapper = () => {
|
||||
const sessionId = selection?.sessionId ?? undefined;
|
||||
const model = selection?.model ?? undefined;
|
||||
|
||||
const chat = usePiChat(sessionId, model, { replaceUrl: false, projectScoped: scoped, ...chatContext });
|
||||
|
||||
useEffect(() => {
|
||||
setActiveSession(chat.sessionId);
|
||||
}, [chat.sessionId]);
|
||||
|
||||
return (
|
||||
<EmbeddableChat
|
||||
<ChatPanelInner
|
||||
key={sessionId ?? 'new'}
|
||||
className="h-full"
|
||||
chat={chat}
|
||||
sessionId={sessionId}
|
||||
initialModel={model}
|
||||
cwd={cwdParam}
|
||||
model={model}
|
||||
scoped={scoped}
|
||||
sandboxed={sandboxed}
|
||||
replaceUrl={false}
|
||||
cwdParam={cwdParam}
|
||||
promptPrefix={promptPrefix}
|
||||
{...chatContext}
|
||||
chatContext={chatContext}
|
||||
setActiveSession={setActiveSession}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,6 +46,14 @@ export function useChatSessions(filter?: ChatSessionsFilter) {
|
||||
);
|
||||
}
|
||||
|
||||
async function clearSessions() {
|
||||
await client.delete('/pi/sessions', {
|
||||
...(filter?.context ? { context: filter.context } : {}),
|
||||
...(filter?.contextId ? { contextId: filter.contextId } : {}),
|
||||
});
|
||||
queryClient.setQueryData<SessionEntry[]>(['PI_SESSIONS', filter?.context, filter?.contextId], []);
|
||||
}
|
||||
|
||||
function searchSessions(query: string) {
|
||||
return client.get<{ results: SessionEntry[] }>(`/pi/sessions/search?q=${encodeURIComponent(query)}`);
|
||||
}
|
||||
@@ -60,6 +68,7 @@ export function useChatSessions(filter?: ChatSessionsFilter) {
|
||||
saveMessages,
|
||||
renameSession,
|
||||
deleteSession,
|
||||
clearSessions,
|
||||
searchSessions,
|
||||
invalidate,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user