saved sessions: make routable via /chat/saved/:id URLs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -54,6 +54,7 @@ export function App() {
|
||||
<Route path="/new-automation" element={<Dashboard.NewAutomation />} />
|
||||
<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,10 +1,12 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useEffect, useMemo, useRef } 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';
|
||||
@@ -33,12 +35,15 @@ type 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 rawWorkspace = useDashboardState<LayoutNode>('screens/chat', defaultLayout);
|
||||
const isMobile = useIsMobile();
|
||||
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
|
||||
const workspace = useMemo(() => {
|
||||
@@ -59,9 +64,38 @@ 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]);
|
||||
}, [sessionId, isNew, savedId]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full pt-2">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { usePanelChannel } from 'hooks/usePanelChannel';
|
||||
import { useSavedSessions, messagesToTranscript, type RawMessage } from 'state/useSavedSessions';
|
||||
@@ -9,6 +9,8 @@ import { SessionContextMenu } from './SessionContextMenu';
|
||||
|
||||
export const SessionList = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const isOnChatPage = location.pathname.startsWith('/chat');
|
||||
const { sessions, deleteSavedSession, resumeSession } = useSavedSessions();
|
||||
const [selected, setSelected] = usePanelChannel<SelectedSession>('chat:selected-session', null);
|
||||
const [isResuming, setIsResuming] = useState<number | null>(null);
|
||||
@@ -22,6 +24,10 @@ export const SessionList = () => {
|
||||
}, []);
|
||||
|
||||
const handleSelect = async (session: (typeof sessions)[number]) => {
|
||||
if (isOnChatPage) {
|
||||
navigate(`/chat/saved/${session.id}`);
|
||||
return;
|
||||
}
|
||||
setIsResuming(session.id);
|
||||
try {
|
||||
const result = await resumeSession(session.id);
|
||||
@@ -43,12 +49,11 @@ export const SessionList = () => {
|
||||
});
|
||||
const transcript = messagesToTranscript(rawMessages);
|
||||
setSelected({
|
||||
id: `resume:${session.id}`,
|
||||
id: `saved:${session.id}`,
|
||||
model: result.model,
|
||||
resumeSummary: transcript,
|
||||
initialMessages: chatMessages,
|
||||
});
|
||||
navigate(`/chat/new`, { replace: true });
|
||||
} catch {
|
||||
// Failed to resume
|
||||
} finally {
|
||||
@@ -57,9 +62,9 @@ export const SessionList = () => {
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (selected?.id === `resume:${id}`) {
|
||||
if (selected?.id === `saved:${id}`) {
|
||||
setSelected(null);
|
||||
navigate('/chat', { replace: true });
|
||||
if (isOnChatPage) navigate('/chat', { replace: true });
|
||||
}
|
||||
await deleteSavedSession(id);
|
||||
};
|
||||
@@ -90,7 +95,7 @@ export const SessionList = () => {
|
||||
)}
|
||||
|
||||
{sessions.map((session) => {
|
||||
const isActive = selected?.id === `resume:${session.id}`;
|
||||
const isActive = selected?.id === `saved:${session.id}`;
|
||||
return (
|
||||
<div
|
||||
key={session.id}
|
||||
|
||||
Reference in New Issue
Block a user