Files
SuperCharged-Claude-Code-Up…/routes/session-routes.js
uroma 55aafbae9a Fix project isolation: Make loadChatHistory respect active project sessions
- 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>
2026-01-22 14:43:05 +00:00

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;