mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-17 05:12:02 +08:00
feat(chat): unify session gateway with stable IDs and a single WS protocol
The frontend previously juggled placeholder IDs, provider-native IDs, and session_created handoffs, which caused race conditions and provider-specific branching. This introduces app-allocated session IDs, a chat run registry with event replay, delta sidebar updates, and one kind-based websocket contract so the UI can treat every provider the same while JSONL remains the source of truth.
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
|
||||
import { useWebSocket } from '../../../contexts/WebSocketContext';
|
||||
import PermissionContext from '../../../contexts/PermissionContext';
|
||||
import { QuickSettingsPanel } from '../../quick-settings-panel';
|
||||
import type { ChatInterfaceProps, Provider } from '../types/types';
|
||||
@@ -22,7 +23,6 @@ function ChatInterface({
|
||||
selectedSession,
|
||||
ws,
|
||||
sendMessage,
|
||||
latestMessage,
|
||||
onFileOpen,
|
||||
onInputFocusChange,
|
||||
onSessionProcessing,
|
||||
@@ -40,14 +40,19 @@ function ChatInterface({
|
||||
onShowAllTasks,
|
||||
}: ChatInterfaceProps) {
|
||||
const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings();
|
||||
const { subscribe } = useWebSocket();
|
||||
const { t } = useTranslation('chat');
|
||||
|
||||
const sessionStore = useSessionStore();
|
||||
const streamTimerRef = useRef<number | null>(null);
|
||||
const accumulatedStreamRef = useRef('');
|
||||
// When each session's `check-session-status` was last sent; idle replies
|
||||
// older than a later local request are discarded as stale.
|
||||
// When each session's `chat.subscribe` was last sent; idle acks older than
|
||||
// a later local request are discarded as stale.
|
||||
const statusCheckSentAtRef = useRef(new Map<string, number>());
|
||||
// Highest live `seq` observed per session. Written by the realtime handler
|
||||
// on every sequenced frame, read whenever a `chat.subscribe` is sent so the
|
||||
// server replays only the events this client actually missed.
|
||||
const lastSeqRef = useRef(new Map<string, number>());
|
||||
|
||||
const resetStreamingState = useCallback(() => {
|
||||
if (streamTimerRef.current) {
|
||||
@@ -126,9 +131,18 @@ function ChatInterface({
|
||||
onSessionIdle,
|
||||
resetStreamingState,
|
||||
statusCheckSentAtRef,
|
||||
lastSeqRef,
|
||||
sessionStore,
|
||||
});
|
||||
|
||||
// Brand-new conversation: the composer allocated a stable session id via
|
||||
// the session gateway before the first send. Record it locally and put it
|
||||
// in the URL — this id never changes again, so there is no later handoff.
|
||||
const handleSessionEstablished = useCallback((sessionId: string) => {
|
||||
setCurrentSessionId(sessionId);
|
||||
onNavigateToSession?.(sessionId);
|
||||
}, [setCurrentSessionId, onNavigateToSession]);
|
||||
|
||||
const {
|
||||
input,
|
||||
setInput,
|
||||
@@ -191,6 +205,7 @@ function ChatInterface({
|
||||
sendMessage,
|
||||
sendByCtrlEnter,
|
||||
onSessionProcessing,
|
||||
onSessionEstablished: handleSessionEstablished,
|
||||
onInputFocusChange,
|
||||
onFileOpen,
|
||||
onShowSettings,
|
||||
@@ -201,9 +216,9 @@ function ChatInterface({
|
||||
});
|
||||
|
||||
// On WebSocket reconnect, re-fetch the current session's messages from the
|
||||
// server so missed streaming events are shown, then re-check the session's
|
||||
// processing status — the authoritative reply restores or clears the
|
||||
// activity indicator depending on whether the run is still active.
|
||||
// server so missed streaming events are shown, then re-subscribe — the
|
||||
// `chat_subscribed` ack restores or clears the activity indicator, replays
|
||||
// missed live events, and re-attaches a still-running stream to this socket.
|
||||
const handleWebSocketReconnect = useCallback(async () => {
|
||||
if (!selectedProject || !selectedSession) return;
|
||||
const providerVal =
|
||||
@@ -217,23 +232,28 @@ function ChatInterface({
|
||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
||||
});
|
||||
statusCheckSentAtRef.current.set(selectedSession.id, Date.now());
|
||||
sendMessage({ type: 'check-session-status', sessionId: selectedSession.id, provider: providerVal });
|
||||
sendMessage({
|
||||
type: 'chat.subscribe',
|
||||
sessions: [{
|
||||
sessionId: selectedSession.id,
|
||||
lastSeq: lastSeqRef.current.get(selectedSession.id) ?? 0,
|
||||
}],
|
||||
});
|
||||
}, [selectedProject, selectedSession, sendMessage, sessionStore]);
|
||||
|
||||
useChatRealtimeHandlers({
|
||||
latestMessage,
|
||||
subscribe,
|
||||
provider,
|
||||
selectedSession,
|
||||
currentSessionId,
|
||||
setCurrentSessionId,
|
||||
setTokenBudget,
|
||||
setPendingPermissionRequests,
|
||||
streamTimerRef,
|
||||
accumulatedStreamRef,
|
||||
lastSeqRef,
|
||||
statusCheckSentAtRef,
|
||||
onSessionProcessing,
|
||||
onSessionIdle,
|
||||
onNavigateToSession,
|
||||
onWebSocketReconnect: handleWebSocketReconnect,
|
||||
sessionStore,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user