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,1208 @@
1
+ class SubGraphWindow {
2
+ constructor(fileId, targetSymbolId = null, targetLine = null, targetMessage = "", initialOffset = 30, initialTab = "graph") {
3
+ let clean = (fileId || "").replace(/^module::/, "").replace(/^\.\//, "").trim();
4
+ let symId = targetSymbolId;
5
+ if (clean.includes("::")) {
6
+ const parts = clean.split("::");
7
+ clean = parts[0];
8
+ if (!symId) symId = fileId;
9
+ }
10
+ this.fileId = clean;
11
+ this.targetSymbolId = symId;
12
+ this.targetLine = targetLine;
13
+ this.targetMessage = targetMessage;
14
+ this.initialTab = initialTab;
15
+ this.winId = `subgraph-win-${this.fileId.replace(/[^a-zA-Z0-9]/g, "_")}`;
16
+ this.data = null;
17
+ this.fileContent = "";
18
+ this.canvasInstance = null;
19
+ this.offset = initialOffset;
20
+ this.showCrossUsage = false;
21
+ this.moveMode = false;
22
+ this.createDOM();
23
+ this.loadData();
24
+ }
25
+
26
+ createDOM() {
27
+ const existing = document.getElementById(this.winId);
28
+ if (existing) {
29
+ existing.remove();
30
+ }
31
+
32
+ const container = document.getElementById("canvas-container");
33
+ const win = document.createElement("div");
34
+ win.className = "subgraph-window";
35
+ win.id = this.winId;
36
+ win.style.left = `${this.offset}px`;
37
+ win.style.top = `${this.offset}px`;
38
+
39
+ const filename = this.fileId.split("/").pop();
40
+
41
+ win.innerHTML = `
42
+ <div class="subgraph-window-header" id="${this.winId}-header">
43
+ <div class="subgraph-window-title" title="${this.fileId}">
44
+ <span>📄</span>
45
+ <span class="subgraph-header-path" id="${this.winId}-header-path">${this.fileId}</span>
46
+ </div>
47
+ <div class="subgraph-nav">
48
+ <button class="subtab-btn active" onclick="SubGraphManager.switchTab('${this.fileId}', 'graph')">Symbols Graph</button>
49
+ <button class="subtab-btn" onclick="SubGraphManager.switchTab('${this.fileId}', 'usages')">Cross Usages</button>
50
+ <button class="subtab-btn" onclick="SubGraphManager.switchTab('${this.fileId}', 'code')">Code Editor</button>
51
+ </div>
52
+ <div class="subgraph-window-actions">
53
+ <button class="btn-move-window secondary" id="${this.winId}-move-btn" onclick="SubGraphManager.toggleMoveMode('${this.fileId}')" title="Toggle Move Mode to Drag Window" style="cursor:pointer; padding:2px 8px; font-weight:700;">✥ Move</button>
54
+ <button class="secondary" onclick="SubGraphManager.minimizeWindow('${this.fileId}')">_</button>
55
+ <button class="secondary" onclick="SubGraphManager.maximizeWindow('${this.fileId}')">⛶</button>
56
+ <button class="danger" onclick="SubGraphManager.closeWindow('${this.fileId}')">✕</button>
57
+ </div>
58
+ </div>
59
+
60
+ <div class="subgraph-panel active" id="${this.winId}-panel-graph" style="position:relative;">
61
+ <div class="subgraph-top-controls" style="position:absolute; top:8px; left:8px; z-index:15; display:flex; gap:6px; align-items:center;">
62
+ <button class="secondary" id="${this.winId}-btn-cross" onclick="SubGraphManager.toggleCrossUsage('${this.fileId}')" title="Toggle Related & Cross Usages" style="font-size:0.7rem; padding:2px 7px; font-weight:600; background:var(--bg-surface); border:1px solid var(--border-subtle); backdrop-filter:blur(6px);">🌐 Cross</button>
63
+ <button class="secondary" onclick="SubGraphManager.resetSubgraphCanvas('${this.fileId}')" title="Auto Fit View" style="font-size:0.7rem; padding:2px 7px; font-weight:600; background:var(--bg-surface); border:1px solid var(--border-subtle); backdrop-filter:blur(6px);">⛶ Fit</button>
64
+ </div>
65
+ <canvas id="${this.winId}-canvas"></canvas>
66
+ <div class="subgraph-canvas-bar">
67
+ <div class="subgraph-canvas-left">
68
+ <span id="${this.winId}-stat-symbols">0 symbols</span>
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <div class="subgraph-panel" id="${this.winId}-panel-usages">
74
+ <div class="usage-analysis-box" id="${this.winId}-usages-content">
75
+ <div style="color:var(--text-muted); text-align:center; padding:20px;">Analyzing incoming & outgoing references...</div>
76
+ </div>
77
+ </div>
78
+
79
+ <div class="subgraph-panel" id="${this.winId}-panel-code">
80
+ <div class="subgraph-editor-bar">
81
+ <div style="display:flex; align-items:center; gap:8px; margin-left:auto;">
82
+ <label class="subgraph-toggle-label" title="When enabled, saving writes directly to the project file on disk instead of in-memory override store">
83
+ <input type="checkbox" class="subgraph-toggle-disk-input" id="${this.winId}-toggle-disk" ${appState.directDiskWrite ? "checked" : ""} onchange="App.setDirectDiskEdit(this.checked)">
84
+ <span>💾 Direct Disk</span>
85
+ </label>
86
+ <button class="secondary" style="font-size:0.68rem; padding:2px 6px;" onclick="SubGraphManager.revertFile('${this.fileId}')">Revert</button>
87
+ <button class="success" style="font-size:0.68rem; padding:2px 8px;" onclick="SubGraphManager.saveFile('${this.fileId}')">Apply Edit</button>
88
+ </div>
89
+ </div>
90
+ <div class="editor-wrapper" id="${this.winId}-editor-wrapper">
91
+ <div class="line-numbers" id="${this.winId}-line-numbers"></div>
92
+ <div id="${this.winId}-symbol-highlight" class="symbol-highlight-overlay" style="display:none;"></div>
93
+ <div id="${this.winId}-error-highlight" class="error-highlight-overlay" style="display:none;"></div>
94
+ <textarea class="subgraph-textarea" id="${this.winId}-textarea" spellcheck="false"></textarea>
95
+ </div>
96
+ </div>
97
+ `;
98
+
99
+ container.appendChild(win);
100
+ this.setupInteractions(win);
101
+ }
102
+
103
+ setupInteractions(win) {
104
+ const header = document.getElementById(`${this.winId}-header`);
105
+ const moveBtn = document.getElementById(`${this.winId}-move-btn`);
106
+ let isDragging = false;
107
+ let startX, startY, origLeft, origTop;
108
+
109
+ const onDragStart = e => {
110
+ if (!this.moveMode) return;
111
+ if (e.target.closest("button") && e.target !== moveBtn) return;
112
+ if (e.target.closest("select") || e.target.closest("input")) return;
113
+ isDragging = true;
114
+ const clientX = (e.touches && e.touches.length > 0) ? e.touches[0].clientX : (e.clientX ?? 0);
115
+ const clientY = (e.touches && e.touches.length > 0) ? e.touches[0].clientY : (e.clientY ?? 0);
116
+ startX = clientX;
117
+ startY = clientY;
118
+ origLeft = win.offsetLeft;
119
+ origTop = win.offsetTop;
120
+ win.style.zIndex = ++SubGraphManager.topZ;
121
+ document.body.style.userSelect = "none";
122
+ };
123
+
124
+ const onDragMove = e => {
125
+ if (!isDragging) return;
126
+ const clientX = (e.touches && e.touches.length > 0) ? e.touches[0].clientX : (e.clientX ?? 0);
127
+ const clientY = (e.touches && e.touches.length > 0) ? e.touches[0].clientY : (e.clientY ?? 0);
128
+ const dx = clientX - startX;
129
+ const dy = clientY - startY;
130
+ win.style.left = `${Math.max(0, origLeft + dx)}px`;
131
+ win.style.top = `${Math.max(0, origTop + dy)}px`;
132
+ };
133
+
134
+ const onDragEnd = () => {
135
+ if (isDragging) {
136
+ isDragging = false;
137
+ document.body.style.userSelect = "";
138
+ }
139
+ };
140
+
141
+ header.addEventListener("mousedown", onDragStart);
142
+ header.addEventListener("touchstart", onDragStart, { passive: true });
143
+ window.addEventListener("mousemove", onDragMove);
144
+ window.addEventListener("touchmove", onDragMove, { passive: true });
145
+ window.addEventListener("mouseup", onDragEnd);
146
+ window.addEventListener("touchend", onDragEnd);
147
+
148
+ win.addEventListener("mousedown", () => {
149
+ win.style.zIndex = ++SubGraphManager.topZ;
150
+ });
151
+ }
152
+
153
+ async loadData() {
154
+ try {
155
+ this.data = await CodeUIAPI.getSubGraph(this.fileId, this.targetSymbolId);
156
+ const fileRes = await CodeUIAPI.getFile(this.fileId);
157
+ this.fileContent = fileRes.content || "";
158
+ if (fileRes.path && fileRes.path !== this.fileId && !fileRes.is_external) {
159
+ const oldFileId = this.fileId;
160
+ this.fileId = fileRes.path;
161
+ appState.activeSubgraphs.set(this.fileId, this);
162
+ appState.activeSubgraphs.set(oldFileId, this);
163
+ }
164
+ if (fileRes.resolved_symbol) this.targetSymbolId = this.targetSymbolId || fileRes.resolved_symbol;
165
+ if (fileRes.target_line) this.targetLine = this.targetLine || fileRes.target_line;
166
+ const isReallyExternal = Boolean(fileRes.is_external && this.data?.is_external);
167
+ if (isReallyExternal) {
168
+ this.isExternal = true;
169
+ const pathEl = document.getElementById(`${this.winId}-header-path`);
170
+ if (pathEl) {
171
+ pathEl.innerHTML = `<span class="badge shared" style="margin-right:6px; font-size:0.65rem;">🌐 External</span>${this.fileId}`;
172
+ }
173
+ const saveBtn = document.querySelector(`#${this.winId} button[onclick*="saveFile"]`);
174
+ if (saveBtn) {
175
+ saveBtn.outerHTML = `<span class="badge shared" style="font-size:0.68rem; padding:2px 8px; font-weight:600;">ReadOnly</span>`;
176
+ }
177
+ }
178
+ this.renderSymbolsGraph();
179
+ this.renderUsages();
180
+ this.renderEditor();
181
+ if (this.initialTab === "code" || (this.targetLine && !this.targetSymbolId)) {
182
+ SubGraphManager.switchTab(this.fileId, "code");
183
+ if (this.targetSymbolId) {
184
+ this.scrollToSymbol(this.targetSymbolId);
185
+ } else if (this.targetLine) {
186
+ this.highlightError(this.targetLine, this.targetMessage);
187
+ }
188
+ } else {
189
+ SubGraphManager.switchTab(this.fileId, "graph");
190
+ if (this.targetSymbolId && this.canvasInstance) {
191
+ const symName = this.targetSymbolId.split("::").pop();
192
+ const symNode = (this.canvasInstance.nodes || []).find(n =>
193
+ n.id === this.targetSymbolId || n.rawSymbol?.id === this.targetSymbolId || n.label === symName || n.id.endsWith("::" + symName)
194
+ );
195
+ if (symNode) {
196
+ this.canvasInstance.selectedNodeId = symNode.id;
197
+ this.canvasInstance.isolateNeighborhood(symNode.id);
198
+ if (this.updateSymbolBar) {
199
+ this.updateSymbolBar(symNode);
200
+ }
201
+ }
202
+ }
203
+ }
204
+ } catch (e) {
205
+ App.showToast(`Error loading ${this.fileId}: ${e.message}`, "error");
206
+ }
207
+ }
208
+
209
+ renderSymbolsGraph() {
210
+ const canvasId = `${this.winId}-canvas`;
211
+ const canvasEl = document.getElementById(canvasId);
212
+ if (!canvasEl) return;
213
+ if (this.canvasInstance && this.canvasInstance.canvas !== canvasEl) {
214
+ this.canvasInstance.destroy();
215
+ this.canvasInstance = null;
216
+ }
217
+ if (!this.canvasInstance) {
218
+ this.canvasInstance = new GraphCanvas(canvasId, false);
219
+ }
220
+
221
+ let localNodes = (this.data?.symbols || [])
222
+ .filter(s => s && s.kind !== "file")
223
+ .map(s => ({
224
+ id: s.id,
225
+ label: s.name,
226
+ kind: s.kind,
227
+ layer: this.data?.classification?.layer || "frontend",
228
+ feature: this.data?.classification?.feature || "subgraph",
229
+ radius: s.kind === "class" ? 14 : (s.kind === "function" ? 12 : 10),
230
+ rawSymbol: s,
231
+ isExternal: false,
232
+ fileId: this.fileId
233
+ }));
234
+
235
+ if (localNodes.length === 0) {
236
+ const fileName = this.fileId.split("/").pop();
237
+ localNodes.push({
238
+ id: this.fileId,
239
+ label: fileName,
240
+ kind: "file",
241
+ layer: this.data?.classification?.layer || "shared",
242
+ feature: this.data?.classification?.feature || "file",
243
+ radius: 14,
244
+ rawSymbol: { id: this.fileId, name: fileName, kind: "file" },
245
+ isExternal: false,
246
+ fileId: this.fileId
247
+ });
248
+ }
249
+
250
+ const localSymIds = new Set(localNodes.map(n => n.id));
251
+
252
+ let displayNodes = [...localNodes];
253
+ let displayLinks = [];
254
+
255
+ if (this.showCrossUsage) {
256
+ const rawExt = Array.isArray(this.data?.related_symbols) ? this.data.related_symbols : Object.values(this.data?.related_symbols || {});
257
+ const seenExtIds = new Set(localSymIds);
258
+ const externalNodes = [];
259
+ for (const s of rawExt) {
260
+ if (!s || !s.id || seenExtIds.has(s.id)) continue;
261
+ const fileForExt = s.file_id || s.location?.file_id || (s.id.includes("::") ? s.id.split("::")[0] : null);
262
+ if (!fileForExt || fileForExt.startsWith("module::")) continue;
263
+ if (fileForExt === this.fileId && s.kind !== "file") continue;
264
+ seenExtIds.add(s.id);
265
+ externalNodes.push({
266
+ id: s.id,
267
+ label: s.name || s.id.split("::").pop(),
268
+ kind: s.kind || "symbol",
269
+ layer: s.layer || "shared",
270
+ feature: s.feature || "external",
271
+ radius: s.kind === "class" ? 13 : (s.kind === "function" ? 11 : 9),
272
+ rawSymbol: s,
273
+ isExternal: true,
274
+ fileId: fileForExt
275
+ });
276
+ }
277
+ displayNodes = [...localNodes, ...externalNodes];
278
+ const allNodeIds = new Set(displayNodes.map(n => n.id));
279
+ const hasLocalFileNode = localNodes.some(n => n.id === this.fileId);
280
+ displayLinks = (this.data?.edges || []).filter(e => {
281
+ const srcMatch = allNodeIds.has(e.source_id) || (hasLocalFileNode && e.source_id === this.fileId);
282
+ const tgtMatch = allNodeIds.has(e.target_id) || (hasLocalFileNode && e.target_id === this.fileId);
283
+ return srcMatch && tgtMatch && e.source_id !== e.target_id;
284
+ }).map(e => ({
285
+ source: allNodeIds.has(e.source_id) ? e.source_id : this.fileId,
286
+ target: allNodeIds.has(e.target_id) ? e.target_id : this.fileId,
287
+ kind: e.kind,
288
+ weight: e.weight
289
+ }));
290
+ } else {
291
+ displayLinks = (this.data?.edges || []).filter(e => localSymIds.has(e.source_id) && localSymIds.has(e.target_id)).map(e => ({
292
+ source: e.source_id,
293
+ target: e.target_id,
294
+ kind: e.kind,
295
+ weight: e.weight
296
+ }));
297
+ }
298
+
299
+ this.canvasInstance.setData(displayNodes, displayLinks);
300
+ setTimeout(() => {
301
+ if (this.canvasInstance) this.canvasInstance.fitToScreen();
302
+ }, 50);
303
+
304
+ const updateSymbolBar = (node) => {
305
+ const statEl = document.getElementById(`${this.winId}-stat-symbols`);
306
+ if (!statEl) return;
307
+ if (node) {
308
+ const sym = node.rawSymbol || {};
309
+ const safeName = (node.label || "").replace(/'/g, "\\'");
310
+ const safeId = (node.id || "").replace(/'/g, "\\'");
311
+ if (node.isExternal) {
312
+ const safeFile = (node.fileId || "").replace(/'/g, "\\'");
313
+ const incomingEdgesToLocal = (this.data?.edges || []).filter(e => e.source_id === node.id && localSymIds.has(e.target_id));
314
+ const outgoingEdgesFromLocal = (this.data?.edges || []).filter(e => e.target_id === node.id && localSymIds.has(e.source_id));
315
+ let relInfo = "";
316
+ if (incomingEdgesToLocal.length > 0) {
317
+ relInfo += `<span class="badge warning" style="font-size:0.65rem; background:rgba(234,179,8,0.2); color:var(--warning);" title="Calls ${incomingEdgesToLocal.length} symbol(s) in this file">⚡ Calls this file (${incomingEdgesToLocal.length})</span> `;
318
+ }
319
+ if (outgoingEdgesFromLocal.length > 0) {
320
+ relInfo += `<span class="badge shared" style="font-size:0.65rem;" title="Called by ${outgoingEdgesFromLocal.length} symbol(s) in this file">➡️ Called by this file (${outgoingEdgesFromLocal.length})</span> `;
321
+ }
322
+ statEl.innerHTML = `
323
+ <span class="badge shared" style="background:var(--accent); color:var(--bg-base); font-weight:700;">🌐 External</span>
324
+ <span style="font-weight:700; color:var(--text-main);">${node.label}</span>
325
+ <span style="font-size:0.68rem; color:var(--text-dim);" title="${node.fileId}">(${node.fileId ? node.fileId.split('/').pop() : 'other file'})</span>
326
+ ${relInfo}
327
+ <button class="secondary" style="padding:2px 8px; font-size:0.68rem; margin-left:6px; font-weight:600;" onclick="SubGraphManager.openWindow('${safeFile}', '${safeId}', null, '', 'graph')">📂 Open ${node.fileId ? node.fileId.split('/').pop() : 'File'}</button>
328
+ <button class="success" style="padding:2px 8px; font-size:0.68rem; margin-left:6px; font-weight:600;" onclick="SubGraphManager.openWindow('${safeFile}', '${safeId}', null, '', 'code')">✏️ Edit Symbol</button>
329
+ `;
330
+ } else {
331
+ const affectedCallers = (this.data?.edges || []).filter(e => e.target_id === node.id && !localSymIds.has(e.source_id));
332
+ const outgoingCalls = (this.data?.edges || []).filter(e => e.source_id === node.id && !localSymIds.has(e.target_id));
333
+ let impactBadge = "";
334
+ if (affectedCallers.length > 0) {
335
+ impactBadge = `<span class="badge warning" style="margin-left:6px; font-size:0.68rem; background:rgba(234,179,8,0.2); color:var(--warning); border:1px solid rgba(234,179,8,0.4);" title="Modifying this symbol may affect ${affectedCallers.length} external caller node(s)">⚠️ ${affectedCallers.length} external caller(s) affected</span>`;
336
+ }
337
+ if (outgoingCalls.length > 0) {
338
+ impactBadge += ` <span class="badge shared" style="margin-left:4px; font-size:0.68rem;" title="Calls ${outgoingCalls.length} external dependencies">➡️ ${outgoingCalls.length} ext calls</span>`;
339
+ }
340
+ statEl.innerHTML = `
341
+ <span style="font-weight:700; color:var(--text-main);">${node.label}</span>
342
+ <span class="badge ${node.kind || 'other'}">${node.kind || 'symbol'}</span>
343
+ <button class="success" style="padding:2px 8px; font-size:0.68rem; margin-left:6px; font-weight:600;" onclick="SubGraphManager.editSymbol('${this.fileId}', '${safeId}')">✏️ Edit Symbol</button>
344
+ ${impactBadge}
345
+ `;
346
+ }
347
+ } else {
348
+ const extCount = (this.data?.related_symbols || []).length;
349
+ if (this.showCrossUsage && extCount > 0) {
350
+ statEl.innerHTML = `<span>${localNodes.length} local symbols, <span style="color:var(--purple, var(--accent)); font-weight:600;">+${extCount} cross-file impact nodes</span> (${displayLinks.length} total edges)</span>`;
351
+ } else {
352
+ const availableCross = (this.data?.related_symbols || []).length;
353
+ const crossHint = availableCross > 0 ? ` <span style="color:var(--text-dim); font-size:0.65rem;">(${availableCross} cross-file nodes available)</span>` : "";
354
+ statEl.innerHTML = `<span>${localNodes.length} symbols, ${displayLinks.length} internal links${crossHint}</span>`;
355
+ }
356
+ }
357
+ };
358
+
359
+ this.updateSymbolBar = updateSymbolBar;
360
+
361
+ let initialFocusNode = null;
362
+ if (this.targetSymbolId) {
363
+ const symName = this.targetSymbolId.split("::").pop();
364
+ initialFocusNode = displayNodes.find(n =>
365
+ n.id === this.targetSymbolId ||
366
+ n.rawSymbol?.id === this.targetSymbolId ||
367
+ n.label === symName ||
368
+ n.id.endsWith("::" + symName)
369
+ );
370
+ }
371
+ if (initialFocusNode) {
372
+ this.canvasInstance.selectedNodeId = initialFocusNode.id;
373
+ this.canvasInstance.isolateNeighborhood(initialFocusNode.id);
374
+ updateSymbolBar(initialFocusNode);
375
+ } else {
376
+ updateSymbolBar(null);
377
+ }
378
+
379
+ this.canvasInstance.onNodeSelected = (node) => {
380
+ this.targetSymbolId = node.id;
381
+ this.canvasInstance.selectedNodeId = node.id;
382
+ this.canvasInstance.isolateNeighborhood(node.id);
383
+ updateSymbolBar(node);
384
+ this.renderUsages();
385
+
386
+ if (!node.isExternal) {
387
+ this.scrollToSymbol(node.id);
388
+ }
389
+ };
390
+
391
+ this.canvasInstance.onNodeDoubleClick = (node) => {
392
+ if (node.isExternal) {
393
+ SubGraphManager.openWindow(node.fileId, node.id, null, "", "graph");
394
+ } else {
395
+ this.targetSymbolId = node.id;
396
+ SubGraphManager.editSymbol(this.fileId, node.id);
397
+ }
398
+ };
399
+
400
+ this.canvasInstance.onEmptyCanvasClick = () => {
401
+ this.targetSymbolId = null;
402
+ this.canvasInstance.selectedNodeId = null;
403
+ this.canvasInstance.clearNeighborhoodIsolation();
404
+ updateSymbolBar(null);
405
+ this.renderUsages();
406
+ };
407
+ }
408
+
409
+ renderUsages() {
410
+ const container = document.getElementById(`${this.winId}-usages-content`);
411
+ if (!container) return;
412
+
413
+ const allEdges = this.data?.edges || [];
414
+ const selectedSymId = this.targetSymbolId;
415
+ const selectedSymName = selectedSymId ? selectedSymId.replace("module::", "").split("::").pop() : "";
416
+
417
+ const matchesTarget = (edgeEndpoint, targetId) => {
418
+ if (!targetId || !edgeEndpoint) return false;
419
+ if (edgeEndpoint === targetId) return true;
420
+ const cleanEdge = edgeEndpoint.replace(/^module::/, "");
421
+ const cleanTarget = targetId.replace(/^module::/, "");
422
+ if (cleanEdge === cleanTarget) return true;
423
+ if (cleanTarget.includes("::") && cleanEdge.includes("::")) {
424
+ const [targetFile, targetScope] = cleanTarget.split("::");
425
+ const [edgeFile, edgeScope] = cleanEdge.split("::");
426
+ const fileMatches = targetFile === edgeFile || targetFile.endsWith("/" + edgeFile) || edgeFile.endsWith("/" + targetFile);
427
+ return fileMatches && targetScope === edgeScope;
428
+ }
429
+ return false;
430
+ };
431
+
432
+ let extIncoming = [];
433
+ let extOutgoing = [];
434
+
435
+ if (selectedSymId) {
436
+ extIncoming = allEdges.filter(e => matchesTarget(e.target_id, selectedSymId) && !matchesTarget(e.source_id, selectedSymId));
437
+ extOutgoing = allEdges.filter(e => matchesTarget(e.source_id, selectedSymId) && !matchesTarget(e.target_id, selectedSymId));
438
+ } else {
439
+ extIncoming = allEdges.filter(e => e.source_id.split("::")[0] !== this.fileId);
440
+ extOutgoing = allEdges.filter(e => e.target_id.split("::")[0] !== this.fileId);
441
+ }
442
+
443
+ let html = "";
444
+
445
+ if (selectedSymId) {
446
+ html += `
447
+ <div class="usage-active-filter" style="display:flex; justify-content:space-between; align-items:center; background:var(--bg-elevated); padding:6px 10px; border-radius:6px; margin-bottom:8px; border:1px solid var(--accent); font-size:0.72rem;">
448
+ <div style="display:flex; align-items:center; gap:6px;">
449
+ <span class="badge shared" style="font-size:0.65rem;">Symbol Filter</span>
450
+ <span style="font-weight:700; color:var(--text-main); font-size:0.75rem;">${selectedSymName}</span>
451
+ </div>
452
+ <button class="secondary" style="padding:2px 8px; font-size:0.68rem;" onclick="SubGraphManager.clearSymbolFilter('${this.fileId}')">View All File Usages</button>
453
+ </div>
454
+ `;
455
+ }
456
+
457
+ html += `<div class="usage-card">
458
+ <div class="usage-card-title">
459
+ <span>${selectedSymId ? `Incoming References to ${selectedSymName}` : "Incoming References (Called By)"}</span>
460
+ <span class="badge shared">${extIncoming.length}</span>
461
+ </div>`;
462
+ if (extIncoming.length === 0) {
463
+ html += `<div style="font-size:0.72rem; color:var(--text-dim); padding:4px;">${selectedSymId ? `No external callers found for '${selectedSymName}'.` : "No external files call this module."}</div>`;
464
+ } else {
465
+ extIncoming.forEach(e => {
466
+ const srcFile = e.source_id.split("::")[0];
467
+ const srcName = e.source_id.split("::")[1] || srcFile;
468
+ html += `
469
+ <div class="usage-item" onclick="SubGraphManager.openWindow('${srcFile}', '${e.source_id}')">
470
+ <div>
471
+ <div style="font-weight:600; color:var(--text-main);">${srcName}</div>
472
+ <div style="font-size:0.65rem; color:var(--text-dim);">${srcFile}</div>
473
+ </div>
474
+ <span class="badge frontend">${e.kind}</span>
475
+ </div>
476
+ `;
477
+ });
478
+ }
479
+ html += `</div>`;
480
+
481
+ html += `<div class="usage-card" style="margin-top:8px;">
482
+ <div class="usage-card-title">
483
+ <span>${selectedSymId ? `Outgoing Calls from ${selectedSymName}` : "Outgoing Calls (Depends On)"}</span>
484
+ <span class="badge backend">${extOutgoing.length}</span>
485
+ </div>`;
486
+ if (extOutgoing.length === 0) {
487
+ html += `<div style="font-size:0.72rem; color:var(--text-dim); padding:4px;">${selectedSymId ? `No outgoing calls found for '${selectedSymName}'.` : "No external calls from this module."}</div>`;
488
+ } else {
489
+ extOutgoing.forEach(e => {
490
+ const tgtRaw = e.target_id.replace("module::", "");
491
+ const tgtFile = tgtRaw.split("::")[0];
492
+ const tgtName = tgtRaw.split("::")[1] || tgtRaw;
493
+ html += `
494
+ <div class="usage-item" onclick="SubGraphManager.openWindow('${tgtFile}', '${e.target_id}')">
495
+ <div>
496
+ <div style="font-weight:600; color:var(--text-main);">${tgtName}</div>
497
+ <div style="font-size:0.65rem; color:var(--text-dim);">${tgtFile}</div>
498
+ </div>
499
+ <span class="badge shared">${e.kind}</span>
500
+ </div>
501
+ `;
502
+ });
503
+ }
504
+ html += `</div>`;
505
+
506
+ container.innerHTML = html;
507
+ }
508
+
509
+ renderEditor() {
510
+ const textarea = document.getElementById(`${this.winId}-textarea`);
511
+ const lineNums = document.getElementById(`${this.winId}-line-numbers`);
512
+ if (!textarea || !lineNums) return;
513
+
514
+ textarea.value = this.fileContent;
515
+ this.updateLineNumbers();
516
+
517
+ textarea.addEventListener("input", () => {
518
+ this.updateLineNumbers();
519
+ this.updateHighlights();
520
+ });
521
+
522
+ textarea.addEventListener("scroll", () => {
523
+ lineNums.scrollTop = textarea.scrollTop;
524
+ this.updateHighlights();
525
+ });
526
+
527
+ if (this.targetSymbolId) {
528
+ this.scrollToSymbol(this.targetSymbolId);
529
+ }
530
+ }
531
+
532
+ updateLineNumbers() {
533
+ const textarea = document.getElementById(`${this.winId}-textarea`);
534
+ const lineNums = document.getElementById(`${this.winId}-line-numbers`);
535
+ if (!textarea || !lineNums) return;
536
+ const count = textarea.value.split("\n").length;
537
+ lineNums.innerHTML = Array.from({ length: count }, (_, i) => i + 1).join("<br>");
538
+ }
539
+
540
+ getLineHeight() {
541
+ const textarea = document.getElementById(`${this.winId}-textarea`);
542
+ if (textarea) {
543
+ const computed = window.getComputedStyle(textarea);
544
+ const lh = parseFloat(computed.lineHeight);
545
+ if (lh && !isNaN(lh) && lh > 0) return lh;
546
+ }
547
+ return 20;
548
+ }
549
+
550
+ scrollToSymbol(symbolId) {
551
+ const textarea = document.getElementById(`${this.winId}-textarea`);
552
+ const lineNums = document.getElementById(`${this.winId}-line-numbers`);
553
+ if (!textarea) return;
554
+
555
+ const cleanSymId = (symbolId || "").replace(/^module::/, "").trim();
556
+ const symName = cleanSymId.split("::").pop();
557
+ const shortName = symName && symName.includes(".") ? symName.split(".").pop() : (symName || "");
558
+
559
+ let sym = null;
560
+ if (this.canvasInstance?.nodes) {
561
+ const foundNode = this.canvasInstance.nodes.find(n =>
562
+ n.id === symbolId || n.id === cleanSymId ||
563
+ n.rawSymbol?.id === symbolId || n.rawSymbol?.id === cleanSymId ||
564
+ n.label === symName || n.label === shortName
565
+ );
566
+ if (foundNode && foundNode.rawSymbol && foundNode.rawSymbol.location) {
567
+ sym = foundNode.rawSymbol;
568
+ }
569
+ }
570
+
571
+ if (!sym && this.data?.symbols) {
572
+ sym = this.data.symbols.find(s =>
573
+ s.id === symbolId || s.id === cleanSymId ||
574
+ s.name === symName || s.name === shortName ||
575
+ s.qualified_name === symbolId || s.qualified_name === cleanSymId ||
576
+ (symName && s.id.endsWith("::" + symName)) || (shortName && s.id.endsWith("::" + shortName))
577
+ );
578
+ }
579
+
580
+ if (!sym && window.appState?.rawGraph?.symbols) {
581
+ sym = window.appState.rawGraph.symbols.find(s =>
582
+ (s.location?.file_id === this.fileId || s.id.startsWith(this.fileId + "::")) &&
583
+ (s.id === symbolId || s.name === symName || s.name === shortName || (symName && s.id.endsWith("::" + symName)))
584
+ );
585
+ }
586
+
587
+ let startLine = sym?.location?.start_line;
588
+ let endLine = sym?.location?.end_line || startLine;
589
+
590
+ if ((!startLine || startLine <= 0) && textarea.value && shortName) {
591
+ const lines = textarea.value.split("\n");
592
+ const defRegex = new RegExp(`(^|\\s)(class|def|function|const|let|var|type|interface)\\s+${shortName}\\b`);
593
+ for (let i = 0; i < lines.length; i++) {
594
+ if (defRegex.test(lines[i])) {
595
+ startLine = i + 1;
596
+ endLine = startLine + 10;
597
+ break;
598
+ }
599
+ }
600
+ if (!startLine) {
601
+ const wordRegex = new RegExp(`\\b${shortName}\\b`);
602
+ for (let i = 0; i < lines.length; i++) {
603
+ if (wordRegex.test(lines[i])) {
604
+ startLine = i + 1;
605
+ endLine = startLine + 5;
606
+ break;
607
+ }
608
+ }
609
+ }
610
+ }
611
+
612
+ if (startLine && startLine > 0) {
613
+ this.targetLine = startLine;
614
+ this.targetEndLine = endLine || startLine;
615
+ this.targetSymbolId = symbolId;
616
+
617
+ const applyScrollAndHighlight = () => {
618
+ const lineHeight = this.getLineHeight();
619
+ const targetY = (startLine - 1) * lineHeight;
620
+
621
+ textarea.scrollTop = Math.max(0, targetY - 40);
622
+ if (lineNums) lineNums.scrollTop = textarea.scrollTop;
623
+
624
+ const highlight = document.getElementById(`${this.winId}-symbol-highlight`);
625
+ if (highlight) {
626
+ highlight.style.display = "block";
627
+ const numLines = Math.max(1, (endLine || startLine) - startLine + 1);
628
+ highlight.style.top = `${targetY - textarea.scrollTop + 8}px`;
629
+ highlight.style.height = `${numLines * lineHeight}px`;
630
+ highlight.title = `${sym?.signature || shortName} (Lines ${startLine}-${endLine || startLine})`;
631
+ highlight.innerHTML = `<span class="symbol-highlight-badge">⚡ ${sym?.name || shortName} (L${startLine}-${endLine || startLine})</span>`;
632
+ }
633
+
634
+ const lines = textarea.value.split("\n");
635
+ let charStart = 0;
636
+ for (let i = 0; i < startLine - 1 && i < lines.length; i++) {
637
+ charStart += lines[i].length + 1;
638
+ }
639
+ let charEnd = charStart;
640
+ for (let i = startLine - 1; i < (endLine || startLine) && i < lines.length; i++) {
641
+ charEnd += lines[i].length + 1;
642
+ }
643
+
644
+ try {
645
+ textarea.setSelectionRange(charStart, charStart);
646
+ } catch (_) {}
647
+
648
+ textarea.scrollTop = Math.max(0, targetY - 40);
649
+ if (lineNums) lineNums.scrollTop = textarea.scrollTop;
650
+ if (highlight) {
651
+ highlight.style.top = `${targetY - textarea.scrollTop + 8}px`;
652
+ }
653
+ };
654
+
655
+ applyScrollAndHighlight();
656
+ requestAnimationFrame(() => applyScrollAndHighlight());
657
+ setTimeout(() => applyScrollAndHighlight(), 50);
658
+ }
659
+ }
660
+
661
+ highlightError(lineNumber, message = "") {
662
+ const textarea = document.getElementById(`${this.winId}-textarea`);
663
+ const lineNums = document.getElementById(`${this.winId}-line-numbers`);
664
+ if (!textarea) return;
665
+ this.targetErrorLine = lineNumber;
666
+
667
+ const applyErrorScroll = () => {
668
+ const lineHeight = this.getLineHeight();
669
+ const targetY = (lineNumber - 1) * lineHeight;
670
+ textarea.scrollTop = Math.max(0, targetY - 40);
671
+ if (lineNums) lineNums.scrollTop = textarea.scrollTop;
672
+
673
+ const errHighlight = document.getElementById(`${this.winId}-error-highlight`);
674
+ if (errHighlight) {
675
+ errHighlight.style.display = "block";
676
+ errHighlight.style.top = `${targetY - textarea.scrollTop + 8}px`;
677
+ errHighlight.style.height = `${lineHeight}px`;
678
+ errHighlight.title = message;
679
+ if (message) {
680
+ errHighlight.innerHTML = `<span class="error-badge" style="position:absolute; right:8px; top:1px; font-size:0.68rem; background:rgba(239,68,68,0.25); border:1px solid #ef4444; color:#f87171; padding:0 6px; border-radius:3px; pointer-events:none; white-space:nowrap; z-index:5;">🚨 L${lineNumber}: ${message}</span>`;
681
+ } else {
682
+ errHighlight.innerHTML = "";
683
+ }
684
+ }
685
+
686
+ const lines = textarea.value.split("\n");
687
+ let charStart = 0;
688
+ for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) {
689
+ charStart += lines[i].length + 1;
690
+ }
691
+
692
+ try {
693
+ textarea.setSelectionRange(charStart, charStart);
694
+ } catch (_) {}
695
+
696
+ textarea.scrollTop = Math.max(0, targetY - 40);
697
+ if (lineNums) lineNums.scrollTop = textarea.scrollTop;
698
+ if (errHighlight) {
699
+ errHighlight.style.top = `${targetY - textarea.scrollTop + 8}px`;
700
+ }
701
+ };
702
+
703
+ applyErrorScroll();
704
+ requestAnimationFrame(() => applyErrorScroll());
705
+ setTimeout(() => applyErrorScroll(), 50);
706
+ }
707
+
708
+ updateHighlights() {
709
+ const textarea = document.getElementById(`${this.winId}-textarea`);
710
+ const highlight = document.getElementById(`${this.winId}-symbol-highlight`);
711
+ const errHighlight = document.getElementById(`${this.winId}-error-highlight`);
712
+ if (!textarea) return;
713
+ const lineHeight = this.getLineHeight();
714
+
715
+ if (highlight && highlight.style.display !== "none" && (this.targetLine || this.targetSymbolId)) {
716
+ let startLine = this.targetLine;
717
+ let endLine = this.targetEndLine || startLine;
718
+ if (!startLine && this.targetSymbolId && this.data) {
719
+ const symName = this.targetSymbolId.split("::").pop();
720
+ const sym = (this.data.symbols || []).find(s =>
721
+ s.id === this.targetSymbolId || s.name === this.targetSymbolId || s.qualified_name === this.targetSymbolId ||
722
+ (symName && s.id.endsWith("::" + symName)) || s.name === symName
723
+ );
724
+ if (sym?.location?.start_line) {
725
+ startLine = sym.location.start_line;
726
+ endLine = sym.location.end_line || startLine;
727
+ }
728
+ }
729
+ if (startLine && startLine > 0) {
730
+ const targetY = (startLine - 1) * lineHeight;
731
+ highlight.style.top = `${targetY - textarea.scrollTop + 8}px`;
732
+ highlight.style.height = `${Math.max(1, (endLine || startLine) - startLine + 1) * lineHeight}px`;
733
+ }
734
+ }
735
+
736
+ if (errHighlight && errHighlight.style.display !== "none" && this.targetErrorLine) {
737
+ const targetY = (this.targetErrorLine - 1) * lineHeight;
738
+ errHighlight.style.top = `${targetY - textarea.scrollTop + 8}px`;
739
+ }
740
+ }
741
+ }
742
+
743
+ class SubGraphManager {
744
+ static topZ = 300;
745
+
746
+ static getWindow(fileId) {
747
+ if (!fileId) return null;
748
+ const norm = fileId.replace(/\\/g, "/").trim();
749
+ let win = appState.activeSubgraphs.get(norm) || appState.activeSubgraphs.get(fileId);
750
+ if (win) return win;
751
+ for (const [k, v] of appState.activeSubgraphs.entries()) {
752
+ if (v.fileId === fileId || v.fileId === norm ||
753
+ k.endsWith(norm) || norm.endsWith(k) ||
754
+ k.endsWith(fileId) || fileId.endsWith(k) ||
755
+ (v.fileId && (v.fileId.endsWith("/" + norm) || norm.endsWith("/" + v.fileId)))) {
756
+ return v;
757
+ }
758
+ }
759
+ return null;
760
+ }
761
+
762
+ static updateBackgroundInteractivity() {
763
+ let hasActiveWindow = false;
764
+ appState.activeSubgraphs.forEach(win => {
765
+ if (!win || !win.winId) return;
766
+ const dom = document.getElementById(win.winId);
767
+ if (dom && dom.style.display !== "none" && !dom.classList.contains("minimized")) {
768
+ hasActiveWindow = true;
769
+ }
770
+ });
771
+
772
+ const mainCanvasEl = document.getElementById("graph-canvas");
773
+ if (mainCanvasEl) {
774
+ if (hasActiveWindow) {
775
+ mainCanvasEl.classList.add("background-inert");
776
+ } else {
777
+ mainCanvasEl.classList.remove("background-inert");
778
+ }
779
+ }
780
+ if (window.mainCanvas) {
781
+ window.mainCanvas.isInteractive = !hasActiveWindow;
782
+ }
783
+ }
784
+
785
+ static clearSymbolFilter(fileId) {
786
+ const win = this.getWindow(fileId);
787
+ if (!win) return;
788
+ win.targetSymbolId = null;
789
+ if (win.canvasInstance) {
790
+ win.canvasInstance.selectedNodeId = null;
791
+ win.canvasInstance.clearNeighborhoodIsolation();
792
+ }
793
+ if (win.updateSymbolBar) {
794
+ win.updateSymbolBar(null);
795
+ }
796
+ win.renderUsages();
797
+ }
798
+
799
+ static toggleMoveMode(fileId) {
800
+ const win = this.getWindow(fileId);
801
+ if (!win) return;
802
+ win.moveMode = !win.moveMode;
803
+ const btn = document.getElementById(`${win.winId}-move-btn`);
804
+ const header = document.getElementById(`${win.winId}-header`);
805
+ if (btn) {
806
+ btn.classList.toggle("active", win.moveMode);
807
+ btn.classList.toggle("primary", win.moveMode);
808
+ }
809
+ if (header) {
810
+ header.style.cursor = win.moveMode ? "move" : "default";
811
+ }
812
+ }
813
+
814
+ static tileWindows() {
815
+ const activeWins = [];
816
+ const seenDomIds = new Set();
817
+
818
+ appState.activeSubgraphs.forEach((win) => {
819
+ if (win && win.winId && !seenDomIds.has(win.winId)) {
820
+ seenDomIds.add(win.winId);
821
+ const dom = document.getElementById(win.winId);
822
+ if (dom && dom.style.display !== "none" && !dom.classList.contains("minimized") && !dom.classList.contains("maximized")) {
823
+ activeWins.push(win);
824
+ }
825
+ }
826
+ });
827
+
828
+ const count = activeWins.length;
829
+ if (count === 0) return;
830
+
831
+ const container = document.getElementById("canvas-container") || document.body;
832
+ const containerW = container.clientWidth || window.innerWidth || 1000;
833
+ const containerH = container.clientHeight || window.innerHeight || 700;
834
+
835
+ const margin = 20;
836
+ const gap = 16;
837
+ const availW = Math.max(320, containerW - (margin * 2));
838
+ const availH = Math.max(240, containerH - (margin * 2));
839
+
840
+ let cols = 1;
841
+ let rows = 1;
842
+
843
+ if (count === 1) {
844
+ cols = 1; rows = 1;
845
+ } else if (count === 2) {
846
+ if (availW >= 550) {
847
+ cols = 2; rows = 1;
848
+ } else {
849
+ cols = 1; rows = 2;
850
+ }
851
+ } else if (count <= 4) {
852
+ cols = 2; rows = 2;
853
+ } else if (count <= 6) {
854
+ cols = 3; rows = 2;
855
+ } else {
856
+ cols = Math.ceil(Math.sqrt(count));
857
+ rows = Math.ceil(count / cols);
858
+ }
859
+
860
+ const winWidth = (count === 1)
861
+ ? Math.min(640, availW)
862
+ : Math.max(300, Math.floor((availW - (gap * (cols - 1))) / cols));
863
+
864
+ const winHeight = (count === 1)
865
+ ? Math.min(500, availH)
866
+ : Math.max(240, Math.floor((availH - (gap * (rows - 1))) / rows));
867
+
868
+ activeWins.forEach((win, index) => {
869
+ const r = Math.floor(index / cols);
870
+ const c = index % cols;
871
+ const left = margin + c * (winWidth + gap);
872
+ const top = margin + r * (winHeight + gap);
873
+
874
+ const dom = document.getElementById(win.winId);
875
+ if (dom) {
876
+ dom.style.left = `${left}px`;
877
+ dom.style.top = `${top}px`;
878
+ dom.style.width = `${winWidth}px`;
879
+ dom.style.height = `${winHeight}px`;
880
+ if (win.canvasInstance) {
881
+ setTimeout(() => {
882
+ if (win.canvasInstance) {
883
+ win.canvasInstance.setupCanvas();
884
+ win.canvasInstance.fitToScreen();
885
+ }
886
+ }, 50);
887
+ }
888
+ }
889
+ });
890
+ }
891
+
892
+ static openWindow(fileId, symbolId = null, line = null, message = "", initialTab = "graph") {
893
+ if (!fileId) return;
894
+ let clean = (fileId || "").replace(/^module::/, "").replace(/^\.\//, "").trim();
895
+ let symId = symbolId;
896
+ if (clean.includes("::")) {
897
+ const parts = clean.split("::");
898
+ clean = parts[0];
899
+ if (!symId) symId = fileId;
900
+ }
901
+
902
+ const rawGraph = appState.rawGraph || {};
903
+ const symList = rawGraph.symbols || [];
904
+ const filesList = rawGraph.files || [];
905
+
906
+ const symName = symId ? symId.split("::").pop().split(".").pop() : clean.split("::").pop().split(".").pop();
907
+ const matched = symList.find(s =>
908
+ s.id === clean || s.name === clean || s.qualified_name === clean ||
909
+ (symId && (s.id === symId || s.name === symId || s.qualified_name === symId || s.name === symName || s.id.endsWith("::" + symName)))
910
+ );
911
+ if (matched && matched.location && matched.location.file_id) {
912
+ clean = matched.location.file_id;
913
+ if (!symId) symId = matched.id;
914
+ if (!line && matched.location.start_line) line = matched.location.start_line;
915
+ } else {
916
+ let matchedFile = filesList.find(f =>
917
+ f === clean || f.endsWith("/" + clean) || f.replace(/\.[^/.]+$/, "") === clean || f.endsWith("/" + clean + ".py") || f.endsWith("/" + clean + ".ts") || f.endsWith("/" + clean + ".js")
918
+ );
919
+ if (matchedFile) {
920
+ clean = matchedFile;
921
+ }
922
+ }
923
+
924
+ const normFileId = clean;
925
+ const winDomId = `subgraph-win-${normFileId.replace(/[^a-zA-Z0-9]/g, "_")}`;
926
+
927
+ let win = appState.activeSubgraphs.get(normFileId);
928
+ if (!win) {
929
+ for (const [key, existingWin] of appState.activeSubgraphs.entries()) {
930
+ if (existingWin.fileId === normFileId || existingWin.winId === winDomId) {
931
+ win = existingWin;
932
+ break;
933
+ }
934
+ }
935
+ }
936
+
937
+ const dom = document.getElementById(winDomId);
938
+ if (win || dom) {
939
+ if (dom) {
940
+ dom.classList.remove("minimized");
941
+ dom.style.display = "flex";
942
+ dom.style.zIndex = ++this.topZ;
943
+ }
944
+ if (win) {
945
+ if (symId) {
946
+ win.targetSymbolId = symId;
947
+ }
948
+ if (line) {
949
+ win.targetLine = line;
950
+ win.targetMessage = message;
951
+ }
952
+ if (initialTab === "code" || (line && !symId)) {
953
+ this.switchTab(normFileId, "code");
954
+ if (symId) {
955
+ win.scrollToSymbol(symId);
956
+ } else if (line) {
957
+ win.highlightError(line, message);
958
+ }
959
+ } else {
960
+ this.switchTab(normFileId, "graph");
961
+ if (symId && win.canvasInstance) {
962
+ const symName = symId.split("::").pop();
963
+ const symNode = (win.canvasInstance.nodes || []).find(n =>
964
+ n.id === symId || n.rawSymbol?.id === symId || n.label === symName || n.id.endsWith("::" + symName)
965
+ );
966
+ if (symNode) {
967
+ win.canvasInstance.selectedNodeId = symNode.id;
968
+ win.canvasInstance.isolateNeighborhood(symNode.id);
969
+ if (win.updateSymbolBar) {
970
+ win.updateSymbolBar(symNode);
971
+ }
972
+ }
973
+ }
974
+ }
975
+ }
976
+ this.tileWindows();
977
+ this.updateTaskbar();
978
+ return;
979
+ }
980
+
981
+ const newWin = new SubGraphWindow(normFileId, symId, line, message, appState.nextWindowOffset, initialTab);
982
+ appState.nextWindowOffset = (appState.nextWindowOffset + 35) % 250 + 20;
983
+ appState.activeSubgraphs.set(normFileId, newWin);
984
+ this.tileWindows();
985
+ this.updateTaskbar();
986
+ this.updateBackgroundInteractivity();
987
+ }
988
+
989
+ static closeWindow(fileId) {
990
+ if (!fileId) return;
991
+ const normFileId = fileId.replace(/^module::/, "").replace(/^\.\//, "").trim();
992
+ let winToDelete = null;
993
+ const keysToDelete = [];
994
+
995
+ appState.activeSubgraphs.forEach((win, key) => {
996
+ if (win.fileId === normFileId || win.fileId === fileId || key === normFileId || key === fileId || key.endsWith(normFileId)) {
997
+ winToDelete = win;
998
+ keysToDelete.push(key);
999
+ }
1000
+ });
1001
+
1002
+ if (winToDelete) {
1003
+ if (winToDelete.canvasInstance) {
1004
+ winToDelete.canvasInstance.destroy();
1005
+ winToDelete.canvasInstance = null;
1006
+ }
1007
+ const dom = document.getElementById(winToDelete.winId);
1008
+ if (dom) dom.remove();
1009
+ keysToDelete.forEach(k => appState.activeSubgraphs.delete(k));
1010
+ } else {
1011
+ const winDomId = `subgraph-win-${normFileId.replace(/[^a-zA-Z0-9]/g, "_")}`;
1012
+ const dom = document.getElementById(winDomId);
1013
+ if (dom) dom.remove();
1014
+ }
1015
+
1016
+ this.tileWindows();
1017
+ this.updateTaskbar();
1018
+ this.updateBackgroundInteractivity();
1019
+ }
1020
+
1021
+ static minimizeWindow(fileId) {
1022
+ if (!fileId) return;
1023
+ const normFileId = fileId.replace(/^module::/, "").replace(/^\.\//, "").trim();
1024
+ const win = appState.activeSubgraphs.get(normFileId) || appState.activeSubgraphs.get(fileId);
1025
+ if (win) {
1026
+ const dom = document.getElementById(win.winId);
1027
+ if (dom) dom.classList.add("minimized");
1028
+ this.tileWindows();
1029
+ this.updateTaskbar();
1030
+ this.updateBackgroundInteractivity();
1031
+ }
1032
+ }
1033
+
1034
+ static maximizeWindow(fileId) {
1035
+ if (!fileId) return;
1036
+ const normFileId = fileId.replace(/^module::/, "").replace(/^\.\//, "").trim();
1037
+ const win = appState.activeSubgraphs.get(normFileId) || appState.activeSubgraphs.get(fileId);
1038
+ if (win) {
1039
+ const dom = document.getElementById(win.winId);
1040
+ if (dom) {
1041
+ dom.classList.toggle("maximized");
1042
+ if (win.canvasInstance) {
1043
+ win.canvasInstance.setupCanvas();
1044
+ win.canvasInstance.fitToScreen();
1045
+ }
1046
+ }
1047
+ }
1048
+ }
1049
+
1050
+ static editSymbol(fileId, symbolId) {
1051
+ const win = this.getWindow(fileId);
1052
+ if (!win) return;
1053
+ win.targetSymbolId = symbolId;
1054
+ this.switchTab(fileId, "code");
1055
+ win.scrollToSymbol(symbolId);
1056
+ }
1057
+
1058
+ static switchTab(fileId, tabName) {
1059
+ const win = this.getWindow(fileId);
1060
+ if (!win) return;
1061
+ const dom = document.getElementById(win.winId);
1062
+ if (!dom) return;
1063
+
1064
+ dom.querySelectorAll(".subtab-btn").forEach(b => b.classList.remove("active"));
1065
+ dom.querySelectorAll(".subgraph-panel").forEach(p => p.classList.remove("active"));
1066
+
1067
+ const tabBtn = dom.querySelector(`button[onclick*="'${tabName}'"]`);
1068
+ const panel = document.getElementById(`${win.winId}-panel-${tabName}`);
1069
+ if (tabBtn) tabBtn.classList.add("active");
1070
+ if (panel) panel.classList.add("active");
1071
+
1072
+ if (tabName === "graph" && win.canvasInstance) {
1073
+ win.canvasInstance.setupCanvas();
1074
+ win.canvasInstance.fitToScreen();
1075
+ if (win.targetSymbolId) {
1076
+ const symName = win.targetSymbolId.split("::").pop();
1077
+ const symNode = (win.canvasInstance.nodes || []).find(n =>
1078
+ n.id === win.targetSymbolId || n.rawSymbol?.id === win.targetSymbolId || n.label === symName || (symName && n.id.endsWith("::" + symName))
1079
+ );
1080
+ if (symNode) {
1081
+ win.canvasInstance.selectedNodeId = symNode.id;
1082
+ win.canvasInstance.isolateNeighborhood(symNode.id);
1083
+ if (win.updateSymbolBar) {
1084
+ win.updateSymbolBar(symNode);
1085
+ }
1086
+ }
1087
+ }
1088
+ } else if (tabName === "code") {
1089
+ if (win.targetSymbolId) {
1090
+ win.scrollToSymbol(win.targetSymbolId);
1091
+ } else if (win.targetLine) {
1092
+ win.highlightError(win.targetLine, win.targetMessage);
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ static resetSubgraphCanvas(fileId) {
1098
+ const win = this.getWindow(fileId);
1099
+ if (win && win.canvasInstance) {
1100
+ win.canvasInstance.fitToScreen();
1101
+ }
1102
+ }
1103
+
1104
+ static toggleCrossUsage(fileId, isEnabled) {
1105
+ let win = this.getWindow(fileId);
1106
+ if (!win) return;
1107
+ if (isEnabled === undefined) {
1108
+ win.showCrossUsage = !win.showCrossUsage;
1109
+ } else {
1110
+ win.showCrossUsage = !!isEnabled;
1111
+ }
1112
+ const btn = document.getElementById(`${win.winId}-btn-cross`);
1113
+ if (btn) {
1114
+ btn.classList.toggle("active", win.showCrossUsage);
1115
+ btn.classList.toggle("primary", win.showCrossUsage);
1116
+ }
1117
+ win.renderSymbolsGraph();
1118
+ if (win.canvasInstance) {
1119
+ setTimeout(() => {
1120
+ if (win.canvasInstance) {
1121
+ win.canvasInstance.fitToScreen();
1122
+ }
1123
+ }, 60);
1124
+ }
1125
+ }
1126
+
1127
+ static async saveFile(fileId) {
1128
+ let win = this.getWindow(fileId);
1129
+ if (!win) {
1130
+ App.showToast(`Error: Window for ${fileId} not found`, "error");
1131
+ return;
1132
+ }
1133
+ const textarea = document.getElementById(`${win.winId}-textarea`);
1134
+ if (!textarea) {
1135
+ App.showToast(`Error: Editor for ${fileId} not found`, "error");
1136
+ return;
1137
+ }
1138
+
1139
+ const diskCheckbox = document.getElementById(`${win.winId}-toggle-disk`);
1140
+ const isDirect = !!(appState.directDiskWrite || (diskCheckbox && diskCheckbox.checked));
1141
+ const saveBtn = document.querySelector(`#${win.winId} button[onclick*="saveFile"]`);
1142
+ if (saveBtn) {
1143
+ saveBtn.disabled = true;
1144
+ saveBtn.textContent = "Saving...";
1145
+ }
1146
+
1147
+ try {
1148
+ const res = await CodeUIAPI.applyEdit(win.fileId, textarea.value, "replace_file", isDirect);
1149
+ if (isDirect) {
1150
+ const displayPath = res.saved_path ? res.saved_path.split("/").slice(-2).join("/") : win.fileId;
1151
+ App.showToast(`Saved directly to disk: ${displayPath}`, "success");
1152
+ } else {
1153
+ App.showToast(`Saved non-destructive override for ${win.fileId}`, "success");
1154
+ }
1155
+ win.fileContent = textarea.value;
1156
+ App.loadData(false);
1157
+ } catch (e) {
1158
+ App.showToast(`Failed to save edit: ${e.message}`, "error");
1159
+ } finally {
1160
+ if (saveBtn) {
1161
+ saveBtn.disabled = false;
1162
+ saveBtn.textContent = "Apply Edit";
1163
+ }
1164
+ }
1165
+ }
1166
+
1167
+ static async revertFile(fileId) {
1168
+ let win = this.getWindow(fileId);
1169
+ try {
1170
+ const target = win ? win.fileId : fileId;
1171
+ await CodeUIAPI.revertEdit(target);
1172
+ App.showToast(`Reverted overrides for ${target}`, "info");
1173
+ if (win) await win.loadData();
1174
+ App.loadData(false);
1175
+ } catch (e) {
1176
+ App.showToast(`Failed to revert: ${e.message}`, "error");
1177
+ }
1178
+ }
1179
+
1180
+ static updateTaskbar() {
1181
+ const taskbar = document.getElementById("subgraph-taskbar");
1182
+ if (!taskbar) return;
1183
+ taskbar.innerHTML = "";
1184
+
1185
+ appState.activeSubgraphs.forEach((win, fileId) => {
1186
+ const dom = document.getElementById(win.winId);
1187
+ const isMin = dom && dom.classList.contains("minimized");
1188
+ const filename = fileId.split("/").pop();
1189
+
1190
+ const pill = document.createElement("div");
1191
+ pill.className = `taskbar-pill ${isMin ? "minimized" : ""}`;
1192
+ pill.innerHTML = `<span>📄</span> ${filename}`;
1193
+ pill.onclick = () => {
1194
+ if (dom) {
1195
+ dom.classList.remove("minimized");
1196
+ dom.style.zIndex = ++this.topZ;
1197
+ this.tileWindows();
1198
+ this.updateTaskbar();
1199
+ this.updateBackgroundInteractivity();
1200
+ }
1201
+ };
1202
+ taskbar.appendChild(pill);
1203
+ });
1204
+ }
1205
+ }
1206
+
1207
+ window.SubGraphManager = SubGraphManager;
1208
+ window.addEventListener("resize", () => SubGraphManager.tileWindows());