@agimon-ai/po-runner 0.0.0-alpha.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 (322) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +257 -0
  3. package/dist/ansiScrub-CsqTSaVT.mjs +71 -0
  4. package/dist/ansiScrub-CsqTSaVT.mjs.map +1 -0
  5. package/dist/ansiScrub-D5gpHDch.cjs +100 -0
  6. package/dist/ansiScrub-D5gpHDch.cjs.map +1 -0
  7. package/dist/ansiScrub.cjs +7 -0
  8. package/dist/ansiScrub.d.mts +26 -0
  9. package/dist/ansiScrub.d.mts.map +1 -0
  10. package/dist/ansiScrub.mjs +2 -0
  11. package/dist/apiContracts.cjs +162 -0
  12. package/dist/apiContracts.cjs.map +1 -0
  13. package/dist/apiContracts.d.mts +53 -0
  14. package/dist/apiContracts.d.mts.map +1 -0
  15. package/dist/apiContracts.mjs +157 -0
  16. package/dist/apiContracts.mjs.map +1 -0
  17. package/dist/bashResult-DNgtahGR.cjs +629 -0
  18. package/dist/bashResult-DNgtahGR.cjs.map +1 -0
  19. package/dist/bashResult-DcrRaBLv.d.mts +49 -0
  20. package/dist/bashResult-DcrRaBLv.d.mts.map +1 -0
  21. package/dist/bashResult-bIVSzoj-.mjs +546 -0
  22. package/dist/bashResult-bIVSzoj-.mjs.map +1 -0
  23. package/dist/bashRunService-CJ02XaAq.mjs +440 -0
  24. package/dist/bashRunService-CJ02XaAq.mjs.map +1 -0
  25. package/dist/bashRunService-Ci0cGdYO.d.mts +71 -0
  26. package/dist/bashRunService-Ci0cGdYO.d.mts.map +1 -0
  27. package/dist/bashRunService-DyqxOSlx.cjs +445 -0
  28. package/dist/bashRunService-DyqxOSlx.cjs.map +1 -0
  29. package/dist/bashRunService.cjs +3 -0
  30. package/dist/bashRunService.d.mts +36 -0
  31. package/dist/bashRunService.d.mts.map +1 -0
  32. package/dist/bashRunService.mjs +2 -0
  33. package/dist/bashSchema.cjs +5 -0
  34. package/dist/bashSchema.d.mts +7 -0
  35. package/dist/bashSchema.d.mts.map +1 -0
  36. package/dist/bashSchema.mjs +2 -0
  37. package/dist/bashTool-B1EwZLo_.d.mts +20 -0
  38. package/dist/bashTool-B1EwZLo_.d.mts.map +1 -0
  39. package/dist/bashTool-C-k7fmyh.mjs +24 -0
  40. package/dist/bashTool-C-k7fmyh.mjs.map +1 -0
  41. package/dist/bashTool-CdgtqVFD.cjs +41 -0
  42. package/dist/bashTool-CdgtqVFD.cjs.map +1 -0
  43. package/dist/bashTool.cjs +13 -0
  44. package/dist/bashTool.cjs.map +1 -0
  45. package/dist/bashTool.d.mts +8 -0
  46. package/dist/bashTool.d.mts.map +1 -0
  47. package/dist/bashTool.mjs +11 -0
  48. package/dist/bashTool.mjs.map +1 -0
  49. package/dist/bin/cli.cjs +78 -0
  50. package/dist/bin/cli.cjs.map +1 -0
  51. package/dist/bin/cli.d.mts +4 -0
  52. package/dist/bin/cli.d.mts.map +1 -0
  53. package/dist/bin/cli.mjs +77 -0
  54. package/dist/bin/cli.mjs.map +1 -0
  55. package/dist/bin/logSink.cjs +102 -0
  56. package/dist/bin/logSink.cjs.map +1 -0
  57. package/dist/bin/logSink.d.mts +4 -0
  58. package/dist/bin/logSink.d.mts.map +1 -0
  59. package/dist/bin/logSink.mjs +99 -0
  60. package/dist/bin/logSink.mjs.map +1 -0
  61. package/dist/bin/runnerHost.cjs +131 -0
  62. package/dist/bin/runnerHost.cjs.map +1 -0
  63. package/dist/bin/runnerHost.d.mts +4 -0
  64. package/dist/bin/runnerHost.d.mts.map +1 -0
  65. package/dist/bin/runnerHost.mjs +127 -0
  66. package/dist/bin/runnerHost.mjs.map +1 -0
  67. package/dist/cliApp-Dj9nwaOv.cjs +244 -0
  68. package/dist/cliApp-Dj9nwaOv.cjs.map +1 -0
  69. package/dist/cliApp-v8Z3rMCi.mjs +237 -0
  70. package/dist/cliApp-v8Z3rMCi.mjs.map +1 -0
  71. package/dist/cliApp.cjs +3 -0
  72. package/dist/cliApp.d.mts +18 -0
  73. package/dist/cliApp.d.mts.map +1 -0
  74. package/dist/cliApp.mjs +2 -0
  75. package/dist/clock-Cv2pddgo.mjs +16 -0
  76. package/dist/clock-Cv2pddgo.mjs.map +1 -0
  77. package/dist/clock-euJkweLq.d.mts +11 -0
  78. package/dist/clock-euJkweLq.d.mts.map +1 -0
  79. package/dist/clock-jqnLWziT.cjs +21 -0
  80. package/dist/clock-jqnLWziT.cjs.map +1 -0
  81. package/dist/clock.cjs +3 -0
  82. package/dist/clock.d.mts +10 -0
  83. package/dist/clock.d.mts.map +1 -0
  84. package/dist/clock.mjs +2 -0
  85. package/dist/compaction-4QSUsBxq.cjs +40 -0
  86. package/dist/compaction-4QSUsBxq.cjs.map +1 -0
  87. package/dist/compaction-DCw80VZj.mjs +29 -0
  88. package/dist/compaction-DCw80VZj.mjs.map +1 -0
  89. package/dist/compaction.cjs +4 -0
  90. package/dist/compaction.d.mts +10 -0
  91. package/dist/compaction.d.mts.map +1 -0
  92. package/dist/compaction.mjs +2 -0
  93. package/dist/config.cjs +41 -0
  94. package/dist/config.d.mts +2 -0
  95. package/dist/config.mjs +2 -0
  96. package/dist/extensions/mcp-ui.mjs +23 -0
  97. package/dist/extensions/mcp-ui.mjs.map +1 -0
  98. package/dist/extensions/mcp.cjs +142 -0
  99. package/dist/extensions/mcp.cjs.map +1 -0
  100. package/dist/extensions/mcp.d.mts +5 -0
  101. package/dist/extensions/mcp.d.mts.map +1 -0
  102. package/dist/extensions/mcp.mjs +114 -0
  103. package/dist/extensions/mcp.mjs.map +1 -0
  104. package/dist/extensions/server.cjs +1156 -0
  105. package/dist/extensions/server.cjs.map +1 -0
  106. package/dist/extensions/server.d.mts +5 -0
  107. package/dist/extensions/server.d.mts.map +1 -0
  108. package/dist/extensions/server.mjs +1146 -0
  109. package/dist/extensions/server.mjs.map +1 -0
  110. package/dist/extensions/web.mjs +1785 -0
  111. package/dist/extensions/web.mjs.map +1 -0
  112. package/dist/headless-BGYfIlNk.mjs +169 -0
  113. package/dist/headless-BGYfIlNk.mjs.map +1 -0
  114. package/dist/headless-CHUsBGhg.cjs +180 -0
  115. package/dist/headless-CHUsBGhg.cjs.map +1 -0
  116. package/dist/index-BZL7keVW.d.mts +31 -0
  117. package/dist/index-BZL7keVW.d.mts.map +1 -0
  118. package/dist/index-BwB_jTI9.d.mts +12 -0
  119. package/dist/index-BwB_jTI9.d.mts.map +1 -0
  120. package/dist/index-D9-IXax4.d.mts +43 -0
  121. package/dist/index-D9-IXax4.d.mts.map +1 -0
  122. package/dist/index-DBz0DxnL.d.mts +39 -0
  123. package/dist/index-DBz0DxnL.d.mts.map +1 -0
  124. package/dist/index-DM7yJpMs.d.mts +25 -0
  125. package/dist/index-DM7yJpMs.d.mts.map +1 -0
  126. package/dist/index-Dhi8asLH.d.mts +22 -0
  127. package/dist/index-Dhi8asLH.d.mts.map +1 -0
  128. package/dist/index.cjs +19 -0
  129. package/dist/index.d.mts +7 -0
  130. package/dist/index.mjs +6 -0
  131. package/dist/launcher-BDm6-DiQ.mjs +150 -0
  132. package/dist/launcher-BDm6-DiQ.mjs.map +1 -0
  133. package/dist/launcher-BG5t9nvG.d.mts +39 -0
  134. package/dist/launcher-BG5t9nvG.d.mts.map +1 -0
  135. package/dist/launcher-SlyE8R3b.cjs +157 -0
  136. package/dist/launcher-SlyE8R3b.cjs.map +1 -0
  137. package/dist/launcher.cjs +3 -0
  138. package/dist/launcher.d.mts +33 -0
  139. package/dist/launcher.d.mts.map +1 -0
  140. package/dist/launcher.mjs +2 -0
  141. package/dist/lifeline-BZaQA0rm.mjs +88 -0
  142. package/dist/lifeline-BZaQA0rm.mjs.map +1 -0
  143. package/dist/lifeline-BuV_erib.cjs +98 -0
  144. package/dist/lifeline-BuV_erib.cjs.map +1 -0
  145. package/dist/lifeline-eJdZV0nN.d.mts +19 -0
  146. package/dist/lifeline-eJdZV0nN.d.mts.map +1 -0
  147. package/dist/lifeline.cjs +6 -0
  148. package/dist/lifeline.d.mts +21 -0
  149. package/dist/lifeline.d.mts.map +1 -0
  150. package/dist/lifeline.mjs +3 -0
  151. package/dist/logFile-C7oQ_v-g.d.mts +16 -0
  152. package/dist/logFile-C7oQ_v-g.d.mts.map +1 -0
  153. package/dist/logFile-CNvepZh0.cjs +67 -0
  154. package/dist/logFile-CNvepZh0.cjs.map +1 -0
  155. package/dist/logFile-KKPbkC-a.mjs +60 -0
  156. package/dist/logFile-KKPbkC-a.mjs.map +1 -0
  157. package/dist/logFile.cjs +3 -0
  158. package/dist/logFile.d.mts +11 -0
  159. package/dist/logFile.d.mts.map +1 -0
  160. package/dist/logFile.mjs +2 -0
  161. package/dist/logReader-B6J7L7Ua.cjs +282 -0
  162. package/dist/logReader-B6J7L7Ua.cjs.map +1 -0
  163. package/dist/logReader-MdW9ffvV.d.mts +43 -0
  164. package/dist/logReader-MdW9ffvV.d.mts.map +1 -0
  165. package/dist/logReader-UfbDbwQl.mjs +257 -0
  166. package/dist/logReader-UfbDbwQl.mjs.map +1 -0
  167. package/dist/logReader.cjs +4 -0
  168. package/dist/logReader.d.mts +3 -0
  169. package/dist/logReader.mjs +2 -0
  170. package/dist/processControl-BAOa3oo_.d.mts +14 -0
  171. package/dist/processControl-BAOa3oo_.d.mts.map +1 -0
  172. package/dist/processControl-DApTpV16.mjs +26 -0
  173. package/dist/processControl-DApTpV16.mjs.map +1 -0
  174. package/dist/processControl-DNJ1WZ9N.cjs +31 -0
  175. package/dist/processControl-DNJ1WZ9N.cjs.map +1 -0
  176. package/dist/processControl.cjs +3 -0
  177. package/dist/processControl.d.mts +10 -0
  178. package/dist/processControl.d.mts.map +1 -0
  179. package/dist/processControl.mjs +2 -0
  180. package/dist/ptyHost-DWWtRrGn.d.mts +38 -0
  181. package/dist/ptyHost-DWWtRrGn.d.mts.map +1 -0
  182. package/dist/ptyHost-DlQtTB8F.mjs +150 -0
  183. package/dist/ptyHost-DlQtTB8F.mjs.map +1 -0
  184. package/dist/ptyHost-MI2Z6WTG.cjs +157 -0
  185. package/dist/ptyHost-MI2Z6WTG.cjs.map +1 -0
  186. package/dist/ptyHost.cjs +3 -0
  187. package/dist/ptyHost.d.mts +26 -0
  188. package/dist/ptyHost.d.mts.map +1 -0
  189. package/dist/ptyHost.mjs +2 -0
  190. package/dist/ptySpawner-B6GsueZd.d.mts +28 -0
  191. package/dist/ptySpawner-B6GsueZd.d.mts.map +1 -0
  192. package/dist/ptySpawner-DEi1KOnd.mjs +17 -0
  193. package/dist/ptySpawner-DEi1KOnd.mjs.map +1 -0
  194. package/dist/ptySpawner-ijw0g5_T.cjs +22 -0
  195. package/dist/ptySpawner-ijw0g5_T.cjs.map +1 -0
  196. package/dist/ptySpawner.cjs +3 -0
  197. package/dist/ptySpawner.d.mts +14 -0
  198. package/dist/ptySpawner.d.mts.map +1 -0
  199. package/dist/ptySpawner.mjs +2 -0
  200. package/dist/reconcile-BugwYFbV.cjs +133 -0
  201. package/dist/reconcile-BugwYFbV.cjs.map +1 -0
  202. package/dist/reconcile-DMeNSxwn.mjs +114 -0
  203. package/dist/reconcile-DMeNSxwn.mjs.map +1 -0
  204. package/dist/reconcile.cjs +5 -0
  205. package/dist/reconcile.d.mts +36 -0
  206. package/dist/reconcile.d.mts.map +1 -0
  207. package/dist/reconcile.mjs +2 -0
  208. package/dist/responseEnvelope.cjs +19 -0
  209. package/dist/responseEnvelope.d.mts +50 -0
  210. package/dist/responseEnvelope.d.mts.map +1 -0
  211. package/dist/responseEnvelope.mjs +3 -0
  212. package/dist/rmuxBackend-CaEWooWn.d.mts +36 -0
  213. package/dist/rmuxBackend-CaEWooWn.d.mts.map +1 -0
  214. package/dist/rmuxBackend-DfyQwJrc.mjs +509 -0
  215. package/dist/rmuxBackend-DfyQwJrc.mjs.map +1 -0
  216. package/dist/rmuxBackend-DlxgqRZ1.cjs +523 -0
  217. package/dist/rmuxBackend-DlxgqRZ1.cjs.map +1 -0
  218. package/dist/rmuxBackend.cjs +4 -0
  219. package/dist/rmuxBackend.d.mts +3 -0
  220. package/dist/rmuxBackend.mjs +2 -0
  221. package/dist/rolldown-runtime-VH7oDXx4.cjs +28 -0
  222. package/dist/rtkProcessor-B4_urXQr.mjs +10 -0
  223. package/dist/rtkProcessor-B4_urXQr.mjs.map +1 -0
  224. package/dist/rtkProcessor-B9KPdyzr.mjs +309 -0
  225. package/dist/rtkProcessor-B9KPdyzr.mjs.map +1 -0
  226. package/dist/rtkProcessor-Bc4L4o_8.cjs +335 -0
  227. package/dist/rtkProcessor-Bc4L4o_8.cjs.map +1 -0
  228. package/dist/rtkProcessor-CVZvNhkJ.d.mts +35 -0
  229. package/dist/rtkProcessor-CVZvNhkJ.d.mts.map +1 -0
  230. package/dist/rtkProcessor-CsrCPyTQ.cjs +33 -0
  231. package/dist/rtkProcessor-CsrCPyTQ.cjs.map +1 -0
  232. package/dist/rtkProcessor.cjs +11 -0
  233. package/dist/rtkProcessor.d.mts +7 -0
  234. package/dist/rtkProcessor.d.mts.map +1 -0
  235. package/dist/rtkProcessor.mjs +3 -0
  236. package/dist/runnerConfig-BQBlVAXn.mjs +134 -0
  237. package/dist/runnerConfig-BQBlVAXn.mjs.map +1 -0
  238. package/dist/runnerConfig-BjtulXBI.d.mts +57 -0
  239. package/dist/runnerConfig-BjtulXBI.d.mts.map +1 -0
  240. package/dist/runnerConfig-D2tGq5ZM.cjs +367 -0
  241. package/dist/runnerConfig-D2tGq5ZM.cjs.map +1 -0
  242. package/dist/runnerDependencies-DruByBGS.mjs +647 -0
  243. package/dist/runnerDependencies-DruByBGS.mjs.map +1 -0
  244. package/dist/runnerDependencies-KMZigIxB.cjs +667 -0
  245. package/dist/runnerDependencies-KMZigIxB.cjs.map +1 -0
  246. package/dist/runnerDependencies.cjs +3 -0
  247. package/dist/runnerDependencies.d.mts +2 -0
  248. package/dist/runnerDependencies.mjs +2 -0
  249. package/dist/runnerNamer-B8FH7n2v.mjs +44 -0
  250. package/dist/runnerNamer-B8FH7n2v.mjs.map +1 -0
  251. package/dist/runnerNamer-we1XVYfM.cjs +49 -0
  252. package/dist/runnerNamer-we1XVYfM.cjs.map +1 -0
  253. package/dist/runnerNamer.cjs +3 -0
  254. package/dist/runnerNamer.d.mts +11 -0
  255. package/dist/runnerNamer.d.mts.map +1 -0
  256. package/dist/runnerNamer.mjs +2 -0
  257. package/dist/runnerPaths-BTEuzxw0.cjs +471 -0
  258. package/dist/runnerPaths-BTEuzxw0.cjs.map +1 -0
  259. package/dist/runnerPaths-B_dAAeco.mjs +451 -0
  260. package/dist/runnerPaths-B_dAAeco.mjs.map +1 -0
  261. package/dist/runnerPaths.cjs +3 -0
  262. package/dist/runnerPaths.d.mts +50 -0
  263. package/dist/runnerPaths.d.mts.map +1 -0
  264. package/dist/runnerPaths.mjs +2 -0
  265. package/dist/runnerRegistry-BmkZTJHi.cjs +371 -0
  266. package/dist/runnerRegistry-BmkZTJHi.cjs.map +1 -0
  267. package/dist/runnerRegistry-CeOHEjBv.mjs +339 -0
  268. package/dist/runnerRegistry-CeOHEjBv.mjs.map +1 -0
  269. package/dist/runnerRegistry-CtJiD02R.d.mts +88 -0
  270. package/dist/runnerRegistry-CtJiD02R.d.mts.map +1 -0
  271. package/dist/runnerRegistry.cjs +4 -0
  272. package/dist/runnerRegistry.d.mts +3 -0
  273. package/dist/runnerRegistry.mjs +2 -0
  274. package/dist/runnerSettings-DiV1lvzx.d.mts +34 -0
  275. package/dist/runnerSettings-DiV1lvzx.d.mts.map +1 -0
  276. package/dist/services/lifeline/client.cjs +52 -0
  277. package/dist/services/lifeline/client.cjs.map +1 -0
  278. package/dist/services/lifeline/client.d.mts +22 -0
  279. package/dist/services/lifeline/client.d.mts.map +1 -0
  280. package/dist/services/lifeline/client.mjs +48 -0
  281. package/dist/services/lifeline/client.mjs.map +1 -0
  282. package/dist/services/runnerSupervisor/index.cjs +207 -0
  283. package/dist/services/runnerSupervisor/index.cjs.map +1 -0
  284. package/dist/services/runnerSupervisor/index.d.mts +71 -0
  285. package/dist/services/runnerSupervisor/index.d.mts.map +1 -0
  286. package/dist/services/runnerSupervisor/index.mjs +195 -0
  287. package/dist/services/runnerSupervisor/index.mjs.map +1 -0
  288. package/dist/session-BC43uEyG.cjs +29 -0
  289. package/dist/session-BC43uEyG.cjs.map +1 -0
  290. package/dist/session-Chsb_Nh2.mjs +18 -0
  291. package/dist/session-Chsb_Nh2.mjs.map +1 -0
  292. package/dist/session-DE2Acesp.mjs +6 -0
  293. package/dist/session-DE2Acesp.mjs.map +1 -0
  294. package/dist/session-SfIPtdZO.cjs +11 -0
  295. package/dist/session-SfIPtdZO.cjs.map +1 -0
  296. package/dist/session.cjs +6 -0
  297. package/dist/session.d.mts +11 -0
  298. package/dist/session.d.mts.map +1 -0
  299. package/dist/session.mjs +3 -0
  300. package/dist/spawner-BxFsINXP.cjs +68 -0
  301. package/dist/spawner-BxFsINXP.cjs.map +1 -0
  302. package/dist/spawner-CdrN_HNk.d.mts +35 -0
  303. package/dist/spawner-CdrN_HNk.d.mts.map +1 -0
  304. package/dist/spawner-LoVsn55-.mjs +63 -0
  305. package/dist/spawner-LoVsn55-.mjs.map +1 -0
  306. package/dist/spawner.cjs +3 -0
  307. package/dist/spawner.d.mts +9 -0
  308. package/dist/spawner.d.mts.map +1 -0
  309. package/dist/spawner.mjs +2 -0
  310. package/dist/type-BXM03qSw.d.mts +44 -0
  311. package/dist/type-BXM03qSw.d.mts.map +1 -0
  312. package/dist/type-C31e1gla.d.mts +43 -0
  313. package/dist/type-C31e1gla.d.mts.map +1 -0
  314. package/dist/type-vWQNTahw.d.mts +12 -0
  315. package/dist/type-vWQNTahw.d.mts.map +1 -0
  316. package/dist/types.cjs +0 -0
  317. package/dist/types.d.mts +3 -0
  318. package/dist/types.mjs +1 -0
  319. package/llms.txt +9 -0
  320. package/package.json +303 -0
  321. package/skills/doom-runner/SKILL.md +78 -0
  322. package/src/prompts/po-use-runner/SKILL.md +47 -0
@@ -0,0 +1,1785 @@
1
+ import { apiResponse, createApiClient, defineActivityGroup, defineApiRoutes, defineChannelFile, defineFill, defineSessionStore, defineToolRenderer, defineWebPlugin, sessionApiAddress } from "@agimon-ai/po-core/web";
2
+ import { AnsiLine, AnsiText, ArrowRightIcon, Button, Checkbox, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Dot, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, EmptyState, FileIcon, Input, MessageItem, MessageItemBody, MessageItemHeader, MessageItemStatus, PlayCircleIcon, PlusIcon, SearchIcon, StatusBadge, SyntaxText, TerminalView, Textarea, ansiSpans, toolTone } from "@agimon-ai/po-web-components";
3
+ import { useStore } from "@tanstack/react-store";
4
+ import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
5
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
+ import { sealedTransport } from "@agimon-ai/po-web-security/browser";
7
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/format.ts
8
+ /**
9
+ * Local copy of the runtime's formatUptime (src/services/bashResult/index.ts):
10
+ * the web plugin may reach only src/types, so the one helper it needs lives here.
11
+ */
12
+ function formatRunnerUptime(startedAt, now) {
13
+ const started = Date.parse(startedAt);
14
+ if (Number.isNaN(started)) return "unknown";
15
+ const seconds = Math.max(0, Math.floor((now - started) / 1e3));
16
+ if (seconds < 60) return `${seconds}s`;
17
+ const minutes = Math.floor(seconds / 60);
18
+ if (minutes < 60) return `${minutes}m`;
19
+ const hours = Math.floor(minutes / 60);
20
+ return hours < 24 ? `${hours}h` : `${Math.floor(hours / 24)}d`;
21
+ }
22
+ /**
23
+ * The lines a log view shows: the slice's text, then whatever the follow
24
+ * stream appended, bounded to the newest `max`.
25
+ *
26
+ * A log file ends with a newline, and the reader hands its text back with that
27
+ * newline intact, so a naive split reports one blank line more than the file
28
+ * has. Dropping exactly one trailing empty entry keeps the count the reader
29
+ * sees equal to the count the server reported.
30
+ */
31
+ function logViewLines(text, appended, max, options = {}) {
32
+ const split = text.split("\n");
33
+ if (split.length > 0 && split[split.length - 1] === "") split.pop();
34
+ if (options.dropPartialTail === true && split.length > 0) split.pop();
35
+ const all = [...split, ...appended];
36
+ const lines = all.slice(-max);
37
+ return {
38
+ lines,
39
+ hidden: all.length - lines.length
40
+ };
41
+ }
42
+ /**
43
+ * Absolute 1-based numbers for a tail, counted back from the last line.
44
+ *
45
+ * A tail is contiguous, so only its end has to be known. Grep results are not
46
+ * contiguous and carry their own numbers from the reader instead.
47
+ */
48
+ function tailLineNumbers(lastLineNumber, count) {
49
+ const first = Math.max(1, lastLineNumber - count + 1);
50
+ return Array.from({ length: count }, (_, index) => first + index);
51
+ }
52
+ /**
53
+ * Whether the view is actually following a growing log.
54
+ *
55
+ * Three things must hold: the reader asked to follow, no query is set (a
56
+ * filtered view is a snapshot of the whole file, which a stream cannot
57
+ * assemble a chunk at a time), and the runner is still writing. Without the
58
+ * last one a finished run would claim to be tailing a file nothing will ever
59
+ * append to.
60
+ */
61
+ function isFollowingLive(following, filtering, running) {
62
+ return following && !filtering && running;
63
+ }
64
+ //#endregion
65
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/launchLine.ts
66
+ /**
67
+ * The `/runners start` line the launch form sends.
68
+ *
69
+ * WHY THE BUILDER LIVES HERE AND THE PARSER DOES NOT:
70
+ * Cockpit code is compiled into the host bundle and may import only this
71
+ * package's own web and type modules, so it cannot share the session's parser
72
+ * in src/services. The two are pinned together by a test that parses what this
73
+ * writes, which is the only guarantee that matters: the line has to survive
74
+ * the trip.
75
+ */
76
+ const RUNNERS_START_LINE = "/runners start";
77
+ /** The no-form launch used when a reader asks for a shell rather than a command. */
78
+ const RUNNER_SHELL_REQUEST = {
79
+ command: "exec \"${SHELL:-/bin/bash}\" -l",
80
+ name: "shell",
81
+ interactive: true
82
+ };
83
+ /** Quotes a value that carries whitespace, which is how a value survives tokenizing. */
84
+ function quote(value) {
85
+ return /\s/u.test(value) ? `"${value}"` : value;
86
+ }
87
+ function runnerLaunchLine(request) {
88
+ const cwd = request.cwd?.trim() ?? "";
89
+ const name = request.name?.trim() ?? "";
90
+ const pairs = [
91
+ ...name === "" ? [] : [`name=${quote(name)}`],
92
+ ...cwd === "" ? [] : [`cwd=${quote(cwd)}`],
93
+ ...request.interactive === true ? ["interactive=true"] : []
94
+ ];
95
+ return [
96
+ RUNNERS_START_LINE,
97
+ ...pairs,
98
+ "--",
99
+ request.command.trim()
100
+ ].join(" ");
101
+ }
102
+ /** What the form will not let a reader send, worded the way the session would answer. */
103
+ function launchProblems(request) {
104
+ const problems = [];
105
+ if (request.command.trim() === "") problems.push("A runner needs a command to run.");
106
+ return problems;
107
+ }
108
+ //#endregion
109
+ //#region src/constants/webRunners.ts
110
+ const RUNNER_RUNS_TYPE = "runner_runs";
111
+ //#endregion
112
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/runnersStore.ts
113
+ /** The runtime's slash verb that stops one runner headlessly. */
114
+ const STOP_COMMAND = "/runners stop";
115
+ const runners = defineSessionStore({
116
+ runs: [],
117
+ stopRequested: []
118
+ });
119
+ /** The runner channel keeps the dock's busy state aligned with its rendered run list. */
120
+ const runnerActivitySource = {
121
+ subscribe(listener) {
122
+ const subscription = runners.store.subscribe(listener);
123
+ return () => subscription.unsubscribe();
124
+ },
125
+ isActive(sessionId) {
126
+ return runners.select(runners.store.state, sessionId).runs.some((run) => run.state === "running");
127
+ }
128
+ };
129
+ function isRecord$3(value) {
130
+ return typeof value === "object" && value !== null && !Array.isArray(value);
131
+ }
132
+ /**
133
+ * Asks the runtime to stop a runner through the same slash command the TUI's
134
+ * Runner Space uses. The request is remembered until the runner's record
135
+ * reports an exit; the runtime, not this click, decides when that is.
136
+ */
137
+ function requestRunnerStop(send, sessionId, id) {
138
+ send(sessionId, {
139
+ type: "prompt",
140
+ message: `${STOP_COMMAND} ${id}`
141
+ });
142
+ runners.update(sessionId, (current) => ({
143
+ ...current,
144
+ stopRequested: [...current.stopRequested.filter((known) => known !== id), id]
145
+ }));
146
+ }
147
+ /**
148
+ * Asks the runtime to start a runner, through the same prompt frame the stop
149
+ * request uses and the same way agents and workflows launch their own work.
150
+ *
151
+ * Nothing is recorded as pending here. The runtime names the run, and it
152
+ * arrives in the feed on its own; guessing at an id before it exists would
153
+ * only have to be reconciled later.
154
+ */
155
+ function requestRunnerStart(send, sessionId, request) {
156
+ send(sessionId, {
157
+ type: "prompt",
158
+ message: runnerLaunchLine(request)
159
+ });
160
+ }
161
+ /** The plugin's session data channel: 'runner_runs' payloads into the store. */
162
+ const runnerRunsChannel = runners.channel({
163
+ channel: RUNNER_RUNS_TYPE,
164
+ parse(input) {
165
+ if (!isRecord$3(input) || !Array.isArray(input.runs)) return null;
166
+ return { runs: input.runs.filter(isRecord$3) };
167
+ },
168
+ reduce(current, { runs }) {
169
+ const stillRunning = new Set(runs.filter((run) => run.state === "running").map((run) => run.id));
170
+ return {
171
+ runs,
172
+ stopRequested: current.stopRequested.filter((id) => stillRunning.has(id))
173
+ };
174
+ }
175
+ });
176
+ //#endregion
177
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/LaunchRunnerDialog.tsx
178
+ const COMMAND_ROWS = 3;
179
+ const INTERACTIVE_FIELD_ID = "runner-launch-interactive-field";
180
+ function FieldLabel({ children }) {
181
+ return /* @__PURE__ */ jsx("span", {
182
+ className: "text-base font-medium text-doom-text",
183
+ children
184
+ });
185
+ }
186
+ /**
187
+ * Starts one runner by hand: the command, where it runs, what to call it, and
188
+ * whether it needs a terminal, then the exact `/runners start` line the
189
+ * session will parse.
190
+ *
191
+ * The line is shown rather than hidden. This sends a shell command through the
192
+ * session, and a reader is entitled to see exactly what they are about to run
193
+ * before they run it.
194
+ */
195
+ function LaunchRunnerDialog({ sessionId, sendSessionFrame, defaultCwd, onClose }) {
196
+ const [command, setCommand] = useState("");
197
+ const [cwd, setCwd] = useState(defaultCwd ?? "");
198
+ const [name, setName] = useState("");
199
+ const [interactive, setInteractive] = useState(false);
200
+ const request = {
201
+ command,
202
+ cwd,
203
+ name,
204
+ interactive
205
+ };
206
+ const problems = launchProblems(request);
207
+ const line = runnerLaunchLine(request);
208
+ const launch = () => {
209
+ if (problems.length > 0) return;
210
+ requestRunnerStart(sendSessionFrame, sessionId, request);
211
+ onClose();
212
+ };
213
+ return /* @__PURE__ */ jsx(Dialog, {
214
+ open: true,
215
+ onOpenChange: (open) => {
216
+ if (!open) onClose();
217
+ },
218
+ children: /* @__PURE__ */ jsxs(DialogContent, {
219
+ width: "lg",
220
+ "data-testid": "runner-launch-dialog",
221
+ "aria-describedby": void 0,
222
+ children: [/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, {
223
+ "data-testid": "runner-launch-title",
224
+ children: "launch a runner"
225
+ }) }), /* @__PURE__ */ jsxs(DialogBody, { children: [
226
+ /* @__PURE__ */ jsxs("div", {
227
+ className: "flex flex-col gap-1",
228
+ children: [/* @__PURE__ */ jsx(FieldLabel, { children: "Command" }), /* @__PURE__ */ jsx(Textarea, {
229
+ "data-testid": "runner-launch-command",
230
+ rows: COMMAND_ROWS,
231
+ value: command,
232
+ placeholder: "pnpm test",
233
+ onChange: (event) => setCommand(event.target.value)
234
+ })]
235
+ }),
236
+ /* @__PURE__ */ jsxs("div", {
237
+ className: "flex flex-col gap-2.5 sm:flex-row",
238
+ children: [/* @__PURE__ */ jsxs("div", {
239
+ className: "flex min-w-0 flex-1 flex-col gap-1",
240
+ children: [/* @__PURE__ */ jsx(FieldLabel, { children: "Cwd" }), /* @__PURE__ */ jsx(Input, {
241
+ size: "sm",
242
+ "data-testid": "runner-launch-cwd",
243
+ value: cwd,
244
+ placeholder: "the session's own directory",
245
+ onChange: (event) => setCwd(event.target.value)
246
+ })]
247
+ }), /* @__PURE__ */ jsxs("div", {
248
+ className: "flex min-w-0 flex-1 flex-col gap-1",
249
+ children: [/* @__PURE__ */ jsx(FieldLabel, { children: "Name" }), /* @__PURE__ */ jsx(Input, {
250
+ size: "sm",
251
+ "data-testid": "runner-launch-name",
252
+ value: name,
253
+ placeholder: "chosen for you when empty",
254
+ onChange: (event) => setName(event.target.value)
255
+ })]
256
+ })]
257
+ }),
258
+ /* @__PURE__ */ jsxs("div", {
259
+ className: "flex items-center gap-2",
260
+ children: [/* @__PURE__ */ jsx(Checkbox, {
261
+ id: INTERACTIVE_FIELD_ID,
262
+ "data-testid": "runner-launch-interactive",
263
+ checked: interactive,
264
+ onCheckedChange: (checked) => setInteractive(checked === true)
265
+ }), /* @__PURE__ */ jsx("label", {
266
+ htmlFor: INTERACTIVE_FIELD_ID,
267
+ className: "text-base text-doom-dim",
268
+ children: "needs a terminal, for a command that prompts (requires rmux or tmux)"
269
+ })]
270
+ }),
271
+ /* @__PURE__ */ jsxs("div", {
272
+ className: "flex flex-col gap-1 rounded-md border border-doom-border bg-doom-deep px-3 py-2",
273
+ children: [/* @__PURE__ */ jsx(FieldLabel, { children: "Sent to the session" }), /* @__PURE__ */ jsx("pre", {
274
+ "data-testid": "runner-launch-line",
275
+ className: "whitespace-pre-wrap break-words font-mono text-sm text-doom-green",
276
+ children: line
277
+ })]
278
+ }),
279
+ problems.length > 0 ? /* @__PURE__ */ jsx("p", {
280
+ "data-testid": "runner-launch-problem",
281
+ className: "text-base text-doom-red",
282
+ children: problems.join(" ")
283
+ }) : null,
284
+ /* @__PURE__ */ jsxs(DialogFooter, {
285
+ className: "flex-wrap sm:flex-nowrap",
286
+ children: [
287
+ /* @__PURE__ */ jsx("span", {
288
+ className: "w-full text-sm text-doom-dim sm:w-auto",
289
+ children: "it appears above once the runtime starts it"
290
+ }),
291
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
292
+ /* @__PURE__ */ jsx(Button, {
293
+ variant: "outline",
294
+ size: "xs",
295
+ "data-testid": "runner-launch-cancel",
296
+ onClick: onClose,
297
+ children: "cancel"
298
+ }),
299
+ /* @__PURE__ */ jsx(Button, {
300
+ variant: "primary",
301
+ size: "xs",
302
+ "data-testid": "runner-launch-submit",
303
+ disabled: problems.length > 0,
304
+ onClick: launch,
305
+ children: "launch"
306
+ })
307
+ ]
308
+ })
309
+ ] })]
310
+ })
311
+ });
312
+ }
313
+ //#endregion
314
+ //#region src/constants/webRunnerLog.ts
315
+ const RUN_ID_PARAM = "runId";
316
+ const RUNNER_SCREEN_EVENT = "screen";
317
+ const RUNNER_LOG_STREAM_EVENT = "append";
318
+ //#endregion
319
+ //#region src/types/webRunnerLog.ts
320
+ /** The named SSE event carrying a RunnerLogStreamEvent payload. */
321
+ /**
322
+ * The named SSE event that carries nothing.
323
+ *
324
+ * A runner can be alive and silent for minutes, which on the wire is
325
+ * indistinguishable from a connection that died. This is sent on a timer so
326
+ * the socket keeps proving itself. Readers that only listen for `append`
327
+ * ignore it, which is what makes it safe to add.
328
+ */
329
+ /** Query parameter names, shared so the page and the route cannot drift apart. */
330
+ const RUNNER_LOG_PARAMS = {
331
+ lines: "lines",
332
+ grep: "grep",
333
+ ignoreCase: "ignoreCase",
334
+ contextLines: "contextLines",
335
+ from: "from"
336
+ };
337
+ //#endregion
338
+ //#region src/types/apiRoutes.ts
339
+ /**
340
+ * This package's routes, as data.
341
+ *
342
+ * No scope and no base path: the build reads both off
343
+ * `src/extensions/workspaces/sessions/(backend)/api/runner/`, so the mount is
344
+ * stated once, by the folder that creates it. This package stated it four times
345
+ * before, once per URL builder, each of them re-spelling `/plugins/` and the
346
+ * session prefix by hand.
347
+ *
348
+ * The Hono app registers these paths verbatim and the browser addresses them,
349
+ * so a path cannot move on one side alone. `:runId` is Hono's own parameter
350
+ * syntax for that reason; a caller supplies the value as `params`, and the
351
+ * client encodes the segment.
352
+ */
353
+ var apiRoutes_default = defineApiRoutes({
354
+ log: {
355
+ method: "GET",
356
+ path: `/runners/:${RUN_ID_PARAM}/log`,
357
+ query: [
358
+ RUNNER_LOG_PARAMS.lines,
359
+ RUNNER_LOG_PARAMS.grep,
360
+ RUNNER_LOG_PARAMS.ignoreCase,
361
+ RUNNER_LOG_PARAMS.contextLines
362
+ ],
363
+ response: apiResponse()
364
+ },
365
+ /**
366
+ * The live tail of that same log.
367
+ *
368
+ * `stream: true`, so the client exposes a URL and no call method: the
369
+ * consumer is the browser's own `EventSource`, and a relayed call would
370
+ * buffer a stream that never ends.
371
+ */
372
+ logStream: {
373
+ method: "GET",
374
+ path: `/runners/:${RUN_ID_PARAM}/log/stream`,
375
+ query: [RUNNER_LOG_PARAMS.from],
376
+ stream: true
377
+ },
378
+ /** The unscrubbed pane bytes, for a terminal emulator rather than a reader. */
379
+ screenStream: {
380
+ method: "GET",
381
+ path: `/runners/:${RUN_ID_PARAM}/screen/stream`,
382
+ query: [RUNNER_LOG_PARAMS.from],
383
+ stream: true
384
+ },
385
+ input: {
386
+ method: "POST",
387
+ path: `/runners/:${RUN_ID_PARAM}/screen/input`
388
+ }
389
+ });
390
+ const api = createApiClient(apiRoutes_default, {
391
+ scopes: ["session"],
392
+ basePath: "runner",
393
+ transport: (input, init) => sealedTransport.fetch(input, init),
394
+ sessionAddress: sessionApiAddress
395
+ });
396
+ //#endregion
397
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/logApi.ts
398
+ const UNREACHABLE = "The cockpit hub is unreachable.";
399
+ function isRecord$2(value) {
400
+ return typeof value === "object" && value !== null && !Array.isArray(value);
401
+ }
402
+ /** Reads one runner's log: the last N lines, or the lines matching a substring with context. */
403
+ async function fetchRunnerLog(sessionId, runId, params = {}, signal) {
404
+ const result = await api.session(sessionId).log({
405
+ params: { [RUN_ID_PARAM]: runId },
406
+ query: {
407
+ [RUNNER_LOG_PARAMS.lines]: params.lines,
408
+ [RUNNER_LOG_PARAMS.grep]: params.grep === "" ? void 0 : params.grep,
409
+ [RUNNER_LOG_PARAMS.ignoreCase]: params.ignoreCase === true ? true : void 0,
410
+ [RUNNER_LOG_PARAMS.contextLines]: params.contextLines
411
+ },
412
+ ...signal === void 0 ? {} : { signal }
413
+ });
414
+ if (!result.ok) {
415
+ if (result.status === 0) return { error: signal?.aborted === true ? "" : UNREACHABLE };
416
+ return { error: result.error === "" ? `The hub answered ${String(result.status)}.` : result.error };
417
+ }
418
+ if (!isRecord$2(result.data) || typeof result.data.text !== "string") return { error: `The hub answered ${String(result.status)}.` };
419
+ return { slice: result.data };
420
+ }
421
+ /** Attempts before a dropped stream is reported as lost rather than retried. */
422
+ const MAX_RETRIES$1 = 5;
423
+ const RETRY_BASE_MS$1 = 500;
424
+ /**
425
+ * Follows a running log from the offset already read, so no line is missed
426
+ * between the slice and the stream. The stream never filters; a page with a
427
+ * query stops following and reads a filtered slice instead.
428
+ *
429
+ * A dropped connection is retried with backoff from the offset the last event
430
+ * reported, not from the offset the page started at, so a reconnect resumes
431
+ * instead of replaying. The browser's own EventSource retry cannot do this:
432
+ * it reopens the URL it was given, whose offset is stale the moment a line
433
+ * arrives, so each of its reconnects would duplicate everything since.
434
+ */
435
+ function followRunnerLog(sessionId, runId, from, handlers) {
436
+ let offset = from;
437
+ let retries = 0;
438
+ let closed = false;
439
+ let source;
440
+ let timer;
441
+ const open = () => {
442
+ if (closed) return;
443
+ const current = new EventSource(api.session(sessionId).logStream.url({
444
+ params: { [RUN_ID_PARAM]: runId },
445
+ query: { [RUNNER_LOG_PARAMS.from]: offset }
446
+ }));
447
+ source = current;
448
+ current.addEventListener(RUNNER_LOG_STREAM_EVENT, (message) => {
449
+ let parsed;
450
+ try {
451
+ parsed = JSON.parse(message.data);
452
+ } catch {
453
+ return;
454
+ }
455
+ if (!isRecord$2(parsed) || !Array.isArray(parsed.lines)) return;
456
+ const event = parsed;
457
+ if (typeof event.offset === "number") offset = event.offset;
458
+ retries = 0;
459
+ if (event.ended === true) {
460
+ closed = true;
461
+ current.close();
462
+ }
463
+ handlers.onEvent(event);
464
+ });
465
+ current.addEventListener("error", () => {
466
+ current.close();
467
+ if (closed) return;
468
+ retries += 1;
469
+ if (retries > MAX_RETRIES$1) {
470
+ handlers.onLost();
471
+ return;
472
+ }
473
+ timer = setTimeout(open, RETRY_BASE_MS$1 * 2 ** (retries - 1));
474
+ });
475
+ };
476
+ open();
477
+ return { close: () => {
478
+ closed = true;
479
+ if (timer !== void 0) clearTimeout(timer);
480
+ source?.close();
481
+ } };
482
+ }
483
+ //#endregion
484
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/RunnerLogPanel.tsx
485
+ const TICK_MS$2 = 1e4;
486
+ /** The tab id doubles as the URL segment, so it stays plain and unique across plugins. */
487
+ const TAB_ID_PREFIX$1 = "runner-log-";
488
+ /** Lines held in the view; a follow that ran all day must not grow without bound. */
489
+ const MAX_VIEW_LINES = 2e3;
490
+ /** Context lines each side of a match, the same either-side window grep would use. */
491
+ const CONTEXT_LINES = 2;
492
+ /** A query is typed a character at a time; each one costs a whole-file read on the hub. */
493
+ const SEARCH_DEBOUNCE_MS = 200;
494
+ /** How close to the bottom still counts as "at the bottom" for following. */
495
+ const NEAR_BOTTOM_PX = 24;
496
+ const BYTES_PER_KB$1 = 1024;
497
+ const BYTES_PER_MB$1 = BYTES_PER_KB$1 * BYTES_PER_KB$1;
498
+ /** The temporary tab for one runner's log; the host keeps this panel while the tab is open. */
499
+ function runnerLogTab(run) {
500
+ const runId = run.id;
501
+ return {
502
+ id: `${TAB_ID_PREFIX$1}${runId}`,
503
+ label: `log · ${run.name}`,
504
+ panel: (props) => /* @__PURE__ */ jsx(RunnerLogPanel, {
505
+ ...props,
506
+ runId
507
+ })
508
+ };
509
+ }
510
+ function formatSize$1(bytes) {
511
+ if (bytes < BYTES_PER_KB$1) return `${bytes} B`;
512
+ if (bytes < BYTES_PER_MB$1) return `${(bytes / BYTES_PER_KB$1).toFixed(1)} KB`;
513
+ return `${(bytes / BYTES_PER_MB$1).toFixed(1)} MB`;
514
+ }
515
+ function badgeOf(run) {
516
+ if (run === void 0) return {
517
+ tone: "neutral",
518
+ label: "gone"
519
+ };
520
+ if (run.state === "running") return {
521
+ tone: "running",
522
+ label: "running"
523
+ };
524
+ if (run.exit === void 0 || run.exit.reason === "stopped") return {
525
+ tone: "neutral",
526
+ label: "stopped"
527
+ };
528
+ return run.exit.reason === "completed" && (run.exit.code === 0 || run.exit.code === null) ? {
529
+ tone: "ok",
530
+ label: "done"
531
+ } : {
532
+ tone: "error",
533
+ label: run.exit.reason.replace("_", " ")
534
+ };
535
+ }
536
+ /** Whether one line is a match rather than the context printed around one. */
537
+ function isMatch(line, needle, ignoreCase) {
538
+ if (needle === "") return false;
539
+ return ignoreCase ? line.toLowerCase().includes(needle.toLowerCase()) : line.includes(needle);
540
+ }
541
+ /** One fact of the runner, on its own line so a long command is never elided. */
542
+ function MetaRow({ label, value }) {
543
+ return /* @__PURE__ */ jsxs("span", {
544
+ className: "flex min-w-0 gap-2.5",
545
+ children: [/* @__PURE__ */ jsx("span", {
546
+ className: "w-14 shrink-0 text-base text-doom-dim",
547
+ children: label
548
+ }), /* @__PURE__ */ jsx("span", {
549
+ className: "min-w-0 flex-1 break-all font-mono text-sm text-doom-dim",
550
+ children: value
551
+ })]
552
+ });
553
+ }
554
+ /**
555
+ * One rendered log line: its number, whether it matched, and the text.
556
+ *
557
+ * A row per line rather than one block of joined text. The block form re-parsed
558
+ * every line's escapes whenever a single line arrived, because the parse was
559
+ * keyed on the joined string; keyed by line number instead, an append only
560
+ * costs the line that appended.
561
+ */
562
+ function LogRow({ number, line, matched }) {
563
+ return /* @__PURE__ */ jsxs("div", {
564
+ "data-testid": "runner-log-row",
565
+ "data-matched": matched,
566
+ className: "flex min-w-0 gap-2",
567
+ children: [
568
+ number === void 0 ? null : /* @__PURE__ */ jsx("span", {
569
+ className: "w-12 shrink-0 select-none text-right text-xs leading-relaxed text-doom-faint",
570
+ children: number
571
+ }),
572
+ /* @__PURE__ */ jsx("span", {
573
+ "aria-hidden": true,
574
+ className: `w-4 shrink-0 select-none text-xs leading-relaxed ${matched ? "text-doom-yellow" : "text-doom-faint"}`,
575
+ children: matched ? ">>" : ""
576
+ }),
577
+ /* @__PURE__ */ jsx(AnsiLine, {
578
+ line,
579
+ className: `min-w-0 flex-1 whitespace-pre-wrap break-words font-mono text-xs leading-relaxed ${matched ? "text-doom-text" : "text-doom-dim"}`
580
+ })
581
+ ]
582
+ });
583
+ }
584
+ /**
585
+ * One runner's log: the bounded tail, following while the runner writes, and
586
+ * a grep over the whole file when the reader types a query.
587
+ *
588
+ * Search is not find-next. The reader matches a literal substring and answers
589
+ * with the matching lines and their context, so a query replaces the view
590
+ * rather than moving a cursor through it, and following pauses while one is
591
+ * set because a filtered view is a snapshot of a file that is still growing.
592
+ */
593
+ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
594
+ const { runs, stopRequested } = useStore(runners.store, (state) => runners.select(state, sessionId));
595
+ const run = runs.find((candidate) => candidate.id === runId);
596
+ const [now, setNow] = useState(() => Date.now());
597
+ const [typed, setTyped] = useState("");
598
+ const [query, setQuery] = useState("");
599
+ const [ignoreCase, setIgnoreCase] = useState(false);
600
+ const [context, setContext] = useState(CONTEXT_LINES);
601
+ const [following, setFollowing] = useState(true);
602
+ const [slice, setSlice] = useState(void 0);
603
+ const [streamFrom, setStreamFrom] = useState(void 0);
604
+ const [appended, setAppended] = useState([]);
605
+ const [error, setError] = useState(void 0);
606
+ /** False once the reader scrolls up: following must not yank them back down. */
607
+ const [pinned, setPinned] = useState(true);
608
+ const bottom = useRef(null);
609
+ const body = useRef(null);
610
+ const filtering = query !== "";
611
+ const live = isFollowingLive(following, filtering, slice?.running === true);
612
+ useEffect(() => {
613
+ const timer = setInterval(() => setNow(Date.now()), TICK_MS$2);
614
+ return () => clearInterval(timer);
615
+ }, []);
616
+ useEffect(() => {
617
+ const timer = setTimeout(() => setQuery(typed), SEARCH_DEBOUNCE_MS);
618
+ return () => clearTimeout(timer);
619
+ }, [typed]);
620
+ const load = useCallback((signal) => {
621
+ if (sessionId === null) return;
622
+ fetchRunnerLog(sessionId, runId, filtering ? {
623
+ grep: query,
624
+ ignoreCase,
625
+ contextLines: context
626
+ } : {}, signal).then((result) => {
627
+ if (signal.aborted) return;
628
+ if ("error" in result) {
629
+ if (result.error !== "") setError(result.error);
630
+ return;
631
+ }
632
+ setError(void 0);
633
+ setAppended([]);
634
+ setSlice(result.slice);
635
+ setStreamFrom(result.slice.completeBytes);
636
+ });
637
+ }, [
638
+ sessionId,
639
+ runId,
640
+ filtering,
641
+ query,
642
+ ignoreCase,
643
+ context
644
+ ]);
645
+ useEffect(() => {
646
+ const controller = new AbortController();
647
+ load(controller.signal);
648
+ return () => controller.abort();
649
+ }, [load]);
650
+ useEffect(() => {
651
+ if (!live || sessionId === null || streamFrom === void 0) return;
652
+ const follow = followRunnerLog(sessionId, runId, streamFrom, {
653
+ onEvent: (event) => {
654
+ if (event.lines.length > 0) setAppended((current) => [...current, ...event.lines].slice(-2e3));
655
+ if (event.ended === true) setFollowing(false);
656
+ },
657
+ onLost: () => setFollowing(false)
658
+ });
659
+ return () => follow.close();
660
+ }, [
661
+ live,
662
+ sessionId,
663
+ runId,
664
+ streamFrom
665
+ ]);
666
+ const partialTail = slice !== void 0 && slice.exists && slice.completeBytes < slice.fileSize;
667
+ const view = logViewLines(slice?.text ?? "", appended, MAX_VIEW_LINES, { dropPartialTail: live && partialTail });
668
+ const numbers = useMemo(() => {
669
+ if (slice === void 0) return [];
670
+ if (filtering) return slice.lineNumbers ?? [];
671
+ return tailLineNumbers(slice.totalLines + appended.length - (live && partialTail ? 1 : 0), view.lines.length);
672
+ }, [
673
+ slice,
674
+ filtering,
675
+ appended.length,
676
+ live,
677
+ partialTail,
678
+ view.lines.length
679
+ ]);
680
+ useEffect(() => {
681
+ if (live && pinned) bottom.current?.scrollIntoView({ block: "end" });
682
+ }, [
683
+ live,
684
+ pinned,
685
+ view.lines.length
686
+ ]);
687
+ const onScroll = () => {
688
+ const element = body.current;
689
+ if (element === null) return;
690
+ setPinned(element.scrollHeight - element.scrollTop - element.clientHeight <= NEAR_BOTTOM_PX);
691
+ };
692
+ const badge = badgeOf(run);
693
+ const stopping = run !== void 0 && stopRequested.includes(run.id);
694
+ const elapsed = run === void 0 ? "" : formatRunnerUptime(run.startedAt, run.exit ? Date.parse(run.exit.finishedAt) : now);
695
+ return /* @__PURE__ */ jsxs("div", {
696
+ "data-testid": "runner-log-panel",
697
+ className: "flex min-h-0 flex-1 flex-col",
698
+ children: [
699
+ /* @__PURE__ */ jsxs("div", {
700
+ className: "flex min-h-11 shrink-0 flex-wrap items-center gap-2 border-b border-doom-border-soft px-3 py-2 sm:h-11 sm:flex-nowrap sm:gap-2.5 sm:px-[26px] sm:py-0",
701
+ children: [
702
+ /* @__PURE__ */ jsx("span", {
703
+ "data-testid": "runner-log-name",
704
+ className: "shrink-0 truncate text-base font-semibold text-doom-hi",
705
+ children: run?.name ?? runId
706
+ }),
707
+ /* @__PURE__ */ jsx(StatusBadge, {
708
+ tone: badge.tone,
709
+ "data-testid": "runner-log-state",
710
+ children: badge.label
711
+ }),
712
+ run ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
713
+ className: "shrink-0 text-sm text-doom-dim",
714
+ children: elapsed
715
+ }), /* @__PURE__ */ jsx("span", {
716
+ className: "shrink-0 text-sm text-doom-dim",
717
+ children: `pid ${String(run.pid)} · ${run.backend}${run.interactive ? " · tty" : ""}`
718
+ })] }) : /* @__PURE__ */ jsx("span", {
719
+ "data-testid": "runner-log-gone",
720
+ className: "text-sm text-doom-dim",
721
+ children: "no longer listed; its log stays readable"
722
+ }),
723
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
724
+ run !== void 0 && run.state === "running" && sessionId !== null ? /* @__PURE__ */ jsx(Button, {
725
+ variant: stopping ? "outline" : "danger-outline",
726
+ size: "xs",
727
+ "data-testid": "runner-log-stop",
728
+ disabled: stopping,
729
+ title: stopping ? "stop requested; the runner reports its own exit" : "ask the runtime to stop this runner",
730
+ onClick: () => requestRunnerStop(sendSessionFrame, sessionId, run.id),
731
+ className: "px-2 ",
732
+ children: stopping ? "stopping…" : "stop"
733
+ }) : null
734
+ ]
735
+ }),
736
+ run ? /* @__PURE__ */ jsxs("div", {
737
+ className: "flex shrink-0 flex-col gap-1.5 border-b border-doom-border-soft px-3 py-2.5 sm:px-[26px]",
738
+ children: [/* @__PURE__ */ jsx(MetaRow, {
739
+ label: "command",
740
+ value: run.command
741
+ }), /* @__PURE__ */ jsx(MetaRow, {
742
+ label: "cwd",
743
+ value: run.cwd
744
+ })]
745
+ }) : null,
746
+ /* @__PURE__ */ jsxs("div", {
747
+ className: "flex min-h-10 shrink-0 flex-wrap items-center gap-2 border-b border-doom-border-soft px-3 py-2 sm:h-10 sm:flex-nowrap sm:gap-2.5 sm:px-[26px] sm:py-0",
748
+ children: [
749
+ /* @__PURE__ */ jsxs("span", {
750
+ className: "relative flex min-w-0 flex-1 items-center sm:w-[300px] sm:flex-none",
751
+ children: [/* @__PURE__ */ jsx(SearchIcon, { className: "pointer-events-none absolute left-2.5 h-3 w-3 text-doom-faint" }), /* @__PURE__ */ jsx(Input, {
752
+ size: "sm",
753
+ "data-testid": "runner-log-search",
754
+ value: typed,
755
+ placeholder: "search this log",
756
+ onChange: (event) => setTyped(event.target.value),
757
+ className: "pl-7 "
758
+ })]
759
+ }),
760
+ /* @__PURE__ */ jsx(Button, {
761
+ variant: ignoreCase ? "subtle" : "outline",
762
+ size: "xs",
763
+ "data-testid": "runner-log-ignore-case",
764
+ "aria-pressed": ignoreCase,
765
+ title: "match regardless of case",
766
+ onClick: () => setIgnoreCase((current) => !current),
767
+ className: "px-2 ",
768
+ children: "aA"
769
+ }),
770
+ /* @__PURE__ */ jsx(Button, {
771
+ variant: context > 0 && filtering ? "subtle" : "outline",
772
+ size: "xs",
773
+ "data-testid": "runner-log-context",
774
+ disabled: !filtering,
775
+ title: "lines of context kept either side of a match",
776
+ onClick: () => setContext((current) => current === 0 ? CONTEXT_LINES : 0),
777
+ className: "px-2 ",
778
+ children: `± ${String(context)} lines`
779
+ }),
780
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
781
+ /* @__PURE__ */ jsxs("span", {
782
+ "data-testid": "runner-log-stats",
783
+ className: "min-w-0 basis-full truncate text-sm text-doom-dim sm:basis-auto",
784
+ children: [slice === void 0 ? "reading…" : filtering ? `${String(slice.lineCount)} matching of ${slice.totalLines.toLocaleString("en-US")} lines` : `${live ? "tailing" : "showing"} last ${String(view.lines.length)} of ${slice.totalLines.toLocaleString("en-US")} lines`, view.hidden > 0 ? ` · ${view.hidden.toLocaleString("en-US")} older hidden` : ""]
785
+ }),
786
+ /* @__PURE__ */ jsx(Button, {
787
+ variant: live ? "subtle" : "outline",
788
+ size: "xs",
789
+ "data-testid": "runner-log-follow",
790
+ "aria-pressed": live,
791
+ disabled: filtering || slice?.running !== true,
792
+ title: filtering ? "a filtered view is a snapshot; clear the query to follow" : "follow the log as it grows",
793
+ onClick: () => setFollowing((current) => !current),
794
+ className: "px-2 ",
795
+ children: filtering ? "follow · paused" : "follow"
796
+ })
797
+ ]
798
+ }),
799
+ /* @__PURE__ */ jsxs("div", {
800
+ className: "relative min-h-0 flex-1",
801
+ children: [/* @__PURE__ */ jsxs("div", {
802
+ ref: body,
803
+ onScroll,
804
+ "data-testid": "runner-log-body",
805
+ className: "h-full overflow-auto bg-doom-deep px-3 py-3 sm:px-[26px]",
806
+ children: [error !== void 0 ? /* @__PURE__ */ jsx("p", {
807
+ "data-testid": "runner-log-error",
808
+ className: "text-base text-doom-red",
809
+ children: error
810
+ }) : slice === void 0 ? /* @__PURE__ */ jsx("p", {
811
+ "data-testid": "runner-log-reading",
812
+ className: "text-sm text-doom-dim",
813
+ children: "reading…"
814
+ }) : slice.exists === false ? /* @__PURE__ */ jsx("p", {
815
+ className: "text-sm text-doom-dim",
816
+ children: "this runner has not written a log yet"
817
+ }) : view.lines.length === 0 ? /* @__PURE__ */ jsx("p", {
818
+ className: "text-sm text-doom-dim",
819
+ children: filtering ? "nothing matched" : "the log is empty"
820
+ }) : view.lines.map((line, index) => {
821
+ const number = numbers[index];
822
+ const previous = numbers[index - 1];
823
+ const gap = previous !== void 0 && number !== void 0 && number - previous > 1;
824
+ return /* @__PURE__ */ jsxs(Fragment, { children: [gap ? /* @__PURE__ */ jsx("div", {
825
+ "data-testid": "runner-log-gap",
826
+ "aria-hidden": true,
827
+ className: "select-none py-0.5 pl-12 text-xs leading-relaxed text-doom-faint",
828
+ children: "⋯"
829
+ }) : null, /* @__PURE__ */ jsx(LogRow, {
830
+ number,
831
+ line,
832
+ matched: filtering && isMatch(line, query, ignoreCase)
833
+ })] }, number ?? `row-${String(index)}`);
834
+ }), /* @__PURE__ */ jsx("div", { ref: bottom })]
835
+ }), live && !pinned ? /* @__PURE__ */ jsx(Button, {
836
+ variant: "subtle",
837
+ size: "xs",
838
+ "data-testid": "runner-log-jump",
839
+ title: "scroll back to the newest lines and keep following",
840
+ onClick: () => {
841
+ setPinned(true);
842
+ bottom.current?.scrollIntoView({ block: "end" });
843
+ },
844
+ className: "absolute bottom-3 right-3 px-2 ",
845
+ children: "jump to latest"
846
+ }) : null]
847
+ }),
848
+ /* @__PURE__ */ jsxs("div", {
849
+ className: "flex h-8 shrink-0 items-center gap-2.5 border-t border-doom-border-soft px-3 sm:px-[26px]",
850
+ children: [/* @__PURE__ */ jsx("span", {
851
+ "data-testid": "runner-log-path",
852
+ className: "min-w-0 flex-1 truncate font-mono text-sm text-doom-dim",
853
+ children: run?.logPath ?? slice?.path ?? ""
854
+ }), slice !== void 0 ? /* @__PURE__ */ jsx("span", {
855
+ className: "shrink-0 text-sm text-doom-dim",
856
+ children: formatSize$1(slice.fileSize)
857
+ }) : null]
858
+ })
859
+ ]
860
+ });
861
+ }
862
+ //#endregion
863
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/screenApi.ts
864
+ /**
865
+ * The page's half of the attached pane.
866
+ *
867
+ * Two directions over two mechanisms, because the hub proxies requests and
868
+ * server-sent events but cannot upgrade a socket: pane bytes arrive on an
869
+ * EventSource, and keystrokes go back as ordinary POSTs. Both addresses come
870
+ * from the generated client: the stream route offers a URL and no call method,
871
+ * which is exactly what an EventSource wants and all it can be given.
872
+ */
873
+ /** Turns one base64 chunk back into the bytes a terminal was sent. */
874
+ function decodeChunk(chunk) {
875
+ const binary = atob(chunk);
876
+ const bytes = new Uint8Array(binary.length);
877
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
878
+ return bytes;
879
+ }
880
+ /** Attempts before a dropped screen stream is reported as lost rather than retried. */
881
+ const MAX_RETRIES = 5;
882
+ const RETRY_BASE_MS = 500;
883
+ function isRecord$1(value) {
884
+ return typeof value === "object" && value !== null && !Array.isArray(value);
885
+ }
886
+ function watchRunnerScreen(sessionId, runId, from, handlers) {
887
+ let offset = from;
888
+ let retries = 0;
889
+ let closed = false;
890
+ let source;
891
+ let timer;
892
+ const open = () => {
893
+ if (closed) return;
894
+ const current = new EventSource(api.session(sessionId).screenStream.url({
895
+ params: { [RUN_ID_PARAM]: runId },
896
+ query: { [RUNNER_LOG_PARAMS.from]: offset }
897
+ }));
898
+ source = current;
899
+ current.addEventListener(RUNNER_SCREEN_EVENT, (message) => {
900
+ let parsed;
901
+ try {
902
+ parsed = JSON.parse(message.data);
903
+ } catch {
904
+ return;
905
+ }
906
+ if (!isRecord$1(parsed) || typeof parsed.chunk !== "string") return;
907
+ const event = parsed;
908
+ if (typeof event.offset === "number") offset = event.offset;
909
+ retries = 0;
910
+ if (event.ended === true) {
911
+ closed = true;
912
+ current.close();
913
+ }
914
+ handlers.onEvent(event);
915
+ });
916
+ current.addEventListener("error", () => {
917
+ current.close();
918
+ if (closed) return;
919
+ retries += 1;
920
+ if (retries > MAX_RETRIES) {
921
+ handlers.onLost();
922
+ return;
923
+ }
924
+ timer = setTimeout(open, RETRY_BASE_MS * 2 ** (retries - 1));
925
+ });
926
+ };
927
+ open();
928
+ return { close: () => {
929
+ closed = true;
930
+ if (timer !== void 0) clearTimeout(timer);
931
+ source?.close();
932
+ } };
933
+ }
934
+ /** Sends one batch of keystrokes. Resolves to whether the pane took them. */
935
+ async function sendRunnerInput(sessionId, runId, text) {
936
+ const body = { text };
937
+ return (await api.session(sessionId).input({
938
+ params: { [RUN_ID_PARAM]: runId },
939
+ body
940
+ })).ok;
941
+ }
942
+ //#endregion
943
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/RunnerShellPanel.tsx
944
+ const TAB_ID_PREFIX = "runner-shell-";
945
+ /** Keystrokes are gathered for this long and sent as one request. */
946
+ const FLUSH_MS = 30;
947
+ /** The temporary tab holding one interactive runner's pane. */
948
+ function runnerShellTab(run) {
949
+ const runId = run.id;
950
+ return {
951
+ id: `${TAB_ID_PREFIX}${runId}`,
952
+ label: `shell · ${run.name}`,
953
+ panel: (props) => /* @__PURE__ */ jsx(RunnerShellPanel, {
954
+ ...props,
955
+ runId
956
+ })
957
+ };
958
+ }
959
+ /**
960
+ * An interactive runner's pane, attached.
961
+ *
962
+ * A real terminal emulator, fed the pane's own bytes. The runner's sink keeps
963
+ * an unscrubbed copy of its output beside the log for exactly this: the log is
964
+ * stripped of cursor movement so it stays worth grepping, which is precisely
965
+ * what a terminal needs. Because these are the original bytes, colour, cursor
966
+ * placement and full-screen programs all render the way they would in a shell.
967
+ *
968
+ * xterm is loaded when the tab opens rather than with the plugin. Most sessions
969
+ * never attach to anything, and an emulator is not a small thing to carry.
970
+ */
971
+ function RunnerShellPanel({ sessionId, runId }) {
972
+ const { runs } = useStore(runners.store, (state) => runners.select(state, sessionId));
973
+ const run = runs.find((candidate) => candidate.id === runId);
974
+ const [ended, setEnded] = useState(false);
975
+ const [lost, setLost] = useState(false);
976
+ const [ready, setReady] = useState(false);
977
+ const terminal = useRef(null);
978
+ const pending = useRef([]);
979
+ const flush = useRef(void 0);
980
+ useEffect(() => {
981
+ if (sessionId === null || !ready) return;
982
+ const watch = watchRunnerScreen(sessionId, runId, 0, {
983
+ onEvent: (event) => {
984
+ if (event.chunk !== "") terminal.current?.write(decodeChunk(event.chunk));
985
+ if (event.ended === true) setEnded(true);
986
+ },
987
+ onLost: () => setLost(true)
988
+ });
989
+ return () => watch.close();
990
+ }, [
991
+ sessionId,
992
+ runId,
993
+ ready
994
+ ]);
995
+ useEffect(() => () => {
996
+ if (flush.current !== void 0) clearTimeout(flush.current);
997
+ }, []);
998
+ const send = (text) => {
999
+ if (sessionId === null) return;
1000
+ pending.current.push(text);
1001
+ if (flush.current !== void 0) return;
1002
+ flush.current = setTimeout(() => {
1003
+ const batch = pending.current.join("");
1004
+ pending.current = [];
1005
+ flush.current = void 0;
1006
+ if (batch !== "") sendRunnerInput(sessionId, runId, batch);
1007
+ }, FLUSH_MS);
1008
+ };
1009
+ const live = run?.state === "running" && !ended;
1010
+ return /* @__PURE__ */ jsxs("div", {
1011
+ "data-testid": "runner-shell-panel",
1012
+ className: "flex min-h-0 flex-1 flex-col",
1013
+ children: [
1014
+ /* @__PURE__ */ jsxs("div", {
1015
+ className: "flex min-h-11 shrink-0 items-center gap-2.5 border-b border-doom-border-soft px-3 sm:h-11 sm:px-[26px]",
1016
+ children: [
1017
+ /* @__PURE__ */ jsx("span", {
1018
+ "data-testid": "runner-shell-name",
1019
+ className: "shrink-0 truncate text-base font-semibold text-doom-hi",
1020
+ children: run?.name ?? runId
1021
+ }),
1022
+ /* @__PURE__ */ jsx(StatusBadge, {
1023
+ tone: live ? "running" : "neutral",
1024
+ "data-testid": "runner-shell-state",
1025
+ children: live ? "attached" : "detached"
1026
+ }),
1027
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
1028
+ lost ? /* @__PURE__ */ jsx("span", {
1029
+ "data-testid": "runner-shell-lost",
1030
+ className: "shrink-0 text-base text-doom-red",
1031
+ children: "the connection to this pane dropped"
1032
+ }) : null,
1033
+ live ? /* @__PURE__ */ jsx(Button, {
1034
+ variant: "outline",
1035
+ size: "xs",
1036
+ "data-testid": "runner-shell-interrupt",
1037
+ title: "send ctrl-c",
1038
+ onClick: () => send(""),
1039
+ className: "px-2 ",
1040
+ children: "ctrl-c"
1041
+ }) : null
1042
+ ]
1043
+ }),
1044
+ /* @__PURE__ */ jsxs("div", {
1045
+ className: "relative min-h-0 flex-1 overflow-hidden bg-doom-deep px-3 py-2 sm:px-[26px]",
1046
+ children: [/* @__PURE__ */ jsx(TerminalView, {
1047
+ ref: terminal,
1048
+ "data-testid": "runner-shell-terminal",
1049
+ onData: (text) => send(text),
1050
+ onReady: () => setReady(true)
1051
+ }), ready ? null : /* @__PURE__ */ jsx("p", {
1052
+ "data-testid": "runner-shell-reading",
1053
+ className: "text-sm text-doom-dim",
1054
+ children: "attaching…"
1055
+ })]
1056
+ }),
1057
+ /* @__PURE__ */ jsx("div", {
1058
+ className: "flex h-8 shrink-0 items-center border-t border-doom-border-soft px-3 sm:px-[26px]",
1059
+ children: /* @__PURE__ */ jsx("span", {
1060
+ className: "min-w-0 flex-1 truncate text-sm text-doom-dim",
1061
+ children: live ? "typing goes straight to the runner" : "this runner is no longer accepting input"
1062
+ })
1063
+ })
1064
+ ]
1065
+ });
1066
+ }
1067
+ //#endregion
1068
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/runnerTail.ts
1069
+ /**
1070
+ * The newest line a running runner has written, for a row that has one line to
1071
+ * spend.
1072
+ *
1073
+ * The dock answers "what is happening", and a command that has not changed
1074
+ * since it started answers "what was asked". The last line of the log is the
1075
+ * closest thing to progress the cockpit can show without opening the log, so a
1076
+ * row that is still running trades its command for it.
1077
+ *
1078
+ * The line comes from the same two calls the log panel makes: read the tail,
1079
+ * then follow the stream from exactly where that read ended. Nothing new goes
1080
+ * on the wire, and a runner nobody is looking at is not followed, because the
1081
+ * stream lives and dies with the row.
1082
+ */
1083
+ /** Enough lines that a trailing blank, or a final progress redraw, is not the whole answer. */
1084
+ const TAIL_REQUEST_LINES = 5;
1085
+ /**
1086
+ * The last line worth showing, as plain text.
1087
+ *
1088
+ * Blank lines are skipped, because a log that ends with a newline would
1089
+ * otherwise report nothing at all. Colour is dropped rather than rendered: the
1090
+ * row is nine pixels of faint text beside a name and a stop control, and a
1091
+ * green tick from someone else's palette in the middle of it reads as damage.
1092
+ */
1093
+ function plainTailLine(lines) {
1094
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
1095
+ const text = ansiSpans(lines[index] ?? "").map((span) => span.text).join("").trim();
1096
+ if (text.length > 0) return text;
1097
+ }
1098
+ }
1099
+ /**
1100
+ * The newest log line of a running runner, or undefined for a runner that is
1101
+ * finished, unreachable, or has not written anything yet. The caller decides
1102
+ * what to show instead; this hook never invents a placeholder.
1103
+ */
1104
+ function useRunnerTail(sessionId, runId, running) {
1105
+ const [tail, setTail] = useState(void 0);
1106
+ useEffect(() => {
1107
+ if (!running || sessionId === null) return;
1108
+ let live = true;
1109
+ let follow;
1110
+ const controller = new AbortController();
1111
+ fetchRunnerLog(sessionId, runId, { lines: TAIL_REQUEST_LINES }, controller.signal).then((result) => {
1112
+ if (!live || "error" in result) return;
1113
+ const initial = plainTailLine(result.slice.text.split("\n"));
1114
+ if (initial !== void 0) setTail({
1115
+ runId,
1116
+ text: initial
1117
+ });
1118
+ if (!result.slice.running) return;
1119
+ follow = followRunnerLog(sessionId, runId, result.slice.completeBytes, {
1120
+ onEvent: (event) => {
1121
+ const next = plainTailLine(event.lines);
1122
+ if (live && next !== void 0) setTail({
1123
+ runId,
1124
+ text: next
1125
+ });
1126
+ },
1127
+ onLost: () => void 0
1128
+ });
1129
+ });
1130
+ return () => {
1131
+ live = false;
1132
+ controller.abort();
1133
+ follow?.close();
1134
+ };
1135
+ }, [
1136
+ sessionId,
1137
+ runId,
1138
+ running
1139
+ ]);
1140
+ return running && tail?.runId === runId ? tail.text : void 0;
1141
+ }
1142
+ //#endregion
1143
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/RunnersPanel.tsx
1144
+ const TICK_MS$1 = 1e4;
1145
+ const SHELL_LAUNCH_TIMEOUT_MS = 3e4;
1146
+ const RUNNERS_TAB_ID = "runner-runs";
1147
+ /** The temporary tab the dock's runners group opens from its own name. */
1148
+ function runnersTab() {
1149
+ return {
1150
+ id: RUNNERS_TAB_ID,
1151
+ label: "runners",
1152
+ panel: RunnersPanel
1153
+ };
1154
+ }
1155
+ /**
1156
+ * Everything this session has up, one card each.
1157
+ *
1158
+ * The dock's rail gives a runner one line, which is enough to notice it and
1159
+ * not enough to work with it. This is the same set with room: the command in
1160
+ * full, where it runs, and the last thing it said.
1161
+ *
1162
+ * Running runners only, the same rule the rail follows. A finished run leaves
1163
+ * the moment it exits, because this answers "what is happening"; its log stays
1164
+ * readable from its own tab, and `doom-runner list` is where history lives.
1165
+ */
1166
+ function RunnersPanel({ sessionId, sendSessionFrame, openTransientTab }) {
1167
+ const session = useStore(runners.store, (state) => runners.select(state, sessionId));
1168
+ const running = session.runs.filter((run) => run.state === "running");
1169
+ const [now, setNow] = useState(() => Date.now());
1170
+ const [launching, setLaunching] = useState(false);
1171
+ const shellBaseline = useRef(null);
1172
+ const shellLaunchTimeout = useRef(void 0);
1173
+ const defaultCwd = running[0]?.cwd;
1174
+ const startShell = () => {
1175
+ if (sessionId === null) return;
1176
+ shellBaseline.current = new Set(session.runs.map((run) => run.id));
1177
+ clearTimeout(shellLaunchTimeout.current);
1178
+ shellLaunchTimeout.current = setTimeout(() => {
1179
+ shellBaseline.current = null;
1180
+ }, SHELL_LAUNCH_TIMEOUT_MS);
1181
+ requestRunnerStart(sendSessionFrame, sessionId, RUNNER_SHELL_REQUEST);
1182
+ };
1183
+ useEffect(() => {
1184
+ const timer = setInterval(() => setNow(Date.now()), TICK_MS$1);
1185
+ return () => clearInterval(timer);
1186
+ }, []);
1187
+ useEffect(() => {
1188
+ const baseline = shellBaseline.current;
1189
+ if (baseline === null) return;
1190
+ const shell = session.runs.find((run) => run.state === "running" && run.interactive && !baseline.has(run.id));
1191
+ if (!shell) return;
1192
+ shellBaseline.current = null;
1193
+ clearTimeout(shellLaunchTimeout.current);
1194
+ openTransientTab(runnerShellTab(shell));
1195
+ }, [openTransientTab, session.runs]);
1196
+ useEffect(() => () => clearTimeout(shellLaunchTimeout.current), []);
1197
+ return /* @__PURE__ */ jsxs("div", {
1198
+ "data-testid": "runners-panel",
1199
+ className: "flex min-h-0 flex-1 flex-col overflow-auto",
1200
+ children: [
1201
+ /* @__PURE__ */ jsxs("div", {
1202
+ className: "flex min-h-11 shrink-0 items-center gap-2.5 border-b border-doom-border-soft px-3 sm:h-11 sm:px-[26px]",
1203
+ children: [
1204
+ /* @__PURE__ */ jsx("span", {
1205
+ className: "text-lg font-semibold text-doom-hi",
1206
+ children: "runners"
1207
+ }),
1208
+ /* @__PURE__ */ jsx("span", {
1209
+ "data-testid": "runners-panel-count",
1210
+ className: "text-sm text-doom-dim",
1211
+ children: running.length === 0 ? "nothing running" : `${String(running.length)} running`
1212
+ }),
1213
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
1214
+ sessionId === null ? null : /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
1215
+ asChild: true,
1216
+ children: /* @__PURE__ */ jsx(Button, {
1217
+ variant: "ghost",
1218
+ size: "icon",
1219
+ "data-testid": "runners-panel-launch",
1220
+ title: "launch a runner",
1221
+ "aria-label": "launch a runner",
1222
+ className: "text-doom-faint hover:text-doom-hi",
1223
+ children: /* @__PURE__ */ jsx(PlusIcon, { className: "h-3 w-3" })
1224
+ })
1225
+ }), /* @__PURE__ */ jsxs(DropdownMenuContent, {
1226
+ align: "end",
1227
+ "data-testid": "runners-panel-launch-menu",
1228
+ children: [/* @__PURE__ */ jsx(DropdownMenuItem, {
1229
+ "data-testid": "runners-panel-start-shell",
1230
+ onSelect: startShell,
1231
+ children: "start shell"
1232
+ }), /* @__PURE__ */ jsx(DropdownMenuItem, {
1233
+ "data-testid": "runners-panel-run-command",
1234
+ onSelect: () => setLaunching(true),
1235
+ children: "run command"
1236
+ })]
1237
+ })] })
1238
+ ]
1239
+ }),
1240
+ running.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
1241
+ "data-testid": "runners-panel-empty",
1242
+ className: "px-4 py-8",
1243
+ title: "nothing running",
1244
+ description: "a command that outlives its turn moves to a runner and appears here while it works.",
1245
+ children: sessionId === null ? null : /* @__PURE__ */ jsx(Button, {
1246
+ variant: "outline",
1247
+ size: "xs",
1248
+ "data-testid": "runners-panel-launch-empty",
1249
+ onClick: () => setLaunching(true),
1250
+ className: "px-2 ",
1251
+ children: "launch a runner"
1252
+ })
1253
+ }) : /* @__PURE__ */ jsx("div", {
1254
+ "data-testid": "runners-panel-grid",
1255
+ className: "grid grid-cols-1 gap-2.5 px-3 py-3 sm:grid-cols-2 sm:px-[26px] xl:grid-cols-3",
1256
+ children: running.map((run) => /* @__PURE__ */ jsx(RunnerCard, {
1257
+ run,
1258
+ now,
1259
+ stopRequested: session.stopRequested.includes(run.id),
1260
+ sessionId,
1261
+ sendSessionFrame,
1262
+ openTransientTab
1263
+ }, run.id))
1264
+ }),
1265
+ launching && sessionId !== null ? /* @__PURE__ */ jsx(LaunchRunnerDialog, {
1266
+ sessionId,
1267
+ sendSessionFrame,
1268
+ ...defaultCwd === void 0 ? {} : { defaultCwd },
1269
+ onClose: () => setLaunching(false)
1270
+ }) : null
1271
+ ]
1272
+ });
1273
+ }
1274
+ /**
1275
+ * One running runner. Its own component because the tail line is a
1276
+ * subscription per card, and a card that leaves the grid closes it.
1277
+ */
1278
+ function RunnerCard({ run, now, stopRequested, sessionId, sendSessionFrame, openTransientTab }) {
1279
+ const tail = useRunnerTail(sessionId, run.id, run.state === "running");
1280
+ return /* @__PURE__ */ jsxs("div", {
1281
+ "data-testid": `runners-card-${run.id}`,
1282
+ className: "flex min-w-0 flex-col gap-1.5 rounded-md border border-doom-border-soft bg-doom-panel p-2.5",
1283
+ children: [
1284
+ /* @__PURE__ */ jsxs("div", {
1285
+ className: "flex min-w-0 items-center gap-1.5",
1286
+ children: [
1287
+ /* @__PURE__ */ jsx(Dot, {
1288
+ tone: "yellow",
1289
+ pulse: true
1290
+ }),
1291
+ /* @__PURE__ */ jsx("span", {
1292
+ className: "min-w-0 flex-1 truncate text-base font-semibold text-doom-hi",
1293
+ children: run.name
1294
+ }),
1295
+ /* @__PURE__ */ jsxs("span", {
1296
+ className: "shrink-0 text-sm text-doom-dim",
1297
+ children: [formatRunnerUptime(run.startedAt, now), run.interactive ? " · tty" : ""]
1298
+ })
1299
+ ]
1300
+ }),
1301
+ /* @__PURE__ */ jsx("span", {
1302
+ className: "min-w-0 break-all font-mono text-sm leading-normal text-doom-dim",
1303
+ children: run.command
1304
+ }),
1305
+ /* @__PURE__ */ jsx("span", {
1306
+ className: "min-w-0 truncate font-mono text-sm text-doom-dim",
1307
+ children: run.cwd
1308
+ }),
1309
+ /* @__PURE__ */ jsx("span", {
1310
+ "data-testid": `runners-card-detail-${run.id}`,
1311
+ "data-detail": tail === void 0 ? "command" : "tail",
1312
+ className: "min-w-0 truncate text-sm text-doom-dim",
1313
+ children: tail ?? "…"
1314
+ }),
1315
+ /* @__PURE__ */ jsxs("div", {
1316
+ className: "flex items-center gap-1.5 pt-0.5",
1317
+ children: [
1318
+ /* @__PURE__ */ jsx(Button, {
1319
+ variant: "outline",
1320
+ size: "xs",
1321
+ "data-testid": `runners-card-log-${run.id}`,
1322
+ title: "open this runner's log",
1323
+ onClick: () => openTransientTab(runnerLogTab(run)),
1324
+ className: "px-2 ",
1325
+ children: "log"
1326
+ }),
1327
+ run.interactive && sessionId !== null ? /* @__PURE__ */ jsx(Button, {
1328
+ variant: "outline",
1329
+ size: "xs",
1330
+ "data-testid": `runners-card-shell-${run.id}`,
1331
+ title: "attach to this runner's terminal",
1332
+ onClick: () => openTransientTab(runnerShellTab(run)),
1333
+ className: "px-2 ",
1334
+ children: "shell"
1335
+ }) : null,
1336
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
1337
+ sessionId !== null ? /* @__PURE__ */ jsx(Button, {
1338
+ variant: stopRequested ? "outline" : "danger-outline",
1339
+ size: "xs",
1340
+ "data-testid": `runners-card-stop-${run.id}`,
1341
+ disabled: stopRequested,
1342
+ title: stopRequested ? "stop requested; the runner reports its own exit" : "ask the runtime to stop this runner",
1343
+ onClick: () => requestRunnerStop(sendSessionFrame, sessionId, run.id),
1344
+ className: "px-2 ",
1345
+ children: stopRequested ? "stopping…" : "stop"
1346
+ }) : null
1347
+ ]
1348
+ })
1349
+ ]
1350
+ });
1351
+ }
1352
+ //#endregion
1353
+ //#region src/extensions/workspaces/sessions/(frontend)/activity-group/runners.web.ts
1354
+ var runners_web_default = defineActivityGroup({
1355
+ keys: "r l",
1356
+ icon: PlayCircleIcon,
1357
+ statusKey: "doom-runner-runners",
1358
+ activeSource: runnerActivitySource,
1359
+ transientTab: runnersTab,
1360
+ order: 20
1361
+ });
1362
+ //#endregion
1363
+ //#region src/extensions/workspaces/sessions/(frontend)/channel/runner-runs.web.ts
1364
+ var runner_runs_web_default = defineChannelFile(runnerRunsChannel);
1365
+ //#endregion
1366
+ //#region src/extensions/workspaces/sessions/(frontend)/fill/_components/RunnersActivitySection.tsx
1367
+ const TICK_MS = 1e4;
1368
+ /**
1369
+ * The runners group's body in the activity dock: what this session has up
1370
+ * right now.
1371
+ *
1372
+ * Only running runners are listed. A finished one leaves the moment it exits,
1373
+ * because the dock answers "what is happening", and a rail that keeps ten
1374
+ * completed playwright calls around buries the one command still working. The
1375
+ * runtime keeps every record; `doom-runner list` and the run's own log tab are
1376
+ * where a finished run is read.
1377
+ */
1378
+ function RunnersActivitySection({ sessionId, sendSessionFrame, openTransientTab }) {
1379
+ const session = useStore(runners.store, (state) => runners.select(state, sessionId));
1380
+ const running = session.runs.filter((run) => run.state === "running");
1381
+ const [now, setNow] = useState(() => Date.now());
1382
+ useEffect(() => {
1383
+ const timer = setInterval(() => setNow(Date.now()), TICK_MS);
1384
+ return () => clearInterval(timer);
1385
+ }, []);
1386
+ if (running.length === 0) return /* @__PURE__ */ jsxs("div", {
1387
+ className: "flex flex-wrap items-center gap-x-4 gap-y-1",
1388
+ children: [/* @__PURE__ */ jsx("p", {
1389
+ "data-testid": "activity-summary-runners",
1390
+ className: "text-sm text-doom-dim",
1391
+ children: "Idle"
1392
+ }), sessionId === null ? null : /* @__PURE__ */ jsxs(Button, {
1393
+ variant: "link",
1394
+ size: "sm",
1395
+ "data-testid": "activity-runner-launch",
1396
+ className: "h-auto gap-1 px-0 text-doom-green",
1397
+ onClick: () => openTransientTab(runnersTab()),
1398
+ children: ["Launch a runner", /* @__PURE__ */ jsx(ArrowRightIcon, {
1399
+ "aria-hidden": true,
1400
+ className: "size-3.5"
1401
+ })]
1402
+ })]
1403
+ });
1404
+ return /* @__PURE__ */ jsx("div", {
1405
+ "data-testid": "activity-runner-runs",
1406
+ className: "flex flex-col gap-0.5",
1407
+ children: running.map((run) => /* @__PURE__ */ jsx(RunnerRow, {
1408
+ run,
1409
+ now,
1410
+ stopRequested: session.stopRequested.includes(run.id),
1411
+ sessionId,
1412
+ sendSessionFrame,
1413
+ openTransientTab
1414
+ }, run.id))
1415
+ });
1416
+ }
1417
+ /**
1418
+ * One running runner: its name, how long it has been up, a stop control, and
1419
+ * one line of what it is doing. Its own component because the tail line is a
1420
+ * subscription per row, and a row that scrolls out of the dock closes it.
1421
+ */
1422
+ function RunnerRow({ run, now, stopRequested, sessionId, sendSessionFrame, openTransientTab }) {
1423
+ const tail = useRunnerTail(sessionId, run.id, run.state === "running");
1424
+ return /* @__PURE__ */ jsxs(Button, {
1425
+ variant: "ghost",
1426
+ size: "card",
1427
+ "data-testid": `activity-runner-${run.id}`,
1428
+ "data-runner-tone": "running",
1429
+ title: "open this runner's log",
1430
+ onClick: () => {
1431
+ if (sessionId === null) return;
1432
+ openTransientTab(runnerLogTab(run));
1433
+ },
1434
+ className: "min-w-0 gap-0.5 rounded-md px-1 py-1 hover:bg-doom-panel",
1435
+ children: [/* @__PURE__ */ jsxs("span", {
1436
+ className: "flex min-w-0 items-center gap-1.5",
1437
+ children: [
1438
+ /* @__PURE__ */ jsx(Dot, {
1439
+ tone: "yellow",
1440
+ pulse: true
1441
+ }),
1442
+ /* @__PURE__ */ jsx("span", {
1443
+ className: "min-w-0 flex-1 truncate text-left text-base font-semibold text-doom-hi",
1444
+ children: run.name
1445
+ }),
1446
+ /* @__PURE__ */ jsxs("span", {
1447
+ className: "shrink-0 text-sm text-doom-dim",
1448
+ children: [formatRunnerUptime(run.startedAt, now), run.interactive ? " · tty" : ""]
1449
+ }),
1450
+ sessionId !== null ? /* @__PURE__ */ jsx(Button, {
1451
+ asChild: true,
1452
+ variant: stopRequested ? "outline" : "danger-outline",
1453
+ size: "xs",
1454
+ "data-testid": `activity-runner-stop-${run.id}`,
1455
+ "data-stopping": stopRequested,
1456
+ title: stopRequested ? "stop requested; the runner reports its own exit" : "ask the runtime to stop this runner",
1457
+ className: "px-1.5",
1458
+ children: /* @__PURE__ */ jsx("span", {
1459
+ role: "button",
1460
+ tabIndex: stopRequested ? -1 : 0,
1461
+ "aria-disabled": stopRequested,
1462
+ onClick: (event) => {
1463
+ event.stopPropagation();
1464
+ if (!stopRequested) requestRunnerStop(sendSessionFrame, sessionId, run.id);
1465
+ },
1466
+ onKeyDown: (event) => {
1467
+ if (event.key !== "Enter" && event.key !== " ") return;
1468
+ event.preventDefault();
1469
+ event.stopPropagation();
1470
+ if (!stopRequested) requestRunnerStop(sendSessionFrame, sessionId, run.id);
1471
+ },
1472
+ children: stopRequested ? "stopping…" : "stop"
1473
+ })
1474
+ }) : null
1475
+ ]
1476
+ }), /* @__PURE__ */ jsx("span", {
1477
+ "data-testid": `activity-runner-detail-${run.id}`,
1478
+ "data-detail": tail === void 0 ? "command" : "tail",
1479
+ className: "truncate pl-3 text-left text-sm text-doom-dim",
1480
+ children: tail ?? run.command
1481
+ })]
1482
+ });
1483
+ }
1484
+ //#endregion
1485
+ //#region src/extensions/workspaces/sessions/(frontend)/fill/Runners.activity.runners.web.tsx
1486
+ var Runners_activity_runners_web_default = defineFill({ component: RunnersActivitySection });
1487
+ //#endregion
1488
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_lib/bashToolFormat.ts
1489
+ /**
1490
+ * Pure view logic for the browser bash tool card.
1491
+ * Every input is the wire JSON of a tool_execution frame.
1492
+ */
1493
+ const ELLIPSIS = "…";
1494
+ /** Spaced so the join never glues two words into one unreadable token. */
1495
+ const GAP = " … ";
1496
+ /** Wide enough to keep a realistic command intact, short enough to stay on a few rows. */
1497
+ const MAX_COMMAND_LENGTH = 160;
1498
+ /** Live output is unbounded while a command runs, so the running state keeps only a window. */
1499
+ const STREAM_TAIL_LINES = 12;
1500
+ const BYTES_PER_KB = 1024;
1501
+ const BYTES_PER_MB = BYTES_PER_KB * BYTES_PER_KB;
1502
+ /** The user's home on the two platforms the runner ships for; the browser cannot ask the OS. */
1503
+ const HOME_PATH = /(?<![\w.-])(?:\/Users|\/home)\/[^/\s]+(?=\/|\s|$)/g;
1504
+ function isRecord(value) {
1505
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1506
+ }
1507
+ /** Narrows the result details to the fields the card reads; anything else is ignored. */
1508
+ function bashResultDetails(details) {
1509
+ if (!isRecord(details)) return {};
1510
+ const out = {};
1511
+ if (typeof details.id === "string") out.id = details.id;
1512
+ if (typeof details.runner === "string") out.runner = details.runner;
1513
+ if (typeof details.exitCode === "number" || details.exitCode === null) out.exitCode = details.exitCode;
1514
+ if (typeof details.logPath === "string") out.logPath = details.logPath;
1515
+ if (typeof details.fileSize === "number") out.fileSize = details.fileSize;
1516
+ if (typeof details.lines === "number") out.lines = details.lines;
1517
+ if (details.timedOut === true) out.timedOut = true;
1518
+ if (details.promoted === true) out.promoted = true;
1519
+ if (typeof details.reason === "string") out.reason = details.reason;
1520
+ if (typeof details.tail === "string") out.tail = details.tail;
1521
+ if (typeof details.tailLines === "number") out.tailLines = details.tailLines;
1522
+ return out;
1523
+ }
1524
+ /** `/Users/me/x` becomes `~/x`. Absolute home paths are what push these lines onto extra rows. */
1525
+ function abbreviateHome(text) {
1526
+ return text.replace(HOME_PATH, "~");
1527
+ }
1528
+ /** Collapse a heredoc or multi-line script to one scannable line plus a count. */
1529
+ function collapseCommand(command) {
1530
+ const lines = command.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
1531
+ const first = lines[0] ?? "";
1532
+ return lines.length > 1 ? `${first} ${ELLIPSIS} +${lines.length - 1} lines` : first;
1533
+ }
1534
+ /** Drop the middle rather than the end, so a long pipeline keeps both its source and its sink. */
1535
+ function truncateMiddle(text, max = MAX_COMMAND_LENGTH) {
1536
+ if (text.length <= max) return text;
1537
+ const keep = max - 3;
1538
+ const head = Math.ceil(keep / 2);
1539
+ let start = text.slice(0, head);
1540
+ let end = text.slice(text.length - (keep - head));
1541
+ const lastSpace = start.lastIndexOf(" ");
1542
+ if (lastSpace > start.length / 2) start = start.slice(0, lastSpace);
1543
+ const firstSpace = end.indexOf(" ");
1544
+ if (firstSpace >= 0 && firstSpace < end.length / 2) end = end.slice(firstSpace + 1);
1545
+ return `${start}${GAP}${end}`;
1546
+ }
1547
+ function formatBashCommand(command) {
1548
+ return truncateMiddle(abbreviateHome(collapseCommand(command)));
1549
+ }
1550
+ /** Modifiers worth seeing at a glance, in the widget's `·` idiom. */
1551
+ function formatBashFlags(args) {
1552
+ const flags = [];
1553
+ if (args.background === true) flags.push("bg");
1554
+ if (args.interactive === true) flags.push("tty");
1555
+ if (typeof args.timeout === "number") flags.push(`${args.timeout}s`);
1556
+ if (typeof args.name === "string" && args.name.length > 0) flags.push(args.name);
1557
+ return flags;
1558
+ }
1559
+ /** Human-readable byte count, matching how pi reports output sizes. */
1560
+ function formatSize(bytes) {
1561
+ if (bytes < BYTES_PER_KB) return `${bytes} B`;
1562
+ if (bytes < BYTES_PER_MB) return `${(bytes / BYTES_PER_KB).toFixed(1)} KB`;
1563
+ return `${(bytes / BYTES_PER_MB).toFixed(1)} MB`;
1564
+ }
1565
+ /** The metadata footer, collapsed from five lines to one. */
1566
+ function formatResultSummary(details) {
1567
+ const parts = [];
1568
+ if (details.lines !== void 0) parts.push(`${details.lines.toLocaleString("en-US")} lines`);
1569
+ if (details.fileSize !== void 0) parts.push(formatSize(details.fileSize));
1570
+ if (details.runner !== void 0) parts.push(details.runner);
1571
+ return parts.join(" · ");
1572
+ }
1573
+ /** Last `max` lines, so a chatty command cannot push the transcript off screen. */
1574
+ function lastLines(text, max) {
1575
+ const all = text.split("\n");
1576
+ while (all.length > 0 && all[all.length - 1]?.trim() === "") all.pop();
1577
+ return all.length <= max ? all : all.slice(-max);
1578
+ }
1579
+ function bashResultView(input) {
1580
+ const details = bashResultDetails(input.details);
1581
+ if (input.isPartial) return {
1582
+ lines: lastLines(input.output, STREAM_TAIL_LINES),
1583
+ status: {
1584
+ glyph: "◐",
1585
+ tone: "running",
1586
+ text: "running"
1587
+ },
1588
+ hidden: 0
1589
+ };
1590
+ if (input.isError) {
1591
+ const all = lastLines(input.output, Number.MAX_SAFE_INTEGER);
1592
+ const lines = input.expanded ? all : all.slice(-12);
1593
+ const summary = formatResultSummary(details);
1594
+ return {
1595
+ lines,
1596
+ status: {
1597
+ glyph: "✗",
1598
+ tone: "error",
1599
+ text: summary.length > 0 ? summary : "failed"
1600
+ },
1601
+ hidden: all.length - lines.length
1602
+ };
1603
+ }
1604
+ if (details.promoted === true) return {
1605
+ lines: [],
1606
+ status: {
1607
+ glyph: "●",
1608
+ tone: "background",
1609
+ text: `${details.runner ?? details.id ?? "runner"}${details.id === void 0 ? "" : ` · ${details.id}`} · background`
1610
+ },
1611
+ hidden: 0
1612
+ };
1613
+ const all = lastLines(details.tail ?? "", Number.MAX_SAFE_INTEGER);
1614
+ const lines = input.expanded ? all : all.slice(-12);
1615
+ const exited = details.exitCode !== void 0 && details.exitCode !== null;
1616
+ const failed = details.timedOut === true || exited && details.exitCode !== 0;
1617
+ const meta = [];
1618
+ if (details.timedOut === true) meta.push("timed out");
1619
+ else if (failed) meta.push(`exit ${details.exitCode}`);
1620
+ const summary = formatResultSummary(details);
1621
+ if (summary.length > 0) meta.push(summary);
1622
+ const text = meta.length > 0 ? meta.join(" · ") : failed ? "failed" : lines.length === 0 ? "done" : "";
1623
+ return {
1624
+ lines,
1625
+ status: text.length > 0 ? {
1626
+ glyph: failed ? "✗" : "✓",
1627
+ tone: failed ? "error" : "ok",
1628
+ text
1629
+ } : null,
1630
+ hidden: all.length - lines.length
1631
+ };
1632
+ }
1633
+ //#endregion
1634
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_components/BashToolMessage.tsx
1635
+ const STATUS_TONE = {
1636
+ running: "running",
1637
+ ok: "ok",
1638
+ error: "error",
1639
+ background: "info"
1640
+ };
1641
+ /**
1642
+ * The bash tool's timeline item, the web half of renderBashCall and
1643
+ * renderBashResult. Collapsed it is a single header row: the tool name, the
1644
+ * command shortened to one line, its modifiers, and the outcome badge, so a
1645
+ * transcript of many calls stays scannable. Expanding it opens the body: the
1646
+ * command exactly as it was run, then the whole tail the frame carries, each
1647
+ * in its own scroll box so one chatty command cannot own the viewport. A
1648
+ * command promoted to a background runner offers the same stop the activity
1649
+ * dock does, for as long as the runners channel reports it running.
1650
+ */
1651
+ function BashToolMessage({ sessionId, args, result, output, running, isError, sendSessionFrame, openTransientTab }) {
1652
+ const details = bashResultDetails(result?.details);
1653
+ const runnerId = details.promoted === true ? details.id : void 0;
1654
+ const run = useStore(runners.store, (state) => details.id === void 0 ? void 0 : runners.select(state, sessionId).runs.find((entry) => entry.id === details.id));
1655
+ const stopping = useStore(runners.store, (state) => runnerId !== void 0 && runners.select(state, sessionId).stopRequested.includes(runnerId));
1656
+ const rawCommand = typeof args.command === "string" ? args.command : "";
1657
+ const command = formatBashCommand(rawCommand);
1658
+ const flags = formatBashFlags(args);
1659
+ const stoppable = sessionId !== null && runnerId !== void 0 && run !== void 0 && run.exit === void 0;
1660
+ return /* @__PURE__ */ jsx(MessageItem, {
1661
+ tone: toolTone({
1662
+ running,
1663
+ isError
1664
+ }),
1665
+ expandable: true,
1666
+ children: ({ expanded }) => {
1667
+ const view = bashResultView({
1668
+ details: result?.details,
1669
+ output,
1670
+ expanded: true,
1671
+ isPartial: running,
1672
+ isError
1673
+ });
1674
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(MessageItemHeader, {
1675
+ title: "bash",
1676
+ children: [
1677
+ /* @__PURE__ */ jsxs("span", {
1678
+ "data-testid": "tool-call-bash",
1679
+ className: "flex min-w-0 flex-1 items-center gap-2",
1680
+ children: [/* @__PURE__ */ jsx("span", {
1681
+ className: "min-w-0 flex-1 truncate font-mono text-doom-text",
1682
+ children: command
1683
+ }), flags.length > 0 ? /* @__PURE__ */ jsxs("span", {
1684
+ className: "hidden shrink-0 text-doom-dim sm:inline",
1685
+ children: ["· ", flags.join(" · ")]
1686
+ }) : null]
1687
+ }),
1688
+ run !== void 0 && sessionId !== null ? /* @__PURE__ */ jsxs(Button, {
1689
+ variant: "outline",
1690
+ size: "xs",
1691
+ "data-testid": "tool-result-bash-open-log",
1692
+ title: "open this runner's full log",
1693
+ onClick: (event) => {
1694
+ event.stopPropagation();
1695
+ openTransientTab(runnerLogTab(run));
1696
+ },
1697
+ className: "shrink-0 gap-1 px-1.5 ",
1698
+ children: [/* @__PURE__ */ jsx(FileIcon, { className: "h-2.5 w-2.5" }), "log"]
1699
+ }) : null,
1700
+ stoppable ? /* @__PURE__ */ jsx(Button, {
1701
+ size: "xs",
1702
+ variant: stopping ? "outline" : "danger-outline",
1703
+ disabled: stopping,
1704
+ "data-testid": "tool-result-bash-stop",
1705
+ "data-stopping": stopping,
1706
+ title: stopping ? "stop requested; the runner reports its own exit" : "ask the runtime to stop this runner",
1707
+ onClick: (event) => {
1708
+ event.stopPropagation();
1709
+ if (sessionId !== null && runnerId !== void 0) requestRunnerStop(sendSessionFrame, sessionId, runnerId);
1710
+ },
1711
+ className: "shrink-0 px-1.5 ",
1712
+ children: stopping ? "stopping…" : "stop"
1713
+ }) : null
1714
+ ]
1715
+ }), expanded ? /* @__PURE__ */ jsxs(MessageItemBody, {
1716
+ "data-testid": "tool-result-bash",
1717
+ className: "flex flex-col gap-2",
1718
+ children: [
1719
+ rawCommand.length > 0 ? /* @__PURE__ */ jsx(SyntaxText, {
1720
+ "data-testid": "tool-result-bash-command",
1721
+ grammar: "shell",
1722
+ text: rawCommand,
1723
+ className: "max-h-40 overflow-auto text-doom-text"
1724
+ }) : null,
1725
+ view.lines.length > 0 ? /* @__PURE__ */ jsx(AnsiText, {
1726
+ "data-testid": "tool-result-bash-output",
1727
+ text: view.lines.join("\n"),
1728
+ className: "max-h-96 overflow-auto whitespace-pre-wrap break-words [overflow-wrap:anywhere] border-doom-border-soft border-t pt-2 font-mono text-doom-dim"
1729
+ }) : null,
1730
+ view.status ? /* @__PURE__ */ jsx(MessageItemStatus, {
1731
+ "data-testid": "tool-result-bash-status",
1732
+ tone: STATUS_TONE[view.status.tone],
1733
+ glyph: view.status.glyph,
1734
+ className: "min-w-0",
1735
+ children: /* @__PURE__ */ jsx("span", {
1736
+ className: "break-words",
1737
+ children: view.status.text
1738
+ })
1739
+ }) : null,
1740
+ details.logPath !== void 0 ? /* @__PURE__ */ jsxs("span", {
1741
+ "data-testid": "tool-result-bash-log",
1742
+ className: "min-w-0 break-all text-doom-dim",
1743
+ children: ["full log · ", /* @__PURE__ */ jsx("span", {
1744
+ className: "text-doom-dim",
1745
+ children: details.logPath
1746
+ })]
1747
+ }) : null
1748
+ ]
1749
+ }) : null] });
1750
+ }
1751
+ });
1752
+ }
1753
+ //#endregion
1754
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/bash.web.tsx
1755
+ var bash_web_default = defineToolRenderer({ message: BashToolMessage });
1756
+ //#endregion
1757
+ //#region generated/web.ts
1758
+ const via = (identity, value) => value === void 0 ? void 0 : {
1759
+ ...identity,
1760
+ ...value
1761
+ };
1762
+ const webPlugin = defineWebPlugin({
1763
+ id: "runner",
1764
+ session: {
1765
+ get activityGroups() {
1766
+ return [via({ name: "runners" }, runners_web_default)];
1767
+ },
1768
+ get channels() {
1769
+ return [via({ channel: "runner_runs" }, runner_runs_web_default)];
1770
+ },
1771
+ get fills() {
1772
+ return [via({
1773
+ slot: "activity.runners",
1774
+ id: "runners"
1775
+ }, Runners_activity_runners_web_default)];
1776
+ },
1777
+ get toolRenderers() {
1778
+ return [via({ tools: ["bash"] }, bash_web_default)];
1779
+ }
1780
+ }
1781
+ });
1782
+ //#endregion
1783
+ export { webPlugin };
1784
+
1785
+ //# sourceMappingURL=web.mjs.map