@kopikocappu/mycelium 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1347 -25
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7157,9 +7157,9 @@ var require_nodefs_handler = __commonJS({
|
|
|
7157
7157
|
if (this.fsw.closed) {
|
|
7158
7158
|
return;
|
|
7159
7159
|
}
|
|
7160
|
-
const
|
|
7160
|
+
const dirname3 = sysPath.dirname(file);
|
|
7161
7161
|
const basename2 = sysPath.basename(file);
|
|
7162
|
-
const parent = this.fsw._getWatchedDir(
|
|
7162
|
+
const parent = this.fsw._getWatchedDir(dirname3);
|
|
7163
7163
|
let prevStats = stats;
|
|
7164
7164
|
if (parent.has(basename2)) return;
|
|
7165
7165
|
const listener = async (path9, newStats) => {
|
|
@@ -7181,7 +7181,7 @@ var require_nodefs_handler = __commonJS({
|
|
|
7181
7181
|
prevStats = newStats2;
|
|
7182
7182
|
}
|
|
7183
7183
|
} catch (error) {
|
|
7184
|
-
this.fsw._remove(
|
|
7184
|
+
this.fsw._remove(dirname3, basename2);
|
|
7185
7185
|
}
|
|
7186
7186
|
} else if (parent.has(basename2)) {
|
|
7187
7187
|
const at = newStats.atimeMs;
|
|
@@ -15727,9 +15727,10 @@ var CbmAdapter = class {
|
|
|
15727
15727
|
}
|
|
15728
15728
|
/** Run cbm index on a project */
|
|
15729
15729
|
async index(repoPath) {
|
|
15730
|
+
const fwdPath = import_path2.default.resolve(repoPath).replace(/\\/g, "/");
|
|
15730
15731
|
const result = (0, import_child_process.spawnSync)(
|
|
15731
15732
|
this.cbmBin,
|
|
15732
|
-
["cli", "index_repository", JSON.stringify({ repo_path:
|
|
15733
|
+
["cli", "index_repository", JSON.stringify({ repo_path: fwdPath })],
|
|
15733
15734
|
{ timeout: 3e5, stdio: "pipe", encoding: "utf-8" }
|
|
15734
15735
|
);
|
|
15735
15736
|
if (result.status !== 0) {
|
|
@@ -15739,18 +15740,19 @@ var CbmAdapter = class {
|
|
|
15739
15740
|
/** Pull the full graph from cbm and convert to Mycelium format */
|
|
15740
15741
|
async getGraph(repoPath) {
|
|
15741
15742
|
const absPath = import_path2.default.resolve(repoPath);
|
|
15743
|
+
const slug = this.slugifyPath(absPath);
|
|
15742
15744
|
const fnResult = this.runCbmCli("search_graph", {
|
|
15743
15745
|
label: "Function",
|
|
15744
|
-
project:
|
|
15746
|
+
project: slug,
|
|
15745
15747
|
limit: 1e4
|
|
15746
15748
|
});
|
|
15747
15749
|
const fileResult = this.runCbmCli("search_graph", {
|
|
15748
15750
|
label: "File",
|
|
15749
|
-
project:
|
|
15751
|
+
project: slug,
|
|
15750
15752
|
limit: 1e4
|
|
15751
15753
|
});
|
|
15752
15754
|
const archResult = this.runCbmCli("get_architecture", {
|
|
15753
|
-
repo_path: absPath
|
|
15755
|
+
repo_path: absPath.replace(/\\/g, "/")
|
|
15754
15756
|
});
|
|
15755
15757
|
const fnNodes = fnResult?.results ?? [];
|
|
15756
15758
|
const fileNodes = fileResult?.results ?? [];
|
|
@@ -15815,15 +15817,29 @@ var CbmAdapter = class {
|
|
|
15815
15817
|
}));
|
|
15816
15818
|
}
|
|
15817
15819
|
// ─── Private helpers ────────────────────────────────────────────────────────
|
|
15820
|
+
/**
|
|
15821
|
+
* cbm stores projects as slugified absolute paths.
|
|
15822
|
+
* e.g. C:\Users\Minh Tran\Desktop\pakky → C-Users-Minh-Tran-Desktop-pakky
|
|
15823
|
+
* This is what the 'project' field expects in search_graph, trace_path, etc.
|
|
15824
|
+
*/
|
|
15825
|
+
slugifyPath(absPath) {
|
|
15826
|
+
return absPath.replace(/\\/g, "-").replace(/\//g, "-").replace(/:/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
15827
|
+
}
|
|
15818
15828
|
runCbmCli(tool, args) {
|
|
15819
15829
|
try {
|
|
15830
|
+
const normalizedArgs = {};
|
|
15831
|
+
for (const [k, v] of Object.entries(args)) {
|
|
15832
|
+
normalizedArgs[k] = typeof v === "string" ? v.replace(/\\/g, "/") : v;
|
|
15833
|
+
}
|
|
15820
15834
|
const result = (0, import_child_process.spawnSync)(
|
|
15821
15835
|
this.cbmBin,
|
|
15822
|
-
["cli", tool, JSON.stringify(
|
|
15836
|
+
["cli", tool, JSON.stringify(normalizedArgs)],
|
|
15823
15837
|
{ timeout: 3e4, stdio: "pipe", encoding: "utf-8" }
|
|
15824
15838
|
);
|
|
15825
|
-
if (
|
|
15826
|
-
|
|
15839
|
+
if (!result.stdout?.trim()) return null;
|
|
15840
|
+
const parsed = JSON.parse(result.stdout.trim());
|
|
15841
|
+
if (parsed?.error) return null;
|
|
15842
|
+
return parsed;
|
|
15827
15843
|
} catch {
|
|
15828
15844
|
return null;
|
|
15829
15845
|
}
|
|
@@ -15869,7 +15885,7 @@ var CbmAdapter = class {
|
|
|
15869
15885
|
for (const fn of sample) {
|
|
15870
15886
|
const result = this.runCbmCli("trace_path", {
|
|
15871
15887
|
function_name: fn.qualified_name ?? fn.name,
|
|
15872
|
-
project: import_path2.default.resolve(repoPath),
|
|
15888
|
+
project: this.slugifyPath(import_path2.default.resolve(repoPath)),
|
|
15873
15889
|
direction: "outbound",
|
|
15874
15890
|
depth: 1
|
|
15875
15891
|
});
|
|
@@ -15896,6 +15912,1295 @@ var CbmAdapter = class {
|
|
|
15896
15912
|
var cbmAdapter = new CbmAdapter();
|
|
15897
15913
|
|
|
15898
15914
|
// src/mcp/server.ts
|
|
15915
|
+
var EMBEDDED_VIEWER = `<!DOCTYPE html>
|
|
15916
|
+
<html lang="en">
|
|
15917
|
+
<head>
|
|
15918
|
+
<meta charset="UTF-8">
|
|
15919
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
15920
|
+
<title>Mycelium Graph</title>
|
|
15921
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
|
15922
|
+
<style>
|
|
15923
|
+
:root {
|
|
15924
|
+
--bg: #0e0e12;
|
|
15925
|
+
--surface: #16161e;
|
|
15926
|
+
--surface2: #1e1e2a;
|
|
15927
|
+
--border: #2a2a3a;
|
|
15928
|
+
--text: #e0e0f0;
|
|
15929
|
+
--text-dim: #7878a0;
|
|
15930
|
+
--accent: #7c6af7;
|
|
15931
|
+
--accent2: #f7916a;
|
|
15932
|
+
--accent3: #6af7c4;
|
|
15933
|
+
--import-edge: #4a8fff;
|
|
15934
|
+
--call-edge: #ff8c42;
|
|
15935
|
+
--file-node: #7c6af7;
|
|
15936
|
+
--fn-node: #6af7c4;
|
|
15937
|
+
--class-node: #f7916a;
|
|
15938
|
+
--cluster-hull: rgba(124, 106, 247, 0.07);
|
|
15939
|
+
--sidebar-w: 340px;
|
|
15940
|
+
}
|
|
15941
|
+
|
|
15942
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
15943
|
+
|
|
15944
|
+
body {
|
|
15945
|
+
background: var(--bg);
|
|
15946
|
+
color: var(--text);
|
|
15947
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
15948
|
+
overflow: hidden;
|
|
15949
|
+
height: 100vh;
|
|
15950
|
+
display: flex;
|
|
15951
|
+
}
|
|
15952
|
+
|
|
15953
|
+
/* \u2500\u2500 Sidebar \u2500\u2500 */
|
|
15954
|
+
#sidebar {
|
|
15955
|
+
width: var(--sidebar-w);
|
|
15956
|
+
min-width: var(--sidebar-w);
|
|
15957
|
+
background: var(--surface);
|
|
15958
|
+
border-right: 1px solid var(--border);
|
|
15959
|
+
display: flex;
|
|
15960
|
+
flex-direction: column;
|
|
15961
|
+
z-index: 10;
|
|
15962
|
+
overflow: hidden;
|
|
15963
|
+
}
|
|
15964
|
+
|
|
15965
|
+
#sidebar-header {
|
|
15966
|
+
padding: 16px;
|
|
15967
|
+
border-bottom: 1px solid var(--border);
|
|
15968
|
+
display: flex;
|
|
15969
|
+
align-items: center;
|
|
15970
|
+
gap: 10px;
|
|
15971
|
+
}
|
|
15972
|
+
|
|
15973
|
+
.logo {
|
|
15974
|
+
font-size: 18px;
|
|
15975
|
+
font-weight: 700;
|
|
15976
|
+
color: var(--accent);
|
|
15977
|
+
letter-spacing: -0.5px;
|
|
15978
|
+
}
|
|
15979
|
+
|
|
15980
|
+
.logo span { color: var(--text-dim); font-weight: 400; }
|
|
15981
|
+
|
|
15982
|
+
#search-wrap {
|
|
15983
|
+
padding: 12px 16px;
|
|
15984
|
+
border-bottom: 1px solid var(--border);
|
|
15985
|
+
position: relative;
|
|
15986
|
+
}
|
|
15987
|
+
|
|
15988
|
+
#search {
|
|
15989
|
+
width: 100%;
|
|
15990
|
+
background: var(--surface2);
|
|
15991
|
+
border: 1px solid var(--border);
|
|
15992
|
+
border-radius: 6px;
|
|
15993
|
+
color: var(--text);
|
|
15994
|
+
font-family: inherit;
|
|
15995
|
+
font-size: 12px;
|
|
15996
|
+
padding: 7px 10px 7px 30px;
|
|
15997
|
+
outline: none;
|
|
15998
|
+
transition: border-color 0.15s;
|
|
15999
|
+
}
|
|
16000
|
+
|
|
16001
|
+
#search:focus { border-color: var(--accent); }
|
|
16002
|
+
|
|
16003
|
+
.search-icon {
|
|
16004
|
+
position: absolute;
|
|
16005
|
+
left: 26px;
|
|
16006
|
+
top: 50%;
|
|
16007
|
+
transform: translateY(-50%);
|
|
16008
|
+
color: var(--text-dim);
|
|
16009
|
+
font-size: 12px;
|
|
16010
|
+
pointer-events: none;
|
|
16011
|
+
}
|
|
16012
|
+
|
|
16013
|
+
/* \u2500\u2500 Controls \u2500\u2500 */
|
|
16014
|
+
#controls {
|
|
16015
|
+
padding: 10px 16px;
|
|
16016
|
+
border-bottom: 1px solid var(--border);
|
|
16017
|
+
display: flex;
|
|
16018
|
+
flex-direction: column;
|
|
16019
|
+
gap: 8px;
|
|
16020
|
+
}
|
|
16021
|
+
|
|
16022
|
+
.control-row {
|
|
16023
|
+
display: flex;
|
|
16024
|
+
align-items: center;
|
|
16025
|
+
gap: 8px;
|
|
16026
|
+
font-size: 11px;
|
|
16027
|
+
color: var(--text-dim);
|
|
16028
|
+
}
|
|
16029
|
+
|
|
16030
|
+
.toggle-btn {
|
|
16031
|
+
background: var(--surface2);
|
|
16032
|
+
border: 1px solid var(--border);
|
|
16033
|
+
border-radius: 4px;
|
|
16034
|
+
color: var(--text-dim);
|
|
16035
|
+
font-family: inherit;
|
|
16036
|
+
font-size: 11px;
|
|
16037
|
+
padding: 3px 8px;
|
|
16038
|
+
cursor: pointer;
|
|
16039
|
+
transition: all 0.15s;
|
|
16040
|
+
}
|
|
16041
|
+
|
|
16042
|
+
.toggle-btn.active {
|
|
16043
|
+
background: var(--accent);
|
|
16044
|
+
border-color: var(--accent);
|
|
16045
|
+
color: white;
|
|
16046
|
+
}
|
|
16047
|
+
|
|
16048
|
+
.zoom-level-btns { display: flex; gap: 4px; }
|
|
16049
|
+
|
|
16050
|
+
/* \u2500\u2500 Node info panel \u2500\u2500 */
|
|
16051
|
+
#node-info {
|
|
16052
|
+
flex: 1;
|
|
16053
|
+
overflow-y: auto;
|
|
16054
|
+
padding: 16px;
|
|
16055
|
+
}
|
|
16056
|
+
|
|
16057
|
+
.node-info-empty {
|
|
16058
|
+
color: var(--text-dim);
|
|
16059
|
+
font-size: 12px;
|
|
16060
|
+
text-align: center;
|
|
16061
|
+
margin-top: 40px;
|
|
16062
|
+
}
|
|
16063
|
+
|
|
16064
|
+
.node-detail { animation: fadeIn 0.2s ease; }
|
|
16065
|
+
|
|
16066
|
+
@keyframes fadeIn {
|
|
16067
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
16068
|
+
to { opacity: 1; transform: translateY(0); }
|
|
16069
|
+
}
|
|
16070
|
+
|
|
16071
|
+
.node-kind-badge {
|
|
16072
|
+
display: inline-block;
|
|
16073
|
+
font-size: 10px;
|
|
16074
|
+
padding: 2px 6px;
|
|
16075
|
+
border-radius: 3px;
|
|
16076
|
+
margin-bottom: 8px;
|
|
16077
|
+
text-transform: uppercase;
|
|
16078
|
+
letter-spacing: 0.5px;
|
|
16079
|
+
}
|
|
16080
|
+
|
|
16081
|
+
.kind-file { background: rgba(124,106,247,0.2); color: var(--accent); }
|
|
16082
|
+
.kind-function { background: rgba(106,247,196,0.2); color: var(--fn-node); }
|
|
16083
|
+
.kind-class { background: rgba(247,145,106,0.2); color: var(--class-node); }
|
|
16084
|
+
|
|
16085
|
+
.node-name {
|
|
16086
|
+
font-size: 15px;
|
|
16087
|
+
font-weight: 600;
|
|
16088
|
+
color: var(--text);
|
|
16089
|
+
margin-bottom: 6px;
|
|
16090
|
+
word-break: break-all;
|
|
16091
|
+
}
|
|
16092
|
+
|
|
16093
|
+
.node-description {
|
|
16094
|
+
font-size: 12px;
|
|
16095
|
+
color: var(--text-dim);
|
|
16096
|
+
line-height: 1.6;
|
|
16097
|
+
margin-bottom: 12px;
|
|
16098
|
+
}
|
|
16099
|
+
|
|
16100
|
+
.tags-wrap { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 14px; }
|
|
16101
|
+
|
|
16102
|
+
.tag {
|
|
16103
|
+
background: var(--surface2);
|
|
16104
|
+
border: 1px solid var(--border);
|
|
16105
|
+
border-radius: 3px;
|
|
16106
|
+
font-size: 10px;
|
|
16107
|
+
padding: 2px 6px;
|
|
16108
|
+
color: var(--text-dim);
|
|
16109
|
+
}
|
|
16110
|
+
|
|
16111
|
+
.section-title {
|
|
16112
|
+
font-size: 10px;
|
|
16113
|
+
text-transform: uppercase;
|
|
16114
|
+
letter-spacing: 0.8px;
|
|
16115
|
+
color: var(--text-dim);
|
|
16116
|
+
margin-bottom: 6px;
|
|
16117
|
+
margin-top: 14px;
|
|
16118
|
+
}
|
|
16119
|
+
|
|
16120
|
+
.dep-list { display: flex; flex-direction: column; gap: 3px; }
|
|
16121
|
+
|
|
16122
|
+
.dep-item {
|
|
16123
|
+
font-size: 11px;
|
|
16124
|
+
color: var(--text);
|
|
16125
|
+
padding: 4px 8px;
|
|
16126
|
+
background: var(--surface2);
|
|
16127
|
+
border-radius: 4px;
|
|
16128
|
+
border-left: 2px solid;
|
|
16129
|
+
cursor: pointer;
|
|
16130
|
+
transition: background 0.1s;
|
|
16131
|
+
word-break: break-all;
|
|
16132
|
+
}
|
|
16133
|
+
|
|
16134
|
+
.dep-item:hover { background: #252535; }
|
|
16135
|
+
.dep-import { border-left-color: var(--import-edge); }
|
|
16136
|
+
.dep-call { border-left-color: var(--call-edge); }
|
|
16137
|
+
.dep-calledby { border-left-color: var(--fn-node); }
|
|
16138
|
+
|
|
16139
|
+
.dep-label {
|
|
16140
|
+
font-size: 9px;
|
|
16141
|
+
color: var(--text-dim);
|
|
16142
|
+
display: block;
|
|
16143
|
+
margin-bottom: 1px;
|
|
16144
|
+
}
|
|
16145
|
+
|
|
16146
|
+
.stat-row {
|
|
16147
|
+
display: flex;
|
|
16148
|
+
justify-content: space-between;
|
|
16149
|
+
font-size: 11px;
|
|
16150
|
+
padding: 3px 0;
|
|
16151
|
+
border-bottom: 1px solid var(--border);
|
|
16152
|
+
}
|
|
16153
|
+
|
|
16154
|
+
.stat-val { color: var(--accent); }
|
|
16155
|
+
|
|
16156
|
+
/* \u2500\u2500 Legend \u2500\u2500 */
|
|
16157
|
+
#legend {
|
|
16158
|
+
padding: 12px 16px;
|
|
16159
|
+
border-top: 1px solid var(--border);
|
|
16160
|
+
display: flex;
|
|
16161
|
+
flex-wrap: wrap;
|
|
16162
|
+
gap: 10px;
|
|
16163
|
+
}
|
|
16164
|
+
|
|
16165
|
+
.legend-item {
|
|
16166
|
+
display: flex;
|
|
16167
|
+
align-items: center;
|
|
16168
|
+
gap: 5px;
|
|
16169
|
+
font-size: 10px;
|
|
16170
|
+
color: var(--text-dim);
|
|
16171
|
+
}
|
|
16172
|
+
|
|
16173
|
+
.legend-dot {
|
|
16174
|
+
width: 8px; height: 8px;
|
|
16175
|
+
border-radius: 50%;
|
|
16176
|
+
}
|
|
16177
|
+
|
|
16178
|
+
.legend-line {
|
|
16179
|
+
width: 16px; height: 2px;
|
|
16180
|
+
border-radius: 1px;
|
|
16181
|
+
}
|
|
16182
|
+
|
|
16183
|
+
/* \u2500\u2500 Main canvas area \u2500\u2500 */
|
|
16184
|
+
#canvas-wrap {
|
|
16185
|
+
flex: 1;
|
|
16186
|
+
position: relative;
|
|
16187
|
+
overflow: hidden;
|
|
16188
|
+
}
|
|
16189
|
+
|
|
16190
|
+
#graph-svg { width: 100%; height: 100%; }
|
|
16191
|
+
|
|
16192
|
+
/* \u2500\u2500 Top bar \u2500\u2500 */
|
|
16193
|
+
#topbar {
|
|
16194
|
+
position: absolute;
|
|
16195
|
+
top: 0; left: 0; right: 0;
|
|
16196
|
+
height: 44px;
|
|
16197
|
+
background: rgba(14,14,18,0.9);
|
|
16198
|
+
backdrop-filter: blur(8px);
|
|
16199
|
+
border-bottom: 1px solid var(--border);
|
|
16200
|
+
display: flex;
|
|
16201
|
+
align-items: center;
|
|
16202
|
+
padding: 0 16px;
|
|
16203
|
+
gap: 12px;
|
|
16204
|
+
z-index: 5;
|
|
16205
|
+
}
|
|
16206
|
+
|
|
16207
|
+
.stats-chip {
|
|
16208
|
+
font-size: 11px;
|
|
16209
|
+
color: var(--text-dim);
|
|
16210
|
+
background: var(--surface2);
|
|
16211
|
+
border: 1px solid var(--border);
|
|
16212
|
+
border-radius: 4px;
|
|
16213
|
+
padding: 3px 8px;
|
|
16214
|
+
}
|
|
16215
|
+
|
|
16216
|
+
.stats-chip strong { color: var(--text); }
|
|
16217
|
+
|
|
16218
|
+
#task-input-wrap {
|
|
16219
|
+
flex: 1;
|
|
16220
|
+
max-width: 400px;
|
|
16221
|
+
position: relative;
|
|
16222
|
+
}
|
|
16223
|
+
|
|
16224
|
+
#task-input {
|
|
16225
|
+
width: 100%;
|
|
16226
|
+
background: var(--surface2);
|
|
16227
|
+
border: 1px solid var(--border);
|
|
16228
|
+
border-radius: 6px;
|
|
16229
|
+
color: var(--text);
|
|
16230
|
+
font-family: inherit;
|
|
16231
|
+
font-size: 12px;
|
|
16232
|
+
padding: 6px 36px 6px 10px;
|
|
16233
|
+
outline: none;
|
|
16234
|
+
transition: border-color 0.15s;
|
|
16235
|
+
}
|
|
16236
|
+
|
|
16237
|
+
#task-input:focus { border-color: var(--accent); }
|
|
16238
|
+
#task-input::placeholder { color: var(--text-dim); }
|
|
16239
|
+
|
|
16240
|
+
#task-submit {
|
|
16241
|
+
position: absolute;
|
|
16242
|
+
right: 6px;
|
|
16243
|
+
top: 50%;
|
|
16244
|
+
transform: translateY(-50%);
|
|
16245
|
+
background: var(--accent);
|
|
16246
|
+
border: none;
|
|
16247
|
+
border-radius: 4px;
|
|
16248
|
+
color: white;
|
|
16249
|
+
font-family: inherit;
|
|
16250
|
+
font-size: 10px;
|
|
16251
|
+
padding: 3px 7px;
|
|
16252
|
+
cursor: pointer;
|
|
16253
|
+
}
|
|
16254
|
+
|
|
16255
|
+
/* \u2500\u2500 Minimap \u2500\u2500 */
|
|
16256
|
+
#minimap {
|
|
16257
|
+
position: absolute;
|
|
16258
|
+
bottom: 16px;
|
|
16259
|
+
right: 16px;
|
|
16260
|
+
width: 160px;
|
|
16261
|
+
height: 100px;
|
|
16262
|
+
background: var(--surface);
|
|
16263
|
+
border: 1px solid var(--border);
|
|
16264
|
+
border-radius: 6px;
|
|
16265
|
+
overflow: hidden;
|
|
16266
|
+
z-index: 5;
|
|
16267
|
+
}
|
|
16268
|
+
|
|
16269
|
+
#minimap-svg { width: 100%; height: 100%; }
|
|
16270
|
+
#minimap-viewport {
|
|
16271
|
+
fill: rgba(124,106,247,0.15);
|
|
16272
|
+
stroke: var(--accent);
|
|
16273
|
+
stroke-width: 1;
|
|
16274
|
+
}
|
|
16275
|
+
|
|
16276
|
+
/* \u2500\u2500 Zoom buttons \u2500\u2500 */
|
|
16277
|
+
#zoom-btns {
|
|
16278
|
+
position: absolute;
|
|
16279
|
+
bottom: 130px;
|
|
16280
|
+
right: 16px;
|
|
16281
|
+
display: flex;
|
|
16282
|
+
flex-direction: column;
|
|
16283
|
+
gap: 4px;
|
|
16284
|
+
z-index: 5;
|
|
16285
|
+
}
|
|
16286
|
+
|
|
16287
|
+
.zoom-btn {
|
|
16288
|
+
background: var(--surface);
|
|
16289
|
+
border: 1px solid var(--border);
|
|
16290
|
+
border-radius: 4px;
|
|
16291
|
+
color: var(--text);
|
|
16292
|
+
width: 32px; height: 32px;
|
|
16293
|
+
display: flex; align-items: center; justify-content: center;
|
|
16294
|
+
cursor: pointer;
|
|
16295
|
+
font-size: 16px;
|
|
16296
|
+
transition: background 0.1s;
|
|
16297
|
+
}
|
|
16298
|
+
|
|
16299
|
+
.zoom-btn:hover { background: var(--surface2); }
|
|
16300
|
+
|
|
16301
|
+
/* \u2500\u2500 Loading \u2500\u2500 */
|
|
16302
|
+
#loading {
|
|
16303
|
+
position: absolute;
|
|
16304
|
+
inset: 0;
|
|
16305
|
+
background: var(--bg);
|
|
16306
|
+
display: flex;
|
|
16307
|
+
flex-direction: column;
|
|
16308
|
+
align-items: center;
|
|
16309
|
+
justify-content: center;
|
|
16310
|
+
gap: 16px;
|
|
16311
|
+
z-index: 100;
|
|
16312
|
+
}
|
|
16313
|
+
|
|
16314
|
+
.spinner {
|
|
16315
|
+
width: 40px; height: 40px;
|
|
16316
|
+
border: 3px solid var(--border);
|
|
16317
|
+
border-top-color: var(--accent);
|
|
16318
|
+
border-radius: 50%;
|
|
16319
|
+
animation: spin 0.7s linear infinite;
|
|
16320
|
+
}
|
|
16321
|
+
|
|
16322
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
16323
|
+
|
|
16324
|
+
#loading-text { font-size: 13px; color: var(--text-dim); }
|
|
16325
|
+
|
|
16326
|
+
/* \u2500\u2500 Tooltip \u2500\u2500 */
|
|
16327
|
+
#tooltip {
|
|
16328
|
+
position: absolute;
|
|
16329
|
+
background: var(--surface);
|
|
16330
|
+
border: 1px solid var(--border);
|
|
16331
|
+
border-radius: 8px;
|
|
16332
|
+
padding: 12px 14px;
|
|
16333
|
+
font-size: 12px;
|
|
16334
|
+
pointer-events: none;
|
|
16335
|
+
z-index: 20;
|
|
16336
|
+
max-width: 320px;
|
|
16337
|
+
display: none;
|
|
16338
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
|
|
16339
|
+
}
|
|
16340
|
+
|
|
16341
|
+
#tooltip .tip-name { font-weight: 700; font-size: 13px; margin-bottom: 4px; color: var(--text); }
|
|
16342
|
+
#tooltip .tip-desc { color: var(--text-dim); font-size: 11px; line-height: 1.6; margin-top: 4px; }
|
|
16343
|
+
#tooltip .tip-tags { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 3px; }
|
|
16344
|
+
#tooltip .tip-tag { background: rgba(124,106,247,0.15); color: var(--accent); font-size: 10px; padding: 2px 6px; border-radius: 3px; }
|
|
16345
|
+
|
|
16346
|
+
/* \u2500\u2500 SVG styles \u2500\u2500 */
|
|
16347
|
+
.node circle { cursor: pointer; }
|
|
16348
|
+
.node text {
|
|
16349
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
16350
|
+
font-size: 10px;
|
|
16351
|
+
fill: var(--text-dim);
|
|
16352
|
+
pointer-events: none;
|
|
16353
|
+
user-select: none;
|
|
16354
|
+
}
|
|
16355
|
+
|
|
16356
|
+
.node.selected circle { filter: drop-shadow(0 0 6px var(--accent)); }
|
|
16357
|
+
.node.highlighted circle { opacity: 1 !important; }
|
|
16358
|
+
.node.dimmed circle { opacity: 0.15 !important; }
|
|
16359
|
+
.node.dimmed text { opacity: 0.15; }
|
|
16360
|
+
|
|
16361
|
+
.link {
|
|
16362
|
+
fill: none;
|
|
16363
|
+
stroke: rgba(255,255,255,0.4);
|
|
16364
|
+
stroke-opacity: 0.7;
|
|
16365
|
+
}
|
|
16366
|
+
|
|
16367
|
+
.link.imports-edge { stroke: rgba(255,255,255,0.35); stroke-width: 1; }
|
|
16368
|
+
.link.calls-edge { stroke: rgba(255,255,255,0.18); stroke-width: 1; stroke-dasharray: 4,4; }
|
|
16369
|
+
.link.contains-edge { stroke: #333344; stroke-width: 0.5; stroke-dasharray: 3,3; }
|
|
16370
|
+
|
|
16371
|
+
.link.highlighted { stroke-opacity: 1 !important; stroke-width: 3 !important; }
|
|
16372
|
+
.link.dimmed { stroke-opacity: 0.04 !important; }
|
|
16373
|
+
|
|
16374
|
+
.hull {
|
|
16375
|
+
fill: none;
|
|
16376
|
+
stroke-width: 1.5;
|
|
16377
|
+
stroke-dasharray: 6,4;
|
|
16378
|
+
opacity: 0.5;
|
|
16379
|
+
}
|
|
16380
|
+
|
|
16381
|
+
.cluster-label {
|
|
16382
|
+
font-family: 'SF Mono', monospace;
|
|
16383
|
+
font-size: 11px;
|
|
16384
|
+
fill: rgba(124,106,247,0.5);
|
|
16385
|
+
font-weight: 600;
|
|
16386
|
+
pointer-events: none;
|
|
16387
|
+
}
|
|
16388
|
+
</style>
|
|
16389
|
+
</head>
|
|
16390
|
+
<body>
|
|
16391
|
+
|
|
16392
|
+
<!-- Sidebar -->
|
|
16393
|
+
<div id="sidebar">
|
|
16394
|
+
<div id="sidebar-header">
|
|
16395
|
+
<div class="logo">mycelium<span>.dev</span></div>
|
|
16396
|
+
</div>
|
|
16397
|
+
<div id="search-wrap">
|
|
16398
|
+
<span class="search-icon">\u2315</span>
|
|
16399
|
+
<input id="search" type="text" placeholder="Search files, functions, tags\u2026" autocomplete="off">
|
|
16400
|
+
</div>
|
|
16401
|
+
<div id="controls">
|
|
16402
|
+
<div class="control-row">
|
|
16403
|
+
<span>Edges:</span>
|
|
16404
|
+
<button class="toggle-btn active" id="toggle-imports">Imports</button>
|
|
16405
|
+
<button class="toggle-btn active" id="toggle-calls">Calls</button>
|
|
16406
|
+
<button class="toggle-btn" id="toggle-fns">Functions</button>
|
|
16407
|
+
</div>
|
|
16408
|
+
<div class="control-row">
|
|
16409
|
+
<span>Zoom level:</span>
|
|
16410
|
+
<div class="zoom-level-btns">
|
|
16411
|
+
<button class="toggle-btn active" id="zoom-files">Files</button>
|
|
16412
|
+
<button class="toggle-btn" id="zoom-symbols">Symbols</button>
|
|
16413
|
+
</div>
|
|
16414
|
+
</div>
|
|
16415
|
+
<div class="control-row">
|
|
16416
|
+
<button class="toggle-btn" id="toggle-clusters">Directory clusters</button>
|
|
16417
|
+
</div>
|
|
16418
|
+
</div>
|
|
16419
|
+
<div id="node-info">
|
|
16420
|
+
<div class="node-info-empty" style="line-height:1.8">Click any dot to see<br>what it imports, exports,<br>and connects to</div>
|
|
16421
|
+
</div>
|
|
16422
|
+
<div id="legend">
|
|
16423
|
+
<div id="dir-legend"></div>
|
|
16424
|
+
<div class="legend-item">
|
|
16425
|
+
<div class="legend-line" style="background:rgba(255,255,255,0.35)"></div>Import
|
|
16426
|
+
</div>
|
|
16427
|
+
<div class="legend-item">
|
|
16428
|
+
<div class="legend-line" style="background:rgba(255,255,255,0.25)"></div>Call
|
|
16429
|
+
</div>
|
|
16430
|
+
</div>
|
|
16431
|
+
</div>
|
|
16432
|
+
|
|
16433
|
+
<!-- Canvas -->
|
|
16434
|
+
<div id="canvas-wrap">
|
|
16435
|
+
<div id="topbar">
|
|
16436
|
+
<span class="stats-chip"><strong id="stat-nodes">\u2013</strong> files</span>
|
|
16437
|
+
<span class="stats-chip"><strong id="stat-edges">\u2013</strong> connections</span>
|
|
16438
|
+
<span class="stats-chip"><strong id="stat-fns">\u2013</strong> functions mapped</span>
|
|
16439
|
+
<div id="task-input-wrap">
|
|
16440
|
+
<input id="task-input" type="text" placeholder="e.g. add stripe checkout, fix auth bug, add new page\u2026">
|
|
16441
|
+
<button id="task-submit">\u2192</button>
|
|
16442
|
+
</div>
|
|
16443
|
+
</div>
|
|
16444
|
+
<svg id="graph-svg"></svg>
|
|
16445
|
+
|
|
16446
|
+
<div id="zoom-btns">
|
|
16447
|
+
<div class="zoom-btn" id="btn-zoom-in">+</div>
|
|
16448
|
+
<div class="zoom-btn" id="btn-zoom-fit">\u22A1</div>
|
|
16449
|
+
<div class="zoom-btn" id="btn-zoom-out">\u2212</div>
|
|
16450
|
+
</div>
|
|
16451
|
+
|
|
16452
|
+
<div id="minimap">
|
|
16453
|
+
<svg id="minimap-svg">
|
|
16454
|
+
<rect id="minimap-viewport" x="0" y="0" width="0" height="0"/>
|
|
16455
|
+
</svg>
|
|
16456
|
+
</div>
|
|
16457
|
+
</div>
|
|
16458
|
+
|
|
16459
|
+
<div id="loading">
|
|
16460
|
+
<div class="spinner"></div>
|
|
16461
|
+
<div id="loading-text">Loading graph\u2026</div>
|
|
16462
|
+
</div>
|
|
16463
|
+
|
|
16464
|
+
<div id="help-overlay" style="
|
|
16465
|
+
position:absolute; inset:0; background:rgba(14,14,18,0.85);
|
|
16466
|
+
display:flex; align-items:center; justify-content:center;
|
|
16467
|
+
z-index:50; cursor:pointer; backdrop-filter:blur(2px);
|
|
16468
|
+
" onclick="this.style.display='none'">
|
|
16469
|
+
<div style="text-align:center; max-width:420px; padding:40px">
|
|
16470
|
+
<div style="font-size:32px; margin-bottom:16px">\u{1F344}</div>
|
|
16471
|
+
<div style="font-size:20px; font-weight:700; color:#e0e0f0; margin-bottom:12px">
|
|
16472
|
+
Your codebase, mapped
|
|
16473
|
+
</div>
|
|
16474
|
+
<div style="font-size:13px; color:#7878a0; line-height:1.8; margin-bottom:24px">
|
|
16475
|
+
Each <span style="color:#7c6af7">\u25CF</span> dot is a file.<br>
|
|
16476
|
+
Color shows which folder it belongs to.<br>
|
|
16477
|
+
<span style="color:#5b9fff">Blue lines</span> show imports \u2014 file A uses file B.<br>
|
|
16478
|
+
<span style="color:#ff8c42">Orange lines</span> show function calls across files.<br><br>
|
|
16479
|
+
Click any dot to see what it connects to.<br>
|
|
16480
|
+
Type a task above to find relevant files instantly.
|
|
16481
|
+
</div>
|
|
16482
|
+
<div style="font-size:11px; color:#7878a0; background:#16161e; padding:8px 16px; border-radius:6px; border:1px solid #2a2a3a">
|
|
16483
|
+
Click anywhere to dismiss
|
|
16484
|
+
</div>
|
|
16485
|
+
</div>
|
|
16486
|
+
</div>
|
|
16487
|
+
|
|
16488
|
+
<div id="tooltip">
|
|
16489
|
+
<div class="tip-name"></div>
|
|
16490
|
+
<div class="tip-desc"></div>
|
|
16491
|
+
<div class="tip-tags"></div>
|
|
16492
|
+
</div>
|
|
16493
|
+
|
|
16494
|
+
<script>
|
|
16495
|
+
// \u2500\u2500\u2500 Config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
16496
|
+
const API_BASE = ''; // same origin
|
|
16497
|
+
// Directory-based color palette \u2014 same folder = same color
|
|
16498
|
+
// Much more intuitive than spatial hulls
|
|
16499
|
+
const DIR_PALETTE = [
|
|
16500
|
+
'#6B8AFF', // soft blue \u2014 app/
|
|
16501
|
+
'#4ECDC4', // muted teal \u2014 lib/
|
|
16502
|
+
'#F7B267', // warm amber \u2014 components/
|
|
16503
|
+
'#C5A3FF', // soft lavender \u2014 src/
|
|
16504
|
+
'#7ECBA1', // sage green \u2014 hooks/
|
|
16505
|
+
'#F28B82', // dusty rose \u2014 context/
|
|
16506
|
+
'#93C5FD', // sky blue \u2014 utils/
|
|
16507
|
+
'#FCD34D', // muted gold \u2014 other
|
|
16508
|
+
];
|
|
16509
|
+
|
|
16510
|
+
const dirColorMap = new Map();
|
|
16511
|
+
let dirColorIdx = 0;
|
|
16512
|
+
|
|
16513
|
+
function getDirColor(nodeId) {
|
|
16514
|
+
const parts = (nodeId || '').split('/');
|
|
16515
|
+
const dir = parts.length > 1 ? parts[0] : 'root';
|
|
16516
|
+
if (!dirColorMap.has(dir)) {
|
|
16517
|
+
dirColorMap.set(dir, DIR_PALETTE[dirColorIdx % DIR_PALETTE.length]);
|
|
16518
|
+
dirColorIdx++;
|
|
16519
|
+
}
|
|
16520
|
+
return dirColorMap.get(dir);
|
|
16521
|
+
}
|
|
16522
|
+
|
|
16523
|
+
const COLORS = {
|
|
16524
|
+
file: '#6B8AFF', // fallback \u2014 uses getDirColor normally
|
|
16525
|
+
function: 'rgba(255,255,255,0.55)', // subtle white \u2014 secondary nodes
|
|
16526
|
+
class: 'rgba(255,255,255,0.45)',
|
|
16527
|
+
import: 'rgba(255,255,255,0.35)',
|
|
16528
|
+
call: 'rgba(255,255,255,0.25)',
|
|
16529
|
+
contains: 'rgba(255,255,255,0.08)',
|
|
16530
|
+
};
|
|
16531
|
+
|
|
16532
|
+
// \u2500\u2500\u2500 State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
16533
|
+
let allNodes = [], allLinks = [];
|
|
16534
|
+
let visNodes = [], visLinks = [];
|
|
16535
|
+
let selectedNode = null;
|
|
16536
|
+
let simulation, zoom, svg, gMain, gHulls, gLinks, gNodes;
|
|
16537
|
+
let showImports = true, showCalls = true, showFunctions = false, showClusters = false;
|
|
16538
|
+
let zoomLevel = 'files'; // 'files' | 'symbols'
|
|
16539
|
+
let preflight = new Set();
|
|
16540
|
+
let _initialFitDone = false; // prevents fitToScreen on every simulation restart
|
|
16541
|
+
|
|
16542
|
+
// \u2500\u2500\u2500 Load graph \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
16543
|
+
async function loadGraph() {
|
|
16544
|
+
try {
|
|
16545
|
+
setLoading('Fetching graph\u2026');
|
|
16546
|
+
const r = await fetch(\`\${API_BASE}/graph\`);
|
|
16547
|
+
const data = await r.json();
|
|
16548
|
+
allNodes = data.nodes || [];
|
|
16549
|
+
allLinks = data.edges || [];
|
|
16550
|
+
setLoading('Building simulation\u2026');
|
|
16551
|
+
buildVis();
|
|
16552
|
+
} catch (e) {
|
|
16553
|
+
document.getElementById('loading-text').textContent = 'Failed to load graph. Is the server running?';
|
|
16554
|
+
}
|
|
16555
|
+
}
|
|
16556
|
+
|
|
16557
|
+
// \u2500\u2500\u2500 Build visualization \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
16558
|
+
function buildVis() {
|
|
16559
|
+
const svgEl = document.getElementById('graph-svg');
|
|
16560
|
+
const W = svgEl.clientWidth, H = svgEl.clientHeight;
|
|
16561
|
+
|
|
16562
|
+
// Filter nodes/links based on current view settings
|
|
16563
|
+
visNodes = allNodes.filter(n => {
|
|
16564
|
+
if (n.kind === 'file') return true;
|
|
16565
|
+
if ((n.kind === 'function' || n.kind === 'class') && zoomLevel === 'symbols') return showFunctions;
|
|
16566
|
+
return false;
|
|
16567
|
+
});
|
|
16568
|
+
|
|
16569
|
+
const nodeIds = new Set(visNodes.map(n => n.id));
|
|
16570
|
+
visLinks = allLinks.filter(l => {
|
|
16571
|
+
if (!nodeIds.has(l.from) || !nodeIds.has(l.to)) return false;
|
|
16572
|
+
if (l.kind === 'imports' && !showImports) return false;
|
|
16573
|
+
if (l.kind === 'calls' && !showCalls) return false;
|
|
16574
|
+
if (l.kind === 'contains') return zoomLevel === 'symbols';
|
|
16575
|
+
return true;
|
|
16576
|
+
});
|
|
16577
|
+
|
|
16578
|
+
// Update stats
|
|
16579
|
+
const fnCount = allNodes.filter(n => n.kind === 'function').length;
|
|
16580
|
+
document.getElementById('stat-nodes').textContent = visNodes.length;
|
|
16581
|
+
document.getElementById('stat-edges').textContent = visLinks.length;
|
|
16582
|
+
document.getElementById('stat-fns').textContent = fnCount;
|
|
16583
|
+
|
|
16584
|
+
// Clear SVG
|
|
16585
|
+
d3.select('#graph-svg').selectAll('*').remove();
|
|
16586
|
+
|
|
16587
|
+
svg = d3.select('#graph-svg');
|
|
16588
|
+
_initialFitDone = false; // reset on each rebuild
|
|
16589
|
+
|
|
16590
|
+
// Zoom behavior
|
|
16591
|
+
zoom = d3.zoom()
|
|
16592
|
+
.scaleExtent([0.05, 8])
|
|
16593
|
+
.filter(event => event.type !== 'dblclick')
|
|
16594
|
+
.on('zoom', ({ transform }) => {
|
|
16595
|
+
gMain.attr('transform', transform);
|
|
16596
|
+
updateMinimap(transform, W, H);
|
|
16597
|
+
});
|
|
16598
|
+
|
|
16599
|
+
svg.call(zoom);
|
|
16600
|
+
|
|
16601
|
+
gMain = svg.append('g').attr('class', 'main-group');
|
|
16602
|
+
gHulls = gMain.append('g').attr('class', 'hulls');
|
|
16603
|
+
gLinks = gMain.append('g').attr('class', 'links');
|
|
16604
|
+
gNodes = gMain.append('g').attr('class', 'nodes');
|
|
16605
|
+
|
|
16606
|
+
// \u2500\u2500 Cluster hulls by directory \u2500\u2500
|
|
16607
|
+
const dirGroups = groupByDirectory(visNodes.filter(n => n.kind === 'file'));
|
|
16608
|
+
|
|
16609
|
+
// \u2500\u2500 Build link objects \u2500\u2500
|
|
16610
|
+
const nodeMap = new Map(visNodes.map(n => [n.id, n]));
|
|
16611
|
+
const linkObjs = visLinks.map(l => ({
|
|
16612
|
+
...l,
|
|
16613
|
+
source: nodeMap.get(l.from),
|
|
16614
|
+
target: nodeMap.get(l.to),
|
|
16615
|
+
})).filter(l => l.source && l.target);
|
|
16616
|
+
|
|
16617
|
+
// \u2500\u2500 Simulation \u2500\u2500
|
|
16618
|
+
simulation = d3.forceSimulation(visNodes)
|
|
16619
|
+
.force('link', d3.forceLink(linkObjs)
|
|
16620
|
+
.id(d => d.id)
|
|
16621
|
+
.distance(d => d.kind === 'contains' ? 40 : d.kind === 'calls' ? 80 : 100)
|
|
16622
|
+
.strength(d => d.kind === 'contains' ? 0.8 : 0.3))
|
|
16623
|
+
.force('charge', d3.forceManyBody().strength(d => d.kind === 'file' ? -250 : -80))
|
|
16624
|
+
.force('center', d3.forceCenter(W / 2, H / 2))
|
|
16625
|
+
.force('collision', d3.forceCollide().radius(d => nodeRadius(d) + 8))
|
|
16626
|
+
.force('cluster', clusterForce(dirGroups, 0.08))
|
|
16627
|
+
.alphaDecay(0.02)
|
|
16628
|
+
.on('tick', ticked)
|
|
16629
|
+
.on('end', () => {
|
|
16630
|
+
if (!_initialFitDone) {
|
|
16631
|
+
hideLoading();
|
|
16632
|
+
if (showClusters) renderHulls(dirGroups);
|
|
16633
|
+
fitToScreen();
|
|
16634
|
+
updateDirLegend();
|
|
16635
|
+
_initialFitDone = true;
|
|
16636
|
+
}
|
|
16637
|
+
});
|
|
16638
|
+
|
|
16639
|
+
// \u2500\u2500 Links \u2500\u2500
|
|
16640
|
+
const link = gLinks.selectAll('.link')
|
|
16641
|
+
.data(linkObjs)
|
|
16642
|
+
.join('line')
|
|
16643
|
+
.attr('class', d => \`link \${d.kind}-edge\`)
|
|
16644
|
+
.attr('marker-end', d => d.kind === 'calls' ? 'url(#arrow-call)' : null);
|
|
16645
|
+
|
|
16646
|
+
// Arrow markers
|
|
16647
|
+
svg.append('defs').html(\`
|
|
16648
|
+
<marker id="arrow-call" viewBox="0 -4 8 8" refX="8" refY="0"
|
|
16649
|
+
markerWidth="6" markerHeight="6" orient="auto">
|
|
16650
|
+
<path d="M0,-4L8,0L0,4" fill="\${COLORS.call}" opacity="0.6"/>
|
|
16651
|
+
</marker>
|
|
16652
|
+
\`);
|
|
16653
|
+
|
|
16654
|
+
// \u2500\u2500 Nodes \u2500\u2500
|
|
16655
|
+
const node = gNodes.selectAll('.node')
|
|
16656
|
+
.data(visNodes)
|
|
16657
|
+
.join('g')
|
|
16658
|
+
.attr('class', 'node')
|
|
16659
|
+
.attr('data-id', d => d.id)
|
|
16660
|
+
.call(d3.drag()
|
|
16661
|
+
.clickDistance(8)
|
|
16662
|
+
.on('start', (event, d) => {
|
|
16663
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
16664
|
+
d.fx = d.x; d.fy = d.y;
|
|
16665
|
+
})
|
|
16666
|
+
.on('drag', (event, d) => { d.fx = event.x; d.fy = event.y; })
|
|
16667
|
+
.on('end', (event, d) => {
|
|
16668
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
16669
|
+
d.fx = null; d.fy = null;
|
|
16670
|
+
}))
|
|
16671
|
+
.on('click', (event, d) => {
|
|
16672
|
+
event.stopPropagation();
|
|
16673
|
+
// Smoothly pan to clicked node, preserving current zoom level
|
|
16674
|
+
if (d.x !== undefined && d.y !== undefined) {
|
|
16675
|
+
const svgEl = document.getElementById('graph-svg');
|
|
16676
|
+
const W = svgEl.clientWidth, H = svgEl.clientHeight;
|
|
16677
|
+
const currentT = d3.zoomTransform(svg.node());
|
|
16678
|
+
const s = currentT.k; // keep current zoom level
|
|
16679
|
+
svg.transition().duration(400).call(
|
|
16680
|
+
zoom.transform,
|
|
16681
|
+
d3.zoomIdentity.translate(W / 2 - d.x * s, H / 2 - d.y * s).scale(s)
|
|
16682
|
+
);
|
|
16683
|
+
}
|
|
16684
|
+
selectNode(d, linkObjs);
|
|
16685
|
+
})
|
|
16686
|
+
.on('mouseenter', (event, d) => showTooltip(event, d))
|
|
16687
|
+
.on('mousemove', (event) => moveTooltip(event))
|
|
16688
|
+
.on('mouseleave', hideTooltip);
|
|
16689
|
+
|
|
16690
|
+
node.append('circle')
|
|
16691
|
+
.attr('r', d => nodeRadius(d))
|
|
16692
|
+
.attr('fill', d => d.kind === 'file' ? getDirColor(d.id) : (COLORS[d.kind] ?? COLORS.file))
|
|
16693
|
+
.attr('fill-opacity', 0.9)
|
|
16694
|
+
.attr('stroke', d => d.kind === 'file' ? getDirColor(d.id) : (COLORS[d.kind] ?? COLORS.file))
|
|
16695
|
+
.attr('stroke-width', 2)
|
|
16696
|
+
.attr('stroke-opacity', 0.35);
|
|
16697
|
+
|
|
16698
|
+
node.append('text')
|
|
16699
|
+
.attr('dy', d => nodeRadius(d) + 11)
|
|
16700
|
+
.attr('text-anchor', 'middle')
|
|
16701
|
+
.text(d => shortName(d.name || d.id));
|
|
16702
|
+
|
|
16703
|
+
// Click background to deselect
|
|
16704
|
+
svg.on('click', () => {
|
|
16705
|
+
selectNode(null, linkObjs);
|
|
16706
|
+
});
|
|
16707
|
+
|
|
16708
|
+
function ticked() {
|
|
16709
|
+
link
|
|
16710
|
+
.attr('x1', d => d.source.x)
|
|
16711
|
+
.attr('y1', d => d.source.y)
|
|
16712
|
+
.attr('x2', d => d.target.x)
|
|
16713
|
+
.attr('y2', d => d.target.y);
|
|
16714
|
+
|
|
16715
|
+
node.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
|
|
16716
|
+
|
|
16717
|
+
// Update minimap nodes
|
|
16718
|
+
updateMinimapNodes(visNodes);
|
|
16719
|
+
}
|
|
16720
|
+
}
|
|
16721
|
+
|
|
16722
|
+
// \u2500\u2500\u2500 Cluster force \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
16723
|
+
function clusterForce(groups, strength) {
|
|
16724
|
+
// Gentle force pulling nodes toward their directory centroid
|
|
16725
|
+
const centroids = new Map();
|
|
16726
|
+
|
|
16727
|
+
for (const [dir, nodes] of Object.entries(groups)) {
|
|
16728
|
+
centroids.set(dir, { x: 0, y: 0 });
|
|
16729
|
+
}
|
|
16730
|
+
|
|
16731
|
+
return function(alpha) {
|
|
16732
|
+
for (const [dir, nodes] of Object.entries(groups)) {
|
|
16733
|
+
if (nodes.length < 2) continue;
|
|
16734
|
+
let cx = 0, cy = 0;
|
|
16735
|
+
for (const n of nodes) { cx += n.x || 0; cy += n.y || 0; }
|
|
16736
|
+
cx /= nodes.length; cy /= nodes.length;
|
|
16737
|
+
centroids.set(dir, { x: cx, y: cy });
|
|
16738
|
+
for (const n of nodes) {
|
|
16739
|
+
n.vx = (n.vx || 0) + (cx - n.x) * strength * alpha;
|
|
16740
|
+
n.vy = (n.vy || 0) + (cy - n.y) * strength * alpha;
|
|
16741
|
+
}
|
|
16742
|
+
}
|
|
16743
|
+
};
|
|
16744
|
+
}
|
|
16745
|
+
|
|
16746
|
+
// \u2500\u2500\u2500 Hull rendering \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
16747
|
+
function renderHulls(dirGroups) {
|
|
16748
|
+
gHulls.selectAll('*').remove();
|
|
16749
|
+
|
|
16750
|
+
for (const [dir, nodes] of Object.entries(dirGroups)) {
|
|
16751
|
+
if (nodes.length < 3) continue;
|
|
16752
|
+
const points = nodes.map(n => [n.x, n.y]);
|
|
16753
|
+
const hull = d3.polygonHull(points);
|
|
16754
|
+
if (!hull) continue;
|
|
16755
|
+
|
|
16756
|
+
// Expand hull
|
|
16757
|
+
const centroid = d3.polygonCentroid(hull);
|
|
16758
|
+
const expanded = hull.map(([px, py]) => {
|
|
16759
|
+
const dx = px - centroid[0], dy = py - centroid[1];
|
|
16760
|
+
const dist = Math.sqrt(dx*dx + dy*dy) || 1;
|
|
16761
|
+
return [px + dx/dist * 30, py + dy/dist * 30];
|
|
16762
|
+
});
|
|
16763
|
+
|
|
16764
|
+
const hullColor = getDirColor(nodes[0]?.id ?? dir);
|
|
16765
|
+
gHulls.append('path')
|
|
16766
|
+
.attr('class', 'hull')
|
|
16767
|
+
.attr('d', \`M\${expanded.join('L')}Z\`)
|
|
16768
|
+
.attr('stroke', hullColor);
|
|
16769
|
+
|
|
16770
|
+
gHulls.append('text')
|
|
16771
|
+
.attr('class', 'cluster-label')
|
|
16772
|
+
.attr('x', centroid[0])
|
|
16773
|
+
.attr('y', centroid[1] - (Math.max(...nodes.map(n => Math.abs(n.y - centroid[1]))) + 40))
|
|
16774
|
+
.attr('text-anchor', 'middle')
|
|
16775
|
+
.attr('fill', hullColor)
|
|
16776
|
+
.text(dir);
|
|
16777
|
+
}
|
|
16778
|
+
}
|
|
16779
|
+
|
|
16780
|
+
// \u2500\u2500\u2500 Node selection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
16781
|
+
function selectNode(nodeData, linkObjs) {
|
|
16782
|
+
selectedNode = nodeData;
|
|
16783
|
+
|
|
16784
|
+
if (!nodeData) {
|
|
16785
|
+
// Deselect all
|
|
16786
|
+
d3.selectAll('.node').classed('selected highlighted dimmed', false);
|
|
16787
|
+
d3.selectAll('.link').classed('highlighted dimmed', false);
|
|
16788
|
+
d3.selectAll('.node circle').attr('fill-opacity', 0.85);
|
|
16789
|
+
renderNodeInfo(null, linkObjs);
|
|
16790
|
+
return;
|
|
16791
|
+
}
|
|
16792
|
+
|
|
16793
|
+
const connectedIds = new Set([nodeData.id]);
|
|
16794
|
+
const connectedLinks = new Set();
|
|
16795
|
+
|
|
16796
|
+
for (const l of linkObjs) {
|
|
16797
|
+
const fromId = l.source?.id ?? l.from;
|
|
16798
|
+
const toId = l.target?.id ?? l.to;
|
|
16799
|
+
if (fromId === nodeData.id || toId === nodeData.id) {
|
|
16800
|
+
connectedIds.add(fromId);
|
|
16801
|
+
connectedIds.add(toId);
|
|
16802
|
+
connectedLinks.add(l);
|
|
16803
|
+
}
|
|
16804
|
+
}
|
|
16805
|
+
|
|
16806
|
+
d3.selectAll('.node')
|
|
16807
|
+
.classed('selected', d => d.id === nodeData.id)
|
|
16808
|
+
.classed('highlighted', d => connectedIds.has(d.id))
|
|
16809
|
+
.classed('dimmed', d => !connectedIds.has(d.id));
|
|
16810
|
+
|
|
16811
|
+
d3.selectAll('.link')
|
|
16812
|
+
.classed('highlighted', l => connectedLinks.has(l))
|
|
16813
|
+
.classed('dimmed', l => !connectedLinks.has(l));
|
|
16814
|
+
|
|
16815
|
+
renderNodeInfo(nodeData, linkObjs);
|
|
16816
|
+
}
|
|
16817
|
+
|
|
16818
|
+
// \u2500\u2500\u2500 Sidebar info \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
16819
|
+
function renderNodeInfo(nodeData, linkObjs) {
|
|
16820
|
+
const panel = document.getElementById('node-info');
|
|
16821
|
+
|
|
16822
|
+
if (!nodeData) {
|
|
16823
|
+
panel.innerHTML = '<div class="node-info-empty" style="line-height:1.8">Click any dot to see<br>what it imports, exports,<br>and connects to</div>';
|
|
16824
|
+
return;
|
|
16825
|
+
}
|
|
16826
|
+
|
|
16827
|
+
const imports = linkObjs.filter(l => {
|
|
16828
|
+
const from = l.source?.id ?? l.from;
|
|
16829
|
+
return from === nodeData.id && l.kind === 'imports';
|
|
16830
|
+
});
|
|
16831
|
+
|
|
16832
|
+
const importedBy = linkObjs.filter(l => {
|
|
16833
|
+
const to = l.target?.id ?? l.to;
|
|
16834
|
+
return to === nodeData.id && l.kind === 'imports';
|
|
16835
|
+
});
|
|
16836
|
+
|
|
16837
|
+
const calls = linkObjs.filter(l => {
|
|
16838
|
+
const from = l.source?.id ?? l.from;
|
|
16839
|
+
return from === nodeData.id && l.kind === 'calls';
|
|
16840
|
+
});
|
|
16841
|
+
|
|
16842
|
+
const calledBy = linkObjs.filter(l => {
|
|
16843
|
+
const to = l.target?.id ?? l.to;
|
|
16844
|
+
return to === nodeData.id && l.kind === 'calls';
|
|
16845
|
+
});
|
|
16846
|
+
|
|
16847
|
+
const tags = (nodeData.tags || []).map(t => \`<span class="tag">\${t}</span>\`).join('');
|
|
16848
|
+
const tagsHtml = tags ? \`<div class="tags-wrap">\${tags}</div>\` : '';
|
|
16849
|
+
|
|
16850
|
+
function depList(items, cssClass, labelText, key) {
|
|
16851
|
+
if (!items.length) return '';
|
|
16852
|
+
return \`
|
|
16853
|
+
<div class="section-title">\${labelText} (\${items.length})</div>
|
|
16854
|
+
<div class="dep-list">
|
|
16855
|
+
\${items.slice(0, 12).map(l => {
|
|
16856
|
+
const id = key === 'to' ? (l.target?.id ?? l.to) : (l.source?.id ?? l.from);
|
|
16857
|
+
const shortId = id.split('/').pop();
|
|
16858
|
+
const extra = l.callCount ? \`<span class="dep-label">called \${l.callCount}\xD7</span>\` : '';
|
|
16859
|
+
return \`<div class="dep-item \${cssClass}" onclick="focusNode('\${id}')">\${extra}\${shortId}</div>\`;
|
|
16860
|
+
}).join('')}
|
|
16861
|
+
\${items.length > 12 ? \`<div style="font-size:10px;color:var(--text-dim);padding:4px 8px">+\${items.length - 12} more</div>\` : ''}
|
|
16862
|
+
</div>\`;
|
|
16863
|
+
}
|
|
16864
|
+
|
|
16865
|
+
panel.innerHTML = \`
|
|
16866
|
+
<div class="node-detail">
|
|
16867
|
+
<div class="node-kind-badge kind-\${nodeData.kind}">\${nodeData.kind}</div>
|
|
16868
|
+
<div class="node-name">\${nodeData.name || nodeData.id}</div>
|
|
16869
|
+
\${nodeData.description ? \`<div class="node-description">\${nodeData.description}</div>\` : ''}
|
|
16870
|
+
\${tagsHtml}
|
|
16871
|
+
|
|
16872
|
+
<div class="section-title">Stats</div>
|
|
16873
|
+
<div class="stat-row"><span>Lines</span><span class="stat-val">\${nodeData.lineCount ?? '\u2013'}</span></div>
|
|
16874
|
+
<div class="stat-row"><span>Imports</span><span class="stat-val">\${imports.length}</span></div>
|
|
16875
|
+
<div class="stat-row"><span>Imported by</span><span class="stat-val">\${importedBy.length}</span></div>
|
|
16876
|
+
\${calls.length || calledBy.length ? \`
|
|
16877
|
+
<div class="stat-row"><span>Calls</span><span class="stat-val">\${calls.length}</span></div>
|
|
16878
|
+
<div class="stat-row"><span>Called by</span><span class="stat-val">\${calledBy.length}</span></div>\` : ''}
|
|
16879
|
+
|
|
16880
|
+
\${depList(imports, 'dep-import', 'Imports', 'to')}
|
|
16881
|
+
\${depList(importedBy, 'dep-import', 'Imported by', 'from')}
|
|
16882
|
+
\${depList(calls, 'dep-call', 'Calls', 'to')}
|
|
16883
|
+
\${depList(calledBy, 'dep-calledby', 'Called by', 'from')}
|
|
16884
|
+
</div>\`;
|
|
16885
|
+
}
|
|
16886
|
+
|
|
16887
|
+
// \u2500\u2500\u2500 Focus a node by ID \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
16888
|
+
function focusNode(nodeId) {
|
|
16889
|
+
const nodeData = allNodes.find(n => n.id === nodeId);
|
|
16890
|
+
if (!nodeData) return;
|
|
16891
|
+
|
|
16892
|
+
// Pan to node
|
|
16893
|
+
const svgEl = document.getElementById('graph-svg');
|
|
16894
|
+
const W = svgEl.clientWidth, H = svgEl.clientHeight;
|
|
16895
|
+
|
|
16896
|
+
const linkObjs = visLinks.map(l => ({
|
|
16897
|
+
...l,
|
|
16898
|
+
source: allNodes.find(n => n.id === l.from),
|
|
16899
|
+
target: allNodes.find(n => n.id === l.to),
|
|
16900
|
+
})).filter(l => l.source && l.target);
|
|
16901
|
+
|
|
16902
|
+
if (nodeData.x !== undefined) {
|
|
16903
|
+
const currentT = d3.zoomTransform(svg.node());
|
|
16904
|
+
const s = currentT.k; // never change the zoom level, only pan
|
|
16905
|
+
svg.transition().duration(400).call(
|
|
16906
|
+
zoom.transform,
|
|
16907
|
+
d3.zoomIdentity.translate(W / 2 - nodeData.x * s, H / 2 - nodeData.y * s).scale(s)
|
|
16908
|
+
);
|
|
16909
|
+
}
|
|
16910
|
+
|
|
16911
|
+
selectNode(nodeData, linkObjs);
|
|
16912
|
+
}
|
|
16913
|
+
|
|
16914
|
+
// \u2500\u2500\u2500 Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
16915
|
+
document.getElementById('search').addEventListener('input', function() {
|
|
16916
|
+
const q = this.value.toLowerCase().trim();
|
|
16917
|
+
if (!q) {
|
|
16918
|
+
d3.selectAll('.node').classed('highlighted dimmed', false);
|
|
16919
|
+
return;
|
|
16920
|
+
}
|
|
16921
|
+
|
|
16922
|
+
const matched = new Set(
|
|
16923
|
+
allNodes
|
|
16924
|
+
.filter(n =>
|
|
16925
|
+
(n.id || '').toLowerCase().includes(q) ||
|
|
16926
|
+
(n.name || '').toLowerCase().includes(q) ||
|
|
16927
|
+
(n.description || '').toLowerCase().includes(q) ||
|
|
16928
|
+
(n.tags || []).some(t => t.toLowerCase().includes(q))
|
|
16929
|
+
)
|
|
16930
|
+
.map(n => n.id)
|
|
16931
|
+
);
|
|
16932
|
+
|
|
16933
|
+
d3.selectAll('.node')
|
|
16934
|
+
.classed('highlighted', d => matched.has(d.id))
|
|
16935
|
+
.classed('dimmed', d => !matched.has(d.id));
|
|
16936
|
+
});
|
|
16937
|
+
|
|
16938
|
+
// \u2500\u2500\u2500 Preflight \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
16939
|
+
document.getElementById('task-submit').addEventListener('click', async () => {
|
|
16940
|
+
const task = document.getElementById('task-input').value.trim();
|
|
16941
|
+
if (!task) return;
|
|
16942
|
+
|
|
16943
|
+
const btn = document.getElementById('task-submit');
|
|
16944
|
+
btn.textContent = '\u2026';
|
|
16945
|
+
|
|
16946
|
+
try {
|
|
16947
|
+
const r = await fetch(\`\${API_BASE}/preflight?task=\${encodeURIComponent(task)}\`);
|
|
16948
|
+
const data = await r.json();
|
|
16949
|
+
const fileIds = new Set((data.files || []).map(f => f.nodeId || f.id));
|
|
16950
|
+
preflight = fileIds;
|
|
16951
|
+
|
|
16952
|
+
// Highlight preflight results
|
|
16953
|
+
d3.selectAll('.node')
|
|
16954
|
+
.classed('highlighted', d => fileIds.has(d.id))
|
|
16955
|
+
.classed('dimmed', d => !fileIds.has(d.id));
|
|
16956
|
+
|
|
16957
|
+
d3.selectAll('.link')
|
|
16958
|
+
.classed('dimmed', l => {
|
|
16959
|
+
const from = l.source?.id ?? l.from;
|
|
16960
|
+
const to = l.target?.id ?? l.to;
|
|
16961
|
+
return !fileIds.has(from) && !fileIds.has(to);
|
|
16962
|
+
});
|
|
16963
|
+
|
|
16964
|
+
// Show results in sidebar
|
|
16965
|
+
const panel = document.getElementById('node-info');
|
|
16966
|
+
const saved = data.tokensSaved ?? 0;
|
|
16967
|
+
panel.innerHTML = \`
|
|
16968
|
+
<div class="node-detail">
|
|
16969
|
+
<div class="node-kind-badge kind-file">Preflight</div>
|
|
16970
|
+
<div class="node-name">"\${task}"</div>
|
|
16971
|
+
<div class="node-description">
|
|
16972
|
+
\${fileIds.size} files \xB7 ~\${saved} tokens saved vs reading everything
|
|
16973
|
+
</div>
|
|
16974
|
+
<div class="section-title">Relevant Files</div>
|
|
16975
|
+
<div class="dep-list">
|
|
16976
|
+
\${(data.files || []).map(f => \`
|
|
16977
|
+
<div class="dep-item dep-import" onclick="focusNode('\${f.nodeId || f.id}')">
|
|
16978
|
+
<span class="dep-label">\${f.reason || ''} \xB7 \${Math.round((f.score||0)*100)}% match</span>
|
|
16979
|
+
\${(f.nodeId || f.id).split('/').pop()}
|
|
16980
|
+
</div>
|
|
16981
|
+
\`).join('')}
|
|
16982
|
+
</div>
|
|
16983
|
+
</div>\`;
|
|
16984
|
+
} catch (e) {
|
|
16985
|
+
console.error('Preflight failed:', e);
|
|
16986
|
+
}
|
|
16987
|
+
|
|
16988
|
+
btn.textContent = '\u2192';
|
|
16989
|
+
});
|
|
16990
|
+
|
|
16991
|
+
document.getElementById('task-input').addEventListener('keydown', e => {
|
|
16992
|
+
if (e.key === 'Enter') document.getElementById('task-submit').click();
|
|
16993
|
+
});
|
|
16994
|
+
|
|
16995
|
+
// \u2500\u2500\u2500 Toggle controls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
16996
|
+
function makeToggle(btnId, getter, setter) {
|
|
16997
|
+
document.getElementById(btnId).addEventListener('click', function() {
|
|
16998
|
+
setter(!getter());
|
|
16999
|
+
this.classList.toggle('active', getter());
|
|
17000
|
+
rebuildVis();
|
|
17001
|
+
});
|
|
17002
|
+
}
|
|
17003
|
+
|
|
17004
|
+
makeToggle('toggle-imports', () => showImports, v => showImports = v);
|
|
17005
|
+
makeToggle('toggle-calls', () => showCalls, v => showCalls = v);
|
|
17006
|
+
makeToggle('toggle-fns', () => showFunctions, v => showFunctions = v);
|
|
17007
|
+
|
|
17008
|
+
document.getElementById('toggle-clusters').addEventListener('click', function() {
|
|
17009
|
+
showClusters = !showClusters;
|
|
17010
|
+
this.classList.toggle('active', showClusters);
|
|
17011
|
+
if (showClusters) {
|
|
17012
|
+
renderHulls(groupByDirectory(visNodes.filter(n => n.kind === 'file')));
|
|
17013
|
+
} else {
|
|
17014
|
+
gHulls.selectAll('*').remove();
|
|
17015
|
+
}
|
|
17016
|
+
});
|
|
17017
|
+
|
|
17018
|
+
document.getElementById('zoom-files').addEventListener('click', function() {
|
|
17019
|
+
zoomLevel = 'files';
|
|
17020
|
+
this.classList.add('active');
|
|
17021
|
+
document.getElementById('zoom-symbols').classList.remove('active');
|
|
17022
|
+
rebuildVis();
|
|
17023
|
+
});
|
|
17024
|
+
|
|
17025
|
+
document.getElementById('zoom-symbols').addEventListener('click', function() {
|
|
17026
|
+
zoomLevel = 'symbols';
|
|
17027
|
+
this.classList.add('active');
|
|
17028
|
+
document.getElementById('zoom-files').classList.remove('active');
|
|
17029
|
+
rebuildVis();
|
|
17030
|
+
});
|
|
17031
|
+
|
|
17032
|
+
function rebuildVis() {
|
|
17033
|
+
if (simulation) simulation.stop();
|
|
17034
|
+
document.getElementById('loading').style.display = 'flex';
|
|
17035
|
+
document.getElementById('loading-text').textContent = 'Rebuilding\u2026';
|
|
17036
|
+
setTimeout(() => buildVis(), 50);
|
|
17037
|
+
}
|
|
17038
|
+
|
|
17039
|
+
// \u2500\u2500\u2500 Zoom controls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
17040
|
+
document.getElementById('btn-zoom-in').onclick = () =>
|
|
17041
|
+
svg.transition().duration(300).call(zoom.scaleBy, 1.5);
|
|
17042
|
+
document.getElementById('btn-zoom-out').onclick = () =>
|
|
17043
|
+
svg.transition().duration(300).call(zoom.scaleBy, 0.667);
|
|
17044
|
+
document.getElementById('btn-zoom-fit').onclick = fitToScreen;
|
|
17045
|
+
|
|
17046
|
+
function fitToScreen() {
|
|
17047
|
+
if (!visNodes.length) return;
|
|
17048
|
+
const svgEl = document.getElementById('graph-svg');
|
|
17049
|
+
const W = svgEl.clientWidth, H = svgEl.clientHeight;
|
|
17050
|
+
|
|
17051
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
17052
|
+
for (const n of visNodes) {
|
|
17053
|
+
if (n.x < minX) minX = n.x;
|
|
17054
|
+
if (n.y < minY) minY = n.y;
|
|
17055
|
+
if (n.x > maxX) maxX = n.x;
|
|
17056
|
+
if (n.y > maxY) maxY = n.y;
|
|
17057
|
+
}
|
|
17058
|
+
|
|
17059
|
+
const pad = 60;
|
|
17060
|
+
const scale = Math.min(
|
|
17061
|
+
(W - pad * 2) / (maxX - minX || 1),
|
|
17062
|
+
(H - pad * 2) / (maxY - minY || 1),
|
|
17063
|
+
2.5
|
|
17064
|
+
);
|
|
17065
|
+
const tx = W / 2 - ((minX + maxX) / 2) * scale;
|
|
17066
|
+
const ty = H / 2 - ((minY + maxY) / 2) * scale;
|
|
17067
|
+
|
|
17068
|
+
svg.transition().duration(700).call(
|
|
17069
|
+
zoom.transform,
|
|
17070
|
+
d3.zoomIdentity.translate(tx, ty).scale(scale)
|
|
17071
|
+
);
|
|
17072
|
+
}
|
|
17073
|
+
|
|
17074
|
+
// \u2500\u2500\u2500 Minimap \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
17075
|
+
let minimapSvg;
|
|
17076
|
+
|
|
17077
|
+
function initMinimap() {
|
|
17078
|
+
minimapSvg = d3.select('#minimap-svg');
|
|
17079
|
+
}
|
|
17080
|
+
|
|
17081
|
+
function updateMinimapNodes(nodes) {
|
|
17082
|
+
if (!minimapSvg || !nodes.length) return;
|
|
17083
|
+
const mm = document.getElementById('minimap');
|
|
17084
|
+
const W = mm.clientWidth, H = mm.clientHeight;
|
|
17085
|
+
|
|
17086
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
17087
|
+
for (const n of nodes) {
|
|
17088
|
+
if (n.x < minX) minX = n.x;
|
|
17089
|
+
if (n.y < minY) minY = n.y;
|
|
17090
|
+
if (n.x > maxX) maxX = n.x;
|
|
17091
|
+
if (n.y > maxY) maxY = n.y;
|
|
17092
|
+
}
|
|
17093
|
+
|
|
17094
|
+
const scaleX = (W - 8) / (maxX - minX || 1);
|
|
17095
|
+
const scaleY = (H - 8) / (maxY - minY || 1);
|
|
17096
|
+
const scale = Math.min(scaleX, scaleY);
|
|
17097
|
+
|
|
17098
|
+
minimapSvg.selectAll('.mm-node').remove();
|
|
17099
|
+
|
|
17100
|
+
minimapSvg.selectAll('.mm-node')
|
|
17101
|
+
.data(nodes.filter(n => n.kind === 'file'))
|
|
17102
|
+
.join('circle')
|
|
17103
|
+
.attr('class', 'mm-node')
|
|
17104
|
+
.attr('cx', d => 4 + (d.x - minX) * scale)
|
|
17105
|
+
.attr('cy', d => 4 + (d.y - minY) * scale)
|
|
17106
|
+
.attr('r', 2)
|
|
17107
|
+
.attr('fill', d => COLORS[d.kind] ?? '#7c6af7')
|
|
17108
|
+
.attr('opacity', 0.7);
|
|
17109
|
+
}
|
|
17110
|
+
|
|
17111
|
+
function updateMinimap(transform, svgW, svgH) {
|
|
17112
|
+
if (!minimapSvg) return;
|
|
17113
|
+
const mm = document.getElementById('minimap');
|
|
17114
|
+
const W = mm.clientWidth, H = mm.clientHeight;
|
|
17115
|
+
|
|
17116
|
+
// Minimap scale is roughly W/svgW * transform.k
|
|
17117
|
+
const vW = (W / transform.k) * (W / svgW);
|
|
17118
|
+
const vH = (H / transform.k) * (H / svgH);
|
|
17119
|
+
const vX = -transform.x * (W / svgW) / transform.k;
|
|
17120
|
+
const vY = -transform.y * (H / svgH) / transform.k;
|
|
17121
|
+
|
|
17122
|
+
d3.select('#minimap-viewport')
|
|
17123
|
+
.attr('x', Math.max(0, vX))
|
|
17124
|
+
.attr('y', Math.max(0, vY))
|
|
17125
|
+
.attr('width', Math.min(W, vW))
|
|
17126
|
+
.attr('height', Math.min(H, vH));
|
|
17127
|
+
}
|
|
17128
|
+
|
|
17129
|
+
// \u2500\u2500\u2500 Tooltip \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
17130
|
+
function showTooltip(event, d) {
|
|
17131
|
+
const tip = document.getElementById('tooltip');
|
|
17132
|
+
const name = (d.id || '').split('/').pop() || d.name || d.id;
|
|
17133
|
+
const dir = (d.id || '').split('/').slice(0, -1).join('/');
|
|
17134
|
+
tip.querySelector('.tip-name').textContent = name;
|
|
17135
|
+
tip.querySelector('.tip-path').textContent = dir ? dir + '/' : '';
|
|
17136
|
+
tip.querySelector('.tip-desc').textContent = d.description || 'No description yet \u2014 run mycelium init to summarize';
|
|
17137
|
+
tip.style.display = 'block';
|
|
17138
|
+
moveTooltip(event);
|
|
17139
|
+
}
|
|
17140
|
+
|
|
17141
|
+
function moveTooltip(event) {
|
|
17142
|
+
const tip = document.getElementById('tooltip');
|
|
17143
|
+
const sidebar = document.getElementById('sidebar');
|
|
17144
|
+
const x = event.clientX - sidebar.clientWidth;
|
|
17145
|
+
const y = event.clientY - 44; // topbar height
|
|
17146
|
+
tip.style.left = (x + 12) + 'px';
|
|
17147
|
+
tip.style.top = (y - tip.offsetHeight - 8) + 'px';
|
|
17148
|
+
}
|
|
17149
|
+
|
|
17150
|
+
function hideTooltip() {
|
|
17151
|
+
document.getElementById('tooltip').style.display = 'none';
|
|
17152
|
+
}
|
|
17153
|
+
|
|
17154
|
+
// \u2500\u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
17155
|
+
function nodeRadius(d) {
|
|
17156
|
+
if (d.kind === 'file') return Math.max(5, Math.min(14, Math.sqrt((d.lineCount || 100) / 10)));
|
|
17157
|
+
if (d.kind === 'function') return 4;
|
|
17158
|
+
if (d.kind === 'class') return 6;
|
|
17159
|
+
return 5;
|
|
17160
|
+
}
|
|
17161
|
+
|
|
17162
|
+
function shortName(name) {
|
|
17163
|
+
if (!name) return '';
|
|
17164
|
+
const parts = name.split('/');
|
|
17165
|
+
const last = parts[parts.length - 1];
|
|
17166
|
+
return last.length > 20 ? last.slice(0, 18) + '\u2026' : last;
|
|
17167
|
+
}
|
|
17168
|
+
|
|
17169
|
+
function groupByDirectory(fileNodes) {
|
|
17170
|
+
const groups = {};
|
|
17171
|
+
for (const n of fileNodes) {
|
|
17172
|
+
const parts = (n.id || '').split('/');
|
|
17173
|
+
const dir = parts.length > 1 ? parts.slice(0, -1).join('/') : '.';
|
|
17174
|
+
if (!groups[dir]) groups[dir] = [];
|
|
17175
|
+
groups[dir].push(n);
|
|
17176
|
+
}
|
|
17177
|
+
return groups;
|
|
17178
|
+
}
|
|
17179
|
+
|
|
17180
|
+
function setLoading(text) {
|
|
17181
|
+
document.getElementById('loading').style.display = 'flex';
|
|
17182
|
+
document.getElementById('loading-text').textContent = text;
|
|
17183
|
+
}
|
|
17184
|
+
|
|
17185
|
+
function hideLoading() {
|
|
17186
|
+
document.getElementById('loading').style.display = 'none';
|
|
17187
|
+
}
|
|
17188
|
+
|
|
17189
|
+
// \u2500\u2500\u2500 Init \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
17190
|
+
initMinimap();
|
|
17191
|
+
loadGraph();
|
|
17192
|
+
|
|
17193
|
+
function updateDirLegend() {
|
|
17194
|
+
const el = document.getElementById('dir-legend');
|
|
17195
|
+
if (!el) return;
|
|
17196
|
+
el.innerHTML = Array.from(dirColorMap.entries()).map(([dir, color]) =>
|
|
17197
|
+
\`<div class="legend-item"><div class="legend-dot" style="background:\${color}"></div>\${dir}/</div>\`
|
|
17198
|
+
).join('');
|
|
17199
|
+
}
|
|
17200
|
+
</script>
|
|
17201
|
+
</body>
|
|
17202
|
+
</html>
|
|
17203
|
+
`;
|
|
15899
17204
|
var RateLimiter = class {
|
|
15900
17205
|
constructor() {
|
|
15901
17206
|
this.counts = /* @__PURE__ */ new Map();
|
|
@@ -16233,25 +17538,32 @@ var McpServer = class {
|
|
|
16233
17538
|
const candidates = [
|
|
16234
17539
|
path4.join(__dirname, "..", "..", "ui", "index.html"),
|
|
16235
17540
|
path4.join(__dirname, "..", "ui", "index.html"),
|
|
16236
|
-
path4.join(
|
|
17541
|
+
path4.join(__dirname, "ui", "index.html"),
|
|
17542
|
+
path4.join(process.cwd(), "ui", "index.html"),
|
|
17543
|
+
// npm global install location
|
|
17544
|
+
path4.join(path4.dirname(process.execPath), "..", "lib", "node_modules", "@kopikocappu", "mycelium", "ui", "index.html")
|
|
16237
17545
|
];
|
|
16238
17546
|
for (const candidate of candidates) {
|
|
16239
17547
|
if (fs3.existsSync(candidate)) {
|
|
16240
17548
|
try {
|
|
16241
17549
|
const html = fs3.readFileSync(candidate, "utf-8");
|
|
16242
|
-
const
|
|
17550
|
+
const injected2 = html.replace(
|
|
16243
17551
|
"const API_BASE = '';",
|
|
16244
17552
|
`const API_BASE = 'http://localhost:${this.config.mcp.port}';`
|
|
16245
17553
|
);
|
|
16246
17554
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
16247
|
-
res.end(
|
|
17555
|
+
res.end(injected2);
|
|
16248
17556
|
return;
|
|
16249
17557
|
} catch {
|
|
16250
17558
|
}
|
|
16251
17559
|
}
|
|
16252
17560
|
}
|
|
16253
17561
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
16254
|
-
|
|
17562
|
+
const injected = EMBEDDED_VIEWER.replace(
|
|
17563
|
+
"const API_BASE = '';",
|
|
17564
|
+
`const API_BASE = 'http://localhost:${this.config.mcp.port}';`
|
|
17565
|
+
);
|
|
17566
|
+
res.end(injected);
|
|
16255
17567
|
}
|
|
16256
17568
|
handleDebug(res) {
|
|
16257
17569
|
const stats = this.store.getStats();
|
|
@@ -16501,19 +17813,29 @@ function detectSourceGlobs(workspaceRoot) {
|
|
|
16501
17813
|
try {
|
|
16502
17814
|
const raw = fs5.readFileSync(tsconfigPath, "utf8").replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
16503
17815
|
const tsconfig = JSON.parse(raw);
|
|
16504
|
-
|
|
16505
|
-
|
|
17816
|
+
const hasExtends = !!tsconfig.extends;
|
|
17817
|
+
if (!hasExtends && Array.isArray(tsconfig.include) && tsconfig.include.length > 0) {
|
|
17818
|
+
const globs = tsconfig.include.filter((p) => {
|
|
17819
|
+
const hasLeadingDir = /^[^*]+\//.test(p);
|
|
17820
|
+
const isFileGlob = p.startsWith("*") && !hasLeadingDir;
|
|
17821
|
+
const isDeclarationFile = p.endsWith(".d.ts");
|
|
17822
|
+
return !isFileGlob && !isDeclarationFile;
|
|
17823
|
+
}).map((p) => {
|
|
16506
17824
|
const base = p.replace(/\/\*\*\/\*(\.\w+)?$/, "").replace(/\/\*(\.\w+)?$/, "").replace(/\.$/, "").replace(/\*$/, "").replace(/\/$/, "");
|
|
16507
17825
|
return base ? `${base}/**/*.${exts}` : `**/*.${exts}`;
|
|
16508
17826
|
}).filter((v, i, a) => a.indexOf(v) === i);
|
|
16509
|
-
|
|
16510
|
-
|
|
16511
|
-
|
|
16512
|
-
|
|
16513
|
-
|
|
16514
|
-
|
|
16515
|
-
|
|
16516
|
-
|
|
17827
|
+
if (globs.length > 0) {
|
|
17828
|
+
console.log(`[GraphMem] Detected source globs from tsconfig.include:`, globs);
|
|
17829
|
+
return globs;
|
|
17830
|
+
}
|
|
17831
|
+
}
|
|
17832
|
+
if (!hasExtends) {
|
|
17833
|
+
const rootDir = tsconfig.compilerOptions?.rootDir;
|
|
17834
|
+
if (rootDir && rootDir !== ".") {
|
|
17835
|
+
const glob2 = `${rootDir.replace(/^\.\//, "")}/**/*.${exts}`;
|
|
17836
|
+
console.log(`[GraphMem] Detected source glob from tsconfig.rootDir:`, glob2);
|
|
17837
|
+
return [glob2];
|
|
17838
|
+
}
|
|
16517
17839
|
}
|
|
16518
17840
|
} catch (e) {
|
|
16519
17841
|
console.warn("[GraphMem] Could not parse tsconfig.json, falling back to directory detection");
|