seeclaudecode 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/app.js ADDED
@@ -0,0 +1,1282 @@
1
+ // SeeClaude - Watch Claude Code Work
2
+ class SeeClaude {
3
+ constructor() {
4
+ this.ws = null;
5
+ this.structure = null;
6
+ this.activeFiles = new Set();
7
+ this.changedFiles = new Set();
8
+ this.changedPaths = new Set(); // Parent directories of changed files
9
+ this.activePaths = new Set();
10
+ this.activities = [];
11
+ this.currentView = 'graph';
12
+ this.expandedNodes = new Set();
13
+ this.graphNodes = [];
14
+ this.graphEdges = [];
15
+ this.isPanelOpen = true;
16
+
17
+ // Pan/zoom state for graph
18
+ this.panX = 0;
19
+ this.panY = 0;
20
+ this.zoom = 1;
21
+ this.isDragging = false;
22
+ this.lastMouseX = 0;
23
+ this.lastMouseY = 0;
24
+
25
+ this.init();
26
+ }
27
+
28
+ async init() {
29
+ this.createTooltip();
30
+ this.setupWebSocket();
31
+ this.setupEventListeners();
32
+ await this.loadStructure();
33
+ await this.loadGitStatus(); // Wait for git status before rendering
34
+ this.buildGraphData(); // Rebuild with change data
35
+ this.render();
36
+ }
37
+
38
+ createTooltip() {
39
+ this.tooltip = document.createElement('div');
40
+ this.tooltip.className = 'tooltip';
41
+ document.body.appendChild(this.tooltip);
42
+ }
43
+
44
+ setupWebSocket() {
45
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
46
+ this.ws = new WebSocket(`${protocol}//${window.location.host}`);
47
+
48
+ this.ws.onopen = () => {
49
+ document.getElementById('connection-state').textContent = 'Connected';
50
+ document.getElementById('status-indicator').classList.add('connected');
51
+ this.addActivity('system', 'Connected to server', 'Connection established');
52
+ };
53
+
54
+ this.ws.onclose = () => {
55
+ document.getElementById('connection-state').textContent = 'Disconnected';
56
+ document.getElementById('status-indicator').classList.remove('connected');
57
+ this.addActivity('system', 'Connection lost', 'Attempting to reconnect...');
58
+ setTimeout(() => this.setupWebSocket(), 3000);
59
+ };
60
+
61
+ this.ws.onmessage = (event) => {
62
+ const data = JSON.parse(event.data);
63
+ this.handleMessage(data);
64
+ };
65
+ }
66
+
67
+ handleMessage(data) {
68
+ switch (data.type) {
69
+ case 'init':
70
+ data.activeFiles?.forEach(f => this.activeFiles.add(f));
71
+ data.changedFiles?.forEach(f => this.changedFiles.add(f));
72
+ this.updateActivePaths();
73
+ this.render();
74
+ break;
75
+
76
+ case 'active':
77
+ this.handleActiveEdit(data);
78
+ break;
79
+
80
+ case 'complete':
81
+ this.handleEditComplete(data);
82
+ break;
83
+ }
84
+ }
85
+
86
+ updateActivePaths() {
87
+ this.activePaths.clear();
88
+ this.activeFiles.forEach(file => {
89
+ const parts = file.split('/');
90
+ let currentPath = '';
91
+ for (let i = 0; i < parts.length - 1; i++) {
92
+ currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];
93
+ this.activePaths.add(currentPath);
94
+ }
95
+ });
96
+ }
97
+
98
+ handleActiveEdit(data) {
99
+ this.activeFiles.add(data.file);
100
+ this.updateActivePaths();
101
+
102
+ const fileName = data.file.split('/').pop();
103
+ document.getElementById('current-task').textContent = `Editing ${fileName}`;
104
+
105
+ this.addActivity('edit', `Editing ${fileName}`, data.file, true);
106
+ this.expandPathTo(data.file);
107
+ this.render();
108
+ }
109
+
110
+ handleEditComplete(data) {
111
+ this.activeFiles.clear();
112
+ this.activePaths.clear();
113
+ data.files.forEach(f => this.changedFiles.add(f));
114
+
115
+ document.getElementById('current-task').textContent = `Finished editing ${data.files.length} file(s)`;
116
+
117
+ this.addActivity('complete', `Completed editing`, `${data.files.length} file(s) modified`);
118
+ this.updateDiffPanel(data.diff);
119
+ this.render();
120
+
121
+ setTimeout(() => {
122
+ document.getElementById('current-task').textContent = 'Waiting for activity...';
123
+ }, 5000);
124
+ }
125
+
126
+ addActivity(type, action, description, isActive = false) {
127
+ const activity = {
128
+ id: Date.now(),
129
+ type,
130
+ action,
131
+ description,
132
+ time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
133
+ isActive
134
+ };
135
+
136
+ this.activities.unshift(activity);
137
+ if (this.activities.length > 20) this.activities.pop();
138
+ this.renderActivityFeed();
139
+ }
140
+
141
+ renderActivityFeed() {
142
+ const container = document.getElementById('activity-feed');
143
+
144
+ if (this.activities.length === 0) {
145
+ container.innerHTML = `
146
+ <div class="activity-empty">
147
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
148
+ <circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>
149
+ </svg>
150
+ <p>Waiting for Claude Code activity...</p>
151
+ </div>
152
+ `;
153
+ return;
154
+ }
155
+
156
+ container.innerHTML = this.activities.map(activity => `
157
+ <div class="activity-item ${activity.isActive ? 'active' : ''}">
158
+ <div class="activity-header">
159
+ <span class="activity-action">${activity.action}</span>
160
+ <span class="activity-time">${activity.time}</span>
161
+ </div>
162
+ <div class="activity-description">${activity.description}</div>
163
+ </div>
164
+ `).join('');
165
+ }
166
+
167
+ updateDiffPanel(diff) {
168
+ if (!diff) return;
169
+
170
+ const filesCount = (diff.modified?.length || 0) + (diff.created?.length || 0);
171
+ document.getElementById('files-count').textContent = filesCount;
172
+
173
+ let additions = 0, deletions = 0;
174
+ diff.fileDiffs?.forEach(fd => {
175
+ additions += fd.additions || 0;
176
+ deletions += fd.deletions || 0;
177
+ });
178
+
179
+ document.getElementById('additions-count').textContent = additions;
180
+ document.getElementById('deletions-count').textContent = deletions;
181
+
182
+ if (diff.modified?.length > 0 || diff.created?.length > 0) {
183
+ const file = diff.modified[0] || diff.created[0];
184
+ document.getElementById('diff-file').textContent = file;
185
+ }
186
+
187
+ const diffContent = document.getElementById('diff-content');
188
+ if (diff.fileDiffs?.length > 0) {
189
+ const fd = diff.fileDiffs[0];
190
+ diffContent.innerHTML = `
191
+ <div class="diff-line context">
192
+ <div class="diff-line-number">...</div>
193
+ <div class="diff-line-content">// Changes in ${fd.file}</div>
194
+ </div>
195
+ ${fd.additions > 0 ? `
196
+ <div class="diff-line added">
197
+ <div class="diff-line-number">+</div>
198
+ <div class="diff-line-content">+ ${fd.additions} line(s) added</div>
199
+ </div>
200
+ ` : ''}
201
+ ${fd.deletions > 0 ? `
202
+ <div class="diff-line removed">
203
+ <div class="diff-line-number">-</div>
204
+ <div class="diff-line-content">- ${fd.deletions} line(s) removed</div>
205
+ </div>
206
+ ` : ''}
207
+ `;
208
+ }
209
+ }
210
+
211
+ async showFileDiff(filePath) {
212
+ if (!filePath) return;
213
+ if (filePath === '.') filePath = '';
214
+
215
+ try {
216
+ const response = await fetch(`/api/diff/${encodeURIComponent(filePath || '.')}`);
217
+ const diff = await response.json();
218
+
219
+ const diffContent = document.getElementById('diff-content');
220
+
221
+ // Handle directory diff
222
+ if (diff.type === 'directory') {
223
+ document.getElementById('diff-file').textContent = filePath || 'Root';
224
+ document.getElementById('additions-count').textContent = diff.totalAdditions || 0;
225
+ document.getElementById('deletions-count').textContent = diff.totalDeletions || 0;
226
+ document.getElementById('files-count').textContent = diff.fileCount || 0;
227
+
228
+ if (diff.status === 'unchanged' || !diff.files || diff.files.length === 0) {
229
+ diffContent.innerHTML = `
230
+ <div class="diff-empty">
231
+ <p>No changes in this directory</p>
232
+ </div>
233
+ `;
234
+ } else {
235
+ // Render all file diffs
236
+ diffContent.innerHTML = diff.files.map(fileDiff => `
237
+ <div class="diff-file-section">
238
+ <div class="diff-file-header">
239
+ <span class="diff-file-name">${this.escapeHtml(fileDiff.file)}</span>
240
+ <span class="diff-file-stats">
241
+ <span class="stat-add">+${fileDiff.additions || 0}</span>
242
+ <span class="stat-del">-${fileDiff.deletions || 0}</span>
243
+ </span>
244
+ </div>
245
+ ${fileDiff.lines?.map(line => {
246
+ if (line.type === 'hunk') {
247
+ return `<div class="diff-line hunk"><div class="diff-line-content">${this.escapeHtml(line.content)}</div></div>`;
248
+ }
249
+ const lineNum = line.lineNumber || '';
250
+ const prefix = line.type === 'added' ? '+' : line.type === 'removed' ? '-' : ' ';
251
+ return `
252
+ <div class="diff-line ${line.type}">
253
+ <div class="diff-line-number">${lineNum}</div>
254
+ <div class="diff-line-prefix">${prefix}</div>
255
+ <div class="diff-line-content">${this.escapeHtml(line.content)}</div>
256
+ </div>
257
+ `;
258
+ }).join('') || '<div class="diff-line context"><div class="diff-line-content">No changes</div></div>'}
259
+ </div>
260
+ `).join('');
261
+ }
262
+ } else {
263
+ // Single file diff
264
+ document.getElementById('diff-file').textContent = filePath;
265
+ document.getElementById('additions-count').textContent = diff.additions || 0;
266
+ document.getElementById('deletions-count').textContent = diff.deletions || 0;
267
+ document.getElementById('files-count').textContent = '1';
268
+
269
+ if (diff.status === 'unchanged') {
270
+ diffContent.innerHTML = `
271
+ <div class="diff-empty">
272
+ <p>No changes in this file</p>
273
+ </div>
274
+ `;
275
+ } else if (!diff.lines || diff.lines.length === 0) {
276
+ diffContent.innerHTML = `
277
+ <div class="diff-empty">
278
+ <p>No diff available for this file</p>
279
+ ${diff.error ? `<p class="diff-error">${diff.error}</p>` : ''}
280
+ </div>
281
+ `;
282
+ } else {
283
+ diffContent.innerHTML = diff.lines.map(line => {
284
+ if (line.type === 'hunk') {
285
+ return `<div class="diff-line hunk"><div class="diff-line-content">${this.escapeHtml(line.content)}</div></div>`;
286
+ }
287
+ const lineNum = line.lineNumber || '';
288
+ const prefix = line.type === 'added' ? '+' : line.type === 'removed' ? '-' : ' ';
289
+ return `
290
+ <div class="diff-line ${line.type}">
291
+ <div class="diff-line-number">${lineNum}</div>
292
+ <div class="diff-line-prefix">${prefix}</div>
293
+ <div class="diff-line-content">${this.escapeHtml(line.content)}</div>
294
+ </div>
295
+ `;
296
+ }).join('');
297
+ }
298
+ }
299
+
300
+ // Make sure the panel is open
301
+ if (!this.isPanelOpen) {
302
+ this.isPanelOpen = true;
303
+ document.getElementById('right-panel').classList.remove('hidden');
304
+ document.getElementById('panel-toggle').classList.add('hidden');
305
+ }
306
+ } catch (error) {
307
+ console.error('Failed to load diff:', error);
308
+ }
309
+ }
310
+
311
+ escapeHtml(text) {
312
+ const div = document.createElement('div');
313
+ div.textContent = text || '';
314
+ return div.innerHTML;
315
+ }
316
+
317
+ async loadStructure() {
318
+ try {
319
+ const response = await fetch('/api/structure');
320
+ this.structure = await response.json();
321
+ this.buildGraphData();
322
+ this.expandFirstLevels(this.structure, 2);
323
+ this.render();
324
+ } catch (error) {
325
+ console.error('Failed to load structure:', error);
326
+ }
327
+ }
328
+
329
+ async loadGitStatus() {
330
+ try {
331
+ const response = await fetch('/api/diff');
332
+ const diff = await response.json();
333
+ this.updateDiffPanel(diff);
334
+
335
+ // Add all changed files
336
+ [...(diff.modified || []), ...(diff.created || []), ...(diff.deleted || [])].forEach(f => {
337
+ // Normalize path separators
338
+ const normalizedPath = f.replace(/\\/g, '/');
339
+ this.changedFiles.add(normalizedPath);
340
+
341
+ // Also mark parent directories as having changes
342
+ const parts = normalizedPath.split('/');
343
+ let currentPath = '';
344
+ for (let i = 0; i < parts.length - 1; i++) {
345
+ currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];
346
+ this.changedPaths.add(currentPath);
347
+ }
348
+ });
349
+
350
+ console.log('Changed files:', Array.from(this.changedFiles));
351
+ console.log('Changed paths:', Array.from(this.changedPaths));
352
+ this.buildGraphData(); // Rebuild graph with change data
353
+ this.render();
354
+ } catch (error) {
355
+ console.error('Failed to load git status:', error);
356
+ }
357
+ }
358
+
359
+ buildGraphData() {
360
+ if (!this.structure) return;
361
+
362
+ this.graphNodes = [];
363
+ this.graphEdges = [];
364
+
365
+ // Layout settings
366
+ const nodeWidth = 160;
367
+ const nodeHeight = 50;
368
+ const horizontalGap = 50;
369
+ const verticalGap = 200; // Extra space for vertical file lists below directories
370
+
371
+ // Count changed files in a directory (recursive)
372
+ const countChangedFiles = (node) => {
373
+ if (!node) return 0;
374
+
375
+ // Check if this exact path is changed
376
+ const thisChanged = this.changedFiles.has(node.path) ? 1 : 0;
377
+
378
+ if (node.type === 'file') {
379
+ return thisChanged;
380
+ }
381
+
382
+ // For directories, count all changed children
383
+ if (!node.children) return thisChanged;
384
+
385
+ const childCount = node.children.reduce((sum, child) => sum + countChangedFiles(child), 0);
386
+ return childCount;
387
+ };
388
+
389
+ // Calculate subtree width
390
+ const calculateWidth = (node, depth) => {
391
+ if (depth >= 4 || node.type !== 'directory' || !node.children) {
392
+ return nodeWidth;
393
+ }
394
+
395
+ const dirs = node.children.filter(c => c.type === 'directory').slice(0, 5);
396
+ if (dirs.length === 0) return nodeWidth;
397
+
398
+ const childWidths = dirs.map(d => calculateWidth(d, depth + 1));
399
+ return Math.max(nodeWidth, childWidths.reduce((a, b) => a + b, 0) + horizontalGap * (dirs.length - 1));
400
+ };
401
+
402
+ // Position directories and changed files
403
+ const positionNode = (node, depth, x, y, parentId = null) => {
404
+ if (node.type !== 'directory') return;
405
+
406
+ const nodeId = node.path || '.';
407
+ const nodeType = this.getNodeType(node);
408
+ const files = node.children?.filter(c => c.type === 'file') || [];
409
+ const dirs = node.children?.filter(c => c.type === 'directory') || [];
410
+ const changedCount = countChangedFiles(node);
411
+
412
+ // Get changed files in this directory
413
+ const changedFiles = files.filter(f => this.changedFiles.has(f.path));
414
+
415
+ this.graphNodes.push({
416
+ id: nodeId,
417
+ label: node.name,
418
+ type: nodeType,
419
+ path: node.path,
420
+ x,
421
+ y,
422
+ category: node.category,
423
+ isDirectory: true,
424
+ fileCount: files.length,
425
+ dirCount: dirs.length,
426
+ childCount: node.children?.length || 0,
427
+ changedCount,
428
+ hasChangedFiles: changedFiles.length > 0
429
+ });
430
+
431
+ if (parentId) {
432
+ this.graphEdges.push({ source: parentId, target: nodeId, hasChanges: changedCount > 0 });
433
+ }
434
+
435
+ // Show changed files VERTICALLY below the directory
436
+ if (changedFiles.length > 0) {
437
+ const fileWidth = 130;
438
+ const fileHeight = 22;
439
+ const fileGap = 4;
440
+ const visibleFiles = changedFiles.slice(0, 5); // Limit to 5 files
441
+ const fileX = x; // Centered below directory
442
+ const startY = y + nodeHeight / 2 + 30; // Start below the directory
443
+
444
+ visibleFiles.forEach((file, idx) => {
445
+ const fileY = startY + idx * (fileHeight + fileGap);
446
+
447
+ this.graphNodes.push({
448
+ id: file.path,
449
+ label: file.name,
450
+ type: file.category || 'file',
451
+ path: file.path,
452
+ x: fileX,
453
+ y: fileY,
454
+ category: file.category,
455
+ isDirectory: false,
456
+ isFile: true,
457
+ isChangedFile: true,
458
+ childCount: 0
459
+ });
460
+
461
+ this.graphEdges.push({ source: nodeId, target: file.path, hasChanges: true, isFileEdge: true });
462
+ });
463
+
464
+ // Show "+N more" indicator if there are more files
465
+ if (changedFiles.length > 5) {
466
+ const moreY = startY + visibleFiles.length * (fileHeight + fileGap);
467
+ this.graphNodes.push({
468
+ id: `${nodeId}-more`,
469
+ label: `+${changedFiles.length - 5} more`,
470
+ path: node.path,
471
+ x: fileX,
472
+ y: moreY,
473
+ isMoreIndicator: true
474
+ });
475
+ }
476
+ }
477
+
478
+ if (depth >= 4 || dirs.length === 0) return;
479
+
480
+ // Position child directories
481
+ const visibleDirs = dirs.slice(0, 5);
482
+ const childWidths = visibleDirs.map(d => calculateWidth(d, depth + 1));
483
+ const totalWidth = childWidths.reduce((a, b) => a + b, 0) + horizontalGap * (visibleDirs.length - 1);
484
+ let childX = x - totalWidth / 2;
485
+
486
+ visibleDirs.forEach((dir, i) => {
487
+ const dirWidth = childWidths[i];
488
+ positionNode(dir, depth + 1, childX + dirWidth / 2, y + verticalGap + nodeHeight, nodeId);
489
+ childX += dirWidth + horizontalGap;
490
+ });
491
+ };
492
+
493
+ positionNode(this.structure, 0, 0, 50);
494
+
495
+ // Reset pan
496
+ this.panX = 0;
497
+ this.panY = 0;
498
+ }
499
+
500
+ getNodeType(node) {
501
+ if (node.type === 'directory') {
502
+ const name = node.name.toLowerCase();
503
+ if (name.includes('server') || name.includes('api') || name.includes('backend')) return 'server';
504
+ if (name.includes('client') || name.includes('src') || name.includes('components') || name.includes('pages')) return 'client';
505
+ if (name.includes('db') || name.includes('prisma') || name.includes('database')) return 'database';
506
+ if (name.includes('public') || name.includes('static') || name.includes('assets')) return 'assets';
507
+ if (name.includes('scripts') || name.includes('docker')) return 'config';
508
+ return 'folder';
509
+ }
510
+ return node.category || 'file';
511
+ }
512
+
513
+ expandFirstLevels(node, levels, currentLevel = 0) {
514
+ if (currentLevel >= levels || !node) return;
515
+ if (node.type === 'directory') {
516
+ this.expandedNodes.add(node.path);
517
+ node.children?.forEach(child => this.expandFirstLevels(child, levels, currentLevel + 1));
518
+ }
519
+ }
520
+
521
+ expandPathTo(filePath) {
522
+ const parts = filePath.split('/');
523
+ let currentPath = '';
524
+ for (let i = 0; i < parts.length - 1; i++) {
525
+ currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];
526
+ this.expandedNodes.add(currentPath);
527
+ }
528
+ this.render();
529
+ }
530
+
531
+ setupEventListeners() {
532
+ // View buttons
533
+ document.querySelectorAll('.view-btn').forEach(btn => {
534
+ btn.addEventListener('click', () => {
535
+ document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
536
+ btn.classList.add('active');
537
+ this.currentView = btn.dataset.view;
538
+ this.panX = 0;
539
+ this.panY = 0;
540
+ this.zoom = 1;
541
+ this.render();
542
+ });
543
+ });
544
+
545
+ // Panel toggle
546
+ document.getElementById('close-panel').addEventListener('click', () => {
547
+ this.isPanelOpen = false;
548
+ document.getElementById('right-panel').classList.add('hidden');
549
+ document.getElementById('panel-toggle').classList.remove('hidden');
550
+ });
551
+
552
+ document.getElementById('panel-toggle').addEventListener('click', () => {
553
+ this.isPanelOpen = true;
554
+ document.getElementById('right-panel').classList.remove('hidden');
555
+ document.getElementById('panel-toggle').classList.add('hidden');
556
+ });
557
+ }
558
+
559
+ setupGraphInteraction(container) {
560
+ const wrapper = container.querySelector('.graph-wrapper');
561
+ if (!wrapper) return;
562
+
563
+ // Mouse wheel zoom
564
+ wrapper.addEventListener('wheel', (e) => {
565
+ e.preventDefault();
566
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
567
+ this.zoom = Math.max(0.2, Math.min(3, this.zoom * delta));
568
+ this.updateGraphTransform();
569
+ }, { passive: false });
570
+
571
+ // Pan with mouse drag
572
+ wrapper.addEventListener('mousedown', (e) => {
573
+ if (e.target.closest('.graph-node')) return;
574
+ this.isDragging = true;
575
+ this.lastMouseX = e.clientX;
576
+ this.lastMouseY = e.clientY;
577
+ wrapper.style.cursor = 'grabbing';
578
+ });
579
+
580
+ wrapper.addEventListener('mousemove', (e) => {
581
+ if (!this.isDragging) return;
582
+ const dx = e.clientX - this.lastMouseX;
583
+ const dy = e.clientY - this.lastMouseY;
584
+ this.panX += dx;
585
+ this.panY += dy;
586
+ this.lastMouseX = e.clientX;
587
+ this.lastMouseY = e.clientY;
588
+ this.updateGraphTransform();
589
+ });
590
+
591
+ wrapper.addEventListener('mouseup', () => {
592
+ this.isDragging = false;
593
+ wrapper.style.cursor = 'grab';
594
+ });
595
+
596
+ wrapper.addEventListener('mouseleave', () => {
597
+ this.isDragging = false;
598
+ wrapper.style.cursor = 'grab';
599
+ });
600
+
601
+ wrapper.style.cursor = 'grab';
602
+ }
603
+
604
+ updateGraphTransform() {
605
+ const svg = document.querySelector('.graph-svg');
606
+ if (svg) {
607
+ svg.style.transform = `translate(${this.panX}px, ${this.panY}px) scale(${this.zoom})`;
608
+ }
609
+ }
610
+
611
+ isPathActive(path) {
612
+ return this.activeFiles.has(path) || this.activePaths.has(path);
613
+ }
614
+
615
+ isPathChanged(path) {
616
+ // Normalize path
617
+ const normalizedPath = path?.replace(/\\/g, '/');
618
+ // Check if this exact file is changed OR if it's a directory containing changes
619
+ return this.changedFiles.has(normalizedPath) || this.changedPaths.has(normalizedPath);
620
+ }
621
+
622
+ render() {
623
+ const container = document.getElementById('view-container');
624
+
625
+ if (!this.structure) {
626
+ container.innerHTML = `
627
+ <div class="loading">
628
+ <div class="loading-spinner"></div>
629
+ <p>Loading repository structure...</p>
630
+ </div>
631
+ `;
632
+ return;
633
+ }
634
+
635
+ switch (this.currentView) {
636
+ case 'graph':
637
+ this.renderGraphView(container);
638
+ break;
639
+ case 'files':
640
+ this.renderFileExplorer(container);
641
+ break;
642
+ case 'tree':
643
+ this.renderSunburstView(container);
644
+ break;
645
+ }
646
+ }
647
+
648
+ renderGraphView(container) {
649
+ const width = container.clientWidth || 1000;
650
+ const height = container.clientHeight || 600;
651
+
652
+ // Calculate bounds
653
+ const padding = 150;
654
+ const minX = Math.min(...this.graphNodes.map(n => n.x)) - padding;
655
+ const maxX = Math.max(...this.graphNodes.map(n => n.x)) + padding;
656
+ const minY = Math.min(...this.graphNodes.map(n => n.y)) - padding;
657
+ const maxY = Math.max(...this.graphNodes.map(n => n.y)) + padding;
658
+
659
+ const svgWidth = maxX - minX + 300;
660
+ const svgHeight = maxY - minY + 200;
661
+
662
+ container.innerHTML = `
663
+ <div class="graph-container">
664
+ <div class="graph-wrapper">
665
+ <svg class="graph-svg" width="${svgWidth}" height="${svgHeight}" viewBox="${minX - 150} ${minY - 100} ${svgWidth} ${svgHeight}">
666
+ <defs>
667
+ <filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
668
+ <feGaussianBlur stdDeviation="3" result="coloredBlur"/>
669
+ <feMerge>
670
+ <feMergeNode in="coloredBlur"/>
671
+ <feMergeNode in="SourceGraphic"/>
672
+ </feMerge>
673
+ </filter>
674
+ <filter id="glow-strong" x="-50%" y="-50%" width="200%" height="200%">
675
+ <feGaussianBlur stdDeviation="6" result="coloredBlur"/>
676
+ <feMerge>
677
+ <feMergeNode in="coloredBlur"/>
678
+ <feMergeNode in="SourceGraphic"/>
679
+ </feMerge>
680
+ </filter>
681
+ <filter id="glow-yellow" x="-50%" y="-50%" width="200%" height="200%">
682
+ <feFlood flood-color="hsl(45, 93%, 58%)" flood-opacity="0.6" result="flood"/>
683
+ <feComposite in="flood" in2="SourceGraphic" operator="in" result="mask"/>
684
+ <feGaussianBlur in="mask" stdDeviation="4" result="coloredBlur"/>
685
+ <feMerge>
686
+ <feMergeNode in="coloredBlur"/>
687
+ <feMergeNode in="SourceGraphic"/>
688
+ </feMerge>
689
+ </filter>
690
+ </defs>
691
+ <g class="edges"></g>
692
+ <g class="nodes"></g>
693
+ </svg>
694
+ </div>
695
+ <div class="graph-controls">
696
+ <button class="control-btn" id="zoom-in" title="Zoom In">+</button>
697
+ <button class="control-btn" id="zoom-out" title="Zoom Out">-</button>
698
+ <button class="control-btn" id="zoom-reset" title="Reset View">⟲</button>
699
+ </div>
700
+ </div>
701
+ `;
702
+
703
+ const svg = container.querySelector('.graph-svg');
704
+ const edgesGroup = svg.querySelector('.edges');
705
+ const nodesGroup = svg.querySelector('.nodes');
706
+
707
+ // Draw edges
708
+ this.graphEdges.forEach(edge => {
709
+ const source = this.graphNodes.find(n => n.id === edge.source);
710
+ const target = this.graphNodes.find(n => n.id === edge.target);
711
+ if (!source || !target) return;
712
+
713
+ const isActive = this.isPathActive(source.path) || this.isPathActive(target.path);
714
+ const hasChanges = edge.hasChanges || this.isPathChanged(target.path);
715
+ const isFileEdge = edge.isFileEdge;
716
+
717
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
718
+
719
+ // Different path for file edges vs directory edges
720
+ if (isFileEdge) {
721
+ // Short vertical edge to file below
722
+ path.setAttribute('d', `M${source.x},${source.y + 25} L${target.x},${target.y - 12}`);
723
+ } else {
724
+ // Vertical edge to directory
725
+ const sourceY = source.y + 25;
726
+ const targetY = target.y - 25;
727
+ const midY = (sourceY + targetY) / 2;
728
+ path.setAttribute('d', `M${source.x},${sourceY} C${source.x},${midY} ${target.x},${midY} ${target.x},${targetY}`);
729
+ }
730
+
731
+ const classes = ['graph-edge'];
732
+ if (isActive) classes.push('active');
733
+ if (hasChanges) classes.push('has-changes');
734
+ if (isFileEdge) classes.push('file-edge');
735
+ path.setAttribute('class', classes.join(' '));
736
+
737
+ // Pulsing dashed line for changes
738
+ if (hasChanges) {
739
+ path.setAttribute('stroke', 'hsl(45, 93%, 58%)');
740
+ path.setAttribute('stroke-dasharray', '8,4');
741
+ path.setAttribute('filter', 'url(#glow-yellow)');
742
+ } else if (isActive) {
743
+ path.setAttribute('stroke-dasharray', '8,4');
744
+ path.setAttribute('filter', 'url(#glow)');
745
+ }
746
+
747
+ edgesGroup.appendChild(path);
748
+ });
749
+
750
+ // Draw nodes (directories and changed files)
751
+ this.graphNodes.forEach(node => {
752
+ // Handle "+N more" indicator
753
+ if (node.isMoreIndicator) {
754
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
755
+ g.setAttribute('class', 'graph-node more-indicator');
756
+ g.setAttribute('transform', `translate(${node.x - 30}, ${node.y - 10})`);
757
+
758
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
759
+ text.setAttribute('x', '0');
760
+ text.setAttribute('y', '12');
761
+ text.setAttribute('font-size', '10');
762
+ text.setAttribute('fill', 'hsl(45, 93%, 58%)');
763
+ text.setAttribute('font-weight', '600');
764
+ text.textContent = node.label;
765
+ g.appendChild(text);
766
+
767
+ g.addEventListener('click', (e) => {
768
+ e.stopPropagation();
769
+ this.showFileDiff(node.path);
770
+ });
771
+ g.style.cursor = 'pointer';
772
+
773
+ nodesGroup.appendChild(g);
774
+ return;
775
+ }
776
+
777
+ const isActive = this.isPathActive(node.path);
778
+ const isDirectlyActive = this.activeFiles.has(node.path);
779
+ const isChanged = this.isPathChanged(node.path);
780
+ const isFile = node.isFile;
781
+ const isChangedFile = node.isChangedFile;
782
+ // Check both pre-calculated count and current changedPaths
783
+ const hasChanges = node.changedCount > 0 || this.changedPaths.has(node.path) || isChanged;
784
+
785
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
786
+ const classes = ['graph-node'];
787
+ if (isActive) classes.push('active');
788
+ if (isDirectlyActive) classes.push('directly-active');
789
+ if (hasChanges || isChangedFile) classes.push('has-changes');
790
+ if (isFile) classes.push('file-node');
791
+ g.setAttribute('class', classes.join(' '));
792
+
793
+ if (isFile) {
794
+ // Render changed file node (compact)
795
+ const width = 140;
796
+ const height = 26;
797
+ g.setAttribute('transform', `translate(${node.x - width/2}, ${node.y - height/2})`);
798
+
799
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
800
+ rect.setAttribute('class', 'node-bg');
801
+ rect.setAttribute('width', width);
802
+ rect.setAttribute('height', height);
803
+ rect.setAttribute('rx', '4');
804
+ rect.setAttribute('filter', 'url(#glow-yellow)');
805
+ g.appendChild(rect);
806
+
807
+ // File icon
808
+ const icon = document.createElementNS('http://www.w3.org/2000/svg', 'text');
809
+ icon.setAttribute('x', '10');
810
+ icon.setAttribute('y', '17');
811
+ icon.setAttribute('font-size', '11');
812
+ icon.textContent = this.getFileIcon(node.path?.split('.').pop() || '');
813
+ g.appendChild(icon);
814
+
815
+ // File name
816
+ const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
817
+ label.setAttribute('class', 'node-label file-label');
818
+ label.setAttribute('x', '26');
819
+ label.setAttribute('y', '17');
820
+ label.setAttribute('font-size', '10');
821
+ label.textContent = node.label.length > 16 ? node.label.slice(0, 14) + '...' : node.label;
822
+ g.appendChild(label);
823
+
824
+ // M badge
825
+ const badge = document.createElementNS('http://www.w3.org/2000/svg', 'text');
826
+ badge.setAttribute('x', width - 12);
827
+ badge.setAttribute('y', '17');
828
+ badge.setAttribute('text-anchor', 'middle');
829
+ badge.setAttribute('font-size', '9');
830
+ badge.setAttribute('font-weight', '700');
831
+ badge.setAttribute('fill', 'hsl(45, 93%, 58%)');
832
+ badge.textContent = 'M';
833
+ g.appendChild(badge);
834
+ } else {
835
+ // Render directory node
836
+ const width = 160;
837
+ const height = 50;
838
+ g.setAttribute('transform', `translate(${node.x - width/2}, ${node.y - height/2})`);
839
+
840
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
841
+ rect.setAttribute('class', 'node-bg');
842
+ rect.setAttribute('width', width);
843
+ rect.setAttribute('height', height);
844
+ rect.setAttribute('rx', '8');
845
+ if (hasChanges) {
846
+ rect.setAttribute('filter', 'url(#glow-yellow)');
847
+ } else if (isDirectlyActive) {
848
+ rect.setAttribute('filter', 'url(#glow-strong)');
849
+ }
850
+ g.appendChild(rect);
851
+
852
+ // Icon background
853
+ const iconBg = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
854
+ iconBg.setAttribute('cx', '25');
855
+ iconBg.setAttribute('cy', '25');
856
+ iconBg.setAttribute('r', '16');
857
+ iconBg.setAttribute('class', 'node-icon-bg');
858
+ g.appendChild(iconBg);
859
+
860
+ // Icon
861
+ const icon = document.createElementNS('http://www.w3.org/2000/svg', 'text');
862
+ icon.setAttribute('class', 'node-icon');
863
+ icon.setAttribute('x', '25');
864
+ icon.setAttribute('y', '30');
865
+ icon.setAttribute('text-anchor', 'middle');
866
+ icon.setAttribute('font-size', '14');
867
+ icon.textContent = this.getNodeIcon(node.type);
868
+ g.appendChild(icon);
869
+
870
+ // Label
871
+ const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
872
+ label.setAttribute('class', 'node-label');
873
+ label.setAttribute('x', '48');
874
+ label.setAttribute('y', '20');
875
+ label.setAttribute('font-size', '11');
876
+ label.textContent = node.label.length > 12 ? node.label.slice(0, 10) + '...' : node.label;
877
+ g.appendChild(label);
878
+
879
+ // Stats line
880
+ const stats = document.createElementNS('http://www.w3.org/2000/svg', 'text');
881
+ stats.setAttribute('class', 'node-type');
882
+ stats.setAttribute('x', '48');
883
+ stats.setAttribute('y', '34');
884
+ stats.setAttribute('font-size', '9');
885
+ const statsText = `${node.fileCount || 0} files, ${node.dirCount || 0} dirs`;
886
+ stats.textContent = statsText;
887
+ g.appendChild(stats);
888
+
889
+ // Changed badge
890
+ if (hasChanges && node.changedCount > 0) {
891
+ const badgeBg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
892
+ badgeBg.setAttribute('x', width - 35);
893
+ badgeBg.setAttribute('y', '5');
894
+ badgeBg.setAttribute('width', '30');
895
+ badgeBg.setAttribute('height', '16');
896
+ badgeBg.setAttribute('rx', '8');
897
+ badgeBg.setAttribute('class', 'change-badge');
898
+ g.appendChild(badgeBg);
899
+
900
+ const badgeText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
901
+ badgeText.setAttribute('x', width - 20);
902
+ badgeText.setAttribute('y', '16');
903
+ badgeText.setAttribute('text-anchor', 'middle');
904
+ badgeText.setAttribute('font-size', '9');
905
+ badgeText.setAttribute('font-weight', '600');
906
+ badgeText.setAttribute('fill', 'hsl(222, 47%, 11%)');
907
+ badgeText.textContent = node.changedCount;
908
+ g.appendChild(badgeText);
909
+ }
910
+ }
911
+
912
+ // Hover events
913
+ g.addEventListener('mouseenter', (e) => this.showTooltip(e, node));
914
+ g.addEventListener('mousemove', (e) => this.moveTooltip(e));
915
+ g.addEventListener('mouseleave', () => this.hideTooltip());
916
+
917
+ // Click to show diff
918
+ g.addEventListener('click', (e) => {
919
+ e.stopPropagation();
920
+ this.showFileDiff(node.path);
921
+ });
922
+
923
+ nodesGroup.appendChild(g);
924
+ });
925
+
926
+ // Setup interactions
927
+ this.setupGraphInteraction(container);
928
+
929
+ // Zoom controls
930
+ document.getElementById('zoom-in').addEventListener('click', () => {
931
+ this.zoom = Math.min(3, this.zoom * 1.2);
932
+ this.updateGraphTransform();
933
+ });
934
+
935
+ document.getElementById('zoom-out').addEventListener('click', () => {
936
+ this.zoom = Math.max(0.3, this.zoom / 1.2);
937
+ this.updateGraphTransform();
938
+ });
939
+
940
+ document.getElementById('zoom-reset').addEventListener('click', () => {
941
+ this.zoom = 1;
942
+ this.panX = 0;
943
+ this.panY = 0;
944
+ this.updateGraphTransform();
945
+ });
946
+ }
947
+
948
+ getNodeIcon(type) {
949
+ const icons = {
950
+ folder: '📁',
951
+ server: '🖥️',
952
+ client: '💻',
953
+ database: '🗄️',
954
+ assets: '🖼️',
955
+ config: '⚙️',
956
+ code: '📄',
957
+ style: '🎨',
958
+ docs: '📝'
959
+ };
960
+ return icons[type] || '📄';
961
+ }
962
+
963
+ renderFileExplorer(container) {
964
+ container.innerHTML = `
965
+ <div class="file-explorer">
966
+ <div class="file-tree">
967
+ <div class="file-tree-header">
968
+ <h3>EXPLORER</h3>
969
+ </div>
970
+ <div class="file-tree-content" id="file-tree-content"></div>
971
+ </div>
972
+ </div>
973
+ `;
974
+
975
+ const content = document.getElementById('file-tree-content');
976
+ this.renderTreeNode(this.structure, content, 0);
977
+ }
978
+
979
+ renderTreeNode(node, container, depth) {
980
+ const isActive = this.isPathActive(node.path);
981
+ const isDirectlyActive = this.activeFiles.has(node.path);
982
+ const isChanged = this.isPathChanged(node.path);
983
+ const isExpanded = this.expandedNodes.has(node.path);
984
+ const hasChildren = node.type === 'directory' && node.children?.length > 0;
985
+
986
+ // For directories, check if any children are changed
987
+ let hasChangedChildren = false;
988
+ if (hasChildren) {
989
+ const countChanges = (n) => {
990
+ if (this.isPathChanged(n.path)) return true;
991
+ if (n.children) return n.children.some(countChanges);
992
+ return false;
993
+ };
994
+ hasChangedChildren = node.children.some(countChanges);
995
+ }
996
+
997
+ const item = document.createElement('div');
998
+ const classes = ['tree-item'];
999
+ if (isActive) classes.push('active');
1000
+ if (isDirectlyActive) classes.push('directly-active');
1001
+ if (isChanged || hasChangedChildren) classes.push('has-changes');
1002
+ item.className = classes.join(' ');
1003
+ item.style.paddingLeft = `${depth * 16 + 8}px`;
1004
+
1005
+ const chevron = hasChildren ? (isExpanded ? '▼' : '▶') : ' ';
1006
+ const icon = node.type === 'directory' ? '📁' : this.getFileIcon(node.extension);
1007
+
1008
+ item.innerHTML = `
1009
+ <span class="tree-chevron ${isExpanded ? 'expanded' : ''}">${chevron}</span>
1010
+ <span class="tree-item-icon">${icon}</span>
1011
+ <span class="tree-item-name">${node.name}</span>
1012
+ ${isChanged ? '<span class="tree-item-badge yellow-pulse">M</span>' : ''}
1013
+ ${hasChangedChildren && !isChanged ? '<span class="tree-item-badge yellow-pulse">•</span>' : ''}
1014
+ ${isDirectlyActive ? '<span class="tree-pulse-dot green"></span>' : ''}
1015
+ `;
1016
+
1017
+ item.addEventListener('click', (e) => {
1018
+ if (hasChildren) {
1019
+ if (this.expandedNodes.has(node.path)) {
1020
+ this.expandedNodes.delete(node.path);
1021
+ } else {
1022
+ this.expandedNodes.add(node.path);
1023
+ }
1024
+ this.render();
1025
+ } else {
1026
+ // File clicked - show diff
1027
+ this.showFileDiff(node.path);
1028
+ }
1029
+ });
1030
+
1031
+ // Double click on directory to show diff for modified files in it
1032
+ if (hasChildren) {
1033
+ item.addEventListener('dblclick', (e) => {
1034
+ e.stopPropagation();
1035
+ this.showFileDiff(node.path);
1036
+ });
1037
+ }
1038
+
1039
+ container.appendChild(item);
1040
+
1041
+ if (hasChildren && isExpanded) {
1042
+ const childContainer = document.createElement('div');
1043
+ childContainer.className = 'tree-children';
1044
+ node.children.forEach(child => this.renderTreeNode(child, childContainer, depth + 1));
1045
+ container.appendChild(childContainer);
1046
+ }
1047
+ }
1048
+
1049
+ getFileIcon(ext) {
1050
+ const icons = {
1051
+ js: '📜', ts: '📘', tsx: '⚛️', jsx: '⚛️',
1052
+ css: '🎨', scss: '🎨', html: '🌐',
1053
+ json: '📋', md: '📝', py: '🐍'
1054
+ };
1055
+ return icons[ext] || '📄';
1056
+ }
1057
+
1058
+ renderSunburstView(container) {
1059
+ const width = container.clientWidth || 800;
1060
+ const height = container.clientHeight || 600;
1061
+ const centerX = width / 2;
1062
+ const centerY = height / 2;
1063
+ const maxRadius = Math.min(width, height) / 2 - 100;
1064
+
1065
+ container.innerHTML = `
1066
+ <div class="sunburst-container">
1067
+ <svg class="sunburst-svg" viewBox="0 0 ${width} ${height}">
1068
+ <defs>
1069
+ <filter id="sunburst-glow-green" x="-50%" y="-50%" width="200%" height="200%">
1070
+ <feFlood flood-color="hsl(150, 100%, 50%)" flood-opacity="0.6" result="flood"/>
1071
+ <feComposite in="flood" in2="SourceGraphic" operator="in" result="mask"/>
1072
+ <feGaussianBlur in="mask" stdDeviation="6" result="coloredBlur"/>
1073
+ <feMerge>
1074
+ <feMergeNode in="coloredBlur"/>
1075
+ <feMergeNode in="SourceGraphic"/>
1076
+ </feMerge>
1077
+ </filter>
1078
+ <filter id="sunburst-glow-yellow" x="-50%" y="-50%" width="200%" height="200%">
1079
+ <feFlood flood-color="hsl(45, 100%, 55%)" flood-opacity="0.7" result="flood"/>
1080
+ <feComposite in="flood" in2="SourceGraphic" operator="in" result="mask"/>
1081
+ <feGaussianBlur in="mask" stdDeviation="8" result="coloredBlur"/>
1082
+ <feMerge>
1083
+ <feMergeNode in="coloredBlur"/>
1084
+ <feMergeNode in="SourceGraphic"/>
1085
+ </feMerge>
1086
+ </filter>
1087
+ </defs>
1088
+ </svg>
1089
+ <div class="sunburst-legend">
1090
+ <h4>STATUS</h4>
1091
+ <div class="legend-items">
1092
+ <div class="legend-row"><span class="legend-swatch yellow-glow" style="background-color: hsl(45, 93%, 50%); display: inline-block;"></span>Pending Changes (Yellow)</div>
1093
+ <div class="legend-row"><span class="legend-swatch green-glow" style="background-color: hsl(150, 80%, 45%); display: inline-block;"></span>Currently Editing (Green)</div>
1094
+ <div class="legend-row"><span class="legend-swatch" style="background-color: hsl(217, 15%, 18%); display: inline-block; opacity: 0.4;"></span>Unchanged (Muted)</div>
1095
+ </div>
1096
+ </div>
1097
+ </div>
1098
+ `;
1099
+
1100
+ const svg = container.querySelector('.sunburst-svg');
1101
+
1102
+ const calculateSize = (node) => {
1103
+ if (node.type === 'file') return 1;
1104
+ if (!node.children?.length) return 1;
1105
+ return node.children.reduce((sum, child) => sum + calculateSize(child), 0);
1106
+ };
1107
+
1108
+ const drawArc = (node, startAngle, endAngle, depth, maxDepth = 4) => {
1109
+ if (depth > maxDepth) return;
1110
+
1111
+ const innerRadius = depth === 0 ? 0 : 60 + (depth - 1) * 50;
1112
+ const outerRadius = depth === 0 ? 60 : innerRadius + 45;
1113
+ const clampedOuter = Math.min(outerRadius, maxRadius);
1114
+
1115
+ if (clampedOuter <= innerRadius) return;
1116
+
1117
+ const isActive = this.isPathActive(node.path);
1118
+ const isDirectlyActive = this.activeFiles.has(node.path);
1119
+ const isChanged = this.isPathChanged(node.path);
1120
+
1121
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1122
+ path.setAttribute('d', this.createArcPath(centerX, centerY, innerRadius, clampedOuter, startAngle, endAngle));
1123
+
1124
+ // Color logic:
1125
+ // - Yellow for pending changes (git diff)
1126
+ // - Green for currently editing (active)
1127
+ // - Muted for unchanged
1128
+ if (isChanged) {
1129
+ // YELLOW for pending changes
1130
+ path.setAttribute('fill', 'hsl(45, 93%, 50%)');
1131
+ path.setAttribute('stroke', 'hsl(45, 100%, 60%)');
1132
+ path.setAttribute('stroke-width', '3');
1133
+ path.setAttribute('opacity', '1');
1134
+ } else if (isDirectlyActive) {
1135
+ // GREEN for currently editing
1136
+ path.setAttribute('fill', 'hsl(150, 80%, 45%)');
1137
+ path.setAttribute('stroke', 'hsl(150, 100%, 50%)');
1138
+ path.setAttribute('stroke-width', '4');
1139
+ path.setAttribute('opacity', '1');
1140
+ } else if (isActive) {
1141
+ // Slightly highlighted for parent of active
1142
+ path.setAttribute('fill', this.getArcColor(node));
1143
+ path.setAttribute('stroke', 'hsl(150, 60%, 40%)');
1144
+ path.setAttribute('stroke-width', '2');
1145
+ path.setAttribute('opacity', '0.8');
1146
+ } else {
1147
+ // Muted but visible for unchanged
1148
+ path.setAttribute('fill', this.getMutedColor(node));
1149
+ path.setAttribute('stroke', 'hsl(222, 20%, 25%)');
1150
+ path.setAttribute('stroke-width', '1');
1151
+ path.setAttribute('opacity', '0.5');
1152
+ }
1153
+
1154
+ const classes = ['sunburst-arc'];
1155
+ if (isDirectlyActive) {
1156
+ classes.push('directly-active');
1157
+ path.setAttribute('filter', 'url(#sunburst-glow-green)');
1158
+ } else if (isChanged) {
1159
+ classes.push('changed');
1160
+ path.setAttribute('filter', 'url(#sunburst-glow-yellow)');
1161
+ }
1162
+ path.setAttribute('class', classes.join(' '));
1163
+
1164
+ path.addEventListener('mouseenter', (e) => this.showTooltip(e, node));
1165
+ path.addEventListener('mousemove', (e) => this.moveTooltip(e));
1166
+ path.addEventListener('mouseleave', () => this.hideTooltip());
1167
+
1168
+ // Click to show diff
1169
+ path.addEventListener('click', (e) => {
1170
+ e.stopPropagation();
1171
+ this.showFileDiff(node.path);
1172
+ });
1173
+
1174
+ svg.appendChild(path);
1175
+
1176
+ if (node.children?.length) {
1177
+ const totalSize = node.children.reduce((sum, child) => sum + calculateSize(child), 0);
1178
+ let currentAngle = startAngle;
1179
+
1180
+ node.children.forEach(child => {
1181
+ const childSize = calculateSize(child);
1182
+ const childAngle = (childSize / totalSize) * (endAngle - startAngle);
1183
+ drawArc(child, currentAngle, currentAngle + childAngle, depth + 1, maxDepth);
1184
+ currentAngle += childAngle;
1185
+ });
1186
+ }
1187
+ };
1188
+
1189
+ drawArc(this.structure, 0, Math.PI * 2, 0);
1190
+
1191
+ // Center label
1192
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
1193
+ text.setAttribute('x', centerX);
1194
+ text.setAttribute('y', centerY);
1195
+ text.setAttribute('text-anchor', 'middle');
1196
+ text.setAttribute('dominant-baseline', 'middle');
1197
+ text.setAttribute('fill', 'white');
1198
+ text.setAttribute('font-size', '14');
1199
+ text.setAttribute('font-weight', '600');
1200
+ text.setAttribute('font-family', 'Orbitron, sans-serif');
1201
+ text.textContent = this.structure.name;
1202
+ svg.appendChild(text);
1203
+ }
1204
+
1205
+ createArcPath(cx, cy, innerRadius, outerRadius, startAngle, endAngle) {
1206
+ const start = startAngle - Math.PI / 2;
1207
+ const end = endAngle - Math.PI / 2;
1208
+
1209
+ const x1 = cx + innerRadius * Math.cos(start);
1210
+ const y1 = cy + innerRadius * Math.sin(start);
1211
+ const x2 = cx + outerRadius * Math.cos(start);
1212
+ const y2 = cy + outerRadius * Math.sin(start);
1213
+ const x3 = cx + outerRadius * Math.cos(end);
1214
+ const y3 = cy + outerRadius * Math.sin(end);
1215
+ const x4 = cx + innerRadius * Math.cos(end);
1216
+ const y4 = cy + innerRadius * Math.sin(end);
1217
+
1218
+ const largeArc = endAngle - startAngle > Math.PI ? 1 : 0;
1219
+
1220
+ if (innerRadius === 0) {
1221
+ return `M ${cx} ${cy} L ${x2} ${y2} A ${outerRadius} ${outerRadius} 0 ${largeArc} 1 ${x3} ${y3} Z`;
1222
+ }
1223
+
1224
+ return `M ${x1} ${y1} L ${x2} ${y2} A ${outerRadius} ${outerRadius} 0 ${largeArc} 1 ${x3} ${y3} L ${x4} ${y4} A ${innerRadius} ${innerRadius} 0 ${largeArc} 0 ${x1} ${y1} Z`;
1225
+ }
1226
+
1227
+ getArcColor(node) {
1228
+ if (node.type === 'directory') return 'hsl(217, 91%, 50%)';
1229
+ const colors = {
1230
+ code: 'hsl(142, 76%, 45%)',
1231
+ style: 'hsl(330, 80%, 55%)',
1232
+ config: 'hsl(45, 90%, 50%)',
1233
+ docs: 'hsl(200, 90%, 50%)',
1234
+ markup: 'hsl(280, 70%, 55%)',
1235
+ data: 'hsl(170, 70%, 45%)',
1236
+ image: 'hsl(25, 80%, 50%)'
1237
+ };
1238
+ return colors[node.category] || 'hsl(215, 20%, 45%)';
1239
+ }
1240
+
1241
+ getMutedColor(node) {
1242
+ // Return desaturated but visible versions for unchanged items
1243
+ if (node.type === 'directory') return 'hsl(217, 20%, 28%)';
1244
+ const mutedColors = {
1245
+ code: 'hsl(142, 15%, 28%)',
1246
+ style: 'hsl(330, 15%, 30%)',
1247
+ config: 'hsl(45, 15%, 30%)',
1248
+ docs: 'hsl(200, 15%, 30%)',
1249
+ markup: 'hsl(280, 12%, 30%)',
1250
+ data: 'hsl(170, 12%, 28%)',
1251
+ image: 'hsl(25, 15%, 30%)'
1252
+ };
1253
+ return mutedColors[node.category] || 'hsl(215, 12%, 26%)';
1254
+ }
1255
+
1256
+ showTooltip(e, node) {
1257
+ const type = node.type === 'directory' ? 'Directory' : (node.category || 'File');
1258
+ const childInfo = node.childCount ? ` (${node.childCount} items)` : (node.children?.length ? ` (${node.children.length} items)` : '');
1259
+
1260
+ this.tooltip.innerHTML = `
1261
+ <strong>${node.label || node.name}</strong>
1262
+ <div class="tooltip-type">${type}${childInfo}</div>
1263
+ <div class="tooltip-path">${node.path}</div>
1264
+ `;
1265
+ this.tooltip.style.display = 'block';
1266
+ this.moveTooltip(e);
1267
+ }
1268
+
1269
+ moveTooltip(e) {
1270
+ this.tooltip.style.left = (e.clientX + 15) + 'px';
1271
+ this.tooltip.style.top = (e.clientY + 15) + 'px';
1272
+ }
1273
+
1274
+ hideTooltip() {
1275
+ this.tooltip.style.display = 'none';
1276
+ }
1277
+ }
1278
+
1279
+ // Initialize
1280
+ document.addEventListener('DOMContentLoaded', () => {
1281
+ window.seeclaude = new SeeClaude();
1282
+ });