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,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">