mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-30 02:07:28 +00:00
feat: unified message architecture with provider adapters and session store (#558)
- Add provider adapter layer (server/providers/) with registry pattern
- Claude, Cursor, Codex, Gemini adapters normalize native formats to NormalizedMessage
- Shared types.js defines ProviderAdapter interface and message kinds
- Registry enables polymorphic provider lookup
- Add unified REST endpoint: GET /api/sessions/:id/messages?provider=...
- Replaces four provider-specific message endpoints with one
- Delegates to provider adapters via registry
- Add frontend session-keyed store (useSessionStore)
- Per-session Map with serverMessages/realtimeMessages/merged
- Dedup by ID, stale threshold for re-fetch, background session accumulation
- No localStorage for messages — backend JSONL is source of truth
- Add normalizedToChatMessages converter (useChatMessages)
- Converts NormalizedMessage[] to existing ChatMessage[] UI format
- Wire unified store into ChatInterface, useChatSessionState, useChatRealtimeHandlers
- Session switch uses store cache for instant render
- Background WebSocket messages routed to correct session slot
This commit is contained in:
@@ -3,10 +3,12 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
|
||||
import { QuickSettingsPanel } from '../../quick-settings-panel';
|
||||
import type { ChatInterfaceProps, Provider } from '../types/types';
|
||||
import type { SessionProvider } from '../../../types/app';
|
||||
import { useChatProviderState } from '../hooks/useChatProviderState';
|
||||
import { useChatSessionState } from '../hooks/useChatSessionState';
|
||||
import { useChatRealtimeHandlers } from '../hooks/useChatRealtimeHandlers';
|
||||
import { useChatComposerState } from '../hooks/useChatComposerState';
|
||||
import { useSessionStore } from '../../../stores/useSessionStore';
|
||||
import ChatMessagesPane from './subcomponents/ChatMessagesPane';
|
||||
import ChatComposer from './subcomponents/ChatComposer';
|
||||
|
||||
@@ -43,8 +45,10 @@ function ChatInterface({
|
||||
const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings();
|
||||
const { t } = useTranslation('chat');
|
||||
|
||||
const sessionStore = useSessionStore();
|
||||
const streamBufferRef = useRef('');
|
||||
const streamTimerRef = useRef<number | null>(null);
|
||||
const accumulatedStreamRef = useRef('');
|
||||
const pendingViewSessionRef = useRef<PendingViewSession | null>(null);
|
||||
|
||||
const resetStreamingState = useCallback(() => {
|
||||
@@ -53,6 +57,7 @@ function ChatInterface({
|
||||
streamTimerRef.current = null;
|
||||
}
|
||||
streamBufferRef.current = '';
|
||||
accumulatedStreamRef.current = '';
|
||||
}, []);
|
||||
|
||||
const {
|
||||
@@ -76,18 +81,17 @@ function ChatInterface({
|
||||
|
||||
const {
|
||||
chatMessages,
|
||||
setChatMessages,
|
||||
addMessage,
|
||||
clearMessages,
|
||||
rewindMessages,
|
||||
isLoading,
|
||||
setIsLoading,
|
||||
currentSessionId,
|
||||
setCurrentSessionId,
|
||||
sessionMessages,
|
||||
setSessionMessages,
|
||||
isLoadingSessionMessages,
|
||||
isLoadingMoreMessages,
|
||||
hasMoreMessages,
|
||||
totalMessages,
|
||||
setIsSystemSessionChange,
|
||||
canAbortSession,
|
||||
setCanAbortSession,
|
||||
isUserScrolledUp,
|
||||
@@ -109,7 +113,6 @@ function ChatInterface({
|
||||
scrollToBottom,
|
||||
scrollToBottomAndReset,
|
||||
handleScroll,
|
||||
loadSessionMessages,
|
||||
} = useChatSessionState({
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
@@ -120,6 +123,7 @@ function ChatInterface({
|
||||
processingSessions,
|
||||
resetStreamingState,
|
||||
pendingViewSessionRef,
|
||||
sessionStore,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -189,8 +193,9 @@ function ChatInterface({
|
||||
onShowSettings,
|
||||
pendingViewSessionRef,
|
||||
scrollToBottom,
|
||||
setChatMessages,
|
||||
setSessionMessages,
|
||||
addMessage,
|
||||
clearMessages,
|
||||
rewindMessages,
|
||||
setIsLoading,
|
||||
setCanAbortSession,
|
||||
setClaudeStatus,
|
||||
@@ -198,22 +203,19 @@ function ChatInterface({
|
||||
setPendingPermissionRequests,
|
||||
});
|
||||
|
||||
// On WebSocket reconnect, re-fetch the current session's messages from JSONL so missed
|
||||
// streaming events (e.g. from long tool calls while iOS had the tab backgrounded) are shown.
|
||||
// Also reset isLoading — if the server restarted or the session died mid-stream, the client
|
||||
// would be stuck in "Processing..." forever without this reset.
|
||||
// On WebSocket reconnect, re-fetch the current session's messages from the server
|
||||
// so missed streaming events are shown. Also reset isLoading.
|
||||
const handleWebSocketReconnect = useCallback(async () => {
|
||||
if (!selectedProject || !selectedSession) return;
|
||||
const provider = (localStorage.getItem('selected-provider') as any) || 'claude';
|
||||
const messages = await loadSessionMessages(selectedProject.name, selectedSession.id, false, provider);
|
||||
if (messages && messages.length > 0) {
|
||||
setChatMessages(messages);
|
||||
}
|
||||
// Reset loading state — if the session is still active, new WebSocket messages will
|
||||
// set it back to true. If it died, this clears the permanent frozen state.
|
||||
const providerVal = (localStorage.getItem('selected-provider') as SessionProvider) || 'claude';
|
||||
await sessionStore.refreshFromServer(selectedSession.id, {
|
||||
provider: (selectedSession.__provider || providerVal) as SessionProvider,
|
||||
projectName: selectedProject.name,
|
||||
projectPath: selectedProject.fullPath || selectedProject.path || '',
|
||||
});
|
||||
setIsLoading(false);
|
||||
setCanAbortSession(false);
|
||||
}, [selectedProject, selectedSession, loadSessionMessages, setChatMessages, setIsLoading, setCanAbortSession]);
|
||||
}, [selectedProject, selectedSession, sessionStore, setIsLoading, setCanAbortSession]);
|
||||
|
||||
useChatRealtimeHandlers({
|
||||
latestMessage,
|
||||
@@ -222,22 +224,22 @@ function ChatInterface({
|
||||
selectedSession,
|
||||
currentSessionId,
|
||||
setCurrentSessionId,
|
||||
setChatMessages,
|
||||
setIsLoading,
|
||||
setCanAbortSession,
|
||||
setClaudeStatus,
|
||||
setTokenBudget,
|
||||
setIsSystemSessionChange,
|
||||
setPendingPermissionRequests,
|
||||
pendingViewSessionRef,
|
||||
streamBufferRef,
|
||||
streamTimerRef,
|
||||
accumulatedStreamRef,
|
||||
onSessionInactive,
|
||||
onSessionProcessing,
|
||||
onSessionNotProcessing,
|
||||
onReplaceTemporarySession,
|
||||
onNavigateToSession,
|
||||
onWebSocketReconnect: handleWebSocketReconnect,
|
||||
sessionStore,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -319,7 +321,7 @@ function ChatInterface({
|
||||
isLoadingMoreMessages={isLoadingMoreMessages}
|
||||
hasMoreMessages={hasMoreMessages}
|
||||
totalMessages={totalMessages}
|
||||
sessionMessagesCount={sessionMessages.length}
|
||||
sessionMessagesCount={chatMessages.length}
|
||||
visibleMessageCount={visibleMessageCount}
|
||||
visibleMessages={visibleMessages}
|
||||
loadEarlierMessages={loadEarlierMessages}
|
||||
|
||||
Reference in New Issue
Block a user