diff --git a/src/apps/officer-web/App.tsx b/src/apps/officer-web/App.tsx index 5fd227ee..b51e99b6 100644 --- a/src/apps/officer-web/App.tsx +++ b/src/apps/officer-web/App.tsx @@ -54,6 +54,7 @@ export function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/apps/officer-web/Screens/Dashboard/ChatHistory/index.tsx b/src/apps/officer-web/Screens/Dashboard/ChatHistory/index.tsx index aa1644e5..8a1b0e0c 100644 --- a/src/apps/officer-web/Screens/Dashboard/ChatHistory/index.tsx +++ b/src/apps/officer-web/Screens/Dashboard/ChatHistory/index.tsx @@ -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('chat:selected-session', null); const rawWorkspace = useDashboardState('screens/chat', defaultLayout); const isMobile = useIsMobile(); const navigate = useNavigate(); - const mobilePanelId = isMobile && (sessionId || isNew) ? 'chat-detail' : undefined; + const { resumeSession } = useSavedSessions(); + const loadedSavedIdRef = useRef(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 (
diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx index 8f860178..9a6fa1b6 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx @@ -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('chat:selected-session', null); const [isResuming, setIsResuming] = useState(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 (