tubeless 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 (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/abort.d.ts +12 -0
  4. package/dist/abort.d.ts.map +1 -0
  5. package/dist/abort.js +49 -0
  6. package/dist/abort.js.map +1 -0
  7. package/dist/batch.d.ts +22 -0
  8. package/dist/batch.d.ts.map +1 -0
  9. package/dist/batch.js +73 -0
  10. package/dist/batch.js.map +1 -0
  11. package/dist/checkpoint.d.ts +33 -0
  12. package/dist/checkpoint.d.ts.map +1 -0
  13. package/dist/checkpoint.js +71 -0
  14. package/dist/checkpoint.js.map +1 -0
  15. package/dist/child-execution.d.ts +30 -0
  16. package/dist/child-execution.d.ts.map +1 -0
  17. package/dist/child-execution.js +270 -0
  18. package/dist/child-execution.js.map +1 -0
  19. package/dist/cli-command.d.ts +29 -0
  20. package/dist/cli-command.d.ts.map +1 -0
  21. package/dist/cli-command.js +314 -0
  22. package/dist/cli-command.js.map +1 -0
  23. package/dist/cli-parser.d.ts +28 -0
  24. package/dist/cli-parser.d.ts.map +1 -0
  25. package/dist/cli-parser.js +557 -0
  26. package/dist/cli-parser.js.map +1 -0
  27. package/dist/cli-pipeline-command.d.ts +84 -0
  28. package/dist/cli-pipeline-command.d.ts.map +1 -0
  29. package/dist/cli-pipeline-command.js +185 -0
  30. package/dist/cli-pipeline-command.js.map +1 -0
  31. package/dist/cli-types.d.ts +220 -0
  32. package/dist/cli-types.d.ts.map +1 -0
  33. package/dist/cli-types.js +19 -0
  34. package/dist/cli-types.js.map +1 -0
  35. package/dist/cli.d.ts +6 -0
  36. package/dist/cli.d.ts.map +1 -0
  37. package/dist/cli.js +6 -0
  38. package/dist/cli.js.map +1 -0
  39. package/dist/env.d.ts +2 -0
  40. package/dist/env.d.ts.map +1 -0
  41. package/dist/env.js +8 -0
  42. package/dist/env.js.map +1 -0
  43. package/dist/file-utils.d.ts +4 -0
  44. package/dist/file-utils.d.ts.map +1 -0
  45. package/dist/file-utils.js +35 -0
  46. package/dist/file-utils.js.map +1 -0
  47. package/dist/interactive-reporter.d.ts +38 -0
  48. package/dist/interactive-reporter.d.ts.map +1 -0
  49. package/dist/interactive-reporter.js +308 -0
  50. package/dist/interactive-reporter.js.map +1 -0
  51. package/dist/lifecycle.d.ts +17 -0
  52. package/dist/lifecycle.d.ts.map +1 -0
  53. package/dist/lifecycle.js +77 -0
  54. package/dist/lifecycle.js.map +1 -0
  55. package/dist/live-ticker.d.ts +21 -0
  56. package/dist/live-ticker.d.ts.map +1 -0
  57. package/dist/live-ticker.js +377 -0
  58. package/dist/live-ticker.js.map +1 -0
  59. package/dist/mapped-child-progress.d.ts +109 -0
  60. package/dist/mapped-child-progress.d.ts.map +1 -0
  61. package/dist/mapped-child-progress.js +133 -0
  62. package/dist/mapped-child-progress.js.map +1 -0
  63. package/dist/node.d.ts +5 -0
  64. package/dist/node.d.ts.map +1 -0
  65. package/dist/node.js +5 -0
  66. package/dist/node.js.map +1 -0
  67. package/dist/paths.d.ts +9 -0
  68. package/dist/paths.d.ts.map +1 -0
  69. package/dist/paths.js +19 -0
  70. package/dist/paths.js.map +1 -0
  71. package/dist/pipeline-command-marker.d.ts +5 -0
  72. package/dist/pipeline-command-marker.d.ts.map +1 -0
  73. package/dist/pipeline-command-marker.js +21 -0
  74. package/dist/pipeline-command-marker.js.map +1 -0
  75. package/dist/pipeline-diagnostics.d.ts +4 -0
  76. package/dist/pipeline-diagnostics.d.ts.map +1 -0
  77. package/dist/pipeline-diagnostics.js +17 -0
  78. package/dist/pipeline-diagnostics.js.map +1 -0
  79. package/dist/pipeline-module.d.ts +62 -0
  80. package/dist/pipeline-module.d.ts.map +1 -0
  81. package/dist/pipeline-module.js +157 -0
  82. package/dist/pipeline-module.js.map +1 -0
  83. package/dist/pipeline.d.ts +606 -0
  84. package/dist/pipeline.d.ts.map +1 -0
  85. package/dist/pipeline.js +1444 -0
  86. package/dist/pipeline.js.map +1 -0
  87. package/dist/progress.d.ts +4 -0
  88. package/dist/progress.d.ts.map +1 -0
  89. package/dist/progress.js +8 -0
  90. package/dist/progress.js.map +1 -0
  91. package/dist/prompt-select.d.ts +81 -0
  92. package/dist/prompt-select.d.ts.map +1 -0
  93. package/dist/prompt-select.js +197 -0
  94. package/dist/prompt-select.js.map +1 -0
  95. package/dist/rate-limit.d.ts +7 -0
  96. package/dist/rate-limit.d.ts.map +1 -0
  97. package/dist/rate-limit.js +23 -0
  98. package/dist/rate-limit.js.map +1 -0
  99. package/dist/render.d.ts +21 -0
  100. package/dist/render.d.ts.map +1 -0
  101. package/dist/render.js +60 -0
  102. package/dist/render.js.map +1 -0
  103. package/dist/reporter-entry.d.ts +3 -0
  104. package/dist/reporter-entry.d.ts.map +1 -0
  105. package/dist/reporter-entry.js +3 -0
  106. package/dist/reporter-entry.js.map +1 -0
  107. package/dist/reporter.d.ts +50 -0
  108. package/dist/reporter.d.ts.map +1 -0
  109. package/dist/reporter.js +167 -0
  110. package/dist/reporter.js.map +1 -0
  111. package/dist/retry.d.ts +28 -0
  112. package/dist/retry.d.ts.map +1 -0
  113. package/dist/retry.js +59 -0
  114. package/dist/retry.js.map +1 -0
  115. package/dist/run-store-sqlite.d.ts +12 -0
  116. package/dist/run-store-sqlite.d.ts.map +1 -0
  117. package/dist/run-store-sqlite.js +187 -0
  118. package/dist/run-store-sqlite.js.map +1 -0
  119. package/dist/run-store-ui-http.d.ts +5 -0
  120. package/dist/run-store-ui-http.d.ts.map +1 -0
  121. package/dist/run-store-ui-http.js +19 -0
  122. package/dist/run-store-ui-http.js.map +1 -0
  123. package/dist/run-store-ui-page.d.ts +2 -0
  124. package/dist/run-store-ui-page.d.ts.map +1 -0
  125. package/dist/run-store-ui-page.js +759 -0
  126. package/dist/run-store-ui-page.js.map +1 -0
  127. package/dist/run-store-ui-protocol.d.ts +28 -0
  128. package/dist/run-store-ui-protocol.d.ts.map +1 -0
  129. package/dist/run-store-ui-protocol.js +84 -0
  130. package/dist/run-store-ui-protocol.js.map +1 -0
  131. package/dist/run-store-ui-state.d.ts +21 -0
  132. package/dist/run-store-ui-state.d.ts.map +1 -0
  133. package/dist/run-store-ui-state.js +65 -0
  134. package/dist/run-store-ui-state.js.map +1 -0
  135. package/dist/run-store-ui.d.ts +25 -0
  136. package/dist/run-store-ui.d.ts.map +1 -0
  137. package/dist/run-store-ui.js +217 -0
  138. package/dist/run-store-ui.js.map +1 -0
  139. package/dist/run-store.d.ts +109 -0
  140. package/dist/run-store.d.ts.map +1 -0
  141. package/dist/run-store.js +345 -0
  142. package/dist/run-store.js.map +1 -0
  143. package/dist/testing.d.ts +40 -0
  144. package/dist/testing.d.ts.map +1 -0
  145. package/dist/testing.js +83 -0
  146. package/dist/testing.js.map +1 -0
  147. package/dist/tracing-internal.d.ts +21 -0
  148. package/dist/tracing-internal.d.ts.map +1 -0
  149. package/dist/tracing-internal.js +206 -0
  150. package/dist/tracing-internal.js.map +1 -0
  151. package/dist/tracing-json.d.ts +12 -0
  152. package/dist/tracing-json.d.ts.map +1 -0
  153. package/dist/tracing-json.js +8 -0
  154. package/dist/tracing-json.js.map +1 -0
  155. package/dist/tracing-otel.d.ts +36 -0
  156. package/dist/tracing-otel.d.ts.map +1 -0
  157. package/dist/tracing-otel.js +60 -0
  158. package/dist/tracing-otel.js.map +1 -0
  159. package/dist/tracing.d.ts +48 -0
  160. package/dist/tracing.d.ts.map +1 -0
  161. package/dist/tracing.js +2 -0
  162. package/dist/tracing.js.map +1 -0
  163. package/dist/workbench-bin.d.ts +3 -0
  164. package/dist/workbench-bin.d.ts.map +1 -0
  165. package/dist/workbench-bin.js +8 -0
  166. package/dist/workbench-bin.js.map +1 -0
  167. package/dist/workbench-graph.d.ts +3 -0
  168. package/dist/workbench-graph.d.ts.map +1 -0
  169. package/dist/workbench-graph.js +64 -0
  170. package/dist/workbench-graph.js.map +1 -0
  171. package/dist/workbench-inspect.d.ts +3 -0
  172. package/dist/workbench-inspect.d.ts.map +1 -0
  173. package/dist/workbench-inspect.js +65 -0
  174. package/dist/workbench-inspect.js.map +1 -0
  175. package/dist/workbench-plan.d.ts +3 -0
  176. package/dist/workbench-plan.d.ts.map +1 -0
  177. package/dist/workbench-plan.js +72 -0
  178. package/dist/workbench-plan.js.map +1 -0
  179. package/dist/workbench-run.d.ts +13 -0
  180. package/dist/workbench-run.d.ts.map +1 -0
  181. package/dist/workbench-run.js +115 -0
  182. package/dist/workbench-run.js.map +1 -0
  183. package/dist/workbench-shared.d.ts +67 -0
  184. package/dist/workbench-shared.d.ts.map +1 -0
  185. package/dist/workbench-shared.js +163 -0
  186. package/dist/workbench-shared.js.map +1 -0
  187. package/dist/workbench-studio.d.ts +26 -0
  188. package/dist/workbench-studio.d.ts.map +1 -0
  189. package/dist/workbench-studio.js +57 -0
  190. package/dist/workbench-studio.js.map +1 -0
  191. package/dist/workbench-ui.d.ts +3 -0
  192. package/dist/workbench-ui.d.ts.map +1 -0
  193. package/dist/workbench-ui.js +262 -0
  194. package/dist/workbench-ui.js.map +1 -0
  195. package/dist/workbench.d.ts +5 -0
  196. package/dist/workbench.d.ts.map +1 -0
  197. package/dist/workbench.js +43 -0
  198. package/dist/workbench.js.map +1 -0
  199. package/docs/README.md +31 -0
  200. package/docs/agent-guide.md +168 -0
  201. package/docs/api-reference.md +269 -0
  202. package/docs/api-report.json +298 -0
  203. package/docs/child-pipeline-composition.md +185 -0
  204. package/docs/cli.md +118 -0
  205. package/docs/concepts.md +502 -0
  206. package/docs/getting-started.md +162 -0
  207. package/docs/llms.txt +62 -0
  208. package/docs/recipes.md +51 -0
  209. package/docs/studio.md +86 -0
  210. package/examples/best-effort.ts +42 -0
  211. package/examples/cancellation-and-testing.ts +45 -0
  212. package/examples/child-pipeline.ts +51 -0
  213. package/examples/cli-job.ts +86 -0
  214. package/examples/conditional-step.ts +41 -0
  215. package/examples/fan-out-progress.ts +53 -0
  216. package/examples/local-observability.ts +80 -0
  217. package/examples/publish-with-gates.ts +56 -0
  218. package/examples/rendering.ts +34 -0
  219. package/examples/resumable-enrichment.ts +75 -0
  220. package/examples/tracing.ts +35 -0
  221. package/examples/tsconfig.json +15 -0
  222. package/examples/typed-import.ts +54 -0
  223. package/examples/validated-boundaries.ts +75 -0
  224. package/package.json +156 -0
@@ -0,0 +1,759 @@
1
+ export const PIPELINE_RUN_STUDIO_HTML = String.raw `<!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="color-scheme" content="light" />
7
+ <title>Tubeless — Local Studio</title>
8
+ <style>
9
+ :root {
10
+ --ink: #171816;
11
+ --muted: #6f716c;
12
+ --faint: #9a9c96;
13
+ --line: #dedfd9;
14
+ --line-strong: #c8cac2;
15
+ --paper: #fff;
16
+ --ground: #f1f2ed;
17
+ --blue: #275efe;
18
+ --blue-soft: #eaf0ff;
19
+ --green: #1c7c54;
20
+ --green-soft: #e8f5ee;
21
+ --red: #bf3b35;
22
+ --red-soft: #fbefee;
23
+ --amber: #9d6512;
24
+ --amber-soft: #fbf3e3;
25
+ --shadow: 0 1px 1px rgba(22, 24, 21, .025), 0 10px 32px rgba(22, 24, 21, .04);
26
+ --sans: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
27
+ --mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
28
+ }
29
+ * { box-sizing: border-box; }
30
+ html, body { margin: 0; min-height: 100%; background: var(--ground); color: var(--ink); font-family: var(--sans); }
31
+ body { min-width: 320px; }
32
+ button, input, select, textarea { font: inherit; }
33
+ button { color: inherit; }
34
+ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, summary:focus-visible { outline: 2px solid #7898ff; outline-offset: 2px; }
35
+ .shell { min-height: 100vh; display: grid; grid-template-columns: 228px minmax(0, 1fr); }
36
+ .rail { position: sticky; top: 0; height: 100vh; padding: 24px 16px 18px; background: #1b1d1a; color: #f8f9f4; display: flex; flex-direction: column; }
37
+ .brand { display: flex; gap: 11px; align-items: center; padding: 0 8px 26px; }
38
+ .mark { width: 30px; height: 30px; display: grid; place-items: center; border: 1px solid #4e524a; border-radius: 8px; background: #252824; }
39
+ .brand strong { display: block; letter-spacing: -.02em; font-size: 15px; }
40
+ .brand small { color: #969b91; font-size: 11px; letter-spacing: .06em; text-transform: uppercase; }
41
+ .rail-label { margin: 14px 10px 7px; color: #73786f; font-size: 10px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; }
42
+ .nav { display: grid; gap: 3px; }
43
+ .nav button { width: 100%; padding: 10px 11px; border: 0; border-radius: 7px; background: transparent; color: #aeb2aa; display: flex; align-items: center; gap: 10px; cursor: pointer; text-align: left; font-size: 13px; }
44
+ .nav button:hover { color: #fff; background: #242723; }
45
+ .nav button.active { color: #fff; background: #30332f; box-shadow: inset 0 0 0 1px #3d413a; }
46
+ .nav svg { width: 16px; height: 16px; }
47
+ .nav-count { margin-left: auto; min-width: 22px; padding: 2px 6px; border-radius: 10px; background: #3a3e37; color: #cbd0c6; font: 10px var(--mono); text-align: center; }
48
+ .rail-foot { margin-top: auto; padding: 12px 9px 3px; border-top: 1px solid #30332e; }
49
+ .connection { display: flex; gap: 8px; align-items: center; color: #aeb3aa; font-size: 11px; }
50
+ .pulse { width: 7px; height: 7px; border-radius: 50%; background: #63d297; box-shadow: 0 0 0 4px rgba(99,210,151,.1); }
51
+ .database { margin-top: 7px; overflow: hidden; text-overflow: ellipsis; color: #6f746c; font: 10px var(--mono); white-space: nowrap; }
52
+ .workspace { width: 100%; max-width: 1760px; min-width: 0; margin: 0 auto; padding: 28px 32px 52px; }
53
+ .topbar { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 22px; }
54
+ .eyebrow { margin-bottom: 5px; color: var(--faint); font-size: 10px; font-weight: 750; letter-spacing: .13em; text-transform: uppercase; }
55
+ h1 { margin: 0; font-size: clamp(24px, 3vw, 36px); line-height: 1.06; letter-spacing: -.045em; font-weight: 680; }
56
+ .lede { margin: 8px 0 0; color: var(--muted); font-size: 13px; line-height: 1.45; }
57
+ .toolbar { display: flex; gap: 8px; align-items: center; }
58
+ .search { width: min(290px, 30vw); padding: 9px 11px 9px 34px; border: 1px solid var(--line); border-radius: 8px; outline: none; background: var(--paper) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%239a9c96' stroke-width='1.5'%3E%3Ccircle cx='7' cy='7' r='4.5'/%3E%3Cpath d='m10.5 10.5 4 4'/%3E%3C/svg%3E") no-repeat 10px center; box-shadow: 0 1px 0 rgba(0,0,0,.02); font-size: 12px; }
59
+ .search:focus { border-color: #9db3ff; box-shadow: 0 0 0 3px var(--blue-soft); }
60
+ .icon-button { width: 36px; height: 36px; border: 1px solid var(--line); border-radius: 8px; background: var(--paper); cursor: pointer; display: grid; place-items: center; }
61
+ .icon-button:hover { border-color: var(--line-strong); }
62
+ .icon-button.spinning svg { animation: spin .65s linear infinite; }
63
+ .primary-button { height: 36px; padding: 0 13px; border: 1px solid #174ce2; border-radius: 8px; background: var(--blue); color: #fff; cursor: pointer; box-shadow: 0 1px 1px rgba(18,54,156,.18); font-size: 11px; font-weight: 700; }
64
+ .primary-button:hover { background: #174fe8; }
65
+ .primary-button:disabled { opacity: .55; cursor: wait; }
66
+ .danger-button { height: 36px; padding: 0 12px; border: 1px solid #e4c6c3; border-radius: 8px; background: #fffafa; color: #a53c36; cursor: pointer; font-size: 11px; font-weight: 700; }
67
+ .danger-button:hover { border-color: #d8a9a5; background: var(--red-soft); }
68
+ .danger-button:disabled { opacity: .45; cursor: not-allowed; }
69
+ .danger-button svg { width: 13px; height: 13px; margin-right: 5px; vertical-align: -2px; }
70
+ @keyframes spin { to { transform: rotate(360deg); } }
71
+ .metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 16px; }
72
+ .metric { padding: 14px 15px 13px; border: 1px solid var(--line); background: rgba(255,255,255,.78); border-radius: 9px; box-shadow: 0 1px 0 rgba(255,255,255,.8); }
73
+ .metric-label { color: var(--muted); font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
74
+ .metric-value { margin-top: 8px; font: 600 24px/1 var(--mono); letter-spacing: -.05em; }
75
+ .metric-note { margin-top: 6px; color: var(--faint); font-size: 10px; }
76
+ .sheet { border: 1px solid var(--line); border-radius: 11px; background: var(--paper); box-shadow: var(--shadow); overflow: hidden; }
77
+ .sheet-head { min-height: 54px; padding: 12px 15px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; gap: 12px; }
78
+ .sheet-title { font-size: 13px; font-weight: 700; letter-spacing: -.01em; }
79
+ .sheet-subtitle { color: var(--faint); font-size: 10px; }
80
+ .content-grid { display: grid; grid-template-columns: minmax(340px, .78fr) minmax(500px, 1.22fr); gap: 14px; align-items: start; }
81
+ .run-list { max-height: calc(100vh - 263px); overflow: auto; }
82
+ .run-group { position: sticky; top: 0; z-index: 2; padding: 8px 15px; border-bottom: 1px solid #e7e8e2; background: rgba(248,249,245,.96); color: var(--faint); font-size: 9px; font-weight: 750; letter-spacing: .1em; text-transform: uppercase; backdrop-filter: blur(5px); }
83
+ .run-row { width: 100%; padding: 14px 15px; border: 0; border-bottom: 1px solid #ecece7; background: #fff; cursor: pointer; text-align: left; display: grid; gap: 8px; }
84
+ .run-row:last-child { border-bottom: 0; }
85
+ .run-row:hover { background: #fafbf8; }
86
+ .run-row.running:not(.selected) { background: #f9faff; }
87
+ .run-row.selected { background: #f5f7ff; box-shadow: inset 3px 0 var(--blue); }
88
+ .run-primary, .run-secondary, .detail-heading, .step-head { display: flex; align-items: center; gap: 9px; }
89
+ .run-primary strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 13px; white-space: nowrap; }
90
+ .run-time { margin-left: auto; color: var(--faint); font: 10px var(--mono); }
91
+ .run-secondary { color: var(--muted); font-size: 10px; }
92
+ .run-secondary code { min-width: 0; overflow: hidden; text-overflow: ellipsis; font: 10px var(--mono); white-space: nowrap; }
93
+ .run-activity { min-width: 0; padding: 8px 9px; border-radius: 6px; background: var(--blue-soft); color: #3153b8; display: flex; gap: 8px; align-items: center; font-size: 9px; }
94
+ .run-activity strong { white-space: nowrap; }
95
+ .run-activity span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
96
+ .run-parentage { margin: 0 0 12px; display: flex; align-items: center; flex-wrap: wrap; gap: 6px; color: var(--faint); font-size: 9px; }
97
+ .run-parentage button { padding: 0; border: 0; background: none; color: #4165cf; cursor: pointer; font: 9px var(--mono); }
98
+ .nested-runs { margin-bottom: 17px; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
99
+ .nested-run { width: 100%; padding: 9px 11px; border: 0; border-bottom: 1px solid #e9eae4; background: #fbfcf9; cursor: pointer; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 8px; text-align: left; }
100
+ .nested-run:last-child { border-bottom: 0; }
101
+ .nested-run:hover { background: #f5f7ff; }
102
+ .nested-run strong { overflow: hidden; text-overflow: ellipsis; font-size: 10px; white-space: nowrap; }
103
+ .nested-run small { color: var(--faint); font: 9px var(--mono); }
104
+ .dot { width: 4px; height: 4px; border-radius: 50%; background: var(--line-strong); }
105
+ .status { display: inline-flex; align-items: center; gap: 5px; padding: 3px 7px; border-radius: 999px; font-size: 9px; font-weight: 750; letter-spacing: .06em; text-transform: uppercase; white-space: nowrap; }
106
+ .status-mark { width: 9px; height: 9px; display: inline-grid; place-items: center; font: 800 10px/1 var(--sans); letter-spacing: 0; }
107
+ .status.completed, .status.complete { color: var(--green); background: var(--green-soft); }
108
+ .status.failed, .status.cancelled { color: var(--red); background: var(--red-soft); }
109
+ .status.running { color: var(--blue); background: var(--blue-soft); }
110
+ .status.running .status-mark { border: 1.5px solid #b9c9fa; border-top-color: currentColor; border-radius: 50%; animation: spin .8s linear infinite; font-size: 0; }
111
+ .status.skipped, .status.planned { color: var(--amber); background: var(--amber-soft); }
112
+ .detail { min-height: 460px; }
113
+ .detail-body { padding: 18px; }
114
+ .detail-heading { align-items: flex-start; }
115
+ .detail-heading h2 { min-width: 0; margin: 0; overflow: hidden; text-overflow: ellipsis; font-size: 19px; letter-spacing: -.03em; white-space: nowrap; }
116
+ .detail-heading .status { margin-top: 2px; }
117
+ .detail-kicker { margin-bottom: 5px; color: var(--faint); font-size: 9px; font-weight: 750; letter-spacing: .09em; text-transform: uppercase; }
118
+ .run-id { margin-top: 6px; color: var(--faint); font: 10px var(--mono); overflow-wrap: anywhere; }
119
+ .detail-meta { margin: 15px 0 18px; display: grid; grid-template-columns: 1.35fr repeat(3, minmax(0, 1fr)); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
120
+ .detail-meta > div { min-width: 0; padding: 10px 11px; border-right: 1px solid var(--line); }
121
+ .detail-meta > div:last-child { border-right: 0; }
122
+ .detail-meta label { display: block; margin-bottom: 5px; color: var(--faint); font-size: 9px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; }
123
+ .detail-meta span { display: block; overflow: hidden; text-overflow: ellipsis; font: 11px var(--mono); white-space: nowrap; }
124
+ .section-title { margin: 18px 0 8px; display: flex; align-items: center; justify-content: space-between; color: var(--muted); font-size: 10px; font-weight: 750; letter-spacing: .1em; text-transform: uppercase; }
125
+ .step-list { border: 1px solid var(--line); border-radius: 9px; overflow: hidden; }
126
+ .step { position: relative; padding: 12px 13px 12px 43px; border-bottom: 1px solid #e9eae4; }
127
+ .step:last-child { border-bottom: 0; }
128
+ .step:not(:last-child)::after { content: ""; position: absolute; top: 31px; bottom: -7px; left: 22px; width: 1px; background: var(--line); }
129
+ .step-status-icon { position: absolute; top: 13px; left: 13px; z-index: 1; width: 19px; height: 19px; border: 1px solid var(--line); border-radius: 5px; background: #f7f8f5; color: var(--faint); display: grid; place-items: center; }
130
+ .step-status-icon svg { width: 11px; height: 11px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2; }
131
+ .step-status-icon.complete { border-color: #bddfce; background: var(--green-soft); color: var(--green); }
132
+ .step-status-icon.running { border-color: #cad6fa; background: var(--blue-soft); color: var(--blue); }
133
+ .step-status-icon.running i { width: 10px; height: 10px; border: 2px solid #bdcaf1; border-top-color: currentColor; border-radius: 50%; animation: spin .8s linear infinite; }
134
+ .step-status-icon.skipped { border-color: #ead5ac; background: var(--amber-soft); color: var(--amber); }
135
+ .step-status-icon.cancelled { border-color: #dfcfcd; background: #f7f1f0; color: #8f5d58; }
136
+ .step-status-icon.failed { border-color: #efcbc8; background: var(--red-soft); color: var(--red); font-size: 11px; font-weight: 800; }
137
+ .step-status-icon.planned { font-size: 12px; line-height: 1; }
138
+ .step-head strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 12px; white-space: nowrap; }
139
+ .step-head code { color: var(--faint); font: 9px var(--mono); }
140
+ .step-duration { margin-left: auto; color: var(--muted); font: 10px var(--mono); }
141
+ .step-description { margin-top: 4px; color: var(--muted); font-size: 10px; line-height: 1.45; }
142
+ .execution { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 5px; }
143
+ .execution-summary { padding: 4px 6px; border: 1px solid var(--line); border-radius: 5px; color: var(--muted); background: #fbfbf8; font: 9px var(--mono); }
144
+ .execution-summary b { color: var(--faint); font: 700 8px var(--sans); letter-spacing: .06em; text-transform: uppercase; }
145
+ .progress { height: 3px; margin-top: 10px; overflow: hidden; border-radius: 2px; background: #e7e9e2; }
146
+ .progress > i { display: block; height: 100%; background: var(--blue); transition: width .3s ease; }
147
+ .progress-copy { margin-top: 5px; color: var(--faint); font-size: 9px; }
148
+ .error-card { padding: 12px; border: 1px solid #efcbc8; border-radius: 8px; background: #fff8f7; }
149
+ .error-code { color: var(--red); font: 10px var(--mono); }
150
+ .error-message { margin-top: 6px; font-size: 11px; line-height: 1.5; }
151
+ .logs { border: 1px solid var(--line); border-radius: 8px; background: #20221f; color: #dfe3da; overflow: auto; max-height: 220px; }
152
+ .log-line { display: grid; grid-template-columns: 70px 42px minmax(0, 1fr); gap: 8px; padding: 7px 9px; border-bottom: 1px solid #30332e; font: 9px/1.45 var(--mono); }
153
+ .log-line:last-child { border-bottom: 0; }
154
+ .log-time { color: #777d72; }
155
+ .log-level { color: #8fb0ff; text-transform: uppercase; }
156
+ .log-level.error { color: #ff8c84; }
157
+ .log-level.warn { color: #e8bd72; }
158
+ .log-message { overflow-wrap: anywhere; }
159
+ .empty { min-height: 330px; padding: 38px; display: grid; place-items: center; text-align: center; color: var(--muted); }
160
+ .empty-icon { width: 44px; height: 44px; margin: 0 auto 13px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 12px; background: #fafbf8; }
161
+ .empty strong { display: block; color: var(--ink); font-size: 13px; }
162
+ .empty p { max-width: 300px; margin: 7px auto 0; font-size: 11px; line-height: 1.55; }
163
+ .catalog { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; padding: 14px; }
164
+ .catalog-card { min-height: 150px; padding: 15px; border: 1px solid var(--line); border-radius: 9px; background: #fbfcf9; display: flex; flex-direction: column; }
165
+ .catalog-card h3 { margin: 0; font-size: 13px; letter-spacing: -.015em; }
166
+ .catalog-card p { margin: 8px 0 14px; color: var(--muted); font-size: 10px; line-height: 1.55; }
167
+ .catalog-actions { margin-top: auto; display: flex; gap: 7px; }
168
+ .modal-backdrop { position: fixed; inset: 0; z-index: 50; padding: 22px; display: grid; place-items: center; background: rgba(22,24,21,.42); backdrop-filter: blur(3px); }
169
+ .modal { width: min(720px, 100%); max-height: calc(100vh - 44px); overflow: auto; border: 1px solid #cfd1ca; border-radius: 13px; background: var(--paper); box-shadow: 0 24px 80px rgba(16,18,15,.22); }
170
+ .confirm-modal { width: min(460px, 100%); }
171
+ .confirm-body { padding: 18px 20px 20px; }
172
+ .confirm-copy { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.6; }
173
+ .confirm-warning { margin: 14px 0 0; padding: 10px 11px; border: 1px solid #efcbc8; border-radius: 8px; background: #fff8f7; color: #87413c; font-size: 10px; line-height: 1.5; }
174
+ .confirm-actions { margin-top: 18px; display: flex; justify-content: flex-end; gap: 8px; }
175
+ .modal-head { position: sticky; top: 0; z-index: 3; padding: 18px 20px 16px; border-bottom: 1px solid var(--line); background: rgba(255,255,255,.97); display: flex; justify-content: space-between; gap: 18px; backdrop-filter: blur(8px); }
176
+ .modal-head h2 { margin: 0; font-size: 18px; letter-spacing: -.03em; }
177
+ .modal-head p { margin: 6px 0 0; color: var(--muted); font-size: 11px; line-height: 1.5; }
178
+ .close-button { width: 30px; height: 30px; flex: 0 0 auto; border: 1px solid var(--line); border-radius: 7px; background: #fafbf8; cursor: pointer; font-size: 16px; }
179
+ .launch-form { padding: 18px 20px 20px; }
180
+ .parameter-fields { margin: 4px 0 18px; display: grid; gap: 18px; }
181
+ .form-section { min-width: 0; }
182
+ .form-section + .form-section { padding-top: 16px; border-top: 1px solid var(--line); }
183
+ .form-section-head { margin-bottom: 10px; display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
184
+ .form-section-head strong { font-size: 11px; letter-spacing: -.01em; }
185
+ .form-section-head span { color: var(--faint); font-size: 9px; }
186
+ .parameter-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 13px; }
187
+ .field { display: grid; gap: 7px; margin-bottom: 15px; }
188
+ .field label { color: var(--muted); font-size: 10px; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; }
189
+ .parameter-grid .field { min-width: 0; margin: 0; }
190
+ .field select, .field textarea, .field input:not([type="checkbox"]) { width: 100%; border: 1px solid var(--line-strong); border-radius: 8px; outline: none; background: #fff; }
191
+ .field select, .field input:not([type="checkbox"]) { height: 38px; padding: 0 10px; font-size: 12px; }
192
+ .field select[multiple] { height: 104px; padding: 6px; }
193
+ .field textarea { min-height: 82px; padding: 10px 11px; resize: vertical; font: 11px/1.55 var(--mono); }
194
+ .field select:focus, .field textarea:focus, .field input:focus { border-color: #8ba7ff; box-shadow: 0 0 0 3px var(--blue-soft); }
195
+ .field-hint { color: var(--faint); font-size: 9px; line-height: 1.5; }
196
+ .boolean-field { min-height: 72px; padding: 11px 12px; border: 1px solid var(--line); border-radius: 8px; background: #fbfcf9; display: flex; align-items: flex-start; gap: 9px; cursor: pointer; transition: border-color .15s ease, background .15s ease; }
197
+ .boolean-field:hover { border-color: var(--line-strong); background: #f8f9f5; }
198
+ .boolean-field input { width: 15px; height: 15px; margin: 1px 0 0 auto; accent-color: var(--blue); }
199
+ .boolean-field strong { display: block; font: 650 10px var(--mono); color: var(--ink); }
200
+ .boolean-field small { display: block; margin-top: 5px; color: var(--muted); font-size: 9px; line-height: 1.4; }
201
+ .required-mark { margin-left: 5px; color: var(--red); font-size: 8px; }
202
+ .advanced { margin: 0 0 14px; border-top: 1px solid var(--line); padding-top: 11px; }
203
+ .advanced summary { color: var(--muted); cursor: pointer; font-size: 10px; font-weight: 700; }
204
+ .advanced .field { margin: 11px 0 0; }
205
+ .command-help { max-height: 190px; margin: 10px 0 0; padding: 11px 12px; overflow: auto; border: 1px solid #dedfd9; border-radius: 8px; background: #f6f7f3; color: #555952; white-space: pre-wrap; font: 9px/1.5 var(--mono); }
206
+ .plan-result { min-height: 110px; max-height: 340px; margin: 2px 0 15px; border: 1px solid #ccd6f2; border-radius: 9px; overflow: auto; }
207
+ .plan-placeholder { padding: 28px; color: var(--faint); font-size: 10px; text-align: center; }
208
+ .plan-summary { padding: 10px 12px; border-bottom: 1px solid var(--line); background: #f8f9f5; display: flex; align-items: center; justify-content: space-between; gap: 12px; font-size: 10px; }
209
+ .plan-steps { display: grid; }
210
+ .plan-step { padding: 10px 12px; border-bottom: 1px solid #e9eae4; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 5px 12px; }
211
+ .plan-step:last-child { border-bottom: 0; }
212
+ .plan-step-title { min-width: 0; display: flex; align-items: center; gap: 7px; }
213
+ .plan-step-title strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 10px; white-space: nowrap; }
214
+ .plan-kind { padding: 2px 5px; border: 1px solid var(--line); border-radius: 999px; color: var(--faint); background: #fff; font-size: 7px; font-weight: 750; letter-spacing: .06em; text-transform: uppercase; white-space: nowrap; }
215
+ .plan-kind.pipeline { border-color: #cad6fa; color: #3153b8; background: var(--blue-soft); }
216
+ .plan-step > small { color: var(--faint); font-size: 9px; line-height: 1.4; }
217
+ .plan-nested { grid-column: 1 / -1; padding: 8px 9px; border-radius: 6px; background: #f5f7ff; color: #4660ad; display: flex; align-items: center; flex-wrap: wrap; gap: 5px; font-size: 8px; }
218
+ .plan-nested strong { margin-right: 3px; font: 650 9px var(--mono); }
219
+ .plan-nested code { padding: 2px 4px; border: 1px solid #d8def2; border-radius: 4px; background: #fff; font: 8px var(--mono); }
220
+ .plan-disposition { grid-row: 1 / span 2; grid-column: 2; align-self: center; padding: 3px 6px; border-radius: 999px; color: var(--green); background: var(--green-soft); font-size: 8px; font-weight: 750; letter-spacing: .05em; text-transform: uppercase; }
221
+ .plan-disposition.skipped { color: var(--amber); background: var(--amber-soft); }
222
+ .launch-error { margin-bottom: 15px; padding: 10px 11px; border: 1px solid #efcbc8; border-radius: 8px; background: #fff8f7; color: var(--red); font-size: 10px; line-height: 1.5; white-space: pre-wrap; }
223
+ .modal-actions { position: sticky; bottom: 0; z-index: 2; margin: 0 -20px -20px; padding: 13px 20px 14px; border-top: 1px solid var(--line); background: rgba(255,255,255,.97); display: flex; justify-content: flex-end; gap: 8px; backdrop-filter: blur(8px); }
224
+ .secondary-button { height: 36px; padding: 0 13px; border: 1px solid var(--line); border-radius: 8px; background: #fff; cursor: pointer; font-size: 11px; font-weight: 650; }
225
+ .toast { position: fixed; right: 22px; bottom: 22px; z-index: 60; max-width: min(420px, calc(100vw - 44px)); padding: 11px 13px; border: 1px solid #bdd8c9; border-radius: 9px; background: #f4fff8; color: #176a47; box-shadow: var(--shadow); font-size: 11px; }
226
+ .hidden { display: none !important; }
227
+ @media (max-width: 980px) {
228
+ .shell { grid-template-columns: 74px minmax(0, 1fr); }
229
+ .rail { padding-inline: 10px; }
230
+ .brand { justify-content: center; padding-inline: 0; }
231
+ .brand-copy, .rail-label, .nav span:not(.nav-count), .rail-foot { display: none; }
232
+ .nav button { justify-content: center; padding: 11px 6px; }
233
+ .nav-count { display: none; }
234
+ .content-grid { grid-template-columns: 1fr; }
235
+ .run-list { max-height: 360px; }
236
+ }
237
+ @media (max-width: 680px) {
238
+ .shell { display: block; }
239
+ .rail { position: sticky; z-index: 10; width: 100%; height: 58px; padding: 8px 12px; flex-direction: row; align-items: center; }
240
+ .brand { padding: 0 8px 0 0; }
241
+ .nav { margin-left: auto; display: flex; }
242
+ .nav button { width: 42px; }
243
+ .workspace { padding: 20px 14px 36px; }
244
+ .topbar { display: block; }
245
+ .toolbar { margin-top: 15px; }
246
+ .search { width: 100%; }
247
+ #clearHistoryButton { width: 36px; padding: 0; font-size: 0; }
248
+ #clearHistoryButton svg { margin: 0; }
249
+ .metrics { grid-template-columns: repeat(2, 1fr); }
250
+ .content-grid { display: block; }
251
+ .detail { margin-top: 12px; }
252
+ .detail-meta { grid-template-columns: repeat(2, 1fr); }
253
+ .detail-meta > div:nth-child(2) { border-right: 0; }
254
+ .detail-meta > div:nth-child(-n+2) { border-bottom: 1px solid var(--line); }
255
+ .parameter-grid { grid-template-columns: 1fr; }
256
+ }
257
+ @media (prefers-reduced-motion: reduce) {
258
+ *, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
259
+ }
260
+ </style>
261
+ </head>
262
+ <body>
263
+ <div class="shell">
264
+ <aside class="rail">
265
+ <div class="brand">
266
+ <div class="mark" aria-hidden="true">
267
+ <svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M3 4.5h6a3 3 0 0 1 3 3v6" stroke="#f7f8f3" stroke-width="1.5"/><circle cx="3" cy="4.5" r="2" fill="#7396ff"/><circle cx="12" cy="13.5" r="2" fill="#63d297"/></svg>
268
+ </div>
269
+ <div class="brand-copy"><strong>Tubeless</strong><small>Local studio</small></div>
270
+ </div>
271
+ <div class="rail-label">Workspace</div>
272
+ <nav class="nav" aria-label="Studio sections">
273
+ <button class="hidden" data-view="pipelines" id="pipelineNav" title="Pipelines">
274
+ <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 5h7M4 10h12M9 15h7"/><circle cx="14.5" cy="5" r="1.5"/><circle cx="5.5" cy="15" r="1.5"/></svg>
275
+ <span>Pipelines</span><b class="nav-count" id="pipelineCount">0</b>
276
+ </button>
277
+ <button class="active" data-view="runs" title="Runs">
278
+ <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M10 3v4l2.5 1.5M17 10a7 7 0 1 1-2.05-4.95"/><path d="M14.5 2.8v3.5H18"/></svg>
279
+ <span>Runs</span><b class="nav-count" id="runCount">0</b>
280
+ </button>
281
+ </nav>
282
+ <div class="rail-foot">
283
+ <div class="connection"><i class="pulse"></i><span id="connectionLabel">Connected · local</span></div>
284
+ <div class="database">append-only SQLite</div>
285
+ </div>
286
+ </aside>
287
+ <main class="workspace">
288
+ <header class="topbar">
289
+ <div><div class="eyebrow">Execution workspace</div><h1 id="pageTitle">Runs</h1><p class="lede" id="pageLede">Live work and durable history in one place.</p></div>
290
+ <div class="toolbar">
291
+ <input class="search" id="search" type="search" placeholder="Filter runs or IDs" aria-label="Filter" />
292
+ <button class="primary-button hidden" id="launchButton">Run pipeline</button>
293
+ <button class="danger-button hidden" id="clearHistoryButton" type="button" title="Clear history"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true"><path d="M3 4.5h10M6 2.5h4M5 4.5l.5 9h5l.5-9M7 7v4M9 7v4"/></svg><span>Clear history</span></button>
294
+ <button class="icon-button" id="refresh" title="Refresh now" aria-label="Refresh now"><svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M16.5 7A7 7 0 1 0 17 11"/><path d="M16.5 3v4h-4"/></svg></button>
295
+ </div>
296
+ </header>
297
+ <section class="metrics" id="metrics"></section>
298
+ <section id="content"></section>
299
+ </main>
300
+ </div>
301
+ <div class="modal-backdrop hidden" id="launchModal" role="dialog" aria-modal="true" aria-labelledby="launchTitle">
302
+ <div class="modal">
303
+ <div class="modal-head">
304
+ <div><div class="detail-kicker">Configure pipeline</div><h2 id="launchTitle">Run a pipeline</h2><p id="launchDescription">Choose a declared pipeline and provide its inputs.</p></div>
305
+ <button class="close-button" id="closeLaunch" type="button" aria-label="Close">&times;</button>
306
+ </div>
307
+ <form class="launch-form" id="launchForm">
308
+ <div class="field"><label for="launchCommand">Pipeline command</label><select id="launchCommand"></select></div>
309
+ <div id="runPane"><div class="parameter-fields" id="parameterFields"></div></div>
310
+ <div class="plan-result hidden" id="planResult" aria-live="polite"></div>
311
+ <div class="launch-error hidden" id="launchError"></div>
312
+ <div class="modal-actions"><button class="secondary-button" id="cancelLaunch" type="button">Cancel</button><button class="secondary-button hidden" id="previewPlan" type="button">Preview plan</button><button class="primary-button" id="submitLaunch" type="submit">Run</button></div>
313
+ </form>
314
+ </div>
315
+ </div>
316
+ <div class="modal-backdrop hidden" id="clearHistoryModal" role="dialog" aria-modal="true" aria-labelledby="clearHistoryTitle">
317
+ <div class="modal confirm-modal">
318
+ <div class="modal-head">
319
+ <div><div class="detail-kicker">Local maintenance</div><h2 id="clearHistoryTitle">Clear run history?</h2><p>This permanently resets the local studio history.</p></div>
320
+ <button class="close-button" id="closeClearHistory" type="button" aria-label="Close">&times;</button>
321
+ </div>
322
+ <div class="confirm-body">
323
+ <p class="confirm-copy" id="clearHistoryCopy"></p>
324
+ <div class="confirm-warning">This cannot be undone. Pipeline definitions and execution remain unchanged; only recorded local events are removed.</div>
325
+ <div class="launch-error hidden" id="clearHistoryError"></div>
326
+ <div class="confirm-actions"><button class="secondary-button" id="cancelClearHistory" type="button">Cancel</button><button class="danger-button" id="confirmClearHistory" type="button">Clear history</button></div>
327
+ </div>
328
+ </div>
329
+ </div>
330
+ <div class="toast hidden" id="toast" role="status"></div>
331
+ <script>
332
+ const state = { snapshot: null, commands: [], view: "runs", selectedRunId: null, query: "", loading: false, launching: false, planning: false, clearing: false, canClearHistory: false, planVersion: 0 };
333
+ const $ = (selector) => document.querySelector(selector);
334
+ const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"})[char]);
335
+ const statusMark = (value) => value === 'completed' || value === 'complete' ? '✓' : value === 'skipped' ? '×' : value === 'cancelled' ? '■' : value === 'failed' ? '!' : value === 'planned' ? '…' : '';
336
+ const status = (value) => '<span class="status ' + esc(value) + '"><i class="status-mark" aria-hidden="true">' + statusMark(value) + '</i>' + esc(value) + '</span>';
337
+ const duration = (ms) => ms == null ? "—" : ms < 1000 ? Math.max(0, Math.round(ms)) + " ms" : ms < 60000 ? (ms / 1000).toFixed(ms < 10000 ? 1 : 0) + " s" : Math.floor(ms / 60000) + "m " + Math.round((ms % 60000) / 1000) + "s";
338
+ const clock = (ms) => new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" }).format(ms);
339
+ const dateTime = (ms) => new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "medium" }).format(ms);
340
+ const isoTime = (ms) => new Date(ms).toISOString();
341
+ const relative = (ms) => { const delta = Math.max(0, Date.now() - ms); if (delta < 60000) return Math.floor(delta / 1000) + "s ago"; if (delta < 3600000) return Math.floor(delta / 60000) + "m ago"; if (delta < 86400000) return Math.floor(delta / 3600000) + "h ago"; return Math.floor(delta / 86400000) + "d ago"; };
342
+ const shortId = (id) => id.length > 24 ? id.slice(0, 12) + "…" + id.slice(-7) : id;
343
+ function selectedCommand() { return state.commands.find((command) => command.id === $('#launchCommand').value); }
344
+ function parameterLabel(parameter) {
345
+ return parameter.key
346
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
347
+ .replace(/[-_]+/g, ' ')
348
+ .replace(/\b\w/g, (letter) => letter.toUpperCase());
349
+ }
350
+ function parameterHint(parameter) {
351
+ const details = [];
352
+ if (parameter.description) details.push(parameter.description);
353
+ if (parameter.environment) details.push('Environment fallback: ' + parameter.environment);
354
+ if (parameter.multiple) details.push(parameter.choices ? 'Choose one or more values.' : 'Enter one value per line.');
355
+ if (parameter.mustExist) details.push('Must be an existing ' + (parameter.pathKind || 'path') + '.');
356
+ return details.join(' ');
357
+ }
358
+ function parameterControl(parameter, index, scope) {
359
+ const id = scope + '-param-' + index;
360
+ const data = ' data-' + scope + '-parameter-index="' + index + '"';
361
+ const hint = parameterHint(parameter);
362
+ const labelText = parameterLabel(parameter);
363
+ if (parameter.type === 'boolean') {
364
+ return '<label class="boolean-field" for="' + id + '"><span><strong>' + esc(labelText) + '</strong><small>' + esc(hint || 'Enable this option.') + '</small></span><input id="' + id + '" type="checkbox"' + data + (parameter.default ? ' checked' : '') + '></label>';
365
+ }
366
+ const required = parameter.required ? ' required' : '';
367
+ const label = '<label for="' + id + '">' + esc(labelText) + (parameter.required ? '<span class="required-mark">required</span>' : '') + '</label>';
368
+ let control;
369
+ if (parameter.choices) {
370
+ const emptyOption = parameter.multiple || parameter.required ? '' : '<option value="">Use default or leave unset</option>';
371
+ const options = parameter.choices.map((choice) => '<option value="' + esc(choice) + '"' + (choice === parameter.default ? ' selected' : '') + '>' + esc(choice) + '</option>').join('');
372
+ control = '<select id="' + id + '"' + data + (parameter.multiple ? ' multiple' : '') + required + '>' + emptyOption + options + '</select>';
373
+ } else if (parameter.multiple) {
374
+ control = '<textarea id="' + id + '"' + data + ' spellcheck="false" placeholder="One value per line"></textarea>';
375
+ } else {
376
+ const inputType = parameter.type === 'number' ? 'number' : 'text';
377
+ const value = parameter.default === undefined ? '' : String(parameter.default);
378
+ const constraints = parameter.type === 'number' ? (parameter.integer ? ' step="1"' : ' step="any"') + (parameter.min === undefined ? '' : ' min="' + esc(parameter.min) + '"') + (parameter.max === undefined ? '' : ' max="' + esc(parameter.max) + '"') : '';
379
+ const placeholder = parameter.type === 'path' ? (parameter.pathKind === 'directory' ? './directory' : './file') : '';
380
+ control = '<input id="' + id + '" type="' + inputType + '"' + data + ' value="' + esc(value) + '" placeholder="' + placeholder + '"' + constraints + required + '>';
381
+ }
382
+ return '<div class="field">' + label + control + (hint ? '<div class="field-hint">' + esc(hint) + '</div>' : '') + '</div>';
383
+ }
384
+ function renderParameters(command, scope, keys) {
385
+ return command.parameters.map((parameter, index) => keys.has(parameter.key) ? parameterControl(parameter, index, scope) : '').join('');
386
+ }
387
+ function bindExclusiveSelections(command, scope) {
388
+ const control = (key) => {
389
+ const index = command.parameters.findIndex((parameter) => parameter.key === key);
390
+ return index < 0 ? null : document.querySelector('[data-' + scope + '-parameter-index="' + index + '"]');
391
+ };
392
+ const step = control('step');
393
+ const target = control('target');
394
+ const clear = (field) => {
395
+ if (!field) return;
396
+ if (field.tagName === 'SELECT') Array.from(field.options).forEach((option) => { option.selected = false; });
397
+ else field.value = '';
398
+ };
399
+ step?.addEventListener('change', () => { if (step.selectedOptions?.length || step.value) clear(target); });
400
+ target?.addEventListener('change', () => { if (target.selectedOptions?.length || target.value) clear(step); });
401
+ }
402
+ function renderCommandForm() {
403
+ const command = selectedCommand();
404
+ $('#launchTitle').textContent = command?.name || 'Run a pipeline';
405
+ $('#launchDescription').textContent = command ? commandDescription(command) : 'Choose a declared pipeline and provide its inputs.';
406
+ if (!command) {
407
+ $('#parameterFields').innerHTML = '';
408
+ $('#previewPlan').classList.add('hidden');
409
+ return;
410
+ }
411
+ const executionKeyNames = new Set(['dryRun', 'resume', 'step', 'target', 'continueOnError']);
412
+ const domainKeys = new Set(command.parameters.filter((parameter) => parameter.key !== 'plan' && !executionKeyNames.has(parameter.key)).map((parameter) => parameter.key));
413
+ const executionKeys = new Set(command.parameters.filter((parameter) => executionKeyNames.has(parameter.key)).map((parameter) => parameter.key));
414
+ const section = (title, note, keys) => keys.size ? '<section class="form-section"><div class="form-section-head"><strong>' + title + '</strong><span>' + note + '</span></div><div class="parameter-grid">' + renderParameters(command, 'run', keys) + '</div></section>' : '';
415
+ $('#parameterFields').innerHTML = section('Pipeline inputs', domainKeys.size + ' parameter' + (domainKeys.size === 1 ? '' : 's'), domainKeys) + section('Execution controls', 'Built into Tubeless', executionKeys);
416
+ bindExclusiveSelections(command, 'run');
417
+ $('#previewPlan').classList.toggle('hidden', !command.canPlan);
418
+ invalidatePlan();
419
+ }
420
+ function parameterValues(parameter, index, scope) {
421
+ const control = document.querySelector('[data-' + scope + '-parameter-index="' + index + '"]');
422
+ if (!control) return [];
423
+ if (parameter.type === 'boolean') return [control.checked];
424
+ const rawValues = parameter.multiple
425
+ ? control.tagName === 'SELECT'
426
+ ? Array.from(control.selectedOptions).map((option) => option.value)
427
+ : control.value.split(/\r?\n/).map((value) => value.trim()).filter(Boolean)
428
+ : [control.value];
429
+ return parameter.type === 'number'
430
+ ? rawValues.map((value) => value === '' ? value : Number(value))
431
+ : rawValues;
432
+ }
433
+ function parameterLaunchValues(command) {
434
+ const values = {};
435
+ command.parameters.forEach((parameter, index) => {
436
+ const parameterValue = parameterValues(parameter, index, 'run');
437
+ if (parameter.type === 'boolean') {
438
+ const checked = parameterValue[0];
439
+ if (checked !== Boolean(parameter.default)) values[parameter.key] = checked;
440
+ return;
441
+ }
442
+ if (parameter.multiple) {
443
+ values[parameter.key] = parameterValue;
444
+ return;
445
+ }
446
+ if (parameterValue[0] !== '') values[parameter.key] = parameterValue[0];
447
+ });
448
+ return values;
449
+ }
450
+ function currentPlanInput(command) {
451
+ const input = { dryRun: false, step: [], target: [] };
452
+ command.parameters.forEach((parameter, index) => {
453
+ const values = parameterValues(parameter, index, 'run');
454
+ if (parameter.key === 'dryRun') input.dryRun = values[0] === true;
455
+ if (parameter.key === 'step') input.step = values;
456
+ if (parameter.key === 'target') input.target = values;
457
+ });
458
+ return input;
459
+ }
460
+ function invalidatePlan() {
461
+ state.planVersion += 1;
462
+ $('#planResult').classList.add('hidden');
463
+ $('#planResult').innerHTML = '';
464
+ }
465
+ function showLaunchError(message) { $('#launchError').textContent = message; $('#launchError').classList.toggle('hidden', !message); }
466
+ function closeLaunch() { if (state.launching || state.planning) return; $('#launchModal').classList.add('hidden'); showLaunchError(''); }
467
+ function openLaunch(commandId) {
468
+ if (commandId) $('#launchCommand').value = commandId;
469
+ $('#launchModal').classList.remove('hidden');
470
+ renderCommandForm();
471
+ $('#parameterFields').querySelector('input, select, textarea')?.focus();
472
+ }
473
+ function showToast(message) { const toast = $('#toast'); toast.textContent = message; toast.classList.remove('hidden'); setTimeout(() => toast.classList.add('hidden'), 4200); }
474
+ function showClearHistoryError(message) { $('#clearHistoryError').textContent = message; $('#clearHistoryError').classList.toggle('hidden', !message); }
475
+ function closeClearHistory() { if (state.clearing) return; $('#clearHistoryModal').classList.add('hidden'); showClearHistoryError(''); }
476
+ function openClearHistory() {
477
+ if (!state.canClearHistory) return;
478
+ const runCount = state.snapshot?.runs.length ?? 0;
479
+ const eventCount = state.snapshot?.runs.reduce((total, run) => total + run.eventCount, 0) ?? 0;
480
+ const activeRunCount = state.snapshot?.activeRunCount ?? 0;
481
+ $('#clearHistoryCopy').textContent = 'Remove ' + runCount + ' recorded run' + (runCount === 1 ? '' : 's') + ' and ' + eventCount + ' event' + (eventCount === 1 ? '' : 's') + ' from this SQLite store.' + (activeRunCount ? ' ' + activeRunCount + ' recorded run' + (activeRunCount === 1 ? ' is' : 's are') + ' still marked active; continue only if no external process is writing to this store.' : '');
482
+ $('#clearHistoryModal').classList.remove('hidden');
483
+ $('#confirmClearHistory').focus();
484
+ }
485
+ async function clearHistory() {
486
+ if (state.clearing) return;
487
+ state.clearing = true;
488
+ const button = $('#confirmClearHistory');
489
+ button.disabled = true;
490
+ button.textContent = 'Clearing…';
491
+ showClearHistoryError('');
492
+ try {
493
+ const response = await fetch('/api/history', { method: 'DELETE', headers: { 'x-tubeless-studio-clear-history': '1' } });
494
+ const result = await response.json();
495
+ if (!response.ok || !result.cleared) throw new Error(result.error || 'History could not be cleared.');
496
+ state.selectedRunId = null;
497
+ $('#clearHistoryModal').classList.add('hidden');
498
+ showToast('Cleared ' + result.eventCount + ' recorded event' + (result.eventCount === 1 ? '' : 's'));
499
+ await refresh(true);
500
+ } catch (error) {
501
+ showClearHistoryError(error.message || String(error));
502
+ } finally {
503
+ state.clearing = false;
504
+ button.disabled = false;
505
+ button.textContent = 'Clear history';
506
+ }
507
+ }
508
+ function metrics(snapshot) {
509
+ const terminal = snapshot.completedRunCount + snapshot.failedRunCount;
510
+ const success = terminal ? Math.round(snapshot.completedRunCount / terminal * 100) : 0;
511
+ const pipelineCount = state.commands.length || snapshot.definitions.length;
512
+ return [
513
+ ["Active now", snapshot.activeRunCount, snapshot.activeRunCount ? "Live execution in progress" : "No work in flight"],
514
+ ["Recorded runs", snapshot.runs.length, "Append-only local history"],
515
+ ["Success rate", success + "%", terminal + " terminal runs"],
516
+ ["Pipelines", pipelineCount, state.commands.length ? "Available to configure" : "Observed in run history"],
517
+ ].map(([label, value, note]) => '<article class="metric"><div class="metric-label">' + label + '</div><div class="metric-value">' + value + '</div><div class="metric-note">' + note + '</div></article>').join("");
518
+ }
519
+ function empty(title, copy) {
520
+ return '<div class="empty"><div><div class="empty-icon"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M4 5.5h12M4 10h12M4 14.5h8"/></svg></div><strong>' + esc(title) + '</strong><p>' + esc(copy) + '</p></div></div>';
521
+ }
522
+ function commandDescription(command) {
523
+ return command.description || 'Run this typed pipeline command.';
524
+ }
525
+ function commandCatalog(commands) {
526
+ return '<section class="sheet"><div class="sheet-head"><div><div class="sheet-title">Available pipelines</div><div class="sheet-subtitle">Declared by the local studio manifest</div></div><span class="sheet-subtitle">' + commands.length + ' shown</span></div><div class="catalog">' + commands.map((command) => '<article class="catalog-card"><h3>' + esc(command.name) + '</h3><p>' + esc(commandDescription(command)) + '</p><div class="catalog-actions"><button class="primary-button" data-command-id="' + esc(command.id) + '">Configure</button></div></article>').join('') + '</div></section>';
527
+ }
528
+ function runRow(run) {
529
+ const activeStep = run.steps.find((step) => step.status === 'running');
530
+ const nestedCount = descendantsOf(run.runId).length;
531
+ const activity = run.status === 'running' ? '<div class="run-activity"><strong>' + esc(activeStep?.name || activeStep?.id || 'Starting') + '</strong><span>' + esc(activeStep?.progress?.message || 'Execution in progress') + '</span></div>' : '';
532
+ return '<button class="run-row ' + esc(run.status) + ' ' + (rootRunId(state.selectedRunId) === run.runId ? 'selected' : '') + '" data-run-id="' + esc(run.runId) + '"><div class="run-primary">' + status(run.status) + '<strong>' + esc(run.pipelineId) + '</strong><time class="run-time" datetime="' + isoTime(run.startedAtMs) + '" title="' + esc(dateTime(run.startedAtMs)) + '">' + relative(run.startedAtMs) + '</time></div><div class="run-secondary"><code>' + esc(shortId(run.runId)) + '</code><i class="dot"></i><span>' + duration(run.durationMs) + '</span><i class="dot"></i><span>' + run.steps.length + ' steps</span>' + (nestedCount ? '<i class="dot"></i><span>' + nestedCount + ' nested run' + (nestedCount === 1 ? '' : 's') + '</span>' : '') + (run.dryRun ? '<i class="dot"></i><span>dry run</span>' : '') + '</div>' + activity + '</button>';
533
+ }
534
+ function stepRow(step) {
535
+ const progressTotal = step.progress?.total;
536
+ const progressWidth = progressTotal ? Math.max(0, Math.min(100, step.progress.completed / progressTotal * 100)) : (step.status === "complete" ? 100 : 18);
537
+ const execution = step.attempt ? '<span class="execution-summary" title="' + esc(step.attempt.attemptId) + '"><b>Execution</b> · ' + esc(shortId(step.attempt.attemptId)) + (step.attempt.retries.length ? ' · ' + step.attempt.retries.length + ' retr' + (step.attempt.retries.length === 1 ? 'y' : 'ies') : '') + '</span>' : '';
538
+ const progress = step.progress ? '<div class="progress"><i style="width:' + progressWidth + '%"></i></div><div class="progress-copy">' + esc(step.progress.message || (step.progress.completed + (progressTotal ? ' / ' + progressTotal : '') + ' complete')) + '</div>' : '';
539
+ return '<article class="step ' + esc(step.status) + '">' + stepStatusIcon(step.status) + '<div class="step-head"><strong>' + esc(step.name || step.id) + '</strong>' + (step.name ? '<code>' + esc(step.id) + '</code>' : '') + '<span class="step-duration">' + duration(step.durationMs) + '</span></div>' + (step.description ? '<div class="step-description">' + esc(step.description) + '</div>' : '') + (execution ? '<div class="execution">' + execution + '</div>' : '') + progress + '</article>';
540
+ }
541
+ function stepStatusIcon(value) {
542
+ const label = value === 'complete' ? 'Completed' : value.charAt(0).toUpperCase() + value.slice(1);
543
+ const icon = value === 'complete' ? '<svg viewBox="0 0 12 12"><path d="m2 6 2.4 2.4L10 3"/></svg>' : value === 'running' ? '<i></i>' : value === 'skipped' ? '<svg viewBox="0 0 12 12"><path d="m3 3 6 6M9 3 3 9"/></svg>' : value === 'cancelled' ? '<svg viewBox="0 0 12 12"><rect x="3" y="3" width="6" height="6" rx="1" fill="currentColor" stroke="none"/></svg>' : value === 'failed' ? '!' : '…';
544
+ return '<span class="step-status-icon ' + esc(value) + '" role="img" aria-label="' + label + '" title="' + label + '">' + icon + '</span>';
545
+ }
546
+ function stepSummary(run) {
547
+ const labels = { running: 'running', failed: 'failed', cancelled: 'cancelled', skipped: 'skipped', complete: 'complete', planned: 'planned' };
548
+ const order = ['running', 'failed', 'cancelled', 'skipped', 'complete', 'planned'];
549
+ return order.map((value) => [value, run.steps.filter((step) => step.status === value).length]).filter(([, count]) => count).map(([value, count]) => count + ' ' + labels[value]).join(' · ') || 'No steps';
550
+ }
551
+ function runDetail(run) {
552
+ if (!run) return '<div class="sheet detail">' + empty('Select a run', 'Choose a run from the history to inspect its steps, retry telemetry, logs, and errors.') + '</div>';
553
+ const ancestors = ancestorsOf(run.runId);
554
+ const parentage = ancestors.length ? '<div class="run-parentage">' + ancestors.map((ancestor) => '<button type="button" data-detail-run-id="' + esc(ancestor.runId) + '">' + esc(ancestor.pipelineId) + '</button><span>/</span>').join('') + '<span>' + esc(run.pipelineId) + '</span></div>' : '';
555
+ const children = childrenOf(run.runId);
556
+ const nested = children.length ? '<div class="section-title"><span>Nested runs</span><span>' + children.length + ' direct · ' + descendantsOf(run.runId).length + ' total</span></div><div class="nested-runs">' + children.map((child) => '<button class="nested-run" type="button" data-detail-run-id="' + esc(child.runId) + '">' + status(child.status) + '<strong>' + esc(child.pipelineId) + '</strong><small>' + duration(child.durationMs) + ' · ' + child.steps.length + ' steps' + (descendantsOf(child.runId).length ? ' · ' + descendantsOf(child.runId).length + ' nested' : '') + '</small></button>').join('') + '</div>' : '';
557
+ const error = run.error ? '<div class="section-title"><span>Error</span></div><div class="error-card"><div class="error-code">' + esc(run.error.code) + ' · ' + esc(run.error.phase) + '</div><div class="error-message">' + esc(run.error.message) + '</div></div>' : '';
558
+ const logs = run.logs.length ? '<div class="section-title"><span>Logs</span><span>' + run.logs.length + '</span></div><div class="logs">' + run.logs.map((log) => '<div class="log-line"><time class="log-time">' + clock(log.timestampMs) + '</time><span class="log-level ' + esc(log.level) + '">' + esc(log.level) + '</span><span class="log-message">' + (log.stepId ? '<b>' + esc(log.stepId) + '</b> · ' : '') + esc(log.message) + '</span></div>').join('') + '</div>' : '';
559
+ return '<article class="sheet detail"><div class="detail-body">' + parentage + '<div class="detail-heading"><div style="min-width:0"><div class="detail-kicker">' + (run.parentRunId ? 'Nested run' : 'Top-level run') + ' · ' + relative(run.startedAtMs) + '</div><h2>' + esc(run.pipelineId) + '</h2><div class="run-id">' + esc(run.runId) + '</div></div><div style="margin-left:auto">' + status(run.status) + '</div></div><div class="detail-meta"><div><label>Started</label><span title="' + esc(isoTime(run.startedAtMs)) + '">' + esc(dateTime(run.startedAtMs)) + '</span></div><div><label>Duration</label><span>' + duration(run.durationMs) + '</span></div><div><label>Steps</label><span>' + run.steps.length + '</span></div><div><label>Events</label><span>' + run.eventCount + '</span></div></div>' + nested + '<div class="section-title"><span>Step timeline</span><span>' + stepSummary(run) + '</span></div>' + (run.steps.length ? '<div class="step-list">' + run.steps.map(stepRow).join('') + '</div>' : empty('No planned steps', 'This run ended before a step plan was recorded.')) + error + logs + '</div></article>';
560
+ }
561
+ function runById(runId) { return state.snapshot?.runs.find((run) => run.runId === runId); }
562
+ function childrenOf(runId) { return state.snapshot.runs.filter((run) => run.parentRunId === runId).sort((left, right) => right.startedAtMs - left.startedAtMs); }
563
+ function descendantsOf(runId, seen = new Set()) {
564
+ if (seen.has(runId)) return [];
565
+ seen.add(runId);
566
+ return childrenOf(runId).filter((child) => !seen.has(child.runId)).flatMap((child) => [child, ...descendantsOf(child.runId, seen)]);
567
+ }
568
+ function ancestorsOf(runId) {
569
+ const ancestors = [];
570
+ const seen = new Set();
571
+ let current = runById(runId);
572
+ while (current?.parentRunId && !seen.has(current.parentRunId)) {
573
+ seen.add(current.parentRunId);
574
+ const parent = runById(current.parentRunId);
575
+ if (!parent) break;
576
+ ancestors.unshift(parent);
577
+ current = parent;
578
+ }
579
+ return ancestors;
580
+ }
581
+ function rootRunId(runId) { return ancestorsOf(runId).at(0)?.runId || runId; }
582
+ function renderRuns() {
583
+ const query = state.query.toLowerCase();
584
+ const roots = state.snapshot.runs
585
+ .filter((run) => !run.parentRunId || !runById(run.parentRunId))
586
+ .filter((run) => !query || [run, ...descendantsOf(run.runId)].some((candidate) => candidate.pipelineId.toLowerCase().includes(query) || candidate.runId.toLowerCase().includes(query)))
587
+ .sort((left, right) => Number([right, ...descendantsOf(right.runId)].some((run) => run.status === 'running')) - Number([left, ...descendantsOf(left.runId)].some((run) => run.status === 'running')) || right.startedAtMs - left.startedAtMs);
588
+ if (!state.selectedRunId || !roots.some((run) => run.runId === rootRunId(state.selectedRunId))) state.selectedRunId = roots[0]?.runId ?? null;
589
+ const selected = state.snapshot.runs.find((run) => run.runId === state.selectedRunId);
590
+ const activeRuns = roots.filter((root) => [root, ...descendantsOf(root.runId)].some((run) => run.status === 'running'));
591
+ const historicalRuns = roots.filter((root) => !activeRuns.includes(root));
592
+ const activeList = activeRuns.length ? '<div class="run-group">Running now · ' + activeRuns.length + '</div>' + activeRuns.map(runRow).join('') : '';
593
+ const historyList = historicalRuns.length ? '<div class="run-group">Recent · ' + historicalRuns.length + '</div>' + historicalRuns.map(runRow).join('') : '';
594
+ $('#content').innerHTML = '<div class="content-grid"><section class="sheet"><div class="sheet-head"><div><div class="sheet-title">Pipeline runs</div><div class="sheet-subtitle">Top-level runs · nested work stays with its parent</div></div><span class="sheet-subtitle">' + roots.length + ' top-level · ' + state.snapshot.runs.length + ' total</span></div><div class="run-list">' + (roots.length ? activeList + historyList : empty('No recorded runs', 'Choose Pipelines to start a run and create local history.')) + '</div></section>' + runDetail(selected) + '</div>';
595
+ document.querySelectorAll('[data-run-id]').forEach((button) => button.addEventListener('click', () => { state.selectedRunId = button.dataset.runId; render(); }));
596
+ document.querySelectorAll('[data-detail-run-id]').forEach((button) => button.addEventListener('click', () => { state.selectedRunId = button.dataset.detailRunId; render(); }));
597
+ }
598
+ function renderPipelines() {
599
+ const query = state.query.toLowerCase();
600
+ const commands = state.commands.filter((command) => !query || command.name.toLowerCase().includes(query) || commandDescription(command).toLowerCase().includes(query));
601
+ $('#content').innerHTML = commands.length ? commandCatalog(commands) : '<div class="sheet">' + empty('No pipelines found', 'Try a different pipeline name or description.') + '</div>';
602
+ document.querySelectorAll('[data-command-id]').forEach((button) => button.addEventListener('click', () => openLaunch(button.dataset.commandId)));
603
+ }
604
+ function render() {
605
+ if (!state.snapshot) return;
606
+ const isPipelines = state.view === 'pipelines';
607
+ $('#pageTitle').textContent = isPipelines ? 'Pipelines' : 'Runs';
608
+ $('#pageLede').textContent = isPipelines ? 'Choose a declared workflow to configure and run.' : 'Live work and durable history in one place.';
609
+ $('#search').placeholder = isPipelines ? 'Filter pipelines' : 'Filter runs or IDs';
610
+ $('#metrics').innerHTML = metrics(state.snapshot);
611
+ $('#pipelineCount').textContent = state.commands.length;
612
+ $('#runCount').textContent = state.snapshot.runs.length;
613
+ $('#clearHistoryButton').disabled = state.snapshot.runs.length === 0;
614
+ $('#clearHistoryButton').title = state.snapshot.activeRunCount > 0 ? 'Clear history, including runs left active by an interrupted process' : 'Clear history';
615
+ document.querySelectorAll('[data-view]').forEach((button) => button.classList.toggle('active', button.dataset.view === state.view));
616
+ isPipelines ? renderPipelines() : renderRuns();
617
+ }
618
+ async function refresh(manual = false) {
619
+ if (state.loading) return;
620
+ state.loading = true;
621
+ if (manual) $('#refresh').classList.add('spinning');
622
+ try {
623
+ const response = await fetch('/api/snapshot', { cache: 'no-store' });
624
+ if (!response.ok) throw new Error('Snapshot request failed');
625
+ state.snapshot = await response.json();
626
+ $('#connectionLabel').textContent = 'Connected · local';
627
+ $('.pulse').style.background = '#63d297';
628
+ render();
629
+ } catch (error) {
630
+ $('#connectionLabel').textContent = 'Connection lost';
631
+ $('.pulse').style.background = '#ff8c84';
632
+ } finally {
633
+ state.loading = false;
634
+ $('#refresh').classList.remove('spinning');
635
+ }
636
+ }
637
+ async function loadCommands() {
638
+ try {
639
+ const response = await fetch('/api/commands', { cache: 'no-store' });
640
+ if (!response.ok) return;
641
+ const payload = await response.json();
642
+ state.commands = Array.isArray(payload.commands) ? payload.commands : [];
643
+ $('#pipelineNav').classList.toggle('hidden', state.commands.length === 0);
644
+ $('#launchButton').classList.toggle('hidden', state.commands.length === 0);
645
+ $('#launchCommand').innerHTML = state.commands.map((command) => '<option value="' + esc(command.id) + '">' + esc(command.name) + '</option>').join('');
646
+ if (state.commands.length > 0) state.view = 'pipelines';
647
+ renderCommandForm();
648
+ render();
649
+ } catch {}
650
+ }
651
+ async function loadCapabilities() {
652
+ try {
653
+ const response = await fetch('/api/capabilities', { cache: 'no-store' });
654
+ if (!response.ok) return;
655
+ const capabilities = await response.json();
656
+ state.canClearHistory = capabilities.canClearHistory === true;
657
+ $('#clearHistoryButton').classList.toggle('hidden', !state.canClearHistory);
658
+ render();
659
+ } catch {}
660
+ }
661
+ function renderPlan(plan) {
662
+ const selected = plan.steps.filter((step) => step.selected && !step.skipReason).length;
663
+ const errors = plan.errors.length ? '<div class="launch-error">' + plan.errors.map((error) => esc(error.message)).join('<br>') + '</div>' : '';
664
+ const steps = plan.steps.map((step) => {
665
+ const disposition = !step.selected ? 'Not selected' : step.skipReason === 'dry-run' ? 'Dry-run skip' : step.skipReason ? 'Skipped' : 'Will run';
666
+ const detail = step.description || (step.dependencies.length ? 'After ' + step.dependencies.join(', ') : 'No required dependencies');
667
+ const nested = step.nestedPipeline;
668
+ const kind = nested ? (nested.mode === 'for-each' ? 'Pipeline fan-out' : 'Nested pipeline') : 'Step';
669
+ const nestedDetail = nested ? '<div class="plan-nested"><strong>' + esc(nested.pipelineId) + '</strong><span>' + nested.stepIds.length + ' declared steps' + (nested.mode === 'for-each' ? ' per runtime item' : '') + '</span>' + nested.stepIds.map((stepId) => '<code>' + esc(stepId) + '</code>').join('') + '</div>' : '';
670
+ return '<div class="plan-step"><div class="plan-step-title"><strong>' + esc(step.name || step.id) + '</strong><span class="plan-kind' + (nested ? ' pipeline' : '') + '">' + kind + '</span></div><small>' + esc(detail) + '</small><span class="plan-disposition' + (disposition === 'Will run' ? '' : ' skipped') + '">' + disposition + '</span>' + nestedDetail + '</div>';
671
+ }).join('');
672
+ $('#planResult').innerHTML = errors + '<div class="plan-summary"><strong>' + esc(plan.pipelineId) + '</strong><span>' + selected + ' of ' + plan.steps.length + ' steps will run' + (plan.dryRun ? ' · dry run' : '') + '</span></div><div class="plan-steps">' + steps + '</div>';
673
+ $('#planResult').classList.remove('hidden');
674
+ }
675
+ async function previewPlan() {
676
+ const command = selectedCommand();
677
+ if (!command?.canPlan || state.planning) return;
678
+ state.planning = true;
679
+ const planVersion = state.planVersion;
680
+ $('#previewPlan').disabled = true;
681
+ $('#previewPlan').textContent = 'Planning…';
682
+ $('#submitLaunch').disabled = true;
683
+ showLaunchError('');
684
+ try {
685
+ const response = await fetch('/api/commands/' + encodeURIComponent(command.id) + '/plan', {
686
+ method: 'POST',
687
+ headers: { 'content-type': 'application/json', 'x-tubeless-studio-plan': '1' },
688
+ body: JSON.stringify(currentPlanInput(command)),
689
+ });
690
+ const result = await response.json();
691
+ if (!response.ok || !result.plan) throw new Error(result.error || 'Plan request failed.');
692
+ if (state.planVersion === planVersion) renderPlan(result.plan);
693
+ } catch (error) {
694
+ showLaunchError(error.message || String(error));
695
+ } finally {
696
+ state.planning = false;
697
+ $('#previewPlan').disabled = false;
698
+ $('#previewPlan').textContent = 'Preview plan';
699
+ $('#submitLaunch').disabled = false;
700
+ }
701
+ }
702
+ async function launch(event) {
703
+ event.preventDefault();
704
+ const command = selectedCommand();
705
+ if (!command || state.launching) return;
706
+ const values = parameterLaunchValues(command);
707
+ state.launching = true;
708
+ $('#previewPlan').disabled = true;
709
+ $('#submitLaunch').disabled = true;
710
+ $('#submitLaunch').textContent = 'Starting…';
711
+ showLaunchError('');
712
+ try {
713
+ const response = await fetch('/api/commands/' + encodeURIComponent(command.id) + '/runs', {
714
+ method: 'POST',
715
+ headers: { 'content-type': 'application/json', 'x-tubeless-studio-launch': '1' },
716
+ body: JSON.stringify({ values }),
717
+ });
718
+ const result = await response.json();
719
+ if (!response.ok || !result.accepted) throw new Error((result.errors || [result.error || 'Launch failed.']).join('\n'));
720
+ state.selectedRunId = result.runId;
721
+ state.view = 'runs';
722
+ $('#launchModal').classList.add('hidden');
723
+ renderCommandForm();
724
+ showToast('Run accepted · ' + shortId(result.runId));
725
+ setTimeout(() => refresh(true), 80);
726
+ } catch (error) {
727
+ showLaunchError(error.message || String(error));
728
+ } finally {
729
+ state.launching = false;
730
+ $('#previewPlan').disabled = false;
731
+ $('#submitLaunch').disabled = false;
732
+ $('#submitLaunch').textContent = 'Run';
733
+ }
734
+ }
735
+ document.querySelectorAll('[data-view]').forEach((button) => button.addEventListener('click', () => { state.view = button.dataset.view; render(); }));
736
+ $('#search').addEventListener('input', (event) => { state.query = event.target.value.trim(); render(); });
737
+ $('#refresh').addEventListener('click', () => refresh(true));
738
+ $('#launchButton').addEventListener('click', () => openLaunch());
739
+ $('#clearHistoryButton').addEventListener('click', openClearHistory);
740
+ $('#confirmClearHistory').addEventListener('click', clearHistory);
741
+ $('#closeClearHistory').addEventListener('click', closeClearHistory);
742
+ $('#cancelClearHistory').addEventListener('click', closeClearHistory);
743
+ $('#previewPlan').addEventListener('click', previewPlan);
744
+ $('#closeLaunch').addEventListener('click', closeLaunch);
745
+ $('#cancelLaunch').addEventListener('click', closeLaunch);
746
+ $('#launchCommand').addEventListener('change', renderCommandForm);
747
+ $('#runPane').addEventListener('input', invalidatePlan);
748
+ $('#launchForm').addEventListener('submit', launch);
749
+ $('#launchModal').addEventListener('click', (event) => { if (event.target === $('#launchModal')) closeLaunch(); });
750
+ $('#clearHistoryModal').addEventListener('click', (event) => { if (event.target === $('#clearHistoryModal')) closeClearHistory(); });
751
+ document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { closeLaunch(); closeClearHistory(); } });
752
+ loadCommands();
753
+ loadCapabilities();
754
+ refresh();
755
+ setInterval(() => refresh(), 1200);
756
+ </script>
757
+ </body>
758
+ </html>`;
759
+ //# sourceMappingURL=run-store-ui-page.js.map