Compare commits

...

12 Commits

Author SHA1 Message Date
simosmik
f16e3e763d Release 1.14.0 2026-01-26 00:11:12 +00:00
simosmik
477bc404b0 fix: switch login mechanism for claude code 2026-01-25 23:55:05 +00:00
viper151
ae5a21cd6e Merge pull request #337 from timbot/main
fix: prevent codex spawn error when codex CLI is not installed
2026-01-25 23:18:57 +01:00
viper151
b2c69d6ea8 Merge branch 'main' into main 2026-01-25 23:17:29 +01:00
viper151
8825baf5b4 Update codex.js 2026-01-25 23:17:08 +01:00
viper151
0d1a3df1f7 Merge pull request #318 from siteboon/fix/session-streamed-to-another-chat
Fix/session streamed to another chat
2026-01-25 23:15:31 +01:00
viper151
80732923b5 Merge branch 'main' into fix/session-streamed-to-another-chat 2026-01-25 23:15:23 +01:00
viper151
6362d35d66 Merge pull request #299 from siteboon/feat/add-highlight-to-file-mentions
feat: add highlight for file mentions in chat input
2026-01-25 23:09:24 +01:00
Tim Smith
dab089b29f fix: prevent codex spawn error when codex CLI is not installed
Return success with empty servers array from the config read endpoint
when no config file exists, so the frontend doesn't fall through to
the CLI list endpoint which attempts to spawn the codex binary.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 11:21:43 -08:00
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
9 changed files with 160 additions and 36 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@siteboon/claude-code-ui", "name": "@siteboon/claude-code-ui",
"version": "1.13.6", "version": "1.14.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@siteboon/claude-code-ui", "name": "@siteboon/claude-code-ui",
"version": "1.13.6", "version": "1.14.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.29", "@anthropic-ai/claude-agent-sdk": "^0.1.29",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@siteboon/claude-code-ui", "name": "@siteboon/claude-code-ui",
"version": "1.13.6", "version": "1.14.0",
"description": "A web-based UI for Claude Code CLI", "description": "A web-based UI for Claude Code CLI",
"type": "module", "type": "module",
"main": "server/index.js", "main": "server/index.js",

View File

@@ -603,7 +603,8 @@ async function queryClaudeSDK(command, options = {}, ws) {
const transformedMessage = transformMessage(message); const transformedMessage = transformMessage(message);
ws.send({ ws.send({
type: 'claude-response', type: 'claude-response',
data: transformedMessage data: transformedMessage,
sessionId: capturedSessionId || sessionId || null
}); });
// Extract and send token budget updates from result messages // 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); console.log('Token budget from modelUsage:', tokenBudget);
ws.send({ ws.send({
type: 'token-budget', 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 // Send error to WebSocket
ws.send({ ws.send({
type: 'claude-error', type: 'claude-error',
error: error.message error: error.message,
sessionId: capturedSessionId || sessionId || null
}); });
throw error; throw error;

View File

@@ -114,7 +114,8 @@ async function spawnCursor(command, options = {}, ws) {
// Send system info to frontend // Send system info to frontend
ws.send({ ws.send({
type: 'cursor-system', type: 'cursor-system',
data: response data: response,
sessionId: capturedSessionId || sessionId || null
}); });
} }
break; break;
@@ -123,7 +124,8 @@ async function spawnCursor(command, options = {}, ws) {
// Forward user message // Forward user message
ws.send({ ws.send({
type: 'cursor-user', type: 'cursor-user',
data: response data: response,
sessionId: capturedSessionId || sessionId || null
}); });
break; break;
@@ -142,7 +144,8 @@ async function spawnCursor(command, options = {}, ws) {
type: 'text_delta', type: 'text_delta',
text: textContent text: textContent
} }
} },
sessionId: capturedSessionId || sessionId || null
}); });
} }
break; break;
@@ -157,7 +160,8 @@ async function spawnCursor(command, options = {}, ws) {
type: 'claude-response', type: 'claude-response',
data: { data: {
type: 'content_block_stop' type: 'content_block_stop'
} },
sessionId: capturedSessionId || sessionId || null
}); });
} }
@@ -174,7 +178,8 @@ async function spawnCursor(command, options = {}, ws) {
// Forward any other message types // Forward any other message types
ws.send({ ws.send({
type: 'cursor-response', type: 'cursor-response',
data: response data: response,
sessionId: capturedSessionId || sessionId || null
}); });
} }
} catch (parseError) { } catch (parseError) {
@@ -182,7 +187,8 @@ async function spawnCursor(command, options = {}, ws) {
// If not JSON, send as raw text // If not JSON, send as raw text
ws.send({ ws.send({
type: 'cursor-output', 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()); console.error('Cursor CLI stderr:', data.toString());
ws.send({ ws.send({
type: 'cursor-error', 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({ ws.send({
type: 'cursor-error', type: 'cursor-error',
error: error.message error: error.message,
sessionId: capturedSessionId || sessionId || null
}); });
reject(error); reject(error);
@@ -264,4 +272,4 @@ export {
abortCursorSession, abortCursorSession,
isCursorSessionActive, isCursorSessionActive,
getActiveCursorSessions getActiveCursorSessions
}; };

View File

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

View File

@@ -262,8 +262,7 @@ router.get('/mcp/config/read', async (req, res) => {
} }
if (!configData) { if (!configData) {
return res.json({ success: false, message: 'No Codex configuration file found', servers: [] }); return res.json({ success: true, configPath, servers: [] }); }
}
const servers = []; const servers = [];

View File

@@ -1907,6 +1907,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
// Streaming throttle buffers // Streaming throttle buffers
const streamBufferRef = useRef(''); const streamBufferRef = useRef('');
const streamTimerRef = useRef(null); 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 commandQueryTimerRef = useRef(null);
const [debouncedInput, setDebouncedInput] = useState(''); const [debouncedInput, setDebouncedInput] = useState('');
const [showFileDropdown, setShowFileDropdown] = useState(false); const [showFileDropdown, setShowFileDropdown] = useState(false);
@@ -1945,6 +1948,14 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
// Track provider transitions so we only clear approvals when provider truly changes. // 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. // This does not sync with the backend; it just prevents UI prompts from disappearing.
const lastProviderRef = useRef(provider); 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 // Load permission mode for the current session
useEffect(() => { useEffect(() => {
if (selectedSession?.id) { if (selectedSession?.id) {
@@ -3019,6 +3030,15 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
const sessionChanged = currentSessionId !== null && currentSessionId !== selectedSession.id; const sessionChanged = currentSessionId !== null && currentSessionId !== selectedSession.id;
if (sessionChanged) { 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 // Reset pagination state when switching sessions
setMessagesOffset(0); setMessagesOffset(0);
setHasMoreMessages(false); setHasMoreMessages(false);
@@ -3088,17 +3108,22 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
} }
} }
} else { } else {
// Only clear messages if this is NOT a system-initiated session change AND we're not loading // New session view (no selected session) - always reset UI state
// During system session changes or while loading, preserve the chat messages if (!isSystemSessionChange) {
if (!isSystemSessionChange && !isLoading) { resetStreamingState();
pendingViewSessionRef.current = null;
setChatMessages([]); setChatMessages([]);
setSessionMessages([]); setSessionMessages([]);
setClaudeStatus(null);
setCanAbortSession(false);
setIsLoading(false);
} }
setCurrentSessionId(null); setCurrentSessionId(null);
sessionStorage.removeItem('cursorSessionId'); sessionStorage.removeItem('cursorSessionId');
setMessagesOffset(0); setMessagesOffset(0);
setHasMoreMessages(false); setHasMoreMessages(false);
setTotalMessages(0); setTotalMessages(0);
setTokenBudget(null);
} }
// Mark loading as complete after messages are set // Mark loading as complete after messages are set
@@ -3109,7 +3134,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
}; };
loadMessages(); loadMessages();
}, [selectedSession, selectedProject, loadCursorSessionMessages, scrollToBottom, isSystemSessionChange]); }, [selectedSession, selectedProject, loadCursorSessionMessages, scrollToBottom, isSystemSessionChange, resetStreamingState]);
// External Message Update Handler: Reload messages when external CLI modifies current session // 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 // This triggers when App.jsx detects a JSONL file change for the currently-viewed session
@@ -3148,6 +3173,13 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
} }
}, [externalMessageUpdate, selectedSession, selectedProject, loadCursorSessionMessages, loadSessionMessages, isNearBottom, autoScrollToBottom, scrollToBottom]); }, [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 // Update chatMessages when convertedMessages changes
useEffect(() => { useEffect(() => {
if (sessionMessages.length > 0) { if (sessionMessages.length > 0) {
@@ -3212,17 +3244,77 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
// Handle WebSocket messages // Handle WebSocket messages
if (messages.length > 0) { if (messages.length > 0) {
const latestMessage = messages[messages.length - 1]; const latestMessage = messages[messages.length - 1];
const messageData = latestMessage.data?.message || latestMessage.data;
// Filter messages by session ID to prevent cross-session interference // Filter messages by session ID to prevent cross-session interference
// Skip filtering for global messages that apply to all sessions // 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 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 const isClaudeSystemInit = latestMessage.type === 'claude-response' &&
if (!isGlobalMessage && latestMessage.sessionId && currentSessionId && latestMessage.sessionId !== currentSessionId) { messageData &&
// Message is for a different session, ignore it messageData.type === 'system' &&
console.log('⏭️ Skipping message for different session:', latestMessage.sessionId, 'current:', currentSessionId); messageData.subtype === 'init';
return; 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) { switch (latestMessage.type) {
@@ -3231,6 +3323,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
// Store it temporarily until conversation completes (prevents premature session association) // Store it temporarily until conversation completes (prevents premature session association)
if (latestMessage.sessionId && !currentSessionId) { if (latestMessage.sessionId && !currentSessionId) {
sessionStorage.setItem('pendingSessionId', latestMessage.sessionId); 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 // Mark as system change to prevent clearing messages when session ID updates
setIsSystemSessionChange(true); setIsSystemSessionChange(true);
@@ -3259,7 +3354,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
break; break;
case 'claude-response': case 'claude-response':
const messageData = latestMessage.data.message || latestMessage.data;
// Handle Cursor streaming format (content_block_delta / content_block_stop) // Handle Cursor streaming format (content_block_delta / content_block_stop)
if (messageData && typeof messageData === 'object' && messageData.type) { if (messageData && typeof messageData === 'object' && messageData.type) {
@@ -3328,7 +3422,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
latestMessage.data.subtype === 'init' && latestMessage.data.subtype === 'init' &&
latestMessage.data.session_id && latestMessage.data.session_id &&
currentSessionId && currentSessionId &&
latestMessage.data.session_id !== currentSessionId) { latestMessage.data.session_id !== currentSessionId &&
isSystemInitForView) {
console.log('🔄 Claude CLI session duplication detected:', { console.log('🔄 Claude CLI session duplication detected:', {
originalSession: currentSessionId, originalSession: currentSessionId,
@@ -3351,7 +3446,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
if (latestMessage.data.type === 'system' && if (latestMessage.data.type === 'system' &&
latestMessage.data.subtype === 'init' && latestMessage.data.subtype === 'init' &&
latestMessage.data.session_id && latestMessage.data.session_id &&
!currentSessionId) { !currentSessionId &&
isSystemInitForView) {
console.log('🔄 New session init detected:', { console.log('🔄 New session init detected:', {
newSession: latestMessage.data.session_id newSession: latestMessage.data.session_id
@@ -3372,7 +3468,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
latestMessage.data.subtype === 'init' && latestMessage.data.subtype === 'init' &&
latestMessage.data.session_id && latestMessage.data.session_id &&
currentSessionId && currentSessionId &&
latestMessage.data.session_id === currentSessionId) { latestMessage.data.session_id === currentSessionId &&
isSystemInitForView) {
console.log('🔄 System init message for current session, ignoring'); console.log('🔄 System init message for current session, ignoring');
return; // Don't process the message further return; // Don't process the message further
} }
@@ -3540,6 +3637,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
try { try {
const cdata = latestMessage.data; const cdata = latestMessage.data;
if (cdata && cdata.type === 'system' && cdata.subtype === 'init' && cdata.session_id) { 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 we already have a session and this differs, switch (duplication/redirect)
if (currentSessionId && cdata.session_id !== currentSessionId) { if (currentSessionId && cdata.session_id !== currentSessionId) {
console.log('🔄 Cursor session switch detected:', { originalSession: currentSessionId, newSession: cdata.session_id }); console.log('🔄 Cursor session switch detected:', { originalSession: currentSessionId, newSession: cdata.session_id });
@@ -4376,6 +4476,11 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
// Session Protection: Mark session as active to prevent automatic project updates during conversation // 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 // Use existing session if available; otherwise a temporary placeholder until backend provides real ID
const sessionToActivate = effectiveSessionId || `new-session-${Date.now()}`; 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) { if (onSessionActive) {
onSessionActive(sessionToActivate); onSessionActive(sessionToActivate);
} }
@@ -4870,7 +4975,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
<div className="flex flex-col items-center justify-center h-full gap-3"> <div className="flex flex-col items-center justify-center h-full gap-3">
<ClaudeLogo className="w-10 h-10" /> <ClaudeLogo className="w-10 h-10" />
<div> <div>
<p className="font-semibold text-gray-900 dark:text-white">Claude</p> <p className="font-semibold text-gray-900 dark:text-white">Claude Code</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{t('providerSelection.providerInfo.anthropic')}</p> <p className="text-xs text-gray-500 dark:text-gray-400">{t('providerSelection.providerInfo.anthropic')}</p>
</div> </div>
</div> </div>

View File

@@ -11,6 +11,7 @@ import StandaloneShell from './StandaloneShell';
* @param {Object} props.project - Project object containing name and path information * @param {Object} props.project - Project object containing name and path information
* @param {Function} props.onComplete - Callback when login process completes (receives exitCode) * @param {Function} props.onComplete - Callback when login process completes (receives exitCode)
* @param {string} props.customCommand - Optional custom command to override defaults * @param {string} props.customCommand - Optional custom command to override defaults
* @param {boolean} props.isAuthenticated - Whether user is already authenticated (for re-auth flow)
*/ */
function LoginModal({ function LoginModal({
isOpen, isOpen,
@@ -18,7 +19,8 @@ function LoginModal({
provider = 'claude', provider = 'claude',
project, project,
onComplete, onComplete,
customCommand customCommand,
isAuthenticated = false
}) { }) {
if (!isOpen) return null; if (!isOpen) return null;
@@ -29,13 +31,13 @@ function LoginModal({
switch (provider) { switch (provider) {
case 'claude': case 'claude':
return 'claude setup-token --dangerously-skip-permissions'; return isAuthenticated ? 'claude /login --dangerously-skip-permissions' : 'claude setup-token --dangerously-skip-permissions';
case 'cursor': case 'cursor':
return 'cursor-agent login'; return 'cursor-agent login';
case 'codex': case 'codex':
return isPlatform ? 'codex login --device-auth' : 'codex login'; return isPlatform ? 'codex login --device-auth' : 'codex login';
default: default:
return 'claude setup-token --dangerously-skip-permissions'; return isAuthenticated ? 'claude /login --dangerously-skip-permissions' : 'claude setup-token --dangerously-skip-permissions';
} }
}; };

View File

@@ -1966,6 +1966,12 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }) {
provider={loginProvider} provider={loginProvider}
project={selectedProject} project={selectedProject}
onComplete={handleLoginComplete} onComplete={handleLoginComplete}
isAuthenticated={
loginProvider === 'claude' ? claudeAuthStatus.authenticated :
loginProvider === 'cursor' ? cursorAuthStatus.authenticated :
loginProvider === 'codex' ? codexAuthStatus.authenticated :
false
}
/> />
</div> </div>
); );