- Modified loadChatHistory() to check for active project before fetching all sessions - When active project exists, use project.sessions instead of fetching from API - Added detailed console logging to debug session filtering - This prevents ALL sessions from appearing in every project's sidebar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
508 lines
13 KiB
JavaScript
508 lines
13 KiB
JavaScript
/**
|
|
* Session Routes
|
|
*
|
|
* RESTful API endpoints for managing Claude Code sessions.
|
|
* All routes are scoped to a specific session via :sessionId parameter.
|
|
*/
|
|
|
|
const express = require('express');
|
|
const eventBus = require('../services/event-bus');
|
|
const { validateSessionId, validateCommand, validateResponse, validateOperations } = require('../middleware/validation');
|
|
|
|
const router = express.Router();
|
|
|
|
// Middleware to ensure services are available
|
|
router.use((req, res, next) => {
|
|
if (!req.app.locals.claudeService) {
|
|
return res.status(500).json({
|
|
error: 'Service not available',
|
|
message: 'claudeService not initialized'
|
|
});
|
|
}
|
|
next();
|
|
});
|
|
|
|
/**
|
|
* Send command/prompt to session
|
|
* POST /api/session/:sessionId/prompt
|
|
*
|
|
* Sends a command or prompt to the Claude Code session.
|
|
* The response will be streamed via SSE to connected clients.
|
|
*
|
|
* Request:
|
|
* {
|
|
* "command": "ls -la",
|
|
* "context": { "optional": "context data" }
|
|
* }
|
|
*
|
|
* Response:
|
|
* {
|
|
* "success": true,
|
|
* "sessionId": "session-123",
|
|
* "message": "Command sent",
|
|
* "timestamp": 1234567890
|
|
* }
|
|
*/
|
|
router.post('/session/:sessionId/prompt', validateSessionId, validateCommand, async (req, res) => {
|
|
const { sessionId } = req.params;
|
|
const { command, context } = req.body;
|
|
|
|
console.log(`[SessionRoutes] Sending command to session ${sessionId}: ${command.substring(0, 100)}...`);
|
|
|
|
try {
|
|
// Send command to Claude service
|
|
await req.app.locals.claudeService.sendCommand(sessionId, command);
|
|
|
|
// Emit command-sent event
|
|
eventBus.emit('command-sent', {
|
|
sessionId,
|
|
command: command.substring(0, 100) + (command.length > 100 ? '...' : ''),
|
|
timestamp: Date.now()
|
|
});
|
|
|
|
// Return immediately (actual response comes via SSE)
|
|
res.json({
|
|
success: true,
|
|
sessionId,
|
|
message: 'Command sent',
|
|
timestamp: Date.now()
|
|
});
|
|
|
|
console.log(`[SessionRoutes] Command sent successfully to session ${sessionId}`);
|
|
} catch (error) {
|
|
console.error(`[SessionRoutes] Error sending command to session ${sessionId}:`, error);
|
|
|
|
// Emit error event
|
|
eventBus.emit('session-error', {
|
|
sessionId,
|
|
error: error.message,
|
|
code: 'COMMAND_SEND_ERROR',
|
|
recoverable: false
|
|
});
|
|
|
|
res.status(500).json({
|
|
error: 'Failed to send command',
|
|
message: error.message,
|
|
sessionId
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Get session status
|
|
* GET /api/session/:sessionId/status
|
|
*
|
|
* Returns current status and information about the session.
|
|
*
|
|
* Response:
|
|
* {
|
|
* "sessionId": "session-123",
|
|
* "status": "running",
|
|
* "mode": "full",
|
|
* "createdAt": "2025-01-21T10:00:00.000Z",
|
|
* "lastActivity": "2025-01-21T10:05:00.000Z",
|
|
* "pid": 12345,
|
|
* "uptime": 300000
|
|
* }
|
|
*/
|
|
router.get('/session/:sessionId/status', validateSessionId, (req, res) => {
|
|
const { sessionId } = req.params;
|
|
|
|
try {
|
|
const session = req.app.locals.claudeService.getSession(sessionId);
|
|
|
|
if (!session) {
|
|
return res.status(404).json({
|
|
error: 'Session not found',
|
|
sessionId
|
|
});
|
|
}
|
|
|
|
const uptime = Date.now() - new Date(session.createdAt).getTime();
|
|
|
|
res.json({
|
|
sessionId: session.id,
|
|
status: session.status || 'unknown',
|
|
mode: session.mode || 'unknown',
|
|
createdAt: session.createdAt,
|
|
lastActivity: session.lastActivity || session.createdAt,
|
|
pid: process.pid,
|
|
uptime
|
|
});
|
|
|
|
console.log(`[SessionRoutes] Status retrieved for session ${sessionId}: ${session.status}`);
|
|
} catch (error) {
|
|
console.error(`[SessionRoutes] Error getting status for session ${sessionId}:`, error);
|
|
res.status(500).json({
|
|
error: 'Failed to get session status',
|
|
message: error.message,
|
|
sessionId
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Get session context files
|
|
* GET /api/session/:sessionId/context
|
|
*
|
|
* Returns the files currently in the session's context.
|
|
*
|
|
* Response:
|
|
* {
|
|
* "sessionId": "session-123",
|
|
* "context": [
|
|
* { "path": "/path/to/file.js", "content": "..." }
|
|
* ],
|
|
* "timestamp": 1234567890
|
|
* }
|
|
*/
|
|
router.get('/session/:sessionId/context', validateSessionId, async (req, res) => {
|
|
const { sessionId } = req.params;
|
|
|
|
try {
|
|
// Assuming claudeService has a getContext method
|
|
// If not, this will need to be implemented
|
|
const context = await req.app.locals.claudeService.getSessionContext?.(sessionId) || { files: [] };
|
|
|
|
res.json({
|
|
sessionId,
|
|
context: context.files,
|
|
timestamp: Date.now()
|
|
});
|
|
|
|
console.log(`[SessionRoutes] Context retrieved for session ${sessionId}: ${context.files.length} files`);
|
|
} catch (error) {
|
|
console.error(`[SessionRoutes] Error getting context for session ${sessionId}:`, error);
|
|
res.status(500).json({
|
|
error: 'Failed to get session context',
|
|
message: error.message,
|
|
sessionId
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Preview operations for session
|
|
* POST /api/session/:sessionId/operations/preview
|
|
*
|
|
* Parses a Claude response to detect file operations.
|
|
*
|
|
* Request:
|
|
* {
|
|
* "response": "I'll create a new file..."
|
|
* }
|
|
*
|
|
* Response:
|
|
* {
|
|
* "success": true,
|
|
* "operations": [
|
|
* { "type": "write", "path": "/path/to/file.js", "content": "..." }
|
|
* ],
|
|
* "count": 1
|
|
* }
|
|
*/
|
|
router.post('/session/:sessionId/operations/preview', validateSessionId, validateResponse, async (req, res) => {
|
|
const { sessionId } = req.params;
|
|
const { response } = req.body;
|
|
|
|
console.log(`[SessionRoutes] Previewing operations for session ${sessionId}`);
|
|
|
|
try {
|
|
const operations = await req.app.locals.claudeService.previewOperations(sessionId, response);
|
|
|
|
// Emit operations-detected event
|
|
eventBus.emit('operations-detected', {
|
|
sessionId,
|
|
operations,
|
|
response: response.substring(0, 200) + '...'
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
operations,
|
|
count: operations.length
|
|
});
|
|
|
|
console.log(`[SessionRoutes] Previewed ${operations.length} operations for session ${sessionId}`);
|
|
} catch (error) {
|
|
console.error(`[SessionRoutes] Error previewing operations for session ${sessionId}:`, error);
|
|
|
|
// Emit error event
|
|
eventBus.emit('session-error', {
|
|
sessionId,
|
|
error: error.message,
|
|
code: 'PREVIEW_ERROR',
|
|
recoverable: true
|
|
});
|
|
|
|
res.status(500).json({
|
|
error: 'Failed to preview operations',
|
|
message: error.message,
|
|
sessionId
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Execute operations for session
|
|
* POST /api/session/:sessionId/operations/execute
|
|
*
|
|
* Executes confirmed file operations.
|
|
*
|
|
* Request:
|
|
* {
|
|
* "operations": [
|
|
* { "type": "write", "path": "/path/to/file.js", "content": "..." }
|
|
* ]
|
|
* }
|
|
*
|
|
* Response:
|
|
* {
|
|
* "success": true,
|
|
* "results": [
|
|
* { "success": true, "path": "/path/to/file.js", "operation": "write" }
|
|
* ],
|
|
* "executed": 1
|
|
* }
|
|
*/
|
|
router.post('/session/:sessionId/operations/execute', validateSessionId, validateOperations, async (req, res) => {
|
|
const { sessionId } = req.params;
|
|
const { operations } = req.body;
|
|
|
|
console.log(`[SessionRoutes] Executing ${operations.length} operations for session ${sessionId}`);
|
|
|
|
try {
|
|
const results = await req.app.locals.claudeService.executeOperations(sessionId, operations);
|
|
|
|
// Emit operations-executed event
|
|
eventBus.emit('operations-executed', {
|
|
sessionId,
|
|
results,
|
|
count: results.length
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
results,
|
|
executed: results.length
|
|
});
|
|
|
|
console.log(`[SessionRoutes] Executed ${results.length} operations for session ${sessionId}`);
|
|
} catch (error) {
|
|
console.error(`[SessionRoutes] Error executing operations for session ${sessionId}:`, error);
|
|
|
|
// Emit operations-error event
|
|
eventBus.emit('operations-error', {
|
|
sessionId,
|
|
error: error.message,
|
|
operations
|
|
});
|
|
|
|
res.status(500).json({
|
|
error: 'Failed to execute operations',
|
|
message: error.message,
|
|
sessionId
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Delete/terminate session
|
|
* DELETE /api/session/:sessionId
|
|
*
|
|
* Terminates the Claude Code session and cleans up resources.
|
|
*
|
|
* Response:
|
|
* {
|
|
* "success": true,
|
|
* "message": "Session deleted",
|
|
* "sessionId": "session-123"
|
|
* }
|
|
*/
|
|
router.delete('/session/:sessionId', validateSessionId, async (req, res) => {
|
|
const { sessionId } = req.params;
|
|
|
|
console.log(`[SessionRoutes] Deleting session ${sessionId}`);
|
|
|
|
try {
|
|
await req.app.locals.claudeService.deleteSession(sessionId);
|
|
|
|
// Emit session-deleted event
|
|
eventBus.emit('session-deleted', {
|
|
sessionId,
|
|
timestamp: Date.now()
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Session deleted',
|
|
sessionId
|
|
});
|
|
|
|
console.log(`[SessionRoutes] Session ${sessionId} deleted successfully`);
|
|
} catch (error) {
|
|
console.error(`[SessionRoutes] Error deleting session ${sessionId}:`, error);
|
|
res.status(500).json({
|
|
error: 'Failed to delete session',
|
|
message: error.message,
|
|
sessionId
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Duplicate session
|
|
* POST /api/session/:sessionId/duplicate
|
|
*
|
|
* Creates a copy of the session with a new ID.
|
|
*
|
|
* Request:
|
|
* {
|
|
* "name": "Copy of session-123" // optional
|
|
* }
|
|
*
|
|
* Response:
|
|
* {
|
|
* "success": true,
|
|
* "newSessionId": "session-456",
|
|
* "originalSessionId": "session-123"
|
|
* }
|
|
*/
|
|
router.post('/session/:sessionId/duplicate', validateSessionId, async (req, res) => {
|
|
const { sessionId } = req.params;
|
|
const { name } = req.body;
|
|
|
|
console.log(`[SessionRoutes] Duplicating session ${sessionId}`);
|
|
|
|
try {
|
|
const result = await req.app.locals.claudeService.duplicateSession(sessionId, name);
|
|
|
|
// Emit session-created event
|
|
eventBus.emit('session-created', {
|
|
sessionId: result.newSessionId,
|
|
sourceSessionId: sessionId,
|
|
mode: 'duplicate'
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
newSessionId: result.newSessionId,
|
|
originalSessionId: sessionId,
|
|
name: result.name
|
|
});
|
|
|
|
console.log(`[SessionRoutes] Session ${sessionId} duplicated as ${result.newSessionId}`);
|
|
} catch (error) {
|
|
console.error(`[SessionRoutes] Error duplicating session ${sessionId}:`, error);
|
|
res.status(500).json({
|
|
error: 'Failed to duplicate session',
|
|
message: error.message,
|
|
sessionId
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Fork session
|
|
* POST /api/session/:sessionId/fork
|
|
*
|
|
* Creates a new session with the same context but starts fresh.
|
|
*
|
|
* Response:
|
|
* {
|
|
* "success": true,
|
|
* "newSessionId": "session-789",
|
|
* "forkedFrom": "session-123"
|
|
* }
|
|
*/
|
|
router.post('/session/:sessionId/fork', validateSessionId, async (req, res) => {
|
|
const { sessionId } = req.params;
|
|
|
|
console.log(`[SessionRoutes] Forking session ${sessionId}`);
|
|
|
|
try {
|
|
const result = await req.app.locals.claudeService.forkSession(sessionId);
|
|
|
|
// Emit session-created event
|
|
eventBus.emit('session-created', {
|
|
sessionId: result.newSessionId,
|
|
sourceSessionId: sessionId,
|
|
mode: 'fork'
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
newSessionId: result.newSessionId,
|
|
forkedFrom: sessionId
|
|
});
|
|
|
|
console.log(`[SessionRoutes] Session ${sessionId} forked as ${result.newSessionId}`);
|
|
} catch (error) {
|
|
console.error(`[SessionRoutes] Error forking session ${sessionId}:`, error);
|
|
res.status(500).json({
|
|
error: 'Failed to fork session',
|
|
message: error.message,
|
|
sessionId
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Move session to project
|
|
* POST /api/session/:sessionId/move
|
|
*
|
|
* Moves a session to a different project.
|
|
*
|
|
* Request:
|
|
* {
|
|
* "projectId": "project-456"
|
|
* }
|
|
*
|
|
* Response:
|
|
* {
|
|
* "success": true,
|
|
* "sessionId": "session-123",
|
|
* "projectId": "project-456"
|
|
* }
|
|
*/
|
|
router.post('/session/:sessionId/move', validateSessionId, async (req, res) => {
|
|
const { sessionId } = req.params;
|
|
const { projectId } = req.body;
|
|
|
|
if (!projectId) {
|
|
return res.status(400).json({
|
|
error: 'Project ID is required'
|
|
});
|
|
}
|
|
|
|
console.log(`[SessionRoutes] Moving session ${sessionId} to project ${projectId}`);
|
|
|
|
try {
|
|
await req.app.locals.claudeService.moveSession(sessionId, projectId);
|
|
|
|
// Emit session-moved event
|
|
eventBus.emit('session-moved', {
|
|
sessionId,
|
|
projectId,
|
|
timestamp: Date.now()
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
sessionId,
|
|
projectId
|
|
});
|
|
|
|
console.log(`[SessionRoutes] Session ${sessionId} moved to project ${projectId}`);
|
|
} catch (error) {
|
|
console.error(`[SessionRoutes] Error moving session ${sessionId}:`, error);
|
|
res.status(500).json({
|
|
error: 'Failed to move session',
|
|
message: error.message,
|
|
sessionId
|
|
});
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|