agentopology 0.1.0

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 (174) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +450 -0
  3. package/dist/analyzer/__tests__/analyzer.test.d.ts +2 -0
  4. package/dist/analyzer/__tests__/analyzer.test.d.ts.map +1 -0
  5. package/dist/analyzer/__tests__/analyzer.test.js +258 -0
  6. package/dist/analyzer/__tests__/analyzer.test.js.map +1 -0
  7. package/dist/analyzer/index.d.ts +44 -0
  8. package/dist/analyzer/index.d.ts.map +1 -0
  9. package/dist/analyzer/index.js +308 -0
  10. package/dist/analyzer/index.js.map +1 -0
  11. package/dist/bindings/__tests__/bindings.test.d.ts +2 -0
  12. package/dist/bindings/__tests__/bindings.test.d.ts.map +1 -0
  13. package/dist/bindings/__tests__/bindings.test.js +2089 -0
  14. package/dist/bindings/__tests__/bindings.test.js.map +1 -0
  15. package/dist/bindings/claude-code.d.ts +12 -0
  16. package/dist/bindings/claude-code.d.ts.map +1 -0
  17. package/dist/bindings/claude-code.js +2063 -0
  18. package/dist/bindings/claude-code.js.map +1 -0
  19. package/dist/bindings/codex.d.ts +18 -0
  20. package/dist/bindings/codex.d.ts.map +1 -0
  21. package/dist/bindings/codex.js +1633 -0
  22. package/dist/bindings/codex.js.map +1 -0
  23. package/dist/bindings/copilot-cli.d.ts +20 -0
  24. package/dist/bindings/copilot-cli.d.ts.map +1 -0
  25. package/dist/bindings/copilot-cli.js +1515 -0
  26. package/dist/bindings/copilot-cli.js.map +1 -0
  27. package/dist/bindings/cursor.d.ts +17 -0
  28. package/dist/bindings/cursor.d.ts.map +1 -0
  29. package/dist/bindings/cursor.js +1715 -0
  30. package/dist/bindings/cursor.js.map +1 -0
  31. package/dist/bindings/gemini-cli.d.ts +18 -0
  32. package/dist/bindings/gemini-cli.d.ts.map +1 -0
  33. package/dist/bindings/gemini-cli.js +1327 -0
  34. package/dist/bindings/gemini-cli.js.map +1 -0
  35. package/dist/bindings/index.d.ts +20 -0
  36. package/dist/bindings/index.d.ts.map +1 -0
  37. package/dist/bindings/index.js +33 -0
  38. package/dist/bindings/index.js.map +1 -0
  39. package/dist/bindings/kiro.d.ts +17 -0
  40. package/dist/bindings/kiro.d.ts.map +1 -0
  41. package/dist/bindings/kiro.js +1624 -0
  42. package/dist/bindings/kiro.js.map +1 -0
  43. package/dist/bindings/openclaw.d.ts +18 -0
  44. package/dist/bindings/openclaw.d.ts.map +1 -0
  45. package/dist/bindings/openclaw.js +1578 -0
  46. package/dist/bindings/openclaw.js.map +1 -0
  47. package/dist/bindings/types.d.ts +33 -0
  48. package/dist/bindings/types.d.ts.map +1 -0
  49. package/dist/bindings/types.js +23 -0
  50. package/dist/bindings/types.js.map +1 -0
  51. package/dist/cli/index.d.ts +19 -0
  52. package/dist/cli/index.d.ts.map +1 -0
  53. package/dist/cli/index.js +679 -0
  54. package/dist/cli/index.js.map +1 -0
  55. package/dist/docs/__tests__/docs.test.d.ts +2 -0
  56. package/dist/docs/__tests__/docs.test.d.ts.map +1 -0
  57. package/dist/docs/__tests__/docs.test.js +438 -0
  58. package/dist/docs/__tests__/docs.test.js.map +1 -0
  59. package/dist/docs/content.d.ts +16 -0
  60. package/dist/docs/content.d.ts.map +1 -0
  61. package/dist/docs/content.js +3273 -0
  62. package/dist/docs/content.js.map +1 -0
  63. package/dist/docs/index.d.ts +17 -0
  64. package/dist/docs/index.d.ts.map +1 -0
  65. package/dist/docs/index.js +117 -0
  66. package/dist/docs/index.js.map +1 -0
  67. package/dist/exporters/__tests__/exporters.test.d.ts +2 -0
  68. package/dist/exporters/__tests__/exporters.test.d.ts.map +1 -0
  69. package/dist/exporters/__tests__/exporters.test.js +731 -0
  70. package/dist/exporters/__tests__/exporters.test.js.map +1 -0
  71. package/dist/exporters/index.d.ts +13 -0
  72. package/dist/exporters/index.d.ts.map +1 -0
  73. package/dist/exporters/index.js +18 -0
  74. package/dist/exporters/index.js.map +1 -0
  75. package/dist/exporters/json.d.ts +8 -0
  76. package/dist/exporters/json.d.ts.map +1 -0
  77. package/dist/exporters/json.js +25 -0
  78. package/dist/exporters/json.js.map +1 -0
  79. package/dist/exporters/markdown.d.ts +9 -0
  80. package/dist/exporters/markdown.d.ts.map +1 -0
  81. package/dist/exporters/markdown.js +686 -0
  82. package/dist/exporters/markdown.js.map +1 -0
  83. package/dist/exporters/mermaid.d.ts +11 -0
  84. package/dist/exporters/mermaid.d.ts.map +1 -0
  85. package/dist/exporters/mermaid.js +161 -0
  86. package/dist/exporters/mermaid.js.map +1 -0
  87. package/dist/exporters/types.d.ts +22 -0
  88. package/dist/exporters/types.d.ts.map +1 -0
  89. package/dist/exporters/types.js +10 -0
  90. package/dist/exporters/types.js.map +1 -0
  91. package/dist/import/__tests__/import.test.d.ts +2 -0
  92. package/dist/import/__tests__/import.test.d.ts.map +1 -0
  93. package/dist/import/__tests__/import.test.js +781 -0
  94. package/dist/import/__tests__/import.test.js.map +1 -0
  95. package/dist/import/claude-code.d.ts +38 -0
  96. package/dist/import/claude-code.d.ts.map +1 -0
  97. package/dist/import/claude-code.js +897 -0
  98. package/dist/import/claude-code.js.map +1 -0
  99. package/dist/import/index.d.ts +18 -0
  100. package/dist/import/index.d.ts.map +1 -0
  101. package/dist/import/index.js +28 -0
  102. package/dist/import/index.js.map +1 -0
  103. package/dist/import/serializer.d.ts +11 -0
  104. package/dist/import/serializer.d.ts.map +1 -0
  105. package/dist/import/serializer.js +960 -0
  106. package/dist/import/serializer.js.map +1 -0
  107. package/dist/index.d.ts +26 -0
  108. package/dist/index.d.ts.map +1 -0
  109. package/dist/index.js +21 -0
  110. package/dist/index.js.map +1 -0
  111. package/dist/parser/__tests__/parser.test.d.ts +2 -0
  112. package/dist/parser/__tests__/parser.test.d.ts.map +1 -0
  113. package/dist/parser/__tests__/parser.test.js +6514 -0
  114. package/dist/parser/__tests__/parser.test.js.map +1 -0
  115. package/dist/parser/ast.d.ts +708 -0
  116. package/dist/parser/ast.d.ts.map +1 -0
  117. package/dist/parser/ast.js +12 -0
  118. package/dist/parser/ast.js.map +1 -0
  119. package/dist/parser/index.d.ts +253 -0
  120. package/dist/parser/index.d.ts.map +1 -0
  121. package/dist/parser/index.js +1985 -0
  122. package/dist/parser/index.js.map +1 -0
  123. package/dist/parser/lexer.d.ts +100 -0
  124. package/dist/parser/lexer.d.ts.map +1 -0
  125. package/dist/parser/lexer.js +304 -0
  126. package/dist/parser/lexer.js.map +1 -0
  127. package/dist/parser/validator.d.ts +42 -0
  128. package/dist/parser/validator.d.ts.map +1 -0
  129. package/dist/parser/validator.js +2568 -0
  130. package/dist/parser/validator.js.map +1 -0
  131. package/dist/scaffold/__tests__/scaffold.test.d.ts +7 -0
  132. package/dist/scaffold/__tests__/scaffold.test.d.ts.map +1 -0
  133. package/dist/scaffold/__tests__/scaffold.test.js +295 -0
  134. package/dist/scaffold/__tests__/scaffold.test.js.map +1 -0
  135. package/dist/scaffold/incremental.d.ts +43 -0
  136. package/dist/scaffold/incremental.d.ts.map +1 -0
  137. package/dist/scaffold/incremental.js +185 -0
  138. package/dist/scaffold/incremental.js.map +1 -0
  139. package/dist/scaffold/index.d.ts +13 -0
  140. package/dist/scaffold/index.d.ts.map +1 -0
  141. package/dist/scaffold/index.js +13 -0
  142. package/dist/scaffold/index.js.map +1 -0
  143. package/dist/scaffold/manifest.d.ts +30 -0
  144. package/dist/scaffold/manifest.d.ts.map +1 -0
  145. package/dist/scaffold/manifest.js +71 -0
  146. package/dist/scaffold/manifest.js.map +1 -0
  147. package/dist/scaffold/merge.d.ts +52 -0
  148. package/dist/scaffold/merge.d.ts.map +1 -0
  149. package/dist/scaffold/merge.js +134 -0
  150. package/dist/scaffold/merge.js.map +1 -0
  151. package/dist/scaffold/types.d.ts +51 -0
  152. package/dist/scaffold/types.d.ts.map +1 -0
  153. package/dist/scaffold/types.js +10 -0
  154. package/dist/scaffold/types.js.map +1 -0
  155. package/dist/sync/index.d.ts +34 -0
  156. package/dist/sync/index.d.ts.map +1 -0
  157. package/dist/sync/index.js +392 -0
  158. package/dist/sync/index.js.map +1 -0
  159. package/dist/visualizer/index.d.ts +17 -0
  160. package/dist/visualizer/index.d.ts.map +1 -0
  161. package/dist/visualizer/index.js +1976 -0
  162. package/dist/visualizer/index.js.map +1 -0
  163. package/examples/code-review.at +167 -0
  164. package/examples/data-processing.at +296 -0
  165. package/examples/openclaw-assistant.at +142 -0
  166. package/examples/scheduled-monitor.at +193 -0
  167. package/examples/simple-pipeline.at +163 -0
  168. package/package.json +77 -0
  169. package/skill/SKILL.md +403 -0
  170. package/skill/scripts/emit.ts +853 -0
  171. package/skill/scripts/visualize.ts +89 -0
  172. package/spec/grammar.md +1466 -0
  173. package/spec/reserved-keywords.md +147 -0
  174. package/spec/validation.md +535 -0
@@ -0,0 +1,1976 @@
1
+ /**
2
+ * AgenTopology Visualizer.
3
+ *
4
+ * Generates a self-contained HTML file that renders an interactive
5
+ * topology graph from a parsed TopologyAST. No external dependencies —
6
+ * all CSS and JS are inlined.
7
+ *
8
+ * @module
9
+ */
10
+ import fs from "node:fs";
11
+ import Module from "node:module";
12
+ import { fileURLToPath } from "node:url";
13
+ import { dirname, join } from "node:path";
14
+ import { validate } from "../parser/validator.js";
15
+ // ---------------------------------------------------------------------------
16
+ // Load dagre source for inlining into the generated HTML
17
+ // ---------------------------------------------------------------------------
18
+ const __filename_viz = fileURLToPath(import.meta.url);
19
+ const __dirname_viz = dirname(__filename_viz);
20
+ function loadDagreSrc() {
21
+ // Use createRequire to resolve dagre — works regardless of hoisting/nesting
22
+ try {
23
+ const { createRequire } = Module;
24
+ const req = createRequire(import.meta.url);
25
+ const dagrePkg = req.resolve("dagre/package.json");
26
+ const dagrePath = join(dirname(dagrePkg), "dist", "dagre.min.js");
27
+ return fs.readFileSync(dagrePath, "utf-8");
28
+ }
29
+ catch {
30
+ // createRequire not available or dagre not resolvable — try manual paths
31
+ }
32
+ const candidates = [
33
+ join(__dirname_viz, "..", "..", "node_modules", "dagre", "dist", "dagre.min.js"),
34
+ join(__dirname_viz, "..", "node_modules", "dagre", "dist", "dagre.min.js"),
35
+ join(__dirname_viz, "node_modules", "dagre", "dist", "dagre.min.js"),
36
+ join(__dirname_viz, "..", "..", "..", "dagre", "dist", "dagre.min.js"),
37
+ join(__dirname_viz, "..", "..", "..", "..", "dagre", "dist", "dagre.min.js"),
38
+ join(process.cwd(), "node_modules", "dagre", "dist", "dagre.min.js"),
39
+ ];
40
+ for (const p of candidates) {
41
+ try {
42
+ return fs.readFileSync(p, "utf-8");
43
+ }
44
+ catch {
45
+ // try next
46
+ }
47
+ }
48
+ throw new Error("Could not find dagre.min.js — run: npm install dagre@0.8.5");
49
+ }
50
+ const DAGRE_SRC = loadDagreSrc();
51
+ // ---------------------------------------------------------------------------
52
+ // Public API
53
+ // ---------------------------------------------------------------------------
54
+ /**
55
+ * Generate a self-contained HTML string that visualizes the given topology.
56
+ *
57
+ * Save the returned string to a `.html` file and open it in any browser.
58
+ */
59
+ export function generateVisualization(ast) {
60
+ const dataJson = JSON.stringify(astToViewData(ast));
61
+ const issues = validate(ast);
62
+ const issuesJson = JSON.stringify(issues);
63
+ return buildHtml(dataJson, ast.topology.name, issuesJson);
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // AST -> view data (matches the DEFAULT_DATA shape in the advanced viewer)
67
+ // ---------------------------------------------------------------------------
68
+ /* eslint-disable @typescript-eslint/no-explicit-any */
69
+ function astToViewData(ast) {
70
+ const nodes = ast.nodes.map((n) => {
71
+ const base = { id: n.id, type: n.type, label: n.label };
72
+ switch (n.type) {
73
+ case "orchestrator": {
74
+ const o = n;
75
+ if (o.model)
76
+ base.model = o.model;
77
+ if (o.generates)
78
+ base.generates = o.generates;
79
+ if (o.handles && o.handles.length > 0)
80
+ base.handles = o.handles;
81
+ if (o.outputs)
82
+ base.outputs = o.outputs;
83
+ break;
84
+ }
85
+ case "agent": {
86
+ const a = n;
87
+ if (a.model)
88
+ base.model = a.model;
89
+ if (a.phase != null)
90
+ base.phase = a.phase;
91
+ if (a.permissions)
92
+ base.permissions = a.permissions;
93
+ if (a.tools)
94
+ base.tools = a.tools;
95
+ if (a.skills)
96
+ base.skills = a.skills;
97
+ if (a.reads)
98
+ base.reads = a.reads;
99
+ if (a.writes)
100
+ base.writes = a.writes;
101
+ if (a.outputs)
102
+ base.outputs = a.outputs;
103
+ if (a.role)
104
+ base.role = a.role;
105
+ if (a.description)
106
+ base.description = a.description;
107
+ if (a.invocation)
108
+ base.invocation = a.invocation;
109
+ if (a.behavior)
110
+ base.behavior = a.behavior;
111
+ if (a.skip)
112
+ base.skip = a.skip;
113
+ if (a.isolation)
114
+ base.isolation = a.isolation;
115
+ if (a.retry != null)
116
+ base.retry = a.retry;
117
+ if (a.background)
118
+ base.background = a.background;
119
+ if (a.mcpServers)
120
+ base.mcpServers = a.mcpServers;
121
+ if (a.scale)
122
+ base.scale = a.scale;
123
+ if (a.hooks && a.hooks.length > 0)
124
+ base.hooks = a.hooks;
125
+ if (a.disallowedTools)
126
+ base.disallowedTools = a.disallowedTools;
127
+ if (a.prompt)
128
+ base.prompt = a.prompt;
129
+ if (a.maxTurns != null)
130
+ base.maxTurns = a.maxTurns;
131
+ if (a.sandbox != null)
132
+ base.sandbox = a.sandbox;
133
+ if (a.fallbackChain)
134
+ base.fallbackChain = a.fallbackChain;
135
+ break;
136
+ }
137
+ case "gate": {
138
+ const g = n;
139
+ if (g.after)
140
+ base.after = g.after;
141
+ if (g.before)
142
+ base.before = g.before;
143
+ if (g.run)
144
+ base.run = g.run;
145
+ if (g.checks)
146
+ base.checks = g.checks;
147
+ if (g.onFail)
148
+ base.onFail = g.onFail;
149
+ if (g.behavior)
150
+ base.behavior = g.behavior;
151
+ if (g.retry != null)
152
+ base.retry = g.retry;
153
+ break;
154
+ }
155
+ case "action": {
156
+ const act = n;
157
+ if (act.kind)
158
+ base.kind = act.kind;
159
+ if (act.description)
160
+ base.description = act.description;
161
+ if (act.source)
162
+ base.source = act.source;
163
+ if (act.commands)
164
+ base.commands = act.commands;
165
+ break;
166
+ }
167
+ case "human": {
168
+ const h = n;
169
+ if (h.description)
170
+ base.description = h.description;
171
+ if (h.timeout)
172
+ base.timeout = h.timeout;
173
+ if (h.onTimeout)
174
+ base.onTimeout = h.onTimeout;
175
+ break;
176
+ }
177
+ }
178
+ return base;
179
+ });
180
+ const edges = ast.edges.map((e) => ({
181
+ from: e.from,
182
+ to: e.to,
183
+ condition: e.condition,
184
+ maxIterations: e.maxIterations,
185
+ }));
186
+ return {
187
+ topology: {
188
+ name: ast.topology.name,
189
+ version: ast.topology.version,
190
+ description: ast.topology.description,
191
+ patterns: ast.topology.patterns,
192
+ foundations: ast.topology.foundations || [],
193
+ advanced: ast.topology.advanced || [],
194
+ },
195
+ nodes,
196
+ edges,
197
+ depth: ast.depth,
198
+ memory: ast.memory,
199
+ batch: ast.batch,
200
+ environments: ast.environments,
201
+ triggers: ast.triggers,
202
+ hooks: ast.hooks,
203
+ settings: ast.settings,
204
+ mcpServers: ast.mcpServers,
205
+ metering: ast.metering,
206
+ skills: ast.skills,
207
+ toolDefs: ast.toolDefs,
208
+ runs: [],
209
+ };
210
+ }
211
+ /* eslint-enable @typescript-eslint/no-explicit-any */
212
+ // ---------------------------------------------------------------------------
213
+ // HTML builder
214
+ // ---------------------------------------------------------------------------
215
+ function buildHtml(dataJson, title, issuesJson) {
216
+ return `<!DOCTYPE html>
217
+ <html lang="en">
218
+ <head>
219
+ <meta charset="UTF-8">
220
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
221
+ <title>${escHtml(title)} — AgenTopology Visualizer</title>
222
+ <link rel="preconnect" href="https://fonts.googleapis.com">
223
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
224
+ <link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
225
+ <style>
226
+ ${CSS}
227
+ </style>
228
+ </head>
229
+ <body>
230
+ <script>` + DAGRE_SRC + `</script>
231
+ <div id="app">
232
+ <div id="header">
233
+ <div style="display:flex;align-items:center;gap:10px">
234
+ <svg viewBox="0 0 20 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:20px;height:24px">
235
+ <line x1="4.5" y1="2" x2="15.5" y2="2" stroke="#878593" stroke-width="1.2"/>
236
+ <line x1="10" y1="2" x2="10" y2="9.5" stroke="#878593" stroke-width="1.2"/>
237
+ <line x1="10" y1="9.5" x2="2.5" y2="22" stroke="#878593" stroke-width="1.2"/>
238
+ <line x1="10" y1="9.5" x2="17.5" y2="22" stroke="#878593" stroke-width="1.2"/>
239
+ <circle cx="4.5" cy="2" r="1.8" stroke="#a78bfa" stroke-width="1.5" fill="none"/>
240
+ <circle cx="15.5" cy="2" r="1.8" stroke="#a78bfa" stroke-width="1.5" fill="none"/>
241
+ <circle cx="10" cy="9.5" r="2" stroke="#a78bfa" stroke-width="1.5" fill="none"/>
242
+ <circle cx="2.5" cy="22" r="1.8" stroke="#fff" stroke-width="1.5" fill="none"/>
243
+ <circle cx="17.5" cy="22" r="1.8" stroke="#fff" stroke-width="1.5" fill="none"/>
244
+ </svg>
245
+ <span style="font-size:13px;color:#878593;font-weight:500;letter-spacing:0.5px">AgenTopology</span>
246
+ <span style="color:#2a2a2a;margin:0 4px">|</span>
247
+ </div>
248
+ <div class="topo-name" id="topo-name"></div>
249
+ <div class="topo-ver" id="topo-ver"></div>
250
+ <div class="topo-desc" id="topo-desc"></div>
251
+ <div class="pattern-badges" id="pattern-badges"></div>
252
+ <input type="text" id="search-input" class="search-input" placeholder="Search nodes..." oninput="onSearchInput(this.value)" />
253
+ <button class="header-btn" id="dataflow-btn" onclick="toggleDataFlow()">Data Flow</button>
254
+ <button class="header-btn" id="orientation-btn" onclick="toggleOrientation()" title="Switch between vertical and horizontal layout">&#8596; Horizontal</button>
255
+ <button class="header-btn" onclick="exportSvg()">Export SVG</button>
256
+ <button class="header-btn" id="issues-btn" onclick="toggleIssuesPanel()" style="display:none">Issues <span id="issues-badge" class="issues-badge">0</span></button>
257
+ <button class="header-btn" onclick="openLoadModal()">Load JSON</button>
258
+ <button class="header-btn" id="panel-btn" onclick="togglePanel()">Panel &#9654;</button>
259
+ </div>
260
+ <div id="main">
261
+ <div id="graph-container">
262
+ <svg id="graph-svg"></svg>
263
+ <div id="zoom-controls">
264
+ <button class="zoom-btn" onclick="zoomIn()" title="Zoom In (+)">+</button>
265
+ <button class="zoom-btn" onclick="zoomOut()" title="Zoom Out (-)">&minus;</button>
266
+ <button class="zoom-btn" onclick="zoomFit()" title="Fit (0)">&#8982;</button>
267
+ </div>
268
+ <div id="legend"></div>
269
+ <div class="svg-tooltip" id="tooltip"></div>
270
+ <div id="trigger-popover" class="trigger-popover" style="display:none"></div>
271
+ </div>
272
+ <div id="issues-panel" class="issues-panel collapsed">
273
+ <div class="issues-panel-header">
274
+ <span class="issues-panel-title">Validation Issues</span>
275
+ <button class="issues-panel-close" onclick="toggleIssuesPanel()">&times;</button>
276
+ </div>
277
+ <div id="issues-list" class="issues-list"></div>
278
+ </div>
279
+ <div id="side-panel" class="collapsed">
280
+ <div id="tab-bar">
281
+ <div class="tab active" data-tab="inspect">Inspect</div>
282
+ <div class="tab" data-tab="memory">Memory</div>
283
+ <div class="tab" data-tab="runs">Runs</div>
284
+ <div class="tab" data-tab="depth">Depth</div>
285
+ <div class="tab" data-tab="batch">Batch</div>
286
+ <div class="tab" data-tab="envs">Envs</div>
287
+ <div class="tab" data-tab="triggers">Triggers</div>
288
+ <div class="tab" data-tab="hooks">Hooks</div>
289
+ <div class="tab" data-tab="mcp">MCP</div>
290
+ <div class="tab" data-tab="skills">Skills</div>
291
+ <div class="tab" data-tab="metering">Metering</div>
292
+ <div class="tab" data-tab="settings">Settings</div>
293
+ </div>
294
+ <div id="tab-content">
295
+ <div id="inspect-empty" style="color:var(--t2);font-size:13px;text-align:center;margin-top:40px">
296
+ Click a node to inspect
297
+ </div>
298
+ </div>
299
+ </div>
300
+ </div>
301
+ </div>
302
+
303
+ <div id="load-modal">
304
+ <div class="modal-content">
305
+ <h3>Load Topology JSON</h3>
306
+ <textarea class="modal-textarea" id="json-input" placeholder="Paste topology JSON here..."></textarea>
307
+ <div class="modal-actions">
308
+ <button class="modal-btn secondary" onclick="closeLoadModal()">Cancel</button>
309
+ <button class="modal-btn primary" onclick="loadFromInput()">Load</button>
310
+ </div>
311
+ </div>
312
+ </div>
313
+
314
+ <div style="position:fixed;bottom:12px;right:16px;font-size:11px;color:#56545e;font-family:'Noto Sans',sans-serif">
315
+ Generated by <span style="color:#a78bfa">agentopology</span>
316
+ </div>
317
+
318
+ <script>
319
+ const DEFAULT_DATA = ${dataJson};
320
+ const VALIDATION_ISSUES = ${issuesJson};
321
+ ${JS}
322
+ </script>
323
+ </body>
324
+ </html>`;
325
+ }
326
+ function escHtml(s) {
327
+ return s
328
+ .replace(/&/g, "&amp;")
329
+ .replace(/</g, "&lt;")
330
+ .replace(/>/g, "&gt;")
331
+ .replace(/"/g, "&quot;");
332
+ }
333
+ // ---------------------------------------------------------------------------
334
+ // Inline CSS
335
+ // ---------------------------------------------------------------------------
336
+ const CSS = `
337
+ *{margin:0;padding:0;box-sizing:border-box}
338
+ :root{
339
+ --bg:#0A0A0A;--s:#111111;--s2:#161616;--b:#1e1e1e;--b2:#2a2a2a;
340
+ --t:#e4e4ef;--t2:#878593;--t3:#56545e;
341
+ --purple:#a78bfa;--purple-bg:rgba(167,139,250,.10);--purple-br:rgba(167,139,250,.25);
342
+ --blue:#60a5fa;--blue-bg:rgba(96,165,250,.10);--blue-br:rgba(96,165,250,.25);
343
+ --green:#4ade80;--green-bg:rgba(74,222,128,.10);--green-br:rgba(74,222,128,.25);
344
+ --orange:#fb923c;--orange-bg:rgba(251,146,60,.10);--orange-br:rgba(251,146,60,.25);
345
+ --gold:#fbbf24;--gold-bg:rgba(251,191,36,.10);--gold-br:rgba(251,191,36,.25);
346
+ --gray:#94a3b8;--gray-bg:rgba(148,163,184,.08);--gray-br:rgba(148,163,184,.2);
347
+ --red:#f87171;--red-bg:rgba(248,113,113,.10);--red-br:rgba(248,113,113,.25);
348
+ --cyan:#22d3ee;--cyan-bg:rgba(34,211,238,.10);--cyan-br:rgba(34,211,238,.25);
349
+ --amber:#f59e0b;--amber-bg:rgba(245,158,11,.10);--amber-br:rgba(245,158,11,.25);
350
+ --pink:#f472b6;
351
+ --font-mono:'JetBrains Mono',monospace;
352
+ --font-body:'Noto Sans',sans-serif;
353
+ --font-heading:'Noto Sans',sans-serif;
354
+ }
355
+ body{background:var(--bg);color:var(--t);font-family:var(--font-body);line-height:1.5;overflow:hidden;height:100vh}
356
+ #app{display:flex;flex-direction:column;height:100vh}
357
+ #header{padding:10px 20px;background:var(--s);border-bottom:1px solid var(--b);display:flex;align-items:center;gap:14px;flex-shrink:0;z-index:10}
358
+ #main{display:flex;flex:1;overflow:hidden}
359
+ #graph-container{flex:1;position:relative;overflow:hidden;cursor:grab}
360
+ #graph-container.dragging{cursor:grabbing}
361
+ #graph-svg{position:absolute;top:0;left:0}
362
+ #side-panel{width:380px;background:var(--s);border-left:1px solid var(--b);display:flex;flex-direction:column;flex-shrink:0;overflow:hidden;transition:width .2s}
363
+ #side-panel.collapsed{width:0;border-left:none}
364
+
365
+ .topo-name{font-family:var(--font-mono);font-size:12px;color:var(--t2);background:var(--s2);padding:3px 10px;border-radius:6px;border:1px solid var(--b)}
366
+ .topo-ver{font-family:var(--font-mono);font-size:11px;color:var(--t3)}
367
+ .topo-desc{font-family:var(--font-body);font-size:12px;color:var(--t2);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
368
+ .pattern-badges{display:flex;gap:5px;flex-wrap:wrap}
369
+ .pattern-badge{font-family:var(--font-mono);font-size:9px;padding:2px 7px;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;font-weight:500}
370
+ .pattern-badge.pipeline{background:var(--blue-bg);color:var(--blue);border:1px solid var(--blue-br)}
371
+ .pattern-badge.blackboard{background:var(--purple-bg);color:var(--purple);border:1px solid var(--purple-br)}
372
+ .pattern-badge.fan-out{background:var(--orange-bg);color:var(--orange);border:1px solid var(--orange-br)}
373
+ .pattern-badge.human-gate{background:var(--gold-bg);color:var(--gold);border:1px solid var(--gold-br)}
374
+ .header-btn{background:var(--s2);border:1px solid var(--b);color:var(--t2);padding:5px 12px;border-radius:6px;font-size:11px;cursor:pointer;transition:all .15s;font-family:var(--font-mono);letter-spacing:.3px}
375
+ .header-btn:hover{background:rgba(255,255,255,.06);color:var(--t);border-color:var(--b2)}
376
+ .header-btn.active{background:var(--amber-bg);color:var(--amber);border-color:var(--amber-br)}
377
+
378
+ #tab-bar{display:flex;border-bottom:1px solid var(--b);flex-shrink:0;overflow-x:auto}
379
+ .tab{padding:8px 13px;font-size:10px;color:var(--t2);cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;transition:all .15s;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.5px;font-weight:500}
380
+ .tab:hover{color:var(--t);background:rgba(255,255,255,.02)}
381
+ .tab.active{color:var(--purple);border-bottom-color:var(--purple)}
382
+ #tab-content{flex:1;overflow-y:auto;padding:16px}
383
+
384
+ .panel-section{margin-bottom:20px}
385
+ .panel-title{font-family:var(--font-mono);font-size:10px;color:var(--t3);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;display:flex;align-items:center;gap:6px;font-weight:600}
386
+ .panel-title::before{content:'';flex:0 0 3px;height:3px;border-radius:50%;background:var(--purple)}
387
+ .panel-section .panel-title{cursor:pointer;user-select:none;display:flex;align-items:center;gap:6px}
388
+ .panel-section .panel-title::before{content:'\\25B8';font-size:10px;color:var(--t3);transition:transform .15s;flex:0 0 auto;width:auto;height:auto;border-radius:0;background:none}
389
+ .panel-section.expanded .panel-title::before{transform:rotate(90deg)}
390
+ .panel-section .panel-body{display:none;margin-top:6px}
391
+ .panel-section.expanded .panel-body{display:block}
392
+ .panel-value{font-size:13px;color:var(--t)}
393
+ .panel-list{list-style:none;padding:0}
394
+ .panel-list li{font-size:12px;color:var(--t2);padding:3px 0;font-family:var(--font-mono);display:flex;align-items:center;gap:6px;font-weight:400}
395
+ .panel-list li::before{content:'';width:4px;height:4px;border-radius:50%;background:var(--b2);flex-shrink:0}
396
+ .panel-card{background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:12px;margin-bottom:8px}
397
+ .panel-card-title{font-family:var(--font-heading);font-size:12px;font-weight:600;margin-bottom:4px}
398
+ .panel-card-desc{font-size:11px;color:var(--t2);font-family:var(--font-body)}
399
+ .panel-tag{display:inline-block;font-family:var(--font-mono);font-size:10px;padding:1px 6px;border-radius:3px;background:var(--s2);color:var(--t2);border:1px solid var(--b);margin:2px;font-weight:400}
400
+
401
+ .node-detail-header{display:flex;align-items:center;gap:12px;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid var(--b)}
402
+ .node-icon{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
403
+ .node-icon.agent{background:var(--purple-bg);border:1px solid var(--purple-br)}
404
+ .node-icon.action{background:var(--gray-bg);border:1px solid var(--gray-br)}
405
+ .node-icon.gate{background:var(--orange-bg);border:1px solid var(--orange-br)}
406
+ .node-icon.orchestrator{background:var(--gold-bg);border:1px solid var(--gold-br)}
407
+ .node-detail-name{font-family:var(--font-heading);font-size:16px;font-weight:600}
408
+ .node-detail-type{font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:1px;padding:2px 8px;border-radius:4px;font-weight:500}
409
+ .node-detail-role{font-size:12px;color:var(--t2);margin-bottom:16px;line-height:1.6}
410
+ .detail-row{display:flex;gap:8px;margin-bottom:6px;font-size:12px}
411
+ .detail-label{color:var(--t3);min-width:80px;font-family:var(--font-mono);flex-shrink:0;font-weight:500}
412
+ .detail-value{color:var(--t);font-family:var(--font-body)}
413
+ .tools-group-title{font-family:var(--font-mono);font-size:9px;color:var(--t3);text-transform:uppercase;letter-spacing:.5px;margin:8px 0 4px;font-weight:600}
414
+ .tool-chip{display:inline-block;font-family:var(--font-mono);font-size:10px;padding:2px 7px;border-radius:4px;margin:2px;background:var(--s2);color:var(--t2);border:1px solid var(--b);font-weight:400}
415
+ .tool-chip.core{background:var(--blue-bg);color:var(--blue);border-color:var(--blue-br)}
416
+ .tool-chip.mcp{background:var(--purple-bg);color:var(--purple);border-color:var(--purple-br)}
417
+ .tool-chip.skill{background:var(--green-bg);color:var(--green);border-color:var(--green-br)}
418
+ .tool-chip.script-tool{background:var(--orange-bg);color:var(--orange);border-color:var(--orange-br)}
419
+ .skill-card{background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:10px 12px;margin-bottom:8px}
420
+ .skill-card-name{font-family:var(--font-mono);font-size:12px;font-weight:600;color:var(--green);margin-bottom:4px}
421
+ .skill-card-desc{font-size:11px;color:var(--t2);line-height:1.5;margin-bottom:6px}
422
+ .skill-card-meta{display:flex;flex-wrap:wrap;gap:4px}
423
+ .skill-card-agents{font-size:10px;color:var(--t3);margin-top:6px;font-style:italic}
424
+ .tool-card{background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:10px 12px;margin-bottom:8px}
425
+ .tool-card-name{font-family:var(--font-mono);font-size:12px;font-weight:600;color:var(--orange);margin-bottom:4px}
426
+ .tool-card-script{font-family:var(--font-mono);font-size:10px;color:var(--t2);margin-bottom:4px}
427
+ .tool-card-desc{font-size:11px;color:var(--t2);line-height:1.5}
428
+ .output-chip{display:inline-block;font-family:var(--font-mono);font-size:10px;padding:2px 7px;border-radius:4px;margin:2px;font-weight:500}
429
+ .output-chip.pass{background:var(--green-bg);color:var(--green);border:1px solid var(--green-br)}
430
+ .output-chip.fail{background:var(--red-bg);color:var(--red);border:1px solid var(--red-br)}
431
+ .output-chip.default{background:var(--gray-bg);color:var(--gray);border:1px solid var(--gray-br)}
432
+
433
+ .node-group{cursor:pointer}
434
+ .node-group.dimmed{opacity:.18}
435
+ .node-group.highlighted{opacity:1}
436
+ .node-label{font-family:var(--font-heading);fill:var(--t);font-size:12px;font-weight:600;text-anchor:middle;dominant-baseline:central;pointer-events:none;letter-spacing:-.2px}
437
+ .node-sublabel{font-family:var(--font-mono);fill:var(--t2);font-size:8.5px;text-anchor:middle;dominant-baseline:central;pointer-events:none;letter-spacing:.3px;font-weight:500}
438
+ .edge-path{fill:none;stroke-width:1.5}
439
+ .edge-path.unconditional{stroke:rgba(255,255,255,.15)}
440
+ .edge-path.conditional{stroke:var(--t3);stroke-dasharray:6 4}
441
+ .edge-path.failure{stroke:var(--red);stroke-dasharray:6 4;opacity:.7}
442
+ .edge-path.loop{stroke:var(--orange);stroke-dasharray:4 4}
443
+ .edge-group.dimmed .edge-path{opacity:.06}
444
+ .edge-group.dimmed .edge-label-group{opacity:.06}
445
+ .edge-group.highlighted .edge-path{stroke-width:2.5;opacity:1}
446
+ .edge-group.highlighted .edge-label-group{opacity:1}
447
+
448
+ @keyframes marchingAnts{from{stroke-dashoffset:0}to{stroke-dashoffset:-20}}
449
+ .edge-path.conditional,.edge-path.failure{animation:marchingAnts 4s linear infinite}
450
+ @keyframes loopPulse{0%,100%{opacity:.7}50%{opacity:1}}
451
+ .edge-path.loop{animation:marchingAnts 4s linear infinite,loopPulse 3s ease-in-out infinite}
452
+ .node-group .node-shape{transition:filter .2s}
453
+ @keyframes nodeFadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
454
+
455
+ #zoom-controls{position:absolute;bottom:16px;right:16px;display:flex;flex-direction:column;gap:4px;z-index:5}
456
+ .zoom-btn{width:32px;height:32px;background:var(--s);border:1px solid var(--b);border-radius:6px;color:var(--t2);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s;font-family:var(--font-mono)}
457
+ .zoom-btn:hover{background:rgba(255,255,255,.06);color:var(--t);border-color:var(--b2)}
458
+
459
+ #legend{position:absolute;bottom:16px;left:16px;background:var(--s);border:1px solid var(--b);border-radius:8px;padding:12px 16px;z-index:5;font-size:10px;backdrop-filter:blur(8px);max-width:240px}
460
+ .legend-section-title{font-family:var(--font-mono);font-size:8px;color:var(--t3);text-transform:uppercase;letter-spacing:1px;margin:6px 0 3px;font-weight:600}
461
+ .legend-section-title:first-child{margin-top:0}
462
+ .legend-row{display:flex;align-items:center;gap:8px;margin:3px 0}
463
+ .legend-swatch{width:12px;height:12px;border-radius:3px;flex-shrink:0}
464
+ .legend-line{width:20px;height:0;border-top:2px solid;flex-shrink:0}
465
+ .legend-line.dashed{border-top-style:dashed}
466
+ .legend-label{color:var(--t2);font-family:var(--font-body);font-size:10px}
467
+ .legend-badge{width:12px;height:12px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:7px;font-weight:700;flex-shrink:0}
468
+
469
+ #load-modal{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.7);backdrop-filter:blur(8px);z-index:100;justify-content:center;align-items:center}
470
+ #load-modal.open{display:flex}
471
+ .modal-content{background:var(--s);border:1px solid var(--b);border-radius:12px;padding:24px;width:90%;max-width:640px;max-height:80vh;display:flex;flex-direction:column}
472
+ .modal-content h3{font-family:var(--font-heading);font-size:16px;margin-bottom:12px;font-weight:600}
473
+ .modal-textarea{width:100%;flex:1;min-height:300px;background:var(--bg);border:1px solid var(--b);border-radius:8px;padding:12px;color:var(--t);font-family:var(--font-mono);font-size:12px;resize:none}
474
+ .modal-textarea:focus{outline:none;border-color:var(--purple)}
475
+ .modal-actions{display:flex;gap:8px;margin-top:12px;justify-content:flex-end}
476
+ .modal-btn{padding:6px 16px;border-radius:6px;font-size:12px;cursor:pointer;border:1px solid var(--b);font-family:var(--font-mono);transition:all .15s;font-weight:500}
477
+ .modal-btn.primary{background:var(--purple);color:#fff;border-color:var(--purple)}
478
+ .modal-btn.primary:hover{filter:brightness(1.15)}
479
+ .modal-btn.secondary{background:var(--s2);color:var(--t2)}
480
+ .modal-btn.secondary:hover{background:rgba(255,255,255,.06);color:var(--t)}
481
+
482
+ .node-selected .node-shape{stroke-width:2.5!important;filter:brightness(1.3)!important}
483
+ .mini-graph{background:var(--bg);border:1px solid var(--b);border-radius:8px;padding:8px;margin-bottom:16px}
484
+ .data-flow-pill{font-family:var(--font-mono);font-size:8px;font-weight:500;letter-spacing:.3px}
485
+
486
+ ::-webkit-scrollbar{width:5px}
487
+ ::-webkit-scrollbar-track{background:transparent}
488
+ ::-webkit-scrollbar-thumb{background:rgba(255,255,255,.08);border-radius:3px}
489
+ ::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.14)}
490
+
491
+ .svg-tooltip{position:absolute;background:var(--s);border:1px solid var(--b);border-radius:6px;padding:6px 10px;font-family:var(--font-mono);font-size:10px;color:var(--cyan);pointer-events:none;z-index:20;white-space:pre-line;backdrop-filter:blur(8px);opacity:0;transition:opacity .15s}
492
+ .svg-tooltip.visible{opacity:1}
493
+
494
+ .search-input{background:var(--s2);border:1px solid var(--b);color:var(--t);padding:5px 10px;border-radius:6px;font-size:11px;font-family:var(--font-mono);width:140px;outline:none;transition:all .15s}
495
+ .search-input:focus{border-color:var(--purple);width:180px}
496
+ .search-input::placeholder{color:var(--t3)}
497
+
498
+ .issues-badge{display:inline-block;background:var(--red);color:#fff;font-size:9px;font-weight:700;padding:1px 5px;border-radius:8px;margin-left:4px;min-width:16px;text-align:center}
499
+
500
+ .issues-panel{position:absolute;top:0;right:0;width:340px;height:100%;background:var(--s);border-left:1px solid var(--b);z-index:15;display:flex;flex-direction:column;transition:transform .2s}
501
+ .issues-panel.collapsed{transform:translateX(100%)}
502
+ .issues-panel-header{padding:12px 16px;border-bottom:1px solid var(--b);display:flex;align-items:center;justify-content:space-between}
503
+ .issues-panel-title{font-family:var(--font-heading);font-size:14px;font-weight:600}
504
+ .issues-panel-close{background:none;border:none;color:var(--t2);font-size:18px;cursor:pointer;padding:2px 6px}
505
+ .issues-panel-close:hover{color:var(--t)}
506
+ .issues-list{flex:1;overflow-y:auto;padding:12px}
507
+ .issue-item{background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:10px 12px;margin-bottom:8px}
508
+ .issue-item.error{border-left:3px solid var(--red)}
509
+ .issue-item.warning{border-left:3px solid var(--gold)}
510
+ .issue-rule{font-family:var(--font-mono);font-size:10px;font-weight:600;margin-bottom:4px}
511
+ .issue-rule.error{color:var(--red)}
512
+ .issue-rule.warning{color:var(--gold)}
513
+ .issue-msg{font-size:12px;color:var(--t2);line-height:1.5}
514
+ .issue-node{font-family:var(--font-mono);font-size:10px;color:var(--t3);margin-top:4px}
515
+
516
+ .node-group.search-dimmed{opacity:.15}
517
+ .prompt-preview{cursor:pointer}
518
+ .prompt-expanded+.prompt-full{display:block!important}
519
+
520
+ .trigger-popover{position:absolute;z-index:100;background:var(--s2);border:1px solid var(--b2);border-radius:10px;padding:10px 0;min-width:180px;max-width:280px;box-shadow:0 8px 32px rgba(0,0,0,.5);pointer-events:auto;animation:popIn .15s ease-out}
521
+ @keyframes popIn{from{opacity:0;transform:scale(.95) translateY(4px)}to{opacity:1;transform:scale(1) translateY(0)}}
522
+ .trigger-popover-title{font-family:var(--font-mono);font-size:9px;color:var(--t3);text-transform:uppercase;letter-spacing:1px;padding:0 12px 6px;border-bottom:1px solid var(--b)}
523
+ .trigger-popover-item{display:flex;align-items:center;gap:8px;padding:6px 12px;transition:background .1s}
524
+ .trigger-popover-item:hover{background:rgba(255,255,255,.04)}
525
+ .trigger-popover-cmd{font-family:var(--font-mono);font-size:12px;color:var(--cyan);font-weight:600}
526
+ .trigger-popover-pattern{font-family:var(--font-mono);font-size:10px;color:var(--t2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
527
+ `;
528
+ // ---------------------------------------------------------------------------
529
+ // Inline JS (the full advanced viewer logic)
530
+ // ---------------------------------------------------------------------------
531
+ const JS = `
532
+ // ═══════════════════════════════════════════════════════════════
533
+ // SECTION: Data & State
534
+ // ═══════════════════════════════════════════════════════════════
535
+
536
+ // --- Application state ---
537
+ let data = null, selectedNode = null, hoveredNode = null;
538
+ let transform = { x: 0, y: 0, scale: 1 };
539
+ let isPanning = false, panStart = { x: 0, y: 0 };
540
+ let nodePositions = {};
541
+ let dataFlowEnabled = false;
542
+ let isHorizontal = false;
543
+ let searchQuery = '';
544
+ let issuesPanelOpen = false;
545
+
546
+ // --- Node size constants ---
547
+ const NODE_W = 170, NODE_H = 60, ORCH_W = 180, ORCH_H = 54;
548
+ const GATE_W = 130, GATE_H = 48, ACTION_W = 150, ACTION_H = 56;
549
+ const TRIGGER_W = 100, TRIGGER_H = 28;
550
+
551
+ // --- Layout spacing ---
552
+ const COL_GAP = 120, ROW_GAP = 140;
553
+
554
+ // --- Zoom / pan constants ---
555
+ const ZOOM_MIN = 0.15, ZOOM_MAX = 3;
556
+ const ZOOM_FIT_MAX = 1.2, ZOOM_FIT_PAD = 80;
557
+ const PAN_STEP = 50;
558
+
559
+ // --- Node stagger animation delay (ms per rank) ---
560
+ const NODE_STAGGER_MS = 50;
561
+
562
+ // --- Mini-graph dimensions ---
563
+ const MINI_GRAPH_W = 340, MINI_GRAPH_H = 100;
564
+
565
+ // --- Color palette ---
566
+ const COLOR_PURPLE = '#a78bfa';
567
+ const COLOR_BLUE = '#60a5fa';
568
+ const COLOR_GREEN = '#4ade80';
569
+ const COLOR_ORANGE = '#fb923c';
570
+ const COLOR_GOLD = '#fbbf24';
571
+ const COLOR_GRAY = '#94a3b8';
572
+ const COLOR_RED = '#f87171';
573
+ const COLOR_CYAN = '#22d3ee';
574
+ const COLOR_EDGE_DEFAULT = 'rgba(255,255,255,.15)'; // used in SVG string attrs
575
+ const COLOR_EDGE_COND = '#5858a0'; // used in SVG string attrs
576
+
577
+ const FOLDER_COLORS = {
578
+ 'explore': COLOR_BLUE, 'plan': COLOR_PURPLE, 'build': COLOR_ORANGE,
579
+ 'qa': COLOR_GREEN, 'security': COLOR_RED, 'design': '#f472b6',
580
+ 'meta-review': COLOR_GOLD, 'ticket': COLOR_GRAY
581
+ };
582
+
583
+ // ═══════════════════════════════════════════════════════════════
584
+ // SECTION: Utilities
585
+ // ═══════════════════════════════════════════════════════════════
586
+
587
+ function esc(str) {
588
+ if (!str) return '';
589
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
590
+ }
591
+ function dr(label, value) {
592
+ return '<div class="detail-row"><span class="detail-label">' + esc(label) + '</span><span class="detail-value">' + esc(String(value)) + '</span></div>';
593
+ }
594
+ function setTabContent(html) { document.getElementById('tab-content').innerHTML = html; }
595
+ function emptyMsg(text) { return '<div style="color:var(--t2);font-size:13px;text-align:center;margin-top:40px">' + text + '</div>'; }
596
+
597
+ function folderColor(path) {
598
+ const folder = (path || '').split('/')[0].replace('/*','');
599
+ return FOLDER_COLORS[folder] || COLOR_GRAY;
600
+ }
601
+ function modelColor(model) {
602
+ if (!model) return { main: COLOR_GRAY, bg: 'rgba(148,163,184,.08)', br: 'rgba(148,163,184,.2)' };
603
+ switch (model.toLowerCase()) {
604
+ case 'opus': return { main: COLOR_PURPLE, bg: 'rgba(167,139,250,.12)', br: 'rgba(167,139,250,.35)' };
605
+ case 'sonnet': return { main: COLOR_BLUE, bg: 'rgba(96,165,250,.12)', br: 'rgba(96,165,250,.35)' };
606
+ case 'haiku': return { main: COLOR_GREEN, bg: 'rgba(74,222,128,.12)', br: 'rgba(74,222,128,.35)' };
607
+ default: return { main: COLOR_GRAY, bg: 'rgba(148,163,184,.08)', br: 'rgba(148,163,184,.2)' };
608
+ }
609
+ }
610
+ function typeColor(type) {
611
+ switch (type) {
612
+ case 'agent': return { main: COLOR_PURPLE, bg: 'rgba(167,139,250,.10)', br: 'rgba(167,139,250,.25)' };
613
+ case 'action': return { main: COLOR_GRAY, bg: 'rgba(148,163,184,.08)', br: 'rgba(148,163,184,.2)' };
614
+ case 'gate': return { main: COLOR_ORANGE, bg: 'rgba(251,146,60,.10)', br: 'rgba(251,146,60,.25)' };
615
+ case 'orchestrator': return { main: COLOR_GOLD, bg: 'rgba(251,191,36,.10)', br: 'rgba(251,191,36,.25)' };
616
+ default: return { main: COLOR_GRAY, bg: 'rgba(148,163,184,.08)', br: 'rgba(148,163,184,.2)' };
617
+ }
618
+ }
619
+ function typeIcon(type) {
620
+ switch (type) {
621
+ case 'agent': return '\\u2726'; case 'action': return '\\u25C7';
622
+ case 'gate': return '\\u2B21'; case 'orchestrator': return '\\u2B22';
623
+ default: return '\\u25CB';
624
+ }
625
+ }
626
+ function nodeSize(node) {
627
+ if (node.type === 'orchestrator') return { w: ORCH_W, h: ORCH_H };
628
+ if (node.type === 'gate') return { w: 120, h: 70 };
629
+ if (node.type === 'action') return { w: ACTION_W, h: ACTION_H };
630
+ return { w: NODE_W, h: NODE_H };
631
+ }
632
+ function isManualNode(node) { return node.invocation === 'manual'; }
633
+
634
+ // ═══════════════════════════════════════════════════════════════
635
+ // SECTION: Layout Engine
636
+ // ═══════════════════════════════════════════════════════════════
637
+
638
+ function layoutNodes(nodes, edges) {
639
+ var positions = {};
640
+ var nodeMap = {};
641
+ nodes.forEach(function(n) { nodeMap[n.id] = n; });
642
+
643
+ var orchestratorNode = nodes.find(function(n) { return n.type === 'orchestrator'; });
644
+ var gateNodes = nodes.filter(function(n) { return n.type === 'gate'; });
645
+ var manualNodes = nodes.filter(function(n) { return isManualNode(n); });
646
+ var triggerNodes = nodes.filter(function(n) { return n.type === 'trigger'; });
647
+
648
+ var gateIds = new Set(gateNodes.map(function(g) { return g.id; }));
649
+ var manualIds = new Set(manualNodes.map(function(n) { return n.id; }));
650
+ var triggerIds = new Set(triggerNodes.map(function(n) { return n.id; }));
651
+
652
+ // Check if orchestrator has any flow edges
653
+ var orchNode = nodes.find(function(n) { return n.type === 'orchestrator'; });
654
+ var orchInFlow = false;
655
+ if (orchNode) {
656
+ orchInFlow = edges.some(function(e) { return e.from === orchNode.id || e.to === orchNode.id; });
657
+ }
658
+
659
+ var dagreNodes = nodes.filter(function(n) {
660
+ if (n.type === 'orchestrator' && !orchInFlow) return false;
661
+ return !gateIds.has(n.id) && !manualIds.has(n.id) && !triggerIds.has(n.id);
662
+ });
663
+
664
+ // Create dagre graph
665
+ var g = new dagre.graphlib.Graph();
666
+ g.setGraph({
667
+ rankdir: 'TB',
668
+ nodesep: 100,
669
+ ranksep: 140,
670
+ edgesep: 50,
671
+ marginx: 40,
672
+ marginy: 40,
673
+ ranker: 'network-simplex'
674
+ });
675
+ g.setDefaultEdgeLabel(function() { return {}; });
676
+
677
+ // Add nodes to dagre
678
+ dagreNodes.forEach(function(n) {
679
+ var sz = nodeSize(n);
680
+ g.setNode(n.id, { width: sz.w, height: sz.h });
681
+ });
682
+
683
+ // Add edges (only between dagre nodes)
684
+ var dagreNodeIds = new Set(dagreNodes.map(function(n) { return n.id; }));
685
+ edges.forEach(function(e) {
686
+ if (dagreNodeIds.has(e.from) && dagreNodeIds.has(e.to)) {
687
+ // dagre does not allow duplicate edges — only add once per (from,to) pair
688
+ if (!g.hasEdge(e.from, e.to)) {
689
+ g.setEdge(e.from, e.to, {
690
+ minlen: 1,
691
+ weight: e.condition ? 1 : 2
692
+ });
693
+ }
694
+ }
695
+ });
696
+
697
+ // Run dagre layout
698
+ dagre.layout(g);
699
+
700
+ // Extract positions (dagre returns center coords, we store top-left y)
701
+ g.nodes().forEach(function(v) {
702
+ var node = g.node(v);
703
+ if (node) {
704
+ var sz = nodeSize(nodeMap[v]);
705
+ positions[v] = { x: node.x, y: node.y - sz.h / 2, w: sz.w, h: sz.h };
706
+ }
707
+ });
708
+
709
+ // Store edge points for smoother edge rendering
710
+ var edgePoints = {};
711
+ g.edges().forEach(function(e) {
712
+ var edge = g.edge(e);
713
+ if (edge && edge.points) {
714
+ edgePoints[e.v + '->' + e.w] = edge.points;
715
+ }
716
+ });
717
+ window.__dagreEdgePoints = edgePoints;
718
+
719
+ // Position gates between their after/before nodes (same logic as before)
720
+ gateNodes.forEach(function(gate) {
721
+ if (gate.after && gate.before && gate.after === gate.before && positions[gate.after]) {
722
+ var refPos = positions[gate.after];
723
+ var sz = nodeSize(gate);
724
+ positions[gate.id] = { x: refPos.x, y: refPos.y + refPos.h + ROW_GAP / 2, w: sz.w, h: sz.h };
725
+ } else if (gate.after && gate.before && positions[gate.after] && positions[gate.before]) {
726
+ var afterPos = positions[gate.after], beforePos = positions[gate.before];
727
+ var sz = nodeSize(gate);
728
+ positions[gate.id] = {
729
+ x: (afterPos.x + beforePos.x) / 2,
730
+ y: afterPos.y + afterPos.h + (beforePos.y - afterPos.y - afterPos.h) / 2 - sz.h / 2,
731
+ w: sz.w, h: sz.h
732
+ };
733
+ } else if (gate.after && positions[gate.after]) {
734
+ var afterPos2 = positions[gate.after];
735
+ var sz2 = nodeSize(gate);
736
+ positions[gate.id] = {
737
+ x: afterPos2.x + afterPos2.w / 2 + sz2.w / 2 + 30,
738
+ y: afterPos2.y + afterPos2.h / 2 - sz2.h / 2,
739
+ w: sz2.w, h: sz2.h
740
+ };
741
+ }
742
+ });
743
+
744
+ // Position manual nodes in sidebar
745
+ if (manualNodes.length > 0) {
746
+ var maxRightX = 0;
747
+ Object.values(positions).forEach(function(p) { maxRightX = Math.max(maxRightX, p.x + p.w / 2); });
748
+ var sidebarX = maxRightX + 160;
749
+ var sideY = 80;
750
+ manualNodes.forEach(function(n) {
751
+ var sz = nodeSize(n);
752
+ positions[n.id] = { x: sidebarX, y: sideY, w: sz.w, h: sz.h };
753
+ sideY += sz.h + 30;
754
+ });
755
+ }
756
+
757
+ // Position orchestrator above the flow when it has no flow edges
758
+ if (orchNode && !orchInFlow) {
759
+ var allPosArr = Object.values(positions);
760
+ if (allPosArr.length > 0) {
761
+ var minY = Infinity, sumX = 0, count = 0;
762
+ allPosArr.forEach(function(p) {
763
+ if (p.y < minY) minY = p.y;
764
+ sumX += p.x; count++;
765
+ });
766
+ var orchSz = nodeSize(orchNode);
767
+ positions[orchNode.id] = { x: sumX / count, y: minY - orchSz.h - 60, w: orchSz.w, h: orchSz.h };
768
+ }
769
+ }
770
+
771
+ // Place any remaining disconnected nodes
772
+ var placed = new Set(Object.keys(positions));
773
+ var disconnected = nodes.filter(function(n) { return n.type !== 'orchestrator' && !placed.has(n.id); });
774
+ if (disconnected.length > 0) {
775
+ var maxRX = 0;
776
+ Object.values(positions).forEach(function(p) { maxRX = Math.max(maxRX, p.x + p.w / 2); });
777
+ var dcX = maxRX + 120;
778
+ var dcY = 40;
779
+ disconnected.forEach(function(n) {
780
+ var sz = nodeSize(n);
781
+ positions[n.id] = { x: dcX, y: dcY, w: sz.w, h: sz.h };
782
+ dcY += sz.h + 24;
783
+ });
784
+ }
785
+ return positions;
786
+ }
787
+
788
+ // ═══════════════════════════════════════════════════════════════
789
+ // SECTION: Rendering
790
+ // ═══════════════════════════════════════════════════════════════
791
+
792
+ function renderGraph() {
793
+ if (!data) return;
794
+ const svg = document.getElementById('graph-svg');
795
+ const container = document.getElementById('graph-container');
796
+ const rect = container.getBoundingClientRect();
797
+
798
+ nodePositions = layoutNodes(data.nodes, data.edges);
799
+ const nodeMap = {};
800
+ data.nodes.forEach(n => nodeMap[n.id] = n);
801
+
802
+ if (isHorizontal) {
803
+ let minNewX = Infinity;
804
+ Object.keys(nodePositions).forEach(id => {
805
+ const p = nodePositions[id];
806
+ const newX = p.y + p.h / 2, newY = p.x;
807
+ p.x = newX; p.y = newY;
808
+ if (newX < minNewX) minNewX = newX;
809
+ });
810
+ Object.keys(nodePositions).forEach(id => {
811
+ const p = nodePositions[id];
812
+ p.x = minNewX + (p.x - minNewX) * 1.4;
813
+ });
814
+ }
815
+
816
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
817
+ Object.values(nodePositions).forEach(p => {
818
+ minX = Math.min(minX, p.x - p.w / 2 - 60); minY = Math.min(minY, p.y - 60);
819
+ maxX = Math.max(maxX, p.x + p.w / 2 + 60); maxY = Math.max(maxY, p.y + p.h + 60);
820
+ });
821
+ const graphW = maxX - minX + 200, graphH = maxY - minY + 100;
822
+ svg.setAttribute('width', Math.max(graphW, rect.width));
823
+ svg.setAttribute('height', Math.max(graphH, rect.height));
824
+
825
+ let s = '';
826
+ s += '<defs>';
827
+ s += '<marker id="arrow" viewBox="0 0 10 6" refX="10" refY="3" markerWidth="8" markerHeight="5" orient="auto-start-reverse"><path d="M0,0 L10,3 L0,6 Z" fill="rgba(255,255,255,.15)"/></marker>';
828
+ s += '<marker id="arrow-cond" viewBox="0 0 10 6" refX="10" refY="3" markerWidth="8" markerHeight="5" orient="auto-start-reverse"><path d="M0,0 L10,3 L0,6 Z" fill="#5858a0"/></marker>';
829
+ s += '<marker id="arrow-fail" viewBox="0 0 10 6" refX="10" refY="3" markerWidth="8" markerHeight="5" orient="auto-start-reverse"><path d="M0,0 L10,3 L0,6 Z" fill="#f87171"/></marker>';
830
+ s += '<marker id="arrow-loop" viewBox="0 0 10 6" refX="10" refY="3" markerWidth="8" markerHeight="5" orient="auto-start-reverse"><path d="M0,0 L10,3 L0,6 Z" fill="#fb923c"/></marker>';
831
+ s += '<marker id="arrow-trigger" viewBox="0 0 10 6" refX="10" refY="3" markerWidth="8" markerHeight="5" orient="auto-start-reverse"><path d="M0,0 L10,3 L0,6 Z" fill="#22d3ee"/></marker>';
832
+ s += '<filter id="glow-purple"><feDropShadow dx="0" dy="0" stdDeviation="4" flood-color="#a78bfa" flood-opacity=".4"/></filter>';
833
+ s += '<filter id="glow-blue"><feDropShadow dx="0" dy="0" stdDeviation="4" flood-color="#60a5fa" flood-opacity=".4"/></filter>';
834
+ s += '<filter id="glow-green"><feDropShadow dx="0" dy="0" stdDeviation="4" flood-color="#4ade80" flood-opacity=".4"/></filter>';
835
+ s += '<filter id="glow-orange"><feDropShadow dx="0" dy="0" stdDeviation="4" flood-color="#fb923c" flood-opacity=".4"/></filter>';
836
+ s += '<filter id="glow-gold"><feDropShadow dx="0" dy="0" stdDeviation="4" flood-color="#fbbf24" flood-opacity=".4"/></filter>';
837
+ s += '<filter id="glow-gray"><feDropShadow dx="0" dy="0" stdDeviation="4" flood-color="#94a3b8" flood-opacity=".3"/></filter>';
838
+ s += '<filter id="glow-cyan"><feDropShadow dx="0" dy="0" stdDeviation="4" flood-color="#22d3ee" flood-opacity=".4"/></filter>';
839
+ s += '</defs>';
840
+
841
+ s += '<g id="graph-transform" transform="translate(' + transform.x + ',' + transform.y + ') scale(' + transform.scale + ')">';
842
+
843
+ const nodeRanks = {};
844
+ data.nodes.forEach(n => {
845
+ const pos = nodePositions[n.id];
846
+ if (pos) nodeRanks[n.id] = Math.round(pos.y / ROW_GAP);
847
+ });
848
+
849
+ // --- Build parallel-edge lookup ---
850
+ const edgePairCount = {};
851
+ const edgePairIndex = {};
852
+ data.edges.forEach(edge => {
853
+ const pairKey = [edge.from, edge.to].sort().join('::');
854
+ if (!edgePairCount[pairKey]) edgePairCount[pairKey] = 0;
855
+ edgePairIndex[edge.from + '::' + edge.to + '::' + (edge.condition||'') + '::' + (edge.maxIterations||'')] = edgePairCount[pairKey];
856
+ edgePairCount[pairKey]++;
857
+ });
858
+
859
+ // --- Edges ---
860
+ const edgeLabelCount = {};
861
+ data.edges.forEach(edge => {
862
+ const fromPos = nodePositions[edge.from], toPos = nodePositions[edge.to];
863
+ if (!fromPos || !toPos) return;
864
+
865
+ const isFailure = edge.condition && edge.condition.includes('fail');
866
+ const isLoop = edge.maxIterations != null;
867
+ const isConditional = edge.condition != null;
868
+
869
+ let edgeClass = 'unconditional', markerEnd = 'url(#arrow)';
870
+ if (isLoop) { edgeClass = 'loop'; markerEnd = 'url(#arrow-loop)'; }
871
+ else if (isFailure) { edgeClass = 'failure'; markerEnd = 'url(#arrow-fail)'; }
872
+ else if (isConditional) { edgeClass = 'conditional'; markerEnd = 'url(#arrow-cond)'; }
873
+
874
+ // Parallel edge offset
875
+ const pairKey = [edge.from, edge.to].sort().join('::');
876
+ const pairTotal = edgePairCount[pairKey] || 1;
877
+ const edgeKey = edge.from + '::' + edge.to + '::' + (edge.condition||'') + '::' + (edge.maxIterations||'');
878
+ const pairIdx = edgePairIndex[edgeKey] || 0;
879
+ const perpOffset = pairTotal > 1 ? (pairIdx - (pairTotal - 1) / 2) * 16 : 0;
880
+
881
+ const fromY = fromPos.y + fromPos.h, toY = toPos.y;
882
+ const fromX = fromPos.x, toX = toPos.x;
883
+
884
+ let pathD;
885
+ const isBackEdge = toPos.y <= fromPos.y + fromPos.h;
886
+ if (isBackEdge && (isLoop || isFailure)) {
887
+ // Back edges: use the far-right curve rendering
888
+ const allPos = Object.values(nodePositions);
889
+ let maxRight = 0;
890
+ allPos.forEach(p => { maxRight = Math.max(maxRight, p.x + p.w / 2); });
891
+ const loopX = maxRight + 60 + perpOffset;
892
+ const startY = fromPos.y + fromPos.h / 2, endY = toPos.y + toPos.h / 2;
893
+ const startX = fromPos.x + fromPos.w / 2, endX = toPos.x + toPos.w / 2;
894
+ pathD = 'M' + startX + ',' + startY + ' C' + loopX + ',' + startY + ' ' + loopX + ',' + endY + ' ' + endX + ',' + endY;
895
+ } else {
896
+ // Try dagre edge points first
897
+ var dagreKey = edge.from + '->' + edge.to;
898
+ var pts = window.__dagreEdgePoints && window.__dagreEdgePoints[dagreKey];
899
+ if (pts && pts.length >= 2) {
900
+ // Build smooth path through dagre points
901
+ pathD = 'M' + pts[0].x + ',' + pts[0].y;
902
+ if (pts.length === 2) {
903
+ pathD += ' L' + pts[1].x + ',' + pts[1].y;
904
+ } else {
905
+ for (var pi = 1; pi < pts.length; pi++) {
906
+ if (pi === 1) {
907
+ var midX1 = (pts[0].x + pts[1].x) / 2;
908
+ var midY1 = (pts[0].y + pts[1].y) / 2;
909
+ pathD += ' Q' + pts[0].x + ',' + pts[0].y + ' ' + midX1 + ',' + midY1;
910
+ }
911
+ if (pi < pts.length - 1) {
912
+ var midX2 = (pts[pi].x + pts[pi+1].x) / 2;
913
+ var midY2 = (pts[pi].y + pts[pi+1].y) / 2;
914
+ pathD += ' Q' + pts[pi].x + ',' + pts[pi].y + ' ' + midX2 + ',' + midY2;
915
+ } else {
916
+ pathD += ' L' + pts[pi].x + ',' + pts[pi].y;
917
+ }
918
+ }
919
+ }
920
+ } else {
921
+ // Fallback: cubic bezier (for gate edges, manual edges, etc.)
922
+ const midY = fromY + (toY - fromY) / 2;
923
+ const dx = toX - fromX, dy = toY - fromY;
924
+ const len = Math.sqrt(dx * dx + dy * dy) || 1;
925
+ const nx = -dy / len, ny = dx / len;
926
+ const offX = nx * perpOffset, offY = ny * perpOffset;
927
+ pathD = 'M' + (fromX + offX) + ',' + (fromY + offY) + ' C' + (fromX + offX) + ',' + (midY + offY) + ' ' + (toX + offX) + ',' + (midY + offY) + ' ' + (toX + offX) + ',' + (toY + offY);
928
+ }
929
+ }
930
+
931
+ // Back-edges with maxIterations get dashed stroke
932
+ const backEdgeDash = isLoop ? ' stroke-dasharray="6 4"' : '';
933
+
934
+ s += '<g class="edge-group" data-from="' + edge.from + '" data-to="' + edge.to + '">';
935
+ s += '<path d="' + pathD + '" class="edge-path ' + edgeClass + '" marker-end="' + markerEnd + '"' + backEdgeDash + '/>';
936
+
937
+ if (edge.condition || isLoop) {
938
+ const isBack = toPos.y <= fromPos.y + fromPos.h;
939
+ let labelX, labelY;
940
+ if (isBack && (isLoop || isFailure)) {
941
+ const allPos = Object.values(nodePositions);
942
+ let mr = 0; allPos.forEach(p => { mr = Math.max(mr, p.x + p.w / 2); });
943
+ labelX = mr + 70; labelY = (fromPos.y + toPos.y + toPos.h) / 2;
944
+ } else {
945
+ // Use dagre edge midpoint if available
946
+ var dlk = edge.from + '->' + edge.to;
947
+ var dpts = window.__dagreEdgePoints && window.__dagreEdgePoints[dlk];
948
+ if (dpts && dpts.length >= 2) {
949
+ var midIdx = Math.floor(dpts.length / 2);
950
+ labelX = dpts[midIdx].x + 10; labelY = dpts[midIdx].y;
951
+ } else {
952
+ labelX = (fromX + toX) / 2 + 10; labelY = fromY + (toY - fromY) / 2;
953
+ }
954
+ labelX += 20;
955
+ }
956
+ let label = '';
957
+ if (edge.condition) label = edge.condition.replace('orchestrator.', '').replace('qa.', '').replace(' == ', '=');
958
+ if (isLoop) label += (label ? ' ' : '') + 'max ' + edge.maxIterations;
959
+ if (label) {
960
+ if (!edgeLabelCount[edge.from]) edgeLabelCount[edge.from] = 0;
961
+ const labelIdx = edgeLabelCount[edge.from]++;
962
+ const offsetY = labelIdx * 24;
963
+ const finalY = labelY + offsetY;
964
+
965
+ // Collision avoidance: push label right if it overlaps any node
966
+ const labelHalfW = (label.length * 5.5 + 16) / 2;
967
+ Object.values(nodePositions).forEach(function(np) {
968
+ var nLeft = np.x - np.w / 2 - 8, nRight = np.x + np.w / 2 + 8;
969
+ var nTop = np.y - 8, nBot = np.y + np.h + 8;
970
+ if (labelX + labelHalfW > nLeft && labelX - labelHalfW < nRight && finalY > nTop && finalY < nBot) {
971
+ labelX = nRight + labelHalfW + 12;
972
+ }
973
+ });
974
+ const labelLen = label.length * 5.5 + 16;
975
+ const pillH = 16, pillR = 8;
976
+ let pillFill = 'rgba(88,88,160,.15)', pillStroke = 'rgba(88,88,160,.3)', textFill = '#8888a8';
977
+ if (isLoop) { pillFill = 'rgba(251,146,60,.12)'; pillStroke = 'rgba(251,146,60,.3)'; textFill = '#fb923c'; }
978
+ else if (isFailure) { pillFill = 'rgba(248,113,113,.12)'; pillStroke = 'rgba(248,113,113,.3)'; textFill = '#f87171'; }
979
+ s += '<g class="edge-label-group">';
980
+ s += '<rect x="' + (labelX - labelLen/2) + '" y="' + (finalY - pillH/2) + '" width="' + labelLen + '" height="' + pillH + '" rx="' + pillR + '" fill="' + pillFill + '" stroke="' + pillStroke + '" stroke-width="0.5"/>';
981
+ let iconOffset = 0;
982
+ if (isLoop) {
983
+ s += '<text x="' + (labelX - labelLen/2 + 6) + '" y="' + (finalY + 1) + '" font-size="8" fill="' + textFill + '" dominant-baseline="central">\\u21BB</text>';
984
+ iconOffset = 10;
985
+ } else if (isFailure) {
986
+ s += '<text x="' + (labelX - labelLen/2 + 6) + '" y="' + (finalY + 1) + '" font-size="8" fill="#f87171" dominant-baseline="central" font-weight="700">\\u2717</text>';
987
+ iconOffset = 10;
988
+ }
989
+ s += '<text x="' + (labelX + iconOffset/2) + '" y="' + (finalY + 1) + '" font-family="monospace" font-size="8.5" fill="' + textFill + '" text-anchor="middle" dominant-baseline="central" font-weight="500">' + esc(label) + '</text>';
990
+ s += '</g>';
991
+ }
992
+ }
993
+ s += '</g>';
994
+ });
995
+
996
+ // Gate advisory connectors
997
+ data.nodes.filter(n => n.type === 'gate' && n.after && !n.before).forEach(gate => {
998
+ const gatePos = nodePositions[gate.id], afterPos = nodePositions[gate.after];
999
+ if (!gatePos || !afterPos) return;
1000
+ const fX = afterPos.x + afterPos.w / 2, fY = afterPos.y + afterPos.h / 2;
1001
+ const tX = gatePos.x - gatePos.w / 2, tY = gatePos.y + gatePos.h / 2;
1002
+ s += '<g class="edge-group" data-from="' + gate.after + '" data-to="' + gate.id + '">';
1003
+ s += '<path d="M' + fX + ',' + fY + ' L' + tX + ',' + tY + '" class="edge-path conditional" stroke-dasharray="3 3" marker-end="url(#arrow-cond)" opacity="0.3"/>';
1004
+ s += '</g>';
1005
+ });
1006
+
1007
+ // --- Trigger badge (compact, not inline) ---
1008
+ // Triggers are shown as a small badge on the entry node, with a popover on click.
1009
+ // This avoids pills stacking on top of nodes in the graph.
1010
+ if (data.triggers && data.triggers.length > 0) {
1011
+ const fwdEdges = data.edges.filter(e => !e.maxIterations);
1012
+ const allFlowIds = new Set();
1013
+ fwdEdges.forEach(e => { allFlowIds.add(e.from); allFlowIds.add(e.to); });
1014
+ data.nodes.filter(n => isManualNode(n)).forEach(n => allFlowIds.delete(n.id));
1015
+ data.nodes.filter(n => n.type === 'gate').forEach(n => allFlowIds.delete(n.id));
1016
+ const inDegree = {};
1017
+ allFlowIds.forEach(id => { inDegree[id] = 0; });
1018
+ fwdEdges.forEach(e => { if (allFlowIds.has(e.to)) inDegree[e.to] = (inDegree[e.to] || 0) + 1; });
1019
+ const rootNodes = [...allFlowIds].filter(id => !inDegree[id] || inDegree[id] === 0);
1020
+ const triggerEntryMap = {};
1021
+ data.triggers.forEach(trigger => {
1022
+ const name = trigger.name;
1023
+ let entry = rootNodes.find(r => r.startsWith(name + '-'));
1024
+ if (!entry && name === 'ship') entry = rootNodes.find(r => r === 'intake');
1025
+ if (!entry) entry = rootNodes.find(r => r === name);
1026
+ if (!entry && rootNodes.length > 0) entry = rootNodes[0];
1027
+ if (entry) triggerEntryMap[name] = entry;
1028
+ });
1029
+ // Group triggers by entry node
1030
+ const triggersByEntry = {};
1031
+ data.triggers.forEach(trigger => {
1032
+ const entryId = triggerEntryMap[trigger.name];
1033
+ if (!entryId) return;
1034
+ if (!triggersByEntry[entryId]) triggersByEntry[entryId] = [];
1035
+ triggersByEntry[entryId].push(trigger);
1036
+ });
1037
+ // Render a small "slash" badge on each entry node — clicking shows popover
1038
+ Object.keys(triggersByEntry).forEach(entryId => {
1039
+ const triggers = triggersByEntry[entryId];
1040
+ const entryPos = nodePositions[entryId];
1041
+ if (!entryPos) return;
1042
+ const bx = entryPos.x - entryPos.w / 2 + 8;
1043
+ const by = entryPos.y - 2;
1044
+ s += '<g class="trigger-badge" style="cursor:pointer" onclick="toggleTriggerPopover(\\'' + esc(entryId) + '\\')">';
1045
+ s += '<circle cx="' + bx + '" cy="' + by + '" r="9" fill="rgba(34,211,238,.12)" stroke="rgba(34,211,238,.4)" stroke-width="0.8"/>';
1046
+ s += '<text x="' + bx + '" y="' + (by + 1) + '" text-anchor="middle" dominant-baseline="central" font-family="monospace" font-size="9" fill="#22d3ee" font-weight="700">/' + (triggers.length > 1 ? triggers.length : '') + '</text>';
1047
+ s += '</g>';
1048
+ });
1049
+ // Store trigger data for popover rendering
1050
+ window.__triggersByEntry = triggersByEntry;
1051
+ window.__triggerNodePositions = nodePositions;
1052
+ }
1053
+
1054
+ // --- Data flow overlay ---
1055
+ if (dataFlowEnabled) s += renderDataFlowOverlay(nodeMap);
1056
+
1057
+ // --- Manual agents label ---
1058
+ const manualNodesArr = data.nodes.filter(n => isManualNode(n));
1059
+ if (manualNodesArr.length > 0) {
1060
+ const firstManual = nodePositions[manualNodesArr[0].id];
1061
+ if (firstManual) {
1062
+ s += '<text x="' + firstManual.x + '" y="' + (firstManual.y - 20) + '" text-anchor="middle" font-family="monospace" font-size="9" fill="rgba(255,255,255,.25)" letter-spacing="2" font-weight="600">MANUAL AGENTS</text>';
1063
+ }
1064
+ }
1065
+
1066
+ // --- Nodes ---
1067
+ data.nodes.forEach(node => {
1068
+ const pos = nodePositions[node.id];
1069
+ if (!pos) return;
1070
+ const isSelected = selectedNode === node.id;
1071
+ const isManual = isManualNode(node);
1072
+ const colors = node.type === 'agent' ? modelColor(node.model) : typeColor(node.type);
1073
+ const glowFilter = getGlowFilter(node);
1074
+ const staggerDelay = (nodeRanks[node.id] || 0) * NODE_STAGGER_MS;
1075
+
1076
+ s += '<g class="node-group' + (isSelected ? ' node-selected' : '') + '" data-id="' + node.id + '" style="animation:nodeFadeIn .4s ease-out ' + staggerDelay + 'ms both" onmouseenter="onNodeHover(\\'' + node.id + '\\')" onmouseleave="onNodeLeave()" onclick="onNodeClick(\\'' + node.id + '\\')">';
1077
+
1078
+ const x = pos.x - pos.w / 2, y = pos.y;
1079
+ const strokeDash = isManual ? 'stroke-dasharray="4 3"' : '';
1080
+ const dimOpacity = isManual ? 'opacity="0.7"' : '';
1081
+
1082
+ if (node.type === 'orchestrator') {
1083
+ s += '<rect class="node-shape" x="' + x + '" y="' + y + '" width="' + pos.w + '" height="' + pos.h + '" rx="8" fill="' + colors.bg + '" stroke="' + colors.main + '" stroke-width="' + (isSelected ? 2.5 : 1.5) + '" data-glow="' + glowFilter + '" ' + dimOpacity + '/>';
1084
+ s += '<rect x="' + x + '" y="' + y + '" width="' + pos.w + '" height="3" rx="8" fill="' + colors.main + '" opacity="0.5"/>';
1085
+ } else if (node.type === 'gate') {
1086
+ const cx = pos.x, cy = y + pos.h / 2;
1087
+ const hw = pos.w / 2, hh = pos.h / 2;
1088
+ // Diamond shape (rotated square)
1089
+ const diamond = cx + ',' + (cy - hh) + ' ' + (cx + hw) + ',' + cy + ' ' + cx + ',' + (cy + hh) + ' ' + (cx - hw) + ',' + cy;
1090
+ const gateDash = node.behavior === 'advisory' ? 'stroke-dasharray="4 3"' : '';
1091
+ s += '<polygon class="node-shape" points="' + diamond + '" fill="' + colors.bg + '" stroke="' + colors.main + '" stroke-width="' + (isSelected ? 2.5 : 1) + '" ' + gateDash + ' data-glow="' + glowFilter + '"/>';
1092
+ } else if (node.type === 'action') {
1093
+ const cx = pos.x, cy = y + pos.h / 2;
1094
+ const hw = pos.w / 2, hh = pos.h / 2;
1095
+ const d = 'M' + cx + ',' + (cy - hh) + ' Q' + (cx + hw * 0.6) + ',' + (cy - hh * 0.4) + ' ' + (cx + hw) + ',' + cy + ' Q' + (cx + hw * 0.6) + ',' + (cy + hh * 0.4) + ' ' + cx + ',' + (cy + hh) + ' Q' + (cx - hw * 0.6) + ',' + (cy + hh * 0.4) + ' ' + (cx - hw) + ',' + cy + ' Q' + (cx - hw * 0.6) + ',' + (cy - hh * 0.4) + ' ' + cx + ',' + (cy - hh) + ' Z';
1096
+ s += '<path class="node-shape" d="' + d + '" fill="' + colors.bg + '" stroke="' + colors.main + '" stroke-width="' + (isSelected ? 2.5 : 1) + '" data-glow="' + glowFilter + '"/>';
1097
+ } else {
1098
+ s += '<rect class="node-shape" x="' + x + '" y="' + y + '" width="' + pos.w + '" height="' + pos.h + '" rx="12" fill="' + colors.bg + '" stroke="' + colors.main + '" stroke-width="' + (isSelected ? 2.5 : 1) + '" ' + strokeDash + ' ' + dimOpacity + ' data-glow="' + glowFilter + '"/>';
1099
+ }
1100
+
1101
+ const labelY = y + pos.h / 2 - (node.type === 'agent' && node.phase != null ? 4 : 0);
1102
+ const fullLabel = node.label || '';
1103
+ const truncLabel = fullLabel.length > 20 ? fullLabel.substring(0, 20) + '\\u2026' : fullLabel;
1104
+ const titleAttr = fullLabel.length > 20 ? ' data-full-label="' + esc(fullLabel) + '"' : '';
1105
+ s += '<text class="node-label" x="' + pos.x + '" y="' + labelY + '" fill="' + colors.main + '" ' + dimOpacity + titleAttr + '><title>' + esc(fullLabel) + '</title>' + esc(truncLabel) + '</text>';
1106
+
1107
+ if (node.type === 'agent' && node.phase != null) {
1108
+ s += '<text class="node-sublabel" x="' + pos.x + '" y="' + (labelY + 14) + '">Phase ' + node.phase + '</text>';
1109
+ } else if (node.type === 'action' && node.kind) {
1110
+ s += '<text class="node-sublabel" x="' + pos.x + '" y="' + (labelY + 13) + '">' + esc(node.kind) + '</text>';
1111
+ } else if (node.type === 'gate') {
1112
+ s += '<text class="node-sublabel" x="' + pos.x + '" y="' + (labelY + 11) + '">gate</text>';
1113
+ }
1114
+
1115
+ const badgeY = y - 2;
1116
+ const nodeBadges = [];
1117
+ if (node.hooks && node.hooks.length > 0) nodeBadges.push({ icon: '\\u26A1', fill: '#22d3ee', bg: 'rgba(34,211,238,.15)', stroke: 'rgba(34,211,238,.35)', tip: node.hooks.map(function(h){return h.name}).join('\\\\n'), font: 'font-size="9"' });
1118
+ if (node.skills && node.skills.length > 0) nodeBadges.push({ icon: 'S', fill: '#4ade80', bg: 'rgba(74,222,128,.15)', stroke: 'rgba(74,222,128,.35)', tip: node.skills.join('\\\\n'), font: 'font-weight="700" font-family="monospace" font-size="8"' });
1119
+ if (node.behavior === 'advisory') nodeBadges.push({ icon: 'A', fill: '#fbbf24', bg: 'rgba(251,191,36,.15)', stroke: 'rgba(251,191,36,.35)', tip: 'Advisory', font: 'font-weight="700" font-family="monospace" font-size="7"' });
1120
+ if (isManual) nodeBadges.push({ icon: 'M', fill: '#94a3b8', bg: 'rgba(148,163,184,.12)', stroke: 'rgba(148,163,184,.25)', tip: 'Manual invocation', font: 'font-weight="700" font-family="monospace" font-size="7"' });
1121
+
1122
+ // Lay out badges horizontally from right edge, each spaced 18px apart
1123
+ var badgeSpacing = 18;
1124
+ var badgeStartX = x + pos.w - 8;
1125
+ nodeBadges.forEach(function(nb, idx) {
1126
+ var bx = badgeStartX - idx * badgeSpacing;
1127
+ s += '<g class="hook-badge" onmouseenter="showTooltip(event, \\'' + nb.tip + '\\')" onmouseleave="hideTooltip()" style="cursor:pointer">';
1128
+ s += '<circle cx="' + bx + '" cy="' + badgeY + '" r="7" fill="' + nb.bg + '" stroke="' + nb.stroke + '" stroke-width="0.5"/>';
1129
+ s += '<text x="' + bx + '" y="' + (badgeY + 1) + '" text-anchor="middle" dominant-baseline="central" ' + nb.font + ' fill="' + nb.fill + '">' + nb.icon + '</text>';
1130
+ s += '</g>';
1131
+ });
1132
+
1133
+ // Track how far left the node badges extend for scale pill placement
1134
+ var badgesRightEdge = badgeStartX + 8;
1135
+ var badgesLeftEdge = nodeBadges.length > 0 ? (badgeStartX - (nodeBadges.length - 1) * badgeSpacing - 8) : badgesRightEdge;
1136
+
1137
+ if (node.scale) {
1138
+ const sc = node.scale;
1139
+ let scaleLabel;
1140
+ if (sc.mode === 'fixed') scaleLabel = '\\u00D7' + sc.min;
1141
+ else if (sc.mode === 'config') scaleLabel = '\\u00D7?';
1142
+ else scaleLabel = '\\u00D7' + sc.min + '\\u2013' + sc.max;
1143
+ const scW = scaleLabel.length * 5.5 + 10;
1144
+ // Place scale pill to the left of all node badges with 6px gap
1145
+ const scX = badgesLeftEdge - 6 - scW;
1146
+ const scY = badgeY - 7;
1147
+ s += '<rect x="' + scX + '" y="' + scY + '" width="' + scW + '" height="14" rx="7" fill="rgba(34,211,238,.12)" stroke="rgba(34,211,238,.35)" stroke-width="0.5"/>';
1148
+ s += '<text x="' + (scX + scW / 2) + '" y="' + (scY + 7.5) + '" text-anchor="middle" dominant-baseline="central" font-family="monospace" font-size="8.5" fill="#22d3ee" font-weight="600">' + esc(scaleLabel) + '</text>';
1149
+ }
1150
+
1151
+ s += '</g>';
1152
+ });
1153
+
1154
+ s += '</g>';
1155
+ svg.innerHTML = s;
1156
+ if (transform.x === 0 && transform.y === 0 && transform.scale === 1) zoomFit();
1157
+ renderLegend();
1158
+ }
1159
+
1160
+ function getGlowFilter(node) {
1161
+ if (node.type === 'orchestrator') return 'glow-gold';
1162
+ if (node.type === 'gate') return 'glow-orange';
1163
+ if (node.type === 'action') return 'glow-gray';
1164
+ const m = (node.model || '').toLowerCase();
1165
+ if (m === 'opus') return 'glow-purple';
1166
+ if (m === 'sonnet') return 'glow-blue';
1167
+ if (m === 'haiku') return 'glow-green';
1168
+ return 'glow-gray';
1169
+ }
1170
+
1171
+ // --- Data Flow Overlay ---
1172
+ function renderDataFlowOverlay(nodeMap) {
1173
+ let s = '<g class="data-flow-layer" opacity="0.6">';
1174
+ const artifacts = new Map();
1175
+ data.nodes.forEach(node => {
1176
+ (node.reads || []).forEach(path => {
1177
+ if (!artifacts.has(path)) artifacts.set(path, { readers: new Set(), writers: new Set() });
1178
+ artifacts.get(path).readers.add(node.id);
1179
+ });
1180
+ (node.writes || []).forEach(path => {
1181
+ if (!artifacts.has(path)) artifacts.set(path, { readers: new Set(), writers: new Set() });
1182
+ artifacts.get(path).writers.add(node.id);
1183
+ });
1184
+ });
1185
+ let artifactIndex = 0;
1186
+ artifacts.forEach((info, path) => {
1187
+ const color = folderColor(path);
1188
+ const writers = [...info.writers].filter(id => nodePositions[id]);
1189
+ const readers = [...info.readers].filter(id => nodePositions[id]);
1190
+ if (writers.length === 0 || readers.length === 0) { artifactIndex++; return; }
1191
+ writers.forEach(wId => {
1192
+ readers.forEach(rId => {
1193
+ if (wId === rId) return;
1194
+ const wPos = nodePositions[wId], rPos = nodePositions[rId];
1195
+ const wx = wPos.x, wy = wPos.y + wPos.h / 2;
1196
+ const rx = rPos.x, ry = rPos.y + rPos.h / 2;
1197
+ const offset = 4 + (artifactIndex % 3) * 3;
1198
+ const midX = (wx + rx) / 2 + offset * (artifactIndex % 2 === 0 ? 1 : -1);
1199
+ s += '<path d="M' + (wx + wPos.w/2 + 4) + ',' + wy + ' Q' + (midX + 20) + ',' + wy + ' ' + midX + ',' + ((wy + ry) / 2) + '" fill="none" stroke="' + color + '" stroke-width="1" opacity="0.5"/>';
1200
+ s += '<path d="M' + midX + ',' + ((wy + ry) / 2) + ' Q' + (midX - 20) + ',' + ry + ' ' + (rx - rPos.w/2 - 4) + ',' + ry + '" fill="none" stroke="' + color + '" stroke-width="1" stroke-dasharray="3 2" opacity="0.5"/>';
1201
+ const pillPath = path.length > 14 ? path.substring(0, 12) + '..' : path;
1202
+ const pw = pillPath.length * 5.2 + 10;
1203
+ const py = (wy + ry) / 2;
1204
+ s += '<rect x="' + (midX - pw/2) + '" y="' + (py - 7) + '" width="' + pw + '" height="14" rx="7" fill="' + color + '" opacity="0.12" stroke="' + color + '" stroke-width="0.5" stroke-opacity="0.3"/>';
1205
+ s += '<text x="' + midX + '" y="' + (py + 1) + '" class="data-flow-pill" text-anchor="middle" dominant-baseline="central" fill="' + color + '" opacity="0.8">' + esc(pillPath) + '</text>';
1206
+ });
1207
+ });
1208
+ artifactIndex++;
1209
+ });
1210
+ s += '</g>';
1211
+ return s;
1212
+ }
1213
+
1214
+ // --- Legend ---
1215
+ function renderLegend() {
1216
+ const el = document.getElementById('legend');
1217
+ let h = '';
1218
+ h += '<div class="legend-section-title">Nodes</div>';
1219
+ h += '<div class="legend-row"><div class="legend-swatch" style="background:rgba(167,139,250,.12);border:1px solid rgba(167,139,250,.25);border-radius:3px"></div><span class="legend-label">Agent (Opus)</span></div>';
1220
+ h += '<div class="legend-row"><div class="legend-swatch" style="background:rgba(96,165,250,.12);border:1px solid rgba(96,165,250,.25);border-radius:3px"></div><span class="legend-label">Agent (Sonnet)</span></div>';
1221
+ h += '<div class="legend-row"><div class="legend-swatch" style="background:rgba(74,222,128,.12);border:1px solid rgba(74,222,128,.25);border-radius:3px"></div><span class="legend-label">Agent (Haiku)</span></div>';
1222
+ h += '<div class="legend-row"><div class="legend-swatch" style="background:rgba(148,163,184,.08);border:1px solid rgba(148,163,184,.2);border-radius:3px"></div><span class="legend-label">Action</span></div>';
1223
+ h += '<div class="legend-row"><div class="legend-swatch" style="background:rgba(251,146,60,.10);border:1px solid rgba(251,146,60,.25);clip-path:polygon(50% 0%,100% 50%,50% 100%,0% 50%);border-radius:0"></div><span class="legend-label">Gate</span></div>';
1224
+ h += '<div class="legend-row"><div class="legend-swatch" style="background:rgba(251,191,36,.10);border:1px solid rgba(251,191,36,.25);border-radius:3px"></div><span class="legend-label">Orchestrator</span></div>';
1225
+ h += '<div class="legend-row"><div class="legend-swatch" style="background:rgba(34,211,238,.08);border:1px solid rgba(34,211,238,.35);border-radius:8px"></div><span class="legend-label">Trigger</span></div>';
1226
+ h += '<div class="legend-section-title" style="margin-top:8px">Badges</div>';
1227
+ h += '<div class="legend-row"><div class="legend-badge" style="background:rgba(34,211,238,.15);border:1px solid rgba(34,211,238,.35);color:#22d3ee;font-size:8px">\\u26A1</div><span class="legend-label">Has Hooks</span></div>';
1228
+ h += '<div class="legend-row"><div class="legend-badge" style="background:rgba(251,191,36,.15);border:1px solid rgba(251,191,36,.35);color:#fbbf24;font-size:7px;font-weight:700">A</div><span class="legend-label">Advisory</span></div>';
1229
+ h += '<div class="legend-row"><div class="legend-badge" style="background:rgba(148,163,184,.12);border:1px solid rgba(148,163,184,.25);color:#94a3b8;font-size:7px;font-weight:700">M</div><span class="legend-label">Manual</span></div>';
1230
+ h += '<div class="legend-section-title" style="margin-top:8px">Edges</div>';
1231
+ h += '<div class="legend-row"><div class="legend-line" style="border-color:rgba(255,255,255,.15)"></div><span class="legend-label">Unconditional</span></div>';
1232
+ h += '<div class="legend-row"><div class="legend-line dashed" style="border-color:#5858a0"></div><span class="legend-label">Conditional</span></div>';
1233
+ h += '<div class="legend-row"><div class="legend-line dashed" style="border-color:#f87171"></div><span class="legend-label">Failure</span></div>';
1234
+ h += '<div class="legend-row"><div class="legend-line dashed" style="border-color:#fb923c"></div><span class="legend-label">Loop (max N)</span></div>';
1235
+ if (dataFlowEnabled) {
1236
+ h += '<div class="legend-section-title" style="margin-top:8px">Data Flow</div>';
1237
+ h += '<div class="legend-row"><div class="legend-line" style="border-color:#f59e0b"></div><span class="legend-label">Active</span></div>';
1238
+ }
1239
+ el.innerHTML = h;
1240
+ }
1241
+
1242
+ // --- Tooltip ---
1243
+ function showTooltip(evt, text) {
1244
+ const tip = document.getElementById('tooltip');
1245
+ const container = document.getElementById('graph-container');
1246
+ const r = container.getBoundingClientRect();
1247
+ tip.textContent = text;
1248
+ tip.style.left = (evt.clientX - r.left + 12) + 'px';
1249
+ tip.style.top = (evt.clientY - r.top - 8) + 'px';
1250
+ tip.classList.add('visible');
1251
+ }
1252
+ function hideTooltip() { document.getElementById('tooltip').classList.remove('visible'); }
1253
+
1254
+ // --- Trigger Popover ---
1255
+ let _activePopoverEntry = null;
1256
+ function toggleTriggerPopover(entryId) {
1257
+ hideTooltip();
1258
+ const pop = document.getElementById('trigger-popover');
1259
+ if (_activePopoverEntry === entryId) {
1260
+ pop.style.display = 'none';
1261
+ _activePopoverEntry = null;
1262
+ return;
1263
+ }
1264
+ _activePopoverEntry = entryId;
1265
+ const triggers = window.__triggersByEntry && window.__triggersByEntry[entryId];
1266
+ if (!triggers || !triggers.length) { pop.style.display = 'none'; return; }
1267
+ const pos = window.__triggerNodePositions && window.__triggerNodePositions[entryId];
1268
+ if (!pos) { pop.style.display = 'none'; return; }
1269
+
1270
+ let h = '<div class="trigger-popover-title">Triggers</div>';
1271
+ triggers.forEach(t => {
1272
+ h += '<div class="trigger-popover-item">';
1273
+ h += '<span class="trigger-popover-cmd">/' + esc(t.name) + '</span>';
1274
+ if (t.pattern && t.pattern !== '/' + t.name) {
1275
+ h += '<span class="trigger-popover-pattern">' + esc(t.pattern) + '</span>';
1276
+ }
1277
+ h += '</div>';
1278
+ });
1279
+ pop.innerHTML = h;
1280
+ pop.style.display = 'block';
1281
+
1282
+ // Position relative to graph-container using SVG coordinates -> screen coordinates
1283
+ const svg = document.getElementById('graph-svg');
1284
+ const container = document.getElementById('graph-container');
1285
+ const cRect = container.getBoundingClientRect();
1286
+ const badgeX = pos.x - pos.w / 2 + 8;
1287
+ const badgeY = pos.y - 2;
1288
+ // Convert SVG coords to screen coords using current transform
1289
+ const screenX = badgeX * transform.scale + transform.x;
1290
+ const screenY = badgeY * transform.scale + transform.y;
1291
+ pop.style.left = (screenX + 16) + 'px';
1292
+ pop.style.top = (screenY - 10) + 'px';
1293
+
1294
+ // Ensure popover stays within container bounds
1295
+ requestAnimationFrame(() => {
1296
+ const pRect = pop.getBoundingClientRect();
1297
+ if (pRect.right > cRect.right - 10) {
1298
+ pop.style.left = (screenX - pRect.width - 10) + 'px';
1299
+ }
1300
+ if (pRect.bottom > cRect.bottom - 10) {
1301
+ pop.style.top = (screenY - pRect.height + 10) + 'px';
1302
+ }
1303
+ });
1304
+ }
1305
+
1306
+ // Close popover on click outside
1307
+ document.addEventListener('click', function(e) {
1308
+ if (_activePopoverEntry && !e.target.closest('.trigger-badge') && !e.target.closest('#trigger-popover')) {
1309
+ document.getElementById('trigger-popover').style.display = 'none';
1310
+ _activePopoverEntry = null;
1311
+ }
1312
+ });
1313
+
1314
+ // ═══════════════════════════════════════════════════════════════
1315
+ // SECTION: Interaction
1316
+ // ═══════════════════════════════════════════════════════════════
1317
+
1318
+ function onNodeClick(id) {
1319
+ selectedNode = id;
1320
+ document.querySelectorAll('.node-group').forEach(g => g.classList.toggle('node-selected', g.dataset.id === id));
1321
+ showNodeDetail(id);
1322
+ switchTab('inspect');
1323
+ }
1324
+ function onNodeHover(id) {
1325
+ hoveredNode = id; highlightConnected(id);
1326
+ const g = document.querySelector('.node-group[data-id="' + id + '"] .node-shape');
1327
+ if (g) g.style.filter = 'url(#' + g.dataset.glow + ')';
1328
+ }
1329
+ function onNodeLeave() {
1330
+ hoveredNode = null; clearHighlights();
1331
+ document.querySelectorAll('.node-shape').forEach(n => n.style.filter = '');
1332
+ }
1333
+ function highlightConnected(nodeId) {
1334
+ const connectedNodes = new Set([nodeId]);
1335
+ data.edges.forEach(e => { if (e.from === nodeId || e.to === nodeId) { connectedNodes.add(e.from); connectedNodes.add(e.to); } });
1336
+ data.nodes.filter(n => n.type === 'gate').forEach(g => {
1337
+ if (g.after === nodeId) connectedNodes.add(g.id);
1338
+ if (g.id === nodeId && g.after) connectedNodes.add(g.after);
1339
+ });
1340
+ document.querySelectorAll('.node-group').forEach(g => {
1341
+ const id = g.dataset.id;
1342
+ g.classList.toggle('dimmed', !connectedNodes.has(id));
1343
+ g.classList.toggle('highlighted', connectedNodes.has(id));
1344
+ });
1345
+ document.querySelectorAll('.edge-group').forEach(g => {
1346
+ const from = g.dataset.from, to = g.dataset.to;
1347
+ const isConn = from === nodeId || to === nodeId;
1348
+ g.classList.toggle('dimmed', !isConn);
1349
+ g.classList.toggle('highlighted', isConn);
1350
+ });
1351
+ }
1352
+ function clearHighlights() {
1353
+ document.querySelectorAll('.node-group').forEach(g => g.classList.remove('dimmed', 'highlighted'));
1354
+ document.querySelectorAll('.edge-group').forEach(g => g.classList.remove('dimmed', 'highlighted'));
1355
+ }
1356
+
1357
+ // --- Pan & Zoom ---
1358
+ const graphContainer = document.getElementById('graph-container');
1359
+ graphContainer.addEventListener('mousedown', e => {
1360
+ if (e.target.closest('.node-group') || e.target.closest('.zoom-btn') || e.target.closest('.hook-badge')) return;
1361
+ isPanning = true;
1362
+ panStart = { x: e.clientX - transform.x, y: e.clientY - transform.y };
1363
+ graphContainer.classList.add('dragging');
1364
+ });
1365
+ window.addEventListener('mousemove', e => {
1366
+ if (!isPanning) return;
1367
+ transform.x = e.clientX - panStart.x; transform.y = e.clientY - panStart.y;
1368
+ applyTransform();
1369
+ });
1370
+ window.addEventListener('mouseup', () => { isPanning = false; graphContainer.classList.remove('dragging'); });
1371
+ graphContainer.addEventListener('wheel', e => {
1372
+ e.preventDefault();
1373
+ const delta = e.deltaY > 0 ? 0.9 : 1.1;
1374
+ const r = graphContainer.getBoundingClientRect();
1375
+ const mx = e.clientX - r.left, my = e.clientY - r.top;
1376
+ const newScale = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, transform.scale * delta));
1377
+ const ratio = newScale / transform.scale;
1378
+ transform.x = mx - ratio * (mx - transform.x);
1379
+ transform.y = my - ratio * (my - transform.y);
1380
+ transform.scale = newScale;
1381
+ applyTransform();
1382
+ }, { passive: false });
1383
+
1384
+ function applyTransform() {
1385
+ const g = document.getElementById('graph-transform');
1386
+ if (g) g.setAttribute('transform', 'translate(' + transform.x + ',' + transform.y + ') scale(' + transform.scale + ')');
1387
+ }
1388
+ function zoomIn() {
1389
+ const r = graphContainer.getBoundingClientRect();
1390
+ const cx = r.width / 2, cy = r.height / 2;
1391
+ const ns = Math.min(ZOOM_MAX, transform.scale * 1.25), ratio = ns / transform.scale;
1392
+ transform.x = cx - ratio * (cx - transform.x); transform.y = cy - ratio * (cy - transform.y);
1393
+ transform.scale = ns; applyTransform();
1394
+ }
1395
+ function zoomOut() {
1396
+ const r = graphContainer.getBoundingClientRect();
1397
+ const cx = r.width / 2, cy = r.height / 2;
1398
+ const ns = Math.max(ZOOM_MIN, transform.scale * 0.8), ratio = ns / transform.scale;
1399
+ transform.x = cx - ratio * (cx - transform.x); transform.y = cy - ratio * (cy - transform.y);
1400
+ transform.scale = ns; applyTransform();
1401
+ }
1402
+ function zoomFit() {
1403
+ const r = graphContainer.getBoundingClientRect();
1404
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1405
+ Object.values(nodePositions).forEach(p => {
1406
+ minX = Math.min(minX, p.x - p.w / 2); minY = Math.min(minY, p.y);
1407
+ maxX = Math.max(maxX, p.x + p.w / 2); maxY = Math.max(maxY, p.y + p.h);
1408
+ });
1409
+ if (minX === Infinity) return;
1410
+ const gw = maxX - minX, gh = maxY - minY, pad = ZOOM_FIT_PAD;
1411
+ const sx = (r.width - pad * 2) / gw, sy = (r.height - pad * 2) / gh;
1412
+ const ns = Math.min(sx, sy, ZOOM_FIT_MAX);
1413
+ transform.scale = ns;
1414
+ transform.x = (r.width - gw * ns) / 2 - minX * ns;
1415
+ transform.y = (r.height - gh * ns) / 2 - minY * ns;
1416
+ applyTransform();
1417
+ }
1418
+
1419
+ // ═══════════════════════════════════════════════════════════════
1420
+ // SECTION: UI Controls
1421
+ // ═══════════════════════════════════════════════════════════════
1422
+
1423
+ // --- Toggle Data Flow ---
1424
+ function toggleDataFlow() {
1425
+ dataFlowEnabled = !dataFlowEnabled;
1426
+ document.getElementById('dataflow-btn').classList.toggle('active', dataFlowEnabled);
1427
+ renderGraph();
1428
+ }
1429
+
1430
+ // --- Tab switching ---
1431
+ document.querySelectorAll('.tab').forEach(tab => {
1432
+ tab.addEventListener('click', () => switchTab(tab.dataset.tab));
1433
+ });
1434
+ function switchTab(tabName) {
1435
+ document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tabName));
1436
+ const panel = document.getElementById('side-panel');
1437
+ if (panel.classList.contains('collapsed')) panel.classList.remove('collapsed');
1438
+ switch (tabName) {
1439
+ case 'inspect': showInspectPanel(); break;
1440
+ case 'memory': showMemoryPanel(); break;
1441
+ case 'runs': showRunsPanel(); break;
1442
+ case 'depth': showDepthPanel(); break;
1443
+ case 'batch': showBatchPanel(); break;
1444
+ case 'envs': showEnvsPanel(); break;
1445
+ case 'triggers': showTriggersPanel(); break;
1446
+ case 'hooks': showHooksPanel(); break;
1447
+ case 'mcp': showMcpPanel(); break;
1448
+ case 'skills': showSkillsPanel(); break;
1449
+ case 'metering': showMeteringPanel(); break;
1450
+ case 'settings': showSettingsPanel(); break;
1451
+ }
1452
+ }
1453
+
1454
+ // --- Panel renderers ---
1455
+ function showInspectPanel() {
1456
+ if (selectedNode) showNodeDetail(selectedNode);
1457
+ else setTabContent('<div style="color:var(--t2);font-size:13px;text-align:center;margin-top:40px">Click a node to inspect</div>');
1458
+ }
1459
+
1460
+ function showNodeDetail(id) {
1461
+ const node = data.nodes.find(n => n.id === id);
1462
+ if (!node) return;
1463
+ const colors = node.type === 'agent' ? modelColor(node.model) : typeColor(node.type);
1464
+ let h = '';
1465
+ h += renderMiniGraph(id);
1466
+ h += '<div class="node-detail-header"><div class="node-icon ' + esc(node.type) + '" style="background:' + colors.bg + ';border-color:' + colors.br + '">' + typeIcon(node.type) + '</div>';
1467
+ h += '<div><div class="node-detail-name">' + esc(node.label) + '</div>';
1468
+ h += '<span class="node-detail-type" style="background:' + colors.bg + ';color:' + colors.main + ';border:1px solid ' + colors.br + '">' + esc(node.type) + '</span>';
1469
+ if (node.hooks && node.hooks.length > 0) h += '<span style="color:#22d3ee;font-size:11px;margin-left:6px">\\u26A1 ' + node.hooks.length + ' hook' + (node.hooks.length > 1 ? 's' : '') + '</span>';
1470
+ h += '</div></div>';
1471
+ if (node.role || node.description) h += '<div class="node-detail-role">' + esc(node.role || node.description) + '</div>';
1472
+ h += '<div class="panel-section expanded">';
1473
+ if (node.model) h += dr('Model', node.model);
1474
+ if (node.phase != null) h += dr('Phase', node.phase);
1475
+ if (node.permissions) h += dr('Perms', node.permissions);
1476
+ if (node.prompt) {
1477
+ var truncated = node.prompt.length > 80 ? node.prompt.substring(0, 80) + '...' : node.prompt;
1478
+ h += '<div class="detail-row"><span class="detail-label">Prompt</span><span class="detail-value prompt-preview" onclick="this.classList.toggle(\\\'prompt-expanded\\\')">' + esc(truncated) + '</span></div>';
1479
+ if (node.prompt.length > 80) {
1480
+ h += '<div class="prompt-full" style="display:none;font-size:11px;color:var(--t2);font-family:var(--font-mono);padding:6px 8px;background:rgba(255,255,255,.03);border-radius:6px;margin:4px 0 8px;white-space:pre-wrap;max-height:200px;overflow-y:auto">' + esc(node.prompt) + '</div>';
1481
+ }
1482
+ }
1483
+ if (node.isolation) h += dr('Isolation', node.isolation);
1484
+ if (node.retry) h += dr('Retry', node.retry);
1485
+ if (node.behavior) h += dr('Behavior', node.behavior);
1486
+ if (node.invocation) h += dr('Invocation', node.invocation);
1487
+ if (node.skip) h += dr('Skip', node.skip);
1488
+ if (node.kind) h += dr('Kind', node.kind);
1489
+ if (node.source) h += dr('Source', node.source);
1490
+ if (node.generates) h += dr('Generates', node.generates);
1491
+ if (node.run) h += dr('Run', node.run);
1492
+ if (node.onFail) h += dr('On Fail', node.onFail);
1493
+ h += '</div>';
1494
+
1495
+ if (node.scale) {
1496
+ const sc = node.scale;
1497
+ h += '<div class="panel-section"><div class="panel-title" style="color:var(--cyan)" onclick="this.parentElement.classList.toggle(\\\'expanded\\\')">Scale</div><div class="panel-body">';
1498
+ h += dr('Mode', sc.mode);
1499
+ if (sc.by) h += dr('By', sc.by);
1500
+ h += dr('Min', sc.min); h += dr('Max', sc.max);
1501
+ if (sc.batchSize != null) h += dr('Batch Size', sc.batchSize);
1502
+ h += '</div></div>';
1503
+ }
1504
+
1505
+ if (node.tools && node.tools.length > 0) {
1506
+ const core = node.tools.filter(t => !t.startsWith('mcp.'));
1507
+ const mcp = node.tools.filter(t => t.startsWith('mcp.'));
1508
+ h += '<div class="panel-section expanded"><div class="panel-title" onclick="this.parentElement.classList.toggle(\\\'expanded\\\')">Tools</div><div class="panel-body">';
1509
+ if (core.length > 0) { h += '<div class="tools-group-title">Core</div>'; h += core.map(t => '<span class="tool-chip core">' + esc(t) + '</span>').join(''); }
1510
+ if (mcp.length > 0) { h += '<div class="tools-group-title">MCP</div>'; h += mcp.map(t => '<span class="tool-chip mcp">' + esc(t.replace('mcp.', '')) + '</span>').join(''); }
1511
+ h += '</div></div>';
1512
+ }
1513
+ if (node.disallowedTools && node.disallowedTools.length > 0) {
1514
+ h += '<div class="panel-section"><div class="panel-title" onclick="this.parentElement.classList.toggle(\\\'expanded\\\')">Disallowed Tools</div><div class="panel-body">';
1515
+ h += node.disallowedTools.map(t => '<span class="tool-chip" style="border-color:var(--red-br);color:var(--red)">' + esc(t) + '</span>').join('');
1516
+ h += '</div></div>';
1517
+ }
1518
+ if (node.skills && node.skills.length > 0) {
1519
+ h += '<div class="panel-section"><div class="panel-title" style="color:var(--green)" onclick="this.parentElement.classList.toggle(\\\'expanded\\\')">Skills</div><div class="panel-body">';
1520
+ h += node.skills.map(sk => {
1521
+ const def = (data.skills || []).find(s => s.id === sk);
1522
+ let chip = '<span class="tool-chip skill">' + esc(sk) + '</span>';
1523
+ if (def && def.description) chip += '<div style="font-size:10px;color:var(--t3);margin:2px 0 4px 4px">' + esc(def.description) + '</div>';
1524
+ return chip;
1525
+ }).join('');
1526
+ h += '</div></div>';
1527
+ }
1528
+ if (node.reads && node.reads.length > 0) {
1529
+ h += '<div class="panel-section"><div class="panel-title" onclick="this.parentElement.classList.toggle(\\\'expanded\\\')">Reads</div><div class="panel-body">';
1530
+ h += '<ul class="panel-list">' + node.reads.map(r => '<li><span style="color:' + folderColor(r) + '">\\u25CF</span> ' + esc(r) + '</li>').join('') + '</ul></div></div>';
1531
+ }
1532
+ if (node.writes && node.writes.length > 0) {
1533
+ h += '<div class="panel-section"><div class="panel-title" onclick="this.parentElement.classList.toggle(\\\'expanded\\\')">Writes</div><div class="panel-body">';
1534
+ h += '<ul class="panel-list">' + node.writes.map(w => '<li><span style="color:' + folderColor(w) + '">\\u25CF</span> ' + esc(w) + '</li>').join('') + '</ul></div></div>';
1535
+ }
1536
+ if (node.outputs) {
1537
+ h += '<div class="panel-section"><div class="panel-title" onclick="this.parentElement.classList.toggle(\\\'expanded\\\')">Outputs</div><div class="panel-body">';
1538
+ Object.entries(node.outputs).forEach(function(entry) {
1539
+ const key = entry[0], vals = entry[1];
1540
+ h += '<div style="margin-bottom:4px"><span style="font-size:11px;color:var(--t3);font-family:var(--font-mono)">' + esc(key) + ':</span> ';
1541
+ h += vals.map(v => { const cls = v === 'pass' ? 'pass' : v === 'fail' ? 'fail' : 'default'; return '<span class="output-chip ' + cls + '">' + esc(v) + '</span>'; }).join(' ');
1542
+ h += '</div>';
1543
+ });
1544
+ h += '</div></div>';
1545
+ }
1546
+ if (node.checks && node.checks.length > 0) {
1547
+ h += '<div class="panel-section"><div class="panel-title" onclick="this.parentElement.classList.toggle(\\\'expanded\\\')">Checks</div><div class="panel-body">';
1548
+ h += '<ul class="panel-list">' + node.checks.map(c => '<li>' + esc(c) + '</li>').join('') + '</ul></div></div>';
1549
+ }
1550
+ if (node.commands && node.commands.length > 0) {
1551
+ h += '<div class="panel-section"><div class="panel-title" onclick="this.parentElement.classList.toggle(\\\'expanded\\\')">Commands</div><div class="panel-body">';
1552
+ h += '<ul class="panel-list">' + node.commands.map(c => '<li>' + esc(c) + '</li>').join('') + '</ul></div></div>';
1553
+ }
1554
+ if (node.handles && node.handles.length > 0) {
1555
+ h += '<div class="panel-section"><div class="panel-title" onclick="this.parentElement.classList.toggle(\\\'expanded\\\')">Handles</div><div class="panel-body">';
1556
+ h += node.handles.map(x => '<span class="panel-tag">' + esc(x) + '</span>').join(' ');
1557
+ h += '</div></div>';
1558
+ }
1559
+ if (node.hooks && node.hooks.length > 0) {
1560
+ h += '<div class="panel-section"><div class="panel-title" style="color:#22d3ee" onclick="this.parentElement.classList.toggle(\\\'expanded\\\')">\\u26A1 Hooks</div><div class="panel-body">';
1561
+ node.hooks.forEach(hk => {
1562
+ const origin = hk.global ? '<span style="font-size:9px;color:var(--t3);margin-left:6px;text-transform:uppercase;letter-spacing:.5px">global</span>' : '';
1563
+ h += '<div class="panel-card" style="padding:8px 10px;margin-bottom:6px;border-color:rgba(34,211,238,.15)">';
1564
+ h += '<div style="font-size:12px;font-weight:600;color:#22d3ee;font-family:var(--font-heading)">' + esc(hk.name) + origin + '</div>';
1565
+ h += '<div style="font-size:11px;color:var(--t2);margin-top:2px"><span class="panel-tag">' + esc(hk.on) + '</span>';
1566
+ if (hk.matcher) h += ' <span class="panel-tag">' + esc(hk.matcher) + '</span>';
1567
+ h += '</div>';
1568
+ if (hk.run) h += '<div style="font-size:11px;font-family:var(--font-mono);color:var(--t3);margin-top:2px">' + esc(hk.run) + '</div>';
1569
+ h += '</div>';
1570
+ });
1571
+ h += '</div></div>';
1572
+ }
1573
+ const inEdges = data.edges.filter(e => e.to === id);
1574
+ const outEdges = data.edges.filter(e => e.from === id);
1575
+ if (inEdges.length > 0 || outEdges.length > 0) {
1576
+ h += '<div class="panel-section expanded"><div class="panel-title" onclick="this.parentElement.classList.toggle(\\\'expanded\\\')">Connections</div><div class="panel-body">';
1577
+ if (inEdges.length > 0) {
1578
+ h += '<div class="tools-group-title">Incoming</div>';
1579
+ inEdges.forEach(e => {
1580
+ const fn = data.nodes.find(n => n.id === e.from);
1581
+ h += '<div style="font-size:11px;margin:2px 0;color:var(--t2)"><span style="color:var(--t)">' + esc(fn?.label || e.from) + '</span>';
1582
+ if (e.condition) h += ' <span style="color:var(--t3);font-family:var(--font-mono);font-size:10px">' + esc(e.condition) + '</span>';
1583
+ if (e.maxIterations) h += ' <span style="color:var(--orange);font-size:10px">max ' + e.maxIterations + '</span>';
1584
+ h += '</div>';
1585
+ });
1586
+ }
1587
+ if (outEdges.length > 0) {
1588
+ h += '<div class="tools-group-title">Outgoing</div>';
1589
+ outEdges.forEach(e => {
1590
+ const tn = data.nodes.find(n => n.id === e.to);
1591
+ h += '<div style="font-size:11px;margin:2px 0;color:var(--t2)">\\u2192 <span style="color:var(--t)">' + esc(tn?.label || e.to) + '</span>';
1592
+ if (e.condition) h += ' <span style="color:var(--t3);font-family:var(--font-mono);font-size:10px">' + esc(e.condition) + '</span>';
1593
+ if (e.maxIterations) h += ' <span style="color:var(--orange);font-size:10px">max ' + e.maxIterations + '</span>';
1594
+ h += '</div>';
1595
+ });
1596
+ }
1597
+ h += '</div></div>';
1598
+ }
1599
+ setTabContent(h);
1600
+ }
1601
+
1602
+ function renderMiniGraph(nodeId) {
1603
+ const connected = new Set([nodeId]);
1604
+ data.edges.forEach(e => { if (e.from === nodeId || e.to === nodeId) { connected.add(e.from); connected.add(e.to); } });
1605
+ data.nodes.filter(n => n.type === 'gate').forEach(g => { if (g.after === nodeId) connected.add(g.id); });
1606
+ const nodes = data.nodes.filter(n => connected.has(n.id));
1607
+ if (nodes.length <= 1) return '';
1608
+ const W = MINI_GRAPH_W, H = MINI_GRAPH_H;
1609
+ const gap = W / (nodes.length + 1);
1610
+ let s = '<div class="mini-graph"><svg width="' + W + '" height="' + H + '" viewBox="0 0 ' + W + ' ' + H + '">';
1611
+ const miniPos = {};
1612
+ nodes.forEach((n, i) => { miniPos[n.id] = { x: gap * (i + 1), y: n.id === nodeId ? 40 : 55 }; });
1613
+ const relevantEdges = data.edges.filter(e => connected.has(e.from) && connected.has(e.to));
1614
+ relevantEdges.forEach(e => {
1615
+ const fp = miniPos[e.from], tp = miniPos[e.to];
1616
+ if (!fp || !tp) return;
1617
+ const isFailure = e.condition && e.condition.includes('fail');
1618
+ const isLoop = e.maxIterations != null;
1619
+ const color = isFailure ? '#f87171' : isLoop ? '#fb923c' : e.condition ? '#5858a0' : 'rgba(255,255,255,.15)';
1620
+ const dash = (e.condition || isLoop) ? 'stroke-dasharray="3 2"' : '';
1621
+ s += '<line x1="' + fp.x + '" y1="' + fp.y + '" x2="' + tp.x + '" y2="' + tp.y + '" stroke="' + color + '" stroke-width="1" ' + dash + '/>';
1622
+ });
1623
+ nodes.forEach(n => {
1624
+ const p = miniPos[n.id];
1625
+ const colors = n.type === 'agent' ? modelColor(n.model) : typeColor(n.type);
1626
+ const isCurrent = n.id === nodeId;
1627
+ const r = isCurrent ? 14 : 10;
1628
+ s += '<circle cx="' + p.x + '" cy="' + p.y + '" r="' + r + '" fill="' + colors.bg + '" stroke="' + colors.main + '" stroke-width="' + (isCurrent ? 2 : 1) + '"/>';
1629
+ s += '<text x="' + p.x + '" y="' + (p.y + r + 12) + '" text-anchor="middle" font-family="monospace" font-size="8" fill="' + (isCurrent ? colors.main : '#8888a8') + '" font-weight="' + (isCurrent ? '600' : '400') + '">' + esc(n.label) + '</text>';
1630
+ });
1631
+ s += '</svg></div>';
1632
+ return s;
1633
+ }
1634
+
1635
+ function showMemoryPanel() {
1636
+ const mem = data.memory;
1637
+ if (!mem || Object.keys(mem).length === 0) { setTabContent(emptyMsg('No memory config')); return; }
1638
+ let h = '';
1639
+ Object.entries(mem).forEach(function(entry) {
1640
+ const key = entry[0], val = entry[1];
1641
+ h += '<div class="panel-section"><div class="panel-title">' + esc(key) + '</div>';
1642
+ h += '<div class="panel-card"><div class="panel-card-desc" style="font-family:var(--font-mono);font-size:10px;word-break:break-all">' + esc(JSON.stringify(val, null, 2)) + '</div></div>';
1643
+ h += '</div>';
1644
+ });
1645
+ setTabContent(h);
1646
+ }
1647
+
1648
+ function showRunsPanel() {
1649
+ setTabContent('<div style="color:var(--t2);font-size:13px;text-align:center;margin-top:40px">No run data embedded.</div>');
1650
+ }
1651
+
1652
+ function showDepthPanel() {
1653
+ const depth = data.depth;
1654
+ if (!depth || (!depth.factors?.length && !depth.levels?.length)) { setTabContent(emptyMsg('No depth config')); return; }
1655
+ let h = '';
1656
+ if (depth.factors && depth.factors.length > 0) {
1657
+ h += '<div class="panel-section"><div class="panel-title">Factors</div>';
1658
+ h += '<ul class="panel-list">' + depth.factors.map(f => '<li>' + esc(f) + '</li>').join('') + '</ul></div>';
1659
+ }
1660
+ if (depth.levels && depth.levels.length > 0) {
1661
+ h += '<div class="panel-section"><div class="panel-title">Levels</div>';
1662
+ depth.levels.forEach(level => {
1663
+ h += '<div class="panel-card"><div class="panel-card-title">' + level.level + ' - ' + esc(level.label) + '</div>';
1664
+ h += '<div class="panel-card-desc">Omit: ' + (level.omit.length > 0 ? level.omit.map(o => '<span class="panel-tag">' + esc(o) + '</span>').join(' ') : '<span style="color:var(--green)">none</span>') + '</div></div>';
1665
+ });
1666
+ h += '</div>';
1667
+ }
1668
+ setTabContent(h);
1669
+ }
1670
+
1671
+ function showBatchPanel() {
1672
+ const batch = data.batch;
1673
+ if (!batch || Object.keys(batch).length === 0) { setTabContent(emptyMsg('No batch config')); return; }
1674
+ let h = '<div class="panel-section"><div class="panel-title">Batch Configuration</div>';
1675
+ h += '<div class="panel-card"><div class="panel-card-desc" style="font-family:var(--font-mono);font-size:10px">' + esc(JSON.stringify(batch, null, 2)) + '</div></div>';
1676
+ h += '</div>';
1677
+ setTabContent(h);
1678
+ }
1679
+
1680
+ function showEnvsPanel() {
1681
+ const envs = data.environments;
1682
+ if (!envs || Object.keys(envs).length === 0) { setTabContent(emptyMsg('No environments')); return; }
1683
+ let h = '';
1684
+ Object.entries(envs).forEach(function(entry) {
1685
+ const name = entry[0], env = entry[1];
1686
+ const color = name === 'production' ? 'var(--red)' : 'var(--green)';
1687
+ h += '<div class="panel-section"><div class="panel-title" style="color:' + color + '">' + esc(name) + '</div>';
1688
+ h += '<div class="panel-card">';
1689
+ Object.entries(env).forEach(function(e2) {
1690
+ h += '<div style="margin:3px 0;font-size:12px"><span style="color:var(--t3);font-family:var(--font-mono);font-size:11px">' + esc(e2[0]) + ':</span> <span style="color:var(--t)">' + esc(String(e2[1])) + '</span></div>';
1691
+ });
1692
+ h += '</div></div>';
1693
+ });
1694
+ setTabContent(h);
1695
+ }
1696
+
1697
+ function showTriggersPanel() {
1698
+ const triggers = data.triggers;
1699
+ if (!triggers || triggers.length === 0) { setTabContent(emptyMsg('No triggers')); return; }
1700
+ let h = '<div class="panel-section"><div class="panel-title">Command Triggers</div>';
1701
+ triggers.forEach(t => {
1702
+ h += '<div class="panel-card"><div class="panel-card-title" style="font-family:var(--font-mono);color:var(--purple)">' + esc(t.pattern) + '</div>';
1703
+ if (t.argument) h += '<div class="panel-card-desc">Argument: <span class="panel-tag">' + esc(t.argument) + '</span></div>';
1704
+ h += '</div>';
1705
+ });
1706
+ h += '</div>';
1707
+ setTabContent(h);
1708
+ }
1709
+
1710
+ function showHooksPanel() {
1711
+ const hooks = data.hooks;
1712
+ if (!hooks || hooks.length === 0) { setTabContent(emptyMsg('No hooks')); return; }
1713
+ let h = '<div class="panel-section"><div class="panel-title">\\u26A1 Global Hooks</div>';
1714
+ hooks.forEach(hk => {
1715
+ h += '<div class="panel-card" style="border-color:rgba(34,211,238,.15)">';
1716
+ h += '<div class="panel-card-title" style="color:#22d3ee">' + esc(hk.name) + '</div>';
1717
+ h += '<div class="panel-card-desc">';
1718
+ h += '<div>Event: <span class="panel-tag">' + esc(hk.on) + '</span></div>';
1719
+ if (hk.matcher) h += '<div>Matcher: <span class="panel-tag">' + esc(hk.matcher) + '</span></div>';
1720
+ if (hk.run) h += '<div>Run: <span style="font-family:var(--font-mono)">' + esc(hk.run) + '</span></div>';
1721
+ if (hk.type) h += '<div>Type: <span class="panel-tag">' + esc(hk.type) + '</span></div>';
1722
+ h += '</div></div>';
1723
+ });
1724
+ h += '</div>';
1725
+ setTabContent(h);
1726
+ }
1727
+
1728
+ function showMcpPanel() {
1729
+ const servers = data.mcpServers;
1730
+ if (!servers || Object.keys(servers).length === 0) { setTabContent(emptyMsg('No MCP servers')); return; }
1731
+ let h = '<div class="panel-section"><div class="panel-title">MCP Servers</div>';
1732
+ Object.entries(servers).forEach(function(entry) {
1733
+ const name = entry[0], cfg = entry[1];
1734
+ h += '<div class="panel-card"><div class="panel-card-title" style="display:flex;align-items:center;gap:8px">';
1735
+ if (cfg.type) h += '<span class="panel-tag" style="background:var(--purple-bg);color:var(--purple);border-color:var(--purple-br)">' + esc(cfg.type) + '</span>';
1736
+ h += esc(name) + '</div>';
1737
+ h += '<div class="panel-card-desc" style="font-family:var(--font-mono);font-size:10px;word-break:break-all">';
1738
+ if (cfg.command) h += esc(cfg.command) + ' ' + (cfg.args || []).map(a => esc(a)).join(' ');
1739
+ if (cfg.url) h += esc(cfg.url);
1740
+ h += '</div></div>';
1741
+ });
1742
+ h += '</div>';
1743
+ setTabContent(h);
1744
+ }
1745
+
1746
+ function showSkillsPanel() {
1747
+ const skills = data.skills || [];
1748
+ const toolDefs = data.toolDefs || [];
1749
+ if (skills.length === 0 && toolDefs.length === 0) { setTabContent(emptyMsg('No skills or custom tools')); return; }
1750
+ let h = '';
1751
+ if (skills.length > 0) {
1752
+ h += '<div class="panel-section"><div class="panel-title" style="color:var(--green)">Skills (' + skills.length + ')</div>';
1753
+ skills.forEach(sk => {
1754
+ h += '<div class="skill-card"><div class="skill-card-name">' + esc(sk.id) + '</div>';
1755
+ if (sk.description) h += '<div class="skill-card-desc">' + esc(sk.description) + '</div>';
1756
+ h += '<div class="skill-card-meta">';
1757
+ if (sk.scripts) sk.scripts.forEach(sc => { h += '<span class="tool-chip script-tool">' + esc(sc) + '</span>'; });
1758
+ if (sk.domains) sk.domains.forEach(d => { h += '<span class="tool-chip" style="background:var(--cyan-bg);color:var(--cyan);border-color:var(--cyan-br)">' + esc(d) + '</span>'; });
1759
+ if (sk.references) sk.references.forEach(r => { h += '<span class="tool-chip" style="background:var(--blue-bg);color:var(--blue);border-color:var(--blue-br)">' + esc(r) + '</span>'; });
1760
+ if (sk.prompt) h += '<span class="tool-chip" style="background:var(--gold-bg);color:var(--gold);border-color:var(--gold-br)">' + esc(sk.prompt) + '</span>';
1761
+ h += '</div>';
1762
+ const users = data.nodes.filter(n => n.skills && n.skills.includes(sk.id));
1763
+ if (users.length > 0) h += '<div class="skill-card-agents">' + users.length + ' agent' + (users.length > 1 ? 's' : '') + ': ' + users.map(u => esc(u.label)).join(', ') + '</div>';
1764
+ h += '</div>';
1765
+ });
1766
+ h += '</div>';
1767
+ }
1768
+ if (toolDefs.length > 0) {
1769
+ h += '<div class="panel-section"><div class="panel-title" style="color:var(--orange)">Custom Tools (' + toolDefs.length + ')</div>';
1770
+ toolDefs.forEach(td => {
1771
+ h += '<div class="tool-card"><div class="tool-card-name">' + esc(td.id) + '</div>';
1772
+ h += '<div class="tool-card-script">' + esc(td.script) + (td.lang ? ' (' + td.lang + ')' : '') + '</div>';
1773
+ if (td.description) h += '<div class="tool-card-desc">' + esc(td.description) + '</div>';
1774
+ if (td.args && td.args.length > 0) h += '<div style="margin-top:4px">' + td.args.map(a => '<span class="panel-tag">' + esc(a) + '</span>').join(' ') + '</div>';
1775
+ h += '</div>';
1776
+ });
1777
+ h += '</div>';
1778
+ }
1779
+ setTabContent(h);
1780
+ }
1781
+
1782
+ function showMeteringPanel() {
1783
+ const metering = data.metering;
1784
+ if (!metering) { setTabContent(emptyMsg('No metering config')); return; }
1785
+ let h = '<div class="panel-section"><div class="panel-title" style="color:var(--cyan)">Metering</div>';
1786
+ if (metering.track && metering.track.length > 0) {
1787
+ h += '<div class="tools-group-title">Track</div>';
1788
+ h += metering.track.map(t => '<span class="panel-tag" style="background:var(--cyan-bg);color:var(--cyan);border-color:var(--cyan-br)">' + esc(t) + '</span>').join(' ');
1789
+ }
1790
+ if (metering.per && metering.per.length > 0) {
1791
+ h += '<div class="tools-group-title" style="margin-top:8px">Per</div>';
1792
+ h += metering.per.map(p => '<span class="panel-tag">' + esc(p) + '</span>').join(' ');
1793
+ }
1794
+ if (metering.output) h += dr('Output', metering.output);
1795
+ if (metering.format) h += dr('Format', metering.format);
1796
+ if (metering.pricing) h += dr('Pricing', metering.pricing);
1797
+ h += '</div>';
1798
+ setTabContent(h);
1799
+ }
1800
+
1801
+ function showSettingsPanel() {
1802
+ const settings = data.settings;
1803
+ if (!settings) { setTabContent(emptyMsg('No settings')); return; }
1804
+ let h = '';
1805
+ ['allow', 'deny', 'ask'].forEach(key => {
1806
+ const color = key === 'allow' ? 'var(--green)' : key === 'deny' ? 'var(--red)' : 'var(--gold)';
1807
+ const bgVar = key === 'allow' ? 'green' : key === 'deny' ? 'red' : 'gold';
1808
+ h += '<div class="panel-section"><div class="panel-title" style="color:' + color + '">' + key.charAt(0).toUpperCase() + key.slice(1) + '</div>';
1809
+ if (settings[key] && settings[key].length > 0) {
1810
+ h += settings[key].map(a => '<div style="margin:3px 0"><span class="tool-chip" style="background:var(--' + bgVar + '-bg);color:var(--' + bgVar + ');border-color:var(--' + bgVar + '-br)">' + esc(a) + '</span></div>').join('');
1811
+ } else {
1812
+ h += '<div style="font-size:12px;color:var(--t3)">none</div>';
1813
+ }
1814
+ h += '</div>';
1815
+ });
1816
+ setTabContent(h);
1817
+ }
1818
+
1819
+ // --- Toggle Panel & Orientation ---
1820
+ function togglePanel() {
1821
+ const panel = document.getElementById('side-panel');
1822
+ panel.classList.toggle('collapsed');
1823
+ const btn = document.getElementById('panel-btn');
1824
+ btn.innerHTML = panel.classList.contains('collapsed') ? 'Panel &#9654;' : '&#9664; Panel';
1825
+ setTimeout(zoomFit, 250);
1826
+ }
1827
+ function toggleOrientation() {
1828
+ isHorizontal = !isHorizontal;
1829
+ const btn = document.getElementById('orientation-btn');
1830
+ btn.innerHTML = isHorizontal ? '&#8597; Vertical' : '&#8596; Horizontal';
1831
+ transform = { x: 0, y: 0, scale: 1 };
1832
+ renderGraph();
1833
+ }
1834
+
1835
+ // --- Load modal ---
1836
+ function openLoadModal() { document.getElementById('load-modal').classList.add('open'); document.getElementById('json-input').focus(); }
1837
+ function closeLoadModal() { document.getElementById('load-modal').classList.remove('open'); }
1838
+ function loadFromInput() {
1839
+ const input = document.getElementById('json-input').value.trim();
1840
+ try { const parsed = JSON.parse(input); loadData(parsed); closeLoadModal(); }
1841
+ catch (e) { alert('Invalid JSON: ' + e.message); }
1842
+ }
1843
+
1844
+ // --- Data loading ---
1845
+ function loadData(json) {
1846
+ data = json; selectedNode = null; hoveredNode = null;
1847
+ transform = { x: 0, y: 0, scale: 1 };
1848
+ const topo = json.topology || {};
1849
+ document.getElementById('topo-name').textContent = topo.name || 'Untitled';
1850
+ document.getElementById('topo-ver').textContent = topo.version ? 'v' + topo.version : '';
1851
+ document.getElementById('topo-desc').textContent = topo.description || '';
1852
+ const badgesEl = document.getElementById('pattern-badges');
1853
+ badgesEl.innerHTML = (topo.patterns || []).map(p => '<span class="pattern-badge ' + esc(p) + '">' + esc(p) + '</span>').join('');
1854
+ if (topo.foundations) {
1855
+ badgesEl.innerHTML += topo.foundations.map(f => '<span class="pattern-badge" style="background:var(--s2);color:var(--t2);border:1px solid var(--b)">' + esc(f) + '</span>').join('');
1856
+ }
1857
+ if (topo.advanced) {
1858
+ badgesEl.innerHTML += topo.advanced.map(a => '<span class="pattern-badge" style="background:var(--amber-bg);color:var(--amber);border:1px solid var(--amber-br)">' + esc(a) + '</span>').join('');
1859
+ }
1860
+ if (data.hooks && data.hooks.length > 0) {
1861
+ data.nodes.forEach(n => {
1862
+ const matched = data.hooks.filter(hk => {
1863
+ if (!hk.matcher) return false;
1864
+ const m = hk.matcher.toLowerCase();
1865
+ return (n.id && n.id.toLowerCase().includes(m)) || (n.label && n.label.toLowerCase().includes(m));
1866
+ });
1867
+ if (matched.length > 0) {
1868
+ if (!n.hooks) n.hooks = [];
1869
+ matched.forEach(hk => {
1870
+ if (!n.hooks.some(h => h.name === hk.name)) n.hooks.push({ ...hk, global: true });
1871
+ });
1872
+ }
1873
+ });
1874
+ }
1875
+ renderGraph();
1876
+ switchTab('inspect');
1877
+ }
1878
+
1879
+ // --- Keyboard shortcuts ---
1880
+ window.addEventListener('keydown', e => {
1881
+ // Skip if focused on an input/textarea
1882
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
1883
+ switch (e.key) {
1884
+ case '+': case '=': e.preventDefault(); zoomIn(); break;
1885
+ case '-': e.preventDefault(); zoomOut(); break;
1886
+ case '0': e.preventDefault(); zoomFit(); break;
1887
+ case 'ArrowUp': e.preventDefault(); transform.y += PAN_STEP; applyTransform(); break;
1888
+ case 'ArrowDown': e.preventDefault(); transform.y -= PAN_STEP; applyTransform(); break;
1889
+ case 'ArrowLeft': e.preventDefault(); transform.x += PAN_STEP; applyTransform(); break;
1890
+ case 'ArrowRight': e.preventDefault(); transform.x -= PAN_STEP; applyTransform(); break;
1891
+ }
1892
+ });
1893
+
1894
+ // --- Export SVG ---
1895
+ function exportSvg() {
1896
+ const svg = document.getElementById('graph-svg');
1897
+ if (!svg) return;
1898
+ const clone = svg.cloneNode(true);
1899
+ // Add inline styles for standalone SVG
1900
+ const styleEl = document.createElementNS('http://www.w3.org/2000/svg', 'style');
1901
+ styleEl.textContent = 'text{font-family:-apple-system,BlinkMacSystemFont,\\'Segoe UI\\',sans-serif}.node-label{font-weight:600;font-size:12px}.node-sublabel{font-size:8.5px}.edge-path{fill:none;stroke-width:1.5}.edge-path.unconditional{stroke:rgba(255,255,255,.15)}.edge-path.conditional{stroke:#5858a0;stroke-dasharray:6 4}.edge-path.failure{stroke:#f87171;stroke-dasharray:6 4}.edge-path.loop{stroke:#fb923c;stroke-dasharray:4 4}';
1902
+ clone.insertBefore(styleEl, clone.firstChild);
1903
+ clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
1904
+ const blob = new Blob([clone.outerHTML], { type: 'image/svg+xml' });
1905
+ const url = URL.createObjectURL(blob);
1906
+ const a = document.createElement('a');
1907
+ a.href = url;
1908
+ a.download = (data && data.topology ? data.topology.name : 'topology') + '.svg';
1909
+ document.body.appendChild(a);
1910
+ a.click();
1911
+ document.body.removeChild(a);
1912
+ URL.revokeObjectURL(url);
1913
+ }
1914
+
1915
+ // --- Search ---
1916
+ function onSearchInput(query) {
1917
+ searchQuery = (query || '').toLowerCase().trim();
1918
+ if (!searchQuery) {
1919
+ document.querySelectorAll('.node-group').forEach(g => g.classList.remove('search-dimmed'));
1920
+ document.querySelectorAll('.edge-group').forEach(g => g.classList.remove('dimmed'));
1921
+ return;
1922
+ }
1923
+ const matchedIds = new Set();
1924
+ data.nodes.forEach(n => {
1925
+ if ((n.id && n.id.toLowerCase().includes(searchQuery)) ||
1926
+ (n.label && n.label.toLowerCase().includes(searchQuery)) ||
1927
+ (n.type && n.type.toLowerCase().includes(searchQuery))) {
1928
+ matchedIds.add(n.id);
1929
+ }
1930
+ });
1931
+ document.querySelectorAll('.node-group').forEach(g => {
1932
+ g.classList.toggle('search-dimmed', !matchedIds.has(g.dataset.id));
1933
+ });
1934
+ document.querySelectorAll('.edge-group').forEach(g => {
1935
+ const isConn = matchedIds.has(g.dataset.from) || matchedIds.has(g.dataset.to);
1936
+ g.classList.toggle('dimmed', !isConn);
1937
+ });
1938
+ }
1939
+
1940
+ // --- Validation Issues Panel ---
1941
+ function initIssuesPanel() {
1942
+ const issues = typeof VALIDATION_ISSUES !== 'undefined' ? VALIDATION_ISSUES : [];
1943
+ if (issues.length === 0) return;
1944
+ const btn = document.getElementById('issues-btn');
1945
+ if (btn) btn.style.display = '';
1946
+ const badge = document.getElementById('issues-badge');
1947
+ if (badge) badge.textContent = issues.length;
1948
+ const list = document.getElementById('issues-list');
1949
+ if (!list) return;
1950
+ let h = '';
1951
+ issues.forEach(issue => {
1952
+ const lvl = issue.level || 'error';
1953
+ h += '<div class="issue-item ' + lvl + '">';
1954
+ h += '<div class="issue-rule ' + lvl + '">' + esc(issue.rule) + ' (' + lvl + ')</div>';
1955
+ h += '<div class="issue-msg">' + esc(issue.message) + '</div>';
1956
+ if (issue.node) h += '<div class="issue-node">Node: ' + esc(issue.node) + '</div>';
1957
+ h += '</div>';
1958
+ });
1959
+ list.innerHTML = h;
1960
+ }
1961
+ function toggleIssuesPanel() {
1962
+ issuesPanelOpen = !issuesPanelOpen;
1963
+ const panel = document.getElementById('issues-panel');
1964
+ if (panel) panel.classList.toggle('collapsed', !issuesPanelOpen);
1965
+ }
1966
+
1967
+ // ═══════════════════════════════════════════════════════════════
1968
+ // SECTION: Init
1969
+ // ═══════════════════════════════════════════════════════════════
1970
+
1971
+ window.addEventListener('resize', () => { if (data) zoomFit(); });
1972
+ loadData(DEFAULT_DATA);
1973
+ initIssuesPanel();
1974
+ `;
1975
+ // ---------------------------------------------------------------------------
1976
+ //# sourceMappingURL=index.js.map