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="/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}
|
||||||
|
|||||||
Reference in New Issue
Block a user