claude-mpm 3.4.13__py3-none-any.whl → 3.4.16__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 (27) hide show
  1. claude_mpm/dashboard/index.html +13 -0
  2. claude_mpm/dashboard/static/css/dashboard.css +2722 -0
  3. claude_mpm/dashboard/static/js/components/agent-inference.js +619 -0
  4. claude_mpm/dashboard/static/js/components/event-processor.js +641 -0
  5. claude_mpm/dashboard/static/js/components/event-viewer.js +914 -0
  6. claude_mpm/dashboard/static/js/components/export-manager.js +362 -0
  7. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +611 -0
  8. claude_mpm/dashboard/static/js/components/hud-library-loader.js +211 -0
  9. claude_mpm/dashboard/static/js/components/hud-manager.js +671 -0
  10. claude_mpm/dashboard/static/js/components/hud-visualizer.js +1718 -0
  11. claude_mpm/dashboard/static/js/components/module-viewer.js +2701 -0
  12. claude_mpm/dashboard/static/js/components/session-manager.js +520 -0
  13. claude_mpm/dashboard/static/js/components/socket-manager.js +343 -0
  14. claude_mpm/dashboard/static/js/components/ui-state-manager.js +427 -0
  15. claude_mpm/dashboard/static/js/components/working-directory.js +866 -0
  16. claude_mpm/dashboard/static/js/dashboard-original.js +4134 -0
  17. claude_mpm/dashboard/static/js/dashboard.js +1978 -0
  18. claude_mpm/dashboard/static/js/socket-client.js +537 -0
  19. claude_mpm/dashboard/templates/index.html +346 -0
  20. claude_mpm/dashboard/test_dashboard.html +372 -0
  21. claude_mpm/services/socketio_server.py +111 -7
  22. {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.16.dist-info}/METADATA +2 -1
  23. {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.16.dist-info}/RECORD +27 -7
  24. {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.16.dist-info}/WHEEL +0 -0
  25. {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.16.dist-info}/entry_points.txt +0 -0
  26. {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.16.dist-info}/licenses/LICENSE +0 -0
  27. {claude_mpm-3.4.13.dist-info → claude_mpm-3.4.16.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,866 @@
1
+ /**
2
+ * Working Directory Module
3
+ *
4
+ * Manages working directory state, session-specific directory tracking,
5
+ * and git branch monitoring for the dashboard.
6
+ *
7
+ * WHY: Extracted from main dashboard to isolate working directory management
8
+ * logic that involves coordination between UI updates, local storage persistence,
9
+ * and git integration. This provides better maintainability for directory state.
10
+ *
11
+ * DESIGN DECISION: Maintains per-session working directories with persistence
12
+ * in localStorage, provides git branch integration, and coordinates with
13
+ * footer directory display for consistent state management.
14
+ */
15
+ class WorkingDirectoryManager {
16
+ constructor(socketManager) {
17
+ this.socketManager = socketManager;
18
+ this.currentWorkingDir = null;
19
+ this.footerDirObserver = null;
20
+ this._updatingFooter = false;
21
+
22
+ this.setupEventHandlers();
23
+ this.initialize();
24
+
25
+ console.log('Working directory manager initialized');
26
+ }
27
+
28
+ /**
29
+ * Initialize working directory management
30
+ */
31
+ initialize() {
32
+ this.initializeWorkingDirectory();
33
+ this.watchFooterDirectory();
34
+ }
35
+
36
+ /**
37
+ * Set up event handlers for working directory controls
38
+ */
39
+ setupEventHandlers() {
40
+ const changeDirBtn = document.getElementById('change-dir-btn');
41
+ const workingDirPath = document.getElementById('working-dir-path');
42
+
43
+ if (changeDirBtn) {
44
+ changeDirBtn.addEventListener('click', () => {
45
+ this.showChangeDirDialog();
46
+ });
47
+ }
48
+
49
+ if (workingDirPath) {
50
+ workingDirPath.addEventListener('click', (e) => {
51
+ // Check if Shift key is held for directory change, otherwise show file viewer
52
+ if (e.shiftKey) {
53
+ this.showChangeDirDialog();
54
+ } else {
55
+ this.showWorkingDirectoryViewer();
56
+ }
57
+ });
58
+ }
59
+
60
+ // Listen for session changes to update working directory
61
+ document.addEventListener('sessionChanged', (e) => {
62
+ const sessionId = e.detail.sessionId;
63
+ console.log('[WORKING-DIR-DEBUG] sessionChanged event received, sessionId:', this.repr(sessionId));
64
+ if (sessionId) {
65
+ this.loadWorkingDirectoryForSession(sessionId);
66
+ }
67
+ });
68
+
69
+ // Listen for git branch responses
70
+ if (this.socketManager && this.socketManager.getSocket) {
71
+ const socket = this.socketManager.getSocket();
72
+ if (socket) {
73
+ console.log('[WORKING-DIR-DEBUG] Setting up git_branch_response listener');
74
+ socket.on('git_branch_response', (response) => {
75
+ console.log('[GIT-BRANCH-DEBUG] Received git_branch_response:', response);
76
+ this.handleGitBranchResponse(response);
77
+ });
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Initialize working directory for current session
84
+ */
85
+ initializeWorkingDirectory() {
86
+ // Set initial loading state to prevent early Git requests
87
+ const pathElement = document.getElementById('working-dir-path');
88
+ if (pathElement && !pathElement.textContent.trim()) {
89
+ pathElement.textContent = 'Loading...';
90
+ }
91
+
92
+ // Check if there's a selected session
93
+ const sessionSelect = document.getElementById('session-select');
94
+ if (sessionSelect && sessionSelect.value && sessionSelect.value !== 'all') {
95
+ // Load working directory for selected session
96
+ this.loadWorkingDirectoryForSession(sessionSelect.value);
97
+ } else {
98
+ // Use default working directory
99
+ this.setWorkingDirectory(this.getDefaultWorkingDir());
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Watch footer directory for changes and sync working directory
105
+ */
106
+ watchFooterDirectory() {
107
+ const footerDir = document.getElementById('footer-working-dir');
108
+ if (!footerDir) return;
109
+
110
+ // Store observer reference for later use
111
+ this.footerDirObserver = new MutationObserver((mutations) => {
112
+ // Skip if we're updating from setWorkingDirectory
113
+ if (this._updatingFooter) return;
114
+
115
+ mutations.forEach((mutation) => {
116
+ if (mutation.type === 'childList' || mutation.type === 'characterData') {
117
+ const newDir = footerDir.textContent.trim();
118
+ console.log('Footer directory changed to:', newDir);
119
+
120
+ // Only update if it's different from current
121
+ if (newDir && newDir !== this.currentWorkingDir) {
122
+ console.log('Syncing working directory from footer change');
123
+ this.setWorkingDirectory(newDir);
124
+ }
125
+ }
126
+ });
127
+ });
128
+
129
+ // Observe changes to footer directory
130
+ this.footerDirObserver.observe(footerDir, {
131
+ childList: true,
132
+ characterData: true,
133
+ subtree: true
134
+ });
135
+
136
+ console.log('Started watching footer directory for changes');
137
+ }
138
+
139
+ /**
140
+ * Load working directory for a specific session
141
+ * @param {string} sessionId - Session ID
142
+ */
143
+ loadWorkingDirectoryForSession(sessionId) {
144
+ console.log('[WORKING-DIR-DEBUG] loadWorkingDirectoryForSession called with sessionId:', this.repr(sessionId));
145
+
146
+ if (!sessionId || sessionId === 'all') {
147
+ console.log('[WORKING-DIR-DEBUG] No sessionId or sessionId is "all", using default working dir');
148
+ const defaultDir = this.getDefaultWorkingDir();
149
+ console.log('[WORKING-DIR-DEBUG] Default working dir:', this.repr(defaultDir));
150
+ this.setWorkingDirectory(defaultDir);
151
+ return;
152
+ }
153
+
154
+ // Load from localStorage
155
+ const sessionDirs = JSON.parse(localStorage.getItem('sessionWorkingDirs') || '{}');
156
+ console.log('[WORKING-DIR-DEBUG] Session directories from localStorage:', sessionDirs);
157
+
158
+ const sessionDir = sessionDirs[sessionId];
159
+ const defaultDir = this.getDefaultWorkingDir();
160
+ const dir = sessionDir || defaultDir;
161
+
162
+ console.log('[WORKING-DIR-DEBUG] Directory selection:', {
163
+ sessionId: sessionId,
164
+ sessionDir: this.repr(sessionDir),
165
+ defaultDir: this.repr(defaultDir),
166
+ finalDir: this.repr(dir)
167
+ });
168
+
169
+ this.setWorkingDirectory(dir);
170
+ }
171
+
172
+ /**
173
+ * Set the working directory for the current session
174
+ * @param {string} dir - Directory path
175
+ */
176
+ setWorkingDirectory(dir) {
177
+ console.log('[WORKING-DIR-DEBUG] setWorkingDirectory called with:', this.repr(dir));
178
+
179
+ this.currentWorkingDir = dir;
180
+
181
+ // Update UI
182
+ const pathElement = document.getElementById('working-dir-path');
183
+ if (pathElement) {
184
+ console.log('[WORKING-DIR-DEBUG] Updating UI path element to:', dir);
185
+ pathElement.textContent = dir;
186
+ } else {
187
+ console.warn('[WORKING-DIR-DEBUG] working-dir-path element not found');
188
+ }
189
+
190
+ // Update footer directory (sync across components)
191
+ const footerDir = document.getElementById('footer-working-dir');
192
+ if (footerDir) {
193
+ const currentFooterText = footerDir.textContent;
194
+ console.log('[WORKING-DIR-DEBUG] Footer directory current text:', this.repr(currentFooterText), 'new text:', this.repr(dir));
195
+
196
+ if (currentFooterText !== dir) {
197
+ // Set flag to prevent observer from triggering
198
+ this._updatingFooter = true;
199
+ footerDir.textContent = dir;
200
+ console.log('[WORKING-DIR-DEBUG] Updated footer directory to:', dir);
201
+
202
+ // Clear flag after a short delay
203
+ setTimeout(() => {
204
+ this._updatingFooter = false;
205
+ console.log('[WORKING-DIR-DEBUG] Cleared _updatingFooter flag');
206
+ }, 100);
207
+ } else {
208
+ console.log('[WORKING-DIR-DEBUG] Footer directory already has correct text');
209
+ }
210
+ } else {
211
+ console.warn('[WORKING-DIR-DEBUG] footer-working-dir element not found');
212
+ }
213
+
214
+ // Save to localStorage for session persistence
215
+ const sessionSelect = document.getElementById('session-select');
216
+ if (sessionSelect && sessionSelect.value && sessionSelect.value !== 'all') {
217
+ const sessionId = sessionSelect.value;
218
+ const sessionDirs = JSON.parse(localStorage.getItem('sessionWorkingDirs') || '{}');
219
+ sessionDirs[sessionId] = dir;
220
+ localStorage.setItem('sessionWorkingDirs', JSON.stringify(sessionDirs));
221
+ console.log(`[WORKING-DIR-DEBUG] Saved working directory for session ${sessionId}:`, dir);
222
+ } else {
223
+ console.log('[WORKING-DIR-DEBUG] No session selected or session is "all", not saving to localStorage');
224
+ }
225
+
226
+ // Update git branch for new directory - only if it's a valid path
227
+ console.log('[WORKING-DIR-DEBUG] About to call updateGitBranch with:', this.repr(dir));
228
+ if (this.validateDirectoryPath(dir)) {
229
+ this.updateGitBranch(dir);
230
+ } else {
231
+ console.log('[WORKING-DIR-DEBUG] Skipping git branch update for invalid directory:', this.repr(dir));
232
+ }
233
+
234
+ // Dispatch event for other modules
235
+ document.dispatchEvent(new CustomEvent('workingDirectoryChanged', {
236
+ detail: { directory: dir }
237
+ }));
238
+
239
+ console.log('[WORKING-DIR-DEBUG] Working directory set to:', dir);
240
+ }
241
+
242
+ /**
243
+ * Update git branch display for current working directory
244
+ * @param {string} dir - Working directory path
245
+ */
246
+ updateGitBranch(dir) {
247
+ console.log('[GIT-BRANCH-DEBUG] updateGitBranch called with dir:', this.repr(dir), 'type:', typeof dir);
248
+
249
+ if (!this.socketManager || !this.socketManager.isConnected()) {
250
+ console.log('[GIT-BRANCH-DEBUG] Not connected to socket server');
251
+ // Not connected, set to unknown
252
+ const footerBranch = document.getElementById('footer-git-branch');
253
+ if (footerBranch) {
254
+ footerBranch.textContent = 'Not Connected';
255
+ footerBranch.style.display = 'inline';
256
+ }
257
+ return;
258
+ }
259
+
260
+ // Enhanced validation with specific checks for common invalid states
261
+ const isValidPath = this.validateDirectoryPath(dir);
262
+ const isLoadingState = dir === 'Loading...' || dir === 'Loading';
263
+ const isUnknown = dir === 'Unknown';
264
+ const isEmptyOrWhitespace = !dir || (typeof dir === 'string' && dir.trim() === '');
265
+
266
+ console.log('[GIT-BRANCH-DEBUG] Validation results:', {
267
+ dir: dir,
268
+ isValidPath: isValidPath,
269
+ isLoadingState: isLoadingState,
270
+ isUnknown: isUnknown,
271
+ isEmptyOrWhitespace: isEmptyOrWhitespace,
272
+ shouldReject: !isValidPath || isLoadingState || isUnknown || isEmptyOrWhitespace
273
+ });
274
+
275
+ // Validate directory before sending to server - reject common invalid states
276
+ if (!isValidPath || isLoadingState || isUnknown || isEmptyOrWhitespace) {
277
+ console.warn('[GIT-BRANCH-DEBUG] Invalid working directory for git branch request:', dir);
278
+ const footerBranch = document.getElementById('footer-git-branch');
279
+ if (footerBranch) {
280
+ if (isLoadingState) {
281
+ footerBranch.textContent = 'Loading...';
282
+ } else if (isUnknown || isEmptyOrWhitespace) {
283
+ footerBranch.textContent = 'No Directory';
284
+ } else {
285
+ footerBranch.textContent = 'Invalid Directory';
286
+ }
287
+ footerBranch.style.display = 'inline';
288
+ }
289
+ return;
290
+ }
291
+
292
+ // Request git branch from server
293
+ const socket = this.socketManager.getSocket();
294
+ if (socket) {
295
+ console.log('[GIT-BRANCH-DEBUG] Requesting git branch for directory:', dir);
296
+ console.log('[GIT-BRANCH-DEBUG] Socket state:', {
297
+ connected: socket.connected,
298
+ id: socket.id
299
+ });
300
+ // Server expects working_dir as a direct parameter, not as an object
301
+ socket.emit('get_git_branch', dir);
302
+ } else {
303
+ console.error('[GIT-BRANCH-DEBUG] No socket available for git branch request');
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Get default working directory
309
+ * @returns {string} - Default directory path
310
+ */
311
+ getDefaultWorkingDir() {
312
+ console.log('[WORKING-DIR-DEBUG] getDefaultWorkingDir called');
313
+
314
+ // Try to get from footer first
315
+ const footerDir = document.getElementById('footer-working-dir');
316
+ if (footerDir?.textContent?.trim()) {
317
+ const footerPath = footerDir.textContent.trim();
318
+ console.log('[WORKING-DIR-DEBUG] Footer path found:', this.repr(footerPath));
319
+
320
+ // Don't use 'Unknown' as a valid directory
321
+ const isUnknown = footerPath === 'Unknown';
322
+ const isValid = this.validateDirectoryPath(footerPath);
323
+
324
+ console.log('[WORKING-DIR-DEBUG] Footer path validation:', {
325
+ footerPath: this.repr(footerPath),
326
+ isUnknown: isUnknown,
327
+ isValid: isValid,
328
+ shouldUse: !isUnknown && isValid
329
+ });
330
+
331
+ if (!isUnknown && isValid) {
332
+ console.log('[WORKING-DIR-DEBUG] Using footer path as default:', footerPath);
333
+ return footerPath;
334
+ }
335
+ } else {
336
+ console.log('[WORKING-DIR-DEBUG] No footer directory element or no text content');
337
+ }
338
+
339
+ // Fallback to a reasonable default - try to get the current project directory
340
+ // This should be set when the dashboard initializes
341
+
342
+ // Try getting from the browser's URL or any other hint about the current project
343
+ if (window.location.pathname.includes('claude-mpm')) {
344
+ // We can infer we're in a claude-mpm project
345
+ const cwdFallback = '/Users/masa/Projects/claude-mpm';
346
+ console.log('[WORKING-DIR-DEBUG] Using inferred project path as fallback:', cwdFallback);
347
+ return cwdFallback;
348
+ }
349
+ const workingDirPath = document.getElementById('working-dir-path');
350
+ if (workingDirPath?.textContent?.trim()) {
351
+ const pathText = workingDirPath.textContent.trim();
352
+ console.log('[WORKING-DIR-DEBUG] Found working-dir-path element text:', this.repr(pathText));
353
+ if (pathText !== 'Unknown' && this.validateDirectoryPath(pathText)) {
354
+ console.log('[WORKING-DIR-DEBUG] Using working-dir-path as fallback:', pathText);
355
+ return pathText;
356
+ }
357
+ }
358
+
359
+ // Final fallback to current directory indicator
360
+ const fallback = process?.cwd?.() || '/Users/masa/Projects/claude-mpm';
361
+ console.log('[WORKING-DIR-DEBUG] Using hard-coded fallback directory:', this.repr(fallback));
362
+ return fallback;
363
+ }
364
+
365
+ /**
366
+ * Show change directory dialog
367
+ */
368
+ showChangeDirDialog() {
369
+ const newDir = prompt('Enter new working directory:', this.currentWorkingDir || '');
370
+ if (newDir && newDir.trim() !== '') {
371
+ this.setWorkingDirectory(newDir.trim());
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Show working directory file viewer overlay
377
+ * WHY: Provides quick file browsing from the header without opening a full modal
378
+ * DESIGN DECISION: Uses overlay positioned below the blue bar for easy access
379
+ */
380
+ showWorkingDirectoryViewer() {
381
+ // Create or show the directory viewer overlay
382
+ this.createDirectoryViewerOverlay();
383
+ }
384
+
385
+ /**
386
+ * Create directory viewer overlay positioned below the working directory display
387
+ * WHY: Positions overlay near the trigger for intuitive user experience
388
+ * without disrupting the main dashboard layout
389
+ */
390
+ createDirectoryViewerOverlay() {
391
+ // Remove existing overlay if present
392
+ this.removeDirectoryViewerOverlay();
393
+
394
+ const workingDirDisplay = document.querySelector('.working-dir-display');
395
+ if (!workingDirDisplay) return;
396
+
397
+ // Create overlay element
398
+ const overlay = document.createElement('div');
399
+ overlay.id = 'directory-viewer-overlay';
400
+ overlay.className = 'directory-viewer-overlay';
401
+
402
+ // Create overlay content
403
+ overlay.innerHTML = `
404
+ <div class="directory-viewer-content">
405
+ <div class="directory-viewer-header">
406
+ <h3 class="directory-viewer-title">
407
+ 📁 ${this.currentWorkingDir || 'Working Directory'}
408
+ </h3>
409
+ <button class="close-btn" onclick="workingDirectoryManager.removeDirectoryViewerOverlay()">✕</button>
410
+ </div>
411
+ <div class="directory-viewer-body">
412
+ <div class="loading-indicator">Loading directory contents...</div>
413
+ </div>
414
+ <div class="directory-viewer-footer">
415
+ <span class="directory-hint">Click file to view • Shift+Click directory path to change</span>
416
+ </div>
417
+ </div>
418
+ `;
419
+
420
+ // Position overlay below the working directory display
421
+ const rect = workingDirDisplay.getBoundingClientRect();
422
+ overlay.style.cssText = `
423
+ position: fixed;
424
+ top: ${rect.bottom + 5}px;
425
+ left: ${rect.left}px;
426
+ min-width: 400px;
427
+ max-width: 600px;
428
+ max-height: 400px;
429
+ z-index: 1001;
430
+ background: white;
431
+ border-radius: 8px;
432
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
433
+ border: 1px solid #e2e8f0;
434
+ `;
435
+
436
+ // Add to document
437
+ document.body.appendChild(overlay);
438
+
439
+ // Load directory contents
440
+ this.loadDirectoryContents();
441
+
442
+ // Add click outside to close
443
+ setTimeout(() => {
444
+ document.addEventListener('click', this.handleOutsideClick.bind(this), true);
445
+ }, 100);
446
+ }
447
+
448
+ /**
449
+ * Remove directory viewer overlay
450
+ */
451
+ removeDirectoryViewerOverlay() {
452
+ const overlay = document.getElementById('directory-viewer-overlay');
453
+ if (overlay) {
454
+ overlay.remove();
455
+ document.removeEventListener('click', this.handleOutsideClick.bind(this), true);
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Handle clicks outside the overlay to close it
461
+ * @param {Event} event - Click event
462
+ */
463
+ handleOutsideClick(event) {
464
+ const overlay = document.getElementById('directory-viewer-overlay');
465
+ const workingDirPath = document.getElementById('working-dir-path');
466
+
467
+ if (overlay && !overlay.contains(event.target) && event.target !== workingDirPath) {
468
+ this.removeDirectoryViewerOverlay();
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Load directory contents using socket connection
474
+ * WHY: Uses existing socket infrastructure to get directory listing
475
+ * without requiring new endpoints
476
+ */
477
+ loadDirectoryContents() {
478
+ if (!this.socketManager || !this.socketManager.isConnected()) {
479
+ this.showDirectoryError('Not connected to server');
480
+ return;
481
+ }
482
+
483
+ const socket = this.socketManager.getSocket();
484
+ if (!socket) {
485
+ this.showDirectoryError('No socket connection available');
486
+ return;
487
+ }
488
+
489
+ // Request directory listing
490
+ socket.emit('get_directory_listing', {
491
+ directory: this.currentWorkingDir,
492
+ limit: 50 // Reasonable limit for overlay display
493
+ });
494
+
495
+ // Listen for response
496
+ const responseHandler = (data) => {
497
+ socket.off('directory_listing_response', responseHandler);
498
+ this.handleDirectoryListingResponse(data);
499
+ };
500
+
501
+ socket.on('directory_listing_response', responseHandler);
502
+
503
+ // Timeout after 5 seconds
504
+ setTimeout(() => {
505
+ socket.off('directory_listing_response', responseHandler);
506
+ const overlay = document.getElementById('directory-viewer-overlay');
507
+ if (overlay && overlay.querySelector('.loading-indicator')) {
508
+ this.showDirectoryError('Request timeout');
509
+ }
510
+ }, 5000);
511
+ }
512
+
513
+ /**
514
+ * Handle directory listing response from server
515
+ * @param {Object} data - Directory listing data
516
+ */
517
+ handleDirectoryListingResponse(data) {
518
+ const bodyElement = document.querySelector('.directory-viewer-body');
519
+ if (!bodyElement) return;
520
+
521
+ if (!data.success) {
522
+ this.showDirectoryError(data.error || 'Failed to load directory');
523
+ return;
524
+ }
525
+
526
+ // Create file listing
527
+ const files = data.files || [];
528
+ const directories = data.directories || [];
529
+
530
+ let html = '';
531
+
532
+ // Add parent directory link if not root
533
+ if (this.currentWorkingDir && this.currentWorkingDir !== '/') {
534
+ const parentDir = this.currentWorkingDir.split('/').slice(0, -1).join('/') || '/';
535
+ html += `
536
+ <div class="file-item directory-item" onclick="workingDirectoryManager.setWorkingDirectory('${parentDir}')">
537
+ <span class="file-icon">📁</span>
538
+ <span class="file-name">..</span>
539
+ <span class="file-type">parent directory</span>
540
+ </div>
541
+ `;
542
+ }
543
+
544
+ // Add directories
545
+ directories.forEach(dir => {
546
+ const fullPath = `${this.currentWorkingDir}/${dir}`.replace(/\/+/g, '/');
547
+ html += `
548
+ <div class="file-item directory-item" onclick="workingDirectoryManager.setWorkingDirectory('${fullPath}')">
549
+ <span class="file-icon">📁</span>
550
+ <span class="file-name">${dir}</span>
551
+ <span class="file-type">directory</span>
552
+ </div>
553
+ `;
554
+ });
555
+
556
+ // Add files
557
+ files.forEach(file => {
558
+ const filePath = `${this.currentWorkingDir}/${file}`.replace(/\/+/g, '/');
559
+ const fileExt = file.split('.').pop().toLowerCase();
560
+ const fileIcon = this.getFileIcon(fileExt);
561
+
562
+ html += `
563
+ <div class="file-item" onclick="workingDirectoryManager.viewFile('${filePath}')">
564
+ <span class="file-icon">${fileIcon}</span>
565
+ <span class="file-name">${file}</span>
566
+ <span class="file-type">${fileExt}</span>
567
+ </div>
568
+ `;
569
+ });
570
+
571
+ if (html === '') {
572
+ html = '<div class="no-files">Empty directory</div>';
573
+ }
574
+
575
+ bodyElement.innerHTML = html;
576
+ }
577
+
578
+ /**
579
+ * Show directory error in the overlay
580
+ * @param {string} message - Error message
581
+ */
582
+ showDirectoryError(message) {
583
+ const bodyElement = document.querySelector('.directory-viewer-body');
584
+ if (bodyElement) {
585
+ bodyElement.innerHTML = `
586
+ <div class="directory-error">
587
+ <span class="error-icon">⚠️</span>
588
+ <span class="error-message">${message}</span>
589
+ </div>
590
+ `;
591
+ }
592
+ }
593
+
594
+ /**
595
+ * Get file icon based on extension
596
+ * @param {string} extension - File extension
597
+ * @returns {string} - File icon emoji
598
+ */
599
+ getFileIcon(extension) {
600
+ const iconMap = {
601
+ 'js': '📄',
602
+ 'py': '🐍',
603
+ 'html': '🌐',
604
+ 'css': '🎨',
605
+ 'json': '📋',
606
+ 'md': '📝',
607
+ 'txt': '📝',
608
+ 'yml': '⚙️',
609
+ 'yaml': '⚙️',
610
+ 'xml': '📄',
611
+ 'pdf': '📕',
612
+ 'png': '🖼️',
613
+ 'jpg': '🖼️',
614
+ 'jpeg': '🖼️',
615
+ 'gif': '🖼️',
616
+ 'svg': '🖼️',
617
+ 'zip': '📦',
618
+ 'tar': '📦',
619
+ 'gz': '📦',
620
+ 'sh': '🔧',
621
+ 'bat': '🔧',
622
+ 'exe': '⚙️',
623
+ 'dll': '⚙️'
624
+ };
625
+
626
+ return iconMap[extension] || '📄';
627
+ }
628
+
629
+ /**
630
+ * View a file using the existing file viewer modal
631
+ * @param {string} filePath - Path to the file to view
632
+ */
633
+ viewFile(filePath) {
634
+ // Close the directory viewer overlay
635
+ this.removeDirectoryViewerOverlay();
636
+
637
+ // Use the existing file viewer modal functionality
638
+ if (window.showFileViewerModal) {
639
+ window.showFileViewerModal(filePath);
640
+ } else {
641
+ console.warn('File viewer modal function not available');
642
+ }
643
+ }
644
+
645
+ /**
646
+ * Get current working directory
647
+ * @returns {string} - Current working directory
648
+ */
649
+ getCurrentWorkingDir() {
650
+ return this.currentWorkingDir;
651
+ }
652
+
653
+ /**
654
+ * Get session working directories from localStorage
655
+ * @returns {Object} - Session directories mapping
656
+ */
657
+ getSessionDirectories() {
658
+ return JSON.parse(localStorage.getItem('sessionWorkingDirs') || '{}');
659
+ }
660
+
661
+ /**
662
+ * Set working directory for a specific session
663
+ * @param {string} sessionId - Session ID
664
+ * @param {string} directory - Directory path
665
+ */
666
+ setSessionDirectory(sessionId, directory) {
667
+ const sessionDirs = this.getSessionDirectories();
668
+ sessionDirs[sessionId] = directory;
669
+ localStorage.setItem('sessionWorkingDirs', JSON.stringify(sessionDirs));
670
+
671
+ // If this is the current session, update the current directory
672
+ const sessionSelect = document.getElementById('session-select');
673
+ if (sessionSelect && sessionSelect.value === sessionId) {
674
+ this.setWorkingDirectory(directory);
675
+ }
676
+ }
677
+
678
+ /**
679
+ * Remove session directory from storage
680
+ * @param {string} sessionId - Session ID to remove
681
+ */
682
+ removeSessionDirectory(sessionId) {
683
+ const sessionDirs = this.getSessionDirectories();
684
+ delete sessionDirs[sessionId];
685
+ localStorage.setItem('sessionWorkingDirs', JSON.stringify(sessionDirs));
686
+ }
687
+
688
+ /**
689
+ * Clear all session directories from storage
690
+ */
691
+ clearAllSessionDirectories() {
692
+ localStorage.removeItem('sessionWorkingDirs');
693
+ }
694
+
695
+ /**
696
+ * Extract working directory from event pair
697
+ * Used by file operations tracking
698
+ * @param {Object} pair - Event pair object
699
+ * @returns {string} - Working directory path
700
+ */
701
+ extractWorkingDirectoryFromPair(pair) {
702
+ // Try different sources for working directory
703
+ if (pair.pre?.working_dir) return pair.pre.working_dir;
704
+ if (pair.post?.working_dir) return pair.post.working_dir;
705
+ if (pair.pre?.data?.working_dir) return pair.pre.data.working_dir;
706
+ if (pair.post?.data?.working_dir) return pair.post.data.working_dir;
707
+
708
+ // Fallback to current working directory
709
+ return this.currentWorkingDir || this.getDefaultWorkingDir();
710
+ }
711
+
712
+ /**
713
+ * Validate directory path
714
+ * @param {string} path - Directory path to validate
715
+ * @returns {boolean} - True if path appears valid
716
+ */
717
+ validateDirectoryPath(path) {
718
+ if (!path || typeof path !== 'string') return false;
719
+
720
+ // Basic path validation
721
+ const trimmed = path.trim();
722
+ if (trimmed.length === 0) return false;
723
+
724
+ // Check for obviously invalid paths
725
+ if (trimmed.includes('\0')) return false;
726
+
727
+ // Check for common invalid placeholder states
728
+ const invalidStates = [
729
+ 'Loading...',
730
+ 'Loading',
731
+ 'Unknown',
732
+ 'undefined',
733
+ 'null',
734
+ 'Not Connected',
735
+ 'Invalid Directory',
736
+ 'No Directory'
737
+ ];
738
+
739
+ if (invalidStates.includes(trimmed)) return false;
740
+
741
+ // Basic path structure validation - should start with / or drive letter on Windows
742
+ if (!trimmed.startsWith('/') && !(/^[A-Za-z]:/.test(trimmed))) {
743
+ // Allow relative paths that look reasonable
744
+ if (trimmed.startsWith('./') || trimmed.startsWith('../') ||
745
+ /^[a-zA-Z0-9._-]+/.test(trimmed)) {
746
+ return true;
747
+ }
748
+ return false;
749
+ }
750
+
751
+ return true;
752
+ }
753
+
754
+ /**
755
+ * Handle git branch response from server
756
+ * @param {Object} response - Git branch response
757
+ */
758
+ handleGitBranchResponse(response) {
759
+ console.log('[GIT-BRANCH-DEBUG] handleGitBranchResponse called with:', response);
760
+
761
+ const footerBranch = document.getElementById('footer-git-branch');
762
+ if (!footerBranch) {
763
+ console.warn('[GIT-BRANCH-DEBUG] footer-git-branch element not found');
764
+ return;
765
+ }
766
+
767
+ if (response.success) {
768
+ console.log('[GIT-BRANCH-DEBUG] Git branch request successful, branch:', response.branch);
769
+ footerBranch.textContent = response.branch;
770
+ footerBranch.style.display = 'inline';
771
+
772
+ // Optional: Add a class to indicate successful git status
773
+ footerBranch.classList.remove('git-error');
774
+ footerBranch.classList.add('git-success');
775
+ } else {
776
+ // Handle different error types more gracefully
777
+ let displayText = 'Git Error';
778
+ const error = response.error || 'Unknown error';
779
+
780
+ if (error.includes('Directory not found') || error.includes('does not exist')) {
781
+ displayText = 'Dir Not Found';
782
+ } else if (error.includes('Not a directory')) {
783
+ displayText = 'Invalid Path';
784
+ } else if (error.includes('Not a git repository')) {
785
+ displayText = 'No Git Repo';
786
+ } else if (error.includes('git')) {
787
+ displayText = 'Git Error';
788
+ } else {
789
+ displayText = 'Unknown';
790
+ }
791
+
792
+ console.log('[GIT-BRANCH-DEBUG] Git branch request failed:', error, '- showing as:', displayText);
793
+ footerBranch.textContent = displayText;
794
+ footerBranch.style.display = 'inline';
795
+
796
+ // Optional: Add a class to indicate error state
797
+ footerBranch.classList.remove('git-success');
798
+ footerBranch.classList.add('git-error');
799
+ }
800
+
801
+ // Log additional debug info from server
802
+ if (response.original_working_dir) {
803
+ console.log('[GIT-BRANCH-DEBUG] Server received original working_dir:', this.repr(response.original_working_dir));
804
+ }
805
+ if (response.working_dir) {
806
+ console.log('[GIT-BRANCH-DEBUG] Server used working_dir:', this.repr(response.working_dir));
807
+ }
808
+ if (response.git_error) {
809
+ console.log('[GIT-BRANCH-DEBUG] Git command stderr:', response.git_error);
810
+ }
811
+ }
812
+
813
+ /**
814
+ * Check if working directory is ready for Git operations
815
+ * @returns {boolean} - True if directory is ready
816
+ */
817
+ isWorkingDirectoryReady() {
818
+ const dir = this.getCurrentWorkingDir();
819
+ return this.validateDirectoryPath(dir) && dir !== 'Loading...' && dir !== 'Unknown';
820
+ }
821
+
822
+ /**
823
+ * Wait for working directory to be ready, then execute callback
824
+ * @param {Function} callback - Function to call when directory is ready
825
+ * @param {number} timeout - Maximum time to wait in milliseconds
826
+ */
827
+ whenDirectoryReady(callback, timeout = 5000) {
828
+ const startTime = Date.now();
829
+
830
+ const checkReady = () => {
831
+ if (this.isWorkingDirectoryReady()) {
832
+ callback();
833
+ } else if (Date.now() - startTime < timeout) {
834
+ setTimeout(checkReady, 100); // Check every 100ms
835
+ } else {
836
+ console.warn('[WORKING-DIR-DEBUG] Timeout waiting for directory to be ready');
837
+ }
838
+ };
839
+
840
+ checkReady();
841
+ }
842
+
843
+ /**
844
+ * Helper function for detailed logging
845
+ * @param {*} value - Value to represent
846
+ * @returns {string} - String representation
847
+ */
848
+ repr(value) {
849
+ if (value === null) return 'null';
850
+ if (value === undefined) return 'undefined';
851
+ if (typeof value === 'string') return `"${value}"`;
852
+ return String(value);
853
+ }
854
+
855
+ /**
856
+ * Cleanup resources
857
+ */
858
+ cleanup() {
859
+ if (this.footerDirObserver) {
860
+ this.footerDirObserver.disconnect();
861
+ this.footerDirObserver = null;
862
+ }
863
+
864
+ console.log('Working directory manager cleaned up');
865
+ }
866
+ }