mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-06 04:55:45 +08:00
Fix/websocket streaming issues (#748)
This commit is contained in:
@@ -10,6 +10,7 @@ import type {
|
||||
TouchEvent,
|
||||
} from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
|
||||
import { authenticatedFetch } from '../../../utils/api';
|
||||
import { thinkingModes } from '../constants/thinkingModes';
|
||||
import { grantClaudeToolPermission } from '../utils/chatPermissions';
|
||||
@@ -21,6 +22,7 @@ import type {
|
||||
} from '../types/types';
|
||||
import type { Project, ProjectSession, LLMProvider } from '../../../types/app';
|
||||
import { escapeRegExp } from '../utils/chatFormatting';
|
||||
|
||||
import { useFileMentions } from './useFileMentions';
|
||||
import { type SlashCommand, useSlashCommands } from './useSlashCommands';
|
||||
|
||||
@@ -80,9 +82,6 @@ const createFakeSubmitEvent = () => {
|
||||
return { preventDefault: () => undefined } as unknown as FormEvent<HTMLFormElement>;
|
||||
};
|
||||
|
||||
const isTemporarySessionId = (sessionId: string | null | undefined) =>
|
||||
Boolean(sessionId && sessionId.startsWith('new-session-'));
|
||||
|
||||
const getNotificationSessionSummary = (
|
||||
selectedSession: ProjectSession | null,
|
||||
fallbackInput: string,
|
||||
@@ -533,7 +532,6 @@ export function useChatComposerState({
|
||||
|
||||
const effectiveSessionId =
|
||||
currentSessionId || selectedSession?.id || sessionStorage.getItem('cursorSessionId');
|
||||
const sessionToActivate = effectiveSessionId || `new-session-${Date.now()}`;
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
type: 'user',
|
||||
@@ -559,10 +557,12 @@ export function useChatComposerState({
|
||||
// Reset stale pending IDs from previous interrupted runs before creating a new one.
|
||||
sessionStorage.removeItem('pendingSessionId');
|
||||
}
|
||||
// For new sessions we intentionally keep this as `null` until the backend
|
||||
// emits `session_created` with the canonical provider session id.
|
||||
pendingViewSessionRef.current = { sessionId: null, startedAt: Date.now() };
|
||||
}
|
||||
onSessionActive?.(sessionToActivate);
|
||||
if (effectiveSessionId && !isTemporarySessionId(effectiveSessionId)) {
|
||||
if (effectiveSessionId) {
|
||||
onSessionActive?.(effectiveSessionId);
|
||||
onSessionProcessing?.(effectiveSessionId);
|
||||
}
|
||||
|
||||
@@ -868,7 +868,7 @@ export function useChatComposerState({
|
||||
];
|
||||
|
||||
const targetSessionId =
|
||||
candidateSessionIds.find((sessionId) => Boolean(sessionId) && !isTemporarySessionId(sessionId)) || null;
|
||||
candidateSessionIds.find((sessionId) => Boolean(sessionId)) || null;
|
||||
|
||||
if (!targetSessionId) {
|
||||
console.warn('Abort requested but no concrete session ID is available yet.');
|
||||
|
||||
@@ -11,8 +11,9 @@ import { decodeHtmlEntities, unescapeWithMathProtection, formatUsageLimitText }
|
||||
* Convert NormalizedMessage[] from the session store into ChatMessage[]
|
||||
* that the existing UI components expect.
|
||||
*
|
||||
* Internal/system content (e.g. <system-reminder>, <command-name>) is already
|
||||
* filtered server-side by the Claude provider module.
|
||||
* Truly internal/system content is already filtered server-side. Some Claude
|
||||
* transcript artifacts such as local slash commands and compact summaries are
|
||||
* intentionally preserved and annotated so they can render like normal chat.
|
||||
*/
|
||||
export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMessage[] {
|
||||
const converted: ChatMessage[] = [];
|
||||
@@ -26,6 +27,16 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes
|
||||
}
|
||||
|
||||
for (const msg of messages) {
|
||||
const sharedMetadata = {
|
||||
displayText: msg.displayText,
|
||||
commandName: msg.commandName,
|
||||
commandMessage: msg.commandMessage,
|
||||
commandArgs: msg.commandArgs,
|
||||
isLocalCommand: msg.isLocalCommand,
|
||||
isLocalCommandStdout: msg.isLocalCommandStdout,
|
||||
isCompactSummary: msg.isCompactSummary,
|
||||
};
|
||||
|
||||
switch (msg.kind) {
|
||||
case 'text': {
|
||||
const content = msg.content || '';
|
||||
@@ -42,12 +53,14 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes
|
||||
timestamp: msg.timestamp,
|
||||
isTaskNotification: true,
|
||||
taskStatus: taskNotifMatch[1]?.trim() || 'completed',
|
||||
...sharedMetadata,
|
||||
});
|
||||
} else {
|
||||
converted.push({
|
||||
type: 'user',
|
||||
content: unescapeWithMathProtection(decodeHtmlEntities(content)),
|
||||
timestamp: msg.timestamp,
|
||||
...sharedMetadata,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -58,6 +71,7 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes
|
||||
type: 'assistant',
|
||||
content: text,
|
||||
timestamp: msg.timestamp,
|
||||
...sharedMetadata,
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -106,6 +120,7 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes
|
||||
isComplete: Boolean(toolResult),
|
||||
}
|
||||
: undefined,
|
||||
...sharedMetadata,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -117,6 +132,7 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes
|
||||
content: unescapeWithMathProtection(msg.content),
|
||||
timestamp: msg.timestamp,
|
||||
isThinking: true,
|
||||
...sharedMetadata,
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -126,6 +142,7 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes
|
||||
type: 'error',
|
||||
content: msg.content || 'Unknown error',
|
||||
timestamp: msg.timestamp,
|
||||
...sharedMetadata,
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -135,6 +152,7 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes
|
||||
content: msg.content || '',
|
||||
timestamp: msg.timestamp,
|
||||
isInteractivePrompt: true,
|
||||
...sharedMetadata,
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -145,6 +163,7 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes
|
||||
timestamp: msg.timestamp,
|
||||
isTaskNotification: true,
|
||||
taskStatus: msg.status || 'completed',
|
||||
...sharedMetadata,
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -155,6 +174,7 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes
|
||||
content: msg.content,
|
||||
timestamp: msg.timestamp,
|
||||
isStreaming: true,
|
||||
...sharedMetadata,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||
|
||||
import { usePaletteOps } from '../../../contexts/PaletteOpsContext';
|
||||
import type { PendingPermissionRequest, SessionNavigationOptions } from '../types/types';
|
||||
import type { Project, ProjectSession, LLMProvider } from '../../../types/app';
|
||||
import type { ProjectSession, LLMProvider } from '../../../types/app';
|
||||
import type { SessionStore, NormalizedMessage } from '../../../stores/useSessionStore';
|
||||
|
||||
type PendingViewSession = {
|
||||
@@ -51,7 +51,6 @@ type LatestChatMessage = {
|
||||
interface UseChatRealtimeHandlersArgs {
|
||||
latestMessage: LatestChatMessage | null;
|
||||
provider: LLMProvider;
|
||||
selectedProject: Project | null;
|
||||
selectedSession: ProjectSession | null;
|
||||
currentSessionId: string | null;
|
||||
setCurrentSessionId: (sessionId: string | null) => void;
|
||||
@@ -61,13 +60,11 @@ interface UseChatRealtimeHandlersArgs {
|
||||
setTokenBudget: (budget: Record<string, unknown> | null) => void;
|
||||
setPendingPermissionRequests: Dispatch<SetStateAction<PendingPermissionRequest[]>>;
|
||||
pendingViewSessionRef: MutableRefObject<PendingViewSession | null>;
|
||||
streamBufferRef: MutableRefObject<string>;
|
||||
streamTimerRef: MutableRefObject<number | null>;
|
||||
accumulatedStreamRef: MutableRefObject<string>;
|
||||
onSessionInactive?: (sessionId?: string | null) => void;
|
||||
onSessionProcessing?: (sessionId?: string | null) => void;
|
||||
onSessionNotProcessing?: (sessionId?: string | null) => void;
|
||||
onReplaceTemporarySession?: (sessionId?: string | null) => void;
|
||||
onNavigateToSession?: (sessionId: string, options?: SessionNavigationOptions) => void;
|
||||
onWebSocketReconnect?: () => void;
|
||||
sessionStore: SessionStore;
|
||||
@@ -80,7 +77,6 @@ interface UseChatRealtimeHandlersArgs {
|
||||
export function useChatRealtimeHandlers({
|
||||
latestMessage,
|
||||
provider,
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
currentSessionId,
|
||||
setCurrentSessionId,
|
||||
@@ -90,13 +86,11 @@ export function useChatRealtimeHandlers({
|
||||
setTokenBudget,
|
||||
setPendingPermissionRequests,
|
||||
pendingViewSessionRef,
|
||||
streamBufferRef,
|
||||
streamTimerRef,
|
||||
accumulatedStreamRef,
|
||||
onSessionInactive,
|
||||
onSessionProcessing,
|
||||
onSessionNotProcessing,
|
||||
onReplaceTemporarySession,
|
||||
onNavigateToSession,
|
||||
onWebSocketReconnect,
|
||||
sessionStore,
|
||||
@@ -187,7 +181,6 @@ export function useChatRealtimeHandlers({
|
||||
if (msg.kind === 'stream_delta') {
|
||||
const text = msg.content || '';
|
||||
if (!text) return;
|
||||
streamBufferRef.current += text;
|
||||
accumulatedStreamRef.current += text;
|
||||
if (!streamTimerRef.current) {
|
||||
streamTimerRef.current = window.setTimeout(() => {
|
||||
@@ -216,12 +209,18 @@ export function useChatRealtimeHandlers({
|
||||
sessionStore.finalizeStreaming(sid);
|
||||
}
|
||||
accumulatedStreamRef.current = '';
|
||||
streamBufferRef.current = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// --- All other messages: route to store ---
|
||||
if (sid) {
|
||||
const shouldPersist =
|
||||
msg.kind !== 'session_created'
|
||||
&& msg.kind !== 'complete'
|
||||
&& msg.kind !== 'status'
|
||||
&& msg.kind !== 'permission_request'
|
||||
&& msg.kind !== 'permission_cancelled';
|
||||
|
||||
if (sid && shouldPersist) {
|
||||
sessionStore.appendRealtime(sid, msg as NormalizedMessage);
|
||||
}
|
||||
|
||||
@@ -231,13 +230,16 @@ export function useChatRealtimeHandlers({
|
||||
const newSessionId = msg.newSessionId;
|
||||
if (!newSessionId) break;
|
||||
|
||||
if (!currentSessionId || currentSessionId.startsWith('new-session-')) {
|
||||
// We no longer synthesize client-side placeholder IDs. Until the provider
|
||||
// announces `session_created`, the active id is expected to be null.
|
||||
if (!currentSessionId) {
|
||||
console.log('Session created with ID:', newSessionId);
|
||||
console.log('Existing session ID:', currentSessionId);
|
||||
sessionStorage.setItem('pendingSessionId', newSessionId);
|
||||
if (pendingViewSessionRef.current && !pendingViewSessionRef.current.sessionId) {
|
||||
pendingViewSessionRef.current.sessionId = newSessionId;
|
||||
}
|
||||
setCurrentSessionId(newSessionId);
|
||||
onReplaceTemporarySession?.(newSessionId);
|
||||
setPendingPermissionRequests((prev) =>
|
||||
prev.map((r) => (r.sessionId ? r : { ...r, sessionId: newSessionId })),
|
||||
);
|
||||
@@ -257,7 +259,6 @@ export function useChatRealtimeHandlers({
|
||||
sessionStore.finalizeStreaming(sid);
|
||||
}
|
||||
accumulatedStreamRef.current = '';
|
||||
streamBufferRef.current = '';
|
||||
|
||||
setIsLoading(false);
|
||||
setCanAbortSession(false);
|
||||
@@ -386,7 +387,6 @@ export function useChatRealtimeHandlers({
|
||||
}, [
|
||||
latestMessage,
|
||||
provider,
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
currentSessionId,
|
||||
setCurrentSessionId,
|
||||
@@ -396,13 +396,11 @@ export function useChatRealtimeHandlers({
|
||||
setTokenBudget,
|
||||
setPendingPermissionRequests,
|
||||
pendingViewSessionRef,
|
||||
streamBufferRef,
|
||||
streamTimerRef,
|
||||
accumulatedStreamRef,
|
||||
onSessionInactive,
|
||||
onSessionProcessing,
|
||||
onSessionNotProcessing,
|
||||
onReplaceTemporarySession,
|
||||
onNavigateToSession,
|
||||
onWebSocketReconnect,
|
||||
sessionStore,
|
||||
|
||||
@@ -182,6 +182,7 @@ export function useChatSessionState({
|
||||
messagesOffsetRef.current = 0;
|
||||
setHasMoreMessages(false);
|
||||
setTotalMessages(0);
|
||||
|
||||
setTokenBudget(null);
|
||||
setVisibleMessageCount(INITIAL_VISIBLE_MESSAGES);
|
||||
setAllMessagesLoaded(false);
|
||||
@@ -318,7 +319,6 @@ export function useChatSessionState({
|
||||
if (!hasMoreMessages || !selectedSession || !selectedProject) return false;
|
||||
|
||||
const sessionProvider = selectedSession.__provider || 'claude';
|
||||
if (sessionProvider === 'cursor') return false;
|
||||
|
||||
isLoadingMoreRef.current = true;
|
||||
const previousScrollHeight = container.scrollHeight;
|
||||
@@ -551,7 +551,6 @@ export function useChatSessionState({
|
||||
const scrollToTarget = async () => {
|
||||
if (!allMessagesLoadedRef.current && selectedSession && selectedProject) {
|
||||
const sessionProvider = selectedSession.__provider || 'claude';
|
||||
if (sessionProvider !== 'cursor') {
|
||||
try {
|
||||
// Load all messages into the store for search navigation
|
||||
const slot = await sessionStore.fetchFromServer(selectedSession.id, {
|
||||
@@ -573,7 +572,6 @@ export function useChatSessionState({
|
||||
} catch {
|
||||
// Fall through and scroll in current messages
|
||||
}
|
||||
}
|
||||
}
|
||||
setVisibleMessageCount(Infinity);
|
||||
|
||||
@@ -628,7 +626,7 @@ export function useChatSessionState({
|
||||
|
||||
// Token usage fetch for Claude
|
||||
useEffect(() => {
|
||||
if (!selectedProject || !selectedSession?.id || selectedSession.id.startsWith('new-session-')) {
|
||||
if (!selectedProject || !selectedSession?.id) {
|
||||
setTokenBudget(null);
|
||||
return;
|
||||
}
|
||||
@@ -721,15 +719,6 @@ export function useChatSessionState({
|
||||
if (!selectedSession || !selectedProject) return;
|
||||
if (isLoadingAllMessages) return;
|
||||
const sessionProvider = selectedSession.__provider || 'claude';
|
||||
if (sessionProvider === 'cursor') {
|
||||
setVisibleMessageCount(Infinity);
|
||||
setAllMessagesLoaded(true);
|
||||
allMessagesLoadedRef.current = true;
|
||||
setLoadAllJustFinished(true);
|
||||
if (loadAllFinishedTimerRef.current) clearTimeout(loadAllFinishedTimerRef.current);
|
||||
loadAllFinishedTimerRef.current = setTimeout(() => { setLoadAllJustFinished(false); setShowLoadAllOverlay(false); }, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
const requestSessionId = selectedSession.id;
|
||||
allMessagesLoadedRef.current = true;
|
||||
|
||||
Reference in New Issue
Block a user