9 Commits

Author SHA1 Message Date
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
72e97c4fbc Release 1.10.5 2025-11-01 11:05:16 +01:00
viper151
b5d1fed354 feat(chat): add CLAUDE.md support and fix scroll behavior (#222) 2025-11-01 07:01:25 +01: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
simos
fefcc0f338 feat(editor): Move code editor preferences to settings and add option to expand editor
Add global settings integration and persistent user preferences for
the code editor. Settings are now stored in localStorage and persist
across sessions.

Changes:
- Add theme, word wrap, minimap, line numbers, and font size settings
- Load editor preferences from localStorage on initialization
- Expose global openSettings function for cross-component access
- Add settingsInitialTab state to control which settings tab opens
- Pass initialTab prop to Settings component for navigation

This improves UX by remembering user preferences and allows other
components to open settings to specific tabs programmatically.
2025-10-31 12:11:47 +00:00
15 changed files with 853 additions and 181 deletions

View File

@@ -1,5 +1,15 @@
# Claude Code UI Environment Configuration # Claude Code UI Environment Configuration
# Only includes variables that are actually used in the code # 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 # SERVER CONFIGURATION
@@ -19,10 +29,11 @@ VITE_PORT=5173
# ============================================================================= # =============================================================================
# Path to the authentication database file # Path to the authentication database file
# This should be set to a persistent volume path when running in containers # This is where user credentials, API keys, and tokens are stored.
# Default: server/database/auth.db (relative to project root) #
# Example for Docker: /data/auth.db # To use a custom location:
# DATABASE_PATH=/data/auth.db # DATABASE_PATH=/path/to/your/custom/auth.db
#
# Claude Code context window size (maximum tokens per session) # Claude Code context window size (maximum tokens per session)
# Note: VITE_ prefix makes it available to frontend # Note: VITE_ prefix makes it available to frontend
VITE_CONTEXT_WINDOW=160000 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). 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) ### Global Installation (For Regular Use)
For frequent use, install globally once: For frequent use, install globally once:
@@ -85,32 +84,71 @@ Then start with a simple command:
claude-code-ui 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. **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 ```bash
# Install PM2 globally (one-time) # Start the server (default command)
npm install -g pm2 claude-code-ui
cloudcli start
# Start the server # Show configuration and data locations
pm2 start claude-code-ui --name "claude-ui" cloudcli status
# Manage the service # Show help information
pm2 list # View status cloudcli help
pm2 restart claude-ui # Restart
pm2 stop claude-ui # Stop # Show version
pm2 logs claude-ui # View logs cloudcli version
pm2 startup # Auto-start on system boot
``` ```
**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 ### Local Development Installation
1. **Clone the repository:** 1. **Clone the repository:**

7
package-lock.json generated
View File

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

View File

@@ -1,11 +1,12 @@
{ {
"name": "@siteboon/claude-code-ui", "name": "@siteboon/claude-code-ui",
"version": "1.10.4", "version": "1.11.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",
"bin": { "bin": {
"claude-code-ui": "server/index.js" "claude-code-ui": "server/cli.js",
"cloudcli": "server/cli.js"
}, },
"files": [ "files": [
"server/", "server/",

View File

@@ -79,6 +79,16 @@ function mapCliOptionsToSDK(options = {}) {
// Map model (default to sonnet) // Map model (default to sonnet)
sdkOptions.model = options.model || 'sonnet'; sdkOptions.model = options.model || 'sonnet';
// Map system prompt configuration
sdkOptions.systemPrompt = {
type: 'preset',
preset: 'claude_code' // Required to use CLAUDE.md
};
// Map setting sources for CLAUDE.md loading
// This loads CLAUDE.md from project, user (~/.config/claude/CLAUDE.md), and local directories
sdkOptions.settingSources = ['project', 'user', 'local'];
// Map resume session // Map resume session
if (sessionId) { if (sessionId) {
sdkOptions.resume = sessionId; sdkOptions.resume = sessionId;
@@ -374,7 +384,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
for await (const message of queryInstance) { for await (const message of queryInstance) {
// Capture session ID from first message // Capture session ID from first message
if (message.session_id && !capturedSessionId) { if (message.session_id && !capturedSessionId) {
console.log('📝 Captured session ID:', message.session_id);
capturedSessionId = message.session_id; capturedSessionId = message.session_id;
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir); addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);

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 __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); 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 // Use DATABASE_PATH environment variable if set, otherwise use default location
const DB_PATH = process.env.DATABASE_PATH || path.join(__dirname, 'auth.db'); const DB_PATH = process.env.DATABASE_PATH || path.join(__dirname, 'auth.db');
const INIT_SQL_PATH = path.join(__dirname, 'init.sql'); const INIT_SQL_PATH = path.join(__dirname, 'init.sql');
@@ -28,7 +42,18 @@ if (process.env.DATABASE_PATH) {
// Create database connection // Create database connection
const db = new Database(DB_PATH); 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 // Initialize database with schema
const initializeDatabase = async () => { const initializeDatabase = async () => {

View File

@@ -8,6 +8,26 @@ import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); 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 { try {
const envPath = path.join(__dirname, '../.env'); const envPath = path.join(__dirname, '../.env');
const envFile = fs.readFileSync(envPath, 'utf8'); const envFile = fs.readFileSync(envPath, 'utf8');
@@ -116,7 +136,7 @@ async function setupProjectsWatcher() {
}); });
} catch (error) { } catch (error) {
console.error(' Error handling project changes:', error); console.error('[ERROR] Error handling project changes:', error);
} }
}, 300); // 300ms debounce (slightly faster than before) }, 300); // 300ms debounce (slightly faster than before)
}; };
@@ -129,13 +149,13 @@ async function setupProjectsWatcher() {
.on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath)) .on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath))
.on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath)) .on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath))
.on('error', (error) => { .on('error', (error) => {
console.error(' Chokidar watcher error:', error); console.error('[ERROR] Chokidar watcher error:', error);
}) })
.on('ready', () => { .on('ready', () => {
}); });
} catch (error) { } 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 // Verify token
const user = authenticateWebSocket(token); const user = authenticateWebSocket(token);
if (!user) { if (!user) {
console.log(' WebSocket authentication failed'); console.log('[WARN] WebSocket authentication failed');
return false; return false;
} }
// Store user info in the request for later use // Store user info in the request for later use
info.req.user = user; info.req.user = user;
console.log(' WebSocket authenticated for user:', user.username); console.log('[OK] WebSocket authenticated for user:', user.username);
return true; return true;
} }
}); });
@@ -462,7 +482,7 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
const { projectName } = req.params; const { projectName } = req.params;
const { filePath } = req.query; 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 // Security: ensure the requested path is inside the project root
if (!filePath) { if (!filePath) {
@@ -503,7 +523,7 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
const { projectName } = req.params; const { projectName } = req.params;
const { path: filePath } = req.query; 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 // Security: ensure the requested path is inside the project root
if (!filePath) { if (!filePath) {
@@ -557,7 +577,7 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
const { projectName } = req.params; const { projectName } = req.params;
const { filePath, content } = req.body; 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 // Security: ensure the requested path is inside the project root
if (!filePath) { if (!filePath) {
@@ -628,7 +648,7 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
const hiddenFiles = files.filter(f => f.name.startsWith('.')); const hiddenFiles = files.filter(f => f.name.startsWith('.'));
res.json(files); res.json(files);
} catch (error) { } catch (error) {
console.error(' File tree error:', error.message); console.error('[ERROR] File tree error:', error.message);
res.status(500).json({ 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 // WebSocket connection handler that routes based on URL path
wss.on('connection', (ws, request) => { wss.on('connection', (ws, request) => {
const url = request.url; 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 // Parse URL to get pathname without query parameters
const urlObj = new URL(url, 'http://localhost'); const urlObj = new URL(url, 'http://localhost');
@@ -647,14 +667,14 @@ wss.on('connection', (ws, request) => {
} else if (pathname === '/ws') { } else if (pathname === '/ws') {
handleChatConnection(ws); handleChatConnection(ws);
} else { } else {
console.log(' Unknown WebSocket path:', pathname); console.log('[WARN] Unknown WebSocket path:', pathname);
ws.close(); ws.close();
} }
}); });
// Handle chat WebSocket connections // Handle chat WebSocket connections
function handleChatConnection(ws) { function handleChatConnection(ws) {
console.log('💬 Chat WebSocket connected'); console.log('[INFO] Chat WebSocket connected');
// Add to connected clients for project updates // Add to connected clients for project updates
connectedClients.add(ws); connectedClients.add(ws);
@@ -664,28 +684,28 @@ function handleChatConnection(ws) {
const data = JSON.parse(message); const data = JSON.parse(message);
if (data.type === 'claude-command') { 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('📁 Project:', data.options?.projectPath || 'Unknown');
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New'); console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
// Use Claude Agents SDK // Use Claude Agents SDK
await queryClaudeSDK(data.command, data.options, ws); await queryClaudeSDK(data.command, data.options, ws);
} else if (data.type === 'cursor-command') { } 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('📁 Project:', data.options?.cwd || 'Unknown');
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New'); console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
console.log('🤖 Model:', data.options?.model || 'default'); console.log('🤖 Model:', data.options?.model || 'default');
await spawnCursor(data.command, data.options, ws); await spawnCursor(data.command, data.options, ws);
} else if (data.type === 'cursor-resume') { } else if (data.type === 'cursor-resume') {
// Backward compatibility: treat as cursor-command with resume and no prompt // 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('', { await spawnCursor('', {
sessionId: data.sessionId, sessionId: data.sessionId,
resume: true, resume: true,
cwd: data.options?.cwd cwd: data.options?.cwd
}, ws); }, ws);
} else if (data.type === 'abort-session') { } 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'; const provider = data.provider || 'claude';
let success; let success;
@@ -703,7 +723,7 @@ function handleChatConnection(ws) {
success success
})); }));
} else if (data.type === 'cursor-abort') { } 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); const success = abortCursorSession(data.sessionId);
ws.send(JSON.stringify({ ws.send(JSON.stringify({
type: 'session-aborted', type: 'session-aborted',
@@ -742,7 +762,7 @@ function handleChatConnection(ws) {
})); }));
} }
} catch (error) { } catch (error) {
console.error(' Chat WebSocket error:', error.message); console.error('[ERROR] Chat WebSocket error:', error.message);
ws.send(JSON.stringify({ ws.send(JSON.stringify({
type: 'error', type: 'error',
error: error.message error: error.message
@@ -776,7 +796,7 @@ function handleShellConnection(ws) {
const initialCommand = data.initialCommand; const initialCommand = data.initialCommand;
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell'; 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('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider); console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
if (initialCommand) { if (initialCommand) {
@@ -889,7 +909,7 @@ function handleShellConnection(ws) {
let match; let match;
while ((match = pattern.exec(data)) !== null) { while ((match = pattern.exec(data)) !== null) {
const url = match[1]; 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 // Send URL opening message to client
ws.send(JSON.stringify({ ws.send(JSON.stringify({
@@ -899,7 +919,7 @@ function handleShellConnection(ws) {
// Replace the OPEN_URL pattern with a user-friendly message // Replace the OPEN_URL pattern with a user-friendly message
if (pattern.source.includes('OPEN_URL')) { 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) { } catch (spawnError) {
console.error(' Error spawning process:', spawnError); console.error('[ERROR] Error spawning process:', spawnError);
ws.send(JSON.stringify({ ws.send(JSON.stringify({
type: 'output', type: 'output',
data: `\r\n\x1b[31mError: ${spawnError.message}\x1b[0m\r\n` data: `\r\n\x1b[31mError: ${spawnError.message}\x1b[0m\r\n`
@@ -951,7 +971,7 @@ function handleShellConnection(ws) {
} }
} }
} catch (error) { } catch (error) {
console.error(' Shell WebSocket error:', error.message); console.error('[ERROR] Shell WebSocket error:', error.message);
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ ws.send(JSON.stringify({
type: 'output', type: 'output',
@@ -970,7 +990,7 @@ function handleShellConnection(ws) {
}); });
ws.on('error', (error) => { ws.on('error', (error) => {
console.error(' Shell WebSocket error:', error); console.error('[ERROR] Shell WebSocket error:', error);
}); });
} }
// Audio transcription endpoint // Audio transcription endpoint
@@ -1411,28 +1431,37 @@ async function startServer() {
try { try {
// Initialize authentication database // Initialize authentication database
await initializeDatabase(); await initializeDatabase();
console.log('✅ Database initialization skipped (testing)');
// Check if running in production mode (dist folder exists) // Check if running in production mode (dist folder exists)
const distIndexPath = path.join(__dirname, '../dist/index.html'); const distIndexPath = path.join(__dirname, '../dist/index.html');
const isProduction = fs.existsSync(distIndexPath); const isProduction = fs.existsSync(distIndexPath);
// Log Claude implementation mode // Log Claude implementation mode
console.log('🚀 Using Claude Agents SDK for Claude integration'); console.log(`${c.info('[INFO]')} Using Claude Agents SDK for Claude integration`);
console.log(`📦 Running in ${isProduction ? 'PRODUCTION' : 'DEVELOPMENT'} mode`); console.log(`${c.info('[INFO]')} Running in ${c.bright(isProduction ? 'PRODUCTION' : 'DEVELOPMENT')} mode`);
if (!isProduction) { 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 () => { 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 // Start watching the projects folder for changes
await setupProjectsWatcher(); await setupProjectsWatcher();
}); });
} catch (error) { } catch (error) {
console.error(' Failed to start server:', error); console.error('[ERROR] Failed to start server:', error);
process.exit(1); process.exit(1);
} }
} }

View File

@@ -502,16 +502,16 @@ router.post('/generate-commit-message', async (req, res) => {
*/ */
async function generateCommitMessageWithAI(files, diffContext, provider, projectPath) { async function generateCommitMessageWithAI(files, diffContext, provider, projectPath) {
// Create the prompt // Create the prompt
const prompt = `You are a git commit message generator. Based on the following file changes and diffs, generate a commit message in conventional commit format. const prompt = `Generate a conventional commit message for these changes.
REQUIREMENTS: REQUIREMENTS:
- Use conventional commit format: type(scope): subject - Format: type(scope): subject
- Include a body that explains what changed and why - Include body explaining what changed and why
- Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore - Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Keep subject line under 50 characters - Subject under 50 chars, body wrapped at 72 chars
- Wrap body at 72 characters - Focus on user-facing changes, not implementation details
- Be specific and descriptive - Consider what's being added AND removed
- Return ONLY the commit message, nothing else - no markdown, no explanations, no code blocks - Return ONLY the commit message (no markdown, explanations, or code blocks)
FILES CHANGED: FILES CHANGED:
${files.map(f => `- ${f}`).join('\n')} ${files.map(f => `- ${f}`).join('\n')}
@@ -519,7 +519,7 @@ ${files.map(f => `- ${f}`).join('\n')}
DIFFS: DIFFS:
${diffContext.substring(0, 4000)} ${diffContext.substring(0, 4000)}
Generate the commit message now:`; Generate the commit message:`;
try { try {
// Create a simple writer that collects the response // Create a simple writer that collects the response

View File

@@ -55,6 +55,7 @@ function AppContent() {
const [isLoadingProjects, setIsLoadingProjects] = useState(true); const [isLoadingProjects, setIsLoadingProjects] = useState(true);
const [isInputFocused, setIsInputFocused] = useState(false); const [isInputFocused, setIsInputFocused] = useState(false);
const [showSettings, setShowSettings] = useState(false); const [showSettings, setShowSettings] = useState(false);
const [settingsInitialTab, setSettingsInitialTab] = useState('tools');
const [showQuickSettings, setShowQuickSettings] = useState(false); const [showQuickSettings, setShowQuickSettings] = useState(false);
const [autoExpandTools, setAutoExpandTools] = useLocalStorage('autoExpandTools', false); const [autoExpandTools, setAutoExpandTools] = useLocalStorage('autoExpandTools', false);
const [showRawParameters, setShowRawParameters] = useLocalStorage('showRawParameters', false); const [showRawParameters, setShowRawParameters] = useLocalStorage('showRawParameters', false);
@@ -308,6 +309,12 @@ function AppContent() {
// Expose fetchProjects globally for component access // Expose fetchProjects globally for component access
window.refreshProjects = fetchProjects; window.refreshProjects = fetchProjects;
// Expose openSettings function globally for component access
window.openSettings = useCallback((tab = 'tools') => {
setSettingsInitialTab(tab);
setShowSettings(true);
}, []);
// Handle URL-based session loading // Handle URL-based session loading
useEffect(() => { useEffect(() => {
if (sessionId && projects.length > 0) { if (sessionId && projects.length > 0) {
@@ -927,6 +934,7 @@ function AppContent() {
isOpen={showSettings} isOpen={showSettings}
onClose={() => setShowSettings(false)} onClose={() => setShowSettings(false)}
projects={projects} projects={projects}
initialTab={settingsInitialTab}
/> />
{/* Version Upgrade Modal */} {/* Version Upgrade Modal */}

View File

@@ -2603,7 +2603,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
const scrollToBottom = useCallback(() => { const scrollToBottom = useCallback(() => {
if (scrollContainerRef.current) { if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight; scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
setIsUserScrolledUp(false); // Don't reset isUserScrolledUp here - let the scroll handler manage it
// This prevents fighting with user's scroll position during streaming
} }
}, []); }, []);
@@ -3536,9 +3537,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
if (scrollContainerRef.current && chatMessages.length > 0 && !isLoadingSessionRef.current) { if (scrollContainerRef.current && chatMessages.length > 0 && !isLoadingSessionRef.current) {
// Only scroll if we're not in the middle of loading a session // Only scroll if we're not in the middle of loading a session
// This prevents the "double scroll" effect during session switching // This prevents the "double scroll" effect during session switching
// Also reset scroll state // Reset scroll state when switching sessions
setIsUserScrolledUp(false); setIsUserScrolledUp(false);
setTimeout(() => scrollToBottom(), 200); // Delay to ensure full rendering setTimeout(() => {
scrollToBottom();
// After scrolling, the scroll event handler will naturally set isUserScrolledUp based on position
}, 200); // Delay to ensure full rendering
} }
}, [selectedSession?.id, selectedProject?.name]); // Only trigger when session/project changes }, [selectedSession?.id, selectedProject?.name]); // Only trigger when session/project changes

View File

@@ -10,23 +10,37 @@ import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView, showPanel, ViewPlugin } from '@codemirror/view'; import { EditorView, showPanel, ViewPlugin } from '@codemirror/view';
import { unifiedMergeView, getChunks } from '@codemirror/merge'; import { unifiedMergeView, getChunks } from '@codemirror/merge';
import { showMinimap } from '@replit/codemirror-minimap'; import { showMinimap } from '@replit/codemirror-minimap';
import { X, Save, Download, Maximize2, Minimize2, Eye, EyeOff } from 'lucide-react'; import { X, Save, Download, Maximize2, Minimize2 } from 'lucide-react';
import { api } from '../utils/api'; import { api } from '../utils/api';
function CodeEditor({ file, onClose, projectPath, isSidebar = false }) { function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded = false, onToggleExpand = null }) {
const [content, setContent] = useState(''); const [content, setContent] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(true); const [isDarkMode, setIsDarkMode] = useState(() => {
const savedTheme = localStorage.getItem('codeEditorTheme');
return savedTheme ? savedTheme === 'dark' : true;
});
const [saveSuccess, setSaveSuccess] = useState(false); const [saveSuccess, setSaveSuccess] = useState(false);
const [showDiff, setShowDiff] = useState(!!file.diffInfo); const [showDiff, setShowDiff] = useState(!!file.diffInfo);
const [wordWrap, setWordWrap] = useState(false); const [wordWrap, setWordWrap] = useState(() => {
return localStorage.getItem('codeEditorWordWrap') === 'true';
});
const [minimapEnabled, setMinimapEnabled] = useState(() => {
return localStorage.getItem('codeEditorShowMinimap') !== 'false';
});
const [showLineNumbers, setShowLineNumbers] = useState(() => {
return localStorage.getItem('codeEditorLineNumbers') !== 'false';
});
const [fontSize, setFontSize] = useState(() => {
return localStorage.getItem('codeEditorFontSize') || '14';
});
const editorRef = useRef(null); const editorRef = useRef(null);
// Create minimap extension with chunk-based gutters // Create minimap extension with chunk-based gutters
const minimapExtension = useMemo(() => { const minimapExtension = useMemo(() => {
if (!file.diffInfo || !showDiff) return []; if (!file.diffInfo || !showDiff || !minimapEnabled) return [];
const gutters = {}; const gutters = {};
@@ -58,7 +72,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
}; };
}) })
]; ];
}, [file.diffInfo, showDiff, isDarkMode]); }, [file.diffInfo, showDiff, minimapEnabled, isDarkMode]);
// Create extension to scroll to first chunk on mount // Create extension to scroll to first chunk on mount
const scrollToFirstChunkExtension = useMemo(() => { const scrollToFirstChunkExtension = useMemo(() => {
@@ -89,24 +103,28 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
]; ];
}, [file.diffInfo, showDiff]); }, [file.diffInfo, showDiff]);
// Create diff navigation panel extension // Create editor toolbar panel - always visible
const diffNavigationPanel = useMemo(() => { const editorToolbarPanel = useMemo(() => {
if (!file.diffInfo || !showDiff) return [];
const createPanel = (view) => { const createPanel = (view) => {
const dom = document.createElement('div'); const dom = document.createElement('div');
dom.className = 'cm-diff-navigation-panel'; dom.className = 'cm-editor-toolbar-panel';
let currentIndex = 0; let currentIndex = 0;
const updatePanel = () => { const updatePanel = () => {
// Use getChunks API to get ALL chunks regardless of viewport // Check if we have diff info and it's enabled
const chunksData = getChunks(view.state); const hasDiff = file.diffInfo && showDiff;
const chunksData = hasDiff ? getChunks(view.state) : null;
const chunks = chunksData?.chunks || []; const chunks = chunksData?.chunks || [];
const chunkCount = chunks.length; const chunkCount = chunks.length;
dom.innerHTML = ` // Build the toolbar HTML
<div style="display: flex; align-items: center; gap: 8px;"> let toolbarHTML = '<div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">';
// Left side - diff navigation (if applicable)
toolbarHTML += '<div style="display: flex; align-items: center; gap: 8px;">';
if (hasDiff) {
toolbarHTML += `
<span style="font-weight: 500;">${chunkCount > 0 ? `${currentIndex + 1}/${chunkCount}` : '0'} changes</span> <span style="font-weight: 500;">${chunkCount > 0 ? `${currentIndex + 1}/${chunkCount}` : '0'} changes</span>
<button class="cm-diff-nav-btn cm-diff-nav-prev" title="Previous change" ${chunkCount === 0 ? 'disabled' : ''}> <button class="cm-diff-nav-btn cm-diff-nav-prev" title="Previous change" ${chunkCount === 0 ? 'disabled' : ''}>
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -118,41 +136,110 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
</button> </button>
</div> `;
}
toolbarHTML += '</div>';
// Right side - action buttons
toolbarHTML += '<div style="display: flex; align-items: center; gap: 4px;">';
// Show/hide diff button (only if there's diff info)
if (file.diffInfo) {
toolbarHTML += `
<button class="cm-toolbar-btn cm-toggle-diff-btn" title="${showDiff ? 'Hide diff highlighting' : 'Show diff highlighting'}">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
${showDiff ?
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />' :
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />'
}
</svg>
</button>
`;
}
// Settings button
toolbarHTML += `
<button class="cm-toolbar-btn cm-settings-btn" title="Editor Settings">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
`; `;
const prevBtn = dom.querySelector('.cm-diff-nav-prev'); // Expand button (only in sidebar mode)
const nextBtn = dom.querySelector('.cm-diff-nav-next'); if (isSidebar && onToggleExpand) {
toolbarHTML += `
<button class="cm-toolbar-btn cm-expand-btn" title="${isExpanded ? 'Collapse editor' : 'Expand editor to full width'}">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
${isExpanded ?
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" />' :
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />'
}
</svg>
</button>
`;
}
prevBtn?.addEventListener('click', () => { toolbarHTML += '</div>';
if (chunks.length === 0) return; toolbarHTML += '</div>';
currentIndex = currentIndex > 0 ? currentIndex - 1 : chunks.length - 1;
// Navigate to the chunk - use fromB which is the position in the current document dom.innerHTML = toolbarHTML;
const chunk = chunks[currentIndex];
if (chunk) { // Attach event listeners for diff navigation
// Scroll to the start of the chunk in the B side (current document) if (hasDiff) {
view.dispatch({ const prevBtn = dom.querySelector('.cm-diff-nav-prev');
effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' }) const nextBtn = dom.querySelector('.cm-diff-nav-next');
});
prevBtn?.addEventListener('click', () => {
if (chunks.length === 0) return;
currentIndex = currentIndex > 0 ? currentIndex - 1 : chunks.length - 1;
const chunk = chunks[currentIndex];
if (chunk) {
view.dispatch({
effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' })
});
}
updatePanel();
});
nextBtn?.addEventListener('click', () => {
if (chunks.length === 0) return;
currentIndex = currentIndex < chunks.length - 1 ? currentIndex + 1 : 0;
const chunk = chunks[currentIndex];
if (chunk) {
view.dispatch({
effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' })
});
}
updatePanel();
});
}
// Attach event listener for toggle diff button
if (file.diffInfo) {
const toggleDiffBtn = dom.querySelector('.cm-toggle-diff-btn');
toggleDiffBtn?.addEventListener('click', () => {
setShowDiff(!showDiff);
});
}
// Attach event listener for settings button
const settingsBtn = dom.querySelector('.cm-settings-btn');
settingsBtn?.addEventListener('click', () => {
if (window.openSettings) {
window.openSettings('appearance');
} }
updatePanel();
}); });
nextBtn?.addEventListener('click', () => { // Attach event listener for expand button
if (chunks.length === 0) return; if (isSidebar && onToggleExpand) {
currentIndex = currentIndex < chunks.length - 1 ? currentIndex + 1 : 0; const expandBtn = dom.querySelector('.cm-expand-btn');
expandBtn?.addEventListener('click', () => {
// Navigate to the chunk - use fromB which is the position in the current document onToggleExpand();
const chunk = chunks[currentIndex]; });
if (chunk) { }
// Scroll to the start of the chunk in the B side (current document)
view.dispatch({
effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' })
});
}
updatePanel();
});
}; };
updatePanel(); updatePanel();
@@ -165,7 +252,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
}; };
return [showPanel.of(createPanel)]; return [showPanel.of(createPanel)];
}, [file.diffInfo, showDiff]); }, [file.diffInfo, showDiff, isSidebar, isExpanded, onToggleExpand]);
// Get language extension based on file extension // Get language extension based on file extension
const getLanguageExtension = (filename) => { const getLanguageExtension = (filename) => {
@@ -290,6 +377,57 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
setIsFullscreen(!isFullscreen); setIsFullscreen(!isFullscreen);
}; };
// Save theme preference to localStorage
useEffect(() => {
localStorage.setItem('codeEditorTheme', isDarkMode ? 'dark' : 'light');
}, [isDarkMode]);
// Save word wrap preference to localStorage
useEffect(() => {
localStorage.setItem('codeEditorWordWrap', wordWrap.toString());
}, [wordWrap]);
// Listen for settings changes from the Settings modal
useEffect(() => {
const handleStorageChange = () => {
const newTheme = localStorage.getItem('codeEditorTheme');
if (newTheme) {
setIsDarkMode(newTheme === 'dark');
}
const newWordWrap = localStorage.getItem('codeEditorWordWrap');
if (newWordWrap !== null) {
setWordWrap(newWordWrap === 'true');
}
const newShowMinimap = localStorage.getItem('codeEditorShowMinimap');
if (newShowMinimap !== null) {
setMinimapEnabled(newShowMinimap !== 'false');
}
const newShowLineNumbers = localStorage.getItem('codeEditorLineNumbers');
if (newShowLineNumbers !== null) {
setShowLineNumbers(newShowLineNumbers !== 'false');
}
const newFontSize = localStorage.getItem('codeEditorFontSize');
if (newFontSize) {
setFontSize(newFontSize);
}
};
// Listen for storage events (changes from other tabs/windows)
window.addEventListener('storage', handleStorageChange);
// Custom event for same-window updates
window.addEventListener('codeEditorSettingsChanged', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener('codeEditorSettingsChanged', handleStorageChange);
};
}, []);
// Handle keyboard shortcuts // Handle keyboard shortcuts
useEffect(() => { useEffect(() => {
const handleKeyDown = (e) => { const handleKeyDown = (e) => {
@@ -329,7 +467,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
</div> </div>
</div> </div>
) : ( ) : (
<div className="fixed inset-0 z-50 md:bg-black/50 md:flex md:items-center md:justify-center"> <div className="fixed inset-0 z-40 md:bg-black/50 md:flex md:items-center md:justify-center">
<div className="code-editor-loading w-full h-full md:rounded-lg md:w-auto md:h-auto p-8 flex items-center justify-center"> <div className="code-editor-loading w-full h-full md:rounded-lg md:w-auto md:h-auto p-8 flex items-center justify-center">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div> <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
@@ -381,8 +519,8 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
background-color: ${isDarkMode ? '#1e1e1e' : '#f5f5f5'}; background-color: ${isDarkMode ? '#1e1e1e' : '#f5f5f5'};
} }
/* Diff navigation panel styling */ /* Editor toolbar panel styling */
.cm-diff-navigation-panel { .cm-editor-toolbar-panel {
padding: 8px 12px; padding: 8px 12px;
background-color: ${isDarkMode ? '#1f2937' : '#ffffff'}; background-color: ${isDarkMode ? '#1f2937' : '#ffffff'};
border-bottom: 1px solid ${isDarkMode ? '#374151' : '#e5e7eb'}; border-bottom: 1px solid ${isDarkMode ? '#374151' : '#e5e7eb'};
@@ -390,7 +528,8 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
font-size: 14px; font-size: 14px;
} }
.cm-diff-nav-btn { .cm-diff-nav-btn,
.cm-toolbar-btn {
padding: 4px; padding: 4px;
background: transparent; background: transparent;
border: none; border: none;
@@ -400,9 +539,11 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: inherit; color: inherit;
transition: background-color 0.2s;
} }
.cm-diff-nav-btn:hover { .cm-diff-nav-btn:hover,
.cm-toolbar-btn:hover {
background-color: ${isDarkMode ? '#374151' : '#f3f4f6'}; background-color: ${isDarkMode ? '#374151' : '#f3f4f6'};
} }
@@ -414,7 +555,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
</style> </style>
<div className={isSidebar ? <div className={isSidebar ?
'w-full h-full flex flex-col' : 'w-full h-full flex flex-col' :
`fixed inset-0 z-50 ${ `fixed inset-0 z-40 ${
// Mobile: native fullscreen, Desktop: modal with backdrop // Mobile: native fullscreen, Desktop: modal with backdrop
'md:bg-black/50 md:flex md:items-center md:justify-center md:p-4' 'md:bg-black/50 md:flex md:items-center md:justify-center md:p-4'
} ${isFullscreen ? 'md:p-0' : ''}`}> } ${isFullscreen ? 'md:p-0' : ''}`}>
@@ -433,7 +574,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
<h3 className="font-medium text-gray-900 dark:text-white truncate">{file.name}</h3> <h3 className="font-medium text-gray-900 dark:text-white truncate">{file.name}</h3>
{file.diffInfo && ( {file.diffInfo && (
<span className="text-xs bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 px-2 py-1 rounded whitespace-nowrap"> <span className="text-xs bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 px-2 py-1 rounded whitespace-nowrap">
📝 Has changes Showing changes
</span> </span>
)} )}
</div> </div>
@@ -442,36 +583,6 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
</div> </div>
<div className="flex items-center gap-1 md:gap-2 flex-shrink-0"> <div className="flex items-center gap-1 md:gap-2 flex-shrink-0">
{file.diffInfo && (
<button
onClick={() => setShowDiff(!showDiff)}
className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
title={showDiff ? "Hide diff highlighting" : "Show diff highlighting"}
>
{showDiff ? <EyeOff className="w-5 h-5 md:w-4 md:h-4" /> : <Eye className="w-5 h-5 md:w-4 md:h-4" />}
</button>
)}
<button
onClick={() => setWordWrap(!wordWrap)}
className={`p-2 md:p-2 rounded-md hover:bg-gray-100 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center ${
wordWrap
? 'text-blue-600 bg-blue-50'
: 'text-gray-600 hover:text-gray-900'
}`}
title={wordWrap ? 'Disable word wrap' : 'Enable word wrap'}
>
<span className="text-sm md:text-xs font-mono font-bold"></span>
</button>
<button
onClick={() => setIsDarkMode(!isDarkMode)}
className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
title="Toggle theme"
>
<span className="text-lg md:text-base">{isDarkMode ? '☀️' : '🌙'}</span>
</button>
<button <button
onClick={handleDownload} onClick={handleDownload}
className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center" className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
@@ -532,6 +643,9 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
onChange={setContent} onChange={setContent}
extensions={[ extensions={[
...getLanguageExtension(file.name), ...getLanguageExtension(file.name),
// Always show the toolbar
...editorToolbarPanel,
// Only show diff-related extensions when diff is enabled
...(file.diffInfo && showDiff && file.diffInfo.old_string !== undefined ...(file.diffInfo && showDiff && file.diffInfo.old_string !== undefined
? [ ? [
unifiedMergeView({ unifiedMergeView({
@@ -543,8 +657,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
// NOTE: NO collapseUnchanged - this shows the full file! // NOTE: NO collapseUnchanged - this shows the full file!
}), }),
...minimapExtension, ...minimapExtension,
...scrollToFirstChunkExtension, ...scrollToFirstChunkExtension
...diffNavigationPanel
] ]
: []), : []),
...(wordWrap ? [EditorView.lineWrapping] : []) ...(wordWrap ? [EditorView.lineWrapping] : [])
@@ -552,11 +665,11 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false }) {
theme={isDarkMode ? oneDark : undefined} theme={isDarkMode ? oneDark : undefined}
height="100%" height="100%"
style={{ style={{
fontSize: '14px', fontSize: `${fontSize}px`,
height: '100%', height: '100%',
}} }}
basicSetup={{ basicSetup={{
lineNumbers: true, lineNumbers: showLineNumbers,
foldGutter: true, foldGutter: true,
dropCursor: false, dropCursor: false,
allowMultipleSelections: false, allowMultipleSelections: false,

View File

@@ -63,6 +63,7 @@ function MainContent({
const [showTaskDetail, setShowTaskDetail] = useState(false); const [showTaskDetail, setShowTaskDetail] = useState(false);
const [editorWidth, setEditorWidth] = useState(600); const [editorWidth, setEditorWidth] = useState(600);
const [isResizing, setIsResizing] = useState(false); const [isResizing, setIsResizing] = useState(false);
const [editorExpanded, setEditorExpanded] = useState(false);
const resizeRef = useRef(null); const resizeRef = useRef(null);
// PRD Editor state // PRD Editor state
@@ -130,6 +131,11 @@ function MainContent({
const handleCloseEditor = () => { const handleCloseEditor = () => {
setEditingFile(null); setEditingFile(null);
setEditorExpanded(false);
};
const handleToggleEditorExpand = () => {
setEditorExpanded(!editorExpanded);
}; };
const handleTaskClick = (task) => { const handleTaskClick = (task) => {
@@ -461,7 +467,7 @@ function MainContent({
{/* Content Area with Right Sidebar */} {/* Content Area with Right Sidebar */}
<div className="flex-1 flex min-h-0 overflow-hidden"> <div className="flex-1 flex min-h-0 overflow-hidden">
{/* Main Content */} {/* Main Content */}
<div className={`flex-1 flex flex-col min-h-0 overflow-hidden ${editingFile ? 'mr-0' : ''}`}> <div className={`flex-1 flex flex-col min-h-0 overflow-hidden ${editingFile ? 'mr-0' : ''} ${editorExpanded ? 'hidden' : ''}`}>
<div className={`h-full ${activeTab === 'chat' ? 'block' : 'hidden'}`}> <div className={`h-full ${activeTab === 'chat' ? 'block' : 'hidden'}`}>
<ErrorBoundary showDetails={true}> <ErrorBoundary showDetails={true}>
<ChatInterface <ChatInterface
@@ -569,27 +575,31 @@ function MainContent({
{/* Code Editor Right Sidebar - Desktop only, Mobile uses modal */} {/* Code Editor Right Sidebar - Desktop only, Mobile uses modal */}
{editingFile && !isMobile && ( {editingFile && !isMobile && (
<> <>
{/* Resize Handle */} {/* Resize Handle - Hidden when expanded */}
<div {!editorExpanded && (
ref={resizeRef} <div
onMouseDown={handleMouseDown} ref={resizeRef}
className="flex-shrink-0 w-1 bg-gray-200 dark:bg-gray-700 hover:bg-blue-500 dark:hover:bg-blue-600 cursor-col-resize transition-colors relative group" onMouseDown={handleMouseDown}
title="Drag to resize" className="flex-shrink-0 w-1 bg-gray-200 dark:bg-gray-700 hover:bg-blue-500 dark:hover:bg-blue-600 cursor-col-resize transition-colors relative group"
> title="Drag to resize"
{/* Visual indicator on hover */} >
<div className="absolute inset-y-0 left-1/2 -translate-x-1/2 w-1 bg-blue-500 dark:bg-blue-600 opacity-0 group-hover:opacity-100 transition-opacity" /> {/* Visual indicator on hover */}
</div> <div className="absolute inset-y-0 left-1/2 -translate-x-1/2 w-1 bg-blue-500 dark:bg-blue-600 opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
)}
{/* Editor Sidebar */} {/* Editor Sidebar */}
<div <div
className="flex-shrink-0 border-l border-gray-200 dark:border-gray-700 h-full overflow-hidden" className={`flex-shrink-0 border-l border-gray-200 dark:border-gray-700 h-full overflow-hidden ${editorExpanded ? 'flex-1' : ''}`}
style={{ width: `${editorWidth}px` }} style={editorExpanded ? {} : { width: `${editorWidth}px` }}
> >
<CodeEditor <CodeEditor
file={editingFile} file={editingFile}
onClose={handleCloseEditor} onClose={handleCloseEditor}
projectPath={selectedProject?.path} projectPath={selectedProject?.path}
isSidebar={true} isSidebar={true}
isExpanded={editorExpanded}
onToggleExpand={handleToggleEditorExpand}
/> />
</div> </div>
</> </>

View File

@@ -112,7 +112,7 @@ const QuickSettingsPanel = ({
type="checkbox" type="checkbox"
checked={autoExpandTools} checked={autoExpandTools}
onChange={(e) => onAutoExpandChange(e.target.checked)} onChange={(e) => onAutoExpandChange(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600" className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 focus:ring-2 dark:focus:ring-blue-400 bg-gray-100 dark:bg-gray-800 checked:bg-blue-600 dark:checked:bg-blue-600"
/> />
</label> </label>
@@ -125,7 +125,7 @@ const QuickSettingsPanel = ({
type="checkbox" type="checkbox"
checked={showRawParameters} checked={showRawParameters}
onChange={(e) => onShowRawParametersChange(e.target.checked)} onChange={(e) => onShowRawParametersChange(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600" className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 focus:ring-2 dark:focus:ring-blue-400 bg-gray-100 dark:bg-gray-800 checked:bg-blue-600 dark:checked:bg-blue-600"
/> />
</label> </label>
@@ -138,7 +138,7 @@ const QuickSettingsPanel = ({
type="checkbox" type="checkbox"
checked={showThinking} checked={showThinking}
onChange={(e) => onShowThinkingChange(e.target.checked)} onChange={(e) => onShowThinkingChange(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600" className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 focus:ring-2 dark:focus:ring-blue-400 bg-gray-100 dark:bg-gray-800 checked:bg-blue-600 dark:checked:bg-blue-600"
/> />
</label> </label>
</div> </div>
@@ -155,7 +155,7 @@ const QuickSettingsPanel = ({
type="checkbox" type="checkbox"
checked={autoScrollToBottom} checked={autoScrollToBottom}
onChange={(e) => onAutoScrollChange(e.target.checked)} onChange={(e) => onAutoScrollChange(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600" className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 focus:ring-2 dark:focus:ring-blue-400 bg-gray-100 dark:bg-gray-800 checked:bg-blue-600 dark:checked:bg-blue-600"
/> />
</label> </label>
</div> </div>
@@ -173,7 +173,7 @@ const QuickSettingsPanel = ({
type="checkbox" type="checkbox"
checked={sendByCtrlEnter} checked={sendByCtrlEnter}
onChange={(e) => onSendByCtrlEnterChange(e.target.checked)} onChange={(e) => onSendByCtrlEnterChange(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600" className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 focus:ring-2 dark:focus:ring-blue-400 bg-gray-100 dark:bg-gray-800 checked:bg-blue-600 dark:checked:bg-blue-600"
/> />
</label> </label>
<p className="text-xs text-gray-500 dark:text-gray-400 ml-3"> <p className="text-xs text-gray-500 dark:text-gray-400 ml-3">

View File

@@ -10,7 +10,7 @@ import ClaudeLogo from './ClaudeLogo';
import CursorLogo from './CursorLogo'; import CursorLogo from './CursorLogo';
import CredentialsSettings from './CredentialsSettings'; import CredentialsSettings from './CredentialsSettings';
function Settings({ isOpen, onClose, projects = [] }) { function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
const { isDarkMode, toggleDarkMode } = useTheme(); const { isDarkMode, toggleDarkMode } = useTheme();
const { const {
tasksEnabled, tasksEnabled,
@@ -52,10 +52,27 @@ function Settings({ isOpen, onClose, projects = [] }) {
const [mcpTestResults, setMcpTestResults] = useState({}); const [mcpTestResults, setMcpTestResults] = useState({});
const [mcpServerTools, setMcpServerTools] = useState({}); const [mcpServerTools, setMcpServerTools] = useState({});
const [mcpToolsLoading, setMcpToolsLoading] = useState({}); const [mcpToolsLoading, setMcpToolsLoading] = useState({});
const [activeTab, setActiveTab] = useState('tools'); const [activeTab, setActiveTab] = useState(initialTab);
const [jsonValidationError, setJsonValidationError] = useState(''); const [jsonValidationError, setJsonValidationError] = useState('');
const [toolsProvider, setToolsProvider] = useState('claude'); // 'claude' or 'cursor' const [toolsProvider, setToolsProvider] = useState('claude'); // 'claude' or 'cursor'
// Code Editor settings
const [codeEditorTheme, setCodeEditorTheme] = useState(() =>
localStorage.getItem('codeEditorTheme') || 'dark'
);
const [codeEditorWordWrap, setCodeEditorWordWrap] = useState(() =>
localStorage.getItem('codeEditorWordWrap') === 'true'
);
const [codeEditorShowMinimap, setCodeEditorShowMinimap] = useState(() =>
localStorage.getItem('codeEditorShowMinimap') !== 'false' // Default true
);
const [codeEditorLineNumbers, setCodeEditorLineNumbers] = useState(() =>
localStorage.getItem('codeEditorLineNumbers') !== 'false' // Default true
);
const [codeEditorFontSize, setCodeEditorFontSize] = useState(() =>
localStorage.getItem('codeEditorFontSize') || '14'
);
// Cursor-specific states // Cursor-specific states
const [cursorAllowedCommands, setCursorAllowedCommands] = useState([]); const [cursorAllowedCommands, setCursorAllowedCommands] = useState([]);
const [cursorDisallowedCommands, setCursorDisallowedCommands] = useState([]); const [cursorDisallowedCommands, setCursorDisallowedCommands] = useState([]);
@@ -327,8 +344,36 @@ function Settings({ isOpen, onClose, projects = [] }) {
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
loadSettings(); loadSettings();
// Set the active tab when the modal opens
setActiveTab(initialTab);
} }
}, [isOpen]); }, [isOpen, initialTab]);
// Persist code editor settings to localStorage
useEffect(() => {
localStorage.setItem('codeEditorTheme', codeEditorTheme);
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
}, [codeEditorTheme]);
useEffect(() => {
localStorage.setItem('codeEditorWordWrap', codeEditorWordWrap.toString());
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
}, [codeEditorWordWrap]);
useEffect(() => {
localStorage.setItem('codeEditorShowMinimap', codeEditorShowMinimap.toString());
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
}, [codeEditorShowMinimap]);
useEffect(() => {
localStorage.setItem('codeEditorLineNumbers', codeEditorLineNumbers.toString());
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
}, [codeEditorLineNumbers]);
useEffect(() => {
localStorage.setItem('codeEditorFontSize', codeEditorFontSize);
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
}, [codeEditorFontSize]);
const loadSettings = async () => { const loadSettings = async () => {
try { try {
@@ -625,7 +670,7 @@ function Settings({ isOpen, onClose, projects = [] }) {
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
<div className="modal-backdrop fixed inset-0 flex items-center justify-center z-[100] md:p-4 bg-background/95"> <div className="modal-backdrop fixed inset-0 flex items-center justify-center z-[9999] md:p-4 bg-background/95">
<div className="bg-background border border-border md:rounded-lg shadow-xl w-full md:max-w-4xl h-full md:h-[90vh] flex flex-col"> <div className="bg-background border border-border md:rounded-lg shadow-xl w-full md:max-w-4xl h-full md:h-[90vh] flex flex-col">
<div className="flex items-center justify-between p-4 md:p-6 border-b border-border flex-shrink-0"> <div className="flex items-center justify-between p-4 md:p-6 border-b border-border flex-shrink-0">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -758,6 +803,158 @@ function Settings({ isOpen, onClose, projects = [] }) {
</div> </div>
</div> </div>
</div> </div>
{/* Code Editor Settings */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-foreground">Code Editor</h3>
{/* Editor Theme */}
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">
Editor Theme
</div>
<div className="text-sm text-muted-foreground">
Default theme for the code editor
</div>
</div>
<button
onClick={() => setCodeEditorTheme(codeEditorTheme === 'dark' ? 'light' : 'dark')}
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 dark:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
role="switch"
aria-checked={codeEditorTheme === 'dark'}
aria-label="Toggle editor theme"
>
<span className="sr-only">Toggle editor theme</span>
<span
className={`${
codeEditorTheme === 'dark' ? 'translate-x-7' : 'translate-x-1'
} inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200 flex items-center justify-center`}
>
{codeEditorTheme === 'dark' ? (
<Moon className="w-3.5 h-3.5 text-gray-700" />
) : (
<Sun className="w-3.5 h-3.5 text-yellow-500" />
)}
</span>
</button>
</div>
</div>
{/* Word Wrap */}
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">
Word Wrap
</div>
<div className="text-sm text-muted-foreground">
Enable word wrapping by default in the editor
</div>
</div>
<button
onClick={() => setCodeEditorWordWrap(!codeEditorWordWrap)}
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 dark:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
role="switch"
aria-checked={codeEditorWordWrap}
aria-label="Toggle word wrap"
>
<span className="sr-only">Toggle word wrap</span>
<span
className={`${
codeEditorWordWrap ? 'translate-x-7' : 'translate-x-1'
} inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200`}
/>
</button>
</div>
</div>
{/* Show Minimap */}
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">
Show Minimap
</div>
<div className="text-sm text-muted-foreground">
Display a minimap for easier navigation in diff view
</div>
</div>
<button
onClick={() => setCodeEditorShowMinimap(!codeEditorShowMinimap)}
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 dark:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
role="switch"
aria-checked={codeEditorShowMinimap}
aria-label="Toggle minimap"
>
<span className="sr-only">Toggle minimap</span>
<span
className={`${
codeEditorShowMinimap ? 'translate-x-7' : 'translate-x-1'
} inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200`}
/>
</button>
</div>
</div>
{/* Show Line Numbers */}
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">
Show Line Numbers
</div>
<div className="text-sm text-muted-foreground">
Display line numbers in the editor
</div>
</div>
<button
onClick={() => setCodeEditorLineNumbers(!codeEditorLineNumbers)}
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 dark:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
role="switch"
aria-checked={codeEditorLineNumbers}
aria-label="Toggle line numbers"
>
<span className="sr-only">Toggle line numbers</span>
<span
className={`${
codeEditorLineNumbers ? 'translate-x-7' : 'translate-x-1'
} inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200`}
/>
</button>
</div>
</div>
{/* Font Size */}
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">
Font Size
</div>
<div className="text-sm text-muted-foreground">
Editor font size in pixels
</div>
</div>
<select
value={codeEditorFontSize}
onChange={(e) => setCodeEditorFontSize(e.target.value)}
className="text-sm bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 w-24"
>
<option value="10">10px</option>
<option value="11">11px</option>
<option value="12">12px</option>
<option value="13">13px</option>
<option value="14">14px</option>
<option value="15">15px</option>
<option value="16">16px</option>
<option value="18">18px</option>
<option value="20">20px</option>
</select>
</div>
</div>
</div>
</div> </div>
)} )}
@@ -818,7 +1015,7 @@ function Settings({ isOpen, onClose, projects = [] }) {
type="checkbox" type="checkbox"
checked={skipPermissions} checked={skipPermissions}
onChange={(e) => setSkipPermissions(e.target.checked)} onChange={(e) => setSkipPermissions(e.target.checked)}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500" className="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:ring-2 checked:bg-blue-600 dark:checked:bg-blue-600"
/> />
<div> <div>
<div className="font-medium text-orange-900 dark:text-orange-100"> <div className="font-medium text-orange-900 dark:text-orange-100">
@@ -1578,7 +1775,7 @@ function Settings({ isOpen, onClose, projects = [] }) {
type="checkbox" type="checkbox"
checked={cursorSkipPermissions} checked={cursorSkipPermissions}
onChange={(e) => setCursorSkipPermissions(e.target.checked)} onChange={(e) => setCursorSkipPermissions(e.target.checked)}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500" className="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:ring-2 checked:bg-blue-600 dark:checked:bg-blue-600"
/> />
<div> <div>
<div className="font-medium text-orange-900 dark:text-orange-100"> <div className="font-medium text-orange-900 dark:text-orange-100">