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]);
|
setChatMessages((previous) => [...previous, userMessage]);
|
||||||
setIsLoading(true);
|
setIsLoading(true); // Processing banner starts
|
||||||
setCanAbortSession(true);
|
setCanAbortSession(true);
|
||||||
setClaudeStatus({
|
setClaudeStatus({
|
||||||
text: 'Processing',
|
text: 'Processing',
|
||||||
|
|||||||
@@ -134,9 +134,10 @@ export function useChatRealtimeHandlers({
|
|||||||
latestMessage.data && typeof latestMessage.data === 'object'
|
latestMessage.data && typeof latestMessage.data === 'object'
|
||||||
? (latestMessage.data as Record<string, any>)
|
? (latestMessage.data as Record<string, any>)
|
||||||
: null;
|
: null;
|
||||||
|
const messageType = String(latestMessage.type);
|
||||||
|
|
||||||
const globalMessageTypes = ['projects_updated', 'taskmaster-project-updated', 'session-created'];
|
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([
|
const lifecycleMessageTypes = new Set([
|
||||||
'claude-complete',
|
'claude-complete',
|
||||||
'codex-complete',
|
'codex-complete',
|
||||||
@@ -146,6 +147,7 @@ export function useChatRealtimeHandlers({
|
|||||||
'cursor-error',
|
'cursor-error',
|
||||||
'codex-error',
|
'codex-error',
|
||||||
'gemini-error',
|
'gemini-error',
|
||||||
|
'error',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isClaudeSystemInit =
|
const isClaudeSystemInit =
|
||||||
@@ -168,9 +170,12 @@ export function useChatRealtimeHandlers({
|
|||||||
|
|
||||||
const activeViewSessionId =
|
const activeViewSessionId =
|
||||||
selectedSession?.id || currentSessionId || pendingViewSessionRef.current?.sessionId || null;
|
selectedSession?.id || currentSessionId || pendingViewSessionRef.current?.sessionId || null;
|
||||||
|
const hasPendingUnboundSession =
|
||||||
|
Boolean(pendingViewSessionRef.current) && !pendingViewSessionRef.current?.sessionId;
|
||||||
const isSystemInitForView =
|
const isSystemInitForView =
|
||||||
systemInitSessionId && (!activeViewSessionId || systemInitSessionId === activeViewSessionId);
|
systemInitSessionId && (!activeViewSessionId || systemInitSessionId === activeViewSessionId);
|
||||||
const shouldBypassSessionFilter = isGlobalMessage || Boolean(isSystemInitForView);
|
const shouldBypassSessionFilter = isGlobalMessage || Boolean(isSystemInitForView);
|
||||||
|
const isLifecycleMessage = lifecycleMessageTypes.has(messageType);
|
||||||
const isUnscopedError =
|
const isUnscopedError =
|
||||||
!latestMessage.sessionId &&
|
!latestMessage.sessionId &&
|
||||||
pendingViewSessionRef.current &&
|
pendingViewSessionRef.current &&
|
||||||
@@ -201,6 +206,30 @@ export function useChatRealtimeHandlers({
|
|||||||
setClaudeStatus(null);
|
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 markSessionsAsCompleted = (...sessionIds: Array<string | null | undefined>) => {
|
||||||
const normalizedSessionIds = collectSessionIds(...sessionIds);
|
const normalizedSessionIds = collectSessionIds(...sessionIds);
|
||||||
normalizedSessionIds.forEach((sessionId) => {
|
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 (!shouldBypassSessionFilter) {
|
||||||
if (!activeViewSessionId) {
|
if (!activeViewSessionId) {
|
||||||
if (latestMessage.sessionId && lifecycleMessageTypes.has(String(latestMessage.type))) {
|
if (latestMessage.sessionId && isLifecycleMessage && !hasPendingUnboundSession) {
|
||||||
handleBackgroundLifecycle(latestMessage.sessionId);
|
handleBackgroundLifecycle(latestMessage.sessionId);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (!isUnscopedError) {
|
if (!isUnscopedError && !hasPendingUnboundSession) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!latestMessage.sessionId && !isUnscopedError) {
|
if (!latestMessage.sessionId && !isUnscopedError && !hasPendingUnboundSession) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (latestMessage.sessionId !== activeViewSessionId) {
|
if (latestMessage.sessionId !== activeViewSessionId) {
|
||||||
if (latestMessage.sessionId && lifecycleMessageTypes.has(String(latestMessage.type))) {
|
const shouldTreatAsPendingViewLifecycle =
|
||||||
handleBackgroundLifecycle(latestMessage.sessionId);
|
!activeViewSessionId &&
|
||||||
|
hasPendingUnboundSession &&
|
||||||
|
latestMessage.sessionId &&
|
||||||
|
isLifecycleMessage;
|
||||||
|
|
||||||
|
if (!shouldTreatAsPendingViewLifecycle) {
|
||||||
|
if (latestMessage.sessionId && isLifecycleMessage) {
|
||||||
|
handleBackgroundLifecycle(latestMessage.sessionId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,6 +595,7 @@ export function useChatRealtimeHandlers({
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'claude-error':
|
case 'claude-error':
|
||||||
|
finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id);
|
||||||
setChatMessages((previous) => [
|
setChatMessages((previous) => [
|
||||||
...previous,
|
...previous,
|
||||||
{
|
{
|
||||||
@@ -604,6 +655,7 @@ export function useChatRealtimeHandlers({
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'cursor-error':
|
case 'cursor-error':
|
||||||
|
finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id);
|
||||||
setChatMessages((previous) => [
|
setChatMessages((previous) => [
|
||||||
...previous,
|
...previous,
|
||||||
{
|
{
|
||||||
@@ -618,8 +670,7 @@ export function useChatRealtimeHandlers({
|
|||||||
const cursorCompletedSessionId = latestMessage.sessionId || currentSessionId;
|
const cursorCompletedSessionId = latestMessage.sessionId || currentSessionId;
|
||||||
const pendingCursorSessionId = sessionStorage.getItem('pendingSessionId');
|
const pendingCursorSessionId = sessionStorage.getItem('pendingSessionId');
|
||||||
|
|
||||||
clearLoadingIndicators();
|
finalizeLifecycleForCurrentView(
|
||||||
markSessionsAsCompleted(
|
|
||||||
cursorCompletedSessionId,
|
cursorCompletedSessionId,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
selectedSession?.id,
|
selectedSession?.id,
|
||||||
@@ -701,8 +752,7 @@ export function useChatRealtimeHandlers({
|
|||||||
const completedSessionId =
|
const completedSessionId =
|
||||||
latestMessage.sessionId || currentSessionId || pendingSessionId;
|
latestMessage.sessionId || currentSessionId || pendingSessionId;
|
||||||
|
|
||||||
clearLoadingIndicators();
|
finalizeLifecycleForCurrentView(
|
||||||
markSessionsAsCompleted(
|
|
||||||
completedSessionId,
|
completedSessionId,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
selectedSession?.id,
|
selectedSession?.id,
|
||||||
@@ -718,7 +768,6 @@ export function useChatRealtimeHandlers({
|
|||||||
if (selectedProject && latestMessage.exitCode === 0) {
|
if (selectedProject && latestMessage.exitCode === 0) {
|
||||||
safeLocalStorage.removeItem(`chat_messages_${selectedProject.name}`);
|
safeLocalStorage.removeItem(`chat_messages_${selectedProject.name}`);
|
||||||
}
|
}
|
||||||
setPendingPermissionRequests([]);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -836,13 +885,11 @@ export function useChatRealtimeHandlers({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (codexData.type === 'turn_complete') {
|
if (codexData.type === 'turn_complete') {
|
||||||
clearLoadingIndicators();
|
finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id);
|
||||||
markSessionsAsCompleted(latestMessage.sessionId, currentSessionId, selectedSession?.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (codexData.type === 'turn_failed') {
|
if (codexData.type === 'turn_failed') {
|
||||||
clearLoadingIndicators();
|
finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id);
|
||||||
markSessionsAsCompleted(latestMessage.sessionId, currentSessionId, selectedSession?.id);
|
|
||||||
setChatMessages((previous) => [
|
setChatMessages((previous) => [
|
||||||
...previous,
|
...previous,
|
||||||
{
|
{
|
||||||
@@ -861,8 +908,7 @@ export function useChatRealtimeHandlers({
|
|||||||
const codexCompletedSessionId =
|
const codexCompletedSessionId =
|
||||||
latestMessage.sessionId || currentSessionId || codexPendingSessionId;
|
latestMessage.sessionId || currentSessionId || codexPendingSessionId;
|
||||||
|
|
||||||
clearLoadingIndicators();
|
finalizeLifecycleForCurrentView(
|
||||||
markSessionsAsCompleted(
|
|
||||||
codexCompletedSessionId,
|
codexCompletedSessionId,
|
||||||
codexActualSessionId,
|
codexActualSessionId,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
@@ -886,8 +932,7 @@ export function useChatRealtimeHandlers({
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'codex-error':
|
case 'codex-error':
|
||||||
setIsLoading(false);
|
finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id);
|
||||||
setCanAbortSession(false);
|
|
||||||
setChatMessages((previous) => [
|
setChatMessages((previous) => [
|
||||||
...previous,
|
...previous,
|
||||||
{
|
{
|
||||||
@@ -937,8 +982,7 @@ export function useChatRealtimeHandlers({
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'gemini-error':
|
case 'gemini-error':
|
||||||
setIsLoading(false);
|
finalizeLifecycleForCurrentView(latestMessage.sessionId, currentSessionId, selectedSession?.id);
|
||||||
setCanAbortSession(false);
|
|
||||||
setChatMessages((previous) => [
|
setChatMessages((previous) => [
|
||||||
...previous,
|
...previous,
|
||||||
{
|
{
|
||||||
@@ -990,13 +1034,11 @@ export function useChatRealtimeHandlers({
|
|||||||
const abortSucceeded = latestMessage.success !== false;
|
const abortSucceeded = latestMessage.success !== false;
|
||||||
|
|
||||||
if (abortSucceeded) {
|
if (abortSucceeded) {
|
||||||
clearLoadingIndicators();
|
finalizeLifecycleForCurrentView(abortedSessionId, currentSessionId, selectedSession?.id, pendingSessionId);
|
||||||
markSessionsAsCompleted(abortedSessionId, currentSessionId, selectedSession?.id, pendingSessionId);
|
|
||||||
if (pendingSessionId && (!abortedSessionId || pendingSessionId === abortedSessionId)) {
|
if (pendingSessionId && (!abortedSessionId || pendingSessionId === abortedSessionId)) {
|
||||||
sessionStorage.removeItem('pendingSessionId');
|
sessionStorage.removeItem('pendingSessionId');
|
||||||
}
|
}
|
||||||
|
|
||||||
setPendingPermissionRequests([]);
|
|
||||||
setChatMessages((previous) => [
|
setChatMessages((previous) => [
|
||||||
...previous,
|
...previous,
|
||||||
{
|
{
|
||||||
@@ -1093,6 +1135,12 @@ export function useChatRealtimeHandlers({
|
|||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ const useWebSocketProviderState = (): WebSocketContextType => {
|
|||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
setLatestMessage(data);
|
setLatestMessage(data);
|
||||||
|
console.log('--->Received WebSocket message:', data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error parsing WebSocket message:', error);
|
console.error('Error parsing WebSocket message:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user