From b3c6e95971dcc5c5d74c2d832b135c289a9ce927 Mon Sep 17 00:00:00 2001 From: Haileyesus Dessie <118998054+blackmammoth@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:04:12 +0300 Subject: [PATCH 1/2] fix: don't stream response to another session --- server/claude-sdk.js | 9 ++-- server/cursor-cli.js | 26 ++++++---- server/openai-codex.js | 3 +- src/components/ChatInterface.jsx | 87 ++++++++++++++++++++++++++++---- 4 files changed, 102 insertions(+), 23 deletions(-) diff --git a/server/claude-sdk.js b/server/claude-sdk.js index 8366691..0f7c39a 100644 --- a/server/claude-sdk.js +++ b/server/claude-sdk.js @@ -603,7 +603,8 @@ async function queryClaudeSDK(command, options = {}, ws) { const transformedMessage = transformMessage(message); ws.send({ type: 'claude-response', - data: transformedMessage + data: transformedMessage, + sessionId: capturedSessionId || sessionId || null }); // Extract and send token budget updates from result messages @@ -613,7 +614,8 @@ async function queryClaudeSDK(command, options = {}, ws) { console.log('Token budget from modelUsage:', tokenBudget); ws.send({ type: 'token-budget', - data: tokenBudget + data: tokenBudget, + sessionId: capturedSessionId || sessionId || null }); } } @@ -651,7 +653,8 @@ async function queryClaudeSDK(command, options = {}, ws) { // Send error to WebSocket ws.send({ type: 'claude-error', - error: error.message + error: error.message, + sessionId: capturedSessionId || sessionId || null }); throw error; diff --git a/server/cursor-cli.js b/server/cursor-cli.js index 1e5c2e9..ffd20c3 100644 --- a/server/cursor-cli.js +++ b/server/cursor-cli.js @@ -114,7 +114,8 @@ async function spawnCursor(command, options = {}, ws) { // Send system info to frontend ws.send({ type: 'cursor-system', - data: response + data: response, + sessionId: capturedSessionId || sessionId || null }); } break; @@ -123,7 +124,8 @@ async function spawnCursor(command, options = {}, ws) { // Forward user message ws.send({ type: 'cursor-user', - data: response + data: response, + sessionId: capturedSessionId || sessionId || null }); break; @@ -142,7 +144,8 @@ async function spawnCursor(command, options = {}, ws) { type: 'text_delta', text: textContent } - } + }, + sessionId: capturedSessionId || sessionId || null }); } break; @@ -157,7 +160,8 @@ async function spawnCursor(command, options = {}, ws) { type: 'claude-response', data: { type: 'content_block_stop' - } + }, + sessionId: capturedSessionId || sessionId || null }); } @@ -174,7 +178,8 @@ async function spawnCursor(command, options = {}, ws) { // Forward any other message types ws.send({ type: 'cursor-response', - data: response + data: response, + sessionId: capturedSessionId || sessionId || null }); } } catch (parseError) { @@ -182,7 +187,8 @@ async function spawnCursor(command, options = {}, ws) { // If not JSON, send as raw text ws.send({ type: 'cursor-output', - data: line + data: line, + sessionId: capturedSessionId || sessionId || null }); } } @@ -193,7 +199,8 @@ async function spawnCursor(command, options = {}, ws) { console.error('Cursor CLI stderr:', data.toString()); ws.send({ type: 'cursor-error', - error: data.toString() + error: data.toString(), + sessionId: capturedSessionId || sessionId || null }); }); @@ -229,7 +236,8 @@ async function spawnCursor(command, options = {}, ws) { ws.send({ type: 'cursor-error', - error: error.message + error: error.message, + sessionId: capturedSessionId || sessionId || null }); reject(error); @@ -264,4 +272,4 @@ export { abortCursorSession, isCursorSessionActive, getActiveCursorSessions -}; \ No newline at end of file +}; diff --git a/server/openai-codex.js b/server/openai-codex.js index d83d39b..1967de4 100644 --- a/server/openai-codex.js +++ b/server/openai-codex.js @@ -272,7 +272,8 @@ export async function queryCodex(command, options = {}, ws) { data: { used: totalTokens, total: 200000 // Default context window for Codex models - } + }, + sessionId: currentSessionId }); } } diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index 42e0ee0..3654a60 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -1894,6 +1894,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess // Streaming throttle buffers const streamBufferRef = useRef(''); const streamTimerRef = useRef(null); + // Track the session that this view expects when starting a brandโ€‘new chat + // (prevents background sessions from streaming into a different view). + const pendingViewSessionRef = useRef(null); const commandQueryTimerRef = useRef(null); const [debouncedInput, setDebouncedInput] = useState(''); const [showFileDropdown, setShowFileDropdown] = useState(false); @@ -1930,6 +1933,14 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess // Track provider transitions so we only clear approvals when provider truly changes. // This does not sync with the backend; it just prevents UI prompts from disappearing. const lastProviderRef = useRef(provider); + + const resetStreamingState = useCallback(() => { + if (streamTimerRef.current) { + clearTimeout(streamTimerRef.current); + streamTimerRef.current = null; + } + streamBufferRef.current = ''; + }, []); // Load permission mode for the current session useEffect(() => { if (selectedSession?.id) { @@ -3004,6 +3015,15 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const sessionChanged = currentSessionId !== null && currentSessionId !== selectedSession.id; if (sessionChanged) { + if (!isSystemSessionChange) { + // Clear any streaming leftovers from the previous session + resetStreamingState(); + pendingViewSessionRef.current = null; + setChatMessages([]); + setSessionMessages([]); + setClaudeStatus(null); + setCanAbortSession(false); + } // Reset pagination state when switching sessions setMessagesOffset(0); setHasMoreMessages(false); @@ -3073,17 +3093,22 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess } } } else { - // Only clear messages if this is NOT a system-initiated session change AND we're not loading - // During system session changes or while loading, preserve the chat messages - if (!isSystemSessionChange && !isLoading) { + // New session view (no selected session) - always reset UI state + if (!isSystemSessionChange) { + resetStreamingState(); + pendingViewSessionRef.current = null; setChatMessages([]); setSessionMessages([]); + setClaudeStatus(null); + setCanAbortSession(false); + setIsLoading(false); } setCurrentSessionId(null); sessionStorage.removeItem('cursorSessionId'); setMessagesOffset(0); setHasMoreMessages(false); setTotalMessages(0); + setTokenBudget(null); } // Mark loading as complete after messages are set @@ -3094,7 +3119,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess }; loadMessages(); - }, [selectedSession, selectedProject, loadCursorSessionMessages, scrollToBottom, isSystemSessionChange]); + }, [selectedSession, selectedProject, loadCursorSessionMessages, scrollToBottom, isSystemSessionChange, resetStreamingState]); // External Message Update Handler: Reload messages when external CLI modifies current session // This triggers when App.jsx detects a JSONL file change for the currently-viewed session @@ -3133,6 +3158,13 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess } }, [externalMessageUpdate, selectedSession, selectedProject, loadCursorSessionMessages, loadSessionMessages, isNearBottom, autoScrollToBottom, scrollToBottom]); + // When the user navigates to a specific session, clear any pending "new session" marker. + useEffect(() => { + if (selectedSession?.id) { + pendingViewSessionRef.current = null; + } + }, [selectedSession?.id]); + // Update chatMessages when convertedMessages changes useEffect(() => { if (sessionMessages.length > 0) { @@ -3197,17 +3229,45 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess // Handle WebSocket messages if (messages.length > 0) { const latestMessage = messages[messages.length - 1]; + const messageData = latestMessage.data?.message || latestMessage.data; // Filter messages by session ID to prevent cross-session interference // Skip filtering for global messages that apply to all sessions const globalMessageTypes = ['projects_updated', 'taskmaster-project-updated', 'session-created', 'claude-complete', 'codex-complete']; const isGlobalMessage = globalMessageTypes.includes(latestMessage.type); - // For new sessions (currentSessionId is null), allow messages through - if (!isGlobalMessage && latestMessage.sessionId && currentSessionId && latestMessage.sessionId !== currentSessionId) { - // Message is for a different session, ignore it - console.log('โญ๏ธ Skipping message for different session:', latestMessage.sessionId, 'current:', currentSessionId); - return; + const isClaudeSystemInit = latestMessage.type === 'claude-response' && + messageData && + messageData.type === 'system' && + messageData.subtype === 'init'; + const isCursorSystemInit = latestMessage.type === 'cursor-system' && + latestMessage.data && + latestMessage.data.type === 'system' && + latestMessage.data.subtype === 'init'; + + const activeViewSessionId = selectedSession?.id || currentSessionId || pendingViewSessionRef.current?.sessionId || null; + const shouldBypassSessionFilter = isGlobalMessage || isClaudeSystemInit || isCursorSystemInit; + const isUnscopedError = !latestMessage.sessionId && + pendingViewSessionRef.current && + !pendingViewSessionRef.current.sessionId && + (latestMessage.type === 'claude-error' || latestMessage.type === 'cursor-error' || latestMessage.type === 'codex-error'); + + if (!shouldBypassSessionFilter) { + if (!activeViewSessionId) { + // No session in view; ignore session-scoped traffic. + if (!isUnscopedError) { + return; + } + } + if (!latestMessage.sessionId && !isUnscopedError) { + // Drop unscoped messages to prevent cross-session bleed. + return; + } + if (latestMessage.sessionId !== activeViewSessionId) { + // Message is for a different session, ignore it + console.log('??-?,? Skipping message for different session:', latestMessage.sessionId, 'current:', activeViewSessionId); + return; + } } switch (latestMessage.type) { @@ -3216,6 +3276,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess // Store it temporarily until conversation completes (prevents premature session association) if (latestMessage.sessionId && !currentSessionId) { sessionStorage.setItem('pendingSessionId', latestMessage.sessionId); + if (pendingViewSessionRef.current && !pendingViewSessionRef.current.sessionId) { + pendingViewSessionRef.current.sessionId = latestMessage.sessionId; + } // Mark as system change to prevent clearing messages when session ID updates setIsSystemSessionChange(true); @@ -3244,7 +3307,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess break; case 'claude-response': - const messageData = latestMessage.data.message || latestMessage.data; // Handle Cursor streaming format (content_block_delta / content_block_stop) if (messageData && typeof messageData === 'object' && messageData.type) { @@ -4316,6 +4378,11 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess // Session Protection: Mark session as active to prevent automatic project updates during conversation // Use existing session if available; otherwise a temporary placeholder until backend provides real ID const sessionToActivate = effectiveSessionId || `new-session-${Date.now()}`; + if (!effectiveSessionId && !selectedSession?.id) { + // We are starting a brand-new session in this view. Track it so we only + // accept streaming updates for this run. + pendingViewSessionRef.current = { sessionId: null, startedAt: Date.now() }; + } if (onSessionActive) { onSessionActive(sessionToActivate); } From ddb26c76526881ef623d18013e47a854c4b1ec30 Mon Sep 17 00:00:00 2001 From: Haileyesus Dessie <118998054+blackmammoth@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:04:37 +0300 Subject: [PATCH 2/2] fix: resolve issue with redirecting to original session after response completion --- src/components/ChatInterface.jsx | 48 ++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index 3654a60..87cdf38 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -3233,8 +3233,17 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess // Filter messages by session ID to prevent cross-session interference // Skip filtering for global messages that apply to all sessions - const globalMessageTypes = ['projects_updated', 'taskmaster-project-updated', 'session-created', 'claude-complete', 'codex-complete']; + const globalMessageTypes = ['projects_updated', 'taskmaster-project-updated', 'session-created']; const isGlobalMessage = globalMessageTypes.includes(latestMessage.type); + const lifecycleMessageTypes = new Set([ + 'claude-complete', + 'codex-complete', + 'cursor-result', + 'session-aborted', + 'claude-error', + 'cursor-error', + 'codex-error' + ]); const isClaudeSystemInit = latestMessage.type === 'claude-response' && messageData && @@ -3245,16 +3254,36 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess latestMessage.data.type === 'system' && latestMessage.data.subtype === 'init'; + const systemInitSessionId = isClaudeSystemInit + ? messageData?.session_id + : isCursorSystemInit + ? latestMessage.data?.session_id + : null; + const activeViewSessionId = selectedSession?.id || currentSessionId || pendingViewSessionRef.current?.sessionId || null; - const shouldBypassSessionFilter = isGlobalMessage || isClaudeSystemInit || isCursorSystemInit; + const isSystemInitForView = systemInitSessionId && (!activeViewSessionId || systemInitSessionId === activeViewSessionId); + const shouldBypassSessionFilter = isGlobalMessage || isSystemInitForView; const isUnscopedError = !latestMessage.sessionId && pendingViewSessionRef.current && !pendingViewSessionRef.current.sessionId && (latestMessage.type === 'claude-error' || latestMessage.type === 'cursor-error' || latestMessage.type === 'codex-error'); + const handleBackgroundLifecycle = (sessionId) => { + if (!sessionId) return; + if (onSessionInactive) { + onSessionInactive(sessionId); + } + if (onSessionNotProcessing) { + onSessionNotProcessing(sessionId); + } + }; + if (!shouldBypassSessionFilter) { if (!activeViewSessionId) { // No session in view; ignore session-scoped traffic. + if (latestMessage.sessionId && lifecycleMessageTypes.has(latestMessage.type)) { + handleBackgroundLifecycle(latestMessage.sessionId); + } if (!isUnscopedError) { return; } @@ -3264,6 +3293,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess return; } if (latestMessage.sessionId !== activeViewSessionId) { + if (latestMessage.sessionId && lifecycleMessageTypes.has(latestMessage.type)) { + handleBackgroundLifecycle(latestMessage.sessionId); + } // Message is for a different session, ignore it console.log('??-?,? Skipping message for different session:', latestMessage.sessionId, 'current:', activeViewSessionId); return; @@ -3375,7 +3407,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess latestMessage.data.subtype === 'init' && latestMessage.data.session_id && currentSessionId && - latestMessage.data.session_id !== currentSessionId) { + latestMessage.data.session_id !== currentSessionId && + isSystemInitForView) { console.log('๐Ÿ”„ Claude CLI session duplication detected:', { originalSession: currentSessionId, @@ -3398,7 +3431,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess if (latestMessage.data.type === 'system' && latestMessage.data.subtype === 'init' && latestMessage.data.session_id && - !currentSessionId) { + !currentSessionId && + isSystemInitForView) { console.log('๐Ÿ”„ New session init detected:', { newSession: latestMessage.data.session_id @@ -3419,7 +3453,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess latestMessage.data.subtype === 'init' && latestMessage.data.session_id && currentSessionId && - latestMessage.data.session_id === currentSessionId) { + latestMessage.data.session_id === currentSessionId && + isSystemInitForView) { console.log('๐Ÿ”„ System init message for current session, ignoring'); return; // Don't process the message further } @@ -3587,6 +3622,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess try { const cdata = latestMessage.data; if (cdata && cdata.type === 'system' && cdata.subtype === 'init' && cdata.session_id) { + if (!isSystemInitForView) { + return; + } // If we already have a session and this differs, switch (duplication/redirect) if (currentSessionId && cdata.session_id !== currentSessionId) { console.log('๐Ÿ”„ Cursor session switch detected:', { originalSession: currentSessionId, newSession: cdata.session_id });