Files
SuperCharged-Claude-Code-Up…/public/claude-ide/sse-client.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

319 lines
11 KiB
JavaScript

/**
* SSE Client - Server-Sent Events for real-time session events
*
* Replaces WebSocket for receiving session events from the server.
* Commands are sent via REST API instead.
*/
class SSEClient {
constructor() {
this.eventSource = null;
this.currentSessionId = null;
this.eventHandlers = new Map();
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.reconnectDelay = 1000;
this.reconnectBackoff = 1.5;
}
/**
* Connect to SSE endpoint for a session
* @param {string} sessionId - Session ID to connect to
*/
connect(sessionId) {
if (this.eventSource) {
this.disconnect();
}
this.currentSessionId = sessionId;
// Use /claude/api prefix for production nginx routing
const url = `/claude/api/session/${encodeURIComponent(sessionId)}/events`;
console.log(`[SSEClient] Connecting to: ${url}`);
if (window.traceExecution) {
window.traceExecution('sse-client', 'connect START', { sessionId, url });
}
this.eventSource = new EventSource(url);
// Connection opened
this.eventSource.onopen = () => {
console.log(`[SSEClient] Connected to session ${sessionId}`);
this.reconnectAttempts = 0;
this.reconnectDelay = 1000;
this.emit('connected', { sessionId });
if (window.traceExecution) {
window.traceExecution('sse-client', 'connected SUCCESS', { sessionId, readyState: this.eventSource.readyState });
}
// Auto-report successful connection
if (window.AutoFixLogger) {
window.AutoFixLogger.success('SSE connected', { sessionId });
}
};
// Connection error
this.eventSource.onerror = (error) => {
console.error(`[SSEClient] Connection error for session ${sessionId}:`, error);
if (window.traceExecution) {
window.traceExecution('sse-client', 'connection ERROR', {
sessionId,
readyState: this.eventSource.readyState,
error: error?.toString()
});
}
// Auto-report connection error
if (window.AutoFixLogger) {
window.AutoFixLogger.error('SSE connection failed', {
sessionId,
readyState: this.eventSource.readyState,
reconnectAttempts: this.reconnectAttempts
});
}
if (this.eventSource.readyState === EventSource.CLOSED) {
this.emit('error', { sessionId, error: 'Connection closed' });
this.handleReconnect();
}
};
// Generic message handler (fallback) - only for unnamed events
this.eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('[SSEClient] Message:', data.type, data);
this.routeEvent(data);
} catch (err) {
console.error('[SSEClient] Error parsing message:', err, event.data);
if (window.traceExecution) {
window.traceExecution('sse-client', 'message parse ERROR', {
error: err?.toString(),
rawData: event.data?.substring(0, 200)
});
}
}
};
// Register specific event listeners
this.registerEventListeners();
}
/**
* Register specific event type listeners
*/
registerEventListeners() {
const eventTypes = [
'connected',
'session-output',
'session-error',
'session-status',
'operations-detected',
'operations-executed',
'operations-error',
'approval-request',
'approval-confirmed',
'approval-expired',
'command-sent',
'terminal-created',
'terminal-closed'
];
eventTypes.forEach(type => {
this.eventSource.addEventListener(type, (event) => {
try {
const data = JSON.parse(event.data);
// Use _eventType for routing to preserve the server's event type
// The 'type' in data is the event subtype (e.g., 'stdout', 'stderr')
this.routeEvent({ _eventType: type, ...data });
} catch (err) {
console.error(`[SSEClient] Error parsing ${type} event:`, err);
}
});
});
}
/**
* Route event to appropriate handler
* @param {Object} event - Event object with type and data
*/
routeEvent(event) {
// Use _eventType for routing (set by server), fall back to type
const eventType = event._eventType || event.type;
if (window.traceExecution) {
window.traceExecution('sse-client', 'routeEvent', {
eventType,
hasContent: !!event.content,
contentLength: event.content?.length || 0
});
}
// Call registered handlers for this event type
const handlers = this.eventHandlers.get(eventType) || [];
handlers.forEach(handler => {
try {
handler(event);
} catch (err) {
console.error(`[SSEClient] Error in handler for ${eventType}:`, err);
if (window.traceExecution) {
window.traceExecution('sse-client', 'handler ERROR', {
eventType,
error: err?.toString()
});
}
}
});
// Call wildcard handlers
const wildcardHandlers = this.eventHandlers.get('*') || [];
wildcardHandlers.forEach(handler => {
try {
handler(event);
} catch (err) {
console.error('[SSEClient] Error in wildcard handler:', err);
}
});
}
/**
* Handle automatic reconnection with exponential backoff
*/
handleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('[SSEClient] Max reconnection attempts reached');
this.emit('disconnected', { sessionId: this.currentSessionId });
return;
}
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(this.reconnectBackoff, this.reconnectAttempts - 1);
console.log(`[SSEClient] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
setTimeout(() => {
if (this.currentSessionId) {
this.connect(this.currentSessionId);
}
}, delay);
}
/**
* Register event handler
* @param {string} eventType - Event type or '*' for wildcard
* @param {Function} handler - Event handler function
* @returns {Function} Unsubscribe function
*/
on(eventType, handler) {
if (!this.eventHandlers.has(eventType)) {
this.eventHandlers.set(eventType, []);
}
const handlers = this.eventHandlers.get(eventType);
// Prevent duplicate handler registration
if (handlers.includes(handler)) {
console.log(`[SSEClient] Handler already registered for: ${eventType}, skipping`);
return () => {}; // Return no-op unsubscribe function
}
handlers.push(handler);
console.log(`[SSEClient] Registered handler for: ${eventType} (total: ${handlers.length})`);
// Return unsubscribe function
return () => {
const currentHandlers = this.eventHandlers.get(eventType);
if (currentHandlers) {
const index = currentHandlers.indexOf(handler);
if (index > -1) {
currentHandlers.splice(index, 1);
console.log(`[SSEClient] Unregistered handler for: ${eventType}`);
}
}
};
}
/**
* Emit event to handlers
* @param {string} eventType - Event type
* @param {Object} data - Event data
*/
emit(eventType, data) {
if (window.traceExecution) {
window.traceExecution('sse-client', 'emit', {
eventType,
dataKeys: Object.keys(data || {})
});
}
this.routeEvent({ _eventType: eventType, ...data });
}
/**
* Disconnect from SSE endpoint
*/
disconnect() {
if (this.eventSource) {
console.log(`[SSEClient] Disconnecting from session ${this.currentSessionId}`);
this.eventSource.close();
this.eventSource = null;
}
const sessionId = this.currentSessionId;
this.currentSessionId = null;
this.reconnectAttempts = 0;
this.emit('disconnected', { sessionId });
}
/**
* Get connection state
* @returns {string} Connection state
*/
getReadyState() {
if (!this.eventSource) return 'DISCONNECTED';
switch (this.eventSource.readyState) {
case EventSource.CONNECTING:
return 'CONNECTING';
case EventSource.OPEN:
return 'OPEN';
case EventSource.CLOSED:
return 'CLOSED';
default:
return 'UNKNOWN';
}
}
/**
* Check if connected
* @returns {boolean} True if connected
*/
isConnected() {
return this.eventSource && this.eventSource.readyState === EventSource.OPEN;
}
}
// Singleton instance
const sseClient = new SSEClient();
// Auto-initialize: connect to session from URL path
document.addEventListener('DOMContentLoaded', () => {
// Extract sessionId from URL path: /claude/ide/session/{sessionId}
const pathMatch = window.location.pathname.match(/\/claude\/ide\/session\/([^/]+)$/);
if (pathMatch && pathMatch[1]) {
const sessionId = decodeURIComponent(pathMatch[1]);
console.log('[SSEClient] Auto-connecting to session from URL:', sessionId);
sseClient.connect(sessionId);
}
});
// Make globally accessible
window.sseClient = sseClient;