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
View File
@@ -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 -3
View File
@@ -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 };
};