claude-code-templates 1.10.1 → 1.12.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,4744 @@
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
+ // Agent data
41
+ this.agents = [];
42
+ this.selectedAgentId = null;
43
+
44
+ // State transition tracking for enhanced user experience
45
+ this.lastMessageTime = new Map(); // Track when last message was received per conversation
46
+
47
+ // Initialize tool display component
48
+ this.toolDisplay = new ToolDisplay();
49
+
50
+ // Subscribe to state changes
51
+ this.unsubscribe = this.stateService.subscribe(this.handleStateChange.bind(this));
52
+
53
+ // Subscribe to DataService events for real-time updates
54
+ this.dataService.addEventListener((type, data) => {
55
+ if (type === 'new_message') {
56
+ console.log('🔄 WebSocket: New message received', { conversationId: data.conversationId });
57
+ this.handleNewMessage(data.conversationId, data.message, data.metadata);
58
+ } else if (type === 'console_interaction') {
59
+ console.log('🔄 WebSocket: Console interaction request received', data);
60
+ this.showConsoleInteraction(data);
61
+ }
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Initialize the agents page
67
+ */
68
+ async initialize() {
69
+ if (this.isInitialized) return;
70
+
71
+ try {
72
+ this.stateService.setLoading(true);
73
+ await this.render();
74
+ await this.initializeComponents();
75
+ await this.loadAgentsData();
76
+ await this.loadConversationsData();
77
+ this.isInitialized = true;
78
+ } catch (error) {
79
+ console.error('Error initializing agents page:', error);
80
+ this.stateService.setError(error);
81
+ } finally {
82
+ this.stateService.setLoading(false);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Handle state changes from StateService (WebSocket updates)
88
+ * @param {Object} state - New state
89
+ * @param {string} action - Action that caused the change
90
+ */
91
+ handleStateChange(state, action) {
92
+ switch (action) {
93
+ case 'update_conversations':
94
+ // Don't replace loaded conversations, just update states
95
+ break;
96
+ case 'update_conversation_states':
97
+ console.log('🔄 WebSocket: Conversation states updated', { count: Object.keys(state.conversationStates?.activeStates || state.conversationStates || {}).length });
98
+
99
+ // Handle both direct states object and nested structure
100
+ const activeStates = state.conversationStates?.activeStates || state.conversationStates || {};
101
+
102
+ this.updateConversationStates(activeStates);
103
+ break;
104
+ case 'set_loading':
105
+ this.updateLoadingState(state.isLoading);
106
+ break;
107
+ case 'set_error':
108
+ this.updateErrorState(state.error);
109
+ break;
110
+ case 'conversation_state_change':
111
+ this.handleConversationStateChange(state);
112
+ break;
113
+ case 'data_refresh':
114
+ // On real-time data refresh, update conversation states but keep pagination
115
+ this.updateConversationStatesOnly();
116
+ break;
117
+ case 'new_message':
118
+ // Handle new message in real-time
119
+ this.handleNewMessage(state.conversationId, state.message, state.metadata);
120
+ break;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Handle new message received via WebSocket
126
+ * @param {string} conversationId - Conversation ID that received new message
127
+ * @param {Object} message - New message object
128
+ * @param {Object} metadata - Additional metadata
129
+ */
130
+ handleNewMessage(conversationId, message, metadata) {
131
+ // Log essential message info for debugging
132
+ console.log('🔄 WebSocket: Processing new message', {
133
+ conversationId,
134
+ role: message?.role,
135
+ hasTools: Array.isArray(message?.content) ? message.content.some(b => b.type === 'tool_use') : false,
136
+ hasToolResults: !!message?.toolResults
137
+ });
138
+
139
+ // Always update the message cache for this conversation
140
+ const existingMessages = this.loadedMessages.get(conversationId) || [];
141
+
142
+
143
+ // Track message timing for better state transitions
144
+ const now = Date.now();
145
+ this.lastMessageTime.set(conversationId, now);
146
+
147
+ // IMMEDIATE STATE TRANSITION based on message appearance
148
+ if (this.selectedConversationId === conversationId) {
149
+ if (message?.role === 'user') {
150
+ // User message just appeared - Claude immediately starts working
151
+ console.log('⚡ User message detected - Claude starting work immediately');
152
+ this.updateStateBanner(conversationId, 'Claude Code working...');
153
+ } else if (message?.role === 'assistant') {
154
+ // Assistant message appeared - analyze for specific state
155
+ const intelligentState = this.analyzeMessageForState(message, existingMessages);
156
+ console.log(`🤖 Assistant message detected - state: ${intelligentState}`);
157
+ this.updateStateBanner(conversationId, intelligentState);
158
+
159
+ // No additional timeout needed - state is determined by message content
160
+ }
161
+ }
162
+
163
+ // Check if we already have this message (avoid duplicates)
164
+ const messageExists = existingMessages.some(msg =>
165
+ msg.id === message.id ||
166
+ (msg.timestamp === message.timestamp && msg.role === message.role)
167
+ );
168
+
169
+ if (!messageExists) {
170
+ // Add new message to the end
171
+ const updatedMessages = [...existingMessages, message];
172
+ this.loadedMessages.set(conversationId, updatedMessages);
173
+
174
+ // Refresh only the conversation states to show updated status/timestamp
175
+ // Don't do full reload as it can interfere with message cache
176
+ this.updateConversationStatesOnly();
177
+
178
+ // If this conversation is currently selected, update the messages view
179
+ if (this.selectedConversationId === conversationId) {
180
+ // Re-render messages with new message
181
+ this.renderCachedMessages(updatedMessages, false);
182
+
183
+ // Auto-scroll to new message
184
+ this.scrollToBottom();
185
+ }
186
+
187
+ // Show notification
188
+ this.showNewMessageNotification(message, metadata);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Update only conversation states without affecting pagination
194
+ */
195
+ async updateConversationStatesOnly() {
196
+ try {
197
+ const statesData = await this.dataService.getConversationStates();
198
+ const activeStates = statesData?.activeStates || {};
199
+
200
+ // Update StateService with fresh states
201
+ this.stateService.updateConversationStates(activeStates);
202
+
203
+ // Update states in already loaded conversations
204
+ this.updateConversationStateElements(activeStates);
205
+
206
+ // Update banner if we have a selected conversation
207
+ if (this.selectedConversationId && activeStates[this.selectedConversationId]) {
208
+ this.updateStateBanner(this.selectedConversationId, activeStates[this.selectedConversationId]);
209
+ }
210
+
211
+ } catch (error) {
212
+ console.error('Error updating conversation states:', error);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Analyze a message to determine intelligent conversation state
218
+ * @param {Object} message - The message to analyze
219
+ * @param {Array} existingMessages - Previous messages in conversation
220
+ * @returns {string} Intelligent state description
221
+ */
222
+ analyzeMessageForState(message, existingMessages = []) {
223
+ const role = message?.role;
224
+ const content = message?.content;
225
+ const hasToolResults = !!message?.toolResults && message.toolResults.length > 0;
226
+ const messageTime = new Date(message?.timestamp || Date.now());
227
+ const now = new Date();
228
+ const messageAge = (now - messageTime) / 1000; // seconds
229
+
230
+ if (role === 'assistant') {
231
+ // Analyze assistant messages with enhanced logic
232
+ if (Array.isArray(content)) {
233
+ const hasToolUse = content.some(block => block.type === 'tool_use');
234
+ const hasText = content.some(block => block.type === 'text');
235
+ const textBlocks = content.filter(block => block.type === 'text');
236
+ const toolUseBlocks = content.filter(block => block.type === 'tool_use');
237
+
238
+ // Enhanced tool execution detection with immediate response
239
+ if (hasToolUse) {
240
+ const toolNames = toolUseBlocks.map(tool => tool.name).join(', ');
241
+
242
+ if (!hasToolResults) {
243
+ // Tool just sent - immediate execution state
244
+ console.log(`🔧 Tools detected: ${toolNames} - showing execution state`);
245
+
246
+ if (toolNames.includes('bash') || toolNames.includes('edit') || toolNames.includes('write') || toolNames.includes('multiedit')) {
247
+ return 'Executing tools...';
248
+ } else if (toolNames.includes('read') || toolNames.includes('grep') || toolNames.includes('glob') || toolNames.includes('task')) {
249
+ return 'Analyzing code...';
250
+ } else if (toolNames.includes('webfetch') || toolNames.includes('websearch')) {
251
+ return 'Fetching data...';
252
+ }
253
+ return 'Awaiting tool response...';
254
+ } else {
255
+ // Has tool results - Claude is processing them
256
+ console.log(`📊 Tools completed: ${toolNames} - analyzing results`);
257
+ return 'Analyzing results...';
258
+ }
259
+ }
260
+
261
+ // Enhanced text analysis
262
+ if (hasText) {
263
+ const textContent = textBlocks.map(block => block.text).join(' ').toLowerCase();
264
+
265
+ // Working indicators
266
+ if (textContent.includes('let me') ||
267
+ textContent.includes('i\'ll') ||
268
+ textContent.includes('i will') ||
269
+ textContent.includes('i\'m going to') ||
270
+ textContent.includes('let\'s') ||
271
+ textContent.includes('first, i\'ll') ||
272
+ textContent.includes('now i\'ll')) {
273
+ return 'Claude Code working...';
274
+ }
275
+
276
+ // Analysis indicators
277
+ if (textContent.includes('analyzing') ||
278
+ textContent.includes('examining') ||
279
+ textContent.includes('looking at') ||
280
+ textContent.includes('reviewing')) {
281
+ return 'Analyzing code...';
282
+ }
283
+
284
+ // Completion indicators
285
+ if (textContent.includes('completed') ||
286
+ textContent.includes('finished') ||
287
+ textContent.includes('done') ||
288
+ textContent.includes('successfully')) {
289
+ return 'Task completed';
290
+ }
291
+
292
+ // User input needed - enhanced detection
293
+ if (textContent.endsWith('?') ||
294
+ textContent.includes('what would you like') ||
295
+ textContent.includes('how can i help') ||
296
+ textContent.includes('would you like me to') ||
297
+ textContent.includes('should i') ||
298
+ textContent.includes('do you want') ||
299
+ textContent.includes('let me know') ||
300
+ textContent.includes('please let me know') ||
301
+ textContent.includes('what do you think') ||
302
+ textContent.includes('any questions')) {
303
+ return 'Waiting for your response';
304
+ }
305
+
306
+ // Error/problem indicators
307
+ if (textContent.includes('error') ||
308
+ textContent.includes('failed') ||
309
+ textContent.includes('problem') ||
310
+ textContent.includes('issue')) {
311
+ return 'Encountered issue';
312
+ }
313
+ }
314
+ }
315
+
316
+ // Recent assistant message suggests waiting for user
317
+ if (messageAge < 300) { // Extended to 5 minutes
318
+ return 'Waiting for your response';
319
+ }
320
+
321
+ // Default for older assistant messages
322
+ return 'Idle';
323
+
324
+ } else if (role === 'user') {
325
+ // User just sent a message - Claude should be processing
326
+ if (messageAge < 10) {
327
+ return 'Claude Code working...';
328
+ } else if (messageAge < 60) {
329
+ return 'Awaiting response...';
330
+ }
331
+
332
+ // Older user messages suggest Claude might be working on something complex
333
+ return 'Processing request...';
334
+ }
335
+
336
+ // Enhanced timing analysis
337
+ const lastMessage = existingMessages[existingMessages.length - 1];
338
+ if (lastMessage) {
339
+ const timeSinceLastMessage = Date.now() - new Date(lastMessage.timestamp).getTime();
340
+
341
+ if (timeSinceLastMessage < 30000) { // Less than 30 seconds
342
+ return lastMessage.role === 'user' ? 'Claude Code working...' : 'Recently active';
343
+ } else if (timeSinceLastMessage < 180000) { // Less than 3 minutes
344
+ return 'Idle';
345
+ } else if (timeSinceLastMessage < 1800000) { // Less than 30 minutes
346
+ return 'Waiting for your response';
347
+ }
348
+ }
349
+
350
+ return 'Inactive';
351
+ }
352
+
353
+
354
+ /**
355
+ * Show console interaction panel for Yes/No prompts
356
+ * @param {Object} interactionData - Interaction data from Claude Code
357
+ */
358
+ showConsoleInteraction(interactionData) {
359
+ const panel = this.container.querySelector('#console-interaction-panel');
360
+ const description = this.container.querySelector('#interaction-description');
361
+ const prompt = this.container.querySelector('#interaction-prompt');
362
+ const choices = this.container.querySelector('#interaction-choices');
363
+ const textInput = this.container.querySelector('#interaction-text-input');
364
+
365
+ // Show the panel
366
+ panel.style.display = 'block';
367
+
368
+ // Set up the interaction content
369
+ if (interactionData.description) {
370
+ description.innerHTML = `
371
+ <div class="tool-action">
372
+ <strong>${interactionData.tool || 'Action'}:</strong>
373
+ <div class="tool-details">${interactionData.description}</div>
374
+ </div>
375
+ `;
376
+ }
377
+
378
+ if (interactionData.prompt) {
379
+ prompt.textContent = interactionData.prompt;
380
+ }
381
+
382
+ // Handle different interaction types
383
+ if (interactionData.type === 'choice' && interactionData.options) {
384
+ // Show multiple choice options
385
+ choices.style.display = 'block';
386
+ textInput.style.display = 'none';
387
+
388
+ const choicesHtml = interactionData.options.map((option, index) => `
389
+ <label class="interaction-choice">
390
+ <input type="radio" name="console-choice" value="${index}" ${index === 0 ? 'checked' : ''}>
391
+ <span class="choice-number">${index + 1}.</span>
392
+ <span class="choice-text">${option}</span>
393
+ </label>
394
+ `).join('');
395
+
396
+ choices.innerHTML = choicesHtml;
397
+
398
+ } else if (interactionData.type === 'text') {
399
+ // Show text input
400
+ choices.style.display = 'none';
401
+ textInput.style.display = 'block';
402
+
403
+ const textarea = this.container.querySelector('#console-text-input');
404
+ textarea.focus();
405
+ }
406
+
407
+ // Store interaction data for submission
408
+ this.currentInteraction = interactionData;
409
+
410
+ // Bind event listeners
411
+ this.bindInteractionEvents();
412
+ }
413
+
414
+ /**
415
+ * Hide console interaction panel
416
+ */
417
+ hideConsoleInteraction() {
418
+ const panel = this.container.querySelector('#console-interaction-panel');
419
+ panel.style.display = 'none';
420
+ this.currentInteraction = null;
421
+ }
422
+
423
+ /**
424
+ * Bind event listeners for console interaction
425
+ */
426
+ bindInteractionEvents() {
427
+ const submitBtn = this.container.querySelector('#interaction-submit');
428
+ const cancelBtn = this.container.querySelector('#interaction-cancel');
429
+
430
+ // Remove existing listeners
431
+ submitBtn.replaceWith(submitBtn.cloneNode(true));
432
+ cancelBtn.replaceWith(cancelBtn.cloneNode(true));
433
+
434
+ // Get fresh references
435
+ const newSubmitBtn = this.container.querySelector('#interaction-submit');
436
+ const newCancelBtn = this.container.querySelector('#interaction-cancel');
437
+
438
+ newSubmitBtn.addEventListener('click', () => this.handleInteractionSubmit());
439
+ newCancelBtn.addEventListener('click', () => this.handleInteractionCancel());
440
+
441
+ // Handle Enter key for text input
442
+ const textarea = this.container.querySelector('#console-text-input');
443
+ if (textarea) {
444
+ textarea.addEventListener('keydown', (e) => {
445
+ if (e.key === 'Enter' && e.ctrlKey) {
446
+ this.handleInteractionSubmit();
447
+ }
448
+ });
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Handle interaction submission
454
+ */
455
+ async handleInteractionSubmit() {
456
+ if (!this.currentInteraction) return;
457
+
458
+ let response;
459
+
460
+ if (this.currentInteraction.type === 'choice') {
461
+ const selectedChoice = this.container.querySelector('input[name="console-choice"]:checked');
462
+ if (selectedChoice) {
463
+ response = {
464
+ type: 'choice',
465
+ value: parseInt(selectedChoice.value),
466
+ text: this.currentInteraction.options[selectedChoice.value]
467
+ };
468
+ }
469
+ } else if (this.currentInteraction.type === 'text') {
470
+ const textarea = this.container.querySelector('#console-text-input');
471
+ response = {
472
+ type: 'text',
473
+ value: textarea.value.trim()
474
+ };
475
+ }
476
+
477
+ if (response) {
478
+ // Send response via WebSocket
479
+ try {
480
+ await this.sendConsoleResponse(this.currentInteraction.id, response);
481
+ console.log('🔄 WebSocket: Console interaction response sent', { id: this.currentInteraction.id, response });
482
+ this.hideConsoleInteraction();
483
+ } catch (error) {
484
+ console.error('Error sending console response:', error);
485
+ // Show error in UI
486
+ this.showInteractionError('Failed to send response. Please try again.');
487
+ }
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Handle interaction cancellation
493
+ */
494
+ async handleInteractionCancel() {
495
+ if (!this.currentInteraction) return;
496
+
497
+ try {
498
+ await this.sendConsoleResponse(this.currentInteraction.id, { type: 'cancel' });
499
+ console.log('🔄 WebSocket: Console interaction cancelled', { id: this.currentInteraction.id });
500
+ this.hideConsoleInteraction();
501
+ } catch (error) {
502
+ console.error('Error cancelling console interaction:', error);
503
+ this.hideConsoleInteraction(); // Hide anyway on cancel
504
+ }
505
+ }
506
+
507
+ /**
508
+ * Send console response via WebSocket
509
+ * @param {string} interactionId - Interaction ID
510
+ * @param {Object} response - Response data
511
+ */
512
+ async sendConsoleResponse(interactionId, response) {
513
+ // Send through DataService which will route to WebSocket
514
+ if (this.dataService && this.dataService.webSocketService) {
515
+ this.dataService.webSocketService.send({
516
+ type: 'console_response',
517
+ data: {
518
+ interactionId,
519
+ response
520
+ }
521
+ });
522
+ } else {
523
+ throw new Error('WebSocket service not available');
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Show error in interaction panel
529
+ * @param {string} message - Error message
530
+ */
531
+ showInteractionError(message) {
532
+ const panel = this.container.querySelector('#console-interaction-panel');
533
+ const existingError = panel.querySelector('.interaction-error');
534
+
535
+ if (existingError) {
536
+ existingError.remove();
537
+ }
538
+
539
+ const errorDiv = document.createElement('div');
540
+ errorDiv.className = 'interaction-error';
541
+ errorDiv.textContent = message;
542
+
543
+ const content = panel.querySelector('.interaction-content');
544
+ content.insertBefore(errorDiv, content.querySelector('.interaction-actions'));
545
+
546
+ // Remove error after 5 seconds
547
+ setTimeout(() => {
548
+ if (errorDiv.parentNode) {
549
+ errorDiv.remove();
550
+ }
551
+ }, 5000);
552
+ }
553
+
554
+
555
+ /**
556
+ * Update conversation state elements in the DOM
557
+ * @param {Object} activeStates - Active conversation states
558
+ */
559
+ updateConversationStateElements(activeStates) {
560
+ const conversationItems = this.container.querySelectorAll('.sidebar-conversation-item');
561
+
562
+ conversationItems.forEach(item => {
563
+ const conversationId = item.dataset.id;
564
+ const state = activeStates[conversationId] || 'unknown';
565
+ const stateClass = this.getStateClass(state);
566
+ const stateLabel = this.getStateLabel(state);
567
+
568
+ // Update status dot
569
+ const statusDot = item.querySelector('.status-dot');
570
+ if (statusDot) {
571
+ statusDot.className = `status-dot ${stateClass}`;
572
+ }
573
+
574
+ // Update status badge
575
+ const statusBadge = item.querySelector('.sidebar-conversation-badge');
576
+ if (statusBadge) {
577
+ statusBadge.className = `sidebar-conversation-badge ${stateClass}`;
578
+ statusBadge.textContent = stateLabel;
579
+ }
580
+ });
581
+ }
582
+
583
+ /**
584
+ * Render the agents page structure
585
+ */
586
+ async render() {
587
+ this.container.innerHTML = `
588
+ <div class="agents-page">
589
+ <!-- Page Header -->
590
+ <div class="page-header conversations-header">
591
+ <div class="header-content">
592
+ <div class="header-left">
593
+ <div class="status-header">
594
+ <span class="session-timer-status-dot active"></span>
595
+ <h1 class="page-title">
596
+ Claude Code web UI
597
+ </h1>
598
+ </div>
599
+ <div class="page-subtitle">
600
+ Monitor and analyze Claude Code agent interactions in real-time
601
+ </div>
602
+ </div>
603
+ </div>
604
+ </div>
605
+
606
+ <!-- Filters Section -->
607
+ <div class="conversations-filters">
608
+ <div class="filters-row">
609
+ <div class="filter-group">
610
+ <label class="filter-label">Status:</label>
611
+ <select class="filter-select" id="status-filter">
612
+ <option value="all">All</option>
613
+ <option value="active">Active</option>
614
+ <option value="inactive">Inactive</option>
615
+ </select>
616
+ </div>
617
+
618
+ <div class="filter-group">
619
+ <label class="filter-label">Time Range:</label>
620
+ <select class="filter-select" id="time-filter">
621
+ <option value="1h">Last Hour</option>
622
+ <option value="24h">Last 24 Hours</option>
623
+ <option value="7d" selected>Last 7 Days</option>
624
+ <option value="30d">Last 30 Days</option>
625
+ </select>
626
+ </div>
627
+
628
+ <div class="filter-group search-group">
629
+ <label class="filter-label">Search:</label>
630
+ <div class="search-input-container">
631
+ <input type="text" class="filter-input search-input" id="search-filter" placeholder="Search conversations, projects, or messages...">
632
+ <button class="search-clear" id="clear-search" title="Clear search">×</button>
633
+ </div>
634
+ </div>
635
+ </div>
636
+ </div>
637
+
638
+ <!-- Agents Section -->
639
+ <div class="agents-section">
640
+ <div class="agents-header">
641
+ <h4>Available Agents</h4>
642
+ <div class="agents-info">
643
+ <span class="agents-count" id="agents-count">0 agents</span>
644
+ <button class="refresh-agents-btn" id="refresh-agents" title="Refresh agents">
645
+ <span class="btn-icon">🔄</span>
646
+ </button>
647
+ </div>
648
+ </div>
649
+
650
+ <div class="agents-list" id="agents-list">
651
+ <!-- Agent items will be rendered here -->
652
+ </div>
653
+
654
+ <!-- Loading state for agents -->
655
+ <div class="agents-loading" id="agents-loading" style="display: none;">
656
+ <div class="loading-spinner"></div>
657
+ <span class="loading-text">Loading agents...</span>
658
+ </div>
659
+
660
+ <!-- Empty state for agents -->
661
+ <div class="agents-empty" id="agents-empty" style="display: none;">
662
+ <div class="empty-icon">🤖</div>
663
+ <p>No agents found</p>
664
+ <small>Create agents in your .claude/agents directory to see them here</small>
665
+ </div>
666
+ </div>
667
+
668
+ <!-- Loading State -->
669
+ <div class="loading-state" id="conversations-loading" style="display: none;">
670
+ <div class="loading-spinner"></div>
671
+ <span class="loading-text">Loading conversations...</span>
672
+ </div>
673
+
674
+ <!-- Error State -->
675
+ <div class="error-state" id="conversations-error" style="display: none;">
676
+ <div class="error-content">
677
+ <span class="error-icon">⚠️</span>
678
+ <span class="error-message"></span>
679
+ <button class="error-retry" id="retry-load">Retry</button>
680
+ </div>
681
+ </div>
682
+
683
+ <!-- Console Interaction Panel (Hidden by default) -->
684
+ <div id="console-interaction-panel" class="console-interaction-panel" style="display: none;">
685
+ <div class="interaction-header">
686
+ <div class="interaction-title">
687
+ <span class="interaction-icon">⚡</span>
688
+ <span class="interaction-text">Claude Code needs your input</span>
689
+ </div>
690
+ <button class="interaction-close" onclick="this.hideConsoleInteraction()">&times;</button>
691
+ </div>
692
+
693
+ <div class="interaction-content">
694
+ <div id="interaction-description" class="interaction-description">
695
+ <!-- Tool description will be inserted here -->
696
+ </div>
697
+
698
+ <div id="interaction-prompt" class="interaction-prompt">
699
+ Do you want to proceed?
700
+ </div>
701
+
702
+ <!-- Multi-choice options -->
703
+ <div id="interaction-choices" class="interaction-choices" style="display: none;">
704
+ <!-- Radio button choices will be inserted here -->
705
+ </div>
706
+
707
+ <!-- Text input area -->
708
+ <div id="interaction-text-input" class="interaction-text-input" style="display: none;">
709
+ <label for="console-text-input">Your response:</label>
710
+ <textarea id="console-text-input" placeholder="Type your response here..." rows="4"></textarea>
711
+ </div>
712
+
713
+ <div class="interaction-actions">
714
+ <button id="interaction-submit" class="interaction-btn primary">Submit</button>
715
+ <button id="interaction-cancel" class="interaction-btn secondary">Cancel</button>
716
+ </div>
717
+ </div>
718
+ </div>
719
+
720
+ <!-- Two Column Layout -->
721
+ <div class="conversations-layout">
722
+ <!-- Left Sidebar: Conversations List -->
723
+ <div class="conversations-sidebar">
724
+ <div class="sidebar-header">
725
+ <h3>Chats</h3>
726
+ <span class="conversation-count" id="sidebar-count">0</span>
727
+ </div>
728
+ <div class="conversations-list" id="conversations-list">
729
+ <!-- Conversation items will be rendered here -->
730
+ </div>
731
+
732
+ <!-- Load More Indicator -->
733
+ <div class="load-more-indicator" id="load-more-indicator" style="display: none;">
734
+ <div class="loading-spinner"></div>
735
+ <span class="loading-text">Loading more conversations...</span>
736
+ </div>
737
+ </div>
738
+
739
+ <!-- Right Panel: Messages Detail -->
740
+ <div class="messages-panel">
741
+ <div class="messages-header" id="messages-header">
742
+ <div class="selected-conversation-info">
743
+ <h3 id="selected-conversation-title">Select a chat</h3>
744
+ <div class="selected-conversation-meta" id="selected-conversation-meta"></div>
745
+ </div>
746
+ <div class="messages-actions">
747
+ <button class="action-btn-small" id="export-conversation" title="Export conversation">
748
+ <span class="btn-icon-small">📁</span>
749
+ Export
750
+ </button>
751
+ </div>
752
+ </div>
753
+
754
+ <div class="messages-content" id="messages-content">
755
+ <div class="no-conversation-selected">
756
+ <div class="no-selection-icon">💬</div>
757
+ <h4>No conversation selected</h4>
758
+ <p>Choose a conversation from the sidebar to view its messages</p>
759
+ </div>
760
+ </div>
761
+
762
+ <!-- Conversation State Banner -->
763
+ <div class="conversation-state-banner" id="conversation-state-banner" style="display: none;">
764
+ <div class="state-indicator">
765
+ <span class="state-dot" id="state-dot"></span>
766
+ <span class="state-text" id="state-text">Ready</span>
767
+ </div>
768
+ <div class="state-timestamp" id="state-timestamp"></div>
769
+ </div>
770
+ </div>
771
+ </div>
772
+
773
+ <!-- Empty State -->
774
+ <div class="empty-state" id="empty-state" style="display: none;">
775
+ <div class="empty-content">
776
+ <span class="empty-icon">💬</span>
777
+ <h3>No conversations found</h3>
778
+ <p>No agent conversations match your current filters.</p>
779
+ <button class="empty-action" id="clear-filters">Clear Filters</button>
780
+ </div>
781
+ </div>
782
+ </div>
783
+ `;
784
+
785
+ this.bindEvents();
786
+ this.setupInfiniteScroll();
787
+ }
788
+
789
+ /**
790
+ * Initialize child components
791
+ */
792
+ async initializeComponents() {
793
+ // Initialize ConversationTable for detailed view if available
794
+ const tableContainer = this.container.querySelector('#conversations-table');
795
+ if (tableContainer && typeof ConversationTable !== 'undefined') {
796
+ try {
797
+ this.components.conversationTable = new ConversationTable(
798
+ tableContainer,
799
+ this.dataService,
800
+ this.stateService
801
+ );
802
+ await this.components.conversationTable.initialize();
803
+ } catch (error) {
804
+ console.warn('ConversationTable initialization failed:', error);
805
+ // Show fallback content
806
+ tableContainer.innerHTML = `
807
+ <div class="conversation-table-placeholder">
808
+ <p>Detailed table view not available</p>
809
+ </div>
810
+ `;
811
+ }
812
+ }
813
+ }
814
+
815
+ /**
816
+ * Bind event listeners
817
+ */
818
+ bindEvents() {
819
+ // Filter controls
820
+ const statusFilter = this.container.querySelector('#status-filter');
821
+ statusFilter.addEventListener('change', (e) => this.updateFilter('status', e.target.value));
822
+
823
+ const timeFilter = this.container.querySelector('#time-filter');
824
+ timeFilter.addEventListener('change', (e) => this.updateFilter('timeRange', e.target.value));
825
+
826
+ const searchInput = this.container.querySelector('#search-filter');
827
+ searchInput.addEventListener('input', (e) => this.updateFilter('search', e.target.value));
828
+
829
+ const clearSearch = this.container.querySelector('#clear-search');
830
+ clearSearch.addEventListener('click', () => this.clearSearch());
831
+
832
+ // Error retry
833
+ const retryBtn = this.container.querySelector('#retry-load');
834
+ if (retryBtn) {
835
+ retryBtn.addEventListener('click', () => this.loadConversationsData());
836
+ }
837
+
838
+ // Clear filters
839
+ const clearFiltersBtn = this.container.querySelector('#clear-filters');
840
+ if (clearFiltersBtn) {
841
+ clearFiltersBtn.addEventListener('click', () => this.clearAllFilters());
842
+ }
843
+
844
+
845
+ // Refresh agents
846
+ const refreshAgentsBtn = this.container.querySelector('#refresh-agents');
847
+ if (refreshAgentsBtn) {
848
+ refreshAgentsBtn.addEventListener('click', () => this.refreshAgents());
849
+ }
850
+ }
851
+
852
+ /**
853
+ * Setup infinite scroll for conversations list
854
+ */
855
+ setupInfiniteScroll() {
856
+ const conversationsContainer = this.container.querySelector('#conversations-list');
857
+ if (!conversationsContainer) return;
858
+
859
+ conversationsContainer.addEventListener('scroll', () => {
860
+ const { scrollTop, scrollHeight, clientHeight } = conversationsContainer;
861
+ const threshold = 100; // Load more when 100px from bottom
862
+
863
+ if (scrollHeight - scrollTop - clientHeight < threshold) {
864
+ this.loadMoreConversations();
865
+ }
866
+ });
867
+ }
868
+
869
+ /**
870
+ * Update loading indicator
871
+ * @param {boolean} isLoading - Whether to show loading indicator
872
+ */
873
+ updateLoadingIndicator(isLoading) {
874
+ const loadingIndicator = this.container.querySelector('#load-more-indicator');
875
+ if (loadingIndicator) {
876
+ loadingIndicator.style.display = isLoading ? 'flex' : 'none';
877
+ }
878
+ }
879
+
880
+ /**
881
+ * Load agents data from API
882
+ */
883
+ async loadAgentsData() {
884
+ try {
885
+ this.showAgentsLoading(true);
886
+
887
+ const agentsData = await this.dataService.cachedFetch('/api/agents');
888
+
889
+ if (agentsData && agentsData.agents) {
890
+ this.agents = agentsData.agents;
891
+ this.renderAgents();
892
+ } else {
893
+ this.showAgentsEmpty();
894
+ }
895
+
896
+ } catch (error) {
897
+ console.error('Error loading agents data:', error);
898
+ this.showAgentsEmpty();
899
+ } finally {
900
+ this.showAgentsLoading(false);
901
+ }
902
+ }
903
+
904
+ /**
905
+ * Render global agents in the agents list (user-level only)
906
+ */
907
+ renderAgents() {
908
+ const agentsList = this.container.querySelector('#agents-list');
909
+ const agentsCount = this.container.querySelector('#agents-count');
910
+
911
+ if (!agentsList || !agentsCount) return;
912
+
913
+ // Filter only global/user agents for main section
914
+ const globalAgents = this.agents.filter(agent => agent.level === 'user');
915
+
916
+ if (globalAgents.length === 0) {
917
+ this.showAgentsEmpty();
918
+ return;
919
+ }
920
+
921
+ // Update count for global agents only
922
+ agentsCount.textContent = `${globalAgents.length} global agent${globalAgents.length !== 1 ? 's' : ''}`;
923
+
924
+ // Render global agent items (compact rectangles)
925
+ const agentsHTML = globalAgents.map(agent => {
926
+ const levelBadge = agent.level === 'project' ? 'P' : 'U';
927
+
928
+ return `
929
+ <div class="agent-item" data-agent-id="${agent.name}">
930
+ <div class="agent-dot" style="background-color: ${agent.color}"></div>
931
+ <span class="agent-name">${agent.name}</span>
932
+ <span class="agent-level-badge ${agent.level}" title="${agent.level === 'project' ? 'Project Agent' : 'User Agent'}">${levelBadge}</span>
933
+ </div>
934
+ `;
935
+ }).join('');
936
+
937
+ agentsList.innerHTML = agentsHTML;
938
+
939
+ // Hide empty state and show list
940
+ this.hideAgentsEmpty();
941
+ agentsList.style.display = 'block';
942
+
943
+ // Bind agent events
944
+ this.bindAgentEvents();
945
+ }
946
+
947
+ /**
948
+ * Bind events for agent items
949
+ */
950
+ bindAgentEvents() {
951
+ const agentItems = this.container.querySelectorAll('.agent-item');
952
+
953
+ agentItems.forEach(item => {
954
+ item.addEventListener('click', () => {
955
+ const agentId = item.dataset.agentId;
956
+ this.selectAgent(agentId);
957
+ });
958
+ });
959
+ }
960
+
961
+ /**
962
+ * Select an agent (opens modal with details)
963
+ * @param {string} agentId - Agent ID
964
+ */
965
+ selectAgent(agentId) {
966
+ const agent = this.agents.find(a => a.name === agentId);
967
+ if (agent) {
968
+ this.openAgentModal(agent);
969
+ }
970
+ }
971
+
972
+ /**
973
+ * Open agent details modal
974
+ * @param {Object} agent - Agent object
975
+ */
976
+ openAgentModal(agent) {
977
+ // If this is a specific tool, open the custom tool modal
978
+ if (agent.isToolDetails) {
979
+ this.openToolModal(agent);
980
+ return;
981
+ }
982
+
983
+ const modalHTML = `
984
+ <div class="agent-modal-overlay" id="agent-modal-overlay">
985
+ <div class="agent-modal">
986
+ <div class="agent-modal-header">
987
+ <div class="agent-modal-title">
988
+ <div class="agent-title-main">
989
+ <div class="agent-dot" style="background-color: ${agent.color}"></div>
990
+ <div class="agent-title-info">
991
+ <h3>${agent.name}</h3>
992
+ <div class="agent-subtitle">
993
+ <span class="agent-level-badge ${agent.level}">${agent.level === 'project' ? 'Project Agent' : 'User Agent'}</span>
994
+ ${agent.projectName ? `<span class="agent-project-name">• ${agent.projectName}</span>` : ''}
995
+ </div>
996
+ </div>
997
+ </div>
998
+ </div>
999
+ <button class="agent-modal-close" id="agent-modal-close">&times;</button>
1000
+ </div>
1001
+
1002
+ <div class="agent-modal-content">
1003
+ <div class="agent-info-section">
1004
+ <h4>Description</h4>
1005
+ <p>${agent.description}</p>
1006
+ </div>
1007
+
1008
+ ${agent.projectName ? `
1009
+ <div class="agent-info-section">
1010
+ <h4>Project</h4>
1011
+ <p>${agent.projectName}</p>
1012
+ </div>
1013
+ ` : ''}
1014
+
1015
+ <div class="agent-info-section">
1016
+ <h4>Tools Access</h4>
1017
+ <p>${agent.tools && agent.tools.length > 0
1018
+ ? `Has access to: ${agent.tools.join(', ')}`
1019
+ : 'Has access to all available tools'}</p>
1020
+ </div>
1021
+
1022
+ <div class="agent-info-section">
1023
+ <h4>System Prompt</h4>
1024
+ <div class="agent-system-prompt">${agent.systemPrompt ? agent.systemPrompt.replace(/\n/g, '<br>') : 'No system prompt available'}</div>
1025
+ </div>
1026
+
1027
+ <div class="agent-usage-tips">
1028
+ <h4>💡 How to Use This Agent</h4>
1029
+ <div class="usage-tips-content">
1030
+ <p><strong>To invoke this agent explicitly:</strong></p>
1031
+ <code class="usage-example">Use the ${agent.name} agent to [describe your request]</code>
1032
+
1033
+ <p><strong>Alternative ways to invoke:</strong></p>
1034
+ <ul>
1035
+ <li><code>Ask the ${agent.name} agent to [task]</code></li>
1036
+ <li><code>Have the ${agent.name} agent [action]</code></li>
1037
+ <li><code>Let the ${agent.name} agent handle [request]</code></li>
1038
+ </ul>
1039
+
1040
+ <p><strong>Best practices:</strong></p>
1041
+ <ul>
1042
+ <li>Be specific about what you want the agent to do</li>
1043
+ <li>Provide context when needed</li>
1044
+ <li>The agent will automatically use appropriate tools</li>
1045
+ </ul>
1046
+ </div>
1047
+ </div>
1048
+
1049
+ <div class="agent-metadata">
1050
+ <small><strong>File:</strong> ${agent.filePath}</small><br>
1051
+ <small><strong>Last modified:</strong> ${new Date(agent.lastModified).toLocaleString()}</small>
1052
+ </div>
1053
+ </div>
1054
+ </div>
1055
+ </div>
1056
+ `;
1057
+
1058
+ // Add modal to DOM
1059
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
1060
+
1061
+ // Bind close events
1062
+ document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
1063
+ document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
1064
+ if (e.target.id === 'agent-modal-overlay') {
1065
+ this.closeAgentModal();
1066
+ }
1067
+ });
1068
+
1069
+ // ESC key to close - store reference for cleanup
1070
+ this.modalKeydownHandler = (e) => {
1071
+ if (e.key === 'Escape') {
1072
+ this.closeAgentModal();
1073
+ }
1074
+ };
1075
+ document.addEventListener('keydown', this.modalKeydownHandler);
1076
+ }
1077
+
1078
+ /**
1079
+ * Open tool-specific modal for any tool
1080
+ * @param {Object} toolData - Tool data object
1081
+ */
1082
+ openToolModal(toolData) {
1083
+ switch (toolData.name) {
1084
+ case 'Read':
1085
+ this.openReadToolModal(toolData);
1086
+ break;
1087
+ case 'Edit':
1088
+ this.openEditToolModal(toolData);
1089
+ break;
1090
+ case 'Write':
1091
+ this.openWriteToolModal(toolData);
1092
+ break;
1093
+ case 'Bash':
1094
+ this.openBashToolModal(toolData);
1095
+ break;
1096
+ case 'Glob':
1097
+ this.openGlobToolModal(toolData);
1098
+ break;
1099
+ case 'Grep':
1100
+ this.openGrepToolModal(toolData);
1101
+ break;
1102
+ case 'TodoWrite':
1103
+ this.openTodoWriteToolModal(toolData);
1104
+ break;
1105
+ default:
1106
+ // Fallback to generic agent modal for unknown tools
1107
+ this.openAgentModal({...toolData, isToolDetails: false});
1108
+ break;
1109
+ }
1110
+ }
1111
+
1112
+ /**
1113
+ * Open Read tool specific modal
1114
+ * @param {Object} readToolData - Read tool data
1115
+ */
1116
+ openReadToolModal(readToolData) {
1117
+ const input = readToolData.input || {};
1118
+ const filePath = input.file_path || 'Unknown file';
1119
+ const fileName = filePath.split('/').pop() || 'Unknown';
1120
+ const fileExtension = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : 'txt';
1121
+ const offset = input.offset;
1122
+ const limit = input.limit;
1123
+ const toolId = readToolData.id || 'unknown';
1124
+
1125
+ // Analyze file context for project
1126
+ const isConfigFile = ['json', 'yml', 'yaml', 'toml', 'ini', 'conf', 'config'].includes(fileExtension);
1127
+ const isDocFile = ['md', 'txt', 'rst', 'adoc'].includes(fileExtension);
1128
+ const isCodeFile = ['js', 'ts', 'py', 'go', 'rs', 'java', 'cpp', 'c', 'h', 'css', 'html', 'jsx', 'tsx'].includes(fileExtension);
1129
+ const isProjectRoot = fileName.toLowerCase().includes('claude') || fileName.toLowerCase().includes('readme') || fileName.toLowerCase().includes('package');
1130
+ const isTestFile = fileName.toLowerCase().includes('test') || fileName.toLowerCase().includes('spec');
1131
+
1132
+ let fileCategory = '';
1133
+ let filePurpose = '';
1134
+ let contextIcon = '📄';
1135
+
1136
+ if (isProjectRoot) {
1137
+ fileCategory = 'Project Documentation';
1138
+ filePurpose = 'Understanding project structure and setup';
1139
+ contextIcon = '📋';
1140
+ } else if (fileName.toLowerCase().includes('claude')) {
1141
+ fileCategory = 'Claude Configuration';
1142
+ filePurpose = 'Reading project instructions for AI assistant';
1143
+ contextIcon = '🤖';
1144
+ } else if (isConfigFile) {
1145
+ fileCategory = 'Configuration File';
1146
+ filePurpose = 'Understanding project settings and dependencies';
1147
+ contextIcon = '⚙️';
1148
+ } else if (isDocFile) {
1149
+ fileCategory = 'Documentation';
1150
+ filePurpose = 'Reading project documentation or specifications';
1151
+ contextIcon = '📚';
1152
+ } else if (isTestFile) {
1153
+ fileCategory = 'Test File';
1154
+ filePurpose = 'Analyzing test cases and specifications';
1155
+ contextIcon = '🧪';
1156
+ } else if (isCodeFile) {
1157
+ fileCategory = 'Source Code';
1158
+ filePurpose = 'Analyzing implementation and logic';
1159
+ contextIcon = '💻';
1160
+ } else {
1161
+ fileCategory = 'Project File';
1162
+ filePurpose = 'Reading project-related content';
1163
+ contextIcon = '📄';
1164
+ }
1165
+
1166
+ // Get directory context
1167
+ const pathParts = filePath.split('/');
1168
+ const projectContext = pathParts.includes('claude-code-templates') ? 'Claude Code Templates Project' : 'Current Project';
1169
+ const relativeDir = pathParts.slice(-3, -1).join('/') || 'root';
1170
+
1171
+ const modalHTML = `
1172
+ <div class="agent-modal-overlay" id="agent-modal-overlay">
1173
+ <div class="agent-modal read-tool-modal">
1174
+ <div class="agent-modal-header">
1175
+ <div class="agent-modal-title">
1176
+ <div class="agent-title-main">
1177
+ <div class="tool-icon read-tool">
1178
+ <span style="font-size: 20px;">${contextIcon}</span>
1179
+ </div>
1180
+ <div class="agent-title-info">
1181
+ <h3>File Read: ${fileName}</h3>
1182
+ <div class="agent-subtitle">
1183
+ <span class="tool-type-badge">${fileCategory}</span>
1184
+ <span class="tool-id-badge">ID: ${toolId.slice(-8)}</span>
1185
+ </div>
1186
+ </div>
1187
+ </div>
1188
+ </div>
1189
+ <button class="agent-modal-close" id="agent-modal-close">&times;</button>
1190
+ </div>
1191
+
1192
+ <div class="agent-modal-content">
1193
+ <div class="raw-parameters-section primary-section">
1194
+ <h4>🔧 Tool Parameters</h4>
1195
+ <div class="raw-params-container">
1196
+ <pre class="raw-params-json">${JSON.stringify(input, null, 2)}</pre>
1197
+ </div>
1198
+ <div class="params-summary">
1199
+ <span class="param-chip">Tool ID: ${toolId.slice(-8)}</span>
1200
+ <span class="param-chip">File: ${fileName}</span>
1201
+ ${offset ? `<span class="param-chip">From line: ${offset}</span>` : ''}
1202
+ ${limit ? `<span class="param-chip">Lines: ${limit}</span>` : '<span class="param-chip">Complete file</span>'}
1203
+ </div>
1204
+ </div>
1205
+
1206
+ <div class="read-operation-section">
1207
+ <h4>📖 Read Operation Details</h4>
1208
+ <div class="operation-details">
1209
+ <div class="operation-item">
1210
+ <span class="operation-label">Full Path:</span>
1211
+ <code class="operation-value">${filePath}</code>
1212
+ </div>
1213
+ ${offset ? `
1214
+ <div class="operation-item">
1215
+ <span class="operation-label">Starting Line:</span>
1216
+ <code class="operation-value">${offset}</code>
1217
+ </div>
1218
+ ` : `
1219
+ <div class="operation-item">
1220
+ <span class="operation-label">Read Scope:</span>
1221
+ <code class="operation-value">From beginning</code>
1222
+ </div>
1223
+ `}
1224
+ ${limit ? `
1225
+ <div class="operation-item">
1226
+ <span class="operation-label">Lines Read:</span>
1227
+ <code class="operation-value">${limit} lines</code>
1228
+ </div>
1229
+ ` : `
1230
+ <div class="operation-item">
1231
+ <span class="operation-label">Read Scope:</span>
1232
+ <code class="operation-value">Complete file</code>
1233
+ </div>
1234
+ `}
1235
+ <div class="operation-item">
1236
+ <span class="operation-label">Tool ID:</span>
1237
+ <code class="operation-value">${toolId}</code>
1238
+ </div>
1239
+ </div>
1240
+ </div>
1241
+
1242
+
1243
+ <div class="file-insights-section">
1244
+ <h4>📊 File Insights</h4>
1245
+ <div class="insights-grid">
1246
+ <div class="insight-card">
1247
+ <div class="insight-header">
1248
+ <span class="insight-icon">${contextIcon}</span>
1249
+ <span class="insight-title">File Classification</span>
1250
+ </div>
1251
+ <div class="insight-content">${fileCategory}</div>
1252
+ </div>
1253
+ <div class="insight-card">
1254
+ <div class="insight-header">
1255
+ <span class="insight-icon">📍</span>
1256
+ <span class="insight-title">Location Context</span>
1257
+ </div>
1258
+ <div class="insight-content">${relativeDir}</div>
1259
+ </div>
1260
+ <div class="insight-card">
1261
+ <div class="insight-header">
1262
+ <span class="insight-icon">🎯</span>
1263
+ <span class="insight-title">Read Strategy</span>
1264
+ </div>
1265
+ <div class="insight-content">${offset && limit ? `Partial read (${limit} lines from ${offset})` : limit ? `Limited read (${limit} lines)` : offset ? `From line ${offset}` : 'Complete file'}</div>
1266
+ </div>
1267
+ <div class="insight-card">
1268
+ <div class="insight-header">
1269
+ <span class="insight-icon">🚀</span>
1270
+ <span class="insight-title">Project Impact</span>
1271
+ </div>
1272
+ <div class="insight-content">${filePurpose}</div>
1273
+ </div>
1274
+ </div>
1275
+ </div>
1276
+
1277
+ </div>
1278
+ </div>
1279
+ </div>
1280
+ `;
1281
+
1282
+ // Add modal to DOM
1283
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
1284
+
1285
+ // Bind close events
1286
+ document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
1287
+ document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
1288
+ if (e.target.id === 'agent-modal-overlay') {
1289
+ this.closeAgentModal();
1290
+ }
1291
+ });
1292
+
1293
+ // ESC key to close - store reference for cleanup
1294
+ this.modalKeydownHandler = (e) => {
1295
+ if (e.key === 'Escape') {
1296
+ this.closeAgentModal();
1297
+ }
1298
+ };
1299
+ document.addEventListener('keydown', this.modalKeydownHandler);
1300
+ }
1301
+
1302
+ /**
1303
+ * Open Edit tool specific modal
1304
+ * @param {Object} editToolData - Edit tool data
1305
+ */
1306
+ openEditToolModal(editToolData) {
1307
+ const input = editToolData.input || {};
1308
+ const filePath = input.file_path || 'Unknown file';
1309
+ const fileName = filePath.split('/').pop() || 'Unknown';
1310
+ const oldString = input.old_string || '';
1311
+ const newString = input.new_string || '';
1312
+ const replaceAll = input.replace_all || false;
1313
+ const toolId = editToolData.id || 'unknown';
1314
+
1315
+ const modalHTML = `
1316
+ <div class="agent-modal-overlay" id="agent-modal-overlay">
1317
+ <div class="agent-modal edit-tool-modal">
1318
+ <div class="agent-modal-header">
1319
+ <div class="agent-modal-title">
1320
+ <div class="agent-title-main">
1321
+ <div class="tool-icon edit-tool">
1322
+ <span style="font-size: 20px;">✏️</span>
1323
+ </div>
1324
+ <div class="agent-title-info">
1325
+ <h3>File Edit: ${fileName}</h3>
1326
+ <div class="agent-subtitle">
1327
+ <span class="tool-type-badge">File Modification</span>
1328
+ <span class="tool-id-badge">ID: ${toolId.slice(-8)}</span>
1329
+ </div>
1330
+ </div>
1331
+ </div>
1332
+ </div>
1333
+ <button class="agent-modal-close" id="agent-modal-close">&times;</button>
1334
+ </div>
1335
+
1336
+ <div class="agent-modal-content">
1337
+ <div class="raw-parameters-section primary-section">
1338
+ <h4>🔧 Tool Parameters</h4>
1339
+ <div class="raw-params-container">
1340
+ <pre class="raw-params-json">${JSON.stringify(input, null, 2)}</pre>
1341
+ </div>
1342
+ <div class="params-summary">
1343
+ <span class="param-chip">Tool ID: ${toolId.slice(-8)}</span>
1344
+ <span class="param-chip">File: ${fileName}</span>
1345
+ <span class="param-chip">Replace All: ${replaceAll ? 'Yes' : 'No'}</span>
1346
+ <span class="param-chip">Change Size: ${oldString.length} → ${newString.length} chars</span>
1347
+ </div>
1348
+ </div>
1349
+
1350
+ <div class="edit-changes-section">
1351
+ <div class="diff-header-section">
1352
+ <h4>📝 Edit Diff</h4>
1353
+ <div class="diff-mode-toggle">
1354
+ <button class="diff-mode-btn active" data-mode="lines" onclick="window.switchDiffMode('lines', this)">
1355
+ <span>📋</span> By Lines
1356
+ </button>
1357
+ <button class="diff-mode-btn" data-mode="compact" onclick="window.switchDiffMode('compact', this)">
1358
+ <span>🔍</span> Smart Diff
1359
+ </button>
1360
+ </div>
1361
+ </div>
1362
+ <div class="diff-container" id="diff-container">
1363
+ ${this.generateDiffView(oldString, newString, 'lines')}
1364
+ </div>
1365
+ </div>
1366
+
1367
+
1368
+ <div class="file-insights-section">
1369
+ <h4>📊 Edit Insights</h4>
1370
+ <div class="insights-grid">
1371
+ <div class="insight-card">
1372
+ <div class="insight-header">
1373
+ <span class="insight-icon">📄</span>
1374
+ <span class="insight-title">Target File</span>
1375
+ </div>
1376
+ <div class="insight-content">${fileName}</div>
1377
+ </div>
1378
+ <div class="insight-card">
1379
+ <div class="insight-header">
1380
+ <span class="insight-icon">📏</span>
1381
+ <span class="insight-title">Content Change</span>
1382
+ </div>
1383
+ <div class="insight-content">${oldString.length > newString.length ? 'Reduced' : oldString.length < newString.length ? 'Expanded' : 'Same'} (${newString.length - oldString.length > 0 ? '+' : ''}${newString.length - oldString.length} chars)</div>
1384
+ </div>
1385
+ <div class="insight-card">
1386
+ <div class="insight-header">
1387
+ <span class="insight-icon">🔄</span>
1388
+ <span class="insight-title">Replace Mode</span>
1389
+ </div>
1390
+ <div class="insight-content">${replaceAll ? 'All Occurrences' : 'First Occurrence'}</div>
1391
+ </div>
1392
+ <div class="insight-card">
1393
+ <div class="insight-header">
1394
+ <span class="insight-icon">🎯</span>
1395
+ <span class="insight-title">Edit Type</span>
1396
+ </div>
1397
+ <div class="insight-content">${oldString.length === 0 ? 'Addition' : newString.length === 0 ? 'Deletion' : 'Modification'}</div>
1398
+ </div>
1399
+ </div>
1400
+ </div>
1401
+ </div>
1402
+ </div>
1403
+ </div>
1404
+ `;
1405
+
1406
+ // Add modal to DOM
1407
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
1408
+
1409
+ // Bind close events
1410
+ document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
1411
+ document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
1412
+ if (e.target.id === 'agent-modal-overlay') {
1413
+ this.closeAgentModal();
1414
+ }
1415
+ });
1416
+
1417
+ // ESC key to close
1418
+ this.modalKeydownHandler = (e) => {
1419
+ if (e.key === 'Escape') {
1420
+ this.closeAgentModal();
1421
+ }
1422
+ };
1423
+ document.addEventListener('keydown', this.modalKeydownHandler);
1424
+
1425
+ // Store the tool data for mode switching
1426
+ window.currentEditData = { oldString, newString };
1427
+
1428
+ // Add global function for diff mode switching
1429
+ const self = this;
1430
+ window.switchDiffMode = function(mode, button) {
1431
+ // Update button states
1432
+ document.querySelectorAll('.diff-mode-btn').forEach(btn => btn.classList.remove('active'));
1433
+ button.classList.add('active');
1434
+
1435
+ // Update diff content
1436
+ const container = document.getElementById('diff-container');
1437
+ if (container && window.currentEditData) {
1438
+ container.innerHTML = self.generateDiffView(
1439
+ window.currentEditData.oldString,
1440
+ window.currentEditData.newString,
1441
+ mode
1442
+ );
1443
+ }
1444
+ };
1445
+ }
1446
+
1447
+ /**
1448
+ * Generate diff view for edit changes
1449
+ * @param {string} oldText - Original text
1450
+ * @param {string} newText - New text
1451
+ * @param {string} mode - 'lines' or 'compact'
1452
+ * @returns {string} HTML diff view
1453
+ */
1454
+ generateDiffView(oldText, newText, mode = 'lines') {
1455
+ if (mode === 'compact') {
1456
+ return this.generateSmartDiff(oldText, newText);
1457
+ }
1458
+
1459
+ // Split text into lines for better diff visualization
1460
+ const oldLines = oldText.split('\n');
1461
+ const newLines = newText.split('\n');
1462
+
1463
+ // Limit lines for display (show first 20 lines max)
1464
+ const maxLines = 20;
1465
+ const oldDisplay = oldLines.slice(0, maxLines);
1466
+ const newDisplay = newLines.slice(0, maxLines);
1467
+ const oldTruncated = oldLines.length > maxLines;
1468
+ const newTruncated = newLines.length > maxLines;
1469
+
1470
+ let diffHtml = '<div class="diff-editor">';
1471
+
1472
+ // Header with file info
1473
+ diffHtml += `
1474
+ <div class="diff-header">
1475
+ <span class="diff-stats">-${oldLines.length} lines, +${newLines.length} lines</span>
1476
+ </div>
1477
+ `;
1478
+
1479
+ // Old text (removed lines)
1480
+ if (oldDisplay.length > 0) {
1481
+ diffHtml += '<div class="diff-section removed">';
1482
+ oldDisplay.forEach((line) => {
1483
+ diffHtml += `
1484
+ <div class="diff-line removed-line">
1485
+ <span class="line-prefix">-</span>
1486
+ <span class="line-content">${this.escapeHtml(line) || ' '}</span>
1487
+ </div>
1488
+ `;
1489
+ });
1490
+ if (oldTruncated) {
1491
+ diffHtml += `
1492
+ <div class="diff-line truncated">
1493
+ <span class="line-prefix"></span>
1494
+ <span class="line-content">... ${oldLines.length - maxLines} more lines ...</span>
1495
+ </div>
1496
+ `;
1497
+ }
1498
+ diffHtml += '</div>';
1499
+ }
1500
+
1501
+ // New text (added lines)
1502
+ if (newDisplay.length > 0) {
1503
+ diffHtml += '<div class="diff-section added">';
1504
+ newDisplay.forEach((line) => {
1505
+ diffHtml += `
1506
+ <div class="diff-line added-line">
1507
+ <span class="line-prefix">+</span>
1508
+ <span class="line-content">${this.escapeHtml(line) || ' '}</span>
1509
+ </div>
1510
+ `;
1511
+ });
1512
+ if (newTruncated) {
1513
+ diffHtml += `
1514
+ <div class="diff-line truncated">
1515
+ <span class="line-prefix"></span>
1516
+ <span class="line-content">... ${newLines.length - maxLines} more lines ...</span>
1517
+ </div>
1518
+ `;
1519
+ }
1520
+ diffHtml += '</div>';
1521
+ }
1522
+
1523
+ diffHtml += '</div>';
1524
+
1525
+ return diffHtml;
1526
+ }
1527
+
1528
+ /**
1529
+ * Generate smart diff that shows only changed sections
1530
+ * @param {string} oldText - Original text
1531
+ * @param {string} newText - New text
1532
+ * @returns {string} HTML smart diff view
1533
+ */
1534
+ generateSmartDiff(oldText, newText) {
1535
+ const oldLines = oldText.split('\n');
1536
+ const newLines = newText.split('\n');
1537
+
1538
+ // Find differences using simple line-by-line comparison
1539
+ const changes = this.findLineChanges(oldLines, newLines);
1540
+
1541
+ let diffHtml = '<div class="diff-editor smart-diff">';
1542
+
1543
+ // Header with file info
1544
+ diffHtml += `
1545
+ <div class="diff-header">
1546
+ <span class="diff-stats">Smart diff showing ${changes.length} change block${changes.length !== 1 ? 's' : ''}</span>
1547
+ </div>
1548
+ `;
1549
+
1550
+ if (changes.length === 0) {
1551
+ diffHtml += `
1552
+ <div class="no-changes">
1553
+ <span class="no-changes-text">No line-level changes detected (whitespace or formatting only)</span>
1554
+ </div>
1555
+ `;
1556
+ } else {
1557
+ changes.forEach((change, index) => {
1558
+ diffHtml += `<div class="change-block" data-change-index="${index}">`;
1559
+
1560
+ // Show removed lines
1561
+ if (change.removed.length > 0) {
1562
+ change.removed.forEach(line => {
1563
+ diffHtml += `
1564
+ <div class="diff-line removed-line smart">
1565
+ <span class="line-prefix">-</span>
1566
+ <span class="line-content">${this.escapeHtml(line) || ' '}</span>
1567
+ </div>
1568
+ `;
1569
+ });
1570
+ }
1571
+
1572
+ // Show added lines
1573
+ if (change.added.length > 0) {
1574
+ change.added.forEach(line => {
1575
+ diffHtml += `
1576
+ <div class="diff-line added-line smart">
1577
+ <span class="line-prefix">+</span>
1578
+ <span class="line-content">${this.escapeHtml(line) || ' '}</span>
1579
+ </div>
1580
+ `;
1581
+ });
1582
+ }
1583
+
1584
+ diffHtml += '</div>';
1585
+
1586
+ // Add separator between change blocks (except for the last one)
1587
+ if (index < changes.length - 1) {
1588
+ diffHtml += '<div class="change-separator">⋯</div>';
1589
+ }
1590
+ });
1591
+ }
1592
+
1593
+ diffHtml += '</div>';
1594
+
1595
+ return diffHtml;
1596
+ }
1597
+
1598
+ /**
1599
+ * Find line changes between old and new text
1600
+ * @param {string[]} oldLines - Original lines
1601
+ * @param {string[]} newLines - New lines
1602
+ * @returns {Array} Array of change blocks
1603
+ */
1604
+ findLineChanges(oldLines, newLines) {
1605
+ const changes = [];
1606
+
1607
+ // Simple approach: if texts are different, show both
1608
+ // This can be enhanced with more sophisticated diff algorithms
1609
+ if (oldLines.join('\n') !== newLines.join('\n')) {
1610
+ // For now, treat as one big change block
1611
+ // This could be enhanced to find actual line-by-line differences
1612
+ changes.push({
1613
+ removed: oldLines.slice(0, 10), // Limit to first 10 lines
1614
+ added: newLines.slice(0, 10) // Limit to first 10 lines
1615
+ });
1616
+ }
1617
+
1618
+ return changes;
1619
+ }
1620
+
1621
+ /**
1622
+ * Open Write tool specific modal
1623
+ * @param {Object} writeToolData - Write tool data
1624
+ */
1625
+ openWriteToolModal(writeToolData) {
1626
+ const input = writeToolData.input || {};
1627
+ const filePath = input.file_path || 'Unknown file';
1628
+ const fileName = filePath.split('/').pop() || 'Unknown';
1629
+ const fileExtension = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : 'txt';
1630
+ const content = input.content || '';
1631
+ const toolId = writeToolData.id || 'unknown';
1632
+
1633
+ // Analyze file context
1634
+ const isConfigFile = ['json', 'yml', 'yaml', 'toml', 'ini', 'conf', 'config'].includes(fileExtension);
1635
+ const isDocFile = ['md', 'txt', 'rst', 'adoc'].includes(fileExtension);
1636
+ const isCodeFile = ['js', 'ts', 'py', 'go', 'rs', 'java', 'cpp', 'c', 'h', 'css', 'html', 'jsx', 'tsx'].includes(fileExtension);
1637
+ const isProjectRoot = fileName.toLowerCase().includes('claude') || fileName.toLowerCase().includes('readme') || fileName.toLowerCase().includes('package');
1638
+
1639
+ let fileCategory = '';
1640
+ let contextIcon = '📝';
1641
+
1642
+ if (isProjectRoot) {
1643
+ fileCategory = 'Project Documentation';
1644
+ contextIcon = '📋';
1645
+ } else if (fileName.toLowerCase().includes('claude')) {
1646
+ fileCategory = 'Claude Configuration';
1647
+ contextIcon = '🤖';
1648
+ } else if (isConfigFile) {
1649
+ fileCategory = 'Configuration File';
1650
+ contextIcon = '⚙️';
1651
+ } else if (isDocFile) {
1652
+ fileCategory = 'Documentation';
1653
+ contextIcon = '📚';
1654
+ } else if (isCodeFile) {
1655
+ fileCategory = 'Source Code';
1656
+ contextIcon = '💻';
1657
+ } else {
1658
+ fileCategory = 'Project File';
1659
+ contextIcon = '📝';
1660
+ }
1661
+
1662
+ const modalHTML = `
1663
+ <div class="agent-modal-overlay" id="agent-modal-overlay">
1664
+ <div class="agent-modal write-tool-modal">
1665
+ <div class="agent-modal-header">
1666
+ <div class="agent-modal-title">
1667
+ <div class="agent-title-main">
1668
+ <div class="tool-icon write-tool">
1669
+ <span style="font-size: 20px;">${contextIcon}</span>
1670
+ </div>
1671
+ <div class="agent-title-info">
1672
+ <h3>File Creation: ${fileName}</h3>
1673
+ <div class="agent-subtitle">
1674
+ <span class="tool-type-badge">${fileCategory}</span>
1675
+ <span class="tool-id-badge">ID: ${toolId.slice(-8)}</span>
1676
+ </div>
1677
+ </div>
1678
+ </div>
1679
+ </div>
1680
+ <button class="agent-modal-close" id="agent-modal-close">&times;</button>
1681
+ </div>
1682
+
1683
+ <div class="agent-modal-content">
1684
+ <div class="raw-parameters-section primary-section">
1685
+ <h4>🔧 Tool Parameters</h4>
1686
+ <div class="raw-params-container">
1687
+ <pre class="raw-params-json">${JSON.stringify(input, null, 2)}</pre>
1688
+ </div>
1689
+ <div class="params-summary">
1690
+ <span class="param-chip">Tool ID: ${toolId.slice(-8)}</span>
1691
+ <span class="param-chip">File: ${fileName}</span>
1692
+ <span class="param-chip">Size: ${content.length} characters</span>
1693
+ <span class="param-chip">Type: ${fileExtension.toUpperCase()}</span>
1694
+ </div>
1695
+ </div>
1696
+
1697
+ <div class="file-content-section">
1698
+ <div class="content-header-section">
1699
+ <h4>📄 File Content Preview</h4>
1700
+ <div class="content-stats">
1701
+ <span class="content-stat">${content.split('\\n').length} lines</span>
1702
+ <span class="content-stat">${content.length} chars</span>
1703
+ <span class="content-stat">${Math.round(content.length / 1024 * 100) / 100} KB</span>
1704
+ </div>
1705
+ </div>
1706
+ <div class="content-preview-container">
1707
+ <pre class="content-preview">${this.escapeHtml(content.substring(0, 1000))}${content.length > 1000 ? '\\n\\n... [truncated, showing first 1000 characters]' : ''}</pre>
1708
+ </div>
1709
+ </div>
1710
+
1711
+
1712
+ <div class="file-insights-section">
1713
+ <h4>📊 Creation Insights</h4>
1714
+ <div class="insights-grid">
1715
+ <div class="insight-card">
1716
+ <div class="insight-header">
1717
+ <span class="insight-icon">${contextIcon}</span>
1718
+ <span class="insight-title">File Type</span>
1719
+ </div>
1720
+ <div class="insight-content">${fileCategory}</div>
1721
+ </div>
1722
+ <div class="insight-card">
1723
+ <div class="insight-header">
1724
+ <span class="insight-icon">📏</span>
1725
+ <span class="insight-title">Content Size</span>
1726
+ </div>
1727
+ <div class="insight-content">${content.length} characters</div>
1728
+ </div>
1729
+ <div class="insight-card">
1730
+ <div class="insight-header">
1731
+ <span class="insight-icon">📊</span>
1732
+ <span class="insight-title">Structure</span>
1733
+ </div>
1734
+ <div class="insight-content">${content.split('\\n').length} lines</div>
1735
+ </div>
1736
+ <div class="insight-card">
1737
+ <div class="insight-header">
1738
+ <span class="insight-icon">🚀</span>
1739
+ <span class="insight-title">Purpose</span>
1740
+ </div>
1741
+ <div class="insight-content">${content.length === 0 ? 'Empty file' : content.length < 100 ? 'Simple file' : content.length < 1000 ? 'Medium file' : 'Large file'}</div>
1742
+ </div>
1743
+ </div>
1744
+ </div>
1745
+ </div>
1746
+ </div>
1747
+ </div>
1748
+ `;
1749
+
1750
+ // Add modal to DOM
1751
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
1752
+
1753
+ // Bind close events
1754
+ document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
1755
+ document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
1756
+ if (e.target.id === 'agent-modal-overlay') {
1757
+ this.closeAgentModal();
1758
+ }
1759
+ });
1760
+
1761
+ // ESC key to close
1762
+ this.modalKeydownHandler = (e) => {
1763
+ if (e.key === 'Escape') {
1764
+ this.closeAgentModal();
1765
+ }
1766
+ };
1767
+ document.addEventListener('keydown', this.modalKeydownHandler);
1768
+ }
1769
+
1770
+ /**
1771
+ * Open Glob tool specific modal
1772
+ * @param {Object} globToolData - Glob tool data
1773
+ */
1774
+ openGlobToolModal(globToolData) {
1775
+ console.log('🔍 Opening Glob tool modal with data:', globToolData);
1776
+
1777
+ if (!globToolData || !globToolData.input) {
1778
+ console.warn('⚠️ Glob tool data missing or invalid');
1779
+ return;
1780
+ }
1781
+
1782
+ // Extract Glob parameters
1783
+ const pattern = globToolData.input.pattern || '';
1784
+ const searchPath = globToolData.input.path || '(current directory)';
1785
+
1786
+ // Categorize glob pattern
1787
+ let patternType = 'General';
1788
+ let patternDescription = 'File pattern search';
1789
+
1790
+ if (pattern.includes('**')) {
1791
+ patternType = 'Recursive';
1792
+ patternDescription = 'Deep directory search with recursive matching';
1793
+ } else if (pattern.includes('*')) {
1794
+ patternType = 'Wildcard';
1795
+ patternDescription = 'Basic wildcard pattern matching';
1796
+ } else if (pattern.includes('.')) {
1797
+ patternType = 'Extension';
1798
+ patternDescription = 'File extension filtering';
1799
+ } else if (pattern.includes('/')) {
1800
+ patternType = 'Path-based';
1801
+ patternDescription = 'Directory structure pattern';
1802
+ }
1803
+
1804
+ // Detect pattern specifics
1805
+ const isJavaScript = pattern.includes('.js') || pattern.includes('.ts') || pattern.includes('.jsx') || pattern.includes('.tsx');
1806
+ const isStyles = pattern.includes('.css') || pattern.includes('.scss') || pattern.includes('.sass');
1807
+ const isMarkdown = pattern.includes('.md') || pattern.includes('.mdx');
1808
+ const isConfig = pattern.includes('config') || pattern.includes('.json') || pattern.includes('.yml') || pattern.includes('.yaml');
1809
+
1810
+ let projectContext = '';
1811
+ if (isJavaScript) projectContext = 'JavaScript/TypeScript files';
1812
+ else if (isStyles) projectContext = 'Stylesheet files';
1813
+ else if (isMarkdown) projectContext = 'Documentation files';
1814
+ else if (isConfig) projectContext = 'Configuration files';
1815
+ else projectContext = 'General file search';
1816
+
1817
+ const modalContent = `
1818
+ <div class="agent-modal-header">
1819
+ <div class="agent-modal-title">
1820
+ <div class="agent-title-main">
1821
+ <div class="tool-icon glob-tool">
1822
+ <span style="font-size: 20px;">🔍</span>
1823
+ </div>
1824
+ <div class="agent-title-info">
1825
+ <h3>Glob Pattern: ${this.escapeHtml(pattern)}</h3>
1826
+ <div class="agent-subtitle">
1827
+ <span class="tool-type-badge">${patternType}</span>
1828
+ <span class="tool-id-badge">ID: ${globToolData.id ? globToolData.id.slice(-8) : 'unknown'}</span>
1829
+ </div>
1830
+ </div>
1831
+ </div>
1832
+ </div>
1833
+ <button class="agent-modal-close" id="agent-modal-close">&times;</button>
1834
+ </div>
1835
+
1836
+ <div class="agent-modal-content">
1837
+ <!-- Primary Section: Tool Parameters -->
1838
+ <div class="raw-parameters-section primary-section">
1839
+ <h4>🔧 Tool Parameters</h4>
1840
+ <div class="raw-params-container">
1841
+ <pre class="raw-params-json">${this.escapeHtml(JSON.stringify(globToolData.input, null, 2))}</pre>
1842
+ </div>
1843
+ </div>
1844
+
1845
+ <!-- Pattern Analysis -->
1846
+ <div class="glob-pattern-section">
1847
+ <h4>🎯 Pattern Analysis</h4>
1848
+ <div class="tool-details-grid">
1849
+ <div class="tool-detail-item">
1850
+ <span class="tool-detail-label">Search Pattern:</span>
1851
+ <span class="tool-detail-value pattern-display">${this.escapeHtml(pattern)}</span>
1852
+ </div>
1853
+ <div class="tool-detail-item">
1854
+ <span class="tool-detail-label">Pattern Type:</span>
1855
+ <span class="tool-detail-value">${patternType}</span>
1856
+ </div>
1857
+ <div class="tool-detail-item">
1858
+ <span class="tool-detail-label">Description:</span>
1859
+ <span class="tool-detail-value">${patternDescription}</span>
1860
+ </div>
1861
+ <div class="tool-detail-item">
1862
+ <span class="tool-detail-label">Search Location:</span>
1863
+ <span class="tool-detail-value">${this.escapeHtml(searchPath)}</span>
1864
+ </div>
1865
+ <div class="tool-detail-item">
1866
+ <span class="tool-detail-label">Target Files:</span>
1867
+ <span class="tool-detail-value">${projectContext}</span>
1868
+ </div>
1869
+ </div>
1870
+ </div>
1871
+
1872
+ <!-- Pattern Breakdown -->
1873
+ <div class="glob-components-section">
1874
+ <h4>🧩 Pattern Components</h4>
1875
+ <div class="pattern-breakdown">
1876
+ ${this.analyzeGlobPattern(pattern)}
1877
+ </div>
1878
+ </div>
1879
+
1880
+ <!-- File Discovery Insights -->
1881
+ <div class="glob-insights-section">
1882
+ <h4>📊 Discovery Insights</h4>
1883
+ <div class="tool-insights">
1884
+ <div class="insight-item">
1885
+ <span class="insight-label">Scope:</span>
1886
+ <span class="insight-value">${pattern.includes('**') ? 'Recursive (all subdirectories)' : 'Current level only'}</span>
1887
+ </div>
1888
+ <div class="insight-item">
1889
+ <span class="insight-label">Efficiency:</span>
1890
+ <span class="insight-value">${pattern.length > 20 ? 'Complex pattern (slower)' : 'Simple pattern (fast)'}</span>
1891
+ </div>
1892
+ <div class="insight-item">
1893
+ <span class="insight-label">Match Strategy:</span>
1894
+ <span class="insight-value">${pattern.includes('*') ? 'Wildcard matching' : 'Exact pattern'}</span>
1895
+ </div>
1896
+ <div class="insight-item">
1897
+ <span class="insight-label">File Context:</span>
1898
+ <span class="insight-value">${projectContext}</span>
1899
+ </div>
1900
+ </div>
1901
+ </div>
1902
+ </div>
1903
+ `;
1904
+
1905
+ const modalHTML = `
1906
+ <div class="agent-modal-overlay" id="agent-modal-overlay">
1907
+ <div class="agent-modal glob-tool-modal">
1908
+ ${modalContent}
1909
+ </div>
1910
+ </div>
1911
+ `;
1912
+
1913
+ // Add modal to DOM
1914
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
1915
+
1916
+ // Bind close events
1917
+ document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
1918
+ document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
1919
+ if (e.target.id === 'agent-modal-overlay') {
1920
+ this.closeAgentModal();
1921
+ }
1922
+ });
1923
+
1924
+ // ESC key to close - store reference for cleanup
1925
+ this.modalKeydownHandler = (e) => {
1926
+ if (e.key === 'Escape') {
1927
+ this.closeAgentModal();
1928
+ }
1929
+ };
1930
+ document.addEventListener('keydown', this.modalKeydownHandler);
1931
+ }
1932
+
1933
+ /**
1934
+ * Analyze glob pattern components
1935
+ * @param {string} pattern - Glob pattern to analyze
1936
+ * @returns {string} HTML breakdown of pattern
1937
+ */
1938
+ analyzeGlobPattern(pattern) {
1939
+ if (!pattern) return '<span class="pattern-empty">No pattern specified</span>';
1940
+
1941
+ const components = [];
1942
+ const parts = pattern.split('/');
1943
+
1944
+ parts.forEach((part, index) => {
1945
+ let description = '';
1946
+ let type = 'literal';
1947
+
1948
+ if (part === '**') {
1949
+ description = 'Recursive directory wildcard (matches any nested path)';
1950
+ type = 'recursive';
1951
+ } else if (part.includes('*')) {
1952
+ if (part === '*') {
1953
+ description = 'Match any single directory or filename';
1954
+ type = 'wildcard';
1955
+ } else if (part.startsWith('*.')) {
1956
+ description = `Match files with ${part.substring(2)} extension`;
1957
+ type = 'extension';
1958
+ } else {
1959
+ description = 'Partial wildcard match';
1960
+ type = 'partial-wildcard';
1961
+ }
1962
+ } else if (part.includes('?')) {
1963
+ description = 'Single character wildcard';
1964
+ type = 'char-wildcard';
1965
+ } else if (part.includes('[') && part.includes(']')) {
1966
+ description = 'Character class match';
1967
+ type = 'char-class';
1968
+ } else if (part) {
1969
+ description = 'Literal directory/filename';
1970
+ type = 'literal';
1971
+ }
1972
+
1973
+ if (part) {
1974
+ components.push(`
1975
+ <div class="pattern-component ${type}">
1976
+ <code class="pattern-part">${this.escapeHtml(part)}</code>
1977
+ <span class="pattern-desc">${description}</span>
1978
+ </div>
1979
+ `);
1980
+ }
1981
+ });
1982
+
1983
+ return components.length > 0 ? components.join('') : '<span class="pattern-empty">Empty pattern</span>';
1984
+ }
1985
+
1986
+ /**
1987
+ * Open Grep tool specific modal
1988
+ * @param {Object} grepToolData - Grep tool data
1989
+ */
1990
+ openGrepToolModal(grepToolData) {
1991
+ console.log('🔍 Opening Grep tool modal with data:', grepToolData);
1992
+
1993
+ if (!grepToolData || !grepToolData.input) {
1994
+ console.warn('⚠️ Grep tool data missing or invalid');
1995
+ return;
1996
+ }
1997
+
1998
+ // Extract Grep parameters
1999
+ const pattern = grepToolData.input.pattern || '';
2000
+ const searchPath = grepToolData.input.path || '(current directory)';
2001
+ const outputMode = grepToolData.input.output_mode || 'files_with_matches';
2002
+ const glob = grepToolData.input.glob || '';
2003
+ const type = grepToolData.input.type || '';
2004
+ const caseInsensitive = grepToolData.input['-i'] || false;
2005
+ const contextBefore = grepToolData.input['-B'] || 0;
2006
+ const contextAfter = grepToolData.input['-A'] || 0;
2007
+ const contextAround = grepToolData.input['-C'] || 0;
2008
+ const showLineNumbers = grepToolData.input['-n'] || false;
2009
+ const multiline = grepToolData.input.multiline || false;
2010
+
2011
+ // Analyze search pattern
2012
+ let patternType = 'Literal';
2013
+ let patternComplexity = 'Simple';
2014
+ let searchScope = 'Text content';
2015
+
2016
+ // Detect regex patterns
2017
+ if (pattern.includes('.*') || pattern.includes('.+') || pattern.includes('\\w') || pattern.includes('\\d')) {
2018
+ patternType = 'Regular Expression';
2019
+ patternComplexity = 'Complex';
2020
+ } else if (pattern.includes('*') || pattern.includes('?') || pattern.includes('[') || pattern.includes(']')) {
2021
+ patternType = 'Wildcard Pattern';
2022
+ patternComplexity = 'Moderate';
2023
+ }
2024
+
2025
+ // Detect specific search types
2026
+ if (pattern.includes('function') || pattern.includes('class') || pattern.includes('import')) {
2027
+ searchScope = 'Code structure';
2028
+ } else if (pattern.includes('TODO') || pattern.includes('FIXME') || pattern.includes('NOTE')) {
2029
+ searchScope = 'Code comments';
2030
+ } else if (pattern.includes('error') || pattern.includes('Error') || pattern.includes('exception')) {
2031
+ searchScope = 'Error handling';
2032
+ } else if (pattern.includes('test') || pattern.includes('spec') || pattern.includes('describe')) {
2033
+ searchScope = 'Test code';
2034
+ }
2035
+
2036
+ // File type analysis
2037
+ let fileContext = 'All files';
2038
+ if (type) {
2039
+ switch (type) {
2040
+ case 'js': fileContext = 'JavaScript files'; break;
2041
+ case 'ts': fileContext = 'TypeScript files'; break;
2042
+ case 'py': fileContext = 'Python files'; break;
2043
+ case 'go': fileContext = 'Go files'; break;
2044
+ case 'rust': fileContext = 'Rust files'; break;
2045
+ case 'java': fileContext = 'Java files'; break;
2046
+ case 'css': fileContext = 'CSS files'; break;
2047
+ case 'html': fileContext = 'HTML files'; break;
2048
+ case 'md': fileContext = 'Markdown files'; break;
2049
+ default: fileContext = `${type} files`; break;
2050
+ }
2051
+ } else if (glob) {
2052
+ if (glob.includes('*.js') || glob.includes('*.ts')) fileContext = 'JavaScript/TypeScript files';
2053
+ else if (glob.includes('*.py')) fileContext = 'Python files';
2054
+ else if (glob.includes('*.md')) fileContext = 'Documentation files';
2055
+ else if (glob.includes('*.css') || glob.includes('*.scss')) fileContext = 'Stylesheet files';
2056
+ else fileContext = `Files matching "${glob}"`;
2057
+ }
2058
+
2059
+ const modalContent = `
2060
+ <div class="agent-modal-header">
2061
+ <div class="agent-modal-title">
2062
+ <div class="agent-title-main">
2063
+ <div class="tool-icon grep-tool">
2064
+ <span style="font-size: 20px;">🔍</span>
2065
+ </div>
2066
+ <div class="agent-title-info">
2067
+ <h3>Grep Search: ${this.escapeHtml(pattern)}</h3>
2068
+ <div class="agent-subtitle">
2069
+ <span class="tool-type-badge">${patternType}</span>
2070
+ <span class="tool-id-badge">ID: ${grepToolData.id ? grepToolData.id.slice(-8) : 'unknown'}</span>
2071
+ </div>
2072
+ </div>
2073
+ </div>
2074
+ </div>
2075
+ <button class="agent-modal-close" id="agent-modal-close">&times;</button>
2076
+ </div>
2077
+
2078
+ <div class="agent-modal-content">
2079
+ <!-- Primary Section: Tool Parameters -->
2080
+ <div class="raw-parameters-section primary-section">
2081
+ <h4>🔧 Tool Parameters</h4>
2082
+ <div class="raw-params-container">
2083
+ <pre class="raw-params-json">${this.escapeHtml(JSON.stringify(grepToolData.input, null, 2))}</pre>
2084
+ </div>
2085
+ </div>
2086
+
2087
+ <!-- Search Configuration -->
2088
+ <div class="grep-config-section">
2089
+ <h4>🎯 Search Configuration</h4>
2090
+ <div class="tool-details-grid">
2091
+ <div class="tool-detail-item">
2092
+ <span class="tool-detail-label">Search Pattern:</span>
2093
+ <span class="tool-detail-value search-pattern">${this.escapeHtml(pattern)}</span>
2094
+ </div>
2095
+ <div class="tool-detail-item">
2096
+ <span class="tool-detail-label">Pattern Type:</span>
2097
+ <span class="tool-detail-value">${patternType}</span>
2098
+ </div>
2099
+ <div class="tool-detail-item">
2100
+ <span class="tool-detail-label">Complexity:</span>
2101
+ <span class="tool-detail-value">${patternComplexity}</span>
2102
+ </div>
2103
+ <div class="tool-detail-item">
2104
+ <span class="tool-detail-label">Search Location:</span>
2105
+ <span class="tool-detail-value">${this.escapeHtml(searchPath)}</span>
2106
+ </div>
2107
+ <div class="tool-detail-item">
2108
+ <span class="tool-detail-label">Target Files:</span>
2109
+ <span class="tool-detail-value">${fileContext}</span>
2110
+ </div>
2111
+ <div class="tool-detail-item">
2112
+ <span class="tool-detail-label">Output Mode:</span>
2113
+ <span class="tool-detail-value">${outputMode.replace('_', ' ')}</span>
2114
+ </div>
2115
+ </div>
2116
+ </div>
2117
+
2118
+ <!-- Search Options -->
2119
+ <div class="grep-options-section">
2120
+ <h4>⚙️ Search Options</h4>
2121
+ <div class="search-options">
2122
+ ${this.generateGrepOptionsDisplay(grepToolData.input)}
2123
+ </div>
2124
+ </div>
2125
+
2126
+ <!-- Pattern Analysis -->
2127
+ <div class="grep-pattern-section">
2128
+ <h4>🧩 Pattern Analysis</h4>
2129
+ <div class="pattern-analysis">
2130
+ ${this.analyzeGrepPattern(pattern)}
2131
+ </div>
2132
+ </div>
2133
+
2134
+ <!-- Search Insights -->
2135
+ <div class="grep-insights-section">
2136
+ <h4>📊 Search Insights</h4>
2137
+ <div class="tool-insights">
2138
+ <div class="insight-item">
2139
+ <span class="insight-label">Search Scope:</span>
2140
+ <span class="insight-value">${searchScope}</span>
2141
+ </div>
2142
+ <div class="insight-item">
2143
+ <span class="insight-label">Case Sensitivity:</span>
2144
+ <span class="insight-value">${caseInsensitive ? 'Case insensitive' : 'Case sensitive'}</span>
2145
+ </div>
2146
+ <div class="insight-item">
2147
+ <span class="insight-label">Multiline Mode:</span>
2148
+ <span class="insight-value">${multiline ? 'Enabled (cross-line patterns)' : 'Disabled (single-line only)'}</span>
2149
+ </div>
2150
+ <div class="insight-item">
2151
+ <span class="insight-label">Context Lines:</span>
2152
+ <span class="insight-value">${contextAround > 0 ? `${contextAround} lines around` : contextBefore > 0 || contextAfter > 0 ? `${contextBefore} before, ${contextAfter} after` : 'None'}</span>
2153
+ </div>
2154
+ </div>
2155
+ </div>
2156
+ </div>
2157
+ `;
2158
+
2159
+ const modalHTML = `
2160
+ <div class="agent-modal-overlay" id="agent-modal-overlay">
2161
+ <div class="agent-modal grep-tool-modal">
2162
+ ${modalContent}
2163
+ </div>
2164
+ </div>
2165
+ `;
2166
+
2167
+ // Add modal to DOM
2168
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
2169
+
2170
+ // Bind close events
2171
+ document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
2172
+ document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
2173
+ if (e.target.id === 'agent-modal-overlay') {
2174
+ this.closeAgentModal();
2175
+ }
2176
+ });
2177
+
2178
+ // ESC key to close - store reference for cleanup
2179
+ this.modalKeydownHandler = (e) => {
2180
+ if (e.key === 'Escape') {
2181
+ this.closeAgentModal();
2182
+ }
2183
+ };
2184
+ document.addEventListener('keydown', this.modalKeydownHandler);
2185
+ }
2186
+
2187
+ /**
2188
+ * Generate Grep options display
2189
+ * @param {Object} input - Grep input parameters
2190
+ * @returns {string} HTML for options display
2191
+ */
2192
+ generateGrepOptionsDisplay(input) {
2193
+ const options = [];
2194
+
2195
+ if (input['-i']) options.push({ name: 'Case Insensitive (-i)', status: 'enabled', desc: 'Ignore case when matching' });
2196
+ if (input['-n']) options.push({ name: 'Line Numbers (-n)', status: 'enabled', desc: 'Show line numbers in output' });
2197
+ if (input.multiline) options.push({ name: 'Multiline Mode', status: 'enabled', desc: 'Allow patterns to span multiple lines' });
2198
+ if (input['-A']) options.push({ name: `After Context (-A ${input['-A']})`, status: 'enabled', desc: `Show ${input['-A']} lines after matches` });
2199
+ if (input['-B']) options.push({ name: `Before Context (-B ${input['-B']})`, status: 'enabled', desc: `Show ${input['-B']} lines before matches` });
2200
+ if (input['-C']) options.push({ name: `Around Context (-C ${input['-C']})`, status: 'enabled', desc: `Show ${input['-C']} lines around matches` });
2201
+ if (input.glob) options.push({ name: `File Filter (--glob)`, status: 'enabled', desc: `Only search files matching "${input.glob}"` });
2202
+ if (input.type) options.push({ name: `File Type (--type)`, status: 'enabled', desc: `Only search ${input.type} files` });
2203
+ if (input.head_limit) options.push({ name: `Result Limit`, status: 'enabled', desc: `Limit to first ${input.head_limit} results` });
2204
+
2205
+ if (options.length === 0) {
2206
+ return '<div class="no-options">Using default search options</div>';
2207
+ }
2208
+
2209
+ return options.map(option => `
2210
+ <div class="search-option ${option.status}">
2211
+ <div class="option-header">
2212
+ <span class="option-name">${option.name}</span>
2213
+ <span class="option-status ${option.status}">${option.status}</span>
2214
+ </div>
2215
+ <div class="option-desc">${option.desc}</div>
2216
+ </div>
2217
+ `).join('');
2218
+ }
2219
+
2220
+ /**
2221
+ * Analyze grep pattern components
2222
+ * @param {string} pattern - Grep pattern to analyze
2223
+ * @returns {string} HTML breakdown of pattern
2224
+ */
2225
+ analyzeGrepPattern(pattern) {
2226
+ if (!pattern) return '<div class="pattern-empty">No pattern specified</div>';
2227
+
2228
+ const analysis = [];
2229
+
2230
+ // Detect regex components
2231
+ if (pattern.includes('.*')) {
2232
+ analysis.push({ component: '.*', desc: 'Match any characters (zero or more)', type: 'regex' });
2233
+ }
2234
+ if (pattern.includes('.+')) {
2235
+ analysis.push({ component: '.+', desc: 'Match any characters (one or more)', type: 'regex' });
2236
+ }
2237
+ if (pattern.includes('\\w')) {
2238
+ analysis.push({ component: '\\w', desc: 'Match word characters (letters, digits, underscore)', type: 'regex' });
2239
+ }
2240
+ if (pattern.includes('\\d')) {
2241
+ analysis.push({ component: '\\d', desc: 'Match digit characters (0-9)', type: 'regex' });
2242
+ }
2243
+ if (pattern.includes('\\s')) {
2244
+ analysis.push({ component: '\\s', desc: 'Match whitespace characters', type: 'regex' });
2245
+ }
2246
+ if (pattern.includes('^')) {
2247
+ analysis.push({ component: '^', desc: 'Match start of line', type: 'anchor' });
2248
+ }
2249
+ if (pattern.includes('$')) {
2250
+ analysis.push({ component: '$', desc: 'Match end of line', type: 'anchor' });
2251
+ }
2252
+ if (pattern.includes('|')) {
2253
+ analysis.push({ component: '|', desc: 'Logical OR (alternative patterns)', type: 'operator' });
2254
+ }
2255
+
2256
+ // Detect character classes
2257
+ const charClassMatch = pattern.match(/\[([^\]]+)\]/g);
2258
+ if (charClassMatch) {
2259
+ charClassMatch.forEach(match => {
2260
+ analysis.push({ component: match, desc: `Match any character in the set: ${match}`, type: 'charclass' });
2261
+ });
2262
+ }
2263
+
2264
+ // Detect groups
2265
+ const groupMatch = pattern.match(/\(([^)]+)\)/g);
2266
+ if (groupMatch) {
2267
+ groupMatch.forEach(match => {
2268
+ analysis.push({ component: match, desc: `Capture group: ${match}`, type: 'group' });
2269
+ });
2270
+ }
2271
+
2272
+ if (analysis.length === 0) {
2273
+ return '<div class="pattern-simple">Simple literal text search</div>';
2274
+ }
2275
+
2276
+ return analysis.map(item => `
2277
+ <div class="pattern-component ${item.type}">
2278
+ <code class="pattern-part">${this.escapeHtml(item.component)}</code>
2279
+ <span class="pattern-desc">${item.desc}</span>
2280
+ </div>
2281
+ `).join('');
2282
+ }
2283
+
2284
+ /**
2285
+ * Open TodoWrite tool specific modal
2286
+ * @param {Object} todoToolData - TodoWrite tool data
2287
+ */
2288
+ openTodoWriteToolModal(todoToolData) {
2289
+ console.log('📝 Opening TodoWrite tool modal with data:', todoToolData);
2290
+
2291
+ if (!todoToolData || !todoToolData.input) {
2292
+ console.warn('⚠️ TodoWrite tool data missing or invalid');
2293
+ return;
2294
+ }
2295
+
2296
+ // Extract TodoWrite parameters
2297
+ const todos = todoToolData.input.todos || [];
2298
+ const todoCount = todos.length;
2299
+
2300
+ // Analyze todos
2301
+ const statusCounts = {
2302
+ pending: 0,
2303
+ in_progress: 0,
2304
+ completed: 0
2305
+ };
2306
+
2307
+ const priorityCounts = {
2308
+ high: 0,
2309
+ medium: 0,
2310
+ low: 0
2311
+ };
2312
+
2313
+ todos.forEach(todo => {
2314
+ if (statusCounts.hasOwnProperty(todo.status)) {
2315
+ statusCounts[todo.status]++;
2316
+ }
2317
+ if (priorityCounts.hasOwnProperty(todo.priority)) {
2318
+ priorityCounts[todo.priority]++;
2319
+ }
2320
+ });
2321
+
2322
+ // Calculate completion rate
2323
+ const completionRate = todoCount > 0 ? Math.round((statusCounts.completed / todoCount) * 100) : 0;
2324
+
2325
+ // Find longest and shortest todos
2326
+ const todoLengths = todos.map(todo => todo.content.length);
2327
+ const avgLength = todoLengths.length > 0 ? Math.round(todoLengths.reduce((a, b) => a + b, 0) / todoLengths.length) : 0;
2328
+
2329
+ const modalContent = `
2330
+ <div class="agent-modal-header">
2331
+ <div class="agent-modal-title">
2332
+ <div class="agent-title-main">
2333
+ <div class="tool-icon todo-tool">
2334
+ <span style="font-size: 20px;">📝</span>
2335
+ </div>
2336
+ <div class="agent-title-info">
2337
+ <h3>Todo Management: ${todoCount} tasks</h3>
2338
+ <div class="agent-subtitle">
2339
+ <span class="tool-type-badge">${completionRate}% Complete</span>
2340
+ <span class="tool-id-badge">ID: ${todoToolData.id ? todoToolData.id.slice(-8) : 'unknown'}</span>
2341
+ </div>
2342
+ </div>
2343
+ </div>
2344
+ </div>
2345
+ <button class="agent-modal-close" id="agent-modal-close">&times;</button>
2346
+ </div>
2347
+
2348
+ <div class="agent-modal-content">
2349
+ <!-- Primary Section: Tool Parameters -->
2350
+ <div class="raw-parameters-section primary-section">
2351
+ <h4>🔧 Tool Parameters</h4>
2352
+ <div class="raw-params-container">
2353
+ <pre class="raw-params-json">${this.escapeHtml(JSON.stringify(todoToolData.input, null, 2))}</pre>
2354
+ </div>
2355
+ </div>
2356
+
2357
+ <!-- Todo Summary -->
2358
+ <div class="todo-summary-section">
2359
+ <h4>📊 Todo Summary</h4>
2360
+ <div class="todo-summary">
2361
+ <div class="summary-stats">
2362
+ <div class="stat-item">
2363
+ <span class="stat-number">${todoCount}</span>
2364
+ <span class="stat-label">Total Todos</span>
2365
+ </div>
2366
+ <div class="stat-item">
2367
+ <span class="stat-number">${completionRate}%</span>
2368
+ <span class="stat-label">Completion Rate</span>
2369
+ </div>
2370
+ <div class="stat-item">
2371
+ <span class="stat-number">${avgLength}</span>
2372
+ <span class="stat-label">Avg Length</span>
2373
+ </div>
2374
+ </div>
2375
+ </div>
2376
+ </div>
2377
+
2378
+ <!-- Status Breakdown -->
2379
+ <div class="todo-status-section">
2380
+ <h4>📈 Status Breakdown</h4>
2381
+ <div class="status-breakdown">
2382
+ <div class="status-item pending">
2383
+ <div class="status-info">
2384
+ <span class="status-name">Pending</span>
2385
+ <span class="status-count">${statusCounts.pending}</span>
2386
+ </div>
2387
+ <div class="status-bar">
2388
+ <div class="status-fill" style="width: ${todoCount > 0 ? (statusCounts.pending / todoCount) * 100 : 0}%"></div>
2389
+ </div>
2390
+ </div>
2391
+ <div class="status-item in-progress">
2392
+ <div class="status-info">
2393
+ <span class="status-name">In Progress</span>
2394
+ <span class="status-count">${statusCounts.in_progress}</span>
2395
+ </div>
2396
+ <div class="status-bar">
2397
+ <div class="status-fill" style="width: ${todoCount > 0 ? (statusCounts.in_progress / todoCount) * 100 : 0}%"></div>
2398
+ </div>
2399
+ </div>
2400
+ <div class="status-item completed">
2401
+ <div class="status-info">
2402
+ <span class="status-name">Completed</span>
2403
+ <span class="status-count">${statusCounts.completed}</span>
2404
+ </div>
2405
+ <div class="status-bar">
2406
+ <div class="status-fill" style="width: ${todoCount > 0 ? (statusCounts.completed / todoCount) * 100 : 0}%"></div>
2407
+ </div>
2408
+ </div>
2409
+ </div>
2410
+ </div>
2411
+
2412
+ <!-- Priority Distribution -->
2413
+ <div class="todo-priority-section">
2414
+ <h4>🎯 Priority Distribution</h4>
2415
+ <div class="priority-distribution">
2416
+ <div class="priority-item high">
2417
+ <span class="priority-label">High Priority</span>
2418
+ <span class="priority-count">${priorityCounts.high}</span>
2419
+ </div>
2420
+ <div class="priority-item medium">
2421
+ <span class="priority-label">Medium Priority</span>
2422
+ <span class="priority-count">${priorityCounts.medium}</span>
2423
+ </div>
2424
+ <div class="priority-item low">
2425
+ <span class="priority-label">Low Priority</span>
2426
+ <span class="priority-count">${priorityCounts.low}</span>
2427
+ </div>
2428
+ </div>
2429
+ </div>
2430
+
2431
+ <!-- Todo Items -->
2432
+ <div class="todo-items-section">
2433
+ <h4>📋 Todo Items</h4>
2434
+ <div class="todo-items">
2435
+ ${this.generateTodoItemsDisplay(todos)}
2436
+ </div>
2437
+ </div>
2438
+ </div>
2439
+ `;
2440
+
2441
+ const modalHTML = `
2442
+ <div class="agent-modal-overlay" id="agent-modal-overlay">
2443
+ <div class="agent-modal todo-tool-modal">
2444
+ ${modalContent}
2445
+ </div>
2446
+ </div>
2447
+ `;
2448
+
2449
+ // Add modal to DOM
2450
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
2451
+
2452
+ // Bind close events
2453
+ document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
2454
+ document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
2455
+ if (e.target.id === 'agent-modal-overlay') {
2456
+ this.closeAgentModal();
2457
+ }
2458
+ });
2459
+
2460
+ // ESC key to close - store reference for cleanup
2461
+ this.modalKeydownHandler = (e) => {
2462
+ if (e.key === 'Escape') {
2463
+ this.closeAgentModal();
2464
+ }
2465
+ };
2466
+ document.addEventListener('keydown', this.modalKeydownHandler);
2467
+ }
2468
+
2469
+ /**
2470
+ * Generate todo items display
2471
+ * @param {Array} todos - Array of todo items
2472
+ * @returns {string} HTML for todo items
2473
+ */
2474
+ generateTodoItemsDisplay(todos) {
2475
+ if (!todos || todos.length === 0) {
2476
+ return '<div class="no-todos">No todos found</div>';
2477
+ }
2478
+
2479
+ return todos.map((todo, index) => `
2480
+ <div class="todo-item ${todo.status} ${todo.priority}">
2481
+ <div class="todo-header">
2482
+ <span class="todo-index">#${index + 1}</span>
2483
+ <span class="todo-status ${todo.status}">${todo.status.replace('_', ' ')}</span>
2484
+ <span class="todo-priority ${todo.priority}">${todo.priority}</span>
2485
+ </div>
2486
+ <div class="todo-content">${this.escapeHtml(todo.content)}</div>
2487
+ <div class="todo-meta">
2488
+ <span class="todo-id">ID: ${todo.id}</span>
2489
+ <span class="todo-length">${todo.content.length} chars</span>
2490
+ </div>
2491
+ </div>
2492
+ `).join('');
2493
+ }
2494
+
2495
+ /**
2496
+ * Open Bash tool specific modal
2497
+ * @param {Object} bashToolData - Bash tool data
2498
+ */
2499
+ openBashToolModal(bashToolData) {
2500
+ const input = bashToolData.input || {};
2501
+ const command = input.command || 'Unknown command';
2502
+ const description = input.description || '';
2503
+ const timeout = input.timeout || 120000; // Default 2 minutes
2504
+ const toolId = bashToolData.id || 'unknown';
2505
+
2506
+ // Analyze command type
2507
+ const isGitCommand = command.startsWith('git ');
2508
+ const isNpmCommand = command.startsWith('npm ') || command.startsWith('yarn ') || command.startsWith('pnpm ');
2509
+ const isFileCommand = command.includes('ls') || command.includes('cat') || command.includes('find') || command.includes('grep');
2510
+ const isBuildCommand = command.includes('build') || command.includes('compile') || command.includes('make');
2511
+ const isTestCommand = command.includes('test') || command.includes('jest') || command.includes('mocha');
2512
+ const isSystemCommand = command.includes('ps') || command.includes('kill') || command.includes('sudo');
2513
+
2514
+ let commandCategory = '';
2515
+ let contextIcon = '⚡';
2516
+
2517
+ if (isGitCommand) {
2518
+ commandCategory = 'Git Operation';
2519
+ contextIcon = '🔧';
2520
+ } else if (isNpmCommand) {
2521
+ commandCategory = 'Package Management';
2522
+ contextIcon = '📦';
2523
+ } else if (isBuildCommand) {
2524
+ commandCategory = 'Build Process';
2525
+ contextIcon = '🔨';
2526
+ } else if (isTestCommand) {
2527
+ commandCategory = 'Testing';
2528
+ contextIcon = '🧪';
2529
+ } else if (isFileCommand) {
2530
+ commandCategory = 'File Operations';
2531
+ contextIcon = '📁';
2532
+ } else if (isSystemCommand) {
2533
+ commandCategory = 'System Command';
2534
+ contextIcon = '🖥️';
2535
+ } else {
2536
+ commandCategory = 'Shell Command';
2537
+ contextIcon = '⚡';
2538
+ }
2539
+
2540
+ // Parse command for better display
2541
+ const commandParts = command.split(' ');
2542
+ const mainCommand = commandParts[0] || '';
2543
+ const args = commandParts.slice(1).join(' ') || '';
2544
+
2545
+ const modalHTML = `
2546
+ <div class="agent-modal-overlay" id="agent-modal-overlay">
2547
+ <div class="agent-modal bash-tool-modal">
2548
+ <div class="agent-modal-header">
2549
+ <div class="agent-modal-title">
2550
+ <div class="agent-title-main">
2551
+ <div class="tool-icon bash-tool">
2552
+ <span style="font-size: 20px;">${contextIcon}</span>
2553
+ </div>
2554
+ <div class="agent-title-info">
2555
+ <h3>Command: ${mainCommand}</h3>
2556
+ <div class="agent-subtitle">
2557
+ <span class="tool-type-badge">${commandCategory}</span>
2558
+ <span class="tool-id-badge">ID: ${toolId.slice(-8)}</span>
2559
+ </div>
2560
+ </div>
2561
+ </div>
2562
+ </div>
2563
+ <button class="agent-modal-close" id="agent-modal-close">&times;</button>
2564
+ </div>
2565
+
2566
+ <div class="agent-modal-content">
2567
+ <div class="raw-parameters-section primary-section">
2568
+ <h4>🔧 Tool Parameters</h4>
2569
+ <div class="raw-params-container">
2570
+ <pre class="raw-params-json">${JSON.stringify(input, null, 2)}</pre>
2571
+ </div>
2572
+ <div class="params-summary">
2573
+ <span class="param-chip">Tool ID: ${toolId.slice(-8)}</span>
2574
+ <span class="param-chip">Command: ${mainCommand}</span>
2575
+ <span class="param-chip">Timeout: ${timeout/1000}s</span>
2576
+ ${description ? `<span class="param-chip">Described: Yes</span>` : `<span class="param-chip">Described: No</span>`}
2577
+ </div>
2578
+ </div>
2579
+
2580
+ <div class="command-execution-section">
2581
+ <div class="command-header-section">
2582
+ <h4>💻 Command Execution</h4>
2583
+ <div class="command-stats">
2584
+ <span class="command-stat">${commandParts.length} parts</span>
2585
+ <span class="command-stat">${command.length} chars</span>
2586
+ <span class="command-stat">${timeout/1000}s timeout</span>
2587
+ </div>
2588
+ </div>
2589
+ <div class="command-display-container">
2590
+ <div class="command-line">
2591
+ <span class="command-prompt">$</span>
2592
+ <span class="command-text">${this.escapeHtml(command)}</span>
2593
+ </div>
2594
+ ${description ? `
2595
+ <div class="command-description">
2596
+ <span class="description-label">Description:</span>
2597
+ <span class="description-text">${this.escapeHtml(description)}</span>
2598
+ </div>
2599
+ ` : ''}
2600
+ </div>
2601
+ </div>
2602
+
2603
+ <div class="command-analysis-section">
2604
+ <h4>🔍 Command Analysis</h4>
2605
+ <div class="analysis-grid">
2606
+ <div class="analysis-item">
2607
+ <span class="analysis-label">Main Command:</span>
2608
+ <code class="analysis-value">${this.escapeHtml(mainCommand)}</code>
2609
+ </div>
2610
+ ${args ? `
2611
+ <div class="analysis-item">
2612
+ <span class="analysis-label">Arguments:</span>
2613
+ <code class="analysis-value">${this.escapeHtml(args)}</code>
2614
+ </div>
2615
+ ` : ''}
2616
+ <div class="analysis-item">
2617
+ <span class="analysis-label">Category:</span>
2618
+ <code class="analysis-value">${commandCategory}</code>
2619
+ </div>
2620
+ <div class="analysis-item">
2621
+ <span class="analysis-label">Timeout:</span>
2622
+ <code class="analysis-value">${timeout === 120000 ? 'Default (2min)' : `${timeout/1000}s`}</code>
2623
+ </div>
2624
+ </div>
2625
+ </div>
2626
+
2627
+ <div class="file-insights-section">
2628
+ <h4>📊 Execution Insights</h4>
2629
+ <div class="insights-grid">
2630
+ <div class="insight-card">
2631
+ <div class="insight-header">
2632
+ <span class="insight-icon">${contextIcon}</span>
2633
+ <span class="insight-title">Command Type</span>
2634
+ </div>
2635
+ <div class="insight-content">${commandCategory}</div>
2636
+ </div>
2637
+ <div class="insight-card">
2638
+ <div class="insight-header">
2639
+ <span class="insight-icon">⏱️</span>
2640
+ <span class="insight-title">Timeout</span>
2641
+ </div>
2642
+ <div class="insight-content">${timeout/1000} seconds</div>
2643
+ </div>
2644
+ <div class="insight-card">
2645
+ <div class="insight-header">
2646
+ <span class="insight-icon">📝</span>
2647
+ <span class="insight-title">Complexity</span>
2648
+ </div>
2649
+ <div class="insight-content">${commandParts.length < 3 ? 'Simple' : commandParts.length < 6 ? 'Medium' : 'Complex'}</div>
2650
+ </div>
2651
+ <div class="insight-card">
2652
+ <div class="insight-header">
2653
+ <span class="insight-icon">🎯</span>
2654
+ <span class="insight-title">Documentation</span>
2655
+ </div>
2656
+ <div class="insight-content">${description ? 'Documented' : 'No description'}</div>
2657
+ </div>
2658
+ </div>
2659
+ </div>
2660
+ </div>
2661
+ </div>
2662
+ </div>
2663
+ `;
2664
+
2665
+ // Add modal to DOM
2666
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
2667
+
2668
+ // Bind close events
2669
+ document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
2670
+ document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
2671
+ if (e.target.id === 'agent-modal-overlay') {
2672
+ this.closeAgentModal();
2673
+ }
2674
+ });
2675
+
2676
+ // ESC key to close
2677
+ this.modalKeydownHandler = (e) => {
2678
+ if (e.key === 'Escape') {
2679
+ this.closeAgentModal();
2680
+ }
2681
+ };
2682
+ document.addEventListener('keydown', this.modalKeydownHandler);
2683
+ }
2684
+
2685
+ /**
2686
+ * Escape HTML to prevent XSS
2687
+ * @param {string} text - Text to escape
2688
+ * @returns {string} Escaped text
2689
+ */
2690
+ escapeHtml(text) {
2691
+ if (typeof text !== 'string') return String(text);
2692
+
2693
+ const div = document.createElement('div');
2694
+ div.textContent = text;
2695
+ return div.innerHTML;
2696
+ }
2697
+
2698
+ /**
2699
+ * Close agent modal
2700
+ */
2701
+ closeAgentModal() {
2702
+ const modal = document.getElementById('agent-modal-overlay');
2703
+ if (modal) {
2704
+ modal.remove();
2705
+ }
2706
+ if (this.modalKeydownHandler) {
2707
+ document.removeEventListener('keydown', this.modalKeydownHandler);
2708
+ this.modalKeydownHandler = null;
2709
+ }
2710
+ }
2711
+
2712
+ /**
2713
+ * Show agents loading state
2714
+ * @param {boolean} show - Whether to show loading
2715
+ */
2716
+ showAgentsLoading(show) {
2717
+ const loading = this.container.querySelector('#agents-loading');
2718
+ const list = this.container.querySelector('#agents-list');
2719
+
2720
+ if (loading && list) {
2721
+ loading.style.display = show ? 'flex' : 'none';
2722
+ list.style.display = show ? 'none' : 'block';
2723
+ }
2724
+ }
2725
+
2726
+ /**
2727
+ * Show agents empty state
2728
+ */
2729
+ showAgentsEmpty() {
2730
+ const empty = this.container.querySelector('#agents-empty');
2731
+ const list = this.container.querySelector('#agents-list');
2732
+ const count = this.container.querySelector('#agents-count');
2733
+
2734
+ if (empty && list && count) {
2735
+ empty.style.display = 'flex';
2736
+ list.style.display = 'none';
2737
+ count.textContent = '0 agents';
2738
+ }
2739
+ }
2740
+
2741
+ /**
2742
+ * Hide agents empty state
2743
+ */
2744
+ hideAgentsEmpty() {
2745
+ const empty = this.container.querySelector('#agents-empty');
2746
+ if (empty) {
2747
+ empty.style.display = 'none';
2748
+ }
2749
+ }
2750
+
2751
+ /**
2752
+ * Get project-specific agents for a conversation
2753
+ * @param {string} conversationId - Conversation ID
2754
+ * @returns {Array} Array of project agents for this conversation
2755
+ */
2756
+ getAgentsForConversation(conversationId) {
2757
+ const conversations = this.stateService.getStateProperty('conversations') || [];
2758
+ const conversation = conversations.find(conv => conv.id === conversationId);
2759
+
2760
+ if (!conversation || !conversation.project) {
2761
+ // Return empty array if no project (global agents are shown in main section)
2762
+ return [];
2763
+ }
2764
+
2765
+ const projectName = conversation.project;
2766
+
2767
+ // Return only project agents for this specific project
2768
+ return this.agents.filter(agent =>
2769
+ agent.level === 'project' && agent.projectName === projectName
2770
+ );
2771
+ }
2772
+
2773
+ /**
2774
+ * Show project agents modal
2775
+ * @param {string} conversationId - Conversation ID
2776
+ */
2777
+ showProjectAgents(conversationId) {
2778
+ const conversations = this.stateService.getStateProperty('conversations') || [];
2779
+ const conversation = conversations.find(conv => conv.id === conversationId);
2780
+ const projectAgents = this.getAgentsForConversation(conversationId);
2781
+
2782
+ const projectName = conversation?.project || 'Unknown Project';
2783
+ const chatTitle = conversation?.title || `Chat ${conversationId.slice(-8)}`;
2784
+
2785
+ const modalHTML = `
2786
+ <div class="agent-modal-overlay" id="project-agents-modal-overlay">
2787
+ <div class="agent-modal project-agents-modal">
2788
+ <div class="agent-modal-header">
2789
+ <div class="agent-modal-title">
2790
+ <div class="agent-title-main">
2791
+ <div class="project-icon">📁</div>
2792
+ <div class="agent-title-info">
2793
+ <h3>Project Agents</h3>
2794
+ <div class="agent-subtitle">
2795
+ <span class="project-info">${chatTitle}</span>
2796
+ <span class="agent-project-name">• ${projectName}</span>
2797
+ </div>
2798
+ </div>
2799
+ </div>
2800
+ </div>
2801
+ <button class="agent-modal-close" id="project-agents-modal-close">&times;</button>
2802
+ </div>
2803
+
2804
+ <div class="agent-modal-content">
2805
+ ${projectAgents.length === 0 ? `
2806
+ <div class="no-agents-message">
2807
+ <div class="no-agents-icon">🤖</div>
2808
+ <h4>No project agents</h4>
2809
+ <p>This project doesn't have any specific agents configured.</p>
2810
+ <p>Create agents in your project's <code>.claude/agents/</code> directory to see them here.</p>
2811
+ <p><strong>Note:</strong> Global agents are available in the main agents section.</p>
2812
+ </div>
2813
+ ` : `
2814
+ <div class="project-agents-grid">
2815
+ ${projectAgents.map(agent => `
2816
+ <div class="project-agent-card" data-agent-id="${agent.name}">
2817
+ <div class="project-agent-header">
2818
+ <div class="agent-dot" style="background-color: ${agent.color}"></div>
2819
+ <div class="project-agent-info">
2820
+ <h4>${agent.name}</h4>
2821
+ <span class="agent-level-badge ${agent.level}">${agent.level === 'project' ? 'Project' : 'User'}</span>
2822
+ </div>
2823
+ </div>
2824
+ <div class="project-agent-description">
2825
+ ${this.truncateText(agent.description, 100)}
2826
+ </div>
2827
+ <div class="project-agent-footer">
2828
+ <span class="project-agent-tools">${agent.tools && agent.tools.length > 0 ? `${agent.tools.length} tools` : 'All tools'}</span>
2829
+ <button class="project-agent-details-btn" data-agent-id="${agent.name}">Details</button>
2830
+ </div>
2831
+ </div>
2832
+ `).join('')}
2833
+ </div>
2834
+
2835
+ <div class="usage-instruction">
2836
+ <h4>💡 How to use these agents</h4>
2837
+ <p>In your conversation, mention any agent by name:</p>
2838
+ <div class="usage-examples">
2839
+ ${projectAgents.slice(0, 3).map(agent =>
2840
+ `<code class="usage-example">Use the ${agent.name} agent to help with this task</code>`
2841
+ ).join('')}
2842
+ </div>
2843
+ </div>
2844
+ `}
2845
+ </div>
2846
+ </div>
2847
+ </div>
2848
+ `;
2849
+
2850
+ // Add modal to DOM
2851
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
2852
+
2853
+ // Bind close events
2854
+ document.getElementById('project-agents-modal-close').addEventListener('click', () => this.closeProjectAgentsModal());
2855
+ document.getElementById('project-agents-modal-overlay').addEventListener('click', (e) => {
2856
+ if (e.target.id === 'project-agents-modal-overlay') {
2857
+ this.closeProjectAgentsModal();
2858
+ }
2859
+ });
2860
+
2861
+ // Bind agent detail buttons
2862
+ const detailButtons = document.querySelectorAll('.project-agent-details-btn');
2863
+ detailButtons.forEach(btn => {
2864
+ btn.addEventListener('click', (e) => {
2865
+ e.stopPropagation();
2866
+ const agentId = btn.dataset.agentId;
2867
+ const agent = this.agents.find(a => a.name === agentId);
2868
+ if (agent) {
2869
+ this.closeProjectAgentsModal();
2870
+ this.openAgentModal(agent);
2871
+ }
2872
+ });
2873
+ });
2874
+
2875
+ // ESC key to close
2876
+ this.projectModalKeydownHandler = (e) => {
2877
+ if (e.key === 'Escape') {
2878
+ this.closeProjectAgentsModal();
2879
+ }
2880
+ };
2881
+ document.addEventListener('keydown', this.projectModalKeydownHandler);
2882
+ }
2883
+
2884
+ /**
2885
+ * Close project agents modal
2886
+ */
2887
+ closeProjectAgentsModal() {
2888
+ const modal = document.getElementById('project-agents-modal-overlay');
2889
+ if (modal) {
2890
+ modal.remove();
2891
+ }
2892
+ if (this.projectModalKeydownHandler) {
2893
+ document.removeEventListener('keydown', this.projectModalKeydownHandler);
2894
+ this.projectModalKeydownHandler = null;
2895
+ }
2896
+ }
2897
+
2898
+ /**
2899
+ * Refresh agents data
2900
+ */
2901
+ async refreshAgents() {
2902
+ const refreshBtn = this.container.querySelector('#refresh-agents');
2903
+ if (refreshBtn) {
2904
+ refreshBtn.disabled = true;
2905
+ const iconElement = refreshBtn.querySelector('.btn-icon');
2906
+ if (iconElement) {
2907
+ iconElement.style.animation = 'spin 1s linear infinite';
2908
+ }
2909
+ }
2910
+
2911
+ try {
2912
+ // Just reload agents data without clearing cache
2913
+ await this.loadAgentsData();
2914
+ } catch (error) {
2915
+ console.error('Error refreshing agents:', error);
2916
+ } finally {
2917
+ if (refreshBtn) {
2918
+ refreshBtn.disabled = false;
2919
+ const iconElement = refreshBtn.querySelector('.btn-icon');
2920
+ if (iconElement) {
2921
+ iconElement.style.animation = '';
2922
+ }
2923
+ }
2924
+ }
2925
+ }
2926
+
2927
+ /**
2928
+ * Load initial conversations data using paginated API
2929
+ */
2930
+ async loadConversationsData() {
2931
+ try {
2932
+
2933
+ // Reset pagination state
2934
+ this.pagination = {
2935
+ currentPage: 0,
2936
+ limit: 10,
2937
+ hasMore: true,
2938
+ isLoading: false
2939
+ };
2940
+ this.loadedConversations = [];
2941
+ this.loadedMessages.clear(); // Clear message cache too
2942
+
2943
+ // Clear the list container
2944
+ const listContainer = this.container.querySelector('#conversations-list');
2945
+ if (listContainer) {
2946
+ listContainer.innerHTML = '';
2947
+ }
2948
+
2949
+ // Hide empty state initially
2950
+ this.hideEmptyState();
2951
+
2952
+ // Load first page and states
2953
+ await this.loadMoreConversations();
2954
+
2955
+
2956
+ } catch (error) {
2957
+ console.error('Error loading conversations data:', error);
2958
+ this.stateService.setError('Failed to load conversations data');
2959
+ }
2960
+ }
2961
+
2962
+ /**
2963
+ * Load more conversations (pagination)
2964
+ */
2965
+ async loadMoreConversations() {
2966
+ if (this.pagination.isLoading || !this.pagination.hasMore) {
2967
+ return;
2968
+ }
2969
+
2970
+ try {
2971
+
2972
+ this.pagination.isLoading = true;
2973
+ this.updateLoadingIndicator(true);
2974
+
2975
+ const [conversationsData, statesData] = await Promise.all([
2976
+ this.dataService.getConversationsPaginated(this.pagination.currentPage, this.pagination.limit),
2977
+ this.dataService.getConversationStates()
2978
+ ]);
2979
+
2980
+
2981
+ // Update pagination info
2982
+ this.pagination.hasMore = conversationsData.pagination.hasMore;
2983
+ this.pagination.currentPage = conversationsData.pagination.page + 1;
2984
+ this.pagination.totalCount = conversationsData.pagination.totalCount;
2985
+
2986
+ // Get only NEW conversations for this page
2987
+ const newConversations = conversationsData.conversations;
2988
+
2989
+ // Add new conversations to loaded list
2990
+ this.loadedConversations.push(...newConversations);
2991
+
2992
+
2993
+ // Extract activeStates from the response structure
2994
+ const activeStates = statesData?.activeStates || {};
2995
+
2996
+ // Update state with correct format
2997
+ this.stateService.updateConversations(this.loadedConversations);
2998
+ this.stateService.updateConversationStates(activeStates);
2999
+
3000
+
3001
+ // For initial load (page 0), replace content. For subsequent loads, append
3002
+ const isInitialLoad = conversationsData.pagination.page === 0;
3003
+ this.renderConversationsList(
3004
+ isInitialLoad ? this.loadedConversations : newConversations,
3005
+ activeStates,
3006
+ !isInitialLoad
3007
+ );
3008
+
3009
+ } catch (error) {
3010
+ console.error('Error loading more conversations:', error);
3011
+ this.stateService.setError('Failed to load more conversations');
3012
+ } finally {
3013
+ this.pagination.isLoading = false;
3014
+ this.updateLoadingIndicator(false);
3015
+ }
3016
+ }
3017
+
3018
+ /**
3019
+ * Render conversations list
3020
+ * @param {Array} conversations - Conversations data
3021
+ * @param {Object} states - Conversation states
3022
+ * @param {boolean} append - Whether to append or replace content
3023
+ */
3024
+ renderConversationsList(conversations, states, append = false) {
3025
+ const listContainer = this.container.querySelector('#conversations-list');
3026
+ if (!listContainer) {
3027
+ console.warn('conversations-list element not found in AgentsPage - component may not be active');
3028
+ return;
3029
+ }
3030
+ const filteredConversations = this.filterConversations(conversations, states);
3031
+
3032
+ // Calculate count based on filters
3033
+ let countToShow;
3034
+ const hasActiveFilters = this.hasActiveFilters();
3035
+
3036
+ if (!hasActiveFilters && this.pagination && this.pagination.totalCount) {
3037
+ // No filters active, show total count from server
3038
+ countToShow = this.pagination.totalCount;
3039
+ } else {
3040
+ // Filters active, count filtered loaded conversations
3041
+ const conversationsToCount = this.loadedConversations && this.loadedConversations.length > 0
3042
+ ? this.loadedConversations
3043
+ : conversations;
3044
+ const allFilteredConversations = this.filterConversations(conversationsToCount, states);
3045
+ countToShow = allFilteredConversations.length;
3046
+ }
3047
+
3048
+ this.updateResultsCount(countToShow, hasActiveFilters);
3049
+ this.updateClearFiltersButton();
3050
+
3051
+ if (filteredConversations.length === 0 && !append) {
3052
+ this.showEmptyState();
3053
+ return;
3054
+ }
3055
+
3056
+ this.hideEmptyState();
3057
+
3058
+ const conversationHTML = filteredConversations.map(conv => {
3059
+ const state = states[conv.id] || 'unknown';
3060
+ const stateClass = this.getStateClass(state);
3061
+
3062
+ // Check for agent usage
3063
+ const agentColor = this.getAgentColorForConversation(conv.id);
3064
+ const agentName = this.getAgentNameForConversation(conv.id);
3065
+
3066
+ // Generate title with agent indicator
3067
+ const titleColor = agentColor ? `style="color: ${agentColor}; border-left: 3px solid ${agentColor}; padding-left: 8px;"` : '';
3068
+ const agentIndicator = agentName ? `<span class="agent-indicator-small" style="background-color: ${agentColor}" title="Using ${agentName} agent">🤖</span>` : '';
3069
+
3070
+ return `
3071
+ <div class="sidebar-conversation-item" data-id="${conv.id}" ${agentColor ? `data-agent-color="${agentColor}"` : ''}>
3072
+ <div class="sidebar-conversation-header">
3073
+ <div class="sidebar-conversation-title" ${titleColor}>
3074
+ <span class="status-dot ${stateClass}"></span>
3075
+ <h4 class="sidebar-conversation-name">${conv.title || `Chat ${conv.id.slice(-8)}`}</h4>
3076
+ ${agentIndicator}
3077
+ </div>
3078
+ <span class="sidebar-conversation-badge ${stateClass}">${this.getStateLabel(state)}</span>
3079
+ </div>
3080
+
3081
+ <div class="sidebar-conversation-meta">
3082
+ <span class="sidebar-meta-item">
3083
+ <span class="sidebar-meta-icon">📁</span>
3084
+ ${this.truncateText(conv.project || 'Unknown', 12)}
3085
+ </span>
3086
+ </div>
3087
+
3088
+ <div class="sidebar-conversation-preview">
3089
+ <p class="sidebar-preview-text">${this.getSimpleConversationPreview(conv)}</p>
3090
+ </div>
3091
+
3092
+ <div class="sidebar-conversation-actions">
3093
+ <button class="conversation-agents-btn" data-conversation-id="${conv.id}" title="View available agents for this project">
3094
+ <span class="agents-icon">🤖</span>
3095
+ <span class="agents-text">Agents</span>
3096
+ </button>
3097
+ </div>
3098
+ </div>
3099
+ `;
3100
+ }).join('');
3101
+
3102
+ if (append) {
3103
+ listContainer.insertAdjacentHTML('beforeend', conversationHTML);
3104
+ } else {
3105
+ listContainer.innerHTML = conversationHTML;
3106
+ }
3107
+
3108
+ // Bind card actions
3109
+ this.bindListActions();
3110
+ }
3111
+
3112
+ /**
3113
+ * Bind list action events
3114
+ */
3115
+ bindListActions() {
3116
+ // Export conversation button
3117
+ const exportBtn = this.container.querySelector('#export-conversation');
3118
+ if (exportBtn) {
3119
+ exportBtn.addEventListener('click', () => {
3120
+ if (this.selectedConversationId) {
3121
+ this.exportSingleConversation(this.selectedConversationId);
3122
+ }
3123
+ });
3124
+ }
3125
+
3126
+ // Click on sidebar conversation item to select and view
3127
+ const conversationItems = this.container.querySelectorAll('.sidebar-conversation-item');
3128
+ conversationItems.forEach(item => {
3129
+ item.addEventListener('click', (e) => {
3130
+ // Don't select conversation if clicking on agents button
3131
+ if (e.target.closest('.conversation-agents-btn')) {
3132
+ return;
3133
+ }
3134
+ const conversationId = item.dataset.id;
3135
+ this.selectConversation(conversationId);
3136
+ });
3137
+ });
3138
+
3139
+ // Bind agents button clicks
3140
+ const agentsButtons = this.container.querySelectorAll('.conversation-agents-btn');
3141
+ agentsButtons.forEach(btn => {
3142
+ btn.addEventListener('click', (e) => {
3143
+ e.stopPropagation();
3144
+ const conversationId = btn.dataset.conversationId;
3145
+ this.showProjectAgents(conversationId);
3146
+ });
3147
+ });
3148
+ }
3149
+
3150
+ /**
3151
+ * Select and display a conversation
3152
+ * @param {string} conversationId - Conversation ID
3153
+ */
3154
+ async selectConversation(conversationId) {
3155
+ // Update selected conversation state
3156
+ this.selectedConversationId = conversationId;
3157
+
3158
+ // Update UI to show selection
3159
+ this.updateSelectedConversation();
3160
+
3161
+ // Load and display conversation messages
3162
+ await this.loadConversationMessages(conversationId);
3163
+ }
3164
+
3165
+ /**
3166
+ * Update selected conversation in sidebar
3167
+ */
3168
+ updateSelectedConversation() {
3169
+ // Remove previous selection
3170
+ const previousSelected = this.container.querySelector('.sidebar-conversation-item.selected');
3171
+ if (previousSelected) {
3172
+ previousSelected.classList.remove('selected');
3173
+ }
3174
+
3175
+ // Add selection to current item
3176
+ const currentItem = this.container.querySelector(`[data-id="${this.selectedConversationId}"]`);
3177
+ if (currentItem) {
3178
+ currentItem.classList.add('selected');
3179
+ }
3180
+
3181
+ // Update header with conversation info
3182
+ const conversations = this.stateService.getStateProperty('conversations') || [];
3183
+ const conversation = conversations.find(conv => conv.id === this.selectedConversationId);
3184
+
3185
+ if (conversation) {
3186
+ const titleElement = this.container.querySelector('#selected-conversation-title');
3187
+ const metaElement = this.container.querySelector('#selected-conversation-meta');
3188
+
3189
+ if (titleElement) {
3190
+ const baseTitle = conversation.title || `Chat ${conversation.id.slice(-8)}`;
3191
+ const agentName = this.getAgentNameForConversation(conversation.id);
3192
+ const agentColor = this.getAgentColorForConversation(conversation.id);
3193
+
3194
+ if (agentName && agentColor) {
3195
+ titleElement.innerHTML = `
3196
+ <span style="color: ${agentColor}; border-left: 3px solid ${agentColor}; padding-left: 8px;">
3197
+ ${baseTitle}
3198
+ </span>
3199
+ <span class="agent-badge" style="background-color: ${agentColor};" title="Using ${agentName} agent">
3200
+ 🤖 ${agentName}
3201
+ </span>
3202
+ `;
3203
+ } else {
3204
+ titleElement.textContent = baseTitle;
3205
+ }
3206
+ }
3207
+
3208
+ if (metaElement) {
3209
+ const messageCount = conversation.messageCount || 0;
3210
+ const lastActivity = this.formatRelativeTime(new Date(conversation.lastModified));
3211
+ metaElement.innerHTML = `
3212
+ <span class="meta-item">
3213
+ <span class="meta-icon">📁</span>
3214
+ ${conversation.project || 'Unknown Project'}
3215
+ </span>
3216
+ <span class="meta-item">
3217
+ <span class="meta-icon">💬</span>
3218
+ ${messageCount} message${messageCount !== 1 ? 's' : ''}
3219
+ </span>
3220
+ <span class="meta-item">
3221
+ <span class="meta-icon">🕒</span>
3222
+ ${lastActivity}
3223
+ </span>
3224
+ `;
3225
+ }
3226
+ }
3227
+
3228
+ // Show and update the state banner
3229
+ this.showStateBanner(this.selectedConversationId);
3230
+ }
3231
+
3232
+ /**
3233
+ * Load and display conversation messages (with caching)
3234
+ * @param {string} conversationId - Conversation ID
3235
+ */
3236
+ async loadConversationMessages(conversationId) {
3237
+ // Reset pagination for new conversation
3238
+ this.messagesPagination = {
3239
+ currentPage: 0,
3240
+ limit: 10,
3241
+ hasMore: true,
3242
+ isLoading: false,
3243
+ conversationId: conversationId
3244
+ };
3245
+
3246
+ // Clear cached messages for this conversation
3247
+ this.loadedMessages.delete(conversationId);
3248
+
3249
+ // Load first page of messages
3250
+ await this.loadMoreMessages(conversationId, true);
3251
+ }
3252
+
3253
+ /**
3254
+ * Show and update conversation state banner
3255
+ * @param {string} conversationId - Conversation ID
3256
+ */
3257
+ showStateBanner(conversationId) {
3258
+ const banner = this.container.querySelector('#conversation-state-banner');
3259
+ if (!banner) return;
3260
+
3261
+ // Show the banner
3262
+ banner.style.display = 'flex';
3263
+
3264
+ // Get current state from WebSocket or cache
3265
+ const conversationStates = this.stateService.getStateProperty('conversationStates') || {};
3266
+ const currentState = conversationStates[conversationId] || 'unknown';
3267
+
3268
+
3269
+ // If we don't have the state yet, try to fetch it after a short delay
3270
+ if (currentState === 'unknown') {
3271
+ setTimeout(() => {
3272
+ this.fetchConversationState(conversationId);
3273
+ }, 100);
3274
+ }
3275
+
3276
+ // Update banner with current state
3277
+ this.updateStateBanner(conversationId, currentState);
3278
+ }
3279
+
3280
+ /**
3281
+ * Update conversation state banner
3282
+ * @param {string} conversationId - Conversation ID
3283
+ * @param {string} state - Current conversation state
3284
+ */
3285
+ updateStateBanner(conversationId, state) {
3286
+ const banner = this.container.querySelector('#conversation-state-banner');
3287
+ const stateDot = this.container.querySelector('#state-dot');
3288
+ const stateText = this.container.querySelector('#state-text');
3289
+ const stateTimestamp = this.container.querySelector('#state-timestamp');
3290
+
3291
+
3292
+ if (!banner || !stateDot || !stateText || !stateTimestamp) {
3293
+ return;
3294
+ }
3295
+
3296
+ // Map states to user-friendly messages with enhanced descriptions
3297
+ const stateMessages = {
3298
+ 'Claude Code working...': {
3299
+ text: '🤖 Claude is thinking and working...',
3300
+ description: 'Claude is processing your request',
3301
+ class: 'status-working',
3302
+ icon: '🧠'
3303
+ },
3304
+ 'Awaiting tool response...': {
3305
+ text: '⚡ Waiting for tool execution...',
3306
+ description: 'Claude is waiting for tool results',
3307
+ class: 'status-tool-pending',
3308
+ icon: '🔧'
3309
+ },
3310
+ 'Executing tools...': {
3311
+ text: '🔧 Executing tools...',
3312
+ description: 'Claude is running system tools',
3313
+ class: 'status-tool-executing',
3314
+ icon: '⚡'
3315
+ },
3316
+ 'Analyzing results...': {
3317
+ text: '📊 Analyzing tool results...',
3318
+ description: 'Claude is processing tool outputs',
3319
+ class: 'status-analyzing',
3320
+ icon: '🔍'
3321
+ },
3322
+ 'Analyzing code...': {
3323
+ text: '🔍 Analyzing code...',
3324
+ description: 'Claude is examining code or files',
3325
+ class: 'status-analyzing',
3326
+ icon: '📝'
3327
+ },
3328
+ 'Fetching data...': {
3329
+ text: '🌐 Fetching data...',
3330
+ description: 'Claude is retrieving web content or external data',
3331
+ class: 'status-fetching',
3332
+ icon: '📶'
3333
+ },
3334
+ 'Task completed': {
3335
+ text: '✅ Task completed',
3336
+ description: 'Claude has finished the requested task',
3337
+ class: 'status-completed',
3338
+ icon: '✨'
3339
+ },
3340
+ 'Processing request...': {
3341
+ text: '⚙️ Processing request...',
3342
+ description: 'Claude is working on a complex request',
3343
+ class: 'status-processing',
3344
+ icon: '🔄'
3345
+ },
3346
+ 'Encountered issue': {
3347
+ text: '⚠️ Encountered issue',
3348
+ description: 'Claude found an error or problem',
3349
+ class: 'status-error',
3350
+ icon: '🚟'
3351
+ },
3352
+ 'Awaiting user input...': {
3353
+ text: '💬 Awaiting your input',
3354
+ description: 'Claude needs your response to continue',
3355
+ class: 'status-waiting',
3356
+ icon: '💭'
3357
+ },
3358
+ 'Waiting for your response': {
3359
+ text: '💬 Waiting for your response',
3360
+ description: 'Claude is ready for your next message',
3361
+ class: 'status-waiting-response',
3362
+ icon: '📝'
3363
+ },
3364
+ 'Awaiting response...': {
3365
+ text: '⏳ Awaiting Claude response',
3366
+ description: 'Waiting for Claude to respond',
3367
+ class: 'status-waiting',
3368
+ icon: '🤔'
3369
+ },
3370
+ 'Recently active': {
3371
+ text: '🟢 Recently active',
3372
+ description: 'Conversation was active recently',
3373
+ class: 'status-active',
3374
+ icon: '✨'
3375
+ },
3376
+ 'Idle': {
3377
+ text: '😴 Conversation idle',
3378
+ description: 'No recent activity',
3379
+ class: 'status-idle',
3380
+ icon: '💤'
3381
+ },
3382
+ 'Inactive': {
3383
+ text: '⚪ Inactive',
3384
+ description: 'Conversation has been inactive',
3385
+ class: 'status-idle',
3386
+ icon: '⏸️'
3387
+ },
3388
+ 'Old': {
3389
+ text: '📚 Archived conversation',
3390
+ description: 'No recent activity in this conversation',
3391
+ class: 'status-idle',
3392
+ icon: '📁'
3393
+ },
3394
+ 'unknown': {
3395
+ text: '🔄 Loading conversation state...',
3396
+ description: 'Determining conversation status',
3397
+ class: 'status-loading',
3398
+ icon: '⏳'
3399
+ }
3400
+ };
3401
+
3402
+ const stateInfo = stateMessages[state] || stateMessages['unknown'];
3403
+
3404
+ // Update dot class with enhanced styling
3405
+ stateDot.className = `state-dot ${stateInfo.class}`;
3406
+
3407
+ // Check for agent usage
3408
+ const agentName = this.getAgentNameForConversation(conversationId);
3409
+ const agentColor = this.getAgentColorForConversation(conversationId);
3410
+
3411
+ // Update text with icon, description, and agent info
3412
+ let stateTextContent = stateInfo.text;
3413
+ let stateDescriptionContent = stateInfo.description;
3414
+
3415
+ // If an agent is detected and state indicates work, update the message
3416
+ if (agentName && (stateInfo.class.includes('working') || stateInfo.class.includes('executing') || stateInfo.class.includes('analyzing'))) {
3417
+ stateTextContent = `🤖 ${agentName} agent working...`;
3418
+ stateDescriptionContent = `The ${agentName} agent is processing your request`;
3419
+
3420
+ // Apply agent color to the dot
3421
+ if (agentColor) {
3422
+ stateDot.style.backgroundColor = agentColor;
3423
+ stateDot.style.borderColor = agentColor;
3424
+ }
3425
+ }
3426
+
3427
+ stateText.innerHTML = `
3428
+ <span class="state-text-main">${stateTextContent}</span>
3429
+ <span class="state-text-description">${stateDescriptionContent}</span>
3430
+ `;
3431
+
3432
+ // Add tooltip for additional context
3433
+ stateText.title = stateInfo.description;
3434
+
3435
+ // Update timestamp with more context
3436
+ const now = new Date();
3437
+ const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
3438
+ stateTimestamp.innerHTML = `
3439
+ <span class="timestamp-label">Last updated:</span>
3440
+ <span class="timestamp-value">${timeString}</span>
3441
+ `;
3442
+
3443
+ // Add pulsing animation for active states
3444
+ if (stateInfo.class.includes('working') || stateInfo.class.includes('executing') || stateInfo.class.includes('analyzing')) {
3445
+ stateDot.classList.add('pulse-animation');
3446
+ setTimeout(() => stateDot.classList.remove('pulse-animation'), 3000);
3447
+ }
3448
+
3449
+ }
3450
+
3451
+ /**
3452
+ * Fetch conversation state from API
3453
+ * @param {string} conversationId - Conversation ID
3454
+ */
3455
+ async fetchConversationState(conversationId) {
3456
+ try {
3457
+ const stateData = await this.dataService.getConversationStates();
3458
+
3459
+ if (stateData && stateData.activeStates && stateData.activeStates[conversationId]) {
3460
+ const state = stateData.activeStates[conversationId];
3461
+
3462
+ // Update the StateService with the new data
3463
+ this.stateService.updateConversationStates(stateData.activeStates);
3464
+
3465
+ // Update the banner with the real state
3466
+ this.updateStateBanner(conversationId, state);
3467
+ } else {
3468
+ // Keep showing unknown for now
3469
+ }
3470
+ } catch (error) {
3471
+ console.error('Error fetching conversation state:', error);
3472
+ }
3473
+ }
3474
+
3475
+ /**
3476
+ * Hide conversation state banner
3477
+ */
3478
+ hideStateBanner() {
3479
+ const banner = this.container.querySelector('#conversation-state-banner');
3480
+ if (banner) {
3481
+ banner.style.display = 'none';
3482
+ }
3483
+ }
3484
+
3485
+ /**
3486
+ * Auto-scroll to bottom of messages
3487
+ */
3488
+ scrollToBottom() {
3489
+ const messagesContent = this.container.querySelector('#messages-content');
3490
+ if (messagesContent) {
3491
+ messagesContent.scrollTop = messagesContent.scrollHeight;
3492
+ }
3493
+ }
3494
+
3495
+ /**
3496
+ * Show notification for new message
3497
+ * @param {Object} message - New message object
3498
+ * @param {Object} metadata - Message metadata
3499
+ */
3500
+ showNewMessageNotification(message, metadata) {
3501
+ // Update banner if it's showing to reflect new activity
3502
+ if (this.selectedConversationId) {
3503
+ const banner = this.container.querySelector('#conversation-state-banner');
3504
+ if (banner && banner.style.display !== 'none') {
3505
+ // Temporarily highlight the banner to show activity
3506
+ banner.style.backgroundColor = 'rgba(213, 116, 85, 0.1)';
3507
+ setTimeout(() => {
3508
+ banner.style.backgroundColor = '';
3509
+ }, 1000);
3510
+ }
3511
+ }
3512
+
3513
+ // Could add visual indicator for new message (pulse, notification badge, etc.)
3514
+ }
3515
+
3516
+ /**
3517
+ * Load more messages (for infinite scroll)
3518
+ * @param {string} conversationId - Conversation ID
3519
+ * @param {boolean} isInitialLoad - Whether this is the initial load
3520
+ */
3521
+ async loadMoreMessages(conversationId, isInitialLoad = false) {
3522
+ const messagesContent = this.container.querySelector('#messages-content');
3523
+ if (!messagesContent) return;
3524
+
3525
+ // Prevent concurrent loading
3526
+ if (this.messagesPagination.isLoading || !this.messagesPagination.hasMore) {
3527
+ return;
3528
+ }
3529
+
3530
+ // Ensure we're loading for the correct conversation
3531
+ if (this.messagesPagination.conversationId !== conversationId) {
3532
+ return;
3533
+ }
3534
+
3535
+ try {
3536
+ this.messagesPagination.isLoading = true;
3537
+
3538
+ if (isInitialLoad) {
3539
+ // Show loading state for initial load
3540
+ messagesContent.innerHTML = `
3541
+ <div class="messages-loading">
3542
+ <div class="loading-spinner"></div>
3543
+ <span>Loading messages...</span>
3544
+ </div>
3545
+ `;
3546
+ } else {
3547
+ // Show loading indicator at top for infinite scroll
3548
+ this.showMessagesLoadingIndicator(true);
3549
+ }
3550
+
3551
+ // Fetch paginated messages from the server
3552
+ const messagesData = await this.dataService.cachedFetch(
3553
+ `/api/conversations/${conversationId}/messages?page=${this.messagesPagination.currentPage}&limit=${this.messagesPagination.limit}`
3554
+ );
3555
+
3556
+ if (messagesData && messagesData.messages) {
3557
+ // Update pagination state - handle both paginated and non-paginated responses
3558
+ if (messagesData.pagination) {
3559
+ // Paginated response
3560
+ this.messagesPagination.hasMore = messagesData.pagination.hasMore;
3561
+ this.messagesPagination.currentPage = messagesData.pagination.page + 1;
3562
+ } else {
3563
+ // Non-paginated response (fallback) - treat as complete data
3564
+ this.messagesPagination.hasMore = false;
3565
+ this.messagesPagination.currentPage = 1;
3566
+ }
3567
+
3568
+ // Get existing messages or initialize
3569
+ let existingMessages = this.loadedMessages.get(conversationId) || [];
3570
+
3571
+ if (isInitialLoad) {
3572
+ // For initial load, replace all messages
3573
+ existingMessages = messagesData.messages;
3574
+ } else {
3575
+ // For infinite scroll, prepend older messages (they come in chronological order)
3576
+ existingMessages = [...messagesData.messages, ...existingMessages];
3577
+ }
3578
+
3579
+ // Cache the combined messages
3580
+ this.loadedMessages.set(conversationId, existingMessages);
3581
+
3582
+ // Render messages
3583
+ this.renderCachedMessages(existingMessages, !isInitialLoad);
3584
+
3585
+ // Setup scroll listener for infinite scroll (only on initial load)
3586
+ if (isInitialLoad) {
3587
+ this.setupMessagesScrollListener(conversationId);
3588
+ }
3589
+
3590
+
3591
+ } else if (isInitialLoad) {
3592
+ messagesContent.innerHTML = `
3593
+ <div class="no-messages-found">
3594
+ <div class="no-messages-icon">💭</div>
3595
+ <h4>No messages found</h4>
3596
+ <p>This conversation has no messages or they could not be loaded.</p>
3597
+ </div>
3598
+ `;
3599
+ }
3600
+
3601
+ } catch (error) {
3602
+ console.error('Error loading messages:', error);
3603
+
3604
+ if (isInitialLoad) {
3605
+ messagesContent.innerHTML = `
3606
+ <div class="messages-error">
3607
+ <span class="error-icon">⚠️</span>
3608
+ <span>Failed to load messages</span>
3609
+ <button class="retry-messages" data-conversation-id="${conversationId}">Retry</button>
3610
+ </div>
3611
+ `;
3612
+
3613
+ // Bind retry button event
3614
+ const retryBtn = messagesContent.querySelector('.retry-messages');
3615
+ if (retryBtn) {
3616
+ retryBtn.addEventListener('click', () => {
3617
+ this.loadConversationMessages(conversationId);
3618
+ });
3619
+ }
3620
+ }
3621
+ } finally {
3622
+ this.messagesPagination.isLoading = false;
3623
+ if (!isInitialLoad) {
3624
+ this.showMessagesLoadingIndicator(false);
3625
+ }
3626
+ }
3627
+ }
3628
+
3629
+ /**
3630
+ * Render cached messages
3631
+ * @param {Array} messages - Array of messages
3632
+ * @param {boolean} prepend - Whether to prepend messages (for infinite scroll)
3633
+ */
3634
+ renderCachedMessages(messages, prepend = false) {
3635
+
3636
+
3637
+ const messagesContent = this.container.querySelector('#messages-content');
3638
+ if (!messagesContent) {
3639
+ console.warn(`⚠️ messages-content element not found!`);
3640
+ return;
3641
+ }
3642
+
3643
+ // Store messages globally for tool result lookup
3644
+ if (typeof window !== 'undefined') {
3645
+ window.currentMessages = messages;
3646
+ }
3647
+
3648
+ const messageHTML = `
3649
+ <div class="messages-loading-indicator" style="display: none;">
3650
+ <div class="loading-spinner small"></div>
3651
+ <span>Loading older messages...</span>
3652
+ </div>
3653
+ <div class="messages-list">
3654
+ ${messages.map(msg => this.renderMessage(msg)).join('')}
3655
+ </div>
3656
+ `;
3657
+
3658
+ if (prepend) {
3659
+ // For infinite scroll, we need to maintain scroll position
3660
+ const oldScrollHeight = messagesContent.scrollHeight;
3661
+
3662
+ // Update content
3663
+ messagesContent.innerHTML = messageHTML;
3664
+
3665
+ // Restore scroll position relative to the bottom
3666
+ const newScrollHeight = messagesContent.scrollHeight;
3667
+ const scrollDifference = newScrollHeight - oldScrollHeight;
3668
+ messagesContent.scrollTop += scrollDifference;
3669
+ } else {
3670
+ // Initial load - just replace content and scroll to bottom
3671
+ messagesContent.innerHTML = messageHTML;
3672
+
3673
+ // Scroll to bottom for new conversation load
3674
+ setTimeout(() => {
3675
+ messagesContent.scrollTop = messagesContent.scrollHeight;
3676
+ }, 100);
3677
+ }
3678
+
3679
+ // Bind tool display events
3680
+ this.toolDisplay.bindEvents(messagesContent);
3681
+ }
3682
+
3683
+ /**
3684
+ * Show/hide messages loading indicator
3685
+ * @param {boolean} show - Whether to show the indicator
3686
+ */
3687
+ showMessagesLoadingIndicator(show) {
3688
+ const messagesContent = this.container.querySelector('#messages-content');
3689
+ if (!messagesContent) return;
3690
+
3691
+ const indicator = messagesContent.querySelector('.messages-loading-indicator');
3692
+ if (indicator) {
3693
+ indicator.style.display = show ? 'flex' : 'none';
3694
+ }
3695
+ }
3696
+
3697
+ /**
3698
+ * Setup scroll listener for infinite scroll in messages
3699
+ * @param {string} conversationId - Current conversation ID
3700
+ */
3701
+ setupMessagesScrollListener(conversationId) {
3702
+ const messagesContent = this.container.querySelector('#messages-content');
3703
+ if (!messagesContent) return;
3704
+
3705
+ // Remove existing listener if any
3706
+ if (this.messagesScrollListener) {
3707
+ messagesContent.removeEventListener('scroll', this.messagesScrollListener);
3708
+ }
3709
+
3710
+ // Create new listener
3711
+ this.messagesScrollListener = () => {
3712
+ // Check if we've scrolled near the top (for loading older messages)
3713
+ const scrollTop = messagesContent.scrollTop;
3714
+ const threshold = 100; // pixels from top
3715
+
3716
+ if (scrollTop <= threshold && this.messagesPagination.hasMore && !this.messagesPagination.isLoading) {
3717
+ this.loadMoreMessages(conversationId, false);
3718
+ }
3719
+ };
3720
+
3721
+ // Add listener
3722
+ messagesContent.addEventListener('scroll', this.messagesScrollListener);
3723
+ }
3724
+
3725
+ /**
3726
+ * Render a single message with terminal-style formatting
3727
+ * @param {Object} message - Message object
3728
+ * @returns {string} HTML string
3729
+ */
3730
+ renderMessage(message) {
3731
+ const timestamp = this.formatRelativeTime(new Date(message.timestamp));
3732
+ const fullTimestamp = new Date(message.timestamp).toLocaleString();
3733
+ // Compact summaries should be displayed as assistant messages even if marked as 'user'
3734
+ const isUser = message.role === 'user' && !message.isCompactSummary;
3735
+
3736
+
3737
+ // Detect if message contains tools
3738
+ const hasTools = Array.isArray(message.content) &&
3739
+ message.content.some(block => block.type === 'tool_use');
3740
+ const toolCount = hasTools ?
3741
+ message.content.filter(block => block.type === 'tool_use').length : 0;
3742
+
3743
+ // Terminal-style prompt
3744
+ const prompt = isUser ? '>' : '#';
3745
+ const roleLabel = isUser ? 'user' : 'claude';
3746
+
3747
+ // Get message ID (short version for display)
3748
+ const messageId = message.id ? message.id.slice(-8) : 'unknown';
3749
+
3750
+ return `
3751
+ <div class="terminal-message ${isUser ? 'user' : 'assistant'}" data-message-id="${message.id || ''}">
3752
+ <div class="message-container">
3753
+ <div class="message-prompt">
3754
+ <span class="prompt-char">${prompt}</span>
3755
+ <div class="message-metadata">
3756
+ <span class="timestamp" title="${fullTimestamp}">${timestamp}</span>
3757
+ <span class="role-label">${roleLabel}</span>
3758
+ <span class="message-id" title="Message ID: ${message.id || 'unknown'}">[${messageId}]</span>
3759
+ ${message.usage ? `
3760
+ <span class="tokens">
3761
+ ${message.usage.input_tokens > 0 ? `i:${message.usage.input_tokens}` : ''}
3762
+ ${message.usage.output_tokens > 0 ? `o:${message.usage.output_tokens}` : ''}
3763
+ ${message.usage.cache_read_input_tokens > 0 ? `c:${message.usage.cache_read_input_tokens}` : ''}
3764
+ </span>
3765
+ ` : ''}
3766
+ ${hasTools ? `<span class="tool-count">[${toolCount}t]</span>` : ''}
3767
+ ${message.model ? `<span class="model">[${message.model.replace('claude-', '').replace('-20250514', '')}]</span>` : ''}
3768
+ </div>
3769
+ </div>
3770
+ <div class="message-body">
3771
+ ${this.formatMessageContent(message.content, message)}
3772
+ </div>
3773
+ </div>
3774
+ </div>
3775
+ `;
3776
+ }
3777
+
3778
+
3779
+ /**
3780
+ * Format message content with support for text and tool calls
3781
+ * @param {string|Array} content - Message content
3782
+ * @returns {string} Formatted HTML
3783
+ */
3784
+ formatMessageContent(content, message = null) {
3785
+ let result = '';
3786
+
3787
+ // Handle different content formats
3788
+ if (Array.isArray(content)) {
3789
+ // Assistant messages with content blocks
3790
+ content.forEach((block, index) => {
3791
+ if (block.type === 'text') {
3792
+ result += this.formatTextContent(block.text);
3793
+ } else if (block.type === 'tool_use') {
3794
+ // Log only tool rendering for debugging
3795
+ console.log('🔧 WebSocket: Rendering tool', { name: block.name, hasResults: !!message?.toolResults });
3796
+ result += this.toolDisplay.renderToolUse(block, message?.toolResults);
3797
+ } else if (block.type === 'tool_result') {
3798
+ result += this.toolDisplay.renderToolResult(block);
3799
+ }
3800
+ });
3801
+ } else if (typeof content === 'string' && content.trim() !== '') {
3802
+ // User messages with plain text - check for special patterns
3803
+ if (content.includes('Tool Result') && content.length > 1000) {
3804
+ // This is likely a large tool result that should be handled specially
3805
+ result += this.formatLargeToolResult(content);
3806
+ } else {
3807
+ // Check if this is a confirmation response "[ok]" or similar
3808
+ const enhancedContent = this.enhanceConfirmationMessage(content, message);
3809
+ result = this.formatTextContent(enhancedContent);
3810
+ }
3811
+ } else if (content && typeof content === 'object') {
3812
+ // Handle edge cases where content might be an object
3813
+ result = this.formatTextContent(JSON.stringify(content, null, 2));
3814
+ }
3815
+
3816
+ return result || '<em class="empty-content">No displayable content available</em>';
3817
+ }
3818
+
3819
+ /**
3820
+ * Format regular text content with enhanced Markdown support
3821
+ * @param {string} text - Text content
3822
+ * @returns {string} Formatted HTML
3823
+ */
3824
+ /**
3825
+ * Apply markdown formatting to HTML-escaped text
3826
+ * @param {string} escapedText - HTML-escaped text to format
3827
+ * @returns {string} Formatted text with markdown styling
3828
+ */
3829
+ applyMarkdownFormatting(escapedText) {
3830
+ let formattedText = escapedText;
3831
+
3832
+ // 1. Code blocks (must be first to avoid conflicts)
3833
+ formattedText = formattedText
3834
+ .replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre class="code-block" data-language="$1"><code>$2</code></pre>');
3835
+
3836
+ // 2. Headers (h1-h6)
3837
+ formattedText = formattedText
3838
+ .replace(/^### (.*$)/gm, '<h3 class="markdown-h3">$1</h3>')
3839
+ .replace(/^## (.*$)/gm, '<h2 class="markdown-h2">$1</h2>')
3840
+ .replace(/^# (.*$)/gm, '<h1 class="markdown-h1">$1</h1>')
3841
+ .replace(/^#### (.*$)/gm, '<h4 class="markdown-h4">$1</h4>')
3842
+ .replace(/^##### (.*$)/gm, '<h5 class="markdown-h5">$1</h5>')
3843
+ .replace(/^###### (.*$)/gm, '<h6 class="markdown-h6">$1</h6>');
3844
+
3845
+ // 3. Bold and italic text
3846
+ formattedText = formattedText
3847
+ .replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em class="markdown-bold-italic">$1</em></strong>')
3848
+ .replace(/\*\*(.*?)\*\*/g, '<strong class="markdown-bold">$1</strong>')
3849
+ .replace(/\*(.*?)\*/g, '<em class="markdown-italic">$1</em>')
3850
+ .replace(/\_\_\_(.*?)\_\_\_/g, '<strong><em class="markdown-bold-italic">$1</em></strong>')
3851
+ .replace(/\_\_(.*?)\_\_/g, '<strong class="markdown-bold">$1</strong>')
3852
+ .replace(/\_(.*?)\_/g, '<em class="markdown-italic">$1</em>');
3853
+
3854
+ // 4. Strikethrough
3855
+ formattedText = formattedText
3856
+ .replace(/~~(.*?)~~/g, '<del class="markdown-strikethrough">$1</del>');
3857
+
3858
+ // 5. Links
3859
+ formattedText = formattedText
3860
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="markdown-link" target="_blank" rel="noopener noreferrer">$1</a>');
3861
+
3862
+ // 6. Inline code (after other formatting to avoid conflicts)
3863
+ formattedText = formattedText
3864
+ .replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
3865
+
3866
+ // 7. Lists (unordered)
3867
+ formattedText = formattedText
3868
+ .replace(/^[\s]*[\*\-\+][\s]+(.*)$/gm, '<li class="markdown-list-item">$1</li>');
3869
+
3870
+ // 8. Lists (ordered)
3871
+ formattedText = formattedText
3872
+ .replace(/^[\s]*\d+\.[\s]+(.*)$/gm, '<li class="markdown-ordered-item">$1</li>');
3873
+
3874
+ // 9. Wrap consecutive list items in ul/ol tags
3875
+ formattedText = formattedText
3876
+ .replace(/(<li class="markdown-list-item">.*<\/li>)/gs, (match) => {
3877
+ return '<ul class="markdown-list">' + match + '</ul>';
3878
+ })
3879
+ .replace(/(<li class="markdown-ordered-item">.*<\/li>)/gs, (match) => {
3880
+ return '<ol class="markdown-ordered-list">' + match + '</ol>';
3881
+ });
3882
+
3883
+ // 10. Blockquotes
3884
+ formattedText = formattedText
3885
+ .replace(/^&gt;[\s]*(.*)$/gm, '<blockquote class="markdown-blockquote">$1</blockquote>');
3886
+
3887
+ // 11. Horizontal rules
3888
+ formattedText = formattedText
3889
+ .replace(/^[\s]*---[\s]*$/gm, '<hr class="markdown-hr">')
3890
+ .replace(/^[\s]*\*\*\*[\s]*$/gm, '<hr class="markdown-hr">');
3891
+
3892
+ // 12. Line breaks (last to avoid conflicts)
3893
+ formattedText = formattedText
3894
+ .replace(/\n\n/g, '</p><p class="markdown-paragraph">')
3895
+ .replace(/\n/g, '<br>');
3896
+
3897
+ // 13. Wrap in paragraph if not already wrapped
3898
+ if (!formattedText.includes('<p') && !formattedText.includes('<h') &&
3899
+ !formattedText.includes('<ul') && !formattedText.includes('<ol') &&
3900
+ !formattedText.includes('<blockquote')) {
3901
+ formattedText = '<p class="markdown-paragraph">' + formattedText + '</p>';
3902
+ }
3903
+
3904
+ return formattedText;
3905
+ }
3906
+
3907
+ formatTextContent(text) {
3908
+ if (!text || text.trim() === '') return '';
3909
+
3910
+ // Escape HTML to prevent XSS
3911
+ const escapeHtml = (str) => {
3912
+ const div = document.createElement('div');
3913
+ div.textContent = str;
3914
+ return div.innerHTML;
3915
+ };
3916
+
3917
+ // Check if text is too long and needs truncation
3918
+ const lines = text.split('\n');
3919
+ const maxVisibleLines = 20; // Increased from 5 to 20 for better visibility
3920
+
3921
+ if (lines.length > maxVisibleLines) {
3922
+ const visibleLines = lines.slice(0, maxVisibleLines);
3923
+ const hiddenLinesCount = lines.length - maxVisibleLines;
3924
+ const contentId = 'text_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
3925
+
3926
+ // Store full content for modal
3927
+ if (typeof window !== 'undefined') {
3928
+ window.storedContent = window.storedContent || {};
3929
+ window.storedContent[contentId] = text;
3930
+ }
3931
+
3932
+ const previewText = escapeHtml(visibleLines.join('\n'));
3933
+ const showMoreButton = `<button class="show-results-btn text-expand-btn" data-content-id="${contentId}">Show +${hiddenLinesCount} lines</button>`;
3934
+
3935
+ // Apply markdown formatting to preview
3936
+ let formattedPreview = this.applyMarkdownFormatting(previewText);
3937
+
3938
+ return `<div class="text-content-preview">${formattedPreview}<div class="text-expand-section"><span class="continuation">… +${hiddenLinesCount} lines hidden</span> ${showMoreButton}</div></div>`;
3939
+ }
3940
+
3941
+ // For non-truncated content, apply full formatting
3942
+ let formattedText = escapeHtml(text);
3943
+ formattedText = this.applyMarkdownFormatting(formattedText);
3944
+
3945
+ return formattedText;
3946
+ }
3947
+
3948
+ /**
3949
+ * Format large tool result content safely
3950
+ * @param {string} content - Large tool result content
3951
+ * @returns {string} Safe formatted content
3952
+ */
3953
+ formatLargeToolResult(content) {
3954
+ // Extract tool result ID if present
3955
+ const toolIdMatch = content.match(/Tool Result\s+([A-Za-z0-9]+)/);
3956
+ const toolId = toolIdMatch ? toolIdMatch[1] : 'unknown';
3957
+
3958
+ const escapeHtml = (str) => {
3959
+ const div = document.createElement('div');
3960
+ div.textContent = str;
3961
+ return div.innerHTML;
3962
+ };
3963
+
3964
+ const preview = content.length > 80
3965
+ ? escapeHtml(content.substring(0, 80)) + '...'
3966
+ : escapeHtml(content);
3967
+
3968
+ return `
3969
+ <div class="terminal-tool tool-result large">
3970
+ <span class="tool-prompt">></span>
3971
+ <span class="tool-status">[LARGE]</span>
3972
+ <span class="tool-id">[${toolId}]</span>
3973
+ <span class="tool-output">${content.length}b: ${preview}</span>
3974
+ </div>
3975
+ `;
3976
+ }
3977
+
3978
+ /**
3979
+ * Enhance confirmation messages like "[ok]" with context information
3980
+ * @param {string} content - Original message content
3981
+ * @param {Object} message - Full message object with metadata
3982
+ * @returns {string} Enhanced message content
3983
+ */
3984
+ enhanceConfirmationMessage(content, message) {
3985
+ const trimmedContent = content.trim();
3986
+
3987
+ // Detect simple confirmation patterns
3988
+ const confirmationPatterns = [
3989
+ /^\[ok\]$/i,
3990
+ /^ok$/i,
3991
+ /^yes$/i,
3992
+ /^\[yes\]$/i,
3993
+ /^y$/i,
3994
+ /^\[y\]$/i,
3995
+ /^1$/, // Choice selection
3996
+ /^2$/,
3997
+ /^3$/
3998
+ ];
3999
+
4000
+ const isConfirmation = confirmationPatterns.some(pattern => pattern.test(trimmedContent));
4001
+
4002
+ if (isConfirmation && message) {
4003
+ // Try to extract context from the message timestamp
4004
+ const messageTime = message.timestamp ? new Date(message.timestamp).toLocaleTimeString() : 'unknown time';
4005
+
4006
+ // Enhanced display for confirmation messages
4007
+ return `${content} <span class="confirmation-context">(User confirmation at ${messageTime})</span>`;
4008
+ }
4009
+
4010
+ // For other potential confirmation-like messages, check if they seem like choices
4011
+ if (/^[1-9]$/.test(trimmedContent)) {
4012
+ return `${content} <span class="confirmation-context">(Menu selection)</span>`;
4013
+ }
4014
+
4015
+ // Check for common CLI responses
4016
+ if (/^(continue|proceed|accept|confirm|done)$/i.test(trimmedContent)) {
4017
+ return `${content} <span class="confirmation-context">(User command)</span>`;
4018
+ }
4019
+
4020
+ return content;
4021
+ }
4022
+
4023
+ /**
4024
+ * Detect which agent is being used in a conversation
4025
+ * @param {string} conversationId - Conversation ID
4026
+ * @returns {Object|null} Agent info or null if no agent detected
4027
+ */
4028
+ detectAgentInConversation(conversationId) {
4029
+ const messages = this.loadedMessages.get(conversationId) || [];
4030
+
4031
+ // Look for agent indicators in recent messages
4032
+ for (let i = messages.length - 1; i >= Math.max(0, messages.length - 10); i--) {
4033
+ const message = messages[i];
4034
+
4035
+ if (message.role === 'assistant' && message.content) {
4036
+ let contentText = '';
4037
+
4038
+ // Extract text content from message
4039
+ if (Array.isArray(message.content)) {
4040
+ contentText = message.content
4041
+ .filter(block => block.type === 'text')
4042
+ .map(block => block.text)
4043
+ .join(' ');
4044
+ } else if (typeof message.content === 'string') {
4045
+ contentText = message.content;
4046
+ }
4047
+
4048
+ // Check for agent usage patterns
4049
+ const agentPatterns = [
4050
+ /use(?:s|d)?\s+the\s+([a-zA-Z0-9\-_]+)\s+(?:sub\s+)?agent/i,
4051
+ /([a-zA-Z0-9\-_]+)\s+agent\s+(?:to|for|will)/i,
4052
+ /delegat(?:e|ing)\s+(?:to|task|this)\s+(?:the\s+)?([a-zA-Z0-9\-_]+)\s+agent/i,
4053
+ /invok(?:e|ing)\s+(?:the\s+)?([a-zA-Z0-9\-_]+)\s+agent/i
4054
+ ];
4055
+
4056
+ for (const pattern of agentPatterns) {
4057
+ const match = contentText.match(pattern);
4058
+ if (match) {
4059
+ const detectedAgentName = match[1].toLowerCase();
4060
+
4061
+ // Find matching agent from our loaded agents
4062
+ const agent = this.agents.find(a =>
4063
+ a.name.toLowerCase() === detectedAgentName ||
4064
+ a.name.toLowerCase().replace(/-/g, '') === detectedAgentName.replace(/-/g, '')
4065
+ );
4066
+
4067
+ if (agent) {
4068
+ return {
4069
+ agent,
4070
+ detectedAt: message.timestamp,
4071
+ confidence: 'high'
4072
+ };
4073
+ }
4074
+ }
4075
+ }
4076
+ }
4077
+ }
4078
+
4079
+ return null;
4080
+ }
4081
+
4082
+ /**
4083
+ * Get agent color for conversation
4084
+ * @param {string} conversationId - Conversation ID
4085
+ * @returns {string|null} Agent color or null
4086
+ */
4087
+ getAgentColorForConversation(conversationId) {
4088
+ const agentInfo = this.detectAgentInConversation(conversationId);
4089
+ return agentInfo ? agentInfo.agent.color : null;
4090
+ }
4091
+
4092
+ /**
4093
+ * Get agent name for conversation
4094
+ * @param {string} conversationId - Conversation ID
4095
+ * @returns {string|null} Agent name or null
4096
+ */
4097
+ getAgentNameForConversation(conversationId) {
4098
+ const agentInfo = this.detectAgentInConversation(conversationId);
4099
+ return agentInfo ? agentInfo.agent.name : null;
4100
+ }
4101
+
4102
+ /**
4103
+ * Format relative time
4104
+ * @param {Date} date - Date to format
4105
+ * @returns {string} Relative time string
4106
+ */
4107
+ formatRelativeTime(date) {
4108
+ const now = new Date();
4109
+ const diffMs = now - date;
4110
+ const diffSecs = Math.floor(diffMs / 1000);
4111
+ const diffMins = Math.floor(diffSecs / 60);
4112
+ const diffHours = Math.floor(diffMins / 60);
4113
+ const diffDays = Math.floor(diffHours / 24);
4114
+
4115
+ if (diffSecs < 60) return 'Just now';
4116
+ if (diffMins < 60) return `${diffMins}m ago`;
4117
+ if (diffHours < 24) return `${diffHours}h ago`;
4118
+ if (diffDays < 7) return `${diffDays}d ago`;
4119
+ return date.toLocaleDateString();
4120
+ }
4121
+
4122
+ /**
4123
+ * Update clear filters button visibility
4124
+ */
4125
+ updateClearFiltersButton() {
4126
+ const clearBtn = this.container.querySelector('#clear-filters');
4127
+ if (!clearBtn) return; // Guard against null when AgentsPage isn't rendered
4128
+
4129
+ const hasActiveFilters = this.filters.status !== 'all' ||
4130
+ this.filters.timeRange !== '7d' ||
4131
+ this.filters.search !== '';
4132
+ clearBtn.style.display = hasActiveFilters ? 'inline-block' : 'none';
4133
+ }
4134
+
4135
+ /**
4136
+ * Handle list action
4137
+ * @param {string} action - Action type
4138
+ * @param {string} conversationId - Conversation ID
4139
+ */
4140
+ handleListAction(action, conversationId) {
4141
+ switch (action) {
4142
+ case 'view':
4143
+ this.viewConversation(conversationId);
4144
+ break;
4145
+ }
4146
+ }
4147
+
4148
+ /**
4149
+ * Filter conversations based on current filters
4150
+ * @param {Array} conversations - All conversations
4151
+ * @param {Object} states - Conversation states
4152
+ * @returns {Array} Filtered conversations
4153
+ */
4154
+ filterConversations(conversations, states) {
4155
+ let filtered = conversations;
4156
+
4157
+ // Filter by status
4158
+ if (this.filters.status !== 'all') {
4159
+ filtered = filtered.filter(conv => {
4160
+ const state = states[conv.id] || 'unknown';
4161
+ const category = this.getStateCategory(state);
4162
+ return category === this.filters.status;
4163
+ });
4164
+ }
4165
+
4166
+ // Filter by time range
4167
+ const timeRange = this.getTimeRangeMs(this.filters.timeRange);
4168
+ if (timeRange > 0) {
4169
+ const cutoff = Date.now() - timeRange;
4170
+ filtered = filtered.filter(conv => {
4171
+ const lastModified = new Date(conv.lastModified).getTime();
4172
+ return lastModified >= cutoff;
4173
+ });
4174
+ }
4175
+
4176
+ // Filter by search
4177
+ if (this.filters.search) {
4178
+ const searchLower = this.filters.search.toLowerCase();
4179
+ filtered = filtered.filter(conv => {
4180
+ return (conv.title || '').toLowerCase().includes(searchLower) ||
4181
+ (conv.project || '').toLowerCase().includes(searchLower) ||
4182
+ (conv.lastMessage || '').toLowerCase().includes(searchLower);
4183
+ });
4184
+ }
4185
+
4186
+ return filtered;
4187
+ }
4188
+
4189
+ /**
4190
+ * Get time range in milliseconds
4191
+ * @param {string} range - Time range string
4192
+ * @returns {number} Milliseconds
4193
+ */
4194
+ getTimeRangeMs(range) {
4195
+ const ranges = {
4196
+ '1h': 60 * 60 * 1000,
4197
+ '24h': 24 * 60 * 60 * 1000,
4198
+ '7d': 7 * 24 * 60 * 60 * 1000,
4199
+ '30d': 30 * 24 * 60 * 60 * 1000
4200
+ };
4201
+ return ranges[range] || 0;
4202
+ }
4203
+
4204
+ /**
4205
+ * Get state category for filtering
4206
+ * @param {string} state - Detailed conversation state
4207
+ * @returns {string} Category: 'active' or 'inactive'
4208
+ */
4209
+ getStateCategory(state) {
4210
+ // Active states - conversation is currently being used or recently active
4211
+ const activeStates = [
4212
+ 'Claude Code working...',
4213
+ 'Awaiting user input...',
4214
+ 'User typing...',
4215
+ 'Awaiting response...',
4216
+ 'Recently active'
4217
+ ];
4218
+
4219
+ // Inactive states - conversation is idle or old
4220
+ const inactiveStates = [
4221
+ 'Idle',
4222
+ 'Inactive',
4223
+ 'Old',
4224
+ 'unknown'
4225
+ ];
4226
+
4227
+ if (activeStates.includes(state)) {
4228
+ return 'active';
4229
+ } else if (inactiveStates.includes(state)) {
4230
+ return 'inactive';
4231
+ } else {
4232
+ // Default for any unknown states
4233
+ return 'inactive';
4234
+ }
4235
+ }
4236
+
4237
+ /**
4238
+ * Get simple conversation preview text (avoids repeating metadata)
4239
+ * @param {Object} conv - Conversation object
4240
+ * @returns {string} Preview text
4241
+ */
4242
+ getSimpleConversationPreview(conv) {
4243
+ // If we have a last message, show it (this is the most useful info)
4244
+ if (conv.lastMessage && conv.lastMessage.trim()) {
4245
+ const lastMsg = conv.lastMessage.trim();
4246
+
4247
+ // Check if last message is a simple confirmation and try to make it more descriptive
4248
+ if (this.isSimpleConfirmation(lastMsg)) {
4249
+ const messageCount = conv.messageCount || 0;
4250
+ const lastActivity = conv.lastModified ? this.formatRelativeTime(new Date(conv.lastModified)) : 'recently';
4251
+ return `User confirmed action • ${messageCount} messages • ${lastActivity}`;
4252
+ }
4253
+
4254
+ // Check if it's a tool-related message
4255
+ if (lastMsg.includes('Tool Result') || lastMsg.includes('[Tool:')) {
4256
+ return `Tool execution completed • ${this.truncateText(lastMsg, 60)}`;
4257
+ }
4258
+
4259
+ return this.truncateText(lastMsg, 80);
4260
+ }
4261
+
4262
+ // For empty conversations, show descriptive text
4263
+ const messageCount = conv.messageCount || 0;
4264
+ if (messageCount === 0) {
4265
+ return 'Empty conversation - click to start chatting';
4266
+ }
4267
+
4268
+ // For conversations without lastMessage but with messages, show informative text
4269
+ const lastActivity = conv.lastModified ? this.formatRelativeTime(new Date(conv.lastModified)) : 'unknown';
4270
+ return `${messageCount} messages • Last activity ${lastActivity}`;
4271
+ }
4272
+
4273
+ /**
4274
+ * Check if a message is a simple confirmation
4275
+ * @param {string} message - Message content
4276
+ * @returns {boolean} True if it's a simple confirmation
4277
+ */
4278
+ isSimpleConfirmation(message) {
4279
+ const trimmed = message.trim();
4280
+ const confirmationPatterns = [
4281
+ /^\[ok\]$/i,
4282
+ /^ok$/i,
4283
+ /^yes$/i,
4284
+ /^\[yes\]$/i,
4285
+ /^y$/i,
4286
+ /^\[y\]$/i,
4287
+ /^[1-9]$/, // Choice selection
4288
+ /^(continue|proceed|accept|confirm|done)$/i
4289
+ ];
4290
+
4291
+ return confirmationPatterns.some(pattern => pattern.test(trimmed));
4292
+ }
4293
+
4294
+ /**
4295
+ * Get conversation preview text (legacy method - still used in other places)
4296
+ * @param {Object} conv - Conversation object
4297
+ * @param {string} state - Conversation state
4298
+ * @returns {string} Preview text
4299
+ */
4300
+ getConversationPreview(conv, state) {
4301
+ // If we have a last message, show it
4302
+ if (conv.lastMessage && conv.lastMessage.trim()) {
4303
+ return this.truncateText(conv.lastMessage, 60);
4304
+ }
4305
+
4306
+ // Otherwise, show conversation info based on state and metadata
4307
+ const messageCount = conv.messageCount || 0;
4308
+
4309
+ if (messageCount === 0) {
4310
+ return `Empty conversation • Project: ${conv.project || 'Unknown'}`;
4311
+ }
4312
+
4313
+ // Show state-based preview
4314
+ if (state === 'Claude Code working...') {
4315
+ return `Claude is working • ${messageCount} messages`;
4316
+ } else if (state === 'Awaiting user input...') {
4317
+ return `Waiting for your input • ${messageCount} messages`;
4318
+ } else if (state === 'User typing...') {
4319
+ return `Ready for your message • ${messageCount} messages`;
4320
+ } else if (state === 'Recently active') {
4321
+ return `Recently active • ${messageCount} messages`;
4322
+ } else {
4323
+ return `${messageCount} messages • Last active ${this.formatRelativeTime(new Date(conv.lastModified))}`;
4324
+ }
4325
+ }
4326
+
4327
+ /**
4328
+ * Get state CSS class
4329
+ * @param {string} state - Conversation state
4330
+ * @returns {string} CSS class
4331
+ */
4332
+ getStateClass(state) {
4333
+ const stateClasses = {
4334
+ 'Claude Code working...': 'status-active',
4335
+ 'Awaiting user input...': 'status-waiting',
4336
+ 'User typing...': 'status-typing',
4337
+ 'Awaiting response...': 'status-pending',
4338
+ 'Recently active': 'status-recent',
4339
+ 'Idle': 'status-idle',
4340
+ 'Inactive': 'status-inactive',
4341
+ 'Old': 'status-old',
4342
+ 'unknown': 'status-unknown'
4343
+ };
4344
+ return stateClasses[state] || 'status-unknown';
4345
+ }
4346
+
4347
+ /**
4348
+ * Get state label
4349
+ * @param {string} state - Conversation state
4350
+ * @returns {string} Human readable label
4351
+ */
4352
+ getStateLabel(state) {
4353
+ const stateLabels = {
4354
+ 'Claude Code working...': 'Working',
4355
+ 'Awaiting user input...': 'Awaiting input',
4356
+ 'User typing...': 'Typing',
4357
+ 'Awaiting response...': 'Awaiting response',
4358
+ 'Recently active': 'Recent',
4359
+ 'Idle': 'Idle',
4360
+ 'Inactive': 'Inactive',
4361
+ 'Old': 'Old',
4362
+ 'unknown': 'Unknown'
4363
+ };
4364
+ return stateLabels[state] || state;
4365
+ }
4366
+
4367
+ /**
4368
+ * Truncate text to specified length
4369
+ * @param {string} text - Text to truncate
4370
+ * @param {number} maxLength - Maximum length
4371
+ * @returns {string} Truncated text
4372
+ */
4373
+ truncateText(text, maxLength) {
4374
+ if (!text || text.length <= maxLength) return text;
4375
+ return text.substring(0, maxLength - 3) + '...';
4376
+ }
4377
+
4378
+ /**
4379
+ * Update filter
4380
+ * @param {string} filterName - Filter name
4381
+ * @param {string} value - Filter value
4382
+ */
4383
+ updateFilter(filterName, value) {
4384
+ this.filters[filterName] = value;
4385
+ // When filters change, restart from beginning
4386
+ this.refreshFromBeginning();
4387
+ }
4388
+
4389
+ /**
4390
+ * Clear search
4391
+ */
4392
+ clearSearch() {
4393
+ const searchInput = this.container.querySelector('#search-filter');
4394
+ if (!searchInput) return; // Guard against null when AgentsPage isn't rendered
4395
+
4396
+ searchInput.value = '';
4397
+ this.updateFilter('search', '');
4398
+ }
4399
+
4400
+ /**
4401
+ * Clear all filters
4402
+ */
4403
+ clearAllFilters() {
4404
+ this.filters = {
4405
+ status: 'all',
4406
+ timeRange: '7d',
4407
+ search: ''
4408
+ };
4409
+
4410
+ // Reset UI
4411
+ const statusFilter = this.container.querySelector('#status-filter');
4412
+ const timeFilter = this.container.querySelector('#time-filter');
4413
+ const searchFilter = this.container.querySelector('#search-filter');
4414
+
4415
+ if (statusFilter) statusFilter.value = 'all';
4416
+ if (timeFilter) timeFilter.value = '7d';
4417
+ if (searchFilter) searchFilter.value = '';
4418
+
4419
+ // Restart from beginning when clearing filters
4420
+ this.refreshFromBeginning();
4421
+ }
4422
+
4423
+ /**
4424
+ * Refresh conversations display
4425
+ */
4426
+ refreshConversationsDisplay() {
4427
+ const conversations = this.stateService.getStateProperty('conversations') || [];
4428
+ const statesData = this.stateService.getStateProperty('conversationStates') || {};
4429
+ // Extract activeStates from the stored state data
4430
+ const activeStates = statesData?.activeStates || {};
4431
+ this.renderConversationsList(conversations, activeStates);
4432
+ }
4433
+
4434
+ /**
4435
+ * Refresh from beginning - resets pagination
4436
+ */
4437
+ async refreshFromBeginning() {
4438
+ // Clear cache
4439
+ this.loadedConversations = [];
4440
+ this.loadedMessages.clear();
4441
+
4442
+ // Reset pagination
4443
+ this.pagination = {
4444
+ currentPage: 0,
4445
+ limit: 10,
4446
+ hasMore: true,
4447
+ isLoading: false
4448
+ };
4449
+
4450
+ // Clear list and reload
4451
+ const listContainer = this.container.querySelector('#conversations-list');
4452
+ if (listContainer) {
4453
+ listContainer.innerHTML = '';
4454
+ }
4455
+
4456
+ await this.loadConversationsData();
4457
+ }
4458
+
4459
+ /**
4460
+ * Refresh conversations data
4461
+ */
4462
+ async refreshConversations() {
4463
+ const refreshBtn = this.container.querySelector('#refresh-conversations');
4464
+ if (!refreshBtn) return; // Guard against null when AgentsPage isn't rendered
4465
+
4466
+ refreshBtn.disabled = true;
4467
+ const iconElement = refreshBtn.querySelector('.btn-icon');
4468
+ if (iconElement) {
4469
+ iconElement.style.animation = 'spin 1s linear infinite';
4470
+ }
4471
+
4472
+ try {
4473
+ // Clear both server and client cache to force fresh data
4474
+ await this.dataService.clearServerCache('conversations');
4475
+ await this.loadConversationsData();
4476
+ } catch (error) {
4477
+ console.error('Error refreshing conversations:', error);
4478
+ this.stateService.setError('Failed to refresh conversations');
4479
+ } finally {
4480
+ refreshBtn.disabled = false;
4481
+ if (iconElement) {
4482
+ iconElement.style.animation = '';
4483
+ }
4484
+ }
4485
+ }
4486
+
4487
+ /**
4488
+ * Check if there are active filters
4489
+ * @returns {boolean} True if filters are active
4490
+ */
4491
+ hasActiveFilters() {
4492
+ const searchInput = this.container.querySelector('#conversation-search');
4493
+ const searchTerm = searchInput ? searchInput.value.trim().toLowerCase() : '';
4494
+
4495
+ // Check if search filter is active
4496
+ if (searchTerm) {
4497
+ return true;
4498
+ }
4499
+
4500
+ // Check if state filters are active
4501
+ const filterButtons = this.container.querySelectorAll('.filter-btn');
4502
+ const activeFilters = Array.from(filterButtons).filter(btn =>
4503
+ btn.classList.contains('active') && btn.getAttribute('data-state') !== 'all'
4504
+ );
4505
+
4506
+ return activeFilters.length > 0;
4507
+ }
4508
+
4509
+ /**
4510
+ * Update results count
4511
+ * @param {number} count - Number of results
4512
+ * @param {boolean} hasActiveFilters - Whether filters are active
4513
+ */
4514
+ updateResultsCount(count, hasActiveFilters = false) {
4515
+ // Update main results count
4516
+ const resultsCount = this.container.querySelector('#results-count');
4517
+ if (resultsCount) {
4518
+ let countText = `${count} conversation${count !== 1 ? 's' : ''} found`;
4519
+ if (hasActiveFilters && this.pagination && this.pagination.totalCount && count < this.pagination.totalCount) {
4520
+ countText += ` (filtered from ${this.pagination.totalCount})`;
4521
+ }
4522
+ resultsCount.textContent = countText;
4523
+ }
4524
+
4525
+ // Update sidebar count
4526
+ const sidebarCount = this.container.querySelector('#sidebar-count');
4527
+ if (sidebarCount) {
4528
+ sidebarCount.textContent = count;
4529
+ }
4530
+ }
4531
+
4532
+ /**
4533
+ * Show empty state
4534
+ */
4535
+ showEmptyState() {
4536
+ const conversationsList = this.container.querySelector('#conversations-list');
4537
+ const emptyState = this.container.querySelector('#empty-state');
4538
+ if (!conversationsList || !emptyState) return; // Guard against null when AgentsPage isn't rendered
4539
+
4540
+ conversationsList.style.display = 'none';
4541
+ emptyState.style.display = 'flex';
4542
+ }
4543
+
4544
+ /**
4545
+ * Hide empty state
4546
+ */
4547
+ hideEmptyState() {
4548
+ const conversationsList = this.container.querySelector('#conversations-list');
4549
+ const emptyState = this.container.querySelector('#empty-state');
4550
+ if (!conversationsList || !emptyState) return; // Guard against null when AgentsPage isn't rendered
4551
+
4552
+ conversationsList.style.display = 'block';
4553
+ emptyState.style.display = 'none';
4554
+ }
4555
+
4556
+ /**
4557
+ * Toggle between grid and table view
4558
+ * @param {string} view - View type ('grid' or 'table')
4559
+ */
4560
+ toggleView(view) {
4561
+ const toggleBtns = this.container.querySelectorAll('.toggle-btn');
4562
+ toggleBtns.forEach(btn => {
4563
+ btn.classList.toggle('active', btn.dataset.view === view);
4564
+ });
4565
+
4566
+ const gridElement = this.container.querySelector('#conversations-grid');
4567
+ const tableSection = this.container.querySelector('.conversations-table-section');
4568
+
4569
+ if (!gridElement || !tableSection) return; // Guard against null when AgentsPage isn't rendered
4570
+
4571
+ const gridSection = gridElement.parentNode;
4572
+
4573
+ if (view === 'table') {
4574
+ gridSection.style.display = 'none';
4575
+ tableSection.style.display = 'block';
4576
+ } else {
4577
+ gridSection.style.display = 'block';
4578
+ tableSection.style.display = 'none';
4579
+ }
4580
+ }
4581
+
4582
+ /**
4583
+ * View conversation details
4584
+ * @param {string} conversationId - Conversation ID
4585
+ */
4586
+ viewConversation(conversationId) {
4587
+ // This would open a detailed conversation view
4588
+ // Implementation would show conversation details modal or navigate to detail page
4589
+ }
4590
+
4591
+ /**
4592
+ * Export single conversation
4593
+ * @param {string} conversationId - Conversation ID
4594
+ */
4595
+ exportSingleConversation(conversationId) {
4596
+ const conversations = this.stateService.getStateProperty('conversations') || [];
4597
+ const conversation = conversations.find(conv => conv.id === conversationId);
4598
+
4599
+ if (conversation) {
4600
+ const dataStr = JSON.stringify(conversation, null, 2);
4601
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
4602
+ const url = URL.createObjectURL(dataBlob);
4603
+
4604
+ const link = document.createElement('a');
4605
+ link.href = url;
4606
+ link.download = `conversation-${conversationId}-${new Date().toISOString().split('T')[0]}.json`;
4607
+ link.click();
4608
+
4609
+ URL.revokeObjectURL(url);
4610
+ }
4611
+ }
4612
+
4613
+ /**
4614
+ * Export all conversations
4615
+ */
4616
+ exportConversations() {
4617
+ const conversations = this.stateService.getStateProperty('conversations') || [];
4618
+ const states = this.stateService.getStateProperty('conversationStates') || {};
4619
+ const filteredConversations = this.filterConversations(conversations, states);
4620
+
4621
+ const dataStr = JSON.stringify({
4622
+ conversations: filteredConversations,
4623
+ states: states,
4624
+ exportDate: new Date().toISOString(),
4625
+ filters: this.filters
4626
+ }, null, 2);
4627
+
4628
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
4629
+ const url = URL.createObjectURL(dataBlob);
4630
+
4631
+ const link = document.createElement('a');
4632
+ link.href = url;
4633
+ link.download = `claude-conversations-${new Date().toISOString().split('T')[0]}.json`;
4634
+ link.click();
4635
+
4636
+ URL.revokeObjectURL(url);
4637
+ }
4638
+
4639
+ /**
4640
+ * Update conversations display
4641
+ * @param {Array} conversations - Conversations data
4642
+ */
4643
+ updateConversationsDisplay(conversations) {
4644
+ const statesData = this.stateService.getStateProperty('conversationStates') || {};
4645
+ const activeStates = statesData?.activeStates || {};
4646
+ this.renderConversationsList(conversations, activeStates);
4647
+ }
4648
+
4649
+ /**
4650
+ * Update conversation states
4651
+ * @param {Object} activeStates - Active conversation states (direct object, not nested)
4652
+ */
4653
+ updateConversationStates(activeStates) {
4654
+ if (!this.isInitialized) {
4655
+ console.warn('AgentsPage: updateConversationStates called before initialization');
4656
+ return;
4657
+ }
4658
+
4659
+ // Check if we're still on the agents page by verifying our key element exists
4660
+ const conversationsContainer = this.container.querySelector('#conversations-list');
4661
+ if (!conversationsContainer) {
4662
+ console.log('AgentsPage: Not on agents page, skipping conversation states update');
4663
+ return;
4664
+ }
4665
+
4666
+ const conversations = this.stateService.getStateProperty('conversations') || [];
4667
+
4668
+
4669
+ // Re-render conversation list with new states
4670
+ this.renderConversationsList(conversations, activeStates || {});
4671
+
4672
+ // Update banner if we have a selected conversation
4673
+ if (this.selectedConversationId && activeStates && activeStates[this.selectedConversationId]) {
4674
+ this.updateStateBanner(this.selectedConversationId, activeStates[this.selectedConversationId]);
4675
+ }
4676
+ }
4677
+
4678
+ /**
4679
+ * Handle conversation state change
4680
+ * @param {Object} _state - New state (unused but required by interface)
4681
+ */
4682
+ handleConversationStateChange(_state) {
4683
+ this.refreshConversationsDisplay();
4684
+ }
4685
+
4686
+ /**
4687
+ * Update loading state
4688
+ * @param {boolean} isLoading - Loading state
4689
+ */
4690
+ updateLoadingState(isLoading) {
4691
+ const loadingState = this.container.querySelector('#conversations-loading');
4692
+ if (loadingState) {
4693
+ loadingState.style.display = isLoading ? 'flex' : 'none';
4694
+ }
4695
+ }
4696
+
4697
+ /**
4698
+ * Update error state
4699
+ * @param {Error|string} error - Error object or message
4700
+ */
4701
+ updateErrorState(error) {
4702
+ const errorState = this.container.querySelector('#conversations-error');
4703
+ const errorMessage = this.container.querySelector('.error-message');
4704
+
4705
+ if (errorState && errorMessage) {
4706
+ if (error) {
4707
+ errorMessage.textContent = error.message || error;
4708
+ errorState.style.display = 'flex';
4709
+ } else {
4710
+ errorState.style.display = 'none';
4711
+ }
4712
+ }
4713
+ }
4714
+
4715
+ /**
4716
+ * Destroy agents page
4717
+ */
4718
+ destroy() {
4719
+ // Cleanup components
4720
+ Object.values(this.components).forEach(component => {
4721
+ if (component.destroy) {
4722
+ component.destroy();
4723
+ }
4724
+ });
4725
+
4726
+ // Cleanup scroll listeners
4727
+ const messagesContent = this.container.querySelector('#messages-content');
4728
+ if (messagesContent && this.messagesScrollListener) {
4729
+ messagesContent.removeEventListener('scroll', this.messagesScrollListener);
4730
+ }
4731
+
4732
+ // Unsubscribe from state changes
4733
+ if (this.unsubscribe) {
4734
+ this.unsubscribe();
4735
+ }
4736
+
4737
+ this.isInitialized = false;
4738
+ }
4739
+ }
4740
+
4741
+ // Export for module use
4742
+ if (typeof module !== 'undefined' && module.exports) {
4743
+ module.exports = AgentsPage;
4744
+ }