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,383 @@
1
+ class MiniMap {
2
+ constructor(mainCanvas) {
3
+ this.mainCanvas = mainCanvas;
4
+ this.container = document.getElementById("graph-minimap-container");
5
+ this.dockBtn = document.getElementById("minimap-dock-btn");
6
+ this.canvas = document.getElementById("minimap-canvas");
7
+ this.countEl = document.getElementById("minimap-count");
8
+ this.dockCountEl = document.getElementById("minimap-dock-count");
9
+ this.tooltip = document.getElementById("minimap-tooltip");
10
+ this.ctx = this.canvas ? this.canvas.getContext("2d") : null;
11
+ this.cssWidth = 220;
12
+ this.cssHeight = 140;
13
+ this.isCollapsed = false;
14
+ this.isDragging = false;
15
+ this.lastMouse = { x: 0, y: 0 };
16
+ this.mapping = {
17
+ scale: 1,
18
+ offsetX: 0,
19
+ offsetY: 0,
20
+ vfX: 0,
21
+ vfY: 0,
22
+ vfW: 0,
23
+ vfH: 0,
24
+ boundsMinX: 0,
25
+ boundsMaxX: 0,
26
+ boundsMinY: 0,
27
+ boundsMaxY: 0
28
+ };
29
+
30
+ if (this.canvas) {
31
+ this.setupHeaderToggle();
32
+ this.resizeCanvas();
33
+ this.restoreState();
34
+ }
35
+ }
36
+
37
+ setupHeaderToggle() {
38
+ const header = this.container ? this.container.querySelector(".minimap-header") : null;
39
+ if (header) {
40
+ header.style.cursor = "pointer";
41
+ header.addEventListener("click", (e) => {
42
+ e.stopPropagation();
43
+ this.isCollapsed = !this.isCollapsed;
44
+ const wrapper = this.container.querySelector(".minimap-canvas-wrapper");
45
+ if (wrapper) {
46
+ wrapper.style.display = this.isCollapsed ? "none" : "block";
47
+ }
48
+ if (!this.isCollapsed) {
49
+ this.update();
50
+ }
51
+ });
52
+ }
53
+ }
54
+
55
+ restoreState() {
56
+ this.isCollapsed = false;
57
+ if (this.container) this.container.style.display = "flex";
58
+ if (this.dockBtn) this.dockBtn.style.display = "none";
59
+ }
60
+
61
+ collapse() {
62
+ this.isCollapsed = false;
63
+ if (this.container) this.container.style.display = "flex";
64
+ }
65
+
66
+ expand() {
67
+ this.isCollapsed = false;
68
+ if (this.container) this.container.style.display = "flex";
69
+ if (this.dockBtn) this.dockBtn.style.display = "none";
70
+ this.resizeCanvas();
71
+ this.update();
72
+ }
73
+
74
+ resizeCanvas() {
75
+ if (!this.canvas) return;
76
+ const dpr = window.devicePixelRatio || 1;
77
+ this.canvas.width = this.cssWidth * dpr;
78
+ this.canvas.height = this.cssHeight * dpr;
79
+ this.ctx.setTransform(1, 0, 0, 1, 0, 0);
80
+ this.ctx.scale(dpr, dpr);
81
+ }
82
+
83
+ getMinimapPos(e) {
84
+ const rect = this.canvas.getBoundingClientRect();
85
+ return {
86
+ x: e.clientX - rect.left,
87
+ y: e.clientY - rect.top
88
+ };
89
+ }
90
+
91
+ isInsideViewfinder(x, y) {
92
+ const { vfX, vfY, vfW, vfH } = this.mapping;
93
+ const hitPad = 4;
94
+ return (
95
+ x >= vfX - hitPad &&
96
+ x <= vfX + vfW + hitPad &&
97
+ y >= vfY - hitPad &&
98
+ y <= vfY + vfH + hitPad
99
+ );
100
+ }
101
+
102
+ setupEvents() {
103
+ if (!this.canvas) return;
104
+
105
+ const onDown = (clientX, clientY) => {
106
+ const rect = this.canvas.getBoundingClientRect();
107
+ const posX = clientX - rect.left;
108
+ const posY = clientY - rect.top;
109
+
110
+ if (this.isInsideViewfinder(posX, posY)) {
111
+ this.isDragging = true;
112
+ this.lastMouse = { x: clientX, y: clientY };
113
+ this.canvas.style.cursor = "grabbing";
114
+ } else {
115
+ const worldX = (posX - this.mapping.offsetX) / this.mapping.scale;
116
+ const worldY = (posY - this.mapping.offsetY) / this.mapping.scale;
117
+ this.mainCanvas.panTo(worldX, worldY, false);
118
+ this.isDragging = true;
119
+ this.lastMouse = { x: clientX, y: clientY };
120
+ this.canvas.style.cursor = "grabbing";
121
+ }
122
+ };
123
+
124
+ const onMove = (clientX, clientY) => {
125
+ if (this.isDragging) {
126
+ const dx = (clientX - this.lastMouse.x) / this.mapping.scale;
127
+ const dy = (clientY - this.lastMouse.y) / this.mapping.scale;
128
+ this.mainCanvas.transform.x -= dx * this.mainCanvas.transform.k;
129
+ this.mainCanvas.transform.y -= dy * this.mainCanvas.transform.k;
130
+ this.lastMouse = { x: clientX, y: clientY };
131
+ if (this.tooltip) this.tooltip.style.display = "none";
132
+ return;
133
+ }
134
+
135
+ const rect = this.canvas.getBoundingClientRect();
136
+ const posX = clientX - rect.left;
137
+ const posY = clientY - rect.top;
138
+
139
+ if (this.isInsideViewfinder(posX, posY)) {
140
+ this.canvas.style.cursor = "grab";
141
+ } else {
142
+ this.canvas.style.cursor = "crosshair";
143
+ }
144
+
145
+ if (this.tooltip) {
146
+ let hoveredNode = null;
147
+ const nodes = this.mainCanvas.nodes || [];
148
+ for (let i = nodes.length - 1; i >= 0; i--) {
149
+ const n = nodes[i];
150
+ const nx = n.x * this.mapping.scale + this.mapping.offsetX;
151
+ const ny = n.y * this.mapping.scale + this.mapping.offsetY;
152
+ const d2 = (nx - posX) * (nx - posX) + (ny - posY) * (ny - posY);
153
+ if (d2 <= 49) {
154
+ hoveredNode = n;
155
+ break;
156
+ }
157
+ }
158
+ if (hoveredNode) {
159
+ this.tooltip.textContent = hoveredNode.label || hoveredNode.id;
160
+ const tipX = Math.max(6, Math.min(posX + 8, this.cssWidth - 90));
161
+ const tipY = Math.max(4, posY - 26);
162
+ this.tooltip.style.left = `${tipX}px`;
163
+ this.tooltip.style.top = `${tipY}px`;
164
+ this.tooltip.style.display = "block";
165
+ } else {
166
+ this.tooltip.style.display = "none";
167
+ }
168
+ }
169
+ };
170
+
171
+ const onUp = () => {
172
+ this.isDragging = false;
173
+ this.canvas.style.cursor = "crosshair";
174
+ };
175
+
176
+ this.canvas.addEventListener("mousedown", e => {
177
+ e.preventDefault();
178
+ onDown(e.clientX, e.clientY);
179
+ });
180
+
181
+ window.addEventListener("mousemove", e => {
182
+ if (this.isDragging) {
183
+ onMove(e.clientX, e.clientY);
184
+ }
185
+ });
186
+
187
+ this.canvas.addEventListener("mousemove", e => {
188
+ if (!this.isDragging) {
189
+ onMove(e.clientX, e.clientY);
190
+ }
191
+ });
192
+
193
+ window.addEventListener("mouseup", onUp);
194
+ this.canvas.addEventListener("mouseleave", () => {
195
+ if (!this.isDragging && this.tooltip) {
196
+ this.tooltip.style.display = "none";
197
+ }
198
+ });
199
+
200
+ this.canvas.addEventListener("wheel", e => {
201
+ e.preventDefault();
202
+ const factor = e.deltaY < 0 ? 1.15 : 0.85;
203
+ this.mainCanvas.zoomBy(factor);
204
+ }, { passive: false });
205
+
206
+ this.canvas.addEventListener("touchstart", e => {
207
+ if (e.touches.length === 1) {
208
+ e.preventDefault();
209
+ onDown(e.touches[0].clientX, e.touches[0].clientY);
210
+ }
211
+ }, { passive: false });
212
+
213
+ this.canvas.addEventListener("touchmove", e => {
214
+ if (e.touches.length === 1) {
215
+ e.preventDefault();
216
+ onMove(e.touches[0].clientX, e.touches[0].clientY);
217
+ }
218
+ }, { passive: false });
219
+
220
+ this.canvas.addEventListener("touchend", onUp);
221
+ this.canvas.addEventListener("touchcancel", onUp);
222
+ }
223
+
224
+ update() {
225
+ if (!this.mainCanvas || !this.mainCanvas.nodes) return;
226
+ const nodes = this.mainCanvas.nodes;
227
+ const modeLabel = appState.graphMode === "files" ? "files" : "symbols";
228
+ const countText = `${nodes.length} ${modeLabel}`;
229
+ if (this.countEl && this.countEl.textContent !== countText) {
230
+ this.countEl.textContent = countText;
231
+ }
232
+ if (this.dockCountEl && this.dockCountEl.textContent !== `(${countText})`) {
233
+ this.dockCountEl.textContent = `(${countText})`;
234
+ }
235
+ if (this.isCollapsed || !this.ctx || !this.canvas) return;
236
+
237
+ const mainContainer = document.getElementById("canvas-container");
238
+ const mainW = mainContainer ? mainContainer.clientWidth || 800 : 800;
239
+ const mainH = mainContainer ? mainContainer.clientHeight || 600 : 600;
240
+ const k = this.mainCanvas.transform ? this.mainCanvas.transform.k || 1 : 1;
241
+ const tx = this.mainCanvas.transform ? this.mainCanvas.transform.x || 0 : 0;
242
+ const ty = this.mainCanvas.transform ? this.mainCanvas.transform.y || 0 : 0;
243
+
244
+ const visMinX = -tx / k;
245
+ const visMinY = -ty / k;
246
+ const visMaxX = (mainW - tx) / k;
247
+ const visMaxY = (mainH - ty) / k;
248
+
249
+ let nMinX = Infinity, nMaxX = -Infinity, nMinY = Infinity, nMaxY = -Infinity;
250
+ for (let i = 0; i < nodes.length; i++) {
251
+ const n = nodes[i];
252
+ if (isFinite(n.x) && isFinite(n.y)) {
253
+ if (n.x < nMinX) nMinX = n.x;
254
+ if (n.x > nMaxX) nMaxX = n.x;
255
+ if (n.y < nMinY) nMinY = n.y;
256
+ if (n.y > nMaxY) nMaxY = n.y;
257
+ }
258
+ }
259
+ if (nMinX === Infinity || nodes.length === 0) {
260
+ nMinX = -200; nMaxX = 200; nMinY = -150; nMaxY = 150;
261
+ }
262
+
263
+ const pad = 40;
264
+ const boundsMinX = Math.min(nMinX - pad, isFinite(visMinX) ? visMinX : -200);
265
+ const boundsMaxX = Math.max(nMaxX + pad, isFinite(visMaxX) ? visMaxX : 200);
266
+ const boundsMinY = Math.min(nMinY - pad, isFinite(visMinY) ? visMinY : -150);
267
+ const boundsMaxY = Math.max(nMaxY + pad, isFinite(visMaxY) ? visMaxY : 150);
268
+
269
+ const spanX = Math.max(80, boundsMaxX - boundsMinX);
270
+ const spanY = Math.max(60, boundsMaxY - boundsMinY);
271
+
272
+ const scaleX = (this.cssWidth - 14) / spanX;
273
+ const scaleY = (this.cssHeight - 14) / spanY;
274
+ const scale = Math.max(0.001, Math.min(scaleX, scaleY));
275
+
276
+ const midX = (boundsMinX + boundsMaxX) / 2;
277
+ const midY = (boundsMinY + boundsMaxY) / 2;
278
+
279
+ const offsetX = this.cssWidth / 2 - midX * scale;
280
+ const offsetY = this.cssHeight / 2 - midY * scale;
281
+
282
+ const vfX = isFinite(visMinX) ? visMinX * scale + offsetX : 0;
283
+ const vfY = isFinite(visMinY) ? visMinY * scale + offsetY : 0;
284
+ const vfW = isFinite(visMaxX) && isFinite(visMinX) ? (visMaxX - visMinX) * scale : this.cssWidth;
285
+ const vfH = isFinite(visMaxY) && isFinite(visMinY) ? (visMaxY - visMinY) * scale : this.cssHeight;
286
+
287
+ this.mapping = {
288
+ scale, offsetX, offsetY,
289
+ vfX, vfY, vfW, vfH,
290
+ boundsMinX, boundsMaxX, boundsMinY, boundsMaxY
291
+ };
292
+
293
+ this.ctx.clearRect(0, 0, this.cssWidth, this.cssHeight);
294
+
295
+ const themeColors = window.ThemeManager ? window.ThemeManager.getCanvasColors() : null;
296
+ const dotsColor = themeColors ? themeColors.minimapDots : "rgba(51, 65, 85, 0.35)";
297
+ const linksColor = themeColors ? themeColors.minimapLinks : "rgba(71, 85, 105, 0.35)";
298
+ const vfBg = themeColors ? themeColors.minimapViewfinder : "rgba(56, 189, 248, 0.12)";
299
+ const vfBorder = themeColors ? themeColors.minimapViewfinderBorder : "#38bdf8";
300
+
301
+ this.ctx.fillStyle = dotsColor;
302
+ for (let gx = 10; gx < this.cssWidth; gx += 20) {
303
+ for (let gy = 10; gy < this.cssHeight; gy += 20) {
304
+ this.ctx.fillRect(gx, gy, 1, 1);
305
+ }
306
+ }
307
+
308
+ const links = this.mainCanvas.links || [];
309
+ this.ctx.strokeStyle = linksColor;
310
+ this.ctx.lineWidth = 0.8;
311
+ this.ctx.beginPath();
312
+ for (let i = 0; i < links.length; i++) {
313
+ const l = links[i];
314
+ const sx = l.source.x * scale + offsetX;
315
+ const sy = l.source.y * scale + offsetY;
316
+ const targetX = l.target.x * scale + offsetX;
317
+ const targetY = l.target.y * scale + offsetY;
318
+ this.ctx.moveTo(sx, sy);
319
+ this.ctx.lineTo(targetX, targetY);
320
+ }
321
+ this.ctx.stroke();
322
+
323
+ const selNodeId = appState.selectedFileNodeId;
324
+ const unusedNodeIds = appState.unusedHighlightMode ? appState.getUnusedNodeIds() : null;
325
+
326
+ for (let i = 0; i < nodes.length; i++) {
327
+ const n = nodes[i];
328
+ const nx = n.x * scale + offsetX;
329
+ const ny = n.y * scale + offsetY;
330
+ const isSelected = selNodeId && n.id === selNodeId;
331
+
332
+ let color = appState.colorMode === "feature"
333
+ ? appState.getFeatureColor(n.feature)
334
+ : (appState.LAYER_COLORS[n.layer] || "#0284c7");
335
+
336
+ if (unusedNodeIds && unusedNodeIds.has(n.id) && !isSelected) {
337
+ color = "#94a3b8";
338
+ }
339
+
340
+ const r = isSelected ? 4 : 2.5;
341
+ this.ctx.beginPath();
342
+ this.ctx.arc(nx, ny, r, 0, Math.PI * 2);
343
+ this.ctx.fillStyle = color;
344
+ this.ctx.fill();
345
+
346
+ if (isSelected) {
347
+ this.ctx.beginPath();
348
+ this.ctx.arc(nx, ny, r + 2.5, 0, Math.PI * 2);
349
+ this.ctx.strokeStyle = vfBorder;
350
+ this.ctx.lineWidth = 1.5;
351
+ this.ctx.stroke();
352
+ }
353
+ }
354
+
355
+ this.ctx.fillStyle = vfBg;
356
+ this.ctx.fillRect(vfX, vfY, vfW, vfH);
357
+ this.ctx.strokeStyle = vfBorder;
358
+ this.ctx.lineWidth = 1.5;
359
+ this.ctx.strokeRect(vfX, vfY, vfW, vfH);
360
+
361
+ const cornerLen = Math.min(6, Math.max(2, Math.min(vfW, vfH) / 3));
362
+ if (cornerLen > 2) {
363
+ this.ctx.strokeStyle = "#ffffff";
364
+ this.ctx.lineWidth = 2;
365
+ this.ctx.beginPath();
366
+ this.ctx.moveTo(vfX, vfY + cornerLen);
367
+ this.ctx.lineTo(vfX, vfY);
368
+ this.ctx.lineTo(vfX + cornerLen, vfY);
369
+ this.ctx.moveTo(vfX + vfW - cornerLen, vfY);
370
+ this.ctx.lineTo(vfX + vfW, vfY);
371
+ this.ctx.lineTo(vfX + vfW, vfY + cornerLen);
372
+ this.ctx.moveTo(vfX, vfY + vfH - cornerLen);
373
+ this.ctx.lineTo(vfX, vfY + vfH);
374
+ this.ctx.lineTo(vfX + cornerLen, vfY + vfH);
375
+ this.ctx.moveTo(vfX + vfW - cornerLen, vfY + vfH);
376
+ this.ctx.lineTo(vfX + vfW, vfY + vfH);
377
+ this.ctx.lineTo(vfX + vfW, vfY + vfH - cornerLen);
378
+ this.ctx.stroke();
379
+ }
380
+ }
381
+ }
382
+
383
+ window.MiniMap = MiniMap;
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Explorer Sidebar Controller for CodeUI.
3
+ */
4
+ class Sidebar {
5
+ static currentTab = "files";
6
+ static collapsed = true;
7
+
8
+ static init() {
9
+ this.collapsed = true;
10
+ const sidebar = document.getElementById("sidebar");
11
+ const dockBtn = document.getElementById("btn-dock-sidebar");
12
+ if (sidebar) sidebar.classList.add("collapsed");
13
+ if (dockBtn) dockBtn.style.display = "flex";
14
+ if (window.mainCanvas) {
15
+ setTimeout(() => window.mainCanvas.handleResize(), 100);
16
+ }
17
+ }
18
+
19
+ static switchTab(tabName) {
20
+ this.currentTab = tabName;
21
+ document.querySelectorAll(".sidebar-tabs .tab-btn").forEach(btn => {
22
+ const active = btn.getAttribute("onclick")?.includes(`'${tabName}'`);
23
+ btn.classList.toggle("active", !!active);
24
+ });
25
+ this.renderCurrentTab();
26
+ }
27
+
28
+ static toggleSidebar() {
29
+ this.collapsed = !this.collapsed;
30
+ const sidebar = document.getElementById("sidebar");
31
+ const dockBtn = document.getElementById("btn-dock-sidebar");
32
+ if (sidebar) sidebar.classList.toggle("collapsed", this.collapsed);
33
+ if (dockBtn) dockBtn.style.display = this.collapsed ? "flex" : "none";
34
+ if (window.mainCanvas) {
35
+ setTimeout(() => window.mainCanvas.handleResize(), 200);
36
+ }
37
+ }
38
+
39
+ static renderCurrentTab() {
40
+ const container = document.getElementById("sidebar-tab-content");
41
+ if (!container || !window.appState) return;
42
+
43
+ const q = (appState.searchQuery || "").toLowerCase();
44
+ const graph = appState.rawGraph || {};
45
+
46
+ if (this.currentTab === "files") {
47
+ const files = (graph.files || []).filter(f => !q || f.toLowerCase().includes(q));
48
+ if (files.length === 0) {
49
+ container.innerHTML = `<div class="empty-state" style="padding:16px; text-align:center; color:var(--text-dim); font-size:0.75rem;">No files found matching filter</div>`;
50
+ return;
51
+ }
52
+ const info = graph.files_info || {};
53
+ let html = `<div class="file-card-list">`;
54
+ files.forEach(f => {
55
+ const meta = info[f] || {};
56
+ const layer = meta.layer || "other";
57
+ const isSelected = appState.selectedFileNodeId === f;
58
+ html += `
59
+ <div class="sidebar-item file-card ${isSelected ? 'selected' : ''}" data-id="${f}" onclick="Sidebar.onFileCardClick('${f}')">
60
+ <div class="file-title" style="display:flex; align-items:center; gap:6px; font-weight:600; font-size:0.8rem; color:var(--text-main);">
61
+ <span class="file-icon">📄</span>
62
+ <span class="file-name" title="${f}">${f.split('/').pop()}</span>
63
+ </div>
64
+ <div class="file-meta" style="display:flex; justify-content:space-between; align-items:center; margin-top:4px;">
65
+ <span class="badge layer-${layer}" style="font-size:0.65rem; padding:1px 5px; border-radius:4px; text-transform:uppercase; background:var(--bg-surface); border:1px solid var(--border-subtle); color:var(--text-muted);">${layer}</span>
66
+ <span class="file-path" style="font-size:0.68rem; color:var(--text-dim); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:140px;" title="${f}">${f}</span>
67
+ </div>
68
+ </div>
69
+ `;
70
+ });
71
+ html += `</div>`;
72
+ container.innerHTML = html;
73
+ } else if (this.currentTab === "features") {
74
+ const info = graph.files_info || {};
75
+ const featureMap = {};
76
+ (graph.files || []).forEach(f => {
77
+ if (q && !f.toLowerCase().includes(q)) return;
78
+ const feat = info[f]?.feature || "root";
79
+ if (!featureMap[feat]) featureMap[feat] = [];
80
+ featureMap[feat].push(f);
81
+ });
82
+
83
+ const entries = Object.entries(featureMap);
84
+ if (entries.length === 0) {
85
+ container.innerHTML = `<div class="empty-state" style="padding:16px; text-align:center; color:var(--text-dim); font-size:0.75rem;">No features found</div>`;
86
+ return;
87
+ }
88
+
89
+ let html = `<div class="feature-group-list">`;
90
+ entries.forEach(([feat, fList]) => {
91
+ const featColor = appState.getFeatureColor(feat);
92
+ html += `
93
+ <div class="feature-group" style="margin-bottom:12px;">
94
+ <div class="feature-header" style="display:flex; align-items:center; justify-content:space-between; padding:6px 8px; background:var(--bg-surface); border:1px solid var(--border-subtle); border-radius:6px; margin-bottom:4px; font-size:0.75rem; font-weight:700;">
95
+ <div style="display:flex; align-items:center; gap:6px;">
96
+ <span class="legend-shape" style="background:${featColor}; display:inline-block; width:10px; height:10px; border-radius:50%;"></span>
97
+ <span class="feature-name" style="color:var(--text-main);">${feat}</span>
98
+ </div>
99
+ <span class="count-badge" style="background:var(--bg-base); padding:1px 6px; border-radius:10px; font-size:0.68rem; color:var(--text-muted);">${fList.length}</span>
100
+ </div>
101
+ <div class="feature-files">
102
+ ${fList.map(f => `
103
+ <div class="sidebar-item file-card" data-id="${f}" style="padding:4px 8px; margin:2px 0;" onclick="Sidebar.onFileCardClick('${f}')">
104
+ <span class="file-name" style="font-size:0.75rem; color:var(--text-main);">${f.split('/').pop()}</span>
105
+ </div>
106
+ `).join('')}
107
+ </div>
108
+ </div>
109
+ `;
110
+ });
111
+ html += `</div>`;
112
+ container.innerHTML = html;
113
+ } else if (this.currentTab === "defects") {
114
+ const allFindings = appState.defectsData || [];
115
+ const apiDriftCount = allFindings.filter(d => d.rule_id === "api_drift").length;
116
+ const defectsCount = allFindings.length - apiDriftCount;
117
+
118
+ if (!this.defectSubFilter) this.defectSubFilter = "all";
119
+
120
+ const defects = allFindings.filter(d => {
121
+ if (this.defectSubFilter === "api_drift" && d.rule_id !== "api_drift") return false;
122
+ if (this.defectSubFilter === "defects" && d.rule_id === "api_drift") return false;
123
+ return !q || d.rule_id.toLowerCase().includes(q) || (d.message || "").toLowerCase().includes(q);
124
+ });
125
+
126
+ let html = `
127
+ <div class="defect-filter-bar" style="display:flex; gap:4px; margin-bottom:8px; padding-bottom:6px; border-bottom:1px solid var(--border-subtle); flex-wrap:wrap; align-items:center;">
128
+ <button class="secondary ${this.defectSubFilter === 'all' ? 'active' : ''}" style="font-size:0.68rem; padding:2px 6px;" onclick="Sidebar.setDefectSubFilter('all')">All (${allFindings.length})</button>
129
+ <button class="secondary ${this.defectSubFilter === 'defects' ? 'active' : ''}" style="font-size:0.68rem; padding:2px 6px;" onclick="Sidebar.setDefectSubFilter('defects')">Defects (${defectsCount})</button>
130
+ <button class="secondary ${this.defectSubFilter === 'api_drift' ? 'active' : ''}" style="font-size:0.68rem; padding:2px 6px; color:var(--accent);" onclick="Sidebar.setDefectSubFilter('api_drift')">🌊 API Drift (${apiDriftCount})</button>
131
+ <button class="secondary" style="font-size:0.65rem; padding:2px 6px; color:var(--accent); border-color:var(--border-subtle); margin-left:auto;" onclick="App.setApiBaseline()" title="Capture public symbols baseline snapshot">📸 Set Baseline</button>
132
+ </div>
133
+ `;
134
+
135
+ if (defects.length === 0) {
136
+ html += `<div class="empty-state" style="padding:16px; text-align:center; color:var(--text-dim); font-size:0.75rem;">No findings or API drift issues detected under current filter</div>`;
137
+ container.innerHTML = html;
138
+ return;
139
+ }
140
+
141
+ html += `<div class="defects-card-list">`;
142
+ defects.forEach(d => {
143
+ const loc = d.location || {};
144
+ const fId = loc.file_id || "unknown";
145
+ const line = loc.start_line || 1;
146
+ const isDrift = d.rule_id === "api_drift";
147
+ const badgeColor = isDrift ? "var(--accent)" : "var(--danger)";
148
+ const badgeLabel = isDrift ? "🌊 API Drift" : `⚠️ ${d.rule_id}`;
149
+ const safeMsg = (d.message || "").replace(/'/g, "\\'");
150
+ const symbolId = d.symbol_id || "";
151
+ html += `
152
+ <div class="sidebar-item defect-card" style="padding:8px; border:1px solid ${isDrift ? 'var(--accent)' : 'var(--border-subtle)'}; border-radius:6px; margin-bottom:6px; background:var(--bg-surface); cursor:pointer;" onclick="Sidebar.openDefectLocation('${fId}', ${line}, '${symbolId}', '${safeMsg}')">
153
+ <div class="defect-header" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:4px;">
154
+ <span class="defect-rule" style="font-weight:700; font-size:0.75rem; color:${badgeColor};">${badgeLabel}</span>
155
+ <span class="defect-loc" style="font-size:0.68rem; color:var(--text-dim);">${fId.split('/').pop()}:${line}</span>
156
+ </div>
157
+ <div class="defect-msg" style="font-size:0.72rem; color:var(--text-muted); display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">${d.message}</div>
158
+ ${d.fix_hint ? `<div style="font-size:0.65rem; color:var(--accent); margin-top:4px; font-style:italic;">💡 ${d.fix_hint}</div>` : ''}
159
+ </div>
160
+ `;
161
+ });
162
+ html += `</div>`;
163
+ container.innerHTML = html;
164
+ } else if (this.currentTab === "stats") {
165
+ const filesCount = (graph.files || []).length;
166
+ const symbolsCount = (graph.symbols || []).length;
167
+ const edgesCount = (graph.edges || []).length;
168
+ const allFindings = appState.defectsData || [];
169
+ const defectsCount = allFindings.filter(d => d.rule_id !== "api_drift").length;
170
+ const apiDriftCount = allFindings.filter(d => d.rule_id === "api_drift").length;
171
+
172
+ const layerCounts = {};
173
+ const info = graph.files_info || {};
174
+ (graph.files || []).forEach(f => {
175
+ const l = info[f]?.layer || "other";
176
+ layerCounts[l] = (layerCounts[l] || 0) + 1;
177
+ });
178
+
179
+ let html = `
180
+ <div class="stats-panel" style="padding:8px;">
181
+ <div class="stats-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:8px;">
182
+ <div class="stat-card" style="background:var(--bg-surface); padding:8px; border-radius:6px; border:1px solid var(--border-subtle); text-align:center;">
183
+ <div class="stat-num" style="font-size:1.1rem; font-weight:800; color:var(--primary);">${filesCount}</div>
184
+ <div class="stat-label" style="font-size:0.68rem; color:var(--text-dim);">Files</div>
185
+ </div>
186
+ <div class="stat-card" style="background:var(--bg-surface); padding:8px; border-radius:6px; border:1px solid var(--border-subtle); text-align:center;">
187
+ <div class="stat-num" style="font-size:1.1rem; font-weight:800; color:var(--primary);">${symbolsCount}</div>
188
+ <div class="stat-label" style="font-size:0.68rem; color:var(--text-dim);">Symbols</div>
189
+ </div>
190
+ <div class="stat-card" style="background:var(--bg-surface); padding:8px; border-radius:6px; border:1px solid var(--border-subtle); text-align:center;">
191
+ <div class="stat-num" style="font-size:1.1rem; font-weight:800; color:var(--primary);">${edgesCount}</div>
192
+ <div class="stat-label" style="font-size:0.68rem; color:var(--text-dim);">Edges</div>
193
+ </div>
194
+ <div class="stat-card" style="background:var(--bg-surface); padding:8px; border-radius:6px; border:1px solid var(--border-subtle); text-align:center;">
195
+ <div class="stat-num" style="font-size:1.1rem; font-weight:800; color:var(--danger);">${defectsCount}</div>
196
+ <div class="stat-label" style="font-size:0.68rem; color:var(--text-dim);">Defects</div>
197
+ </div>
198
+ <div class="stat-card" style="background:var(--bg-surface); padding:8px; border-radius:6px; border:1px solid var(--border-subtle); grid-column: span 2; text-align:center;">
199
+ <div class="stat-num" style="font-size:1.1rem; font-weight:800; color:var(--accent);">${apiDriftCount}</div>
200
+ <div class="stat-label" style="font-size:0.68rem; color:var(--text-dim);">Public API Drift Warnings</div>
201
+ </div>
202
+ </div>
203
+ <div style="margin-top:16px; font-weight:700; font-size:0.8rem; color:var(--text-muted);">Layers Distribution</div>
204
+ <div class="layers-list" style="margin-top:8px;">
205
+ `;
206
+ Object.entries(layerCounts).forEach(([l, count]) => {
207
+ const color = appState.LAYER_COLORS[l] || "var(--text-dim)";
208
+ html += `
209
+ <div class="layer-stat-row" style="display:flex; justify-content:space-between; align-items:center; padding:4px 0; font-size:0.75rem;">
210
+ <span><span style="display:inline-block; width:8px; height:8px; border-radius:50%; background:${color}; margin-right:6px;"></span>${l}</span>
211
+ <span style="color:var(--text-muted); font-weight:600;">${count} files</span>
212
+ </div>
213
+ `;
214
+ });
215
+ html += `</div></div>`;
216
+ container.innerHTML = html;
217
+ }
218
+ }
219
+
220
+ static setDefectSubFilter(sub) {
221
+ this.defectSubFilter = sub;
222
+ this.renderCurrentTab();
223
+ }
224
+
225
+ static highlightSelectedCard(nodeId) {
226
+ this.clearSelection();
227
+ if (!nodeId) return;
228
+ const cards = document.querySelectorAll(`.sidebar-item[data-id="${nodeId}"]`);
229
+ cards.forEach(card => {
230
+ card.classList.add("selected");
231
+ card.scrollIntoView({ block: "nearest", behavior: "smooth" });
232
+ });
233
+ }
234
+
235
+ static clearSelection() {
236
+ document.querySelectorAll(".sidebar-item.selected").forEach(c => c.classList.remove("selected"));
237
+ }
238
+
239
+ static onFileCardClick(fileId) {
240
+ appState.selectedFileNodeId = fileId;
241
+ this.highlightSelectedCard(fileId);
242
+ if (window.mainCanvas) {
243
+ window.mainCanvas.selectAndCenterNode(fileId);
244
+ }
245
+ }
246
+
247
+ static openDefectLocation(fileId, line = 1, symbol = null, message = "") {
248
+ let targetSymbol = symbol && symbol.length > 0 ? symbol : null;
249
+ if (!targetSymbol && appState.rawGraph?.symbols) {
250
+ const fileSyms = appState.rawGraph.symbols.filter(s => {
251
+ const sFile = s.location?.file_id;
252
+ return sFile === fileId || sFile?.endsWith("/" + fileId) || fileId.endsWith("/" + sFile);
253
+ });
254
+ const match = fileSyms.find(s =>
255
+ s.location &&
256
+ s.location.start_line <= line &&
257
+ (s.location.end_line || s.location.start_line) >= line
258
+ );
259
+ if (match) targetSymbol = match.id;
260
+ }
261
+
262
+ if (window.SubGraphManager) {
263
+ SubGraphManager.openWindow(fileId, targetSymbol, line, message, "graph");
264
+ }
265
+ }
266
+ }
267
+
268
+ window.Sidebar = Sidebar;