chat: retire the old saved-session store (Stage 4a)

Deletes all session persistence that isn't Claude's native transcript store, per
the "only harness-native session management survives" rule.

Backend: delete api/pi/storage.ts (meta.json+messages.json file store), the
api/saved-sessions router (+ unmount), the /pi/sessions REST endpoints, and the
storage.save/loadSession calls in the chat WS handler (in-memory session-manager
stays for live turns; no disk persistence — Claude's transcript is the record).
Also drops the Postgres saved_sessions layer: schema/chat.ts, queries/saved-sessions.ts,
its types and re-exports.

Frontend: delete state/useSavedSessions, ChatList, and the ChatHistory Widget
(all pure saved-session UI); slim ChatHeader to a label; strip the auto-load-latest
+ Save wiring from ChatPanelWrapper and ChatDetailPanel; drop the old resume path
from useChat and SessionListPage; remove the /chat/saved/:id route and the
useInitialData prefetch.

Behavior removed (intended): the Save-session button, email/project panels
auto-resuming the last chat, and /chat/saved/:id. /chat itself is unchanged —
already fully on Claude transcripts. The orphaned saved_sessions Postgres table
is dropped on the next `bun db:push`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 15:29:26 +00:00
co-authored by Claude Opus 4.8
parent 2e3c45ddfb
commit 70555c8d71
23 changed files with 31 additions and 1813 deletions
@@ -1,8 +1,4 @@
import { useState } from 'react';
import { MessageSquare, History, Plus, Save, Trash2, Loader2 } from 'lucide-react';
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover';
import { useSavedSessions, messagesToTranscript, type RawMessage } from 'state/useSavedSessions';
import { usePanelChannel } from 'hooks/usePanelChannel';
import { MessageSquare } from 'lucide-react';
import type { ChatMessage } from './types';
export type ChatSessionSelection = {
@@ -13,163 +9,11 @@ export type ChatSessionSelection = {
initialMessages?: ChatMessage[];
};
export const ChatHeader = () => {
const { sessions, saveSession, deleteSavedSession, resumeSession } = useSavedSessions();
const [, setSelection] = usePanelChannel<ChatSessionSelection | null>('chat:panel-session', null);
const [activeSessionId] = usePanelChannel<string | null>('chat:active-session', null);
const [savedId, setSavedId] = usePanelChannel<number | null>('chat:saved-id', null);
const [open, setOpen] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [isResuming, setIsResuming] = useState<number | null>(null);
const isSaved = savedId != null;
const selectSession = (selection: ChatSessionSelection) => {
setSelection(selection);
setOpen(false);
};
const handleSave = async () => {
if (!activeSessionId || isSaving) return;
setIsSaving(true);
try {
const result = await saveSession(activeSessionId);
setSavedId(result.id);
} finally {
setIsSaving(false);
}
};
const handleResume = async (id: number, model: string) => {
setIsResuming(id);
try {
const result = await resumeSession(id);
const rawMessages = result.rawMessages ?? [];
const chatMessages: ChatMessage[] = rawMessages.map((m: RawMessage) => {
if (m.role === 'user') return { role: 'user' as const, text: m.text || '' };
if (m.role === 'assistant') return { role: 'assistant' as const, id: m.id, text: m.text || '' };
if (m.role === 'tool') {
return {
role: 'tool' as const,
toolName: m.toolName || '',
toolInput: m.toolInput || {},
toolCallId: m.toolCallId || '',
output: m.output,
isError: m.isError,
};
}
return { role: 'assistant' as const, text: '' };
});
const transcript = messagesToTranscript(rawMessages);
setSavedId(id);
selectSession({
sessionId: null,
model: result.model ?? model,
resumeSummary: transcript,
initialMessages: chatMessages,
});
} catch {
// Failed to resume
} finally {
setIsResuming(null);
}
};
const handleDelete = async (e: React.MouseEvent, id: number) => {
e.stopPropagation();
await deleteSavedSession(id);
};
return (
<>
<MessageSquare className="h-3.5 w-3.5 shrink-0" />
<span className="text-xs font-medium truncate flex-1">Chat</span>
{isSaving ? (
<span className="flex items-center gap-1 text-[10px] text-muted-foreground shrink-0">
<Loader2 className="h-3 w-3 animate-spin" />
Saving
</span>
) : isSaved ? (
<span className="flex items-center gap-1 text-[10px] text-duck-teal shrink-0">
<Save className="h-3 w-3" />
</span>
) : activeSessionId ? (
<button
type="button"
onClick={handleSave}
className="p-0.5 rounded hover:bg-black/10 transition-colors cursor-pointer shrink-0"
title="Save session"
>
<Save className="h-3.5 w-3.5" />
</button>
) : null}
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
className="p-0.5 rounded hover:bg-black/10 transition-colors cursor-pointer shrink-0"
title="Saved sessions"
>
<History className="h-3.5 w-3.5" />
</button>
</PopoverTrigger>
<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">Saved Sessions</span>
<button
type="button"
onClick={() => selectSession({ sessionId: 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 className="flex-1 overflow-y-auto">
{sessions.length === 0 ? (
<div className="px-3 py-4 text-center text-xs text-muted-foreground">No saved sessions</div>
) : (
sessions.map((session) => (
<button
key={session.id}
type="button"
onClick={() => handleResume(session.id, session.provider)}
disabled={isResuming === session.id}
className="w-full text-left px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer border-b border-border/50 last:border-0 group/item"
>
<div className="flex items-center justify-between">
<div className="text-xs font-medium truncate flex-1">
{isResuming === session.id ? (
<span className="flex items-center gap-1.5">
<Loader2 className="h-3 w-3 animate-spin" />
Loading
</span>
) : (
session.title
)}
</div>
<button
type="button"
onClick={(e) => handleDelete(e, session.id)}
className="p-0.5 rounded hover:bg-destructive/20 text-muted-foreground hover:text-destructive opacity-0 group-hover/item:opacity-100 transition-opacity shrink-0 ml-1"
>
<Trash2 className="h-3 w-3" />
</button>
</div>
<div className="text-[10px] text-muted-foreground">
{new Date(session.createdAt).toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</div>
</button>
))
)}
</div>
</PopoverContent>
</Popover>
</>
);
};
// Chat panel header — just the label now that saved-sessions are retired (Claude's transcript store
// is the source of truth; the /chat screen is where you browse/resume sessions).
export const ChatHeader = () => (
<>
<MessageSquare className="h-3.5 w-3.5 shrink-0" />
<span className="text-xs font-medium truncate flex-1">Chat</span>
</>
);
@@ -1,42 +0,0 @@
import { MessageSquare } from 'lucide-react';
import { useSavedSessions } from 'state/useSavedSessions';
export const ChatList = () => {
const { sessions } = useSavedSessions();
if (sessions.length === 0) {
return (
<div className="h-full flex items-center justify-center text-duck-dark/30 dark:text-foreground/30 text-sm">
No saved sessions yet.
</div>
);
}
return (
<div className="space-y-1.5 p-3">
{sessions.map((session) => (
<div
key={session.id}
className="block p-3 rounded-lg border border-duck-dark/10 dark:border-foreground/10 hover:bg-duck-teal/5 dark:hover:bg-duck-teal/10 transition-colors group"
>
<div className="flex items-start gap-2 min-w-0">
<MessageSquare className="h-4 w-4 shrink-0 text-duck-teal/60 mt-0.5" />
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-duck-dark/80 dark:text-foreground/80 truncate">
{session.title}
</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>
</div>
</div>
</div>
))}
</div>
);
};
@@ -1,19 +1,10 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect } from 'react';
import { useWorkspace } from '../../components/Workspace';
import { usePanelChannel } from 'hooks/usePanelChannel';
import { useSavedSessions, messagesToTranscript, type RawMessage } from 'state/useSavedSessions';
import { useChat } from '../../hooks/useChat';
import { EmbeddableChat } from './EmbeddableChat';
import type { ChatSessionSelection } from './ChatHeader';
import type { ChatMessage } from './types';
type ChatPanelInnerProps = {
sessionId?: string;
model?: string;
resumeSummary?: string;
initialMessages?: ChatMessage[];
savedId?: number;
scoped: boolean;
sandboxed: boolean;
cwdParam?: { root?: string; path: string };
@@ -23,48 +14,13 @@ type ChatPanelInnerProps = {
onTurnComplete?: (hadToolCalls: boolean) => void;
};
const ChatPanelInner = ({
sessionId,
model,
resumeSummary,
initialMessages,
savedId,
scoped,
sandboxed,
cwdParam,
promptPrefix,
chatContext,
setActiveSession,
onTurnComplete,
}: ChatPanelInnerProps) => {
const { updateSessionMessages } = useSavedSessions();
const savedIdRef = useRef(savedId);
savedIdRef.current = savedId;
const chatSessionRef = useRef<string | null>(null);
const resumedMessageCountRef = useRef(initialMessages?.length ?? 0);
const handleTurnComplete = useCallback(
(hadToolCalls: boolean) => {
onTurnComplete?.(hadToolCalls);
const id = savedIdRef.current;
const sid = chatSessionRef.current;
if (id != null && sid) {
const count = resumedMessageCountRef.current;
updateSessionMessages(id, sid, count > 0 ? count : undefined).catch(() => {});
}
},
[onTurnComplete, updateSessionMessages],
);
const chat = useChat(sessionId, model, {
const ChatPanelInner = ({ scoped, sandboxed, cwdParam, promptPrefix, chatContext, setActiveSession, onTurnComplete }: ChatPanelInnerProps) => {
const chat = useChat(undefined, undefined, {
replaceUrl: false,
projectScoped: scoped,
onTurnComplete: handleTurnComplete,
resumeSummary,
initialMessages,
onTurnComplete,
...chatContext,
});
chatSessionRef.current = chat.sessionId;
useEffect(() => {
setActiveSession(chat.sessionId);
@@ -74,8 +30,6 @@ const ChatPanelInner = ({
<EmbeddableChat
className="h-full"
chat={chat}
sessionId={sessionId}
initialModel={model}
cwd={cwdParam}
sandboxed={sandboxed}
replaceUrl={false}
@@ -100,53 +54,7 @@ export const ChatPanelWrapper = () => {
? { context: 'dashboard' as const, contextId: dashboardId }
: {};
const [savedId, setSavedId] = usePanelChannel<number | null>('chat:saved-id', null);
const [selection, setSelection] = usePanelChannel<ChatSessionSelection | null>('chat:panel-session', null);
const [resumeKey, setResumeKey] = useState(0);
const prevSelectionRef = useRef(selection);
if (selection !== prevSelectionRef.current) {
prevSelectionRef.current = selection;
if (selection?.resumeSummary) setResumeKey((k) => k + 1);
}
const [, setActiveSession] = usePanelChannel<string | null>('chat:active-session', null);
// Auto-load the latest saved session for this dashboard/project context
const { sessions, resumeSession } = useSavedSessions();
const autoLoadedRef = useRef<string | null>(null);
const contextId = 'contextId' in chatContext ? chatContext.contextId : undefined;
useEffect(() => {
if (selection || !contextId || autoLoadedRef.current === contextId) return;
const latest = sessions.find((s) => s.contextId === contextId);
if (!latest) return;
autoLoadedRef.current = contextId;
setSavedId(latest.id);
resumeSession(latest.id).then((result) => {
const rawMessages = result.rawMessages ?? [];
const chatMessages: ChatMessage[] = rawMessages.map((m: RawMessage) => {
if (m.role === 'user') return { role: 'user' as const, text: m.text || '' };
if (m.role === 'assistant') return { role: 'assistant' as const, id: m.id, text: m.text || '' };
if (m.role === 'tool') {
return {
role: 'tool' as const,
toolName: m.toolName || '',
toolInput: m.toolInput || {},
toolCallId: m.toolCallId || '',
output: m.output,
isError: m.isError,
};
}
return { role: 'assistant' as const, text: '' };
});
const transcript = messagesToTranscript(rawMessages);
setSelection({
sessionId: null,
model: result.model,
dashboardId: dashboardId ?? undefined,
resumeSummary: transcript,
initialMessages: chatMessages,
});
});
}, [selection, contextId, sessions]);
const [, setPreviewRefresh] = usePanelChannel<number>('preview:refresh', 0);
const [, setFilesRefresh] = usePanelChannel<number>('files:refresh-signal', 0);
@@ -162,20 +70,8 @@ export const ChatPanelWrapper = () => {
const cwdParam = scoped ? { root, path: cwd } : undefined;
const isCurrentWorkspace = !selection?.dashboardId || selection.dashboardId === dashboardId;
const sessionId = isCurrentWorkspace ? (selection?.sessionId ?? undefined) : undefined;
const model = isCurrentWorkspace ? (selection?.model ?? undefined) : undefined;
const resumeSummary = isCurrentWorkspace ? selection?.resumeSummary : undefined;
const initialMessages = isCurrentWorkspace ? selection?.initialMessages : undefined;
return (
<ChatPanelInner
key={resumeSummary ? `resume-${resumeKey}` : (sessionId ?? 'new')}
sessionId={sessionId}
model={model}
resumeSummary={resumeSummary}
initialMessages={initialMessages}
savedId={savedId ?? undefined}
scoped={scoped}
sandboxed={sandboxed}
cwdParam={cwdParam}
@@ -15,7 +15,6 @@ export { AttachButton } from './components/AttachButton';
export { WebpageDialog } from './components/WebpageDialog';
export { EmbeddableChat, type UseEmbeddableChatType } from './EmbeddableChat';
export { useChat, type UseChatType } from '../../hooks/useChat';
export { ChatList } from './ChatList';
export { useSlashCommands } from './useSlashCommands';
export { useAttachments, type UseAttachmentsType } from './useAttachments';
export { useAudioRecording, type UseAudioRecordingType } from './useAudioRecording';
@@ -1,9 +1,7 @@
import { useState, useCallback, useRef } from 'react';
import { useCallback } from 'react';
import { useLocation } from 'react-router';
import { Save, Loader2 } from 'lucide-react';
import { usePanelChannel } from 'hooks/usePanelChannel';
import { useAuth } from 'hooks/useAuth';
import { useSavedSessions } from 'state/useSavedSessions';
import { useClaudeSessions } from 'state/useClaudeSessions';
import { useChat, EmbeddableChat } from '../Chat';
import type { ChatMessage } from '../Chat/types';
@@ -17,7 +15,6 @@ export type SelectedSession = {
} | null;
const CHANNEL = 'chat:selected-session';
const SAVED_ID_CHANNEL = 'chat:saved-id';
type ChatLocationState = {
initialMessage?: string;
@@ -32,36 +29,11 @@ type DetailBarProps = {
sessionTitle: string | undefined;
isConnected: boolean;
isGenerating: boolean;
sessionId: string | null;
isSaved: boolean;
onSave: () => void;
isSaving: boolean;
};
function DetailBar({ sessionTitle, isConnected, isGenerating, sessionId, isSaved, onSave, isSaving }: DetailBarProps) {
function DetailBar({ sessionTitle, isConnected, isGenerating }: DetailBarProps) {
return (
<div className="shrink-0 flex items-center px-4 py-2 border-b border-duck-dark/10 dark:border-foreground/10 bg-background/60">
<div className="flex items-center gap-1">
{isSaving ? (
<span className="flex items-center gap-1.5 px-1 text-xs text-duck-teal">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
Saving
</span>
) : isSaved ? (
<span className="flex items-center gap-1.5 px-1 text-xs text-duck-teal">
<Save className="h-3.5 w-3.5" />
Saved
</span>
) : sessionId ? (
<button
onClick={onSave}
className="p-1 text-duck-dark/40 dark:text-foreground/40 hover:text-duck-teal transition-colors cursor-pointer"
title="Save session"
>
<Save className="h-4 w-4" />
</button>
) : null}
</div>
<div className="flex-1 min-w-0 text-center text-sm font-medium text-duck-dark/70 dark:text-foreground/70 truncate px-3">
{sessionTitle ?? 'New chat'}
</div>
@@ -83,51 +55,23 @@ type NewChatProps = {
resumeSummary?: string;
resumeSessionId?: string;
initialMessages?: ChatMessage[];
savedId?: number;
};
function NewChat({ resumeSummary, resumeSessionId, initialMessages, savedId: initialSavedId }: NewChatProps) {
function NewChat({ resumeSummary, resumeSessionId, initialMessages }: NewChatProps) {
const location = useLocation();
const locationState = location.state as ChatLocationState;
const { user } = useAuth();
const isSuperAdmin = user?.role === 'Super Admin';
const { saveSession, updateSessionMessages } = useSavedSessions();
const { invalidate: invalidateClaudeSessions } = useClaudeSessions();
const [isSaving, setIsSaving] = useState(false);
const [savedId, setSavedId] = usePanelChannel<number | null>(SAVED_ID_CHANNEL, initialSavedId ?? null);
const savedIdRef = useRef(savedId);
savedIdRef.current = savedId;
const chatSessionRef = useRef<string | null>(null);
const resumedMessageCountRef = useRef(initialMessages?.length ?? 0);
// Refresh the /chat list — Claude has just written/appended this session's transcript.
const onTurnComplete = useCallback(() => {
// Refresh the /chat list — Claude has just written/appended this session's transcript.
invalidateClaudeSessions();
const id = savedIdRef.current;
const sid = chatSessionRef.current;
if (id != null && sid) {
const count = resumedMessageCountRef.current;
updateSessionMessages(id, sid, count > 0 ? count : undefined).catch(() => {});
}
}, [updateSessionMessages, invalidateClaudeSessions]);
}, [invalidateClaudeSessions]);
// context 'chat' tells the backend to run this session from the dedicated claude_sessions cwd, so
// its transcript lands in Claude's own store as an isolated project group (source of truth).
const chat = useChat(undefined, locationState?.model, { resumeSummary, resumeSessionId, initialMessages, onTurnComplete, context: 'chat' });
chatSessionRef.current = chat.sessionId;
const isSaved = savedId != null;
const handleSave = async () => {
if (!chat.sessionId || isSaving) return;
setIsSaving(true);
try {
const result = await saveSession(chat.sessionId);
setSavedId(result.id);
} finally {
setIsSaving(false);
}
};
const sandboxed = !isSuperAdmin;
// Run the session in the pwd chosen in the Sessions panel; null → backend default (claude_sessions).
@@ -145,15 +89,7 @@ function NewChat({ resumeSummary, resumeSessionId, initialMessages, savedId: ini
return (
<div className="flex flex-col h-full">
<DetailBar
sessionTitle={undefined}
isConnected={chat.isConnected}
isGenerating={chat.isGenerating}
sessionId={chat.sessionId}
isSaved={isSaved}
onSave={handleSave}
isSaving={isSaving}
/>
<DetailBar sessionTitle={undefined} isConnected={chat.isConnected} isGenerating={chat.isGenerating} />
<EmbeddableChat
chat={chat}
sessionId={undefined}
@@ -179,15 +115,12 @@ export const ChatDetailPanel = () => {
);
}
const savedId = selected.id.startsWith('saved:') ? Number(selected.id.replace('saved:', '')) : undefined;
return (
<NewChat
key={selected.id}
resumeSummary={selected.resumeSummary}
resumeSessionId={selected.resumeSessionId}
initialMessages={selected.initialMessages}
savedId={savedId}
/>
);
};
@@ -207,79 +207,6 @@ export function useChat(initialSessionId?: string, initialModel?: string | null,
const { isConnected, send } = useChatWebSocket({ url: wsUrl, onMessage: handleMessage });
// Load messages from server on mount when resuming a session
useEffect(() => {
if (storage) {
storage
.load()
.then(({ sessionId: sid, messages: msgs }) => {
if (sid) {
sessionIdRef.current = sid;
setSessionId(sid);
}
if (msgs.length > 0) {
setMessages(msgs);
setHasStarted(true);
}
})
.catch(() => {});
return;
}
if (!initialSessionId) return;
client
.get<{ session: { messages: Message[] } }>(`/pi/sessions/${initialSessionId}`)
.then((data) => {
if (data.session?.messages && data.session.messages.length > 0) {
// Convert backend Message[] to ChatMessage[]
const chatMessages = data.session.messages.map((m: Message): ChatMessage => {
if (m.role === 'user') {
return { role: 'user', text: m.text || '' };
} else if (m.role === 'assistant') {
return { role: 'assistant', id: m.id ?? crypto.randomUUID(), text: m.text || '' };
} else if (m.role === 'tool') {
return {
role: 'tool',
toolName: m.toolName || '',
toolInput: m.toolInput || {},
toolCallId: m.toolCallId || '',
output: m.output,
isError: m.isError,
};
}
return { role: 'assistant', id: crypto.randomUUID(), text: '' }; // Fallback
});
setMessages(chatMessages);
setHasStarted(true);
}
})
.catch(() => {});
}, [initialSessionId]);
// Debounced save messages to server
useEffect(() => {
if (!sessionIdRef.current || messages.length === 0) return;
if (saveTimerRef.current !== null) clearTimeout(saveTimerRef.current);
const sid = sessionIdRef.current;
const snapshot = messages;
saveTimerRef.current = window.setTimeout(() => {
if (storage) {
storage.save(sid, snapshot).catch(() => {});
} else {
client.put(`/pi/sessions/${sid}/messages`, snapshot).catch(() => {});
}
saveTimerRef.current = null;
}, SAVE_DEBOUNCE_MS);
return () => {
if (saveTimerRef.current !== null) {
clearTimeout(saveTimerRef.current);
saveTimerRef.current = null;
}
};
}, [messages]);
// Clean up RAF on unmount
useEffect(() => {
return () => {
-1
View File
@@ -17,7 +17,6 @@ export {
WebpageDialog,
EmbeddableChat,
useChat,
ChatList,
useSlashCommands,
useAttachments,
useAudioRecording,
-2
View File
@@ -11,8 +11,6 @@ export { useRecentModels } from './useRecentModels';
export { usePlans } from './usePlans';
export { useLandingPage } from './useLandingPage';
export { useServerSettings } from './useServerSettings';
export { useSavedSessions } from './useSavedSessions';
export type { UseSavedSessionsType, SavedSessionEntry } from './useSavedSessions';
export { useUserApps } from './useUserApps';
export type { AppManifest } from './useUserApps';
export { useServerEnvironment } from './useServerEnvironment';
@@ -1,105 +0,0 @@
import { useAuth } from 'hooks/useAuth';
import { useClient } from 'hooks/useClient';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
export type SavedSessionEntry = {
id: number;
provider: string;
context: string | null;
contextId: string | null;
title: string;
summary: string;
cwd: string;
cost: { inputTokens: number; outputTokens: number; totalUSD: number };
createdAt: string;
};
type RawMessage = {
id: string;
timestamp: number;
role: 'user' | 'assistant' | 'tool';
text?: string;
model?: string;
cost?: { inputTokens: number; outputTokens: number; totalUSD: number };
toolCallId?: string;
toolName?: string;
toolInput?: Record<string, unknown>;
output?: string;
isError?: boolean;
};
export type ResumeResult = {
context: string | null;
cwd: string;
model: string;
cost: { inputTokens: number; outputTokens: number; totalUSD: number };
rawMessages: RawMessage[];
};
export type { RawMessage };
export function messagesToTranscript(messages: RawMessage[]): string {
const lines: string[] = [];
for (const msg of messages) {
if (msg.role === 'user' && msg.text) {
lines.push(`User: ${msg.text}`);
} else if (msg.role === 'assistant' && msg.text) {
lines.push(`Assistant: ${msg.text}`);
} else if (msg.role === 'tool') {
const outcome = msg.isError ? `Error: ${msg.output}` : msg.output ? 'Success' : 'Pending';
lines.push(`Tool [${msg.toolName}]: ${outcome}`);
}
}
return lines.join('\n\n');
}
export function useSavedSessions() {
const client = useClient();
const queryClient = useQueryClient();
const { isAuthenticated } = useAuth();
const { data: sessions = [], isLoading } = useQuery<SavedSessionEntry[]>({
queryKey: ['SAVED_SESSIONS'],
enabled: isAuthenticated,
queryFn: () => client.get<SavedSessionEntry[]>('/saved-sessions'),
});
async function saveSession(sessionId: string) {
try {
const result = await client.post<SavedSessionEntry>('/saved-sessions', { sessionId });
queryClient.invalidateQueries({ queryKey: ['SAVED_SESSIONS'] });
toast.success('Session saved');
return result;
} catch (err) {
toast.error('Failed to save session');
throw err;
}
}
async function deleteSavedSession(id: number) {
await client.delete(`/saved-sessions/${id}`);
queryClient.setQueryData<SavedSessionEntry[]>(['SAVED_SESSIONS'], (prev) => prev?.filter((s) => s.id !== id) ?? []);
}
async function resumeSession(id: number) {
return client.post<ResumeResult>(`/saved-sessions/${id}/resume`);
}
async function updateSessionMessages(savedId: number, sessionId: string, resumedMessageCount?: number) {
await client.put(`/saved-sessions/${savedId}`, { sessionId, resumedMessageCount });
}
return {
sessions,
isLoading,
saveSession,
deleteSavedSession,
resumeSession,
updateSessionMessages,
};
}
export type UseSavedSessionsType = ReturnType<typeof useSavedSessions>;