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:
318
public/claude-ide/sse-client.js
Normal file
318
public/claude-ide/sse-client.js
Normal file
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user