mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-07 15:07:38 +00:00
fix(chat): finalize terminal lifecycle to prevent stuck processing/thinking UI (#483)
This commit is contained in:
@@ -551,7 +551,7 @@ export function useChatComposerState({
|
||||
};
|
||||
|
||||
setChatMessages((previous) => [...previous, userMessage]);
|
||||
setIsLoading(true);
|
||||
setIsLoading(true); // Processing banner starts
|
||||
setCanAbortSession(true);
|
||||
setClaudeStatus({
|
||||
text: 'Processing',
|
||||
|
||||
@@ -134,9 +134,10 @@ export function useChatRealtimeHandlers({
|
||||
latestMessage.data && typeof latestMessage.data === 'object'
|
||||
? (latestMessage.data as Record<string, any>)
|
||||
: null;
|
||||
const messageType = String(latestMessage.type);
|
||||
|
||||
const globalMessageTypes = ['projects_updated', 'taskmaster-project-updated', 'session-created'];
|
||||
const isGlobalMessage = globalMessageTypes.includes(String(latestMessage.type));
|
||||
const isGlobalMessage = globalMessageTypes.includes(messageType);
|
||||
const lifecycleMessageTypes = new Set([
|
||||
'claude-complete',
|
||||
'codex-complete',
|
||||
@@ -146,6 +147,7 @@ export function useChatRealtimeHandlers({
|
||||
'cursor-error',
|
||||
'codex-error',
|
||||
'gemini-error',
|
||||
'error',
|
||||
]);
|
||||
|
||||
const isClaudeSystemInit =
|
||||
@@ -168,9 +170,12 @@ export function useChatRealtimeHandlers({
|
||||
|
||||
const activeViewSessionId =
|
||||
selectedSession?.id || currentSessionId || pendingViewSessionRef.current?.sessionId || null;
|
||||
const hasPendingUnboundSession =
|
||||
Boolean(pendingViewSessionRef.current) && !pendingViewSessionRef.current?.sessionId;
|
||||
const isSystemInitForView =
|
||||
systemInitSessionId && (!activeViewSessionId || systemInitSessionId === activeViewSessionId);
|
||||
const shouldBypassSessionFilter = isGlobalMessage || Boolean(isSystemInitForView);
|
||||
const isLifecycleMessage = lifecycleMessageTypes.has(messageType);
|
||||
const isUnscopedError =
|
||||
!latestMessage.sessionId &&
|
||||
pendingViewSessionRef.current &&
|
||||
@@ -201,6 +206,30 @@ export function useChatRealtimeHandlers({
|
||||
setClaudeStatus(null);
|
||||
};
|
||||
|
||||
const clearPendingViewSession = (resolvedSessionId?: string | null) => {
|
||||
const pendingSession = pendingViewSessionRef.current;
|
||||
if (!pendingSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the in-view request never received a concrete session ID (or this terminal event
|
||||
// resolves the same pending session), clear it to avoid stale "in-flight" UI state.
|
||||
if (!pendingSession.sessionId || !resolvedSessionId || pendingSession.sessionId === resolvedSessionId) {
|
||||
pendingViewSessionRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const flushStreamingState = () => {
|
||||
if (streamTimerRef.current) {
|
||||
clearTimeout(streamTimerRef.current);
|
||||
streamTimerRef.current = null;
|
||||
}
|
||||
const pendingChunk = streamBufferRef.current;
|
||||
streamBufferRef.current = '';
|
||||
appendStreamingChunk(setChatMessages, pendingChunk, false);
|
||||
finalizeStreamingMessage(setChatMessages);
|
||||
};
|
||||
|
||||
const markSessionsAsCompleted = (...sessionIds: Array<string | null | undefined>) => {
|
||||
const normalizedSessionIds = collectSessionIds(...sessionIds);
|
||||
normalizedSessionIds.forEach((sessionId) => {
|
||||
@@ -209,25 +238,46 @@ export function useChatRealtimeHandlers({
|
||||
});
|
||||
};
|
||||
|
||||
const finalizeLifecycleForCurrentView = (...sessionIds: Array<string | null | undefined>) => {
|
||||
const pendingSessionId = typeof window !== 'undefined' ? sessionStorage.getItem('pendingSessionId') : null;
|
||||
const resolvedSessionIds = collectSessionIds(...sessionIds, pendingSessionId, pendingViewSessionRef.current?.sessionId);
|
||||
const resolvedPrimarySessionId = resolvedSessionIds[0] || null;
|
||||
|
||||
flushStreamingState();
|
||||
clearLoadingIndicators();
|
||||
markSessionsAsCompleted(...resolvedSessionIds);
|
||||
setPendingPermissionRequests([]);
|
||||
clearPendingViewSession(resolvedPrimarySessionId);
|
||||
};
|
||||
|
||||
if (!shouldBypassSessionFilter) {
|
||||
if (!activeViewSessionId) {
|
||||
if (latestMessage.sessionId && lifecycleMessageTypes.has(String(latestMessage.type))) {
|
||||
if (latestMessage.sessionId && isLifecycleMessage && !hasPendingUnboundSession) {
|
||||
handleBackgroundLifecycle(latestMessage.sessionId);
|
||||
return;
|
||||
}
|
||||
if (!isUnscopedError) {
|
||||
if (!isUnscopedError && !hasPendingUnboundSession) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!latestMessage.sessionId && !isUnscopedError) {
|
||||
if (!latestMessage.sessionId && !isUnscopedError && !hasPendingUnboundSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (latestMessage.sessionId !== activeViewSessionId) {
|
||||
if (latestMessage.sessionId && lifecycleMessageTypes.has(String(latestMessage.type))) {
|
||||
handleBackgroundLifecycle(latestMessage.sessionId);
|
||||
const shouldTreatAsPendingViewLifecycle =
|
||||
!activeViewSessionId &&
|
||||
hasPendingUnboundSession &&
|
||||
latestMessage.sessionId &&
|
||||
isLifecycleMessage;
|
||||
|
||||
if (!shouldTreatAsPendingViewLifecycle) {
|
||||
if (latestMessage.sessionId && isLifecycleMessage) {
|
||||
handleBackgroundLifecycle(latestMessage.sessionId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,6 +595,7 @@ export function useChatRealtimeHandlers({
|
||||
break;
|
||||
|
||||
case 'claude-error':
|
||||
finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id);
|
||||
setChatMessages((previous) => [
|
||||
...previous,
|
||||
{
|
||||
@@ -604,6 +655,7 @@ export function useChatRealtimeHandlers({
|
||||
break;
|
||||
|
||||
case 'cursor-error':
|
||||
finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id);
|
||||
setChatMessages((previous) => [
|
||||
...previous,
|
||||
{
|
||||
@@ -618,8 +670,7 @@ export function useChatRealtimeHandlers({
|
||||
const cursorCompletedSessionId = latestMessage.sessionId || currentSessionId;
|
||||
const pendingCursorSessionId = sessionStorage.getItem('pendingSessionId');
|
||||
|
||||
clearLoadingIndicators();
|
||||
markSessionsAsCompleted(
|
||||
finalizeLifecycleForCurrentView(
|
||||
cursorCompletedSessionId,
|
||||
currentSessionId,
|
||||
selectedSession?.id,
|
||||
@@ -701,8 +752,7 @@ export function useChatRealtimeHandlers({
|
||||
const completedSessionId =
|
||||
latestMessage.sessionId || currentSessionId || pendingSessionId;
|
||||
|
||||
clearLoadingIndicators();
|
||||
markSessionsAsCompleted(
|
||||
finalizeLifecycleForCurrentView(
|
||||
completedSessionId,
|
||||
currentSessionId,
|
||||
selectedSession?.id,
|
||||
@@ -718,7 +768,6 @@ export function useChatRealtimeHandlers({
|
||||
if (selectedProject && latestMessage.exitCode === 0) {
|
||||
safeLocalStorage.removeItem(`chat_messages_${selectedProject.name}`);
|
||||
}
|
||||
setPendingPermissionRequests([]);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -836,13 +885,11 @@ export function useChatRealtimeHandlers({
|
||||
}
|
||||
|
||||
if (codexData.type === 'turn_complete') {
|
||||
clearLoadingIndicators();
|
||||
markSessionsAsCompleted(latestMessage.sessionId, currentSessionId, selectedSession?.id);
|
||||
finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id);
|
||||
}
|
||||
|
||||
if (codexData.type === 'turn_failed') {
|
||||
clearLoadingIndicators();
|
||||
markSessionsAsCompleted(latestMessage.sessionId, currentSessionId, selectedSession?.id);
|
||||
finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id);
|
||||
setChatMessages((previous) => [
|
||||
...previous,
|
||||
{
|
||||
@@ -861,8 +908,7 @@ export function useChatRealtimeHandlers({
|
||||
const codexCompletedSessionId =
|
||||
latestMessage.sessionId || currentSessionId || codexPendingSessionId;
|
||||
|
||||
clearLoadingIndicators();
|
||||
markSessionsAsCompleted(
|
||||
finalizeLifecycleForCurrentView(
|
||||
codexCompletedSessionId,
|
||||
codexActualSessionId,
|
||||
currentSessionId,
|
||||
@@ -886,8 +932,7 @@ export function useChatRealtimeHandlers({
|
||||
}
|
||||
|
||||
case 'codex-error':
|
||||
setIsLoading(false);
|
||||
setCanAbortSession(false);
|
||||
finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id);
|
||||
setChatMessages((previous) => [
|
||||
...previous,
|
||||
{
|
||||
@@ -937,8 +982,7 @@ export function useChatRealtimeHandlers({
|
||||
}
|
||||
|
||||
case 'gemini-error':
|
||||
setIsLoading(false);
|
||||
setCanAbortSession(false);
|
||||
finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id);
|
||||
setChatMessages((previous) => [
|
||||
...previous,
|
||||
{
|
||||
@@ -990,13 +1034,11 @@ export function useChatRealtimeHandlers({
|
||||
const abortSucceeded = latestMessage.success !== false;
|
||||
|
||||
if (abortSucceeded) {
|
||||
clearLoadingIndicators();
|
||||
markSessionsAsCompleted(abortedSessionId, currentSessionId, selectedSession?.id, pendingSessionId);
|
||||
finalizeLifecycleForCurrentView(abortedSessionId, currentSessionId, selectedSession?.id, pendingSessionId);
|
||||
if (pendingSessionId && (!abortedSessionId || pendingSessionId === abortedSessionId)) {
|
||||
sessionStorage.removeItem('pendingSessionId');
|
||||
}
|
||||
|
||||
setPendingPermissionRequests([]);
|
||||
setChatMessages((previous) => [
|
||||
...previous,
|
||||
{
|
||||
@@ -1093,6 +1135,12 @@ export function useChatRealtimeHandlers({
|
||||
break;
|
||||
}
|
||||
|
||||
case 'error':
|
||||
// Generic backend failure (e.g., provider process failed before a provider-specific
|
||||
// completion event was emitted). Treat it as terminal for current view lifecycle.
|
||||
finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ const useWebSocketProviderState = (): WebSocketContextType => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
setLatestMessage(data);
|
||||
console.log('--->Received WebSocket message:', data);
|
||||
} catch (error) {
|
||||
console.error('Error parsing WebSocket message:', error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user