claude-code-templates 1.15.0 → 1.15.1

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