From 4215c8df0a9fd4f7d4d3713aaf3d1838bd3785af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 24 Jul 2026 10:14:27 +0000 Subject: [PATCH] chat: delete sessions + auto-refresh the /chat list after each turn DELETE /chat/sessions/:id removes Claude's transcript file; the list gets a per-row delete button. The list also invalidates on turn-complete so new and continued sessions surface without a manual refresh. Co-Authored-By: Claude Opus 4.8 --- src/servers/api/chat/chat.ts | 10 ++- src/servers/api/chat/claude-sessions.ts | 10 ++- .../src/apps/ChatHistory/ChatDetailPanel.tsx | 6 +- .../src/apps/ChatHistory/SessionList.tsx | 70 ++++++++++++------- src/workspaces/state/src/useClaudeSessions.ts | 20 +++++- 5 files changed, 83 insertions(+), 33 deletions(-) diff --git a/src/servers/api/chat/chat.ts b/src/servers/api/chat/chat.ts index 762b0163..43ae7333 100644 --- a/src/servers/api/chat/chat.ts +++ b/src/servers/api/chat/chat.ts @@ -1,5 +1,5 @@ import { createRouter } from '../../create-router'; -import { getClaudeSessionsCwd, listClaudeSessions, loadClaudeSession } from './claude-sessions'; +import { getClaudeSessionsCwd, listClaudeSessions, loadClaudeSession, deleteClaudeSession } from './claude-sessions'; export const chatRouter = createRouter(); @@ -18,3 +18,11 @@ chatRouter.get('/sessions/:id', (ctx) => { if (!detail) return ctx.text('Not found', 404); return ctx.json(detail); }); + +// DELETE /chat/sessions/:id — remove a conversation (deletes Claude's transcript file). +chatRouter.delete('/sessions/:id', (ctx) => { + const email = ctx.get('user').email; + const ok = deleteClaudeSession(email, getClaudeSessionsCwd(email), ctx.req.param('id')); + if (!ok) return ctx.text('Not found', 404); + return ctx.json({ ok: true }); +}); diff --git a/src/servers/api/chat/claude-sessions.ts b/src/servers/api/chat/claude-sessions.ts index dc4eb413..4b03d236 100644 --- a/src/servers/api/chat/claude-sessions.ts +++ b/src/servers/api/chat/claude-sessions.ts @@ -1,4 +1,4 @@ -import { readdirSync, readFileSync, existsSync, statSync, mkdirSync } from 'node:fs'; +import { readdirSync, readFileSync, existsSync, statSync, mkdirSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { DATA_PATH } from '../../data-path'; @@ -187,6 +187,14 @@ export function loadClaudeSession(email: string, cwd: string, sessionId: string) return { id: sessionId, model, cwd: sessionCwd, messages }; } +/** Delete a session by removing its transcript file. Returns false if it didn't exist. */ +export function deleteClaudeSession(email: string, cwd: string, sessionId: string): boolean { + const filePath = join(claudeProjectsDir(email), projectSlug(cwd), `${sessionId}.jsonl`); + if (!existsSync(filePath)) return false; + rmSync(filePath); + return true; +} + /** List sessions Claude has stored for a given working directory, newest first. */ export function listClaudeSessions(email: string, cwd: string): ClaudeSessionSummary[] { const dir = join(claudeProjectsDir(email), projectSlug(cwd)); diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx index c88ebedb..dac31c72 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx @@ -5,6 +5,7 @@ import { usePanelChannel } from 'hooks/usePanelChannel'; import { useAuth } from 'hooks/useAuth'; import { getHostHome } from 'state/useModels'; import { useSavedSessions } from 'state/useSavedSessions'; +import { useClaudeSessions } from 'state/useClaudeSessions'; import { usePiChat, EmbeddableChat } from '../Chat'; import type { ChatMessage } from '../Chat/types'; @@ -92,6 +93,7 @@ function NewChat({ resumeSummary, resumeSessionId, initialMessages, savedId: ini const { user } = useAuth(); const isSuperAdmin = user?.role === 'Super Admin'; const { saveSession, updateSessionMessages } = useSavedSessions(); + const { invalidate: invalidateClaudeSessions } = useClaudeSessions(); const [isSaving, setIsSaving] = useState(false); const [savedId, setSavedId] = usePanelChannel(SAVED_ID_CHANNEL, initialSavedId ?? null); const savedIdRef = useRef(savedId); @@ -100,13 +102,15 @@ function NewChat({ resumeSummary, resumeSessionId, initialMessages, savedId: ini const resumedMessageCountRef = useRef(initialMessages?.length ?? 0); const onTurnComplete = useCallback(() => { + // Refresh the /chat list — Claude has just written/appended this session's transcript. + invalidateClaudeSessions(); const id = savedIdRef.current; const sid = chatSessionRef.current; if (id != null && sid) { const count = resumedMessageCountRef.current; updateSessionMessages(id, sid, count > 0 ? count : undefined).catch(() => {}); } - }, [updateSessionMessages]); + }, [updateSessionMessages, invalidateClaudeSessions]); // 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). diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx index f92913f4..c4ee3962 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx @@ -1,6 +1,6 @@ import { useRef, useState, useCallback } from 'react'; import { useNavigate } from 'react-router'; -import { Plus, MessageSquare, RefreshCw, Loader2 } from 'lucide-react'; +import { Plus, MessageSquare, RefreshCw, Loader2, Trash2 } from 'lucide-react'; import { usePanelChannel } from 'hooks/usePanelChannel'; import { useClaudeSessions } from 'state/useClaudeSessions'; import type { SelectedSession } from './ChatDetailPanel'; @@ -10,12 +10,12 @@ import type { ChatMessage } from '../Chat/types'; // Clicking a session loads its transcript and continues the real Claude session via --resume. export const SessionList = () => { const navigate = useNavigate(); - const { sessions, isLoading, refetch, loadSession } = useClaudeSessions(); + const { sessions, isLoading, refetch, loadSession, deleteSession } = useClaudeSessions(); const [selected, setSelected] = usePanelChannel('chat:selected-session', null); const [openingId, setOpeningId] = useState(null); const scrolledRef = useRef(false); - const selectedRef = useCallback((node: HTMLButtonElement | null) => { + const selectedRef = useCallback((node: HTMLDivElement | null) => { if (node && !scrolledRef.current) { scrolledRef.current = true; node.scrollIntoView({ block: 'center' }); @@ -38,6 +38,11 @@ export const SessionList = () => { } }; + const handleDelete = async (id: string) => { + if (selected?.id === id) setSelected(null); + await deleteSession(id); + }; + return (
{/* Header */} @@ -75,38 +80,49 @@ export const SessionList = () => { {sessions.map((session) => { const isActive = selected?.id === session.id; return ( -
- + + + ); })} diff --git a/src/workspaces/state/src/useClaudeSessions.ts b/src/workspaces/state/src/useClaudeSessions.ts index 4111a49c..a18d3c80 100644 --- a/src/workspaces/state/src/useClaudeSessions.ts +++ b/src/workspaces/state/src/useClaudeSessions.ts @@ -1,7 +1,10 @@ -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useCallback } from 'react'; import { useClient } from 'hooks/useClient'; import { useAuth } from 'hooks/useAuth'; +const QUERY_KEY = ['CLAUDE_SESSIONS']; + export type ClaudeSessionSummary = { id: string; // Claude session uuid (= transcript filename) title: string; @@ -22,10 +25,11 @@ export type ClaudeSessionDetail = { id: string; model: string; cwd: string; mess /** The /chat route's sessions, read straight from Claude's own transcript store (source of truth). */ export function useClaudeSessions() { const client = useClient(); + const queryClient = useQueryClient(); const { isAuthenticated } = useAuth(); const { data, isLoading, refetch } = useQuery<{ sessions: ClaudeSessionSummary[] }>({ - queryKey: ['CLAUDE_SESSIONS'], + queryKey: QUERY_KEY, enabled: isAuthenticated, queryFn: () => client.get<{ sessions: ClaudeSessionSummary[] }>('/chat/sessions'), staleTime: 30 * 1000, @@ -33,5 +37,15 @@ export function useClaudeSessions() { const loadSession = (id: string) => client.get(`/chat/sessions/${id}`); - return { sessions: data?.sessions ?? [], isLoading, refetch, loadSession }; + const invalidate = useCallback(() => queryClient.invalidateQueries({ queryKey: QUERY_KEY }), [queryClient]); + + const deleteSession = useCallback( + async (id: string) => { + await client.delete(`/chat/sessions/${id}`); + invalidate(); + }, + [client, invalidate], + ); + + return { sessions: data?.sessions ?? [], isLoading, refetch, loadSession, deleteSession, invalidate }; }