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.
- mcp_vector_search/__init__.py +2 -2
- mcp_vector_search/cli/commands/index.py +15 -24
- mcp_vector_search/cli/commands/install.py +502 -523
- mcp_vector_search/cli/commands/install_old.py +696 -0
- mcp_vector_search/cli/commands/status.py +7 -5
- mcp_vector_search/cli/commands/uninstall.py +485 -0
- mcp_vector_search/cli/commands/visualize.py +406 -120
- mcp_vector_search/cli/didyoumean.py +10 -0
- mcp_vector_search/cli/main.py +39 -21
- mcp_vector_search/core/connection_pool.py +49 -11
- mcp_vector_search/core/database.py +7 -9
- mcp_vector_search/core/directory_index.py +26 -11
- mcp_vector_search/core/indexer.py +89 -29
- mcp_vector_search/core/models.py +4 -1
- mcp_vector_search/core/project.py +16 -5
- mcp_vector_search/parsers/base.py +54 -18
- mcp_vector_search/parsers/javascript.py +41 -20
- mcp_vector_search/parsers/python.py +19 -11
- mcp_vector_search/parsers/registry.py +3 -2
- mcp_vector_search/utils/gitignore.py +3 -1
- {mcp_vector_search-0.12.0.dist-info → mcp_vector_search-0.12.1.dist-info}/METADATA +87 -24
- {mcp_vector_search-0.12.0.dist-info → mcp_vector_search-0.12.1.dist-info}/RECORD +25 -23
- {mcp_vector_search-0.12.0.dist-info → mcp_vector_search-0.12.1.dist-info}/WHEEL +0 -0
- {mcp_vector_search-0.12.0.dist-info → mcp_vector_search-0.12.1.dist-info}/entry_points.txt +0 -0
- {mcp_vector_search-0.12.0.dist-info → mcp_vector_search-0.12.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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) &
|
|
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) &
|
|
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(
|
|
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 =
|
|
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
|
|
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) &
|
|
228
|
-
child_dir_id = f"dir_{hash(dir_path_str) &
|
|
229
|
-
links.append(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
275
|
+
for _file_path_str, file_node in file_nodes.items():
|
|
249
276
|
if file_node.get("parent_dir_id"):
|
|
250
|
-
links.append(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
280
|
-
|
|
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(
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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(
|
|
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]
|
|
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
|
-
|
|
525
|
+
handler = http.server.SimpleHTTPRequestHandler
|
|
480
526
|
try:
|
|
481
|
-
with socketserver.TCPServer(("", port),
|
|
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 =
|
|
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
|
-
#
|
|
668
|
-
position:
|
|
669
|
-
top:
|
|
670
|
-
right:
|
|
671
|
-
width:
|
|
672
|
-
|
|
673
|
-
background: rgba(13, 17, 23, 0.
|
|
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
|
|
679
|
-
|
|
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
|
-
#
|
|
683
|
-
|
|
728
|
+
#content-pane.visible {
|
|
729
|
+
transform: translateX(0);
|
|
684
730
|
}
|
|
685
731
|
|
|
686
|
-
#
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
#
|
|
693
|
-
font-size:
|
|
741
|
+
#content-pane .pane-title {
|
|
742
|
+
font-size: 16px;
|
|
694
743
|
font-weight: bold;
|
|
695
744
|
color: #58a6ff;
|
|
696
|
-
margin-bottom:
|
|
745
|
+
margin-bottom: 8px;
|
|
746
|
+
padding-right: 30px;
|
|
697
747
|
}
|
|
698
748
|
|
|
699
|
-
#
|
|
700
|
-
font-size:
|
|
749
|
+
#content-pane .pane-meta {
|
|
750
|
+
font-size: 12px;
|
|
701
751
|
color: #8b949e;
|
|
702
752
|
}
|
|
703
753
|
|
|
704
|
-
#
|
|
705
|
-
|
|
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:
|
|
760
|
+
font-size: 24px;
|
|
709
761
|
line-height: 1;
|
|
710
|
-
|
|
762
|
+
background: none;
|
|
763
|
+
border: none;
|
|
764
|
+
padding: 0;
|
|
765
|
+
transition: color 0.2s;
|
|
711
766
|
}
|
|
712
767
|
|
|
713
|
-
#
|
|
768
|
+
#content-pane .collapse-btn:hover {
|
|
714
769
|
color: #c9d1d9;
|
|
715
770
|
}
|
|
716
771
|
|
|
717
|
-
#
|
|
772
|
+
#content-pane .pane-content {
|
|
773
|
+
padding: 20px;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
#content-pane pre {
|
|
718
777
|
margin: 0;
|
|
719
|
-
padding:
|
|
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:
|
|
725
|
-
line-height: 1.
|
|
783
|
+
font-size: 12px;
|
|
784
|
+
line-height: 1.6;
|
|
726
785
|
}
|
|
727
786
|
|
|
728
|
-
#
|
|
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="
|
|
795
|
-
<div class="header">
|
|
796
|
-
<
|
|
797
|
-
<div class="title" id="
|
|
798
|
-
<div class="meta" id="
|
|
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
|
-
<
|
|
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 =>
|
|
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
|
-
//
|
|
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
|
|
1222
|
+
function showContentPane(node) {
|
|
1101
1223
|
// Highlight the node
|
|
1102
1224
|
highlightedNode = node;
|
|
1103
1225
|
renderGraph();
|
|
1104
1226
|
|
|
1105
|
-
// Populate
|
|
1106
|
-
const
|
|
1107
|
-
const title = document.getElementById('
|
|
1108
|
-
const meta = document.getElementById('
|
|
1109
|
-
const
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
1408
|
+
html += '<p style="color: #8b949e;">No content available</p>';
|
|
1129
1409
|
}
|
|
1130
1410
|
|
|
1131
|
-
|
|
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
|
|
1135
|
-
const
|
|
1136
|
-
|
|
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)
|