Fix session processing state leaking across session switches

Root cause:
- Processing ownership was derived from UI view state in ChatInterface.
- While switching sessions, an in-flight isLoading=true could stamp the newly selected session as processing, even if a different session was actually running.
- session-status handling only promoted isProcessing=true and did not clear stale processing state on isProcessing=false, which could leave sessions blocked.
Changes:
- ChatInterface: removed implicit processing propagation effect that called onSessionProcessing(selectedSession?.id || currentSessionId) when isLoading was true.
  This decouples processing ownership from transient view/session transitions.
- useChatComposerState: added onSessionProcessing callback usage and explicitly marks processing at submit time for the concrete effectiveSessionId (non-temporary IDs only).
  This ties processing to the session that actually started work.
- useChatRealtimeHandlers: expanded session-status handling to support both states.
  - isProcessing=true: mark session as processing; set loading/abort only when it is the currently viewed session.
  - isProcessing=false: clear active+processing markers and clear loading indicators for the current session view.
Behavioral outcome:
- Running session A no longer blocks session B after navigation.
- Users can work in multiple sessions concurrently, and processing badges/loading state stay session-scoped.
Verification:
- npm run typecheck
- npm run build
This commit is contained in:
Haileyesus
2026-02-20 11:18:40 +03:00
parent 200332f4f5
commit d486ff91a2
3 changed files with 24 additions and 10 deletions

View File

@@ -47,6 +47,7 @@ interface UseChatComposerStateArgs {
sendMessage: (message: unknown) => void; sendMessage: (message: unknown) => void;
sendByCtrlEnter?: boolean; sendByCtrlEnter?: boolean;
onSessionActive?: (sessionId?: string | null) => void; onSessionActive?: (sessionId?: string | null) => void;
onSessionProcessing?: (sessionId?: string | null) => void;
onInputFocusChange?: (focused: boolean) => void; onInputFocusChange?: (focused: boolean) => void;
onFileOpen?: (filePath: string, diffInfo?: unknown) => void; onFileOpen?: (filePath: string, diffInfo?: unknown) => void;
onShowSettings?: () => void; onShowSettings?: () => void;
@@ -98,6 +99,7 @@ export function useChatComposerState({
sendMessage, sendMessage,
sendByCtrlEnter, sendByCtrlEnter,
onSessionActive, onSessionActive,
onSessionProcessing,
onInputFocusChange, onInputFocusChange,
onFileOpen, onFileOpen,
onShowSettings, onShowSettings,
@@ -569,6 +571,9 @@ export function useChatComposerState({
pendingViewSessionRef.current = { sessionId: null, startedAt: Date.now() }; pendingViewSessionRef.current = { sessionId: null, startedAt: Date.now() };
} }
onSessionActive?.(sessionToActivate); onSessionActive?.(sessionToActivate);
if (effectiveSessionId && !isTemporarySessionId(effectiveSessionId)) {
onSessionProcessing?.(effectiveSessionId);
}
const getToolsSettings = () => { const getToolsSettings = () => {
try { try {
@@ -666,6 +671,7 @@ export function useChatComposerState({
executeCommand, executeCommand,
isLoading, isLoading,
onSessionActive, onSessionActive,
onSessionProcessing,
pendingViewSessionRef, pendingViewSessionRef,
permissionMode, permissionMode,
provider, provider,

View File

@@ -956,12 +956,26 @@ export function useChatRealtimeHandlers({
case 'session-status': { case 'session-status': {
const statusSessionId = latestMessage.sessionId; const statusSessionId = latestMessage.sessionId;
if (!statusSessionId) {
break;
}
const isCurrentSession = const isCurrentSession =
statusSessionId === currentSessionId || (selectedSession && statusSessionId === selectedSession.id); statusSessionId === currentSessionId || (selectedSession && statusSessionId === selectedSession.id);
if (isCurrentSession && latestMessage.isProcessing) {
setIsLoading(true); if (latestMessage.isProcessing) {
setCanAbortSession(true);
onSessionProcessing?.(statusSessionId); onSessionProcessing?.(statusSessionId);
if (isCurrentSession) {
setIsLoading(true);
setCanAbortSession(true);
}
break;
}
onSessionInactive?.(statusSessionId);
onSessionNotProcessing?.(statusSessionId);
if (isCurrentSession) {
clearLoadingIndicators();
} }
break; break;
} }

View File

@@ -180,6 +180,7 @@ function ChatInterface({
sendMessage, sendMessage,
sendByCtrlEnter, sendByCtrlEnter,
onSessionActive, onSessionActive,
onSessionProcessing,
onInputFocusChange, onInputFocusChange,
onFileOpen, onFileOpen,
onShowSettings, onShowSettings,
@@ -238,13 +239,6 @@ function ChatInterface({
}; };
}, [canAbortSession, handleAbortSession, isLoading]); }, [canAbortSession, handleAbortSession, isLoading]);
useEffect(() => {
const processingSessionId = selectedSession?.id || currentSessionId;
if (processingSessionId && isLoading && onSessionProcessing) {
onSessionProcessing(processingSessionId);
}
}, [currentSessionId, isLoading, onSessionProcessing, selectedSession?.id]);
useEffect(() => { useEffect(() => {
return () => { return () => {
resetStreamingState(); resetStreamingState();