mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-13 05:39:41 +00:00
feat: Enhance session management and tool settings for Claude and Cursor
- Updated ClaudeStatus component to accept a provider prop for better flexibility. - Added CursorLogo component for displaying cursor sessions. - Modified MainContent to conditionally display session names based on provider. - Updated Shell component to show session names and summaries based on provider. - Enhanced Sidebar to handle both Claude and Cursor sessions, including sorting and displaying session icons. - Introduced new ToolsSettings functionality to manage tools for both Claude and Cursor, including allowed and disallowed commands. - Implemented fetching and saving of Cursor-specific settings and commands. - Added UI elements for managing Cursor tools, including permission settings and command lists.
This commit is contained in:
250
server/cursor-cli.js
Normal file
250
server/cursor-cli.js
Normal file
@@ -0,0 +1,250 @@
|
||||
import { spawn } from 'child_process';
|
||||
import crossSpawn from 'cross-spawn';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
// Use cross-spawn on Windows for better command execution
|
||||
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
||||
|
||||
let activeCursorProcesses = new Map(); // Track active processes by session ID
|
||||
|
||||
async function spawnCursor(command, options = {}, ws) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { sessionId, projectPath, cwd, resume, toolsSettings, skipPermissions, model, images } = options;
|
||||
let capturedSessionId = sessionId; // Track session ID throughout the process
|
||||
let sessionCreatedSent = false; // Track if we've already sent session-created event
|
||||
let messageBuffer = ''; // Buffer for accumulating assistant messages
|
||||
|
||||
// Use tools settings passed from frontend, or defaults
|
||||
const settings = toolsSettings || {
|
||||
allowedShellCommands: [],
|
||||
skipPermissions: false
|
||||
};
|
||||
|
||||
// Build Cursor CLI command
|
||||
const args = [];
|
||||
|
||||
// Build flags allowing both resume and prompt together (reply in existing session)
|
||||
if (resume && sessionId) {
|
||||
// Resume existing session
|
||||
args.push('--resume=' + sessionId);
|
||||
}
|
||||
|
||||
if (command && command.trim()) {
|
||||
// Provide a prompt (works for both new and resumed sessions)
|
||||
args.push('-p', command);
|
||||
|
||||
// Add model flag if specified (only meaningful for new sessions; harmless on resume)
|
||||
if (!resume && model) {
|
||||
args.push('--model', model);
|
||||
}
|
||||
|
||||
// Request streaming JSON when we are providing a prompt
|
||||
args.push('--output-format', 'stream-json');
|
||||
}
|
||||
|
||||
// Add skip permissions flag if enabled
|
||||
if (skipPermissions || settings.skipPermissions) {
|
||||
args.push('-f');
|
||||
console.log('⚠️ Using -f flag (skip permissions)');
|
||||
}
|
||||
|
||||
// Use cwd (actual project directory) instead of projectPath
|
||||
const workingDir = cwd || projectPath || process.cwd();
|
||||
|
||||
console.log('Spawning Cursor CLI:', 'cursor-agent', args.join(' '));
|
||||
console.log('Working directory:', workingDir);
|
||||
console.log('Session info - Input sessionId:', sessionId, 'Resume:', resume);
|
||||
|
||||
const cursorProcess = spawnFunction('cursor-agent', args, {
|
||||
cwd: workingDir,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: { ...process.env } // Inherit all environment variables
|
||||
});
|
||||
|
||||
// Store process reference for potential abort
|
||||
const processKey = capturedSessionId || Date.now().toString();
|
||||
activeCursorProcesses.set(processKey, cursorProcess);
|
||||
|
||||
// Handle stdout (streaming JSON responses)
|
||||
cursorProcess.stdout.on('data', (data) => {
|
||||
const rawOutput = data.toString();
|
||||
console.log('📤 Cursor CLI stdout:', rawOutput);
|
||||
|
||||
const lines = rawOutput.split('\n').filter(line => line.trim());
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const response = JSON.parse(line);
|
||||
console.log('📄 Parsed JSON response:', response);
|
||||
|
||||
// Handle different message types
|
||||
switch (response.type) {
|
||||
case 'system':
|
||||
if (response.subtype === 'init') {
|
||||
// Capture session ID
|
||||
if (response.session_id && !capturedSessionId) {
|
||||
capturedSessionId = response.session_id;
|
||||
console.log('📝 Captured session ID:', capturedSessionId);
|
||||
|
||||
// Update process key with captured session ID
|
||||
if (processKey !== capturedSessionId) {
|
||||
activeCursorProcesses.delete(processKey);
|
||||
activeCursorProcesses.set(capturedSessionId, cursorProcess);
|
||||
}
|
||||
|
||||
// Send session-created event only once for new sessions
|
||||
if (!sessionId && !sessionCreatedSent) {
|
||||
sessionCreatedSent = true;
|
||||
ws.send(JSON.stringify({
|
||||
type: 'session-created',
|
||||
sessionId: capturedSessionId,
|
||||
model: response.model,
|
||||
cwd: response.cwd
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Send system info to frontend
|
||||
ws.send(JSON.stringify({
|
||||
type: 'cursor-system',
|
||||
data: response
|
||||
}));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
// Forward user message
|
||||
ws.send(JSON.stringify({
|
||||
type: 'cursor-user',
|
||||
data: response
|
||||
}));
|
||||
break;
|
||||
|
||||
case 'assistant':
|
||||
// Accumulate assistant message chunks
|
||||
if (response.message && response.message.content && response.message.content.length > 0) {
|
||||
const textContent = response.message.content[0].text;
|
||||
messageBuffer += textContent;
|
||||
|
||||
// Send as Claude-compatible format for frontend
|
||||
ws.send(JSON.stringify({
|
||||
type: 'claude-response',
|
||||
data: {
|
||||
type: 'content_block_delta',
|
||||
delta: {
|
||||
type: 'text_delta',
|
||||
text: textContent
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'result':
|
||||
// Session complete
|
||||
console.log('Cursor session result:', response);
|
||||
|
||||
// Send final message if we have buffered content
|
||||
if (messageBuffer) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'claude-response',
|
||||
data: {
|
||||
type: 'content_block_stop'
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Send completion event
|
||||
ws.send(JSON.stringify({
|
||||
type: 'cursor-result',
|
||||
data: response,
|
||||
success: response.subtype === 'success'
|
||||
}));
|
||||
break;
|
||||
|
||||
default:
|
||||
// Forward any other message types
|
||||
ws.send(JSON.stringify({
|
||||
type: 'cursor-response',
|
||||
data: response
|
||||
}));
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.log('📄 Non-JSON response:', line);
|
||||
// If not JSON, send as raw text
|
||||
ws.send(JSON.stringify({
|
||||
type: 'cursor-output',
|
||||
data: line
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle stderr
|
||||
cursorProcess.stderr.on('data', (data) => {
|
||||
console.error('Cursor CLI stderr:', data.toString());
|
||||
ws.send(JSON.stringify({
|
||||
type: 'cursor-error',
|
||||
error: data.toString()
|
||||
}));
|
||||
});
|
||||
|
||||
// Handle process completion
|
||||
cursorProcess.on('close', async (code) => {
|
||||
console.log(`Cursor CLI process exited with code ${code}`);
|
||||
|
||||
// Clean up process reference
|
||||
const finalSessionId = capturedSessionId || sessionId || processKey;
|
||||
activeCursorProcesses.delete(finalSessionId);
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'claude-complete',
|
||||
exitCode: code,
|
||||
isNewSession: !sessionId && !!command // Flag to indicate this was a new session
|
||||
}));
|
||||
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Cursor CLI exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
// Handle process errors
|
||||
cursorProcess.on('error', (error) => {
|
||||
console.error('Cursor CLI process error:', error);
|
||||
|
||||
// Clean up process reference on error
|
||||
const finalSessionId = capturedSessionId || sessionId || processKey;
|
||||
activeCursorProcesses.delete(finalSessionId);
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'cursor-error',
|
||||
error: error.message
|
||||
}));
|
||||
|
||||
reject(error);
|
||||
});
|
||||
|
||||
// Close stdin since Cursor doesn't need interactive input
|
||||
cursorProcess.stdin.end();
|
||||
});
|
||||
}
|
||||
|
||||
function abortCursorSession(sessionId) {
|
||||
const process = activeCursorProcesses.get(sessionId);
|
||||
if (process) {
|
||||
console.log(`🛑 Aborting Cursor session: ${sessionId}`);
|
||||
process.kill('SIGTERM');
|
||||
activeCursorProcesses.delete(sessionId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export {
|
||||
spawnCursor,
|
||||
abortCursorSession
|
||||
};
|
||||
Reference in New Issue
Block a user