claude-code-templates 1.10.1 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2535 @@
1
+ /**
2
+ * AgentsPage - Dedicated page for managing and viewing agent conversations
3
+ * Handles conversation display, filtering, and detailed analysis
4
+ */
5
+ class AgentsPage {
6
+ constructor(container, services) {
7
+ this.container = container;
8
+ this.dataService = services.data;
9
+ this.stateService = services.state;
10
+
11
+ this.components = {};
12
+ this.filters = {
13
+ status: 'all',
14
+ timeRange: '7d',
15
+ search: ''
16
+ };
17
+ this.isInitialized = false;
18
+
19
+ // Pagination state for conversations
20
+ this.pagination = {
21
+ currentPage: 0,
22
+ limit: 10,
23
+ hasMore: true,
24
+ isLoading: false
25
+ };
26
+
27
+ // Pagination state for messages
28
+ this.messagesPagination = {
29
+ currentPage: 0,
30
+ limit: 10,
31
+ hasMore: true,
32
+ isLoading: false,
33
+ conversationId: null
34
+ };
35
+
36
+ // Loaded conversations cache
37
+ this.loadedConversations = [];
38
+ this.loadedMessages = new Map(); // Cache messages by conversation ID (now stores paginated data)
39
+
40
+ // State transition tracking for enhanced user experience
41
+ this.lastMessageTime = new Map(); // Track when last message was received per conversation
42
+
43
+ // Initialize tool display component
44
+ this.toolDisplay = new ToolDisplay();
45
+
46
+ // Subscribe to state changes
47
+ this.unsubscribe = this.stateService.subscribe(this.handleStateChange.bind(this));
48
+
49
+ // Subscribe to DataService events for real-time updates
50
+ this.dataService.addEventListener((type, data) => {
51
+ if (type === 'new_message') {
52
+ console.log('🔄 WebSocket: New message received', { conversationId: data.conversationId });
53
+ this.handleNewMessage(data.conversationId, data.message, data.metadata);
54
+ } else if (type === 'console_interaction') {
55
+ console.log('🔄 WebSocket: Console interaction request received', data);
56
+ this.showConsoleInteraction(data);
57
+ }
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Initialize the agents page
63
+ */
64
+ async initialize() {
65
+ if (this.isInitialized) return;
66
+
67
+ try {
68
+ this.stateService.setLoading(true);
69
+ await this.render();
70
+ await this.initializeComponents();
71
+ await this.loadConversationsData();
72
+ this.isInitialized = true;
73
+ } catch (error) {
74
+ console.error('Error initializing agents page:', error);
75
+ this.stateService.setError(error);
76
+ } finally {
77
+ this.stateService.setLoading(false);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Handle state changes from StateService (WebSocket updates)
83
+ * @param {Object} state - New state
84
+ * @param {string} action - Action that caused the change
85
+ */
86
+ handleStateChange(state, action) {
87
+ switch (action) {
88
+ case 'update_conversations':
89
+ // Don't replace loaded conversations, just update states
90
+ break;
91
+ case 'update_conversation_states':
92
+ console.log('🔄 WebSocket: Conversation states updated', { count: Object.keys(state.conversationStates?.activeStates || state.conversationStates || {}).length });
93
+
94
+ // Handle both direct states object and nested structure
95
+ const activeStates = state.conversationStates?.activeStates || state.conversationStates || {};
96
+
97
+ this.updateConversationStates(activeStates);
98
+ break;
99
+ case 'set_loading':
100
+ this.updateLoadingState(state.isLoading);
101
+ break;
102
+ case 'set_error':
103
+ this.updateErrorState(state.error);
104
+ break;
105
+ case 'conversation_state_change':
106
+ this.handleConversationStateChange(state);
107
+ break;
108
+ case 'data_refresh':
109
+ // On real-time data refresh, update conversation states but keep pagination
110
+ this.updateConversationStatesOnly();
111
+ break;
112
+ case 'new_message':
113
+ // Handle new message in real-time
114
+ this.handleNewMessage(state.conversationId, state.message, state.metadata);
115
+ break;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Handle new message received via WebSocket
121
+ * @param {string} conversationId - Conversation ID that received new message
122
+ * @param {Object} message - New message object
123
+ * @param {Object} metadata - Additional metadata
124
+ */
125
+ handleNewMessage(conversationId, message, metadata) {
126
+ // Log essential message info for debugging
127
+ console.log('🔄 WebSocket: Processing new message', {
128
+ conversationId,
129
+ role: message?.role,
130
+ hasTools: Array.isArray(message?.content) ? message.content.some(b => b.type === 'tool_use') : false,
131
+ hasToolResults: !!message?.toolResults
132
+ });
133
+
134
+ // Always update the message cache for this conversation
135
+ const existingMessages = this.loadedMessages.get(conversationId) || [];
136
+
137
+
138
+ // Track message timing for better state transitions
139
+ const now = Date.now();
140
+ this.lastMessageTime.set(conversationId, now);
141
+
142
+ // IMMEDIATE STATE TRANSITION based on message appearance
143
+ if (this.selectedConversationId === conversationId) {
144
+ if (message?.role === 'user') {
145
+ // User message just appeared - Claude immediately starts working
146
+ console.log('⚡ User message detected - Claude starting work immediately');
147
+ this.updateStateBanner(conversationId, 'Claude Code working...');
148
+ } else if (message?.role === 'assistant') {
149
+ // Assistant message appeared - analyze for specific state
150
+ const intelligentState = this.analyzeMessageForState(message, existingMessages);
151
+ console.log(`🤖 Assistant message detected - state: ${intelligentState}`);
152
+ this.updateStateBanner(conversationId, intelligentState);
153
+
154
+ // No additional timeout needed - state is determined by message content
155
+ }
156
+ }
157
+
158
+ // Check if we already have this message (avoid duplicates)
159
+ const messageExists = existingMessages.some(msg =>
160
+ msg.id === message.id ||
161
+ (msg.timestamp === message.timestamp && msg.role === message.role)
162
+ );
163
+
164
+ if (!messageExists) {
165
+ // Add new message to the end
166
+ const updatedMessages = [...existingMessages, message];
167
+ this.loadedMessages.set(conversationId, updatedMessages);
168
+
169
+ // Refresh only the conversation states to show updated status/timestamp
170
+ // Don't do full reload as it can interfere with message cache
171
+ this.updateConversationStatesOnly();
172
+
173
+ // If this conversation is currently selected, update the messages view
174
+ if (this.selectedConversationId === conversationId) {
175
+ // Re-render messages with new message
176
+ this.renderCachedMessages(updatedMessages, false);
177
+
178
+ // Auto-scroll to new message
179
+ this.scrollToBottom();
180
+ }
181
+
182
+ // Show notification
183
+ this.showNewMessageNotification(message, metadata);
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Update only conversation states without affecting pagination
189
+ */
190
+ async updateConversationStatesOnly() {
191
+ try {
192
+ const statesData = await this.dataService.getConversationStates();
193
+ const activeStates = statesData?.activeStates || {};
194
+
195
+ // Update StateService with fresh states
196
+ this.stateService.updateConversationStates(activeStates);
197
+
198
+ // Update states in already loaded conversations
199
+ this.updateConversationStateElements(activeStates);
200
+
201
+ // Update banner if we have a selected conversation
202
+ if (this.selectedConversationId && activeStates[this.selectedConversationId]) {
203
+ this.updateStateBanner(this.selectedConversationId, activeStates[this.selectedConversationId]);
204
+ }
205
+
206
+ } catch (error) {
207
+ console.error('Error updating conversation states:', error);
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Analyze a message to determine intelligent conversation state
213
+ * @param {Object} message - The message to analyze
214
+ * @param {Array} existingMessages - Previous messages in conversation
215
+ * @returns {string} Intelligent state description
216
+ */
217
+ analyzeMessageForState(message, existingMessages = []) {
218
+ const role = message?.role;
219
+ const content = message?.content;
220
+ const hasToolResults = !!message?.toolResults && message.toolResults.length > 0;
221
+ const messageTime = new Date(message?.timestamp || Date.now());
222
+ const now = new Date();
223
+ const messageAge = (now - messageTime) / 1000; // seconds
224
+
225
+ if (role === 'assistant') {
226
+ // Analyze assistant messages with enhanced logic
227
+ if (Array.isArray(content)) {
228
+ const hasToolUse = content.some(block => block.type === 'tool_use');
229
+ const hasText = content.some(block => block.type === 'text');
230
+ const textBlocks = content.filter(block => block.type === 'text');
231
+ const toolUseBlocks = content.filter(block => block.type === 'tool_use');
232
+
233
+ // Enhanced tool execution detection with immediate response
234
+ if (hasToolUse) {
235
+ const toolNames = toolUseBlocks.map(tool => tool.name).join(', ');
236
+
237
+ if (!hasToolResults) {
238
+ // Tool just sent - immediate execution state
239
+ console.log(`🔧 Tools detected: ${toolNames} - showing execution state`);
240
+
241
+ if (toolNames.includes('bash') || toolNames.includes('edit') || toolNames.includes('write') || toolNames.includes('multiedit')) {
242
+ return 'Executing tools...';
243
+ } else if (toolNames.includes('read') || toolNames.includes('grep') || toolNames.includes('glob') || toolNames.includes('task')) {
244
+ return 'Analyzing code...';
245
+ } else if (toolNames.includes('webfetch') || toolNames.includes('websearch')) {
246
+ return 'Fetching data...';
247
+ }
248
+ return 'Awaiting tool response...';
249
+ } else {
250
+ // Has tool results - Claude is processing them
251
+ console.log(`📊 Tools completed: ${toolNames} - analyzing results`);
252
+ return 'Analyzing results...';
253
+ }
254
+ }
255
+
256
+ // Enhanced text analysis
257
+ if (hasText) {
258
+ const textContent = textBlocks.map(block => block.text).join(' ').toLowerCase();
259
+
260
+ // Working indicators
261
+ if (textContent.includes('let me') ||
262
+ textContent.includes('i\'ll') ||
263
+ textContent.includes('i will') ||
264
+ textContent.includes('i\'m going to') ||
265
+ textContent.includes('let\'s') ||
266
+ textContent.includes('first, i\'ll') ||
267
+ textContent.includes('now i\'ll')) {
268
+ return 'Claude Code working...';
269
+ }
270
+
271
+ // Analysis indicators
272
+ if (textContent.includes('analyzing') ||
273
+ textContent.includes('examining') ||
274
+ textContent.includes('looking at') ||
275
+ textContent.includes('reviewing')) {
276
+ return 'Analyzing code...';
277
+ }
278
+
279
+ // Completion indicators
280
+ if (textContent.includes('completed') ||
281
+ textContent.includes('finished') ||
282
+ textContent.includes('done') ||
283
+ textContent.includes('successfully')) {
284
+ return 'Task completed';
285
+ }
286
+
287
+ // User input needed - enhanced detection
288
+ if (textContent.endsWith('?') ||
289
+ textContent.includes('what would you like') ||
290
+ textContent.includes('how can i help') ||
291
+ textContent.includes('would you like me to') ||
292
+ textContent.includes('should i') ||
293
+ textContent.includes('do you want') ||
294
+ textContent.includes('let me know') ||
295
+ textContent.includes('please let me know') ||
296
+ textContent.includes('what do you think') ||
297
+ textContent.includes('any questions')) {
298
+ return 'Waiting for your response';
299
+ }
300
+
301
+ // Error/problem indicators
302
+ if (textContent.includes('error') ||
303
+ textContent.includes('failed') ||
304
+ textContent.includes('problem') ||
305
+ textContent.includes('issue')) {
306
+ return 'Encountered issue';
307
+ }
308
+ }
309
+ }
310
+
311
+ // Recent assistant message suggests waiting for user
312
+ if (messageAge < 300) { // Extended to 5 minutes
313
+ return 'Waiting for your response';
314
+ }
315
+
316
+ // Default for older assistant messages
317
+ return 'Idle';
318
+
319
+ } else if (role === 'user') {
320
+ // User just sent a message - Claude should be processing
321
+ if (messageAge < 10) {
322
+ return 'Claude Code working...';
323
+ } else if (messageAge < 60) {
324
+ return 'Awaiting response...';
325
+ }
326
+
327
+ // Older user messages suggest Claude might be working on something complex
328
+ return 'Processing request...';
329
+ }
330
+
331
+ // Enhanced timing analysis
332
+ const lastMessage = existingMessages[existingMessages.length - 1];
333
+ if (lastMessage) {
334
+ const timeSinceLastMessage = Date.now() - new Date(lastMessage.timestamp).getTime();
335
+
336
+ if (timeSinceLastMessage < 30000) { // Less than 30 seconds
337
+ return lastMessage.role === 'user' ? 'Claude Code working...' : 'Recently active';
338
+ } else if (timeSinceLastMessage < 180000) { // Less than 3 minutes
339
+ return 'Idle';
340
+ } else if (timeSinceLastMessage < 1800000) { // Less than 30 minutes
341
+ return 'Waiting for your response';
342
+ }
343
+ }
344
+
345
+ return 'Inactive';
346
+ }
347
+
348
+
349
+ /**
350
+ * Show console interaction panel for Yes/No prompts
351
+ * @param {Object} interactionData - Interaction data from Claude Code
352
+ */
353
+ showConsoleInteraction(interactionData) {
354
+ const panel = this.container.querySelector('#console-interaction-panel');
355
+ const description = this.container.querySelector('#interaction-description');
356
+ const prompt = this.container.querySelector('#interaction-prompt');
357
+ const choices = this.container.querySelector('#interaction-choices');
358
+ const textInput = this.container.querySelector('#interaction-text-input');
359
+
360
+ // Show the panel
361
+ panel.style.display = 'block';
362
+
363
+ // Set up the interaction content
364
+ if (interactionData.description) {
365
+ description.innerHTML = `
366
+ <div class="tool-action">
367
+ <strong>${interactionData.tool || 'Action'}:</strong>
368
+ <div class="tool-details">${interactionData.description}</div>
369
+ </div>
370
+ `;
371
+ }
372
+
373
+ if (interactionData.prompt) {
374
+ prompt.textContent = interactionData.prompt;
375
+ }
376
+
377
+ // Handle different interaction types
378
+ if (interactionData.type === 'choice' && interactionData.options) {
379
+ // Show multiple choice options
380
+ choices.style.display = 'block';
381
+ textInput.style.display = 'none';
382
+
383
+ const choicesHtml = interactionData.options.map((option, index) => `
384
+ <label class="interaction-choice">
385
+ <input type="radio" name="console-choice" value="${index}" ${index === 0 ? 'checked' : ''}>
386
+ <span class="choice-number">${index + 1}.</span>
387
+ <span class="choice-text">${option}</span>
388
+ </label>
389
+ `).join('');
390
+
391
+ choices.innerHTML = choicesHtml;
392
+
393
+ } else if (interactionData.type === 'text') {
394
+ // Show text input
395
+ choices.style.display = 'none';
396
+ textInput.style.display = 'block';
397
+
398
+ const textarea = this.container.querySelector('#console-text-input');
399
+ textarea.focus();
400
+ }
401
+
402
+ // Store interaction data for submission
403
+ this.currentInteraction = interactionData;
404
+
405
+ // Bind event listeners
406
+ this.bindInteractionEvents();
407
+ }
408
+
409
+ /**
410
+ * Hide console interaction panel
411
+ */
412
+ hideConsoleInteraction() {
413
+ const panel = this.container.querySelector('#console-interaction-panel');
414
+ panel.style.display = 'none';
415
+ this.currentInteraction = null;
416
+ }
417
+
418
+ /**
419
+ * Bind event listeners for console interaction
420
+ */
421
+ bindInteractionEvents() {
422
+ const submitBtn = this.container.querySelector('#interaction-submit');
423
+ const cancelBtn = this.container.querySelector('#interaction-cancel');
424
+
425
+ // Remove existing listeners
426
+ submitBtn.replaceWith(submitBtn.cloneNode(true));
427
+ cancelBtn.replaceWith(cancelBtn.cloneNode(true));
428
+
429
+ // Get fresh references
430
+ const newSubmitBtn = this.container.querySelector('#interaction-submit');
431
+ const newCancelBtn = this.container.querySelector('#interaction-cancel');
432
+
433
+ newSubmitBtn.addEventListener('click', () => this.handleInteractionSubmit());
434
+ newCancelBtn.addEventListener('click', () => this.handleInteractionCancel());
435
+
436
+ // Handle Enter key for text input
437
+ const textarea = this.container.querySelector('#console-text-input');
438
+ if (textarea) {
439
+ textarea.addEventListener('keydown', (e) => {
440
+ if (e.key === 'Enter' && e.ctrlKey) {
441
+ this.handleInteractionSubmit();
442
+ }
443
+ });
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Handle interaction submission
449
+ */
450
+ async handleInteractionSubmit() {
451
+ if (!this.currentInteraction) return;
452
+
453
+ let response;
454
+
455
+ if (this.currentInteraction.type === 'choice') {
456
+ const selectedChoice = this.container.querySelector('input[name="console-choice"]:checked');
457
+ if (selectedChoice) {
458
+ response = {
459
+ type: 'choice',
460
+ value: parseInt(selectedChoice.value),
461
+ text: this.currentInteraction.options[selectedChoice.value]
462
+ };
463
+ }
464
+ } else if (this.currentInteraction.type === 'text') {
465
+ const textarea = this.container.querySelector('#console-text-input');
466
+ response = {
467
+ type: 'text',
468
+ value: textarea.value.trim()
469
+ };
470
+ }
471
+
472
+ if (response) {
473
+ // Send response via WebSocket
474
+ try {
475
+ await this.sendConsoleResponse(this.currentInteraction.id, response);
476
+ console.log('🔄 WebSocket: Console interaction response sent', { id: this.currentInteraction.id, response });
477
+ this.hideConsoleInteraction();
478
+ } catch (error) {
479
+ console.error('Error sending console response:', error);
480
+ // Show error in UI
481
+ this.showInteractionError('Failed to send response. Please try again.');
482
+ }
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Handle interaction cancellation
488
+ */
489
+ async handleInteractionCancel() {
490
+ if (!this.currentInteraction) return;
491
+
492
+ try {
493
+ await this.sendConsoleResponse(this.currentInteraction.id, { type: 'cancel' });
494
+ console.log('🔄 WebSocket: Console interaction cancelled', { id: this.currentInteraction.id });
495
+ this.hideConsoleInteraction();
496
+ } catch (error) {
497
+ console.error('Error cancelling console interaction:', error);
498
+ this.hideConsoleInteraction(); // Hide anyway on cancel
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Send console response via WebSocket
504
+ * @param {string} interactionId - Interaction ID
505
+ * @param {Object} response - Response data
506
+ */
507
+ async sendConsoleResponse(interactionId, response) {
508
+ // Send through DataService which will route to WebSocket
509
+ if (this.dataService && this.dataService.webSocketService) {
510
+ this.dataService.webSocketService.send({
511
+ type: 'console_response',
512
+ data: {
513
+ interactionId,
514
+ response
515
+ }
516
+ });
517
+ } else {
518
+ throw new Error('WebSocket service not available');
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Show error in interaction panel
524
+ * @param {string} message - Error message
525
+ */
526
+ showInteractionError(message) {
527
+ const panel = this.container.querySelector('#console-interaction-panel');
528
+ const existingError = panel.querySelector('.interaction-error');
529
+
530
+ if (existingError) {
531
+ existingError.remove();
532
+ }
533
+
534
+ const errorDiv = document.createElement('div');
535
+ errorDiv.className = 'interaction-error';
536
+ errorDiv.textContent = message;
537
+
538
+ const content = panel.querySelector('.interaction-content');
539
+ content.insertBefore(errorDiv, content.querySelector('.interaction-actions'));
540
+
541
+ // Remove error after 5 seconds
542
+ setTimeout(() => {
543
+ if (errorDiv.parentNode) {
544
+ errorDiv.remove();
545
+ }
546
+ }, 5000);
547
+ }
548
+
549
+ /**
550
+ * Test console interaction functionality (for development)
551
+ */
552
+ testConsoleInteraction() {
553
+ // Test choice-based interaction (like your example)
554
+ const testChoiceInteraction = {
555
+ id: 'test-choice-' + Date.now(),
556
+ type: 'choice',
557
+ tool: 'Search',
558
+ description: 'Search(pattern: "(?:Yes|No|yes|no)(?:,\\s*and\\s*don\'t\\s*ask\\s*again)?", path: "../../../../../../../.claude/projects/-Users-danipower-Proyectos-Github-claude-code-templates", include: "*.jsonl")',
559
+ prompt: 'Do you want to proceed?',
560
+ options: [
561
+ 'Yes',
562
+ 'Yes, and add /Users/danipower/.claude/projects/-Users-danipower-Proyectos-Github-claude-code-templates as a working directory for this session',
563
+ 'No, and tell Claude what to do differently'
564
+ ]
565
+ };
566
+
567
+ // Test text input interaction
568
+ const testTextInteraction = {
569
+ id: 'test-text-' + Date.now(),
570
+ type: 'text',
571
+ tool: 'Console Input',
572
+ description: 'Claude Code is requesting text input from the console.',
573
+ prompt: 'Please provide your input:'
574
+ };
575
+
576
+ // Randomly choose which type to test, or ask user
577
+ const testType = Math.random() > 0.5 ? testChoiceInteraction : testTextInteraction;
578
+
579
+ console.log('🧪 Testing console interaction:', testType);
580
+ this.showConsoleInteraction(testType);
581
+ }
582
+
583
+ /**
584
+ * Update conversation state elements in the DOM
585
+ * @param {Object} activeStates - Active conversation states
586
+ */
587
+ updateConversationStateElements(activeStates) {
588
+ const conversationItems = this.container.querySelectorAll('.sidebar-conversation-item');
589
+
590
+ conversationItems.forEach(item => {
591
+ const conversationId = item.dataset.id;
592
+ const state = activeStates[conversationId] || 'unknown';
593
+ const stateClass = this.getStateClass(state);
594
+ const stateLabel = this.getStateLabel(state);
595
+
596
+ // Update status dot
597
+ const statusDot = item.querySelector('.status-dot');
598
+ if (statusDot) {
599
+ statusDot.className = `status-dot ${stateClass}`;
600
+ }
601
+
602
+ // Update status badge
603
+ const statusBadge = item.querySelector('.sidebar-conversation-badge');
604
+ if (statusBadge) {
605
+ statusBadge.className = `sidebar-conversation-badge ${stateClass}`;
606
+ statusBadge.textContent = stateLabel;
607
+ }
608
+ });
609
+ }
610
+
611
+ /**
612
+ * Render the agents page structure
613
+ */
614
+ async render() {
615
+ this.container.innerHTML = `
616
+ <div class="agents-page">
617
+ <!-- Page Header -->
618
+ <div class="page-header conversations-header">
619
+ <div class="header-content">
620
+ <div class="header-left">
621
+ <div class="status-header">
622
+ <span class="session-timer-status-dot active"></span>
623
+ <h1 class="page-title">
624
+ Claude Code web UI
625
+ </h1>
626
+ </div>
627
+ <div class="page-subtitle">
628
+ Monitor and analyze Claude Code agent interactions in real-time
629
+ </div>
630
+ </div>
631
+ </div>
632
+ </div>
633
+
634
+ <!-- Filters Section -->
635
+ <div class="conversations-filters">
636
+ <div class="filters-row">
637
+ <div class="filter-group">
638
+ <label class="filter-label">Status:</label>
639
+ <select class="filter-select" id="status-filter">
640
+ <option value="all">All</option>
641
+ <option value="active">Active</option>
642
+ <option value="inactive">Inactive</option>
643
+ </select>
644
+ </div>
645
+
646
+ <div class="filter-group">
647
+ <label class="filter-label">Time Range:</label>
648
+ <select class="filter-select" id="time-filter">
649
+ <option value="1h">Last Hour</option>
650
+ <option value="24h">Last 24 Hours</option>
651
+ <option value="7d" selected>Last 7 Days</option>
652
+ <option value="30d">Last 30 Days</option>
653
+ </select>
654
+ </div>
655
+
656
+ <div class="filter-group search-group">
657
+ <label class="filter-label">Search:</label>
658
+ <div class="search-input-container">
659
+ <input type="text" class="filter-input search-input" id="search-filter" placeholder="Search conversations, projects, or messages...">
660
+ <button class="search-clear" id="clear-search" title="Clear search">×</button>
661
+ </div>
662
+ </div>
663
+ </div>
664
+ </div>
665
+
666
+ <!-- Loading State -->
667
+ <div class="loading-state" id="conversations-loading" style="display: none;">
668
+ <div class="loading-spinner"></div>
669
+ <span class="loading-text">Loading conversations...</span>
670
+ </div>
671
+
672
+ <!-- Error State -->
673
+ <div class="error-state" id="conversations-error" style="display: none;">
674
+ <div class="error-content">
675
+ <span class="error-icon">⚠️</span>
676
+ <span class="error-message"></span>
677
+ <button class="error-retry" id="retry-load">Retry</button>
678
+ </div>
679
+ </div>
680
+
681
+ <!-- Console Interaction Panel (Hidden by default) -->
682
+ <div id="console-interaction-panel" class="console-interaction-panel" style="display: none;">
683
+ <div class="interaction-header">
684
+ <div class="interaction-title">
685
+ <span class="interaction-icon">⚡</span>
686
+ <span class="interaction-text">Claude Code needs your input</span>
687
+ </div>
688
+ <button class="interaction-close" onclick="this.hideConsoleInteraction()">&times;</button>
689
+ </div>
690
+
691
+ <div class="interaction-content">
692
+ <div id="interaction-description" class="interaction-description">
693
+ <!-- Tool description will be inserted here -->
694
+ </div>
695
+
696
+ <div id="interaction-prompt" class="interaction-prompt">
697
+ Do you want to proceed?
698
+ </div>
699
+
700
+ <!-- Multi-choice options -->
701
+ <div id="interaction-choices" class="interaction-choices" style="display: none;">
702
+ <!-- Radio button choices will be inserted here -->
703
+ </div>
704
+
705
+ <!-- Text input area -->
706
+ <div id="interaction-text-input" class="interaction-text-input" style="display: none;">
707
+ <label for="console-text-input">Your response:</label>
708
+ <textarea id="console-text-input" placeholder="Type your response here..." rows="4"></textarea>
709
+ </div>
710
+
711
+ <div class="interaction-actions">
712
+ <button id="interaction-submit" class="interaction-btn primary">Submit</button>
713
+ <button id="interaction-cancel" class="interaction-btn secondary">Cancel</button>
714
+ </div>
715
+ </div>
716
+ </div>
717
+
718
+ <!-- Two Column Layout -->
719
+ <div class="conversations-layout">
720
+ <!-- Left Sidebar: Conversations List -->
721
+ <div class="conversations-sidebar">
722
+ <div class="sidebar-header">
723
+ <h3>Chats</h3>
724
+ <span class="conversation-count" id="sidebar-count">0</span>
725
+ </div>
726
+ <div class="conversations-list" id="conversations-list">
727
+ <!-- Conversation items will be rendered here -->
728
+ </div>
729
+
730
+ <!-- Load More Indicator -->
731
+ <div class="load-more-indicator" id="load-more-indicator" style="display: none;">
732
+ <div class="loading-spinner"></div>
733
+ <span class="loading-text">Loading more conversations...</span>
734
+ </div>
735
+ </div>
736
+
737
+ <!-- Right Panel: Messages Detail -->
738
+ <div class="messages-panel">
739
+ <div class="messages-header" id="messages-header">
740
+ <div class="selected-conversation-info">
741
+ <h3 id="selected-conversation-title">Select a chat</h3>
742
+ <div class="selected-conversation-meta" id="selected-conversation-meta"></div>
743
+ </div>
744
+ <div class="messages-actions">
745
+ <button class="action-btn-small" id="export-conversation" title="Export conversation">
746
+ <span class="btn-icon-small">📁</span>
747
+ Export
748
+ </button>
749
+ </div>
750
+ </div>
751
+
752
+ <div class="messages-content" id="messages-content">
753
+ <div class="no-conversation-selected">
754
+ <div class="no-selection-icon">💬</div>
755
+ <h4>No conversation selected</h4>
756
+ <p>Choose a conversation from the sidebar to view its messages</p>
757
+ </div>
758
+ </div>
759
+
760
+ <!-- Conversation State Banner -->
761
+ <div class="conversation-state-banner" id="conversation-state-banner" style="display: none;">
762
+ <div class="state-indicator">
763
+ <span class="state-dot" id="state-dot"></span>
764
+ <span class="state-text" id="state-text">Ready</span>
765
+ </div>
766
+ <div class="state-timestamp" id="state-timestamp"></div>
767
+ </div>
768
+ </div>
769
+ </div>
770
+
771
+ <!-- Empty State -->
772
+ <div class="empty-state" id="empty-state" style="display: none;">
773
+ <div class="empty-content">
774
+ <span class="empty-icon">💬</span>
775
+ <h3>No conversations found</h3>
776
+ <p>No agent conversations match your current filters.</p>
777
+ <button class="empty-action" id="clear-filters">Clear Filters</button>
778
+ </div>
779
+ </div>
780
+ </div>
781
+ `;
782
+
783
+ this.bindEvents();
784
+ this.setupInfiniteScroll();
785
+ }
786
+
787
+ /**
788
+ * Initialize child components
789
+ */
790
+ async initializeComponents() {
791
+ // Initialize ConversationTable for detailed view if available
792
+ const tableContainer = this.container.querySelector('#conversations-table');
793
+ if (tableContainer && typeof ConversationTable !== 'undefined') {
794
+ try {
795
+ this.components.conversationTable = new ConversationTable(
796
+ tableContainer,
797
+ this.dataService,
798
+ this.stateService
799
+ );
800
+ await this.components.conversationTable.initialize();
801
+ } catch (error) {
802
+ console.warn('ConversationTable initialization failed:', error);
803
+ // Show fallback content
804
+ tableContainer.innerHTML = `
805
+ <div class="conversation-table-placeholder">
806
+ <p>Detailed table view not available</p>
807
+ </div>
808
+ `;
809
+ }
810
+ }
811
+ }
812
+
813
+ /**
814
+ * Bind event listeners
815
+ */
816
+ bindEvents() {
817
+ // Filter controls
818
+ const statusFilter = this.container.querySelector('#status-filter');
819
+ statusFilter.addEventListener('change', (e) => this.updateFilter('status', e.target.value));
820
+
821
+ const timeFilter = this.container.querySelector('#time-filter');
822
+ timeFilter.addEventListener('change', (e) => this.updateFilter('timeRange', e.target.value));
823
+
824
+ const searchInput = this.container.querySelector('#search-filter');
825
+ searchInput.addEventListener('input', (e) => this.updateFilter('search', e.target.value));
826
+
827
+ const clearSearch = this.container.querySelector('#clear-search');
828
+ clearSearch.addEventListener('click', () => this.clearSearch());
829
+
830
+ // Error retry
831
+ const retryBtn = this.container.querySelector('#retry-load');
832
+ if (retryBtn) {
833
+ retryBtn.addEventListener('click', () => this.loadConversationsData());
834
+ }
835
+
836
+ // Clear filters
837
+ const clearFiltersBtn = this.container.querySelector('#clear-filters');
838
+ if (clearFiltersBtn) {
839
+ clearFiltersBtn.addEventListener('click', () => this.clearAllFilters());
840
+ }
841
+
842
+ // Test console interaction
843
+ const testConsoleBtn = this.container.querySelector('#test-console-interaction');
844
+ if (testConsoleBtn) {
845
+ testConsoleBtn.addEventListener('click', () => this.testConsoleInteraction());
846
+ }
847
+ }
848
+
849
+ /**
850
+ * Setup infinite scroll for conversations list
851
+ */
852
+ setupInfiniteScroll() {
853
+ const conversationsContainer = this.container.querySelector('#conversations-list');
854
+ if (!conversationsContainer) return;
855
+
856
+ conversationsContainer.addEventListener('scroll', () => {
857
+ const { scrollTop, scrollHeight, clientHeight } = conversationsContainer;
858
+ const threshold = 100; // Load more when 100px from bottom
859
+
860
+ if (scrollHeight - scrollTop - clientHeight < threshold) {
861
+ this.loadMoreConversations();
862
+ }
863
+ });
864
+ }
865
+
866
+ /**
867
+ * Update loading indicator
868
+ * @param {boolean} isLoading - Whether to show loading indicator
869
+ */
870
+ updateLoadingIndicator(isLoading) {
871
+ const loadingIndicator = this.container.querySelector('#load-more-indicator');
872
+ if (loadingIndicator) {
873
+ loadingIndicator.style.display = isLoading ? 'flex' : 'none';
874
+ }
875
+ }
876
+
877
+ /**
878
+ * Load initial conversations data using paginated API
879
+ */
880
+ async loadConversationsData() {
881
+ try {
882
+
883
+ // Reset pagination state
884
+ this.pagination = {
885
+ currentPage: 0,
886
+ limit: 10,
887
+ hasMore: true,
888
+ isLoading: false
889
+ };
890
+ this.loadedConversations = [];
891
+ this.loadedMessages.clear(); // Clear message cache too
892
+
893
+ // Clear the list container
894
+ const listContainer = this.container.querySelector('#conversations-list');
895
+ if (listContainer) {
896
+ listContainer.innerHTML = '';
897
+ }
898
+
899
+ // Hide empty state initially
900
+ this.hideEmptyState();
901
+
902
+ // Load first page and states
903
+ await this.loadMoreConversations();
904
+
905
+
906
+ } catch (error) {
907
+ console.error('Error loading conversations data:', error);
908
+ this.stateService.setError('Failed to load conversations data');
909
+ }
910
+ }
911
+
912
+ /**
913
+ * Load more conversations (pagination)
914
+ */
915
+ async loadMoreConversations() {
916
+ if (this.pagination.isLoading || !this.pagination.hasMore) {
917
+ return;
918
+ }
919
+
920
+ try {
921
+
922
+ this.pagination.isLoading = true;
923
+ this.updateLoadingIndicator(true);
924
+
925
+ const [conversationsData, statesData] = await Promise.all([
926
+ this.dataService.getConversationsPaginated(this.pagination.currentPage, this.pagination.limit),
927
+ this.dataService.getConversationStates()
928
+ ]);
929
+
930
+
931
+ // Update pagination info
932
+ this.pagination.hasMore = conversationsData.pagination.hasMore;
933
+ this.pagination.currentPage = conversationsData.pagination.page + 1;
934
+ this.pagination.totalCount = conversationsData.pagination.totalCount;
935
+
936
+ // Get only NEW conversations for this page
937
+ const newConversations = conversationsData.conversations;
938
+
939
+ // Add new conversations to loaded list
940
+ this.loadedConversations.push(...newConversations);
941
+
942
+
943
+ // Extract activeStates from the response structure
944
+ const activeStates = statesData?.activeStates || {};
945
+
946
+ // Update state with correct format
947
+ this.stateService.updateConversations(this.loadedConversations);
948
+ this.stateService.updateConversationStates(activeStates);
949
+
950
+
951
+ // For initial load (page 0), replace content. For subsequent loads, append
952
+ const isInitialLoad = conversationsData.pagination.page === 0;
953
+ this.renderConversationsList(
954
+ isInitialLoad ? this.loadedConversations : newConversations,
955
+ activeStates,
956
+ !isInitialLoad
957
+ );
958
+
959
+ } catch (error) {
960
+ console.error('Error loading more conversations:', error);
961
+ this.stateService.setError('Failed to load more conversations');
962
+ } finally {
963
+ this.pagination.isLoading = false;
964
+ this.updateLoadingIndicator(false);
965
+ }
966
+ }
967
+
968
+ /**
969
+ * Render conversations list
970
+ * @param {Array} conversations - Conversations data
971
+ * @param {Object} states - Conversation states
972
+ * @param {boolean} append - Whether to append or replace content
973
+ */
974
+ renderConversationsList(conversations, states, append = false) {
975
+ const listContainer = this.container.querySelector('#conversations-list');
976
+ const filteredConversations = this.filterConversations(conversations, states);
977
+
978
+ // Calculate count based on filters
979
+ let countToShow;
980
+ const hasActiveFilters = this.hasActiveFilters();
981
+
982
+ if (!hasActiveFilters && this.pagination && this.pagination.totalCount) {
983
+ // No filters active, show total count from server
984
+ countToShow = this.pagination.totalCount;
985
+ } else {
986
+ // Filters active, count filtered loaded conversations
987
+ const conversationsToCount = this.loadedConversations && this.loadedConversations.length > 0
988
+ ? this.loadedConversations
989
+ : conversations;
990
+ const allFilteredConversations = this.filterConversations(conversationsToCount, states);
991
+ countToShow = allFilteredConversations.length;
992
+ }
993
+
994
+ this.updateResultsCount(countToShow, hasActiveFilters);
995
+ this.updateClearFiltersButton();
996
+
997
+ if (filteredConversations.length === 0 && !append) {
998
+ this.showEmptyState();
999
+ return;
1000
+ }
1001
+
1002
+ this.hideEmptyState();
1003
+
1004
+ const conversationHTML = filteredConversations.map(conv => {
1005
+ const state = states[conv.id] || 'unknown';
1006
+ const stateClass = this.getStateClass(state);
1007
+
1008
+ return `
1009
+ <div class="sidebar-conversation-item" data-id="${conv.id}">
1010
+ <div class="sidebar-conversation-header">
1011
+ <div class="sidebar-conversation-title">
1012
+ <span class="status-dot ${stateClass}"></span>
1013
+ <h4 class="sidebar-conversation-name">${conv.title || `Chat ${conv.id.slice(-8)}`}</h4>
1014
+ </div>
1015
+ <span class="sidebar-conversation-badge ${stateClass}">${this.getStateLabel(state)}</span>
1016
+ </div>
1017
+
1018
+ <div class="sidebar-conversation-meta">
1019
+ <span class="sidebar-meta-item">
1020
+ <span class="sidebar-meta-icon">📁</span>
1021
+ ${this.truncateText(conv.project || 'Unknown', 12)}
1022
+ </span>
1023
+ </div>
1024
+
1025
+ <div class="sidebar-conversation-preview">
1026
+ <p class="sidebar-preview-text">${this.getSimpleConversationPreview(conv)}</p>
1027
+ </div>
1028
+ </div>
1029
+ `;
1030
+ }).join('');
1031
+
1032
+ if (append) {
1033
+ listContainer.insertAdjacentHTML('beforeend', conversationHTML);
1034
+ } else {
1035
+ listContainer.innerHTML = conversationHTML;
1036
+ }
1037
+
1038
+ // Bind card actions
1039
+ this.bindListActions();
1040
+ }
1041
+
1042
+ /**
1043
+ * Bind list action events
1044
+ */
1045
+ bindListActions() {
1046
+ // Export conversation button
1047
+ const exportBtn = this.container.querySelector('#export-conversation');
1048
+ if (exportBtn) {
1049
+ exportBtn.addEventListener('click', () => {
1050
+ if (this.selectedConversationId) {
1051
+ this.exportSingleConversation(this.selectedConversationId);
1052
+ }
1053
+ });
1054
+ }
1055
+
1056
+ // Click on sidebar conversation item to select and view
1057
+ const conversationItems = this.container.querySelectorAll('.sidebar-conversation-item');
1058
+ conversationItems.forEach(item => {
1059
+ item.addEventListener('click', () => {
1060
+ const conversationId = item.dataset.id;
1061
+ this.selectConversation(conversationId);
1062
+ });
1063
+ });
1064
+ }
1065
+
1066
+ /**
1067
+ * Select and display a conversation
1068
+ * @param {string} conversationId - Conversation ID
1069
+ */
1070
+ async selectConversation(conversationId) {
1071
+ // Update selected conversation state
1072
+ this.selectedConversationId = conversationId;
1073
+
1074
+ // Update UI to show selection
1075
+ this.updateSelectedConversation();
1076
+
1077
+ // Load and display conversation messages
1078
+ await this.loadConversationMessages(conversationId);
1079
+ }
1080
+
1081
+ /**
1082
+ * Update selected conversation in sidebar
1083
+ */
1084
+ updateSelectedConversation() {
1085
+ // Remove previous selection
1086
+ const previousSelected = this.container.querySelector('.sidebar-conversation-item.selected');
1087
+ if (previousSelected) {
1088
+ previousSelected.classList.remove('selected');
1089
+ }
1090
+
1091
+ // Add selection to current item
1092
+ const currentItem = this.container.querySelector(`[data-id="${this.selectedConversationId}"]`);
1093
+ if (currentItem) {
1094
+ currentItem.classList.add('selected');
1095
+ }
1096
+
1097
+ // Update header with conversation info
1098
+ const conversations = this.stateService.getStateProperty('conversations') || [];
1099
+ const conversation = conversations.find(conv => conv.id === this.selectedConversationId);
1100
+
1101
+ if (conversation) {
1102
+ const titleElement = this.container.querySelector('#selected-conversation-title');
1103
+ const metaElement = this.container.querySelector('#selected-conversation-meta');
1104
+
1105
+ if (titleElement) {
1106
+ titleElement.textContent = conversation.title || `Chat ${conversation.id.slice(-8)}`;
1107
+ }
1108
+
1109
+ if (metaElement) {
1110
+ const messageCount = conversation.messageCount || 0;
1111
+ const lastActivity = this.formatRelativeTime(new Date(conversation.lastModified));
1112
+ metaElement.innerHTML = `
1113
+ <span class="meta-item">
1114
+ <span class="meta-icon">📁</span>
1115
+ ${conversation.project || 'Unknown Project'}
1116
+ </span>
1117
+ <span class="meta-item">
1118
+ <span class="meta-icon">💬</span>
1119
+ ${messageCount} message${messageCount !== 1 ? 's' : ''}
1120
+ </span>
1121
+ <span class="meta-item">
1122
+ <span class="meta-icon">🕒</span>
1123
+ ${lastActivity}
1124
+ </span>
1125
+ `;
1126
+ }
1127
+ }
1128
+
1129
+ // Show and update the state banner
1130
+ this.showStateBanner(this.selectedConversationId);
1131
+ }
1132
+
1133
+ /**
1134
+ * Load and display conversation messages (with caching)
1135
+ * @param {string} conversationId - Conversation ID
1136
+ */
1137
+ async loadConversationMessages(conversationId) {
1138
+ // Reset pagination for new conversation
1139
+ this.messagesPagination = {
1140
+ currentPage: 0,
1141
+ limit: 10,
1142
+ hasMore: true,
1143
+ isLoading: false,
1144
+ conversationId: conversationId
1145
+ };
1146
+
1147
+ // Clear cached messages for this conversation
1148
+ this.loadedMessages.delete(conversationId);
1149
+
1150
+ // Load first page of messages
1151
+ await this.loadMoreMessages(conversationId, true);
1152
+ }
1153
+
1154
+ /**
1155
+ * Show and update conversation state banner
1156
+ * @param {string} conversationId - Conversation ID
1157
+ */
1158
+ showStateBanner(conversationId) {
1159
+ const banner = this.container.querySelector('#conversation-state-banner');
1160
+ if (!banner) return;
1161
+
1162
+ // Show the banner
1163
+ banner.style.display = 'flex';
1164
+
1165
+ // Get current state from WebSocket or cache
1166
+ const conversationStates = this.stateService.getStateProperty('conversationStates') || {};
1167
+ const currentState = conversationStates[conversationId] || 'unknown';
1168
+
1169
+
1170
+ // If we don't have the state yet, try to fetch it after a short delay
1171
+ if (currentState === 'unknown') {
1172
+ setTimeout(() => {
1173
+ this.fetchConversationState(conversationId);
1174
+ }, 100);
1175
+ }
1176
+
1177
+ // Update banner with current state
1178
+ this.updateStateBanner(conversationId, currentState);
1179
+ }
1180
+
1181
+ /**
1182
+ * Update conversation state banner
1183
+ * @param {string} conversationId - Conversation ID
1184
+ * @param {string} state - Current conversation state
1185
+ */
1186
+ updateStateBanner(conversationId, state) {
1187
+ const banner = this.container.querySelector('#conversation-state-banner');
1188
+ const stateDot = this.container.querySelector('#state-dot');
1189
+ const stateText = this.container.querySelector('#state-text');
1190
+ const stateTimestamp = this.container.querySelector('#state-timestamp');
1191
+
1192
+
1193
+ if (!banner || !stateDot || !stateText || !stateTimestamp) {
1194
+ return;
1195
+ }
1196
+
1197
+ // Map states to user-friendly messages with enhanced descriptions
1198
+ const stateMessages = {
1199
+ 'Claude Code working...': {
1200
+ text: '🤖 Claude is thinking and working...',
1201
+ description: 'Claude is processing your request',
1202
+ class: 'status-working',
1203
+ icon: '🧠'
1204
+ },
1205
+ 'Awaiting tool response...': {
1206
+ text: '⚡ Waiting for tool execution...',
1207
+ description: 'Claude is waiting for tool results',
1208
+ class: 'status-tool-pending',
1209
+ icon: '🔧'
1210
+ },
1211
+ 'Executing tools...': {
1212
+ text: '🔧 Executing tools...',
1213
+ description: 'Claude is running system tools',
1214
+ class: 'status-tool-executing',
1215
+ icon: '⚡'
1216
+ },
1217
+ 'Analyzing results...': {
1218
+ text: '📊 Analyzing tool results...',
1219
+ description: 'Claude is processing tool outputs',
1220
+ class: 'status-analyzing',
1221
+ icon: '🔍'
1222
+ },
1223
+ 'Analyzing code...': {
1224
+ text: '🔍 Analyzing code...',
1225
+ description: 'Claude is examining code or files',
1226
+ class: 'status-analyzing',
1227
+ icon: '📝'
1228
+ },
1229
+ 'Fetching data...': {
1230
+ text: '🌐 Fetching data...',
1231
+ description: 'Claude is retrieving web content or external data',
1232
+ class: 'status-fetching',
1233
+ icon: '📶'
1234
+ },
1235
+ 'Task completed': {
1236
+ text: '✅ Task completed',
1237
+ description: 'Claude has finished the requested task',
1238
+ class: 'status-completed',
1239
+ icon: '✨'
1240
+ },
1241
+ 'Processing request...': {
1242
+ text: '⚙️ Processing request...',
1243
+ description: 'Claude is working on a complex request',
1244
+ class: 'status-processing',
1245
+ icon: '🔄'
1246
+ },
1247
+ 'Encountered issue': {
1248
+ text: '⚠️ Encountered issue',
1249
+ description: 'Claude found an error or problem',
1250
+ class: 'status-error',
1251
+ icon: '🚟'
1252
+ },
1253
+ 'Awaiting user input...': {
1254
+ text: '💬 Awaiting your input',
1255
+ description: 'Claude needs your response to continue',
1256
+ class: 'status-waiting',
1257
+ icon: '💭'
1258
+ },
1259
+ 'Waiting for your response': {
1260
+ text: '💬 Waiting for your response',
1261
+ description: 'Claude is ready for your next message',
1262
+ class: 'status-waiting-response',
1263
+ icon: '📝'
1264
+ },
1265
+ 'Awaiting response...': {
1266
+ text: '⏳ Awaiting Claude response',
1267
+ description: 'Waiting for Claude to respond',
1268
+ class: 'status-waiting',
1269
+ icon: '🤔'
1270
+ },
1271
+ 'Recently active': {
1272
+ text: '🟢 Recently active',
1273
+ description: 'Conversation was active recently',
1274
+ class: 'status-active',
1275
+ icon: '✨'
1276
+ },
1277
+ 'Idle': {
1278
+ text: '😴 Conversation idle',
1279
+ description: 'No recent activity',
1280
+ class: 'status-idle',
1281
+ icon: '💤'
1282
+ },
1283
+ 'Inactive': {
1284
+ text: '⚪ Inactive',
1285
+ description: 'Conversation has been inactive',
1286
+ class: 'status-idle',
1287
+ icon: '⏸️'
1288
+ },
1289
+ 'Old': {
1290
+ text: '📚 Archived conversation',
1291
+ description: 'No recent activity in this conversation',
1292
+ class: 'status-idle',
1293
+ icon: '📁'
1294
+ },
1295
+ 'unknown': {
1296
+ text: '🔄 Loading conversation state...',
1297
+ description: 'Determining conversation status',
1298
+ class: 'status-loading',
1299
+ icon: '⏳'
1300
+ }
1301
+ };
1302
+
1303
+ const stateInfo = stateMessages[state] || stateMessages['unknown'];
1304
+
1305
+ // Update dot class with enhanced styling
1306
+ stateDot.className = `state-dot ${stateInfo.class}`;
1307
+
1308
+ // Update text with icon and description
1309
+ stateText.innerHTML = `
1310
+ <span class="state-text-main">${stateInfo.text}</span>
1311
+ <span class="state-text-description">${stateInfo.description}</span>
1312
+ `;
1313
+
1314
+ // Add tooltip for additional context
1315
+ stateText.title = stateInfo.description;
1316
+
1317
+ // Update timestamp with more context
1318
+ const now = new Date();
1319
+ const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
1320
+ stateTimestamp.innerHTML = `
1321
+ <span class="timestamp-label">Last updated:</span>
1322
+ <span class="timestamp-value">${timeString}</span>
1323
+ `;
1324
+
1325
+ // Add pulsing animation for active states
1326
+ if (stateInfo.class.includes('working') || stateInfo.class.includes('executing') || stateInfo.class.includes('analyzing')) {
1327
+ stateDot.classList.add('pulse-animation');
1328
+ setTimeout(() => stateDot.classList.remove('pulse-animation'), 3000);
1329
+ }
1330
+
1331
+ }
1332
+
1333
+ /**
1334
+ * Fetch conversation state from API
1335
+ * @param {string} conversationId - Conversation ID
1336
+ */
1337
+ async fetchConversationState(conversationId) {
1338
+ try {
1339
+ const stateData = await this.dataService.getConversationStates();
1340
+
1341
+ if (stateData && stateData.activeStates && stateData.activeStates[conversationId]) {
1342
+ const state = stateData.activeStates[conversationId];
1343
+
1344
+ // Update the StateService with the new data
1345
+ this.stateService.updateConversationStates(stateData.activeStates);
1346
+
1347
+ // Update the banner with the real state
1348
+ this.updateStateBanner(conversationId, state);
1349
+ } else {
1350
+ // Keep showing unknown for now
1351
+ }
1352
+ } catch (error) {
1353
+ console.error('Error fetching conversation state:', error);
1354
+ }
1355
+ }
1356
+
1357
+ /**
1358
+ * Hide conversation state banner
1359
+ */
1360
+ hideStateBanner() {
1361
+ const banner = this.container.querySelector('#conversation-state-banner');
1362
+ if (banner) {
1363
+ banner.style.display = 'none';
1364
+ }
1365
+ }
1366
+
1367
+ /**
1368
+ * Auto-scroll to bottom of messages
1369
+ */
1370
+ scrollToBottom() {
1371
+ const messagesContent = this.container.querySelector('#messages-content');
1372
+ if (messagesContent) {
1373
+ messagesContent.scrollTop = messagesContent.scrollHeight;
1374
+ }
1375
+ }
1376
+
1377
+ /**
1378
+ * Show notification for new message
1379
+ * @param {Object} message - New message object
1380
+ * @param {Object} metadata - Message metadata
1381
+ */
1382
+ showNewMessageNotification(message, metadata) {
1383
+ // Update banner if it's showing to reflect new activity
1384
+ if (this.selectedConversationId) {
1385
+ const banner = this.container.querySelector('#conversation-state-banner');
1386
+ if (banner && banner.style.display !== 'none') {
1387
+ // Temporarily highlight the banner to show activity
1388
+ banner.style.backgroundColor = 'rgba(213, 116, 85, 0.1)';
1389
+ setTimeout(() => {
1390
+ banner.style.backgroundColor = '';
1391
+ }, 1000);
1392
+ }
1393
+ }
1394
+
1395
+ // Could add visual indicator for new message (pulse, notification badge, etc.)
1396
+ }
1397
+
1398
+ /**
1399
+ * Load more messages (for infinite scroll)
1400
+ * @param {string} conversationId - Conversation ID
1401
+ * @param {boolean} isInitialLoad - Whether this is the initial load
1402
+ */
1403
+ async loadMoreMessages(conversationId, isInitialLoad = false) {
1404
+ const messagesContent = this.container.querySelector('#messages-content');
1405
+ if (!messagesContent) return;
1406
+
1407
+ // Prevent concurrent loading
1408
+ if (this.messagesPagination.isLoading || !this.messagesPagination.hasMore) {
1409
+ return;
1410
+ }
1411
+
1412
+ // Ensure we're loading for the correct conversation
1413
+ if (this.messagesPagination.conversationId !== conversationId) {
1414
+ return;
1415
+ }
1416
+
1417
+ try {
1418
+ this.messagesPagination.isLoading = true;
1419
+
1420
+ if (isInitialLoad) {
1421
+ // Show loading state for initial load
1422
+ messagesContent.innerHTML = `
1423
+ <div class="messages-loading">
1424
+ <div class="loading-spinner"></div>
1425
+ <span>Loading messages...</span>
1426
+ </div>
1427
+ `;
1428
+ } else {
1429
+ // Show loading indicator at top for infinite scroll
1430
+ this.showMessagesLoadingIndicator(true);
1431
+ }
1432
+
1433
+ // Fetch paginated messages from the server
1434
+ const messagesData = await this.dataService.cachedFetch(
1435
+ `/api/conversations/${conversationId}/messages?page=${this.messagesPagination.currentPage}&limit=${this.messagesPagination.limit}`
1436
+ );
1437
+
1438
+ if (messagesData && messagesData.messages) {
1439
+ // Update pagination state - handle both paginated and non-paginated responses
1440
+ if (messagesData.pagination) {
1441
+ // Paginated response
1442
+ this.messagesPagination.hasMore = messagesData.pagination.hasMore;
1443
+ this.messagesPagination.currentPage = messagesData.pagination.page + 1;
1444
+ } else {
1445
+ // Non-paginated response (fallback) - treat as complete data
1446
+ this.messagesPagination.hasMore = false;
1447
+ this.messagesPagination.currentPage = 1;
1448
+ }
1449
+
1450
+ // Get existing messages or initialize
1451
+ let existingMessages = this.loadedMessages.get(conversationId) || [];
1452
+
1453
+ if (isInitialLoad) {
1454
+ // For initial load, replace all messages
1455
+ existingMessages = messagesData.messages;
1456
+ } else {
1457
+ // For infinite scroll, prepend older messages (they come in chronological order)
1458
+ existingMessages = [...messagesData.messages, ...existingMessages];
1459
+ }
1460
+
1461
+ // Cache the combined messages
1462
+ this.loadedMessages.set(conversationId, existingMessages);
1463
+
1464
+ // Render messages
1465
+ this.renderCachedMessages(existingMessages, !isInitialLoad);
1466
+
1467
+ // Setup scroll listener for infinite scroll (only on initial load)
1468
+ if (isInitialLoad) {
1469
+ this.setupMessagesScrollListener(conversationId);
1470
+ }
1471
+
1472
+
1473
+ } else if (isInitialLoad) {
1474
+ messagesContent.innerHTML = `
1475
+ <div class="no-messages-found">
1476
+ <div class="no-messages-icon">💭</div>
1477
+ <h4>No messages found</h4>
1478
+ <p>This conversation has no messages or they could not be loaded.</p>
1479
+ </div>
1480
+ `;
1481
+ }
1482
+
1483
+ } catch (error) {
1484
+ console.error('Error loading messages:', error);
1485
+
1486
+ if (isInitialLoad) {
1487
+ messagesContent.innerHTML = `
1488
+ <div class="messages-error">
1489
+ <span class="error-icon">⚠️</span>
1490
+ <span>Failed to load messages</span>
1491
+ <button class="retry-messages" data-conversation-id="${conversationId}">Retry</button>
1492
+ </div>
1493
+ `;
1494
+
1495
+ // Bind retry button event
1496
+ const retryBtn = messagesContent.querySelector('.retry-messages');
1497
+ if (retryBtn) {
1498
+ retryBtn.addEventListener('click', () => {
1499
+ this.loadConversationMessages(conversationId);
1500
+ });
1501
+ }
1502
+ }
1503
+ } finally {
1504
+ this.messagesPagination.isLoading = false;
1505
+ if (!isInitialLoad) {
1506
+ this.showMessagesLoadingIndicator(false);
1507
+ }
1508
+ }
1509
+ }
1510
+
1511
+ /**
1512
+ * Render cached messages
1513
+ * @param {Array} messages - Array of messages
1514
+ * @param {boolean} prepend - Whether to prepend messages (for infinite scroll)
1515
+ */
1516
+ renderCachedMessages(messages, prepend = false) {
1517
+
1518
+
1519
+ const messagesContent = this.container.querySelector('#messages-content');
1520
+ if (!messagesContent) {
1521
+ console.warn(`⚠️ messages-content element not found!`);
1522
+ return;
1523
+ }
1524
+
1525
+ // Store messages globally for tool result lookup
1526
+ if (typeof window !== 'undefined') {
1527
+ window.currentMessages = messages;
1528
+ }
1529
+
1530
+ const messageHTML = `
1531
+ <div class="messages-loading-indicator" style="display: none;">
1532
+ <div class="loading-spinner small"></div>
1533
+ <span>Loading older messages...</span>
1534
+ </div>
1535
+ <div class="messages-list">
1536
+ ${messages.map(msg => this.renderMessage(msg)).join('')}
1537
+ </div>
1538
+ `;
1539
+
1540
+ if (prepend) {
1541
+ // For infinite scroll, we need to maintain scroll position
1542
+ const oldScrollHeight = messagesContent.scrollHeight;
1543
+
1544
+ // Update content
1545
+ messagesContent.innerHTML = messageHTML;
1546
+
1547
+ // Restore scroll position relative to the bottom
1548
+ const newScrollHeight = messagesContent.scrollHeight;
1549
+ const scrollDifference = newScrollHeight - oldScrollHeight;
1550
+ messagesContent.scrollTop += scrollDifference;
1551
+ } else {
1552
+ // Initial load - just replace content and scroll to bottom
1553
+ messagesContent.innerHTML = messageHTML;
1554
+
1555
+ // Scroll to bottom for new conversation load
1556
+ setTimeout(() => {
1557
+ messagesContent.scrollTop = messagesContent.scrollHeight;
1558
+ }, 100);
1559
+ }
1560
+
1561
+ // Bind tool display events
1562
+ this.toolDisplay.bindEvents(messagesContent);
1563
+ }
1564
+
1565
+ /**
1566
+ * Show/hide messages loading indicator
1567
+ * @param {boolean} show - Whether to show the indicator
1568
+ */
1569
+ showMessagesLoadingIndicator(show) {
1570
+ const messagesContent = this.container.querySelector('#messages-content');
1571
+ if (!messagesContent) return;
1572
+
1573
+ const indicator = messagesContent.querySelector('.messages-loading-indicator');
1574
+ if (indicator) {
1575
+ indicator.style.display = show ? 'flex' : 'none';
1576
+ }
1577
+ }
1578
+
1579
+ /**
1580
+ * Setup scroll listener for infinite scroll in messages
1581
+ * @param {string} conversationId - Current conversation ID
1582
+ */
1583
+ setupMessagesScrollListener(conversationId) {
1584
+ const messagesContent = this.container.querySelector('#messages-content');
1585
+ if (!messagesContent) return;
1586
+
1587
+ // Remove existing listener if any
1588
+ if (this.messagesScrollListener) {
1589
+ messagesContent.removeEventListener('scroll', this.messagesScrollListener);
1590
+ }
1591
+
1592
+ // Create new listener
1593
+ this.messagesScrollListener = () => {
1594
+ // Check if we've scrolled near the top (for loading older messages)
1595
+ const scrollTop = messagesContent.scrollTop;
1596
+ const threshold = 100; // pixels from top
1597
+
1598
+ if (scrollTop <= threshold && this.messagesPagination.hasMore && !this.messagesPagination.isLoading) {
1599
+ this.loadMoreMessages(conversationId, false);
1600
+ }
1601
+ };
1602
+
1603
+ // Add listener
1604
+ messagesContent.addEventListener('scroll', this.messagesScrollListener);
1605
+ }
1606
+
1607
+ /**
1608
+ * Render a single message with terminal-style formatting
1609
+ * @param {Object} message - Message object
1610
+ * @returns {string} HTML string
1611
+ */
1612
+ renderMessage(message) {
1613
+ const timestamp = this.formatRelativeTime(new Date(message.timestamp));
1614
+ const fullTimestamp = new Date(message.timestamp).toLocaleString();
1615
+ // Compact summaries should be displayed as assistant messages even if marked as 'user'
1616
+ const isUser = message.role === 'user' && !message.isCompactSummary;
1617
+
1618
+
1619
+ // Detect if message contains tools
1620
+ const hasTools = Array.isArray(message.content) &&
1621
+ message.content.some(block => block.type === 'tool_use');
1622
+ const toolCount = hasTools ?
1623
+ message.content.filter(block => block.type === 'tool_use').length : 0;
1624
+
1625
+ // Terminal-style prompt
1626
+ const prompt = isUser ? '>' : '#';
1627
+ const roleLabel = isUser ? 'user' : 'claude';
1628
+
1629
+ // Get message ID (short version for display)
1630
+ const messageId = message.id ? message.id.slice(-8) : 'unknown';
1631
+
1632
+ return `
1633
+ <div class="terminal-message ${isUser ? 'user' : 'assistant'}" data-message-id="${message.id || ''}">
1634
+ <div class="message-container">
1635
+ <div class="message-prompt">
1636
+ <span class="prompt-char">${prompt}</span>
1637
+ <div class="message-metadata">
1638
+ <span class="timestamp" title="${fullTimestamp}">${timestamp}</span>
1639
+ <span class="role-label">${roleLabel}</span>
1640
+ <span class="message-id" title="Message ID: ${message.id || 'unknown'}">[${messageId}]</span>
1641
+ ${message.usage ? `
1642
+ <span class="tokens">
1643
+ ${message.usage.input_tokens > 0 ? `i:${message.usage.input_tokens}` : ''}
1644
+ ${message.usage.output_tokens > 0 ? `o:${message.usage.output_tokens}` : ''}
1645
+ ${message.usage.cache_read_input_tokens > 0 ? `c:${message.usage.cache_read_input_tokens}` : ''}
1646
+ </span>
1647
+ ` : ''}
1648
+ ${hasTools ? `<span class="tool-count">[${toolCount}t]</span>` : ''}
1649
+ ${message.model ? `<span class="model">[${message.model.replace('claude-', '').replace('-20250514', '')}]</span>` : ''}
1650
+ </div>
1651
+ </div>
1652
+ <div class="message-body">
1653
+ ${this.formatMessageContent(message.content, message)}
1654
+ </div>
1655
+ </div>
1656
+ </div>
1657
+ `;
1658
+ }
1659
+
1660
+
1661
+ /**
1662
+ * Format message content with support for text and tool calls
1663
+ * @param {string|Array} content - Message content
1664
+ * @returns {string} Formatted HTML
1665
+ */
1666
+ formatMessageContent(content, message = null) {
1667
+ let result = '';
1668
+
1669
+ // Handle different content formats
1670
+ if (Array.isArray(content)) {
1671
+ // Assistant messages with content blocks
1672
+ content.forEach((block, index) => {
1673
+ if (block.type === 'text') {
1674
+ result += this.formatTextContent(block.text);
1675
+ } else if (block.type === 'tool_use') {
1676
+ // Log only tool rendering for debugging
1677
+ console.log('🔧 WebSocket: Rendering tool', { name: block.name, hasResults: !!message?.toolResults });
1678
+ result += this.toolDisplay.renderToolUse(block, message?.toolResults);
1679
+ } else if (block.type === 'tool_result') {
1680
+ result += this.toolDisplay.renderToolResult(block);
1681
+ }
1682
+ });
1683
+ } else if (typeof content === 'string' && content.trim() !== '') {
1684
+ // User messages with plain text - check for special patterns
1685
+ if (content.includes('Tool Result') && content.length > 1000) {
1686
+ // This is likely a large tool result that should be handled specially
1687
+ result += this.formatLargeToolResult(content);
1688
+ } else {
1689
+ // Check if this is a confirmation response "[ok]" or similar
1690
+ const enhancedContent = this.enhanceConfirmationMessage(content, message);
1691
+ result = this.formatTextContent(enhancedContent);
1692
+ }
1693
+ } else if (content && typeof content === 'object') {
1694
+ // Handle edge cases where content might be an object
1695
+ result = this.formatTextContent(JSON.stringify(content, null, 2));
1696
+ }
1697
+
1698
+ return result || '<em class="empty-content">No displayable content available</em>';
1699
+ }
1700
+
1701
+ /**
1702
+ * Format regular text content with enhanced Markdown support
1703
+ * @param {string} text - Text content
1704
+ * @returns {string} Formatted HTML
1705
+ */
1706
+ /**
1707
+ * Apply markdown formatting to HTML-escaped text
1708
+ * @param {string} escapedText - HTML-escaped text to format
1709
+ * @returns {string} Formatted text with markdown styling
1710
+ */
1711
+ applyMarkdownFormatting(escapedText) {
1712
+ let formattedText = escapedText;
1713
+
1714
+ // 1. Code blocks (must be first to avoid conflicts)
1715
+ formattedText = formattedText
1716
+ .replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre class="code-block" data-language="$1"><code>$2</code></pre>');
1717
+
1718
+ // 2. Headers (h1-h6)
1719
+ formattedText = formattedText
1720
+ .replace(/^### (.*$)/gm, '<h3 class="markdown-h3">$1</h3>')
1721
+ .replace(/^## (.*$)/gm, '<h2 class="markdown-h2">$1</h2>')
1722
+ .replace(/^# (.*$)/gm, '<h1 class="markdown-h1">$1</h1>')
1723
+ .replace(/^#### (.*$)/gm, '<h4 class="markdown-h4">$1</h4>')
1724
+ .replace(/^##### (.*$)/gm, '<h5 class="markdown-h5">$1</h5>')
1725
+ .replace(/^###### (.*$)/gm, '<h6 class="markdown-h6">$1</h6>');
1726
+
1727
+ // 3. Bold and italic text
1728
+ formattedText = formattedText
1729
+ .replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em class="markdown-bold-italic">$1</em></strong>')
1730
+ .replace(/\*\*(.*?)\*\*/g, '<strong class="markdown-bold">$1</strong>')
1731
+ .replace(/\*(.*?)\*/g, '<em class="markdown-italic">$1</em>')
1732
+ .replace(/\_\_\_(.*?)\_\_\_/g, '<strong><em class="markdown-bold-italic">$1</em></strong>')
1733
+ .replace(/\_\_(.*?)\_\_/g, '<strong class="markdown-bold">$1</strong>')
1734
+ .replace(/\_(.*?)\_/g, '<em class="markdown-italic">$1</em>');
1735
+
1736
+ // 4. Strikethrough
1737
+ formattedText = formattedText
1738
+ .replace(/~~(.*?)~~/g, '<del class="markdown-strikethrough">$1</del>');
1739
+
1740
+ // 5. Links
1741
+ formattedText = formattedText
1742
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="markdown-link" target="_blank" rel="noopener noreferrer">$1</a>');
1743
+
1744
+ // 6. Inline code (after other formatting to avoid conflicts)
1745
+ formattedText = formattedText
1746
+ .replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
1747
+
1748
+ // 7. Lists (unordered)
1749
+ formattedText = formattedText
1750
+ .replace(/^[\s]*[\*\-\+][\s]+(.*)$/gm, '<li class="markdown-list-item">$1</li>');
1751
+
1752
+ // 8. Lists (ordered)
1753
+ formattedText = formattedText
1754
+ .replace(/^[\s]*\d+\.[\s]+(.*)$/gm, '<li class="markdown-ordered-item">$1</li>');
1755
+
1756
+ // 9. Wrap consecutive list items in ul/ol tags
1757
+ formattedText = formattedText
1758
+ .replace(/(<li class="markdown-list-item">.*<\/li>)/gs, (match) => {
1759
+ return '<ul class="markdown-list">' + match + '</ul>';
1760
+ })
1761
+ .replace(/(<li class="markdown-ordered-item">.*<\/li>)/gs, (match) => {
1762
+ return '<ol class="markdown-ordered-list">' + match + '</ol>';
1763
+ });
1764
+
1765
+ // 10. Blockquotes
1766
+ formattedText = formattedText
1767
+ .replace(/^&gt;[\s]*(.*)$/gm, '<blockquote class="markdown-blockquote">$1</blockquote>');
1768
+
1769
+ // 11. Horizontal rules
1770
+ formattedText = formattedText
1771
+ .replace(/^[\s]*---[\s]*$/gm, '<hr class="markdown-hr">')
1772
+ .replace(/^[\s]*\*\*\*[\s]*$/gm, '<hr class="markdown-hr">');
1773
+
1774
+ // 12. Line breaks (last to avoid conflicts)
1775
+ formattedText = formattedText
1776
+ .replace(/\n\n/g, '</p><p class="markdown-paragraph">')
1777
+ .replace(/\n/g, '<br>');
1778
+
1779
+ // 13. Wrap in paragraph if not already wrapped
1780
+ if (!formattedText.includes('<p') && !formattedText.includes('<h') &&
1781
+ !formattedText.includes('<ul') && !formattedText.includes('<ol') &&
1782
+ !formattedText.includes('<blockquote')) {
1783
+ formattedText = '<p class="markdown-paragraph">' + formattedText + '</p>';
1784
+ }
1785
+
1786
+ return formattedText;
1787
+ }
1788
+
1789
+ formatTextContent(text) {
1790
+ if (!text || text.trim() === '') return '';
1791
+
1792
+ // Escape HTML to prevent XSS
1793
+ const escapeHtml = (str) => {
1794
+ const div = document.createElement('div');
1795
+ div.textContent = str;
1796
+ return div.innerHTML;
1797
+ };
1798
+
1799
+ // Check if text is too long and needs truncation
1800
+ const lines = text.split('\n');
1801
+ const maxVisibleLines = 20; // Increased from 5 to 20 for better visibility
1802
+
1803
+ if (lines.length > maxVisibleLines) {
1804
+ const visibleLines = lines.slice(0, maxVisibleLines);
1805
+ const hiddenLinesCount = lines.length - maxVisibleLines;
1806
+ const contentId = 'text_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
1807
+
1808
+ // Store full content for modal
1809
+ if (typeof window !== 'undefined') {
1810
+ window.storedContent = window.storedContent || {};
1811
+ window.storedContent[contentId] = text;
1812
+ }
1813
+
1814
+ const previewText = escapeHtml(visibleLines.join('\n'));
1815
+ const showMoreButton = `<button class="show-results-btn text-expand-btn" data-content-id="${contentId}">Show +${hiddenLinesCount} lines</button>`;
1816
+
1817
+ // Apply markdown formatting to preview
1818
+ let formattedPreview = this.applyMarkdownFormatting(previewText);
1819
+
1820
+ return `<div class="text-content-preview">${formattedPreview}<div class="text-expand-section"><span class="continuation">… +${hiddenLinesCount} lines hidden</span> ${showMoreButton}</div></div>`;
1821
+ }
1822
+
1823
+ // For non-truncated content, apply full formatting
1824
+ let formattedText = escapeHtml(text);
1825
+ formattedText = this.applyMarkdownFormatting(formattedText);
1826
+
1827
+ return formattedText;
1828
+ }
1829
+
1830
+ /**
1831
+ * Format large tool result content safely
1832
+ * @param {string} content - Large tool result content
1833
+ * @returns {string} Safe formatted content
1834
+ */
1835
+ formatLargeToolResult(content) {
1836
+ // Extract tool result ID if present
1837
+ const toolIdMatch = content.match(/Tool Result\s+([A-Za-z0-9]+)/);
1838
+ const toolId = toolIdMatch ? toolIdMatch[1] : 'unknown';
1839
+
1840
+ const escapeHtml = (str) => {
1841
+ const div = document.createElement('div');
1842
+ div.textContent = str;
1843
+ return div.innerHTML;
1844
+ };
1845
+
1846
+ const preview = content.length > 80
1847
+ ? escapeHtml(content.substring(0, 80)) + '...'
1848
+ : escapeHtml(content);
1849
+
1850
+ return `
1851
+ <div class="terminal-tool tool-result large">
1852
+ <span class="tool-prompt">></span>
1853
+ <span class="tool-status">[LARGE]</span>
1854
+ <span class="tool-id">[${toolId}]</span>
1855
+ <span class="tool-output">${content.length}b: ${preview}</span>
1856
+ </div>
1857
+ `;
1858
+ }
1859
+
1860
+ /**
1861
+ * Enhance confirmation messages like "[ok]" with context information
1862
+ * @param {string} content - Original message content
1863
+ * @param {Object} message - Full message object with metadata
1864
+ * @returns {string} Enhanced message content
1865
+ */
1866
+ enhanceConfirmationMessage(content, message) {
1867
+ const trimmedContent = content.trim();
1868
+
1869
+ // Detect simple confirmation patterns
1870
+ const confirmationPatterns = [
1871
+ /^\[ok\]$/i,
1872
+ /^ok$/i,
1873
+ /^yes$/i,
1874
+ /^\[yes\]$/i,
1875
+ /^y$/i,
1876
+ /^\[y\]$/i,
1877
+ /^1$/, // Choice selection
1878
+ /^2$/,
1879
+ /^3$/
1880
+ ];
1881
+
1882
+ const isConfirmation = confirmationPatterns.some(pattern => pattern.test(trimmedContent));
1883
+
1884
+ if (isConfirmation && message) {
1885
+ // Try to extract context from the message timestamp
1886
+ const messageTime = message.timestamp ? new Date(message.timestamp).toLocaleTimeString() : 'unknown time';
1887
+
1888
+ // Enhanced display for confirmation messages
1889
+ return `${content} <span class="confirmation-context">(User confirmation at ${messageTime})</span>`;
1890
+ }
1891
+
1892
+ // For other potential confirmation-like messages, check if they seem like choices
1893
+ if (/^[1-9]$/.test(trimmedContent)) {
1894
+ return `${content} <span class="confirmation-context">(Menu selection)</span>`;
1895
+ }
1896
+
1897
+ // Check for common CLI responses
1898
+ if (/^(continue|proceed|accept|confirm|done)$/i.test(trimmedContent)) {
1899
+ return `${content} <span class="confirmation-context">(User command)</span>`;
1900
+ }
1901
+
1902
+ return content;
1903
+ }
1904
+
1905
+ /**
1906
+ * Format relative time
1907
+ * @param {Date} date - Date to format
1908
+ * @returns {string} Relative time string
1909
+ */
1910
+ formatRelativeTime(date) {
1911
+ const now = new Date();
1912
+ const diffMs = now - date;
1913
+ const diffSecs = Math.floor(diffMs / 1000);
1914
+ const diffMins = Math.floor(diffSecs / 60);
1915
+ const diffHours = Math.floor(diffMins / 60);
1916
+ const diffDays = Math.floor(diffHours / 24);
1917
+
1918
+ if (diffSecs < 60) return 'Just now';
1919
+ if (diffMins < 60) return `${diffMins}m ago`;
1920
+ if (diffHours < 24) return `${diffHours}h ago`;
1921
+ if (diffDays < 7) return `${diffDays}d ago`;
1922
+ return date.toLocaleDateString();
1923
+ }
1924
+
1925
+ /**
1926
+ * Update clear filters button visibility
1927
+ */
1928
+ updateClearFiltersButton() {
1929
+ const clearBtn = this.container.querySelector('#clear-filters');
1930
+ if (!clearBtn) return; // Guard against null when AgentsPage isn't rendered
1931
+
1932
+ const hasActiveFilters = this.filters.status !== 'all' ||
1933
+ this.filters.timeRange !== '7d' ||
1934
+ this.filters.search !== '';
1935
+ clearBtn.style.display = hasActiveFilters ? 'inline-block' : 'none';
1936
+ }
1937
+
1938
+ /**
1939
+ * Handle list action
1940
+ * @param {string} action - Action type
1941
+ * @param {string} conversationId - Conversation ID
1942
+ */
1943
+ handleListAction(action, conversationId) {
1944
+ switch (action) {
1945
+ case 'view':
1946
+ this.viewConversation(conversationId);
1947
+ break;
1948
+ }
1949
+ }
1950
+
1951
+ /**
1952
+ * Filter conversations based on current filters
1953
+ * @param {Array} conversations - All conversations
1954
+ * @param {Object} states - Conversation states
1955
+ * @returns {Array} Filtered conversations
1956
+ */
1957
+ filterConversations(conversations, states) {
1958
+ let filtered = conversations;
1959
+
1960
+ // Filter by status
1961
+ if (this.filters.status !== 'all') {
1962
+ filtered = filtered.filter(conv => {
1963
+ const state = states[conv.id] || 'unknown';
1964
+ const category = this.getStateCategory(state);
1965
+ return category === this.filters.status;
1966
+ });
1967
+ }
1968
+
1969
+ // Filter by time range
1970
+ const timeRange = this.getTimeRangeMs(this.filters.timeRange);
1971
+ if (timeRange > 0) {
1972
+ const cutoff = Date.now() - timeRange;
1973
+ filtered = filtered.filter(conv => {
1974
+ const lastModified = new Date(conv.lastModified).getTime();
1975
+ return lastModified >= cutoff;
1976
+ });
1977
+ }
1978
+
1979
+ // Filter by search
1980
+ if (this.filters.search) {
1981
+ const searchLower = this.filters.search.toLowerCase();
1982
+ filtered = filtered.filter(conv => {
1983
+ return (conv.title || '').toLowerCase().includes(searchLower) ||
1984
+ (conv.project || '').toLowerCase().includes(searchLower) ||
1985
+ (conv.lastMessage || '').toLowerCase().includes(searchLower);
1986
+ });
1987
+ }
1988
+
1989
+ return filtered;
1990
+ }
1991
+
1992
+ /**
1993
+ * Get time range in milliseconds
1994
+ * @param {string} range - Time range string
1995
+ * @returns {number} Milliseconds
1996
+ */
1997
+ getTimeRangeMs(range) {
1998
+ const ranges = {
1999
+ '1h': 60 * 60 * 1000,
2000
+ '24h': 24 * 60 * 60 * 1000,
2001
+ '7d': 7 * 24 * 60 * 60 * 1000,
2002
+ '30d': 30 * 24 * 60 * 60 * 1000
2003
+ };
2004
+ return ranges[range] || 0;
2005
+ }
2006
+
2007
+ /**
2008
+ * Get state category for filtering
2009
+ * @param {string} state - Detailed conversation state
2010
+ * @returns {string} Category: 'active' or 'inactive'
2011
+ */
2012
+ getStateCategory(state) {
2013
+ // Active states - conversation is currently being used or recently active
2014
+ const activeStates = [
2015
+ 'Claude Code working...',
2016
+ 'Awaiting user input...',
2017
+ 'User typing...',
2018
+ 'Awaiting response...',
2019
+ 'Recently active'
2020
+ ];
2021
+
2022
+ // Inactive states - conversation is idle or old
2023
+ const inactiveStates = [
2024
+ 'Idle',
2025
+ 'Inactive',
2026
+ 'Old',
2027
+ 'unknown'
2028
+ ];
2029
+
2030
+ if (activeStates.includes(state)) {
2031
+ return 'active';
2032
+ } else if (inactiveStates.includes(state)) {
2033
+ return 'inactive';
2034
+ } else {
2035
+ // Default for any unknown states
2036
+ return 'inactive';
2037
+ }
2038
+ }
2039
+
2040
+ /**
2041
+ * Get simple conversation preview text (avoids repeating metadata)
2042
+ * @param {Object} conv - Conversation object
2043
+ * @returns {string} Preview text
2044
+ */
2045
+ getSimpleConversationPreview(conv) {
2046
+ // If we have a last message, show it (this is the most useful info)
2047
+ if (conv.lastMessage && conv.lastMessage.trim()) {
2048
+ const lastMsg = conv.lastMessage.trim();
2049
+
2050
+ // Check if last message is a simple confirmation and try to make it more descriptive
2051
+ if (this.isSimpleConfirmation(lastMsg)) {
2052
+ const messageCount = conv.messageCount || 0;
2053
+ const lastActivity = conv.lastModified ? this.formatRelativeTime(new Date(conv.lastModified)) : 'recently';
2054
+ return `User confirmed action • ${messageCount} messages • ${lastActivity}`;
2055
+ }
2056
+
2057
+ // Check if it's a tool-related message
2058
+ if (lastMsg.includes('Tool Result') || lastMsg.includes('[Tool:')) {
2059
+ return `Tool execution completed • ${this.truncateText(lastMsg, 60)}`;
2060
+ }
2061
+
2062
+ return this.truncateText(lastMsg, 80);
2063
+ }
2064
+
2065
+ // For empty conversations, show descriptive text
2066
+ const messageCount = conv.messageCount || 0;
2067
+ if (messageCount === 0) {
2068
+ return 'Empty conversation - click to start chatting';
2069
+ }
2070
+
2071
+ // For conversations without lastMessage but with messages, show informative text
2072
+ const lastActivity = conv.lastModified ? this.formatRelativeTime(new Date(conv.lastModified)) : 'unknown';
2073
+ return `${messageCount} messages • Last activity ${lastActivity}`;
2074
+ }
2075
+
2076
+ /**
2077
+ * Check if a message is a simple confirmation
2078
+ * @param {string} message - Message content
2079
+ * @returns {boolean} True if it's a simple confirmation
2080
+ */
2081
+ isSimpleConfirmation(message) {
2082
+ const trimmed = message.trim();
2083
+ const confirmationPatterns = [
2084
+ /^\[ok\]$/i,
2085
+ /^ok$/i,
2086
+ /^yes$/i,
2087
+ /^\[yes\]$/i,
2088
+ /^y$/i,
2089
+ /^\[y\]$/i,
2090
+ /^[1-9]$/, // Choice selection
2091
+ /^(continue|proceed|accept|confirm|done)$/i
2092
+ ];
2093
+
2094
+ return confirmationPatterns.some(pattern => pattern.test(trimmed));
2095
+ }
2096
+
2097
+ /**
2098
+ * Get conversation preview text (legacy method - still used in other places)
2099
+ * @param {Object} conv - Conversation object
2100
+ * @param {string} state - Conversation state
2101
+ * @returns {string} Preview text
2102
+ */
2103
+ getConversationPreview(conv, state) {
2104
+ // If we have a last message, show it
2105
+ if (conv.lastMessage && conv.lastMessage.trim()) {
2106
+ return this.truncateText(conv.lastMessage, 60);
2107
+ }
2108
+
2109
+ // Otherwise, show conversation info based on state and metadata
2110
+ const messageCount = conv.messageCount || 0;
2111
+
2112
+ if (messageCount === 0) {
2113
+ return `Empty conversation • Project: ${conv.project || 'Unknown'}`;
2114
+ }
2115
+
2116
+ // Show state-based preview
2117
+ if (state === 'Claude Code working...') {
2118
+ return `Claude is working • ${messageCount} messages`;
2119
+ } else if (state === 'Awaiting user input...') {
2120
+ return `Waiting for your input • ${messageCount} messages`;
2121
+ } else if (state === 'User typing...') {
2122
+ return `Ready for your message • ${messageCount} messages`;
2123
+ } else if (state === 'Recently active') {
2124
+ return `Recently active • ${messageCount} messages`;
2125
+ } else {
2126
+ return `${messageCount} messages • Last active ${this.formatRelativeTime(new Date(conv.lastModified))}`;
2127
+ }
2128
+ }
2129
+
2130
+ /**
2131
+ * Get state CSS class
2132
+ * @param {string} state - Conversation state
2133
+ * @returns {string} CSS class
2134
+ */
2135
+ getStateClass(state) {
2136
+ const stateClasses = {
2137
+ 'Claude Code working...': 'status-active',
2138
+ 'Awaiting user input...': 'status-waiting',
2139
+ 'User typing...': 'status-typing',
2140
+ 'Awaiting response...': 'status-pending',
2141
+ 'Recently active': 'status-recent',
2142
+ 'Idle': 'status-idle',
2143
+ 'Inactive': 'status-inactive',
2144
+ 'Old': 'status-old',
2145
+ 'unknown': 'status-unknown'
2146
+ };
2147
+ return stateClasses[state] || 'status-unknown';
2148
+ }
2149
+
2150
+ /**
2151
+ * Get state label
2152
+ * @param {string} state - Conversation state
2153
+ * @returns {string} Human readable label
2154
+ */
2155
+ getStateLabel(state) {
2156
+ const stateLabels = {
2157
+ 'Claude Code working...': 'Working',
2158
+ 'Awaiting user input...': 'Awaiting input',
2159
+ 'User typing...': 'Typing',
2160
+ 'Awaiting response...': 'Awaiting response',
2161
+ 'Recently active': 'Recent',
2162
+ 'Idle': 'Idle',
2163
+ 'Inactive': 'Inactive',
2164
+ 'Old': 'Old',
2165
+ 'unknown': 'Unknown'
2166
+ };
2167
+ return stateLabels[state] || state;
2168
+ }
2169
+
2170
+ /**
2171
+ * Truncate text to specified length
2172
+ * @param {string} text - Text to truncate
2173
+ * @param {number} maxLength - Maximum length
2174
+ * @returns {string} Truncated text
2175
+ */
2176
+ truncateText(text, maxLength) {
2177
+ if (!text || text.length <= maxLength) return text;
2178
+ return text.substring(0, maxLength - 3) + '...';
2179
+ }
2180
+
2181
+ /**
2182
+ * Update filter
2183
+ * @param {string} filterName - Filter name
2184
+ * @param {string} value - Filter value
2185
+ */
2186
+ updateFilter(filterName, value) {
2187
+ this.filters[filterName] = value;
2188
+ // When filters change, restart from beginning
2189
+ this.refreshFromBeginning();
2190
+ }
2191
+
2192
+ /**
2193
+ * Clear search
2194
+ */
2195
+ clearSearch() {
2196
+ const searchInput = this.container.querySelector('#search-filter');
2197
+ if (!searchInput) return; // Guard against null when AgentsPage isn't rendered
2198
+
2199
+ searchInput.value = '';
2200
+ this.updateFilter('search', '');
2201
+ }
2202
+
2203
+ /**
2204
+ * Clear all filters
2205
+ */
2206
+ clearAllFilters() {
2207
+ this.filters = {
2208
+ status: 'all',
2209
+ timeRange: '7d',
2210
+ search: ''
2211
+ };
2212
+
2213
+ // Reset UI
2214
+ const statusFilter = this.container.querySelector('#status-filter');
2215
+ const timeFilter = this.container.querySelector('#time-filter');
2216
+ const searchFilter = this.container.querySelector('#search-filter');
2217
+
2218
+ if (statusFilter) statusFilter.value = 'all';
2219
+ if (timeFilter) timeFilter.value = '7d';
2220
+ if (searchFilter) searchFilter.value = '';
2221
+
2222
+ // Restart from beginning when clearing filters
2223
+ this.refreshFromBeginning();
2224
+ }
2225
+
2226
+ /**
2227
+ * Refresh conversations display
2228
+ */
2229
+ refreshConversationsDisplay() {
2230
+ const conversations = this.stateService.getStateProperty('conversations') || [];
2231
+ const statesData = this.stateService.getStateProperty('conversationStates') || {};
2232
+ // Extract activeStates from the stored state data
2233
+ const activeStates = statesData?.activeStates || {};
2234
+ this.renderConversationsList(conversations, activeStates);
2235
+ }
2236
+
2237
+ /**
2238
+ * Refresh from beginning - resets pagination
2239
+ */
2240
+ async refreshFromBeginning() {
2241
+ // Clear cache
2242
+ this.loadedConversations = [];
2243
+ this.loadedMessages.clear();
2244
+
2245
+ // Reset pagination
2246
+ this.pagination = {
2247
+ currentPage: 0,
2248
+ limit: 10,
2249
+ hasMore: true,
2250
+ isLoading: false
2251
+ };
2252
+
2253
+ // Clear list and reload
2254
+ const listContainer = this.container.querySelector('#conversations-list');
2255
+ if (listContainer) {
2256
+ listContainer.innerHTML = '';
2257
+ }
2258
+
2259
+ await this.loadConversationsData();
2260
+ }
2261
+
2262
+ /**
2263
+ * Refresh conversations data
2264
+ */
2265
+ async refreshConversations() {
2266
+ const refreshBtn = this.container.querySelector('#refresh-conversations');
2267
+ if (!refreshBtn) return; // Guard against null when AgentsPage isn't rendered
2268
+
2269
+ refreshBtn.disabled = true;
2270
+ const iconElement = refreshBtn.querySelector('.btn-icon');
2271
+ if (iconElement) {
2272
+ iconElement.style.animation = 'spin 1s linear infinite';
2273
+ }
2274
+
2275
+ try {
2276
+ // Clear both server and client cache to force fresh data
2277
+ await this.dataService.clearServerCache('conversations');
2278
+ await this.loadConversationsData();
2279
+ } catch (error) {
2280
+ console.error('Error refreshing conversations:', error);
2281
+ this.stateService.setError('Failed to refresh conversations');
2282
+ } finally {
2283
+ refreshBtn.disabled = false;
2284
+ if (iconElement) {
2285
+ iconElement.style.animation = '';
2286
+ }
2287
+ }
2288
+ }
2289
+
2290
+ /**
2291
+ * Check if there are active filters
2292
+ * @returns {boolean} True if filters are active
2293
+ */
2294
+ hasActiveFilters() {
2295
+ const searchInput = this.container.querySelector('#conversation-search');
2296
+ const searchTerm = searchInput ? searchInput.value.trim().toLowerCase() : '';
2297
+
2298
+ // Check if search filter is active
2299
+ if (searchTerm) {
2300
+ return true;
2301
+ }
2302
+
2303
+ // Check if state filters are active
2304
+ const filterButtons = this.container.querySelectorAll('.filter-btn');
2305
+ const activeFilters = Array.from(filterButtons).filter(btn =>
2306
+ btn.classList.contains('active') && btn.getAttribute('data-state') !== 'all'
2307
+ );
2308
+
2309
+ return activeFilters.length > 0;
2310
+ }
2311
+
2312
+ /**
2313
+ * Update results count
2314
+ * @param {number} count - Number of results
2315
+ * @param {boolean} hasActiveFilters - Whether filters are active
2316
+ */
2317
+ updateResultsCount(count, hasActiveFilters = false) {
2318
+ // Update main results count
2319
+ const resultsCount = this.container.querySelector('#results-count');
2320
+ if (resultsCount) {
2321
+ let countText = `${count} conversation${count !== 1 ? 's' : ''} found`;
2322
+ if (hasActiveFilters && this.pagination && this.pagination.totalCount && count < this.pagination.totalCount) {
2323
+ countText += ` (filtered from ${this.pagination.totalCount})`;
2324
+ }
2325
+ resultsCount.textContent = countText;
2326
+ }
2327
+
2328
+ // Update sidebar count
2329
+ const sidebarCount = this.container.querySelector('#sidebar-count');
2330
+ if (sidebarCount) {
2331
+ sidebarCount.textContent = count;
2332
+ }
2333
+ }
2334
+
2335
+ /**
2336
+ * Show empty state
2337
+ */
2338
+ showEmptyState() {
2339
+ const conversationsList = this.container.querySelector('#conversations-list');
2340
+ const emptyState = this.container.querySelector('#empty-state');
2341
+ if (!conversationsList || !emptyState) return; // Guard against null when AgentsPage isn't rendered
2342
+
2343
+ conversationsList.style.display = 'none';
2344
+ emptyState.style.display = 'flex';
2345
+ }
2346
+
2347
+ /**
2348
+ * Hide empty state
2349
+ */
2350
+ hideEmptyState() {
2351
+ const conversationsList = this.container.querySelector('#conversations-list');
2352
+ const emptyState = this.container.querySelector('#empty-state');
2353
+ if (!conversationsList || !emptyState) return; // Guard against null when AgentsPage isn't rendered
2354
+
2355
+ conversationsList.style.display = 'block';
2356
+ emptyState.style.display = 'none';
2357
+ }
2358
+
2359
+ /**
2360
+ * Toggle between grid and table view
2361
+ * @param {string} view - View type ('grid' or 'table')
2362
+ */
2363
+ toggleView(view) {
2364
+ const toggleBtns = this.container.querySelectorAll('.toggle-btn');
2365
+ toggleBtns.forEach(btn => {
2366
+ btn.classList.toggle('active', btn.dataset.view === view);
2367
+ });
2368
+
2369
+ const gridElement = this.container.querySelector('#conversations-grid');
2370
+ const tableSection = this.container.querySelector('.conversations-table-section');
2371
+
2372
+ if (!gridElement || !tableSection) return; // Guard against null when AgentsPage isn't rendered
2373
+
2374
+ const gridSection = gridElement.parentNode;
2375
+
2376
+ if (view === 'table') {
2377
+ gridSection.style.display = 'none';
2378
+ tableSection.style.display = 'block';
2379
+ } else {
2380
+ gridSection.style.display = 'block';
2381
+ tableSection.style.display = 'none';
2382
+ }
2383
+ }
2384
+
2385
+ /**
2386
+ * View conversation details
2387
+ * @param {string} conversationId - Conversation ID
2388
+ */
2389
+ viewConversation(conversationId) {
2390
+ // This would open a detailed conversation view
2391
+ // Implementation would show conversation details modal or navigate to detail page
2392
+ }
2393
+
2394
+ /**
2395
+ * Export single conversation
2396
+ * @param {string} conversationId - Conversation ID
2397
+ */
2398
+ exportSingleConversation(conversationId) {
2399
+ const conversations = this.stateService.getStateProperty('conversations') || [];
2400
+ const conversation = conversations.find(conv => conv.id === conversationId);
2401
+
2402
+ if (conversation) {
2403
+ const dataStr = JSON.stringify(conversation, null, 2);
2404
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
2405
+ const url = URL.createObjectURL(dataBlob);
2406
+
2407
+ const link = document.createElement('a');
2408
+ link.href = url;
2409
+ link.download = `conversation-${conversationId}-${new Date().toISOString().split('T')[0]}.json`;
2410
+ link.click();
2411
+
2412
+ URL.revokeObjectURL(url);
2413
+ }
2414
+ }
2415
+
2416
+ /**
2417
+ * Export all conversations
2418
+ */
2419
+ exportConversations() {
2420
+ const conversations = this.stateService.getStateProperty('conversations') || [];
2421
+ const states = this.stateService.getStateProperty('conversationStates') || {};
2422
+ const filteredConversations = this.filterConversations(conversations, states);
2423
+
2424
+ const dataStr = JSON.stringify({
2425
+ conversations: filteredConversations,
2426
+ states: states,
2427
+ exportDate: new Date().toISOString(),
2428
+ filters: this.filters
2429
+ }, null, 2);
2430
+
2431
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
2432
+ const url = URL.createObjectURL(dataBlob);
2433
+
2434
+ const link = document.createElement('a');
2435
+ link.href = url;
2436
+ link.download = `claude-conversations-${new Date().toISOString().split('T')[0]}.json`;
2437
+ link.click();
2438
+
2439
+ URL.revokeObjectURL(url);
2440
+ }
2441
+
2442
+ /**
2443
+ * Update conversations display
2444
+ * @param {Array} conversations - Conversations data
2445
+ */
2446
+ updateConversationsDisplay(conversations) {
2447
+ const statesData = this.stateService.getStateProperty('conversationStates') || {};
2448
+ const activeStates = statesData?.activeStates || {};
2449
+ this.renderConversationsList(conversations, activeStates);
2450
+ }
2451
+
2452
+ /**
2453
+ * Update conversation states
2454
+ * @param {Object} activeStates - Active conversation states (direct object, not nested)
2455
+ */
2456
+ updateConversationStates(activeStates) {
2457
+ const conversations = this.stateService.getStateProperty('conversations') || [];
2458
+
2459
+
2460
+ // Re-render conversation list with new states
2461
+ this.renderConversationsList(conversations, activeStates || {});
2462
+
2463
+ // Update banner if we have a selected conversation
2464
+ if (this.selectedConversationId && activeStates && activeStates[this.selectedConversationId]) {
2465
+ this.updateStateBanner(this.selectedConversationId, activeStates[this.selectedConversationId]);
2466
+ }
2467
+ }
2468
+
2469
+ /**
2470
+ * Handle conversation state change
2471
+ * @param {Object} _state - New state (unused but required by interface)
2472
+ */
2473
+ handleConversationStateChange(_state) {
2474
+ this.refreshConversationsDisplay();
2475
+ }
2476
+
2477
+ /**
2478
+ * Update loading state
2479
+ * @param {boolean} isLoading - Loading state
2480
+ */
2481
+ updateLoadingState(isLoading) {
2482
+ const loadingState = this.container.querySelector('#conversations-loading');
2483
+ if (loadingState) {
2484
+ loadingState.style.display = isLoading ? 'flex' : 'none';
2485
+ }
2486
+ }
2487
+
2488
+ /**
2489
+ * Update error state
2490
+ * @param {Error|string} error - Error object or message
2491
+ */
2492
+ updateErrorState(error) {
2493
+ const errorState = this.container.querySelector('#conversations-error');
2494
+ const errorMessage = this.container.querySelector('.error-message');
2495
+
2496
+ if (errorState && errorMessage) {
2497
+ if (error) {
2498
+ errorMessage.textContent = error.message || error;
2499
+ errorState.style.display = 'flex';
2500
+ } else {
2501
+ errorState.style.display = 'none';
2502
+ }
2503
+ }
2504
+ }
2505
+
2506
+ /**
2507
+ * Destroy agents page
2508
+ */
2509
+ destroy() {
2510
+ // Cleanup components
2511
+ Object.values(this.components).forEach(component => {
2512
+ if (component.destroy) {
2513
+ component.destroy();
2514
+ }
2515
+ });
2516
+
2517
+ // Cleanup scroll listeners
2518
+ const messagesContent = this.container.querySelector('#messages-content');
2519
+ if (messagesContent && this.messagesScrollListener) {
2520
+ messagesContent.removeEventListener('scroll', this.messagesScrollListener);
2521
+ }
2522
+
2523
+ // Unsubscribe from state changes
2524
+ if (this.unsubscribe) {
2525
+ this.unsubscribe();
2526
+ }
2527
+
2528
+ this.isInitialized = false;
2529
+ }
2530
+ }
2531
+
2532
+ // Export for module use
2533
+ if (typeof module !== 'undefined' && module.exports) {
2534
+ module.exports = AgentsPage;
2535
+ }