mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-09 10:49:38 +00:00
Compare commits
12 Commits
v1.10.4
...
c7dbab086b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7dbab086b | ||
|
|
b31f7afdf5 | ||
|
|
57739a659f | ||
|
|
a5813e66d9 | ||
|
|
18ea4a19dd | ||
|
|
1c95c598eb | ||
|
|
72e97c4fbc | ||
|
|
b5d1fed354 | ||
|
|
d1733f34e0 | ||
|
|
fefcc0f338 | ||
|
|
36f8f50d63 | ||
|
|
4e14222487 |
19
.env.example
19
.env.example
@@ -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
|
||||
|
||||
74
README.md
74
README.md
@@ -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
7
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@siteboon/claude-code-ui",
|
||||
"version": "1.10.4",
|
||||
"version": "1.11.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@siteboon/claude-code-ui",
|
||||
"version": "1.10.4",
|
||||
"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",
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "@siteboon/claude-code-ui",
|
||||
"version": "1.10.4",
|
||||
"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/",
|
||||
|
||||
@@ -79,6 +79,16 @@ function mapCliOptionsToSDK(options = {}) {
|
||||
// Map model (default to 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
|
||||
if (sessionId) {
|
||||
sdkOptions.resume = sessionId;
|
||||
@@ -374,7 +384,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
||||
for await (const message of queryInstance) {
|
||||
// Capture session ID from first message
|
||||
if (message.session_id && !capturedSessionId) {
|
||||
console.log('📝 Captured session ID:', message.session_id);
|
||||
|
||||
capturedSessionId = message.session_id;
|
||||
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
|
||||
|
||||
|
||||
225
server/cli.js
Executable file
225
server/cli.js
Executable 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);
|
||||
});
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,16 +502,16 @@ router.post('/generate-commit-message', async (req, res) => {
|
||||
*/
|
||||
async function generateCommitMessageWithAI(files, diffContext, provider, projectPath) {
|
||||
// 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:
|
||||
- Use conventional commit format: type(scope): subject
|
||||
- Include a body that explains what changed and why
|
||||
- Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
|
||||
- Keep subject line under 50 characters
|
||||
- Wrap body at 72 characters
|
||||
- Be specific and descriptive
|
||||
- Return ONLY the commit message, nothing else - no markdown, no explanations, no code blocks
|
||||
- Format: type(scope): subject
|
||||
- Include body explaining what changed and why
|
||||
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
|
||||
- Subject under 50 chars, body wrapped at 72 chars
|
||||
- Focus on user-facing changes, not implementation details
|
||||
- Consider what's being added AND removed
|
||||
- Return ONLY the commit message (no markdown, explanations, or code blocks)
|
||||
|
||||
FILES CHANGED:
|
||||
${files.map(f => `- ${f}`).join('\n')}
|
||||
@@ -519,7 +519,7 @@ ${files.map(f => `- ${f}`).join('\n')}
|
||||
DIFFS:
|
||||
${diffContext.substring(0, 4000)}
|
||||
|
||||
Generate the commit message now:`;
|
||||
Generate the commit message:`;
|
||||
|
||||
try {
|
||||
// Create a simple writer that collects the response
|
||||
|
||||
103
src/App.jsx
103
src/App.jsx
@@ -20,6 +20,7 @@
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Settings as SettingsIcon, Sparkles } from 'lucide-react';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import MainContent from './components/MainContent';
|
||||
import MobileNav from './components/MobileNav';
|
||||
@@ -54,12 +55,14 @@ function AppContent() {
|
||||
const [isLoadingProjects, setIsLoadingProjects] = useState(true);
|
||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [settingsInitialTab, setSettingsInitialTab] = useState('tools');
|
||||
const [showQuickSettings, setShowQuickSettings] = useState(false);
|
||||
const [autoExpandTools, setAutoExpandTools] = useLocalStorage('autoExpandTools', false);
|
||||
const [showRawParameters, setShowRawParameters] = useLocalStorage('showRawParameters', false);
|
||||
const [showThinking, setShowThinking] = useLocalStorage('showThinking', true);
|
||||
const [autoScrollToBottom, setAutoScrollToBottom] = useLocalStorage('autoScrollToBottom', true);
|
||||
const [sendByCtrlEnter, setSendByCtrlEnter] = useLocalStorage('sendByCtrlEnter', false);
|
||||
const [sidebarVisible, setSidebarVisible] = useLocalStorage('sidebarVisible', true);
|
||||
// Session Protection System: Track sessions with active conversations to prevent
|
||||
// automatic project updates from interrupting ongoing chats. When a user sends
|
||||
// a message, the session is marked as "active" and project updates are paused
|
||||
@@ -306,6 +309,12 @@ function AppContent() {
|
||||
// Expose fetchProjects globally for component access
|
||||
window.refreshProjects = fetchProjects;
|
||||
|
||||
// Expose openSettings function globally for component access
|
||||
window.openSettings = useCallback((tab = 'tools') => {
|
||||
setSettingsInitialTab(tab);
|
||||
setShowSettings(true);
|
||||
}, []);
|
||||
|
||||
// Handle URL-based session loading
|
||||
useEffect(() => {
|
||||
if (sessionId && projects.length > 0) {
|
||||
@@ -734,28 +743,78 @@ function AppContent() {
|
||||
<div className="fixed inset-0 flex bg-background">
|
||||
{/* Fixed Desktop Sidebar */}
|
||||
{!isMobile && (
|
||||
<div className="w-80 flex-shrink-0 border-r border-border bg-card">
|
||||
<div
|
||||
className={`flex-shrink-0 border-r border-border bg-card transition-all duration-300 ${
|
||||
sidebarVisible ? 'w-80' : 'w-14'
|
||||
}`}
|
||||
>
|
||||
<div className="h-full overflow-hidden">
|
||||
<Sidebar
|
||||
projects={projects}
|
||||
selectedProject={selectedProject}
|
||||
selectedSession={selectedSession}
|
||||
onProjectSelect={handleProjectSelect}
|
||||
onSessionSelect={handleSessionSelect}
|
||||
onNewSession={handleNewSession}
|
||||
onSessionDelete={handleSessionDelete}
|
||||
onProjectDelete={handleProjectDelete}
|
||||
isLoading={isLoadingProjects}
|
||||
onRefresh={handleSidebarRefresh}
|
||||
onShowSettings={() => setShowSettings(true)}
|
||||
updateAvailable={updateAvailable}
|
||||
latestVersion={latestVersion}
|
||||
currentVersion={currentVersion}
|
||||
releaseInfo={releaseInfo}
|
||||
onShowVersionModal={() => setShowVersionModal(true)}
|
||||
isPWA={isPWA}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
{sidebarVisible ? (
|
||||
<Sidebar
|
||||
projects={projects}
|
||||
selectedProject={selectedProject}
|
||||
selectedSession={selectedSession}
|
||||
onProjectSelect={handleProjectSelect}
|
||||
onSessionSelect={handleSessionSelect}
|
||||
onNewSession={handleNewSession}
|
||||
onSessionDelete={handleSessionDelete}
|
||||
onProjectDelete={handleProjectDelete}
|
||||
isLoading={isLoadingProjects}
|
||||
onRefresh={handleSidebarRefresh}
|
||||
onShowSettings={() => setShowSettings(true)}
|
||||
updateAvailable={updateAvailable}
|
||||
latestVersion={latestVersion}
|
||||
currentVersion={currentVersion}
|
||||
releaseInfo={releaseInfo}
|
||||
onShowVersionModal={() => setShowVersionModal(true)}
|
||||
isPWA={isPWA}
|
||||
isMobile={isMobile}
|
||||
onToggleSidebar={() => setSidebarVisible(false)}
|
||||
/>
|
||||
) : (
|
||||
/* Collapsed Sidebar */
|
||||
<div className="h-full flex flex-col items-center py-4 gap-4">
|
||||
{/* Expand Button */}
|
||||
<button
|
||||
onClick={() => setSidebarVisible(true)}
|
||||
className="p-2 hover:bg-accent rounded-md transition-colors duration-200 group"
|
||||
aria-label="Show sidebar"
|
||||
title="Show sidebar"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5 text-foreground group-hover:scale-110 transition-transform"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 5l7 7-7 7M5 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Settings Icon */}
|
||||
<button
|
||||
onClick={() => setShowSettings(true)}
|
||||
className="p-2 hover:bg-accent rounded-md transition-colors duration-200"
|
||||
aria-label="Settings"
|
||||
title="Settings"
|
||||
>
|
||||
<SettingsIcon className="w-5 h-5 text-muted-foreground hover:text-foreground transition-colors" />
|
||||
</button>
|
||||
|
||||
{/* Update Indicator */}
|
||||
{updateAvailable && (
|
||||
<button
|
||||
onClick={() => setShowVersionModal(true)}
|
||||
className="relative p-2 hover:bg-accent rounded-md transition-colors duration-200"
|
||||
aria-label="Update available"
|
||||
title="Update available"
|
||||
>
|
||||
<Sparkles className="w-5 h-5 text-blue-500" />
|
||||
<span className="absolute top-1 right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -805,6 +864,7 @@ function AppContent() {
|
||||
onShowVersionModal={() => setShowVersionModal(true)}
|
||||
isPWA={isPWA}
|
||||
isMobile={isMobile}
|
||||
onToggleSidebar={() => setSidebarVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -874,6 +934,7 @@ function AppContent() {
|
||||
isOpen={showSettings}
|
||||
onClose={() => setShowSettings(false)}
|
||||
projects={projects}
|
||||
initialTab={settingsInitialTab}
|
||||
/>
|
||||
|
||||
{/* Version Upgrade Modal */}
|
||||
|
||||
@@ -514,7 +514,6 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="text-lg leading-none">📝</span>
|
||||
<span>View edit diff for</span>
|
||||
</span>
|
||||
<button
|
||||
@@ -2604,7 +2603,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
const scrollToBottom = useCallback(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
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
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -3523,7 +3523,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
const prevTop = scrollPositionRef.current.top;
|
||||
const newHeight = container.scrollHeight;
|
||||
const heightDiff = newHeight - prevHeight;
|
||||
|
||||
|
||||
// If content was added above the current view, adjust scroll position
|
||||
if (heightDiff > 0 && prevTop > 0) {
|
||||
container.scrollTop = prevTop + heightDiff;
|
||||
@@ -3537,9 +3537,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
if (scrollContainerRef.current && chatMessages.length > 0 && !isLoadingSessionRef.current) {
|
||||
// Only scroll if we're not in the middle of loading a session
|
||||
// This prevents the "double scroll" effect during session switching
|
||||
// Also reset scroll state
|
||||
// Reset scroll state when switching sessions
|
||||
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
|
||||
|
||||
@@ -4448,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
|
||||
@@ -4626,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"
|
||||
|
||||
@@ -10,23 +10,37 @@ import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { EditorView, showPanel, ViewPlugin } from '@codemirror/view';
|
||||
import { unifiedMergeView, getChunks } from '@codemirror/merge';
|
||||
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';
|
||||
|
||||
function CodeEditor({ file, onClose, projectPath }) {
|
||||
function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded = false, onToggleExpand = null }) {
|
||||
const [content, setContent] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = 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 [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);
|
||||
|
||||
// Create minimap extension with chunk-based gutters
|
||||
const minimapExtension = useMemo(() => {
|
||||
if (!file.diffInfo || !showDiff) return [];
|
||||
if (!file.diffInfo || !showDiff || !minimapEnabled) return [];
|
||||
|
||||
const gutters = {};
|
||||
|
||||
@@ -58,7 +72,7 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
};
|
||||
})
|
||||
];
|
||||
}, [file.diffInfo, showDiff, isDarkMode]);
|
||||
}, [file.diffInfo, showDiff, minimapEnabled, isDarkMode]);
|
||||
|
||||
// Create extension to scroll to first chunk on mount
|
||||
const scrollToFirstChunkExtension = useMemo(() => {
|
||||
@@ -89,24 +103,28 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
];
|
||||
}, [file.diffInfo, showDiff]);
|
||||
|
||||
// Create diff navigation panel extension
|
||||
const diffNavigationPanel = useMemo(() => {
|
||||
if (!file.diffInfo || !showDiff) return [];
|
||||
|
||||
// Create editor toolbar panel - always visible
|
||||
const editorToolbarPanel = useMemo(() => {
|
||||
const createPanel = (view) => {
|
||||
const dom = document.createElement('div');
|
||||
dom.className = 'cm-diff-navigation-panel';
|
||||
dom.className = 'cm-editor-toolbar-panel';
|
||||
|
||||
let currentIndex = 0;
|
||||
|
||||
const updatePanel = () => {
|
||||
// Use getChunks API to get ALL chunks regardless of viewport
|
||||
const chunksData = getChunks(view.state);
|
||||
// Check if we have diff info and it's enabled
|
||||
const hasDiff = file.diffInfo && showDiff;
|
||||
const chunksData = hasDiff ? getChunks(view.state) : null;
|
||||
const chunks = chunksData?.chunks || [];
|
||||
const chunkCount = chunks.length;
|
||||
|
||||
dom.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
// Build the toolbar HTML
|
||||
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>
|
||||
<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">
|
||||
@@ -118,41 +136,110 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</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');
|
||||
const nextBtn = dom.querySelector('.cm-diff-nav-next');
|
||||
// Expand button (only in sidebar mode)
|
||||
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', () => {
|
||||
if (chunks.length === 0) return;
|
||||
currentIndex = currentIndex > 0 ? currentIndex - 1 : chunks.length - 1;
|
||||
toolbarHTML += '</div>';
|
||||
toolbarHTML += '</div>';
|
||||
|
||||
// Navigate to the chunk - use fromB which is the position in the current document
|
||||
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' })
|
||||
});
|
||||
dom.innerHTML = toolbarHTML;
|
||||
|
||||
// Attach event listeners for diff navigation
|
||||
if (hasDiff) {
|
||||
const prevBtn = dom.querySelector('.cm-diff-nav-prev');
|
||||
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', () => {
|
||||
if (chunks.length === 0) return;
|
||||
currentIndex = currentIndex < chunks.length - 1 ? currentIndex + 1 : 0;
|
||||
|
||||
// Navigate to the chunk - use fromB which is the position in the current document
|
||||
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();
|
||||
});
|
||||
// Attach event listener for expand button
|
||||
if (isSidebar && onToggleExpand) {
|
||||
const expandBtn = dom.querySelector('.cm-expand-btn');
|
||||
expandBtn?.addEventListener('click', () => {
|
||||
onToggleExpand();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
updatePanel();
|
||||
@@ -165,7 +252,7 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
};
|
||||
|
||||
return [showPanel.of(createPanel)];
|
||||
}, [file.diffInfo, showDiff]);
|
||||
}, [file.diffInfo, showDiff, isSidebar, isExpanded, onToggleExpand]);
|
||||
|
||||
// Get language extension based on file extension
|
||||
const getLanguageExtension = (filename) => {
|
||||
@@ -290,6 +377,57 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
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
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
@@ -321,14 +459,23 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div className="fixed inset-0 z-50 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">
|
||||
{isSidebar ? (
|
||||
<div className="w-full h-full flex items-center justify-center bg-white dark:bg-gray-900">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||
<span className="text-gray-900 dark:text-white">Loading {file.name}...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<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="flex items-center gap-3">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||
<span className="text-gray-900 dark:text-white">Loading {file.name}...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -372,8 +519,8 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
background-color: ${isDarkMode ? '#1e1e1e' : '#f5f5f5'};
|
||||
}
|
||||
|
||||
/* Diff navigation panel styling */
|
||||
.cm-diff-navigation-panel {
|
||||
/* Editor toolbar panel styling */
|
||||
.cm-editor-toolbar-panel {
|
||||
padding: 8px 12px;
|
||||
background-color: ${isDarkMode ? '#1f2937' : '#ffffff'};
|
||||
border-bottom: 1px solid ${isDarkMode ? '#374151' : '#e5e7eb'};
|
||||
@@ -381,7 +528,8 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cm-diff-nav-btn {
|
||||
.cm-diff-nav-btn,
|
||||
.cm-toolbar-btn {
|
||||
padding: 4px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -391,9 +539,11 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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'};
|
||||
}
|
||||
|
||||
@@ -403,67 +553,36 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div className={`fixed inset-0 z-50 ${
|
||||
<div className={isSidebar ?
|
||||
'w-full h-full flex flex-col' :
|
||||
`fixed inset-0 z-40 ${
|
||||
// Mobile: native fullscreen, Desktop: modal with backdrop
|
||||
'md:bg-black/50 md:flex md:items-center md:justify-center md:p-4'
|
||||
} ${isFullscreen ? 'md:p-0' : ''}`}>
|
||||
<div className={`bg-white shadow-2xl flex flex-col ${
|
||||
// Mobile: always fullscreen, Desktop: modal sizing
|
||||
'w-full h-full md:rounded-lg md:shadow-2xl' +
|
||||
(isFullscreen ? ' md:w-full md:h-full md:rounded-none' : ' md:w-full md:max-w-6xl md:h-[80vh] md:max-h-[80vh]')
|
||||
}`}>
|
||||
<div className={isSidebar ?
|
||||
'bg-white dark:bg-gray-900 flex flex-col w-full h-full' :
|
||||
`bg-white shadow-2xl flex flex-col ${
|
||||
// Mobile: always fullscreen, Desktop: modal sizing
|
||||
'w-full h-full md:rounded-lg md:shadow-2xl' +
|
||||
(isFullscreen ? ' md:w-full md:h-full md:rounded-none' : ' md:w-full md:max-w-6xl md:h-[80vh] md:max-h-[80vh]')
|
||||
}`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 flex-shrink-0 min-w-0">
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0 min-w-0">
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
<div className="w-8 h-8 bg-blue-600 rounded flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-white text-sm font-mono">
|
||||
{file.name.split('.').pop()?.toUpperCase() || 'FILE'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<h3 className="font-medium text-gray-900 truncate">{file.name}</h3>
|
||||
<h3 className="font-medium text-gray-900 dark:text-white truncate">{file.name}</h3>
|
||||
{file.diffInfo && (
|
||||
<span className="text-xs bg-blue-100 text-blue-600 px-2 py-1 rounded whitespace-nowrap">
|
||||
📝 Has changes
|
||||
<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">
|
||||
Showing changes
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 truncate">{file.path}</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 truncate">{file.path}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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
|
||||
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"
|
||||
@@ -496,13 +615,15 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={toggleFullscreen}
|
||||
className="hidden md:flex 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 items-center justify-center"
|
||||
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
|
||||
>
|
||||
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||
</button>
|
||||
{!isSidebar && (
|
||||
<button
|
||||
onClick={toggleFullscreen}
|
||||
className="hidden md:flex 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 items-center justify-center"
|
||||
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
|
||||
>
|
||||
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -522,6 +643,9 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
onChange={setContent}
|
||||
extensions={[
|
||||
...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
|
||||
? [
|
||||
unifiedMergeView({
|
||||
@@ -533,8 +657,7 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
// NOTE: NO collapseUnchanged - this shows the full file!
|
||||
}),
|
||||
...minimapExtension,
|
||||
...scrollToFirstChunkExtension,
|
||||
...diffNavigationPanel
|
||||
...scrollToFirstChunkExtension
|
||||
]
|
||||
: []),
|
||||
...(wordWrap ? [EditorView.lineWrapping] : [])
|
||||
@@ -542,11 +665,11 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
theme={isDarkMode ? oneDark : undefined}
|
||||
height="100%"
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontSize: `${fontSize}px`,
|
||||
height: '100%',
|
||||
}}
|
||||
basicSetup={{
|
||||
lineNumbers: true,
|
||||
lineNumbers: showLineNumbers,
|
||||
foldGutter: true,
|
||||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
@@ -565,7 +688,6 @@ function CodeEditor({ file, onClose, projectPath }) {
|
||||
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>Lines: {content.split('\n').length}</span>
|
||||
<span>Characters: {content.length}</span>
|
||||
<span>Language: {file.name.split('.').pop()?.toUpperCase() || 'Text'}</span>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* No session protection logic is implemented here - it's purely a props bridge.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import ChatInterface from './ChatInterface';
|
||||
import FileTree from './FileTree';
|
||||
import CodeEditor from './CodeEditor';
|
||||
@@ -28,13 +28,13 @@ import { useTaskMaster } from '../contexts/TaskMasterContext';
|
||||
import { useTasksSettings } from '../contexts/TasksSettingsContext';
|
||||
import { api } from '../utils/api';
|
||||
|
||||
function MainContent({
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
ws,
|
||||
sendMessage,
|
||||
function MainContent({
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
ws,
|
||||
sendMessage,
|
||||
messages,
|
||||
isMobile,
|
||||
isPWA,
|
||||
@@ -61,6 +61,10 @@ function MainContent({
|
||||
const [editingFile, setEditingFile] = useState(null);
|
||||
const [selectedTask, setSelectedTask] = useState(null);
|
||||
const [showTaskDetail, setShowTaskDetail] = useState(false);
|
||||
const [editorWidth, setEditorWidth] = useState(600);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [editorExpanded, setEditorExpanded] = useState(false);
|
||||
const resizeRef = useRef(null);
|
||||
|
||||
// PRD Editor state
|
||||
const [showPRDEditor, setShowPRDEditor] = useState(false);
|
||||
@@ -127,6 +131,11 @@ function MainContent({
|
||||
|
||||
const handleCloseEditor = () => {
|
||||
setEditingFile(null);
|
||||
setEditorExpanded(false);
|
||||
};
|
||||
|
||||
const handleToggleEditorExpand = () => {
|
||||
setEditorExpanded(!editorExpanded);
|
||||
};
|
||||
|
||||
const handleTaskClick = (task) => {
|
||||
@@ -153,6 +162,52 @@ function MainContent({
|
||||
console.log('Update task status:', taskId, newStatus);
|
||||
refreshTasks?.();
|
||||
};
|
||||
|
||||
// Handle resize functionality
|
||||
const handleMouseDown = (e) => {
|
||||
if (isMobile) return; // Disable resize on mobile
|
||||
setIsResizing(true);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (e) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
const container = resizeRef.current?.parentElement;
|
||||
if (!container) return;
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const newWidth = containerRect.right - e.clientX;
|
||||
|
||||
// Min width: 300px, Max width: 80% of container
|
||||
const minWidth = 300;
|
||||
const maxWidth = containerRect.width * 0.8;
|
||||
|
||||
if (newWidth >= minWidth && newWidth <= maxWidth) {
|
||||
setEditorWidth(newWidth);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsResizing(false);
|
||||
};
|
||||
|
||||
if (isResizing) {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
}, [isResizing]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
@@ -234,10 +289,10 @@ function MainContent({
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header with tabs */}
|
||||
<div
|
||||
<div
|
||||
className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 pwa-header-safe flex-shrink-0"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between relative">
|
||||
<div className="flex items-center space-x-2 sm:space-x-3">
|
||||
{isMobile && (
|
||||
<button
|
||||
@@ -409,11 +464,13 @@ function MainContent({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||
<div className={`h-full ${activeTab === 'chat' ? 'block' : 'hidden'}`}>
|
||||
<ErrorBoundary showDetails={true}>
|
||||
<ChatInterface
|
||||
{/* Content Area with Right Sidebar */}
|
||||
<div className="flex-1 flex min-h-0 overflow-hidden">
|
||||
{/* Main Content */}
|
||||
<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'}`}>
|
||||
<ErrorBoundary showDetails={true}>
|
||||
<ChatInterface
|
||||
selectedProject={selectedProject}
|
||||
selectedSession={selectedSession}
|
||||
ws={ws}
|
||||
@@ -513,14 +570,49 @@ function MainContent({
|
||||
onClearLogs={() => setServerLogs([])}
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Code Editor Right Sidebar - Desktop only, Mobile uses modal */}
|
||||
{editingFile && !isMobile && (
|
||||
<>
|
||||
{/* Resize Handle - Hidden when expanded */}
|
||||
{!editorExpanded && (
|
||||
<div
|
||||
ref={resizeRef}
|
||||
onMouseDown={handleMouseDown}
|
||||
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" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Editor Sidebar */}
|
||||
<div
|
||||
className={`flex-shrink-0 border-l border-gray-200 dark:border-gray-700 h-full overflow-hidden ${editorExpanded ? 'flex-1' : ''}`}
|
||||
style={editorExpanded ? {} : { width: `${editorWidth}px` }}
|
||||
>
|
||||
<CodeEditor
|
||||
file={editingFile}
|
||||
onClose={handleCloseEditor}
|
||||
projectPath={selectedProject?.path}
|
||||
isSidebar={true}
|
||||
isExpanded={editorExpanded}
|
||||
onToggleExpand={handleToggleEditorExpand}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Code Editor Modal */}
|
||||
{editingFile && (
|
||||
{/* Code Editor Modal for Mobile */}
|
||||
{editingFile && isMobile && (
|
||||
<CodeEditor
|
||||
file={editingFile}
|
||||
onClose={handleCloseEditor}
|
||||
projectPath={selectedProject?.path}
|
||||
isSidebar={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ const QuickSettingsPanel = ({
|
||||
type="checkbox"
|
||||
checked={autoExpandTools}
|
||||
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>
|
||||
|
||||
@@ -125,7 +125,7 @@ const QuickSettingsPanel = ({
|
||||
type="checkbox"
|
||||
checked={showRawParameters}
|
||||
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>
|
||||
|
||||
@@ -138,7 +138,7 @@ const QuickSettingsPanel = ({
|
||||
type="checkbox"
|
||||
checked={showThinking}
|
||||
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>
|
||||
</div>
|
||||
@@ -155,7 +155,7 @@ const QuickSettingsPanel = ({
|
||||
type="checkbox"
|
||||
checked={autoScrollToBottom}
|
||||
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>
|
||||
</div>
|
||||
@@ -173,7 +173,7 @@ const QuickSettingsPanel = ({
|
||||
type="checkbox"
|
||||
checked={sendByCtrlEnter}
|
||||
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>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 ml-3">
|
||||
|
||||
@@ -10,7 +10,7 @@ import ClaudeLogo from './ClaudeLogo';
|
||||
import CursorLogo from './CursorLogo';
|
||||
import CredentialsSettings from './CredentialsSettings';
|
||||
|
||||
function Settings({ isOpen, onClose, projects = [] }) {
|
||||
function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
|
||||
const { isDarkMode, toggleDarkMode } = useTheme();
|
||||
const {
|
||||
tasksEnabled,
|
||||
@@ -52,9 +52,26 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
||||
const [mcpTestResults, setMcpTestResults] = useState({});
|
||||
const [mcpServerTools, setMcpServerTools] = useState({});
|
||||
const [mcpToolsLoading, setMcpToolsLoading] = useState({});
|
||||
const [activeTab, setActiveTab] = useState('tools');
|
||||
const [activeTab, setActiveTab] = useState(initialTab);
|
||||
const [jsonValidationError, setJsonValidationError] = useState('');
|
||||
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
|
||||
const [cursorAllowedCommands, setCursorAllowedCommands] = useState([]);
|
||||
@@ -327,8 +344,36 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
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 () => {
|
||||
try {
|
||||
@@ -625,7 +670,7 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
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="flex items-center justify-between p-4 md:p-6 border-b border-border flex-shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -758,6 +803,158 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
||||
</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>
|
||||
)}
|
||||
|
||||
@@ -818,7 +1015,7 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
||||
type="checkbox"
|
||||
checked={skipPermissions}
|
||||
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 className="font-medium text-orange-900 dark:text-orange-100">
|
||||
@@ -1578,7 +1775,7 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
||||
type="checkbox"
|
||||
checked={cursorSkipPermissions}
|
||||
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 className="font-medium text-orange-900 dark:text-orange-100">
|
||||
|
||||
@@ -57,7 +57,8 @@ function Sidebar({
|
||||
releaseInfo,
|
||||
onShowVersionModal,
|
||||
isPWA,
|
||||
isMobile
|
||||
isMobile,
|
||||
onToggleSidebar
|
||||
}) {
|
||||
const [expandedProjects, setExpandedProjects] = useState(new Set());
|
||||
const [editingProject, setEditingProject] = useState(null);
|
||||
@@ -587,34 +588,24 @@ function Sidebar({
|
||||
<p className="text-sm text-muted-foreground">AI coding assistant interface</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{onToggleSidebar && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 w-9 px-0 hover:bg-accent transition-colors duration-200 group"
|
||||
onClick={async () => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
await onRefresh();
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}}
|
||||
disabled={isRefreshing}
|
||||
title="Refresh projects and sessions (Ctrl+R)"
|
||||
className="h-8 w-8 px-0 hover:bg-accent transition-colors duration-200"
|
||||
onClick={onToggleSidebar}
|
||||
title="Hide sidebar"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''} group-hover:rotate-180 transition-transform duration-300`} />
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="h-9 w-9 px-0 bg-primary hover:bg-primary/90 transition-all duration-200 shadow-sm hover:shadow-md"
|
||||
onClick={() => setShowNewProject(true)}
|
||||
title="Create new project (Ctrl+N)"
|
||||
>
|
||||
<FolderPlus className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile Header */}
|
||||
@@ -914,9 +905,9 @@ function Sidebar({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search Filter */}
|
||||
{/* Search Filter and Actions */}
|
||||
{projects.length > 0 && !isLoading && (
|
||||
<div className="px-3 md:px-4 py-2 border-b border-border">
|
||||
<div className="px-3 md:px-4 py-2 border-b border-border space-y-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
@@ -935,6 +926,39 @@ function Sidebar({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons - Desktop only */}
|
||||
{!isMobile && (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-8 text-xs bg-primary hover:bg-primary/90 transition-all duration-200"
|
||||
onClick={() => setShowNewProject(true)}
|
||||
title="Create new project (Ctrl+N)"
|
||||
>
|
||||
<FolderPlus className="w-3.5 h-3.5 mr-1.5" />
|
||||
New Project
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 w-8 px-0 hover:bg-accent transition-colors duration-200 group"
|
||||
onClick={async () => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
await onRefresh();
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}}
|
||||
disabled={isRefreshing}
|
||||
title="Refresh projects and sessions (Ctrl+R)"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${isRefreshing ? 'animate-spin' : ''} group-hover:rotate-180 transition-transform duration-300`} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user