mcp-vector-search 0.12.1__py3-none-any.whl → 0.12.3__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.

Potentially problematic release.


This version of mcp-vector-search might be problematic. Click here for more details.

Files changed (32) hide show
  1. mcp_vector_search/__init__.py +2 -2
  2. mcp_vector_search/cli/commands/visualize.py +22 -12
  3. mcp_vector_search/visualization/favicon-v1-1024.png +0 -0
  4. mcp_vector_search/visualization/favicon-v1-128.png +0 -0
  5. mcp_vector_search/visualization/favicon-v1-16.png +0 -0
  6. mcp_vector_search/visualization/favicon-v1-256.png +0 -0
  7. mcp_vector_search/visualization/favicon-v1-32.png +0 -0
  8. mcp_vector_search/visualization/favicon-v1-512.png +0 -0
  9. mcp_vector_search/visualization/favicon-v1-64.png +0 -0
  10. mcp_vector_search/visualization/favicon-v1.ico +0 -0
  11. mcp_vector_search/visualization/favicon-v2-1024.png +0 -0
  12. mcp_vector_search/visualization/favicon-v2-128.png +0 -0
  13. mcp_vector_search/visualization/favicon-v2-16.png +0 -0
  14. mcp_vector_search/visualization/favicon-v2-256.png +0 -0
  15. mcp_vector_search/visualization/favicon-v2-32.png +0 -0
  16. mcp_vector_search/visualization/favicon-v2-512.png +0 -0
  17. mcp_vector_search/visualization/favicon-v2-64.png +0 -0
  18. mcp_vector_search/visualization/favicon-v2.ico +0 -0
  19. mcp_vector_search/visualization/favicon-v3-1024.png +0 -0
  20. mcp_vector_search/visualization/favicon-v3-128.png +0 -0
  21. mcp_vector_search/visualization/favicon-v3-16.png +0 -0
  22. mcp_vector_search/visualization/favicon-v3-256.png +0 -0
  23. mcp_vector_search/visualization/favicon-v3-32.png +0 -0
  24. mcp_vector_search/visualization/favicon-v3-512.png +0 -0
  25. mcp_vector_search/visualization/favicon-v3-64.png +0 -0
  26. mcp_vector_search/visualization/favicon-v3.ico +0 -0
  27. mcp_vector_search/visualization/index.html +1592 -193
  28. {mcp_vector_search-0.12.1.dist-info → mcp_vector_search-0.12.3.dist-info}/METADATA +62 -2
  29. {mcp_vector_search-0.12.1.dist-info → mcp_vector_search-0.12.3.dist-info}/RECORD +32 -8
  30. {mcp_vector_search-0.12.1.dist-info → mcp_vector_search-0.12.3.dist-info}/WHEEL +0 -0
  31. {mcp_vector_search-0.12.1.dist-info → mcp_vector_search-0.12.3.dist-info}/entry_points.txt +0 -0
  32. {mcp_vector_search-0.12.1.dist-info → mcp_vector_search-0.12.3.dist-info}/licenses/LICENSE +0 -0
@@ -2,10 +2,15 @@
2
2
  <html>
3
3
  <head>
4
4
  <meta charset="utf-8">
5
- <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
6
- <meta http-equiv="Pragma" content="no-cache">
7
- <meta http-equiv="Expires" content="0">
8
5
  <title>Code Chunk Relationship Graph</title>
6
+
7
+ <!-- Favicon -->
8
+ <link rel="icon" type="image/x-icon" href="favicon-v1.ico">
9
+ <link rel="icon" type="image/png" sizes="16x16" href="favicon-v1-16.png">
10
+ <link rel="icon" type="image/png" sizes="32x32" href="favicon-v1-32.png">
11
+ <link rel="icon" type="image/png" sizes="64x64" href="favicon-v1-64.png">
12
+ <link rel="apple-touch-icon" sizes="180x180" href="favicon-v1-512.png">
13
+
9
14
  <script src="https://d3js.org/d3.v7.min.js"></script>
10
15
  <style>
11
16
  body {
@@ -80,26 +85,43 @@
80
85
  cursor: pointer;
81
86
  stroke: #c9d1d9;
82
87
  stroke-width: 1.5px;
88
+ transition: all 0.2s ease;
89
+ }
90
+
91
+ .node circle:hover,
92
+ .node rect:hover {
93
+ stroke-width: 3px !important;
94
+ filter: brightness(1.2);
95
+ cursor: pointer;
83
96
  }
84
97
 
85
- .node.directory circle { fill: #ffa657; }
86
- .node.file circle { fill: #58a6ff; }
87
98
  .node.module circle { fill: #238636; }
88
99
  .node.class circle { fill: #1f6feb; }
89
100
  .node.function circle { fill: #d29922; }
90
101
  .node.method circle { fill: #8957e5; }
91
- .node.imports circle { fill: #6e7681; }
92
- .node.text circle { fill: #6e7681; }
93
102
  .node.code circle { fill: #6e7681; }
103
+ .node.file circle {
104
+ fill: none;
105
+ stroke: #58a6ff;
106
+ stroke-width: 2px;
107
+ stroke-dasharray: 5,3;
108
+ opacity: 0.6;
109
+ }
110
+ .node.directory circle {
111
+ fill: none;
112
+ stroke: #79c0ff;
113
+ stroke-width: 2px;
114
+ stroke-dasharray: 3,3;
115
+ opacity: 0.5;
116
+ }
94
117
  .node.subproject circle { fill: #da3633; stroke-width: 3px; }
95
118
 
96
- .node-icon {
97
- font-size: 35px !important;
98
- text-anchor: middle;
99
- pointer-events: none;
100
- user-select: none;
101
- fill: #0d1117;
102
- font-weight: bold;
119
+ /* Non-code document nodes - squares */
120
+ .node.docstring rect { fill: #8b949e; }
121
+ .node.comment rect { fill: #6e7681; }
122
+ .node rect {
123
+ stroke: #c9d1d9;
124
+ stroke-width: 1.5px;
103
125
  }
104
126
 
105
127
  .node text {
@@ -110,16 +132,25 @@
110
132
  user-select: none;
111
133
  }
112
134
 
135
+ .node-icon {
136
+ font-size: 35px !important;
137
+ text-anchor: middle;
138
+ pointer-events: none;
139
+ user-select: none;
140
+ fill: #0d1117;
141
+ font-weight: bold;
142
+ }
143
+
113
144
  .link {
114
145
  stroke: #58a6ff;
115
- stroke-opacity: 0.4;
116
- stroke-width: 2px;
146
+ stroke-opacity: 0.8;
147
+ stroke-width: 3px;
117
148
  }
118
149
 
119
150
  .link.dependency {
120
151
  stroke: #d29922;
121
152
  stroke-opacity: 0.8;
122
- stroke-width: 2px;
153
+ stroke-width: 3px;
123
154
  stroke-dasharray: 5,5;
124
155
  }
125
156
 
@@ -144,77 +175,167 @@
144
175
  color: #8b949e;
145
176
  }
146
177
 
147
- #code-viewer {
148
- position: absolute;
149
- top: 20px;
150
- right: 20px;
151
- width: 500px;
152
- max-height: 80vh;
153
- background: rgba(13, 17, 23, 0.95);
154
- border: 1px solid #30363d;
155
- border-radius: 6px;
156
- padding: 16px;
178
+ #content-pane {
179
+ position: fixed;
180
+ top: 0;
181
+ right: 0;
182
+ width: 600px;
183
+ height: 100vh;
184
+ background: rgba(13, 17, 23, 0.98);
185
+ border-left: 1px solid #30363d;
157
186
  overflow-y: auto;
158
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
159
- display: none;
187
+ box-shadow: -4px 0 24px rgba(0, 0, 0, 0.5);
188
+ transform: translateX(100%);
189
+ transition: transform 0.3s ease-in-out;
190
+ z-index: 1000;
160
191
  }
161
192
 
162
- #code-viewer.visible {
163
- display: block;
193
+ #content-pane.visible {
194
+ transform: translateX(0);
164
195
  }
165
196
 
166
- #code-viewer .header {
167
- margin-bottom: 12px;
168
- padding-bottom: 12px;
197
+ #content-pane .pane-header {
198
+ position: sticky;
199
+ top: 0;
200
+ background: rgba(13, 17, 23, 0.98);
201
+ padding: 20px;
169
202
  border-bottom: 1px solid #30363d;
203
+ z-index: 1;
170
204
  }
171
205
 
172
- #code-viewer .title {
173
- font-size: 14px;
206
+ #content-pane .pane-title {
207
+ font-size: 16px;
174
208
  font-weight: bold;
175
209
  color: #58a6ff;
176
- margin-bottom: 4px;
210
+ margin-bottom: 8px;
211
+ padding-right: 30px;
177
212
  }
178
213
 
179
- #code-viewer .meta {
180
- font-size: 11px;
214
+ #content-pane .pane-meta {
215
+ font-size: 12px;
181
216
  color: #8b949e;
182
217
  }
183
218
 
184
- #code-viewer .close-btn {
185
- float: right;
219
+ #content-pane .collapse-btn {
220
+ position: absolute;
221
+ top: 20px;
222
+ right: 20px;
186
223
  cursor: pointer;
187
224
  color: #8b949e;
188
- font-size: 20px;
225
+ font-size: 24px;
189
226
  line-height: 1;
190
- margin-top: -4px;
227
+ background: none;
228
+ border: none;
229
+ padding: 0;
230
+ transition: color 0.2s;
191
231
  }
192
232
 
193
- #code-viewer .close-btn:hover {
233
+ #content-pane .collapse-btn:hover {
194
234
  color: #c9d1d9;
195
235
  }
196
236
 
197
- #code-viewer pre {
237
+ #content-pane .pane-content {
238
+ padding: 20px;
239
+ }
240
+
241
+ #content-pane pre {
198
242
  margin: 0;
199
- padding: 12px;
243
+ padding: 16px;
200
244
  background: #0d1117;
201
245
  border: 1px solid #30363d;
202
246
  border-radius: 6px;
203
247
  overflow-x: auto;
204
- font-size: 11px;
205
- line-height: 1.5;
248
+ font-size: 12px;
249
+ line-height: 1.6;
206
250
  }
207
251
 
208
- #code-viewer code {
252
+ #content-pane code {
209
253
  color: #c9d1d9;
210
254
  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
211
255
  }
212
256
 
213
- .node.highlighted circle {
257
+ #content-pane .directory-list {
258
+ list-style: none;
259
+ padding: 0;
260
+ margin: 0;
261
+ }
262
+
263
+ #content-pane .directory-list li {
264
+ padding: 8px 12px;
265
+ margin: 4px 0;
266
+ background: #161b22;
267
+ border: 1px solid #30363d;
268
+ border-radius: 4px;
269
+ font-size: 12px;
270
+ display: flex;
271
+ align-items: center;
272
+ }
273
+
274
+ #content-pane .directory-list .item-icon {
275
+ margin-right: 8px;
276
+ font-size: 14px;
277
+ }
278
+
279
+ #content-pane .directory-list .item-type {
280
+ margin-left: auto;
281
+ padding-left: 12px;
282
+ font-size: 10px;
283
+ color: #8b949e;
284
+ }
285
+
286
+ #content-pane .import-details {
287
+ background: #161b22;
288
+ border: 1px solid #30363d;
289
+ border-radius: 6px;
290
+ padding: 16px;
291
+ }
292
+
293
+ #content-pane .import-details .import-statement {
294
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
295
+ font-size: 12px;
296
+ color: #79c0ff;
297
+ margin-bottom: 12px;
298
+ }
299
+
300
+ #content-pane .import-details .detail-row {
301
+ font-size: 11px;
302
+ color: #8b949e;
303
+ margin: 4px 0;
304
+ }
305
+
306
+ #content-pane .import-details .detail-label {
307
+ color: #c9d1d9;
308
+ font-weight: 600;
309
+ }
310
+
311
+ .node.highlighted circle,
312
+ .node.highlighted rect {
214
313
  stroke: #f0e68c;
215
314
  stroke-width: 3px;
216
315
  filter: drop-shadow(0 0 8px #f0e68c);
217
316
  }
317
+
318
+ .control-button {
319
+ padding: 8px 12px;
320
+ background: #21262d;
321
+ border: 1px solid #30363d;
322
+ border-radius: 6px;
323
+ color: #c9d1d9;
324
+ font-size: 12px;
325
+ cursor: pointer;
326
+ transition: background-color 0.2s;
327
+ margin-top: 8px;
328
+ width: 100%;
329
+ }
330
+
331
+ .control-button:hover {
332
+ background: #30363d;
333
+ }
334
+
335
+ .control-button:disabled {
336
+ opacity: 0.5;
337
+ cursor: not-allowed;
338
+ }
218
339
  </style>
219
340
  </head>
220
341
  <body>
@@ -225,28 +346,44 @@
225
346
  <label>⏳ Loading graph data...</label>
226
347
  </div>
227
348
 
349
+ <div class="control-group">
350
+ <button id="focus-button" class="control-button" disabled>📍 Focus on Selected</button>
351
+ </div>
352
+
228
353
  <h3>Legend</h3>
229
354
  <div class="legend">
230
355
  <div class="legend-item">
231
- 📁 <span class="legend-color" style="background: #ffa657;"></span> Directory
356
+ <span class="legend-color" style="background: #da3633;"></span> Subproject
232
357
  </div>
233
358
  <div class="legend-item">
234
- 📄 <span class="legend-color" style="background: #58a6ff;"></span> File
359
+ <span class="legend-color" style="border: 2px dashed #79c0ff; border-radius: 50%; background: transparent;"></span> Directory
235
360
  </div>
236
361
  <div class="legend-item">
237
- C <span class="legend-color" style="background: #1f6feb;"></span> Class
362
+ <span class="legend-color" style="border: 2px dashed #58a6ff; border-radius: 50%; background: transparent;"></span> File
238
363
  </div>
239
364
  <div class="legend-item">
240
- ƒ <span class="legend-color" style="background: #d29922;"></span> Function
365
+ <span class="legend-color" style="background: #238636;"></span> Module
241
366
  </div>
242
367
  <div class="legend-item">
243
- m <span class="legend-color" style="background: #8957e5;"></span> Method
368
+ <span class="legend-color" style="background: #1f6feb;"></span> Class
244
369
  </div>
245
370
  <div class="legend-item">
246
- M <span class="legend-color" style="background: #238636;"></span> Module
371
+ <span class="legend-color" style="background: #d29922;"></span> Function
247
372
  </div>
248
373
  <div class="legend-item">
249
- <span class="legend-color" style="background: #6e7681;"></span> Text/Other
374
+ <span class="legend-color" style="background: #8957e5;"></span> Method
375
+ </div>
376
+ <div class="legend-item">
377
+ <span class="legend-color" style="background: #6e7681;"></span> Code
378
+ </div>
379
+ <div class="legend-item" style="font-style: italic; color: #79c0ff;">
380
+ <span class="legend-color" style="background: #6e7681;"></span> Import
381
+ </div>
382
+ <div class="legend-item">
383
+ <span class="legend-color" style="background: #8b949e; border-radius: 2px;"></span> Docstring ▢
384
+ </div>
385
+ <div class="legend-item">
386
+ <span class="legend-color" style="background: #6e7681; border-radius: 2px;"></span> Comment ▢
250
387
  </div>
251
388
  </div>
252
389
 
@@ -261,13 +398,13 @@
261
398
  <svg id="graph"></svg>
262
399
  <div id="tooltip" class="tooltip"></div>
263
400
 
264
- <div id="code-viewer">
265
- <div class="header">
266
- <span class="close-btn" onclick="closeCodeViewer()">×</span>
267
- <div class="title" id="viewer-title"></div>
268
- <div class="meta" id="viewer-meta"></div>
401
+ <div id="content-pane">
402
+ <div class="pane-header">
403
+ <button class="collapse-btn" onclick="closeContentPane()">×</button>
404
+ <div class="pane-title" id="pane-title"></div>
405
+ <div class="pane-meta" id="pane-meta"></div>
269
406
  </div>
270
- <pre><code id="viewer-code"></code></pre>
407
+ <div class="pane-content" id="pane-content"></div>
271
408
  </div>
272
409
 
273
410
  <script>
@@ -276,28 +413,52 @@
276
413
 
277
414
  const svg = d3.select("#graph")
278
415
  .attr("width", width)
279
- .attr("height", height)
280
- .call(d3.zoom().on("zoom", (event) => {
281
- g.attr("transform", event.transform);
282
- }));
416
+ .attr("height", height);
283
417
 
284
418
  const g = svg.append("g");
285
419
  const tooltip = d3.select("#tooltip");
420
+
421
+ const zoom = d3.zoom().on("zoom", (event) => {
422
+ g.attr("transform", event.transform);
423
+ });
424
+ svg.call(zoom);
425
+
286
426
  let simulation;
287
427
  let allNodes = [];
288
428
  let allLinks = [];
289
- let allLinksOriginal = []; // Keep original link structure before D3 modifies it
290
429
  let visibleNodes = new Set();
291
430
  let collapsedNodes = new Set();
292
431
  let highlightedNode = null;
432
+ let currentNode = null; // Track currently selected node
433
+
434
+ // Add arrow markers for different relationship types
435
+ const defs = svg.append("defs");
436
+
437
+ const markerTypes = [
438
+ {id: "arrow-directory", color: "#58a6ff"}, // Blue for folder→file
439
+ {id: "arrow-file", color: "#8b949e"}, // Gray for file→section
440
+ {id: "arrow-ast", color: "#a371f7"} // Purple for section→AST
441
+ ];
442
+
443
+ markerTypes.forEach(type => {
444
+ defs.append("marker")
445
+ .attr("id", type.id)
446
+ .attr("viewBox", "0 -5 10 10")
447
+ .attr("refX", 20)
448
+ .attr("refY", 0)
449
+ .attr("markerWidth", 6)
450
+ .attr("markerHeight", 6)
451
+ .attr("orient", "auto")
452
+ .append("path")
453
+ .attr("d", "M0,-5L10,0L0,5")
454
+ .attr("fill", type.color);
455
+ });
293
456
 
294
457
  function visualizeGraph(data) {
295
458
  g.selectAll("*").remove();
296
459
 
297
460
  allNodes = data.nodes;
298
461
  allLinks = data.links;
299
- // Deep copy links before D3 modifies them
300
- allLinksOriginal = JSON.parse(JSON.stringify(data.links));
301
462
 
302
463
  // Find root nodes - start with only top-level nodes
303
464
  let rootNodes;
@@ -305,21 +466,36 @@
305
466
  // In monorepos, subproject nodes are roots
306
467
  rootNodes = allNodes.filter(n => n.type === 'subproject');
307
468
  } else {
308
- // Regular projects: prefer directories, fallback to files
309
- const dirNodes = allNodes.filter(n => n.type === 'directory');
310
- const fileNodes = allNodes.filter(n => n.type === 'file');
311
-
312
- if (dirNodes.length > 0) {
313
- // Show root-level directories (minimum depth)
314
- const minDirDepth = Math.min(...dirNodes.map(n => n.depth || 0));
315
- rootNodes = dirNodes.filter(n => (n.depth || 0) === minDirDepth);
316
- } else if (fileNodes.length > 0) {
317
- // No directories, show root-level files
318
- const minFileDepth = Math.min(...fileNodes.map(n => n.depth || 0));
319
- rootNodes = fileNodes.filter(n => (n.depth || 0) === minFileDepth);
320
- } else {
321
- // No directories or files, show all nodes
322
- rootNodes = allNodes;
469
+ // Regular projects: Find root nodes by checking graph structure
470
+ // Root nodes are nodes with no incoming links (no parent)
471
+ const targetIds = new Set(allLinks.map(link => link.target));
472
+ const allRootNodes = allNodes.filter(node => !targetIds.has(node.id));
473
+
474
+ console.log(`All root nodes found via links: ${allRootNodes.length}`, allRootNodes.map(n => `${n.name} (${n.type})`));
475
+
476
+ // Filter to show only directories and files at root
477
+ // Exclude orphaned files (files with no directory parent) from initial view
478
+ // These are typically config/doc files at project root
479
+ rootNodes = allRootNodes.filter(n => {
480
+ // Include all directories
481
+ if (n.type === 'directory') return true;
482
+
483
+ // Include files only if they're in a directory structure
484
+ // Exclude standalone files with no dir_path (project root files)
485
+ if (n.type === 'file' && n.dir_path === null) {
486
+ console.log(`Filtering out orphaned root file: ${n.name}`);
487
+ return false;
488
+ }
489
+
490
+ return false; // Exclude all other types (classes, functions, etc.)
491
+ });
492
+
493
+ console.log(`Filtered root nodes (directories only): ${rootNodes.length}`, rootNodes.map(n => n.name));
494
+
495
+ // Fallback only if we got 0 root nodes (shouldn't happen with correct data)
496
+ if (rootNodes.length === 0) {
497
+ console.warn('No root nodes after filtering, using all root nodes');
498
+ rootNodes = allRootNodes;
323
499
  }
324
500
  }
325
501
 
@@ -327,58 +503,36 @@
327
503
  visibleNodes = new Set(rootNodes.map(n => n.id));
328
504
  collapsedNodes = new Set(rootNodes.map(n => n.id));
329
505
 
506
+ console.log('=== INITIAL STATE ===');
507
+ console.log(`Total nodes: ${allNodes.length}`);
508
+ console.log(`Total links: ${allLinks.length}`);
509
+ console.log(`Root nodes: ${rootNodes.length}`, rootNodes.map(n => n.name));
510
+ console.log(`Initial visibleNodes:`, Array.from(visibleNodes));
511
+ console.log(`Initial collapsedNodes:`, Array.from(collapsedNodes));
512
+
330
513
  renderGraph();
331
514
  }
332
515
 
333
516
  function renderGraph() {
517
+ console.log('=== renderGraph() called ===');
518
+ console.log(`visibleNodes.size: ${visibleNodes.size}`);
519
+ console.log(`collapsedNodes.size: ${collapsedNodes.size}`);
520
+
334
521
  const visibleNodesList = allNodes.filter(n => visibleNodes.has(n.id));
335
- const visibleLinks = allLinks.filter(l =>
336
- visibleNodes.has(l.source.id || l.source) &&
337
- visibleNodes.has(l.target.id || l.target)
338
- );
339
-
340
- // Dynamic collision radius based on node type
341
- const getNodeRadius = (d) => {
342
- if (d.type === 'directory') return 35;
343
- if (d.type === 'file') return 32;
344
- if (d.type === 'subproject') return 38;
345
- return d.complexity ? Math.min(20 + d.complexity * 2, 35) : 25;
346
- };
347
-
348
- // Dynamic link distance based on node types
349
- const getLinkDistance = (link) => {
350
- const source = typeof link.source === 'object' ? link.source : visibleNodesList.find(n => n.id === link.source);
351
- const target = typeof link.target === 'object' ? link.target : visibleNodesList.find(n => n.id === link.target);
352
-
353
- if (!source || !target) return 150;
354
-
355
- // Longer distances for directory relationships
356
- if (source.type === 'directory' || target.type === 'directory') return 200;
357
- if (source.type === 'file' || target.type === 'file') return 150;
358
- return 100;
359
- };
522
+ console.log(`Rendering ${visibleNodesList.length} visible nodes`);
523
+
524
+ const visibleLinks = allLinks.filter(l => {
525
+ const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
526
+ const targetId = typeof l.target === 'object' ? l.target.id : l.target;
527
+ return visibleNodes.has(sourceId) && visibleNodes.has(targetId);
528
+ });
529
+ console.log(`Rendering ${visibleLinks.length} visible links`);
360
530
 
361
531
  simulation = d3.forceSimulation(visibleNodesList)
362
- .force("link", d3.forceLink(visibleLinks)
363
- .id(d => d.id)
364
- .distance(getLinkDistance)
365
- .strength(0.5))
366
- .force("charge", d3.forceManyBody()
367
- .strength(d => {
368
- // Stronger repulsion for directories and files
369
- if (d.type === 'directory') return -800;
370
- if (d.type === 'file') return -600;
371
- return -400;
372
- })
373
- .distanceMax(500))
532
+ .force("link", d3.forceLink(visibleLinks).id(d => d.id).distance(100))
533
+ .force("charge", d3.forceManyBody().strength(-400))
374
534
  .force("center", d3.forceCenter(width / 2, height / 2))
375
- .force("collision", d3.forceCollide()
376
- .radius(getNodeRadius)
377
- .strength(0.9))
378
- .force("x", d3.forceX(width / 2).strength(0.05))
379
- .force("y", d3.forceY(height / 2).strength(0.05))
380
- .alphaDecay(0.02)
381
- .velocityDecay(0.3);
535
+ .force("collision", d3.forceCollide().radius(40));
382
536
 
383
537
  g.selectAll("*").remove();
384
538
 
@@ -386,7 +540,33 @@
386
540
  .selectAll("line")
387
541
  .data(visibleLinks)
388
542
  .join("line")
389
- .attr("class", d => d.type === "dependency" ? "link dependency" : "link");
543
+ .attr("class", d => d.type === "dependency" ? "link dependency" : "link")
544
+ .attr("marker-end", d => {
545
+ const sourceId = typeof d.source === 'object' ? d.source.id : d.source;
546
+ const targetId = typeof d.target === 'object' ? d.target.id : d.target;
547
+ const source = allNodes.find(n => n.id === sourceId);
548
+ const target = allNodes.find(n => n.id === targetId);
549
+
550
+ if (source && target) {
551
+ if (source.type === 'directory' && target.type === 'file') {
552
+ return "url(#arrow-directory)";
553
+ } else if (source.type === 'file' && ['class', 'function', 'method', 'imports'].includes(target.type)) {
554
+ return "url(#arrow-file)";
555
+ } else {
556
+ return "url(#arrow-ast)";
557
+ }
558
+ }
559
+ return "url(#arrow-ast)";
560
+ })
561
+ .attr("stroke", d => {
562
+ const sourceId = typeof d.source === 'object' ? d.source.id : d.source;
563
+ const source = allNodes.find(n => n.id === sourceId);
564
+ if (source) {
565
+ if (source.type === 'directory') return "#58a6ff";
566
+ if (source.type === 'file') return "#8b949e";
567
+ }
568
+ return "#a371f7";
569
+ });
390
570
 
391
571
  const node = g.append("g")
392
572
  .selectAll("g")
@@ -404,15 +584,73 @@
404
584
  .on("mouseover", showTooltip)
405
585
  .on("mouseout", hideTooltip);
406
586
 
407
- // Add circles
408
- node.append("circle")
587
+ // Add shapes based on node type (circles for code, squares for docs)
588
+ const isDocNode = d => ['docstring', 'comment'].includes(d.type);
589
+
590
+ // Add circles for code nodes
591
+ node.filter(d => !isDocNode(d))
592
+ .append("circle")
409
593
  .attr("r", d => {
410
- if (d.type === 'directory') return 25;
411
- if (d.type === 'file') return 22;
412
- if (d.type === 'subproject') return 28;
413
- return d.complexity ? Math.min(12 + d.complexity * 2, 25) : 15;
594
+ if (d.type === 'subproject') return 20;
595
+ if (d.type === 'directory') return 40; // Largest for directory containers
596
+ if (d.type === 'file') return 30; // Larger transparent circle for files
597
+ return d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
598
+ })
599
+ .attr("stroke", d => hasChildren(d) ? "#ffffff" : "none")
600
+ .attr("stroke-width", d => hasChildren(d) ? 3 : 0)
601
+ .style("fill", d => d.color || null) // Use custom color if available
602
+ .style("cursor", "pointer"); // Ensure cursor shows pointer on all nodes
603
+
604
+ // Add rectangles for document nodes
605
+ node.filter(d => isDocNode(d))
606
+ .append("rect")
607
+ .attr("width", d => {
608
+ const size = d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
609
+ return size * 2;
610
+ })
611
+ .attr("height", d => {
612
+ const size = d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
613
+ return size * 2;
614
+ })
615
+ .attr("x", d => {
616
+ const size = d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
617
+ return -size;
618
+ })
619
+ .attr("y", d => {
620
+ const size = d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
621
+ return -size;
414
622
  })
415
- .style("fill", d => d.color || null);
623
+ .attr("rx", 2) // Rounded corners
624
+ .attr("ry", 2)
625
+ .attr("stroke", d => hasChildren(d) ? "#ffffff" : "none")
626
+ .attr("stroke-width", d => hasChildren(d) ? 3 : 0)
627
+ .style("fill", d => d.color || null)
628
+ .style("cursor", "pointer"); // Ensure cursor shows pointer on all nodes
629
+
630
+ // Add expand/collapse indicator - circle background below node name
631
+ node.filter(d => hasChildren(d))
632
+ .append("circle")
633
+ .attr("class", "expand-circle")
634
+ .attr("r", 14)
635
+ .attr("cy", 35) // Position below the text
636
+ .attr("fill", "#2c2c2c")
637
+ .attr("stroke", "#ffeb3b")
638
+ .attr("stroke-width", 2.5)
639
+ .style("cursor", "pointer")
640
+ .style("pointer-events", "all");
641
+
642
+ // Add +/- text inside the circle
643
+ node.filter(d => hasChildren(d))
644
+ .append("text")
645
+ .attr("class", "expand-indicator")
646
+ .attr("y", 35) // Same position as circle
647
+ .attr("dy", 6) // Center vertically within circle
648
+ .attr("text-anchor", "middle")
649
+ .style("font-size", "22px")
650
+ .style("font-weight", "bold")
651
+ .style("fill", "#ffeb3b")
652
+ .style("pointer-events", "none")
653
+ .text(d => collapsedNodes.has(d.id) ? "+" : "−");
416
654
 
417
655
  // Add icons based on node type
418
656
  node.append("text")
@@ -426,24 +664,66 @@
426
664
  if (d.type === 'method') return 'm';
427
665
  if (d.type === 'module') return 'M';
428
666
  if (d.type === 'imports') return '⇄';
667
+ if (d.type === 'docstring') return '📝';
668
+ if (d.type === 'comment') return '💬';
669
+ if (d.type === 'subproject') return '📦';
429
670
  return '•';
430
671
  });
431
672
 
432
- // Add expand/collapse indicator for nodes with children
433
- node.filter(d => hasChildren(d))
434
- .append("text")
435
- .attr("class", "expand-indicator")
436
- .attr("text-anchor", "middle")
437
- .attr("dy", -20)
438
- .style("font-size", "14px")
439
- .style("font-weight", "bold")
440
- .style("fill", "#58a6ff")
441
- .style("pointer-events", "none")
442
- .text(d => collapsedNodes.has(d.id) ? "+" : "−");
443
-
444
- // Add labels
673
+ // Add labels (show actual import statement for import nodes, better names for text chunks)
445
674
  node.append("text")
446
- .text(d => d.name)
675
+ .text(d => {
676
+ // Import nodes have type === 'imports'
677
+ if (d.type === 'imports') {
678
+ if (d.content) {
679
+ // Extract first line of import statement
680
+ const importLine = d.content.split('\n')[0].trim();
681
+ // Truncate if too long (max 60 chars)
682
+ return importLine.length > 60 ? importLine.substring(0, 57) + '...' : importLine;
683
+ }
684
+ return d.name; // Fallback to name if no content
685
+ }
686
+
687
+ // Text chunk nodes - show content preview instead of just line numbers
688
+ if (d.type === 'text' && d.content) {
689
+ // Extract first line of content
690
+ const firstLine = d.content.split('\n')[0].trim();
691
+ if (firstLine.length > 0) {
692
+ // Truncate if too long (max 40 chars for graph labels)
693
+ const preview = firstLine.length > 40 ? firstLine.substring(0, 37) + '...' : firstLine;
694
+ return preview;
695
+ }
696
+ // Fallback to line numbers if content is empty
697
+ if (d.start_line) {
698
+ return `L${d.start_line}-${d.end_line}`;
699
+ }
700
+ return d.name;
701
+ }
702
+
703
+ // Code chunk nodes - show parent context if available
704
+ if (d.type === 'code' && d.start_line) {
705
+ // Try to find parent class/function by looking at incoming links
706
+ const parentLink = allLinks.find(l => {
707
+ const targetId = typeof l.target === 'object' ? l.target.id : l.target;
708
+ return targetId === d.id;
709
+ });
710
+
711
+ if (parentLink) {
712
+ const sourceId = typeof parentLink.source === 'object' ? parentLink.source.id : parentLink.source;
713
+ const parentNode = allNodes.find(n => n.id === sourceId);
714
+
715
+ if (parentNode && ['class', 'function', 'method'].includes(parentNode.type)) {
716
+ // Show parent context with line numbers
717
+ return `${parentNode.name}:L${d.start_line}-${d.end_line}`;
718
+ }
719
+ }
720
+
721
+ // Fallback: just show line numbers if no parent found
722
+ return `L${d.start_line}-${d.end_line}`;
723
+ }
724
+
725
+ return d.name;
726
+ })
447
727
  .attr("dy", 30);
448
728
 
449
729
  simulation.on("tick", () => {
@@ -459,60 +739,235 @@
459
739
  updateStats({nodes: visibleNodesList, links: visibleLinks, metadata: {total_files: allNodes.length}});
460
740
  }
461
741
 
742
+ function highlightNode(node) {
743
+ // Remove previous highlights - reset all to default stroke
744
+ d3.selectAll('.node circle, .node rect')
745
+ .attr('stroke-width', d => hasChildren(d) ? (d.type === 'file' || d.type === 'directory' ? 2 : 3) : 1.5)
746
+ .attr('stroke', d => {
747
+ if (hasChildren(d)) {
748
+ return '#ffffff';
749
+ }
750
+ return d.type === 'file' || d.type === 'directory' ? (d.type === 'file' ? '#58a6ff' : '#79c0ff') : '#30363d';
751
+ });
752
+
753
+ // Highlight the current node with blue stroke
754
+ const currentNodeElement = d3.selectAll('.node')
755
+ .filter(d => d.id === node.id);
756
+
757
+ currentNodeElement.select('circle, rect')
758
+ .attr('stroke', '#58a6ff')
759
+ .attr('stroke-width', 4);
760
+ }
761
+
762
+ function centerOnNode(node) {
763
+ const nodeElement = d3.selectAll('.node')
764
+ .filter(d => d.id === node.id);
765
+
766
+ if (nodeElement.empty()) return;
767
+
768
+ const nodeData = nodeElement.datum();
769
+ const transform = d3.zoomTransform(svg.node());
770
+
771
+ // Calculate center position
772
+ const x = -nodeData.x * transform.k + width / 2;
773
+ const y = -nodeData.y * transform.k + height / 2;
774
+
775
+ svg.transition()
776
+ .duration(750)
777
+ .call(zoom.transform, d3.zoomIdentity.translate(x, y).scale(transform.k));
778
+ }
779
+
780
+ function updateGraphForNode(node) {
781
+ if (!node) {
782
+ // Show default root view
783
+ renderGraph();
784
+ return;
785
+ }
786
+
787
+ // Find parent
788
+ const parentLinks = allLinks.filter(l => {
789
+ const targetId = typeof l.target === 'object' ? l.target.id : l.target;
790
+ return targetId === node.id;
791
+ });
792
+
793
+ if (parentLinks.length === 0) {
794
+ // This is a root node - show all roots
795
+ renderGraph();
796
+ highlightNode(node);
797
+ return;
798
+ }
799
+
800
+ // Get parent
801
+ const parentId = typeof parentLinks[0].source === 'object'
802
+ ? parentLinks[0].source.id
803
+ : parentLinks[0].source;
804
+ const parent = allNodes.find(n => n.id === parentId);
805
+
806
+ // Get all siblings (children of same parent)
807
+ const siblingLinks = allLinks.filter(l => {
808
+ const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
809
+ return sourceId === parentId;
810
+ });
811
+
812
+ const siblings = siblingLinks.map(l => {
813
+ const targetId = typeof l.target === 'object' ? l.target.id : l.target;
814
+ return allNodes.find(n => n.id === targetId);
815
+ }).filter(n => n);
816
+
817
+ // Update visible nodes to show parent + siblings + current node's children
818
+ visibleNodes.clear();
819
+ visibleNodes.add(parentId); // Parent
820
+ siblings.forEach(s => visibleNodes.add(s.id)); // All siblings including current
821
+
822
+ // If current node is expanded, show its children too
823
+ if (!collapsedNodes.has(node.id)) {
824
+ const childLinks = allLinks.filter(l => {
825
+ const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
826
+ return sourceId === node.id;
827
+ });
828
+ childLinks.forEach(l => {
829
+ const targetId = typeof l.target === 'object' ? l.target.id : l.target;
830
+ visibleNodes.add(targetId);
831
+ });
832
+ }
833
+
834
+ renderGraph();
835
+ highlightNode(node);
836
+ }
837
+
462
838
  function hasChildren(node) {
463
- // Use original links (not modified by D3)
464
- return allLinksOriginal.some(l => l.source === node.id);
839
+ // Handle both pre-mutation (string IDs) and post-mutation (object references)
840
+ const result = allLinks.some(l => {
841
+ const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
842
+ return sourceId === node.id;
843
+ });
844
+ console.log(`hasChildren(${node.name}):`, result,
845
+ `Checking ${allLinks.length} links for node ID: ${node.id}`);
846
+ return result;
465
847
  }
466
848
 
467
849
  function handleNodeClick(event, d) {
850
+ console.log('=== NODE CLICKED ===');
851
+ console.log('Node:', d.name, 'Type:', d.type, 'ID:', d.id);
852
+ console.log('Event:', event);
853
+
468
854
  event.stopPropagation();
469
855
 
470
- // If node has children, toggle expansion
471
- if (hasChildren(d)) {
472
- if (collapsedNodes.has(d.id)) {
856
+ // Track current node
857
+ currentNode = d;
858
+
859
+ // Always show content pane when clicking any node
860
+ showContentPane(d);
861
+
862
+ // If node has children, also toggle expansion
863
+ const nodeHasChildren = hasChildren(d);
864
+ console.log('Node has children:', nodeHasChildren);
865
+
866
+ if (nodeHasChildren) {
867
+ const isCollapsed = collapsedNodes.has(d.id);
868
+ console.log('Node is collapsed:', isCollapsed);
869
+
870
+ if (isCollapsed) {
871
+ console.log('Expanding node...');
473
872
  expandNode(d);
474
873
  } else {
874
+ console.log('Collapsing node...');
475
875
  collapseNode(d);
476
876
  }
877
+ console.log('Calling renderGraph()...');
477
878
  renderGraph();
879
+ console.log('renderGraph() complete');
478
880
  } else {
479
- // Leaf node - show code viewer
480
- showCodeViewer(d);
881
+ console.log('Node has no children, skipping expansion');
481
882
  }
883
+
884
+ // Highlight in graph
885
+ highlightNode(d);
482
886
  }
483
887
 
484
888
  function expandNode(node) {
889
+ console.log(`expandNode(${node.name}):`, 'Removing from collapsedNodes');
485
890
  collapsedNodes.delete(node.id);
486
891
 
487
- // Find direct children using original links (not modified by D3)
488
- const children = allLinksOriginal
489
- .filter(l => l.source === node.id)
490
- .map(l => allNodes.find(n => n.id === l.target))
892
+ // Find direct children
893
+ const childLinks = allLinks.filter(l => {
894
+ const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
895
+ return sourceId === node.id;
896
+ });
897
+ console.log(` Found ${childLinks.length} child links for node ${node.id}`);
898
+
899
+ const children = childLinks
900
+ .map(l => {
901
+ const targetId = typeof l.target === 'object' ? l.target.id : l.target;
902
+ const child = allNodes.find(n => n.id === targetId);
903
+ if (!child) {
904
+ console.warn(` Could not find child node with ID: ${targetId}`);
905
+ }
906
+ return child;
907
+ })
491
908
  .filter(n => n);
492
909
 
910
+ console.log(` Found ${children.length} child nodes:`, children.map(c => c.name));
911
+
493
912
  children.forEach(child => {
913
+ console.log(` Adding child to visibleNodes: ${child.name} (${child.id})`);
494
914
  visibleNodes.add(child.id);
495
915
  collapsedNodes.add(child.id); // Children start collapsed
496
916
  });
917
+
918
+ console.log(` visibleNodes now has ${visibleNodes.size} items`);
497
919
  }
498
920
 
499
921
  function collapseNode(node) {
922
+ console.log(`Collapsing node: ${node.name}`);
500
923
  collapsedNodes.add(node.id);
501
924
 
502
- // Hide all descendants recursively using original links
503
- function hideDescendants(parentId) {
504
- const children = allLinksOriginal
505
- .filter(l => l.source === parentId)
506
- .map(l => l.target);
925
+ // Check if this is a root node
926
+ const isRootNode = !allLinks.some(l => {
927
+ const targetId = typeof l.target === 'object' ? l.target.id : l.target;
928
+ return targetId === node.id;
929
+ });
507
930
 
508
- children.forEach(childId => {
509
- visibleNodes.delete(childId);
510
- collapsedNodes.delete(childId);
511
- hideDescendants(childId);
931
+ if (isRootNode) {
932
+ // Collapsing a root node - return to root-only view
933
+ console.log(' Collapsing root node - resetting to root view');
934
+
935
+ // Clear all visible nodes except root nodes
936
+ const rootNodes = allNodes.filter(n => {
937
+ const hasIncomingLinks = allLinks.some(l => {
938
+ const targetId = typeof l.target === 'object' ? l.target.id : l.target;
939
+ return targetId === n.id;
940
+ });
941
+ return !hasIncomingLinks && n.type === 'directory';
512
942
  });
513
- }
514
943
 
515
- hideDescendants(node.id);
944
+ visibleNodes.clear();
945
+ rootNodes.forEach(n => visibleNodes.add(n.id));
946
+
947
+ // Clear all collapsed states
948
+ collapsedNodes.clear();
949
+ // Re-add collapsed state to all root nodes
950
+ rootNodes.forEach(n => collapsedNodes.add(n.id));
951
+ } else {
952
+ // Non-root node - existing recursive collapse logic
953
+ // Hide all descendants recursively
954
+ function hideDescendants(parentId) {
955
+ const children = allLinks
956
+ .filter(l => {
957
+ const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
958
+ return sourceId === parentId;
959
+ })
960
+ .map(l => typeof l.target === 'object' ? l.target.id : l.target);
961
+
962
+ children.forEach(childId => {
963
+ visibleNodes.delete(childId);
964
+ collapsedNodes.delete(childId);
965
+ hideDescendants(childId);
966
+ });
967
+ }
968
+
969
+ hideDescendants(node.id);
970
+ }
516
971
  }
517
972
 
518
973
  function showTooltip(event, d) {
@@ -586,19 +1041,67 @@
586
1041
  }
587
1042
  }
588
1043
 
589
- function showCodeViewer(node) {
590
- // Highlight the node
1044
+ function findReferences(node) {
1045
+ // Find all nodes that link TO this node (reverse lookup)
1046
+ const incomingLinks = allLinks.filter(l => {
1047
+ const targetId = typeof l.target === 'object' ? l.target.id : l.target;
1048
+ return targetId === node.id;
1049
+ });
1050
+
1051
+ const callers = incomingLinks.map(l => {
1052
+ const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
1053
+ return allNodes.find(n => n.id === sourceId);
1054
+ }).filter(n => n);
1055
+
1056
+ return callers;
1057
+ }
1058
+
1059
+ function findSemanticReferences(node) {
1060
+ // For semantic search, we'd ideally use the MCP vector search
1061
+ // For now, use simple heuristics based on name/content similarity
1062
+
1063
+ if (!node.content && !node.name) return [];
1064
+
1065
+ const searchTerms = node.name.toLowerCase().split(/[_\s]/);
1066
+
1067
+ const semanticMatches = allNodes.filter(other => {
1068
+ if (other.id === node.id) return false;
1069
+ if (!other.content && !other.name) return false;
1070
+
1071
+ const otherText = (other.content || other.name).toLowerCase();
1072
+ return searchTerms.some(term => term.length > 2 && otherText.includes(term));
1073
+ });
1074
+
1075
+ return semanticMatches.slice(0, 10); // Limit to top 10
1076
+ }
1077
+
1078
+ function showContentPane(node) {
1079
+ // Track and highlight the node
1080
+ currentNode = node;
591
1081
  highlightedNode = node;
592
- renderGraph();
593
1082
 
594
- // Populate code viewer
595
- const viewer = document.getElementById('code-viewer');
596
- const title = document.getElementById('viewer-title');
597
- const meta = document.getElementById('viewer-meta');
598
- const code = document.getElementById('viewer-code');
1083
+ // Enable focus button
1084
+ document.getElementById('focus-button').disabled = false;
599
1085
 
600
- title.textContent = node.name;
1086
+ // Populate content pane
1087
+ const pane = document.getElementById('content-pane');
1088
+ const title = document.getElementById('pane-title');
1089
+ const meta = document.getElementById('pane-meta');
1090
+ const content = document.getElementById('pane-content');
1091
+
1092
+ // Set title with actual import statement for import nodes
1093
+ if (node.type === 'imports') {
1094
+ if (node.content) {
1095
+ const importLine = node.content.split('\n')[0].trim();
1096
+ title.textContent = importLine;
1097
+ } else {
1098
+ title.textContent = `Import: ${node.name}`;
1099
+ }
1100
+ } else {
1101
+ title.textContent = node.name;
1102
+ }
601
1103
 
1104
+ // Set metadata
602
1105
  let metaText = `${node.type} • ${node.file_path}`;
603
1106
  if (node.start_line) {
604
1107
  metaText += ` • Lines ${node.start_line}-${node.end_line}`;
@@ -608,33 +1111,929 @@
608
1111
  }
609
1112
  meta.textContent = metaText;
610
1113
 
611
- // Show content if available
1114
+ // Display content based on node type
1115
+ if (node.type === 'directory') {
1116
+ showDirectoryContents(node, content);
1117
+ } else if (node.type === 'file') {
1118
+ showFileContents(node, content);
1119
+ } else if (node.type === 'imports') {
1120
+ // Import nodes show import details
1121
+ showImportDetails(node, content);
1122
+ } else {
1123
+ // Class, function, method, code nodes
1124
+ showCodeContent(node, content);
1125
+ }
1126
+
1127
+ pane.classList.add('visible');
1128
+ }
1129
+
1130
+ function showDirectoryContents(node, container) {
1131
+ container.innerHTML = ''; // Clear first
1132
+
1133
+ // Add "Up" button if node has parent
1134
+ const parentLinks = allLinks.filter(l => {
1135
+ const targetId = typeof l.target === 'object' ? l.target.id : l.target;
1136
+ return targetId === node.id;
1137
+ });
1138
+
1139
+ if (parentLinks.length > 0) {
1140
+ const parentId = typeof parentLinks[0].source === 'object'
1141
+ ? parentLinks[0].source.id
1142
+ : parentLinks[0].source;
1143
+ const parent = allNodes.find(n => n.id === parentId);
1144
+
1145
+ if (parent) {
1146
+ const upButton = document.createElement('div');
1147
+ upButton.style.cssText = 'margin-bottom: 15px; padding: 8px 12px; background-color: #21262d; border-radius: 6px; border: 1px solid #30363d; cursor: pointer; display: flex; align-items: center; gap: 8px; transition: background-color 0.2s;';
1148
+
1149
+ const upArrow = document.createElement('span');
1150
+ upArrow.textContent = '↑';
1151
+ upArrow.style.cssText = 'font-size: 20px; color: #58a6ff;';
1152
+
1153
+ const upText = document.createElement('span');
1154
+ upText.textContent = `Up to ${parent.name}`;
1155
+ upText.style.color = '#c9d1d9';
1156
+
1157
+ upButton.appendChild(upArrow);
1158
+ upButton.appendChild(upText);
1159
+
1160
+ upButton.addEventListener('mouseover', function() {
1161
+ this.style.backgroundColor = '#30363d';
1162
+ });
1163
+ upButton.addEventListener('mouseout', function() {
1164
+ this.style.backgroundColor = '#21262d';
1165
+ });
1166
+ upButton.addEventListener('click', () => {
1167
+ showContentPane(parent);
1168
+ updateGraphForNode(parent);
1169
+ });
1170
+
1171
+ container.appendChild(upButton);
1172
+ }
1173
+ }
1174
+
1175
+ // Find all direct children of this directory
1176
+ const children = allLinks
1177
+ .filter(l => {
1178
+ const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
1179
+ return sourceId === node.id;
1180
+ })
1181
+ .map(l => {
1182
+ const targetId = typeof l.target === 'object' ? l.target.id : l.target;
1183
+ return allNodes.find(n => n.id === targetId);
1184
+ })
1185
+ .filter(n => n);
1186
+
1187
+ // Re-find siblings (already found parentLinks above)
1188
+ // const parentLinks = allLinks.filter(l => {
1189
+ // const targetId = typeof l.target === 'object' ? l.target.id : l.target;
1190
+ // return targetId === node.id;
1191
+ // });
1192
+
1193
+ let siblings = [];
1194
+ if (parentLinks.length > 0) {
1195
+ const parentId = typeof parentLinks[0].source === 'object'
1196
+ ? parentLinks[0].source.id
1197
+ : parentLinks[0].source;
1198
+
1199
+ const siblingLinks = allLinks.filter(l => {
1200
+ const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
1201
+ return sourceId === parentId;
1202
+ });
1203
+
1204
+ siblings = siblingLinks
1205
+ .map(l => {
1206
+ const targetId = typeof l.target === 'object' ? l.target.id : l.target;
1207
+ return allNodes.find(n => n.id === targetId);
1208
+ })
1209
+ .filter(n => n && n.id !== node.id); // Exclude self
1210
+ }
1211
+
1212
+ // Display siblings first if they exist
1213
+ if (siblings.length > 0) {
1214
+ const siblingsHeader = document.createElement('h4');
1215
+ siblingsHeader.textContent = 'Siblings';
1216
+ siblingsHeader.style.color = '#8b949e';
1217
+ siblingsHeader.style.marginTop = '0';
1218
+ container.appendChild(siblingsHeader);
1219
+
1220
+ const siblingsList = document.createElement('ul');
1221
+ siblingsList.className = 'directory-list';
1222
+
1223
+ siblings.forEach(sibling => {
1224
+ const listItem = document.createElement('li');
1225
+ listItem.style.cursor = 'pointer';
1226
+ listItem.style.transition = 'background-color 0.2s';
1227
+
1228
+ const icon = sibling.type === 'directory' ? '📁' : sibling.type === 'file' ? '📄' : '📝';
1229
+ listItem.innerHTML = `
1230
+ <span class="item-icon">${icon}</span>
1231
+ ${sibling.name}
1232
+ <span class="item-type">${sibling.type}</span>
1233
+ `;
1234
+
1235
+ listItem.addEventListener('mouseover', function() {
1236
+ this.style.backgroundColor = '#30363d';
1237
+ });
1238
+ listItem.addEventListener('mouseout', function() {
1239
+ this.style.backgroundColor = 'transparent';
1240
+ });
1241
+ listItem.addEventListener('click', function(event) {
1242
+ event.stopPropagation();
1243
+ if (hasChildren(sibling)) {
1244
+ if (collapsedNodes.has(sibling.id)) {
1245
+ expandNode(sibling);
1246
+ } else {
1247
+ collapseNode(sibling);
1248
+ }
1249
+ renderGraph();
1250
+ }
1251
+ showContentPane(sibling);
1252
+ updateGraphForNode(sibling);
1253
+ });
1254
+
1255
+ siblingsList.appendChild(listItem);
1256
+ });
1257
+
1258
+ container.appendChild(siblingsList);
1259
+ }
1260
+
1261
+ // Add references section for applicable node types
1262
+ if (node.type === 'function' || node.type === 'class' || node.type === 'method' || node.type === 'code') {
1263
+ const directCallers = findReferences(node);
1264
+ const semanticRefs = findSemanticReferences(node);
1265
+
1266
+ if (directCallers.length > 0 || semanticRefs.length > 0) {
1267
+ const refsHeader = document.createElement('h4');
1268
+ refsHeader.textContent = 'References';
1269
+ refsHeader.style.color = '#8b949e';
1270
+ refsHeader.style.marginTop = siblings.length > 0 ? '20px' : '0';
1271
+ container.appendChild(refsHeader);
1272
+
1273
+ if (directCallers.length > 0) {
1274
+ const callersSubheader = document.createElement('h5');
1275
+ callersSubheader.textContent = 'Direct Callers (AST)';
1276
+ callersSubheader.style.color = '#58a6ff';
1277
+ callersSubheader.style.fontSize = '14px';
1278
+ callersSubheader.style.marginTop = '10px';
1279
+ container.appendChild(callersSubheader);
1280
+
1281
+ const callersList = document.createElement('ul');
1282
+ callersList.className = 'directory-list';
1283
+
1284
+ directCallers.forEach(caller => {
1285
+ const listItem = document.createElement('li');
1286
+ listItem.style.cursor = 'pointer';
1287
+ listItem.style.transition = 'background-color 0.2s';
1288
+ listItem.innerHTML = `
1289
+ <span class="item-icon">🔗</span>
1290
+ ${caller.name}
1291
+ <span class="item-type">${caller.type}</span>
1292
+ `;
1293
+
1294
+ listItem.addEventListener('mouseover', function() {
1295
+ this.style.backgroundColor = '#30363d';
1296
+ });
1297
+ listItem.addEventListener('mouseout', function() {
1298
+ this.style.backgroundColor = 'transparent';
1299
+ });
1300
+ listItem.addEventListener('click', () => {
1301
+ showContentPane(caller);
1302
+ updateGraphForNode(caller);
1303
+ });
1304
+
1305
+ callersList.appendChild(listItem);
1306
+ });
1307
+
1308
+ container.appendChild(callersList);
1309
+ }
1310
+
1311
+ if (semanticRefs.length > 0) {
1312
+ const semanticSubheader = document.createElement('h5');
1313
+ semanticSubheader.textContent = 'Semantic References';
1314
+ semanticSubheader.style.color = '#a371f7';
1315
+ semanticSubheader.style.fontSize = '14px';
1316
+ semanticSubheader.style.marginTop = '10px';
1317
+ container.appendChild(semanticSubheader);
1318
+
1319
+ const semanticList = document.createElement('ul');
1320
+ semanticList.className = 'directory-list';
1321
+
1322
+ semanticRefs.forEach(ref => {
1323
+ const listItem = document.createElement('li');
1324
+ listItem.style.cursor = 'pointer';
1325
+ listItem.style.transition = 'background-color 0.2s';
1326
+ listItem.style.borderLeft = '2px dotted #a371f7';
1327
+ listItem.style.paddingLeft = '10px';
1328
+ listItem.innerHTML = `
1329
+ <span class="item-icon">🔗</span>
1330
+ ${ref.name}
1331
+ <span class="item-type">${ref.type}</span>
1332
+ `;
1333
+
1334
+ listItem.addEventListener('mouseover', function() {
1335
+ this.style.backgroundColor = '#30363d';
1336
+ });
1337
+ listItem.addEventListener('mouseout', function() {
1338
+ this.style.backgroundColor = 'transparent';
1339
+ });
1340
+ listItem.addEventListener('click', () => {
1341
+ showContentPane(ref);
1342
+ updateGraphForNode(ref);
1343
+ });
1344
+
1345
+ semanticList.appendChild(listItem);
1346
+ });
1347
+
1348
+ container.appendChild(semanticList);
1349
+ }
1350
+ }
1351
+ }
1352
+
1353
+ // Display children
1354
+ if (children.length === 0) {
1355
+ const emptyMsg = document.createElement('p');
1356
+ emptyMsg.textContent = 'Empty directory';
1357
+ emptyMsg.style.color = '#8b949e';
1358
+ container.appendChild(emptyMsg);
1359
+ return;
1360
+ }
1361
+
1362
+ const childrenHeader = document.createElement('h4');
1363
+ childrenHeader.textContent = 'Contents';
1364
+ childrenHeader.style.color = '#8b949e';
1365
+ if (siblings.length > 0) {
1366
+ childrenHeader.style.marginTop = '20px';
1367
+ } else {
1368
+ childrenHeader.style.marginTop = '0';
1369
+ }
1370
+ container.appendChild(childrenHeader);
1371
+
1372
+ // Group by type
1373
+ const files = children.filter(n => n.type === 'file');
1374
+ const subdirs = children.filter(n => n.type === 'directory');
1375
+ const chunks = children.filter(n => n.type !== 'file' && n.type !== 'directory');
1376
+
1377
+ const childrenList = document.createElement('ul');
1378
+ childrenList.className = 'directory-list';
1379
+
1380
+ // Show subdirectories first
1381
+ subdirs.forEach(child => {
1382
+ const listItem = document.createElement('li');
1383
+ listItem.style.cursor = 'pointer';
1384
+ listItem.style.transition = 'background-color 0.2s';
1385
+ listItem.innerHTML = `
1386
+ <span class="item-icon">📁</span>
1387
+ ${child.name}
1388
+ <span class="item-type">directory</span>
1389
+ `;
1390
+
1391
+ listItem.addEventListener('mouseover', function() {
1392
+ this.style.backgroundColor = '#30363d';
1393
+ });
1394
+ listItem.addEventListener('mouseout', function() {
1395
+ this.style.backgroundColor = 'transparent';
1396
+ });
1397
+ listItem.addEventListener('click', function(event) {
1398
+ event.stopPropagation();
1399
+ if (hasChildren(child)) {
1400
+ if (collapsedNodes.has(child.id)) {
1401
+ expandNode(child);
1402
+ } else {
1403
+ collapseNode(child);
1404
+ }
1405
+ renderGraph();
1406
+ }
1407
+ showContentPane(child);
1408
+ updateGraphForNode(child);
1409
+ });
1410
+
1411
+ childrenList.appendChild(listItem);
1412
+ });
1413
+
1414
+ // Then files
1415
+ files.forEach(child => {
1416
+ const listItem = document.createElement('li');
1417
+ listItem.style.cursor = 'pointer';
1418
+ listItem.style.transition = 'background-color 0.2s';
1419
+ listItem.innerHTML = `
1420
+ <span class="item-icon">📄</span>
1421
+ ${child.name}
1422
+ <span class="item-type">file</span>
1423
+ `;
1424
+
1425
+ listItem.addEventListener('mouseover', function() {
1426
+ this.style.backgroundColor = '#30363d';
1427
+ });
1428
+ listItem.addEventListener('mouseout', function() {
1429
+ this.style.backgroundColor = 'transparent';
1430
+ });
1431
+ listItem.addEventListener('click', function(event) {
1432
+ event.stopPropagation();
1433
+ if (hasChildren(child)) {
1434
+ if (collapsedNodes.has(child.id)) {
1435
+ expandNode(child);
1436
+ } else {
1437
+ collapseNode(child);
1438
+ }
1439
+ renderGraph();
1440
+ }
1441
+ showContentPane(child);
1442
+ updateGraphForNode(child);
1443
+ });
1444
+
1445
+ childrenList.appendChild(listItem);
1446
+ });
1447
+
1448
+ // Then code chunks
1449
+ chunks.forEach(child => {
1450
+ const icon = child.type === 'class' ? '🔷' : child.type === 'function' ? '⚡' : '📝';
1451
+ const listItem = document.createElement('li');
1452
+ listItem.style.cursor = 'pointer';
1453
+ listItem.style.transition = 'background-color 0.2s';
1454
+
1455
+ // Improve display name for text chunks
1456
+ let displayName = child.name;
1457
+ if (child.type === 'text' && child.content) {
1458
+ // For text chunks, show first line of content as preview
1459
+ const firstLine = child.content.split('\n')[0].trim();
1460
+ if (firstLine.length > 0) {
1461
+ displayName = firstLine.length > 50 ? firstLine.substring(0, 47) + '...' : firstLine;
1462
+ }
1463
+ }
1464
+
1465
+ listItem.innerHTML = `
1466
+ <span class="item-icon">${icon}</span>
1467
+ ${displayName}
1468
+ <span class="item-type">${child.type}</span>
1469
+ `;
1470
+
1471
+ listItem.addEventListener('mouseover', function() {
1472
+ this.style.backgroundColor = '#30363d';
1473
+ });
1474
+ listItem.addEventListener('mouseout', function() {
1475
+ this.style.backgroundColor = 'transparent';
1476
+ });
1477
+ listItem.addEventListener('click', function(event) {
1478
+ event.stopPropagation();
1479
+ if (hasChildren(child)) {
1480
+ if (collapsedNodes.has(child.id)) {
1481
+ expandNode(child);
1482
+ } else {
1483
+ collapseNode(child);
1484
+ }
1485
+ renderGraph();
1486
+ }
1487
+ showContentPane(child);
1488
+ updateGraphForNode(child);
1489
+ });
1490
+
1491
+ childrenList.appendChild(listItem);
1492
+ });
1493
+
1494
+ container.appendChild(childrenList);
1495
+
1496
+ // Add summary
1497
+ const summary = document.createElement('p');
1498
+ summary.style.color = '#8b949e';
1499
+ summary.style.fontSize = '11px';
1500
+ summary.style.marginTop = '16px';
1501
+ summary.textContent = `Total: ${children.length} items (${subdirs.length} directories, ${files.length} files, ${chunks.length} code chunks)`;
1502
+ container.appendChild(summary);
1503
+ }
1504
+
1505
+ function showFileContents(node, container) {
1506
+ container.innerHTML = ''; // Clear first
1507
+
1508
+ // Add "Up" button if node has parent
1509
+ const parentLinks = allLinks.filter(l => {
1510
+ const targetId = typeof l.target === 'object' ? l.target.id : l.target;
1511
+ return targetId === node.id;
1512
+ });
1513
+
1514
+ if (parentLinks.length > 0) {
1515
+ const parentId = typeof parentLinks[0].source === 'object'
1516
+ ? parentLinks[0].source.id
1517
+ : parentLinks[0].source;
1518
+ const parent = allNodes.find(n => n.id === parentId);
1519
+
1520
+ if (parent) {
1521
+ const upButton = document.createElement('div');
1522
+ upButton.style.cssText = 'margin-bottom: 15px; padding: 8px 12px; background-color: #21262d; border-radius: 6px; border: 1px solid #30363d; cursor: pointer; display: flex; align-items: center; gap: 8px; transition: background-color 0.2s;';
1523
+
1524
+ const upArrow = document.createElement('span');
1525
+ upArrow.textContent = '↑';
1526
+ upArrow.style.cssText = 'font-size: 20px; color: #58a6ff;';
1527
+
1528
+ const upText = document.createElement('span');
1529
+ upText.textContent = `Up to ${parent.name}`;
1530
+ upText.style.color = '#c9d1d9';
1531
+
1532
+ upButton.appendChild(upArrow);
1533
+ upButton.appendChild(upText);
1534
+
1535
+ upButton.addEventListener('mouseover', function() {
1536
+ this.style.backgroundColor = '#30363d';
1537
+ });
1538
+ upButton.addEventListener('mouseout', function() {
1539
+ this.style.backgroundColor = '#21262d';
1540
+ });
1541
+ upButton.addEventListener('click', () => {
1542
+ showContentPane(parent);
1543
+ updateGraphForNode(parent);
1544
+ });
1545
+
1546
+ container.appendChild(upButton);
1547
+ }
1548
+ }
1549
+
1550
+ // Re-find siblings (already found parentLinks above)
1551
+ // const parentLinks = allLinks.filter(l => {
1552
+ // const targetId = typeof l.target === 'object' ? l.target.id : l.target;
1553
+ // return targetId === node.id;
1554
+ // });
1555
+
1556
+ let siblings = [];
1557
+ if (parentLinks.length > 0) {
1558
+ const parentId = typeof parentLinks[0].source === 'object'
1559
+ ? parentLinks[0].source.id
1560
+ : parentLinks[0].source;
1561
+
1562
+ const siblingLinks = allLinks.filter(l => {
1563
+ const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
1564
+ return sourceId === parentId;
1565
+ });
1566
+
1567
+ siblings = siblingLinks
1568
+ .map(l => {
1569
+ const targetId = typeof l.target === 'object' ? l.target.id : l.target;
1570
+ return allNodes.find(n => n.id === targetId);
1571
+ })
1572
+ .filter(n => n && n.id !== node.id); // Exclude self
1573
+ }
1574
+
1575
+ // Display siblings first if they exist
1576
+ if (siblings.length > 0) {
1577
+ const siblingsHeader = document.createElement('h4');
1578
+ siblingsHeader.textContent = 'Siblings';
1579
+ siblingsHeader.style.color = '#8b949e';
1580
+ siblingsHeader.style.marginTop = '0';
1581
+ container.appendChild(siblingsHeader);
1582
+
1583
+ const siblingsList = document.createElement('ul');
1584
+ siblingsList.className = 'directory-list';
1585
+
1586
+ siblings.forEach(sibling => {
1587
+ const listItem = document.createElement('li');
1588
+ listItem.style.cursor = 'pointer';
1589
+ listItem.style.transition = 'background-color 0.2s';
1590
+
1591
+ const icon = sibling.type === 'directory' ? '📁' : sibling.type === 'file' ? '📄' : '📝';
1592
+ listItem.innerHTML = `
1593
+ <span class="item-icon">${icon}</span>
1594
+ ${sibling.name}
1595
+ <span class="item-type">${sibling.type}</span>
1596
+ `;
1597
+
1598
+ listItem.addEventListener('mouseover', function() {
1599
+ this.style.backgroundColor = '#30363d';
1600
+ });
1601
+ listItem.addEventListener('mouseout', function() {
1602
+ this.style.backgroundColor = 'transparent';
1603
+ });
1604
+ listItem.addEventListener('click', function(event) {
1605
+ event.stopPropagation();
1606
+ if (hasChildren(sibling)) {
1607
+ if (collapsedNodes.has(sibling.id)) {
1608
+ expandNode(sibling);
1609
+ } else {
1610
+ collapseNode(sibling);
1611
+ }
1612
+ renderGraph();
1613
+ }
1614
+ showContentPane(sibling);
1615
+ updateGraphForNode(sibling);
1616
+ });
1617
+
1618
+ siblingsList.appendChild(listItem);
1619
+ });
1620
+
1621
+ container.appendChild(siblingsList);
1622
+ }
1623
+
1624
+ // Add references section for applicable node types
1625
+ if (node.type === 'function' || node.type === 'class' || node.type === 'method' || node.type === 'code') {
1626
+ const directCallers = findReferences(node);
1627
+ const semanticRefs = findSemanticReferences(node);
1628
+
1629
+ if (directCallers.length > 0 || semanticRefs.length > 0) {
1630
+ const refsHeader = document.createElement('h4');
1631
+ refsHeader.textContent = 'References';
1632
+ refsHeader.style.color = '#8b949e';
1633
+ refsHeader.style.marginTop = siblings.length > 0 ? '20px' : '0';
1634
+ container.appendChild(refsHeader);
1635
+
1636
+ if (directCallers.length > 0) {
1637
+ const callersSubheader = document.createElement('h5');
1638
+ callersSubheader.textContent = 'Direct Callers (AST)';
1639
+ callersSubheader.style.color = '#58a6ff';
1640
+ callersSubheader.style.fontSize = '14px';
1641
+ callersSubheader.style.marginTop = '10px';
1642
+ container.appendChild(callersSubheader);
1643
+
1644
+ const callersList = document.createElement('ul');
1645
+ callersList.className = 'directory-list';
1646
+
1647
+ directCallers.forEach(caller => {
1648
+ const listItem = document.createElement('li');
1649
+ listItem.style.cursor = 'pointer';
1650
+ listItem.style.transition = 'background-color 0.2s';
1651
+ listItem.innerHTML = `
1652
+ <span class="item-icon">🔗</span>
1653
+ ${caller.name}
1654
+ <span class="item-type">${caller.type}</span>
1655
+ `;
1656
+
1657
+ listItem.addEventListener('mouseover', function() {
1658
+ this.style.backgroundColor = '#30363d';
1659
+ });
1660
+ listItem.addEventListener('mouseout', function() {
1661
+ this.style.backgroundColor = 'transparent';
1662
+ });
1663
+ listItem.addEventListener('click', () => {
1664
+ showContentPane(caller);
1665
+ updateGraphForNode(caller);
1666
+ });
1667
+
1668
+ callersList.appendChild(listItem);
1669
+ });
1670
+
1671
+ container.appendChild(callersList);
1672
+ }
1673
+
1674
+ if (semanticRefs.length > 0) {
1675
+ const semanticSubheader = document.createElement('h5');
1676
+ semanticSubheader.textContent = 'Semantic References';
1677
+ semanticSubheader.style.color = '#a371f7';
1678
+ semanticSubheader.style.fontSize = '14px';
1679
+ semanticSubheader.style.marginTop = '10px';
1680
+ container.appendChild(semanticSubheader);
1681
+
1682
+ const semanticList = document.createElement('ul');
1683
+ semanticList.className = 'directory-list';
1684
+
1685
+ semanticRefs.forEach(ref => {
1686
+ const listItem = document.createElement('li');
1687
+ listItem.style.cursor = 'pointer';
1688
+ listItem.style.transition = 'background-color 0.2s';
1689
+ listItem.style.borderLeft = '2px dotted #a371f7';
1690
+ listItem.style.paddingLeft = '10px';
1691
+ listItem.innerHTML = `
1692
+ <span class="item-icon">🔗</span>
1693
+ ${ref.name}
1694
+ <span class="item-type">${ref.type}</span>
1695
+ `;
1696
+
1697
+ listItem.addEventListener('mouseover', function() {
1698
+ this.style.backgroundColor = '#30363d';
1699
+ });
1700
+ listItem.addEventListener('mouseout', function() {
1701
+ this.style.backgroundColor = 'transparent';
1702
+ });
1703
+ listItem.addEventListener('click', () => {
1704
+ showContentPane(ref);
1705
+ updateGraphForNode(ref);
1706
+ });
1707
+
1708
+ semanticList.appendChild(listItem);
1709
+ });
1710
+
1711
+ container.appendChild(semanticList);
1712
+ }
1713
+ }
1714
+ }
1715
+
1716
+ // Find all chunks in this file
1717
+ const fileChunks = allLinks
1718
+ .filter(l => {
1719
+ const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
1720
+ return sourceId === node.id;
1721
+ })
1722
+ .map(l => {
1723
+ const targetId = typeof l.target === 'object' ? l.target.id : l.target;
1724
+ return allNodes.find(n => n.id === targetId);
1725
+ })
1726
+ .filter(n => n);
1727
+
1728
+ if (fileChunks.length === 0) {
1729
+ const emptyMsg = document.createElement('p');
1730
+ emptyMsg.textContent = 'No code chunks found in this file';
1731
+ emptyMsg.style.color = '#8b949e';
1732
+ if (siblings.length > 0) {
1733
+ emptyMsg.style.marginTop = '20px';
1734
+ }
1735
+ container.appendChild(emptyMsg);
1736
+ return;
1737
+ }
1738
+
1739
+ // Add chunks header
1740
+ const chunksHeader = document.createElement('h4');
1741
+ chunksHeader.textContent = 'Code Chunks';
1742
+ chunksHeader.style.color = '#8b949e';
1743
+ if (siblings.length > 0) {
1744
+ chunksHeader.style.marginTop = '20px';
1745
+ } else {
1746
+ chunksHeader.style.marginTop = '0';
1747
+ }
1748
+ container.appendChild(chunksHeader);
1749
+
1750
+ // Create clickable list of chunks
1751
+ const chunksList = document.createElement('ul');
1752
+ chunksList.className = 'directory-list';
1753
+
1754
+ fileChunks.forEach(chunk => {
1755
+ const icon = chunk.type === 'class' ? '🔷' :
1756
+ chunk.type === 'function' ? '⚡' :
1757
+ chunk.type === 'imports' ? '⇄' : '📝';
1758
+ const listItem = document.createElement('li');
1759
+ listItem.style.cursor = 'pointer';
1760
+ listItem.style.transition = 'background-color 0.2s';
1761
+
1762
+ // Improve display name for text chunks
1763
+ let displayName = chunk.name;
1764
+ if (chunk.type === 'text' && chunk.content) {
1765
+ // For text chunks, show first line of content as preview
1766
+ const firstLine = chunk.content.split('\n')[0].trim();
1767
+ if (firstLine.length > 0) {
1768
+ displayName = firstLine.length > 50 ? firstLine.substring(0, 47) + '...' : firstLine;
1769
+ }
1770
+ if (chunk.start_line) {
1771
+ displayName += ` (L${chunk.start_line}-${chunk.end_line})`;
1772
+ }
1773
+ } else if (chunk.start_line) {
1774
+ displayName += ` (L${chunk.start_line}-${chunk.end_line})`;
1775
+ }
1776
+
1777
+ listItem.innerHTML = `
1778
+ <span class="item-icon">${icon}</span>
1779
+ ${displayName}
1780
+ <span class="item-type">${chunk.type}</span>
1781
+ `;
1782
+
1783
+ listItem.addEventListener('mouseover', function() {
1784
+ this.style.backgroundColor = '#30363d';
1785
+ });
1786
+ listItem.addEventListener('mouseout', function() {
1787
+ this.style.backgroundColor = 'transparent';
1788
+ });
1789
+ listItem.addEventListener('click', function(event) {
1790
+ event.stopPropagation();
1791
+ if (hasChildren(chunk)) {
1792
+ if (collapsedNodes.has(chunk.id)) {
1793
+ expandNode(chunk);
1794
+ } else {
1795
+ collapseNode(chunk);
1796
+ }
1797
+ renderGraph();
1798
+ }
1799
+ showContentPane(chunk);
1800
+ updateGraphForNode(chunk);
1801
+ });
1802
+
1803
+ chunksList.appendChild(listItem);
1804
+ });
1805
+
1806
+ container.appendChild(chunksList);
1807
+
1808
+ // Collect all content from chunks and sort by line number
1809
+ const sortedChunks = fileChunks
1810
+ .filter(c => c.content)
1811
+ .sort((a, b) => a.start_line - b.start_line);
1812
+
1813
+ if (sortedChunks.length > 0) {
1814
+ // Add full content section
1815
+ const contentHeader = document.createElement('h4');
1816
+ contentHeader.textContent = 'Full File Content';
1817
+ contentHeader.style.color = '#8b949e';
1818
+ contentHeader.style.marginTop = '20px';
1819
+ container.appendChild(contentHeader);
1820
+
1821
+ const contentInfo = document.createElement('p');
1822
+ contentInfo.textContent = `Contains ${fileChunks.length} code chunks`;
1823
+ contentInfo.style.color = '#8b949e';
1824
+ contentInfo.style.fontSize = '11px';
1825
+ contentInfo.style.marginBottom = '12px';
1826
+ container.appendChild(contentInfo);
1827
+
1828
+ // Combine all chunks to show full file
1829
+ const fullContent = sortedChunks.map(c => c.content).join('\n\n');
1830
+ const pre = document.createElement('pre');
1831
+ const code = document.createElement('code');
1832
+ code.textContent = fullContent;
1833
+ pre.appendChild(code);
1834
+ container.appendChild(pre);
1835
+ }
1836
+ }
1837
+
1838
+ function showImportDetails(node, container) {
1839
+ // Import nodes (type === 'imports') - show import content prominently
1840
+ const importHtml = `
1841
+ <div class="import-details">
1842
+ ${node.content ? `
1843
+ <div style="margin-bottom: 16px;">
1844
+ <div class="detail-label" style="margin-bottom: 8px;">Import Statement:</div>
1845
+ <pre><code>${escapeHtml(node.content)}</code></pre>
1846
+ </div>
1847
+ ` : '<p style="color: #8b949e;">No import content available</p>'}
1848
+ <div class="detail-row">
1849
+ <span class="detail-label">File:</span> ${node.file_path}
1850
+ </div>
1851
+ ${node.start_line ? `
1852
+ <div class="detail-row">
1853
+ <span class="detail-label">Location:</span> Lines ${node.start_line}-${node.end_line}
1854
+ </div>
1855
+ ` : ''}
1856
+ ${node.language ? `
1857
+ <div class="detail-row">
1858
+ <span class="detail-label">Language:</span> ${node.language}
1859
+ </div>
1860
+ ` : ''}
1861
+ </div>
1862
+ `;
1863
+
1864
+ container.innerHTML = importHtml;
1865
+ }
1866
+
1867
+ function showCodeContent(node, container) {
1868
+ container.innerHTML = ''; // Clear first
1869
+
1870
+ // Add references section for applicable node types
1871
+ if (node.type === 'function' || node.type === 'class' || node.type === 'method' || node.type === 'code') {
1872
+ const directCallers = findReferences(node);
1873
+ const semanticRefs = findSemanticReferences(node);
1874
+
1875
+ if (directCallers.length > 0 || semanticRefs.length > 0) {
1876
+ const refsHeader = document.createElement('h4');
1877
+ refsHeader.textContent = 'References';
1878
+ refsHeader.style.color = '#8b949e';
1879
+ refsHeader.style.marginTop = '0';
1880
+ refsHeader.style.marginBottom = '10px';
1881
+ container.appendChild(refsHeader);
1882
+
1883
+ if (directCallers.length > 0) {
1884
+ const callersSubheader = document.createElement('h5');
1885
+ callersSubheader.textContent = 'Direct Callers (AST)';
1886
+ callersSubheader.style.color = '#58a6ff';
1887
+ callersSubheader.style.fontSize = '14px';
1888
+ callersSubheader.style.marginTop = '10px';
1889
+ container.appendChild(callersSubheader);
1890
+
1891
+ const callersList = document.createElement('ul');
1892
+ callersList.className = 'directory-list';
1893
+
1894
+ directCallers.forEach(caller => {
1895
+ const listItem = document.createElement('li');
1896
+ listItem.style.cursor = 'pointer';
1897
+ listItem.style.transition = 'background-color 0.2s';
1898
+ listItem.innerHTML = `
1899
+ <span class="item-icon">🔗</span>
1900
+ ${caller.name}
1901
+ <span class="item-type">${caller.type}</span>
1902
+ `;
1903
+
1904
+ listItem.addEventListener('mouseover', function() {
1905
+ this.style.backgroundColor = '#30363d';
1906
+ });
1907
+ listItem.addEventListener('mouseout', function() {
1908
+ this.style.backgroundColor = 'transparent';
1909
+ });
1910
+ listItem.addEventListener('click', () => {
1911
+ showContentPane(caller);
1912
+ updateGraphForNode(caller);
1913
+ });
1914
+
1915
+ callersList.appendChild(listItem);
1916
+ });
1917
+
1918
+ container.appendChild(callersList);
1919
+ }
1920
+
1921
+ if (semanticRefs.length > 0) {
1922
+ const semanticSubheader = document.createElement('h5');
1923
+ semanticSubheader.textContent = 'Semantic References';
1924
+ semanticSubheader.style.color = '#a371f7';
1925
+ semanticSubheader.style.fontSize = '14px';
1926
+ semanticSubheader.style.marginTop = '10px';
1927
+ container.appendChild(semanticSubheader);
1928
+
1929
+ const semanticList = document.createElement('ul');
1930
+ semanticList.className = 'directory-list';
1931
+
1932
+ semanticRefs.forEach(ref => {
1933
+ const listItem = document.createElement('li');
1934
+ listItem.style.cursor = 'pointer';
1935
+ listItem.style.transition = 'background-color 0.2s';
1936
+ listItem.style.borderLeft = '2px dotted #a371f7';
1937
+ listItem.style.paddingLeft = '10px';
1938
+ listItem.innerHTML = `
1939
+ <span class="item-icon">🔗</span>
1940
+ ${ref.name}
1941
+ <span class="item-type">${ref.type}</span>
1942
+ `;
1943
+
1944
+ listItem.addEventListener('mouseover', function() {
1945
+ this.style.backgroundColor = '#30363d';
1946
+ });
1947
+ listItem.addEventListener('mouseout', function() {
1948
+ this.style.backgroundColor = 'transparent';
1949
+ });
1950
+ listItem.addEventListener('click', () => {
1951
+ showContentPane(ref);
1952
+ updateGraphForNode(ref);
1953
+ });
1954
+
1955
+ semanticList.appendChild(listItem);
1956
+ });
1957
+
1958
+ container.appendChild(semanticList);
1959
+ }
1960
+
1961
+ // Add separator
1962
+ const separator = document.createElement('h4');
1963
+ separator.textContent = 'Code Content';
1964
+ separator.style.color = '#8b949e';
1965
+ separator.style.marginTop = '20px';
1966
+ separator.style.marginBottom = '10px';
1967
+ container.appendChild(separator);
1968
+ }
1969
+ }
1970
+
1971
+ // Show code for function, class, method, or code chunks
1972
+ if (node.docstring) {
1973
+ const docstringDiv = document.createElement('div');
1974
+ docstringDiv.style.cssText = 'margin-bottom: 16px; padding: 12px; background: #161b22; border: 1px solid #30363d; border-radius: 6px;';
1975
+
1976
+ const docLabel = document.createElement('div');
1977
+ docLabel.textContent = 'DOCSTRING';
1978
+ docLabel.style.cssText = 'font-size: 11px; color: #8b949e; margin-bottom: 8px; font-weight: 600;';
1979
+
1980
+ const docPre = document.createElement('pre');
1981
+ docPre.style.cssText = 'margin: 0; padding: 0; background: transparent; border: none;';
1982
+ const docCode = document.createElement('code');
1983
+ docCode.textContent = node.docstring;
1984
+ docPre.appendChild(docCode);
1985
+
1986
+ docstringDiv.appendChild(docLabel);
1987
+ docstringDiv.appendChild(docPre);
1988
+ container.appendChild(docstringDiv);
1989
+ }
1990
+
612
1991
  if (node.content) {
1992
+ const pre = document.createElement('pre');
1993
+ const code = document.createElement('code');
613
1994
  code.textContent = node.content;
614
- } else if (node.docstring) {
615
- code.textContent = `// Docstring:
616
- ${node.docstring}`;
1995
+ pre.appendChild(code);
1996
+ container.appendChild(pre);
617
1997
  } else {
618
- code.textContent = '// No content available';
1998
+ const noContent = document.createElement('p');
1999
+ noContent.textContent = 'No content available';
2000
+ noContent.style.color = '#8b949e';
2001
+ container.appendChild(noContent);
619
2002
  }
2003
+ }
620
2004
 
621
- viewer.classList.add('visible');
2005
+ function escapeHtml(text) {
2006
+ const div = document.createElement('div');
2007
+ div.textContent = text;
2008
+ return div.innerHTML;
622
2009
  }
623
2010
 
624
- function closeCodeViewer() {
625
- const viewer = document.getElementById('code-viewer');
626
- viewer.classList.remove('visible');
2011
+ function closeContentPane() {
2012
+ const pane = document.getElementById('content-pane');
2013
+ pane.classList.remove('visible');
627
2014
 
628
- // Remove highlight
2015
+ // Remove highlight and current node
2016
+ currentNode = null;
629
2017
  highlightedNode = null;
2018
+
2019
+ // Disable focus button
2020
+ document.getElementById('focus-button').disabled = true;
2021
+
630
2022
  renderGraph();
631
2023
  }
632
2024
 
2025
+ // Wire up Focus button
2026
+ document.getElementById('focus-button').addEventListener('click', () => {
2027
+ if (currentNode) {
2028
+ centerOnNode(currentNode);
2029
+ }
2030
+ });
2031
+
633
2032
  // Auto-load graph data on page load
634
2033
  window.addEventListener('DOMContentLoaded', () => {
635
2034
  const loadingEl = document.getElementById('loading');
636
2035
 
637
- fetch(`chunk-graph.json?t=${Date.now()}`)
2036
+ fetch("chunk-graph.json")
638
2037
  .then(response => {
639
2038
  if (!response.ok) {
640
2039
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);