@syke1/mcp-server 1.4.0 → 1.4.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.
@@ -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,8 @@ document.addEventListener("DOMContentLoaded", async () => {
417
463
  setupTabs();
418
464
  setupSettings();
419
465
  setupProjectModal();
466
+ setupLicenseModal();
467
+ setupFileTree();
420
468
  initSSE();
421
469
  startHealthCheck();
422
470
  });
@@ -548,6 +596,7 @@ async function loadGraph() {
548
596
  if (isReload) {
549
597
  Graph.graphData(graphData);
550
598
  buildLegend(layerCounts);
599
+ renderFileTreeDebounced();
551
600
  console.log("[SYKE] Graph updated (reload), birth animations:", birthAnimations.size);
552
601
 
553
602
  // ── Star birth camera choreography ──
@@ -595,7 +644,7 @@ async function loadGraph() {
595
644
  const container = document.getElementById("3d-graph");
596
645
 
597
646
  Graph = ForceGraph3D()(container)
598
- .width(window.innerWidth - 380)
647
+ .width(getGraphPanelWidth())
599
648
  .height(window.innerHeight - 100)
600
649
  .graphData(graphData)
601
650
  .backgroundColor(SETTINGS.scene.background)
@@ -757,10 +806,11 @@ async function loadGraph() {
757
806
  container.addEventListener("touchstart", stopUser);
758
807
 
759
808
  window.addEventListener("resize", () => {
760
- if (Graph) Graph.width(window.innerWidth - 380).height(window.innerHeight - 100);
809
+ if (Graph) Graph.width(getGraphPanelWidth()).height(window.innerHeight - 100);
761
810
  });
762
811
 
763
812
  buildLegend(layerCounts);
813
+ renderFileTreeDebounced();
764
814
  createNodeLabels();
765
815
  updateLabelsLoop();
766
816
 
@@ -1141,6 +1191,8 @@ async function handleNodeClick(node) {
1141
1191
  loadSimulation(node.id);
1142
1192
  // Start Star Wars code crawl
1143
1193
  startCodeCrawl(node.id);
1194
+ // Sync file tree selection
1195
+ treeScrollToFile(node.id);
1144
1196
  }
1145
1197
 
1146
1198
  function handleBackgroundClick() {
@@ -1844,6 +1896,9 @@ function setupKeyboardShortcuts() {
1844
1896
  case "s":
1845
1897
  if (selectedFile) { loadSimulation(selectedFile); switchTab("simulate"); }
1846
1898
  break;
1899
+ case "t":
1900
+ toggleFileTreePanel();
1901
+ break;
1847
1902
  case "d":
1848
1903
  detectCycles();
1849
1904
  break;
@@ -2059,8 +2114,9 @@ function setupEventListeners() {
2059
2114
  });
2060
2115
  });
2061
2116
 
2062
- // ── Resizable Right Panel ──
2117
+ // ── Resizable Panels ──
2063
2118
  setupResizeHandle();
2119
+ setupTreeResizeHandle();
2064
2120
  }
2065
2121
 
2066
2122
  // ═══════════════════════════════════════════
@@ -2707,6 +2763,16 @@ async function initSSE() {
2707
2763
  const data = JSON.parse(e.data);
2708
2764
  console.log("[SYKE:SSE] File change:", data.file, data.type, data.diffCount, "diffs");
2709
2765
 
2766
+ // ── 0. Update file tree modification tracking ──
2767
+ fileTreeModified.set(data.file, { type: data.type, timestamp: Date.now() });
2768
+ renderFileTreeDebounced();
2769
+ // Auto-clear pulse after pulseDuration
2770
+ const pulseDur = (SETTINGS.fileTree.pulseDuration || 5) * 1000;
2771
+ setTimeout(() => {
2772
+ fileTreeModified.delete(data.file);
2773
+ renderFileTreeDebounced();
2774
+ }, pulseDur);
2775
+
2710
2776
  // ── 1. Auto-select the modified node in 3D graph ──
2711
2777
  selectedFile = data.file;
2712
2778
  selectedNodeId = data.file;
@@ -2861,6 +2927,7 @@ async function initSSE() {
2861
2927
  setTimeout(async () => {
2862
2928
  await loadGraph();
2863
2929
  await loadHubFiles();
2930
+ renderFileTreeDebounced();
2864
2931
  }, 1000);
2865
2932
  });
2866
2933
 
@@ -2980,6 +3047,489 @@ function renderRealtimePanel() {
2980
3047
  // ═══════════════════════════════════════════
2981
3048
  // RESIZABLE RIGHT PANEL
2982
3049
  // ═══════════════════════════════════════════
3050
+ // ═══════════════════════════════════════════
3051
+ // FILE TREE PANEL
3052
+ // ═══════════════════════════════════════════
3053
+
3054
+ function getGraphPanelWidth() {
3055
+ const rightPanel = document.getElementById("right-panel");
3056
+ const treePanel = document.getElementById("file-tree-panel");
3057
+ const rw = rightPanel ? rightPanel.offsetWidth : 380;
3058
+ const tw = (treePanel && !treePanel.classList.contains("hidden")) ? treePanel.offsetWidth : 0;
3059
+ // 8px per resize handle (tree + right)
3060
+ return window.innerWidth - rw - tw - 16;
3061
+ }
3062
+
3063
+ function updateDynamicRightOffsets() {
3064
+ const rightPanel = document.getElementById("right-panel");
3065
+ const treePanel = document.getElementById("file-tree-panel");
3066
+ const rw = rightPanel ? rightPanel.offsetWidth : 380;
3067
+ const tw = (treePanel && !treePanel.classList.contains("hidden")) ? treePanel.offsetWidth : 0;
3068
+ const total = rw + tw + 16; // 2 resize handles × 8px
3069
+ document.documentElement.style.setProperty("--right-offset", total + "px");
3070
+ }
3071
+
3072
+ function toggleFileTreePanel() {
3073
+ const panel = document.getElementById("file-tree-panel");
3074
+ const treeResize = document.getElementById("tree-resize-handle");
3075
+ if (!panel) return;
3076
+ fileTreeVisible = !fileTreeVisible;
3077
+ panel.classList.toggle("hidden", !fileTreeVisible);
3078
+ if (treeResize) treeResize.style.display = fileTreeVisible ? "" : "none";
3079
+ updateDynamicRightOffsets();
3080
+ if (Graph) {
3081
+ const gp = document.getElementById("graph-panel");
3082
+ if (gp) Graph.width(gp.clientWidth);
3083
+ }
3084
+ }
3085
+
3086
+ function buildFileTree(nodes) {
3087
+ if (!nodes || !nodes.length) { fileTreeData = null; return; }
3088
+ const root = { name: "", children: {}, files: [], path: "" };
3089
+
3090
+ nodes.forEach(n => {
3091
+ const parts = (n.fullPath || n.id).split("/").filter(Boolean);
3092
+ let current = root;
3093
+ for (let i = 0; i < parts.length - 1; i++) {
3094
+ const folderName = parts[i];
3095
+ const folderPath = parts.slice(0, i + 1).join("/");
3096
+ if (!current.children[folderName]) {
3097
+ current.children[folderName] = { name: folderName, children: {}, files: [], path: folderPath };
3098
+ }
3099
+ current = current.children[folderName];
3100
+ }
3101
+ current.files.push({
3102
+ name: parts[parts.length - 1] || n.label,
3103
+ node: n,
3104
+ path: n.fullPath || n.id,
3105
+ });
3106
+ });
3107
+
3108
+ fileTreeData = root;
3109
+ }
3110
+
3111
+ function countFilesRecursive(folder) {
3112
+ let count = folder.files.length;
3113
+ for (const child of Object.values(folder.children)) {
3114
+ count += countFilesRecursive(child);
3115
+ }
3116
+ return count;
3117
+ }
3118
+
3119
+ function getAggregateRisk(folder) {
3120
+ const order = ["HIGH", "MEDIUM", "LOW", "NONE"];
3121
+ let highest = 3; // NONE
3122
+ for (const f of folder.files) {
3123
+ const idx = order.indexOf(f.node.riskLevel);
3124
+ if (idx >= 0 && idx < highest) highest = idx;
3125
+ }
3126
+ for (const child of Object.values(folder.children)) {
3127
+ const childRisk = getAggregateRisk(child);
3128
+ const idx = order.indexOf(childRisk);
3129
+ if (idx >= 0 && idx < highest) highest = idx;
3130
+ }
3131
+ return order[highest];
3132
+ }
3133
+
3134
+ function folderHasModifiedChild(folder) {
3135
+ for (const f of folder.files) {
3136
+ if (fileTreeModified.has(f.path)) return true;
3137
+ }
3138
+ for (const child of Object.values(folder.children)) {
3139
+ if (folderHasModifiedChild(child)) return true;
3140
+ }
3141
+ return false;
3142
+ }
3143
+
3144
+ function folderMatchesFilter(folder, filter) {
3145
+ if (!filter) return true;
3146
+ const fl = filter.toLowerCase();
3147
+ for (const f of folder.files) {
3148
+ if (f.name.toLowerCase().includes(fl) || f.path.toLowerCase().includes(fl)) return true;
3149
+ }
3150
+ for (const child of Object.values(folder.children)) {
3151
+ if (child.name.toLowerCase().includes(fl) || folderMatchesFilter(child, filter)) return true;
3152
+ }
3153
+ return false;
3154
+ }
3155
+
3156
+ function sortTreeItems(items, sortType) {
3157
+ return [...items].sort((a, b) => {
3158
+ switch (sortType) {
3159
+ case "layer": {
3160
+ const la = a.node ? (a.node.layer || "UTIL") : "";
3161
+ const lb = b.node ? (b.node.layer || "UTIL") : "";
3162
+ return la.localeCompare(lb) || (a.name || "").localeCompare(b.name || "");
3163
+ }
3164
+ case "risk": {
3165
+ const order = { HIGH: 0, MEDIUM: 1, LOW: 2, NONE: 3 };
3166
+ const ra = a.node ? (order[a.node.riskLevel] ?? 3) : 3;
3167
+ const rb = b.node ? (order[b.node.riskLevel] ?? 3) : 3;
3168
+ return ra - rb || (a.name || "").localeCompare(b.name || "");
3169
+ }
3170
+ case "deps": {
3171
+ const da = a.node ? (a.node.dependentCount || 0) : 0;
3172
+ const db = b.node ? (b.node.dependentCount || 0) : 0;
3173
+ return db - da || (a.name || "").localeCompare(b.name || "");
3174
+ }
3175
+ case "modified": {
3176
+ const ma = a.path && fileTreeModified.has(a.path) ? fileTreeModified.get(a.path).timestamp : 0;
3177
+ const mb = b.path && fileTreeModified.has(b.path) ? fileTreeModified.get(b.path).timestamp : 0;
3178
+ return mb - ma || (a.name || "").localeCompare(b.name || "");
3179
+ }
3180
+ default: // name
3181
+ return (a.name || "").localeCompare(b.name || "");
3182
+ }
3183
+ });
3184
+ }
3185
+
3186
+ function renderFileTree() {
3187
+ if (!graphData || !graphData.nodes) return;
3188
+ buildFileTree(graphData.nodes);
3189
+ if (!fileTreeData) return;
3190
+
3191
+ const container = document.getElementById("tree-content");
3192
+ const countEl = document.getElementById("tree-file-count");
3193
+ if (!container) return;
3194
+
3195
+ const s = SETTINGS.fileTree;
3196
+ const filter = fileTreeFilter.toLowerCase();
3197
+ let totalFiles = 0;
3198
+
3199
+ // Nested rendering: each folder level produces a <div class="tree-group">
3200
+ function renderGroup(folder) {
3201
+ // Collect visible items (folders first, then files)
3202
+ const visibleFolders = Object.values(folder.children)
3203
+ .filter(c => !filter || folderMatchesFilter(c, filter))
3204
+ .sort((a, b) => a.name.localeCompare(b.name));
3205
+
3206
+ const visibleFiles = sortTreeItems(
3207
+ folder.files.filter(f => !filter || f.name.toLowerCase().includes(filter) || f.path.toLowerCase().includes(filter)),
3208
+ fileTreeSort
3209
+ );
3210
+
3211
+ const itemCount = visibleFolders.length + visibleFiles.length;
3212
+ if (itemCount === 0) return "";
3213
+
3214
+ const hasModified = folderHasModifiedChild(folder);
3215
+ const singleClass = itemCount === 1 ? " single-child" : "";
3216
+ const modGroupClass = hasModified ? " has-modified" : "";
3217
+ let html = `<div class="tree-group${singleClass}${modGroupClass}">`;
3218
+
3219
+ // ── Folders ──
3220
+ for (const child of visibleFolders) {
3221
+ const isOpen = fileTreeExpanded.has(child.path);
3222
+ const fileCount = countFilesRecursive(child);
3223
+ const hasModChild = folderHasModifiedChild(child);
3224
+ const compactClass = s.compactMode ? " compact" : "";
3225
+ const glowClass = hasModChild ? " folder-glow" : "";
3226
+ const col = "var(--accent)";
3227
+
3228
+ html += `<div class="tree-node${glowClass}${compactClass}" data-path="${child.path}" data-type="folder">`;
3229
+ html += `<div class="tree-dot" style="border-color:${col};box-shadow:0 0 8px rgba(0,212,255,0.4)"></div>`;
3230
+ html += `<span class="tree-toggle${isOpen ? " open" : ""}" data-path="${child.path}">&#9654;</span>`;
3231
+ html += `<span class="tree-name folder-name">${escHtml(child.name)}</span>`;
3232
+ html += `<span class="tree-folder-count">${fileCount}</span>`;
3233
+ html += `</div>`;
3234
+
3235
+ // Render expanded children as nested tree-group
3236
+ if (isOpen) {
3237
+ html += renderGroup(child);
3238
+ }
3239
+ }
3240
+
3241
+ // ── Files ──
3242
+ for (const f of visibleFiles) {
3243
+ totalFiles++;
3244
+ const isSelected = selectedNodeId === f.node.id;
3245
+ const isModified = fileTreeModified.has(f.path);
3246
+ const modData = isModified ? fileTreeModified.get(f.path) : null;
3247
+ const col = LAYER_HEX[f.node.layer] || "#999";
3248
+ const compactClass = s.compactMode ? " compact" : "";
3249
+ const selClass = isSelected ? " selected" : "";
3250
+ const modClass = isModified ? " modified" : "";
3251
+
3252
+ 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">`;
3253
+ html += `<div class="tree-dot" style="background:${col};border-color:${col};box-shadow:0 0 8px ${col}"></div>`;
3254
+ html += `<span class="tree-name">${escHtml(f.name)}</span>`;
3255
+
3256
+ // Badges
3257
+ if (isModified && modData) {
3258
+ const modLabel = modData.type === "added" ? "A" : modData.type === "deleted" ? "D" : "M";
3259
+ const modCls = modData.type === "added" ? "mod-A" : modData.type === "deleted" ? "mod-D" : "mod-M";
3260
+ html += `<span class="tree-mod-badge ${modCls}">${modLabel}</span>`;
3261
+ }
3262
+ if (s.showRisk && f.node.riskLevel && f.node.riskLevel !== "NONE") {
3263
+ html += `<span class="tree-badge badge-risk-${f.node.riskLevel}">${f.node.riskLevel[0]}</span>`;
3264
+ }
3265
+ if (s.showDeps && f.node.dependentCount > 0) {
3266
+ html += `<span class="tree-badge badge-deps">D${f.node.dependentCount}</span>`;
3267
+ }
3268
+ if (s.showLineCount && f.node.lineCount > 0) {
3269
+ html += `<span class="tree-badge badge-lines">L${f.node.lineCount}</span>`;
3270
+ }
3271
+ html += `</div>`;
3272
+ }
3273
+
3274
+ html += `</div>`; // close tree-group
3275
+ return html;
3276
+ }
3277
+
3278
+ container.innerHTML = renderGroup(fileTreeData);
3279
+ if (countEl) countEl.textContent = totalFiles;
3280
+ }
3281
+
3282
+ function escHtml(s) {
3283
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3284
+ }
3285
+
3286
+ function renderFileTreeDebounced() {
3287
+ if (_fileTreeRenderTimer) clearTimeout(_fileTreeRenderTimer);
3288
+ _fileTreeRenderTimer = setTimeout(() => {
3289
+ renderFileTree();
3290
+ _fileTreeRenderTimer = null;
3291
+ }, 100);
3292
+ }
3293
+
3294
+ function toggleFolder(path) {
3295
+ if (fileTreeExpanded.has(path)) fileTreeExpanded.delete(path);
3296
+ else fileTreeExpanded.add(path);
3297
+ renderFileTree();
3298
+ }
3299
+
3300
+ function expandAllFolders() {
3301
+ if (!fileTreeData) return;
3302
+ function collectPaths(folder) {
3303
+ for (const child of Object.values(folder.children)) {
3304
+ fileTreeExpanded.add(child.path);
3305
+ collectPaths(child);
3306
+ }
3307
+ }
3308
+ collectPaths(fileTreeData);
3309
+ renderFileTree();
3310
+ }
3311
+
3312
+ function collapseAllFolders() {
3313
+ fileTreeExpanded.clear();
3314
+ renderFileTree();
3315
+ }
3316
+
3317
+ function treeScrollToFile(nodeId) {
3318
+ if (!fileTreeData || !nodeId || _treeScrollLock) return;
3319
+ // Find file path and expand parent folders
3320
+ const node = graphData?.nodes.find(n => n.id === nodeId);
3321
+ if (!node) return;
3322
+ const fullPath = node.fullPath || nodeId;
3323
+ const parts = fullPath.split("/").filter(Boolean);
3324
+
3325
+ // Expand all parent folders
3326
+ for (let i = 1; i < parts.length; i++) {
3327
+ const folderPath = parts.slice(0, i).join("/");
3328
+ fileTreeExpanded.add(folderPath);
3329
+ }
3330
+
3331
+ renderFileTree();
3332
+
3333
+ // Scroll to the file element
3334
+ requestAnimationFrame(() => {
3335
+ const container = document.getElementById("tree-scroll-container");
3336
+ const el = document.querySelector(`[data-node-id="${CSS.escape(nodeId)}"]`);
3337
+ if (el && container && SETTINGS.fileTree.autoScrollOnChange) {
3338
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
3339
+ }
3340
+ });
3341
+ }
3342
+
3343
+ function applyFileTreeSettings() {
3344
+ const panel = document.getElementById("file-tree-panel");
3345
+ if (panel) {
3346
+ const w = SETTINGS.fileTree.panelWidth;
3347
+ panel.style.flex = `0 0 ${w}px`;
3348
+ panel.style.width = `${w}px`;
3349
+ }
3350
+ updateDynamicRightOffsets();
3351
+ renderFileTree();
3352
+ if (Graph) {
3353
+ const gp = document.getElementById("graph-panel");
3354
+ if (gp) Graph.width(gp.clientWidth);
3355
+ }
3356
+ }
3357
+
3358
+ function setupFileTree() {
3359
+ const container = document.getElementById("tree-content");
3360
+ const searchInput = document.getElementById("tree-search-input");
3361
+ const sortBtn = document.getElementById("tree-sort-btn");
3362
+ const sortMenu = document.getElementById("tree-sort-menu");
3363
+ const collapseBtn = document.getElementById("tree-collapse-all");
3364
+ const scrollContainer = document.getElementById("tree-scroll-container");
3365
+
3366
+ if (!container) return;
3367
+
3368
+ // Click delegation for tree nodes
3369
+ container.addEventListener("click", (e) => {
3370
+ const node = e.target.closest(".tree-node");
3371
+ if (!node) return;
3372
+
3373
+ if (node.dataset.type === "folder") {
3374
+ toggleFolder(node.dataset.path);
3375
+ return;
3376
+ }
3377
+
3378
+ // File click → select in 3D graph
3379
+ const nodeId = node.dataset.nodeId;
3380
+ if (nodeId && graphData) {
3381
+ const gNode = graphData.nodes.find(n => n.id === nodeId);
3382
+ if (gNode) {
3383
+ handleNodeClick(gNode);
3384
+ }
3385
+ }
3386
+ });
3387
+
3388
+ // Toggle arrow click
3389
+ container.addEventListener("click", (e) => {
3390
+ const toggle = e.target.closest(".tree-toggle");
3391
+ if (toggle) {
3392
+ e.stopPropagation();
3393
+ toggleFolder(toggle.dataset.path);
3394
+ }
3395
+ });
3396
+
3397
+ // Hover → highlight 3D node
3398
+ container.addEventListener("mouseenter", (e) => {
3399
+ const node = e.target.closest(".tree-node[data-type='file']");
3400
+ if (!node || !Graph) return;
3401
+ const nodeId = node.dataset.nodeId;
3402
+ if (nodeId) {
3403
+ highlightNodes.add(nodeId);
3404
+ refreshGraph();
3405
+ }
3406
+ }, true);
3407
+
3408
+ container.addEventListener("mouseleave", (e) => {
3409
+ const node = e.target.closest(".tree-node[data-type='file']");
3410
+ if (!node || !Graph) return;
3411
+ const nodeId = node.dataset.nodeId;
3412
+ if (nodeId && nodeId !== selectedNodeId) {
3413
+ highlightNodes.delete(nodeId);
3414
+ refreshGraph();
3415
+ }
3416
+ }, true);
3417
+
3418
+ // Search filter
3419
+ if (searchInput) {
3420
+ searchInput.addEventListener("input", () => {
3421
+ fileTreeFilter = searchInput.value.trim();
3422
+ // Auto-expand when filtering
3423
+ if (fileTreeFilter && fileTreeData) expandAllFolders();
3424
+ else renderFileTree();
3425
+ });
3426
+ }
3427
+
3428
+ // Sort button + menu
3429
+ if (sortBtn && sortMenu) {
3430
+ sortBtn.addEventListener("click", (e) => {
3431
+ e.stopPropagation();
3432
+ sortMenu.classList.toggle("hidden");
3433
+ });
3434
+
3435
+ sortMenu.querySelectorAll(".tree-sort-option").forEach(opt => {
3436
+ opt.addEventListener("click", (e) => {
3437
+ e.stopPropagation();
3438
+ fileTreeSort = opt.dataset.sort;
3439
+ sortMenu.querySelectorAll(".tree-sort-option").forEach(o => o.classList.remove("active"));
3440
+ opt.classList.add("active");
3441
+ sortMenu.classList.add("hidden");
3442
+ renderFileTree();
3443
+ });
3444
+ });
3445
+
3446
+ // Close sort menu on outside click
3447
+ document.addEventListener("click", () => {
3448
+ if (sortMenu) sortMenu.classList.add("hidden");
3449
+ });
3450
+ }
3451
+
3452
+ // Collapse/expand all toggle
3453
+ let allExpanded = false;
3454
+ if (collapseBtn) {
3455
+ collapseBtn.addEventListener("click", () => {
3456
+ if (allExpanded) { collapseAllFolders(); collapseBtn.innerHTML = "&#9660;"; }
3457
+ else { expandAllFolders(); collapseBtn.innerHTML = "&#9650;"; }
3458
+ allExpanded = !allExpanded;
3459
+ });
3460
+ }
3461
+
3462
+ // Scroll lock: disable auto-scroll when user manually scrolls
3463
+ if (scrollContainer) {
3464
+ scrollContainer.addEventListener("scroll", () => {
3465
+ _treeScrollLock = true;
3466
+ if (_treeScrollLockTimer) clearTimeout(_treeScrollLockTimer);
3467
+ _treeScrollLockTimer = setTimeout(() => { _treeScrollLock = false; }, 3000);
3468
+ }, { passive: true });
3469
+ }
3470
+
3471
+ // Apply initial settings
3472
+ applyFileTreeSettings();
3473
+ updateDynamicRightOffsets();
3474
+ }
3475
+
3476
+ function setupTreeResizeHandle() {
3477
+ const handle = document.getElementById("tree-resize-handle");
3478
+ const panel = document.getElementById("file-tree-panel");
3479
+ if (!handle || !panel) return;
3480
+
3481
+ let startX = 0;
3482
+ let startW = 0;
3483
+
3484
+ function onMove(e) {
3485
+ e.preventDefault();
3486
+ const dx = e.clientX - startX;
3487
+ const newW = Math.max(200, Math.min(500, startW + dx));
3488
+ panel.style.flex = "0 0 " + newW + "px";
3489
+ panel.style.width = newW + "px";
3490
+ SETTINGS.fileTree.panelWidth = newW;
3491
+ updateDynamicRightOffsets();
3492
+ if (Graph) {
3493
+ const gp = document.getElementById("graph-panel");
3494
+ if (gp) Graph.width(gp.clientWidth);
3495
+ }
3496
+ }
3497
+
3498
+ function onUp() {
3499
+ window.removeEventListener("mousemove", onMove, true);
3500
+ window.removeEventListener("mouseup", onUp, true);
3501
+ handle.classList.remove("dragging");
3502
+ document.body.style.cursor = "";
3503
+ document.body.style.userSelect = "";
3504
+ const overlay = document.getElementById("tree-resize-overlay");
3505
+ if (overlay) overlay.remove();
3506
+ saveSettings();
3507
+ if (Graph) {
3508
+ const gp = document.getElementById("graph-panel");
3509
+ if (gp) Graph.width(gp.clientWidth);
3510
+ }
3511
+ }
3512
+
3513
+ handle.addEventListener("mousedown", (e) => {
3514
+ e.preventDefault();
3515
+ e.stopPropagation();
3516
+ startX = e.clientX;
3517
+ startW = panel.offsetWidth;
3518
+ handle.classList.add("dragging");
3519
+ document.body.style.cursor = "col-resize";
3520
+ document.body.style.userSelect = "none";
3521
+ const overlay = document.createElement("div");
3522
+ overlay.id = "tree-resize-overlay";
3523
+ overlay.style.cssText = "position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:9999;cursor:col-resize;background:transparent;";
3524
+ document.body.appendChild(overlay);
3525
+ window.addEventListener("mousemove", onMove, true);
3526
+ window.addEventListener("mouseup", onUp, true);
3527
+ });
3528
+ }
3529
+
3530
+ // ═══════════════════════════════════════════
3531
+ // RESIZE HANDLE (Right Panel)
3532
+ // ═══════════════════════════════════════════
2983
3533
  function setupResizeHandle() {
2984
3534
  const handle = document.getElementById("resize-handle");
2985
3535
  const panel = document.getElementById("right-panel");
@@ -2995,7 +3545,7 @@ function setupResizeHandle() {
2995
3545
  const newW = Math.max(250, Math.min(900, startW + dx));
2996
3546
  panel.style.flex = "0 0 " + newW + "px";
2997
3547
  panel.style.width = newW + "px";
2998
- if (hub) hub.style.right = newW + "px";
3548
+ updateDynamicRightOffsets();
2999
3549
  if (Graph) {
3000
3550
  const gp = document.getElementById("graph-panel");
3001
3551
  if (gp) Graph.width(gp.clientWidth);
@@ -3045,21 +3595,14 @@ function updateLicenseBadge(plan, expiresAt) {
3045
3595
 
3046
3596
  badge.className = "license-badge";
3047
3597
 
3048
- if (plan === "pro" && expiresAt) {
3049
- const diffMs = new Date(expiresAt) - new Date();
3050
- if (diffMs <= 0) {
3598
+ if (plan === "pro") {
3599
+ if (expiresAt && new Date(expiresAt) < new Date()) {
3051
3600
  badge.classList.add("expired");
3052
3601
  badge.textContent = "EXPIRED";
3053
- } else if (Math.ceil(diffMs / (1000 * 60 * 60 * 24)) <= 14) {
3054
- badge.classList.add("trial");
3055
- badge.textContent = "TRIAL";
3056
3602
  } else {
3057
3603
  badge.classList.add("pro");
3058
3604
  badge.textContent = "PRO";
3059
3605
  }
3060
- } else if (plan === "pro") {
3061
- badge.classList.add("pro");
3062
- badge.textContent = "PRO";
3063
3606
  } else {
3064
3607
  badge.classList.add("free");
3065
3608
  badge.textContent = "FREE";
@@ -3119,6 +3662,7 @@ async function loadProjectInfo() {
3119
3662
  }
3120
3663
  hideServerOffline();
3121
3664
  updateLicenseBadge(info.plan, info.expiresAt);
3665
+ updateLicenseButton(info.plan);
3122
3666
  // Bottom bar: fetch version from npm registry
3123
3667
  updateBottomBar();
3124
3668
  } catch (e) {
@@ -3260,6 +3804,106 @@ async function switchProject(projectPath) {
3260
3804
  }
3261
3805
  }
3262
3806
 
3807
+ // ══════════════════════════════════════════════════════════════
3808
+ // LICENSE MODAL
3809
+ // ══════════════════════════════════════════════════════════════
3810
+ function setupLicenseModal() {
3811
+ const btn = document.getElementById("btn-license");
3812
+ const modal = document.getElementById("license-modal");
3813
+ const input = document.getElementById("license-key-input");
3814
+ const activateBtn = document.getElementById("btn-license-activate");
3815
+ const deactivateBtn = document.getElementById("btn-license-deactivate");
3816
+ const cancelBtn = document.getElementById("btn-license-cancel");
3817
+ const statusEl = document.getElementById("license-modal-status");
3818
+ if (!btn || !modal) return;
3819
+
3820
+ function openModal() {
3821
+ modal.classList.remove("hidden");
3822
+ input.value = "";
3823
+ statusEl.textContent = "";
3824
+ statusEl.className = "";
3825
+ input.focus();
3826
+ }
3827
+ function closeModal() {
3828
+ modal.classList.add("hidden");
3829
+ }
3830
+
3831
+ btn.addEventListener("click", openModal);
3832
+ cancelBtn.addEventListener("click", closeModal);
3833
+ modal.addEventListener("click", (e) => { if (e.target === modal) closeModal(); });
3834
+
3835
+ activateBtn.addEventListener("click", async () => {
3836
+ const key = input.value.trim();
3837
+ if (!key || !key.startsWith("SYKE-")) {
3838
+ statusEl.className = "error";
3839
+ statusEl.textContent = "Key must start with SYKE-";
3840
+ return;
3841
+ }
3842
+ statusEl.className = "loading";
3843
+ statusEl.textContent = "VALIDATING...";
3844
+ activateBtn.disabled = true;
3845
+
3846
+ try {
3847
+ const res = await fetch("/api/set-license-key", {
3848
+ method: "POST",
3849
+ headers: { "Content-Type": "application/json" },
3850
+ body: JSON.stringify({ key }),
3851
+ });
3852
+ const data = await res.json();
3853
+ if (data.success && data.plan === "pro") {
3854
+ statusEl.className = "success";
3855
+ statusEl.textContent = "PRO ACTIVATED";
3856
+ updateLicenseBadge("pro", data.expiresAt);
3857
+ updateLicenseButton("pro");
3858
+ setTimeout(closeModal, 1200);
3859
+ } else {
3860
+ statusEl.className = "error";
3861
+ statusEl.textContent = data.error || "Activation failed";
3862
+ }
3863
+ } catch (err) {
3864
+ statusEl.className = "error";
3865
+ statusEl.textContent = "Network error";
3866
+ }
3867
+ activateBtn.disabled = false;
3868
+ });
3869
+
3870
+ deactivateBtn.addEventListener("click", async () => {
3871
+ if (!confirm("Remove license key? Dashboard will switch to Free mode.")) return;
3872
+ statusEl.className = "loading";
3873
+ statusEl.textContent = "REMOVING...";
3874
+
3875
+ try {
3876
+ const res = await fetch("/api/set-license-key", {
3877
+ method: "POST",
3878
+ headers: { "Content-Type": "application/json" },
3879
+ body: JSON.stringify({ key: null }),
3880
+ });
3881
+ const data = await res.json();
3882
+ if (data.success) {
3883
+ statusEl.className = "success";
3884
+ statusEl.textContent = "KEY REMOVED";
3885
+ updateLicenseBadge("free", null);
3886
+ updateLicenseButton("free");
3887
+ setTimeout(closeModal, 800);
3888
+ }
3889
+ } catch {
3890
+ statusEl.className = "error";
3891
+ statusEl.textContent = "Failed to remove key";
3892
+ }
3893
+ });
3894
+
3895
+ input.addEventListener("keydown", (e) => {
3896
+ if (e.key === "Enter") activateBtn.click();
3897
+ if (e.key === "Escape") closeModal();
3898
+ });
3899
+ }
3900
+
3901
+ function updateLicenseButton(plan) {
3902
+ const btn = document.getElementById("btn-license");
3903
+ if (!btn) return;
3904
+ btn.textContent = plan === "pro" ? "LICENSED" : "LICENSE";
3905
+ }
3906
+
3263
3907
  function setupProjectModal() {
3264
3908
  const openBtn = document.getElementById("btn-change-project");
3265
3909
  const modal = document.getElementById("project-modal");