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:
@@ -52,7 +52,6 @@ export function App() {
|
||||
<Route path="/settings/apps" element={<Dashboard.AppsSettings />} />
|
||||
<Route path="/chat" element={<Dashboard.SessionListPage />} />
|
||||
<Route path="/chat/new" element={<Dashboard.SessionListPage isNew />} />
|
||||
<Route path="/chat/saved/:id" element={<Dashboard.SessionListPage />} />
|
||||
<Route path="/chat/:sessionId" element={<Dashboard.SessionListPage />} />
|
||||
<Route path="/plans" element={<Dashboard.Plans />} />
|
||||
<Route path="/files" element={<Dashboard.FilesScreen />} />
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { MessageSquare, Trash2 } from 'lucide-react';
|
||||
import { Widget } from 'widgets/Widget';
|
||||
import { useSavedSessions } from 'state/useSavedSessions';
|
||||
|
||||
export const ChatHistory = () => {
|
||||
const { sessions, deleteSavedSession } = useSavedSessions();
|
||||
|
||||
return (
|
||||
<Widget title="Saved Sessions">
|
||||
<div className="px-4 pb-3 max-h-72 overflow-y-auto">
|
||||
{sessions.length === 0 ? (
|
||||
<p className="text-xs text-duck-dark/40 py-4 text-center">No saved sessions yet</p>
|
||||
) : (
|
||||
<ul className="space-y-0.5">
|
||||
{sessions.map((session) => (
|
||||
<li
|
||||
key={session.id}
|
||||
className="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-duck-dark/5 group"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<MessageSquare className="h-4 w-4 shrink-0 text-duck-teal/60" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="text-sm text-duck-dark dark:text-foreground truncate block">{session.title}</span>
|
||||
<span className="text-xs text-duck-dark/40 dark:text-foreground/40 truncate block">
|
||||
{new Date(session.createdAt).toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => deleteSavedSession(session.id)}
|
||||
className="shrink-0 p-1 rounded text-duck-dark/20 md:opacity-0 md:group-hover:opacity-100 hover:text-red-500 transition-opacity cursor-pointer"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +1,12 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router';
|
||||
import type { LayoutNode, SelectedSession } from 'officerdev';
|
||||
import type { ChatMessage } from 'officerdev';
|
||||
import { WorkspaceView } from 'officerdev';
|
||||
import { useIsMobile } from 'hooks/useIsMobile';
|
||||
import { useDashboardState } from 'state/useDashboardState';
|
||||
import { usePanelChannel } from 'hooks/usePanelChannel';
|
||||
import { useSavedSessions, messagesToTranscript, type RawMessage } from 'state/useSavedSessions';
|
||||
import { defaultLayout } from './defaultLayout';
|
||||
|
||||
export { ChatHistory as ChatHistoryApp } from './Widget';
|
||||
|
||||
// Allowed panel app types for the /chat screen
|
||||
const ALLOWED_APP_TYPES = new Set<string | null>(['chat-session-list', 'chat-detail', null]);
|
||||
|
||||
@@ -35,15 +31,12 @@ type SessionListPageProps = {
|
||||
};
|
||||
|
||||
export const SessionListPage = ({ isNew }: SessionListPageProps) => {
|
||||
const { sessionId, id: savedIdParam } = useParams<{ sessionId: string; id: string }>();
|
||||
const { sessionId } = useParams<{ sessionId: string }>();
|
||||
const [, setSelected] = usePanelChannel<SelectedSession>('chat:selected-session', null);
|
||||
const rawWorkspace = useDashboardState<LayoutNode>('screens/chat', defaultLayout);
|
||||
const isMobile = useIsMobile();
|
||||
const navigate = useNavigate();
|
||||
const { resumeSession } = useSavedSessions();
|
||||
const loadedSavedIdRef = useRef<number | null>(null);
|
||||
const savedId = savedIdParam ? Number(savedIdParam) : null;
|
||||
const mobilePanelId = isMobile && (sessionId || isNew || savedId) ? 'chat-detail' : undefined;
|
||||
const mobilePanelId = isMobile && (sessionId || isNew) ? 'chat-detail' : undefined;
|
||||
|
||||
// Normalize synchronously so the wrong panel never renders
|
||||
const workspace = useMemo(() => {
|
||||
@@ -64,38 +57,9 @@ export const SessionListPage = ({ isNew }: SessionListPageProps) => {
|
||||
setSelected({ id: `new:${Date.now()}` });
|
||||
return;
|
||||
}
|
||||
if (savedId && !Number.isNaN(savedId) && loadedSavedIdRef.current !== savedId) {
|
||||
loadedSavedIdRef.current = savedId;
|
||||
resumeSession(savedId).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);
|
||||
setSelected({
|
||||
id: `saved:${savedId}`,
|
||||
model: result.model,
|
||||
resumeSummary: transcript,
|
||||
initialMessages: chatMessages,
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!sessionId) return;
|
||||
setSelected({ id: sessionId });
|
||||
}, [sessionId, isNew, savedId]);
|
||||
}, [sessionId, isNew]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full pt-2">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useSavedSessions } from 'state/useSavedSessions';
|
||||
import { usePlans } from 'state/usePlans';
|
||||
import { useSettings } from 'state/useSettings';
|
||||
import { usePiModels } from 'state/useModels';
|
||||
@@ -6,12 +5,11 @@ import { useAccessPolicy } from 'state/useAccessPolicy';
|
||||
import { useColorModeSync } from './useThemeSync';
|
||||
|
||||
export const useInitialData = () => {
|
||||
const { sessions } = useSavedSessions();
|
||||
const { plans } = usePlans();
|
||||
const { settings } = useSettings();
|
||||
usePiModels();
|
||||
useAccessPolicy();
|
||||
useColorModeSync();
|
||||
|
||||
return { sessions, plans, settings };
|
||||
return { plans, settings };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user