mcp-vector-search 0.12.0__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,7 +101,9 @@ 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
@@ -105,7 +115,9 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
105
115
 
106
116
  # Add subproject root nodes for monorepos
107
117
  if subprojects:
108
- 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
+ )
109
121
  for sp_name, sp_data in subprojects.items():
110
122
  node = {
111
123
  "id": f"subproject_{sp_name}",
@@ -123,14 +135,19 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
123
135
  # Load directory index for enhanced directory metadata
124
136
  console.print("[cyan]Loading directory index...[/cyan]")
125
137
  from ...core.directory_index import DirectoryIndex
126
- dir_index_path = project_manager.project_root / ".mcp-vector-search" / "directory_index.json"
138
+
139
+ dir_index_path = (
140
+ project_manager.project_root / ".mcp-vector-search" / "directory_index.json"
141
+ )
127
142
  dir_index = DirectoryIndex(dir_index_path)
128
143
  dir_index.load()
129
144
 
130
145
  # Create directory nodes from directory index
131
- console.print(f"[green]✓[/green] Loaded {len(dir_index.directories)} directories")
146
+ console.print(
147
+ f"[green]✓[/green] Loaded {len(dir_index.directories)} directories"
148
+ )
132
149
  for dir_path_str, directory in dir_index.directories.items():
133
- dir_id = f"dir_{hash(dir_path_str) & 0xffffffff:08x}"
150
+ dir_id = f"dir_{hash(dir_path_str) & 0xFFFFFFFF:08x}"
134
151
  dir_nodes[dir_path_str] = {
135
152
  "id": dir_id,
136
153
  "name": directory.name,
@@ -156,14 +173,18 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
156
173
 
157
174
  # Create file node with parent directory reference
158
175
  if file_path_str not in file_nodes:
159
- file_id = f"file_{hash(file_path_str) & 0xffffffff:08x}"
176
+ file_id = f"file_{hash(file_path_str) & 0xFFFFFFFF:08x}"
160
177
 
161
178
  # Convert absolute path to relative path for parent directory lookup
162
179
  try:
163
- relative_file_path = file_path.relative_to(project_manager.project_root)
180
+ relative_file_path = file_path.relative_to(
181
+ project_manager.project_root
182
+ )
164
183
  parent_dir = relative_file_path.parent
165
184
  # Use relative path for parent directory (matches directory_index)
166
- parent_dir_str = str(parent_dir) if parent_dir != Path(".") else None
185
+ parent_dir_str = (
186
+ str(parent_dir) if parent_dir != Path(".") else None
187
+ )
167
188
  except ValueError:
168
189
  # File is outside project root
169
190
  parent_dir_str = None
@@ -198,7 +219,9 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
198
219
  for chunk in chunks:
199
220
  node = {
200
221
  "id": chunk.chunk_id or chunk.id,
201
- "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}",
202
225
  "type": chunk.chunk_type,
203
226
  "file_path": str(chunk.file_path),
204
227
  "start_line": chunk.start_line,
@@ -224,34 +247,40 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
224
247
  if dir_info.parent_path:
225
248
  parent_path_str = str(dir_info.parent_path)
226
249
  if parent_path_str in dir_nodes:
227
- parent_dir_id = f"dir_{hash(parent_path_str) & 0xffffffff:08x}"
228
- child_dir_id = f"dir_{hash(dir_path_str) & 0xffffffff:08x}"
229
- links.append({
230
- "source": parent_dir_id,
231
- "target": child_dir_id,
232
- "type": "dir_hierarchy",
233
- })
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
+ )
234
259
 
235
260
  # Link directories to subprojects in monorepos (simple flat structure)
236
261
  if subprojects:
237
262
  for dir_path_str, dir_node in dir_nodes.items():
238
263
  for sp_name, sp_data in subprojects.items():
239
264
  if dir_path_str.startswith(sp_data.get("path", "")):
240
- links.append({
241
- "source": f"subproject_{sp_name}",
242
- "target": dir_node["id"],
243
- "type": "dir_containment",
244
- })
265
+ links.append(
266
+ {
267
+ "source": f"subproject_{sp_name}",
268
+ "target": dir_node["id"],
269
+ "type": "dir_containment",
270
+ }
271
+ )
245
272
  break
246
273
 
247
274
  # Link files to their parent directories
248
- for file_path_str, file_node in file_nodes.items():
275
+ for _file_path_str, file_node in file_nodes.items():
249
276
  if file_node.get("parent_dir_id"):
250
- links.append({
251
- "source": file_node["parent_dir_id"],
252
- "target": file_node["id"],
253
- "type": "dir_containment",
254
- })
277
+ links.append(
278
+ {
279
+ "source": file_node["parent_dir_id"],
280
+ "target": file_node["id"],
281
+ "type": "dir_containment",
282
+ }
283
+ )
255
284
 
256
285
  # Build hierarchical links from parent-child relationships
257
286
  for chunk in chunks:
@@ -260,36 +289,43 @@ async def _export_chunks(output: Path, file_filter: str | None) -> None:
260
289
 
261
290
  # Link chunk to its file node if it has no parent (top-level chunks)
262
291
  if not chunk.parent_chunk_id and file_path in file_nodes:
263
- links.append({
264
- "source": file_nodes[file_path]["id"],
265
- "target": chunk_id,
266
- "type": "file_containment",
267
- })
292
+ links.append(
293
+ {
294
+ "source": file_nodes[file_path]["id"],
295
+ "target": chunk_id,
296
+ "type": "file_containment",
297
+ }
298
+ )
268
299
 
269
300
  # Link to subproject root if in monorepo
270
301
  if chunk.subproject_name and not chunk.parent_chunk_id:
271
- links.append({
272
- "source": f"subproject_{chunk.subproject_name}",
273
- "target": chunk_id,
274
- })
302
+ links.append(
303
+ {
304
+ "source": f"subproject_{chunk.subproject_name}",
305
+ "target": chunk_id,
306
+ }
307
+ )
275
308
 
276
309
  # Link to parent chunk
277
310
  if chunk.parent_chunk_id and chunk.parent_chunk_id in chunk_id_map:
278
- links.append({
279
- "source": chunk.parent_chunk_id,
280
- "target": chunk_id,
281
- })
311
+ links.append(
312
+ {
313
+ "source": chunk.parent_chunk_id,
314
+ "target": chunk_id,
315
+ }
316
+ )
282
317
 
283
318
  # Parse inter-project dependencies for monorepos
284
319
  if subprojects:
285
320
  console.print("[cyan]Parsing inter-project dependencies...[/cyan]")
286
321
  dep_links = _parse_project_dependencies(
287
- project_manager.project_root,
288
- subprojects
322
+ project_manager.project_root, subprojects
289
323
  )
290
324
  links.extend(dep_links)
291
325
  if dep_links:
292
- 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
+ )
293
329
 
294
330
  # Get stats
295
331
  stats = await database.get_stats()
@@ -383,11 +419,13 @@ def _parse_project_dependencies(project_root: Path, subprojects: dict) -> list[d
383
419
  for other_sp_name in subprojects.keys():
384
420
  if other_sp_name != sp_name and dep_name == other_sp_name:
385
421
  # Found inter-project dependency
386
- dependency_links.append({
387
- "source": f"subproject_{sp_name}",
388
- "target": f"subproject_{other_sp_name}",
389
- "type": "dependency",
390
- })
422
+ dependency_links.append(
423
+ {
424
+ "source": f"subproject_{sp_name}",
425
+ "target": f"subproject_{other_sp_name}",
426
+ "type": "dependency",
427
+ }
428
+ )
391
429
 
392
430
  except Exception as e:
393
431
  logger.debug(f"Failed to parse {package_json}: {e}")
@@ -398,7 +436,9 @@ def _parse_project_dependencies(project_root: Path, subprojects: dict) -> list[d
398
436
 
399
437
  @app.command()
400
438
  def serve(
401
- 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
+ ),
402
442
  graph_file: Path = typer.Option(
403
443
  Path("chunk-graph.json"),
404
444
  "--graph",
@@ -461,24 +501,30 @@ def serve(
461
501
 
462
502
  # Copy graph file to visualization directory if it exists
463
503
  if graph_file.exists():
464
- import shutil
465
-
466
504
  dest = viz_dir / "chunk-graph.json"
467
505
  shutil.copy(graph_file, dest)
468
506
  console.print(f"[green]✓[/green] Copied graph data to {dest}")
469
507
  else:
508
+ # Auto-generate graph file if it doesn't exist
470
509
  console.print(
471
- f"[yellow]Warning: Graph file {graph_file} not found. "
472
- "Run 'mcp-vector-search visualize export' first.[/yellow]"
510
+ f"[yellow]Graph file {graph_file} not found. Generating it now...[/yellow]"
473
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}")
474
520
 
475
521
  # Change to visualization directory
476
522
  os.chdir(viz_dir)
477
523
 
478
524
  # Start server
479
- Handler = http.server.SimpleHTTPRequestHandler
525
+ handler = http.server.SimpleHTTPRequestHandler
480
526
  try:
481
- with socketserver.TCPServer(("", port), Handler) as httpd:
527
+ with socketserver.TCPServer(("", port), handler) as httpd:
482
528
  url = f"http://localhost:{port}"
483
529
  console.print()
484
530
  console.print(
@@ -512,7 +558,7 @@ def serve(
512
558
 
513
559
  def _create_visualization_html(html_file: Path) -> None:
514
560
  """Create the D3.js visualization HTML file."""
515
- html_content = '''<!DOCTYPE html>
561
+ html_content = """<!DOCTYPE html>
516
562
  <html>
517
563
  <head>
518
564
  <meta charset="utf-8">
@@ -664,72 +710,139 @@ def _create_visualization_html(html_file: Path) -> None:
664
710
  color: #8b949e;
665
711
  }
666
712
 
667
- #code-viewer {
668
- position: absolute;
669
- top: 20px;
670
- right: 20px;
671
- width: 500px;
672
- max-height: 80vh;
673
- background: rgba(13, 17, 23, 0.95);
674
- border: 1px solid #30363d;
675
- border-radius: 6px;
676
- padding: 16px;
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;
677
721
  overflow-y: auto;
678
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
679
- display: none;
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;
680
726
  }
681
727
 
682
- #code-viewer.visible {
683
- display: block;
728
+ #content-pane.visible {
729
+ transform: translateX(0);
684
730
  }
685
731
 
686
- #code-viewer .header {
687
- margin-bottom: 12px;
688
- padding-bottom: 12px;
732
+ #content-pane .pane-header {
733
+ position: sticky;
734
+ top: 0;
735
+ background: rgba(13, 17, 23, 0.98);
736
+ padding: 20px;
689
737
  border-bottom: 1px solid #30363d;
738
+ z-index: 1;
690
739
  }
691
740
 
692
- #code-viewer .title {
693
- font-size: 14px;
741
+ #content-pane .pane-title {
742
+ font-size: 16px;
694
743
  font-weight: bold;
695
744
  color: #58a6ff;
696
- margin-bottom: 4px;
745
+ margin-bottom: 8px;
746
+ padding-right: 30px;
697
747
  }
698
748
 
699
- #code-viewer .meta {
700
- font-size: 11px;
749
+ #content-pane .pane-meta {
750
+ font-size: 12px;
701
751
  color: #8b949e;
702
752
  }
703
753
 
704
- #code-viewer .close-btn {
705
- float: right;
754
+ #content-pane .collapse-btn {
755
+ position: absolute;
756
+ top: 20px;
757
+ right: 20px;
706
758
  cursor: pointer;
707
759
  color: #8b949e;
708
- font-size: 20px;
760
+ font-size: 24px;
709
761
  line-height: 1;
710
- margin-top: -4px;
762
+ background: none;
763
+ border: none;
764
+ padding: 0;
765
+ transition: color 0.2s;
711
766
  }
712
767
 
713
- #code-viewer .close-btn:hover {
768
+ #content-pane .collapse-btn:hover {
714
769
  color: #c9d1d9;
715
770
  }
716
771
 
717
- #code-viewer pre {
772
+ #content-pane .pane-content {
773
+ padding: 20px;
774
+ }
775
+
776
+ #content-pane pre {
718
777
  margin: 0;
719
- padding: 12px;
778
+ padding: 16px;
720
779
  background: #0d1117;
721
780
  border: 1px solid #30363d;
722
781
  border-radius: 6px;
723
782
  overflow-x: auto;
724
- font-size: 11px;
725
- line-height: 1.5;
783
+ font-size: 12px;
784
+ line-height: 1.6;
726
785
  }
727
786
 
728
- #code-viewer code {
787
+ #content-pane code {
729
788
  color: #c9d1d9;
730
789
  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
731
790
  }
732
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
+
733
846
  .node.highlighted circle,
734
847
  .node.highlighted rect {
735
848
  stroke: #f0e68c;
@@ -772,6 +885,9 @@ def _create_visualization_html(html_file: Path) -> None:
772
885
  <div class="legend-item">
773
886
  <span class="legend-color" style="background: #6e7681;"></span> Code
774
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>
775
891
  <div class="legend-item">
776
892
  <span class="legend-color" style="background: #8b949e; border-radius: 2px;"></span> Docstring ▢
777
893
  </div>
@@ -791,13 +907,13 @@ def _create_visualization_html(html_file: Path) -> None:
791
907
  <svg id="graph"></svg>
792
908
  <div id="tooltip" class="tooltip"></div>
793
909
 
794
- <div id="code-viewer">
795
- <div class="header">
796
- <span class="close-btn" onclick="closeCodeViewer()">×</span>
797
- <div class="title" id="viewer-title"></div>
798
- <div class="meta" id="viewer-meta"></div>
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>
799
915
  </div>
800
- <pre><code id="viewer-code"></code></pre>
916
+ <div class="pane-content" id="pane-content"></div>
801
917
  </div>
802
918
 
803
919
  <script>
@@ -953,9 +1069,15 @@ def _create_visualization_html(html_file: Path) -> None:
953
1069
  .style("pointer-events", "none")
954
1070
  .text(d => collapsedNodes.has(d.id) ? "+" : "−");
955
1071
 
956
- // Add labels
1072
+ // Add labels (show "Import:" prefix for L1 nodes)
957
1073
  node.append("text")
958
- .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
+ })
959
1081
  .attr("dy", 30);
960
1082
 
961
1083
  simulation.on("tick", () => {
@@ -978,7 +1100,10 @@ def _create_visualization_html(html_file: Path) -> None:
978
1100
  function handleNodeClick(event, d) {
979
1101
  event.stopPropagation();
980
1102
 
981
- // If node has children, toggle expansion
1103
+ // Always show content pane when clicking any node
1104
+ showContentPane(d);
1105
+
1106
+ // If node has children, also toggle expansion
982
1107
  if (hasChildren(d)) {
983
1108
  if (collapsedNodes.has(d.id)) {
984
1109
  expandNode(d);
@@ -986,9 +1111,6 @@ def _create_visualization_html(html_file: Path) -> None:
986
1111
  collapseNode(d);
987
1112
  }
988
1113
  renderGraph();
989
- } else {
990
- // Leaf node - show code viewer
991
- showCodeViewer(d);
992
1114
  }
993
1115
  }
994
1116
 
@@ -1097,19 +1219,25 @@ def _create_visualization_html(html_file: Path) -> None:
1097
1219
  }
1098
1220
  }
1099
1221
 
1100
- function showCodeViewer(node) {
1222
+ function showContentPane(node) {
1101
1223
  // Highlight the node
1102
1224
  highlightedNode = node;
1103
1225
  renderGraph();
1104
1226
 
1105
- // Populate code viewer
1106
- const viewer = document.getElementById('code-viewer');
1107
- const title = document.getElementById('viewer-title');
1108
- const meta = document.getElementById('viewer-meta');
1109
- const code = document.getElementById('viewer-code');
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');
1110
1232
 
1111
- title.textContent = node.name;
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
+ }
1112
1239
 
1240
+ // Set metadata
1113
1241
  let metaText = `${node.type} • ${node.file_path}`;
1114
1242
  if (node.start_line) {
1115
1243
  metaText += ` • Lines ${node.start_line}-${node.end_line}`;
@@ -1119,21 +1247,179 @@ def _create_visualization_html(html_file: Path) -> None:
1119
1247
  }
1120
1248
  meta.textContent = metaText;
1121
1249
 
1122
- // Show content if available
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
+
1123
1405
  if (node.content) {
1124
- code.textContent = node.content;
1125
- } else if (node.docstring) {
1126
- code.textContent = `// Docstring:\n${node.docstring}`;
1406
+ html += `<pre><code>${escapeHtml(node.content)}</code></pre>`;
1127
1407
  } else {
1128
- code.textContent = '// No content available';
1408
+ html += '<p style="color: #8b949e;">No content available</p>';
1129
1409
  }
1130
1410
 
1131
- viewer.classList.add('visible');
1411
+ container.innerHTML = html;
1412
+ }
1413
+
1414
+ function escapeHtml(text) {
1415
+ const div = document.createElement('div');
1416
+ div.textContent = text;
1417
+ return div.innerHTML;
1132
1418
  }
1133
1419
 
1134
- function closeCodeViewer() {
1135
- const viewer = document.getElementById('code-viewer');
1136
- viewer.classList.remove('visible');
1420
+ function closeContentPane() {
1421
+ const pane = document.getElementById('content-pane');
1422
+ pane.classList.remove('visible');
1137
1423
 
1138
1424
  // Remove highlight
1139
1425
  highlightedNode = null;
@@ -1165,7 +1451,7 @@ def _create_visualization_html(html_file: Path) -> None:
1165
1451
  });
1166
1452
  </script>
1167
1453
  </body>
1168
- </html>'''
1454
+ </html>"""
1169
1455
 
1170
1456
  with open(html_file, "w") as f:
1171
1457
  f.write(html_content)