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,417 @@
1
+ // ── Test App — Page Module ────────────────────────────────────────────
2
+ // ES module that exports mount(container, ctx), unmount(container),
3
+ // and onProjectChange(project).
4
+
5
+ import { timeAgo, formatDuration } from '/shared-assets/ui-utils.js';
6
+
7
+ let activeProjectPath = null;
8
+ let _refreshTimer = null;
9
+ let _container = null;
10
+ let _visibilityHandler = null;
11
+
12
+ // ── HTML (body content, no script tags) ──────────────────────────────
13
+
14
+ const BODY_HTML = `
15
+ <header>
16
+ <div class="brand">
17
+ Test
18
+ </div>
19
+ <div class="header-meta">
20
+ <span id="pending-badge" class="badge badge-pending" role="status" aria-live="polite" style="display:none">
21
+ <span id="pending-count">0</span> pending
22
+ </span>
23
+ <span class="badge badge-idle" id="status-badge">ready</span>
24
+ <span>
25
+ auto-refresh 5s
26
+ <span class="refresh-indicator" id="refresh-dot"></span>
27
+ </span>
28
+ </div>
29
+ </header>
30
+
31
+ <main>
32
+ <div class="saved-section">
33
+ <div class="section-title">Saved Tests</div>
34
+ <div id="saved-list">
35
+ <div class="saved-empty" id="saved-empty">No saved tests yet.</div>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="results-section">
40
+ <div class="results-header">
41
+ <div class="section-title" style="margin-bottom:0">Recent Runs</div>
42
+ <button class="btn btn-clear" id="clear-results-btn" style="display:none">Clear</button>
43
+ </div>
44
+ <div id="results-list">
45
+ <div class="results-empty" id="results-empty">No recent runs.</div>
46
+ </div>
47
+ </div>
48
+
49
+ <div class="section-title">Pending Scenarios</div>
50
+ <div id="pending-list">
51
+ <div class="empty" id="empty-state">
52
+ No pending scenarios.
53
+ </div>
54
+ </div>
55
+
56
+ <details class="info-card-details">
57
+ <summary>Setup &amp; Usage</summary>
58
+ <div class="info-card">
59
+ Automated browser testing that executes real user interactions — clicks, typing, navigation,
60
+ and assertions — against a live app. Submit test scenarios via the
61
+ <code>POST /api/trigger/scenarios</code> REST endpoint or the
62
+ <code>test_run_scenario</code> MCP tool, and watch them run in real time.<br/><br/>
63
+ <strong>Setup:</strong> For external apps, add one script tag to enable both automation and log capture:<br/>
64
+ <code>&lt;script src="http://localhost:7000/devtools.js?target=/path/to/app"&gt;&lt;/script&gt;</code><br/>
65
+ The <code>?target</code> param is required — it sets the app directory for log capture and test scenarios.<br/><br/>
66
+ DevGlide monorepo apps use the shared middleware and need no manual setup.<br/>
67
+ Use simple app names as targets (e.g. <code>"kanban"</code>, <code>"dashboard"</code>) — absolute paths also work.<br/><br/>
68
+ <strong>Tip:</strong> Prefer clicking links and buttons over using <code>navigate</code>.
69
+ The <code>navigate</code> command should only be used when no interactive element is available
70
+ (e.g. initial deep-link before a test starts). Overusing it bypasses the UI layer and can
71
+ cause false positives.
72
+ </div>
73
+ </details>
74
+ </main>
75
+ `;
76
+
77
+ // ── Helpers ──────────────────────────────────────────────────────────
78
+
79
+ function flash() {
80
+ if (!_container) return;
81
+ const dot = _container.querySelector('#refresh-dot');
82
+ if (!dot) return;
83
+ dot.classList.add('flash');
84
+ setTimeout(() => dot.classList.remove('flash'), 300);
85
+ }
86
+
87
+ // ── Data fetching ───────────────────────────────────────────────────
88
+
89
+ async function refresh() {
90
+ if (!_container) return;
91
+ try {
92
+ const params = activeProjectPath
93
+ ? '?projectPath=' + encodeURIComponent(activeProjectPath)
94
+ : '';
95
+ const res = await fetch('/api/test/trigger/status' + params);
96
+ if (!res.ok) return;
97
+ const { pendingScenarios } = await res.json();
98
+
99
+ const pendingBadge = _container.querySelector('#pending-badge');
100
+ if (pendingScenarios > 0) {
101
+ _container.querySelector('#pending-count').textContent = pendingScenarios;
102
+ pendingBadge.style.display = '';
103
+ } else {
104
+ pendingBadge.style.display = 'none';
105
+ }
106
+
107
+ flash();
108
+ refreshSaved();
109
+ refreshResults();
110
+ } catch (e) {
111
+ // ignore network errors
112
+ }
113
+ }
114
+
115
+ async function refreshSaved() {
116
+ if (!_container) return;
117
+ try {
118
+ const params = activeProjectPath
119
+ ? '?projectPath=' + encodeURIComponent(activeProjectPath)
120
+ : '';
121
+ const res = await fetch('/api/test/trigger/scenarios/saved' + params);
122
+ if (!res.ok) return;
123
+ const scenarios = await res.json();
124
+
125
+ const list = _container.querySelector('#saved-list');
126
+ const emptyEl = _container.querySelector('#saved-empty');
127
+
128
+ if (!scenarios.length) {
129
+ list.innerHTML = '';
130
+ list.appendChild(emptyEl);
131
+ emptyEl.style.display = '';
132
+ return;
133
+ }
134
+
135
+ emptyEl.style.display = 'none';
136
+ const fragment = document.createDocumentFragment();
137
+
138
+ for (const s of scenarios) {
139
+ const card = document.createElement('div');
140
+ card.className = 'saved-card';
141
+ card.dataset.id = s.id;
142
+
143
+ const info = document.createElement('div');
144
+ info.className = 'saved-info';
145
+
146
+ const name = document.createElement('div');
147
+ name.className = 'saved-name';
148
+ name.textContent = s.name;
149
+
150
+ const details = document.createElement('div');
151
+ details.className = 'saved-details';
152
+ const stepCount = s.steps ? s.steps.length : 0;
153
+ const runCount = s.runCount || 0;
154
+ const lastRun = s.lastRunAt ? timeAgo(s.lastRunAt) : 'never';
155
+ details.textContent = (s.target || 'no target')
156
+ + ' \u00b7 ' + stepCount + ' step' + (stepCount !== 1 ? 's' : '')
157
+ + ' \u00b7 ' + runCount + ' run' + (runCount !== 1 ? 's' : '')
158
+ + ' \u00b7 last run ' + lastRun;
159
+
160
+ info.appendChild(name);
161
+ info.appendChild(details);
162
+
163
+ const actions = document.createElement('div');
164
+ actions.className = 'saved-actions';
165
+
166
+ const runBtn = document.createElement('button');
167
+ runBtn.className = 'btn btn-run';
168
+ runBtn.textContent = 'Run';
169
+ runBtn.addEventListener('click', () => runSaved(s.id));
170
+
171
+ const delBtn = document.createElement('button');
172
+ delBtn.className = 'btn btn-delete';
173
+ delBtn.textContent = 'Delete';
174
+ delBtn.addEventListener('click', () => deleteSaved(s.id));
175
+
176
+ actions.appendChild(runBtn);
177
+ actions.appendChild(delBtn);
178
+
179
+ card.appendChild(info);
180
+ card.appendChild(actions);
181
+ fragment.appendChild(card);
182
+ }
183
+
184
+ // Preserve the hidden empty element
185
+ list.innerHTML = '';
186
+ emptyEl.style.display = 'none';
187
+ list.appendChild(emptyEl);
188
+ list.appendChild(fragment);
189
+ } catch (e) {
190
+ // ignore network errors
191
+ }
192
+ }
193
+
194
+ async function runSaved(id) {
195
+ if (!_container) return;
196
+ try {
197
+ await fetch('/api/test/trigger/scenarios/saved/' + encodeURIComponent(id) + '/run', {
198
+ method: 'POST'
199
+ });
200
+ const card = _container.querySelector('.saved-card[data-id="' + id + '"]');
201
+ if (card) {
202
+ card.classList.add('just-triggered');
203
+ setTimeout(() => card.classList.remove('just-triggered'), 1200);
204
+ }
205
+ refresh();
206
+ refreshSaved();
207
+ } catch (e) {
208
+ // ignore
209
+ }
210
+ }
211
+
212
+ async function deleteSaved(id) {
213
+ try {
214
+ await fetch('/api/test/trigger/scenarios/saved/' + encodeURIComponent(id), {
215
+ method: 'DELETE'
216
+ });
217
+ refreshSaved();
218
+ } catch (e) {
219
+ // ignore
220
+ }
221
+ }
222
+
223
+ // ── Results history ─────────────────────────────────────────────────
224
+
225
+ let _resultsCleared = false;
226
+ let _expandedErrors = new Set();
227
+
228
+ function clearResults() {
229
+ _resultsCleared = true;
230
+ _expandedErrors.clear();
231
+ renderResults([]);
232
+ }
233
+
234
+ function renderResults(results) {
235
+ if (!_container) return;
236
+ const list = _container.querySelector('#results-list');
237
+ const emptyEl = _container.querySelector('#results-empty');
238
+ const clearBtn = _container.querySelector('#clear-results-btn');
239
+ if (!list || !emptyEl) return;
240
+
241
+ if (!results.length) {
242
+ list.innerHTML = '';
243
+ list.appendChild(emptyEl);
244
+ emptyEl.style.display = '';
245
+ if (clearBtn) clearBtn.style.display = 'none';
246
+ return;
247
+ }
248
+
249
+ emptyEl.style.display = 'none';
250
+ if (clearBtn) clearBtn.style.display = '';
251
+
252
+ // Sort newest first
253
+ const sorted = results.slice().sort((a, b) =>
254
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
255
+ );
256
+
257
+ const fragment = document.createDocumentFragment();
258
+
259
+ for (const r of sorted) {
260
+ const card = document.createElement('div');
261
+ card.className = 'result-card';
262
+
263
+ const info = document.createElement('div');
264
+ info.className = 'result-info';
265
+
266
+ // Top row: ID + status badge
267
+ const topRow = document.createElement('div');
268
+ topRow.className = 'result-top-row';
269
+
270
+ const idEl = document.createElement('span');
271
+ idEl.className = 'result-id';
272
+ idEl.textContent = r.id.length > 12 ? r.id.slice(0, 12) + '\u2026' : r.id;
273
+ idEl.title = r.id;
274
+
275
+ const statusEl = document.createElement('span');
276
+ statusEl.className = 'result-status ' + r.status;
277
+ statusEl.textContent = r.status;
278
+
279
+ topRow.appendChild(idEl);
280
+ topRow.appendChild(statusEl);
281
+
282
+ // Details row: duration + time ago + failed step info
283
+ const details = document.createElement('div');
284
+ details.className = 'result-details';
285
+
286
+ const parts = [];
287
+ const dur = formatDuration(r.duration);
288
+ if (dur) parts.push(dur);
289
+ parts.push(timeAgo(r.createdAt));
290
+ if (r.status === 'failed' && r.failedStep != null) {
291
+ parts.push('failed at step ' + r.failedStep);
292
+ }
293
+ details.textContent = parts.join(' \u00b7 ');
294
+
295
+ // Error toggle for failed scenarios
296
+ if (r.status === 'failed' && r.error) {
297
+ const toggleBtn = document.createElement('button');
298
+ toggleBtn.className = 'result-error-toggle';
299
+ const isExpanded = _expandedErrors.has(r.id);
300
+ toggleBtn.textContent = isExpanded ? 'hide error' : 'show error';
301
+
302
+ const errorEl = document.createElement('div');
303
+ errorEl.className = 'result-error';
304
+ errorEl.textContent = r.error;
305
+ errorEl.style.display = isExpanded ? '' : 'none';
306
+
307
+ toggleBtn.addEventListener('click', () => {
308
+ const nowExpanded = errorEl.style.display === 'none';
309
+ errorEl.style.display = nowExpanded ? '' : 'none';
310
+ toggleBtn.textContent = nowExpanded ? 'hide error' : 'show error';
311
+ if (nowExpanded) {
312
+ _expandedErrors.add(r.id);
313
+ } else {
314
+ _expandedErrors.delete(r.id);
315
+ }
316
+ });
317
+
318
+ details.appendChild(toggleBtn);
319
+ info.appendChild(topRow);
320
+ info.appendChild(details);
321
+ info.appendChild(errorEl);
322
+ } else {
323
+ info.appendChild(topRow);
324
+ info.appendChild(details);
325
+ }
326
+
327
+ card.appendChild(info);
328
+ fragment.appendChild(card);
329
+ }
330
+
331
+ list.innerHTML = '';
332
+ emptyEl.style.display = 'none';
333
+ list.appendChild(emptyEl);
334
+ list.appendChild(fragment);
335
+ }
336
+
337
+ async function refreshResults() {
338
+ if (!_container || _resultsCleared) return;
339
+ try {
340
+ const params = activeProjectPath
341
+ ? '?projectPath=' + encodeURIComponent(activeProjectPath)
342
+ : '';
343
+ const res = await fetch('/api/test/trigger/scenarios/results' + params);
344
+ if (!res.ok) return;
345
+ const results = await res.json();
346
+ renderResults(results);
347
+ } catch (e) {
348
+ // ignore network errors
349
+ }
350
+ }
351
+
352
+ // ── Refresh polling ─────────────────────────────────────────────────
353
+
354
+ function _startRefreshPoll() {
355
+ if (_refreshTimer) return;
356
+ refresh();
357
+ _refreshTimer = setInterval(refresh, 5000);
358
+ }
359
+
360
+ function _stopRefreshPoll() {
361
+ if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; }
362
+ }
363
+
364
+ // ── Exports ─────────────────────────────────────────────────────────
365
+
366
+ export function mount(container, ctx) {
367
+ _container = container;
368
+
369
+ // 1. Scope the container
370
+ container.classList.add('page-test');
371
+
372
+ // 2. Build HTML
373
+ container.innerHTML = BODY_HTML;
374
+
375
+ // 3. Set initial project from context
376
+ activeProjectPath = ctx?.project?.path || null;
377
+
378
+ // 4. Clear results button
379
+ const clearBtn = container.querySelector('#clear-results-btn');
380
+ if (clearBtn) clearBtn.addEventListener('click', clearResults);
381
+
382
+ // 5. Visibility-change handler for auto-refresh
383
+ _visibilityHandler = () => {
384
+ if (document.hidden) _stopRefreshPoll();
385
+ else _startRefreshPoll();
386
+ };
387
+ document.addEventListener('visibilitychange', _visibilityHandler);
388
+
389
+ // 6. Start polling
390
+ _startRefreshPoll();
391
+ }
392
+
393
+ export function unmount(container) {
394
+ // 1. Stop refresh timer
395
+ _stopRefreshPoll();
396
+
397
+ // 2. Remove visibility handler
398
+ if (_visibilityHandler) {
399
+ document.removeEventListener('visibilitychange', _visibilityHandler);
400
+ _visibilityHandler = null;
401
+ }
402
+
403
+ // 3. Remove scope class & clear HTML
404
+ container.classList.remove('page-test');
405
+ container.innerHTML = '';
406
+
407
+ // 4. Clear module references
408
+ _container = null;
409
+ activeProjectPath = null;
410
+ _resultsCleared = false;
411
+ _expandedErrors.clear();
412
+ }
413
+
414
+ export function onProjectChange(project) {
415
+ activeProjectPath = project?.path || null;
416
+ refresh();
417
+ }