claude-mpm 3.4.10__py3-none-any.whl → 3.4.14__py3-none-any.whl

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 (29) hide show
  1. claude_mpm/cli/commands/run.py +10 -10
  2. claude_mpm/dashboard/index.html +13 -0
  3. claude_mpm/dashboard/static/css/dashboard.css +2722 -0
  4. claude_mpm/dashboard/static/js/components/agent-inference.js +619 -0
  5. claude_mpm/dashboard/static/js/components/event-processor.js +641 -0
  6. claude_mpm/dashboard/static/js/components/event-viewer.js +914 -0
  7. claude_mpm/dashboard/static/js/components/export-manager.js +362 -0
  8. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +611 -0
  9. claude_mpm/dashboard/static/js/components/hud-library-loader.js +211 -0
  10. claude_mpm/dashboard/static/js/components/hud-manager.js +671 -0
  11. claude_mpm/dashboard/static/js/components/hud-visualizer.js +1718 -0
  12. claude_mpm/dashboard/static/js/components/module-viewer.js +2701 -0
  13. claude_mpm/dashboard/static/js/components/session-manager.js +520 -0
  14. claude_mpm/dashboard/static/js/components/socket-manager.js +343 -0
  15. claude_mpm/dashboard/static/js/components/ui-state-manager.js +427 -0
  16. claude_mpm/dashboard/static/js/components/working-directory.js +866 -0
  17. claude_mpm/dashboard/static/js/dashboard-original.js +4134 -0
  18. claude_mpm/dashboard/static/js/dashboard.js +1978 -0
  19. claude_mpm/dashboard/static/js/socket-client.js +537 -0
  20. claude_mpm/dashboard/templates/index.html +346 -0
  21. claude_mpm/dashboard/test_dashboard.html +372 -0
  22. claude_mpm/scripts/socketio_daemon.py +51 -6
  23. claude_mpm/services/socketio_server.py +41 -5
  24. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/METADATA +2 -1
  25. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/RECORD +29 -9
  26. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/WHEEL +0 -0
  27. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/entry_points.txt +0 -0
  28. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/licenses/LICENSE +0 -0
  29. {claude_mpm-3.4.10.dist-info → claude_mpm-3.4.14.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1978 @@
1
+ /**
2
+ * Refactored Dashboard Coordinator
3
+ *
4
+ * Main coordinator class that orchestrates all dashboard modules while maintaining
5
+ * backward compatibility with the original dashboard interface.
6
+ *
7
+ * WHY: This refactored version breaks down the monolithic 4,133-line dashboard
8
+ * into manageable, focused modules while preserving all existing functionality.
9
+ * Each module handles a specific concern, improving maintainability and testability.
10
+ *
11
+ * DESIGN DECISION: Acts as a thin coordinator layer that initializes modules,
12
+ * manages inter-module communication through events, and provides backward
13
+ * compatibility for existing code that depends on the dashboard interface.
14
+ */
15
+ class Dashboard {
16
+ constructor() {
17
+ // Core components (existing)
18
+ this.eventViewer = null;
19
+ this.moduleViewer = null;
20
+ this.sessionManager = null;
21
+
22
+ // New modular components
23
+ this.socketManager = null;
24
+ this.agentInference = null;
25
+ this.uiStateManager = null;
26
+ this.eventProcessor = null;
27
+ this.exportManager = null;
28
+ this.workingDirectoryManager = null;
29
+ this.fileToolTracker = null;
30
+
31
+ // Initialize the dashboard
32
+ this.init();
33
+ }
34
+
35
+ /**
36
+ * Initialize the dashboard and all modules
37
+ */
38
+ init() {
39
+ console.log('Initializing refactored Claude MPM Dashboard...');
40
+
41
+ // Initialize modules in dependency order
42
+ this.initializeSocketManager();
43
+ this.initializeCoreComponents();
44
+ this.initializeAgentInference();
45
+ this.initializeUIStateManager();
46
+ this.initializeWorkingDirectoryManager();
47
+ this.initializeFileToolTracker();
48
+ this.initializeEventProcessor();
49
+ this.initializeExportManager();
50
+
51
+ // Set up inter-module communication
52
+ this.setupModuleInteractions();
53
+
54
+ // Initialize from URL parameters
55
+ this.initializeFromURL();
56
+
57
+ console.log('Claude MPM Dashboard initialized successfully');
58
+ }
59
+
60
+ /**
61
+ * Initialize socket manager
62
+ */
63
+ initializeSocketManager() {
64
+ this.socketManager = new SocketManager();
65
+
66
+ // Set up connection controls
67
+ this.socketManager.setupConnectionControls();
68
+
69
+ // Backward compatibility
70
+ this.socketClient = this.socketManager.getSocketClient();
71
+ window.socketClient = this.socketClient;
72
+ }
73
+
74
+ /**
75
+ * Initialize core existing components
76
+ */
77
+ initializeCoreComponents() {
78
+ // Initialize existing components with socket client
79
+ this.eventViewer = new EventViewer('events-list', this.socketClient);
80
+ this.moduleViewer = new ModuleViewer();
81
+ this.sessionManager = new SessionManager(this.socketClient);
82
+
83
+ // Backward compatibility
84
+ window.eventViewer = this.eventViewer;
85
+ window.moduleViewer = this.moduleViewer;
86
+ window.sessionManager = this.sessionManager;
87
+ }
88
+
89
+ /**
90
+ * Initialize agent inference system
91
+ */
92
+ initializeAgentInference() {
93
+ this.agentInference = new AgentInference(this.eventViewer);
94
+ this.agentInference.initialize();
95
+ }
96
+
97
+ /**
98
+ * Initialize UI state manager
99
+ */
100
+ initializeUIStateManager() {
101
+ this.uiStateManager = new UIStateManager();
102
+ this.setupTabFilters(); // Set up filters after UI state manager
103
+ }
104
+
105
+ /**
106
+ * Initialize working directory manager
107
+ */
108
+ initializeWorkingDirectoryManager() {
109
+ this.workingDirectoryManager = new WorkingDirectoryManager(this.socketManager);
110
+ }
111
+
112
+ /**
113
+ * Initialize file-tool tracker
114
+ */
115
+ initializeFileToolTracker() {
116
+ this.fileToolTracker = new FileToolTracker(this.agentInference, this.workingDirectoryManager);
117
+ }
118
+
119
+ /**
120
+ * Initialize event processor
121
+ */
122
+ initializeEventProcessor() {
123
+ this.eventProcessor = new EventProcessor(this.eventViewer, this.agentInference);
124
+ }
125
+
126
+
127
+ /**
128
+ * Initialize export manager
129
+ */
130
+ initializeExportManager() {
131
+ this.exportManager = new ExportManager(this.eventViewer);
132
+ }
133
+
134
+ /**
135
+ * Set up interactions between modules
136
+ */
137
+ setupModuleInteractions() {
138
+ // Socket events to update file operations and tool calls
139
+ this.socketManager.onEventUpdate((events) => {
140
+ this.fileToolTracker.updateFileOperations(events);
141
+ this.fileToolTracker.updateToolCalls(events);
142
+
143
+ // Process agent inference for new events
144
+ this.agentInference.processAgentInference();
145
+
146
+ // Auto-scroll events list if on events tab
147
+ if (this.uiStateManager.getCurrentTab() === 'events') {
148
+ this.exportManager.scrollListToBottom('events-list');
149
+ }
150
+
151
+ // Re-render current tab
152
+ this.renderCurrentTab();
153
+ });
154
+
155
+ // Connection status changes
156
+ this.socketManager.onConnectionStatusChange((status, type) => {
157
+ // Set up git branch listener when connected
158
+ if (type === 'connected') {
159
+ console.log('[DASHBOARD-INIT-DEBUG] Connection established, waiting for directory to be ready...');
160
+
161
+ // Wait for working directory to be properly initialized
162
+ this.workingDirectoryManager.whenDirectoryReady(() => {
163
+ const currentDir = this.workingDirectoryManager.getCurrentWorkingDir();
164
+ console.log('[DASHBOARD-INIT-DEBUG] Directory ready, requesting git branch for:', currentDir);
165
+ this.workingDirectoryManager.updateGitBranch(currentDir);
166
+ });
167
+ }
168
+ });
169
+
170
+ // Tab changes
171
+ document.addEventListener('tabChanged', (e) => {
172
+ this.renderCurrentTab();
173
+ this.uiStateManager.updateTabNavigationItems();
174
+ });
175
+
176
+ // Events clearing
177
+ document.addEventListener('eventsClearing', () => {
178
+ this.fileToolTracker.clear();
179
+ this.agentInference.initialize();
180
+ });
181
+
182
+ // Card details requests
183
+ document.addEventListener('showCardDetails', (e) => {
184
+ this.showCardDetails(e.detail.tabName, e.detail.index);
185
+ });
186
+
187
+ // Session changes
188
+ document.addEventListener('sessionFilterChanged', (e) => {
189
+ console.log('Session filter changed, re-rendering current tab:', this.uiStateManager.getCurrentTab());
190
+ this.renderCurrentTab();
191
+ });
192
+ }
193
+
194
+ /**
195
+ * Set up tab filters
196
+ */
197
+ setupTabFilters() {
198
+ // Agents tab filters
199
+ const agentsSearchInput = document.getElementById('agents-search-input');
200
+ const agentsTypeFilter = document.getElementById('agents-type-filter');
201
+
202
+ if (agentsSearchInput) {
203
+ agentsSearchInput.addEventListener('input', () => {
204
+ if (this.uiStateManager.getCurrentTab() === 'agents') this.renderCurrentTab();
205
+ });
206
+ }
207
+
208
+ if (agentsTypeFilter) {
209
+ agentsTypeFilter.addEventListener('change', () => {
210
+ if (this.uiStateManager.getCurrentTab() === 'agents') this.renderCurrentTab();
211
+ });
212
+ }
213
+
214
+ // Tools tab filters
215
+ const toolsSearchInput = document.getElementById('tools-search-input');
216
+ const toolsTypeFilter = document.getElementById('tools-type-filter');
217
+
218
+ if (toolsSearchInput) {
219
+ toolsSearchInput.addEventListener('input', () => {
220
+ if (this.uiStateManager.getCurrentTab() === 'tools') this.renderCurrentTab();
221
+ });
222
+ }
223
+
224
+ if (toolsTypeFilter) {
225
+ toolsTypeFilter.addEventListener('change', () => {
226
+ if (this.uiStateManager.getCurrentTab() === 'tools') this.renderCurrentTab();
227
+ });
228
+ }
229
+
230
+ // Files tab filters
231
+ const filesSearchInput = document.getElementById('files-search-input');
232
+ const filesTypeFilter = document.getElementById('files-type-filter');
233
+
234
+ if (filesSearchInput) {
235
+ filesSearchInput.addEventListener('input', () => {
236
+ if (this.uiStateManager.getCurrentTab() === 'files') this.renderCurrentTab();
237
+ });
238
+ }
239
+
240
+ if (filesTypeFilter) {
241
+ filesTypeFilter.addEventListener('change', () => {
242
+ if (this.uiStateManager.getCurrentTab() === 'files') this.renderCurrentTab();
243
+ });
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Initialize from URL parameters
249
+ */
250
+ initializeFromURL() {
251
+ const params = new URLSearchParams(window.location.search);
252
+ this.socketManager.initializeFromURL(params);
253
+ }
254
+
255
+ /**
256
+ * Render current tab content
257
+ */
258
+ renderCurrentTab() {
259
+ const currentTab = this.uiStateManager.getCurrentTab();
260
+
261
+ switch (currentTab) {
262
+ case 'events':
263
+ // Events tab is handled by EventViewer
264
+ break;
265
+ case 'agents':
266
+ this.renderAgents();
267
+ break;
268
+ case 'tools':
269
+ this.renderTools();
270
+ break;
271
+ case 'files':
272
+ this.renderFiles();
273
+ break;
274
+ }
275
+
276
+ // Update selection UI if we have a selected card
277
+ const selectedCard = this.uiStateManager.getSelectedCard();
278
+ if (selectedCard.tab === currentTab) {
279
+ this.uiStateManager.updateCardSelectionUI();
280
+ }
281
+
282
+ // Update unified selection UI to maintain consistency
283
+ this.uiStateManager.updateUnifiedSelectionUI();
284
+ }
285
+
286
+ /**
287
+ * Render agents tab with unique instance view (one row per PM delegation)
288
+ */
289
+ renderAgents() {
290
+ const agentsList = document.getElementById('agents-list');
291
+ if (!agentsList) return;
292
+
293
+ // Process agent inference to get PM delegations
294
+ this.agentInference.processAgentInference();
295
+
296
+ // Generate HTML for unique agent instances
297
+ const events = this.eventProcessor.getFilteredEventsForTab('agents');
298
+ const agentHTML = this.eventProcessor.generateAgentHTML(events);
299
+
300
+ agentsList.innerHTML = agentHTML;
301
+ this.exportManager.scrollListToBottom('agents-list');
302
+
303
+ // Update filter dropdowns with unique instances
304
+ const uniqueInstances = this.agentInference.getUniqueAgentInstances();
305
+ this.updateAgentsFilterDropdowns(uniqueInstances);
306
+ }
307
+
308
+ /**
309
+ * Render tools tab with unique instance view (one row per unique tool call)
310
+ */
311
+ renderTools() {
312
+ const toolsList = document.getElementById('tools-list');
313
+ if (!toolsList) return;
314
+
315
+ const toolCalls = this.fileToolTracker.getToolCalls();
316
+ const toolCallsArray = Array.from(toolCalls.entries());
317
+ const uniqueToolInstances = this.eventProcessor.getUniqueToolInstances(toolCallsArray);
318
+ const toolHTML = this.eventProcessor.generateToolHTML(uniqueToolInstances);
319
+
320
+ toolsList.innerHTML = toolHTML;
321
+ this.exportManager.scrollListToBottom('tools-list');
322
+
323
+ // Update filter dropdowns
324
+ this.updateToolsFilterDropdowns(uniqueToolInstances);
325
+ }
326
+
327
+ /**
328
+ * Render files tab with unique instance view (one row per unique file)
329
+ */
330
+ renderFiles() {
331
+ const filesList = document.getElementById('files-list');
332
+ if (!filesList) return;
333
+
334
+ const fileOperations = this.fileToolTracker.getFileOperations();
335
+ const filesArray = Array.from(fileOperations.entries());
336
+ const uniqueFileInstances = this.eventProcessor.getUniqueFileInstances(filesArray);
337
+ const fileHTML = this.eventProcessor.generateFileHTML(uniqueFileInstances);
338
+
339
+ filesList.innerHTML = fileHTML;
340
+ this.exportManager.scrollListToBottom('files-list');
341
+
342
+ // Update filter dropdowns
343
+ this.updateFilesFilterDropdowns(filesArray);
344
+ }
345
+
346
+ /**
347
+ * Update agents filter dropdowns for unique instances
348
+ */
349
+ updateAgentsFilterDropdowns(uniqueInstances) {
350
+ const agentTypes = new Set();
351
+
352
+ // uniqueInstances is already an array of unique agent instances
353
+ uniqueInstances.forEach(instance => {
354
+ if (instance.agentName && instance.agentName !== 'Unknown') {
355
+ agentTypes.add(instance.agentName);
356
+ }
357
+ });
358
+
359
+ const sortedTypes = Array.from(agentTypes).filter(type => type && type.trim() !== '');
360
+ this.populateFilterDropdown('agents-type-filter', sortedTypes, 'All Agent Types');
361
+
362
+ // Debug log
363
+ if (sortedTypes.length > 0) {
364
+ console.log('Agent types found for filter:', sortedTypes);
365
+ } else {
366
+ console.log('No agent types found for filter. Unique instances:', uniqueInstances.length);
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Update tools filter dropdowns
372
+ */
373
+ updateToolsFilterDropdowns(toolCallsArray) {
374
+ const toolNames = [...new Set(toolCallsArray.map(([key, toolCall]) => toolCall.tool_name))]
375
+ .filter(name => name);
376
+
377
+ this.populateFilterDropdown('tools-type-filter', toolNames, 'All Tools');
378
+ }
379
+
380
+ /**
381
+ * Update files filter dropdowns
382
+ */
383
+ updateFilesFilterDropdowns(filesArray) {
384
+ const operations = [...new Set(filesArray.flatMap(([path, data]) =>
385
+ data.operations.map(op => op.operation)
386
+ ))].filter(op => op);
387
+
388
+ this.populateFilterDropdown('files-type-filter', operations, 'All Operations');
389
+ }
390
+
391
+ /**
392
+ * Populate filter dropdown with values
393
+ */
394
+ populateFilterDropdown(selectId, values, allOption = 'All') {
395
+ const select = document.getElementById(selectId);
396
+ if (!select) return;
397
+
398
+ const currentValue = select.value;
399
+ const sortedValues = values.sort((a, b) => a.localeCompare(b));
400
+
401
+ // Clear existing options except the first "All" option
402
+ select.innerHTML = `<option value="">${allOption}</option>`;
403
+
404
+ // Add sorted values
405
+ sortedValues.forEach(value => {
406
+ const option = document.createElement('option');
407
+ option.value = value;
408
+ option.textContent = value;
409
+ select.appendChild(option);
410
+ });
411
+
412
+ // Restore previous selection if it still exists
413
+ if (currentValue && sortedValues.includes(currentValue)) {
414
+ select.value = currentValue;
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Show card details for specified tab and index
420
+ */
421
+ showCardDetails(tabName, index) {
422
+ switch (tabName) {
423
+ case 'events':
424
+ if (this.eventViewer) {
425
+ this.eventViewer.showEventDetails(index);
426
+ }
427
+ break;
428
+ case 'agents':
429
+ this.showAgentDetailsByIndex(index);
430
+ break;
431
+ case 'tools':
432
+ this.showToolDetailsByIndex(index);
433
+ break;
434
+ case 'files':
435
+ this.showFileDetailsByIndex(index);
436
+ break;
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Show agent details by index
442
+ */
443
+ showAgentDetailsByIndex(index) {
444
+ const events = this.eventProcessor.getFilteredEventsForTab('agents');
445
+
446
+ // Defensive checks
447
+ if (!events || !Array.isArray(events) || index < 0 || index >= events.length) {
448
+ console.warn('Dashboard: Invalid agent index or events array');
449
+ return;
450
+ }
451
+
452
+ const filteredSingleEvent = this.eventProcessor.applyAgentsFilters([events[index]]);
453
+
454
+ if (filteredSingleEvent.length > 0 && this.moduleViewer &&
455
+ typeof this.moduleViewer.showAgentEvent === 'function') {
456
+ const event = filteredSingleEvent[0];
457
+ this.moduleViewer.showAgentEvent(event, index);
458
+ }
459
+ }
460
+
461
+ /**
462
+ * Show agent instance details for unique instance view
463
+ * @param {string} instanceId - Agent instance ID
464
+ */
465
+ showAgentInstanceDetails(instanceId) {
466
+ const pmDelegations = this.agentInference.getPMDelegations();
467
+ const instance = pmDelegations.get(instanceId);
468
+
469
+ if (!instance) {
470
+ // Check if it's an implied delegation
471
+ const uniqueInstances = this.agentInference.getUniqueAgentInstances();
472
+ const impliedInstance = uniqueInstances.find(inst => inst.id === instanceId);
473
+
474
+ if (!impliedInstance) {
475
+ console.error('Agent instance not found:', instanceId);
476
+ return;
477
+ }
478
+
479
+ // For implied instances, show basic info
480
+ this.showImpliedAgentDetails(impliedInstance);
481
+ return;
482
+ }
483
+
484
+ // Show full PM delegation details
485
+ if (this.moduleViewer && typeof this.moduleViewer.showAgentInstance === 'function') {
486
+ this.moduleViewer.showAgentInstance(instance);
487
+ } else {
488
+ // Fallback: show in console or basic modal
489
+ console.log('Agent Instance Details:', {
490
+ id: instanceId,
491
+ agentName: instance.agentName,
492
+ type: 'PM Delegation',
493
+ eventCount: instance.agentEvents.length,
494
+ startTime: instance.timestamp,
495
+ pmCall: instance.pmCall
496
+ });
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Show implied agent details (agents without explicit PM delegation)
502
+ * @param {Object} impliedInstance - Implied agent instance
503
+ */
504
+ showImpliedAgentDetails(impliedInstance) {
505
+ if (this.moduleViewer && typeof this.moduleViewer.showImpliedAgent === 'function') {
506
+ this.moduleViewer.showImpliedAgent(impliedInstance);
507
+ } else {
508
+ // Fallback: show in console or basic modal
509
+ console.log('Implied Agent Details:', {
510
+ id: impliedInstance.id,
511
+ agentName: impliedInstance.agentName,
512
+ type: 'Implied PM Delegation',
513
+ eventCount: impliedInstance.eventCount,
514
+ startTime: impliedInstance.timestamp,
515
+ note: 'No explicit PM call found - inferred from agent activity'
516
+ });
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Show tool details by index
522
+ */
523
+ showToolDetailsByIndex(index) {
524
+ const toolCalls = this.fileToolTracker.getToolCalls();
525
+ const toolCallsArray = Array.from(toolCalls.entries());
526
+ const filteredToolCalls = this.eventProcessor.applyToolCallFilters(toolCallsArray);
527
+
528
+ if (index >= 0 && index < filteredToolCalls.length) {
529
+ const [toolCallKey] = filteredToolCalls[index];
530
+ this.showToolCallDetails(toolCallKey);
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Show file details by index
536
+ */
537
+ showFileDetailsByIndex(index) {
538
+ const fileOperations = this.fileToolTracker.getFileOperations();
539
+ let filesArray = Array.from(fileOperations.entries());
540
+ filesArray = this.eventProcessor.applyFilesFilters(filesArray);
541
+
542
+ if (index >= 0 && index < filesArray.length) {
543
+ const [filePath] = filesArray[index];
544
+ this.showFileDetails(filePath);
545
+ }
546
+ }
547
+
548
+ /**
549
+ * Show tool call details
550
+ */
551
+ showToolCallDetails(toolCallKey) {
552
+ const toolCall = this.fileToolTracker.getToolCall(toolCallKey);
553
+ if (toolCall && this.moduleViewer) {
554
+ this.moduleViewer.showToolCall(toolCall, toolCallKey);
555
+ }
556
+ }
557
+
558
+ /**
559
+ * Show file details
560
+ */
561
+ showFileDetails(filePath) {
562
+ const fileData = this.fileToolTracker.getFileOperationsForFile(filePath);
563
+ if (fileData && this.moduleViewer) {
564
+ this.moduleViewer.showFileOperations(fileData, filePath);
565
+ }
566
+ }
567
+
568
+ // ====================================
569
+ // BACKWARD COMPATIBILITY METHODS
570
+ // ====================================
571
+
572
+ /**
573
+ * Switch tab (backward compatibility)
574
+ */
575
+ switchTab(tabName) {
576
+ this.uiStateManager.switchTab(tabName);
577
+ }
578
+
579
+ /**
580
+ * Select card (backward compatibility)
581
+ */
582
+ selectCard(tabName, index, type, data) {
583
+ this.uiStateManager.selectCard(tabName, index, type, data);
584
+ }
585
+
586
+ /**
587
+ * Clear events (backward compatibility)
588
+ */
589
+ clearEvents() {
590
+ this.exportManager.clearEvents();
591
+ }
592
+
593
+ /**
594
+ * Export events (backward compatibility)
595
+ */
596
+ exportEvents() {
597
+ this.exportManager.exportEvents();
598
+ }
599
+
600
+ /**
601
+ * Clear selection (backward compatibility)
602
+ */
603
+ clearSelection() {
604
+ this.uiStateManager.clearSelection();
605
+ if (this.eventViewer) {
606
+ this.eventViewer.clearSelection();
607
+ }
608
+ if (this.moduleViewer) {
609
+ this.moduleViewer.clear();
610
+ }
611
+ }
612
+
613
+
614
+ /**
615
+ * Get current working directory (backward compatibility)
616
+ */
617
+ get currentWorkingDir() {
618
+ return this.workingDirectoryManager.getCurrentWorkingDir();
619
+ }
620
+
621
+ /**
622
+ * Set current working directory (backward compatibility)
623
+ */
624
+ set currentWorkingDir(dir) {
625
+ this.workingDirectoryManager.setWorkingDirectory(dir);
626
+ }
627
+
628
+ /**
629
+ * Get current tab (backward compatibility)
630
+ */
631
+ get currentTab() {
632
+ return this.uiStateManager.getCurrentTab();
633
+ }
634
+
635
+ /**
636
+ * Get selected card (backward compatibility)
637
+ */
638
+ get selectedCard() {
639
+ return this.uiStateManager.getSelectedCard();
640
+ }
641
+
642
+ /**
643
+ * Get file operations (backward compatibility)
644
+ */
645
+ get fileOperations() {
646
+ return this.fileToolTracker.getFileOperations();
647
+ }
648
+
649
+ /**
650
+ * Get tool calls (backward compatibility)
651
+ */
652
+ get toolCalls() {
653
+ return this.fileToolTracker.getToolCalls();
654
+ }
655
+
656
+
657
+ /**
658
+ * Get tab navigation state (backward compatibility)
659
+ */
660
+ get tabNavigation() {
661
+ return this.uiStateManager ? this.uiStateManager.tabNavigation : null;
662
+ }
663
+ }
664
+
665
+ // Global functions for backward compatibility
666
+ window.clearEvents = function() {
667
+ if (window.dashboard) {
668
+ window.dashboard.clearEvents();
669
+ }
670
+ };
671
+
672
+ window.exportEvents = function() {
673
+ if (window.dashboard) {
674
+ window.dashboard.exportEvents();
675
+ }
676
+ };
677
+
678
+ window.clearSelection = function() {
679
+ if (window.dashboard) {
680
+ window.dashboard.clearSelection();
681
+ }
682
+ };
683
+
684
+ window.switchTab = function(tabName) {
685
+ if (window.dashboard) {
686
+ window.dashboard.switchTab(tabName);
687
+ }
688
+ };
689
+
690
+ // File Viewer Modal Functions - REMOVED DUPLICATE (keeping the one at line 1553)
691
+ window.showFileViewerModal = function(filePath, workingDir) {
692
+ // Use the dashboard's current working directory if not provided
693
+ if (!workingDir && window.dashboard && window.dashboard.currentWorkingDir) {
694
+ workingDir = window.dashboard.currentWorkingDir;
695
+ }
696
+
697
+ // Create modal if it doesn't exist
698
+ let modal = document.getElementById('file-viewer-modal');
699
+ if (!modal) {
700
+ modal = createFileViewerModal();
701
+ document.body.appendChild(modal);
702
+ }
703
+
704
+ // Update modal content
705
+ updateFileViewerModal(modal, filePath, workingDir);
706
+
707
+ // Show the modal as flex container
708
+ modal.style.display = 'flex';
709
+ document.body.style.overflow = 'hidden'; // Prevent background scrolling
710
+ };
711
+
712
+ window.hideFileViewerModal = function() {
713
+ const modal = document.getElementById('file-viewer-modal');
714
+ if (modal) {
715
+ modal.style.display = 'none';
716
+ document.body.style.overflow = ''; // Restore background scrolling
717
+ }
718
+ };
719
+
720
+ window.copyFileContent = function() {
721
+ const modal = document.getElementById('file-viewer-modal');
722
+ if (!modal) return;
723
+
724
+ const codeElement = modal.querySelector('.file-content-code');
725
+ if (!codeElement) return;
726
+
727
+ const text = codeElement.textContent;
728
+
729
+ if (navigator.clipboard && navigator.clipboard.writeText) {
730
+ navigator.clipboard.writeText(text).then(() => {
731
+ // Show brief feedback
732
+ const button = modal.querySelector('.file-content-copy');
733
+ const originalText = button.textContent;
734
+ button.textContent = '✅ Copied!';
735
+ setTimeout(() => {
736
+ button.textContent = originalText;
737
+ }, 2000);
738
+ }).catch(err => {
739
+ console.error('Failed to copy text:', err);
740
+ });
741
+ } else {
742
+ // Fallback for older browsers
743
+ const textarea = document.createElement('textarea');
744
+ textarea.value = text;
745
+ document.body.appendChild(textarea);
746
+ textarea.select();
747
+ document.execCommand('copy');
748
+ document.body.removeChild(textarea);
749
+
750
+ const button = modal.querySelector('.file-content-copy');
751
+ const originalText = button.textContent;
752
+ button.textContent = '✅ Copied!';
753
+ setTimeout(() => {
754
+ button.textContent = originalText;
755
+ }, 2000);
756
+ }
757
+ };
758
+
759
+ function createFileViewerModal() {
760
+ const modal = document.createElement('div');
761
+ modal.id = 'file-viewer-modal';
762
+ modal.className = 'modal file-viewer-modal';
763
+
764
+ modal.innerHTML = `
765
+ <div class="modal-content file-viewer-content">
766
+ <div class="file-viewer-header">
767
+ <h2 class="file-viewer-title">
768
+ <span class="file-viewer-icon">📄</span>
769
+ <span class="file-viewer-title-text">File Viewer</span>
770
+ </h2>
771
+ <div class="file-viewer-meta">
772
+ <span class="file-viewer-file-path"></span>
773
+ <span class="file-viewer-file-size"></span>
774
+ </div>
775
+ <button class="file-viewer-close" onclick="hideFileViewerModal()">
776
+ <span>&times;</span>
777
+ </button>
778
+ </div>
779
+ <div class="file-viewer-body">
780
+ <div class="file-viewer-loading">
781
+ <div class="loading-spinner"></div>
782
+ <span>Loading file content...</span>
783
+ </div>
784
+ <div class="file-viewer-error" style="display: none;">
785
+ <div class="error-icon">⚠️</div>
786
+ <div class="error-message"></div>
787
+ <div class="error-suggestions"></div>
788
+ </div>
789
+ <div class="file-viewer-content-area" style="display: none;">
790
+ <div class="file-viewer-toolbar">
791
+ <div class="file-viewer-info">
792
+ <span class="file-extension"></span>
793
+ <span class="file-encoding"></span>
794
+ </div>
795
+ <div class="file-viewer-actions">
796
+ <button class="file-content-copy" onclick="copyFileContent()">
797
+ 📋 Copy
798
+ </button>
799
+ </div>
800
+ </div>
801
+ <div class="file-viewer-scroll-wrapper">
802
+ <pre class="file-content-display"><code class="file-content-code"></code></pre>
803
+ </div>
804
+ </div>
805
+ </div>
806
+ </div>
807
+ `;
808
+
809
+ // Close modal when clicking outside
810
+ modal.addEventListener('click', (e) => {
811
+ if (e.target === modal) {
812
+ hideFileViewerModal();
813
+ }
814
+ });
815
+
816
+ // Close modal with Escape key
817
+ document.addEventListener('keydown', (e) => {
818
+ if (e.key === 'Escape' && modal.style.display === 'flex') {
819
+ hideFileViewerModal();
820
+ }
821
+ });
822
+
823
+ return modal;
824
+ }
825
+
826
+ async function updateFileViewerModal(modal, filePath, workingDir) {
827
+ // Update header info
828
+ const filePathElement = modal.querySelector('.file-viewer-file-path');
829
+ const fileSizeElement = modal.querySelector('.file-viewer-file-size');
830
+
831
+ filePathElement.textContent = filePath;
832
+ fileSizeElement.textContent = '';
833
+
834
+ // Show loading state
835
+ modal.querySelector('.file-viewer-loading').style.display = 'flex';
836
+ modal.querySelector('.file-viewer-error').style.display = 'none';
837
+ modal.querySelector('.file-viewer-content-area').style.display = 'none';
838
+
839
+ try {
840
+ // Get the Socket.IO client
841
+ const socket = window.socket || window.dashboard?.socketClient?.socket;
842
+ if (!socket) {
843
+ throw new Error('No socket connection available');
844
+ }
845
+
846
+ // Set up one-time listener for file content response
847
+ const responsePromise = new Promise((resolve, reject) => {
848
+ const responseHandler = (data) => {
849
+ if (data.file_path === filePath) {
850
+ socket.off('file_content_response', responseHandler);
851
+ if (data.success) {
852
+ resolve(data);
853
+ } else {
854
+ reject(new Error(data.error || 'Failed to read file'));
855
+ }
856
+ }
857
+ };
858
+
859
+ socket.on('file_content_response', responseHandler);
860
+
861
+ // Timeout after 10 seconds
862
+ setTimeout(() => {
863
+ socket.off('file_content_response', responseHandler);
864
+ reject(new Error('Request timeout'));
865
+ }, 10000);
866
+ });
867
+
868
+ // Send file read request
869
+ socket.emit('read_file', {
870
+ file_path: filePath,
871
+ working_dir: workingDir
872
+ });
873
+
874
+ console.log('📄 File viewer request sent:', {
875
+ filePath,
876
+ workingDir
877
+ });
878
+
879
+ // Wait for response
880
+ const result = await responsePromise;
881
+ console.log('📦 File content received:', result);
882
+
883
+ // Hide loading
884
+ modal.querySelector('.file-viewer-loading').style.display = 'none';
885
+
886
+ // Show successful content
887
+ displayFileContent(modal, result);
888
+
889
+ } catch (error) {
890
+ console.error('❌ Failed to fetch file content:', error);
891
+
892
+ modal.querySelector('.file-viewer-loading').style.display = 'none';
893
+
894
+ // Create detailed error message
895
+ let errorMessage = error.message || 'Unknown error occurred';
896
+ let suggestions = [];
897
+
898
+ if (error.message.includes('No socket connection')) {
899
+ errorMessage = 'Failed to connect to the monitoring server';
900
+ suggestions = [
901
+ 'Check if the monitoring server is running',
902
+ 'Verify the socket connection in the dashboard',
903
+ 'Try refreshing the page and reconnecting'
904
+ ];
905
+ } else if (error.message.includes('timeout')) {
906
+ errorMessage = 'Request timed out';
907
+ suggestions = [
908
+ 'The file may be too large to load quickly',
909
+ 'Check your network connection',
910
+ 'Try again in a few moments'
911
+ ];
912
+ } else if (error.message.includes('File does not exist')) {
913
+ errorMessage = 'File not found';
914
+ suggestions = [
915
+ 'The file may have been moved or deleted',
916
+ 'Check the file path spelling',
917
+ 'Refresh the file list to see current files'
918
+ ];
919
+ } else if (error.message.includes('Access denied')) {
920
+ errorMessage = 'Access denied';
921
+ suggestions = [
922
+ 'The file is outside the allowed directories',
923
+ 'File access is restricted for security reasons'
924
+ ];
925
+ }
926
+
927
+ displayFileError(modal, {
928
+ error: errorMessage,
929
+ file_path: filePath,
930
+ working_dir: workingDir,
931
+ suggestions: suggestions
932
+ });
933
+ }
934
+ }
935
+
936
+ function displayFileContent(modal, result) {
937
+ console.log('📝 displayFileContent called with:', result);
938
+ const contentArea = modal.querySelector('.file-viewer-content-area');
939
+ const extensionElement = modal.querySelector('.file-extension');
940
+ const encodingElement = modal.querySelector('.file-encoding');
941
+ const fileSizeElement = modal.querySelector('.file-viewer-file-size');
942
+ const codeElement = modal.querySelector('.file-content-code');
943
+
944
+ // Update metadata
945
+ if (extensionElement) extensionElement.textContent = `Type: ${result.extension || 'unknown'}`;
946
+ if (encodingElement) encodingElement.textContent = `Encoding: ${result.encoding || 'unknown'}`;
947
+ if (fileSizeElement) fileSizeElement.textContent = `Size: ${formatFileSize(result.file_size)}`;
948
+
949
+ // Update content with basic syntax highlighting
950
+ if (codeElement && result.content) {
951
+ console.log('💡 Setting file content, length:', result.content.length);
952
+ codeElement.innerHTML = highlightCode(result.content, result.extension);
953
+
954
+ // Force scrolling to work by setting explicit heights
955
+ const wrapper = modal.querySelector('.file-viewer-scroll-wrapper');
956
+ if (wrapper) {
957
+ // Give it a moment for content to render
958
+ setTimeout(() => {
959
+ const modalContent = modal.querySelector('.modal-content');
960
+ const header = modal.querySelector('.file-viewer-header');
961
+ const toolbar = modal.querySelector('.file-viewer-toolbar');
962
+
963
+ const modalHeight = modalContent?.offsetHeight || 0;
964
+ const headerHeight = header?.offsetHeight || 0;
965
+ const toolbarHeight = toolbar?.offsetHeight || 0;
966
+
967
+ const availableHeight = modalHeight - headerHeight - toolbarHeight - 40; // 40px for padding
968
+
969
+ console.log('🎯 Setting file viewer scroll height:', {
970
+ modalHeight,
971
+ headerHeight,
972
+ toolbarHeight,
973
+ availableHeight
974
+ });
975
+
976
+ wrapper.style.maxHeight = `${availableHeight}px`;
977
+ wrapper.style.overflowY = 'auto';
978
+ }, 50);
979
+ }
980
+ } else {
981
+ console.warn('⚠️ Missing codeElement or file content');
982
+ }
983
+
984
+ // Show content area
985
+ if (contentArea) {
986
+ contentArea.style.display = 'block';
987
+ console.log('✅ File content area displayed');
988
+ }
989
+ }
990
+
991
+ function displayFileError(modal, result) {
992
+ const errorArea = modal.querySelector('.file-viewer-error');
993
+ const messageElement = modal.querySelector('.error-message');
994
+ const suggestionsElement = modal.querySelector('.error-suggestions');
995
+
996
+ let errorMessage = result.error || 'Unknown error occurred';
997
+
998
+ messageElement.innerHTML = `
999
+ <div class="error-main">${errorMessage}</div>
1000
+ ${result.file_path ? `<div class="error-file">File: ${result.file_path}</div>` : ''}
1001
+ ${result.working_dir ? `<div class="error-dir">Working directory: ${result.working_dir}</div>` : ''}
1002
+ `;
1003
+
1004
+ if (result.suggestions && result.suggestions.length > 0) {
1005
+ suggestionsElement.innerHTML = `
1006
+ <h4>Suggestions:</h4>
1007
+ <ul>
1008
+ ${result.suggestions.map(s => `<li>${s}</li>`).join('')}
1009
+ </ul>
1010
+ `;
1011
+ } else {
1012
+ suggestionsElement.innerHTML = '';
1013
+ }
1014
+
1015
+ console.log('📋 Displaying file viewer error:', {
1016
+ originalError: result.error,
1017
+ processedMessage: errorMessage,
1018
+ suggestions: result.suggestions
1019
+ });
1020
+
1021
+ errorArea.style.display = 'block';
1022
+ }
1023
+
1024
+ function highlightCode(code, extension) {
1025
+ /**
1026
+ * Apply basic syntax highlighting to code content
1027
+ * WHY: Provides basic highlighting for common file types to improve readability.
1028
+ * This is a simple implementation that can be enhanced with full syntax highlighting
1029
+ * libraries like highlight.js or Prism.js if needed.
1030
+ */
1031
+
1032
+ // Escape HTML entities first
1033
+ const escaped = code
1034
+ .replace(/&/g, '&amp;')
1035
+ .replace(/</g, '&lt;')
1036
+ .replace(/>/g, '&gt;');
1037
+
1038
+ // Basic highlighting based on file extension
1039
+ switch (extension) {
1040
+ case '.js':
1041
+ case '.jsx':
1042
+ case '.ts':
1043
+ case '.tsx':
1044
+ return highlightJavaScript(escaped);
1045
+ case '.py':
1046
+ return highlightPython(escaped);
1047
+ case '.json':
1048
+ return highlightJSON(escaped);
1049
+ case '.css':
1050
+ return highlightCSS(escaped);
1051
+ case '.html':
1052
+ case '.htm':
1053
+ return highlightHTML(escaped);
1054
+ case '.md':
1055
+ case '.markdown':
1056
+ return highlightMarkdown(escaped);
1057
+ default:
1058
+ // Return with line numbers for plain text
1059
+ return addLineNumbers(escaped);
1060
+ }
1061
+ }
1062
+
1063
+ function highlightJavaScript(code) {
1064
+ return addLineNumbers(code
1065
+ .replace(/\b(function|const|let|var|if|else|for|while|return|import|export|class|extends)\b/g, '<span class="keyword">$1</span>')
1066
+ .replace(/(\/\*[\s\S]*?\*\/|\/\/.*)/g, '<span class="comment">$1</span>')
1067
+ .replace(/('[^']*'|"[^"]*"|`[^`]*`)/g, '<span class="string">$1</span>')
1068
+ .replace(/\b(\d+)\b/g, '<span class="number">$1</span>'));
1069
+ }
1070
+
1071
+ function highlightPython(code) {
1072
+ return addLineNumbers(code
1073
+ .replace(/\b(def|class|if|elif|else|for|while|return|import|from|as|try|except|finally|with)\b/g, '<span class="keyword">$1</span>')
1074
+ .replace(/(#.*)/g, '<span class="comment">$1</span>')
1075
+ .replace(/('[^']*'|"[^"]*"|"""[\s\S]*?""")/g, '<span class="string">$1</span>')
1076
+ .replace(/\b(\d+)\b/g, '<span class="number">$1</span>'));
1077
+ }
1078
+
1079
+ function highlightJSON(code) {
1080
+ return addLineNumbers(code
1081
+ .replace(/("[\w\s]*")\s*:/g, '<span class="property">$1</span>:')
1082
+ .replace(/:\s*(".*?")/g, ': <span class="string">$1</span>')
1083
+ .replace(/:\s*(\d+)/g, ': <span class="number">$1</span>')
1084
+ .replace(/:\s*(true|false|null)/g, ': <span class="keyword">$1</span>'));
1085
+ }
1086
+
1087
+ function highlightCSS(code) {
1088
+ return addLineNumbers(code
1089
+ .replace(/([.#]?[\w-]+)\s*\{/g, '<span class="selector">$1</span> {')
1090
+ .replace(/([\w-]+)\s*:/g, '<span class="property">$1</span>:')
1091
+ .replace(/:\s*([^;]+);/g, ': <span class="value">$1</span>;')
1092
+ .replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="comment">$1</span>'));
1093
+ }
1094
+
1095
+ function highlightHTML(code) {
1096
+ return addLineNumbers(code
1097
+ .replace(/(&lt;\/?[\w-]+)/g, '<span class="tag">$1</span>')
1098
+ .replace(/([\w-]+)=(['"][^'"]*['"])/g, '<span class="attribute">$1</span>=<span class="string">$2</span>')
1099
+ .replace(/(&lt;!--[\s\S]*?--&gt;)/g, '<span class="comment">$1</span>'));
1100
+ }
1101
+
1102
+ function highlightMarkdown(code) {
1103
+ return addLineNumbers(code
1104
+ .replace(/^(#{1,6})\s+(.*)$/gm, '<span class="header">$1</span> <span class="header-text">$2</span>')
1105
+ .replace(/\*\*(.*?)\*\*/g, '<span class="bold">**$1**</span>')
1106
+ .replace(/\*(.*?)\*/g, '<span class="italic">*$1*</span>')
1107
+ .replace(/`([^`]+)`/g, '<span class="code">`$1`</span>')
1108
+ .replace(/^\s*[-*+]\s+(.*)$/gm, '<span class="list-marker">•</span> $1'));
1109
+ }
1110
+
1111
+ function addLineNumbers(code) {
1112
+ const lines = code.split('\n');
1113
+ return lines.map((line, index) =>
1114
+ `<span class="line-number">${String(index + 1).padStart(3, ' ')}</span> ${line || ' '}`
1115
+ ).join('\n');
1116
+ }
1117
+
1118
+ function formatFileSize(bytes) {
1119
+ if (!bytes) return '0 B';
1120
+ const k = 1024;
1121
+ const sizes = ['B', 'KB', 'MB', 'GB'];
1122
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1123
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1124
+ }
1125
+
1126
+ // Git Diff Modal Functions - restored from original dashboard
1127
+ window.showGitDiffModal = function(filePath, timestamp, workingDir) {
1128
+ // Use the dashboard's current working directory if not provided
1129
+ if (!workingDir && window.dashboard && window.dashboard.currentWorkingDir) {
1130
+ workingDir = window.dashboard.currentWorkingDir;
1131
+ }
1132
+
1133
+ // Create modal if it doesn't exist
1134
+ let modal = document.getElementById('git-diff-modal');
1135
+ if (!modal) {
1136
+ modal = createGitDiffModal();
1137
+ document.body.appendChild(modal);
1138
+ }
1139
+
1140
+ // Update modal content
1141
+ updateGitDiffModal(modal, filePath, timestamp, workingDir);
1142
+
1143
+ // Show the modal as flex container
1144
+ modal.style.display = 'flex';
1145
+ document.body.style.overflow = 'hidden'; // Prevent background scrolling
1146
+ };
1147
+
1148
+ window.hideGitDiffModal = function() {
1149
+ const modal = document.getElementById('git-diff-modal');
1150
+ if (modal) {
1151
+ modal.style.display = 'none';
1152
+ document.body.style.overflow = ''; // Restore background scrolling
1153
+ }
1154
+ };
1155
+
1156
+ window.copyGitDiff = function() {
1157
+ const modal = document.getElementById('git-diff-modal');
1158
+ if (!modal) return;
1159
+
1160
+ const codeElement = modal.querySelector('.git-diff-code');
1161
+ if (!codeElement) return;
1162
+
1163
+ const text = codeElement.textContent;
1164
+
1165
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1166
+ navigator.clipboard.writeText(text).then(() => {
1167
+ // Show brief feedback
1168
+ const button = modal.querySelector('.git-diff-copy');
1169
+ const originalText = button.textContent;
1170
+ button.textContent = '✅ Copied!';
1171
+ setTimeout(() => {
1172
+ button.textContent = originalText;
1173
+ }, 2000);
1174
+ }).catch(err => {
1175
+ console.error('Failed to copy text:', err);
1176
+ });
1177
+ } else {
1178
+ // Fallback for older browsers
1179
+ const textarea = document.createElement('textarea');
1180
+ textarea.value = text;
1181
+ document.body.appendChild(textarea);
1182
+ textarea.select();
1183
+ document.execCommand('copy');
1184
+ document.body.removeChild(textarea);
1185
+
1186
+ const button = modal.querySelector('.git-diff-copy');
1187
+ const originalText = button.textContent;
1188
+ button.textContent = '✅ Copied!';
1189
+ setTimeout(() => {
1190
+ button.textContent = originalText;
1191
+ }, 2000);
1192
+ }
1193
+ };
1194
+
1195
+ function createGitDiffModal() {
1196
+ const modal = document.createElement('div');
1197
+ modal.id = 'git-diff-modal';
1198
+ modal.className = 'modal git-diff-modal';
1199
+
1200
+ modal.innerHTML = `
1201
+ <div class="modal-content git-diff-content">
1202
+ <div class="git-diff-header">
1203
+ <h2 class="git-diff-title">
1204
+ <span class="git-diff-icon">📋</span>
1205
+ <span class="git-diff-title-text">Git Diff</span>
1206
+ </h2>
1207
+ <div class="git-diff-meta">
1208
+ <span class="git-diff-file-path"></span>
1209
+ <span class="git-diff-timestamp"></span>
1210
+ </div>
1211
+ <button class="git-diff-close" onclick="hideGitDiffModal()">
1212
+ <span>&times;</span>
1213
+ </button>
1214
+ </div>
1215
+ <div class="git-diff-body">
1216
+ <div class="git-diff-loading">
1217
+ <div class="loading-spinner"></div>
1218
+ <span>Loading git diff...</span>
1219
+ </div>
1220
+ <div class="git-diff-error" style="display: none;">
1221
+ <div class="error-icon">⚠️</div>
1222
+ <div class="error-message"></div>
1223
+ <div class="error-suggestions"></div>
1224
+ </div>
1225
+ <div class="git-diff-content-area" style="display: none;">
1226
+ <div class="git-diff-toolbar">
1227
+ <div class="git-diff-info">
1228
+ <span class="commit-hash"></span>
1229
+ <span class="diff-method"></span>
1230
+ </div>
1231
+ <div class="git-diff-actions">
1232
+ <button class="git-diff-copy" onclick="copyGitDiff()">
1233
+ 📋 Copy
1234
+ </button>
1235
+ </div>
1236
+ </div>
1237
+ <div class="git-diff-scroll-wrapper">
1238
+ <pre class="git-diff-display"><code class="git-diff-code"></code></pre>
1239
+ </div>
1240
+ </div>
1241
+ </div>
1242
+ </div>
1243
+ `;
1244
+
1245
+ // Close modal when clicking outside
1246
+ modal.addEventListener('click', (e) => {
1247
+ if (e.target === modal) {
1248
+ hideGitDiffModal();
1249
+ }
1250
+ });
1251
+
1252
+ // Close modal with Escape key
1253
+ document.addEventListener('keydown', (e) => {
1254
+ if (e.key === 'Escape' && modal.style.display === 'flex') {
1255
+ hideGitDiffModal();
1256
+ }
1257
+ });
1258
+
1259
+ return modal;
1260
+ }
1261
+
1262
+ async function updateGitDiffModal(modal, filePath, timestamp, workingDir) {
1263
+ // Update header info
1264
+ const filePathElement = modal.querySelector('.git-diff-file-path');
1265
+ const timestampElement = modal.querySelector('.git-diff-timestamp');
1266
+
1267
+ filePathElement.textContent = filePath;
1268
+ timestampElement.textContent = timestamp ? new Date(timestamp).toLocaleString() : 'Latest';
1269
+
1270
+ // Show loading state
1271
+ modal.querySelector('.git-diff-loading').style.display = 'flex';
1272
+ modal.querySelector('.git-diff-error').style.display = 'none';
1273
+ modal.querySelector('.git-diff-content-area').style.display = 'none';
1274
+
1275
+ try {
1276
+ // Get the Socket.IO server port with multiple fallbacks
1277
+ let port = 8765; // Default fallback
1278
+
1279
+ // Try to get port from socketClient first
1280
+ if (window.dashboard && window.dashboard.socketClient && window.dashboard.socketClient.port) {
1281
+ port = window.dashboard.socketClient.port;
1282
+ }
1283
+ // Fallback to port input field if socketClient port is not available
1284
+ else {
1285
+ const portInput = document.getElementById('port-input');
1286
+ if (portInput && portInput.value) {
1287
+ port = portInput.value;
1288
+ }
1289
+ }
1290
+
1291
+ // Build URL parameters
1292
+ const params = new URLSearchParams({
1293
+ file: filePath
1294
+ });
1295
+
1296
+ if (timestamp) {
1297
+ params.append('timestamp', timestamp);
1298
+ }
1299
+ if (workingDir) {
1300
+ params.append('working_dir', workingDir);
1301
+ }
1302
+
1303
+ const requestUrl = `http://localhost:${port}/api/git-diff?${params}`;
1304
+ console.log('🌐 Making git diff request to:', requestUrl);
1305
+ console.log('📋 Git diff request parameters:', {
1306
+ filePath,
1307
+ timestamp,
1308
+ workingDir,
1309
+ urlParams: params.toString()
1310
+ });
1311
+
1312
+ // Test server connectivity first
1313
+ try {
1314
+ const healthResponse = await fetch(`http://localhost:${port}/health`, {
1315
+ method: 'GET',
1316
+ headers: {
1317
+ 'Accept': 'application/json',
1318
+ 'Content-Type': 'application/json'
1319
+ },
1320
+ mode: 'cors'
1321
+ });
1322
+
1323
+ if (!healthResponse.ok) {
1324
+ throw new Error(`Server health check failed: ${healthResponse.status} ${healthResponse.statusText}`);
1325
+ }
1326
+
1327
+ console.log('✅ Server health check passed');
1328
+ } catch (healthError) {
1329
+ throw new Error(`Cannot reach server at localhost:${port}. Health check failed: ${healthError.message}`);
1330
+ }
1331
+
1332
+ // Make the actual git diff request
1333
+ const response = await fetch(requestUrl, {
1334
+ method: 'GET',
1335
+ headers: {
1336
+ 'Accept': 'application/json',
1337
+ 'Content-Type': 'application/json'
1338
+ },
1339
+ mode: 'cors'
1340
+ });
1341
+
1342
+ if (!response.ok) {
1343
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1344
+ }
1345
+
1346
+ const result = await response.json();
1347
+ console.log('📦 Git diff response:', result);
1348
+
1349
+ // Hide loading
1350
+ modal.querySelector('.git-diff-loading').style.display = 'none';
1351
+
1352
+ if (result.success) {
1353
+ console.log('📊 Displaying successful git diff');
1354
+ // Show successful diff
1355
+ displayGitDiff(modal, result);
1356
+ } else {
1357
+ console.log('⚠️ Displaying git diff error:', result);
1358
+ // Show error
1359
+ displayGitDiffError(modal, result);
1360
+ }
1361
+
1362
+ } catch (error) {
1363
+ console.error('❌ Failed to fetch git diff:', error);
1364
+ console.error('Error details:', {
1365
+ name: error.name,
1366
+ message: error.message,
1367
+ stack: error.stack,
1368
+ filePath,
1369
+ timestamp,
1370
+ workingDir
1371
+ });
1372
+
1373
+ modal.querySelector('.git-diff-loading').style.display = 'none';
1374
+
1375
+ // Create detailed error message based on error type
1376
+ let errorMessage = `Network error: ${error.message}`;
1377
+ let suggestions = [];
1378
+
1379
+ if (error.message.includes('Failed to fetch')) {
1380
+ errorMessage = 'Failed to connect to the monitoring server';
1381
+ suggestions = [
1382
+ 'Check if the monitoring server is running on port 8765',
1383
+ 'Verify the port configuration in the dashboard',
1384
+ 'Check browser console for CORS or network errors',
1385
+ 'Try refreshing the page and reconnecting'
1386
+ ];
1387
+ } else if (error.message.includes('health check failed')) {
1388
+ errorMessage = error.message;
1389
+ suggestions = [
1390
+ 'The server may be starting up - try again in a few seconds',
1391
+ 'Check if another process is using port 8765',
1392
+ 'Restart the claude-mpm monitoring server'
1393
+ ];
1394
+ } else if (error.message.includes('HTTP')) {
1395
+ errorMessage = `Server error: ${error.message}`;
1396
+ suggestions = [
1397
+ 'The server encountered an internal error',
1398
+ 'Check the server logs for more details',
1399
+ 'Try with a different file or working directory'
1400
+ ];
1401
+ }
1402
+
1403
+ displayGitDiffError(modal, {
1404
+ error: errorMessage,
1405
+ file_path: filePath,
1406
+ working_dir: workingDir,
1407
+ suggestions: suggestions,
1408
+ debug_info: {
1409
+ error_type: error.name,
1410
+ original_message: error.message,
1411
+ port: window.dashboard?.socketClient?.port || document.getElementById('port-input')?.value || '8765',
1412
+ timestamp: new Date().toISOString()
1413
+ }
1414
+ });
1415
+ }
1416
+ }
1417
+
1418
+ function highlightGitDiff(diffText) {
1419
+ /**
1420
+ * Apply basic syntax highlighting to git diff output
1421
+ * WHY: Git diffs have a standard format that can be highlighted for better readability:
1422
+ * - Lines starting with '+' are additions (green)
1423
+ * - Lines starting with '-' are deletions (red)
1424
+ * - Lines starting with '@@' are context headers (blue)
1425
+ * - File headers and metadata get special formatting
1426
+ */
1427
+ return diffText
1428
+ .split('\n')
1429
+ .map(line => {
1430
+ // Escape HTML entities
1431
+ const escaped = line
1432
+ .replace(/&/g, '&amp;')
1433
+ .replace(/</g, '&lt;')
1434
+ .replace(/>/g, '&gt;');
1435
+
1436
+ // Apply diff highlighting
1437
+ if (line.startsWith('+++') || line.startsWith('---')) {
1438
+ return `<span class="diff-header">${escaped}</span>`;
1439
+ } else if (line.startsWith('@@')) {
1440
+ return `<span class="diff-meta">${escaped}</span>`;
1441
+ } else if (line.startsWith('+')) {
1442
+ return `<span class="diff-addition">${escaped}</span>`;
1443
+ } else if (line.startsWith('-')) {
1444
+ return `<span class="diff-deletion">${escaped}</span>`;
1445
+ } else if (line.startsWith('commit ') || line.startsWith('Author:') || line.startsWith('Date:')) {
1446
+ return `<span class="diff-header">${escaped}</span>`;
1447
+ } else {
1448
+ return `<span class="diff-context">${escaped}</span>`;
1449
+ }
1450
+ })
1451
+ .join('\n');
1452
+ }
1453
+
1454
+ function displayGitDiff(modal, result) {
1455
+ console.log('📝 displayGitDiff called with:', result);
1456
+ const contentArea = modal.querySelector('.git-diff-content-area');
1457
+ const commitHashElement = modal.querySelector('.commit-hash');
1458
+ const methodElement = modal.querySelector('.diff-method');
1459
+ const codeElement = modal.querySelector('.git-diff-code');
1460
+
1461
+ console.log('🔍 Elements found:', {
1462
+ contentArea: !!contentArea,
1463
+ commitHashElement: !!commitHashElement,
1464
+ methodElement: !!methodElement,
1465
+ codeElement: !!codeElement
1466
+ });
1467
+
1468
+ // Update metadata
1469
+ if (commitHashElement) commitHashElement.textContent = `Commit: ${result.commit_hash}`;
1470
+ if (methodElement) methodElement.textContent = `Method: ${result.method}`;
1471
+
1472
+ // Update diff content with basic syntax highlighting
1473
+ if (codeElement && result.diff) {
1474
+ console.log('💡 Setting diff content, length:', result.diff.length);
1475
+ codeElement.innerHTML = highlightGitDiff(result.diff);
1476
+
1477
+ // Force scrolling to work by setting explicit heights
1478
+ const wrapper = modal.querySelector('.git-diff-scroll-wrapper');
1479
+ if (wrapper) {
1480
+ // Give it a moment for content to render
1481
+ setTimeout(() => {
1482
+ const modalContent = modal.querySelector('.modal-content');
1483
+ const header = modal.querySelector('.git-diff-header');
1484
+ const toolbar = modal.querySelector('.git-diff-toolbar');
1485
+
1486
+ const modalHeight = modalContent?.offsetHeight || 0;
1487
+ const headerHeight = header?.offsetHeight || 0;
1488
+ const toolbarHeight = toolbar?.offsetHeight || 0;
1489
+
1490
+ const availableHeight = modalHeight - headerHeight - toolbarHeight - 40; // 40px for padding
1491
+
1492
+ console.log('🎯 Setting explicit scroll height:', {
1493
+ modalHeight,
1494
+ headerHeight,
1495
+ toolbarHeight,
1496
+ availableHeight
1497
+ });
1498
+
1499
+ wrapper.style.maxHeight = `${availableHeight}px`;
1500
+ wrapper.style.overflowY = 'auto';
1501
+ }, 50);
1502
+ }
1503
+ } else {
1504
+ console.warn('⚠️ Missing codeElement or diff data');
1505
+ }
1506
+
1507
+ // Show content area
1508
+ if (contentArea) {
1509
+ contentArea.style.display = 'block';
1510
+ console.log('✅ Content area displayed');
1511
+ }
1512
+ }
1513
+
1514
+ function displayGitDiffError(modal, result) {
1515
+ const errorArea = modal.querySelector('.git-diff-error');
1516
+ const messageElement = modal.querySelector('.error-message');
1517
+ const suggestionsElement = modal.querySelector('.error-suggestions');
1518
+
1519
+ // Create more user-friendly error messages
1520
+ let errorMessage = result.error || 'Unknown error occurred';
1521
+ let isUntracked = false;
1522
+
1523
+ if (errorMessage.includes('not tracked by git')) {
1524
+ errorMessage = '📝 This file is not tracked by git yet';
1525
+ isUntracked = true;
1526
+ } else if (errorMessage.includes('No git history found')) {
1527
+ errorMessage = '📋 No git history available for this file';
1528
+ }
1529
+
1530
+ messageElement.innerHTML = `
1531
+ <div class="error-main">${errorMessage}</div>
1532
+ ${result.file_path ? `<div class="error-file">File: ${result.file_path}</div>` : ''}
1533
+ ${result.working_dir ? `<div class="error-dir">Working directory: ${result.working_dir}</div>` : ''}
1534
+ `;
1535
+
1536
+ if (result.suggestions && result.suggestions.length > 0) {
1537
+ const suggestionTitle = isUntracked ? 'How to track this file:' : 'Suggestions:';
1538
+ suggestionsElement.innerHTML = `
1539
+ <h4>${suggestionTitle}</h4>
1540
+ <ul>
1541
+ ${result.suggestions.map(s => `<li>${s}</li>`).join('')}
1542
+ </ul>
1543
+ `;
1544
+ } else {
1545
+ suggestionsElement.innerHTML = '';
1546
+ }
1547
+
1548
+ console.log('📋 Displaying git diff error:', {
1549
+ originalError: result.error,
1550
+ processedMessage: errorMessage,
1551
+ isUntracked,
1552
+ suggestions: result.suggestions
1553
+ });
1554
+
1555
+ errorArea.style.display = 'block';
1556
+ }
1557
+
1558
+ // File Viewer Modal Functions
1559
+ window.showFileViewerModal = function(filePath) {
1560
+ // Use the dashboard's current working directory
1561
+ let workingDir = '';
1562
+ if (window.dashboard && window.dashboard.currentWorkingDir) {
1563
+ workingDir = window.dashboard.currentWorkingDir;
1564
+ }
1565
+
1566
+ // Create modal if it doesn't exist
1567
+ let modal = document.getElementById('file-viewer-modal');
1568
+ if (!modal) {
1569
+ modal = createFileViewerModal();
1570
+ document.body.appendChild(modal);
1571
+ }
1572
+
1573
+ // Update modal content
1574
+ updateFileViewerModal(modal, filePath, workingDir);
1575
+
1576
+ // Show the modal as flex container
1577
+ modal.style.display = 'flex';
1578
+ document.body.style.overflow = 'hidden'; // Prevent background scrolling
1579
+ };
1580
+
1581
+ window.hideFileViewerModal = function() {
1582
+ const modal = document.getElementById('file-viewer-modal');
1583
+ if (modal) {
1584
+ modal.style.display = 'none';
1585
+ document.body.style.overflow = ''; // Restore background scrolling
1586
+ }
1587
+ };
1588
+
1589
+ window.copyFileContent = function() {
1590
+ const modal = document.getElementById('file-viewer-modal');
1591
+ if (!modal) return;
1592
+
1593
+ const codeElement = modal.querySelector('.file-content-code');
1594
+ if (!codeElement) return;
1595
+
1596
+ const text = codeElement.textContent;
1597
+
1598
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1599
+ navigator.clipboard.writeText(text).then(() => {
1600
+ // Show brief feedback
1601
+ const button = modal.querySelector('.file-content-copy');
1602
+ const originalText = button.textContent;
1603
+ button.textContent = '✅ Copied!';
1604
+ setTimeout(() => {
1605
+ button.textContent = originalText;
1606
+ }, 2000);
1607
+ }).catch(err => {
1608
+ console.error('Failed to copy text:', err);
1609
+ });
1610
+ } else {
1611
+ // Fallback for older browsers
1612
+ const textarea = document.createElement('textarea');
1613
+ textarea.value = text;
1614
+ document.body.appendChild(textarea);
1615
+ textarea.select();
1616
+ document.execCommand('copy');
1617
+ document.body.removeChild(textarea);
1618
+
1619
+ const button = modal.querySelector('.file-content-copy');
1620
+ const originalText = button.textContent;
1621
+ button.textContent = '✅ Copied!';
1622
+ setTimeout(() => {
1623
+ button.textContent = originalText;
1624
+ }, 2000);
1625
+ }
1626
+ };
1627
+
1628
+ function createFileViewerModal() {
1629
+ const modal = document.createElement('div');
1630
+ modal.id = 'file-viewer-modal';
1631
+ modal.className = 'modal file-viewer-modal';
1632
+
1633
+ modal.innerHTML = `
1634
+ <div class="modal-content file-viewer-content">
1635
+ <div class="file-viewer-header">
1636
+ <h2 class="file-viewer-title">
1637
+ <span class="file-viewer-icon">👁️</span>
1638
+ <span class="file-viewer-title-text">File Viewer</span>
1639
+ </h2>
1640
+ <div class="file-viewer-meta">
1641
+ <span class="file-viewer-file-path"></span>
1642
+ <span class="file-viewer-file-size"></span>
1643
+ </div>
1644
+ <button class="file-viewer-close" onclick="hideFileViewerModal()">
1645
+ <span>&times;</span>
1646
+ </button>
1647
+ </div>
1648
+ <div class="file-viewer-body">
1649
+ <div class="file-viewer-loading">
1650
+ <div class="loading-spinner"></div>
1651
+ <span>Loading file contents...</span>
1652
+ </div>
1653
+ <div class="file-viewer-error" style="display: none;">
1654
+ <div class="error-icon">⚠️</div>
1655
+ <div class="error-message"></div>
1656
+ <div class="error-suggestions"></div>
1657
+ </div>
1658
+ <div class="file-viewer-content-area" style="display: none;">
1659
+ <div class="file-viewer-toolbar">
1660
+ <div class="file-viewer-info">
1661
+ <span class="file-extension"></span>
1662
+ <span class="file-encoding">UTF-8</span>
1663
+ </div>
1664
+ <div class="file-viewer-actions">
1665
+ <button class="file-content-copy" onclick="copyFileContent()">
1666
+ 📋 Copy
1667
+ </button>
1668
+ </div>
1669
+ </div>
1670
+ <div class="file-viewer-scroll-wrapper">
1671
+ <pre class="file-content-display line-numbers"><code class="file-content-code"></code></pre>
1672
+ </div>
1673
+ </div>
1674
+ </div>
1675
+ </div>
1676
+ `;
1677
+
1678
+ // Close modal when clicking outside
1679
+ modal.addEventListener('click', (e) => {
1680
+ if (e.target === modal) {
1681
+ hideFileViewerModal();
1682
+ }
1683
+ });
1684
+
1685
+ // Close modal with Escape key
1686
+ document.addEventListener('keydown', (e) => {
1687
+ if (e.key === 'Escape' && modal.style.display === 'flex') {
1688
+ hideFileViewerModal();
1689
+ }
1690
+ });
1691
+
1692
+ return modal;
1693
+ }
1694
+
1695
+ async function updateFileViewerModal(modal, filePath, workingDir) {
1696
+ // Update header info
1697
+ const filePathElement = modal.querySelector('.file-viewer-file-path');
1698
+ const fileSizeElement = modal.querySelector('.file-viewer-file-size');
1699
+ const fileExtensionElement = modal.querySelector('.file-extension');
1700
+
1701
+ filePathElement.textContent = filePath;
1702
+
1703
+ // Extract and display file extension
1704
+ const extension = filePath.split('.').pop() || 'txt';
1705
+ fileExtensionElement.textContent = `Type: ${extension.toUpperCase()}`;
1706
+
1707
+ // Show loading state
1708
+ modal.querySelector('.file-viewer-loading').style.display = 'flex';
1709
+ modal.querySelector('.file-viewer-error').style.display = 'none';
1710
+ modal.querySelector('.file-viewer-content-area').style.display = 'none';
1711
+
1712
+ try {
1713
+ // Get the Socket.IO server port with multiple fallbacks
1714
+ let port = 8765; // Default fallback
1715
+
1716
+ // Try to get port from socketClient first
1717
+ if (window.dashboard && window.dashboard.socketClient && window.dashboard.socketClient.port) {
1718
+ port = window.dashboard.socketClient.port;
1719
+ }
1720
+ // Fallback to port input field if socketClient port is not available
1721
+ else if (document.getElementById('port-input')) {
1722
+ const portInput = document.getElementById('port-input');
1723
+ if (portInput.value && !isNaN(portInput.value)) {
1724
+ port = parseInt(portInput.value);
1725
+ }
1726
+ }
1727
+
1728
+ // Construct API request URL for file content
1729
+ const params = new URLSearchParams();
1730
+ params.append('file_path', filePath);
1731
+ if (workingDir) {
1732
+ params.append('working_dir', workingDir);
1733
+ }
1734
+
1735
+ const requestUrl = `http://localhost:${port}/api/file-content?${params}`;
1736
+ console.log('🌐 Making file content request to:', requestUrl);
1737
+ console.log('📄 File content request parameters:', {
1738
+ filePath,
1739
+ workingDir,
1740
+ urlParams: params.toString()
1741
+ });
1742
+
1743
+ // Test server connectivity first
1744
+ try {
1745
+ const healthResponse = await fetch(`http://localhost:${port}/health`, {
1746
+ method: 'GET',
1747
+ mode: 'cors',
1748
+ timeout: 5000
1749
+ });
1750
+
1751
+ if (!healthResponse.ok) {
1752
+ throw new Error(`Health check failed: ${healthResponse.status}`);
1753
+ }
1754
+
1755
+ console.log('✅ Server health check passed');
1756
+ } catch (healthError) {
1757
+ console.warn('⚠️ Server health check failed:', healthError);
1758
+ throw new Error(`Unable to connect to monitoring server on port ${port}. Please ensure the server is running.`);
1759
+ }
1760
+
1761
+ // Make the actual file content request
1762
+ const response = await fetch(requestUrl, {
1763
+ method: 'GET',
1764
+ headers: {
1765
+ 'Accept': 'application/json',
1766
+ 'Content-Type': 'application/json'
1767
+ },
1768
+ mode: 'cors'
1769
+ });
1770
+
1771
+ if (!response.ok) {
1772
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1773
+ }
1774
+
1775
+ const result = await response.json();
1776
+ console.log('📦 File content received:', result);
1777
+
1778
+ // Hide loading
1779
+ modal.querySelector('.file-viewer-loading').style.display = 'none';
1780
+
1781
+ if (result.success) {
1782
+ console.log('📊 Displaying file content');
1783
+ // Show file content
1784
+ displayFileContent(modal, result, extension);
1785
+ } else {
1786
+ console.log('⚠️ Displaying file content error:', result);
1787
+ // Show error
1788
+ displayFileContentError(modal, result);
1789
+ }
1790
+
1791
+ } catch (error) {
1792
+ console.error('❌ Failed to fetch file content:', error);
1793
+
1794
+ modal.querySelector('.file-viewer-loading').style.display = 'none';
1795
+
1796
+ // Create detailed error message based on error type
1797
+ let errorMessage = `Network error: ${error.message}`;
1798
+ let suggestions = [];
1799
+
1800
+ if (error.message.includes('Failed to fetch')) {
1801
+ errorMessage = 'Failed to connect to the monitoring server';
1802
+ suggestions = [
1803
+ 'Check if the monitoring server is running on port 8765',
1804
+ 'Verify the port configuration in the dashboard',
1805
+ 'Ensure CORS is enabled on the server'
1806
+ ];
1807
+ } else if (error.message.includes('Health check failed')) {
1808
+ errorMessage = 'Unable to connect to monitoring server';
1809
+ suggestions = [
1810
+ 'The server may be starting up - try again in a few seconds',
1811
+ 'Check if another process is using the port',
1812
+ 'Restart the claude-mpm monitoring server'
1813
+ ];
1814
+ } else if (error.message.includes('HTTP 404')) {
1815
+ errorMessage = 'File content API endpoint not found';
1816
+ suggestions = [
1817
+ 'The server may be running an older version',
1818
+ 'Try restarting the monitoring server',
1819
+ 'Check server logs for errors'
1820
+ ];
1821
+ } else if (error.message.includes('timeout')) {
1822
+ errorMessage = 'Request timed out while fetching file content';
1823
+ suggestions = [
1824
+ 'File may be too large or server is slow',
1825
+ 'Try again in a moment',
1826
+ 'Check server logs for performance issues'
1827
+ ];
1828
+ }
1829
+
1830
+ displayFileContentError(modal, {
1831
+ success: false,
1832
+ error: errorMessage,
1833
+ suggestions: suggestions
1834
+ });
1835
+ }
1836
+ }
1837
+
1838
+ function displayFileContent(modal, result, extension) {
1839
+ console.log('📝 displayFileContent called with:', result);
1840
+ const contentArea = modal.querySelector('.file-viewer-content-area');
1841
+ const fileSizeElement = modal.querySelector('.file-viewer-file-size');
1842
+ const codeElement = modal.querySelector('.file-content-code');
1843
+
1844
+ console.log('🔍 File content elements found:', {
1845
+ contentArea: !!contentArea,
1846
+ fileSizeElement: !!fileSizeElement,
1847
+ codeElement: !!codeElement
1848
+ });
1849
+
1850
+ // Update file size
1851
+ if (fileSizeElement && result.file_size) {
1852
+ const sizeInBytes = result.file_size;
1853
+ const sizeFormatted = sizeInBytes < 1024
1854
+ ? `${sizeInBytes} bytes`
1855
+ : sizeInBytes < 1024 * 1024
1856
+ ? `${(sizeInBytes / 1024).toFixed(1)} KB`
1857
+ : `${(sizeInBytes / (1024 * 1024)).toFixed(1)} MB`;
1858
+ fileSizeElement.textContent = `Size: ${sizeFormatted}`;
1859
+ }
1860
+
1861
+ // Set up content with syntax highlighting
1862
+ if (codeElement && result.content) {
1863
+ // Determine language for Prism.js
1864
+ const languageMap = {
1865
+ 'js': 'javascript',
1866
+ 'jsx': 'javascript',
1867
+ 'ts': 'typescript',
1868
+ 'tsx': 'typescript',
1869
+ 'py': 'python',
1870
+ 'rb': 'ruby',
1871
+ 'php': 'php',
1872
+ 'html': 'html',
1873
+ 'htm': 'html',
1874
+ 'css': 'css',
1875
+ 'scss': 'scss',
1876
+ 'sass': 'sass',
1877
+ 'json': 'json',
1878
+ 'xml': 'xml',
1879
+ 'yaml': 'yaml',
1880
+ 'yml': 'yaml',
1881
+ 'md': 'markdown',
1882
+ 'sh': 'bash',
1883
+ 'bash': 'bash',
1884
+ 'zsh': 'bash',
1885
+ 'sql': 'sql',
1886
+ 'go': 'go',
1887
+ 'java': 'java',
1888
+ 'c': 'c',
1889
+ 'cpp': 'cpp',
1890
+ 'h': 'c',
1891
+ 'hpp': 'cpp'
1892
+ };
1893
+
1894
+ const language = languageMap[extension.toLowerCase()] || 'text';
1895
+
1896
+ // Set up code element with Prism.js classes
1897
+ codeElement.className = `file-content-code language-${language}`;
1898
+ codeElement.textContent = result.content;
1899
+
1900
+ // Apply syntax highlighting with Prism.js if available
1901
+ if (window.Prism) {
1902
+ // Add line numbers
1903
+ codeElement.parentElement.className = 'file-content-display line-numbers';
1904
+
1905
+ // Apply highlighting
1906
+ window.Prism.highlightElement(codeElement);
1907
+ }
1908
+
1909
+ contentArea.style.display = 'flex';
1910
+
1911
+ console.log('✅ File content displayed successfully');
1912
+ } else {
1913
+ console.error('❌ Missing code element or content');
1914
+ }
1915
+ }
1916
+
1917
+ function displayFileContentError(modal, result) {
1918
+ const errorArea = modal.querySelector('.file-viewer-error');
1919
+ const messageElement = modal.querySelector('.error-message');
1920
+ const suggestionsElement = modal.querySelector('.error-suggestions');
1921
+
1922
+ // Create user-friendly error messages
1923
+ let errorMessage = result.error || 'Unknown error occurred';
1924
+
1925
+ if (errorMessage.includes('not found')) {
1926
+ errorMessage = '📁 File not found or not accessible';
1927
+ } else if (errorMessage.includes('permission')) {
1928
+ errorMessage = '🔒 Permission denied accessing this file';
1929
+ } else if (errorMessage.includes('too large')) {
1930
+ errorMessage = '📏 File is too large to display';
1931
+ } else if (!errorMessage.includes('📁') && !errorMessage.includes('🔒') && !errorMessage.includes('📏')) {
1932
+ errorMessage = `⚠️ ${errorMessage}`;
1933
+ }
1934
+
1935
+ messageElement.textContent = errorMessage;
1936
+
1937
+ // Add suggestions if available
1938
+ if (result.suggestions && result.suggestions.length > 0) {
1939
+ suggestionsElement.innerHTML = `
1940
+ <h4>Suggestions:</h4>
1941
+ <ul>
1942
+ ${result.suggestions.map(suggestion => `<li>${suggestion}</li>`).join('')}
1943
+ </ul>
1944
+ `;
1945
+ } else {
1946
+ suggestionsElement.innerHTML = `
1947
+ <h4>Try:</h4>
1948
+ <ul>
1949
+ <li>Check if the file exists and is readable</li>
1950
+ <li>Verify file permissions</li>
1951
+ <li>Ensure the monitoring server has access to this file</li>
1952
+ </ul>
1953
+ `;
1954
+ }
1955
+
1956
+ console.log('📋 Displaying file content error:', {
1957
+ originalError: result.error,
1958
+ processedMessage: errorMessage,
1959
+ suggestions: result.suggestions
1960
+ });
1961
+
1962
+ errorArea.style.display = 'block';
1963
+ }
1964
+
1965
+ // Global window functions for backward compatibility
1966
+ window.showAgentInstanceDetails = function(instanceId) {
1967
+ if (window.dashboard && typeof window.dashboard.showAgentInstanceDetails === 'function') {
1968
+ window.dashboard.showAgentInstanceDetails(instanceId);
1969
+ } else {
1970
+ console.error('Dashboard not available or method not found');
1971
+ }
1972
+ };
1973
+
1974
+ // Initialize dashboard when page loads
1975
+ document.addEventListener('DOMContentLoaded', function() {
1976
+ window.dashboard = new Dashboard();
1977
+ console.log('Dashboard loaded and initialized');
1978
+ });