saved sessions: make routable via /chat/saved/:id URLs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 20:15:36 +00:00
co-authored by Claude Opus 4.6
parent 2a8df0def1
commit 406d8f9e54
3 changed files with 50 additions and 10 deletions
+1
View File
@@ -54,6 +54,7 @@ export function App() {
<Route path="/new-automation" element={<Dashboard.NewAutomation />} /> <Route path="/new-automation" element={<Dashboard.NewAutomation />} />
<Route path="/chat" element={<Dashboard.SessionListPage />} /> <Route path="/chat" element={<Dashboard.SessionListPage />} />
<Route path="/chat/new" element={<Dashboard.SessionListPage isNew />} /> <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="/chat/:sessionId" element={<Dashboard.SessionListPage />} />
<Route path="/plans" element={<Dashboard.Plans />} /> <Route path="/plans" element={<Dashboard.Plans />} />
<Route path="/files" element={<Dashboard.FilesScreen />} /> <Route path="/files" element={<Dashboard.FilesScreen />} />
@@ -1,10 +1,12 @@
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo, useRef } from 'react';
import { useParams, useNavigate } from 'react-router'; import { useParams, useNavigate } from 'react-router';
import type { LayoutNode, SelectedSession } from 'officerdev'; import type { LayoutNode, SelectedSession } from 'officerdev';
import type { ChatMessage } from 'officerdev';
import { WorkspaceView } from 'officerdev'; import { WorkspaceView } from 'officerdev';
import { useIsMobile } from 'hooks/useIsMobile'; import { useIsMobile } from 'hooks/useIsMobile';
import { useDashboardState } from 'state/useDashboardState'; import { useDashboardState } from 'state/useDashboardState';
import { usePanelChannel } from 'hooks/usePanelChannel'; import { usePanelChannel } from 'hooks/usePanelChannel';
import { useSavedSessions, messagesToTranscript, type RawMessage } from 'state/useSavedSessions';
import { defaultLayout } from './defaultLayout'; import { defaultLayout } from './defaultLayout';
export { ChatHistory as ChatHistoryApp } from './Widget'; export { ChatHistory as ChatHistoryApp } from './Widget';
@@ -33,12 +35,15 @@ type SessionListPageProps = {
}; };
export const SessionListPage = ({ isNew }: SessionListPageProps) => { export const SessionListPage = ({ isNew }: SessionListPageProps) => {
const { sessionId } = useParams<{ sessionId: string }>(); const { sessionId, id: savedIdParam } = useParams<{ sessionId: string; id: string }>();
const [, setSelected] = usePanelChannel<SelectedSession>('chat:selected-session', null); const [, setSelected] = usePanelChannel<SelectedSession>('chat:selected-session', null);
const rawWorkspace = useDashboardState<LayoutNode>('screens/chat', defaultLayout); const rawWorkspace = useDashboardState<LayoutNode>('screens/chat', defaultLayout);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const navigate = useNavigate(); const navigate = useNavigate();
const mobilePanelId = isMobile && (sessionId || isNew) ? 'chat-detail' : undefined; const { resumeSession } = useSavedSessions();
const loadedSavedIdRef = useRef<number | null>(null);
const savedId = savedIdParam ? Number(savedIdParam) : null;
const mobilePanelId = isMobile && (sessionId || isNew || savedId) ? 'chat-detail' : undefined;
// Normalize synchronously so the wrong panel never renders // Normalize synchronously so the wrong panel never renders
const workspace = useMemo(() => { const workspace = useMemo(() => {
@@ -59,9 +64,38 @@ export const SessionListPage = ({ isNew }: SessionListPageProps) => {
setSelected({ id: `new:${Date.now()}` }); setSelected({ id: `new:${Date.now()}` });
return; 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; if (!sessionId) return;
setSelected({ id: sessionId }); setSelected({ id: sessionId });
}, [sessionId, isNew]); }, [sessionId, isNew, savedId]);
return ( return (
<div className="h-full w-full pt-2"> <div className="h-full w-full pt-2">
@@ -1,5 +1,5 @@
import { useState, useRef, useCallback } from 'react'; import { useState, useRef, useCallback } from 'react';
import { useNavigate } from 'react-router'; import { useNavigate, useLocation } from 'react-router';
import { Plus, MessageSquare, Loader2 } from 'lucide-react'; import { Plus, MessageSquare, Loader2 } from 'lucide-react';
import { usePanelChannel } from 'hooks/usePanelChannel'; import { usePanelChannel } from 'hooks/usePanelChannel';
import { useSavedSessions, messagesToTranscript, type RawMessage } from 'state/useSavedSessions'; import { useSavedSessions, messagesToTranscript, type RawMessage } from 'state/useSavedSessions';
@@ -9,6 +9,8 @@ import { SessionContextMenu } from './SessionContextMenu';
export const SessionList = () => { export const SessionList = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const isOnChatPage = location.pathname.startsWith('/chat');
const { sessions, deleteSavedSession, resumeSession } = useSavedSessions(); const { sessions, deleteSavedSession, resumeSession } = useSavedSessions();
const [selected, setSelected] = usePanelChannel<SelectedSession>('chat:selected-session', null); const [selected, setSelected] = usePanelChannel<SelectedSession>('chat:selected-session', null);
const [isResuming, setIsResuming] = useState<number | null>(null); const [isResuming, setIsResuming] = useState<number | null>(null);
@@ -22,6 +24,10 @@ export const SessionList = () => {
}, []); }, []);
const handleSelect = async (session: (typeof sessions)[number]) => { const handleSelect = async (session: (typeof sessions)[number]) => {
if (isOnChatPage) {
navigate(`/chat/saved/${session.id}`);
return;
}
setIsResuming(session.id); setIsResuming(session.id);
try { try {
const result = await resumeSession(session.id); const result = await resumeSession(session.id);
@@ -43,12 +49,11 @@ export const SessionList = () => {
}); });
const transcript = messagesToTranscript(rawMessages); const transcript = messagesToTranscript(rawMessages);
setSelected({ setSelected({
id: `resume:${session.id}`, id: `saved:${session.id}`,
model: result.model, model: result.model,
resumeSummary: transcript, resumeSummary: transcript,
initialMessages: chatMessages, initialMessages: chatMessages,
}); });
navigate(`/chat/new`, { replace: true });
} catch { } catch {
// Failed to resume // Failed to resume
} finally { } finally {
@@ -57,9 +62,9 @@ export const SessionList = () => {
}; };
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
if (selected?.id === `resume:${id}`) { if (selected?.id === `saved:${id}`) {
setSelected(null); setSelected(null);
navigate('/chat', { replace: true }); if (isOnChatPage) navigate('/chat', { replace: true });
} }
await deleteSavedSession(id); await deleteSavedSession(id);
}; };
@@ -90,7 +95,7 @@ export const SessionList = () => {
)} )}
{sessions.map((session) => { {sessions.map((session) => {
const isActive = selected?.id === `resume:${session.id}`; const isActive = selected?.id === `saved:${session.id}`;
return ( return (
<div <div
key={session.id} key={session.id}