From add5351dc7ba1cc7870a148380451ba5e47a5f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 24 Jul 2026 09:50:38 +0000 Subject: [PATCH] chat: list /chat sessions from Claude's store; fix the dedicated-cwd routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /chat detail (ChatDetailPanel) now passes context 'chat' to usePiChat, so the backend actually runs the session from claude_sessions (the earlier tag was on the wrong component). The left panel (SessionList) now reads GET /chat/sessions — Claude's own transcripts — instead of the old saved-sessions model. List-only: rows display title/time/count; click-to-resume comes next. Co-Authored-By: Claude Opus 4.8 --- .../src/apps/Chat/ChatPanelWrapper.tsx | 12 +- .../src/apps/ChatHistory/ChatDetailPanel.tsx | 4 +- .../src/apps/ChatHistory/SessionList.tsx | 131 ++++++------------ src/workspaces/state/src/index.ts | 2 + src/workspaces/state/src/useClaudeSessions.ts | 27 ++++ 5 files changed, 77 insertions(+), 99 deletions(-) create mode 100644 src/workspaces/state/src/useClaudeSessions.ts diff --git a/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx b/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx index 40a6f1f7..742c5013 100644 --- a/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/ChatPanelWrapper.tsx @@ -94,13 +94,11 @@ export const ChatPanelWrapper = () => { const chatContext = dashboardId === 'email' || dashboardId === 'screens/email' ? { context: 'email' as const } - : dashboardId === 'screens/chat' - ? { context: 'chat' as const } - : dashboardId?.startsWith('proj-layout-') - ? { context: 'project' as const, contextId: dashboardId.replace('proj-layout-', '') } - : dashboardId && !dashboardId.startsWith('screens/') - ? { context: 'dashboard' as const, contextId: dashboardId } - : {}; + : dashboardId?.startsWith('proj-layout-') + ? { context: 'project' as const, contextId: dashboardId.replace('proj-layout-', '') } + : dashboardId && !dashboardId.startsWith('screens/') + ? { context: 'dashboard' as const, contextId: dashboardId } + : {}; const [savedId, setSavedId] = usePanelChannel('chat:saved-id', null); const [selection, setSelection] = usePanelChannel('chat:panel-session', null); diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx index 3a2103d7..7854b7d3 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx @@ -106,7 +106,9 @@ function NewChat({ resumeSummary, initialMessages, savedId: initialSavedId }: Ne } }, [updateSessionMessages]); - const chat = usePiChat(undefined, locationState?.model, { resumeSummary, initialMessages, onTurnComplete }); + // context 'chat' tells the backend to run this session from the dedicated claude_sessions cwd, so + // its transcript lands in Claude's own store as an isolated project group (source of truth). + const chat = usePiChat(undefined, locationState?.model, { resumeSummary, initialMessages, onTurnComplete, context: 'chat' }); chatSessionRef.current = chat.sessionId; const isSaved = savedId != null; diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx index 9a6fa1b6..c46a716e 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx @@ -1,19 +1,16 @@ -import { useState, useRef, useCallback } from 'react'; -import { useNavigate, useLocation } from 'react-router'; -import { Plus, MessageSquare, Loader2 } from 'lucide-react'; +import { useRef, useCallback } from 'react'; +import { useNavigate } from 'react-router'; +import { Plus, MessageSquare, RefreshCw } from 'lucide-react'; import { usePanelChannel } from 'hooks/usePanelChannel'; -import { useSavedSessions, messagesToTranscript, type RawMessage } from 'state/useSavedSessions'; +import { useClaudeSessions } from 'state/useClaudeSessions'; import type { SelectedSession } from './ChatDetailPanel'; -import type { ChatMessage } from '../Chat/types'; -import { SessionContextMenu } from './SessionContextMenu'; +// Reads the /chat conversation list from Claude's own transcript store (source of truth). +// List-only for now: rows display sessions; clicking to resume is the next slice. export const SessionList = () => { const navigate = useNavigate(); - const location = useLocation(); - const isOnChatPage = location.pathname.startsWith('/chat'); - const { sessions, deleteSavedSession, resumeSession } = useSavedSessions(); + const { sessions, isLoading, refetch } = useClaudeSessions(); const [selected, setSelected] = usePanelChannel('chat:selected-session', null); - const [isResuming, setIsResuming] = useState(null); const scrolledRef = useRef(false); const selectedRef = useCallback((node: HTMLDivElement | null) => { @@ -23,115 +20,67 @@ 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); - 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:${session.id}`, - model: result.model, - resumeSummary: transcript, - initialMessages: chatMessages, - }); - } catch { - // Failed to resume - } finally { - setIsResuming(null); - } - }; - - const handleDelete = async (id: number) => { - if (selected?.id === `saved:${id}`) { - setSelected(null); - if (isOnChatPage) navigate('/chat', { replace: true }); - } - await deleteSavedSession(id); - }; - return (
{/* Header */}
-

Saved Sessions

- +

Sessions

+
+ + +
{/* Session list */}
{sessions.length === 0 && (
- No saved sessions yet. Save a chat session to see it here. + {isLoading ? 'Loading…' : 'No sessions yet. Start a new chat to see it here.'}
)} {sessions.map((session) => { - const isActive = selected?.id === `saved:${session.id}`; + const isActive = selected?.id === session.id; return (
- -
-
); diff --git a/src/workspaces/state/src/index.ts b/src/workspaces/state/src/index.ts index eeafeb67..ad3cd44e 100644 --- a/src/workspaces/state/src/index.ts +++ b/src/workspaces/state/src/index.ts @@ -5,6 +5,8 @@ export { useDashboardState } from './useDashboardState'; export { usePiModels, useVisiblePiModels, useUserVisibleModels, useEnabledPiModels, modelKey } from './useModels'; export type { ModelOption } from './useModels'; export { useAccessPolicy } from './useAccessPolicy'; +export { useClaudeSessions } from './useClaudeSessions'; +export type { ClaudeSessionSummary } from './useClaudeSessions'; export { useRecentModels } from './useRecentModels'; export { usePlans } from './usePlans'; export { useLandingPage } from './useLandingPage'; diff --git a/src/workspaces/state/src/useClaudeSessions.ts b/src/workspaces/state/src/useClaudeSessions.ts new file mode 100644 index 00000000..800e3f4c --- /dev/null +++ b/src/workspaces/state/src/useClaudeSessions.ts @@ -0,0 +1,27 @@ +import { useQuery } from '@tanstack/react-query'; +import { useClient } from 'hooks/useClient'; +import { useAuth } from 'hooks/useAuth'; + +export type ClaudeSessionSummary = { + id: string; // Claude session uuid (= transcript filename) + title: string; + cwd: string; + createdAt: string; + updatedAt: string; + messageCount: number; +}; + +/** The /chat route's sessions, read straight from Claude's own transcript store (source of truth). */ +export function useClaudeSessions() { + const client = useClient(); + const { isAuthenticated } = useAuth(); + + const { data, isLoading, refetch } = useQuery<{ sessions: ClaudeSessionSummary[] }>({ + queryKey: ['CLAUDE_SESSIONS'], + enabled: isAuthenticated, + queryFn: () => client.get<{ sessions: ClaudeSessionSummary[] }>('/chat/sessions'), + staleTime: 30 * 1000, + }); + + return { sessions: data?.sessions ?? [], isLoading, refetch }; +}