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",
"version": "1.13.6",
"version": "1.14.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@siteboon/claude-code-ui",
"version": "1.13.6",
"version": "1.14.0",
"license": "MIT",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.29",

View File

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

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

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

View File

@@ -1907,6 +1907,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);
@@ -1945,6 +1948,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) {
@@ -3019,6 +3030,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);
@@ -3088,17 +3108,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
@@ -3109,7 +3134,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
@@ -3148,6 +3173,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) {
@@ -3212,17 +3244,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) {
@@ -3231,6 +3323,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);
@@ -3259,7 +3354,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) {
@@ -3328,7 +3422,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,
@@ -3351,7 +3446,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
@@ -3372,7 +3468,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
}
@@ -3540,6 +3637,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 });
@@ -4376,6 +4476,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);
}
@@ -4870,7 +4975,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
<div className="flex flex-col items-center justify-center h-full gap-3">
<ClaudeLogo className="w-10 h-10" />
<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>
</div>
</div>

View File

@@ -11,6 +11,7 @@ import StandaloneShell from './StandaloneShell';
* @param {Object} props.project - Project object containing name and path information
* @param {Function} props.onComplete - Callback when login process completes (receives exitCode)
* @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({
isOpen,
@@ -18,7 +19,8 @@ function LoginModal({
provider = 'claude',
project,
onComplete,
customCommand
customCommand,
isAuthenticated = false
}) {
if (!isOpen) return null;
@@ -29,13 +31,13 @@ function LoginModal({
switch (provider) {
case 'claude':
return 'claude setup-token --dangerously-skip-permissions';
return isAuthenticated ? 'claude /login --dangerously-skip-permissions' : 'claude setup-token --dangerously-skip-permissions';
case 'cursor':
return 'cursor-agent login';
case 'codex':
return isPlatform ? 'codex login --device-auth' : 'codex login';
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}
project={selectedProject}
onComplete={handleLoginComplete}
isAuthenticated={
loginProvider === 'claude' ? claudeAuthStatus.authenticated :
loginProvider === 'cursor' ? cursorAuthStatus.authenticated :
loginProvider === 'codex' ? codexAuthStatus.authenticated :
false
}
/>
</div>
);