codeui-python 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. codeui/__init__.py +1 -1
  2. codeui/agent/context.py +19 -3
  3. codeui/analysis/dead_code.py +13 -2
  4. codeui/analysis/undefined.py +70 -0
  5. codeui/analysis/unresolved_import.py +34 -4
  6. codeui/cli/main.py +101 -7
  7. codeui/core/cache.py +54 -0
  8. codeui/core/graph.py +233 -3
  9. codeui/core/override.py +1 -0
  10. codeui/core/resolver.py +51 -37
  11. codeui/errors.py +10 -0
  12. codeui/lang/base.py +5 -2
  13. codeui/lang/cpp.py +271 -0
  14. codeui/lang/csharp.py +262 -0
  15. codeui/lang/dart.py +278 -0
  16. codeui/lang/generic.py +6 -4
  17. codeui/lang/go.py +5 -2
  18. codeui/lang/java.py +320 -0
  19. codeui/lang/kotlin.py +303 -0
  20. codeui/lang/php.py +275 -0
  21. codeui/lang/python.py +49 -7
  22. codeui/lang/registry.py +22 -2
  23. codeui/lang/ruby.py +270 -0
  24. codeui/lang/rust.py +5 -2
  25. codeui/lang/scala.py +290 -0
  26. codeui/lang/swift.py +276 -0
  27. codeui/lang/ts.py +99 -8
  28. codeui/report/sarif_emitter.py +2 -1
  29. codeui/server/server.py +156 -85
  30. codeui/server/static/css/styles.css +1609 -0
  31. codeui/server/static/index.html +199 -0
  32. codeui/server/static/js/api.js +485 -0
  33. codeui/server/static/js/app.js +1044 -0
  34. codeui/server/static/js/graph_canvas.js +1485 -0
  35. codeui/server/static/js/minimap.js +383 -0
  36. codeui/server/static/js/sidebar.js +268 -0
  37. codeui/server/static/js/state.js +83 -0
  38. codeui/server/static/js/subgraph_window.js +1208 -0
  39. codeui/server/static/js/theme.js +332 -0
  40. codeui/tracer/__init__.py +4 -1
  41. codeui/tracer/tracer.py +109 -118
  42. codeui_python-0.1.1.dist-info/METADATA +171 -0
  43. codeui_python-0.1.1.dist-info/RECORD +71 -0
  44. {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/licenses/LICENSE +1 -1
  45. codeui_python-0.1.0.dist-info/METADATA +0 -191
  46. codeui_python-0.1.0.dist-info/RECORD +0 -52
  47. {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/WHEEL +0 -0
  48. {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/entry_points.txt +0 -0
  49. {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1044 @@
1
+ /**
2
+ * Primary Application Controller orchestrating CodeUI interactive explorer.
3
+ */
4
+ class App {
5
+ static async init() {
6
+ if (window.ThemeManager) {
7
+ window.ThemeManager.init();
8
+ }
9
+ if (window.Sidebar) {
10
+ window.Sidebar.init();
11
+ }
12
+ window.mainCanvas = new GraphCanvas("graph-canvas", true);
13
+ this.setupKeyboardShortcuts();
14
+ this.setupGlobalClickListener();
15
+
16
+ const urlParams = new URLSearchParams(window.location.search);
17
+ const repoParam = urlParams.get("repo") || urlParams.get("repository");
18
+ if (repoParam) {
19
+ const input = document.getElementById("clone-repo-input");
20
+ if (input) input.value = repoParam;
21
+ await this.cloneRepository(repoParam);
22
+ return;
23
+ }
24
+
25
+ await this.loadData();
26
+ }
27
+
28
+ static showLoading(isLoading, text = "Processing...") {
29
+ const overlay = document.getElementById("canvas-loading-overlay");
30
+ const txt = document.getElementById("loading-overlay-text");
31
+ if (overlay) {
32
+ overlay.style.display = isLoading ? "flex" : "none";
33
+ if (txt && text) txt.textContent = text;
34
+ }
35
+ }
36
+
37
+ static async loadData(forceRefresh = false) {
38
+ try {
39
+ this.showLoading(true, "Loading universal code graph...");
40
+ const graphData = await CodeUIAPI.getGraph(forceRefresh);
41
+ appState.rawGraph = graphData;
42
+
43
+ const defectsRes = await CodeUIAPI.getDefects();
44
+ appState.defectsData = defectsRes.findings || [];
45
+
46
+ this.initSymbolFilters();
47
+ this.applyGraphFilter();
48
+ Sidebar.renderCurrentTab();
49
+ } catch (e) {
50
+ this.showToast(`Failed to load graph: ${e.message}`, "error");
51
+ } finally {
52
+ this.showLoading(false);
53
+ }
54
+ }
55
+
56
+ static async cloneRepository(repoOverride = null) {
57
+ const input = document.getElementById("clone-repo-input");
58
+ const btn = document.getElementById("btn-clone-repo");
59
+ const repo = (repoOverride || input?.value || "").trim();
60
+ if (!repo) {
61
+ this.showToast("Please enter a GitHub repository name (e.g. expressjs/express) or Git URL", "error");
62
+ return;
63
+ }
64
+
65
+ if (btn) {
66
+ btn.disabled = true;
67
+ btn.textContent = "Loading...";
68
+ }
69
+
70
+ this.showToast(`Loading repository ${repo}...`, "info");
71
+ this.showLoading(true, `Loading repository ${repo}...`);
72
+ try {
73
+ const res = await CodeUIAPI.cloneRepo(repo);
74
+ if (res.graph) {
75
+ appState.rawGraph = res.graph;
76
+ appState.defectsData = [];
77
+ this.initSymbolFilters();
78
+ this.applyGraphFilter();
79
+ Sidebar.renderCurrentTab();
80
+ } else {
81
+ await this.loadData(false);
82
+ }
83
+ this.showToast(`Successfully loaded ${repo} (${res.files_count} files)`, "success");
84
+ if (input && !repoOverride) input.value = "";
85
+ } catch (e) {
86
+ this.showToast(`Failed to load repository: ${e.message}`, "error");
87
+ } finally {
88
+ this.showLoading(false);
89
+ if (btn) {
90
+ btn.disabled = false;
91
+ btn.textContent = "Clone";
92
+ }
93
+ }
94
+ }
95
+
96
+ static switchGraphMode(mode) {
97
+ appState.graphMode = mode;
98
+ document.getElementById("btn-mode-files")?.classList.toggle("active", mode === "files");
99
+ document.getElementById("btn-mode-symbols")?.classList.toggle("active", mode === "symbols");
100
+ this.renderSymbolFilterPopover();
101
+ this.applyGraphFilter();
102
+ }
103
+
104
+ static getDiscoveredSymbolKinds() {
105
+ if (!appState.rawGraph || !Array.isArray(appState.rawGraph.symbols)) {
106
+ return [];
107
+ }
108
+ const counts = new Map();
109
+ for (const s of appState.rawGraph.symbols) {
110
+ if (!s || s.kind === "file") continue;
111
+ const k = String(s.kind || "other").toLowerCase();
112
+ counts.set(k, (counts.get(k) || 0) + 1);
113
+ }
114
+ return Array.from(counts.entries())
115
+ .map(([kind, count]) => ({ kind, count }))
116
+ .sort((a, b) => b.count - a.count || a.kind.localeCompare(b.kind));
117
+ }
118
+
119
+ static initSymbolFilters() {
120
+ if (!appState.symbolKindFilters) {
121
+ appState.symbolKindFilters = {};
122
+ }
123
+ const discovered = this.getDiscoveredSymbolKinds();
124
+ discovered.forEach(({ kind }) => {
125
+ if (appState.symbolKindFilters[kind] === undefined) {
126
+ appState.symbolKindFilters[kind] = (kind !== "variable" && kind !== "var");
127
+ }
128
+ });
129
+ this.renderSymbolFilterPopover();
130
+ }
131
+
132
+ static renderSymbolFilterPopover() {
133
+ const container = document.getElementById("symbol-filter-items-container");
134
+ if (!container) return;
135
+
136
+ const discovered = this.getDiscoveredSymbolKinds();
137
+ if (discovered.length === 0) {
138
+ container.innerHTML = `<div style="font-size:0.72rem; color:var(--text-dim); text-align:center; padding:8px;">No symbol categories discovered</div>`;
139
+ return;
140
+ }
141
+
142
+ const formatLabel = (k) => {
143
+ const pretty = k.replace(/_/g, " ");
144
+ return pretty.charAt(0).toUpperCase() + pretty.slice(1);
145
+ };
146
+
147
+ container.innerHTML = discovered.map(({ kind, count }) => {
148
+ const isChecked = appState.symbolKindFilters?.[kind] !== false;
149
+ const safeKind = kind.replace(/[^a-zA-Z0-9_-]/g, "");
150
+ return `
151
+ <div class="symbol-filter-item" onclick="App.toggleSymbolFilter('${safeKind}')">
152
+ <label onclick="event.stopPropagation()">
153
+ <input type="checkbox" id="filter-dyn-${safeKind}" ${isChecked ? "checked" : ""} onchange="App.toggleSymbolFilter('${safeKind}')">
154
+ <span class="badge ${safeKind}" style="font-size:0.65rem; padding:1px 5px;">${formatLabel(kind)}</span>
155
+ </label>
156
+ <span class="symbol-filter-count">${count}</span>
157
+ </div>
158
+ `;
159
+ }).join("");
160
+
161
+ const filterBtn = document.getElementById("btn-toggle-symbol-filter-popover");
162
+ if (filterBtn) {
163
+ const hasDisabled = discovered.some(({ kind }) => appState.symbolKindFilters?.[kind] === false);
164
+ filterBtn.classList.toggle("active", hasDisabled);
165
+ }
166
+ }
167
+
168
+ static toggleSymbolFilter(kind) {
169
+ if (!appState.symbolKindFilters) {
170
+ appState.symbolKindFilters = {};
171
+ }
172
+ const current = appState.symbolKindFilters[kind] !== false;
173
+ appState.symbolKindFilters[kind] = !current;
174
+ this.renderSymbolFilterPopover();
175
+ this.applyGraphFilter();
176
+ if (window.mainCanvas) {
177
+ window.mainCanvas.alpha = Math.max(window.mainCanvas.alpha || 0, 0.65);
178
+ }
179
+ }
180
+
181
+ static setAllSymbolFilters(enableAll) {
182
+ if (!appState.symbolKindFilters) {
183
+ appState.symbolKindFilters = {};
184
+ }
185
+ const discovered = this.getDiscoveredSymbolKinds();
186
+ discovered.forEach(({ kind }) => {
187
+ appState.symbolKindFilters[kind] = !!enableAll;
188
+ });
189
+ this.renderSymbolFilterPopover();
190
+ this.applyGraphFilter();
191
+ if (window.mainCanvas) {
192
+ window.mainCanvas.alpha = Math.max(window.mainCanvas.alpha || 0, 0.65);
193
+ }
194
+ }
195
+
196
+ static toggleSymbolFilterPopover() {
197
+ const popover = document.getElementById("symbol-filter-popover");
198
+ if (!popover) return;
199
+ const isHidden = popover.style.display === "none" || !popover.style.display;
200
+ if (isHidden) {
201
+ this.renderSymbolFilterPopover();
202
+ popover.style.display = "block";
203
+ } else {
204
+ popover.style.display = "none";
205
+ }
206
+ }
207
+
208
+ static closeSymbolFilterPopover() {
209
+ const popover = document.getElementById("symbol-filter-popover");
210
+ if (popover) {
211
+ popover.style.display = "none";
212
+ }
213
+ }
214
+
215
+ static switchColorMode(mode) {
216
+ appState.colorMode = mode;
217
+ window.mainCanvas?.render();
218
+ Sidebar.renderCurrentTab();
219
+ }
220
+
221
+ static switchLayoutMode(mode) {
222
+ appState.layoutMode = mode;
223
+ const select = document.getElementById("select-layout-mode");
224
+ if (select) select.value = mode;
225
+ window.mainCanvas?.setLayout(mode);
226
+ }
227
+
228
+ static switchTheme(theme) {
229
+ if (window.ThemeManager) {
230
+ window.ThemeManager.applyTheme(theme);
231
+ } else {
232
+ appState.theme = theme;
233
+ document.body.setAttribute("data-theme", theme);
234
+ const select = document.getElementById("select-theme");
235
+ if (select) select.value = theme;
236
+ window.mainCanvas?.render();
237
+ if (window.mainCanvas?.miniMap) window.mainCanvas.miniMap.update();
238
+ }
239
+ }
240
+
241
+ static toggleUnusedHighlight() {
242
+ appState.unusedHighlightMode = !appState.unusedHighlightMode;
243
+ const btn = document.getElementById("btn-toggle-unused");
244
+ if (btn) {
245
+ btn.textContent = appState.unusedHighlightMode ? "⚠️ Unused: ON" : "⚠️ Unused: OFF";
246
+ btn.classList.toggle("active", appState.unusedHighlightMode);
247
+ }
248
+ window.mainCanvas?.render();
249
+ if (window.mainCanvas?.miniMap) {
250
+ window.mainCanvas.miniMap.update();
251
+ }
252
+ }
253
+
254
+ static toggleDirectDiskEdit() {
255
+ this.setDirectDiskEdit(!appState.directDiskWrite);
256
+ }
257
+
258
+ static setDirectDiskEdit(isEnabled) {
259
+ appState.directDiskWrite = !!isEnabled;
260
+ const btn = document.getElementById("btn-toggle-direct-disk");
261
+ if (btn) {
262
+ btn.textContent = appState.directDiskWrite ? "💾 Disk Edit: ON" : "💾 Disk Edit: OFF";
263
+ btn.classList.toggle("active", appState.directDiskWrite);
264
+ btn.classList.toggle("success", appState.directDiskWrite);
265
+ }
266
+ // Update all open subgraph editor toggle checkboxes/badges if present
267
+ document.querySelectorAll(".subgraph-toggle-disk-input").forEach(inp => {
268
+ inp.checked = appState.directDiskWrite;
269
+ });
270
+ const msg = appState.directDiskWrite
271
+ ? "Direct disk edit enabled: modifications will write directly to project files on disk"
272
+ : "In-memory override mode enabled: modifications are saved non-destructively in session store";
273
+ this.showToast(msg, appState.directDiskWrite ? "warning" : "info");
274
+ }
275
+
276
+ static showApiDriftPanel() {
277
+ if (Sidebar.collapsed) {
278
+ Sidebar.toggleSidebar();
279
+ }
280
+ Sidebar.switchTab("defects");
281
+ Sidebar.setDefectSubFilter("api_drift");
282
+ const count = (appState.defectsData || []).filter(d => d.rule_id === "api_drift").length;
283
+ if (count > 0) {
284
+ this.showToast(`Detected ${count} API Drift warnings against baseline snapshot`, "warning");
285
+ } else {
286
+ this.showToast("No API Drift detected (symbols match API baseline)", "info");
287
+ }
288
+ }
289
+
290
+ static async setApiBaseline() {
291
+ try {
292
+ this.showToast("Creating Public API Baseline snapshot...", "info");
293
+ const res = await CodeUIAPI.setApiBaseline();
294
+ this.showToast(`API Baseline created for ${res.baseline_count} public symbols`, "success");
295
+ await this.loadData(false);
296
+ if (Sidebar.collapsed) {
297
+ Sidebar.toggleSidebar();
298
+ }
299
+ Sidebar.switchTab("defects");
300
+ Sidebar.setDefectSubFilter("api_drift");
301
+ } catch (e) {
302
+ this.showToast(`Failed to set API baseline: ${e.message}`, "error");
303
+ }
304
+ }
305
+
306
+ static toggleClusterHulls() {
307
+ appState.clusterHullsMode = !appState.clusterHullsMode;
308
+ window.mainCanvas?.render();
309
+ }
310
+
311
+ static toggleSim() {
312
+ if (!window.mainCanvas) return;
313
+ window.mainCanvas.isRunning = !window.mainCanvas.isRunning;
314
+ const btn = document.getElementById("btn-pause-sim");
315
+ if (btn) btn.textContent = window.mainCanvas.isRunning ? "Pause" : "Resume";
316
+ }
317
+
318
+ static reheatSim() {
319
+ if (!window.mainCanvas) return;
320
+ window.mainCanvas.alpha = 1.0;
321
+ window.mainCanvas.isRunning = true;
322
+ const btn = document.getElementById("btn-pause-sim");
323
+ if (btn) btn.textContent = "Pause";
324
+ }
325
+
326
+ static filterGraph(query) {
327
+ appState.searchQuery = query.toLowerCase();
328
+ this.applyGraphFilter();
329
+ Sidebar.renderCurrentTab();
330
+ }
331
+
332
+ static applyGraphFilter() {
333
+ if (!appState.rawGraph || !window.mainCanvas) return;
334
+ const filesInfo = appState.rawGraph.files_info || {};
335
+ const query = appState.searchQuery;
336
+
337
+ if (appState.graphMode === "files") {
338
+ const allFiles = appState.rawGraph.files || [];
339
+ const files = allFiles.filter(f => {
340
+ const info = filesInfo[f] || {};
341
+ if (appState.activeLayerFilter !== "all" && info.layer !== appState.activeLayerFilter) return false;
342
+ if (query && !f.toLowerCase().includes(query)) return false;
343
+ return true;
344
+ });
345
+
346
+ const validFileSet = new Set(files);
347
+ const allFileSet = new Set(allFiles);
348
+
349
+ // Universal file path resolver for intra/inter-module links
350
+ const resolveFile = (id, fromFile = null) => {
351
+ if (!id) return null;
352
+ if (validFileSet.has(id)) return id;
353
+
354
+ // Handle symbol ID with ::
355
+ const filePart = id.split("::")[0].replace(/^module::/, "");
356
+ if (validFileSet.has(filePart)) return filePart;
357
+
358
+ // Clean relative paths
359
+ const clean = filePart.replace(/^\.\//, "");
360
+ if (validFileSet.has(clean)) return clean;
361
+
362
+ // Match exact file without extension in valid files
363
+ for (const f of validFileSet) {
364
+ const fNoExt = f.replace(/\.[^/.]+$/, "");
365
+ if (f === clean || fNoExt === clean) {
366
+ return f;
367
+ }
368
+ }
369
+
370
+ // Match dotted imports or normalized slash paths
371
+ const slashed = clean.replace(/\./g, "/");
372
+ for (const f of validFileSet) {
373
+ const fClean = f.replace(/\.[^/.]+$/, "");
374
+ if (f === slashed || fClean === slashed || f.endsWith("/" + slashed) || fClean.endsWith("/" + slashed)) {
375
+ return f;
376
+ }
377
+ if (f.endsWith("/" + slashed + "/index") || fClean.endsWith("/" + slashed + "/index") || f.endsWith("/" + slashed + "/__init__") || fClean.endsWith("/" + slashed + "/__init__")) {
378
+ return f;
379
+ }
380
+ }
381
+
382
+ // Relative path resolution from source file directory
383
+ if (fromFile && (clean.startsWith("./") || clean.startsWith("../") || !clean.includes("/"))) {
384
+ const fromDir = fromFile.includes("/") ? fromFile.substring(0, fromFile.lastIndexOf("/")) : "";
385
+ const candidate = fromDir ? `${fromDir}/${clean.replace(/^\.\//, "")}` : clean.replace(/^\.\//, "");
386
+ if (validFileSet.has(candidate)) return candidate;
387
+ for (const f of validFileSet) {
388
+ const fNoExt = f.replace(/\.[^/.]+$/, "");
389
+ if (fNoExt === candidate || fNoExt === candidate + "/index" || fNoExt === candidate + "/__init__") {
390
+ return f;
391
+ }
392
+ }
393
+ }
394
+
395
+ // Match by file stem/name fallback
396
+ const stem = clean.split("/").pop().split(".")[0];
397
+ if (stem && stem.length > 2) {
398
+ for (const f of validFileSet) {
399
+ const fName = f.split("/").pop().split(".")[0];
400
+ if (fName === stem) return f;
401
+ }
402
+ }
403
+
404
+ return null;
405
+ };
406
+
407
+ const nodes = files.map(f => {
408
+ const info = filesInfo[f] || {};
409
+ return {
410
+ id: f,
411
+ label: f.split("/").pop(),
412
+ layer: info.layer || "other",
413
+ feature: info.feature || "root",
414
+ radius: info.layer === "entry" ? 10 : 8
415
+ };
416
+ });
417
+
418
+ const linkMap = new Map();
419
+ (appState.rawGraph.edges || []).forEach(e => {
420
+ if (e.kind === "contains") return;
421
+ const srcFile = resolveFile(e.source_id);
422
+ const tgtFile = resolveFile(e.target_id, srcFile);
423
+ if (srcFile && tgtFile && srcFile !== tgtFile) {
424
+ const linkKey = `${srcFile}-->${tgtFile}`;
425
+ if (linkMap.has(linkKey)) {
426
+ const existing = linkMap.get(linkKey);
427
+ existing.weight += (e.weight || 1);
428
+ } else {
429
+ linkMap.set(linkKey, {
430
+ source: srcFile,
431
+ target: tgtFile,
432
+ kind: e.kind || "imports",
433
+ weight: e.weight || 1
434
+ });
435
+ }
436
+ }
437
+ });
438
+
439
+ const links = Array.from(linkMap.values());
440
+ window.mainCanvas.setData(nodes, links);
441
+ } else {
442
+ const isKindActive = (k) => {
443
+ if (!k) return false;
444
+ const kind = String(k).toLowerCase();
445
+ if (appState.symbolKindFilters && appState.symbolKindFilters[kind] !== undefined) {
446
+ return !!appState.symbolKindFilters[kind];
447
+ }
448
+ return true;
449
+ };
450
+
451
+ const nodes = [];
452
+ const filesWithSymbols = new Set();
453
+
454
+ (appState.rawGraph.symbols || []).forEach(s => {
455
+ if (s.kind === "file") return;
456
+ if (!isKindActive(s.kind)) return;
457
+ if (query && !s.name.toLowerCase().includes(query) && !s.qualified_name?.toLowerCase().includes(query)) {
458
+ return;
459
+ }
460
+
461
+ const fileId = s.location?.file_id || "";
462
+ if (fileId) filesWithSymbols.add(fileId);
463
+
464
+ const fileMeta = filesInfo[fileId] || {};
465
+ const cleanPath = fileId.replace(/\\/g, "/").replace(/^\.\//, "");
466
+ const parts = cleanPath.split("/").filter(Boolean);
467
+ const folder = fileMeta.folder || (parts.length > 1 ? parts.slice(0, -1).join("/") : "root");
468
+ const mainFolder = fileMeta.main_folder || (parts.length > 1 ? parts[0] : "root");
469
+ const subfolder = fileMeta.subfolder || (parts.length > 2 ? parts[parts.length - 2] : (parts.length > 1 ? parts[0] : "root"));
470
+
471
+ nodes.push({
472
+ id: s.id,
473
+ label: s.name,
474
+ kind: s.kind,
475
+ type: "symbol",
476
+ fileId: fileId,
477
+ folder: folder,
478
+ mainFolder: mainFolder,
479
+ subfolder: subfolder,
480
+ layer: fileMeta.layer || "shared",
481
+ feature: fileMeta.feature || s.kind,
482
+ rawSymbol: s,
483
+ radius: s.kind === "class" ? 11 : (s.kind === "function" ? 9 : (s.kind === "method" ? 7 : (s.kind === "variable" ? 5 : 8)))
484
+ });
485
+ });
486
+
487
+ // Add lone file nodes ONLY for files that have 0 detected symbols
488
+ (appState.rawGraph.files || []).forEach(file => {
489
+ const fileInfo = filesInfo[file] || {};
490
+ const symCount = typeof fileInfo.symbol_count === "number" ? fileInfo.symbol_count : 0;
491
+ if (!filesWithSymbols.has(file) && symCount === 0) {
492
+ if (query && !file.toLowerCase().includes(query)) return;
493
+ const cleanPath = file.replace(/\\/g, "/").replace(/^\.\//, "");
494
+ const parts = cleanPath.split("/").filter(Boolean);
495
+ const folder = fileInfo.folder || (parts.length > 1 ? parts.slice(0, -1).join("/") : "root");
496
+ const mainFolder = fileInfo.main_folder || (parts.length > 1 ? parts[0] : "root");
497
+ const subfolder = fileInfo.subfolder || (parts.length > 2 ? parts[parts.length - 2] : (parts.length > 1 ? parts[0] : "root"));
498
+
499
+ nodes.push({
500
+ id: file,
501
+ label: file.split("/").pop(),
502
+ kind: "file",
503
+ type: "file",
504
+ fileId: file,
505
+ folder: folder,
506
+ mainFolder: mainFolder,
507
+ subfolder: subfolder,
508
+ layer: fileInfo.layer || "other",
509
+ feature: fileInfo.feature || "root",
510
+ radius: 12,
511
+ isLoneFile: true
512
+ });
513
+ }
514
+ });
515
+
516
+ const activeNodeIds = new Set(nodes.map(n => n.id));
517
+ const links = [];
518
+ (appState.rawGraph.edges || []).forEach(e => {
519
+ if (activeNodeIds.has(e.source_id) && activeNodeIds.has(e.target_id) && e.source_id !== e.target_id) {
520
+ links.push({
521
+ source: e.source_id,
522
+ target: e.target_id,
523
+ kind: e.kind || "calls",
524
+ weight: e.weight || 1
525
+ });
526
+ }
527
+ });
528
+
529
+ window.mainCanvas.setData(nodes, links);
530
+ }
531
+ }
532
+
533
+ static togglePathTracer() {
534
+ appState.tracerActive = !appState.tracerActive;
535
+ const btn = document.getElementById("btn-path-tracer");
536
+ if (btn) btn.classList.toggle("active", appState.tracerActive);
537
+
538
+ if (appState.tracerActive) {
539
+ appState.traceStartNode = null;
540
+ appState.traceEndNode = null;
541
+ appState.activeTracePath = [];
542
+ appState.currentTraceHopIdx = 0;
543
+ this.updateTracerHUD();
544
+ this.showToast("Path Tracer Active: Click Source node on canvas", "info");
545
+ } else {
546
+ this.clearTracer();
547
+ }
548
+ }
549
+
550
+ static handleTracerNodeClick(nodeId) {
551
+ if (!appState.tracerActive || !nodeId) return;
552
+ if (!appState.traceStartNode) {
553
+ appState.traceStartNode = nodeId;
554
+ appState.traceEndNode = null;
555
+ appState.activeTracePath = [];
556
+ appState.currentTraceHopIdx = 0;
557
+ this.showToast(`Tracer Source: ${nodeId.split("/").pop()}. Now click Target node`, "info");
558
+ } else if (!appState.traceEndNode) {
559
+ if (appState.traceStartNode === nodeId) {
560
+ this.showToast(`Selected node is already the source. Please choose a different target node`, "info");
561
+ return;
562
+ }
563
+ appState.traceEndNode = nodeId;
564
+ appState.currentTraceHopIdx = 0;
565
+ this.computeAndHighlightPath(appState.traceStartNode, appState.traceEndNode);
566
+ } else {
567
+ if (appState.traceStartNode === nodeId) {
568
+ this.showToast(`Source is already ${nodeId.split("/").pop()}. Click a different node to trace`, "info");
569
+ return;
570
+ }
571
+ if (appState.traceEndNode === nodeId) {
572
+ return;
573
+ }
574
+ appState.traceStartNode = nodeId;
575
+ appState.traceEndNode = null;
576
+ appState.activeTracePath = [];
577
+ appState.currentTraceHopIdx = 0;
578
+ this.showToast(`Tracer Reset: Source is now ${nodeId.split("/").pop()}. Click Target`, "info");
579
+ }
580
+ this.updateTracerHUD();
581
+ window.mainCanvas?.render();
582
+ }
583
+
584
+ static computeAndHighlightPath(startId, endId) {
585
+ if (!window.mainCanvas) return;
586
+ const adj = new Map();
587
+ window.mainCanvas.links.forEach(l => {
588
+ const s = typeof l.source === "object" ? l.source.id : l.source;
589
+ const t = typeof l.target === "object" ? l.target.id : l.target;
590
+ if (!adj.has(s)) adj.set(s, []);
591
+ adj.get(s).push(t);
592
+ });
593
+
594
+ const queue = [[startId]];
595
+ const visited = new Set([startId]);
596
+ let foundPath = null;
597
+
598
+ while (queue.length > 0) {
599
+ const path = queue.shift();
600
+ const curr = path[path.length - 1];
601
+ if (curr === endId) {
602
+ foundPath = path;
603
+ break;
604
+ }
605
+ const neighbors = adj.get(curr) || [];
606
+ for (let next of neighbors) {
607
+ if (!visited.has(next)) {
608
+ visited.add(next);
609
+ queue.push([...path, next]);
610
+ }
611
+ }
612
+ }
613
+
614
+ if (foundPath) {
615
+ appState.activeTracePath = foundPath;
616
+ appState.currentTraceHopIdx = 0;
617
+ this.showToast(`Found dependency path: ${foundPath.length - 1} hops!`, "success");
618
+ } else {
619
+ appState.activeTracePath = [];
620
+ this.showToast("No direct directed path found between nodes", "error");
621
+ }
622
+ this.updateTracerHUD();
623
+ window.mainCanvas.render();
624
+ }
625
+
626
+ static tracerStepPrev() {
627
+ if (!appState.activeTracePath || appState.activeTracePath.length === 0) return;
628
+ appState.currentTraceHopIdx = Math.max(0, (appState.currentTraceHopIdx || 0) - 1);
629
+ const nodeId = appState.activeTracePath[appState.currentTraceHopIdx];
630
+ const node = (window.mainCanvas?.nodes || []).find(n => n.id === nodeId);
631
+ if (node && window.mainCanvas) {
632
+ window.mainCanvas.smoothPanZoomTo(node.x, node.y, 1.8);
633
+ }
634
+ this.updateTracerHUD();
635
+ }
636
+
637
+ static tracerStepNext() {
638
+ if (!appState.activeTracePath || appState.activeTracePath.length === 0) return;
639
+ appState.currentTraceHopIdx = Math.min(appState.activeTracePath.length - 1, (appState.currentTraceHopIdx || 0) + 1);
640
+ const nodeId = appState.activeTracePath[appState.currentTraceHopIdx];
641
+ const node = (window.mainCanvas?.nodes || []).find(n => n.id === nodeId);
642
+ if (node && window.mainCanvas) {
643
+ window.mainCanvas.smoothPanZoomTo(node.x, node.y, 1.8);
644
+ }
645
+ this.updateTracerHUD();
646
+ }
647
+
648
+ static clearTracer() {
649
+ appState.tracerActive = false;
650
+ appState.traceStartNode = null;
651
+ appState.traceEndNode = null;
652
+ appState.activeTracePath = [];
653
+ appState.currentTraceHopIdx = 0;
654
+ const btn = document.getElementById("btn-path-tracer");
655
+ if (btn) btn.classList.remove("active");
656
+ this.hideNodeHUD();
657
+ window.mainCanvas?.render();
658
+ }
659
+
660
+ static updateTracerHUD() {
661
+ if (!appState.tracerActive) {
662
+ this.hideNodeHUD();
663
+ return;
664
+ }
665
+
666
+ let hud = document.getElementById("node-hud-panel");
667
+ if (!hud) {
668
+ hud = document.createElement("div");
669
+ hud.id = "node-hud-panel";
670
+ hud.className = "node-hud-panel";
671
+ document.getElementById("canvas-container")?.appendChild(hud);
672
+ }
673
+
674
+ const srcName = appState.traceStartNode ? appState.traceStartNode.split("/").pop() : "Click node...";
675
+ const tgtName = appState.traceEndNode ? appState.traceEndNode.split("/").pop() : (appState.traceStartNode ? "Click target node..." : "Pending");
676
+ const path = appState.activeTracePath || [];
677
+ const hopsCount = path.length > 1 ? `${path.length - 1} Hops Found` : (appState.traceEndNode ? "No direct path" : "Waiting for endpoints");
678
+
679
+ let pathListHtml = "";
680
+ if (path.length > 0) {
681
+ pathListHtml = path.map((id, idx) => {
682
+ const name = id.split("/").pop();
683
+ const isCurrent = idx === (appState.currentTraceHopIdx || 0);
684
+ return `<span class="hop-chip ${isCurrent ? 'active' : ''}">${idx + 1}. ${name}</span>`;
685
+ }).join(" <span class='hop-arrow'>➔</span> ");
686
+ }
687
+
688
+ hud.innerHTML = `
689
+ <div class="hud-header">
690
+ <span class="hud-tracer-title">⚡ Path Tracer Control</span>
691
+ <button class="secondary btn-close-hud" onclick="App.clearTracer()" title="Exit Tracer">✕</button>
692
+ </div>
693
+ <div class="hud-tracer-flow">
694
+ <div class="tracer-endpoint source" title="${appState.traceStartNode || ''}">
695
+ <span class="endpoint-label">SOURCE</span>
696
+ <span class="endpoint-val">${srcName}</span>
697
+ </div>
698
+ <div class="tracer-arrow">➔</div>
699
+ <div class="tracer-endpoint target" title="${appState.traceEndNode || ''}">
700
+ <span class="endpoint-label">TARGET</span>
701
+ <span class="endpoint-val">${tgtName}</span>
702
+ </div>
703
+ </div>
704
+ ${path.length > 0 ? `
705
+ <div class="hud-tracer-results">
706
+ <div class="tracer-hops-count">${hopsCount} (Step ${(appState.currentTraceHopIdx || 0) + 1}/${path.length})</div>
707
+ <div class="tracer-hops-list">${pathListHtml}</div>
708
+ </div>
709
+ <div class="hud-actions">
710
+ <button class="secondary" onclick="App.tracerStepPrev()">◀ Prev Hop</button>
711
+ <button class="secondary" onclick="App.tracerStepNext()">Next Hop ▶</button>
712
+ <button class="danger" onclick="App.clearTracer()">Reset</button>
713
+ </div>
714
+ ` : `
715
+ <div class="hud-tracer-results">
716
+ <div style="color:var(--text-muted); font-size:0.72rem;">
717
+ ${appState.traceStartNode ? 'Now click a target node to find the dependency path.' : 'Click any node on the graph to set the source.'}
718
+ </div>
719
+ </div>
720
+ <div class="hud-actions">
721
+ <button class="danger" onclick="App.clearTracer()">Close</button>
722
+ </div>
723
+ `}
724
+ `;
725
+ hud.style.display = "block";
726
+ }
727
+
728
+ static showNodeHUD(node, force = false) {
729
+ if (!appState.tracerActive) {
730
+ this.hideNodeHUD();
731
+ return;
732
+ }
733
+ this.updateTracerHUD();
734
+ }
735
+
736
+ static hideNodeHUD(explicitClose = false) {
737
+ if (explicitClose) {
738
+ appState.hudExplicitlyClosed = true;
739
+ }
740
+ const hud = document.getElementById("node-hud-panel");
741
+ if (hud) hud.style.display = "none";
742
+ }
743
+
744
+ // --- Quick Command Palette (Cmd / Ctrl + K) ---
745
+ static openQuickPalette() {
746
+ let palette = document.getElementById("quick-palette-modal");
747
+ if (!palette) {
748
+ palette = document.createElement("div");
749
+ palette.id = "quick-palette-modal";
750
+ palette.className = "palette-modal-backdrop";
751
+ palette.innerHTML = `
752
+ <div class="palette-modal-card">
753
+ <div class="palette-input-wrapper">
754
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
755
+ <input type="text" id="palette-search-input" placeholder="Search files, symbols, defects..." autocomplete="off">
756
+ <kbd class="kbd-badge">ESC</kbd>
757
+ </div>
758
+ <div class="palette-results" id="palette-results-list"></div>
759
+ </div>
760
+ `;
761
+ document.body.appendChild(palette);
762
+
763
+ palette.addEventListener("click", e => {
764
+ if (e.target === palette) this.closeQuickPalette();
765
+ });
766
+
767
+ const input = document.getElementById("palette-search-input");
768
+ input?.addEventListener("input", () => this.filterPaletteResults(input.value));
769
+ input?.addEventListener("keydown", e => this.handlePaletteKeyNav(e));
770
+ }
771
+
772
+ palette.style.display = "flex";
773
+ const input = document.getElementById("palette-search-input");
774
+ if (input) {
775
+ input.value = "";
776
+ input.focus();
777
+ this.filterPaletteResults("");
778
+ }
779
+ }
780
+
781
+ static closeQuickPalette() {
782
+ const palette = document.getElementById("quick-palette-modal");
783
+ if (palette) palette.style.display = "none";
784
+ }
785
+
786
+ static filterPaletteResults(query) {
787
+ const resultsEl = document.getElementById("palette-results-list");
788
+ if (!resultsEl) return;
789
+ const q = (query || "").toLowerCase();
790
+
791
+ const files = (appState.rawGraph.files || []).filter(f => f.toLowerCase().includes(q)).slice(0, 8);
792
+ const symbols = (appState.rawGraph.symbols || []).filter(s => s.name.toLowerCase().includes(q) || s.qualified_name.toLowerCase().includes(q)).slice(0, 8);
793
+ const defects = (appState.defectsData || []).filter(d => d.rule_id.toLowerCase().includes(q) || d.message.toLowerCase().includes(q)).slice(0, 6);
794
+
795
+ let html = "";
796
+ if (files.length > 0) {
797
+ html += `<div class="palette-group-title">Project Files</div>`;
798
+ files.forEach((f, idx) => {
799
+ html += `
800
+ <div class="palette-item ${idx === 0 ? 'selected' : ''}" data-type="file" data-id="${f}" onclick="App.selectPaletteItem('file', '${f}')">
801
+ <span>📄 ${f.split('/').pop()}</span>
802
+ <span style="font-size:0.68rem; color:var(--text-dim);">${f}</span>
803
+ </div>
804
+ `;
805
+ });
806
+ }
807
+
808
+ if (symbols.length > 0) {
809
+ html += `<div class="palette-group-title">Symbols & Functions</div>`;
810
+ symbols.forEach(s => {
811
+ html += `
812
+ <div class="palette-item" data-type="symbol" data-id="${s.id}" data-file="${s.id.split('::')[0]}" onclick="App.selectPaletteItem('symbol', '${s.id}')">
813
+ <span>⚡ ${s.name}</span>
814
+ <span class="badge ${s.kind}">${s.kind}</span>
815
+ </div>
816
+ `;
817
+ });
818
+ }
819
+
820
+ if (defects.length > 0) {
821
+ html += `<div class="palette-group-title">Defect Findings</div>`;
822
+ defects.forEach(d => {
823
+ const fId = d.location?.file_id || "unknown";
824
+ html += `
825
+ <div class="palette-item" data-type="defect" data-id="${d.id}" data-file="${fId}" data-line="${d.location?.start_line || 1}" onclick="App.selectPaletteItem('defect', '${fId}', ${d.location?.start_line || 1})">
826
+ <span style="color:var(--danger);">⚠️ ${d.rule_id}</span>
827
+ <span style="font-size:0.68rem; color:var(--text-muted);">${d.message.slice(0, 45)}...</span>
828
+ </div>
829
+ `;
830
+ });
831
+ }
832
+
833
+ if (!html) {
834
+ html = `<div style="padding:20px; text-align:center; color:var(--text-dim); font-size:0.75rem;">No matching files, symbols, or findings found.</div>`;
835
+ }
836
+ resultsEl.innerHTML = html;
837
+ }
838
+
839
+ static handlePaletteKeyNav(e) {
840
+ if (e.key === "Escape") {
841
+ this.closeQuickPalette();
842
+ return;
843
+ }
844
+ const items = document.querySelectorAll(".palette-item");
845
+ if (items.length === 0) return;
846
+ let selectedIdx = Array.from(items).findIndex(i => i.classList.contains("selected"));
847
+
848
+ if (e.key === "ArrowDown") {
849
+ e.preventDefault();
850
+ items[selectedIdx]?.classList.remove("selected");
851
+ selectedIdx = (selectedIdx + 1) % items.length;
852
+ items[selectedIdx]?.classList.add("selected");
853
+ items[selectedIdx]?.scrollIntoView({ block: "nearest" });
854
+ } else if (e.key === "ArrowUp") {
855
+ e.preventDefault();
856
+ items[selectedIdx]?.classList.remove("selected");
857
+ selectedIdx = (selectedIdx - 1 + items.length) % items.length;
858
+ items[selectedIdx]?.classList.add("selected");
859
+ items[selectedIdx]?.scrollIntoView({ block: "nearest" });
860
+ } else if (e.key === "Enter") {
861
+ e.preventDefault();
862
+ const cur = items[selectedIdx] || items[0];
863
+ if (cur) {
864
+ const type = cur.getAttribute("data-type");
865
+ const id = cur.getAttribute("data-id");
866
+ const line = cur.getAttribute("data-line");
867
+ this.selectPaletteItem(type, id, line);
868
+ }
869
+ }
870
+ }
871
+
872
+ static selectPaletteItem(type, id, line = 1) {
873
+ this.closeQuickPalette();
874
+ if (type === "file") {
875
+ Sidebar.onFileCardClick(id);
876
+ } else if (type === "symbol") {
877
+ const fileId = id.split("::")[0];
878
+ SubGraphManager.openWindow(fileId, id);
879
+ } else if (type === "defect") {
880
+ Sidebar.openDefectLocation(id, line, null, "");
881
+ }
882
+ }
883
+
884
+ // --- Keyboard Shortcuts ---
885
+ static setupKeyboardShortcuts() {
886
+ window.addEventListener("keydown", e => {
887
+ if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") {
888
+ if (e.key === "Escape") {
889
+ this.closeQuickPalette();
890
+ this.hideShortcutsModal();
891
+ }
892
+ return;
893
+ }
894
+
895
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
896
+ e.preventDefault();
897
+ this.openQuickPalette();
898
+ return;
899
+ }
900
+
901
+ if (e.key === "f" || e.key === "F") {
902
+ window.mainCanvas?.fitToScreen();
903
+ } else if (e.key === " ") {
904
+ e.preventDefault();
905
+ this.toggleSim();
906
+ } else if (e.key === "l" || e.key === "L") {
907
+ const layouts = ["force", "hierarchical", "radial", "clusters"];
908
+ const next = layouts[(layouts.indexOf(appState.layoutMode) + 1) % layouts.length];
909
+ this.switchLayoutMode(next);
910
+ } else if (e.key === "t" || e.key === "T") {
911
+ const themeKeys = window.ThemeManager ? Object.keys(window.ThemeManager.themes) : ["dark", "cyber", "light", "forest", "monokai"];
912
+ const currentIndex = Math.max(0, themeKeys.indexOf(appState.theme));
913
+ const next = themeKeys[(currentIndex + 1) % themeKeys.length];
914
+ this.switchTheme(next);
915
+ this.showToast(`Theme: ${window.ThemeManager?.getTheme(next)?.name || next}`, "info");
916
+ } else if (e.key === "1") {
917
+ Sidebar.switchTab("files");
918
+ } else if (e.key === "2") {
919
+ Sidebar.switchTab("features");
920
+ } else if (e.key === "3") {
921
+ Sidebar.switchTab("defects");
922
+ } else if (e.key === "4") {
923
+ Sidebar.switchTab("stats");
924
+ } else if (e.key === "?") {
925
+ this.toggleShortcutsModal();
926
+ } else if (e.key === "Escape") {
927
+ this.closeQuickPalette();
928
+ this.hideShortcutsModal();
929
+ this.closeSymbolFilterPopover();
930
+ this.clearTracer();
931
+ this.hideNodeHUD();
932
+ window.mainCanvas?.clearNeighborhoodIsolation();
933
+ }
934
+ });
935
+ }
936
+
937
+ static toggleShortcutsModal() {
938
+ let modal = document.getElementById("shortcuts-modal");
939
+ if (!modal) {
940
+ modal = document.createElement("div");
941
+ modal.id = "shortcuts-modal";
942
+ modal.className = "palette-modal-backdrop";
943
+ modal.innerHTML = `
944
+ <div class="palette-modal-card" style="max-width:440px;">
945
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
946
+ <span style="font-weight:700; font-size:0.9rem; color:var(--text-main);">Keyboard Shortcuts</span>
947
+ <button class="secondary" onclick="App.hideShortcutsModal()">✕</button>
948
+ </div>
949
+ <div class="shortcuts-grid">
950
+ <div class="shortcut-row"><span>Quick Find & Jump</span><kbd>⌘K / Ctrl+K</kbd></div>
951
+ <div class="shortcut-row"><span>Fit / Center Canvas</span><kbd>F</kbd></div>
952
+ <div class="shortcut-row"><span>Pause / Resume Physics</span><kbd>Space</kbd></div>
953
+ <div class="shortcut-row"><span>Cycle Layout Modes</span><kbd>L</kbd></div>
954
+ <div class="shortcut-row"><span>Cycle Color Themes</span><kbd>T</kbd></div>
955
+ <div class="shortcut-row"><span>Sidebar Tabs 1–4</span><kbd>1 · 2 · 3 · 4</kbd></div>
956
+ <div class="shortcut-row"><span>Clear / Close Modals</span><kbd>Esc</kbd></div>
957
+ </div>
958
+ </div>
959
+ `;
960
+ document.body.appendChild(modal);
961
+ modal.addEventListener("click", e => {
962
+ if (e.target === modal) this.hideShortcutsModal();
963
+ });
964
+ }
965
+ modal.style.display = modal.style.display === "flex" ? "none" : "flex";
966
+ }
967
+
968
+ static hideShortcutsModal() {
969
+ const modal = document.getElementById("shortcuts-modal");
970
+ if (modal) modal.style.display = "none";
971
+ }
972
+
973
+ static exportZip() {
974
+ CodeUIAPI.exportZip();
975
+ }
976
+
977
+ static setupGlobalClickListener() {
978
+ document.addEventListener("click", e => {
979
+ const isInsideFilter = e.target.closest("#symbol-filter-popover, #btn-toggle-symbol-filter-popover");
980
+ if (!isInsideFilter) {
981
+ App.closeSymbolFilterPopover();
982
+ }
983
+
984
+ if (e.target.closest("button, select, input, textarea, a, .palette-modal-card, .minimap-container, .sim-controls, .zoom-controls, .shortcuts-grid, .symbol-filter-popover")) {
985
+ return;
986
+ }
987
+ if (e.target.tagName !== "CANVAS") {
988
+ if (appState.selectedFileNodeId || appState.focusedNeighborhood) {
989
+ window.mainCanvas?.clearNeighborhoodIsolation();
990
+ Sidebar.clearSelection();
991
+ if (!appState.tracerActive) {
992
+ App.hideNodeHUD();
993
+ }
994
+ }
995
+ }
996
+ });
997
+ }
998
+
999
+ static toggleHeaderCompact() {
1000
+ const header = document.querySelector("header");
1001
+ const btn = document.getElementById("btn-toggle-header-compact");
1002
+ const icon = document.getElementById("header-toggle-icon");
1003
+ if (!header) return;
1004
+
1005
+ const isCompact = header.classList.toggle("compact-header");
1006
+
1007
+ if (btn) {
1008
+ btn.title = isCompact
1009
+ ? "Show Header Controls & Settings"
1010
+ : "Toggle Fullscreen Graph Mode (Hide Header Controls)";
1011
+ btn.classList.toggle("active", isCompact);
1012
+ }
1013
+
1014
+ if (icon) {
1015
+ icon.innerHTML = isCompact
1016
+ ? '<path d="m6 9 6 6 6-6"/>'
1017
+ : '<path d="m18 15-6-6-6 6"/>';
1018
+ }
1019
+
1020
+ setTimeout(() => {
1021
+ window.dispatchEvent(new Event("resize"));
1022
+ if (window.mainCanvas) {
1023
+ window.mainCanvas.setupCanvas();
1024
+ }
1025
+ }, 50);
1026
+ }
1027
+
1028
+ static showToast(message, type = "info") {
1029
+ const container = document.getElementById("toast-container");
1030
+ if (!container) return;
1031
+ const toast = document.createElement("div");
1032
+ toast.className = `toast ${type}`;
1033
+ toast.textContent = message;
1034
+ container.appendChild(toast);
1035
+ setTimeout(() => {
1036
+ toast.style.opacity = "0";
1037
+ toast.style.transform = "translateY(-10px)";
1038
+ setTimeout(() => toast.remove(), 300);
1039
+ }, 3200);
1040
+ }
1041
+ }
1042
+
1043
+ window.App = App;
1044
+ window.addEventListener("DOMContentLoaded", () => App.init());