Compare commits

...

14 Commits

Author SHA1 Message Date
viper151
8fb43d358c Merge pull request #283 from siteboon/fix/server-crash-when-opening-settings 2026-01-05 18:59:05 +01:00
Haileyesus Dessie
4c40a33255 fix: improve error handling and response structure in MCP CLI routes for codex 2026-01-05 20:54:26 +03:00
viper151
4086fdaa4e Merge pull request #275 from siteboon/fix/navigate-to-correct-session-id-using-codex
fix: navigate to the correct session ID when updating session state
2026-01-05 17:00:23 +01:00
viper151
124c1ac600 Merge branch 'main' into fix/navigate-to-correct-session-id-using-codex 2026-01-05 16:59:24 +01:00
Haileyesus Dessie
9efe433d99 fix: get codex sessions in windows; improve message counting logic; fix session navigation in ChatInterface 2026-01-05 16:35:20 +03:00
Haileyesus Dessie
189a1b174c Merge pull request #244 from ybalbert001/main
[FixBug] The Desktop version's "New Project" button is always hidden
2026-01-01 14:53:28 +03:00
Haileyesus Dessie
04a0ff311e Merge branch 'main' into main 2026-01-01 14:49:30 +03:00
Haileyesus Dessie
efae890e34 Update button title for creating new project 2026-01-01 14:46:09 +03:00
Haileyesus Dessie
ba70ad8e81 fix: navigate to the correct session ID when updating session state 2025-12-31 19:10:33 +03:00
simosmik
b066ec4c01 fix: change codex login for platform mode 2025-12-31 10:47:55 +00:00
simosmik
104e4260a7 Release 1.13.6 2025-12-31 08:00:36 +00:00
simosmik
8af982e706 feat: add update command to CLI for checking and installing the latest version 2025-12-31 07:59:13 +00:00
viper151
29783f609f Merge branch 'main' into main 2025-12-31 08:53:45 +01:00
Yuanbo Li
73a0b5bebd [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. 2025-11-26 11:45:01 +08:00
10 changed files with 188 additions and 66 deletions

View File

@@ -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 <port>` | `-p` | Set server port (default: 3001) |

4
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

@@ -2971,6 +2971,9 @@ 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
@@ -3530,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);
}

View File

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

View File

@@ -595,9 +595,44 @@ function Sidebar({
</div>
</div>
{/* Search Filter and Actions */}
{/* Action Buttons - Desktop only - Always show when not loading */}
{!isLoading && !isMobile && (
<div className="px-3 md:px-4 py-2 border-b border-border">
<div className="flex gap-2">
<Button
variant="default"
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"
>
<FolderPlus className="w-3.5 h-3.5 mr-1.5" />
New Project
</Button>
<Button
variant="outline"
size="sm"
className="h-8 w-8 px-0 hover:bg-accent transition-colors duration-200 group"
onClick={async () => {
setIsRefreshing(true);
try {
await onRefresh();
} finally {
setIsRefreshing(false);
}
}}
disabled={isRefreshing}
title="Refresh projects and sessions (Ctrl+R)"
>
<RefreshCw className={`w-3.5 h-3.5 ${isRefreshing ? 'animate-spin' : ''} group-hover:rotate-180 transition-transform duration-300`} />
</Button>
</div>
</div>
)}
{/* Search Filter - Only show when there are projects */}
{projects.length > 0 && !isLoading && (
<div className="px-3 md:px-4 py-2 border-b border-border space-y-2">
<div className="px-3 md:px-4 py-2 border-b border-border">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
@@ -616,39 +651,6 @@ function Sidebar({
</button>
)}
</div>
{/* Action Buttons - Desktop only */}
{!isMobile && (
<div className="flex gap-2">
<Button
variant="default"
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)"
>
<FolderPlus className="w-3.5 h-3.5 mr-1.5" />
New Project
</Button>
<Button
variant="outline"
size="sm"
className="h-8 w-8 px-0 hover:bg-accent transition-colors duration-200 group"
onClick={async () => {
setIsRefreshing(true);
try {
await onRefresh();
} finally {
setIsRefreshing(false);
}
}}
disabled={isRefreshing}
title="Refresh projects and sessions (Ctrl+R)"
>
<RefreshCw className={`w-3.5 h-3.5 ${isRefreshing ? 'animate-spin' : ''} group-hover:rotate-180 transition-transform duration-300`} />
</Button>
</div>
)}
</div>
)}
@@ -1392,4 +1394,4 @@ function Sidebar({
);
}
export default Sidebar;
export default Sidebar;