devglide 0.1.1

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 (252) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +338 -0
  3. package/bin/claude-md-template.js +94 -0
  4. package/bin/devglide.js +387 -0
  5. package/package.json +85 -0
  6. package/pnpm-workspace.yaml +3 -0
  7. package/src/apps/coder/.turbo/turbo-lint.log +5 -0
  8. package/src/apps/coder/package.json +16 -0
  9. package/src/apps/coder/public/favicon.svg +7 -0
  10. package/src/apps/coder/public/page.css +275 -0
  11. package/src/apps/coder/public/page.js +528 -0
  12. package/src/apps/coder/server.js +3 -0
  13. package/src/apps/documentation/public/page.css +597 -0
  14. package/src/apps/documentation/public/page.js +609 -0
  15. package/src/apps/kanban/.turbo/turbo-lint.log +97 -0
  16. package/src/apps/kanban/.turbo/turbo-typecheck.log +5 -0
  17. package/src/apps/kanban/package.json +32 -0
  18. package/src/apps/kanban/public/favicon.svg +7 -0
  19. package/src/apps/kanban/public/page.css +1010 -0
  20. package/src/apps/kanban/public/page.js +1730 -0
  21. package/src/apps/kanban/public/vendor/marked.min.js +6 -0
  22. package/src/apps/kanban/public/vendor/sortable.min.js +2 -0
  23. package/src/apps/kanban/src/db.ts +319 -0
  24. package/src/apps/kanban/src/index.ts +14 -0
  25. package/src/apps/kanban/src/mcp-helpers.test.ts +88 -0
  26. package/src/apps/kanban/src/mcp-helpers.ts +60 -0
  27. package/src/apps/kanban/src/mcp.ts +59 -0
  28. package/src/apps/kanban/src/routes/attachments.ts +161 -0
  29. package/src/apps/kanban/src/routes/features.ts +233 -0
  30. package/src/apps/kanban/src/routes/issues.ts +373 -0
  31. package/src/apps/kanban/src/tools/feature-tools.ts +164 -0
  32. package/src/apps/kanban/src/tools/item-tools.ts +307 -0
  33. package/src/apps/kanban/src/tools/versioned-entry-tools.ts +72 -0
  34. package/src/apps/kanban/tsconfig.check.json +9 -0
  35. package/src/apps/kanban/tsconfig.json +9 -0
  36. package/src/apps/keymap/.turbo/turbo-lint.log +5 -0
  37. package/src/apps/keymap/package.json +16 -0
  38. package/src/apps/keymap/public/page.css +275 -0
  39. package/src/apps/keymap/public/page.js +294 -0
  40. package/src/apps/keymap/server.js +25 -0
  41. package/src/apps/log/.turbo/turbo-build.log +5 -0
  42. package/src/apps/log/.turbo/turbo-lint.log +45 -0
  43. package/src/apps/log/.turbo/turbo-typecheck.log +5 -0
  44. package/src/apps/log/node_modules/.bin/tsc +21 -0
  45. package/src/apps/log/node_modules/.bin/tsserver +21 -0
  46. package/src/apps/log/node_modules/.bin/tsx +21 -0
  47. package/src/apps/log/package.json +36 -0
  48. package/src/apps/log/public/console-sniffer.js +221 -0
  49. package/src/apps/log/public/favicon.svg +7 -0
  50. package/src/apps/log/public/page.css +322 -0
  51. package/src/apps/log/public/page.js +463 -0
  52. package/src/apps/log/src/index.ts +9 -0
  53. package/src/apps/log/src/mcp.ts +122 -0
  54. package/src/apps/log/src/routes/log.ts +333 -0
  55. package/src/apps/log/src/routes/status.ts +25 -0
  56. package/src/apps/log/src/server-sniffer.ts +118 -0
  57. package/src/apps/log/src/services/file-patterns.ts +39 -0
  58. package/src/apps/log/src/services/file-tailer.ts +228 -0
  59. package/src/apps/log/src/services/line-parser.ts +94 -0
  60. package/src/apps/log/src/services/log-writer.ts +39 -0
  61. package/src/apps/log/tsconfig.json +8 -0
  62. package/src/apps/prompts/.turbo/turbo-build.log +5 -0
  63. package/src/apps/prompts/.turbo/turbo-lint.log +24 -0
  64. package/src/apps/prompts/.turbo/turbo-typecheck.log +5 -0
  65. package/src/apps/prompts/mcp.ts +175 -0
  66. package/src/apps/prompts/node_modules/.bin/tsc +21 -0
  67. package/src/apps/prompts/node_modules/.bin/tsserver +21 -0
  68. package/src/apps/prompts/node_modules/.bin/tsx +21 -0
  69. package/src/apps/prompts/package.json +25 -0
  70. package/src/apps/prompts/public/page.css +315 -0
  71. package/src/apps/prompts/public/page.js +541 -0
  72. package/src/apps/prompts/services/prompt-store.ts +212 -0
  73. package/src/apps/prompts/src/index.ts +9 -0
  74. package/src/apps/prompts/tsconfig.json +8 -0
  75. package/src/apps/prompts/types.ts +27 -0
  76. package/src/apps/shell/.turbo/turbo-build.log +5 -0
  77. package/src/apps/shell/.turbo/turbo-lint.log +34 -0
  78. package/src/apps/shell/.turbo/turbo-typecheck.log +5 -0
  79. package/src/apps/shell/package.json +35 -0
  80. package/src/apps/shell/public/favicon.svg +7 -0
  81. package/src/apps/shell/public/page.css +407 -0
  82. package/src/apps/shell/public/page.js +1577 -0
  83. package/src/apps/shell/src/index.ts +150 -0
  84. package/src/apps/shell/src/mcp.ts +398 -0
  85. package/src/apps/shell/src/shell-types.ts +41 -0
  86. package/src/apps/shell/tsconfig.json +8 -0
  87. package/src/apps/test/.turbo/turbo-build.log +5 -0
  88. package/src/apps/test/.turbo/turbo-lint.log +27 -0
  89. package/src/apps/test/.turbo/turbo-typecheck.log +5 -0
  90. package/src/apps/test/node_modules/.bin/tsc +21 -0
  91. package/src/apps/test/node_modules/.bin/tsserver +21 -0
  92. package/src/apps/test/node_modules/.bin/tsx +21 -0
  93. package/src/apps/test/node_modules/.bin/uuid +21 -0
  94. package/src/apps/test/package.json +35 -0
  95. package/src/apps/test/public/favicon.svg +7 -0
  96. package/src/apps/test/public/page.css +499 -0
  97. package/src/apps/test/public/page.js +417 -0
  98. package/src/apps/test/public/scenario-runner.js +450 -0
  99. package/src/apps/test/src/index.ts +9 -0
  100. package/src/apps/test/src/mcp.ts +192 -0
  101. package/src/apps/test/src/routes/trigger.ts +285 -0
  102. package/src/apps/test/src/services/scenario-broadcaster.ts +60 -0
  103. package/src/apps/test/src/services/scenario-manager.ts +361 -0
  104. package/src/apps/test/src/services/scenario-store.ts +145 -0
  105. package/src/apps/test/tsconfig.json +8 -0
  106. package/src/apps/vocabulary/.turbo/turbo-build.log +5 -0
  107. package/src/apps/vocabulary/.turbo/turbo-lint.log +25 -0
  108. package/src/apps/vocabulary/.turbo/turbo-typecheck.log +5 -0
  109. package/src/apps/vocabulary/mcp.ts +173 -0
  110. package/src/apps/vocabulary/node_modules/.bin/tsc +21 -0
  111. package/src/apps/vocabulary/node_modules/.bin/tsserver +21 -0
  112. package/src/apps/vocabulary/node_modules/.bin/tsx +21 -0
  113. package/src/apps/vocabulary/package.json +25 -0
  114. package/src/apps/vocabulary/public/page.css +247 -0
  115. package/src/apps/vocabulary/public/page.js +444 -0
  116. package/src/apps/vocabulary/services/vocabulary-store.ts +179 -0
  117. package/src/apps/vocabulary/src/index.ts +10 -0
  118. package/src/apps/vocabulary/tsconfig.json +8 -0
  119. package/src/apps/vocabulary/types.ts +22 -0
  120. package/src/apps/voice/.turbo/turbo-build.log +5 -0
  121. package/src/apps/voice/.turbo/turbo-lint.log +43 -0
  122. package/src/apps/voice/.turbo/turbo-typecheck.log +5 -0
  123. package/src/apps/voice/node_modules/.bin/openai +21 -0
  124. package/src/apps/voice/node_modules/.bin/tsc +21 -0
  125. package/src/apps/voice/node_modules/.bin/tsserver +21 -0
  126. package/src/apps/voice/node_modules/.bin/tsx +21 -0
  127. package/src/apps/voice/package.json +35 -0
  128. package/src/apps/voice/public/favicon.svg +7 -0
  129. package/src/apps/voice/public/page.css +388 -0
  130. package/src/apps/voice/public/page.js +718 -0
  131. package/src/apps/voice/src/index.ts +10 -0
  132. package/src/apps/voice/src/mcp.ts +70 -0
  133. package/src/apps/voice/src/providers/index.ts +85 -0
  134. package/src/apps/voice/src/providers/openai-compatible.ts +94 -0
  135. package/src/apps/voice/src/providers/types.ts +27 -0
  136. package/src/apps/voice/src/routes/config.ts +118 -0
  137. package/src/apps/voice/src/routes/transcribe.ts +90 -0
  138. package/src/apps/voice/src/services/config-store.ts +129 -0
  139. package/src/apps/voice/src/services/stats.ts +108 -0
  140. package/src/apps/voice/src/transcribe.ts +11 -0
  141. package/src/apps/voice/src/utils/mime.ts +16 -0
  142. package/src/apps/voice/tsconfig.json +8 -0
  143. package/src/apps/workflow/.turbo/turbo-build.log +5 -0
  144. package/src/apps/workflow/.turbo/turbo-lint.log +96 -0
  145. package/src/apps/workflow/.turbo/turbo-typecheck.log +5 -0
  146. package/src/apps/workflow/engine/executors/decision-executor.ts +87 -0
  147. package/src/apps/workflow/engine/executors/file-executor.ts +90 -0
  148. package/src/apps/workflow/engine/executors/git-executor.ts +137 -0
  149. package/src/apps/workflow/engine/executors/http-executor.ts +65 -0
  150. package/src/apps/workflow/engine/executors/index.ts +28 -0
  151. package/src/apps/workflow/engine/executors/kanban-executor.ts +154 -0
  152. package/src/apps/workflow/engine/executors/llm-executor.ts +46 -0
  153. package/src/apps/workflow/engine/executors/log-executor.ts +62 -0
  154. package/src/apps/workflow/engine/executors/loop-executor.ts +14 -0
  155. package/src/apps/workflow/engine/executors/shell-executor.ts +107 -0
  156. package/src/apps/workflow/engine/executors/sub-workflow-executor.ts +61 -0
  157. package/src/apps/workflow/engine/executors/test-executor.ts +73 -0
  158. package/src/apps/workflow/engine/executors/trigger-executor.ts +39 -0
  159. package/src/apps/workflow/engine/expression-evaluator.ts +117 -0
  160. package/src/apps/workflow/engine/graph-runner.ts +438 -0
  161. package/src/apps/workflow/engine/node-executor.ts +104 -0
  162. package/src/apps/workflow/engine/node-registry.ts +15 -0
  163. package/src/apps/workflow/engine/variable-resolver.ts +109 -0
  164. package/src/apps/workflow/mcp.ts +223 -0
  165. package/src/apps/workflow/node_modules/.bin/tsc +21 -0
  166. package/src/apps/workflow/node_modules/.bin/tsserver +21 -0
  167. package/src/apps/workflow/node_modules/.bin/tsx +21 -0
  168. package/src/apps/workflow/package.json +25 -0
  169. package/src/apps/workflow/public/editor/canvas.js +366 -0
  170. package/src/apps/workflow/public/editor/drag-manager.js +326 -0
  171. package/src/apps/workflow/public/editor/edge-renderer.js +235 -0
  172. package/src/apps/workflow/public/editor/history-manager.js +147 -0
  173. package/src/apps/workflow/public/editor/layout-engine.js +159 -0
  174. package/src/apps/workflow/public/editor/node-renderer.js +199 -0
  175. package/src/apps/workflow/public/editor/selection-manager.js +193 -0
  176. package/src/apps/workflow/public/favicon.svg +7 -0
  177. package/src/apps/workflow/public/models/node-types.js +300 -0
  178. package/src/apps/workflow/public/models/workflow-model.js +257 -0
  179. package/src/apps/workflow/public/page.css +406 -0
  180. package/src/apps/workflow/public/page.js +658 -0
  181. package/src/apps/workflow/public/panels/inspector.js +360 -0
  182. package/src/apps/workflow/public/panels/palette.js +106 -0
  183. package/src/apps/workflow/public/panels/run-view.js +275 -0
  184. package/src/apps/workflow/public/panels/toolbar.js +232 -0
  185. package/src/apps/workflow/public/panels/workflow-list.js +237 -0
  186. package/src/apps/workflow/public/state/store.js +47 -0
  187. package/src/apps/workflow/services/custom-node-loader.ts +48 -0
  188. package/src/apps/workflow/services/legacy-converter.ts +72 -0
  189. package/src/apps/workflow/services/run-manager.ts +190 -0
  190. package/src/apps/workflow/services/workflow-store.ts +424 -0
  191. package/src/apps/workflow/services/workflow-validator.test.ts +103 -0
  192. package/src/apps/workflow/services/workflow-validator.ts +98 -0
  193. package/src/apps/workflow/src/index.ts +10 -0
  194. package/src/apps/workflow/templates/ci-pipeline.json +18 -0
  195. package/src/apps/workflow/templates/code-review.json +22 -0
  196. package/src/apps/workflow/templates/kanban-testing.json +24 -0
  197. package/src/apps/workflow/tsconfig.json +8 -0
  198. package/src/apps/workflow/types.ts +268 -0
  199. package/src/packages/auth-middleware.ts +14 -0
  200. package/src/packages/design-tokens/.turbo/turbo-build.log +10 -0
  201. package/src/packages/design-tokens/STYLEGUIDE.md +414 -0
  202. package/src/packages/design-tokens/build.js +413 -0
  203. package/src/packages/design-tokens/demo/index.html +1367 -0
  204. package/src/packages/design-tokens/demo/proposition-a.html +717 -0
  205. package/src/packages/design-tokens/demo/proposition-b.html +1239 -0
  206. package/src/packages/design-tokens/demo/proposition-c.html +1049 -0
  207. package/src/packages/design-tokens/dist/tailwind-preset.js +115 -0
  208. package/src/packages/design-tokens/dist/tokens.css +345 -0
  209. package/src/packages/design-tokens/dist/tokens.d.ts +229 -0
  210. package/src/packages/design-tokens/dist/tokens.js +386 -0
  211. package/src/packages/design-tokens/package.json +25 -0
  212. package/src/packages/design-tokens/tokens.json +228 -0
  213. package/src/packages/devtools-middleware.ts +22 -0
  214. package/src/packages/eslint-config/index.js +63 -0
  215. package/src/packages/eslint-config/node_modules/.bin/eslint +21 -0
  216. package/src/packages/eslint-config/package.json +18 -0
  217. package/src/packages/json-file-store.ts +232 -0
  218. package/src/packages/mcp-utils/.turbo/turbo-build.log +5 -0
  219. package/src/packages/mcp-utils/dist/index.d.ts +33 -0
  220. package/src/packages/mcp-utils/dist/index.d.ts.map +1 -0
  221. package/src/packages/mcp-utils/dist/index.js +126 -0
  222. package/src/packages/mcp-utils/dist/index.js.map +1 -0
  223. package/src/packages/mcp-utils/node_modules/.bin/tsc +21 -0
  224. package/src/packages/mcp-utils/node_modules/.bin/tsserver +21 -0
  225. package/src/packages/mcp-utils/package.json +32 -0
  226. package/src/packages/mcp-utils/src/index.ts +171 -0
  227. package/src/packages/mcp-utils/tsconfig.json +9 -0
  228. package/src/packages/paths.ts +18 -0
  229. package/src/packages/project-context/index.js +55 -0
  230. package/src/packages/project-context/package.json +13 -0
  231. package/src/packages/project-store.ts +127 -0
  232. package/src/packages/server-sniffer.ts +132 -0
  233. package/src/packages/shared-assets/favicon.svg +7 -0
  234. package/src/packages/shared-assets/keymap-registry.js +512 -0
  235. package/src/packages/shared-assets/logo.svg +6 -0
  236. package/src/packages/shared-assets/package.json +11 -0
  237. package/src/packages/shared-assets/ui-utils.js +48 -0
  238. package/src/packages/shared-assets/voice-widget.d.ts +37 -0
  239. package/src/packages/shared-assets/voice-widget.js +695 -0
  240. package/src/packages/shared-types/.turbo/turbo-build.log +5 -0
  241. package/src/packages/shared-types/dist/index.d.ts +39 -0
  242. package/src/packages/shared-types/dist/index.d.ts.map +1 -0
  243. package/src/packages/shared-types/node_modules/.bin/tsc +21 -0
  244. package/src/packages/shared-types/node_modules/.bin/tsserver +21 -0
  245. package/src/packages/shared-types/package.json +25 -0
  246. package/src/packages/shared-types/src/index.ts +41 -0
  247. package/src/packages/shared-types/tsconfig.json +11 -0
  248. package/src/packages/tsconfig/base.json +15 -0
  249. package/src/packages/tsconfig/next.json +14 -0
  250. package/src/packages/tsconfig/node.json +11 -0
  251. package/src/packages/tsconfig/package.json +10 -0
  252. package/turbo.json +25 -0
@@ -0,0 +1,658 @@
1
+ // ── Workflow App — Page Module ────────────────────────────────────────
2
+ // Visual workflow builder with drag-and-drop node editor.
3
+ // ES module: mount(container, ctx), unmount(container), onProjectChange(project)
4
+
5
+ import { escapeHtml } from '/shared-assets/ui-utils.js';
6
+
7
+ let _container = null;
8
+ let _builderModules = null;
9
+ let _builderMounted = false;
10
+ let _keydownHandler = null;
11
+ let _renderUnsubs = [];
12
+
13
+ // ── HTML ────────────────────────────────────────────────────────────────
14
+
15
+ const BODY_HTML = `
16
+ <div class="wf-builder-layout" id="wf-builder-layout">
17
+ <div class="wb-toolbar" id="wb-toolbar"></div>
18
+ <div class="wb-main-layout">
19
+ <div class="wb-canvas-container" id="wb-canvas"></div>
20
+ <div class="wb-inspector" id="wb-inspector"></div>
21
+ </div>
22
+ <div id="wb-run-view"></div>
23
+ </div>
24
+ <div class="modal-overlay hidden" id="wf-modal" role="dialog" aria-modal="true">
25
+ <div class="modal">
26
+ <div class="modal-header">
27
+ <h2 class="wf-modal-title"></h2>
28
+ <div class="modal-desc wf-modal-body"></div>
29
+ </div>
30
+ <div class="modal-actions wf-modal-actions"></div>
31
+ </div>
32
+ </div>
33
+ <div class="wf-toast-container" id="wf-toast-container"></div>
34
+ `;
35
+
36
+ // ── Helpers ──────────────────────────────────────────────────────────
37
+
38
+ function $(selector) {
39
+ return _container?.querySelector(selector) ?? null;
40
+ }
41
+
42
+ const esc = escapeHtml;
43
+
44
+ // ── Modal / Toast helpers ───────────────────────────────────────────
45
+
46
+ function showModal(title, bodyHtml, buttons) {
47
+ return new Promise(resolve => {
48
+ const overlay = _container?.querySelector('.modal-overlay');
49
+ if (!overlay) { resolve(null); return; }
50
+
51
+ overlay.querySelector('.wf-modal-title').textContent = title;
52
+ overlay.querySelector('.wf-modal-body').innerHTML = bodyHtml;
53
+ const actionsEl = overlay.querySelector('.wf-modal-actions');
54
+ actionsEl.innerHTML = buttons.map(b =>
55
+ `<button class="${b.cls}" data-value="${b.value}">${b.label}</button>`
56
+ ).join('');
57
+
58
+ overlay.classList.remove('hidden');
59
+
60
+ const ac = new AbortController();
61
+ const close = (value) => {
62
+ overlay.classList.add('hidden');
63
+ ac.abort();
64
+ resolve(value);
65
+ };
66
+
67
+ overlay.addEventListener('click', (e) => {
68
+ if (e.target === overlay) close(null);
69
+ }, { signal: ac.signal });
70
+
71
+ actionsEl.querySelectorAll('button').forEach(btn => {
72
+ btn.addEventListener('click', () => close(btn.dataset.value), { signal: ac.signal });
73
+ });
74
+
75
+ document.addEventListener('keydown', (e) => {
76
+ if (e.key === 'Escape') close(null);
77
+ }, { signal: ac.signal });
78
+ });
79
+ }
80
+
81
+ async function wfConfirm(title, message) {
82
+ const result = await showModal(title, `<p>${esc(message)}</p>`, [
83
+ { label: 'Cancel', cls: 'btn btn-secondary', value: 'cancel' },
84
+ { label: 'Delete', cls: 'btn btn-danger', value: 'confirm' },
85
+ ]);
86
+ return result === 'confirm';
87
+ }
88
+
89
+ async function wfAlert(title, bodyHtml) {
90
+ await showModal(title, bodyHtml, [
91
+ { label: 'OK', cls: 'btn btn-primary', value: 'ok' },
92
+ ]);
93
+ }
94
+
95
+ function wfToast(message, type = 'info') {
96
+ const container = _container?.querySelector('.wf-toast-container');
97
+ if (!container) return;
98
+ const toast = document.createElement('div');
99
+ toast.className = `wf-toast wf-toast-${type}`;
100
+ toast.textContent = message;
101
+ container.appendChild(toast);
102
+ setTimeout(() => {
103
+ toast.style.opacity = '0';
104
+ setTimeout(() => toast.remove(), 300);
105
+ }, 3000);
106
+ }
107
+
108
+ // ── Builder API ─────────────────────────────────────────────────────
109
+
110
+ const API = '/api/workflow';
111
+
112
+ async function loadBuilderModules() {
113
+ if (_builderModules) return _builderModules;
114
+
115
+ const [
116
+ { Inspector },
117
+ { Toolbar },
118
+ { WorkflowList },
119
+ { RunView },
120
+ { store },
121
+ { WorkflowModel },
122
+ { Canvas },
123
+ { NodeRenderer },
124
+ { EdgeRenderer },
125
+ { DragManager },
126
+ { HistoryManager },
127
+ { NODE_TYPES },
128
+ ] = await Promise.all([
129
+ import('./panels/inspector.js'),
130
+ import('./panels/toolbar.js'),
131
+ import('./panels/workflow-list.js'),
132
+ import('./panels/run-view.js'),
133
+ import('./state/store.js'),
134
+ import('./models/workflow-model.js'),
135
+ import('./editor/canvas.js'),
136
+ import('./editor/node-renderer.js'),
137
+ import('./editor/edge-renderer.js'),
138
+ import('./editor/drag-manager.js'),
139
+ import('./editor/history-manager.js'),
140
+ import('./models/node-types.js'),
141
+ ]);
142
+
143
+ _builderModules = {
144
+ Inspector, Toolbar, WorkflowList, RunView,
145
+ store, WorkflowModel,
146
+ Canvas, NodeRenderer, EdgeRenderer, DragManager, HistoryManager, NODE_TYPES,
147
+ };
148
+ return _builderModules;
149
+ }
150
+
151
+ async function mountBuilder() {
152
+ if (_builderMounted) return;
153
+
154
+ await loadBuilderModules();
155
+ _builderMounted = true;
156
+
157
+ showBuilderList();
158
+ }
159
+
160
+ function showBuilderList() {
161
+ if (!_builderModules || !_container) return;
162
+ const { WorkflowList, Toolbar, Inspector, RunView, Canvas, DragManager, HistoryManager } = _builderModules;
163
+
164
+ // Clean up render subscriptions
165
+ for (const unsub of _renderUnsubs) unsub();
166
+ _renderUnsubs = [];
167
+
168
+ Toolbar.unmount();
169
+ Inspector.unmount();
170
+ RunView.unmount();
171
+ Canvas.unmount();
172
+ DragManager.destroy();
173
+ HistoryManager.destroy();
174
+
175
+ const builderLayout = $('#wf-builder-layout');
176
+ if (!builderLayout) return;
177
+
178
+ builderLayout.innerHTML = `<div id="wb-workflow-list" style="display:flex;flex-direction:column;flex:1;overflow:hidden;"></div>`;
179
+
180
+ const listContainer = builderLayout.querySelector('#wb-workflow-list');
181
+ WorkflowList.mount(listContainer);
182
+ WorkflowList.setConfirm(wfConfirm);
183
+ WorkflowList.setToast(wfToast);
184
+
185
+ WorkflowList.onSelect((wf) => openWorkflowInEditor(wf));
186
+ WorkflowList.onNew(() => openWorkflowInEditor(null));
187
+ }
188
+
189
+ function addNodeAtPosition(type, x, y) {
190
+ if (!_builderModules) return;
191
+ const { store, WorkflowModel, NODE_TYPES } = _builderModules;
192
+ const wf = store.get('workflow');
193
+ const typeDef = NODE_TYPES[type];
194
+ const label = typeDef?.label ?? type;
195
+ WorkflowModel.addNode(type, label, { x, y });
196
+ }
197
+
198
+ async function openWorkflowInEditor(wf) {
199
+ if (!_builderModules || !_container) return;
200
+ const {
201
+ Inspector, Toolbar, WorkflowList, RunView,
202
+ store, WorkflowModel,
203
+ Canvas, NodeRenderer, EdgeRenderer, DragManager, HistoryManager, NODE_TYPES,
204
+ } = _builderModules;
205
+
206
+ // Clean up render subscriptions from previous editor session
207
+ for (const unsub of _renderUnsubs) unsub();
208
+ _renderUnsubs = [];
209
+
210
+ WorkflowList.unmount();
211
+ Canvas.unmount();
212
+ DragManager.destroy();
213
+ HistoryManager.destroy();
214
+
215
+ if (wf) {
216
+ try {
217
+ const res = await fetch(`${API}/workflows/${wf.id}`);
218
+ if (res.ok) {
219
+ WorkflowModel.load(await res.json());
220
+ } else {
221
+ WorkflowModel.load(wf);
222
+ }
223
+ } catch {
224
+ WorkflowModel.load(wf);
225
+ }
226
+ } else {
227
+ WorkflowModel.load({
228
+ name: 'Untitled Workflow',
229
+ description: '',
230
+ nodes: [],
231
+ edges: [],
232
+ });
233
+ }
234
+
235
+ const builderLayout = $('#wf-builder-layout');
236
+ if (!builderLayout) return;
237
+
238
+ builderLayout.innerHTML = `
239
+ <div class="wb-toolbar" id="wb-toolbar"></div>
240
+ <div class="wb-main-layout">
241
+ <div class="wb-canvas-container" id="wb-canvas"></div>
242
+ <div class="wb-inspector" id="wb-inspector"></div>
243
+ </div>
244
+ <div id="wb-run-view"></div>
245
+ `;
246
+
247
+ const canvasContainerEl = builderLayout.querySelector('#wb-canvas');
248
+
249
+ Toolbar.mount(builderLayout.querySelector('#wb-toolbar'));
250
+ Inspector.mount(builderLayout.querySelector('#wb-inspector'));
251
+
252
+ // Init undo/redo history and connect to toolbar
253
+ HistoryManager.init();
254
+ Toolbar.setHistoryManager(HistoryManager);
255
+
256
+ Toolbar.setHandlers({
257
+ onBack: () => showBuilderList(),
258
+ onSave: () => saveWorkflow(),
259
+ onAddStep: (nodeType) => {
260
+ // Add node of selected type at center of visible canvas
261
+ const canvasRoot = Canvas.getRootElement();
262
+ if (canvasRoot) {
263
+ const rect = canvasRoot.getBoundingClientRect();
264
+ const center = Canvas.screenToWorld(
265
+ rect.left + rect.width / 2,
266
+ rect.top + rect.height / 2
267
+ );
268
+ addNodeAtPosition(nodeType || 'step', center.x, center.y);
269
+ }
270
+ },
271
+ onExport: () => exportWorkflow(),
272
+ });
273
+
274
+ // Mount Canvas into the canvas container
275
+ Canvas.mount(canvasContainerEl);
276
+
277
+ // Render function — creates/updates nodes and edges on canvas
278
+ function renderGraph() {
279
+ const wfData = store.get('workflow');
280
+ if (!wfData) return;
281
+
282
+ const world = Canvas.getWorldElement();
283
+ const svgWorld = Canvas.getSvgElement();
284
+ if (!world || !svgWorld) return;
285
+
286
+ // Reconcile nodes: update existing, add new, remove old
287
+ const existingNodeEls = world.querySelectorAll('.wfb-node');
288
+ const existingMap = new Map();
289
+ for (const el of existingNodeEls) existingMap.set(el.dataset.nodeId, el);
290
+
291
+ const currentIds = new Set(wfData.nodes.map(n => n.id));
292
+
293
+ // Remove nodes that no longer exist
294
+ for (const [id, el] of existingMap) {
295
+ if (!currentIds.has(id)) el.remove();
296
+ }
297
+
298
+ // Add or update nodes
299
+ for (const node of wfData.nodes) {
300
+ const existing = existingMap.get(node.id);
301
+ if (existing) {
302
+ NodeRenderer.updateNodeElement(existing, node);
303
+ } else {
304
+ const el = NodeRenderer.createNodeElement(node);
305
+ world.appendChild(el);
306
+ }
307
+ }
308
+
309
+ // Update selection visuals
310
+ const selectedIds = store.get('selectedNodeIds') ?? new Set();
311
+ for (const node of wfData.nodes) {
312
+ const el = world.querySelector(`[data-node-id="${node.id}"]`);
313
+ if (el) NodeRenderer.setSelected(el, selectedIds.has(node.id));
314
+ }
315
+
316
+ // Re-render all edges (simpler than reconciling)
317
+ const edgeEls = svgWorld.querySelectorAll('.wfb-edge');
318
+ for (const e of edgeEls) e.remove();
319
+
320
+ for (const edge of wfData.edges) {
321
+ const srcNode = wfData.nodes.find(n => n.id === edge.source);
322
+ const tgtNode = wfData.nodes.find(n => n.id === edge.target);
323
+ if (srcNode && tgtNode) {
324
+ const srcType = NODE_TYPES[srcNode.type];
325
+ const tgtType = NODE_TYPES[tgtNode.type];
326
+ const el = EdgeRenderer.createEdgePath(edge, srcNode, tgtNode, srcType, tgtType);
327
+ svgWorld.appendChild(el);
328
+ }
329
+ }
330
+
331
+ // Update edge selection
332
+ const selectedEdgeIds = store.get('selectedEdgeIds') ?? new Set();
333
+ for (const edge of wfData.edges) {
334
+ const el = svgWorld.querySelector(`[data-edge-id="${edge.id}"]`);
335
+ if (el) EdgeRenderer.setSelected(el, selectedEdgeIds.has(edge.id));
336
+ }
337
+ }
338
+
339
+ // Subscribe to store changes for re-rendering
340
+ const unsubWorkflow = store.on('workflow', renderGraph);
341
+ const unsubSelection = store.on('selectedNodeIds', renderGraph);
342
+ const unsubEdgeSelection = store.on('selectedEdgeIds', renderGraph);
343
+ _renderUnsubs.push(unsubWorkflow, unsubSelection, unsubEdgeSelection);
344
+
345
+ // Initial render
346
+ renderGraph();
347
+
348
+ // Init drag manager for node moves and edge creation
349
+ DragManager.init(Canvas, Canvas.getWorldElement(), Canvas.getSvgElement());
350
+
351
+ // Double-click on canvas to create step
352
+ const canvasRoot = Canvas.getRootElement();
353
+ if (canvasRoot) {
354
+ canvasRoot.addEventListener('dblclick', (e) => {
355
+ // Don't create step if double-clicking on an existing node
356
+ if (e.target.closest('.wfb-node')) return;
357
+ const pos = Canvas.screenToWorld(e.clientX, e.clientY);
358
+ addNodeAtPosition('step', pos.x, pos.y);
359
+ });
360
+
361
+ // Click on canvas background to deselect, or on edge to select it
362
+ canvasRoot.addEventListener('pointerdown', (e) => {
363
+ if (e.button !== 0) return;
364
+ // Edge click — check SVG hit areas
365
+ const hitPath = e.target.closest?.('.wfb-edge-hit');
366
+ if (hitPath) {
367
+ const edgeGroup = hitPath.closest('.wfb-edge');
368
+ const edgeId = edgeGroup?.dataset?.edgeId;
369
+ if (edgeId) {
370
+ if (e.shiftKey) {
371
+ const sel = new Set(store.get('selectedEdgeIds') ?? []);
372
+ if (sel.has(edgeId)) sel.delete(edgeId); else sel.add(edgeId);
373
+ store.set('selectedEdgeIds', sel);
374
+ } else {
375
+ store.set('selectedEdgeIds', new Set([edgeId]));
376
+ store.set('selectedNodeIds', new Set());
377
+ }
378
+ return;
379
+ }
380
+ }
381
+ if (!e.target.closest('.wfb-node') && !e.target.closest('.wfb-port')) {
382
+ store.set('selectedNodeIds', new Set());
383
+ store.set('selectedEdgeIds', new Set());
384
+ }
385
+ });
386
+ }
387
+ }
388
+
389
+ async function saveWorkflow() {
390
+ if (!_builderModules) return;
391
+ const { WorkflowModel, Toolbar } = _builderModules;
392
+
393
+ const data = WorkflowModel.save();
394
+ if (!data) return;
395
+
396
+ try {
397
+ let res;
398
+ if (data.id) {
399
+ res = await fetch(`${API}/workflows/${data.id}`, {
400
+ method: 'PUT',
401
+ headers: { 'Content-Type': 'application/json' },
402
+ body: JSON.stringify(data),
403
+ });
404
+ if (res.status === 404) {
405
+ res = await fetch(`${API}/workflows`, {
406
+ method: 'POST',
407
+ headers: { 'Content-Type': 'application/json' },
408
+ body: JSON.stringify(data),
409
+ });
410
+ }
411
+ } else {
412
+ res = await fetch(`${API}/workflows`, {
413
+ method: 'POST',
414
+ headers: { 'Content-Type': 'application/json' },
415
+ body: JSON.stringify(data),
416
+ });
417
+ }
418
+
419
+ if (res.ok) {
420
+ const saved = await res.json();
421
+ WorkflowModel.load(saved);
422
+ Toolbar.setDirty(false);
423
+ wfToast('Workflow saved', 'success');
424
+ } else {
425
+ wfToast('Save failed', 'error');
426
+ }
427
+ } catch (e) {
428
+ console.error('Save failed:', e);
429
+ wfToast('Save failed: ' + e.message, 'error');
430
+ }
431
+ }
432
+
433
+ async function exportWorkflow() {
434
+ if (!_builderModules) return;
435
+ const { WorkflowModel } = _builderModules;
436
+
437
+ const data = WorkflowModel.save();
438
+ if (!data || !data.nodes?.length) {
439
+ wfToast('No steps to export', 'error');
440
+ return;
441
+ }
442
+
443
+ // Build ordered step list by following edges
444
+ const nodeMap = new Map(data.nodes.map(n => [n.id, n]));
445
+ const outEdges = new Map();
446
+ for (const e of data.edges) {
447
+ outEdges.set(e.source, e);
448
+ }
449
+
450
+ // Find start nodes (no incoming edges)
451
+ const hasIncoming = new Set(data.edges.map(e => e.target));
452
+ const startNodes = data.nodes.filter(n => !hasIncoming.has(n.id));
453
+
454
+ let md = `# ${data.name}\n\n`;
455
+ if (data.description) md += `${data.description}\n\n`;
456
+ md += `---\n\n`;
457
+
458
+ // Walk graph from each start node
459
+ let stepNum = 1;
460
+ const visited = new Set();
461
+
462
+ function walkNode(nodeId) {
463
+ if (visited.has(nodeId)) return;
464
+ visited.add(nodeId);
465
+
466
+ const node = nodeMap.get(nodeId);
467
+ if (!node) return;
468
+
469
+ md += `## Step ${stepNum}: ${node.label}\n\n`;
470
+ stepNum++;
471
+
472
+ const instructions = node.config?.instructions;
473
+ const file = node.config?.instructionFile;
474
+
475
+ if (instructions) {
476
+ md += `${instructions}\n\n`;
477
+ }
478
+ if (file) {
479
+ md += `> Instructions file: \`${file}\`\n\n`;
480
+ }
481
+ if (!instructions && !file) {
482
+ md += `*(No instructions defined)*\n\n`;
483
+ }
484
+
485
+ // Follow outgoing edge
486
+ const edge = outEdges.get(nodeId);
487
+ if (edge) walkNode(edge.target);
488
+ }
489
+
490
+ if (startNodes.length === 0 && data.nodes.length > 0) {
491
+ // No clear start — just list all nodes
492
+ for (const node of data.nodes) walkNode(node.id);
493
+ } else {
494
+ for (const node of startNodes) walkNode(node.id);
495
+ }
496
+
497
+ // Any unvisited nodes
498
+ for (const node of data.nodes) {
499
+ if (!visited.has(node.id)) walkNode(node.id);
500
+ }
501
+
502
+ // Show in a modal with copy button
503
+ const escaped = md.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
504
+ const result = await showModal('Export Workflow',
505
+ `<pre style="max-height:300px;overflow:auto;padding:var(--df-space-3);background:var(--df-color-bg-raised);border:1px solid var(--df-color-border-default);border-radius:6px;font-size:var(--df-font-size-xs);white-space:pre-wrap;word-wrap:break-word;">${escaped}</pre>`,
506
+ [
507
+ { label: 'Copy', cls: 'btn btn-primary', value: 'copy' },
508
+ { label: 'Close', cls: 'btn btn-secondary', value: 'close' },
509
+ ]
510
+ );
511
+
512
+ if (result === 'copy') {
513
+ try {
514
+ await navigator.clipboard.writeText(md);
515
+ wfToast('Copied to clipboard', 'success');
516
+ } catch {
517
+ wfToast('Copy failed', 'error');
518
+ }
519
+ }
520
+ }
521
+
522
+
523
+
524
+ function unmountBuilder() {
525
+ if (!_builderMounted || !_builderModules) return;
526
+ const { Inspector, Toolbar, WorkflowList, RunView, Canvas, DragManager, HistoryManager } = _builderModules;
527
+
528
+ // Clean up render subscriptions
529
+ for (const unsub of _renderUnsubs) unsub();
530
+ _renderUnsubs = [];
531
+
532
+ Toolbar.unmount();
533
+ Inspector.unmount();
534
+ WorkflowList.unmount();
535
+ RunView.unmount();
536
+ Canvas.unmount();
537
+ DragManager.destroy();
538
+ HistoryManager.destroy();
539
+
540
+ _builderMounted = false;
541
+ }
542
+
543
+ // ── Keyboard shortcuts ──────────────────────────────────────────────
544
+
545
+ function setupKeyboardShortcuts() {
546
+ _keydownHandler = async (e) => {
547
+ if (!_builderModules) return;
548
+
549
+ const { store, WorkflowModel } = _builderModules;
550
+ const ctrl = e.ctrlKey || e.metaKey;
551
+
552
+ // Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z — handled by HistoryManager
553
+
554
+ if (ctrl && e.key === 's') {
555
+ e.preventDefault();
556
+ await saveWorkflow();
557
+ return;
558
+ }
559
+
560
+ if (e.key === 'Delete' || e.key === 'Backspace') {
561
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return;
562
+ e.preventDefault();
563
+ const selectedNodes = store.get('selectedNodeIds');
564
+ const selectedEdges = store.get('selectedEdgeIds');
565
+ if (selectedNodes?.size) {
566
+ for (const id of selectedNodes) WorkflowModel.removeNode(id);
567
+ }
568
+ if (selectedEdges?.size) {
569
+ for (const id of selectedEdges) WorkflowModel.removeEdge(id);
570
+ }
571
+ return;
572
+ }
573
+
574
+ if (ctrl && e.key === 'a') {
575
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
576
+ e.preventDefault();
577
+ const wf = store.get('workflow');
578
+ if (wf?.nodes) {
579
+ store.set('selectedNodeIds', new Set(wf.nodes.map(n => n.id)));
580
+ }
581
+ return;
582
+ }
583
+
584
+ if (ctrl && e.key === 'Enter') {
585
+ e.preventDefault();
586
+ await runWorkflow();
587
+ return;
588
+ }
589
+
590
+ if (ctrl && e.key === '0') {
591
+ e.preventDefault();
592
+ store.set('zoom', 1);
593
+ store.set('panX', 0);
594
+ store.set('panY', 0);
595
+ return;
596
+ }
597
+
598
+ if (e.key === 'Escape') {
599
+ store.set('selectedNodeIds', new Set());
600
+ store.set('selectedEdgeIds', new Set());
601
+ return;
602
+ }
603
+
604
+ if (e.key === '+' || e.key === '=') {
605
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
606
+ e.preventDefault();
607
+ store.set('zoom', Math.min(3, (store.get('zoom') ?? 1) + 0.1));
608
+ return;
609
+ }
610
+
611
+ if (e.key === '-') {
612
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
613
+ e.preventDefault();
614
+ store.set('zoom', Math.max(0.25, (store.get('zoom') ?? 1) - 0.1));
615
+ return;
616
+ }
617
+ };
618
+
619
+ document.addEventListener('keydown', _keydownHandler);
620
+ }
621
+
622
+ function teardownKeyboardShortcuts() {
623
+ if (_keydownHandler) {
624
+ document.removeEventListener('keydown', _keydownHandler);
625
+ _keydownHandler = null;
626
+ }
627
+ }
628
+
629
+ // ── Exports ──────────────────────────────────────────────────────────
630
+
631
+ export function mount(container, ctx) {
632
+ _container = container;
633
+
634
+ container.classList.add('page-workflow');
635
+ container.innerHTML = BODY_HTML;
636
+
637
+ setupKeyboardShortcuts();
638
+ mountBuilder();
639
+ }
640
+
641
+ export function unmount(container) {
642
+ unmountBuilder();
643
+ teardownKeyboardShortcuts();
644
+
645
+ container.classList.remove('page-workflow');
646
+ container.innerHTML = '';
647
+
648
+ _container = null;
649
+ _builderMounted = false;
650
+ }
651
+
652
+ export function onProjectChange(project) {
653
+ // Refresh the builder list if it's showing
654
+ if (_builderModules) {
655
+ const { WorkflowList } = _builderModules;
656
+ WorkflowList.refresh?.();
657
+ }
658
+ }