7 Commits

Author SHA1 Message Date
simos
c7dbab086b fixing slash commands button 2025-11-02 09:36:31 +00:00
simos
b31f7afdf5 Release 1.11.0 2025-11-02 10:27:25 +01:00
simos
57739a659f package-lock.json 2025-11-02 08:01:11 +00:00
viper151
a5813e66d9 Merge pull request #223 from siteboon/feature/cli-commands
Feature/cli commands
2025-11-02 08:59:39 +01:00
viper151
18ea4a19dd Merge branch 'main' into feature/cli-commands 2025-11-02 08:59:28 +01:00
simos
1c95c598eb docs: update installation and CLI documentation
Update .env.example with comprehensive CLI command documentation and
clearer DATABASE_PATH configuration comments. Enhance README.md with
restructured installation guide featuring new cloudcli commands,
detailed PM2 background service setup instructions, and improved
organization of global installation benefits and restart procedures.

Add CLI command reference showing cloudcli start, status, help, and
version commands. Expand PM2 section with separate subsections for
installation, service startup, and auto-start configuration.
2025-11-02 07:53:22 +00:00
simos
d1733f34e0 feat(chat): add CLAUDE.md support and fix scroll behavior
Add system prompt configuration to enable CLAUDE.md loading from
project, user config, and local directories. This allows Claude Code
to use custom instructions defined in CLAUDE.md files.

Fix scroll position management during message streaming to prevent
conflicting with user's manual scroll actions. Remove automatic
scroll state reset in scrollToBottom function and let scroll event
handler manage the state naturally.

Also remove debug logging for session ID capture.
2025-11-01 05:55:11 +00:00
8 changed files with 434 additions and 104 deletions

View File

@@ -1,5 +1,15 @@
# Claude Code UI Environment Configuration
# Only includes variables that are actually used in the code
#
# TIP: Run 'cloudcli status' to see where this file should be located
# and to view your current configuration.
#
# Available CLI commands:
# claude-code-ui - Start the server (default)
# cloudcli start - Start the server
# cloudcli status - Show configuration and data locations
# cloudcli help - Show help information
# cloudcli version - Show version information
# =============================================================================
# SERVER CONFIGURATION
@@ -19,10 +29,11 @@ VITE_PORT=5173
# =============================================================================
# Path to the authentication database file
# This should be set to a persistent volume path when running in containers
# Default: server/database/auth.db (relative to project root)
# Example for Docker: /data/auth.db
# DATABASE_PATH=/data/auth.db
# This is where user credentials, API keys, and tokens are stored.
#
# To use a custom location:
# DATABASE_PATH=/path/to/your/custom/auth.db
#
# Claude Code context window size (maximum tokens per session)
# Note: VITE_ prefix makes it available to frontend
VITE_CONTEXT_WINDOW=160000

View File

@@ -69,8 +69,7 @@ npx @siteboon/claude-code-ui
The server will start and be accessible at `http://localhost:3001` (or your configured PORT).
**To restart**: Simply run the same `npx` command again after stopping the server (Ctrl+C or Cmd+C).
**To restart**: Simply run the same `npx` command again after stopping the server
### Global Installation (For Regular Use)
For frequent use, install globally once:
@@ -85,32 +84,71 @@ Then start with a simple command:
claude-code-ui
```
**Benefits**:
- Faster startup (no download/cache check)
- Simple command to remember
- Same experience every time
**To restart**: Stop with Ctrl+C and run `claude-code-ui` again.
### Run as Background Service (Optional)
### CLI Commands
To keep the server running in the background, use PM2:
After global installation, you have access to both `claude-code-ui` and `cloudcli` commands:
```bash
# Install PM2 globally (one-time)
npm install -g pm2
# Start the server (default command)
claude-code-ui
cloudcli start
# Start the server
pm2 start claude-code-ui --name "claude-ui"
# Show configuration and data locations
cloudcli status
# Manage the service
pm2 list # View status
pm2 restart claude-ui # Restart
pm2 stop claude-ui # Stop
pm2 logs claude-ui # View logs
pm2 startup # Auto-start on system boot
# Show help information
cloudcli help
# Show version
cloudcli version
```
**The `cloudcli status` command shows you:**
- Installation directory location
- Database location (where credentials are stored)
- Current configuration (PORT, DATABASE_PATH, etc.)
- Claude projects folder location
- Configuration file location
```
### Run as Background Service (Recommended for Production)
For production use, run Claude Code UI as a background service using PM2 (Process Manager 2):
#### Install PM2
```bash
npm install -g pm2
```
#### Start as Background Service
```bash
# Start the server in background
pm2 start claude-code-ui --name "claude-code-ui"
# Or using the shorter alias
pm2 start cloudcli --name "claude-code-ui"
```
#### Auto-Start on System Boot
To make Claude Code UI start automatically when your system boots:
```bash
# Generate startup script for your platform
pm2 startup
# Save current process list
pm2 save
```
### Local Development Installation
1. **Clone the repository:**

7
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@siteboon/claude-code-ui",
"version": "1.10.5",
"version": "1.11.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@siteboon/claude-code-ui",
"version": "1.10.5",
"version": "1.11.0",
"license": "MIT",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.29",
@@ -54,7 +54,8 @@
"ws": "^8.14.2"
},
"bin": {
"claude-code-ui": "server/index.js"
"claude-code-ui": "server/cli.js",
"cloudcli": "server/cli.js"
},
"devDependencies": {
"@types/react": "^18.2.43",

View File

@@ -1,11 +1,12 @@
{
"name": "@siteboon/claude-code-ui",
"version": "1.10.5",
"version": "1.11.0",
"description": "A web-based UI for Claude Code CLI",
"type": "module",
"main": "server/index.js",
"bin": {
"claude-code-ui": "server/index.js"
"claude-code-ui": "server/cli.js",
"cloudcli": "server/cli.js"
},
"files": [
"server/",

225
server/cli.js Executable file
View File

@@ -0,0 +1,225 @@
#!/usr/bin/env node
/**
* Claude Code UI CLI
*
* Provides command-line utilities for managing Claude Code UI
*
* Commands:
* (no args) - Start the server (default)
* start - Start the server
* status - Show configuration and data locations
* help - Show help information
* version - Show version information
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// ANSI color codes for terminal output
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
// Foreground colors
cyan: '\x1b[36m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
white: '\x1b[37m',
gray: '\x1b[90m',
};
// Helper to colorize text
const c = {
info: (text) => `${colors.cyan}${text}${colors.reset}`,
ok: (text) => `${colors.green}${text}${colors.reset}`,
warn: (text) => `${colors.yellow}${text}${colors.reset}`,
error: (text) => `${colors.yellow}${text}${colors.reset}`,
tip: (text) => `${colors.blue}${text}${colors.reset}`,
bright: (text) => `${colors.bright}${text}${colors.reset}`,
dim: (text) => `${colors.dim}${text}${colors.reset}`,
};
// Load package.json for version info
const packageJsonPath = path.join(__dirname, '../package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
// Load environment variables from .env file if it exists
function loadEnvFile() {
try {
const envPath = path.join(__dirname, '../.env');
const envFile = fs.readFileSync(envPath, 'utf8');
envFile.split('\n').forEach(line => {
const trimmedLine = line.trim();
if (trimmedLine && !trimmedLine.startsWith('#')) {
const [key, ...valueParts] = trimmedLine.split('=');
if (key && valueParts.length > 0 && !process.env[key]) {
process.env[key] = valueParts.join('=').trim();
}
}
});
} catch (e) {
// .env file is optional
}
}
// Get the database path (same logic as db.js)
function getDatabasePath() {
loadEnvFile();
return process.env.DATABASE_PATH || path.join(__dirname, 'database', 'auth.db');
}
// Get the installation directory
function getInstallDir() {
return path.join(__dirname, '..');
}
// Show status command
function showStatus() {
console.log(`\n${c.bright('Claude Code UI - Status')}\n`);
console.log(c.dim('═'.repeat(60)));
// Version info
console.log(`\n${c.info('[INFO]')} Version: ${c.bright(packageJson.version)}`);
// Installation location
const installDir = getInstallDir();
console.log(`\n${c.info('[INFO]')} Installation Directory:`);
console.log(` ${c.dim(installDir)}`);
// Database location
const dbPath = getDatabasePath();
const dbExists = fs.existsSync(dbPath);
console.log(`\n${c.info('[INFO]')} Database Location:`);
console.log(` ${c.dim(dbPath)}`);
console.log(` Status: ${dbExists ? c.ok('[OK] Exists') : c.warn('[WARN] Not created yet (will be created on first run)')}`);
if (dbExists) {
const stats = fs.statSync(dbPath);
console.log(` Size: ${c.dim((stats.size / 1024).toFixed(2) + ' KB')}`);
console.log(` Modified: ${c.dim(stats.mtime.toLocaleString())}`);
}
// Environment variables
console.log(`\n${c.info('[INFO]')} Configuration:`);
console.log(` PORT: ${c.bright(process.env.PORT || '3001')} ${c.dim(process.env.PORT ? '' : '(default)')}`);
console.log(` DATABASE_PATH: ${c.dim(process.env.DATABASE_PATH || '(using default location)')}`);
console.log(` CLAUDE_CLI_PATH: ${c.dim(process.env.CLAUDE_CLI_PATH || 'claude (default)')}`);
console.log(` CONTEXT_WINDOW: ${c.dim(process.env.CONTEXT_WINDOW || '160000 (default)')}`);
// Claude projects folder
const claudeProjectsPath = path.join(process.env.HOME, '.claude', 'projects');
const projectsExists = fs.existsSync(claudeProjectsPath);
console.log(`\n${c.info('[INFO]')} Claude Projects Folder:`);
console.log(` ${c.dim(claudeProjectsPath)}`);
console.log(` Status: ${projectsExists ? c.ok('[OK] Exists') : c.warn('[WARN] Not found')}`);
// Config file location
const envFilePath = path.join(__dirname, '../.env');
const envExists = fs.existsSync(envFilePath);
console.log(`\n${c.info('[INFO]')} Configuration File:`);
console.log(` ${c.dim(envFilePath)}`);
console.log(` Status: ${envExists ? c.ok('[OK] Exists') : c.warn('[WARN] Not found (using defaults)')}`);
console.log('\n' + c.dim('═'.repeat(60)));
console.log(`\n${c.tip('[TIP]')} Hints:`);
console.log(` ${c.dim('>')} Set DATABASE_PATH env variable to use a custom database location`);
console.log(` ${c.dim('>')} Create .env file in installation directory for persistent config`);
console.log(` ${c.dim('>')} Run "claude-code-ui" or "cloudcli start" to start the server`);
console.log(` ${c.dim('>')} Access the UI at http://localhost:3001 (or custom PORT)\n`);
}
// Show help
function showHelp() {
console.log(`
╔═══════════════════════════════════════════════════════════════╗
║ Claude Code UI - Command Line Tool ║
╚═══════════════════════════════════════════════════════════════╝
Usage:
claude-code-ui [command]
cloudcli [command]
Commands:
start Start the Claude Code UI server (default)
status Show configuration and data locations
help Show this help information
version Show version information
Examples:
$ claude-code-ui # Start the server
$ cloudcli status # Show configuration
$ cloudcli help # Show help
Environment Variables:
PORT Set server port (default: 3001)
DATABASE_PATH Set custom database location
CLAUDE_CLI_PATH Set custom Claude CLI path
CONTEXT_WINDOW Set context window size (default: 160000)
Configuration:
Create a .env file in the installation directory to set
persistent environment variables. Use 'cloudcli status' to
see the installation directory path.
Documentation:
${packageJson.homepage || 'https://github.com/siteboon/claudecodeui'}
Report Issues:
${packageJson.bugs?.url || 'https://github.com/siteboon/claudecodeui/issues'}
`);
}
// Show version
function showVersion() {
console.log(`${packageJson.version}`);
}
// Start the server
async function startServer() {
// Import and run the server
await import('./index.js');
}
// Main CLI handler
async function main() {
const args = process.argv.slice(2);
const command = args[0] || 'start';
switch (command) {
case 'start':
await startServer();
break;
case 'status':
case 'info':
showStatus();
break;
case 'help':
case '-h':
case '--help':
showHelp();
break;
case 'version':
case '-v':
case '--version':
showVersion();
break;
default:
console.error(`\n❌ Unknown command: ${command}`);
console.log(' Run "cloudcli help" for usage information.\n');
process.exit(1);
}
}
// Run the CLI
main().catch(error => {
console.error('\n❌ Error:', error.message);
process.exit(1);
});

View File

@@ -8,6 +8,20 @@ import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// ANSI color codes for terminal output
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
cyan: '\x1b[36m',
dim: '\x1b[2m',
};
const c = {
info: (text) => `${colors.cyan}${text}${colors.reset}`,
bright: (text) => `${colors.bright}${text}${colors.reset}`,
dim: (text) => `${colors.dim}${text}${colors.reset}`,
};
// Use DATABASE_PATH environment variable if set, otherwise use default location
const DB_PATH = process.env.DATABASE_PATH || path.join(__dirname, 'auth.db');
const INIT_SQL_PATH = path.join(__dirname, 'init.sql');
@@ -28,7 +42,18 @@ if (process.env.DATABASE_PATH) {
// Create database connection
const db = new Database(DB_PATH);
console.log(`Connected to SQLite database at: ${DB_PATH}`);
// Show app installation path prominently
const appInstallPath = path.join(__dirname, '../..');
console.log('');
console.log(c.dim('═'.repeat(60)));
console.log(`${c.info('[INFO]')} App Installation: ${c.bright(appInstallPath)}`);
console.log(`${c.info('[INFO]')} Database: ${c.dim(path.relative(appInstallPath, DB_PATH))}`);
if (process.env.DATABASE_PATH) {
console.log(` ${c.dim('(Using custom DATABASE_PATH from environment)')}`);
}
console.log(c.dim('═'.repeat(60)));
console.log('');
// Initialize database with schema
const initializeDatabase = async () => {

View File

@@ -8,6 +8,26 @@ import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// ANSI color codes for terminal output
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
cyan: '\x1b[36m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
dim: '\x1b[2m',
};
const c = {
info: (text) => `${colors.cyan}${text}${colors.reset}`,
ok: (text) => `${colors.green}${text}${colors.reset}`,
warn: (text) => `${colors.yellow}${text}${colors.reset}`,
tip: (text) => `${colors.blue}${text}${colors.reset}`,
bright: (text) => `${colors.bright}${text}${colors.reset}`,
dim: (text) => `${colors.dim}${text}${colors.reset}`,
};
try {
const envPath = path.join(__dirname, '../.env');
const envFile = fs.readFileSync(envPath, 'utf8');
@@ -116,7 +136,7 @@ async function setupProjectsWatcher() {
});
} catch (error) {
console.error(' Error handling project changes:', error);
console.error('[ERROR] Error handling project changes:', error);
}
}, 300); // 300ms debounce (slightly faster than before)
};
@@ -129,13 +149,13 @@ async function setupProjectsWatcher() {
.on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath))
.on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath))
.on('error', (error) => {
console.error(' Chokidar watcher error:', error);
console.error('[ERROR] Chokidar watcher error:', error);
})
.on('ready', () => {
});
} catch (error) {
console.error(' Failed to setup projects watcher:', error);
console.error('[ERROR] Failed to setup projects watcher:', error);
}
}
@@ -157,13 +177,13 @@ const wss = new WebSocketServer({
// Verify token
const user = authenticateWebSocket(token);
if (!user) {
console.log(' WebSocket authentication failed');
console.log('[WARN] WebSocket authentication failed');
return false;
}
// Store user info in the request for later use
info.req.user = user;
console.log(' WebSocket authenticated for user:', user.username);
console.log('[OK] WebSocket authenticated for user:', user.username);
return true;
}
});
@@ -462,7 +482,7 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
const { projectName } = req.params;
const { filePath } = req.query;
console.log('📄 File read request:', projectName, filePath);
console.log('[DEBUG] File read request:', projectName, filePath);
// Security: ensure the requested path is inside the project root
if (!filePath) {
@@ -503,7 +523,7 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
const { projectName } = req.params;
const { path: filePath } = req.query;
console.log('🖼️ Binary file serve request:', projectName, filePath);
console.log('[DEBUG] Binary file serve request:', projectName, filePath);
// Security: ensure the requested path is inside the project root
if (!filePath) {
@@ -557,7 +577,7 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
const { projectName } = req.params;
const { filePath, content } = req.body;
console.log('💾 File save request:', projectName, filePath);
console.log('[DEBUG] File save request:', projectName, filePath);
// Security: ensure the requested path is inside the project root
if (!filePath) {
@@ -628,7 +648,7 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
const hiddenFiles = files.filter(f => f.name.startsWith('.'));
res.json(files);
} catch (error) {
console.error(' File tree error:', error.message);
console.error('[ERROR] File tree error:', error.message);
res.status(500).json({ error: error.message });
}
});
@@ -636,7 +656,7 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
// WebSocket connection handler that routes based on URL path
wss.on('connection', (ws, request) => {
const url = request.url;
console.log('🔗 Client connected to:', url);
console.log('[INFO] Client connected to:', url);
// Parse URL to get pathname without query parameters
const urlObj = new URL(url, 'http://localhost');
@@ -647,14 +667,14 @@ wss.on('connection', (ws, request) => {
} else if (pathname === '/ws') {
handleChatConnection(ws);
} else {
console.log(' Unknown WebSocket path:', pathname);
console.log('[WARN] Unknown WebSocket path:', pathname);
ws.close();
}
});
// Handle chat WebSocket connections
function handleChatConnection(ws) {
console.log('💬 Chat WebSocket connected');
console.log('[INFO] Chat WebSocket connected');
// Add to connected clients for project updates
connectedClients.add(ws);
@@ -664,28 +684,28 @@ function handleChatConnection(ws) {
const data = JSON.parse(message);
if (data.type === 'claude-command') {
console.log('💬 User message:', data.command || '[Continue/Resume]');
console.log('[DEBUG] User message:', data.command || '[Continue/Resume]');
console.log('📁 Project:', data.options?.projectPath || 'Unknown');
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
// Use Claude Agents SDK
await queryClaudeSDK(data.command, data.options, ws);
} else if (data.type === 'cursor-command') {
console.log('🖱️ Cursor message:', data.command || '[Continue/Resume]');
console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]');
console.log('📁 Project:', data.options?.cwd || 'Unknown');
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
console.log('🤖 Model:', data.options?.model || 'default');
await spawnCursor(data.command, data.options, ws);
} else if (data.type === 'cursor-resume') {
// Backward compatibility: treat as cursor-command with resume and no prompt
console.log('🖱️ Cursor resume session (compat):', data.sessionId);
console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
await spawnCursor('', {
sessionId: data.sessionId,
resume: true,
cwd: data.options?.cwd
}, ws);
} else if (data.type === 'abort-session') {
console.log('🛑 Abort session request:', data.sessionId);
console.log('[DEBUG] Abort session request:', data.sessionId);
const provider = data.provider || 'claude';
let success;
@@ -703,7 +723,7 @@ function handleChatConnection(ws) {
success
}));
} else if (data.type === 'cursor-abort') {
console.log('🛑 Abort Cursor session:', data.sessionId);
console.log('[DEBUG] Abort Cursor session:', data.sessionId);
const success = abortCursorSession(data.sessionId);
ws.send(JSON.stringify({
type: 'session-aborted',
@@ -742,7 +762,7 @@ function handleChatConnection(ws) {
}));
}
} catch (error) {
console.error(' Chat WebSocket error:', error.message);
console.error('[ERROR] Chat WebSocket error:', error.message);
ws.send(JSON.stringify({
type: 'error',
error: error.message
@@ -776,7 +796,7 @@ function handleShellConnection(ws) {
const initialCommand = data.initialCommand;
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
console.log('🚀 Starting shell in:', projectPath);
console.log('[INFO] Starting shell in:', projectPath);
console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
if (initialCommand) {
@@ -889,7 +909,7 @@ function handleShellConnection(ws) {
let match;
while ((match = pattern.exec(data)) !== null) {
const url = match[1];
console.log('🔗 Detected URL for opening:', url);
console.log('[DEBUG] Detected URL for opening:', url);
// Send URL opening message to client
ws.send(JSON.stringify({
@@ -899,7 +919,7 @@ function handleShellConnection(ws) {
// Replace the OPEN_URL pattern with a user-friendly message
if (pattern.source.includes('OPEN_URL')) {
outputData = outputData.replace(match[0], `🌐 Opening in browser: ${url}`);
outputData = outputData.replace(match[0], `[INFO] Opening in browser: ${url}`);
}
}
});
@@ -925,7 +945,7 @@ function handleShellConnection(ws) {
});
} catch (spawnError) {
console.error(' Error spawning process:', spawnError);
console.error('[ERROR] Error spawning process:', spawnError);
ws.send(JSON.stringify({
type: 'output',
data: `\r\n\x1b[31mError: ${spawnError.message}\x1b[0m\r\n`
@@ -951,7 +971,7 @@ function handleShellConnection(ws) {
}
}
} catch (error) {
console.error(' Shell WebSocket error:', error.message);
console.error('[ERROR] Shell WebSocket error:', error.message);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'output',
@@ -970,7 +990,7 @@ function handleShellConnection(ws) {
});
ws.on('error', (error) => {
console.error(' Shell WebSocket error:', error);
console.error('[ERROR] Shell WebSocket error:', error);
});
}
// Audio transcription endpoint
@@ -1411,28 +1431,37 @@ async function startServer() {
try {
// Initialize authentication database
await initializeDatabase();
console.log('✅ Database initialization skipped (testing)');
// Check if running in production mode (dist folder exists)
const distIndexPath = path.join(__dirname, '../dist/index.html');
const isProduction = fs.existsSync(distIndexPath);
// Log Claude implementation mode
console.log('🚀 Using Claude Agents SDK for Claude integration');
console.log(`📦 Running in ${isProduction ? 'PRODUCTION' : 'DEVELOPMENT'} mode`);
console.log(`${c.info('[INFO]')} Using Claude Agents SDK for Claude integration`);
console.log(`${c.info('[INFO]')} Running in ${c.bright(isProduction ? 'PRODUCTION' : 'DEVELOPMENT')} mode`);
if (!isProduction) {
console.log(`⚠️ Note: Requests will be proxied to Vite dev server at http://localhost:${process.env.VITE_PORT || 5173}`);
console.log(`${c.warn('[WARN]')} Note: Requests will be proxied to Vite dev server at ${c.dim('http://localhost:' + (process.env.VITE_PORT || 5173))}`);
}
server.listen(PORT, '0.0.0.0', async () => {
console.log(`Claude Code UI server running on http://0.0.0.0:${PORT}`);
const appInstallPath = path.join(__dirname, '..');
console.log('');
console.log(c.dim('═'.repeat(63)));
console.log(` ${c.bright('Claude Code UI Server - Ready')}`);
console.log(c.dim('═'.repeat(63)));
console.log('');
console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://0.0.0.0:' + PORT)}`);
console.log(`${c.info('[INFO]')} Installed at: ${c.dim(appInstallPath)}`);
console.log(`${c.tip('[TIP]')} Run "cloudcli status" for full configuration details`);
console.log('');
// Start watching the projects folder for changes
await setupProjectsWatcher();
});
} catch (error) {
console.error(' Failed to start server:', error);
console.error('[ERROR] Failed to start server:', error);
process.exit(1);
}
}

View File

@@ -4451,6 +4451,51 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
total={tokenBudget?.total || parseInt(import.meta.env.VITE_CONTEXT_WINDOW) || 160000}
/>
{/* Slash commands button */}
<button
type="button"
onClick={() => {
const isOpening = !showCommandMenu;
setShowCommandMenu(isOpening);
setCommandQuery('');
setSelectedCommandIndex(-1);
// When opening, ensure all commands are shown
if (isOpening) {
setFilteredCommands(slashCommands);
}
if (textareaRef.current) {
textareaRef.current.focus();
}
}}
className="relative w-8 h-8 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-full flex items-center justify-center transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:ring-offset-gray-800"
title="Show all commands"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"
/>
</svg>
{/* Command count badge */}
{slashCommands.length > 0 && (
<span
className="absolute -top-1 -right-1 bg-blue-600 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center"
style={{ fontSize: '10px' }}
>
{slashCommands.length}
</span>
)}
</button>
{/* Clear input button - positioned to the right of token pie, only shows when there's input */}
{input.trim() && (
<button
@@ -4629,57 +4674,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
{/* Mic button - HIDDEN */}
<div className="absolute right-16 sm:right-16 top-1/2 transform -translate-y-1/2" style={{ display: 'none' }}>
<MicButton
<MicButton
onTranscript={handleTranscript}
className="w-10 h-10 sm:w-10 sm:h-10"
/>
</div>
{/* Slash commands button */}
<button
type="button"
onClick={() => {
const isOpening = !showCommandMenu;
setShowCommandMenu(isOpening);
setCommandQuery('');
setSelectedCommandIndex(-1);
// When opening, ensure all commands are shown
if (isOpening) {
setFilteredCommands(slashCommands);
}
if (textareaRef.current) {
textareaRef.current.focus();
}
}}
className="absolute right-14 sm:right-36 top-1/2 transform -translate-y-1/2 w-10 h-10 sm:w-10 sm:h-10 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-full flex items-center justify-center transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:ring-offset-gray-800 relative z-10"
title="Show all commands"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"
/>
</svg>
{/* Command count badge */}
{slashCommands.length > 0 && (
<span
className="absolute -top-1 -right-1 bg-blue-600 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center"
style={{ fontSize: '10px' }}
>
{slashCommands.length}
</span>
)}
</button>
{/* Send button */}
<button
type="submit"