@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.
@@ -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
- el.addEventListener("input", () => {
275
- const v = type === "color" ? el.value : parseFloat(el.value);
276
- SETTINGS[group][key] = v;
277
- if (valEl) valEl.textContent = formatSetVal(v, type);
278
- applySettings(group);
279
- saveSettings();
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.value = SETTINGS[group][key];
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.value = SETTINGS[group][key];
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.value = SETTINGS[group][key];
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(window.innerWidth - 380)
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(window.innerWidth - 380).height(window.innerHeight - 100);
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 Right Panel ──
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}">&#9654;</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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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 = "&#9660;"; }
3456
+ else { expandAllFolders(); collapseBtn.innerHTML = "&#9650;"; }
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
- if (hub) hub.style.right = newW + "px";
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" && expiresAt) {
3049
- const diffMs = new Date(expiresAt) - new Date();
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";