Compare commits

...

3 Commits

Author SHA1 Message Date
viper151
9cd1b5811a Merge branch 'main' into fix/session-streamed-to-another-chat 2026-01-20 12:59:42 +01:00
Haileyesus Dessie
ddb26c7652 fix: resolve issue with redirecting to original session after response completion 2026-01-16 14:04:37 +03:00
Haileyesus Dessie
b3c6e95971 fix: don't stream response to another session 2026-01-16 14:04:12 +03:00
4 changed files with 144 additions and 27 deletions

View File

@@ -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;

View File

@@ -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
};
};

View File

@@ -272,7 +272,8 @@ export async function queryCodex(command, options = {}, ws) {
data: {
used: totalTokens,
total: 200000 // Default context window for Codex models
}
},
sessionId: currentSessionId
});
}
}

View File

@@ -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 brandnew 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,77 @@ 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 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'
]);
// 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 systemInitSessionId = isClaudeSystemInit
? messageData?.session_id
: isCursorSystemInit
? latestMessage.data?.session_id
: null;
const activeViewSessionId = selectedSession?.id || currentSessionId || pendingViewSessionRef.current?.sessionId || null;
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;
}
}
if (!latestMessage.sessionId && !isUnscopedError) {
// Drop unscoped messages to prevent cross-session bleed.
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;
}
}
switch (latestMessage.type) {
@@ -3216,6 +3308,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 +3339,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) {
@@ -3313,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,
@@ -3336,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
@@ -3357,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
}
@@ -3525,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 });
@@ -4316,6 +4416,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);
}