From 9da8e69476a795ce70f0705d444208f5e77ad910 Mon Sep 17 00:00:00 2001
From: Haileyesus Dessie <118998054+blackmammoth@users.noreply.github.com>
Date: Wed, 14 Jan 2026 17:01:38 +0300
Subject: [PATCH 1/3] feat: add highlight for file mentions in chat input
---
src/components/ChatInterface.jsx | 68 +++++++++++++++++++++++++++++++-
1 file changed, 66 insertions(+), 2 deletions(-)
diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx
index a9d5110..909d718 100644
--- a/src/components/ChatInterface.jsx
+++ b/src/components/ChatInterface.jsx
@@ -93,6 +93,10 @@ function unescapeWithMathProtection(text) {
return processedText;
}
+function escapeRegExp(value) {
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
// Small wrapper to keep markdown behavior consistent in one place
const Markdown = ({ children, className }) => {
const content = normalizeInlineCodeFences(String(children ?? ''));
@@ -1855,6 +1859,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
const messagesEndRef = useRef(null);
const textareaRef = useRef(null);
const inputContainerRef = useRef(null);
+ const inputHighlightRef = useRef(null);
const scrollContainerRef = useRef(null);
const isLoadingSessionRef = useRef(false); // Track session loading to prevent multiple scrolls
const isLoadingMoreRef = useRef(false);
@@ -1867,6 +1872,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
const [debouncedInput, setDebouncedInput] = useState('');
const [showFileDropdown, setShowFileDropdown] = useState(false);
const [fileList, setFileList] = useState([]);
+ const [fileMentions, setFileMentions] = useState([]);
const [filteredFiles, setFilteredFiles] = useState([]);
const [selectedFileIndex, setSelectedFileIndex] = useState(-1);
const [cursorPosition, setCursorPosition] = useState(0);
@@ -3962,6 +3968,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
return result;
};
+
// Handle @ symbol detection and file filtering
useEffect(() => {
const textBeforeCursor = input.slice(0, cursorPosition);
@@ -3992,6 +3999,43 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
}
}, [input, cursorPosition, fileList]);
+ const activeFileMentions = useMemo(() => {
+ if (!input || fileMentions.length === 0) return [];
+ return fileMentions.filter(path => input.includes(path));
+ }, [fileMentions, input]);
+
+ const sortedFileMentions = useMemo(() => {
+ if (activeFileMentions.length === 0) return [];
+ const unique = Array.from(new Set(activeFileMentions));
+ return unique.sort((a, b) => b.length - a.length);
+ }, [activeFileMentions]);
+
+ const fileMentionRegex = useMemo(() => {
+ if (sortedFileMentions.length === 0) return null;
+ const pattern = sortedFileMentions.map(escapeRegExp).join('|');
+ return new RegExp(`(${pattern})`, 'g');
+ }, [sortedFileMentions]);
+
+ const fileMentionSet = useMemo(() => new Set(sortedFileMentions), [sortedFileMentions]);
+
+ const renderInputWithMentions = useCallback((text) => {
+ if (!text) return '';
+ if (!fileMentionRegex) return text;
+ const parts = text.split(fileMentionRegex);
+ return parts.map((part, index) => (
+ fileMentionSet.has(part) ? (
+
+ {part}
+
+ ) : (
+ {part}
+ )
+ ));
+ }, [fileMentionRegex, fileMentionSet]);
+
// Debounced input handling
useEffect(() => {
const timer = setTimeout(() => {
@@ -4566,8 +4610,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
const spaceIndex = textAfterAtQuery.indexOf(' ');
const textAfterQuery = spaceIndex !== -1 ? textAfterAtQuery.slice(spaceIndex) : '';
- const newInput = textBeforeAt + '@' + file.path + ' ' + textAfterQuery;
- const newCursorPos = textBeforeAt.length + 1 + file.path.length + 1;
+ const newInput = textBeforeAt + file.path + ' ' + textAfterQuery;
+ const newCursorPos = textBeforeAt.length + file.path.length + 1;
// Immediately ensure focus is maintained
if (textareaRef.current && !textareaRef.current.matches(':focus')) {
@@ -4577,6 +4621,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
// Update input and cursor position
setInput(newInput);
setCursorPosition(newCursorPos);
+ setFileMentions(prev => (prev.includes(file.path) ? prev : [...prev, file.path]));
// Hide dropdown
setShowFileDropdown(false);
@@ -4670,6 +4715,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
}
};
+ const syncInputOverlayScroll = useCallback((target) => {
+ if (!inputHighlightRef.current || !target) return;
+ inputHighlightRef.current.scrollTop = target.scrollTop;
+ inputHighlightRef.current.scrollLeft = target.scrollLeft;
+ }, []);
+
const handleTextareaClick = (e) => {
setCursorPosition(e.target.selectionStart);
};
@@ -5370,6 +5421,16 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
+
+
+ {renderInputWithMentions(input)}
+
+
+
+
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 2/3] 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 3/3] 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 });