project-graph-mcp 2.3.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/package.json +1 -3
  2. package/project-graph-mcp-2.3.0.tgz +0 -0
  3. package/src/network/web-server.js +1 -1
  4. package/vendor/symbiote-node/engine/AgentUICommands.js +100 -0
  5. package/vendor/symbiote-node/engine/Executor.js +371 -0
  6. package/vendor/symbiote-node/engine/Graph.js +314 -0
  7. package/vendor/symbiote-node/engine/GraphServer.js +353 -0
  8. package/vendor/symbiote-node/engine/HandlerLoader.js +145 -0
  9. package/vendor/symbiote-node/engine/History.js +83 -0
  10. package/vendor/symbiote-node/engine/Lifecycle.js +118 -0
  11. package/vendor/symbiote-node/engine/Persistence.js +84 -0
  12. package/vendor/symbiote-node/engine/Registry.js +264 -0
  13. package/vendor/symbiote-node/engine/SocketTypes.js +79 -0
  14. package/vendor/symbiote-node/engine/cli.js +404 -0
  15. package/vendor/symbiote-node/engine/index.js +56 -0
  16. package/vendor/symbiote-node/engine/nanoid.js +28 -0
  17. package/vendor/symbiote-node/engine/package.json +26 -0
  18. package/vendor/symbiote-node/engine/packs/ai/beat-detect.handler.js +215 -0
  19. package/vendor/symbiote-node/engine/packs/ai/content-adapt.handler.js +238 -0
  20. package/vendor/symbiote-node/engine/packs/ai/face-detect.handler.js +287 -0
  21. package/vendor/symbiote-node/engine/packs/ai/grok-generate.handler.js +565 -0
  22. package/vendor/symbiote-node/engine/packs/ai/kling-lipsync.handler.js +414 -0
  23. package/vendor/symbiote-node/engine/packs/ai/lesson-generate.handler.js +343 -0
  24. package/vendor/symbiote-node/engine/packs/ai/opencode.handler.js +164 -0
  25. package/vendor/symbiote-node/engine/packs/ai/replicate-lipsync.handler.js +341 -0
  26. package/vendor/symbiote-node/engine/packs/ai/tts.handler.js +241 -0
  27. package/vendor/symbiote-node/engine/packs/ai/whisper.handler.js +191 -0
  28. package/vendor/symbiote-node/engine/packs/data/db-query.handler.js +67 -0
  29. package/vendor/symbiote-node/engine/packs/data/news-accumulate.handler.js +281 -0
  30. package/vendor/symbiote-node/engine/packs/data/personas.handler.js +160 -0
  31. package/vendor/symbiote-node/engine/packs/data/prompt-loader.handler.js +193 -0
  32. package/vendor/symbiote-node/engine/packs/data/roles.handler.js +216 -0
  33. package/vendor/symbiote-node/engine/packs/data/rss-feed.handler.js +244 -0
  34. package/vendor/symbiote-node/engine/packs/debug/inject.handler.js +52 -0
  35. package/vendor/symbiote-node/engine/packs/flow/agent.handler.js +73 -0
  36. package/vendor/symbiote-node/engine/packs/flow/if.handler.js +107 -0
  37. package/vendor/symbiote-node/engine/packs/flow/loop.handler.js +58 -0
  38. package/vendor/symbiote-node/engine/packs/flow/merge.handler.js +60 -0
  39. package/vendor/symbiote-node/engine/packs/flow/retry.handler.js +65 -0
  40. package/vendor/symbiote-node/engine/packs/flow/switch.handler.js +64 -0
  41. package/vendor/symbiote-node/engine/packs/flow/wait-all.handler.js +39 -0
  42. package/vendor/symbiote-node/engine/packs/io/http-request.handler.js +82 -0
  43. package/vendor/symbiote-node/engine/packs/io/read-file.handler.js +60 -0
  44. package/vendor/symbiote-node/engine/packs/io/write-file.handler.js +63 -0
  45. package/vendor/symbiote-node/engine/packs/transform/anchor-match.handler.js +494 -0
  46. package/vendor/symbiote-node/engine/packs/transform/effects-skeleton.handler.js +417 -0
  47. package/vendor/symbiote-node/engine/packs/transform/json-parse.handler.js +43 -0
  48. package/vendor/symbiote-node/engine/packs/transform/lipsync-select.handler.js +339 -0
  49. package/vendor/symbiote-node/engine/packs/transform/riopla-adapt.handler.js +432 -0
  50. package/vendor/symbiote-node/engine/packs/transform/set.handler.js +57 -0
  51. package/vendor/symbiote-node/engine/packs/transform/template-builder.handler.js +134 -0
  52. package/vendor/symbiote-node/engine/packs/transform/template.handler.js +79 -0
  53. package/vendor/symbiote-node/engine/packs/transform/timeline-build.handler.js +399 -0
  54. package/vendor/symbiote-node/engine/packs/util/delay.handler.js +39 -0
  55. package/vendor/symbiote-node/engine/packs/util/log.handler.js +44 -0
  56. package/vendor/symbiote-node/engine/packs/video-pack.js +323 -0
  57. package/vendor/symbiote-node/package.json +2 -2
  58. package/web/app.js +6 -3
  59. package/web/components/canvas-graph.js +50 -11
  60. package/web/components/code-block.js +1 -1
  61. package/web/components/event-feed/MiniGraphWidget.js +105 -15
  62. package/web/components/follow-ribbon.js +134 -0
  63. package/web/follow-controller.js +241 -0
  64. package/web/panels/code-viewer.js +1 -1
  65. package/web/panels/dep-graph.js +21 -42
  66. package/web/style.css +6 -0
@@ -0,0 +1,323 @@
1
+ /**
2
+ * video-pack.js - Video domain pack for agi-graph
3
+ *
4
+ * Registers video-specific node types and socket types.
5
+ * Extracted from symbiote-video/src/graph/NodeTypes.js + NodeProcessors.js.
6
+ *
7
+ * @module agi-graph/packs/video-pack
8
+ */
9
+
10
+ import { registerPack } from '../index.js';
11
+
12
+ /**
13
+ * Video-domain socket types
14
+ */
15
+ const socketTypes = {
16
+ image: { color: '#C79650', label: 'Image', compatible: ['image'] },
17
+ audio: { color: '#C1990E', label: 'Audio', compatible: ['audio'] },
18
+ timeline: { color: '#5090C7', label: 'Timeline', compatible: ['timeline'] },
19
+ skeleton: { color: '#90C750', label: 'Skeleton', compatible: ['skeleton'] },
20
+ effect: { color: '#C750C7', label: 'Effect', compatible: ['effect'] },
21
+ };
22
+
23
+ /**
24
+ * Video node type definitions
25
+ */
26
+ const nodes = [
27
+ // ─── Source ─────────────────────────────────────────────────────────
28
+ {
29
+ type: 'source/audio',
30
+ category: 'source',
31
+ icon: 'music_note',
32
+ driver: {
33
+ description: 'Audio source file',
34
+ capabilities: ['source', 'audio'],
35
+ inputs: [],
36
+ outputs: [
37
+ { name: 'audio', type: 'audio' },
38
+ { name: 'duration', type: 'float' },
39
+ ],
40
+ params: {
41
+ src: { type: 'string', required: true, description: 'Audio file path or URL' },
42
+ volume: { type: 'float', default: 1.0, min: 0, max: 2 },
43
+ },
44
+ },
45
+ process: (_inputs, params) => ({
46
+ audio: { src: params.src, volume: params.volume },
47
+ duration: params.duration || 0,
48
+ }),
49
+ },
50
+ {
51
+ type: 'source/webp-sequence',
52
+ category: 'source',
53
+ icon: 'movie',
54
+ driver: {
55
+ description: 'WebP image sequence as video frames',
56
+ capabilities: ['source', 'video'],
57
+ inputs: [],
58
+ outputs: [
59
+ { name: 'frames', type: 'image' },
60
+ { name: 'frameCount', type: 'int' },
61
+ ],
62
+ params: {
63
+ directory: { type: 'string', required: true },
64
+ pattern: { type: 'string', default: '*.webp' },
65
+ fps: { type: 'int', default: 30, min: 1, max: 120 },
66
+ },
67
+ },
68
+ process: (_inputs, params) => ({
69
+ frames: { directory: params.directory, pattern: params.pattern, fps: params.fps },
70
+ frameCount: params.frameCount || 0,
71
+ }),
72
+ },
73
+ {
74
+ type: 'source/image',
75
+ category: 'source',
76
+ icon: 'image',
77
+ driver: {
78
+ description: 'Static image source',
79
+ capabilities: ['source', 'image'],
80
+ inputs: [],
81
+ outputs: [{ name: 'image', type: 'image' }],
82
+ params: {
83
+ src: { type: 'string', required: true },
84
+ fit: { type: 'string', default: 'cover', enum: ['cover', 'contain', 'fill', 'none'] },
85
+ },
86
+ },
87
+ process: (_inputs, params) => ({
88
+ image: { src: params.src, fit: params.fit },
89
+ }),
90
+ },
91
+ {
92
+ type: 'source/text',
93
+ category: 'source',
94
+ icon: 'text_fields',
95
+ driver: {
96
+ description: 'Text source for overlays and subtitles',
97
+ capabilities: ['source', 'text'],
98
+ inputs: [],
99
+ outputs: [{ name: 'text', type: 'string' }],
100
+ params: {
101
+ content: { type: 'string', default: '' },
102
+ style: { type: 'string', default: 'default' },
103
+ },
104
+ },
105
+ process: (_inputs, params) => ({
106
+ text: { content: params.content, style: params.style },
107
+ }),
108
+ },
109
+
110
+ // ─── Analysis ──────────────────────────────────────────────────────
111
+ {
112
+ type: 'analysis/beat-analyzer',
113
+ category: 'analysis',
114
+ icon: 'graphic_eq',
115
+ driver: {
116
+ description: 'Analyzes audio for beats, energy, and generates effect skeleton',
117
+ capabilities: ['analysis', 'audio', 'ai'],
118
+ inputs: [{ name: 'audio', type: 'audio', required: true }],
119
+ outputs: [
120
+ { name: 'skeleton', type: 'skeleton' },
121
+ { name: 'beats', type: 'float' },
122
+ { name: 'energy', type: 'float' },
123
+ ],
124
+ params: {
125
+ energyPerSecond: { type: 'int', default: 10, min: 1, max: 100 },
126
+ strongThreshold: { type: 'float', default: 1.3, min: 0.5, max: 3 },
127
+ },
128
+ },
129
+ process: (inputs, params) => ({
130
+ skeleton: {
131
+ intensityZones: [],
132
+ fadeZones: [],
133
+ dropPoints: [],
134
+ beatMarkers: [],
135
+ transitionAnchors: [],
136
+ },
137
+ beats: [],
138
+ energy: [],
139
+ }),
140
+ },
141
+ {
142
+ type: 'analysis/ai-director',
143
+ category: 'analysis',
144
+ icon: 'smart_toy',
145
+ driver: {
146
+ description: 'AI-driven timeline composition from skeleton and prompt',
147
+ capabilities: ['analysis', 'ai', 'llm'],
148
+ inputs: [
149
+ { name: 'skeleton', type: 'skeleton', required: true },
150
+ { name: 'prompt', type: 'string' },
151
+ ],
152
+ outputs: [{ name: 'timeline', type: 'timeline' }],
153
+ params: {
154
+ model: { type: 'string', default: 'auto' },
155
+ temperature: { type: 'float', default: 0.7, min: 0, max: 2 },
156
+ },
157
+ constraints: { requiresSecret: 'OPENAI_API_KEY' },
158
+ },
159
+ },
160
+
161
+ // ─── Processing ────────────────────────────────────────────────────
162
+ {
163
+ type: 'processing/physics-vfx',
164
+ category: 'processing',
165
+ icon: 'bolt',
166
+ driver: {
167
+ description: 'Physics-based visual effects synced to beats',
168
+ capabilities: ['effects', 'animation'],
169
+ inputs: [
170
+ { name: 'input', type: 'any' },
171
+ { name: 'beats', type: 'float' },
172
+ ],
173
+ outputs: [{ name: 'output', type: 'effect' }],
174
+ params: {
175
+ preset: { type: 'string', default: 'bounceIn', enum: ['bounceIn', 'dropImpact', 'glitch', 'shake', 'zoom', 'rubberBand', 'pulse'] },
176
+ beatSync: { type: 'boolean', default: true },
177
+ intensity: { type: 'float', default: 1.0, min: 0, max: 3 },
178
+ },
179
+ },
180
+ },
181
+ {
182
+ type: 'processing/color-correction',
183
+ category: 'processing',
184
+ icon: 'palette',
185
+ driver: {
186
+ description: 'Color grading and correction filters',
187
+ capabilities: ['effects', 'color'],
188
+ inputs: [{ name: 'input', type: 'image', required: true }],
189
+ outputs: [{ name: 'output', type: 'image' }],
190
+ params: {
191
+ brightness: { type: 'float', default: 0, min: -100, max: 100 },
192
+ contrast: { type: 'float', default: 0, min: -100, max: 100 },
193
+ saturation: { type: 'float', default: 0, min: -100, max: 100 },
194
+ hueRotate: { type: 'float', default: 0, min: 0, max: 360 },
195
+ },
196
+ },
197
+ },
198
+ {
199
+ type: 'processing/transition',
200
+ category: 'processing',
201
+ icon: 'shuffle',
202
+ driver: {
203
+ description: 'Transition effect between two sources',
204
+ capabilities: ['effects', 'transition'],
205
+ inputs: [
206
+ { name: 'from', type: 'image', required: true },
207
+ { name: 'to', type: 'image', required: true },
208
+ ],
209
+ outputs: [{ name: 'output', type: 'image' }],
210
+ params: {
211
+ type: { type: 'string', default: 'fade', enum: ['fade', 'slide', 'wipe', 'zoom', 'dissolve'] },
212
+ duration: { type: 'int', default: 30, min: 1 },
213
+ direction: { type: 'string', default: 'left', enum: ['left', 'right', 'up', 'down'] },
214
+ },
215
+ },
216
+ },
217
+
218
+ // ─── Composition ───────────────────────────────────────────────────
219
+ {
220
+ type: 'composition/layout',
221
+ category: 'composition',
222
+ icon: 'grid_view',
223
+ driver: {
224
+ description: 'Positions content in the viewport',
225
+ capabilities: ['composition', 'layout'],
226
+ inputs: [{ name: 'content', type: 'any', required: true }],
227
+ outputs: [{ name: 'output', type: 'image' }],
228
+ params: {
229
+ anchor: { type: 'string', default: 'center' },
230
+ x: { type: 'string', default: '50%' },
231
+ y: { type: 'string', default: '50%' },
232
+ width: { type: 'string', default: '100%' },
233
+ height: { type: 'string', default: '100%' },
234
+ rotation: { type: 'float', default: 0 },
235
+ },
236
+ },
237
+ },
238
+ {
239
+ type: 'composition/blend',
240
+ category: 'composition',
241
+ icon: 'theaters',
242
+ driver: {
243
+ description: 'Blends/composites two image layers',
244
+ capabilities: ['composition', 'blend'],
245
+ inputs: [
246
+ { name: 'base', type: 'image', required: true },
247
+ { name: 'overlay', type: 'image', required: true },
248
+ ],
249
+ outputs: [{ name: 'output', type: 'image' }],
250
+ params: {
251
+ mode: { type: 'string', default: 'normal', enum: ['normal', 'multiply', 'screen', 'overlay', 'add'] },
252
+ opacity: { type: 'float', default: 1.0, min: 0, max: 1 },
253
+ },
254
+ },
255
+ },
256
+ {
257
+ type: 'composition/timeline',
258
+ category: 'composition',
259
+ icon: 'timer',
260
+ driver: {
261
+ description: 'Assembles layers into a timeline for rendering',
262
+ capabilities: ['composition', 'timeline'],
263
+ inputs: [
264
+ { name: 'layers', type: 'any', required: true },
265
+ { name: 'dynamics', type: 'skeleton' },
266
+ ],
267
+ outputs: [{ name: 'timeline', type: 'timeline' }],
268
+ params: {
269
+ fps: { type: 'int', default: 30, min: 1, max: 120 },
270
+ duration: { type: 'string', default: 'auto' },
271
+ },
272
+ },
273
+ },
274
+
275
+ // ─── Output ────────────────────────────────────────────────────────
276
+ {
277
+ type: 'output/viewport',
278
+ category: 'output',
279
+ icon: 'desktop_windows',
280
+ driver: {
281
+ description: 'Preview viewport for real-time monitoring',
282
+ capabilities: ['output', 'preview'],
283
+ inputs: [{ name: 'timeline', type: 'timeline', required: true }],
284
+ outputs: [],
285
+ params: {
286
+ width: { type: 'int', default: 1080, min: 1 },
287
+ height: { type: 'int', default: 1920, min: 1 },
288
+ background: { type: 'string', default: '#000000' },
289
+ },
290
+ },
291
+ },
292
+ {
293
+ type: 'output/render',
294
+ category: 'output',
295
+ icon: 'videocam',
296
+ driver: {
297
+ description: 'Renders timeline to video file',
298
+ capabilities: ['output', 'render'],
299
+ inputs: [{ name: 'timeline', type: 'timeline', required: true }],
300
+ outputs: [],
301
+ params: {
302
+ format: { type: 'string', default: 'mp4', enum: ['mp4', 'webm', 'mov'] },
303
+ codec: { type: 'string', default: 'h264', enum: ['h264', 'h265', 'vp9', 'av1'] },
304
+ quality: { type: 'string', default: 'high', enum: ['low', 'medium', 'high', 'lossless'] },
305
+ preset: { type: 'string', default: 'vertical', enum: ['vertical', 'horizontal', 'square'] },
306
+ },
307
+ },
308
+ },
309
+ ];
310
+
311
+ /**
312
+ * Register the video pack
313
+ */
314
+ export function registerVideoPack() {
315
+ registerPack({
316
+ name: 'video',
317
+ socketTypes,
318
+ nodes,
319
+ });
320
+ }
321
+
322
+ // Auto-register when imported
323
+ registerVideoPack();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symbiote-node",
3
- "version": "0.3.0-alpha.0",
3
+ "version": "0.3.0-alpha.1",
4
4
  "type": "module",
5
5
  "description": "Visual Node Graph Editor + Execution Engine — extensible, themeable graph editor and runtime built on Symbiote.js",
6
6
  "main": "index.js",
@@ -56,4 +56,4 @@
56
56
  "engines": {
57
57
  "node": ">=18.0.0"
58
58
  }
59
- }
59
+ }
package/web/app.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // @ctx .context/web/app.ctx
2
2
  import{Layout as e,LayoutTree as t,applyTheme as n,CARBON as o}from"symbiote-node";
3
+ import{followController}from"./follow-controller.js";
4
+ import"./components/follow-ribbon.js";
3
5
  import{state as a,subscribe as s,onEvent as i,call as r,connect as c}from"./state.js";
4
6
  import"./panels/file-tree.js";
5
7
  import"./panels/code-viewer.js";
@@ -16,9 +18,10 @@ export const baseUrl=new URL(".",import.meta.url).href;const l=baseUrl;
16
18
  export async function api(e,t={}){if(a.connected&&e.startsWith("/api/")){const n=await async function(e,t){const n={"/api/skeleton":{name:"get_skeleton",args:e=>({path:e.path})},"/api/file":{name:"compact",args:e=>({action:"compact_file",path:e.path,beautify:!0})},"/api/analysis":{name:"analyze",args:e=>({action:"full_analysis",path:e.path})},"/api/analysis-summary":{name:"analyze",args:e=>({action:"analysis_summary",path:e.path})},"/api/deps":{name:"navigate",args:e=>({action:"deps",symbol:e.symbol})},"/api/usages":{name:"navigate",args:e=>({action:"usages",symbol:e.symbol})},"/api/expand":{name:"navigate",args:e=>({action:"expand",symbol:e.symbol})},"/api/chain":{name:"navigate",args:e=>({action:"call_chain",from:e.from,to:e.to})}}[e];return n?r(n.name,n.args(t)):null}(e,t);if(null!==n)return n}const n=new URLSearchParams(t).toString(),o=e.replace(/^\//, ""),s=n?`${l}${o}?${n}`:`${l}${o}`,i=await fetch(s);if(!i.ok)throw new Error(`API error: ${i.status}`);return i.json()}
17
19
  export const events=new EventTarget;
18
20
  export function emit(e,t={}){events.dispatchEvent(new CustomEvent(e,{detail:t}))}
19
- const p={"file-tree":{title:"Files",icon:"folder",component:"pg-file-tree"},"code-viewer":{title:"Code",icon:"code",component:"pg-code-viewer"},"ctx-panel":{title:"Documentation",icon:"description",component:"pg-ctx-panel"},"dep-graph":{title:"Dependencies",icon:"account_tree",component:"pg-dep-graph"},health:{title:"Health",icon:"analytics",component:"pg-health-panel"},monitor:{title:"Live Monitor",icon:"monitor_heart",component:"pg-live-monitor"},settings:{title:"Settings",icon:"settings",component:"pg-settings-panel"}},m=[{id:"explorer",icon:"folder_open",label:"Explorer"},{id:"graph",icon:"developer_board",label:"Graph"},{id:"analysis",icon:"analytics",label:"Analysis"},{id:"monitor",icon:"monitor_heart",label:"Monitor"},{id:"settings",icon:"settings",label:"Settings"}],d={explorer:()=>t.createSplit("horizontal",t.createPanel("file-tree"),t.createSplit("horizontal",t.createPanel("code-viewer"),t.createPanel("ctx-panel"),.65),.2),graph:()=>t.createSplit("horizontal",t.createPanel("file-tree"),t.createPanel("dep-graph"),.18),analysis:()=>t.createPanel("health"),monitor:()=>t.createPanel("monitor"),settings:()=>t.createPanel("settings")};
21
+ const p={"file-tree":{title:"Files",icon:"folder",component:"pg-file-tree"},"code-viewer":{title:"Code",icon:"code",component:"pg-code-viewer"},"ctx-panel":{title:"Documentation",icon:"description",component:"pg-ctx-panel"},"dep-graph":{title:"Dependencies",icon:"account_tree",component:"pg-dep-graph"},health:{title:"Health",icon:"analytics",component:"pg-health-panel"},monitor:{title:"Live Monitor",icon:"monitor_heart",component:"pg-live-monitor"},settings:{title:"Settings",icon:"settings",component:"pg-settings-panel"}},m=[{id:"explorer",icon:"folder_open",label:"Explorer"},{id:"graph",icon:"developer_board",label:"Graph"},{id:"follow",icon:"smart_toy",label:"Follow"},{id:"analysis",icon:"analytics",label:"Analysis"},{id:"monitor",icon:"monitor_heart",label:"Monitor"},{id:"settings",icon:"settings",label:"Settings"}],d={explorer:()=>t.createSplit("horizontal",t.createPanel("file-tree"),t.createSplit("horizontal",t.createPanel("code-viewer"),t.createPanel("ctx-panel"),.65),.2),graph:()=>t.createSplit("horizontal",t.createPanel("file-tree"),t.createPanel("dep-graph"),.18),follow:()=>t.createSplit("horizontal",t.createPanel("file-tree"),t.createSplit("vertical",t.createSplit("horizontal",t.createPanel("dep-graph"),t.createPanel("code-viewer"),.65),t.createPanel("monitor"),.72),.12),analysis:()=>t.createPanel("health"),monitor:()=>t.createPanel("monitor"),settings:()=>t.createPanel("settings")};
20
22
  async function u(){(function(){n(document.documentElement,o);const e=document.querySelector(".app-workspace"),t=document.createElement("layout-sidebar");e.prepend(t);const a=e.querySelector(".app-content"),s=document.createElement("panel-layout");s.setAttribute("storage-key","pg-explorer-layout"),s.setAttribute("min-panel-size","150"),s.id="main-layout",a.appendChild(s),requestAnimationFrame(()=>{for(const[e,t]of Object.entries(p))s.registerPanelType(e,t);let lastSection="";function e(){const e=location.hash.replace("#","")||"explorer",t=e.indexOf("?"),n=t>=0?e.substring(0,t):e,o=n.indexOf("/"),a=o>=0?n.substring(0,o):n,i=o>=0?n.substring(o+1):"";if(d[a]&&a!==lastSection){lastSection=a;s.setLayout(d[a]())}"explorer"===a&&i&&requestAnimationFrame(()=>{state.activeFile=i,emit("file-selected",{path:i,fromRoute:!0})})}t.setSections(m),window.addEventListener("hashchange",e),events.addEventListener("file-selected",e=>{if(e.detail.fromRoute)return;if(e.detail.source==="canvas")return;const t=e.detail.path;const _sec=(location.hash.replace("#","").split("?")[0].split("/")[0])||"explorer";if(t&&_sec==="explorer")history.replaceState(null,"",`#explorer/${t}`)}),localStorage.getItem("pg-explorer-layout")||s.setLayout(d.explorer()),location.hash&&"#"!==location.hash?e():location.hash="explorer"})})(),s("project",e=>{e&&(document.title=`${e.name} — Project Graph`,document.getElementById("project-name").textContent=e.name,document.documentElement.style.setProperty("--project-accent",e.color),g(e.agents))}),events.addEventListener("skeleton-loaded",e=>{const t=e.detail;if(!t)return;state.skeleton=t;const n=new Set;for(const e of Object.values(t.n||{}))e.f&&n.add(e.f);for(const e of Object.keys(t.X||{}))n.add(e);for(const[e,o]of Object.entries(t.f||{}))for(const t of o)n.add("./"===e?t:`${e}${t}`);for(const[e,o]of Object.entries(t.a||{}))for(const t of o)n.add("./"===e?t:`${e}${t}`);const o=document.getElementById("project-files");o&&(o.textContent=`${n.size} files`)}),s("skeleton",e=>{if(!e)return;state.skeleton=e;emit("skeleton-loaded",e)}),s("connected",e=>{const t=document.getElementById("status-indicator");t&&(t.className=e?"status connected":"status disconnected")}),i(e=>{if("agent_connect"===e.type||"agent_disconnect"===e.type)return g(e.agents),void emit("agent-event",e);state.monitorEvents.push(e),state.monitorEvents.length>500&&state.monitorEvents.shift(),emit("tool-event",e)}),c()}
21
23
  function g(e){let t=document.getElementById("agent-badge");if(!t){const e=document.querySelector(".app-topbar");if(!e)return;t=document.createElement("span"),t.id="agent-badge",t.className="agent-badge",e.appendChild(t)}t.textContent=e>0?`● ${e} agent${1!==e?"s":""}`:"",t.style.display=e>0?"":"none"}
22
24
  function f(){document.querySelector("pg-quick-open")||document.body.appendChild(document.createElement("pg-quick-open"))}
23
- function h(){const btn=document.getElementById("follow-btn");if(!btn)return;let active=false;btn.addEventListener("click",()=>{active=!active;if(active){btn.setAttribute("data-active","");btn.classList.add("active")}else{btn.removeAttribute("data-active");btn.classList.remove("active")}events.dispatchEvent(new CustomEvent("follow-mode-changed",{detail:{enabled:active}}))})}
24
- "loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{u(),f(),h()}):(u(),f(),h());
25
+ function h(){const btn=document.getElementById("follow-btn");if(!btn)return;let active=false;btn.addEventListener("click",()=>{active=!active;if(active){btn.setAttribute("data-active","");btn.classList.add("active");followController.enable();location.hash="follow"}else{btn.removeAttribute("data-active");btn.classList.remove("active");const prev=followController.getPreviousHash();followController.disable();if(prev&&prev!=="#follow")location.hash=prev.replace(/^#/,"")}events.dispatchEvent(new CustomEvent("follow-mode-changed",{detail:{enabled:active}}))});events.addEventListener("follow-state-changed",e=>{const en=e.detail?.enabled;if(en&&!active){active=true;btn.setAttribute("data-active","");btn.classList.add("active")}else if(!en&&active){active=false;btn.removeAttribute("data-active");btn.classList.remove("active")}});window.addEventListener("hashchange",()=>{const sec=(location.hash.replace("#","").split("?")[0].split("/")[0])||"explorer";if(sec==="follow"&&!active){active=true;btn.setAttribute("data-active","");btn.classList.add("active");followController.enable()}else if(sec!=="follow"&&active){active=false;btn.removeAttribute("data-active");btn.classList.remove("active");followController.disable()}})}
26
+ function _initRibbon(){if(!document.querySelector("follow-ribbon"))document.body.appendChild(document.createElement("follow-ribbon"))}
27
+ "loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{u(),f(),followController.init(events,emit),h(),_initRibbon()}):(u(),f(),followController.init(events,emit),h(),_initRibbon());
@@ -16,12 +16,18 @@ const HIT_RADIUS = 14;
16
16
  function getNodeRadius(node, conns, opts = {}) {
17
17
  const hubScale = 1 + Math.min(conns, 8) * 0.1;
18
18
  const aScale = opts.scale ?? (node.aScale || 1);
19
- let r = DOT_RADIUS * hubScale * aScale;
19
+ let baseR = DOT_RADIUS * hubScale * aScale;
20
20
  if (node.isGroup) {
21
- const childCount = node.children?.length || 1;
22
- r *= 1.0 + Math.sqrt(Math.max(1, Math.min(childCount, 25))) * 0.5;
21
+ const childCount = Math.max(2, Math.min(12, node.children?.length || 3));
22
+ const innerR = baseR * Math.max(0.1, 0.18 - (childCount - 3) * 0.008);
23
+ const spacing = innerR * 2.5;
24
+ const orbitR = spacing / (2 * Math.sin(Math.PI / childCount));
25
+ // r = orbitR + innerR + ringW + padding
26
+ // ringW = r * 0.12
27
+ // r = (orbitR + innerR + 2) / 0.88
28
+ return (orbitR + innerR + 2) / 0.88;
23
29
  }
24
- return r;
30
+ return baseR;
25
31
  }
26
32
 
27
33
  const NODE_TYPES = ['data', 'action', 'output', 'config', 'external', 'style', 'docs', 'asset'];
@@ -280,6 +286,17 @@ export class CanvasGraph extends Symbiote {
280
286
  this._wakeLoop();
281
287
  }
282
288
 
289
+ pulseNode(nodeId, durationMs = 1500) {
290
+ this._pulses = this._pulses || [];
291
+ this._pulses.push({
292
+ id: nodeId,
293
+ startTime: performance.now(),
294
+ duration: durationMs
295
+ });
296
+ this.needsDraw = true;
297
+ this._wakeLoop();
298
+ }
299
+
283
300
  flyToNode(nodeId, options = {}) {
284
301
  const node = this.graphDB?.nodes.get(nodeId);
285
302
  if (node && node.parentId) {
@@ -617,8 +634,8 @@ export class CanvasGraph extends Symbiote {
617
634
  nodeWidth: this.renderMode === 'dots' ? DOT_RADIUS * 2 : 160,
618
635
  nodeHeight: this.renderMode === 'dots' ? DOT_RADIUS * 2 : 40,
619
636
  mode: 'continuous',
620
- activeGroupId: groupId,
621
- boundaryRadius: groupId ? this.graphDB.nodes.get(groupId).w / 2 : null,
637
+ activeGroupId: this.currentGroupId,
638
+ boundaryRadius: this.currentGroupId ? this.graphDB.nodes.get(this.currentGroupId).w / 2 : null,
622
639
  attractors: null,
623
640
  };
624
641
 
@@ -1063,15 +1080,13 @@ export class CanvasGraph extends Symbiote {
1063
1080
  currentCtx.fillStyle = this.blendBg(tc[0], tc[1], tc[2], layerOpacity);
1064
1081
  currentCtx.fill();
1065
1082
 
1083
+ let baseR = DOT_RADIUS * hubScale * (node.aScale || 1);
1066
1084
  const childCount = Math.max(2, Math.min(12, node.children?.length || 3));
1067
- const innerR = r * Math.max(0.1, 0.18 - (childCount - 3) * 0.008);
1085
+ const innerR = baseR * Math.max(0.1, 0.18 - (childCount - 3) * 0.008);
1068
1086
 
1069
1087
  // Calculate perfect orbit radius to maintain consistent spacing between dots
1070
1088
  const spacing = innerR * 2.5; // Gap between dots
1071
- const idealOrbitR = spacing / (2 * Math.sin(Math.PI / childCount));
1072
- // Ensure they never spill out of the golden ring
1073
- const maxOrbitR = Math.max(0, r - ringW - innerR - 2);
1074
- const orbitR = Math.min(idealOrbitR, maxOrbitR);
1089
+ const orbitR = spacing / (2 * Math.sin(Math.PI / childCount));
1075
1090
  const isHovered = this.hoverNode && this.hoverNode.id === node.id;
1076
1091
  node.aRotSpeed = node.aRotSpeed || 0;
1077
1092
  const targetRotSpeed = (isActive || isHovered) ? 0.025 : 0;
@@ -1130,6 +1145,29 @@ export class CanvasGraph extends Symbiote {
1130
1145
  mainCtx.setTransform(dpr * this.zoom, 0, 0, dpr * this.zoom, dpr * this.panX, dpr * this.panY);
1131
1146
  }
1132
1147
  drawDepth(0, mainCtx);
1148
+
1149
+ if (this._pulses && this._pulses.length > 0) {
1150
+ const now = performance.now();
1151
+ this._pulses = this._pulses.filter(p => {
1152
+ const elapsed = now - p.startTime;
1153
+ if (elapsed > p.duration) return false;
1154
+ const pos = this.getSmooth(p.id) || this.nodePositions.get(p.id);
1155
+ if (!pos) return false;
1156
+ const progress = elapsed / p.duration;
1157
+ const pulsePhase = (progress * 3) % 1;
1158
+ const r = 20 + (pulsePhase * 80);
1159
+ const opacity = 1 - pulsePhase;
1160
+ mainCtx.beginPath();
1161
+ mainCtx.arc(pos.x, pos.y, r, 0, Math.PI * 2);
1162
+ mainCtx.fillStyle = `rgba(76, 139, 245, ${opacity * 0.4})`;
1163
+ mainCtx.fill();
1164
+ mainCtx.lineWidth = 2;
1165
+ mainCtx.strokeStyle = `rgba(76, 139, 245, ${opacity * 0.8})`;
1166
+ mainCtx.stroke();
1167
+ this.needsDraw = true;
1168
+ return true;
1169
+ });
1170
+ }
1133
1171
  }
1134
1172
 
1135
1173
  const showMenu = this.activeNode && !this.dragNode && !this.deactivating;
@@ -1557,6 +1595,7 @@ export class CanvasGraph extends Symbiote {
1557
1595
  });
1558
1596
 
1559
1597
  this.canvas.addEventListener('pointerup', (e) => {
1598
+ const draggedNode = this.dragNode;
1560
1599
  if (this.dragNode) {
1561
1600
  this.worker.postMessage({ type: 'unpin', id: this.dragNode.id });
1562
1601
  this.dragNode = null;
@@ -29,6 +29,6 @@ this.$.highlighted=hl;
29
29
  const e=o.split("\n").length,t=[];for(let o=1;o<=e;o++)t.push(o);this.$.lineNums=t.join("\n");
30
30
  }
31
31
  }),this.sub("isMarkdown",v=>{this.toggleAttribute("mode-markdown",v)}),this.sub("isImage",v=>{this.toggleAttribute("mode-image",v)})
32
- }setBasePath(p){this._basePath=p}}
32
+ }setBasePath(p){this._basePath=p}scrollToLine(line){const pre=this.querySelector('.cb-pre');const scroll=this.querySelector('.cb-scroll');if(!pre||!scroll)return;const lineHeight=parseFloat(window.getComputedStyle(pre).lineHeight)||19.2;scroll.scrollTo({top:Math.max(0,(line-1)*lineHeight-scroll.clientHeight/2+lineHeight/2),behavior:'smooth'})}}
33
33
  CodeBlock.template='\n <div class="cb-scroll">\n <pre class="cb-gutter" bind="textContent: lineNums"></pre>\n <pre class="cb-pre"><code bind="innerHTML: highlighted"></code></pre>\n <div class="cb-md" bind="innerHTML: highlighted"></div>\n <div class="cb-img-wrap"><img class="cb-img" bind="src: imageSrc"></div>\n </div>\n';
34
34
  CodeBlock.rootStyles="\n code-block {\n display: block;\n height: 100%;\n overflow: hidden;\n }\n code-block .cb-scroll {\n display: flex;\n height: 100%;\n overflow: auto;\n align-items: stretch;\n }\n code-block .cb-gutter {\n position: sticky;\n left: 0;\n z-index: 1;\n margin: 0;\n padding: 12px 8px 12px 12px;\n font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;\n font-size: 12px;\n line-height: 1.6;\n text-align: right;\n color: var(--sn-text-dim, hsl(30, 10%, 55%));\n opacity: 0.45;\n background: var(--sn-bg, hsl(37, 30%, 96%));\n border-right: 1px solid var(--sn-node-border, hsl(35, 18%, 88%));\n user-select: none;\n white-space: pre;\n min-width: 32px;\n flex-shrink: 0;\n }\n code-block .cb-pre {\n margin: 0;\n padding: 12px;\n flex: 1;\n min-width: 0;\n font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;\n font-size: 12px;\n line-height: 1.6;\n color: var(--sn-text, hsl(30, 15%, 18%));\n tab-size: 2;\n white-space: pre;\n box-sizing: border-box;\n }\n /* Markdown container — hidden by default */\n code-block .cb-md {\n display: none;\n padding: 20px 28px;\n flex: 1;\n min-width: 0;\n overflow-wrap: break-word;\n word-wrap: break-word;\n line-height: 1.7;\n color: var(--sn-text, hsl(30, 15%, 18%));\n font-family: var(--sn-font, 'Inter', -apple-system, sans-serif);\n font-size: 14px;\n }\n /* Image container — hidden by default */\n code-block .cb-img-wrap {\n display: none;\n flex: 1;\n padding: 20px;\n justify-content: center;\n align-items: center;\n background: repeating-conic-gradient(hsl(30, 10%, 88%) 0% 25%, hsl(30, 10%, 94%) 0% 50%) 0 0 / 16px 16px;\n }\n code-block .cb-img {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n border-radius: 4px;\n box-shadow: 0 2px 12px rgba(0,0,0,0.12);\n }\n /* In markdown mode: hide code, show md */\n code-block[mode-markdown] .cb-gutter { display: none; }\n code-block[mode-markdown] .cb-pre { display: none; }\n code-block[mode-markdown] .cb-md { display: block; }\n /* In image mode: hide code, show image */\n code-block[mode-image] .cb-gutter { display: none; }\n code-block[mode-image] .cb-pre { display: none; }\n code-block[mode-image] .cb-img-wrap { display: flex; }\n\n /* Markdown styles */\n code-block .md-h { margin: 20px 0 8px; color: var(--sn-text, #222); font-weight: 700; }\n code-block h1.md-h { font-size: 24px; border-bottom: 2px solid var(--sn-node-border, #ddd); padding-bottom: 8px; }\n code-block h2.md-h { font-size: 20px; border-bottom: 1px solid var(--sn-node-border, #ddd); padding-bottom: 6px; }\n code-block h3.md-h { font-size: 16px; }\n code-block h4.md-h { font-size: 14px; }\n code-block .md-p { margin: 8px 0; }\n code-block .md-quote {\n margin: 8px 0;\n padding: 8px 16px;\n border-left: 4px solid var(--sn-cat-server, hsl(210, 45%, 55%));\n background: hsla(210, 40%, 55%, 0.08);\n border-radius: 0 4px 4px 0;\n font-style: italic;\n }\n code-block .md-list {\n margin: 8px 0;\n padding-left: 24px;\n }\n code-block .md-list li {\n margin: 3px 0;\n }\n code-block .md-code-block {\n margin: 12px 0;\n padding: 12px 16px;\n background: var(--sn-bg, hsl(37, 30%, 94%));\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 85%));\n border-radius: 6px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 12px;\n line-height: 1.6;\n overflow-x: auto;\n white-space: pre;\n }\n code-block .md-inline-code {\n padding: 1px 5px;\n background: hsla(30, 20%, 50%, 0.12);\n border-radius: 3px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 0.9em;\n }\n code-block .md-link {\n color: var(--sn-cat-server, hsl(210, 55%, 50%));\n text-decoration: underline;\n text-decoration-style: dotted;\n }\n code-block .md-link:hover { text-decoration-style: solid; }\n code-block .md-img {\n max-width: 100%;\n height: auto;\n border-radius: 6px;\n margin: 8px 0;\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 85%));\n box-shadow: 0 2px 8px rgba(0,0,0,0.08);\n }\n code-block .md-hr {\n border: none;\n border-top: 1px solid var(--sn-node-border, hsl(35, 18%, 85%));\n margin: 16px 0;\n }\n code-block .md-table {\n width: 100%;\n border-collapse: collapse;\n margin: 12px 0;\n font-size: 13px;\n }\n code-block .md-table th,\n code-block .md-table td {\n padding: 6px 12px;\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 85%));\n text-align: left;\n }\n code-block .md-table th {\n background: hsla(30, 15%, 50%, 0.08);\n font-weight: 600;\n }\n code-block .md-table tr:hover td {\n background: hsla(30, 15%, 50%, 0.04);\n }\n /* Token colors */\n code-block .t-kw { color: rgb(254, 165, 176); }\n code-block .t-str { color: rgb(251, 182, 79); }\n code-block .t-cm { color: rgb(149, 149, 149); font-style: italic; }\n code-block .t-fn { color: rgb(180, 243, 255); }\n code-block .t-num { color: rgb(251, 182, 79); }\n code-block .t-bi { color: rgb(180, 243, 255); }\n code-block .t-prop { color: rgb(238, 131, 252); }\n code-block .t-lit { color: rgb(254, 165, 176); }\n /* JSDoc */\n code-block .t-jd { color: rgb(130, 155, 130); font-style: italic; }\n code-block .t-jd-tag { color: rgb(180, 220, 140); font-style: normal; font-weight: 500; }\n code-block .t-jd-type { color: rgb(130, 210, 240); font-style: normal; }\n",CodeBlock.reg("code-block");
@@ -20,36 +20,126 @@ export class MiniGraphWidget extends Symbiote {
20
20
  }
21
21
 
22
22
  _renderSVG(data) {
23
- // Basic SVG renderer for mini graphs to avoid WebGL context explosion
24
- // Extracts nodes and links if present in the data
25
23
  const nodes = data.nodes || (data.n ? Object.keys(data.n).map(id => ({ id, ...data.n[id] })) : []);
26
- const links = data.links || [];
24
+ const rawLinks = data.links || data.e || [];
25
+
26
+ // Some formats use {from, to}, some use {source, target}
27
+ const links = rawLinks.map(l => ({
28
+ from: l.from || l.source,
29
+ to: l.to || l.target
30
+ }));
27
31
 
28
32
  if (!nodes.length) {
29
33
  this.$.svgContent = '<text x="10" y="20" fill="var(--sn-text-dim)">No graph data</text>';
30
34
  return;
31
35
  }
32
36
 
33
- // Simple circle layout for demonstration
34
37
  const width = 300;
35
38
  const height = 150;
36
- const cx = width / 2;
37
- const cy = height / 2;
38
- const radius = 50;
39
+
40
+ // Initial deterministic positions (circle)
41
+ nodes.forEach((n, i) => {
42
+ const angle = (i / nodes.length) * Math.PI * 2;
43
+ n.x = width / 2 + Math.cos(angle) * (Math.min(width, height) / 4);
44
+ n.y = height / 2 + Math.sin(angle) * (Math.min(width, height) / 4);
45
+ n.vx = 0; n.vy = 0;
46
+ });
47
+
48
+ const K = 0.05; // Spring
49
+ const L = 40; // Ideal length
50
+ const REP = 300; // Repulsion
51
+ const DAMP = 0.8;
39
52
 
53
+ // Run 100 iterations
54
+ for (let i = 0; i < 100; i++) {
55
+ for (let j = 0; j < nodes.length; j++) {
56
+ for (let k = j + 1; k < nodes.length; k++) {
57
+ const a = nodes[j], b = nodes[k];
58
+ let dx = a.x - b.x, dy = a.y - b.y;
59
+ let dist = Math.sqrt(dx*dx + dy*dy) || 0.1;
60
+ let f = REP / (dist * dist);
61
+ const fx = (dx / dist) * f;
62
+ const fy = (dy / dist) * f;
63
+ a.vx += fx; a.vy += fy;
64
+ b.vx -= fx; b.vy -= fy;
65
+ }
66
+ }
67
+ links.forEach(link => {
68
+ const a = nodes.find(n => n.id === link.from);
69
+ const b = nodes.find(n => n.id === link.to);
70
+ if (!a || !b) return;
71
+ let dx = b.x - a.x, dy = b.y - a.y;
72
+ let dist = Math.sqrt(dx*dx + dy*dy) || 0.1;
73
+ let f = (dist - L) * K;
74
+ const fx = (dx / dist) * f;
75
+ const fy = (dy / dist) * f;
76
+ a.vx += fx; a.vy += fy;
77
+ b.vx -= fx; b.vy -= fy;
78
+ });
79
+ nodes.forEach(n => {
80
+ n.vx += (width/2 - n.x) * 0.015;
81
+ n.vy += (height/2 - n.y) * 0.015;
82
+ n.vx *= DAMP; n.vy *= DAMP;
83
+ n.x += n.vx; n.y += n.vy;
84
+ });
85
+ }
86
+
87
+ // Now build SVG
40
88
  let svg = '';
41
89
 
42
- // Draw links
43
- // (Needs actual layout logic, this is a placeholder circle layout)
90
+ // Bounds and scaling to fit within 300x150
91
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
92
+ nodes.forEach(n => {
93
+ minX = Math.min(minX, n.x);
94
+ minY = Math.min(minY, n.y);
95
+ maxX = Math.max(maxX, n.x);
96
+ maxY = Math.max(maxY, n.y);
97
+ });
44
98
 
45
- // Draw nodes
46
- nodes.forEach((n, i) => {
47
- const angle = (i / nodes.length) * Math.PI * 2;
48
- const x = cx + Math.cos(angle) * radius;
49
- const y = cy + Math.sin(angle) * radius;
50
- svg += `<circle cx="${x}" cy="${y}" r="4" fill="var(--sn-node-selected, #4c8bf5)"></circle>`;
99
+ const pad = 20;
100
+ const gW = Math.max(1, maxX - minX);
101
+ const gH = Math.max(1, maxY - minY);
102
+ const scale = Math.min((width - pad*2) / gW, (height - pad*2) / gH, 1.2);
103
+ const cx = (minX + maxX) / 2;
104
+ const cy = (minY + maxY) / 2;
105
+ const offX = width / 2 - cx * scale;
106
+ const offY = height / 2 - cy * scale;
107
+
108
+ // Draw Links
109
+ svg += '<g stroke="var(--sn-edge, #666)" stroke-width="1.5" opacity="0.5">';
110
+ links.forEach(link => {
111
+ const a = nodes.find(n => n.id === link.from);
112
+ const b = nodes.find(n => n.id === link.to);
113
+ if (!a || !b) return;
114
+ const x1 = a.x * scale + offX;
115
+ const y1 = a.y * scale + offY;
116
+ const x2 = b.x * scale + offX;
117
+ const y2 = b.y * scale + offY;
118
+
119
+ // Curved paths
120
+ const dx = x2 - x1, dy = y2 - y1;
121
+ const cx1 = x1 + dx * 0.3 - dy * 0.1;
122
+ const cy1 = y1 + dy * 0.3 + dx * 0.1;
123
+ const cx2 = x1 + dx * 0.7 - dy * 0.1;
124
+ const cy2 = y1 + dy * 0.7 + dx * 0.1;
125
+
126
+ svg += `<path d="M${x1},${y1} C${cx1},${cy1} ${cx2},${cy2} ${x2},${y2}" fill="none" />`;
127
+ });
128
+ svg += '</g>';
129
+
130
+ // Draw Nodes
131
+ svg += '<g>';
132
+ nodes.forEach(n => {
133
+ const x = n.x * scale + offX;
134
+ const y = n.y * scale + offY;
135
+ const tc = n.type === 'action' ? '#ff968c' :
136
+ n.type === 'output' ? '#78d2aa' :
137
+ n.type === 'config' ? '#ffc878' : '#78b4ff';
138
+
139
+ svg += `<circle cx="${x}" cy="${y}" r="4" fill="${tc}"></circle>`;
51
140
  svg += `<text x="${x + 6}" y="${y + 3}" fill="var(--sn-text)" font-size="10">${this._esc(n.id || n.name || 'node')}</text>`;
52
141
  });
142
+ svg += '</g>';
53
143
 
54
144
  this.$.svgContent = svg;
55
145
  }