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

@@ -2,6 +2,7 @@
2
2
 
3
3
  import asyncio
4
4
  import json
5
+ import shutil
5
6
  from pathlib import Path
6
7
 
7
8
  import typer
@@ -57,7 +58,9 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
57
58
  project_manager = ProjectManager(Path.cwd())
58
59
 
59
60
  if not project_manager.is_initialized():
60
- console.print("[red]Project not initialized. Run 'mcp-vector-search init' first.[/red]")
61
+ console.print(
62
+ "[red]Project not initialized. Run 'mcp-vector-search init' first.[/red]"
63
+ )
61
64
  raise typer.Exit(1)
62
65
 
63
66
  config = project_manager.load_config()
@@ -75,7 +78,9 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
75
78
  chunks = await database.get_all_chunks()
76
79
 
77
80
  if len(chunks) == 0:
78
- console.print("[yellow]No chunks found in index. Run 'mcp-vector-search index' first.[/yellow]")
81
+ console.print(
82
+ "[yellow]No chunks found in index. Run 'mcp-vector-search index' first.[/yellow]"
83
+ )
79
84
  raise typer.Exit(1)
80
85
 
81
86
  console.print(f"[green]✓[/green] Retrieved {len(chunks)} chunks")
@@ -83,8 +88,11 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
83
88
  # Apply file filter if specified
84
89
  if file_filter:
85
90
  from fnmatch import fnmatch
91
+
86
92
  chunks = [c for c in chunks if fnmatch(str(c.file_path), file_filter)]
87
- console.print(f"[cyan]Filtered to {len(chunks)} chunks matching '{file_filter}'[/cyan]")
93
+ console.print(
94
+ f"[cyan]Filtered to {len(chunks)} chunks matching '{file_filter}'[/cyan]"
95
+ )
88
96
 
89
97
  # Collect subprojects for monorepo support
90
98
  subprojects = {}
@@ -93,17 +101,23 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
93
101
  subprojects[chunk.subproject_name] = {
94
102
  "name": chunk.subproject_name,
95
103
  "path": chunk.subproject_path,
96
- "color": _get_subproject_color(chunk.subproject_name, len(subprojects)),
104
+ "color": _get_subproject_color(
105
+ chunk.subproject_name, len(subprojects)
106
+ ),
97
107
  }
98
108
 
99
109
  # Build graph data structure
100
110
  nodes = []
101
111
  links = []
102
112
  chunk_id_map = {} # Map chunk IDs to array indices
113
+ file_nodes = {} # Track file nodes by path
114
+ dir_nodes = {} # Track directory nodes by path
103
115
 
104
116
  # Add subproject root nodes for monorepos
105
117
  if subprojects:
106
- console.print(f"[cyan]Detected monorepo with {len(subprojects)} subprojects[/cyan]")
118
+ console.print(
119
+ f"[cyan]Detected monorepo with {len(subprojects)} subprojects[/cyan]"
120
+ )
107
121
  for sp_name, sp_data in subprojects.items():
108
122
  node = {
109
123
  "id": f"subproject_{sp_name}",
@@ -118,11 +132,96 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
118
132
  }
119
133
  nodes.append(node)
120
134
 
135
+ # Load directory index for enhanced directory metadata
136
+ console.print("[cyan]Loading directory index...[/cyan]")
137
+ from ...core.directory_index import DirectoryIndex
138
+
139
+ dir_index_path = (
140
+ project_manager.project_root / ".mcp-vector-search" / "directory_index.json"
141
+ )
142
+ dir_index = DirectoryIndex(dir_index_path)
143
+ dir_index.load()
144
+
145
+ # Create directory nodes from directory index
146
+ console.print(
147
+ f"[green]✓[/green] Loaded {len(dir_index.directories)} directories"
148
+ )
149
+ for dir_path_str, directory in dir_index.directories.items():
150
+ dir_id = f"dir_{hash(dir_path_str) & 0xFFFFFFFF:08x}"
151
+ dir_nodes[dir_path_str] = {
152
+ "id": dir_id,
153
+ "name": directory.name,
154
+ "type": "directory",
155
+ "file_path": dir_path_str,
156
+ "start_line": 0,
157
+ "end_line": 0,
158
+ "complexity": 0,
159
+ "depth": directory.depth,
160
+ "dir_path": dir_path_str,
161
+ "file_count": directory.file_count,
162
+ "subdirectory_count": directory.subdirectory_count,
163
+ "total_chunks": directory.total_chunks,
164
+ "languages": directory.languages or {},
165
+ "is_package": directory.is_package,
166
+ "last_modified": directory.last_modified,
167
+ }
168
+
169
+ # Create file nodes from chunks
170
+ for chunk in chunks:
171
+ file_path_str = str(chunk.file_path)
172
+ file_path = Path(file_path_str)
173
+
174
+ # Create file node with parent directory reference
175
+ if file_path_str not in file_nodes:
176
+ file_id = f"file_{hash(file_path_str) & 0xFFFFFFFF:08x}"
177
+
178
+ # Convert absolute path to relative path for parent directory lookup
179
+ try:
180
+ relative_file_path = file_path.relative_to(
181
+ project_manager.project_root
182
+ )
183
+ parent_dir = relative_file_path.parent
184
+ # Use relative path for parent directory (matches directory_index)
185
+ parent_dir_str = (
186
+ str(parent_dir) if parent_dir != Path(".") else None
187
+ )
188
+ except ValueError:
189
+ # File is outside project root
190
+ parent_dir_str = None
191
+
192
+ # Look up parent directory ID from dir_nodes (must match exactly)
193
+ parent_dir_id = None
194
+ if parent_dir_str and parent_dir_str in dir_nodes:
195
+ parent_dir_id = dir_nodes[parent_dir_str]["id"]
196
+
197
+ file_nodes[file_path_str] = {
198
+ "id": file_id,
199
+ "name": file_path.name,
200
+ "type": "file",
201
+ "file_path": file_path_str,
202
+ "start_line": 0,
203
+ "end_line": 0,
204
+ "complexity": 0,
205
+ "depth": len(file_path.parts) - 1,
206
+ "parent_dir_id": parent_dir_id,
207
+ "parent_dir_path": parent_dir_str,
208
+ }
209
+
210
+ # Add directory nodes to graph
211
+ for dir_node in dir_nodes.values():
212
+ nodes.append(dir_node)
213
+
214
+ # Add file nodes to graph
215
+ for file_node in file_nodes.values():
216
+ nodes.append(file_node)
217
+
121
218
  # Add chunk nodes
122
219
  for chunk in chunks:
123
220
  node = {
124
221
  "id": chunk.chunk_id or chunk.id,
125
- "name": chunk.function_name or chunk.class_name or f"L{chunk.start_line}",
222
+ "name": chunk.function_name
223
+ or chunk.class_name
224
+ or f"L{chunk.start_line}",
126
225
  "type": chunk.chunk_type,
127
226
  "file_path": str(chunk.file_path),
128
227
  "start_line": chunk.start_line,
@@ -130,6 +229,9 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
130
229
  "complexity": chunk.complexity_score,
131
230
  "parent_id": chunk.parent_chunk_id,
132
231
  "depth": chunk.chunk_depth,
232
+ "content": chunk.content, # Add content for code viewer
233
+ "docstring": chunk.docstring,
234
+ "language": chunk.language,
133
235
  }
134
236
 
135
237
  # Add subproject info for monorepos
@@ -140,34 +242,90 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
140
242
  nodes.append(node)
141
243
  chunk_id_map[node["id"]] = len(nodes) - 1
142
244
 
245
+ # Link directories to their parent directories (hierarchical structure)
246
+ for dir_path_str, dir_info in dir_index.directories.items():
247
+ if dir_info.parent_path:
248
+ parent_path_str = str(dir_info.parent_path)
249
+ if parent_path_str in dir_nodes:
250
+ parent_dir_id = f"dir_{hash(parent_path_str) & 0xFFFFFFFF:08x}"
251
+ child_dir_id = f"dir_{hash(dir_path_str) & 0xFFFFFFFF:08x}"
252
+ links.append(
253
+ {
254
+ "source": parent_dir_id,
255
+ "target": child_dir_id,
256
+ "type": "dir_hierarchy",
257
+ }
258
+ )
259
+
260
+ # Link directories to subprojects in monorepos (simple flat structure)
261
+ if subprojects:
262
+ for dir_path_str, dir_node in dir_nodes.items():
263
+ for sp_name, sp_data in subprojects.items():
264
+ if dir_path_str.startswith(sp_data.get("path", "")):
265
+ links.append(
266
+ {
267
+ "source": f"subproject_{sp_name}",
268
+ "target": dir_node["id"],
269
+ "type": "dir_containment",
270
+ }
271
+ )
272
+ break
273
+
274
+ # Link files to their parent directories
275
+ for _file_path_str, file_node in file_nodes.items():
276
+ if file_node.get("parent_dir_id"):
277
+ links.append(
278
+ {
279
+ "source": file_node["parent_dir_id"],
280
+ "target": file_node["id"],
281
+ "type": "dir_containment",
282
+ }
283
+ )
284
+
143
285
  # Build hierarchical links from parent-child relationships
144
286
  for chunk in chunks:
145
287
  chunk_id = chunk.chunk_id or chunk.id
288
+ file_path = str(chunk.file_path)
289
+
290
+ # Link chunk to its file node if it has no parent (top-level chunks)
291
+ if not chunk.parent_chunk_id and file_path in file_nodes:
292
+ links.append(
293
+ {
294
+ "source": file_nodes[file_path]["id"],
295
+ "target": chunk_id,
296
+ "type": "file_containment",
297
+ }
298
+ )
146
299
 
147
300
  # Link to subproject root if in monorepo
148
301
  if chunk.subproject_name and not chunk.parent_chunk_id:
149
- links.append({
150
- "source": f"subproject_{chunk.subproject_name}",
151
- "target": chunk_id,
152
- })
302
+ links.append(
303
+ {
304
+ "source": f"subproject_{chunk.subproject_name}",
305
+ "target": chunk_id,
306
+ }
307
+ )
153
308
 
154
309
  # Link to parent chunk
155
310
  if chunk.parent_chunk_id and chunk.parent_chunk_id in chunk_id_map:
156
- links.append({
157
- "source": chunk.parent_chunk_id,
158
- "target": chunk_id,
159
- })
311
+ links.append(
312
+ {
313
+ "source": chunk.parent_chunk_id,
314
+ "target": chunk_id,
315
+ }
316
+ )
160
317
 
161
318
  # Parse inter-project dependencies for monorepos
162
319
  if subprojects:
163
320
  console.print("[cyan]Parsing inter-project dependencies...[/cyan]")
164
321
  dep_links = _parse_project_dependencies(
165
- project_manager.project_root,
166
- subprojects
322
+ project_manager.project_root, subprojects
167
323
  )
168
324
  links.extend(dep_links)
169
325
  if dep_links:
170
- console.print(f"[green]✓[/green] Found {len(dep_links)} inter-project dependencies")
326
+ console.print(
327
+ f"[green]✓[/green] Found {len(dep_links)} inter-project dependencies"
328
+ )
171
329
 
172
330
  # Get stats
173
331
  stats = await database.get_stats()
@@ -261,11 +419,13 @@ def _parse_project_dependencies(project_root: Path, subprojects: dict) -> list[d
261
419
  for other_sp_name in subprojects.keys():
262
420
  if other_sp_name != sp_name and dep_name == other_sp_name:
263
421
  # Found inter-project dependency
264
- dependency_links.append({
265
- "source": f"subproject_{sp_name}",
266
- "target": f"subproject_{other_sp_name}",
267
- "type": "dependency",
268
- })
422
+ dependency_links.append(
423
+ {
424
+ "source": f"subproject_{sp_name}",
425
+ "target": f"subproject_{other_sp_name}",
426
+ "type": "dependency",
427
+ }
428
+ )
269
429
 
270
430
  except Exception as e:
271
431
  logger.debug(f"Failed to parse {package_json}: {e}")
@@ -276,7 +436,9 @@ def _parse_project_dependencies(project_root: Path, subprojects: dict) -> list[d
276
436
 
277
437
  @app.command()
278
438
  def serve(
279
- port: int = typer.Option(8080, "--port", "-p", help="Port for visualization server"),
439
+ port: int = typer.Option(
440
+ 8080, "--port", "-p", help="Port for visualization server"
441
+ ),
280
442
  graph_file: Path = typer.Option(
281
443
  Path("chunk-graph.json"),
282
444
  "--graph",
@@ -339,24 +501,30 @@ def serve(
339
501
 
340
502
  # Copy graph file to visualization directory if it exists
341
503
  if graph_file.exists():
342
- import shutil
343
-
344
504
  dest = viz_dir / "chunk-graph.json"
345
505
  shutil.copy(graph_file, dest)
346
506
  console.print(f"[green]✓[/green] Copied graph data to {dest}")
347
507
  else:
508
+ # Auto-generate graph file if it doesn't exist
348
509
  console.print(
349
- f"[yellow]Warning: Graph file {graph_file} not found. "
350
- "Run 'mcp-vector-search visualize export' first.[/yellow]"
510
+ f"[yellow]Graph file {graph_file} not found. Generating it now...[/yellow]"
351
511
  )
512
+ asyncio.run(_export_chunks(graph_file, None))
513
+ console.print()
514
+
515
+ # Copy the newly generated graph to visualization directory
516
+ if graph_file.exists():
517
+ dest = viz_dir / "chunk-graph.json"
518
+ shutil.copy(graph_file, dest)
519
+ console.print(f"[green]✓[/green] Copied graph data to {dest}")
352
520
 
353
521
  # Change to visualization directory
354
522
  os.chdir(viz_dir)
355
523
 
356
524
  # Start server
357
- Handler = http.server.SimpleHTTPRequestHandler
525
+ handler = http.server.SimpleHTTPRequestHandler
358
526
  try:
359
- with socketserver.TCPServer(("", port), Handler) as httpd:
527
+ with socketserver.TCPServer(("", port), handler) as httpd:
360
528
  url = f"http://localhost:{port}"
361
529
  console.print()
362
530
  console.print(
@@ -390,7 +558,7 @@ def serve(
390
558
 
391
559
  def _create_visualization_html(html_file: Path) -> None:
392
560
  """Create the D3.js visualization HTML file."""
393
- html_content = '''<!DOCTYPE html>
561
+ html_content = """<!DOCTYPE html>
394
562
  <html>
395
563
  <head>
396
564
  <meta charset="utf-8">
@@ -476,8 +644,30 @@ def _create_visualization_html(html_file: Path) -> None:
476
644
  .node.function circle { fill: #d29922; }
477
645
  .node.method circle { fill: #8957e5; }
478
646
  .node.code circle { fill: #6e7681; }
647
+ .node.file circle {
648
+ fill: none;
649
+ stroke: #58a6ff;
650
+ stroke-width: 2px;
651
+ stroke-dasharray: 5,3;
652
+ opacity: 0.6;
653
+ }
654
+ .node.directory circle {
655
+ fill: none;
656
+ stroke: #79c0ff;
657
+ stroke-width: 2px;
658
+ stroke-dasharray: 3,3;
659
+ opacity: 0.5;
660
+ }
479
661
  .node.subproject circle { fill: #da3633; stroke-width: 3px; }
480
662
 
663
+ /* Non-code document nodes - squares */
664
+ .node.docstring rect { fill: #8b949e; }
665
+ .node.comment rect { fill: #6e7681; }
666
+ .node rect {
667
+ stroke: #c9d1d9;
668
+ stroke-width: 1.5px;
669
+ }
670
+
481
671
  .node text {
482
672
  font-size: 11px;
483
673
  fill: #c9d1d9;
@@ -519,6 +709,146 @@ def _create_visualization_html(html_file: Path) -> None:
519
709
  font-size: 12px;
520
710
  color: #8b949e;
521
711
  }
712
+
713
+ #content-pane {
714
+ position: fixed;
715
+ top: 0;
716
+ right: 0;
717
+ width: 600px;
718
+ height: 100vh;
719
+ background: rgba(13, 17, 23, 0.98);
720
+ border-left: 1px solid #30363d;
721
+ overflow-y: auto;
722
+ box-shadow: -4px 0 24px rgba(0, 0, 0, 0.5);
723
+ transform: translateX(100%);
724
+ transition: transform 0.3s ease-in-out;
725
+ z-index: 1000;
726
+ }
727
+
728
+ #content-pane.visible {
729
+ transform: translateX(0);
730
+ }
731
+
732
+ #content-pane .pane-header {
733
+ position: sticky;
734
+ top: 0;
735
+ background: rgba(13, 17, 23, 0.98);
736
+ padding: 20px;
737
+ border-bottom: 1px solid #30363d;
738
+ z-index: 1;
739
+ }
740
+
741
+ #content-pane .pane-title {
742
+ font-size: 16px;
743
+ font-weight: bold;
744
+ color: #58a6ff;
745
+ margin-bottom: 8px;
746
+ padding-right: 30px;
747
+ }
748
+
749
+ #content-pane .pane-meta {
750
+ font-size: 12px;
751
+ color: #8b949e;
752
+ }
753
+
754
+ #content-pane .collapse-btn {
755
+ position: absolute;
756
+ top: 20px;
757
+ right: 20px;
758
+ cursor: pointer;
759
+ color: #8b949e;
760
+ font-size: 24px;
761
+ line-height: 1;
762
+ background: none;
763
+ border: none;
764
+ padding: 0;
765
+ transition: color 0.2s;
766
+ }
767
+
768
+ #content-pane .collapse-btn:hover {
769
+ color: #c9d1d9;
770
+ }
771
+
772
+ #content-pane .pane-content {
773
+ padding: 20px;
774
+ }
775
+
776
+ #content-pane pre {
777
+ margin: 0;
778
+ padding: 16px;
779
+ background: #0d1117;
780
+ border: 1px solid #30363d;
781
+ border-radius: 6px;
782
+ overflow-x: auto;
783
+ font-size: 12px;
784
+ line-height: 1.6;
785
+ }
786
+
787
+ #content-pane code {
788
+ color: #c9d1d9;
789
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
790
+ }
791
+
792
+ #content-pane .directory-list {
793
+ list-style: none;
794
+ padding: 0;
795
+ margin: 0;
796
+ }
797
+
798
+ #content-pane .directory-list li {
799
+ padding: 8px 12px;
800
+ margin: 4px 0;
801
+ background: #161b22;
802
+ border: 1px solid #30363d;
803
+ border-radius: 4px;
804
+ font-size: 12px;
805
+ display: flex;
806
+ align-items: center;
807
+ }
808
+
809
+ #content-pane .directory-list .item-icon {
810
+ margin-right: 8px;
811
+ font-size: 14px;
812
+ }
813
+
814
+ #content-pane .directory-list .item-type {
815
+ margin-left: auto;
816
+ padding-left: 12px;
817
+ font-size: 10px;
818
+ color: #8b949e;
819
+ }
820
+
821
+ #content-pane .import-details {
822
+ background: #161b22;
823
+ border: 1px solid #30363d;
824
+ border-radius: 6px;
825
+ padding: 16px;
826
+ }
827
+
828
+ #content-pane .import-details .import-statement {
829
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
830
+ font-size: 12px;
831
+ color: #79c0ff;
832
+ margin-bottom: 12px;
833
+ }
834
+
835
+ #content-pane .import-details .detail-row {
836
+ font-size: 11px;
837
+ color: #8b949e;
838
+ margin: 4px 0;
839
+ }
840
+
841
+ #content-pane .import-details .detail-label {
842
+ color: #c9d1d9;
843
+ font-weight: 600;
844
+ }
845
+
846
+ .node.highlighted circle,
847
+ .node.highlighted rect {
848
+ stroke: #f0e68c;
849
+ stroke-width: 3px;
850
+ filter: drop-shadow(0 0 8px #f0e68c);
851
+ }
522
852
  </style>
523
853
  </head>
524
854
  <body>
@@ -534,6 +864,12 @@ def _create_visualization_html(html_file: Path) -> None:
534
864
  <div class="legend-item">
535
865
  <span class="legend-color" style="background: #da3633;"></span> Subproject
536
866
  </div>
867
+ <div class="legend-item">
868
+ <span class="legend-color" style="border: 2px dashed #79c0ff; border-radius: 50%; background: transparent;"></span> Directory
869
+ </div>
870
+ <div class="legend-item">
871
+ <span class="legend-color" style="border: 2px dashed #58a6ff; border-radius: 50%; background: transparent;"></span> File
872
+ </div>
537
873
  <div class="legend-item">
538
874
  <span class="legend-color" style="background: #238636;"></span> Module
539
875
  </div>
@@ -549,6 +885,15 @@ def _create_visualization_html(html_file: Path) -> None:
549
885
  <div class="legend-item">
550
886
  <span class="legend-color" style="background: #6e7681;"></span> Code
551
887
  </div>
888
+ <div class="legend-item" style="font-style: italic; color: #79c0ff;">
889
+ <span class="legend-color" style="background: #6e7681;"></span> Import (L1)
890
+ </div>
891
+ <div class="legend-item">
892
+ <span class="legend-color" style="background: #8b949e; border-radius: 2px;"></span> Docstring ▢
893
+ </div>
894
+ <div class="legend-item">
895
+ <span class="legend-color" style="background: #6e7681; border-radius: 2px;"></span> Comment ▢
896
+ </div>
552
897
  </div>
553
898
 
554
899
  <div id="subprojects-legend" style="display: none;">
@@ -562,6 +907,15 @@ def _create_visualization_html(html_file: Path) -> None:
562
907
  <svg id="graph"></svg>
563
908
  <div id="tooltip" class="tooltip"></div>
564
909
 
910
+ <div id="content-pane">
911
+ <div class="pane-header">
912
+ <button class="collapse-btn" onclick="closeContentPane()">×</button>
913
+ <div class="pane-title" id="pane-title"></div>
914
+ <div class="pane-meta" id="pane-meta"></div>
915
+ </div>
916
+ <div class="pane-content" id="pane-content"></div>
917
+ </div>
918
+
565
919
  <script>
566
920
  const width = window.innerWidth;
567
921
  const height = window.innerHeight;
@@ -580,6 +934,7 @@ def _create_visualization_html(html_file: Path) -> None:
580
934
  let allLinks = [];
581
935
  let visibleNodes = new Set();
582
936
  let collapsedNodes = new Set();
937
+ let highlightedNode = null;
583
938
 
584
939
  function visualizeGraph(data) {
585
940
  g.selectAll("*").remove();
@@ -587,19 +942,37 @@ def _create_visualization_html(html_file: Path) -> None:
587
942
  allNodes = data.nodes;
588
943
  allLinks = data.links;
589
944
 
590
- // Find root nodes
945
+ // Find root nodes - start with only top-level nodes
591
946
  let rootNodes;
592
947
  if (data.metadata && data.metadata.is_monorepo) {
593
948
  // In monorepos, subproject nodes are roots
594
949
  rootNodes = allNodes.filter(n => n.type === 'subproject');
595
950
  } else {
596
- // Regular projects: nodes without parents or depth 0/1
597
- rootNodes = allNodes.filter(n =>
598
- !n.parent_id || n.depth === 0 || n.depth === 1 || n.type === 'module'
599
- );
951
+ // Regular projects: show root-level directories AND files
952
+ const dirNodes = allNodes.filter(n => n.type === 'directory');
953
+ const fileNodes = allNodes.filter(n => n.type === 'file');
954
+
955
+ // Find minimum depth for directories and files
956
+ const minDirDepth = dirNodes.length > 0
957
+ ? Math.min(...dirNodes.map(n => n.depth))
958
+ : Infinity;
959
+ const minFileDepth = fileNodes.length > 0
960
+ ? Math.min(...fileNodes.map(n => n.depth))
961
+ : Infinity;
962
+
963
+ // Include both root-level directories and root-level files
964
+ rootNodes = [
965
+ ...dirNodes.filter(n => n.depth === minDirDepth),
966
+ ...fileNodes.filter(n => n.depth === minFileDepth)
967
+ ];
968
+
969
+ // Fallback to all files if nothing found
970
+ if (rootNodes.length === 0) {
971
+ rootNodes = fileNodes;
972
+ }
600
973
  }
601
974
 
602
- // Start with only root nodes visible
975
+ // Start with only root nodes visible, all collapsed
603
976
  visibleNodes = new Set(rootNodes.map(n => n.id));
604
977
  collapsedNodes = new Set(rootNodes.map(n => n.id));
605
978
 
@@ -631,22 +1004,59 @@ def _create_visualization_html(html_file: Path) -> None:
631
1004
  .selectAll("g")
632
1005
  .data(visibleNodesList)
633
1006
  .join("g")
634
- .attr("class", d => `node ${d.type}`)
1007
+ .attr("class", d => {
1008
+ let classes = `node ${d.type}`;
1009
+ if (highlightedNode && d.id === highlightedNode.id) {
1010
+ classes += ' highlighted';
1011
+ }
1012
+ return classes;
1013
+ })
635
1014
  .call(drag(simulation))
636
- .on("click", toggleNode)
1015
+ .on("click", handleNodeClick)
637
1016
  .on("mouseover", showTooltip)
638
1017
  .on("mouseout", hideTooltip);
639
1018
 
640
- // Add circles with expand indicator
641
- node.append("circle")
1019
+ // Add shapes based on node type (circles for code, squares for docs)
1020
+ const isDocNode = d => ['docstring', 'comment'].includes(d.type);
1021
+
1022
+ // Add circles for code nodes
1023
+ node.filter(d => !isDocNode(d))
1024
+ .append("circle")
642
1025
  .attr("r", d => {
643
1026
  if (d.type === 'subproject') return 20;
1027
+ if (d.type === 'directory') return 40; // Largest for directory containers
1028
+ if (d.type === 'file') return 30; // Larger transparent circle for files
644
1029
  return d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
645
1030
  })
646
1031
  .attr("stroke", d => hasChildren(d) ? "#ffffff" : "none")
647
1032
  .attr("stroke-width", d => hasChildren(d) ? 2 : 0)
648
1033
  .style("fill", d => d.color || null); // Use custom color if available
649
1034
 
1035
+ // Add rectangles for document nodes
1036
+ node.filter(d => isDocNode(d))
1037
+ .append("rect")
1038
+ .attr("width", d => {
1039
+ const size = d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
1040
+ return size * 2;
1041
+ })
1042
+ .attr("height", d => {
1043
+ const size = d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
1044
+ return size * 2;
1045
+ })
1046
+ .attr("x", d => {
1047
+ const size = d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
1048
+ return -size;
1049
+ })
1050
+ .attr("y", d => {
1051
+ const size = d.complexity ? Math.min(8 + d.complexity * 2, 25) : 12;
1052
+ return -size;
1053
+ })
1054
+ .attr("rx", 2) // Rounded corners
1055
+ .attr("ry", 2)
1056
+ .attr("stroke", d => hasChildren(d) ? "#ffffff" : "none")
1057
+ .attr("stroke-width", d => hasChildren(d) ? 2 : 0)
1058
+ .style("fill", d => d.color || null);
1059
+
650
1060
  // Add expand/collapse indicator
651
1061
  node.filter(d => hasChildren(d))
652
1062
  .append("text")
@@ -659,9 +1069,15 @@ def _create_visualization_html(html_file: Path) -> None:
659
1069
  .style("pointer-events", "none")
660
1070
  .text(d => collapsedNodes.has(d.id) ? "+" : "−");
661
1071
 
662
- // Add labels
1072
+ // Add labels (show "Import:" prefix for L1 nodes)
663
1073
  node.append("text")
664
- .text(d => d.name)
1074
+ .text(d => {
1075
+ // L1 (depth 1) nodes are imports
1076
+ if (d.depth === 1 && d.type !== 'directory' && d.type !== 'file') {
1077
+ return `Import: ${d.name}`;
1078
+ }
1079
+ return d.name;
1080
+ })
665
1081
  .attr("dy", 30);
666
1082
 
667
1083
  simulation.on("tick", () => {
@@ -681,20 +1097,21 @@ def _create_visualization_html(html_file: Path) -> None:
681
1097
  return allLinks.some(l => (l.source.id || l.source) === node.id);
682
1098
  }
683
1099
 
684
- function toggleNode(event, d) {
1100
+ function handleNodeClick(event, d) {
685
1101
  event.stopPropagation();
686
1102
 
687
- if (!hasChildren(d)) return;
1103
+ // Always show content pane when clicking any node
1104
+ showContentPane(d);
688
1105
 
689
- if (collapsedNodes.has(d.id)) {
690
- // Expand: show children
691
- expandNode(d);
692
- } else {
693
- // Collapse: hide children
694
- collapseNode(d);
1106
+ // If node has children, also toggle expansion
1107
+ if (hasChildren(d)) {
1108
+ if (collapsedNodes.has(d.id)) {
1109
+ expandNode(d);
1110
+ } else {
1111
+ collapseNode(d);
1112
+ }
1113
+ renderGraph();
695
1114
  }
696
-
697
- renderGraph();
698
1115
  }
699
1116
 
700
1117
  function expandNode(node) {
@@ -802,6 +1219,213 @@ def _create_visualization_html(html_file: Path) -> None:
802
1219
  }
803
1220
  }
804
1221
 
1222
+ function showContentPane(node) {
1223
+ // Highlight the node
1224
+ highlightedNode = node;
1225
+ renderGraph();
1226
+
1227
+ // Populate content pane
1228
+ const pane = document.getElementById('content-pane');
1229
+ const title = document.getElementById('pane-title');
1230
+ const meta = document.getElementById('pane-meta');
1231
+ const content = document.getElementById('pane-content');
1232
+
1233
+ // Set title with "Import:" prefix for L1 nodes
1234
+ if (node.depth === 1 && node.type !== 'directory' && node.type !== 'file') {
1235
+ title.textContent = `Import: ${node.name}`;
1236
+ } else {
1237
+ title.textContent = node.name;
1238
+ }
1239
+
1240
+ // Set metadata
1241
+ let metaText = `${node.type} • ${node.file_path}`;
1242
+ if (node.start_line) {
1243
+ metaText += ` • Lines ${node.start_line}-${node.end_line}`;
1244
+ }
1245
+ if (node.language) {
1246
+ metaText += ` • ${node.language}`;
1247
+ }
1248
+ meta.textContent = metaText;
1249
+
1250
+ // Display content based on node type
1251
+ if (node.type === 'directory') {
1252
+ showDirectoryContents(node, content);
1253
+ } else if (node.type === 'file') {
1254
+ showFileContents(node, content);
1255
+ } else if (node.depth === 1 && node.type !== 'directory' && node.type !== 'file') {
1256
+ // L1 nodes are imports
1257
+ showImportDetails(node, content);
1258
+ } else {
1259
+ // Class, function, method, code nodes
1260
+ showCodeContent(node, content);
1261
+ }
1262
+
1263
+ pane.classList.add('visible');
1264
+ }
1265
+
1266
+ function showDirectoryContents(node, container) {
1267
+ // Find all direct children of this directory
1268
+ const children = allLinks
1269
+ .filter(l => (l.source.id || l.source) === node.id)
1270
+ .map(l => allNodes.find(n => n.id === (l.target.id || l.target)))
1271
+ .filter(n => n);
1272
+
1273
+ if (children.length === 0) {
1274
+ container.innerHTML = '<p style="color: #8b949e;">Empty directory</p>';
1275
+ return;
1276
+ }
1277
+
1278
+ // Group by type
1279
+ const files = children.filter(n => n.type === 'file');
1280
+ const subdirs = children.filter(n => n.type === 'directory');
1281
+ const chunks = children.filter(n => n.type !== 'file' && n.type !== 'directory');
1282
+
1283
+ let html = '<ul class="directory-list">';
1284
+
1285
+ // Show subdirectories first
1286
+ subdirs.forEach(child => {
1287
+ html += `
1288
+ <li>
1289
+ <span class="item-icon">📁</span>
1290
+ ${child.name}
1291
+ <span class="item-type">directory</span>
1292
+ </li>
1293
+ `;
1294
+ });
1295
+
1296
+ // Then files
1297
+ files.forEach(child => {
1298
+ html += `
1299
+ <li>
1300
+ <span class="item-icon">📄</span>
1301
+ ${child.name}
1302
+ <span class="item-type">file</span>
1303
+ </li>
1304
+ `;
1305
+ });
1306
+
1307
+ // Then code chunks
1308
+ chunks.forEach(child => {
1309
+ const icon = child.type === 'class' ? '🔷' : child.type === 'function' ? '⚡' : '📝';
1310
+ html += `
1311
+ <li>
1312
+ <span class="item-icon">${icon}</span>
1313
+ ${child.name}
1314
+ <span class="item-type">${child.type}</span>
1315
+ </li>
1316
+ `;
1317
+ });
1318
+
1319
+ html += '</ul>';
1320
+
1321
+ // Add summary
1322
+ const summary = `<p style="color: #8b949e; font-size: 11px; margin-top: 16px;">
1323
+ Total: ${children.length} items (${subdirs.length} directories, ${files.length} files, ${chunks.length} code chunks)
1324
+ </p>`;
1325
+
1326
+ container.innerHTML = html + summary;
1327
+ }
1328
+
1329
+ function showFileContents(node, container) {
1330
+ // Find all chunks in this file
1331
+ const fileChunks = allLinks
1332
+ .filter(l => (l.source.id || l.source) === node.id)
1333
+ .map(l => allNodes.find(n => n.id === (l.target.id || l.target)))
1334
+ .filter(n => n);
1335
+
1336
+ if (fileChunks.length === 0) {
1337
+ container.innerHTML = '<p style="color: #8b949e;">No code chunks found in this file</p>';
1338
+ return;
1339
+ }
1340
+
1341
+ // Collect all content from chunks and sort by line number
1342
+ const sortedChunks = fileChunks
1343
+ .filter(c => c.content)
1344
+ .sort((a, b) => a.start_line - b.start_line);
1345
+
1346
+ if (sortedChunks.length === 0) {
1347
+ container.innerHTML = '<p style="color: #8b949e;">File content not available</p>';
1348
+ return;
1349
+ }
1350
+
1351
+ // Combine all chunks to show full file
1352
+ const fullContent = sortedChunks.map(c => c.content).join('\n\n');
1353
+
1354
+ container.innerHTML = `
1355
+ <p style="color: #8b949e; font-size: 11px; margin-bottom: 12px;">
1356
+ Contains ${fileChunks.length} code chunks
1357
+ </p>
1358
+ <pre><code>${escapeHtml(fullContent)}</code></pre>
1359
+ `;
1360
+ }
1361
+
1362
+ function showImportDetails(node, container) {
1363
+ // L1 nodes are import statements
1364
+ const importHtml = `
1365
+ <div class="import-details">
1366
+ <div class="import-statement">${escapeHtml(node.name)}</div>
1367
+ <div class="detail-row">
1368
+ <span class="detail-label">File:</span> ${node.file_path}
1369
+ </div>
1370
+ ${node.start_line ? `
1371
+ <div class="detail-row">
1372
+ <span class="detail-label">Location:</span> Lines ${node.start_line}-${node.end_line}
1373
+ </div>
1374
+ ` : ''}
1375
+ ${node.language ? `
1376
+ <div class="detail-row">
1377
+ <span class="detail-label">Language:</span> ${node.language}
1378
+ </div>
1379
+ ` : ''}
1380
+ ${node.content ? `
1381
+ <div style="margin-top: 16px;">
1382
+ <div class="detail-label" style="margin-bottom: 8px;">Import Statement:</div>
1383
+ <pre><code>${escapeHtml(node.content)}</code></pre>
1384
+ </div>
1385
+ ` : ''}
1386
+ </div>
1387
+ `;
1388
+
1389
+ container.innerHTML = importHtml;
1390
+ }
1391
+
1392
+ function showCodeContent(node, container) {
1393
+ // Show code for function, class, method, or code chunks
1394
+ let html = '';
1395
+
1396
+ if (node.docstring) {
1397
+ html += `
1398
+ <div style="margin-bottom: 16px; padding: 12px; background: #161b22; border: 1px solid #30363d; border-radius: 6px;">
1399
+ <div style="font-size: 11px; color: #8b949e; margin-bottom: 8px; font-weight: 600;">DOCSTRING</div>
1400
+ <pre style="margin: 0; padding: 0; background: transparent; border: none;"><code>${escapeHtml(node.docstring)}</code></pre>
1401
+ </div>
1402
+ `;
1403
+ }
1404
+
1405
+ if (node.content) {
1406
+ html += `<pre><code>${escapeHtml(node.content)}</code></pre>`;
1407
+ } else {
1408
+ html += '<p style="color: #8b949e;">No content available</p>';
1409
+ }
1410
+
1411
+ container.innerHTML = html;
1412
+ }
1413
+
1414
+ function escapeHtml(text) {
1415
+ const div = document.createElement('div');
1416
+ div.textContent = text;
1417
+ return div.innerHTML;
1418
+ }
1419
+
1420
+ function closeContentPane() {
1421
+ const pane = document.getElementById('content-pane');
1422
+ pane.classList.remove('visible');
1423
+
1424
+ // Remove highlight
1425
+ highlightedNode = null;
1426
+ renderGraph();
1427
+ }
1428
+
805
1429
  // Auto-load graph data on page load
806
1430
  window.addEventListener('DOMContentLoaded', () => {
807
1431
  const loadingEl = document.getElementById('loading');
@@ -827,7 +1451,7 @@ def _create_visualization_html(html_file: Path) -> None:
827
1451
  });
828
1452
  </script>
829
1453
  </body>
830
- </html>'''
1454
+ </html>"""
831
1455
 
832
1456
  with open(html_file, "w") as f:
833
1457
  f.write(html_content)