From 73a0b5bebd6950e730b954dc0c11c4b82e5f3a2c Mon Sep 17 00:00:00 2001 From: Yuanbo Li Date: Wed, 26 Nov 2025 11:45:01 +0800 Subject: [PATCH 1/8] [FixBug] The Desktop version's "New Project" button is wrapped by the conditional logic projects.length > 0, causing it to not display when there are no projects, preventing users from creating new projects. --- src/components/Sidebar.jsx | 72 ++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 8a160cc..926379f 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -581,9 +581,44 @@ function Sidebar({ - {/* Search Filter and Actions */} + {/* Action Buttons - Desktop only - Always show when not loading */} + {!isLoading && !isMobile && ( +
+
+ + +
+
+ )} + + {/* Search Filter - Only show when there are projects */} {projects.length > 0 && !isLoading && ( -
+
)}
- - {/* Action Buttons - Desktop only */} - {!isMobile && ( -
- - -
- )}
)} From 8af982e706616679bb5fc79a2666d84c88336995 Mon Sep 17 00:00:00 2001 From: simosmik Date: Wed, 31 Dec 2025 07:59:13 +0000 Subject: [PATCH 2/8] feat: add update command to CLI for checking and installing the latest version --- README.md | 6 +++++ server/cli.js | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/README.md b/README.md index 1b8cbac..b01ce35 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,11 @@ claude-code-ui **To restart**: Stop with Ctrl+C and run `claude-code-ui` again. +**To update**: +```bash +cloudcli update +``` + ### CLI Usage After global installation, you have access to both `claude-code-ui` and `cloudcli` commands: @@ -97,6 +102,7 @@ After global installation, you have access to both `claude-code-ui` and `cloudcl | `cloudcli` or `claude-code-ui` | | Start the server (default) | | `cloudcli start` | | Start the server explicitly | | `cloudcli status` | | Show configuration and data locations | +| `cloudcli update` | | Update to the latest version | | `cloudcli help` | | Show help information | | `cloudcli version` | | Show version information | | `--port ` | `-p` | Set server port (default: 3001) | diff --git a/server/cli.js b/server/cli.js index f817dd7..ebff4a0 100755 --- a/server/cli.js +++ b/server/cli.js @@ -151,6 +151,7 @@ Usage: Commands: start Start the Claude Code UI server (default) status Show configuration and data locations + update Update to the latest version help Show this help information version Show version information @@ -186,8 +187,67 @@ function showVersion() { console.log(`${packageJson.version}`); } +// Compare semver versions, returns true if v1 > v2 +function isNewerVersion(v1, v2) { + const parts1 = v1.split('.').map(Number); + const parts2 = v2.split('.').map(Number); + for (let i = 0; i < 3; i++) { + if (parts1[i] > parts2[i]) return true; + if (parts1[i] < parts2[i]) return false; + } + return false; +} + +// Check for updates +async function checkForUpdates(silent = false) { + try { + const { execSync } = await import('child_process'); + const latestVersion = execSync('npm show @siteboon/claude-code-ui version', { encoding: 'utf8' }).trim(); + const currentVersion = packageJson.version; + + if (isNewerVersion(latestVersion, currentVersion)) { + console.log(`\n${c.warn('[UPDATE]')} New version available: ${c.bright(latestVersion)} (current: ${currentVersion})`); + console.log(` Run ${c.bright('cloudcli update')} to update\n`); + return { hasUpdate: true, latestVersion, currentVersion }; + } else if (!silent) { + console.log(`${c.ok('[OK]')} You are on the latest version (${currentVersion})`); + } + return { hasUpdate: false, latestVersion, currentVersion }; + } catch (e) { + if (!silent) { + console.log(`${c.warn('[WARN]')} Could not check for updates`); + } + return { hasUpdate: false, error: e.message }; + } +} + +// Update the package +async function updatePackage() { + try { + const { execSync } = await import('child_process'); + console.log(`${c.info('[INFO]')} Checking for updates...`); + + const { hasUpdate, latestVersion, currentVersion } = await checkForUpdates(true); + + if (!hasUpdate) { + console.log(`${c.ok('[OK]')} Already on the latest version (${currentVersion})`); + return; + } + + console.log(`${c.info('[INFO]')} Updating from ${currentVersion} to ${latestVersion}...`); + execSync('npm update -g @siteboon/claude-code-ui', { stdio: 'inherit' }); + console.log(`${c.ok('[OK]')} Update complete! Restart cloudcli to use the new version.`); + } catch (e) { + console.error(`${c.error('[ERROR]')} Update failed: ${e.message}`); + console.log(`${c.tip('[TIP]')} Try running manually: npm update -g @siteboon/claude-code-ui`); + } +} + // Start the server async function startServer() { + // Check for updates silently on startup + checkForUpdates(true); + // Import and run the server await import('./index.js'); } @@ -250,6 +310,9 @@ async function main() { case '--version': showVersion(); break; + case 'update': + await updatePackage(); + break; default: console.error(`\n❌ Unknown command: ${command}`); console.log(' Run "cloudcli help" for usage information.\n'); From 104e4260a7b6874f57408069cd4a21cb4c43c884 Mon Sep 17 00:00:00 2001 From: simosmik Date: Wed, 31 Dec 2025 08:00:36 +0000 Subject: [PATCH 3/8] Release 1.13.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b0e70b8..b7ab7c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@siteboon/claude-code-ui", - "version": "1.13.5", + "version": "1.13.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@siteboon/claude-code-ui", - "version": "1.13.5", + "version": "1.13.6", "license": "MIT", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.1.29", diff --git a/package.json b/package.json index d5f35ef..81c8ded 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@siteboon/claude-code-ui", - "version": "1.13.5", + "version": "1.13.6", "description": "A web-based UI for Claude Code CLI", "type": "module", "main": "server/index.js", From b066ec4c0114f1ff333a83848794bc38087a2ccb Mon Sep 17 00:00:00 2001 From: simosmik Date: Wed, 31 Dec 2025 10:47:55 +0000 Subject: [PATCH 4/8] fix: change codex login for platform mode --- src/components/LoginModal.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/LoginModal.jsx b/src/components/LoginModal.jsx index 633aaf9..c6f6db5 100644 --- a/src/components/LoginModal.jsx +++ b/src/components/LoginModal.jsx @@ -25,13 +25,15 @@ function LoginModal({ const getCommand = () => { if (customCommand) return customCommand; + const isPlatform = import.meta.env.VITE_IS_PLATFORM === 'true'; + switch (provider) { case 'claude': return 'claude setup-token --dangerously-skip-permissions'; case 'cursor': return 'cursor-agent login'; case 'codex': - return 'codex login'; + return isPlatform ? 'codex login --device-auth' : 'codex login'; default: return 'claude setup-token --dangerously-skip-permissions'; } From ba70ad8e81443916224f9808c50cd29bf283c4e1 Mon Sep 17 00:00:00 2001 From: Haileyesus Dessie <118998054+blackmammoth@users.noreply.github.com> Date: Wed, 31 Dec 2025 19:10:33 +0300 Subject: [PATCH 5/8] fix: navigate to the correct session ID when updating session state --- src/components/ChatInterface.jsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index 337513c..e8b0a40 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -2971,12 +2971,20 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess if (latestMessage.sessionId && !currentSessionId) { sessionStorage.setItem('pendingSessionId', latestMessage.sessionId); + // Mark as system change to prevent clearing messages when session ID updates + setIsSystemSessionChange(true); + // Session Protection: Replace temporary "new-session-*" identifier with real session ID // This maintains protection continuity - no gap between temp ID and real ID // The temporary session is removed and real session is marked as active if (onReplaceTemporarySession) { onReplaceTemporarySession(latestMessage.sessionId); } + + // Navigate to the new session to ensure URL and selectedSession are updated + if (onNavigateToSession) { + onNavigateToSession(latestMessage.sessionId); + } } break; From efae890e34caae224d1e2e340fc6a77113ed4ce1 Mon Sep 17 00:00:00 2001 From: Haileyesus Dessie <118998054+blackmammoth@users.noreply.github.com> Date: Thu, 1 Jan 2026 14:46:09 +0300 Subject: [PATCH 6/8] Update button title for creating new project --- src/components/Sidebar.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 328029f..b06f56e 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -604,7 +604,7 @@ function Sidebar({ size="sm" className="flex-1 h-8 text-xs bg-primary hover:bg-primary/90 transition-all duration-200" onClick={() => setShowNewProject(true)} - title="Create new project (Ctrl+N)" + title="Create new project" > New Project @@ -1394,4 +1394,4 @@ function Sidebar({ ); } -export default Sidebar; \ No newline at end of file +export default Sidebar; From 9efe433d99215838f6bbe9f796959c0066121f72 Mon Sep 17 00:00:00 2001 From: Haileyesus Dessie <118998054+blackmammoth@users.noreply.github.com> Date: Mon, 5 Jan 2026 16:35:20 +0300 Subject: [PATCH 7/8] fix: get codex sessions in windows; improve message counting logic; fix session navigation in ChatInterface --- server/openai-codex.js | 3 ++- server/projects.js | 13 +++++++++---- src/components/ChatInterface.jsx | 12 ++++++------ 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/server/openai-codex.js b/server/openai-codex.js index ddc023f..d83d39b 100644 --- a/server/openai-codex.js +++ b/server/openai-codex.js @@ -280,7 +280,8 @@ export async function queryCodex(command, options = {}, ws) { // Send completion event sendMessage(ws, { type: 'codex-complete', - sessionId: currentSessionId + sessionId: currentSessionId, + actualSessionId: thread.id }); } catch (error) { diff --git a/server/projects.js b/server/projects.js index a4efb10..ff7a8d1 100755 --- a/server/projects.js +++ b/server/projects.js @@ -1206,7 +1206,12 @@ async function getCodexSessions(projectPath) { const sessionData = await parseCodexSessionFile(filePath); // Check if this session matches the project path - if (sessionData && sessionData.cwd === projectPath) { + // Handle Windows long paths with \\?\ prefix + const sessionCwd = sessionData?.cwd || ''; + const cleanSessionCwd = sessionCwd.startsWith('\\\\?\\') ? sessionCwd.slice(4) : sessionCwd; + const cleanProjectPath = projectPath.startsWith('\\\\?\\') ? projectPath.slice(4) : projectPath; + + if (sessionData && (sessionData.cwd === projectPath || cleanSessionCwd === cleanProjectPath || path.relative(cleanSessionCwd, cleanProjectPath) === '')) { sessions.push({ id: sessionData.id, summary: sessionData.summary || 'Codex Session', @@ -1273,12 +1278,12 @@ async function parseCodexSessionFile(filePath) { // Count messages and extract user messages for summary if (entry.type === 'event_msg' && entry.payload?.type === 'user_message') { messageCount++; - if (entry.payload.text) { - lastUserMessage = entry.payload.text; + if (entry.payload.message) { + lastUserMessage = entry.payload.message; } } - if (entry.type === 'response_item' && entry.payload?.type === 'message') { + if (entry.type === 'response_item' && entry.payload?.type === 'message' && entry.payload.role === 'assistant') { messageCount++; } diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index e8b0a40..5d86001 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -2980,11 +2980,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess if (onReplaceTemporarySession) { onReplaceTemporarySession(latestMessage.sessionId); } - - // Navigate to the new session to ensure URL and selectedSession are updated - if (onNavigateToSession) { - onNavigateToSession(latestMessage.sessionId); - } } break; @@ -3538,8 +3533,13 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess } const codexPendingSessionId = sessionStorage.getItem('pendingSessionId'); + const codexActualSessionId = latestMessage.actualSessionId || codexPendingSessionId; if (codexPendingSessionId && !currentSessionId) { - setCurrentSessionId(codexPendingSessionId); + setCurrentSessionId(codexActualSessionId); + setIsSystemSessionChange(true); + if (onNavigateToSession) { + onNavigateToSession(codexActualSessionId); + } sessionStorage.removeItem('pendingSessionId'); console.log('Codex session complete, ID set to:', codexPendingSessionId); } From 4c40a332557203860556abb70b0030a956eb7e3e Mon Sep 17 00:00:00 2001 From: Haileyesus Dessie <118998054+blackmammoth@users.noreply.github.com> Date: Mon, 5 Jan 2026 20:54:26 +0300 Subject: [PATCH 8/8] fix: improve error handling and response structure in MCP CLI routes for codex --- server/routes/codex.js | 75 +++++++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/server/routes/codex.js b/server/routes/codex.js index 0699efb..40c02a7 100644 --- a/server/routes/codex.js +++ b/server/routes/codex.js @@ -8,6 +8,17 @@ import { getCodexSessions, getCodexSessionMessages, deleteCodexSession } from '. const router = express.Router(); +function createCliResponder(res) { + let responded = false; + return (status, payload) => { + if (responded || res.headersSent) { + return; + } + responded = true; + res.status(status).json(payload); + }; +} + router.get('/config', async (req, res) => { try { const configPath = path.join(os.homedir(), '.codex', 'config.toml'); @@ -88,24 +99,30 @@ router.delete('/sessions/:sessionId', async (req, res) => { router.get('/mcp/cli/list', async (req, res) => { try { + const respond = createCliResponder(res); const proc = spawn('codex', ['mcp', 'list'], { stdio: ['pipe', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; - proc.stdout.on('data', (data) => { stdout += data.toString(); }); - proc.stderr.on('data', (data) => { stderr += data.toString(); }); + proc.stdout?.on('data', (data) => { stdout += data.toString(); }); + proc.stderr?.on('data', (data) => { stderr += data.toString(); }); proc.on('close', (code) => { if (code === 0) { - res.json({ success: true, output: stdout, servers: parseCodexListOutput(stdout) }); + respond(200, { success: true, output: stdout, servers: parseCodexListOutput(stdout) }); } else { - res.status(500).json({ error: 'Codex CLI command failed', details: stderr }); + respond(500, { error: 'Codex CLI command failed', details: stderr || `Exited with code ${code}` }); } }); proc.on('error', (error) => { - res.status(500).json({ error: 'Failed to run Codex CLI', details: error.message }); + const isMissing = error?.code === 'ENOENT'; + respond(isMissing ? 503 : 500, { + error: isMissing ? 'Codex CLI not installed' : 'Failed to run Codex CLI', + details: error.message, + code: error.code + }); }); } catch (error) { res.status(500).json({ error: 'Failed to list MCP servers', details: error.message }); @@ -133,24 +150,30 @@ router.post('/mcp/cli/add', async (req, res) => { cliArgs.push(...args); } + const respond = createCliResponder(res); const proc = spawn('codex', cliArgs, { stdio: ['pipe', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; - proc.stdout.on('data', (data) => { stdout += data.toString(); }); - proc.stderr.on('data', (data) => { stderr += data.toString(); }); + proc.stdout?.on('data', (data) => { stdout += data.toString(); }); + proc.stderr?.on('data', (data) => { stderr += data.toString(); }); proc.on('close', (code) => { if (code === 0) { - res.json({ success: true, output: stdout, message: `MCP server "${name}" added successfully` }); + respond(200, { success: true, output: stdout, message: `MCP server "${name}" added successfully` }); } else { - res.status(400).json({ error: 'Codex CLI command failed', details: stderr }); + respond(400, { error: 'Codex CLI command failed', details: stderr || `Exited with code ${code}` }); } }); proc.on('error', (error) => { - res.status(500).json({ error: 'Failed to run Codex CLI', details: error.message }); + const isMissing = error?.code === 'ENOENT'; + respond(isMissing ? 503 : 500, { + error: isMissing ? 'Codex CLI not installed' : 'Failed to run Codex CLI', + details: error.message, + code: error.code + }); }); } catch (error) { res.status(500).json({ error: 'Failed to add MCP server', details: error.message }); @@ -161,24 +184,30 @@ router.delete('/mcp/cli/remove/:name', async (req, res) => { try { const { name } = req.params; + const respond = createCliResponder(res); const proc = spawn('codex', ['mcp', 'remove', name], { stdio: ['pipe', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; - proc.stdout.on('data', (data) => { stdout += data.toString(); }); - proc.stderr.on('data', (data) => { stderr += data.toString(); }); + proc.stdout?.on('data', (data) => { stdout += data.toString(); }); + proc.stderr?.on('data', (data) => { stderr += data.toString(); }); proc.on('close', (code) => { if (code === 0) { - res.json({ success: true, output: stdout, message: `MCP server "${name}" removed successfully` }); + respond(200, { success: true, output: stdout, message: `MCP server "${name}" removed successfully` }); } else { - res.status(400).json({ error: 'Codex CLI command failed', details: stderr }); + respond(400, { error: 'Codex CLI command failed', details: stderr || `Exited with code ${code}` }); } }); proc.on('error', (error) => { - res.status(500).json({ error: 'Failed to run Codex CLI', details: error.message }); + const isMissing = error?.code === 'ENOENT'; + respond(isMissing ? 503 : 500, { + error: isMissing ? 'Codex CLI not installed' : 'Failed to run Codex CLI', + details: error.message, + code: error.code + }); }); } catch (error) { res.status(500).json({ error: 'Failed to remove MCP server', details: error.message }); @@ -189,24 +218,30 @@ router.get('/mcp/cli/get/:name', async (req, res) => { try { const { name } = req.params; + const respond = createCliResponder(res); const proc = spawn('codex', ['mcp', 'get', name], { stdio: ['pipe', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; - proc.stdout.on('data', (data) => { stdout += data.toString(); }); - proc.stderr.on('data', (data) => { stderr += data.toString(); }); + proc.stdout?.on('data', (data) => { stdout += data.toString(); }); + proc.stderr?.on('data', (data) => { stderr += data.toString(); }); proc.on('close', (code) => { if (code === 0) { - res.json({ success: true, output: stdout, server: parseCodexGetOutput(stdout) }); + respond(200, { success: true, output: stdout, server: parseCodexGetOutput(stdout) }); } else { - res.status(404).json({ error: 'Codex CLI command failed', details: stderr }); + respond(404, { error: 'Codex CLI command failed', details: stderr || `Exited with code ${code}` }); } }); proc.on('error', (error) => { - res.status(500).json({ error: 'Failed to run Codex CLI', details: error.message }); + const isMissing = error?.code === 'ENOENT'; + respond(isMissing ? 503 : 500, { + error: isMissing ? 'Codex CLI not installed' : 'Failed to run Codex CLI', + details: error.message, + code: error.code + }); }); } catch (error) { res.status(500).json({ error: 'Failed to get MCP server details', details: error.message });