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>
This commit is contained in:
507
routes/session-routes.js
Normal file
507
routes/session-routes.js
Normal file
@@ -0,0 +1,507 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user