plum-e2e 2.8.5 → 2.9.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 (235) hide show
  1. package/backend/_scaffold/utils/browser.ts +184 -30
  2. package/backend/_scaffold/utils/hooks.ts +37 -8
  3. package/backend/app.js +0 -5
  4. package/backend/config/scripts/generate-report.js +2 -2
  5. package/backend/constants/socketEvents.js +7 -4
  6. package/backend/lib/reportFilename.js +1 -2
  7. package/backend/lib/{screenshotPoller.js → rrwebPoller.js} +7 -4
  8. package/backend/lib/serverBootstrap.js +14 -0
  9. package/backend/logs/runner-cmtbz5b1l0000mr0110b27w5k.log +8 -0
  10. package/backend/mcp/server.js +3 -47
  11. package/backend/package-lock.json +199 -1
  12. package/backend/package.json +3 -1
  13. package/backend/prisma/migrations/20260828120000_add_recording_and_split_runner_worker_count/migration.sql +39 -0
  14. package/backend/prisma/migrations/20260828140000_add_recording_started_ended_at/migration.sql +5 -0
  15. package/backend/prisma/migrations/20260828150000_strip_screenshot_refs_from_reports/migration.sql +34 -0
  16. package/backend/prisma/migrations/20260828160000_add_backup_include_reports/migration.sql +4 -0
  17. package/backend/prisma/schema.prisma +97 -70
  18. package/backend/routes/backup.routes.js +47 -1
  19. package/backend/routes/reports.routes.js +23 -0
  20. package/backend/routes/test-cases.routes.js +7 -2
  21. package/backend/server.js +1 -1
  22. package/backend/services/backupCronService.js +1 -1
  23. package/backend/services/backupService.js +134 -44
  24. package/backend/services/cronService.js +23 -15
  25. package/backend/services/nodeExecutionService.js +41 -15
  26. package/backend/services/nodeStreamRegistry.js +24 -0
  27. package/backend/services/reportService.js +174 -84
  28. package/backend/services/runnerService.js +19 -7
  29. package/backend/services/settingsService.js +6 -3
  30. package/backend/services/testCaseService.js +3 -2
  31. package/backend/services/triggerService.js +20 -13
  32. package/backend/services/userService.js +1 -3
  33. package/backend/websockets/nodeSocketHandler.js +40 -0
  34. package/backend/websockets/socketHandler.js +9 -7
  35. package/frontend/.svelte-kit/adapter-node/.vite/manifest.json +1 -1
  36. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.C1TbMUws.css +6 -0
  37. package/frontend/.svelte-kit/adapter-node/chunks/internal.js +1 -1
  38. package/frontend/.svelte-kit/adapter-node/entries/pages/_page.svelte.js +22 -22
  39. package/frontend/.svelte-kit/adapter-node/entries/pages/settings/_page.svelte.js +3 -3
  40. package/frontend/.svelte-kit/adapter-node/manifest-full.js +1 -1
  41. package/frontend/.svelte-kit/adapter-node/manifest.js +1 -1
  42. package/frontend/.svelte-kit/adapter-node/nodes/0.js +1 -1
  43. package/frontend/.svelte-kit/adapter-node/nodes/1.js +1 -1
  44. package/frontend/.svelte-kit/adapter-node/nodes/11.js +1 -1
  45. package/frontend/.svelte-kit/adapter-node/nodes/12.js +1 -1
  46. package/frontend/.svelte-kit/adapter-node/nodes/2.js +2 -2
  47. package/frontend/.svelte-kit/adapter-node/nodes/3.js +1 -1
  48. package/frontend/.svelte-kit/adapter-node/nodes/5.js +1 -1
  49. package/frontend/.svelte-kit/adapter-node/nodes/6.js +1 -1
  50. package/frontend/.svelte-kit/adapter-node/nodes/8.js +1 -1
  51. package/frontend/.svelte-kit/adapter-node/nodes/9.js +1 -1
  52. package/frontend/.svelte-kit/ambient.d.ts +4 -0
  53. package/frontend/.svelte-kit/generated/server/internal.js +1 -1
  54. package/frontend/.svelte-kit/output/client/.vite/manifest.json +35 -35
  55. package/frontend/.svelte-kit/output/client/_app/immutable/assets/2.C1TbMUws.css +6 -0
  56. package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.C1TbMUws.css +6 -0
  57. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/{DEKAA-Fm.js → DDZw0L0T.js} +1 -1
  58. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/{Dhf4xQKo.js → kkA1mrsH.js} +2 -2
  59. package/frontend/.svelte-kit/output/client/_app/immutable/entry/{app.B0GH5ONo.js → app.KrtMZ3my.js} +2 -2
  60. package/frontend/.svelte-kit/output/client/_app/immutable/entry/{start.Bn25CW9X.js → start.4qxHHAcl.js} +1 -1
  61. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/{0.-imXt0f5.js → 0.C8DodwXs.js} +1 -1
  62. package/frontend/{build/client/_app/immutable/nodes/1.Dom3CKWF.js → .svelte-kit/output/client/_app/immutable/nodes/1.Df9sLw8b.js} +1 -1
  63. package/frontend/{build/client/_app/immutable/nodes/11.DMlayLCK.js → .svelte-kit/output/client/_app/immutable/nodes/11.BViaDK2m.js} +1 -1
  64. package/frontend/{build/client/_app/immutable/nodes/12.Bu89A1eX.js → .svelte-kit/output/client/_app/immutable/nodes/12.BMSCSd88.js} +1 -1
  65. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/2.BCPSg7H4.js +6 -0
  66. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/{3.B86rlHXU.js → 3.8BFAJoHT.js} +1 -1
  67. package/frontend/{build/client/_app/immutable/nodes/5.CqTXvadV.js → .svelte-kit/output/client/_app/immutable/nodes/5.DVeH2eQP.js} +1 -1
  68. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/{6.BJZ62MCd.js → 6.DI7-IJhN.js} +1 -1
  69. package/frontend/{build/client/_app/immutable/nodes/8.B4fj2Xyu.js → .svelte-kit/output/client/_app/immutable/nodes/8.93Mb3ofT.js} +4 -4
  70. package/frontend/{build/client/_app/immutable/nodes/9.C57_2xq6.js → .svelte-kit/output/client/_app/immutable/nodes/9.CvfGx3Hu.js} +1 -1
  71. package/frontend/.svelte-kit/output/client/_app/version.json +1 -1
  72. package/frontend/.svelte-kit/output/server/.vite/manifest.json +1 -1
  73. package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.C1TbMUws.css +6 -0
  74. package/frontend/.svelte-kit/output/server/chunks/internal.js +1 -1
  75. package/frontend/.svelte-kit/output/server/entries/pages/_page.svelte.js +22 -22
  76. package/frontend/.svelte-kit/output/server/entries/pages/settings/_page.svelte.js +3 -3
  77. package/frontend/.svelte-kit/output/server/manifest-full.js +1 -1
  78. package/frontend/.svelte-kit/output/server/manifest.js +1 -1
  79. package/frontend/.svelte-kit/output/server/nodes/0.js +1 -1
  80. package/frontend/.svelte-kit/output/server/nodes/1.js +1 -1
  81. package/frontend/.svelte-kit/output/server/nodes/11.js +1 -1
  82. package/frontend/.svelte-kit/output/server/nodes/12.js +1 -1
  83. package/frontend/.svelte-kit/output/server/nodes/2.js +2 -2
  84. package/frontend/.svelte-kit/output/server/nodes/3.js +1 -1
  85. package/frontend/.svelte-kit/output/server/nodes/5.js +1 -1
  86. package/frontend/.svelte-kit/output/server/nodes/6.js +1 -1
  87. package/frontend/.svelte-kit/output/server/nodes/8.js +1 -1
  88. package/frontend/.svelte-kit/output/server/nodes/9.js +1 -1
  89. package/frontend/build/client/_app/immutable/assets/2.C1TbMUws.css +6 -0
  90. package/frontend/build/client/_app/immutable/assets/2.C1TbMUws.css.br +0 -0
  91. package/frontend/build/client/_app/immutable/assets/2.C1TbMUws.css.gz +0 -0
  92. package/frontend/build/client/_app/immutable/assets/_page.C1TbMUws.css +6 -0
  93. package/frontend/build/client/_app/immutable/assets/_page.C1TbMUws.css.br +0 -0
  94. package/frontend/build/client/_app/immutable/assets/_page.C1TbMUws.css.gz +0 -0
  95. package/frontend/build/client/_app/immutable/chunks/{DEKAA-Fm.js → DDZw0L0T.js} +1 -1
  96. package/frontend/build/client/_app/immutable/chunks/DDZw0L0T.js.br +0 -0
  97. package/frontend/build/client/_app/immutable/chunks/DDZw0L0T.js.gz +0 -0
  98. package/frontend/build/client/_app/immutable/chunks/{Dhf4xQKo.js → kkA1mrsH.js} +2 -2
  99. package/frontend/build/client/_app/immutable/chunks/kkA1mrsH.js.br +0 -0
  100. package/frontend/build/client/_app/immutable/chunks/kkA1mrsH.js.gz +0 -0
  101. package/frontend/build/client/_app/immutable/entry/{app.B0GH5ONo.js → app.KrtMZ3my.js} +2 -2
  102. package/frontend/build/client/_app/immutable/entry/app.KrtMZ3my.js.br +0 -0
  103. package/frontend/build/client/_app/immutable/entry/app.KrtMZ3my.js.gz +0 -0
  104. package/frontend/build/client/_app/immutable/entry/{start.Bn25CW9X.js → start.4qxHHAcl.js} +1 -1
  105. package/frontend/build/client/_app/immutable/entry/{start.Bn25CW9X.js.br → start.4qxHHAcl.js.br} +0 -0
  106. package/frontend/build/client/_app/immutable/entry/{start.Bn25CW9X.js.gz → start.4qxHHAcl.js.gz} +0 -0
  107. package/frontend/build/client/_app/immutable/nodes/{0.-imXt0f5.js → 0.C8DodwXs.js} +1 -1
  108. package/frontend/build/client/_app/immutable/nodes/0.C8DodwXs.js.br +0 -0
  109. package/frontend/build/client/_app/immutable/nodes/0.C8DodwXs.js.gz +0 -0
  110. package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/1.Dom3CKWF.js → build/client/_app/immutable/nodes/1.Df9sLw8b.js} +1 -1
  111. package/frontend/build/client/_app/immutable/nodes/1.Df9sLw8b.js.br +0 -0
  112. package/frontend/build/client/_app/immutable/nodes/1.Df9sLw8b.js.gz +0 -0
  113. package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/11.DMlayLCK.js → build/client/_app/immutable/nodes/11.BViaDK2m.js} +1 -1
  114. package/frontend/build/client/_app/immutable/nodes/11.BViaDK2m.js.br +0 -0
  115. package/frontend/build/client/_app/immutable/nodes/11.BViaDK2m.js.gz +0 -0
  116. package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/12.Bu89A1eX.js → build/client/_app/immutable/nodes/12.BMSCSd88.js} +1 -1
  117. package/frontend/build/client/_app/immutable/nodes/12.BMSCSd88.js.br +0 -0
  118. package/frontend/build/client/_app/immutable/nodes/{12.Bu89A1eX.js.gz → 12.BMSCSd88.js.gz} +0 -0
  119. package/frontend/build/client/_app/immutable/nodes/2.BCPSg7H4.js +6 -0
  120. package/frontend/build/client/_app/immutable/nodes/2.BCPSg7H4.js.br +0 -0
  121. package/frontend/build/client/_app/immutable/nodes/2.BCPSg7H4.js.gz +0 -0
  122. package/frontend/build/client/_app/immutable/nodes/{3.B86rlHXU.js → 3.8BFAJoHT.js} +1 -1
  123. package/frontend/build/client/_app/immutable/nodes/3.8BFAJoHT.js.br +0 -0
  124. package/frontend/build/client/_app/immutable/nodes/3.8BFAJoHT.js.gz +0 -0
  125. package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/5.CqTXvadV.js → build/client/_app/immutable/nodes/5.DVeH2eQP.js} +1 -1
  126. package/frontend/build/client/_app/immutable/nodes/5.DVeH2eQP.js.br +0 -0
  127. package/frontend/build/client/_app/immutable/nodes/5.DVeH2eQP.js.gz +0 -0
  128. package/frontend/build/client/_app/immutable/nodes/{6.BJZ62MCd.js → 6.DI7-IJhN.js} +1 -1
  129. package/frontend/build/client/_app/immutable/nodes/6.DI7-IJhN.js.br +0 -0
  130. package/frontend/build/client/_app/immutable/nodes/6.DI7-IJhN.js.gz +0 -0
  131. package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/8.B4fj2Xyu.js → build/client/_app/immutable/nodes/8.93Mb3ofT.js} +4 -4
  132. package/frontend/build/client/_app/immutable/nodes/8.93Mb3ofT.js.br +0 -0
  133. package/frontend/build/client/_app/immutable/nodes/8.93Mb3ofT.js.gz +0 -0
  134. package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/9.C57_2xq6.js → build/client/_app/immutable/nodes/9.CvfGx3Hu.js} +1 -1
  135. package/frontend/build/client/_app/immutable/nodes/9.CvfGx3Hu.js.br +0 -0
  136. package/frontend/build/client/_app/immutable/nodes/9.CvfGx3Hu.js.gz +0 -0
  137. package/frontend/build/client/_app/version.json +1 -1
  138. package/frontend/build/client/_app/version.json.br +0 -0
  139. package/frontend/build/client/_app/version.json.gz +0 -0
  140. package/frontend/build/server/chunks/{0-DG1Uv07a.js → 0-D5YWFygh.js} +2 -2
  141. package/frontend/build/server/chunks/{0-DG1Uv07a.js.map → 0-D5YWFygh.js.map} +1 -1
  142. package/frontend/build/server/chunks/{1-iB0ogJU_.js → 1-Kxn9d30Z.js} +2 -2
  143. package/frontend/build/server/chunks/{1-iB0ogJU_.js.map → 1-Kxn9d30Z.js.map} +1 -1
  144. package/frontend/build/server/chunks/{11-Df9vGZ_v.js → 11-CNLerze_.js} +2 -2
  145. package/frontend/build/server/chunks/{11-Df9vGZ_v.js.map → 11-CNLerze_.js.map} +1 -1
  146. package/frontend/build/server/chunks/{12-DqvjCln4.js → 12-De6Ol6pu.js} +2 -2
  147. package/frontend/build/server/chunks/{12-DqvjCln4.js.map → 12-De6Ol6pu.js.map} +1 -1
  148. package/frontend/build/server/chunks/{2-Cte30Dgy.js → 2-Dc_rKWoK.js} +4 -4
  149. package/frontend/build/server/chunks/{2-Cte30Dgy.js.map → 2-Dc_rKWoK.js.map} +1 -1
  150. package/frontend/build/server/chunks/{3-BYKCZi4U.js → 3-DrRqVrcX.js} +2 -2
  151. package/frontend/build/server/chunks/{3-BYKCZi4U.js.map → 3-DrRqVrcX.js.map} +1 -1
  152. package/frontend/build/server/chunks/{5-B6Jc1foX.js → 5-CFeE5dPZ.js} +2 -2
  153. package/frontend/build/server/chunks/{5-B6Jc1foX.js.map → 5-CFeE5dPZ.js.map} +1 -1
  154. package/frontend/build/server/chunks/{6-dXZKtN86.js → 6-DZk37MZH.js} +2 -2
  155. package/frontend/build/server/chunks/{6-dXZKtN86.js.map → 6-DZk37MZH.js.map} +1 -1
  156. package/frontend/build/server/chunks/{8-D-Bp7HzS.js → 8-CKPVFOqJ.js} +3 -3
  157. package/frontend/build/server/chunks/{8-D-Bp7HzS.js.map → 8-CKPVFOqJ.js.map} +1 -1
  158. package/frontend/build/server/chunks/{9-BynjuGUO.js → 9-DirZG_Lu.js} +2 -2
  159. package/frontend/build/server/chunks/{9-BynjuGUO.js.map → 9-DirZG_Lu.js.map} +1 -1
  160. package/frontend/build/server/chunks/{_page.svelte-CpB-fgJl.js → _page.svelte-04WxjOuH.js} +23 -23
  161. package/frontend/build/server/chunks/_page.svelte-04WxjOuH.js.map +5 -0
  162. package/frontend/build/server/chunks/{_page.svelte-wKyUmA2e.js → _page.svelte-8gELc_B1.js} +4 -4
  163. package/frontend/build/server/chunks/_page.svelte-8gELc_B1.js.map +5 -0
  164. package/frontend/build/server/index.js +1 -1
  165. package/frontend/build/server/index.js.map +1 -1
  166. package/frontend/build/server/manifest.js +11 -11
  167. package/frontend/build/server/manifest.js.map +1 -1
  168. package/frontend/package-lock.json +121 -32
  169. package/frontend/package.json +1 -0
  170. package/frontend/src/lib/api/reports.js +13 -4
  171. package/frontend/src/lib/api/repository.js +2 -2
  172. package/frontend/src/lib/api/settings.js +16 -1
  173. package/frontend/src/lib/components/layout/RunnerPanel.svelte +9 -24
  174. package/frontend/src/lib/components/reports/ElementInspector.svelte +141 -0
  175. package/frontend/src/lib/components/reports/LiveReplayer.svelte +110 -0
  176. package/frontend/src/lib/components/reports/MultiTabTimeline.svelte +115 -0
  177. package/frontend/src/lib/components/reports/RecordingPlayer.svelte +786 -0
  178. package/frontend/src/lib/components/reports/StepsRail.svelte +109 -0
  179. package/frontend/src/lib/components/ui/CodeViewer.svelte +61 -0
  180. package/frontend/src/lib/constants.js +0 -1
  181. package/frontend/src/lib/copy/reports.js +18 -8
  182. package/frontend/src/lib/copy/repository.js +6 -0
  183. package/frontend/src/lib/copy/settings.js +25 -4
  184. package/frontend/src/lib/socketEvents.js +7 -4
  185. package/frontend/src/lib/stores/runner.js +26 -3
  186. package/frontend/src/lib/styles/tokens.css +7 -0
  187. package/frontend/src/lib/utils/format.js +108 -2
  188. package/frontend/src/lib/utils/inspectElement.js +34 -0
  189. package/frontend/src/routes/+page.svelte +6 -1
  190. package/frontend/src/routes/reports/+page.svelte +79 -1
  191. package/frontend/src/routes/reports/[id]/+page.svelte +304 -495
  192. package/frontend/src/routes/reports/live/+page.svelte +236 -260
  193. package/frontend/src/routes/settings/+page.svelte +246 -8
  194. package/frontend/src/routes/test-repository/suites/[id]/+page.svelte +116 -0
  195. package/package.json +1 -1
  196. package/backend/playwright.config.js +0 -85
  197. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.DvMkmYA-.css +0 -6
  198. package/frontend/.svelte-kit/output/client/_app/immutable/assets/2.DvMkmYA-.css +0 -6
  199. package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.DvMkmYA-.css +0 -6
  200. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/2.BnHalSCi.js +0 -6
  201. package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.DvMkmYA-.css +0 -6
  202. package/frontend/build/client/_app/immutable/assets/2.DvMkmYA-.css +0 -6
  203. package/frontend/build/client/_app/immutable/assets/2.DvMkmYA-.css.br +0 -0
  204. package/frontend/build/client/_app/immutable/assets/2.DvMkmYA-.css.gz +0 -0
  205. package/frontend/build/client/_app/immutable/assets/_page.DvMkmYA-.css +0 -6
  206. package/frontend/build/client/_app/immutable/assets/_page.DvMkmYA-.css.br +0 -0
  207. package/frontend/build/client/_app/immutable/assets/_page.DvMkmYA-.css.gz +0 -0
  208. package/frontend/build/client/_app/immutable/chunks/DEKAA-Fm.js.br +0 -0
  209. package/frontend/build/client/_app/immutable/chunks/DEKAA-Fm.js.gz +0 -0
  210. package/frontend/build/client/_app/immutable/chunks/Dhf4xQKo.js.br +0 -0
  211. package/frontend/build/client/_app/immutable/chunks/Dhf4xQKo.js.gz +0 -0
  212. package/frontend/build/client/_app/immutable/entry/app.B0GH5ONo.js.br +0 -0
  213. package/frontend/build/client/_app/immutable/entry/app.B0GH5ONo.js.gz +0 -0
  214. package/frontend/build/client/_app/immutable/nodes/0.-imXt0f5.js.br +0 -0
  215. package/frontend/build/client/_app/immutable/nodes/0.-imXt0f5.js.gz +0 -0
  216. package/frontend/build/client/_app/immutable/nodes/1.Dom3CKWF.js.br +0 -0
  217. package/frontend/build/client/_app/immutable/nodes/1.Dom3CKWF.js.gz +0 -0
  218. package/frontend/build/client/_app/immutable/nodes/11.DMlayLCK.js.br +0 -0
  219. package/frontend/build/client/_app/immutable/nodes/11.DMlayLCK.js.gz +0 -0
  220. package/frontend/build/client/_app/immutable/nodes/12.Bu89A1eX.js.br +0 -0
  221. package/frontend/build/client/_app/immutable/nodes/2.BnHalSCi.js +0 -6
  222. package/frontend/build/client/_app/immutable/nodes/2.BnHalSCi.js.br +0 -0
  223. package/frontend/build/client/_app/immutable/nodes/2.BnHalSCi.js.gz +0 -0
  224. package/frontend/build/client/_app/immutable/nodes/3.B86rlHXU.js.br +0 -0
  225. package/frontend/build/client/_app/immutable/nodes/3.B86rlHXU.js.gz +0 -0
  226. package/frontend/build/client/_app/immutable/nodes/5.CqTXvadV.js.br +0 -0
  227. package/frontend/build/client/_app/immutable/nodes/5.CqTXvadV.js.gz +0 -0
  228. package/frontend/build/client/_app/immutable/nodes/6.BJZ62MCd.js.br +0 -0
  229. package/frontend/build/client/_app/immutable/nodes/6.BJZ62MCd.js.gz +0 -0
  230. package/frontend/build/client/_app/immutable/nodes/8.B4fj2Xyu.js.br +0 -0
  231. package/frontend/build/client/_app/immutable/nodes/8.B4fj2Xyu.js.gz +0 -0
  232. package/frontend/build/client/_app/immutable/nodes/9.C57_2xq6.js.br +0 -0
  233. package/frontend/build/client/_app/immutable/nodes/9.C57_2xq6.js.gz +0 -0
  234. package/frontend/build/server/chunks/_page.svelte-CpB-fgJl.js.map +0 -5
  235. package/frontend/build/server/chunks/_page.svelte-wKyUmA2e.js.map +0 -5
@@ -7,10 +7,11 @@
7
7
  import { afterUpdate, onDestroy } from 'svelte';
8
8
  import { goto } from '$app/navigation';
9
9
  import { page } from '$app/stores';
10
- import { fly, fade } from 'svelte/transition';
10
+ import { fly } from 'svelte/transition';
11
11
  import { runnerState, cancelRun, backgroundRuns } from '$lib/stores/runner';
12
12
  import { reportUrl } from '$lib/api/reports';
13
- import { REDIRECT_DELAY_MS } from '$lib/constants';
13
+ import { REDIRECT_DELAY_MS, BUILTIN_RUNNER_ID } from '$lib/constants';
14
+ import LiveReplayer from '$lib/components/reports/LiveReplayer.svelte';
14
15
  import { ALL_TESTS_LABEL, pluralize } from '$lib/copy/common';
15
16
  import {
16
17
  REPORTS_BACK_LABEL,
@@ -26,8 +27,6 @@
26
27
  ALL_TESTS_PASSED,
27
28
  SOME_TESTS_FAILED,
28
29
  VIEW_REPORT_NOW_LABEL,
29
- LIVE_STEP_LABEL,
30
- LIVE_BROWSER_VIEW_ALT,
31
30
  AWAITING_STREAM_LABEL,
32
31
  NO_STREAM_LABEL,
33
32
  RUNNER_LABEL,
@@ -37,7 +36,8 @@
37
36
  runsInProgressHeading,
38
37
  workersCountLabel,
39
38
  redirectingIn,
40
- runnersBadge
39
+ runnersBadge,
40
+ workerLabel
41
41
  } from '$lib/copy/reports';
42
42
  import { triggerLabel } from '$lib/utils/format';
43
43
  import BackLink from '$lib/components/ui/BackLink.svelte';
@@ -51,7 +51,7 @@
51
51
  status: 'idle',
52
52
  lanes: [],
53
53
  currentRun: null,
54
- latestScreenshot: null
54
+ rrwebByLane: {}
55
55
  };
56
56
 
57
57
  let terminalEl;
@@ -60,6 +60,7 @@
60
60
  let redirectCountdown = REDIRECT_DELAY_MS / 1000;
61
61
  let countdownInterval = null;
62
62
  let activeTab = null;
63
+ let activeWorkerId = null;
63
64
 
64
65
  $: bgRunId = $page.url.searchParams.get('run');
65
66
  $: state = bgRunId ? ($backgroundRuns[bgRunId] ?? EMPTY_BG_STATE) : $runnerState;
@@ -82,6 +83,21 @@
82
83
  }
83
84
  $: activeLane = state.lanes.find((l) => l.id === activeTab) ?? null;
84
85
 
86
+ // rrwebByLane is always keyed by lane id, even for a plain single-runner
87
+ // run (BUILTIN_RUNNER_ID) — so worker tabs work the same way regardless of
88
+ // whether this run actually has multiple runner lanes.
89
+ $: activeLaneId = isMulti ? activeTab : BUILTIN_RUNNER_ID;
90
+ $: laneWorkers = state.rrwebByLane[activeLaneId] ?? {};
91
+ $: workerIds = Object.keys(laneWorkers).sort((a, b) => Number(a) - Number(b));
92
+ $: {
93
+ if (workerIds.length === 0) {
94
+ activeWorkerId = null;
95
+ } else if (!workerIds.includes(activeWorkerId)) {
96
+ activeWorkerId = workerIds[0];
97
+ }
98
+ }
99
+ $: activeWorkerEvents = activeWorkerId ? (laneWorkers[activeWorkerId]?.events ?? []) : [];
100
+
85
101
  afterUpdate(() => {
86
102
  if (terminalEl) terminalEl.scrollTop = terminalEl.scrollHeight;
87
103
  if (laneTerminalEl) laneTerminalEl.scrollTop = laneTerminalEl.scrollHeight;
@@ -110,9 +126,8 @@
110
126
 
111
127
  <svelte:head><title>{LIVE_PAGE_TITLE}</title></svelte:head>
112
128
 
113
- <BackLink href="/reports" label={REPORTS_BACK_LABEL} />
114
-
115
129
  {#if showIdle}
130
+ <BackLink href="/reports" label={REPORTS_BACK_LABEL} />
116
131
  <div class="idle-state">
117
132
  <div class="idle-icon">
118
133
  <svg
@@ -134,6 +149,7 @@
134
149
  <a href="/reports" class="idle-link">{VIEW_PAST_REPORTS_LINK}</a>
135
150
  </div>
136
151
  {:else if showPicker}
152
+ <BackLink href="/reports" label={REPORTS_BACK_LABEL} />
137
153
  <div class="cron-running-state">
138
154
  <div class="cron-running-icon"><span class="cron-pulse-dot"></span></div>
139
155
  <h2>{runsInProgressHeading(runningBgList.length)}</h2>
@@ -147,101 +163,47 @@
147
163
  <p>{SELECT_RUN_HINT}</p>
148
164
  </div>
149
165
  {:else}
150
- <!-- ── Run header ── -->
151
- <div
152
- class="run-header"
153
- class:header-pass={state.status === 'pass'}
154
- class:header-fail={state.status === 'fail'}
155
- >
156
- <div class="header-left">
157
- {#if state.running}
158
- <span class="live-badge"><span class="live-dot"></span>{LIVE_BADGE_LABEL}</span>
159
- {:else if state.status === 'pass'}
160
- <Badge variant="pass">{PASSED_LABEL}</Badge>
161
- {:else}
162
- <Badge variant="fail">{FAILED_LABEL}</Badge>
163
- {/if}
164
-
165
- {#if state.currentRun}
166
- <div class="run-info">
167
- {#if state.currentRun.runTitle}
168
- <span class="run-title-label">{state.currentRun.runTitle}</span>
169
- <span class="run-sep">·</span>
170
- {/if}
171
- <span class="run-tag-label">{state.currentRun.tag || ALL_TESTS_LABEL}</span>
172
- <span class="run-sep">·</span>
173
- <span class="run-detail">{workersCountLabel(state.currentRun.workers)}</span>
174
- <span class="run-sep">·</span>
175
- <span class="run-detail">{state.currentRun.browser}</span>
176
- {#if state.lanes.length > 1}
177
- <span class="run-sep">·</span>
178
- <span class="run-detail">{runnersBadge(state.lanes.length)}</span>
179
- {/if}
180
- </div>
181
- {/if}
182
- </div>
183
-
184
- {#if state.running && !bgRunId}
185
- <button class="cancel-btn" on:click={cancelRun}>
186
- <svg
187
- width="12"
188
- height="12"
189
- viewBox="0 0 24 24"
190
- fill="none"
191
- stroke="currentColor"
192
- stroke-width="2"
193
- stroke-linecap="round"
194
- stroke-linejoin="round"
195
- >
196
- <rect x="3" y="3" width="18" height="18" rx="2" />
197
- <line x1="9" y1="9" x2="15" y2="15" />
198
- <line x1="15" y1="9" x2="9" y2="15" />
199
- </svg>
200
- {CANCEL_RUN_LABEL}
201
- </button>
202
- {/if}
203
- </div>
204
-
205
- <!-- ── Completion bar ── -->
206
- {#if state.testCompleted && state.latestReportId}
166
+ <div class="live-fullscreen">
167
+ <!-- ── Run header ── -->
207
168
  <div
208
- class="completion-bar"
209
- class:pass-bar={state.status === 'pass'}
210
- class:fail-bar={state.status === 'fail'}
211
- transition:fly={{ y: -8, duration: 250 }}
169
+ class="run-header"
170
+ class:header-pass={state.status === 'pass'}
171
+ class:header-fail={state.status === 'fail'}
212
172
  >
213
- <div class="completion-left">
214
- {#if state.status === 'pass'}
215
- <svg
216
- width="16"
217
- height="16"
218
- viewBox="0 0 24 24"
219
- fill="none"
220
- stroke="currentColor"
221
- stroke-width="2.5"
222
- stroke-linecap="round"
223
- stroke-linejoin="round"><polyline points="20 6 9 17 4 12" /></svg
224
- >
225
- {ALL_TESTS_PASSED}
173
+ <div class="header-left">
174
+ <div class="header-back">
175
+ <BackLink href="/reports" label={REPORTS_BACK_LABEL} />
176
+ </div>
177
+
178
+ {#if state.running}
179
+ <span class="live-badge"><span class="live-dot"></span>{LIVE_BADGE_LABEL}</span>
180
+ {:else if state.status === 'pass'}
181
+ <Badge variant="pass">{PASSED_LABEL}</Badge>
226
182
  {:else}
227
- <svg
228
- width="16"
229
- height="16"
230
- viewBox="0 0 24 24"
231
- fill="none"
232
- stroke="currentColor"
233
- stroke-width="2.5"
234
- stroke-linecap="round"
235
- stroke-linejoin="round"
236
- ><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg
237
- >
238
- {SOME_TESTS_FAILED}
183
+ <Badge variant="fail">{FAILED_LABEL}</Badge>
184
+ {/if}
185
+
186
+ {#if state.currentRun}
187
+ <div class="run-info">
188
+ {#if state.currentRun.runTitle}
189
+ <span class="run-title-label">{state.currentRun.runTitle}</span>
190
+ <span class="run-sep">·</span>
191
+ {/if}
192
+ <span class="run-tag-label">{state.currentRun.tag || ALL_TESTS_LABEL}</span>
193
+ <span class="run-sep">·</span>
194
+ <span class="run-detail">{workersCountLabel(state.currentRun.workers)}</span>
195
+ <span class="run-sep">·</span>
196
+ <span class="run-detail">{state.currentRun.browser}</span>
197
+ {#if state.lanes.length > 1}
198
+ <span class="run-sep">·</span>
199
+ <span class="run-detail">{runnersBadge(state.lanes.length)}</span>
200
+ {/if}
201
+ </div>
239
202
  {/if}
240
203
  </div>
241
- <div class="completion-right">
242
- <span class="redirect-hint">{redirectingIn(redirectCountdown)}</span>
243
- <button class="view-now-btn" on:click={goNow}>
244
- {VIEW_REPORT_NOW_LABEL}
204
+
205
+ {#if state.running && !bgRunId}
206
+ <button class="cancel-btn" on:click={cancelRun}>
245
207
  <svg
246
208
  width="12"
247
209
  height="12"
@@ -251,55 +213,117 @@
251
213
  stroke-width="2"
252
214
  stroke-linecap="round"
253
215
  stroke-linejoin="round"
254
- ><line x1="5" y1="12" x2="19" y2="12" /><polyline points="12 5 19 12 12 19" /></svg
255
216
  >
217
+ <rect x="3" y="3" width="18" height="18" rx="2" />
218
+ <line x1="9" y1="9" x2="15" y2="15" />
219
+ <line x1="15" y1="9" x2="9" y2="15" />
220
+ </svg>
221
+ {CANCEL_RUN_LABEL}
256
222
  </button>
257
- </div>
223
+ {/if}
258
224
  </div>
259
- {/if}
260
-
261
- <!-- ── Multi-runner tabs ── -->
262
- {#if isMulti}
263
- <div class="lane-tabs">
264
- {#each state.lanes as lane}
265
- <button
266
- class="lane-tab"
267
- class:active={activeTab === lane.id}
268
- on:click={() => (activeTab = lane.id)}
269
- >
270
- <span
271
- class="tab-dot"
272
- class:tab-dot-running={lane.status === 'running'}
273
- class:tab-dot-done={lane.status === 'done'}
274
- class:tab-dot-error={lane.status === 'error'}
275
- ></span>
276
- {lane.name}
277
- {#if lane.testCount}
278
- <span class="tab-count">{lane.testCount}</span>
225
+
226
+ <!-- ── Completion bar ── -->
227
+ {#if state.testCompleted && state.latestReportId}
228
+ <div
229
+ class="completion-bar"
230
+ class:pass-bar={state.status === 'pass'}
231
+ class:fail-bar={state.status === 'fail'}
232
+ transition:fly={{ y: -8, duration: 250 }}
233
+ >
234
+ <div class="completion-left">
235
+ {#if state.status === 'pass'}
236
+ <svg
237
+ width="16"
238
+ height="16"
239
+ viewBox="0 0 24 24"
240
+ fill="none"
241
+ stroke="currentColor"
242
+ stroke-width="2.5"
243
+ stroke-linecap="round"
244
+ stroke-linejoin="round"><polyline points="20 6 9 17 4 12" /></svg
245
+ >
246
+ {ALL_TESTS_PASSED}
247
+ {:else}
248
+ <svg
249
+ width="16"
250
+ height="16"
251
+ viewBox="0 0 24 24"
252
+ fill="none"
253
+ stroke="currentColor"
254
+ stroke-width="2.5"
255
+ stroke-linecap="round"
256
+ stroke-linejoin="round"
257
+ ><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg
258
+ >
259
+ {SOME_TESTS_FAILED}
279
260
  {/if}
280
- </button>
281
- {/each}
282
- </div>
283
- {/if}
284
-
285
- <!-- ── Dual-stream view ── -->
286
- <div class="run-view" class:no-top-radius={isMulti}>
287
- <!-- Screenshot panel -->
288
- <div class="screenshot-panel">
289
- {#if isMulti}
290
- {#if activeLane?.latestScreenshot}
291
- {#key activeLane.latestScreenshot.data}
292
- <img
293
- src="data:image/jpeg;base64,{activeLane.latestScreenshot.data}"
294
- class="live-shot"
295
- alt={LIVE_BROWSER_VIEW_ALT}
296
- in:fade={{ duration: 120 }}
297
- />
298
- {/key}
299
- <div class="step-overlay">
300
- <span class="step-keyword">{LIVE_STEP_LABEL}</span>
301
- <span class="step-name">{activeLane.latestScreenshot.stepName}</span>
302
- </div>
261
+ </div>
262
+ <div class="completion-right">
263
+ <span class="redirect-hint">{redirectingIn(redirectCountdown)}</span>
264
+ <button class="view-now-btn" on:click={goNow}>
265
+ {VIEW_REPORT_NOW_LABEL}
266
+ <svg
267
+ width="12"
268
+ height="12"
269
+ viewBox="0 0 24 24"
270
+ fill="none"
271
+ stroke="currentColor"
272
+ stroke-width="2"
273
+ stroke-linecap="round"
274
+ stroke-linejoin="round"
275
+ ><line x1="5" y1="12" x2="19" y2="12" /><polyline points="12 5 19 12 12 19" /></svg
276
+ >
277
+ </button>
278
+ </div>
279
+ </div>
280
+ {/if}
281
+
282
+ <!-- ── Multi-runner tabs ── -->
283
+ {#if isMulti}
284
+ <div class="lane-tabs">
285
+ {#each state.lanes as lane}
286
+ <button
287
+ class="lane-tab"
288
+ class:active={activeTab === lane.id}
289
+ on:click={() => (activeTab = lane.id)}
290
+ >
291
+ <span
292
+ class="tab-dot"
293
+ class:tab-dot-running={lane.status === 'running'}
294
+ class:tab-dot-done={lane.status === 'done'}
295
+ class:tab-dot-error={lane.status === 'error'}
296
+ ></span>
297
+ {lane.name}
298
+ {#if lane.testCount}
299
+ <span class="tab-count">{lane.testCount}</span>
300
+ {/if}
301
+ </button>
302
+ {/each}
303
+ </div>
304
+ {/if}
305
+
306
+ <!-- ── Worker tabs (only when this lane has more than one) ── -->
307
+ {#if workerIds.length > 1}
308
+ <div class="lane-tabs worker-tabs">
309
+ {#each workerIds as workerId}
310
+ <button
311
+ class="lane-tab"
312
+ class:active={activeWorkerId === workerId}
313
+ on:click={() => (activeWorkerId = workerId)}
314
+ >
315
+ {workerLabel(workerId)}
316
+ </button>
317
+ {/each}
318
+ </div>
319
+ {/if}
320
+
321
+ <!-- ── Dual-stream view ── -->
322
+ <div class="run-view">
323
+ <!-- Live stream panel -->
324
+ <div class="stream-panel">
325
+ {#if activeWorkerEvents.length > 0}
326
+ <LiveReplayer events={activeWorkerEvents} />
303
327
  {:else}
304
328
  <div class="awaiting-state">
305
329
  <svg
@@ -316,64 +340,32 @@
316
340
  <rect x="2" y="5" width="20" height="14" rx="2" />
317
341
  <circle cx="12" cy="12" r="3" />
318
342
  </svg>
319
- <span>{activeLane?.status === 'running' ? AWAITING_STREAM_LABEL : NO_STREAM_LABEL}</span
320
- >
343
+ <span>{state.running ? AWAITING_STREAM_LABEL : NO_STREAM_LABEL}</span>
321
344
  </div>
322
345
  {/if}
323
- {:else if state.latestScreenshot}
324
- {#key state.latestScreenshot.data}
325
- <img
326
- src="data:image/jpeg;base64,{state.latestScreenshot.data}"
327
- class="live-shot"
328
- alt={LIVE_BROWSER_VIEW_ALT}
329
- in:fade={{ duration: 120 }}
330
- />
331
- {/key}
332
- <div class="step-overlay">
333
- <span class="step-keyword">{LIVE_STEP_LABEL}</span>
334
- <span class="step-name">{state.latestScreenshot.stepName}</span>
335
- </div>
336
- {:else}
337
- <div class="awaiting-state">
338
- <svg
339
- width="32"
340
- height="32"
341
- viewBox="0 0 24 24"
342
- fill="none"
343
- stroke="currentColor"
344
- stroke-width="1.2"
345
- stroke-linecap="round"
346
- stroke-linejoin="round"
347
- opacity="0.25"
348
- >
349
- <rect x="2" y="5" width="20" height="14" rx="2" />
350
- <circle cx="12" cy="12" r="3" />
351
- </svg>
352
- <span>{state.running ? AWAITING_STREAM_LABEL : NO_STREAM_LABEL}</span>
353
- </div>
354
- {/if}
355
- </div>
346
+ </div>
356
347
 
357
- <!-- Terminal panel -->
358
- <div class="terminal-panel">
359
- <div class="terminal-bar">
360
- <span class="dot red"></span>
361
- <span class="dot yellow"></span>
362
- <span class="dot green"></span>
363
- <span class="terminal-label">
364
- {#if isMulti}
365
- {activeLane?.name ?? RUNNER_LABEL}
366
- {:else}
367
- {state.running ? RUNNING_LABEL : FINISHED_LABEL}
368
- {/if}
369
- </span>
348
+ <!-- Terminal panel -->
349
+ <div class="terminal-panel">
350
+ <div class="terminal-bar">
351
+ <span class="dot red"></span>
352
+ <span class="dot yellow"></span>
353
+ <span class="dot green"></span>
354
+ <span class="terminal-label">
355
+ {#if isMulti}
356
+ {activeLane?.name ?? RUNNER_LABEL}
357
+ {:else}
358
+ {state.running ? RUNNING_LABEL : FINISHED_LABEL}
359
+ {/if}
360
+ </span>
361
+ </div>
362
+ {#if isMulti}
363
+ <pre class="terminal" bind:this={laneTerminalEl}>{activeLane?.logs ||
364
+ WAITING_FOR_OUTPUT}</pre>
365
+ {:else}
366
+ <pre class="terminal" bind:this={terminalEl}>{state.output}</pre>
367
+ {/if}
370
368
  </div>
371
- {#if isMulti}
372
- <pre class="terminal" bind:this={laneTerminalEl}>{activeLane?.logs ||
373
- WAITING_FOR_OUTPUT}</pre>
374
- {:else}
375
- <pre class="terminal" bind:this={terminalEl}>{state.output}</pre>
376
- {/if}
377
369
  </div>
378
370
  </div>
379
371
  {/if}
@@ -476,18 +468,35 @@
476
468
  max-width: 360px;
477
469
  }
478
470
 
471
+ /* ── Fullscreen run shell ──
472
+ Breaks out of PageShell's centered 1200px column so the live stream gets
473
+ the whole viewport; height is pinned to the viewport (minus Nav) so the
474
+ stream/log split scrolls internally instead of the page. */
475
+ .live-fullscreen {
476
+ width: 100vw;
477
+ position: relative;
478
+ left: 50%;
479
+ margin-left: -50vw;
480
+ margin-top: -2.5rem;
481
+ margin-bottom: -5rem;
482
+ padding-bottom: 5rem;
483
+ height: calc(100vh - 56px);
484
+ box-sizing: border-box;
485
+ display: flex;
486
+ flex-direction: column;
487
+ }
488
+
479
489
  /* ── Run header ── */
480
490
  .run-header {
481
491
  display: flex;
482
492
  align-items: center;
483
493
  justify-content: space-between;
484
494
  gap: 1rem;
485
- padding: 0.875rem 1.25rem;
495
+ padding: 0.75rem clamp(1rem, 3vw, 2rem);
486
496
  background: var(--bg-elevated);
487
- border: 1px solid var(--border);
488
- border-bottom: none;
489
- border-radius: var(--radius-lg) var(--radius-lg) 0 0;
497
+ border-bottom: 1px solid var(--border);
490
498
  animation: fadeUp 0.3s var(--ease-out) both;
499
+ flex-shrink: 0;
491
500
  }
492
501
  .run-header.header-pass {
493
502
  border-top: 3px solid var(--pass);
@@ -503,6 +512,16 @@
503
512
  flex-wrap: wrap;
504
513
  }
505
514
 
515
+ .header-back {
516
+ display: flex;
517
+ align-items: center;
518
+ padding-right: 0.875rem;
519
+ border-right: 1px solid var(--border);
520
+ }
521
+ .header-back :global(.back-row) {
522
+ margin-bottom: 0;
523
+ }
524
+
506
525
  .live-badge {
507
526
  display: inline-flex;
508
527
  align-items: center;
@@ -585,20 +604,19 @@
585
604
  display: flex;
586
605
  align-items: center;
587
606
  justify-content: space-between;
588
- padding: 0.75rem 1.25rem;
607
+ padding: 0.75rem clamp(1rem, 3vw, 2rem);
589
608
  gap: 1rem;
590
- border: 1px solid var(--border);
591
- border-top: none;
592
- border-bottom: none;
609
+ border-bottom: 1px solid var(--border);
610
+ flex-shrink: 0;
593
611
  }
594
612
  .completion-bar.pass-bar {
595
613
  background: var(--pass-soft);
596
- border-color: color-mix(in srgb, var(--pass) 30%, var(--border));
614
+ border-bottom-color: color-mix(in srgb, var(--pass) 30%, var(--border));
597
615
  color: var(--pass);
598
616
  }
599
617
  .completion-bar.fail-bar {
600
618
  background: var(--fail-soft);
601
- border-color: color-mix(in srgb, var(--fail) 30%, var(--border));
619
+ border-bottom-color: color-mix(in srgb, var(--fail) 30%, var(--border));
602
620
  color: var(--fail);
603
621
  }
604
622
  .completion-left {
@@ -642,11 +660,10 @@
642
660
  .lane-tabs {
643
661
  display: flex;
644
662
  gap: 2px;
645
- padding: 0.5rem 1rem 0;
663
+ padding: 0.5rem clamp(1rem, 3vw, 2rem) 0;
646
664
  background: var(--bg-elevated);
647
- border: 1px solid var(--border);
648
- border-top: none;
649
- border-bottom: none;
665
+ border-bottom: 1px solid var(--border);
666
+ flex-shrink: 0;
650
667
  }
651
668
  .lane-tab {
652
669
  display: inline-flex;
@@ -701,24 +718,20 @@
701
718
  opacity: 0.55;
702
719
  }
703
720
 
704
- /* ── Dual-stream view ── */
721
+ /* ── Dual-stream view ──
722
+ Fills whatever vertical space live-fullscreen has left over after the
723
+ header/tabs above it, rather than a fixed viewport-relative height. */
705
724
  .run-view {
706
725
  display: grid;
707
- grid-template-columns: 1fr 420px;
708
- height: calc(100vh - 290px);
709
- min-height: 380px;
710
- border: 1px solid var(--border);
711
- border-radius: 0 0 var(--radius-lg) var(--radius-lg);
712
- border-top: none;
726
+ grid-template-columns: 1fr 340px;
727
+ flex: 1;
728
+ min-height: 0;
713
729
  overflow: hidden;
714
730
  animation: fadeUp 0.35s var(--ease-out) 0.05s both;
715
731
  }
716
- .run-view.no-top-radius {
717
- border-radius: 0 0 var(--radius-lg) var(--radius-lg);
718
- }
719
732
 
720
- /* ── Screenshot panel ── */
721
- .screenshot-panel {
733
+ /* ── Live stream panel ── */
734
+ .stream-panel {
722
735
  position: relative;
723
736
  background: var(--terminal-bg);
724
737
  display: flex;
@@ -728,43 +741,6 @@
728
741
  border-right: 1px solid var(--border);
729
742
  }
730
743
 
731
- .live-shot {
732
- max-width: 100%;
733
- max-height: 100%;
734
- object-fit: contain;
735
- display: block;
736
- border-radius: 2px;
737
- }
738
-
739
- .step-overlay {
740
- position: absolute;
741
- bottom: 0;
742
- left: 0;
743
- right: 0;
744
- padding: 1.25rem 1rem 0.625rem;
745
- background: linear-gradient(transparent, rgba(0, 0, 0, 0.75));
746
- display: flex;
747
- align-items: baseline;
748
- gap: 0.5rem;
749
- pointer-events: none;
750
- }
751
- .step-keyword {
752
- font-size: 0.65rem;
753
- font-weight: 700;
754
- letter-spacing: 0.1em;
755
- text-transform: uppercase;
756
- color: var(--accent);
757
- flex-shrink: 0;
758
- }
759
- .step-name {
760
- font-size: 0.8125rem;
761
- color: rgba(255, 255, 255, 0.85);
762
- font-family: 'JetBrains Mono', monospace;
763
- white-space: nowrap;
764
- overflow: hidden;
765
- text-overflow: ellipsis;
766
- }
767
-
768
744
  .awaiting-state {
769
745
  display: flex;
770
746
  flex-direction: column;