diff --git a/src/servers/api/chat/types.ts b/src/servers/api/chat/types.ts index fe4b8cee..6201291a 100644 --- a/src/servers/api/chat/types.ts +++ b/src/servers/api/chat/types.ts @@ -67,6 +67,11 @@ export type ClientMessage = } | { type: 'stop'; + } + | { + // Tear down the whole session (kill any in-flight turn + drop the in-memory session), not just + // the current turn. Frees the session so its transcript can be resumed elsewhere. + type: 'disconnect'; }; export type ServerMessage = @@ -117,6 +122,10 @@ export type ServerMessage = } | { type: 'stopped'; + } + | { + // Ack for a client 'disconnect': the session was torn down server-side. + type: 'disconnected'; }; export type ChatEvent = diff --git a/src/servers/api/chat/websocket.ts b/src/servers/api/chat/websocket.ts index 7e5695f3..d6a17d7d 100644 --- a/src/servers/api/chat/websocket.ts +++ b/src/servers/api/chat/websocket.ts @@ -120,6 +120,8 @@ export function message(ws: ServerWebSocket, raw: string | Buffer): void await handleResume(ws, clientMsg); } else if (clientMsg.type === 'stop') { await handleStop(ws); + } else if (clientMsg.type === 'disconnect') { + await handleDisconnect(ws); } } catch (err) { logger.error('Error handling WebSocket message', { email: ws.data.email, error: String(err) }); @@ -538,6 +540,23 @@ async function handleStop(ws: ServerWebSocket): Promise { sendToClient(ws, { type: 'stopped' }); } +// Tear down the whole session (not just the current turn): deleteSession fires _claudeKill (kills any +// in-flight Claude/OpenCode turn) + _sidecarUnsub, clears the idle timer, and drops the session from the +// manager's maps. The WS stays open so the client can immediately start a fresh session. +async function handleDisconnect(ws: ServerWebSocket): Promise { + const sessionId = wsToSessionMap.get(ws); + if (sessionId) { + try { + sessionManager.deleteSession(sessionId); + wsToSessionMap.delete(ws as any); + logger.info('Disconnected chat session', { sessionId }); + } catch (err) { + logger.error('Failed to disconnect session', { sessionId, error: String(err) }); + } + } + sendToClient(ws, { type: 'disconnected' }); +} + export const chatWebsocket = { open, message, diff --git a/src/workspaces/officerdev/src/apps/Chat/types.ts b/src/workspaces/officerdev/src/apps/Chat/types.ts index 83636e7c..5ce9b6a4 100644 --- a/src/workspaces/officerdev/src/apps/Chat/types.ts +++ b/src/workspaces/officerdev/src/apps/Chat/types.ts @@ -49,7 +49,8 @@ export type ServerMessage = | { type: 'result'; sessionId: string; cost: MessageCost } | { type: 'sync:messages'; sessionId: string; messages: Message[]; isGenerating: boolean; streamingText: string } | { type: 'error'; message: string; errorCode?: string } - | { type: 'stopped' }; + | { type: 'stopped' } + | { type: 'disconnected' }; export type Message = { id: string; diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx index 0f6d7fb6..28a2abc6 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx @@ -1,5 +1,6 @@ import { useCallback } from 'react'; import { useLocation } from 'react-router'; +import { Unplug } from 'lucide-react'; import { usePanelChannel } from 'hooks/usePanelChannel'; import { useAuth } from 'hooks/useAuth'; import { useClaudeSessions } from 'state/useClaudeSessions'; @@ -29,9 +30,10 @@ type DetailBarProps = { sessionTitle: string | undefined; isConnected: boolean; isGenerating: boolean; + onDisconnect?: () => void; }; -function DetailBar({ sessionTitle, isConnected, isGenerating }: DetailBarProps) { +function DetailBar({ sessionTitle, isConnected, isGenerating, onDisconnect }: DetailBarProps) { return (
@@ -46,6 +48,16 @@ function DetailBar({ sessionTitle, isConnected, isGenerating }: DetailBarProps) )} {!isConnected ? 'Disconnected' : isGenerating ? 'Working...' : ''} + {isConnected && onDisconnect && ( + + )}
); @@ -92,7 +104,7 @@ function NewChat({ resumeSummary, resumeSessionId, initialMessages }: NewChatPro return (
- +