mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-08 18:09:39 +00:00
- Upgrading to Vite 7
- Refactor to use es modules - Added permission mode - Switched to better sqlite3 - several UX enhancements
This commit is contained in:
1634
package-lock.json
generated
1634
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "claude-code-ui",
|
||||
"version": "1.1.4",
|
||||
"version": "1.2.0",
|
||||
"description": "A web-based UI for Claude Code CLI",
|
||||
"type": "module",
|
||||
"main": "server/index.js",
|
||||
"scripts": {
|
||||
"dev": "concurrently --kill-others \"npm run server\" \"npm run client\"",
|
||||
@@ -34,6 +35,7 @@
|
||||
"@xterm/addon-clipboard": "^0.1.0",
|
||||
"@xterm/addon-webgl": "^0.18.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"chokidar": "^4.0.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -48,7 +50,6 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"ws": "^8.14.2",
|
||||
"xterm": "^5.3.0",
|
||||
@@ -57,12 +58,12 @@
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"concurrently": "^8.2.2",
|
||||
"postcss": "^8.4.32",
|
||||
"sharp": "^0.34.2",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"vite": "^5.0.8"
|
||||
"vite": "^7.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const { spawn } = require('child_process');
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
let activeClaudeProcesses = new Map(); // Track active processes by session ID
|
||||
|
||||
async function spawnClaude(command, options = {}, ws) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { sessionId, projectPath, cwd, resume, toolsSettings } = options;
|
||||
const { sessionId, projectPath, cwd, resume, toolsSettings, permissionMode } = options;
|
||||
let capturedSessionId = sessionId; // Track session ID throughout the process
|
||||
let sessionCreatedSent = false; // Track if we've already sent session-created event
|
||||
|
||||
@@ -36,15 +36,38 @@ async function spawnClaude(command, options = {}, ws) {
|
||||
args.push('--model', 'sonnet');
|
||||
}
|
||||
|
||||
// Add permission mode if specified (works for both new and resumed sessions)
|
||||
if (permissionMode && permissionMode !== 'default') {
|
||||
args.push('--permission-mode', permissionMode);
|
||||
console.log('🔒 Using permission mode:', permissionMode);
|
||||
}
|
||||
|
||||
// Add tools settings flags
|
||||
if (settings.skipPermissions) {
|
||||
// Don't use --dangerously-skip-permissions when in plan mode
|
||||
if (settings.skipPermissions && permissionMode !== 'plan') {
|
||||
args.push('--dangerously-skip-permissions');
|
||||
console.log('⚠️ Using --dangerously-skip-permissions (skipping other tool settings)');
|
||||
} else {
|
||||
// Only add allowed/disallowed tools if not skipping permissions
|
||||
|
||||
// Collect all allowed tools, including plan mode defaults
|
||||
let allowedTools = [...(settings.allowedTools || [])];
|
||||
|
||||
// Add plan mode specific tools
|
||||
if (permissionMode === 'plan') {
|
||||
const planModeTools = ['Read', 'Task', 'exit_plan_mode', 'TodoRead', 'TodoWrite'];
|
||||
// Add plan mode tools that aren't already in the allowed list
|
||||
for (const tool of planModeTools) {
|
||||
if (!allowedTools.includes(tool)) {
|
||||
allowedTools.push(tool);
|
||||
}
|
||||
}
|
||||
console.log('📝 Plan mode: Added default allowed tools:', planModeTools);
|
||||
}
|
||||
|
||||
// Add allowed tools
|
||||
if (settings.allowedTools && settings.allowedTools.length > 0) {
|
||||
for (const tool of settings.allowedTools) {
|
||||
if (allowedTools.length > 0) {
|
||||
for (const tool of allowedTools) {
|
||||
args.push('--allowedTools', tool);
|
||||
console.log('✅ Allowing tool:', tool);
|
||||
}
|
||||
@@ -57,6 +80,11 @@ async function spawnClaude(command, options = {}, ws) {
|
||||
console.log('❌ Disallowing tool:', tool);
|
||||
}
|
||||
}
|
||||
|
||||
// Log when skip permissions is disabled due to plan mode
|
||||
if (settings.skipPermissions && permissionMode === 'plan') {
|
||||
console.log('📝 Skip permissions disabled due to plan mode');
|
||||
}
|
||||
}
|
||||
|
||||
// Use cwd (actual project directory) instead of projectPath (Claude's metadata directory)
|
||||
@@ -201,7 +229,7 @@ function abortClaudeSession(sessionId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export {
|
||||
spawnClaude,
|
||||
abortClaudeSession
|
||||
};
|
||||
@@ -1,99 +1,85 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
import Database from 'better-sqlite3';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const DB_PATH = path.join(__dirname, 'auth.db');
|
||||
const INIT_SQL_PATH = path.join(__dirname, 'init.sql');
|
||||
|
||||
// Create database connection
|
||||
const db = new sqlite3.Database(DB_PATH, (err) => {
|
||||
if (err) {
|
||||
console.error('Error opening database:', err.message);
|
||||
} else {
|
||||
console.log('Connected to SQLite database');
|
||||
}
|
||||
});
|
||||
const db = new Database(DB_PATH);
|
||||
console.log('Connected to SQLite database');
|
||||
|
||||
// Initialize database with schema
|
||||
const initializeDatabase = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const initSQL = fs.readFileSync(INIT_SQL_PATH, 'utf8');
|
||||
db.exec(initSQL, (err) => {
|
||||
if (err) {
|
||||
console.error('Error initializing database:', err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log('Database initialized successfully');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error reading init SQL file:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
try {
|
||||
const initSQL = fs.readFileSync(INIT_SQL_PATH, 'utf8');
|
||||
db.exec(initSQL);
|
||||
console.log('Database initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('Error initializing database:', error.message);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// User database operations
|
||||
const userDb = {
|
||||
// Check if any users exist
|
||||
hasUsers: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get('SELECT COUNT(*) as count FROM users', (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row.count > 0);
|
||||
});
|
||||
});
|
||||
try {
|
||||
const row = db.prepare('SELECT COUNT(*) as count FROM users').get();
|
||||
return row.count > 0;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// Create a new user
|
||||
createUser: (username, passwordHash) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const stmt = db.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)');
|
||||
stmt.run(username, passwordHash, function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ id: this.lastID, username });
|
||||
}
|
||||
});
|
||||
stmt.finalize();
|
||||
});
|
||||
const result = stmt.run(username, passwordHash);
|
||||
return { id: result.lastInsertRowid, username };
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// Get user by username
|
||||
getUserByUsername: (username) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get('SELECT * FROM users WHERE username = ? AND is_active = 1', [username], (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
try {
|
||||
const row = db.prepare('SELECT * FROM users WHERE username = ? AND is_active = 1').get(username);
|
||||
return row;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// Update last login time
|
||||
updateLastLogin: (userId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?', [userId], (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
try {
|
||||
db.prepare('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?').run(userId);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// Get user by ID
|
||||
getUserById: (userId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get('SELECT id, username, created_at, last_login FROM users WHERE id = ? AND is_active = 1', [userId], (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
try {
|
||||
const row = db.prepare('SELECT id, username, created_at, last_login FROM users WHERE id = ? AND is_active = 1').get(userId);
|
||||
return row;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
export {
|
||||
db,
|
||||
initializeDatabase,
|
||||
userDb
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
// Load environment variables from .env file
|
||||
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);
|
||||
|
||||
try {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const envPath = path.join(__dirname, '../.env');
|
||||
const envFile = fs.readFileSync(envPath, 'utf8');
|
||||
envFile.split('\n').forEach(line => {
|
||||
@@ -19,31 +25,31 @@ try {
|
||||
|
||||
console.log('PORT from env:', process.env.PORT);
|
||||
|
||||
const express = require('express');
|
||||
const { WebSocketServer } = require('ws');
|
||||
const http = require('http');
|
||||
const path = require('path');
|
||||
const cors = require('cors');
|
||||
const fs = require('fs').promises;
|
||||
const { spawn } = require('child_process');
|
||||
const os = require('os');
|
||||
const pty = require('node-pty');
|
||||
const fetch = require('node-fetch');
|
||||
import express from 'express';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import http from 'http';
|
||||
import cors from 'cors';
|
||||
import { promises as fsPromises } from 'fs';
|
||||
import { spawn } from 'child_process';
|
||||
import os from 'os';
|
||||
import pty from 'node-pty';
|
||||
import fetch from 'node-fetch';
|
||||
import mime from 'mime-types';
|
||||
|
||||
const { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } = require('./projects');
|
||||
const { spawnClaude, abortClaudeSession } = require('./claude-cli');
|
||||
const gitRoutes = require('./routes/git');
|
||||
const authRoutes = require('./routes/auth');
|
||||
const { initializeDatabase } = require('./database/db');
|
||||
const { validateApiKey, authenticateToken, authenticateWebSocket } = require('./middleware/auth');
|
||||
import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js';
|
||||
import { spawnClaude, abortClaudeSession } from './claude-cli.js';
|
||||
import gitRoutes from './routes/git.js';
|
||||
import authRoutes from './routes/auth.js';
|
||||
import { initializeDatabase } from './database/db.js';
|
||||
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
||||
|
||||
// File system watcher for projects folder
|
||||
let projectsWatcher = null;
|
||||
const connectedClients = new Set();
|
||||
|
||||
// Setup file system watcher for Claude projects folder using chokidar
|
||||
function setupProjectsWatcher() {
|
||||
const chokidar = require('chokidar');
|
||||
async function setupProjectsWatcher() {
|
||||
const chokidar = (await import('chokidar')).default;
|
||||
const claudeProjectsPath = path.join(process.env.HOME, '.claude', 'projects');
|
||||
|
||||
if (projectsWatcher) {
|
||||
@@ -283,14 +289,14 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
||||
|
||||
console.log('📄 File read request:', projectName, filePath);
|
||||
|
||||
const fs = require('fs').promises;
|
||||
// Using fsPromises from import
|
||||
|
||||
// Security check - ensure the path is safe and absolute
|
||||
if (!filePath || !path.isAbsolute(filePath)) {
|
||||
return res.status(400).json({ error: 'Invalid file path' });
|
||||
}
|
||||
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
const content = await fsPromises.readFile(filePath, 'utf8');
|
||||
res.json({ content, path: filePath });
|
||||
} catch (error) {
|
||||
console.error('Error reading file:', error);
|
||||
@@ -312,8 +318,8 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
|
||||
|
||||
console.log('🖼️ Binary file serve request:', projectName, filePath);
|
||||
|
||||
const fs = require('fs');
|
||||
const mime = require('mime-types');
|
||||
// Using fs from import
|
||||
// Using mime from import
|
||||
|
||||
// Security check - ensure the path is safe and absolute
|
||||
if (!filePath || !path.isAbsolute(filePath)) {
|
||||
@@ -322,7 +328,7 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
|
||||
|
||||
// Check if file exists
|
||||
try {
|
||||
await fs.promises.access(filePath);
|
||||
await fsPromises.access(filePath);
|
||||
} catch (error) {
|
||||
return res.status(404).json({ error: 'File not found' });
|
||||
}
|
||||
@@ -358,7 +364,7 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
||||
|
||||
console.log('💾 File save request:', projectName, filePath);
|
||||
|
||||
const fs = require('fs').promises;
|
||||
// Using fsPromises from import
|
||||
|
||||
// Security check - ensure the path is safe and absolute
|
||||
if (!filePath || !path.isAbsolute(filePath)) {
|
||||
@@ -372,14 +378,14 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
||||
// Create backup of original file
|
||||
try {
|
||||
const backupPath = filePath + '.backup.' + Date.now();
|
||||
await fs.copyFile(filePath, backupPath);
|
||||
await fsPromises.copyFile(filePath, backupPath);
|
||||
console.log('📋 Created backup:', backupPath);
|
||||
} catch (backupError) {
|
||||
console.warn('Could not create backup:', backupError.message);
|
||||
}
|
||||
|
||||
// Write the new content
|
||||
await fs.writeFile(filePath, content, 'utf8');
|
||||
await fsPromises.writeFile(filePath, content, 'utf8');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -401,7 +407,7 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
||||
app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
|
||||
const fs = require('fs').promises;
|
||||
// Using fsPromises from import
|
||||
|
||||
// Use extractProjectDirectory to get the actual project path
|
||||
let actualPath;
|
||||
@@ -415,7 +421,7 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
|
||||
|
||||
// Check if path exists
|
||||
try {
|
||||
await fs.access(actualPath);
|
||||
await fsPromises.access(actualPath);
|
||||
} catch (e) {
|
||||
return res.status(404).json({ error: `Project path not found: ${actualPath}` });
|
||||
}
|
||||
@@ -662,7 +668,7 @@ function handleShellConnection(ws) {
|
||||
// Audio transcription endpoint
|
||||
app.post('/api/transcribe', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const multer = require('multer');
|
||||
const multer = (await import('multer')).default;
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
// Handle multipart form data
|
||||
@@ -682,7 +688,7 @@ app.post('/api/transcribe', authenticateToken, async (req, res) => {
|
||||
|
||||
try {
|
||||
// Create form data for OpenAI
|
||||
const FormData = require('form-data');
|
||||
const FormData = (await import('form-data')).default;
|
||||
const formData = new FormData();
|
||||
formData.append('file', req.file.buffer, {
|
||||
filename: req.file.originalname,
|
||||
@@ -725,7 +731,7 @@ app.post('/api/transcribe', authenticateToken, async (req, res) => {
|
||||
|
||||
// Handle different enhancement modes
|
||||
try {
|
||||
const OpenAI = require('openai');
|
||||
const OpenAI = (await import('openai')).default;
|
||||
const openai = new OpenAI({ apiKey });
|
||||
|
||||
let prompt, systemMessage, temperature = 0.7, maxTokens = 800;
|
||||
@@ -815,11 +821,11 @@ app.get('*', (req, res) => {
|
||||
});
|
||||
|
||||
async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden = true) {
|
||||
const fs = require('fs').promises;
|
||||
// Using fsPromises from import
|
||||
const items = [];
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||
const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
// Debug: log all entries including hidden files
|
||||
@@ -840,7 +846,7 @@ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden =
|
||||
// Recursively get subdirectories but limit depth
|
||||
try {
|
||||
// Check if we can access the directory before trying to read it
|
||||
await fs.access(item.path, fs.constants.R_OK);
|
||||
await fsPromises.access(item.path, fs.constants.R_OK);
|
||||
item.children = await getFileTree(item.path, maxDepth, currentDepth + 1, showHidden);
|
||||
} catch (e) {
|
||||
// Silently skip directories we can't access (permission denied, etc.)
|
||||
@@ -872,13 +878,13 @@ async function startServer() {
|
||||
try {
|
||||
// Initialize authentication database
|
||||
await initializeDatabase();
|
||||
console.log('✅ Database initialized successfully');
|
||||
console.log('✅ Database initialization skipped (testing)');
|
||||
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
server.listen(PORT, '0.0.0.0', async () => {
|
||||
console.log(`Claude Code UI server running on http://0.0.0.0:${PORT}`);
|
||||
|
||||
// Start watching the projects folder for changes
|
||||
setupProjectsWatcher();
|
||||
await setupProjectsWatcher(); // Re-enabled with better-sqlite3
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to start server:', error);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { userDb } = require('../database/db');
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { userDb } from '../database/db.js';
|
||||
|
||||
// Get JWT secret from environment or use default (for development)
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'claude-ui-dev-secret-change-in-production';
|
||||
@@ -31,7 +31,7 @@ const authenticateToken = async (req, res, next) => {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
|
||||
// Verify user still exists and is active
|
||||
const user = await userDb.getUserById(decoded.userId);
|
||||
const user = userDb.getUserById(decoded.userId);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Invalid token. User not found.' });
|
||||
}
|
||||
@@ -71,7 +71,7 @@ const authenticateWebSocket = (token) => {
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
export {
|
||||
validateApiKey,
|
||||
authenticateToken,
|
||||
generateToken,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const readline = require('readline');
|
||||
import { promises as fs } from 'fs';
|
||||
import fsSync from 'fs';
|
||||
import path from 'path';
|
||||
import readline from 'readline';
|
||||
|
||||
// Cache for extracted project directories
|
||||
const projectDirectoryCache = new Map();
|
||||
@@ -91,7 +92,7 @@ async function extractProjectDirectory(projectName) {
|
||||
// Process all JSONL files to collect cwd values
|
||||
for (const file of jsonlFiles) {
|
||||
const jsonlFile = path.join(projectDir, file);
|
||||
const fileStream = require('fs').createReadStream(jsonlFile);
|
||||
const fileStream = fsSync.createReadStream(jsonlFile);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity
|
||||
@@ -325,7 +326,7 @@ async function parseJsonlSessions(filePath) {
|
||||
const sessions = new Map();
|
||||
|
||||
try {
|
||||
const fileStream = require('fs').createReadStream(filePath);
|
||||
const fileStream = fsSync.createReadStream(filePath);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity
|
||||
@@ -409,7 +410,7 @@ async function getSessionMessages(projectName, sessionId) {
|
||||
// Process all JSONL files to find messages for this session
|
||||
for (const file of jsonlFiles) {
|
||||
const jsonlFile = path.join(projectDir, file);
|
||||
const fileStream = require('fs').createReadStream(jsonlFile);
|
||||
const fileStream = fsSync.createReadStream(jsonlFile);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity
|
||||
@@ -601,7 +602,7 @@ async function addProjectManually(projectPath, displayName = null) {
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
export {
|
||||
getProjects,
|
||||
getSessions,
|
||||
getSessionMessages,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcrypt');
|
||||
const { userDb } = require('../database/db');
|
||||
const { generateToken, authenticateToken } = require('../middleware/auth');
|
||||
import express from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { userDb } from '../database/db.js';
|
||||
import { generateToken, authenticateToken } from '../middleware/auth.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -34,7 +34,7 @@ router.post('/register', async (req, res) => {
|
||||
}
|
||||
|
||||
// Check if users already exist (only allow one user)
|
||||
const hasUsers = await userDb.hasUsers();
|
||||
const hasUsers = userDb.hasUsers();
|
||||
if (hasUsers) {
|
||||
return res.status(403).json({ error: 'User already exists. This is a single-user system.' });
|
||||
}
|
||||
@@ -44,13 +44,13 @@ router.post('/register', async (req, res) => {
|
||||
const passwordHash = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
// Create user
|
||||
const user = await userDb.createUser(username, passwordHash);
|
||||
const user = userDb.createUser(username, passwordHash);
|
||||
|
||||
// Generate token
|
||||
const token = generateToken(user);
|
||||
|
||||
// Update last login
|
||||
await userDb.updateLastLogin(user.id);
|
||||
userDb.updateLastLogin(user.id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -79,7 +79,7 @@ router.post('/login', async (req, res) => {
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
const user = await userDb.getUserByUsername(username);
|
||||
const user = userDb.getUserByUsername(username);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Invalid username or password' });
|
||||
}
|
||||
@@ -94,7 +94,7 @@ router.post('/login', async (req, res) => {
|
||||
const token = generateToken(user);
|
||||
|
||||
// Update last login
|
||||
await userDb.updateLastLogin(user.id);
|
||||
userDb.updateLastLogin(user.id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -122,4 +122,4 @@ router.post('/logout', authenticateToken, (req, res) => {
|
||||
res.json({ success: true, message: 'Logged out successfully' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
@@ -1,9 +1,9 @@
|
||||
const express = require('express');
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const { extractProjectDirectory } = require('../projects');
|
||||
import express from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import { extractProjectDirectory } from '../projects.js';
|
||||
|
||||
const router = express.Router();
|
||||
const execAsync = promisify(exec);
|
||||
@@ -420,4 +420,4 @@ function generateSimpleCommitMessage(files, diff) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
@@ -585,6 +585,7 @@ function AppContent() {
|
||||
onShowSettings={() => setShowToolsSettings(true)}
|
||||
autoExpandTools={autoExpandTools}
|
||||
showRawParameters={showRawParameters}
|
||||
autoScrollToBottom={autoScrollToBottom}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -903,6 +903,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
const [sessionMessages, setSessionMessages] = useState([]);
|
||||
const [isLoadingSessionMessages, setIsLoadingSessionMessages] = useState(false);
|
||||
const [isSystemSessionChange, setIsSystemSessionChange] = useState(false);
|
||||
const [permissionMode, setPermissionMode] = useState('default');
|
||||
const messagesEndRef = useRef(null);
|
||||
const textareaRef = useRef(null);
|
||||
const scrollContainerRef = useRef(null);
|
||||
@@ -1681,7 +1682,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
cwd: selectedProject.fullPath,
|
||||
sessionId: currentSessionId,
|
||||
resume: !!currentSessionId,
|
||||
toolsSettings: toolsSettings
|
||||
toolsSettings: toolsSettings,
|
||||
permissionMode: permissionMode
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1726,6 +1728,16 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Tab key for mode switching (only when file dropdown is not showing)
|
||||
if (e.key === 'Tab' && !showFileDropdown) {
|
||||
e.preventDefault();
|
||||
const modes = ['default', 'acceptEdits', 'bypassPermissions', 'plan'];
|
||||
const currentIndex = modes.indexOf(permissionMode);
|
||||
const nextIndex = (currentIndex + 1) % modes.length;
|
||||
setPermissionMode(modes[nextIndex]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Enter key: Ctrl+Enter (Cmd+Enter on Mac) sends, Shift+Enter creates new line
|
||||
if (e.key === 'Enter') {
|
||||
if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||
@@ -1790,6 +1802,13 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
}
|
||||
};
|
||||
|
||||
const handleModeSwitch = () => {
|
||||
const modes = ['default', 'acceptEdits', 'bypassPermissions', 'plan'];
|
||||
const currentIndex = modes.indexOf(permissionMode);
|
||||
const nextIndex = (currentIndex + 1) % modes.length;
|
||||
setPermissionMode(modes[nextIndex]);
|
||||
};
|
||||
|
||||
// Don't render if no project is selected
|
||||
if (!selectedProject) {
|
||||
return (
|
||||
@@ -1888,18 +1907,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Floating scroll to bottom button - positioned outside scrollable container */}
|
||||
{isUserScrolledUp && chatMessages.length > 0 && (
|
||||
<button
|
||||
onClick={scrollToBottom}
|
||||
className="fixed bottom-20 sm:bottom-24 right-4 sm:right-6 w-12 h-12 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all duration-200 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:ring-offset-gray-800 z-50"
|
||||
title="Scroll to bottom"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Input Area - Fixed Bottom */}
|
||||
<div className={`p-2 sm:p-4 md:p-6 flex-shrink-0 ${
|
||||
@@ -1912,6 +1919,57 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
onAbort={handleAbortSession}
|
||||
/>
|
||||
|
||||
{/* Permission Mode Selector with scroll to bottom button - Above input, clickable for mobile */}
|
||||
<div className="max-w-4xl mx-auto mb-3">
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleModeSwitch}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-all duration-200 ${
|
||||
permissionMode === 'default'
|
||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||
: permissionMode === 'acceptEdits'
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 border-green-300 dark:border-green-600 hover:bg-green-100 dark:hover:bg-green-900/30'
|
||||
: permissionMode === 'bypassPermissions'
|
||||
? 'bg-orange-50 dark:bg-orange-900/20 text-orange-700 dark:text-orange-300 border-orange-300 dark:border-orange-600 hover:bg-orange-100 dark:hover:bg-orange-900/30'
|
||||
: 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900/30'
|
||||
}`}
|
||||
title="Click to change permission mode (or press Tab in input)"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${
|
||||
permissionMode === 'default'
|
||||
? 'bg-gray-500'
|
||||
: permissionMode === 'acceptEdits'
|
||||
? 'bg-green-500'
|
||||
: permissionMode === 'bypassPermissions'
|
||||
? 'bg-orange-500'
|
||||
: 'bg-blue-500'
|
||||
}`} />
|
||||
<span>
|
||||
{permissionMode === 'default' && 'Default Mode'}
|
||||
{permissionMode === 'acceptEdits' && 'Accept Edits'}
|
||||
{permissionMode === 'bypassPermissions' && 'Bypass Permissions'}
|
||||
{permissionMode === 'plan' && 'Plan Mode'}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Scroll to bottom button - positioned next to mode indicator */}
|
||||
{isUserScrolledUp && chatMessages.length > 0 && (
|
||||
<button
|
||||
onClick={scrollToBottom}
|
||||
className="w-8 h-8 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all duration-200 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:ring-offset-gray-800"
|
||||
title="Scroll to bottom"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="relative max-w-4xl mx-auto">
|
||||
<div className={`relative bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-600 focus-within:ring-2 focus-within:ring-blue-500 dark:focus-within:ring-blue-500 focus-within:border-blue-500 transition-all duration-200 ${isTextareaExpanded ? 'chat-input-expanded' : ''}`}>
|
||||
<textarea
|
||||
@@ -2041,12 +2099,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
||||
</div>
|
||||
{/* Hint text */}
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 text-center mt-2 hidden sm:block">
|
||||
Press Enter to send • Shift+Enter for new line • @ to reference files
|
||||
Press Enter to send • Shift+Enter for new line • Tab to change modes • @ to reference files
|
||||
</div>
|
||||
<div className={`text-xs text-gray-500 dark:text-gray-400 text-center mt-2 sm:hidden transition-opacity duration-200 ${
|
||||
isInputFocused ? 'opacity-100' : 'opacity-0'
|
||||
}`}>
|
||||
Enter to send • @ for files
|
||||
Enter to send • Tab for modes • @ for files
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -66,9 +66,13 @@ function Sidebar({
|
||||
const [editingSessionName, setEditingSessionName] = useState('');
|
||||
const [generatingSummary, setGeneratingSummary] = useState({});
|
||||
|
||||
// Touch handler to prevent double-tap issues on iPad
|
||||
// Touch handler to prevent double-tap issues on iPad (only for buttons, not scroll areas)
|
||||
const handleTouchClick = (callback) => {
|
||||
return (e) => {
|
||||
// Only prevent default for buttons/clickable elements, not scrollable areas
|
||||
if (e.target.closest('.overflow-y-auto') || e.target.closest('[data-scroll-container]')) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
callback();
|
||||
|
||||
@@ -7,7 +7,13 @@ const ScrollArea = React.forwardRef(({ className, children, ...props }, ref) =>
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="h-full w-full rounded-[inherit] overflow-auto">
|
||||
<div
|
||||
className="h-full w-full rounded-[inherit] overflow-auto"
|
||||
style={{
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
touchAction: 'pan-y'
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -436,6 +436,12 @@
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Allow vertical scrolling in scroll containers */
|
||||
.overflow-y-auto, [data-scroll-container] {
|
||||
touch-action: pan-y;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Preserve checkbox visibility */
|
||||
input[type="checkbox"] {
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
|
||||
|
||||
Reference in New Issue
Block a user