@syke1/mcp-server 1.4.0 → 1.4.1
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/index.js +3 -3
- package/dist/web/public/app.js +570 -28
- package/dist/web/public/index.html +43 -0
- package/dist/web/public/style.css +564 -2
- package/dist/web/server.js +1 -0
- package/package.json +1 -1
package/dist/web/public/app.js
CHANGED
|
@@ -23,6 +23,17 @@ let birthAnimations = new Map(); // nodeId → { startTime, spawnPos, targetPos
|
|
|
23
23
|
let searchActive = false; // true when search input has text
|
|
24
24
|
let _searchRAF = null; // RAF loop for search glow animation
|
|
25
25
|
|
|
26
|
+
// ── File Tree state ──
|
|
27
|
+
let fileTreeData = null; // built tree structure
|
|
28
|
+
let fileTreeExpanded = new Set(); // open folder paths
|
|
29
|
+
let fileTreeFilter = ''; // search filter
|
|
30
|
+
let fileTreeSort = 'name'; // sort mode
|
|
31
|
+
let fileTreeModified = new Map(); // path → {type, timestamp}
|
|
32
|
+
let fileTreeVisible = true; // panel visibility
|
|
33
|
+
let _fileTreeRenderTimer = null; // debounce timer
|
|
34
|
+
let _treeScrollLock = false; // manual scroll lock
|
|
35
|
+
let _treeScrollLockTimer = null; // scroll lock timer
|
|
36
|
+
|
|
26
37
|
let LAYER_HEX = {
|
|
27
38
|
FE: "#00d4ff", BE: "#c084fc", DB: "#ff6b35",
|
|
28
39
|
API: "#00ffaa", CONFIG: "#ffd700", UTIL: "#ff69b4",
|
|
@@ -97,6 +108,17 @@ const SETTINGS_DEFAULTS = {
|
|
|
97
108
|
crossLayerDistance: 900,
|
|
98
109
|
clusterStrength: 0.015,
|
|
99
110
|
},
|
|
111
|
+
fileTree: {
|
|
112
|
+
panelWidth: 280,
|
|
113
|
+
fontSize: 11,
|
|
114
|
+
indentSize: 14,
|
|
115
|
+
pulseDuration: 5,
|
|
116
|
+
showLineCount: false,
|
|
117
|
+
showDeps: true,
|
|
118
|
+
showRisk: true,
|
|
119
|
+
autoScrollOnChange: true,
|
|
120
|
+
compactMode: false,
|
|
121
|
+
},
|
|
100
122
|
};
|
|
101
123
|
|
|
102
124
|
function deepMerge(defaults, override) {
|
|
@@ -208,6 +230,9 @@ function applySettings(group) {
|
|
|
208
230
|
case "animation":
|
|
209
231
|
// Animation values are read when new nodes appear, no action needed
|
|
210
232
|
break;
|
|
233
|
+
case "fileTree":
|
|
234
|
+
applyFileTreeSettings();
|
|
235
|
+
break;
|
|
211
236
|
}
|
|
212
237
|
}
|
|
213
238
|
|
|
@@ -261,6 +286,15 @@ function setupSettings() {
|
|
|
261
286
|
["animation", "spawnX", "#set-anim-spawnX", "range"],
|
|
262
287
|
["animation", "spawnY", "#set-anim-spawnY", "range"],
|
|
263
288
|
["animation", "spawnZ", "#set-anim-spawnZ", "range"],
|
|
289
|
+
["fileTree", "panelWidth", "#set-ft-panelWidth", "range"],
|
|
290
|
+
["fileTree", "fontSize", "#set-ft-fontSize", "range"],
|
|
291
|
+
["fileTree", "indentSize", "#set-ft-indentSize", "range"],
|
|
292
|
+
["fileTree", "pulseDuration", "#set-ft-pulseDuration", "range"],
|
|
293
|
+
["fileTree", "showLineCount", "#set-ft-showLineCount", "checkbox"],
|
|
294
|
+
["fileTree", "showDeps", "#set-ft-showDeps", "checkbox"],
|
|
295
|
+
["fileTree", "showRisk", "#set-ft-showRisk", "checkbox"],
|
|
296
|
+
["fileTree", "autoScrollOnChange", "#set-ft-autoScrollOnChange", "checkbox"],
|
|
297
|
+
["fileTree", "compactMode", "#set-ft-compactMode", "checkbox"],
|
|
264
298
|
];
|
|
265
299
|
|
|
266
300
|
// Init values and bind events
|
|
@@ -268,16 +302,25 @@ function setupSettings() {
|
|
|
268
302
|
const el = document.querySelector(selector);
|
|
269
303
|
if (!el) continue;
|
|
270
304
|
const valEl = el.parentElement?.querySelector(".set-val");
|
|
271
|
-
el.value = SETTINGS[group][key];
|
|
272
|
-
if (valEl) valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
273
305
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
306
|
+
if (type === "checkbox") {
|
|
307
|
+
el.checked = !!SETTINGS[group][key];
|
|
308
|
+
el.addEventListener("change", () => {
|
|
309
|
+
SETTINGS[group][key] = el.checked;
|
|
310
|
+
applySettings(group);
|
|
311
|
+
saveSettings();
|
|
312
|
+
});
|
|
313
|
+
} else {
|
|
314
|
+
el.value = SETTINGS[group][key];
|
|
315
|
+
if (valEl) valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
316
|
+
el.addEventListener("input", () => {
|
|
317
|
+
const v = type === "color" ? el.value : parseFloat(el.value);
|
|
318
|
+
SETTINGS[group][key] = v;
|
|
319
|
+
if (valEl) valEl.textContent = formatSetVal(v, type);
|
|
320
|
+
applySettings(group);
|
|
321
|
+
saveSettings();
|
|
322
|
+
});
|
|
323
|
+
}
|
|
281
324
|
}
|
|
282
325
|
|
|
283
326
|
// Collapsible sections
|
|
@@ -302,9 +345,10 @@ function setupSettings() {
|
|
|
302
345
|
if (g !== group) continue;
|
|
303
346
|
const el = document.querySelector(selector);
|
|
304
347
|
if (!el) continue;
|
|
305
|
-
el.
|
|
348
|
+
if (type === "checkbox") { el.checked = !!SETTINGS[group][key]; }
|
|
349
|
+
else { el.value = SETTINGS[group][key]; }
|
|
306
350
|
const valEl = el.parentElement?.querySelector(".set-val");
|
|
307
|
-
if (valEl) valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
351
|
+
if (valEl && type !== "checkbox") valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
308
352
|
}
|
|
309
353
|
});
|
|
310
354
|
});
|
|
@@ -319,9 +363,10 @@ function setupSettings() {
|
|
|
319
363
|
for (const [group, key, selector, type] of CTRL_MAP) {
|
|
320
364
|
const el = document.querySelector(selector);
|
|
321
365
|
if (!el) continue;
|
|
322
|
-
el.
|
|
366
|
+
if (type === "checkbox") { el.checked = !!SETTINGS[group][key]; }
|
|
367
|
+
else { el.value = SETTINGS[group][key]; }
|
|
323
368
|
const valEl = el.parentElement?.querySelector(".set-val");
|
|
324
|
-
if (valEl) valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
369
|
+
if (valEl && type !== "checkbox") valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
325
370
|
}
|
|
326
371
|
});
|
|
327
372
|
}
|
|
@@ -365,9 +410,10 @@ function setupSettings() {
|
|
|
365
410
|
for (const [group, key, selector, type] of CTRL_MAP) {
|
|
366
411
|
const el = document.querySelector(selector);
|
|
367
412
|
if (!el) continue;
|
|
368
|
-
el.
|
|
413
|
+
if (type === "checkbox") { el.checked = !!SETTINGS[group][key]; }
|
|
414
|
+
else { el.value = SETTINGS[group][key]; }
|
|
369
415
|
const valEl = el.parentElement?.querySelector(".set-val");
|
|
370
|
-
if (valEl) valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
416
|
+
if (valEl && type !== "checkbox") valEl.textContent = formatSetVal(SETTINGS[group][key], type);
|
|
371
417
|
}
|
|
372
418
|
} catch(e) {
|
|
373
419
|
console.error("[SYKE] Import failed:", e);
|
|
@@ -417,6 +463,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
417
463
|
setupTabs();
|
|
418
464
|
setupSettings();
|
|
419
465
|
setupProjectModal();
|
|
466
|
+
setupFileTree();
|
|
420
467
|
initSSE();
|
|
421
468
|
startHealthCheck();
|
|
422
469
|
});
|
|
@@ -548,6 +595,7 @@ async function loadGraph() {
|
|
|
548
595
|
if (isReload) {
|
|
549
596
|
Graph.graphData(graphData);
|
|
550
597
|
buildLegend(layerCounts);
|
|
598
|
+
renderFileTreeDebounced();
|
|
551
599
|
console.log("[SYKE] Graph updated (reload), birth animations:", birthAnimations.size);
|
|
552
600
|
|
|
553
601
|
// ── Star birth camera choreography ──
|
|
@@ -595,7 +643,7 @@ async function loadGraph() {
|
|
|
595
643
|
const container = document.getElementById("3d-graph");
|
|
596
644
|
|
|
597
645
|
Graph = ForceGraph3D()(container)
|
|
598
|
-
.width(
|
|
646
|
+
.width(getGraphPanelWidth())
|
|
599
647
|
.height(window.innerHeight - 100)
|
|
600
648
|
.graphData(graphData)
|
|
601
649
|
.backgroundColor(SETTINGS.scene.background)
|
|
@@ -757,10 +805,11 @@ async function loadGraph() {
|
|
|
757
805
|
container.addEventListener("touchstart", stopUser);
|
|
758
806
|
|
|
759
807
|
window.addEventListener("resize", () => {
|
|
760
|
-
if (Graph) Graph.width(
|
|
808
|
+
if (Graph) Graph.width(getGraphPanelWidth()).height(window.innerHeight - 100);
|
|
761
809
|
});
|
|
762
810
|
|
|
763
811
|
buildLegend(layerCounts);
|
|
812
|
+
renderFileTreeDebounced();
|
|
764
813
|
createNodeLabels();
|
|
765
814
|
updateLabelsLoop();
|
|
766
815
|
|
|
@@ -1141,6 +1190,8 @@ async function handleNodeClick(node) {
|
|
|
1141
1190
|
loadSimulation(node.id);
|
|
1142
1191
|
// Start Star Wars code crawl
|
|
1143
1192
|
startCodeCrawl(node.id);
|
|
1193
|
+
// Sync file tree selection
|
|
1194
|
+
treeScrollToFile(node.id);
|
|
1144
1195
|
}
|
|
1145
1196
|
|
|
1146
1197
|
function handleBackgroundClick() {
|
|
@@ -1844,6 +1895,9 @@ function setupKeyboardShortcuts() {
|
|
|
1844
1895
|
case "s":
|
|
1845
1896
|
if (selectedFile) { loadSimulation(selectedFile); switchTab("simulate"); }
|
|
1846
1897
|
break;
|
|
1898
|
+
case "t":
|
|
1899
|
+
toggleFileTreePanel();
|
|
1900
|
+
break;
|
|
1847
1901
|
case "d":
|
|
1848
1902
|
detectCycles();
|
|
1849
1903
|
break;
|
|
@@ -2059,8 +2113,9 @@ function setupEventListeners() {
|
|
|
2059
2113
|
});
|
|
2060
2114
|
});
|
|
2061
2115
|
|
|
2062
|
-
// ── Resizable
|
|
2116
|
+
// ── Resizable Panels ──
|
|
2063
2117
|
setupResizeHandle();
|
|
2118
|
+
setupTreeResizeHandle();
|
|
2064
2119
|
}
|
|
2065
2120
|
|
|
2066
2121
|
// ═══════════════════════════════════════════
|
|
@@ -2707,6 +2762,16 @@ async function initSSE() {
|
|
|
2707
2762
|
const data = JSON.parse(e.data);
|
|
2708
2763
|
console.log("[SYKE:SSE] File change:", data.file, data.type, data.diffCount, "diffs");
|
|
2709
2764
|
|
|
2765
|
+
// ── 0. Update file tree modification tracking ──
|
|
2766
|
+
fileTreeModified.set(data.file, { type: data.type, timestamp: Date.now() });
|
|
2767
|
+
renderFileTreeDebounced();
|
|
2768
|
+
// Auto-clear pulse after pulseDuration
|
|
2769
|
+
const pulseDur = (SETTINGS.fileTree.pulseDuration || 5) * 1000;
|
|
2770
|
+
setTimeout(() => {
|
|
2771
|
+
fileTreeModified.delete(data.file);
|
|
2772
|
+
renderFileTreeDebounced();
|
|
2773
|
+
}, pulseDur);
|
|
2774
|
+
|
|
2710
2775
|
// ── 1. Auto-select the modified node in 3D graph ──
|
|
2711
2776
|
selectedFile = data.file;
|
|
2712
2777
|
selectedNodeId = data.file;
|
|
@@ -2861,6 +2926,7 @@ async function initSSE() {
|
|
|
2861
2926
|
setTimeout(async () => {
|
|
2862
2927
|
await loadGraph();
|
|
2863
2928
|
await loadHubFiles();
|
|
2929
|
+
renderFileTreeDebounced();
|
|
2864
2930
|
}, 1000);
|
|
2865
2931
|
});
|
|
2866
2932
|
|
|
@@ -2980,6 +3046,489 @@ function renderRealtimePanel() {
|
|
|
2980
3046
|
// ═══════════════════════════════════════════
|
|
2981
3047
|
// RESIZABLE RIGHT PANEL
|
|
2982
3048
|
// ═══════════════════════════════════════════
|
|
3049
|
+
// ═══════════════════════════════════════════
|
|
3050
|
+
// FILE TREE PANEL
|
|
3051
|
+
// ═══════════════════════════════════════════
|
|
3052
|
+
|
|
3053
|
+
function getGraphPanelWidth() {
|
|
3054
|
+
const rightPanel = document.getElementById("right-panel");
|
|
3055
|
+
const treePanel = document.getElementById("file-tree-panel");
|
|
3056
|
+
const rw = rightPanel ? rightPanel.offsetWidth : 380;
|
|
3057
|
+
const tw = (treePanel && !treePanel.classList.contains("hidden")) ? treePanel.offsetWidth : 0;
|
|
3058
|
+
// 8px per resize handle (tree + right)
|
|
3059
|
+
return window.innerWidth - rw - tw - 16;
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
function updateDynamicRightOffsets() {
|
|
3063
|
+
const rightPanel = document.getElementById("right-panel");
|
|
3064
|
+
const treePanel = document.getElementById("file-tree-panel");
|
|
3065
|
+
const rw = rightPanel ? rightPanel.offsetWidth : 380;
|
|
3066
|
+
const tw = (treePanel && !treePanel.classList.contains("hidden")) ? treePanel.offsetWidth : 0;
|
|
3067
|
+
const total = rw + tw + 16; // 2 resize handles × 8px
|
|
3068
|
+
document.documentElement.style.setProperty("--right-offset", total + "px");
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
function toggleFileTreePanel() {
|
|
3072
|
+
const panel = document.getElementById("file-tree-panel");
|
|
3073
|
+
const treeResize = document.getElementById("tree-resize-handle");
|
|
3074
|
+
if (!panel) return;
|
|
3075
|
+
fileTreeVisible = !fileTreeVisible;
|
|
3076
|
+
panel.classList.toggle("hidden", !fileTreeVisible);
|
|
3077
|
+
if (treeResize) treeResize.style.display = fileTreeVisible ? "" : "none";
|
|
3078
|
+
updateDynamicRightOffsets();
|
|
3079
|
+
if (Graph) {
|
|
3080
|
+
const gp = document.getElementById("graph-panel");
|
|
3081
|
+
if (gp) Graph.width(gp.clientWidth);
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
function buildFileTree(nodes) {
|
|
3086
|
+
if (!nodes || !nodes.length) { fileTreeData = null; return; }
|
|
3087
|
+
const root = { name: "", children: {}, files: [], path: "" };
|
|
3088
|
+
|
|
3089
|
+
nodes.forEach(n => {
|
|
3090
|
+
const parts = (n.fullPath || n.id).split("/").filter(Boolean);
|
|
3091
|
+
let current = root;
|
|
3092
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
3093
|
+
const folderName = parts[i];
|
|
3094
|
+
const folderPath = parts.slice(0, i + 1).join("/");
|
|
3095
|
+
if (!current.children[folderName]) {
|
|
3096
|
+
current.children[folderName] = { name: folderName, children: {}, files: [], path: folderPath };
|
|
3097
|
+
}
|
|
3098
|
+
current = current.children[folderName];
|
|
3099
|
+
}
|
|
3100
|
+
current.files.push({
|
|
3101
|
+
name: parts[parts.length - 1] || n.label,
|
|
3102
|
+
node: n,
|
|
3103
|
+
path: n.fullPath || n.id,
|
|
3104
|
+
});
|
|
3105
|
+
});
|
|
3106
|
+
|
|
3107
|
+
fileTreeData = root;
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
function countFilesRecursive(folder) {
|
|
3111
|
+
let count = folder.files.length;
|
|
3112
|
+
for (const child of Object.values(folder.children)) {
|
|
3113
|
+
count += countFilesRecursive(child);
|
|
3114
|
+
}
|
|
3115
|
+
return count;
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
function getAggregateRisk(folder) {
|
|
3119
|
+
const order = ["HIGH", "MEDIUM", "LOW", "NONE"];
|
|
3120
|
+
let highest = 3; // NONE
|
|
3121
|
+
for (const f of folder.files) {
|
|
3122
|
+
const idx = order.indexOf(f.node.riskLevel);
|
|
3123
|
+
if (idx >= 0 && idx < highest) highest = idx;
|
|
3124
|
+
}
|
|
3125
|
+
for (const child of Object.values(folder.children)) {
|
|
3126
|
+
const childRisk = getAggregateRisk(child);
|
|
3127
|
+
const idx = order.indexOf(childRisk);
|
|
3128
|
+
if (idx >= 0 && idx < highest) highest = idx;
|
|
3129
|
+
}
|
|
3130
|
+
return order[highest];
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
function folderHasModifiedChild(folder) {
|
|
3134
|
+
for (const f of folder.files) {
|
|
3135
|
+
if (fileTreeModified.has(f.path)) return true;
|
|
3136
|
+
}
|
|
3137
|
+
for (const child of Object.values(folder.children)) {
|
|
3138
|
+
if (folderHasModifiedChild(child)) return true;
|
|
3139
|
+
}
|
|
3140
|
+
return false;
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
function folderMatchesFilter(folder, filter) {
|
|
3144
|
+
if (!filter) return true;
|
|
3145
|
+
const fl = filter.toLowerCase();
|
|
3146
|
+
for (const f of folder.files) {
|
|
3147
|
+
if (f.name.toLowerCase().includes(fl) || f.path.toLowerCase().includes(fl)) return true;
|
|
3148
|
+
}
|
|
3149
|
+
for (const child of Object.values(folder.children)) {
|
|
3150
|
+
if (child.name.toLowerCase().includes(fl) || folderMatchesFilter(child, filter)) return true;
|
|
3151
|
+
}
|
|
3152
|
+
return false;
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
function sortTreeItems(items, sortType) {
|
|
3156
|
+
return [...items].sort((a, b) => {
|
|
3157
|
+
switch (sortType) {
|
|
3158
|
+
case "layer": {
|
|
3159
|
+
const la = a.node ? (a.node.layer || "UTIL") : "";
|
|
3160
|
+
const lb = b.node ? (b.node.layer || "UTIL") : "";
|
|
3161
|
+
return la.localeCompare(lb) || (a.name || "").localeCompare(b.name || "");
|
|
3162
|
+
}
|
|
3163
|
+
case "risk": {
|
|
3164
|
+
const order = { HIGH: 0, MEDIUM: 1, LOW: 2, NONE: 3 };
|
|
3165
|
+
const ra = a.node ? (order[a.node.riskLevel] ?? 3) : 3;
|
|
3166
|
+
const rb = b.node ? (order[b.node.riskLevel] ?? 3) : 3;
|
|
3167
|
+
return ra - rb || (a.name || "").localeCompare(b.name || "");
|
|
3168
|
+
}
|
|
3169
|
+
case "deps": {
|
|
3170
|
+
const da = a.node ? (a.node.dependentCount || 0) : 0;
|
|
3171
|
+
const db = b.node ? (b.node.dependentCount || 0) : 0;
|
|
3172
|
+
return db - da || (a.name || "").localeCompare(b.name || "");
|
|
3173
|
+
}
|
|
3174
|
+
case "modified": {
|
|
3175
|
+
const ma = a.path && fileTreeModified.has(a.path) ? fileTreeModified.get(a.path).timestamp : 0;
|
|
3176
|
+
const mb = b.path && fileTreeModified.has(b.path) ? fileTreeModified.get(b.path).timestamp : 0;
|
|
3177
|
+
return mb - ma || (a.name || "").localeCompare(b.name || "");
|
|
3178
|
+
}
|
|
3179
|
+
default: // name
|
|
3180
|
+
return (a.name || "").localeCompare(b.name || "");
|
|
3181
|
+
}
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
function renderFileTree() {
|
|
3186
|
+
if (!graphData || !graphData.nodes) return;
|
|
3187
|
+
buildFileTree(graphData.nodes);
|
|
3188
|
+
if (!fileTreeData) return;
|
|
3189
|
+
|
|
3190
|
+
const container = document.getElementById("tree-content");
|
|
3191
|
+
const countEl = document.getElementById("tree-file-count");
|
|
3192
|
+
if (!container) return;
|
|
3193
|
+
|
|
3194
|
+
const s = SETTINGS.fileTree;
|
|
3195
|
+
const filter = fileTreeFilter.toLowerCase();
|
|
3196
|
+
let totalFiles = 0;
|
|
3197
|
+
|
|
3198
|
+
// Nested rendering: each folder level produces a <div class="tree-group">
|
|
3199
|
+
function renderGroup(folder) {
|
|
3200
|
+
// Collect visible items (folders first, then files)
|
|
3201
|
+
const visibleFolders = Object.values(folder.children)
|
|
3202
|
+
.filter(c => !filter || folderMatchesFilter(c, filter))
|
|
3203
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
3204
|
+
|
|
3205
|
+
const visibleFiles = sortTreeItems(
|
|
3206
|
+
folder.files.filter(f => !filter || f.name.toLowerCase().includes(filter) || f.path.toLowerCase().includes(filter)),
|
|
3207
|
+
fileTreeSort
|
|
3208
|
+
);
|
|
3209
|
+
|
|
3210
|
+
const itemCount = visibleFolders.length + visibleFiles.length;
|
|
3211
|
+
if (itemCount === 0) return "";
|
|
3212
|
+
|
|
3213
|
+
const hasModified = folderHasModifiedChild(folder);
|
|
3214
|
+
const singleClass = itemCount === 1 ? " single-child" : "";
|
|
3215
|
+
const modGroupClass = hasModified ? " has-modified" : "";
|
|
3216
|
+
let html = `<div class="tree-group${singleClass}${modGroupClass}">`;
|
|
3217
|
+
|
|
3218
|
+
// ── Folders ──
|
|
3219
|
+
for (const child of visibleFolders) {
|
|
3220
|
+
const isOpen = fileTreeExpanded.has(child.path);
|
|
3221
|
+
const fileCount = countFilesRecursive(child);
|
|
3222
|
+
const hasModChild = folderHasModifiedChild(child);
|
|
3223
|
+
const compactClass = s.compactMode ? " compact" : "";
|
|
3224
|
+
const glowClass = hasModChild ? " folder-glow" : "";
|
|
3225
|
+
const col = "var(--accent)";
|
|
3226
|
+
|
|
3227
|
+
html += `<div class="tree-node${glowClass}${compactClass}" data-path="${child.path}" data-type="folder">`;
|
|
3228
|
+
html += `<div class="tree-dot" style="border-color:${col};box-shadow:0 0 8px rgba(0,212,255,0.4)"></div>`;
|
|
3229
|
+
html += `<span class="tree-toggle${isOpen ? " open" : ""}" data-path="${child.path}">▶</span>`;
|
|
3230
|
+
html += `<span class="tree-name folder-name">${escHtml(child.name)}</span>`;
|
|
3231
|
+
html += `<span class="tree-folder-count">${fileCount}</span>`;
|
|
3232
|
+
html += `</div>`;
|
|
3233
|
+
|
|
3234
|
+
// Render expanded children as nested tree-group
|
|
3235
|
+
if (isOpen) {
|
|
3236
|
+
html += renderGroup(child);
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
// ── Files ──
|
|
3241
|
+
for (const f of visibleFiles) {
|
|
3242
|
+
totalFiles++;
|
|
3243
|
+
const isSelected = selectedNodeId === f.node.id;
|
|
3244
|
+
const isModified = fileTreeModified.has(f.path);
|
|
3245
|
+
const modData = isModified ? fileTreeModified.get(f.path) : null;
|
|
3246
|
+
const col = LAYER_HEX[f.node.layer] || "#999";
|
|
3247
|
+
const compactClass = s.compactMode ? " compact" : "";
|
|
3248
|
+
const selClass = isSelected ? " selected" : "";
|
|
3249
|
+
const modClass = isModified ? " modified" : "";
|
|
3250
|
+
|
|
3251
|
+
html += `<div class="tree-node${selClass}${modClass}${compactClass}" style="font-size:${s.fontSize}px" data-path="${f.path}" data-node-id="${f.node.id}" data-type="file">`;
|
|
3252
|
+
html += `<div class="tree-dot" style="background:${col};border-color:${col};box-shadow:0 0 8px ${col}"></div>`;
|
|
3253
|
+
html += `<span class="tree-name">${escHtml(f.name)}</span>`;
|
|
3254
|
+
|
|
3255
|
+
// Badges
|
|
3256
|
+
if (isModified && modData) {
|
|
3257
|
+
const modLabel = modData.type === "added" ? "A" : modData.type === "deleted" ? "D" : "M";
|
|
3258
|
+
const modCls = modData.type === "added" ? "mod-A" : modData.type === "deleted" ? "mod-D" : "mod-M";
|
|
3259
|
+
html += `<span class="tree-mod-badge ${modCls}">${modLabel}</span>`;
|
|
3260
|
+
}
|
|
3261
|
+
if (s.showRisk && f.node.riskLevel && f.node.riskLevel !== "NONE") {
|
|
3262
|
+
html += `<span class="tree-badge badge-risk-${f.node.riskLevel}">${f.node.riskLevel[0]}</span>`;
|
|
3263
|
+
}
|
|
3264
|
+
if (s.showDeps && f.node.dependentCount > 0) {
|
|
3265
|
+
html += `<span class="tree-badge badge-deps">D${f.node.dependentCount}</span>`;
|
|
3266
|
+
}
|
|
3267
|
+
if (s.showLineCount && f.node.lineCount > 0) {
|
|
3268
|
+
html += `<span class="tree-badge badge-lines">L${f.node.lineCount}</span>`;
|
|
3269
|
+
}
|
|
3270
|
+
html += `</div>`;
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
html += `</div>`; // close tree-group
|
|
3274
|
+
return html;
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
container.innerHTML = renderGroup(fileTreeData);
|
|
3278
|
+
if (countEl) countEl.textContent = totalFiles;
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
function escHtml(s) {
|
|
3282
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
function renderFileTreeDebounced() {
|
|
3286
|
+
if (_fileTreeRenderTimer) clearTimeout(_fileTreeRenderTimer);
|
|
3287
|
+
_fileTreeRenderTimer = setTimeout(() => {
|
|
3288
|
+
renderFileTree();
|
|
3289
|
+
_fileTreeRenderTimer = null;
|
|
3290
|
+
}, 100);
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
function toggleFolder(path) {
|
|
3294
|
+
if (fileTreeExpanded.has(path)) fileTreeExpanded.delete(path);
|
|
3295
|
+
else fileTreeExpanded.add(path);
|
|
3296
|
+
renderFileTree();
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
function expandAllFolders() {
|
|
3300
|
+
if (!fileTreeData) return;
|
|
3301
|
+
function collectPaths(folder) {
|
|
3302
|
+
for (const child of Object.values(folder.children)) {
|
|
3303
|
+
fileTreeExpanded.add(child.path);
|
|
3304
|
+
collectPaths(child);
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
collectPaths(fileTreeData);
|
|
3308
|
+
renderFileTree();
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
function collapseAllFolders() {
|
|
3312
|
+
fileTreeExpanded.clear();
|
|
3313
|
+
renderFileTree();
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
function treeScrollToFile(nodeId) {
|
|
3317
|
+
if (!fileTreeData || !nodeId || _treeScrollLock) return;
|
|
3318
|
+
// Find file path and expand parent folders
|
|
3319
|
+
const node = graphData?.nodes.find(n => n.id === nodeId);
|
|
3320
|
+
if (!node) return;
|
|
3321
|
+
const fullPath = node.fullPath || nodeId;
|
|
3322
|
+
const parts = fullPath.split("/").filter(Boolean);
|
|
3323
|
+
|
|
3324
|
+
// Expand all parent folders
|
|
3325
|
+
for (let i = 1; i < parts.length; i++) {
|
|
3326
|
+
const folderPath = parts.slice(0, i).join("/");
|
|
3327
|
+
fileTreeExpanded.add(folderPath);
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
renderFileTree();
|
|
3331
|
+
|
|
3332
|
+
// Scroll to the file element
|
|
3333
|
+
requestAnimationFrame(() => {
|
|
3334
|
+
const container = document.getElementById("tree-scroll-container");
|
|
3335
|
+
const el = document.querySelector(`[data-node-id="${CSS.escape(nodeId)}"]`);
|
|
3336
|
+
if (el && container && SETTINGS.fileTree.autoScrollOnChange) {
|
|
3337
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
3338
|
+
}
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
function applyFileTreeSettings() {
|
|
3343
|
+
const panel = document.getElementById("file-tree-panel");
|
|
3344
|
+
if (panel) {
|
|
3345
|
+
const w = SETTINGS.fileTree.panelWidth;
|
|
3346
|
+
panel.style.flex = `0 0 ${w}px`;
|
|
3347
|
+
panel.style.width = `${w}px`;
|
|
3348
|
+
}
|
|
3349
|
+
updateDynamicRightOffsets();
|
|
3350
|
+
renderFileTree();
|
|
3351
|
+
if (Graph) {
|
|
3352
|
+
const gp = document.getElementById("graph-panel");
|
|
3353
|
+
if (gp) Graph.width(gp.clientWidth);
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
function setupFileTree() {
|
|
3358
|
+
const container = document.getElementById("tree-content");
|
|
3359
|
+
const searchInput = document.getElementById("tree-search-input");
|
|
3360
|
+
const sortBtn = document.getElementById("tree-sort-btn");
|
|
3361
|
+
const sortMenu = document.getElementById("tree-sort-menu");
|
|
3362
|
+
const collapseBtn = document.getElementById("tree-collapse-all");
|
|
3363
|
+
const scrollContainer = document.getElementById("tree-scroll-container");
|
|
3364
|
+
|
|
3365
|
+
if (!container) return;
|
|
3366
|
+
|
|
3367
|
+
// Click delegation for tree nodes
|
|
3368
|
+
container.addEventListener("click", (e) => {
|
|
3369
|
+
const node = e.target.closest(".tree-node");
|
|
3370
|
+
if (!node) return;
|
|
3371
|
+
|
|
3372
|
+
if (node.dataset.type === "folder") {
|
|
3373
|
+
toggleFolder(node.dataset.path);
|
|
3374
|
+
return;
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
// File click → select in 3D graph
|
|
3378
|
+
const nodeId = node.dataset.nodeId;
|
|
3379
|
+
if (nodeId && graphData) {
|
|
3380
|
+
const gNode = graphData.nodes.find(n => n.id === nodeId);
|
|
3381
|
+
if (gNode) {
|
|
3382
|
+
handleNodeClick(gNode);
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
});
|
|
3386
|
+
|
|
3387
|
+
// Toggle arrow click
|
|
3388
|
+
container.addEventListener("click", (e) => {
|
|
3389
|
+
const toggle = e.target.closest(".tree-toggle");
|
|
3390
|
+
if (toggle) {
|
|
3391
|
+
e.stopPropagation();
|
|
3392
|
+
toggleFolder(toggle.dataset.path);
|
|
3393
|
+
}
|
|
3394
|
+
});
|
|
3395
|
+
|
|
3396
|
+
// Hover → highlight 3D node
|
|
3397
|
+
container.addEventListener("mouseenter", (e) => {
|
|
3398
|
+
const node = e.target.closest(".tree-node[data-type='file']");
|
|
3399
|
+
if (!node || !Graph) return;
|
|
3400
|
+
const nodeId = node.dataset.nodeId;
|
|
3401
|
+
if (nodeId) {
|
|
3402
|
+
highlightNodes.add(nodeId);
|
|
3403
|
+
refreshGraph();
|
|
3404
|
+
}
|
|
3405
|
+
}, true);
|
|
3406
|
+
|
|
3407
|
+
container.addEventListener("mouseleave", (e) => {
|
|
3408
|
+
const node = e.target.closest(".tree-node[data-type='file']");
|
|
3409
|
+
if (!node || !Graph) return;
|
|
3410
|
+
const nodeId = node.dataset.nodeId;
|
|
3411
|
+
if (nodeId && nodeId !== selectedNodeId) {
|
|
3412
|
+
highlightNodes.delete(nodeId);
|
|
3413
|
+
refreshGraph();
|
|
3414
|
+
}
|
|
3415
|
+
}, true);
|
|
3416
|
+
|
|
3417
|
+
// Search filter
|
|
3418
|
+
if (searchInput) {
|
|
3419
|
+
searchInput.addEventListener("input", () => {
|
|
3420
|
+
fileTreeFilter = searchInput.value.trim();
|
|
3421
|
+
// Auto-expand when filtering
|
|
3422
|
+
if (fileTreeFilter && fileTreeData) expandAllFolders();
|
|
3423
|
+
else renderFileTree();
|
|
3424
|
+
});
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
// Sort button + menu
|
|
3428
|
+
if (sortBtn && sortMenu) {
|
|
3429
|
+
sortBtn.addEventListener("click", (e) => {
|
|
3430
|
+
e.stopPropagation();
|
|
3431
|
+
sortMenu.classList.toggle("hidden");
|
|
3432
|
+
});
|
|
3433
|
+
|
|
3434
|
+
sortMenu.querySelectorAll(".tree-sort-option").forEach(opt => {
|
|
3435
|
+
opt.addEventListener("click", (e) => {
|
|
3436
|
+
e.stopPropagation();
|
|
3437
|
+
fileTreeSort = opt.dataset.sort;
|
|
3438
|
+
sortMenu.querySelectorAll(".tree-sort-option").forEach(o => o.classList.remove("active"));
|
|
3439
|
+
opt.classList.add("active");
|
|
3440
|
+
sortMenu.classList.add("hidden");
|
|
3441
|
+
renderFileTree();
|
|
3442
|
+
});
|
|
3443
|
+
});
|
|
3444
|
+
|
|
3445
|
+
// Close sort menu on outside click
|
|
3446
|
+
document.addEventListener("click", () => {
|
|
3447
|
+
if (sortMenu) sortMenu.classList.add("hidden");
|
|
3448
|
+
});
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
// Collapse/expand all toggle
|
|
3452
|
+
let allExpanded = false;
|
|
3453
|
+
if (collapseBtn) {
|
|
3454
|
+
collapseBtn.addEventListener("click", () => {
|
|
3455
|
+
if (allExpanded) { collapseAllFolders(); collapseBtn.innerHTML = "▼"; }
|
|
3456
|
+
else { expandAllFolders(); collapseBtn.innerHTML = "▲"; }
|
|
3457
|
+
allExpanded = !allExpanded;
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
// Scroll lock: disable auto-scroll when user manually scrolls
|
|
3462
|
+
if (scrollContainer) {
|
|
3463
|
+
scrollContainer.addEventListener("scroll", () => {
|
|
3464
|
+
_treeScrollLock = true;
|
|
3465
|
+
if (_treeScrollLockTimer) clearTimeout(_treeScrollLockTimer);
|
|
3466
|
+
_treeScrollLockTimer = setTimeout(() => { _treeScrollLock = false; }, 3000);
|
|
3467
|
+
}, { passive: true });
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
// Apply initial settings
|
|
3471
|
+
applyFileTreeSettings();
|
|
3472
|
+
updateDynamicRightOffsets();
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
function setupTreeResizeHandle() {
|
|
3476
|
+
const handle = document.getElementById("tree-resize-handle");
|
|
3477
|
+
const panel = document.getElementById("file-tree-panel");
|
|
3478
|
+
if (!handle || !panel) return;
|
|
3479
|
+
|
|
3480
|
+
let startX = 0;
|
|
3481
|
+
let startW = 0;
|
|
3482
|
+
|
|
3483
|
+
function onMove(e) {
|
|
3484
|
+
e.preventDefault();
|
|
3485
|
+
const dx = e.clientX - startX;
|
|
3486
|
+
const newW = Math.max(200, Math.min(500, startW + dx));
|
|
3487
|
+
panel.style.flex = "0 0 " + newW + "px";
|
|
3488
|
+
panel.style.width = newW + "px";
|
|
3489
|
+
SETTINGS.fileTree.panelWidth = newW;
|
|
3490
|
+
updateDynamicRightOffsets();
|
|
3491
|
+
if (Graph) {
|
|
3492
|
+
const gp = document.getElementById("graph-panel");
|
|
3493
|
+
if (gp) Graph.width(gp.clientWidth);
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
function onUp() {
|
|
3498
|
+
window.removeEventListener("mousemove", onMove, true);
|
|
3499
|
+
window.removeEventListener("mouseup", onUp, true);
|
|
3500
|
+
handle.classList.remove("dragging");
|
|
3501
|
+
document.body.style.cursor = "";
|
|
3502
|
+
document.body.style.userSelect = "";
|
|
3503
|
+
const overlay = document.getElementById("tree-resize-overlay");
|
|
3504
|
+
if (overlay) overlay.remove();
|
|
3505
|
+
saveSettings();
|
|
3506
|
+
if (Graph) {
|
|
3507
|
+
const gp = document.getElementById("graph-panel");
|
|
3508
|
+
if (gp) Graph.width(gp.clientWidth);
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
handle.addEventListener("mousedown", (e) => {
|
|
3513
|
+
e.preventDefault();
|
|
3514
|
+
e.stopPropagation();
|
|
3515
|
+
startX = e.clientX;
|
|
3516
|
+
startW = panel.offsetWidth;
|
|
3517
|
+
handle.classList.add("dragging");
|
|
3518
|
+
document.body.style.cursor = "col-resize";
|
|
3519
|
+
document.body.style.userSelect = "none";
|
|
3520
|
+
const overlay = document.createElement("div");
|
|
3521
|
+
overlay.id = "tree-resize-overlay";
|
|
3522
|
+
overlay.style.cssText = "position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:9999;cursor:col-resize;background:transparent;";
|
|
3523
|
+
document.body.appendChild(overlay);
|
|
3524
|
+
window.addEventListener("mousemove", onMove, true);
|
|
3525
|
+
window.addEventListener("mouseup", onUp, true);
|
|
3526
|
+
});
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
// ═══════════════════════════════════════════
|
|
3530
|
+
// RESIZE HANDLE (Right Panel)
|
|
3531
|
+
// ═══════════════════════════════════════════
|
|
2983
3532
|
function setupResizeHandle() {
|
|
2984
3533
|
const handle = document.getElementById("resize-handle");
|
|
2985
3534
|
const panel = document.getElementById("right-panel");
|
|
@@ -2995,7 +3544,7 @@ function setupResizeHandle() {
|
|
|
2995
3544
|
const newW = Math.max(250, Math.min(900, startW + dx));
|
|
2996
3545
|
panel.style.flex = "0 0 " + newW + "px";
|
|
2997
3546
|
panel.style.width = newW + "px";
|
|
2998
|
-
|
|
3547
|
+
updateDynamicRightOffsets();
|
|
2999
3548
|
if (Graph) {
|
|
3000
3549
|
const gp = document.getElementById("graph-panel");
|
|
3001
3550
|
if (gp) Graph.width(gp.clientWidth);
|
|
@@ -3045,21 +3594,14 @@ function updateLicenseBadge(plan, expiresAt) {
|
|
|
3045
3594
|
|
|
3046
3595
|
badge.className = "license-badge";
|
|
3047
3596
|
|
|
3048
|
-
if (plan === "pro"
|
|
3049
|
-
|
|
3050
|
-
if (diffMs <= 0) {
|
|
3597
|
+
if (plan === "pro") {
|
|
3598
|
+
if (expiresAt && new Date(expiresAt) < new Date()) {
|
|
3051
3599
|
badge.classList.add("expired");
|
|
3052
3600
|
badge.textContent = "EXPIRED";
|
|
3053
|
-
} else if (Math.ceil(diffMs / (1000 * 60 * 60 * 24)) <= 14) {
|
|
3054
|
-
badge.classList.add("trial");
|
|
3055
|
-
badge.textContent = "TRIAL";
|
|
3056
3601
|
} else {
|
|
3057
3602
|
badge.classList.add("pro");
|
|
3058
3603
|
badge.textContent = "PRO";
|
|
3059
3604
|
}
|
|
3060
|
-
} else if (plan === "pro") {
|
|
3061
|
-
badge.classList.add("pro");
|
|
3062
|
-
badge.textContent = "PRO";
|
|
3063
3605
|
} else {
|
|
3064
3606
|
badge.classList.add("free");
|
|
3065
3607
|
badge.textContent = "FREE";
|