zidane 7.2.5 → 8.0.4

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 (376) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +34 -6
  3. package/dist/{acp-D-tVyUjw.js → acp-DQDhi2dj.js} +230 -77
  4. package/dist/acp-DQDhi2dj.js.map +1 -0
  5. package/dist/acp-cli.d.ts +1 -1
  6. package/dist/acp-cli.js +9 -66
  7. package/dist/acp-cli.js.map +1 -1
  8. package/dist/acp.d.ts +10 -590
  9. package/dist/acp.d.ts.map +1 -1
  10. package/dist/acp.js +3 -3
  11. package/dist/{agent-D0zZnGbt.js → agent-DSLNmBp0.js} +14401 -13214
  12. package/dist/agent-DSLNmBp0.js.map +1 -0
  13. package/dist/agent.d.ts +1 -1
  14. package/dist/agent.js +3 -2
  15. package/dist/aliasing-lVFbm81n.js +217 -0
  16. package/dist/aliasing-lVFbm81n.js.map +1 -0
  17. package/dist/{anthropic-C2aole0G.js → anthropic-CD00LhUP.js} +1442 -1264
  18. package/dist/anthropic-CD00LhUP.js.map +1 -0
  19. package/dist/artifacts-CV2hUKnL.js.map +1 -1
  20. package/dist/{atomic-write-B7zr1ddF.js → atomic-write-Ijgf2Mhc.js} +13 -3
  21. package/dist/atomic-write-Ijgf2Mhc.js.map +1 -0
  22. package/dist/call-tool-dHlbGOIY.js +70 -0
  23. package/dist/call-tool-dHlbGOIY.js.map +1 -0
  24. package/dist/chat/pure.d.ts +372 -6
  25. package/dist/chat/pure.d.ts.map +1 -1
  26. package/dist/chat/pure.js +35 -244
  27. package/dist/chat/pure.js.map +1 -1
  28. package/dist/{approvals-DKaJZYlm.js → chat-JPJJ8uu5.js} +15879 -14631
  29. package/dist/chat-JPJJ8uu5.js.map +1 -0
  30. package/dist/chat.d.ts +4 -6
  31. package/dist/chat.js +23 -12
  32. package/dist/cli-shared-C-YCaglL.js +159 -0
  33. package/dist/cli-shared-C-YCaglL.js.map +1 -0
  34. package/dist/client-core-BQSyDALT.d.ts +2115 -0
  35. package/dist/client-core-BQSyDALT.d.ts.map +1 -0
  36. package/dist/codemode-worker.d.ts +1 -0
  37. package/dist/codemode-worker.js +155 -0
  38. package/dist/codemode-worker.js.map +1 -0
  39. package/dist/codemode.d.ts +352 -0
  40. package/dist/codemode.d.ts.map +1 -0
  41. package/dist/codemode.js +5 -0
  42. package/dist/connection-Dr-Js4yZ.d.ts +28 -0
  43. package/dist/connection-Dr-Js4yZ.d.ts.map +1 -0
  44. package/dist/connection-loop-gdh22LTI.js +112 -0
  45. package/dist/connection-loop-gdh22LTI.js.map +1 -0
  46. package/dist/context-segments-BHKlagL6.js +1016 -0
  47. package/dist/context-segments-BHKlagL6.js.map +1 -0
  48. package/dist/{context-transport-T_6AYIvy.js → context-transport-DSMTUsK1.js} +3 -3
  49. package/dist/{context-transport-T_6AYIvy.js.map → context-transport-DSMTUsK1.js.map} +1 -1
  50. package/dist/contexts/daytona.d.ts +19 -8
  51. package/dist/contexts/daytona.d.ts.map +1 -1
  52. package/dist/contexts/daytona.js +1 -440
  53. package/dist/contexts/docker.d.ts +36 -7
  54. package/dist/contexts/docker.d.ts.map +1 -1
  55. package/dist/contexts/docker.js +1 -456
  56. package/dist/contexts/e2b.d.ts +68 -13
  57. package/dist/contexts/e2b.d.ts.map +1 -1
  58. package/dist/contexts/e2b.js +1 -382
  59. package/dist/contexts/sandbox.d.ts +1 -1
  60. package/dist/contexts/sandbox.js +5 -0
  61. package/dist/contexts/sandbox.js.map +1 -1
  62. package/dist/{contexts-CRE9pw07.js → contexts-d7UolYot.js} +1059 -524
  63. package/dist/contexts-d7UolYot.js.map +1 -0
  64. package/dist/contexts.d.ts +3 -3
  65. package/dist/contexts.js +1 -1
  66. package/dist/cred-keys-GpBg71cP.js +105 -0
  67. package/dist/cred-keys-GpBg71cP.js.map +1 -0
  68. package/dist/{auth-BG2rVKSt.js → credentials-JttEuRON.js} +55 -206
  69. package/dist/credentials-JttEuRON.js.map +1 -0
  70. package/dist/{cursor-iojRXHF9.js → cursor-CdjaQTBW.js} +3 -3
  71. package/dist/{cursor-iojRXHF9.js.map → cursor-CdjaQTBW.js.map} +1 -1
  72. package/dist/daemon-cli.d.ts +1 -1
  73. package/dist/daemon-cli.js +3 -2
  74. package/dist/daemon-cli.js.map +1 -1
  75. package/dist/daytona-DG3omipZ.js +513 -0
  76. package/dist/daytona-DG3omipZ.js.map +1 -0
  77. package/dist/defaults-Cx3zvDOV.js +23 -0
  78. package/dist/defaults-Cx3zvDOV.js.map +1 -0
  79. package/dist/dispatcher-BMlu2BmZ.js +4941 -0
  80. package/dist/dispatcher-BMlu2BmZ.js.map +1 -0
  81. package/dist/docker-BZhMMJUp.js +727 -0
  82. package/dist/docker-BZhMMJUp.js.map +1 -0
  83. package/dist/e2b-CEVOdRib.js +480 -0
  84. package/dist/e2b-CEVOdRib.js.map +1 -0
  85. package/dist/edit-utils-EGosADZq.js +234 -0
  86. package/dist/edit-utils-EGosADZq.js.map +1 -0
  87. package/dist/environment-8wsKE2Ln.js +214 -0
  88. package/dist/environment-8wsKE2Ln.js.map +1 -0
  89. package/dist/eval-BNcvle_J.js +1595 -0
  90. package/dist/eval-BNcvle_J.js.map +1 -0
  91. package/dist/eval.d.ts +2 -2
  92. package/dist/eval.js +1 -1489
  93. package/dist/executor-Cx5-8yqi.js +621 -0
  94. package/dist/executor-Cx5-8yqi.js.map +1 -0
  95. package/dist/extension-authoring-DyGGmq9V.js +385 -0
  96. package/dist/extension-authoring-DyGGmq9V.js.map +1 -0
  97. package/dist/extensions.d.ts +2 -2
  98. package/dist/extensions.js +3 -3
  99. package/dist/{fetch-url-DX75gpL0.js → fetch-url-Ct2TjDWt.js} +4 -4
  100. package/dist/{fetch-url-DX75gpL0.js.map → fetch-url-Ct2TjDWt.js.map} +1 -1
  101. package/dist/{format-BF745pLL.js → format-C2NQEVUH.js} +39 -2
  102. package/dist/format-C2NQEVUH.js.map +1 -0
  103. package/dist/format-bytes-C7tfc3Zm.js +55 -0
  104. package/dist/format-bytes-C7tfc3Zm.js.map +1 -0
  105. package/dist/headless-C_rNUuYC.js +960 -0
  106. package/dist/headless-C_rNUuYC.js.map +1 -0
  107. package/dist/headless.d.ts +1 -1
  108. package/dist/headless.js +1 -413
  109. package/dist/{history-search-C4bk1Wz_.js → history-search-CcYzF8-0.js} +7 -3
  110. package/dist/history-search-CcYzF8-0.js.map +1 -0
  111. package/dist/history-search-worker.d.ts +1 -1
  112. package/dist/history-search-worker.js +1 -1
  113. package/dist/host-D7NrI03V.js +378 -0
  114. package/dist/host-D7NrI03V.js.map +1 -0
  115. package/dist/host-signals-BcP5QKUD.js +118 -0
  116. package/dist/host-signals-BcP5QKUD.js.map +1 -0
  117. package/dist/identity-CwfO8K-t.js +26 -0
  118. package/dist/identity-CwfO8K-t.js.map +1 -0
  119. package/dist/{index-BgN1T3lk.d.ts → index-BGe1fQT7.d.ts} +10987 -9320
  120. package/dist/index-BGe1fQT7.d.ts.map +1 -0
  121. package/dist/index-BVQsIKGn.d.ts +448 -0
  122. package/dist/index-BVQsIKGn.d.ts.map +1 -0
  123. package/dist/{index-C6NEd5y_.d.ts → index-CHABJd-L.d.ts} +3386 -502
  124. package/dist/index-CHABJd-L.d.ts.map +1 -0
  125. package/dist/{index-C37_ppWv.d.ts → index-D6a7qXpg.d.ts} +2 -3
  126. package/dist/index-D6a7qXpg.d.ts.map +1 -0
  127. package/dist/index.d.ts +6 -6
  128. package/dist/index.js +27 -23
  129. package/dist/{completion-core-BRo86R_L.js → inline-ghost-BVRQeGoL.js} +50 -2
  130. package/dist/inline-ghost-BVRQeGoL.js.map +1 -0
  131. package/dist/ipc/remote.d.ts +313 -0
  132. package/dist/ipc/remote.d.ts.map +1 -0
  133. package/dist/ipc/remote.js +4 -0
  134. package/dist/ipc.d.ts +6 -964
  135. package/dist/ipc.js +403 -4
  136. package/dist/ipc.js.map +1 -0
  137. package/dist/{isolation-Kfoof0MM.d.ts → isolation-D9L650W5.d.ts} +100 -15
  138. package/dist/isolation-D9L650W5.d.ts.map +1 -0
  139. package/dist/json-rpc--HXQti1E.js +350 -0
  140. package/dist/json-rpc--HXQti1E.js.map +1 -0
  141. package/dist/links-j8rED1sR.js +603 -0
  142. package/dist/links-j8rED1sR.js.map +1 -0
  143. package/dist/local-terminal-D8QThwo_.js +252 -0
  144. package/dist/local-terminal-D8QThwo_.js.map +1 -0
  145. package/dist/{logger--C1VQjwJ.js → logger-Gtqlayx8.js} +199 -173
  146. package/dist/logger-Gtqlayx8.js.map +1 -0
  147. package/dist/logs-view-CWG8aUAG.js.map +1 -1
  148. package/dist/{mcp-CgNd-f3F.js → mcp-_wecH9Qe.js} +608 -562
  149. package/dist/mcp-_wecH9Qe.js.map +1 -0
  150. package/dist/mcp.d.ts +2 -2
  151. package/dist/mcp.js +2 -2
  152. package/dist/media-sniff-UKijEaiQ.js.map +1 -1
  153. package/dist/{messages-2jcOA_5R.js → messages-BqiwcD2Q.js} +1017 -1014
  154. package/dist/messages-BqiwcD2Q.js.map +1 -0
  155. package/dist/{oauth-DxHO17JE.js → oauth-Px_pBB9W.js} +5 -5
  156. package/dist/{oauth-DxHO17JE.js.map → oauth-Px_pBB9W.js.map} +1 -1
  157. package/dist/{openai-compat-Dy56weK8.js → openai-compat-CFrpH7uq.js} +432 -589
  158. package/dist/openai-compat-CFrpH7uq.js.map +1 -0
  159. package/dist/orchestration-BCGmB8Ps.js +511 -0
  160. package/dist/orchestration-BCGmB8Ps.js.map +1 -0
  161. package/dist/output/stream-json.d.ts +1 -2
  162. package/dist/output/stream-json.d.ts.map +1 -1
  163. package/dist/output/stream-json.js +1 -1
  164. package/dist/output/terminal.d.ts +1 -2
  165. package/dist/output/terminal.d.ts.map +1 -1
  166. package/dist/output/terminal.js +13 -7
  167. package/dist/output/terminal.js.map +1 -1
  168. package/dist/{plugins-gbI7SBA6.js → plugins-DS7mahzZ.js} +20 -101
  169. package/dist/plugins-DS7mahzZ.js.map +1 -0
  170. package/dist/plugins.d.ts +1 -1
  171. package/dist/plugins.js +1 -2
  172. package/dist/prelude-Cxo4t74B.js +517 -0
  173. package/dist/prelude-Cxo4t74B.js.map +1 -0
  174. package/dist/presets.d.ts +1 -1
  175. package/dist/presets.js +1 -2
  176. package/dist/project-root-B8zADr4o.js +167 -0
  177. package/dist/project-root-B8zADr4o.js.map +1 -0
  178. package/dist/project-user-paths-BSgtrRxl.js +58 -0
  179. package/dist/project-user-paths-BSgtrRxl.js.map +1 -0
  180. package/dist/{prompts-Pqc3JdW_.d.ts → prompts-DYcvqShf.d.ts} +14 -11
  181. package/dist/prompts-DYcvqShf.d.ts.map +1 -0
  182. package/dist/protocol-DnhqqSmE.js +13 -0
  183. package/dist/protocol-DnhqqSmE.js.map +1 -0
  184. package/dist/providers/anthropic.d.ts +2 -2
  185. package/dist/providers/anthropic.js +1 -2
  186. package/dist/providers/arcee.d.ts +1 -1
  187. package/dist/providers/arcee.js +1 -1
  188. package/dist/providers/baseten.d.ts +1 -1
  189. package/dist/providers/baseten.js +1 -1
  190. package/dist/providers/cerebras.d.ts +1 -1
  191. package/dist/providers/cerebras.js +3 -2
  192. package/dist/providers/cerebras.js.map +1 -1
  193. package/dist/providers/local.d.ts +1 -1
  194. package/dist/providers/local.js +1 -1
  195. package/dist/providers/openai-compat.d.ts +1 -1
  196. package/dist/providers/openai-compat.js +1 -2
  197. package/dist/providers/openai.d.ts +1 -1
  198. package/dist/providers/openai.js +15 -12
  199. package/dist/providers/openai.js.map +1 -1
  200. package/dist/providers/openrouter.d.ts +1 -1
  201. package/dist/providers/openrouter.js +3 -2
  202. package/dist/providers/openrouter.js.map +1 -1
  203. package/dist/providers/xai.d.ts +1 -1
  204. package/dist/providers/xai.js +3 -3
  205. package/dist/{providers-BVhnFyiY.js → providers-XUR7F4Hi.js} +4 -4
  206. package/dist/{providers-BVhnFyiY.js.map → providers-XUR7F4Hi.js.map} +1 -1
  207. package/dist/providers.d.ts +1 -1
  208. package/dist/providers.js +4 -4
  209. package/dist/registry-D6ShBWJU.js +5234 -0
  210. package/dist/registry-D6ShBWJU.js.map +1 -0
  211. package/dist/remote-hosts-BtA_9ZGp.js +97 -0
  212. package/dist/remote-hosts-BtA_9ZGp.js.map +1 -0
  213. package/dist/restate.d.ts +2 -3
  214. package/dist/restate.d.ts.map +1 -1
  215. package/dist/restate.js +631 -572
  216. package/dist/restate.js.map +1 -1
  217. package/dist/run-summary-CklLiZ4y.js +288 -0
  218. package/dist/run-summary-CklLiZ4y.js.map +1 -0
  219. package/dist/{sandbox-BH0WQkXn.d.ts → sandbox-C5dR5DwQ.d.ts} +6 -3
  220. package/dist/sandbox-C5dR5DwQ.d.ts.map +1 -0
  221. package/dist/session/sqlite.d.ts +1 -2
  222. package/dist/session/sqlite.d.ts.map +1 -1
  223. package/dist/session/sqlite.js +1 -616
  224. package/dist/session-factory-BUGY5P40.js +1576 -0
  225. package/dist/session-factory-BUGY5P40.js.map +1 -0
  226. package/dist/{session-BqRP1hS7.js → session-xh9JXu6i.js} +94 -52
  227. package/dist/session-xh9JXu6i.js.map +1 -0
  228. package/dist/session.d.ts +1 -1
  229. package/dist/session.js +2 -2
  230. package/dist/settings-keys-Ckf29NeM.js +677 -0
  231. package/dist/settings-keys-Ckf29NeM.js.map +1 -0
  232. package/dist/shell-path-Dqaoow2J.js +412 -0
  233. package/dist/shell-path-Dqaoow2J.js.map +1 -0
  234. package/dist/{skills-BC3dodWp.js → skills-DKqZJm4X.js} +430 -470
  235. package/dist/skills-DKqZJm4X.js.map +1 -0
  236. package/dist/skills.d.ts +2 -2
  237. package/dist/skills.js +1 -2
  238. package/dist/source-mPExaUm8.js +381 -0
  239. package/dist/source-mPExaUm8.js.map +1 -0
  240. package/dist/sqlite-DV1DQzN2.js +698 -0
  241. package/dist/sqlite-DV1DQzN2.js.map +1 -0
  242. package/dist/{src-CkNQKFKr.js → src-BRMYRJib.js} +903 -1121
  243. package/dist/src-BRMYRJib.js.map +1 -0
  244. package/dist/{stdio-DPpBDPx5.js → stdio-C6YyoW3I.js} +4 -4
  245. package/dist/{stdio-DPpBDPx5.js.map → stdio-C6YyoW3I.js.map} +1 -1
  246. package/dist/{stdio-loader-Bbx19dMZ.js → stdio-loader-C830Vg-Z.js} +4 -4
  247. package/dist/{stdio-loader-Bbx19dMZ.js.map → stdio-loader-C830Vg-Z.js.map} +1 -1
  248. package/dist/system-prompt-MNVHatSD.d.ts.map +1 -1
  249. package/dist/timeout-DEaqslLO.d.ts.map +1 -1
  250. package/dist/todo-edits-Dn251YUP.js +1790 -0
  251. package/dist/todo-edits-Dn251YUP.js.map +1 -0
  252. package/dist/{turn-operations-DrbJlpq9.js → tool-formatters-IQFDrdrs.js} +1456 -2120
  253. package/dist/tool-formatters-IQFDrdrs.js.map +1 -0
  254. package/dist/tool-state-Dku6YqAB.js +989 -0
  255. package/dist/tool-state-Dku6YqAB.js.map +1 -0
  256. package/dist/tools/fetch-url.d.ts +1 -2
  257. package/dist/tools/fetch-url.d.ts.map +1 -1
  258. package/dist/tools/fetch-url.js +1 -2
  259. package/dist/tools/prompts.d.ts +1 -1
  260. package/dist/tools/prompts.js +7 -7
  261. package/dist/tools/prompts.js.map +1 -1
  262. package/dist/tools/web-search.d.ts +1 -2
  263. package/dist/tools/web-search.d.ts.map +1 -1
  264. package/dist/tools/web-search.js +2 -2
  265. package/dist/tools/web-search.js.map +1 -1
  266. package/dist/tools-_DnT3AGM.js +578 -0
  267. package/dist/tools-_DnT3AGM.js.map +1 -0
  268. package/dist/tools.d.ts +3 -3
  269. package/dist/tools.js +4 -2
  270. package/dist/tui.d.ts +534 -517
  271. package/dist/tui.d.ts.map +1 -1
  272. package/dist/tui.js +34933 -27243
  273. package/dist/tui.js.map +1 -1
  274. package/dist/turn-operations-B8sokwi5.js +333 -0
  275. package/dist/turn-operations-B8sokwi5.js.map +1 -0
  276. package/dist/turn-operations-DQX5ZZni.d.ts +3089 -0
  277. package/dist/turn-operations-DQX5ZZni.d.ts.map +1 -0
  278. package/dist/types-C7ZFOztE.d.ts +132 -0
  279. package/dist/types-C7ZFOztE.d.ts.map +1 -0
  280. package/dist/types-CH5XpIHw.js +114 -0
  281. package/dist/types-CH5XpIHw.js.map +1 -0
  282. package/dist/{types-CWgENVLY.js → types-Cu0ZGDu9.js} +10 -6
  283. package/dist/types-Cu0ZGDu9.js.map +1 -0
  284. package/dist/types-IqegbJEE.d.ts +624 -0
  285. package/dist/types-IqegbJEE.d.ts.map +1 -0
  286. package/dist/types.d.ts +4 -4
  287. package/dist/types.js +1 -1
  288. package/dist/url-D8HCl65b.js +1259 -0
  289. package/dist/url-D8HCl65b.js.map +1 -0
  290. package/dist/wasm-BVT-j5cl.js +33 -0
  291. package/dist/wasm-BVT-j5cl.js.map +1 -0
  292. package/dist/workspace-exec-stream-SV2R9d5S.js +355 -0
  293. package/dist/workspace-exec-stream-SV2R9d5S.js.map +1 -0
  294. package/dist/ws-CIHMvlSh.js +952 -0
  295. package/dist/ws-CIHMvlSh.js.map +1 -0
  296. package/dist/ws-client-N7NxzvQp.js +230 -0
  297. package/dist/ws-client-N7NxzvQp.js.map +1 -0
  298. package/dist/{xai-CazjStOK.js → xai-CvJTeLrI.js} +165 -5
  299. package/dist/xai-CvJTeLrI.js.map +1 -0
  300. package/dist/xdg-zweed0-t.js +117 -0
  301. package/dist/xdg-zweed0-t.js.map +1 -0
  302. package/docs/ACP.md +52 -0
  303. package/docs/ARCHITECTURE.md +103 -6
  304. package/docs/ARTIFACTS.md +8 -4
  305. package/docs/CHAT.md +165 -14
  306. package/docs/EXECUTION_CONTEXT.md +8 -0
  307. package/docs/EXTENSIONS.md +123 -2
  308. package/docs/HEADLESS.md +2 -0
  309. package/docs/IPC.md +775 -64
  310. package/docs/MACHINES.md +456 -0
  311. package/docs/PANELS.md +3 -0
  312. package/docs/README.md +4 -0
  313. package/docs/RESTATE.md +3 -2
  314. package/docs/RUN_IN_BACKGROUND.md +10 -2
  315. package/docs/SERVER.md +1085 -0
  316. package/docs/SKILL.md +6 -1
  317. package/docs/TERMINAL.md +75 -0
  318. package/docs/TUI.md +65 -14
  319. package/package.json +58 -26
  320. package/scripts/eval.ts +4 -0
  321. package/dist/acp-D-tVyUjw.js.map +0 -1
  322. package/dist/agent-D0zZnGbt.js.map +0 -1
  323. package/dist/anthropic-C2aole0G.js.map +0 -1
  324. package/dist/approvals-DKaJZYlm.js.map +0 -1
  325. package/dist/atomic-write-B7zr1ddF.js.map +0 -1
  326. package/dist/auth-BG2rVKSt.js.map +0 -1
  327. package/dist/completion-core-BRo86R_L.js.map +0 -1
  328. package/dist/contexts/daytona.js.map +0 -1
  329. package/dist/contexts/docker.js.map +0 -1
  330. package/dist/contexts/e2b.js.map +0 -1
  331. package/dist/contexts-CRE9pw07.js.map +0 -1
  332. package/dist/eval.js.map +0 -1
  333. package/dist/format-BF745pLL.js.map +0 -1
  334. package/dist/glob-DluQFSw1.js +0 -270
  335. package/dist/glob-DluQFSw1.js.map +0 -1
  336. package/dist/headless.js.map +0 -1
  337. package/dist/history-search-C4bk1Wz_.js.map +0 -1
  338. package/dist/index-BgN1T3lk.d.ts.map +0 -1
  339. package/dist/index-C37_ppWv.d.ts.map +0 -1
  340. package/dist/index-C6NEd5y_.d.ts.map +0 -1
  341. package/dist/ipc-BKVIIOqH.js +0 -331
  342. package/dist/ipc-BKVIIOqH.js.map +0 -1
  343. package/dist/ipc.d.ts.map +0 -1
  344. package/dist/isolation-Kfoof0MM.d.ts.map +0 -1
  345. package/dist/json-rpc-rSWMUh9b.js +0 -192
  346. package/dist/json-rpc-rSWMUh9b.js.map +0 -1
  347. package/dist/logger--C1VQjwJ.js.map +0 -1
  348. package/dist/mcp-CgNd-f3F.js.map +0 -1
  349. package/dist/messages-2jcOA_5R.js.map +0 -1
  350. package/dist/openai-compat-Dy56weK8.js.map +0 -1
  351. package/dist/output-cap-C9zukcCj.js +0 -100
  352. package/dist/output-cap-C9zukcCj.js.map +0 -1
  353. package/dist/panels-B6gRk4bf.js +0 -789
  354. package/dist/panels-B6gRk4bf.js.map +0 -1
  355. package/dist/plugins-gbI7SBA6.js.map +0 -1
  356. package/dist/prompts-Pqc3JdW_.d.ts.map +0 -1
  357. package/dist/run-summary-BU_NgwUT.js +0 -269
  358. package/dist/run-summary-BU_NgwUT.js.map +0 -1
  359. package/dist/sandbox-BH0WQkXn.d.ts.map +0 -1
  360. package/dist/session/sqlite.js.map +0 -1
  361. package/dist/session-BqRP1hS7.js.map +0 -1
  362. package/dist/shell-path-DfdXrGg9.js +0 -1549
  363. package/dist/shell-path-DfdXrGg9.js.map +0 -1
  364. package/dist/skills-BC3dodWp.js.map +0 -1
  365. package/dist/src-CkNQKFKr.js.map +0 -1
  366. package/dist/todo-edits-UOwgxGKu.js +0 -528
  367. package/dist/todo-edits-UOwgxGKu.js.map +0 -1
  368. package/dist/tool-formatters-DoRLnfuD.d.ts +0 -759
  369. package/dist/tool-formatters-DoRLnfuD.d.ts.map +0 -1
  370. package/dist/transcript-anchors-BeudMbXx.d.ts +0 -1762
  371. package/dist/transcript-anchors-BeudMbXx.d.ts.map +0 -1
  372. package/dist/turn-operations-C8bMBDl_.d.ts +0 -1258
  373. package/dist/turn-operations-C8bMBDl_.d.ts.map +0 -1
  374. package/dist/turn-operations-DrbJlpq9.js.map +0 -1
  375. package/dist/types-CWgENVLY.js.map +0 -1
  376. package/dist/xai-CazjStOK.js.map +0 -1
@@ -1,1555 +1,67 @@
1
- import { _ as resolveOldString, y as styleReplacementForVia } from "./panels-B6gRk4bf.js";
1
+ import { i as readFileInputPaths, t as formatBytes } from "./format-bytes-C7tfc3Zm.js";
2
+ import { a as fmtTokens, c as formatElapsed, d as previewLine, f as shortId, s as formatDuration, t as ageString } from "./format-C2NQEVUH.js";
2
3
  import { r as utf8ByteLength } from "./utils-ngQzYzZD.js";
3
- import { a as formatDuration, i as fmtTokens, l as shortId, t as ageString } from "./format-BF745pLL.js";
4
- import { t as escapeXml } from "./xml-D5HBhJle.js";
5
4
  import { t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
6
- import { a as searchHighlightRanges } from "./history-search-C4bk1Wz_.js";
5
+ import { _ as shortSessionId } from "./orchestration-BCGmB8Ps.js";
6
+ import { a as searchHighlightRanges } from "./history-search-CcYzF8-0.js";
7
7
  import { Fzf } from "fzf";
8
- //#region src/chat/completion-chats.ts
9
- const CHATS_TRIGGER = "#";
10
- const DEFAULT_RESULT_LIMIT$1 = 50;
11
- const MIN_TOKEN_LENGTH = 6;
12
- function normalizeId(id) {
13
- return id.replace(/-/g, "").toLowerCase();
14
- }
15
- function projectLabel(projectRoot) {
16
- if (!projectRoot) return "";
17
- const parts = projectRoot.split(/[\\/]/).filter(Boolean);
18
- return parts[parts.length - 1] ?? projectRoot;
19
- }
8
+ //#region src/chat/todo-glyphs.ts
9
+ const TODO_STATUS_GLYPHS = {
10
+ pending: "☐",
11
+ in_progress: "◐",
12
+ completed: "☑",
13
+ cancelled: "☒"
14
+ };
15
+ //#endregion
16
+ //#region src/chat/agent-accent.ts
20
17
  /**
21
- * Per-catalog memo for `tokenFor`, keyed on catalog reference identity.
22
- * Token derivation is O(catalog) per id, and both `suggest()` (every
23
- * keystroke) and `parseReferences()` (every submit/highlight pass) need
24
- * a token for EVERY entry — quadratic over catalogs that can reach
25
- * thousands of sessions. The catalog getters return a stable array until
26
- * a refresh swaps it, so a WeakMap invalidates exactly when it should.
18
+ * Resolve a profile's `accent` token to a concrete hex color via the
19
+ * caller's color palette. Renderer-agnostic — accepts the four-role
20
+ * subset of `ThemeColors` that's relevant to accent surfaces, so this
21
+ * helper composes against either `useColors()` (TUI) or a CSS-variable
22
+ * lookup (GUI). Omitted / unknown tokens fall back to `accent`.
27
23
  */
28
- const tokensByCatalog = /* @__PURE__ */ new WeakMap();
29
- function catalogTokens(catalog) {
30
- let cached = tokensByCatalog.get(catalog);
31
- if (!cached) {
32
- cached = {
33
- normalizedIds: catalog.map((entry) => normalizeId(entry.id)),
34
- tokens: /* @__PURE__ */ new Map()
35
- };
36
- tokensByCatalog.set(catalog, cached);
37
- }
38
- return cached;
39
- }
40
- function tokenFor(id, catalog) {
41
- const { normalizedIds, tokens } = catalogTokens(catalog);
42
- const known = tokens.get(id);
43
- if (known !== void 0) return known;
44
- const normalized = normalizeId(id);
45
- let result;
46
- for (let len = Math.min(MIN_TOKEN_LENGTH, normalized.length); len <= normalized.length; len++) {
47
- const token = normalized.slice(0, len);
48
- if (normalizedIds.filter((candidate) => candidate.startsWith(token)).length === 1) {
49
- result = token;
50
- break;
51
- }
52
- }
53
- result ??= normalized || shortId(id);
54
- tokens.set(id, result);
55
- return result;
56
- }
57
- function stripTrailingPunctuation(candidate) {
58
- return candidate.replace(/[.,;:)\]}!?]+$/, "");
59
- }
60
- function descriptionFor(entry, token, scope) {
61
- const bits = [
62
- `#${token}`,
63
- ageString(entry.updatedAt),
64
- `${entry.userMessageCount} msg${entry.userMessageCount === 1 ? "" : "s"}`
65
- ];
66
- if ((entry.artifactCount ?? 0) > 0) bits.push(`${entry.artifactCount} artifact${entry.artifactCount === 1 ? "" : "s"}`);
67
- const project = projectLabel(entry.projectRoot);
68
- if (scope === "all" && project) bits.push(project);
69
- return bits.join(" · ");
70
- }
71
- function scoreChats(catalog, tokenCatalog, query, limit, scope) {
72
- const q = query.trim().toLowerCase();
73
- const rows = catalog.map((entry) => ({
74
- entry,
75
- token: tokenFor(entry.id, tokenCatalog),
76
- project: projectLabel(entry.projectRoot)
77
- }));
78
- const fuzzyHits = q.length > 0 ? new Set(new Fzf(rows, {
79
- selector: (r) => `${r.entry.title} ${r.token} ${r.project}`,
80
- casing: "case-insensitive"
81
- }).find(q).map((r) => r.item)) : null;
82
- const scored = [];
83
- for (const row of rows) {
84
- const title = row.entry.title.toLowerCase();
85
- const token = row.token.toLowerCase();
86
- const project = row.project.toLowerCase();
87
- if (q.length === 0) {
88
- scored.push({
89
- entry: row.entry,
90
- token: row.token,
91
- rank: 5
92
- });
93
- continue;
94
- }
95
- if (token.startsWith(q)) {
96
- scored.push({
97
- entry: row.entry,
98
- token: row.token,
99
- rank: 0
100
- });
101
- continue;
102
- }
103
- if (title.startsWith(q)) {
104
- scored.push({
105
- entry: row.entry,
106
- token: row.token,
107
- rank: 1
108
- });
109
- continue;
110
- }
111
- if (title.includes(q)) {
112
- scored.push({
113
- entry: row.entry,
114
- token: row.token,
115
- rank: 2
116
- });
117
- continue;
118
- }
119
- if (project.includes(q)) {
120
- scored.push({
121
- entry: row.entry,
122
- token: row.token,
123
- rank: 3
124
- });
125
- continue;
126
- }
127
- if (fuzzyHits?.has(row)) scored.push({
128
- entry: row.entry,
129
- token: row.token,
130
- rank: 4
131
- });
132
- }
133
- scored.sort((a, b) => {
134
- if (a.rank !== b.rank) return a.rank - b.rank;
135
- return b.entry.updatedAt - a.entry.updatedAt;
136
- });
137
- return scored.slice(0, limit).map(({ entry, token }) => ({
138
- id: token,
139
- label: entry.title,
140
- description: descriptionFor(entry, token, scope),
141
- insertText: `#${token} `,
142
- data: {
143
- entry,
144
- token
145
- }
146
- }));
147
- }
148
- function createChatsCompletionProvider(opts) {
149
- const limit = opts.limit ?? DEFAULT_RESULT_LIMIT$1;
150
- const scope = () => opts.getScope?.() ?? "project";
151
- return {
152
- id: "chats",
153
- trigger: "#",
154
- label: "Chats",
155
- suggest(query) {
156
- const current = opts.getCatalog();
157
- const tokenCatalog = opts.getReferenceCatalog?.() ?? current;
158
- const effectiveTokenCatalog = tokenCatalog.length > 0 ? tokenCatalog : current;
159
- if (current.length > 0) return scoreChats(current, effectiveTokenCatalog, query, limit, scope());
160
- if (opts.ensureCatalog) return opts.ensureCatalog().then((loaded) => {
161
- const refreshed = opts.getReferenceCatalog?.() ?? loaded;
162
- return scoreChats(loaded, refreshed.length > 0 ? refreshed : loaded, query, limit, scope());
163
- });
164
- return [];
165
- },
166
- parseReferences(text, _ctx) {
167
- const catalog = opts.getReferenceCatalog?.() ?? opts.getCatalog();
168
- if (catalog.length === 0) return [];
169
- const byToken = /* @__PURE__ */ new Map();
170
- for (const entry of catalog) {
171
- const token = tokenFor(entry.id, catalog);
172
- byToken.set(token, {
173
- entry,
174
- token
175
- });
176
- }
177
- const refs = [];
178
- for (const m of text.matchAll(/(^|\s)#(\S+)/g)) {
179
- const candidate = stripTrailingPunctuation(m[2]);
180
- const data = byToken.get(candidate.toLowerCase());
181
- if (!data) continue;
182
- const start = m.index + m[1].length;
183
- refs.push({
184
- providerId: "chats",
185
- start,
186
- end: start + 1 + candidate.length,
187
- itemId: data.entry.id,
188
- data
189
- });
190
- }
191
- return refs;
192
- }
193
- };
194
- }
195
- function uniqueChatIdsFromReferences(references) {
196
- const out = [];
197
- const seen = /* @__PURE__ */ new Set();
198
- for (const ref of references) {
199
- if (ref.providerId !== "chats") continue;
200
- if (seen.has(ref.itemId)) continue;
201
- seen.add(ref.itemId);
202
- out.push(ref.itemId);
203
- }
204
- return out;
205
- }
206
- function chatReferenceTokensFromText(text) {
207
- const out = [];
208
- const seen = /* @__PURE__ */ new Set();
209
- for (const m of text.matchAll(/(^|\s)#(\S+)/g)) {
210
- const token = stripTrailingPunctuation(m[2]).toLowerCase();
211
- if (!token || seen.has(token)) continue;
212
- seen.add(token);
213
- out.push(token);
24
+ function accentColor(accent, COLOR) {
25
+ switch (accent) {
26
+ case "brand": return COLOR.brand;
27
+ case "warn": return COLOR.warn;
28
+ case "model": return COLOR.model;
29
+ default: return COLOR.accent;
214
30
  }
215
- return out;
216
31
  }
217
32
  //#endregion
218
- //#region src/chat/edit-approval.ts
33
+ //#region src/chat/turn-selection.ts
34
+ /** Tools whose `tool-result` event is suppressed when `showEditDiffs` is on. */
35
+ const EDIT_TOOL_NAMES = /* @__PURE__ */ new Set([
36
+ "edit",
37
+ "multi_edit",
38
+ "write_file"
39
+ ]);
219
40
  /**
220
- * Convert a per-hunk approval mask into an `EditOutcome[]`. `true` →
221
- * `applied`; `false` → `denied` with the supplied reason.
222
- *
223
- * Length is `Math.max(mask.length, fallbackLength)` so callers passing a
224
- * shorter mask still get a fully-populated array — missing entries
225
- * default to applied, matching the "no decision => keep" convention.
41
+ * Keep turn-selection mode alive after deleting its active row. Prefer the
42
+ * row that slides into the deleted index; at the tail, select the new last
43
+ * row. Returns null only when no selectable rows survive.
226
44
  */
227
- function maskToOutcomeKinds(mask, fallbackLength, deniedReason = "denied by user") {
228
- const len = Math.max(mask.length, fallbackLength);
229
- const out = [];
230
- for (let i = 0; i < len; i++) {
231
- const keep = i < mask.length ? mask[i] : true;
232
- out.push(keep ? { kind: "applied" } : {
233
- kind: "denied",
234
- reason: deniedReason
235
- });
236
- }
237
- return out;
238
- }
239
- function resolveApprovalForPayload(decision, payload) {
240
- const total = payload.hunks.length;
241
- if (decision === "deny") {
242
- const outcomes = Array.from({ length: total }, () => ({
243
- kind: "denied",
244
- reason: "denied by user"
245
- }));
246
- return {
247
- outcomes,
248
- shouldBlock: true,
249
- syntheticEvent: {
250
- ...payload,
251
- outcomes
252
- }
253
- };
254
- }
255
- if (typeof decision === "object" && decision.kind === "partial") {
256
- const outcomes = maskToOutcomeKinds(decision.mask, total);
257
- return {
258
- outcomes,
259
- shouldBlock: !outcomes.some((o) => o.kind === "applied"),
260
- syntheticEvent: {
261
- ...payload,
262
- outcomes
263
- }
264
- };
265
- }
266
- return {
267
- outcomes: Array.from({ length: total }, () => ({ kind: "applied" })),
268
- shouldBlock: false,
269
- syntheticEvent: null
270
- };
271
- }
272
- /** Sentinel tags used by {@link buildEditOutcomesAnnotation} / parser. */
273
- const ANNOTATION_OPEN = "<edit-outcomes>";
274
- const ANNOTATION_CLOSE = "</edit-outcomes>";
275
- const OUTCOME_KIND_RE = /^applied|denied|skipped|failed$/;
276
- const OUTCOME_LINE_RE = /^#(\d+) (applied|denied|skipped|failed)(?:: ?(.*))?$/;
277
- /**
278
- * Render an `EditOutcome[]` as the wire-format annotation block. Returns
279
- * the body to APPEND to a tool result; callers join with a leading
280
- * `\n\n` separator. Idempotent on missing reasons — bare `applied` lines
281
- * stay terse.
282
- */
283
- function buildEditOutcomesAnnotation(outcomes) {
284
- const lines = [ANNOTATION_OPEN];
285
- for (let i = 0; i < outcomes.length; i++) {
286
- const o = outcomes[i];
287
- if (!OUTCOME_KIND_RE.test(o.kind)) continue;
288
- const reason = o.reason ? `: ${o.reason}` : "";
289
- lines.push(`#${i + 1} ${o.kind}${reason}`);
290
- }
291
- lines.push(ANNOTATION_CLOSE);
292
- return lines.join("\n");
45
+ function selectionAfterTurnRemoval(before, removedId, after) {
46
+ if (after.includes(removedId)) return removedId;
47
+ if (after.length === 0) return null;
48
+ const index = before.indexOf(removedId);
49
+ return after[Math.min(Math.max(0, index), after.length - 1)] ?? null;
293
50
  }
294
51
  /**
295
- * Parse an `<edit-outcomes>…</edit-outcomes>` annotation block out of a
296
- * tool result body. Returns the outcomes keyed by 1-based hunk index, or
297
- * `null` when the block is missing / malformed.
298
- *
299
- * Anchored on the explicit tag pair so the parser doesn't false-positive
300
- * on natural prose that happens to contain `#1 applied`.
301
- */
302
- function parseEditOutcomesFromResult(result) {
303
- const text = typeof result === "string" ? result : result.filter((b) => b.type === "text").map((b) => b.text).join("\n");
304
- if (!text) return null;
305
- const openIdx = text.indexOf(`\n${ANNOTATION_OPEN}\n`);
306
- const startIdx = openIdx >= 0 ? openIdx + 1 : text.startsWith(`${ANNOTATION_OPEN}\n`) ? 0 : -1;
307
- if (startIdx < 0) return null;
308
- const closeNeedle = `\n${ANNOTATION_CLOSE}`;
309
- const closeIdx = text.indexOf(closeNeedle, startIdx);
310
- if (closeIdx < 0) return null;
311
- const body = text.slice(startIdx + 15 + 1, closeIdx);
312
- const found = [];
313
- for (const line of body.split("\n")) {
314
- if (line.length === 0) continue;
315
- const m = OUTCOME_LINE_RE.exec(line);
316
- if (!m) return null;
317
- const idx = Number.parseInt(m[1], 10);
318
- if (!Number.isFinite(idx) || idx < 1) return null;
319
- const kind = m[2];
320
- const reason = m[3]?.trim();
321
- found.push({
322
- idx,
323
- outcome: {
324
- kind,
325
- ...reason ? { reason } : {}
326
- }
327
- });
328
- }
329
- if (found.length === 0) return null;
330
- const maxIdx = Math.max(...found.map((f) => f.idx));
331
- const outcomes = Array.from({ length: maxIdx }, () => ({ kind: "applied" }));
332
- for (const { idx, outcome } of found) outcomes[idx - 1] = outcome;
333
- return outcomes;
334
- }
335
- /**
336
- * Strip the first `<edit-outcomes>…</edit-outcomes>` block out of a tool
337
- * result body, returning the surrounding text. Used by the
338
- * `tool:transform` hook to peel a body-emitted annotation before
339
- * re-appending the merged (approval ∪ body) version — otherwise the
340
- * result would carry two annotation blocks and
341
- * {@link parseEditOutcomesFromResult} would only see the first.
52
+ * Recognize a tool-result body as carrying NON-success information so the
53
+ * renderer doesn't suppress it under `showEditDiffs`. Three categories:
342
54
  *
343
- * Anchored on the same `\n<edit-outcomes>\n` / start-of-string newline
344
- * shape the parser uses, so prose that incidentally mentions
345
- * `<edit-outcomes>` (e.g. a model summarizing its own format) isn't
346
- * mistakenly stripped. Trims a single leading `\n\n` separator when
347
- * present so successive strips don't leave dangling blank lines.
348
- * Idempotent on inputs that don't contain a properly-anchored block.
349
- */
350
- function stripEditOutcomesAnnotation(text) {
351
- const newlineNeedle = `\n${ANNOTATION_OPEN}\n`;
352
- const newlineIdx = text.indexOf(newlineNeedle);
353
- let openIdx;
354
- if (newlineIdx >= 0) openIdx = newlineIdx + 1;
355
- else if (text.startsWith(`${ANNOTATION_OPEN}\n`)) openIdx = 0;
356
- else return text;
357
- const closeIdx = text.indexOf(ANNOTATION_CLOSE, openIdx);
358
- if (closeIdx < 0) return text;
359
- const blockEnd = closeIdx + 16;
360
- const sepStart = openIdx >= 2 && text.slice(openIdx - 2, openIdx) === "\n\n" ? openIdx - 2 : openIdx;
361
- return text.slice(0, sepStart) + text.slice(blockEnd);
362
- }
363
- /**
364
- * Merge body-side outcomes (keyed against the approved subset the tool
365
- * actually ran on, in subset-position order) into approval-side outcomes
366
- * (1:1 with the model's ORIGINAL `edits` list, with `denied` entries for
367
- * every hunk the user dropped).
368
- *
369
- * Algorithm: walk the approval array; every `applied` placeholder
370
- * corresponds to one approved hunk that the body ran. Consume body's
371
- * outcomes in order against those placeholders. Non-`applied` approval
372
- * entries (`denied`, `skipped`) stay untouched — they describe gate-
373
- * level decisions the body never saw.
374
- *
375
- * Pure. Returns a fresh array; never mutates either input.
376
- *
377
- * Edge cases:
378
- * - `body` is empty / shorter than the approved count → remaining
379
- * approval `applied` placeholders stay as `applied` (the body ran
380
- * happily; absence of a body entry means nothing failed).
381
- * - `body` longer than approved count → trailing body entries are
382
- * ignored. Shouldn't happen in practice (body sees the rebound
383
- * subset), but the guard keeps the merge total-pure.
384
- */
385
- function mergeApprovalAndBodyOutcomes(approval, body) {
386
- if (!body || body.length === 0) return approval.slice();
387
- const out = [];
388
- let bi = 0;
389
- for (const entry of approval) if (entry.kind === "applied" && bi < body.length) {
390
- out.push(body[bi]);
391
- bi++;
392
- } else out.push(entry);
393
- return out;
394
- }
395
- /**
396
- * Rewrite a `multi_edit` body header so the totals reflect the model's
397
- * ORIGINAL edit list (the merged outcomes count) instead of the subset
398
- * the body actually saw after gate rebinding. Without this, a partially
399
- * approved call surfaces a misleading `applied 2 of 2 edits` (subset
400
- * counts) on the wire even when the original was `applied 2 of 3`.
401
- *
402
- * Three body-side shapes are handled (matching `multi_edit`'s emit):
403
- * 1. `Edited <path>: applied N edits (R replacements).`
404
- * 2. `Edited <path>: applied N of M edits (R replacements).`
405
- * 3. `multi_edit error: no edits applied to <path> (M attempted).`
406
- *
407
- * The replacements count is preserved verbatim — it's a body-side stat
408
- * the chat layer can't recompute. When the first line doesn't look like
409
- * any of the three shapes (e.g. an unrelated error preamble bubbled up),
410
- * the text is returned unchanged.
411
- */
412
- function rewriteMultiEditHeader(text, merged, path) {
413
- const newlineIdx = text.indexOf("\n");
414
- const firstLine = newlineIdx < 0 ? text : text.slice(0, newlineIdx);
415
- const rest = newlineIdx < 0 ? "" : text.slice(newlineIdx);
416
- const successMatch = firstLine.match(/^Edited .+: applied \d+(?: of \d+)? edits? \((\d+) replacement/);
417
- const isFailedShape = firstLine.startsWith("multi_edit error: no edits applied to ") && firstLine.endsWith(" attempted).");
418
- if (!successMatch && !isFailedShape) return text;
419
- const replacements = successMatch ? Number.parseInt(successMatch[1], 10) || 0 : 0;
420
- const applied = summarizeOutcomes(merged).applied;
421
- const total = merged.length;
422
- let newHeader;
423
- if (applied === total) newHeader = `Edited ${path}: applied ${total} edit${total === 1 ? "" : "s"} (${replacements} replacement${replacements === 1 ? "" : "s"}).`;
424
- else if (applied > 0) newHeader = `Edited ${path}: applied ${applied} of ${total} edits (${replacements} replacement${replacements === 1 ? "" : "s"}).`;
425
- else newHeader = `multi_edit error: no edits applied to ${path} (${total} attempted).`;
426
- return newHeader + rest;
427
- }
428
- /**
429
- * Aggregate counts for the transcript's summary badge (`3 applied · 1
430
- * denied · 1 skipped`). Exported so renderers don't reimplement the
431
- * tally. Pure / O(n).
432
- */
433
- function summarizeOutcomes(outcomes) {
434
- const counts = {
435
- applied: 0,
436
- denied: 0,
437
- skipped: 0,
438
- failed: 0,
439
- pending: 0
440
- };
441
- if (!outcomes) return {
442
- ...counts,
443
- total: 0
444
- };
445
- for (const o of outcomes) counts[o.kind] += 1;
446
- return {
447
- ...counts,
448
- total: outcomes.length
449
- };
450
- }
451
- //#endregion
452
- //#region src/chat/edit-diff.ts
453
- function extractEditPayload(name, input, priorContent) {
454
- const path = input.path;
455
- if (typeof path !== "string" || path === "") return void 0;
456
- if (name === "edit") {
457
- const oldString = input.old_string;
458
- const newString = input.new_string;
459
- if (typeof oldString !== "string" || typeof newString !== "string") return void 0;
460
- return {
461
- tool: "edit",
462
- path,
463
- hunks: [{
464
- oldString,
465
- newString,
466
- ...input.replace_all === true ? { replaceAll: true } : {}
467
- }],
468
- ...priorContent !== void 0 ? { priorContent } : {}
469
- };
470
- }
471
- if (name === "multi_edit") {
472
- const steps = input.edits;
473
- if (!Array.isArray(steps) || steps.length === 0) return void 0;
474
- const hunks = [];
475
- for (const raw of steps) {
476
- if (typeof raw?.old_string !== "string" || typeof raw?.new_string !== "string") return void 0;
477
- hunks.push({
478
- oldString: raw.old_string,
479
- newString: raw.new_string,
480
- ...raw.replace_all === true ? { replaceAll: true } : {}
481
- });
482
- }
483
- return {
484
- tool: "multi_edit",
485
- path,
486
- hunks,
487
- ...priorContent !== void 0 ? { priorContent } : {}
488
- };
489
- }
490
- if (name === "write_file") {
491
- const content = input.content;
492
- if (typeof content !== "string") return void 0;
493
- return {
494
- tool: "write_file",
495
- path,
496
- hunks: [{
497
- oldString: priorContent ?? "",
498
- newString: content
499
- }],
500
- ...priorContent !== void 0 ? { priorContent } : {}
501
- };
502
- }
503
- }
504
- /**
505
- * LCS table cell budget shared by the line- and token-level diffs. 4M
506
- * cells ≈ low tens of MB of `number[]` rows — imperceptible — while
507
- * anything needing more (multi-thousand-line change cores) wouldn't
508
- * produce a human-readable minimal diff anyway.
509
- */
510
- const LCS_CELL_BUDGET = 4e6;
511
- function computeLineDiff(oldString, newString) {
512
- const oldLines = splitLines(oldString);
513
- const newLines = splitLines(newString);
514
- let lo = 0;
515
- while (lo < oldLines.length && lo < newLines.length && oldLines[lo] === newLines[lo]) lo++;
516
- let oldHi = oldLines.length;
517
- let newHi = newLines.length;
518
- while (oldHi > lo && newHi > lo && oldLines[oldHi - 1] === newLines[newHi - 1]) {
519
- oldHi--;
520
- newHi--;
521
- }
522
- const out = [];
523
- for (let k = 0; k < lo; k++) out.push({
524
- op: "context",
525
- text: oldLines[k]
526
- });
527
- const n = oldHi - lo;
528
- const m = newHi - lo;
529
- if ((n + 1) * (m + 1) > LCS_CELL_BUDGET) {
530
- for (let k = lo; k < oldHi; k++) out.push({
531
- op: "remove",
532
- text: oldLines[k]
533
- });
534
- for (let k = lo; k < newHi; k++) out.push({
535
- op: "add",
536
- text: newLines[k]
537
- });
538
- } else if (n > 0 || m > 0) appendLcsDiff(out, oldLines, newLines, lo, oldHi, newHi);
539
- for (let k = oldHi; k < oldLines.length; k++) out.push({
540
- op: "context",
541
- text: oldLines[k]
542
- });
543
- return out;
544
- }
545
- /**
546
- * The classic LCS table + reverse walk over `old[lo..oldHi) ×
547
- * new[lo..newHi)`, appending ops to `out` in forward order. Diagonals at
548
- * equal lines → context; otherwise step into the higher of (up, left)
549
- * and emit remove/add respectively. Ties break so adjacent remove/add
550
- * pairs stay grouped (the intra-line highlighter pairs them up).
551
- */
552
- function appendLcsDiff(out, oldLines, newLines, lo, oldHi, newHi) {
553
- const n = oldHi - lo;
554
- const m = newHi - lo;
555
- const lcs = Array.from({ length: n + 1 }, () => Array.from({ length: m + 1 }).fill(0));
556
- for (let i = 0; i < n; i++) for (let j = 0; j < m; j++) lcs[i + 1][j + 1] = oldLines[lo + i] === newLines[lo + j] ? lcs[i][j] + 1 : Math.max(lcs[i][j + 1], lcs[i + 1][j]);
557
- const core = [];
558
- let i = n;
559
- let j = m;
560
- while (i > 0 || j > 0) {
561
- if (i > 0 && j > 0 && oldLines[lo + i - 1] === newLines[lo + j - 1]) {
562
- core.push({
563
- op: "context",
564
- text: oldLines[lo + i - 1]
565
- });
566
- i--;
567
- j--;
568
- continue;
569
- }
570
- if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
571
- core.push({
572
- op: "add",
573
- text: newLines[lo + j - 1]
574
- });
575
- j--;
576
- continue;
577
- }
578
- core.push({
579
- op: "remove",
580
- text: oldLines[lo + i - 1]
581
- });
582
- i--;
583
- }
584
- core.reverse();
585
- out.push(...core);
586
- }
587
- /**
588
- * Split a string into lines preserving empty lines but dropping the
589
- * implicit trailing `""` produced by a final `\n`. Exported only for
590
- * its tests — callers should use `computeLineDiff`.
591
- */
592
- function splitLines(s) {
593
- if (s === "") return [];
594
- const parts = s.split("\n");
595
- if (parts[parts.length - 1] === "") parts.pop();
596
- return parts;
597
- }
598
- function computeInlineDiff(oldLine, newLine) {
599
- const oldTokens = tokenize(oldLine);
600
- const newTokens = tokenize(newLine);
601
- const n = oldTokens.length;
602
- const m = newTokens.length;
603
- if ((n + 1) * (m + 1) > LCS_CELL_BUDGET) return {
604
- oldSegments: oldLine === "" ? [] : [{
605
- text: oldLine,
606
- changed: true
607
- }],
608
- newSegments: newLine === "" ? [] : [{
609
- text: newLine,
610
- changed: true
611
- }]
612
- };
613
- const lcs = Array.from({ length: n + 1 }, () => Array.from({ length: m + 1 }).fill(0));
614
- for (let i = 0; i < n; i++) for (let j = 0; j < m; j++) lcs[i + 1][j + 1] = oldTokens[i] === newTokens[j] ? lcs[i][j] + 1 : Math.max(lcs[i][j + 1], lcs[i + 1][j]);
615
- const oldSegments = [];
616
- const newSegments = [];
617
- let i = n;
618
- let j = m;
619
- while (i > 0 || j > 0) {
620
- if (i > 0 && j > 0 && oldTokens[i - 1] === newTokens[j - 1]) {
621
- pushSegment(oldSegments, {
622
- text: oldTokens[i - 1],
623
- changed: false
624
- });
625
- pushSegment(newSegments, {
626
- text: newTokens[j - 1],
627
- changed: false
628
- });
629
- i--;
630
- j--;
631
- continue;
632
- }
633
- if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
634
- pushSegment(newSegments, {
635
- text: newTokens[j - 1],
636
- changed: true
637
- });
638
- j--;
639
- continue;
640
- }
641
- pushSegment(oldSegments, {
642
- text: oldTokens[i - 1],
643
- changed: true
644
- });
645
- i--;
646
- }
647
- oldSegments.reverse();
648
- newSegments.reverse();
649
- return {
650
- oldSegments,
651
- newSegments
652
- };
653
- }
654
- /**
655
- * Coalesce adjacent same-state segments so the renderer emits one
656
- * `<span>` per run instead of one per token — keeps the React tree
657
- * shallow on dense lines without changing the visual output.
658
- *
659
- * Walking direction is reverse (we push during the reverse walk, then
660
- * the caller reverses the array), so we coalesce against the *tail*.
661
- */
662
- function pushSegment(buf, seg) {
663
- const tail = buf[buf.length - 1];
664
- if (tail && tail.changed === seg.changed) tail.text = seg.text + tail.text;
665
- else buf.push(seg);
666
- }
667
- /**
668
- * Tokenize on word / non-word boundaries. Each run of `\w+` is one
669
- * token; each run of `\W+` (whitespace, punctuation) is another. This
670
- * gives the right granularity for renames (`oldName` → `newName`) and
671
- * for symbol swaps (`+ → -`) without exploding into per-char segments.
672
- *
673
- * Exported only for its tests.
674
- */
675
- function tokenize(s) {
676
- if (s === "") return [];
677
- const out = [];
678
- for (const match of s.matchAll(/\w+|\W+/g)) out.push(match[0]);
679
- return out;
680
- }
681
- /**
682
- * Apply the payload's hunks against `priorContent` and return the
683
- * resulting file body. Mirrors the agent's tool-side semantics:
684
- * - `replaceAll === true` → `String.replaceAll`
685
- * - otherwise → first-occurrence `String.replace`
686
- *
687
- * Hunks are applied in order — a `multi_edit` later hunk operates on
688
- * the output of the earlier ones, just like the actual tool.
689
- */
690
- function applyEditPayload(payload, priorContent) {
691
- let out = priorContent;
692
- for (const hunk of payload.hunks) out = hunk.replaceAll ? out.replaceAll(hunk.oldString, hunk.newString) : out.replace(hunk.oldString, hunk.newString);
693
- return out;
694
- }
695
- /**
696
- * Like `buildUnifiedDiff` but operating against the full file content
697
- * so the diff carries *real* file line numbers and configurable
698
- * surrounding context.
699
- *
700
- * Strategy:
701
- * 1. Apply the payload to `priorContent` → `newContent`.
702
- * 2. Run `computeLineDiff` over the whole file.
703
- * 3. Group non-context ops into hunks, padding each with up to
704
- * `contextLines` of context above and below. Adjacent hunks
705
- * whose context regions touch are merged so we don't emit two
706
- * `@@` headers separated by zero context lines.
707
- *
708
- * The output line numbers in the `@@` header are 1-based and reflect
709
- * the change's position in the actual file — what the user expects
710
- * when reading a diff alongside their editor.
711
- *
712
- * For `write_file` creating a new file (priorContent === ''), this
713
- * falls back to the same `--- /dev/null` convention as
714
- * `buildUnifiedDiff`.
715
- */
716
- function buildContextualDiff(payload, priorContent, contextLines = 3) {
717
- const newContent = applyEditPayload(payload, priorContent);
718
- const isNewFile = priorContent === "";
719
- const ops = computeLineDiff(priorContent, newContent);
720
- const oldLineFor = [];
721
- const newLineFor = [];
722
- let ol = 1;
723
- let nl = 1;
724
- for (const op of ops) {
725
- oldLineFor.push(ol);
726
- newLineFor.push(nl);
727
- if (op.op !== "add") ol++;
728
- if (op.op !== "remove") nl++;
729
- }
730
- const hunks = [];
731
- for (let i = 0; i < ops.length; i++) {
732
- if (ops[i].op === "context") continue;
733
- const start = Math.max(0, i - contextLines);
734
- const end = Math.min(ops.length - 1, i + contextLines);
735
- const last = hunks[hunks.length - 1];
736
- if (last && start <= last[1] + 1) last[1] = Math.max(last[1], end);
737
- else hunks.push([start, end]);
738
- }
739
- if (hunks.length === 0) return "";
740
- const parts = [];
741
- parts.push(isNewFile ? "--- /dev/null" : `--- a/${payload.path}`);
742
- parts.push(`+++ b/${payload.path}`);
743
- for (const [start, end] of hunks) {
744
- const slice = ops.slice(start, end + 1);
745
- const oldCount = slice.filter((l) => l.op !== "add").length;
746
- const newCount = slice.filter((l) => l.op !== "remove").length;
747
- const oldStart = oldCount === 0 ? Math.max(0, oldLineFor[start] - 1) : oldLineFor[start];
748
- const newStart = newCount === 0 ? Math.max(0, newLineFor[start] - 1) : newLineFor[start];
749
- parts.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`);
750
- for (const line of slice) {
751
- const prefix = line.op === "add" ? "+" : line.op === "remove" ? "-" : " ";
752
- parts.push(`${prefix}${line.text}`);
753
- }
754
- }
755
- return `${parts.join("\n")}\n`;
756
- }
757
- /**
758
- * Build a per-hunk digest used by the compact diff view.
759
- *
760
- * Strategy:
761
- * - When `priorContent` is present and the payload describes a real
762
- * file transformation, compute the contextual diff once, then walk
763
- * the LCS ops splitting at runs of `add` / `remove` to anchor each
764
- * summary entry to the **real** file line. This guarantees the
765
- * summary's `L<n>` matches what the user sees in their editor.
766
- * - Otherwise, fall back to per-hunk LCS over the (oldString,
767
- * newString) snippet pair. Line numbers are absent because the
768
- * snippet has no file position.
769
- */
770
- function summarizeEditPayload(payload) {
771
- const prior = payload.priorContent;
772
- if (prior !== void 0) return summarizeOpsByHunk(computeLineDiff(prior, applyEditPayload(payload, prior)));
773
- const hunks = [];
774
- let totalAdded = 0;
775
- let totalRemoved = 0;
776
- for (const hunk of payload.hunks) {
777
- const ops = computeLineDiff(hunk.oldString, hunk.newString);
778
- let added = 0;
779
- let removed = 0;
780
- let firstOld;
781
- let firstNew;
782
- for (const op of ops) if (op.op === "add") {
783
- added++;
784
- if (firstNew === void 0) firstNew = op.text;
785
- } else if (op.op === "remove") {
786
- removed++;
787
- if (firstOld === void 0) firstOld = op.text;
788
- }
789
- totalAdded += added;
790
- totalRemoved += removed;
791
- hunks.push({
792
- added,
793
- removed,
794
- ...firstOld !== void 0 ? { firstOld } : {},
795
- ...firstNew !== void 0 ? { firstNew } : {}
796
- });
797
- }
798
- return {
799
- totalAdded,
800
- totalRemoved,
801
- hunks
802
- };
803
- }
804
- /**
805
- * Walk an LCS op stream and emit one summary entry per *run* of
806
- * non-context ops, with the new-file line number where each run
807
- * starts. Adjacent add/remove ops collapse into the same entry —
808
- * matches git's hunk grouping at zero context.
809
- */
810
- function summarizeOpsByHunk(ops) {
811
- const hunks = [];
812
- let totalAdded = 0;
813
- let totalRemoved = 0;
814
- let nl = 1;
815
- let i = 0;
816
- while (i < ops.length) {
817
- if (ops[i].op === "context") {
818
- nl++;
819
- i++;
820
- continue;
821
- }
822
- const runStartLine = nl;
823
- let added = 0;
824
- let removed = 0;
825
- let firstOld;
826
- let firstNew;
827
- while (i < ops.length && ops[i].op !== "context") {
828
- const cur = ops[i];
829
- if (cur.op === "add") {
830
- added++;
831
- if (firstNew === void 0) firstNew = cur.text;
832
- nl++;
833
- } else {
834
- removed++;
835
- if (firstOld === void 0) firstOld = cur.text;
836
- }
837
- i++;
838
- }
839
- totalAdded += added;
840
- totalRemoved += removed;
841
- hunks.push({
842
- line: runStartLine,
843
- added,
844
- removed,
845
- ...firstOld !== void 0 ? { firstOld } : {},
846
- ...firstNew !== void 0 ? { firstNew } : {}
847
- });
848
- }
849
- return {
850
- totalAdded,
851
- totalRemoved,
852
- hunks
853
- };
854
- }
855
- function previewEditPayload(payload, priorContent, contextLines = 3) {
856
- const resolution = [];
857
- const resolvedHunks = [];
858
- const perHunkDiff = [];
859
- let running = priorContent;
860
- for (const hunk of payload.hunks) {
861
- if (hunk.oldString === "" || hunk.oldString === running) {
862
- resolution.push({
863
- resolved: true,
864
- via: "exact",
865
- occurrences: 1
866
- });
867
- resolvedHunks.push(hunk);
868
- perHunkDiff.push(buildContextualDiff({
869
- ...payload,
870
- hunks: [hunk]
871
- }, running, contextLines));
872
- running = hunk.newString;
873
- continue;
874
- }
875
- const match = resolveOldString(running, hunk.oldString);
876
- if (!match) {
877
- resolution.push({ resolved: false });
878
- resolvedHunks.push(hunk);
879
- perHunkDiff.push("");
880
- continue;
881
- }
882
- const ambiguous = match.occurrences > 1 && !hunk.replaceAll;
883
- const styledNew = styleReplacementForVia(hunk.newString, match.via, match.actual);
884
- const resolvedHunk = {
885
- oldString: match.actual,
886
- newString: styledNew,
887
- ...hunk.replaceAll ? { replaceAll: true } : {}
888
- };
889
- resolution.push({
890
- resolved: !ambiguous,
891
- via: match.via,
892
- occurrences: match.occurrences,
893
- ...ambiguous ? { ambiguous: true } : {}
894
- });
895
- resolvedHunks.push(resolvedHunk);
896
- perHunkDiff.push(ambiguous ? "" : buildContextualDiff({
897
- ...payload,
898
- hunks: [resolvedHunk]
899
- }, running, contextLines));
900
- if (!ambiguous) running = hunk.replaceAll ? running.replaceAll(match.actual, styledNew) : running.replace(match.actual, styledNew);
901
- }
902
- const resolvedPayload = {
903
- ...payload,
904
- hunks: resolvedHunks
905
- };
906
- const applicableHunks = resolvedHunks.filter((_, i) => resolution[i].resolved);
907
- return {
908
- diffText: applicableHunks.length === 0 ? "" : buildContextualDiff({
909
- ...payload,
910
- hunks: applicableHunks
911
- }, priorContent, contextLines),
912
- resolution,
913
- perHunkDiff,
914
- resolvedPayload
915
- };
916
- }
917
- /** Default size budget (UTF-16 code units) for transcript diff rows. */
918
- const DIFF_RENDER_MAX_BYTES = 512 * 1024;
919
- /** Marker appended to a line hard-truncated mid-content by the size cap. */
920
- const CLIP_LINE_MARKER = " …⟨line clipped⟩";
921
- /**
922
- * Truncate `line` so it fits `budget` code units including the marker,
923
- * without splitting a UTF-16 surrogate pair. Returns the line unchanged
924
- * when it already fits.
925
- */
926
- function clipLine(line, budget) {
927
- if (line.length <= budget) return line;
928
- let end = Math.max(0, budget - 16);
929
- const code = line.charCodeAt(end - 1);
930
- if (end > 0 && code >= 55296 && code <= 56319) end--;
931
- return line.slice(0, end) + CLIP_LINE_MARKER;
932
- }
933
- function clipDiffForRender(diff, maxLines = 600, maxBytes = DIFF_RENDER_MAX_BYTES) {
934
- const lines = diff.split("\n");
935
- const totalLines = lines.length > 0 && lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
936
- if (totalLines <= maxLines && diff.length <= maxBytes) return {
937
- text: diff,
938
- hiddenLines: 0,
939
- totalLines
940
- };
941
- const kept = [];
942
- let used = 0;
943
- for (let i = 0; i < totalLines; i++) {
944
- if (kept.length >= maxLines) break;
945
- const line = lines[i];
946
- const cost = line.length + 1;
947
- if (used + cost > maxBytes) {
948
- if (kept.length === 0) kept.push(clipLine(line, maxBytes));
949
- break;
950
- }
951
- kept.push(line);
952
- used += cost;
953
- }
954
- return {
955
- text: kept.join("\n"),
956
- hiddenLines: totalLines - kept.length,
957
- totalLines
958
- };
959
- }
960
- function buildUnifiedDiff(payload) {
961
- const parts = [];
962
- const isNewFile = payload.tool === "write_file" && payload.hunks[0]?.oldString === "";
963
- parts.push(isNewFile ? `--- /dev/null` : `--- a/${payload.path}`);
964
- parts.push(`+++ b/${payload.path}`);
965
- for (const hunk of payload.hunks) {
966
- const lines = computeLineDiff(hunk.oldString, hunk.newString);
967
- const oldCount = lines.filter((l) => l.op !== "add").length;
968
- const newCount = lines.filter((l) => l.op !== "remove").length;
969
- const oldStart = oldCount === 0 ? 0 : 1;
970
- const newStart = newCount === 0 ? 0 : 1;
971
- parts.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`);
972
- for (const line of lines) {
973
- const prefix = line.op === "add" ? "+" : line.op === "remove" ? "-" : " ";
974
- parts.push(`${prefix}${line.text}`);
975
- }
976
- }
977
- return `${parts.join("\n")}\n`;
978
- }
979
- const FILETYPE_BY_EXT = {
980
- ts: "typescript",
981
- mts: "typescript",
982
- cts: "typescript",
983
- tsx: "typescriptreact",
984
- js: "javascript",
985
- mjs: "javascript",
986
- cjs: "javascript",
987
- jsx: "javascriptreact",
988
- py: "python",
989
- pyi: "python",
990
- rs: "rust",
991
- go: "go",
992
- json: "json",
993
- jsonc: "json",
994
- sh: "bash",
995
- bash: "bash",
996
- zsh: "bash",
997
- yaml: "yaml",
998
- yml: "yaml",
999
- html: "html",
1000
- htm: "html",
1001
- css: "css",
1002
- md: "markdown",
1003
- markdown: "markdown",
1004
- zig: "zig",
1005
- lua: "lua",
1006
- toml: "toml",
1007
- sql: "sql"
1008
- };
1009
- function filetypeFromPath(path) {
1010
- const cleaned = path.split(/[?#]/, 1)[0];
1011
- const lastDot = cleaned.lastIndexOf(".");
1012
- if (lastDot === -1 || lastDot === cleaned.length - 1) return void 0;
1013
- const ext = cleaned.slice(lastDot + 1).toLowerCase();
1014
- return FILETYPE_BY_EXT[ext];
1015
- }
1016
- /** `1.2k` / `48k` / `1.3M` — token counts are read at a glance, not counted. */
1017
- function formatTokenCount(tokens) {
1018
- if (tokens >= 1e6) return `${(tokens / 1e6).toFixed(1)}M`;
1019
- if (tokens >= 1e3) return `${(tokens / 1e3).toFixed(tokens >= 1e4 ? 0 : 1)}k`;
1020
- return String(tokens);
1021
- }
1022
- /** `2m18s` / `48s` / `<1s` / `1h05m`. Compact — this is a receipt, not a profiler. */
1023
- function formatRunDuration(ms) {
1024
- if (ms < 1e3) return "<1s";
1025
- const s = Math.round(ms / 1e3);
1026
- if (s < 60) return `${s}s`;
1027
- const m = Math.floor(s / 60);
1028
- const rest = s % 60;
1029
- if (m < 60) return rest ? `${m}m${String(rest).padStart(2, "0")}s` : `${m}m`;
1030
- return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
1031
- }
1032
- /** `$0.04`, or four decimals under a cent so a cheap run is not shown as `$0.00`. */
1033
- function formatCostUsd(cost) {
1034
- return `$${cost.toFixed(cost < .01 ? 4 : 2)}`;
1035
- }
1036
- /**
1037
- * Build the receipt for a run record. `now` stands in for `endedAt` when the
1038
- * record is not finalized yet (`run:end` is broadcast a beat before it is).
1039
- */
1040
- function runReceiptFromRun(run, now = Date.now()) {
1041
- const sumIn = (u) => u.input + (u.cacheRead ?? 0) + (u.cacheCreation ?? 0);
1042
- const usage = run.totalUsage ? {
1043
- input: sumIn(run.totalUsage),
1044
- output: run.totalUsage.output
1045
- } : run.turnUsage?.reduce((acc, u) => ({
1046
- input: acc.input + sumIn(u),
1047
- output: acc.output + u.output
1048
- }), {
1049
- input: 0,
1050
- output: 0
1051
- });
1052
- const models = (run.turnUsage ?? []).map((u) => u.modelId).filter((m) => typeof m === "string" && m.length > 0);
1053
- const modelId = models.at(-1);
1054
- const otherModels = modelId ? new Set(models).size - 1 : 0;
1055
- const durationMs = (run.endedAt ?? now) - run.startedAt;
1056
- const cost = run.cost ?? run.turnUsage?.reduce((n, u) => n + (u.cost ?? 0), 0) ?? 0;
1057
- const turns = run.turns ?? run.turnUsage?.length ?? 0;
1058
- let genMs = 0;
1059
- let genOut = 0;
1060
- for (const u of run.turnUsage ?? []) {
1061
- if (u.streamDurationMs === void 0 || u.output <= 0) continue;
1062
- const ms = u.streamDurationMs - (u.timeToFirstTokenMs ?? 0);
1063
- if (ms <= 0) continue;
1064
- genMs += ms;
1065
- genOut += u.output;
1066
- }
1067
- const tokensPerSecond = genMs > 0 && genOut > 0 ? genOut * 1e3 / genMs : void 0;
1068
- return {
1069
- ...modelId ? { modelId } : {},
1070
- ...otherModels > 0 ? { otherModels } : {},
1071
- ...durationMs > 0 ? { durationMs } : {},
1072
- ...turns > 0 ? { turns } : {},
1073
- ...cost > 0 ? { costUsd: cost } : {},
1074
- ...usage && usage.input > 0 ? { tokensIn: usage.input } : {},
1075
- ...usage && usage.output > 0 ? { tokensOut: usage.output } : {},
1076
- ...tokensPerSecond !== void 0 ? { tokensPerSecond } : {}
1077
- };
1078
- }
1079
- /** `42 tk/s`; one decimal under 10 so a slow model is not shown as `0 tk/s`. */
1080
- function formatTokensPerSecond(tps) {
1081
- return `${tps < 10 ? tps.toFixed(1) : Math.round(tps).toString()} tk/s`;
1082
- }
1083
- /**
1084
- * The receipt's cells, in display order. Renderers join them with their own
1085
- * separator, tint them by `kind`, and truncate to their own width; the CONTENT
1086
- * is fixed here.
1087
- */
1088
- function formatRunReceiptCells(r) {
1089
- const cells = [];
1090
- if (r.modelId) cells.push({
1091
- kind: "model",
1092
- text: r.otherModels ? `${r.modelId} +${r.otherModels}` : r.modelId
1093
- });
1094
- if (r.durationMs !== void 0) cells.push({
1095
- kind: "duration",
1096
- text: formatRunDuration(r.durationMs)
1097
- });
1098
- if (r.turns !== void 0) cells.push({
1099
- kind: "turns",
1100
- text: `↻${r.turns}`
1101
- });
1102
- if (r.costUsd !== void 0) cells.push({
1103
- kind: "cost",
1104
- text: formatCostUsd(r.costUsd)
1105
- });
1106
- if (r.tokensIn !== void 0 || r.tokensOut !== void 0) {
1107
- const segments = [{
1108
- kind: "tokens-in",
1109
- text: `↑${formatTokenCount(r.tokensIn ?? 0)}`
1110
- }, {
1111
- kind: "tokens-out",
1112
- text: `↓${formatTokenCount(r.tokensOut ?? 0)}`
1113
- }];
1114
- cells.push({
1115
- kind: "tokens",
1116
- text: segments.map((seg) => seg.text).join(" "),
1117
- segments
1118
- });
1119
- }
1120
- if (r.tokensPerSecond !== void 0) cells.push({
1121
- kind: "speed",
1122
- text: formatTokensPerSecond(r.tokensPerSecond)
1123
- });
1124
- return cells;
1125
- }
1126
- /** The glyph that leads the line on every surface. */
1127
- const RUN_RECEIPT_GLYPH = "≡";
1128
- /**
1129
- * Fit cells to `width` columns for a tinted renderer: returns the cells that
1130
- * fit whole, plus whether the last one was cut. A cut cell is truncated with
1131
- * `…`; anything after it is dropped. Never wraps. `prefix` is the columns the
1132
- * renderer already spent (glyph + space).
1133
- */
1134
- function fitRunReceiptCells(cells, width, prefix = 0) {
1135
- let used = prefix;
1136
- const out = [];
1137
- for (const cell of cells) {
1138
- const need = (out.length ? 3 : 0) + cell.text.length;
1139
- if (used + need <= width) {
1140
- out.push(cell);
1141
- used += need;
1142
- continue;
1143
- }
1144
- const room = width - used - (out.length ? 3 : 0);
1145
- if (room >= 2) out.push({
1146
- kind: cell.kind,
1147
- text: `${cell.text.slice(0, room - 1).trimEnd()}…`
1148
- });
1149
- break;
1150
- }
1151
- return out;
1152
- }
1153
- /** One line, ` · `-joined, ellipsized to `width` columns. Never wraps. */
1154
- function formatRunReceiptLine(r, width = Number.POSITIVE_INFINITY) {
1155
- const line = formatRunReceiptCells(r).map((c) => c.text).join(" · ");
1156
- if (line.length <= width) return line;
1157
- if (width <= 1) return width === 1 ? "…" : "";
1158
- return `${line.slice(0, width - 1).trimEnd()}…`;
1159
- }
1160
- /** Anything to show? A run with no record data renders no line at all. */
1161
- function hasRunReceipt(r) {
1162
- return formatRunReceiptCells(r).length > 0;
1163
- }
1164
- //#endregion
1165
- //#region src/chat/orchestration.ts
1166
- /**
1167
- * Orchestration — the pure model for a "main" session that drives others.
1168
- *
1169
- * A main session is an ordinary session with a role, and a worker is an
1170
- * ordinary session with a parent pointer. Both are `metadata` keys, so every
1171
- * existing surface (sidebar, history, attach, search, replay) works on them
1172
- * with no special cases; only the surfaces that WANT to treat them specially
1173
- * (pinning, the event row, the jump shortcut) read these keys.
1174
- *
1175
- * This is deliberately not `spawn`. A spawned child is ephemeral, inline, and
1176
- * dies with the parent's run; a worker is a persistent, resumable session with
1177
- * its own cwd, model chain, todos and history, visible in every window, and it
1178
- * outlives the main's run. The parent pointer is what makes them a tree.
1179
- *
1180
- * Node-free: re-exported from `zidane/chat/pure` for the Electron renderer,
1181
- * which needs `isMainSession` / `orchestratedBy` for pinning and the event row.
1182
- */
1183
- /** Title the main session is created with. Users can rename it; the ROLE is what matters. */
1184
- const MAIN_SESSION_TITLE = "Main";
1185
- /** `session.metadata.role` — present and `'main'` on the one orchestrating session. */
1186
- const SESSION_ROLE_KEY = "role";
1187
- /** `session.metadata.orchestratedBy` — the main session's id, on a worker. */
1188
- const ORCHESTRATED_BY_KEY = "orchestratedBy";
1189
- /** Type guard over untrusted metadata. */
1190
- function isMainSession(metadata) {
1191
- return metadata?.[SESSION_ROLE_KEY] === "main";
1192
- }
1193
- /** The main session id a worker reports to, or `null` for an ordinary session. */
1194
- function orchestratedBy(metadata) {
1195
- const value = metadata?.[ORCHESTRATED_BY_KEY];
1196
- return typeof value === "string" && value.length > 0 ? value : null;
1197
- }
1198
- /** Tools whose `path` input means "this file changed". */
1199
- const CHANGE_TOOLS = /* @__PURE__ */ new Set([
1200
- "edit",
1201
- "multi_edit",
1202
- "write_file"
1203
- ]);
1204
- const ARTIFACT_PATH_CAP = 12;
1205
- /**
1206
- * What a run touched, read off its assistant turns' tool calls. Only the
1207
- * tools that MUTATE count as "changed" — reads are not artifacts. Capped so a
1208
- * refactor across a hundred files does not turn the event into a manifest;
1209
- * the count still tells the truth.
1210
- */
1211
- function artifactsFromTurns(turns) {
1212
- const changed = [];
1213
- const seen = /* @__PURE__ */ new Set();
1214
- let commands = 0;
1215
- let toolCalls = 0;
1216
- for (const turn of turns) {
1217
- if (turn.role !== "assistant") continue;
1218
- for (const block of turn.content) {
1219
- if (block.type !== "tool_call" || !block.name) continue;
1220
- toolCalls++;
1221
- if (block.name === "shell") commands++;
1222
- if (CHANGE_TOOLS.has(block.name)) {
1223
- const path = block.input?.path;
1224
- if (typeof path === "string" && path && !seen.has(path)) {
1225
- seen.add(path);
1226
- changed.push(path);
1227
- }
1228
- }
1229
- }
1230
- }
1231
- if (toolCalls === 0) return void 0;
1232
- return {
1233
- changed: changed.slice(0, ARTIFACT_PATH_CAP),
1234
- commands,
1235
- toolCalls
1236
- };
1237
- }
1238
- const STATUSES = [
1239
- "done",
1240
- "error",
1241
- "cancelled",
1242
- "needs-attention"
1243
- ];
1244
- /**
1245
- * Wire format between the harness and the model — and, because it is persisted
1246
- * inside the user turn's text block, between a live session and its replay.
1247
- *
1248
- * Follows `<task-notification>` exactly: child-tag XML, one block per event,
1249
- * escaped with `escapeXml`. The model already knows this shape as "the harness
1250
- * telling me something", which is the point — a main that mistook a worker
1251
- * report for a human instruction would act on it as one. `eventsFromTurns`
1252
- * detects the block on replay and re-synthesizes the structured row instead of
1253
- * painting the raw XML as a user prompt.
1254
- */
1255
- function renderSessionEventXml(event) {
1256
- const tag = (name, value) => value === void 0 ? [] : [` <${name}>${escapeXml(String(value))}</${name}>`];
1257
- return [
1258
- "<session-event>",
1259
- ...tag("session-id", event.sessionId),
1260
- ...tag("title", event.title),
1261
- ...tag("status", event.status),
1262
- ...tag("at", event.at),
1263
- ...tag("cwd", event.cwd ?? ""),
1264
- ...tag("cost-usd", event.costUsd),
1265
- ...tag("duration-ms", event.durationMs),
1266
- ...tag("tokens", event.tokens),
1267
- ...tag("turns", event.turns),
1268
- ...tag("worker", event.worker === void 0 ? void 0 : String(event.worker)),
1269
- ...tag("seq", event.seq),
1270
- ...tag("finish-reason", event.finishReason),
1271
- ...tag("error", event.error),
1272
- ...tag("detail", event.detail),
1273
- ...tag("goal", event.goal),
1274
- ...event.artifacts ? [
1275
- ` <artifacts tool-calls="${event.artifacts.toolCalls}" commands="${event.artifacts.commands}">`,
1276
- ...event.artifacts.changed.map((p) => ` <changed>${escapeXml(p)}</changed>`),
1277
- " </artifacts>"
1278
- ] : [],
1279
- ...tag("summary", event.summary),
1280
- "</session-event>"
1281
- ].join("\n");
1282
- }
1283
- const SESSION_EVENT_RE = /^\s*<session-event>([\s\S]*?)<\/session-event>\s*$/;
1284
- const FIELD = {
1285
- sessionId: /<session-id>([\s\S]*?)<\/session-id>/,
1286
- title: /<title>([\s\S]*?)<\/title>/,
1287
- status: /<status>([\s\S]*?)<\/status>/,
1288
- at: /<at>([\s\S]*?)<\/at>/,
1289
- cwd: /<cwd>([\s\S]*?)<\/cwd>/,
1290
- costUsd: /<cost-usd>([\s\S]*?)<\/cost-usd>/,
1291
- durationMs: /<duration-ms>([\s\S]*?)<\/duration-ms>/,
1292
- tokens: /<tokens>([\s\S]*?)<\/tokens>/,
1293
- turns: /<turns>([\s\S]*?)<\/turns>/,
1294
- worker: /<worker>([\s\S]*?)<\/worker>/,
1295
- error: /<error>([\s\S]*?)<\/error>/,
1296
- detail: /<detail>([\s\S]*?)<\/detail>/,
1297
- seq: /<seq>([\s\S]*?)<\/seq>/,
1298
- goal: /<goal>([\s\S]*?)<\/goal>/,
1299
- finishReason: /<finish-reason>([\s\S]*?)<\/finish-reason>/,
1300
- artifacts: /<artifacts tool-calls="(\d+)" commands="(\d+)">([\s\S]*?)<\/artifacts>/,
1301
- summary: /<summary>([\s\S]*?)<\/summary>/
1302
- };
1303
- const CHANGED_RE = /<changed>([\s\S]*?)<\/changed>/g;
1304
- const FINISH_REASONS = /* @__PURE__ */ new Set([
1305
- "length",
1306
- "pause",
1307
- "content-filter"
1308
- ]);
1309
- /**
1310
- * Inverse of {@link renderSessionEventXml}. Untrusted input — it comes off disk
1311
- * and outlives this module's versions — so it validates each field and returns
1312
- * `null` for anything malformed, in which case the turn replays as the plain
1313
- * user text it structurally is.
1314
- */
1315
- function parseSessionEventXml(text) {
1316
- const m = text.match(SESSION_EVENT_RE);
1317
- if (!m) return null;
1318
- const body = m[1];
1319
- const pick = (re) => {
1320
- const inner = body.match(re);
1321
- return inner ? unescapeXml(inner[1].trim()) : void 0;
1322
- };
1323
- const sessionId = pick(FIELD.sessionId);
1324
- const title = pick(FIELD.title);
1325
- const status = pick(FIELD.status);
1326
- const at = Number(pick(FIELD.at));
1327
- const summary = pick(FIELD.summary);
1328
- if (!sessionId || title === void 0 || summary === void 0) return null;
1329
- if (!status || !STATUSES.includes(status)) return null;
1330
- if (!Number.isFinite(at)) return null;
1331
- const cost = Number(pick(FIELD.costUsd));
1332
- const cwd = pick(FIELD.cwd);
1333
- const positive = (raw) => {
1334
- const n = Number(raw);
1335
- return Number.isFinite(n) && n > 0 ? n : void 0;
1336
- };
1337
- const durationMs = positive(pick(FIELD.durationMs));
1338
- const tokens = positive(pick(FIELD.tokens));
1339
- const turns = positive(pick(FIELD.turns));
1340
- const worker = pick(FIELD.worker);
1341
- const error = pick(FIELD.error);
1342
- const detail = pick(FIELD.detail);
1343
- const seq = positive(pick(FIELD.seq));
1344
- const goal = pick(FIELD.goal);
1345
- const finishReason = pick(FIELD.finishReason);
1346
- const art = body.match(FIELD.artifacts);
1347
- const artifacts = art ? {
1348
- toolCalls: Number(art[1]),
1349
- commands: Number(art[2]),
1350
- changed: [...art[3].matchAll(CHANGED_RE)].map((m) => unescapeXml(m[1].trim()))
1351
- } : void 0;
1352
- return {
1353
- sessionId,
1354
- title,
1355
- status,
1356
- summary,
1357
- at,
1358
- ...Number.isFinite(cost) && cost > 0 ? { costUsd: cost } : {},
1359
- ...cwd !== void 0 ? { cwd } : {},
1360
- ...durationMs !== void 0 ? { durationMs } : {},
1361
- ...tokens !== void 0 ? { tokens } : {},
1362
- ...turns !== void 0 ? { turns } : {},
1363
- ...worker === "true" ? { worker: true } : worker === "false" ? { worker: false } : {},
1364
- ...error ? { error } : {},
1365
- ...detail ? { detail } : {},
1366
- ...seq !== void 0 ? { seq } : {},
1367
- ...goal ? { goal } : {},
1368
- ...finishReason && FINISH_REASONS.has(finishReason) ? { finishReason } : {},
1369
- ...artifacts ? { artifacts } : {}
1370
- };
1371
- }
1372
- function unescapeXml(s) {
1373
- return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&apos;/g, "'").replace(/&amp;/g, "&");
1374
- }
1375
- /**
1376
- * The short label a transcript row shows — `api-refactor · done`. Status words
1377
- * are chosen to read as a verb phrase after the title.
1378
- */
1379
- function sessionEventLabel(event) {
1380
- return `${event.title} ${{
1381
- "done": "finished",
1382
- "error": "failed",
1383
- "cancelled": "was cancelled",
1384
- "needs-attention": "needs attention"
1385
- }[event.status]}`;
1386
- }
1387
- /**
1388
- * Status → how it reads. `needs-attention` is deliberately the loudest: it is
1389
- * the one status where the human, not the main, has to act.
1390
- */
1391
- const SESSION_EVENT_STATUS_STYLE = {
1392
- "done": {
1393
- glyph: "✓",
1394
- tone: "accent",
1395
- verb: "finished"
1396
- },
1397
- "error": {
1398
- glyph: "✗",
1399
- tone: "error",
1400
- verb: "failed"
1401
- },
1402
- "cancelled": {
1403
- glyph: "⊘",
1404
- tone: "dim",
1405
- verb: "was cancelled"
1406
- },
1407
- "needs-attention": {
1408
- glyph: "!",
1409
- tone: "warn",
1410
- verb: "needs your input"
1411
- }
1412
- };
1413
- /**
1414
- * The directory name a session lives in — the same basename label the sidebar
1415
- * groups by. `undefined` for a directory-less session (the main) or no cwd.
1416
- */
1417
- function sessionDirLabel(cwd) {
1418
- if (!cwd) return void 0;
1419
- const parts = cwd.split(/[\\/]+/).filter(Boolean);
1420
- return parts[parts.length - 1];
1421
- }
1422
- /** Short, stable id fragment for the header — enough to disambiguate, not to read. */
1423
- function shortSessionId(id) {
1424
- return id.length <= 8 ? id : id.slice(0, 8);
1425
- }
1426
- /**
1427
- * The header's metadata cells, in display order, omitting anything unknown.
1428
- * Renderers join them with their own separator; the CONTENT is fixed here so
1429
- * both surfaces show the same receipt for the same run.
1430
- *
1431
- * The session id is deliberately NOT a cell: it is the least legible datum and
1432
- * exists only to disambiguate. Renderers append `shortSessionId` LAST and
1433
- * dimmer than the rest.
1434
- */
1435
- function sessionEventMeta(event) {
1436
- const cells = [];
1437
- const dir = sessionDirLabel(event.cwd);
1438
- if (dir) cells.push(dir);
1439
- if (event.durationMs !== void 0) cells.push(formatRunDuration(event.durationMs));
1440
- if (event.tokens !== void 0) cells.push(`${formatTokenCount(event.tokens)} tok`);
1441
- if (event.turns !== void 0) cells.push(`${event.turns} turn${event.turns === 1 ? "" : "s"}`);
1442
- if (event.costUsd !== void 0) cells.push(formatCostUsd(event.costUsd));
1443
- return cells;
1444
- }
1445
- /**
1446
- * The dimmed second line: the last thing the session said, collapsed to one
1447
- * paragraph and capped. Renderers may clamp further (line-clamp in the GUI,
1448
- * width in the TUI); this is the upper bound and the shared ellipsis rule.
1449
- */
1450
- function sessionEventExcerpt(event, max = 240) {
1451
- const flat = (event.status === "needs-attention" && event.detail ? event.detail : event.status === "error" && event.error ? event.error : event.summary).replace(/\s+/g, " ").trim();
1452
- return flat.length <= max ? flat : `${flat.slice(0, max - 1).trimEnd()}…`;
1453
- }
1454
- /** Ellipsize a title to `max` characters; titles are user text and can be a paragraph. */
1455
- function truncateTitle(title, max = 48) {
1456
- const flat = title.replace(/\s+/g, " ").trim();
1457
- return flat.length <= max ? flat : `${flat.slice(0, max - 1).trimEnd()}…`;
1458
- }
1459
- /**
1460
- * System-prompt fragment appended to every WORKER's agent. A worker's last
1461
- * message becomes the main's `summary`, so it must be a report, not a sign-off
1462
- * — the main should be able to act on it without reading the transcript.
1463
- */
1464
- const WORKER_REPORT_FRAGMENT = `You are a worker session created by an orchestrating main session. Your final message of a run is delivered to it verbatim as your report, so end every run with a short structured report:
1465
-
1466
- ## Report
1467
- - **Goal**: what you were asked, in one line
1468
- - **Outcome**: done / partial / blocked — one line, and why if not done
1469
- - **Changed**: files or artifacts touched (paths), or "none"
1470
- - **Checks**: what you ran and the result (tests, typecheck, lint), or "none"
1471
- - **Open questions**: anything the main or a human must decide, or "none"
1472
- - **Next**: the concrete next step you recommend, or "none"
1473
-
1474
- Keep it factual and under ~15 lines. This block is what the main decides on; prose before it is fine, prose instead of it is not.`;
1475
- /**
1476
- * System-prompt fragment appended to the MAIN's agent: how to delegate without
1477
- * wasting calls. The `sessions` tool description says what each verb does;
1478
- * this says which one to reach for, and when not to call at all.
1479
- */
1480
- const MAIN_PLAYBOOK_FRAGMENT = `You are the main session: an orchestrator. Work happens in worker sessions; you brief them, integrate their reports, and keep the human informed. Do not do the workers' work here.
1481
-
1482
- Work efficiently with the \`sessions\` tool:
1483
- - Never poll. After create or send you WILL receive a <session-event> when the worker finishes, fails, or blocks. Do not read or list a worker that has not reported yet.
1484
- - Reuse before creating. To continue work in a project, send to its existing idle session (listed under Projects below; its context is already warm). Create a worker only for a new, independent unit of work, or when the project has no suitable session.
1485
- - You already know the ids. Take them from Projects below, from a <session-event>, or from an earlier list. Do not list just to find an id you have.
1486
- - send vs steer: send when the worker is idle or the new prompt should queue after its current run; steer only to change course mid-run.
1487
- - Fire-and-forget when you can. For work you will not act on when it lands (docs, cleanup, low-stakes checks), pass wake: false on create or send; the report still reaches your history, you just are not woken for it.
1488
- - Brief completely. A worker prompt states the goal, the constraints, what to check (tests, typecheck, lint), and that it must end with its report. Follow-up questions cost a full round trip.
1489
- - read is cheap (last message); transcript is not. Use transcript only when a report is unclear.
1490
- - Report to the human briefly: what went where, what came back, what needs a decision. Do not re-narrate worker reports.`;
1491
- /**
1492
- * The main's "Projects" briefing: which directories have ongoing work and
1493
- * the session to address in each, so the main does not rediscover them with
1494
- * a list call every run. Snapshot at agent start — live state is one `list`
1495
- * away and the text says so.
1496
- */
1497
- function renderProjectsBriefing(projects, now) {
1498
- const lines = ["## Projects", "Known as of this session's start; live state comes from `sessions list`. Directory-less sessions are omitted."];
1499
- if (projects.length === 0) {
1500
- lines.push("- none yet");
1501
- return lines.join("\n");
1502
- }
1503
- for (const p of projects) {
1504
- const cells = [`${p.sessions} session${p.sessions === 1 ? "" : "s"}`, `last active ${agoLabel(now - p.lastActiveAt)}`];
1505
- if (p.active > 0) cells.push(`${p.active} active`);
1506
- lines.push(`- ${sessionDirLabel(p.root) ?? p.root} (${p.root}) · ${cells.join(" · ")}`);
1507
- lines.push(` latest: ${p.latest.sessionId} "${truncateTitle(p.latest.title, 60)}" (${p.latest.state})`);
1508
- }
1509
- return lines.join("\n");
1510
- }
1511
- /** Coarse relative time for the briefing: `just now`, `12m ago`, `3h ago`, `5d ago`. */
1512
- function agoLabel(elapsedMs) {
1513
- const m = Math.floor(Math.max(0, elapsedMs) / 6e4);
1514
- if (m < 1) return "just now";
1515
- if (m < 60) return `${m}m ago`;
1516
- const h = Math.floor(m / 60);
1517
- if (h < 48) return `${h}h ago`;
1518
- return `${Math.floor(h / 24)}d ago`;
1519
- }
1520
- //#endregion
1521
- //#region src/chat/turn-selection.ts
1522
- /** Tools whose `tool-result` event is suppressed when `showEditDiffs` is on. */
1523
- const EDIT_TOOL_NAMES = /* @__PURE__ */ new Set([
1524
- "edit",
1525
- "multi_edit",
1526
- "write_file"
1527
- ]);
1528
- /**
1529
- * Keep turn-selection mode alive after deleting its active row. Prefer the
1530
- * row that slides into the deleted index; at the tail, select the new last
1531
- * row. Returns null only when no selectable rows survive.
1532
- */
1533
- function selectionAfterTurnRemoval(before, removedId, after) {
1534
- if (after.includes(removedId)) return removedId;
1535
- if (after.length === 0) return null;
1536
- const index = before.indexOf(removedId);
1537
- return after[Math.min(Math.max(0, index), after.length - 1)] ?? null;
1538
- }
1539
- /**
1540
- * Recognize a tool-result body as carrying NON-success information so the
1541
- * renderer doesn't suppress it under `showEditDiffs`. Three categories:
1542
- *
1543
- * - `edit` → "Edit error: …"
1544
- * - `write_file` permission errors wrapped by the loop → "Tool failed: …"
1545
- * - `multi_edit` → legacy single-line error `multi_edit error: …`, OR
1546
- * a result carrying an `<edit-outcomes>…</edit-outcomes>` annotation
1547
- * block. The TUI only appends the annotation when at least one hunk
1548
- * was NOT applied, so its mere presence is the signal — the result
1549
- * body needs to stay visible next to the diff so the user can read
1550
- * denial / skip / failure reasons longer than the per-hunk badge.
1551
- * - Fully-denied gate emit (`[fully denied] <edit-outcomes>…`) likewise
1552
- * stays visible.
55
+ * - `edit` → "Edit error: …"
56
+ * - `write_file` permission errors wrapped by the loop → "Tool failed: …"
57
+ * - `multi_edit` → legacy single-line error `multi_edit error: …`, OR
58
+ * a result carrying an `<edit-outcomes>…</edit-outcomes>` annotation
59
+ * block. The TUI only appends the annotation when at least one hunk
60
+ * was NOT applied, so its mere presence is the signal — the result
61
+ * body needs to stay visible next to the diff so the user can read
62
+ * denial / skip / failure reasons longer than the per-hunk badge.
63
+ * - Fully-denied gate emit (`[fully denied] <edit-outcomes>…`) likewise
64
+ * stays visible.
1553
65
  *
1554
66
  * Exported for unit-testability of the visibility matrix.
1555
67
  */
@@ -1562,8 +74,8 @@ function isEditErrorResult(text) {
1562
74
  return false;
1563
75
  }
1564
76
  /**
1565
- * `callId` prefix minted by the TUI's `!` bash mode (see `runBashCommand`
1566
- * in `src/tui/app.tsx`). The prefix is the durable signal — it survives
77
+ * `callId` prefix minted by the TUI's `!` bash mode (see `runBashMode`
78
+ * in `src/tui/commands/run-bash-mode.ts`). The prefix is the durable signal — it survives
1567
79
  * persistence and replay — that a `shell` tool exchange was USER-initiated,
1568
80
  * so renderers can keep it visible even when the tool-display settings
1569
81
  * hide model-driven plumbing: the user typed the command and expects to
@@ -1692,7 +204,7 @@ function isVisibleDuringSearch(event, settings, query) {
1692
204
  * Renderer-agnostic — returns plain `boolean` so TUI / GUI consumers
1693
205
  * can filter events identically.
1694
206
  */
1695
- /** `notice.label` the host stamps on prompt-cache break/expiry rows (`host/engine.ts`). */
207
+ /** `notice.label` the host stamps on prompt-cache break/expiry rows (`host/notices/index.ts`). */
1696
208
  const CACHE_NOTICE_LABEL = "cache";
1697
209
  function isVisible(event, settings) {
1698
210
  if (settings.hideSubagentOutput) {
@@ -1706,6 +218,7 @@ function isVisible(event, settings) {
1706
218
  case "tool-result": return settings.showToolResults || isUserBashEvent(event);
1707
219
  case "run-summary": return settings.showRunReceipt;
1708
220
  case "info": return settings.showCacheNotices || event.notice?.label !== "cache";
221
+ case "spawn-queued": return settings.transcriptDensity === "compact";
1709
222
  default: return true;
1710
223
  }
1711
224
  }
@@ -1768,68 +281,621 @@ function turnSelectionOwnership(events) {
1768
281
  else if (lastToolEmitterTurnId) ownership.set(tid, lastToolEmitterTurnId);
1769
282
  continue;
1770
283
  }
1771
- if (kinds.includes("tool")) lastToolEmitterTurnId = tid;
1772
- }
1773
- return ownership;
1774
- }
1775
- /**
1776
- * Render-time check: should `event` paint with the selection accent?
1777
- *
1778
- * `true` when the event's own turn is selected, OR when the selected turn
1779
- * `owns` the event's turn via {@link turnSelectionOwnership} (the call and
1780
- * its tool-result rows highlight together). `false` when nothing is
1781
- * selected or the relationship doesn't apply.
1782
- *
1783
- * Pure. Renderer-agnostic — the TUI's `<Transcript>` uses it; a GUI's
1784
- * equivalent walks the same rule.
1785
- */
1786
- function isTurnHighlighted(event, selectedTurnId, ownership) {
1787
- if (selectedTurnId === null || !event.turnId) return false;
1788
- if (event.turnId === selectedTurnId) return true;
1789
- return ownership.get(event.turnId) === selectedTurnId || ownership.get(selectedTurnId) === event.turnId;
284
+ if (kinds.includes("tool")) lastToolEmitterTurnId = tid;
285
+ }
286
+ return ownership;
287
+ }
288
+ /**
289
+ * Render-time check: should `event` paint with the selection accent?
290
+ *
291
+ * `true` when the event's own turn is selected, OR when the selected turn
292
+ * `owns` the event's turn via {@link turnSelectionOwnership} (the call and
293
+ * its tool-result rows highlight together). `false` when nothing is
294
+ * selected or the relationship doesn't apply.
295
+ *
296
+ * Pure. Renderer-agnostic — the TUI's `<Transcript>` uses it; a GUI's
297
+ * equivalent walks the same rule.
298
+ */
299
+ function isTurnHighlighted(event, selectedTurnId, ownership) {
300
+ if (selectedTurnId === null || !event.turnId) return false;
301
+ if (event.turnId === selectedTurnId) return true;
302
+ return ownership.get(event.turnId) === selectedTurnId || ownership.get(selectedTurnId) === event.turnId;
303
+ }
304
+ /**
305
+ * Ownership for "one selection stop per message": result-only turns fold
306
+ * into their assistant turn (`turnSelectionOwnership`), and turns that only
307
+ * did work (thinking, tool calls, worker markers) fold into the answer that
308
+ * follows them in the same exchange. "Read three files, then answered" is
309
+ * one stop, so ↑ moves one visible message at a time. An exchange that ends
310
+ * in work alone stops on its first work turn. Same rule as the GUI's
311
+ * `computeSelectStops`.
312
+ */
313
+ function messageSelectionOwnership(events) {
314
+ const base = turnSelectionOwnership(events);
315
+ const resolve = (id) => base.get(id) ?? id;
316
+ const stops = /* @__PURE__ */ new Set();
317
+ const merged = /* @__PURE__ */ new Map();
318
+ let pending = [];
319
+ const settle = (into) => {
320
+ const target = into ?? pending[0];
321
+ for (const id of pending) if (target !== void 0 && id !== target) merged.set(id, target);
322
+ if (target !== void 0) stops.add(target);
323
+ pending = [];
324
+ };
325
+ for (const event of events) {
326
+ if (event.childId !== void 0 || (event.depth ?? 0) > 0) continue;
327
+ if (event.kind === "separator" || event.kind === "user-prompt" || event.kind === "compact-summary") settle(null);
328
+ if (!event.turnId) continue;
329
+ const owner = resolve(event.turnId);
330
+ const work = event.kind === "tool" || event.kind === "tool-result" || event.kind === "thinking" || event.kind === "spawn-start" || event.kind === "spawn-end" || event.kind === "command-detail" || event.kind === "info" && event.notice === void 0;
331
+ if (event.kind === "markdown" && event.text.trim()) {
332
+ stops.add(owner);
333
+ settle(owner);
334
+ } else if (work) {
335
+ if (!stops.has(owner) && !pending.includes(owner)) pending.push(owner);
336
+ } else if (event.kind !== "user-prompt" && event.kind !== "compact-summary") {
337
+ settle(null);
338
+ stops.add(owner);
339
+ } else stops.add(owner);
340
+ }
341
+ settle(null);
342
+ const out = /* @__PURE__ */ new Map();
343
+ const final = (id) => merged.get(id) ?? id;
344
+ for (const [raw, owner] of base) {
345
+ const stop = final(owner);
346
+ if (stop !== raw) out.set(raw, stop);
347
+ }
348
+ for (const [id, stop] of merged) out.set(id, stop);
349
+ return out;
350
+ }
351
+ /**
352
+ * Deduplicated, in-order list of **parent-conversation** turn ids that appear
353
+ * in a rendered transcript — the navigation index for the TUI's select-turn
354
+ * mode. Three classes of turns are deliberately skipped:
355
+ *
356
+ * - **Subagent turns** (`childId` set). Nested execution detail; the
357
+ * user's mental model of a "message" is the conversational exchange,
358
+ * not each spawn turn. Also filtered out by `isVisible` under
359
+ * `hideSubagentOutput: true` — selecting them would highlight nothing.
360
+ * - **Result-only turns** — see {@link turnSelectionOwnership}. These get
361
+ * coalesced into the assistant turn that emitted their tool_calls.
362
+ * - **Settings-hidden turns** (when `settings` is supplied). A turn whose
363
+ * every event fails {@link isVisible} would render no rows — landing
364
+ * the cursor there hides it from the user entirely. The check is opt-
365
+ * in so SDK callers without a Settings object keep the legacy
366
+ * "everything visible" behavior.
367
+ *
368
+ * Synthetic events (separator, spawn-start, spawn-end) have no `turnId` and
369
+ * are skipped naturally.
370
+ */
371
+ function selectableTurnIds(events, settings, visible, opts) {
372
+ const ownership = opts?.byMessage ? messageSelectionOwnership(events) : turnSelectionOwnership(events);
373
+ const visibleCount = settings ? /* @__PURE__ */ new Map() : null;
374
+ if (settings && visibleCount) for (const e of events) {
375
+ if (!e.turnId || e.childId) continue;
376
+ if (!(visible ? visible(e) : isVisible(e, settings))) continue;
377
+ visibleCount.set(e.turnId, (visibleCount.get(e.turnId) ?? 0) + 1);
378
+ }
379
+ const seen = /* @__PURE__ */ new Set();
380
+ const ordered = [];
381
+ for (const e of events) {
382
+ if (!e.turnId) continue;
383
+ if (e.childId) continue;
384
+ if (seen.has(e.turnId)) continue;
385
+ if (ownership.has(e.turnId)) continue;
386
+ if (visibleCount && (visibleCount.get(e.turnId) ?? 0) === 0) continue;
387
+ seen.add(e.turnId);
388
+ ordered.push(e.turnId);
389
+ }
390
+ return ordered;
391
+ }
392
+ //#endregion
393
+ //#region src/chat/machine-protocol.ts
394
+ /** Bumped on an incompatible change; a host refuses another major. */
395
+ const MACHINE_PROTOCOL_VERSION = 1;
396
+ /** Largest `data` chunk (decoded bytes) of one `exec.stream.data` notification or `exec.stream.write`. */
397
+ const MACHINE_STREAM_CHUNK_BYTES = 262144;
398
+ /**
399
+ * Calls whose re-send after a lost reply cannot change anything (reads, and
400
+ * the idempotent handle calls: the machine keys handles by the host's id).
401
+ * Everything else that was sent is reported as a lost result, never re-sent.
402
+ */
403
+ const MACHINE_IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
404
+ "machine.ping",
405
+ "exec.spawn",
406
+ "exec.cancel",
407
+ "exec.destroy",
408
+ "fs.read",
409
+ "fs.list",
410
+ "fs.size",
411
+ "bg.list",
412
+ "bg.wait",
413
+ "env.checkFolder",
414
+ "terminal.attach"
415
+ ]);
416
+ /** Error codes of the machine protocol (JSON-RPC server-error range, apart from `IpcErrorCode`). */
417
+ const MachineErrorCode = {
418
+ /** A machine connection called a client method, or a client a machine one. */
419
+ MethodNotAllowed: -32040,
420
+ /** `machine.register` from another major `MACHINE_PROTOCOL_VERSION`. */
421
+ VersionSkew: -32041,
422
+ /** The name is held by another machine on this host. */
423
+ NameTaken: -32042,
424
+ /** A path or cwd outside the machine's roots, or in its data dir. */
425
+ OutsideRoots: -32043,
426
+ /** The machine id is bound to another (live) token, or this token was used by the machine before the host forgot it. */
427
+ IdBound: -32044,
428
+ /** No room now (a capacity limit): try again later, unlike a refusal. */
429
+ Busy: -32045
430
+ };
431
+ /** Where a local host keeps the machine token a saved server gave it (credentials store). */
432
+ function machineCredentialKey(hostId) {
433
+ return `machine:${hostId}`;
434
+ }
435
+ /** A machine name from a free-form one (a server's display name): the valid characters kept, the rest dashed. */
436
+ function toMachineName(raw) {
437
+ return raw.trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "machine";
438
+ }
439
+ /** A machine-qualified environment kind: `<base>@<machine name>`. */
440
+ function machineKind(base, name) {
441
+ return `${base}@${name}`;
442
+ }
443
+ /** `{ base, name }` of a machine-qualified kind, `null` for any other. */
444
+ function parseMachineKind(kind) {
445
+ const match = /^(local|worktree)@(.+)$/.exec(kind);
446
+ return match ? {
447
+ base: match[1],
448
+ name: match[2]
449
+ } : null;
450
+ }
451
+ /** A machine name: one path-safe segment, so `local@<name>` is unambiguous. */
452
+ function isValidMachineName(name) {
453
+ return /^[\w.-]{1,64}$/.test(name);
454
+ }
455
+ //#endregion
456
+ //#region src/chat/feature-gate.ts
457
+ /** `process.env` when there is one; a renderer has no `process`. */
458
+ function ambientEnv() {
459
+ return globalThis.process?.env ?? {};
460
+ }
461
+ function defineFeatureGate({ setting, killSwitchEnv, flag }) {
462
+ const isKilled = (env = ambientEnv()) => env[killSwitchEnv] === "1";
463
+ return {
464
+ setting,
465
+ killSwitchEnv,
466
+ flag,
467
+ isKilled,
468
+ isEnabled: (settings, env) => settings[setting] === true && !isKilled(env)
469
+ };
470
+ }
471
+ /**
472
+ * The shared daemon runtime. `ZIDANE_NO_DAEMON=1` is set by
473
+ * `zidane --no-daemon`.
474
+ */
475
+ const daemonGate = defineFeatureGate({
476
+ setting: "useDaemon",
477
+ killSwitchEnv: "ZIDANE_NO_DAEMON",
478
+ flag: "--no-daemon"
479
+ });
480
+ /**
481
+ * Remote hosts (zidane-server). The setting ("Remote hosts")
482
+ * gates the saved servers: the Hosts settings, the per-host session groups
483
+ * and picking a host for a new session. Connecting through an explicit `--host` /
484
+ * `ZIDANE_HOST_URL` is its own opt-in, but `ZIDANE_NO_REMOTE=1` still wins
485
+ * over it (`src/tui/host-target.ts`). `ZIDANE_NO_REMOTE=1` is set by
486
+ * `zidane --no-remote`.
487
+ */
488
+ const remoteHostsGate = defineFeatureGate({
489
+ setting: "remoteHosts",
490
+ killSwitchEnv: "ZIDANE_NO_REMOTE",
491
+ flag: "--no-remote"
492
+ });
493
+ /** Every gate, so a client can report which kill switches are engaged. */
494
+ const FEATURE_GATES = [daemonGate, remoteHostsGate];
495
+ /**
496
+ * The kill-switch env vars engaged in `env`, sorted. The GUI main process
497
+ * hands this to its renderer (which has no `process.env`) so settings rows
498
+ * can say why they are inert.
499
+ */
500
+ function engagedKillSwitches(env = ambientEnv()) {
501
+ return FEATURE_GATES.filter((g) => g.isKilled(env)).map((g) => g.killSwitchEnv).sort();
502
+ }
503
+ /** `name` is set to `'1'` in `env`: the kill-switch test, for a row that only knows the variable. */
504
+ function isKillSwitchEngaged(name, env = ambientEnv()) {
505
+ return env[name] === "1";
506
+ }
507
+ //#endregion
508
+ //#region src/chat/footer-chips.ts
509
+ /**
510
+ * Abbreviate a re-billed token count: `1200` → `1.2k`, `800` → `800`.
511
+ * Thousands only — a cache prefix large enough to need `M` would be past any
512
+ * provider's window.
513
+ */
514
+ function formatTokens(tokens) {
515
+ return tokens >= 1e3 ? `${(tokens / 1e3).toFixed(1)}k` : String(tokens);
516
+ }
517
+ /**
518
+ * Build the chip for a cache break.
519
+ *
520
+ * `costUsd` is OPTIONAL and omitted entirely on a rate miss — never rendered
521
+ * as `$0.00`. A fabricated zero would read as "this was free", which is the
522
+ * opposite of what the chip exists to say. When present it is prefixed `~`
523
+ * because it is an estimate (registry rates × re-billed prefix), and clamped
524
+ * to `<0.01` rather than rounding a real charge down to nothing.
525
+ */
526
+ function cacheBreakChip(info) {
527
+ const rebilled = formatTokens(info.rebilledTokens);
528
+ const cost = info.costUsd !== void 0 ? ` · ~$${info.costUsd < .01 ? "<0.01" : info.costUsd.toFixed(2)}` : "";
529
+ if (info.changed.length === 0) return {
530
+ tone: "dim",
531
+ glyph: "⚡",
532
+ label: `cache expired · +${rebilled} tok${cost}`
533
+ };
534
+ return {
535
+ tone: "warn",
536
+ glyph: "⚡",
537
+ label: `cache break · ${info.changed.join(",")} · +${rebilled} tok${cost}`
538
+ };
539
+ }
540
+ /**
541
+ * Transcript-row text for a persisted cache-break notice. Multi-line, one
542
+ * fact per line, so both hosts can show a short headline and fold or list
543
+ * the rest:
544
+ *
545
+ * cache break ↑116.9k tokens ~$0.48
546
+ * messagePrefix: message 16 of 41→43 rewritten
547
+ * likely compaction or history trimming
548
+ *
549
+ * The footer chip stays terse; the row carries the culprit so a break is
550
+ * diagnosable from the transcript. Only the headline on a TTL / eviction
551
+ * break (nothing was done wrong).
552
+ */
553
+ function cacheBreakNoticeText(info) {
554
+ const rebilled = formatTokens(info.rebilledTokens);
555
+ const cost = info.costUsd !== void 0 ? ` ~$${info.costUsd < .01 ? "<0.01" : info.costUsd.toFixed(2)}` : "";
556
+ return [`${info.changed.length === 0 ? "cache expired" : "cache break"} ↑${rebilled} tokens${cost}`, ...info.changed.flatMap((dim) => {
557
+ const detail = info.details[dim];
558
+ if (typeof detail !== "string" || detail.length === 0) return [`${dim} changed`];
559
+ const [first, ...rest] = detail.split("\n");
560
+ return [`${dim}: ${first}`, ...rest];
561
+ })].join("\n");
562
+ }
563
+ /**
564
+ * Provider-retry label — `retrying 2/5`, or `retrying 2/5 · child-3` when the
565
+ * stalled stream belongs to a subagent rather than the parent.
566
+ *
567
+ * Naming the source matters: with subagents in flight, an unattributed
568
+ * "retrying" leaves the user unable to tell whether their own turn is stuck or
569
+ * a delegated worker is.
570
+ */
571
+ function retryLabel(info) {
572
+ const suffix = info.source === "parent" ? "" : ` · ${info.source}`;
573
+ return `retrying ${info.attempt}/${info.max}${suffix}`;
574
+ }
575
+ /**
576
+ * Footer label while a compaction summary is being generated:
577
+ * `Compacting...`, then `~12k` once the loop reports a token estimate, then
578
+ * ` · 8s` once a second has passed. Same rule on every surface so a
579
+ * minutes-long compaction of a large transcript reads as progress — not a
580
+ * hang — identically in the TUI and the GUI.
581
+ *
582
+ * `now` is injectable so callers driving a 1 Hz tick don't have to re-read
583
+ * the clock, and so tests are deterministic.
584
+ */
585
+ /**
586
+ * The live run readout: `1m 15s · ↓1.2k · 41 tok/s`. Time runs from the
587
+ * run's start, the count covers the whole run (settled turns plus the one
588
+ * streaming), and the rate is the honest windowed figure from
589
+ * `stepStreamRate` (`null` until it has one). `↓~` marks a count that is
590
+ * still the character heuristic. A tool-only stretch shows just the time.
591
+ *
592
+ * `detail` drops the least useful part first when space runs out:
593
+ * `'full'` → `'count'` (no rate) → `'time'`.
594
+ */
595
+ function runStatusLabel(run, detail = "full") {
596
+ const parts = [formatElapsed(Math.max(0, Math.floor(run.elapsedSec)))];
597
+ if (detail !== "time" && run.outputTokens > 0) parts.push(`↓${run.estimated ? "~" : ""}${fmtTokens(run.outputTokens)}`);
598
+ if (detail === "full" && run.outputTokens > 0 && run.rate !== null && run.rate > 0) parts.push(`${run.rate} tok/s`);
599
+ return parts.join(" · ");
600
+ }
601
+ /**
602
+ * Live counter for one streaming turn: `↓1.2k`, `↓~1.2k` for the character
603
+ * heuristic, `↓1.2k · 41 tok/s` when an honest rate is passed in (see
604
+ * `stepStreamRate`; dividing tokens by time since the request counted the
605
+ * wait for the first token and the estimate correction). Empty until the
606
+ * first token.
607
+ */
608
+ function streamingLabel(progress, opts = {}) {
609
+ if (!progress || progress.outputTokens <= 0) return "";
610
+ const count = `↓${progress.estimated ? "~" : ""}${fmtTokens(progress.outputTokens)}`;
611
+ return opts.rate ? `${count} · ${opts.rate} tok/s` : count;
612
+ }
613
+ /**
614
+ * Session cost while a turn streams: settled figure plus the running estimate
615
+ * for the in-flight turn. `estimated` tells the surface to render it dimmed
616
+ * (the number will move, then snap to the settled value at turn end). `null`
617
+ * when there is nothing to show — never `$0.00`.
618
+ */
619
+ function liveSessionCost(settledUsd, progress) {
620
+ const live = progress?.estCostUsd ?? 0;
621
+ const total = settledUsd + live;
622
+ if (total <= 0) return null;
623
+ return {
624
+ usd: total,
625
+ estimated: live > 0
626
+ };
627
+ }
628
+ function compactingLabel(estTokens, startedAt, now = Date.now()) {
629
+ const base = "Compacting...";
630
+ const elapsedMs = typeof startedAt === "number" ? now - startedAt : 0;
631
+ const elapsed = elapsedMs >= 1e3 ? ` · ${formatDuration(elapsedMs)}` : "";
632
+ return typeof estTokens === "number" && estTokens > 0 ? `${base} ~${fmtTokens(estTokens)}${elapsed}` : `${base}${elapsed}`;
633
+ }
634
+ //#endregion
635
+ //#region src/chat/completion-chats.ts
636
+ const CHATS_TRIGGER = "#";
637
+ const DEFAULT_RESULT_LIMIT$1 = 50;
638
+ const MIN_TOKEN_LENGTH = 6;
639
+ function normalizeId(id) {
640
+ return id.replace(/-/g, "").toLowerCase();
641
+ }
642
+ function projectLabel(projectRoot) {
643
+ if (!projectRoot) return "";
644
+ const parts = projectRoot.split(/[\\/]/).filter(Boolean);
645
+ return parts[parts.length - 1] ?? projectRoot;
646
+ }
647
+ /**
648
+ * Per-catalog memo for `tokenFor`, keyed on catalog reference identity.
649
+ * Token derivation is O(catalog) per id, and both `suggest()` (every
650
+ * keystroke) and `parseReferences()` (every submit/highlight pass) need
651
+ * a token for EVERY entry — quadratic over catalogs that can reach
652
+ * thousands of sessions. The catalog getters return a stable array until
653
+ * a refresh swaps it, so a WeakMap invalidates exactly when it should.
654
+ */
655
+ const tokensByCatalog = /* @__PURE__ */ new WeakMap();
656
+ function catalogTokens(catalog) {
657
+ let cached = tokensByCatalog.get(catalog);
658
+ if (!cached) {
659
+ cached = {
660
+ normalizedIds: catalog.map((entry) => normalizeId(entry.id)),
661
+ tokens: /* @__PURE__ */ new Map()
662
+ };
663
+ tokensByCatalog.set(catalog, cached);
664
+ }
665
+ return cached;
666
+ }
667
+ function tokenFor(id, catalog) {
668
+ const { normalizedIds, tokens } = catalogTokens(catalog);
669
+ const known = tokens.get(id);
670
+ if (known !== void 0) return known;
671
+ const normalized = normalizeId(id);
672
+ let result;
673
+ for (let len = Math.min(MIN_TOKEN_LENGTH, normalized.length); len <= normalized.length; len++) {
674
+ const token = normalized.slice(0, len);
675
+ if (normalizedIds.filter((candidate) => candidate.startsWith(token)).length === 1) {
676
+ result = token;
677
+ break;
678
+ }
679
+ }
680
+ result ??= normalized || shortId(id);
681
+ tokens.set(id, result);
682
+ return result;
683
+ }
684
+ function stripTrailingPunctuation(candidate) {
685
+ return candidate.replace(/[.,;:)\]}!?]+$/, "");
686
+ }
687
+ function descriptionFor(entry, token, scope) {
688
+ const bits = [
689
+ `#${token}`,
690
+ ageString(entry.updatedAt),
691
+ `${entry.userMessageCount} msg${entry.userMessageCount === 1 ? "" : "s"}`
692
+ ];
693
+ if ((entry.artifactCount ?? 0) > 0) bits.push(`${entry.artifactCount} artifact${entry.artifactCount === 1 ? "" : "s"}`);
694
+ const project = projectLabel(entry.projectRoot);
695
+ if (scope === "all" && project) bits.push(project);
696
+ return bits.join(" · ");
697
+ }
698
+ function scoreChats(catalog, tokenCatalog, query, limit, scope) {
699
+ const q = query.trim().toLowerCase();
700
+ const rows = catalog.map((entry) => ({
701
+ entry,
702
+ token: tokenFor(entry.id, tokenCatalog),
703
+ project: projectLabel(entry.projectRoot)
704
+ }));
705
+ const fuzzyHits = q.length > 0 ? new Set(new Fzf(rows, {
706
+ selector: (r) => `${r.entry.title} ${r.token} ${r.project}`,
707
+ casing: "case-insensitive"
708
+ }).find(q).map((r) => r.item)) : null;
709
+ const scored = [];
710
+ for (const row of rows) {
711
+ const title = row.entry.title.toLowerCase();
712
+ const token = row.token.toLowerCase();
713
+ const project = row.project.toLowerCase();
714
+ if (q.length === 0) {
715
+ scored.push({
716
+ entry: row.entry,
717
+ token: row.token,
718
+ rank: 5
719
+ });
720
+ continue;
721
+ }
722
+ if (token.startsWith(q)) {
723
+ scored.push({
724
+ entry: row.entry,
725
+ token: row.token,
726
+ rank: 0
727
+ });
728
+ continue;
729
+ }
730
+ if (title.startsWith(q)) {
731
+ scored.push({
732
+ entry: row.entry,
733
+ token: row.token,
734
+ rank: 1
735
+ });
736
+ continue;
737
+ }
738
+ if (title.includes(q)) {
739
+ scored.push({
740
+ entry: row.entry,
741
+ token: row.token,
742
+ rank: 2
743
+ });
744
+ continue;
745
+ }
746
+ if (project.includes(q)) {
747
+ scored.push({
748
+ entry: row.entry,
749
+ token: row.token,
750
+ rank: 3
751
+ });
752
+ continue;
753
+ }
754
+ if (fuzzyHits?.has(row)) scored.push({
755
+ entry: row.entry,
756
+ token: row.token,
757
+ rank: 4
758
+ });
759
+ }
760
+ scored.sort((a, b) => {
761
+ if (a.rank !== b.rank) return a.rank - b.rank;
762
+ return b.entry.updatedAt - a.entry.updatedAt;
763
+ });
764
+ return scored.slice(0, limit).map(({ entry, token }) => ({
765
+ id: token,
766
+ label: entry.title,
767
+ description: descriptionFor(entry, token, scope),
768
+ insertText: `#${token} `,
769
+ data: {
770
+ entry,
771
+ token
772
+ }
773
+ }));
774
+ }
775
+ function createChatsCompletionProvider(opts) {
776
+ const limit = opts.limit ?? DEFAULT_RESULT_LIMIT$1;
777
+ const scope = () => opts.getScope?.() ?? "project";
778
+ return {
779
+ id: "chats",
780
+ trigger: "#",
781
+ label: "Chats",
782
+ suggest(query) {
783
+ const current = opts.getCatalog();
784
+ const tokenCatalog = opts.getReferenceCatalog?.() ?? current;
785
+ const effectiveTokenCatalog = tokenCatalog.length > 0 ? tokenCatalog : current;
786
+ if (current.length > 0) return scoreChats(current, effectiveTokenCatalog, query, limit, scope());
787
+ if (opts.ensureCatalog) return opts.ensureCatalog().then((loaded) => {
788
+ const refreshed = opts.getReferenceCatalog?.() ?? loaded;
789
+ return scoreChats(loaded, refreshed.length > 0 ? refreshed : loaded, query, limit, scope());
790
+ });
791
+ return [];
792
+ },
793
+ parseReferences(text, _ctx) {
794
+ const catalog = opts.getReferenceCatalog?.() ?? opts.getCatalog();
795
+ if (catalog.length === 0) return [];
796
+ const byToken = /* @__PURE__ */ new Map();
797
+ for (const entry of catalog) {
798
+ const token = tokenFor(entry.id, catalog);
799
+ byToken.set(token, {
800
+ entry,
801
+ token
802
+ });
803
+ }
804
+ const refs = [];
805
+ for (const m of text.matchAll(/(^|\s)#(\S+)/g)) {
806
+ const candidate = stripTrailingPunctuation(m[2]);
807
+ const data = byToken.get(candidate.toLowerCase());
808
+ if (!data) continue;
809
+ const start = m.index + m[1].length;
810
+ refs.push({
811
+ providerId: "chats",
812
+ start,
813
+ end: start + 1 + candidate.length,
814
+ itemId: data.entry.id,
815
+ data
816
+ });
817
+ }
818
+ return refs;
819
+ }
820
+ };
1790
821
  }
1791
- /**
1792
- * Deduplicated, in-order list of **parent-conversation** turn ids that appear
1793
- * in a rendered transcript — the navigation index for the TUI's select-turn
1794
- * mode. Three classes of turns are deliberately skipped:
1795
- *
1796
- * - **Subagent turns** (`childId` set). Nested execution detail; the
1797
- * user's mental model of a "message" is the conversational exchange,
1798
- * not each spawn turn. Also filtered out by `isVisible` under
1799
- * `hideSubagentOutput: true` — selecting them would highlight nothing.
1800
- * - **Result-only turns** — see {@link turnSelectionOwnership}. These get
1801
- * coalesced into the assistant turn that emitted their tool_calls.
1802
- * - **Settings-hidden turns** (when `settings` is supplied). A turn whose
1803
- * every event fails {@link isVisible} would render no rows — landing
1804
- * the cursor there hides it from the user entirely. The check is opt-
1805
- * in so SDK callers without a Settings object keep the legacy
1806
- * "everything visible" behavior.
1807
- *
1808
- * Synthetic events (separator, spawn-start, spawn-end) have no `turnId` and
1809
- * are skipped naturally.
1810
- */
1811
- function selectableTurnIds(events, settings, visible) {
1812
- const ownership = turnSelectionOwnership(events);
1813
- const visibleCount = settings ? /* @__PURE__ */ new Map() : null;
1814
- if (settings && visibleCount) for (const e of events) {
1815
- if (!e.turnId || e.childId) continue;
1816
- if (!(visible ? visible(e) : isVisible(e, settings))) continue;
1817
- visibleCount.set(e.turnId, (visibleCount.get(e.turnId) ?? 0) + 1);
822
+ function uniqueChatIdsFromReferences(references) {
823
+ const out = [];
824
+ const seen = /* @__PURE__ */ new Set();
825
+ for (const ref of references) {
826
+ if (ref.providerId !== "chats") continue;
827
+ if (seen.has(ref.itemId)) continue;
828
+ seen.add(ref.itemId);
829
+ out.push(ref.itemId);
1818
830
  }
831
+ return out;
832
+ }
833
+ function chatReferenceTokensFromText(text) {
834
+ const out = [];
1819
835
  const seen = /* @__PURE__ */ new Set();
1820
- const ordered = [];
1821
- for (const e of events) {
1822
- if (!e.turnId) continue;
1823
- if (e.childId) continue;
1824
- if (seen.has(e.turnId)) continue;
1825
- if (ownership.has(e.turnId)) continue;
1826
- if (visibleCount && (visibleCount.get(e.turnId) ?? 0) === 0) continue;
1827
- seen.add(e.turnId);
1828
- ordered.push(e.turnId);
836
+ for (const m of text.matchAll(/(^|\s)#(\S+)/g)) {
837
+ const token = stripTrailingPunctuation(m[2]).toLowerCase();
838
+ if (!token || seen.has(token)) continue;
839
+ seen.add(token);
840
+ out.push(token);
1829
841
  }
1830
- return ordered;
842
+ return out;
1831
843
  }
1832
844
  //#endregion
845
+ //#region src/chat/browser/protocol.ts
846
+ const BROWSER_OPS = [
847
+ "open",
848
+ "navigate",
849
+ "snapshot",
850
+ "screenshot",
851
+ "click",
852
+ "type",
853
+ "press",
854
+ "scroll",
855
+ "wait_for",
856
+ "evaluate",
857
+ "console"
858
+ ];
859
+ var BrowserOpError = class extends Error {
860
+ code;
861
+ constructor(code, message) {
862
+ super(message);
863
+ this.code = code;
864
+ this.name = "BrowserOpError";
865
+ }
866
+ };
867
+ /** Default and ceiling for an action's own wait (element to become actionable, text to appear). */
868
+ const BROWSER_ACTION_TIMEOUT_MS = 5e3;
869
+ const BROWSER_WAIT_TIMEOUT_MS = 1e4;
870
+ const BROWSER_MAX_WAIT_MS = 3e4;
871
+ /** How long the host waits for a page to mount and register before `open` gives up. */
872
+ const BROWSER_OPEN_MOUNT_MS = 15e3;
873
+ /** Transport budget on top of the action's own wait. */
874
+ const BROWSER_REQUEST_SLACK_MS = 5e3;
875
+ const BROWSER_EVALUATE_MAX_BYTES = 65536;
876
+ const BROWSER_OUTLINE_MAX_ELEMENTS = 250;
877
+ const BROWSER_TEXT_MAX_CHARS = 8e3;
878
+ //#endregion
879
+ //#region src/chat/terminal/protocol.ts
880
+ /** Characters a client may be behind before the host stops sending to it. */
881
+ const TERMINAL_OUTPUT_WINDOW = 262144;
882
+ /** History kept per terminal for new views (characters). */
883
+ const TERMINAL_HISTORY_MAX_CHARS = 4194304;
884
+ /** Plain-text lines kept per terminal for the agent. */
885
+ const TERMINAL_TEXT_MAX_LINES = 5e3;
886
+ /** Most the agent gets from one read (characters). */
887
+ const TERMINAL_READ_MAX_CHARS = 16384;
888
+ /** Terminals a session may have open at once. */
889
+ const TERMINAL_MAX_PER_SESSION = 12;
890
+ var TerminalError = class extends Error {
891
+ code;
892
+ constructor(code, message) {
893
+ super(message);
894
+ this.code = code;
895
+ this.name = "TerminalError";
896
+ }
897
+ };
898
+ //#endregion
1833
899
  //#region src/chat/color-gradient.ts
1834
900
  /** Parse `#rrggbb` (case-insensitive) into `[r, g, b]` 0–255 integers. */
1835
901
  function parseHex(hex) {
@@ -2349,326 +1415,43 @@ function createSkillsCompletionProvider(opts) {
2349
1415
  });
2350
1416
  }
2351
1417
  return refs;
2352
- }
2353
- };
2354
- }
2355
- /**
2356
- * Walk a parsed prompt for skill references and return the deduplicated
2357
- * list of skill names — input to `agent.activateSkill(name)` calls on
2358
- * submit.
2359
- */
2360
- function uniqueSkillNamesFromReferences(references) {
2361
- const out = [];
2362
- const seen = /* @__PURE__ */ new Set();
2363
- for (const ref of references) {
2364
- if (ref.providerId !== "skills") continue;
2365
- if (seen.has(ref.itemId)) continue;
2366
- seen.add(ref.itemId);
2367
- out.push(ref.itemId);
2368
- }
2369
- return out;
2370
- }
2371
- //#endregion
2372
- //#region src/chat/model-chain.ts
2373
- /** Identity of a chain entry — the pair, not object identity. */
2374
- function chainEntryKey(entry) {
2375
- return `${entry.provider}\u0000${entry.model}`;
2376
- }
2377
- /** Toggle `entry`: absent → appended, present → removed. Pure. */
2378
- function toggleKeyed(chain, entry, keyOf) {
2379
- const key = keyOf(entry);
2380
- const idx = chain.findIndex((e) => keyOf(e) === key);
2381
- if (idx === -1) return [...chain, entry];
2382
- return [...chain.slice(0, idx), ...chain.slice(idx + 1)];
2383
- }
2384
- /**
2385
- * Make `entry` the primary (position 0), keeping the rest in order. Inserts
2386
- * when absent, promotes when present; no-op when it already leads.
2387
- *
2388
- * This is `↵` in the picker. It exists because toggling APPENDS: with a chain
2389
- * of `[current]`, toggling a new model produced `[current, new]` — the pick
2390
- * became a fallback and the active model never changed. "Highlight and ↵"
2391
- * means "use this one", so that gesture promotes; building a fallback list is
2392
- * the secondary gesture (`⇥`) and keeps toggle semantics.
2393
- */
2394
- function pickPrimaryKeyed(chain, entry, keyOf) {
2395
- const key = keyOf(entry);
2396
- return [entry, ...chain.filter((e) => keyOf(e) !== key)];
2397
- }
2398
- /** 1-based position of `key` in the chain, or `null` when absent. */
2399
- function positionKeyed(chain, key, keyOf) {
2400
- const idx = chain.findIndex((e) => keyOf(e) === key);
2401
- return idx === -1 ? null : idx + 1;
2402
- }
2403
- /** Same entries in the same order. */
2404
- function chainsEqualKeyed(a, b, keyOf) {
2405
- return a.length === b.length && a.every((e, i) => keyOf(e) === keyOf(b[i]));
2406
- }
2407
- /** Move the entry with `key` by one priority slot; boundary or missing = identity. */
2408
- function reorderKeyed(chain, key, delta, keyOf) {
2409
- const from = chain.findIndex((e) => keyOf(e) === key);
2410
- const to = from + delta;
2411
- if (from < 0 || to < 0 || to >= chain.length) return chain;
2412
- const next = [...chain];
2413
- const [entry] = next.splice(from, 1);
2414
- next.splice(to, 0, entry);
2415
- return next;
2416
- }
2417
- function toggleChainEntry(chain, entry) {
2418
- return toggleKeyed(chain, entry, chainEntryKey);
2419
- }
2420
- function pickChainPrimary(chain, entry) {
2421
- return pickPrimaryKeyed(chain, entry, chainEntryKey);
2422
- }
2423
- /** 1-based chain position of a provider+model pair, or `null` when not in the chain. */
2424
- function chainPositionOf(chain, provider, model) {
2425
- return positionKeyed(chain, chainEntryKey({
2426
- provider,
2427
- model
2428
- }), chainEntryKey);
2429
- }
2430
- function chainsEqual(a, b) {
2431
- return chainsEqualKeyed(a, b, chainEntryKey);
2432
- }
2433
- /**
2434
- * Did the SET of provider keys change between two chains? The agent is
2435
- * built with one provider instance per distinct chain key, so a set
2436
- * change requires a session re-activation (rebuild); a pure model /
2437
- * order change within the same providers does not.
2438
- */
2439
- function providerKeySetChanged(prev, next) {
2440
- const prevKeys = new Set(prev.map((e) => e.provider));
2441
- const nextKeys = new Set(next.map((e) => e.provider));
2442
- if (prevKeys.size !== nextKeys.size) return true;
2443
- for (const k of nextKeys) if (!prevKeys.has(k)) return true;
2444
- return false;
2445
- }
2446
- /**
2447
- * Assemble the `agent.run` `model` option from a chain.
2448
- *
2449
- * - Single-entry chain → the bare model id string (byte-identical to the
2450
- * pre-chain behavior).
2451
- * - Multi-entry chain → pinned `{ provider, model }` targets, where
2452
- * `provider` is the runtime `Provider.name` resolved via
2453
- * `providerNameFor` (chain entries carry chat-level `ProviderKey`s,
2454
- * which need not equal the instance names the agent loop matches on).
2455
- *
2456
- * Throws only on an empty chain — a programmer error, since chains
2457
- * always carry a primary. Stale or unresolvable entries degrade
2458
- * defensively instead (never throw, never produce an entry the agent
2459
- * would reject):
2460
- * - Entries whose key has no resolvable name (provider not constructed
2461
- * into the current agent) are dropped.
2462
- * - If the PRIMARY itself doesn't resolve, or fewer than two entries
2463
- * survive, fall back to the bare primary model string.
2464
- *
2465
- * `contextWindowFor` (optional) stamps each target with its model's
2466
- * context window from the host catalog, so the loop re-bases the
2467
- * auto-compact trigger when a fallback advances onto it (the host's
2468
- * explicit window is sized for the PRIMARY otherwise).
2469
- */
2470
- function buildRunModel(chain, providerNameFor, contextWindowFor) {
2471
- const primary = chain[0];
2472
- if (!primary) throw new Error("buildRunModel: chain must not be empty");
2473
- if (chain.length === 1 || providerNameFor(primary.provider) === void 0) return primary.model;
2474
- const resolved = [];
2475
- for (const entry of chain) {
2476
- const name = providerNameFor(entry.provider);
2477
- if (name === void 0) continue;
2478
- const contextWindow = contextWindowFor?.(entry);
2479
- resolved.push({
2480
- provider: name,
2481
- model: entry.model,
2482
- ...typeof contextWindow === "number" && contextWindow > 0 ? { contextWindow } : {}
2483
- });
2484
- }
2485
- return resolved.length > 1 ? resolved : primary.model;
2486
- }
2487
- /**
2488
- * Validate a persisted chain read back from `state.json`. The state file
2489
- * is user-editable JSON, so the value can be arbitrarily malformed —
2490
- * return only the well-shaped `{ provider, model }` entries (both
2491
- * non-empty strings), or `[]` for anything else. Never throws.
2492
- */
2493
- function sanitizeModelChain(value) {
2494
- if (!Array.isArray(value)) return [];
2495
- const entries = [];
2496
- for (const item of value) {
2497
- if (typeof item !== "object" || item === null || !("provider" in item) || !("model" in item)) continue;
2498
- const { provider, model } = item;
2499
- if (typeof provider === "string" && provider.length > 0 && typeof model === "string" && model.length > 0) entries.push({
2500
- provider,
2501
- model
2502
- });
2503
- }
2504
- return entries;
2505
- }
2506
- /**
2507
- * Restore the fallback tail of a persisted chain for a resolved primary
2508
- * pair. Returns entries 1+ only when the persisted chain's entry 0
2509
- * matches `primary` — a divergent chain belongs to a different selection
2510
- * (e.g. a `--model` launch override) and silently attaching it would
2511
- * surprise. Entries whose provider isn't registered, entries equal to
2512
- * the primary, and exact duplicates drop. Accepts the raw persisted
2513
- * value (`state.json` is user-editable) — shape validation included.
2514
- */
2515
- function restoreChainFallbacks(saved, primary, isRegistered) {
2516
- const chain = sanitizeModelChain(saved);
2517
- const head = chain[0];
2518
- if (chain.length < 2 || !head || head.provider !== primary.provider || head.model !== primary.model) return [];
2519
- return filterChainFallbacks(chain.slice(1), primary, isRegistered);
2520
- }
2521
- /**
2522
- * Filter candidate fallback entries against a primary pair, preserving
2523
- * picked order: entries whose provider `isAvailable` rejects, entries
2524
- * equal to the primary, and exact duplicates drop. Shared by the
2525
- * picker's apply path (live-auth availability) and persisted-chain
2526
- * restore (registry availability).
2527
- */
2528
- function filterChainFallbacks(entries, primary, isAvailable) {
2529
- const fallbacks = [];
2530
- for (const entry of entries) {
2531
- if (!isAvailable(entry.provider)) continue;
2532
- if (entry.provider === primary.provider && entry.model === primary.model) continue;
2533
- if (fallbacks.some((f) => f.provider === entry.provider && f.model === entry.model)) continue;
2534
- fallbacks.push({
2535
- provider: entry.provider,
2536
- model: entry.model
2537
- });
2538
- }
2539
- return fallbacks;
2540
- }
2541
- //#endregion
2542
- //#region src/chat/daemon-health.ts
2543
- const DAEMON_HEALTH_GLYPH = "●";
2544
- const DAEMON_HEALTH_STYLE = {
2545
- connected: {
2546
- tone: "accent",
2547
- label: "daemon connected"
2548
- },
2549
- connecting: {
2550
- tone: "warn",
2551
- label: "connecting to daemon…"
2552
- },
2553
- disconnected: {
2554
- tone: "error",
2555
- label: "daemon unreachable"
2556
- }
2557
- };
2558
- //#endregion
2559
- //#region src/chat/footer-chips.ts
2560
- /**
2561
- * Abbreviate a re-billed token count: `1200` → `1.2k`, `800` → `800`.
2562
- * Thousands only — a cache prefix large enough to need `M` would be past any
2563
- * provider's window.
2564
- */
2565
- function formatTokens(tokens) {
2566
- return tokens >= 1e3 ? `${(tokens / 1e3).toFixed(1)}k` : String(tokens);
2567
- }
2568
- /**
2569
- * Build the chip for a cache break.
2570
- *
2571
- * `costUsd` is OPTIONAL and omitted entirely on a rate miss — never rendered
2572
- * as `$0.00`. A fabricated zero would read as "this was free", which is the
2573
- * opposite of what the chip exists to say. When present it is prefixed `~`
2574
- * because it is an estimate (registry rates × re-billed prefix), and clamped
2575
- * to `<0.01` rather than rounding a real charge down to nothing.
2576
- */
2577
- function cacheBreakChip(info) {
2578
- const rebilled = formatTokens(info.rebilledTokens);
2579
- const cost = info.costUsd !== void 0 ? ` · ~$${info.costUsd < .01 ? "<0.01" : info.costUsd.toFixed(2)}` : "";
2580
- if (info.changed.length === 0) return {
2581
- tone: "dim",
2582
- glyph: "⚡",
2583
- label: `cache expired · +${rebilled} tok${cost}`
2584
- };
2585
- return {
2586
- tone: "warn",
2587
- glyph: "⚡",
2588
- label: `cache break · ${info.changed.join(",")} · +${rebilled} tok${cost}`
2589
- };
2590
- }
2591
- /**
2592
- * Transcript-row text for a persisted cache-break notice. Multi-line, one
2593
- * fact per line, so both hosts can show a short headline and fold or list
2594
- * the rest:
2595
- *
2596
- * cache break ↑116.9k tokens ~$0.48
2597
- * messagePrefix: message 16 of 41→43 rewritten
2598
- * likely compaction or history trimming
2599
- *
2600
- * The footer chip stays terse; the row carries the culprit so a break is
2601
- * diagnosable from the transcript. Only the headline on a TTL / eviction
2602
- * break (nothing was done wrong).
2603
- */
2604
- function cacheBreakNoticeText(info) {
2605
- const rebilled = formatTokens(info.rebilledTokens);
2606
- const cost = info.costUsd !== void 0 ? ` ~$${info.costUsd < .01 ? "<0.01" : info.costUsd.toFixed(2)}` : "";
2607
- return [`${info.changed.length === 0 ? "cache expired" : "cache break"} ↑${rebilled} tokens${cost}`, ...info.changed.flatMap((dim) => {
2608
- const detail = info.details[dim];
2609
- if (typeof detail !== "string" || detail.length === 0) return [`${dim} changed`];
2610
- const [first, ...rest] = detail.split("\n");
2611
- return [`${dim}: ${first}`, ...rest];
2612
- })].join("\n");
2613
- }
2614
- /**
2615
- * Provider-retry label — `retrying 2/5`, or `retrying 2/5 · child-3` when the
2616
- * stalled stream belongs to a subagent rather than the parent.
2617
- *
2618
- * Naming the source matters: with subagents in flight, an unattributed
2619
- * "retrying" leaves the user unable to tell whether their own turn is stuck or
2620
- * a delegated worker is.
2621
- */
2622
- function retryLabel(info) {
2623
- const suffix = info.source === "parent" ? "" : ` · ${info.source}`;
2624
- return `retrying ${info.attempt}/${info.max}${suffix}`;
2625
- }
2626
- /**
2627
- * Footer label while a compaction summary is being generated:
2628
- * `Compacting...`, then `~12k` once the loop reports a token estimate, then
2629
- * ` · 8s` once a second has passed. Same rule on every surface so a
2630
- * minutes-long compaction of a large transcript reads as progress — not a
2631
- * hang — identically in the TUI and the GUI.
2632
- *
2633
- * `now` is injectable so callers driving a 1 Hz tick don't have to re-read
2634
- * the clock, and so tests are deterministic.
2635
- */
2636
- /**
2637
- * Live counter for the parent turn currently streaming: `↓1.2k` when the
2638
- * provider counted the tokens, `↓~1.2k` when it is the character heuristic.
2639
- * Empty until the first token so a tool-only turn shows just the spinner.
2640
- * Tokens-per-second appears after two seconds (`↓1.2k · 41 tok/s`) — earlier
2641
- * the rate is dominated by time-to-first-token and would swing wildly.
2642
- */
2643
- function streamingLabel(progress, opts = {}) {
2644
- if (!progress || progress.outputTokens <= 0) return "";
2645
- const count = `↓${progress.estimated ? "~" : ""}${fmtTokens(progress.outputTokens)}`;
2646
- if (opts.rate === false || progress.elapsedMs < 2e3) return count;
2647
- const perSecond = Math.round(progress.outputTokens / (progress.elapsedMs / 1e3));
2648
- return perSecond > 0 ? `${count} · ${perSecond} tok/s` : count;
2649
- }
2650
- /**
2651
- * Session cost while a turn streams: settled figure plus the running estimate
2652
- * for the in-flight turn. `estimated` tells the surface to render it dimmed
2653
- * (the number will move, then snap to the settled value at turn end). `null`
2654
- * when there is nothing to show — never `$0.00`.
2655
- */
2656
- function liveSessionCost(settledUsd, progress) {
2657
- const live = progress?.estCostUsd ?? 0;
2658
- const total = settledUsd + live;
2659
- if (total <= 0) return null;
2660
- return {
2661
- usd: total,
2662
- estimated: live > 0
2663
- };
2664
- }
2665
- function compactingLabel(estTokens, startedAt, now = Date.now()) {
2666
- const base = "Compacting...";
2667
- const elapsedMs = typeof startedAt === "number" ? now - startedAt : 0;
2668
- const elapsed = elapsedMs >= 1e3 ? ` · ${formatDuration(elapsedMs)}` : "";
2669
- return typeof estTokens === "number" && estTokens > 0 ? `${base} ~${fmtTokens(estTokens)}${elapsed}` : `${base}${elapsed}`;
1418
+ }
1419
+ };
1420
+ }
1421
+ /**
1422
+ * Walk a parsed prompt for skill references and return the deduplicated
1423
+ * list of skill names — input to `agent.activateSkill(name)` calls on
1424
+ * submit.
1425
+ */
1426
+ function uniqueSkillNamesFromReferences(references) {
1427
+ const out = [];
1428
+ const seen = /* @__PURE__ */ new Set();
1429
+ for (const ref of references) {
1430
+ if (ref.providerId !== "skills") continue;
1431
+ if (seen.has(ref.itemId)) continue;
1432
+ seen.add(ref.itemId);
1433
+ out.push(ref.itemId);
1434
+ }
1435
+ return out;
2670
1436
  }
2671
1437
  //#endregion
1438
+ //#region src/chat/daemon-health.ts
1439
+ const DAEMON_HEALTH_GLYPH = "●";
1440
+ const DAEMON_HEALTH_STYLE = {
1441
+ connected: {
1442
+ tone: "accent",
1443
+ label: "daemon connected"
1444
+ },
1445
+ connecting: {
1446
+ tone: "warn",
1447
+ label: "connecting to daemon…"
1448
+ },
1449
+ disconnected: {
1450
+ tone: "error",
1451
+ label: "daemon unreachable"
1452
+ }
1453
+ };
1454
+ //#endregion
2672
1455
  //#region src/chat/markdown-segments.ts
2673
1456
  /**
2674
1457
  * Split markdown text into alternating prose / fenced-code segments.
@@ -3058,6 +1841,160 @@ function isSpaceOrBreak(ch) {
3058
1841
  return ch === "\n" || ch === "\r" || ch === " " || ch === " ";
3059
1842
  }
3060
1843
  //#endregion
1844
+ //#region src/chat/mcp-allowlist.ts
1845
+ /**
1846
+ * Edits to the `enabledMcps` allowlist. Without an explicit list every
1847
+ * server is on, so there is nothing to change. Servers that merely appear
1848
+ * in a config file stay off once a list exists (they don't opt in
1849
+ * silently); a server the user adds in Settings is turned on with it.
1850
+ */
1851
+ /** The list with `name` turned on, or `undefined` when it already is. */
1852
+ function enabledMcpsWith(list, name) {
1853
+ return list === void 0 || list.includes(name) ? void 0 : [...list, name];
1854
+ }
1855
+ /** The list without `name`, or `undefined` when it isn't listed. */
1856
+ function enabledMcpsWithout(list, name) {
1857
+ return list?.includes(name) ? list.filter((n) => n !== name) : void 0;
1858
+ }
1859
+ //#endregion
1860
+ //#region src/chat/mcp-auth-state.ts
1861
+ /**
1862
+ * The status words both apps show for a server, so the desktop app's badge
1863
+ * and the TUI's read the same. `idle` says nothing.
1864
+ */
1865
+ const MCP_STATUS_LABEL = {
1866
+ "authed": "Signed in",
1867
+ "needs-auth": "Needs sign-in",
1868
+ "authorizing": "Signing in…",
1869
+ "error": "Sign-in failed",
1870
+ "bootstrap-error": "Couldn't connect",
1871
+ "key-refused": "Key refused"
1872
+ };
1873
+ /**
1874
+ * Apply one event to the state map. Pure, immutable — returns a new map
1875
+ * with only the affected key reassigned.
1876
+ *
1877
+ * State transitions are simple "last write wins"; we don't gate (e.g.) a
1878
+ * `login:start` from overriding `authorizing` because the loop layer
1879
+ * never doubles those up — interactive login is serialized in the TUI.
1880
+ */
1881
+ function reduceMcpAuth(state, event) {
1882
+ const next = (status) => ({
1883
+ ...state,
1884
+ [event.name]: status
1885
+ });
1886
+ switch (event.type) {
1887
+ case "auth-required": return next({
1888
+ kind: "needs-auth",
1889
+ reason: event.reason
1890
+ });
1891
+ case "auth-url": return next({
1892
+ kind: "authorizing",
1893
+ url: event.url
1894
+ });
1895
+ case "auth-success": return next({ kind: "authed" });
1896
+ case "auth-error": return next({
1897
+ kind: "error",
1898
+ error: event.error
1899
+ });
1900
+ case "bootstrap-error": return next({
1901
+ kind: "bootstrap-error",
1902
+ error: event.error
1903
+ });
1904
+ case "key-refused": return next({
1905
+ kind: "key-refused",
1906
+ error: event.error
1907
+ });
1908
+ case "connected":
1909
+ if (state[event.name]?.kind === "needs-auth" && state[event.name].reason === "refused") {
1910
+ const { [event.name]: _, ...rest } = state;
1911
+ return rest;
1912
+ }
1913
+ if (state[event.name]?.kind === "authorizing" || state[event.name]?.kind === "needs-auth" || state[event.name]?.kind === "authed") return next({ kind: "authed" });
1914
+ if (state[event.name]?.kind === "bootstrap-error" || state[event.name]?.kind === "key-refused") {
1915
+ const { [event.name]: _, ...rest } = state;
1916
+ return rest;
1917
+ }
1918
+ return state;
1919
+ case "login:start": return next({ kind: "authorizing" });
1920
+ case "login:cancel": return next({
1921
+ kind: "needs-auth",
1922
+ reason: "no-tokens"
1923
+ });
1924
+ case "logout":
1925
+ case "reset": {
1926
+ const { [event.name]: _, ...rest } = state;
1927
+ return rest;
1928
+ }
1929
+ }
1930
+ }
1931
+ /**
1932
+ * The status a failed connection leaves, from `classifyMcpTest`'s answer
1933
+ * (never called with `ok`: a connection that worked leaves no failure).
1934
+ * A server that asked for OAuth keeps "sign-in failed" for an unexplained
1935
+ * failure (its tokens are the likely cause, and sign-in stays offered).
1936
+ */
1937
+ function mcpFailureEvent(name, outcome, oauthTagged) {
1938
+ if (outcome.kind === "needs-auth") return {
1939
+ type: "auth-required",
1940
+ name,
1941
+ reason: "refused"
1942
+ };
1943
+ if (outcome.kind === "token-rejected") return {
1944
+ type: "key-refused",
1945
+ name,
1946
+ error: outcome.message ?? ""
1947
+ };
1948
+ return oauthTagged ? {
1949
+ type: "auth-error",
1950
+ name,
1951
+ error: outcome.message ?? ""
1952
+ } : {
1953
+ type: "bootstrap-error",
1954
+ name,
1955
+ error: outcome.message ?? ""
1956
+ };
1957
+ }
1958
+ /** Convenience getter — treats `undefined` as `idle`. */
1959
+ function getMcpAuthStatus(state, name) {
1960
+ return state[name] ?? { kind: "idle" };
1961
+ }
1962
+ //#endregion
1963
+ //#region src/chat/mcp-test-outcome.ts
1964
+ /** What to do about a refused key, under the "Key refused" headline. */
1965
+ const MCP_KEY_REFUSED_HINT = "Check the headers in mcps.json.";
1966
+ /** A refusal for want of credentials: HTTP 401 / 403, however the SDK words it. */
1967
+ function isMcpAuthRefusal(error) {
1968
+ const code = error?.code;
1969
+ if (code === 401 || code === 403) return true;
1970
+ const message = error instanceof Error ? error.message : String(error ?? "");
1971
+ return /\b(?:401|403)\b|unauthori[sz]ed|forbidden|invalid_token/i.test(message);
1972
+ }
1973
+ /** The first line of a failure, never empty. */
1974
+ function mcpFailureLine(error) {
1975
+ const code = error?.code;
1976
+ return (error instanceof Error && error.message ? error.message : code !== void 0 ? `The server answered ${String(code)}.` : typeof error === "string" ? error : "").split("\n")[0]?.trim() || "Couldn't connect.";
1977
+ }
1978
+ function classifyMcpTest(config, result) {
1979
+ const http = config.transport !== "stdio";
1980
+ const ownKey = Object.keys(config.headers ?? {}).length > 0;
1981
+ if (http && (result.authRequired || result.error !== void 0 && isMcpAuthRefusal(result.error))) return ownKey ? {
1982
+ kind: "token-rejected",
1983
+ message: MCP_KEY_REFUSED_HINT
1984
+ } : {
1985
+ kind: "needs-auth",
1986
+ message: "Sign in to connect."
1987
+ };
1988
+ if (result.error !== void 0) return {
1989
+ kind: "error",
1990
+ message: mcpFailureLine(result.error)
1991
+ };
1992
+ return {
1993
+ kind: "ok",
1994
+ tools: result.tools ?? 0
1995
+ };
1996
+ }
1997
+ //#endregion
3061
1998
  //#region src/chat/model-catalog.ts
3062
1999
  /**
3063
2000
  * Build the unified catalog from a list of available providers.
@@ -3163,24 +2100,60 @@ function indexOfEntry(catalog, target) {
3163
2100
  * provider name.
3164
2101
  */
3165
2102
  function parseCustomModelQuery(query, providers, fallbackProviderKey) {
2103
+ const { modelId, named } = splitProviderPrefix(query, providers);
2104
+ const provider = named ?? providers.find((p) => p.key === fallbackProviderKey);
2105
+ if (!provider || !modelId) return null;
2106
+ return {
2107
+ providerKey: provider.key,
2108
+ providerLabel: provider.label,
2109
+ modelId
2110
+ };
2111
+ }
2112
+ /**
2113
+ * Every route for a typed model id, one per provider: the provider the query
2114
+ * names first (`xai grok-9`), then `fallbackProviderKey` (the current pick),
2115
+ * then the rest in the given order. Lets a picker offer "use this id via…"
2116
+ * for each signed-in provider instead of guessing one. Empty when the query
2117
+ * names no model id.
2118
+ */
2119
+ function customModelTargets(query, providers, fallbackProviderKey) {
2120
+ const { modelId, named } = splitProviderPrefix(query, providers);
2121
+ if (!modelId) return [];
2122
+ const rank = (p) => p.key === named?.key ? 0 : p.key === fallbackProviderKey ? 1 : 2;
2123
+ return providers.map((p, i) => ({
2124
+ p,
2125
+ i
2126
+ })).sort((a, b) => rank(a.p) - rank(b.p) || a.i - b.i).map(({ p }) => ({
2127
+ providerKey: p.key,
2128
+ providerLabel: p.label,
2129
+ modelId,
2130
+ ...p.key === named?.key ? { named: true } : {}
2131
+ }));
2132
+ }
2133
+ /**
2134
+ * Split an optional leading provider name off a typed query. Longest prefix
2135
+ * wins, so a multi-word label ("OpenAI Codex gpt-9") beats the bare key it
2136
+ * starts with. The trailing space is required: a query that is only a
2137
+ * provider name has no id in it and reads as an id itself. Only the prefix
2138
+ * is dropped; the rest is kept verbatim, so an id with spaces round-trips.
2139
+ */
2140
+ function splitProviderPrefix(query, providers) {
3166
2141
  const trimmed = query.trim().replace(/\s+/g, " ");
3167
- if (!trimmed) return null;
2142
+ if (!trimmed) return {
2143
+ modelId: "",
2144
+ named: void 0
2145
+ };
3168
2146
  const lower = trimmed.toLowerCase();
3169
- const named = providers.flatMap((p) => [{
2147
+ const match = providers.flatMap((p) => [{
3170
2148
  provider: p,
3171
2149
  token: p.label.toLowerCase()
3172
2150
  }, {
3173
2151
  provider: p,
3174
2152
  token: p.key.toLowerCase()
3175
2153
  }]).filter((candidate) => lower.startsWith(`${candidate.token} `)).sort((a, b) => b.token.length - a.token.length)[0];
3176
- const provider = named?.provider ?? providers.find((p) => p.key === fallbackProviderKey);
3177
- if (!provider) return null;
3178
- const modelId = named ? trimmed.slice(named.token.length).trim() : trimmed;
3179
- if (!modelId) return null;
3180
2154
  return {
3181
- providerKey: provider.key,
3182
- providerLabel: provider.label,
3183
- modelId
2155
+ modelId: match ? trimmed.slice(match.token.length).trim() : trimmed,
2156
+ named: match?.provider
3184
2157
  };
3185
2158
  }
3186
2159
  function buildSearchCorpus(provider, model) {
@@ -3193,57 +2166,159 @@ function buildSearchCorpus(provider, model) {
3193
2166
  ].join(" ").toLowerCase();
3194
2167
  }
3195
2168
  //#endregion
3196
- //#region src/chat/prompt-segments.ts
2169
+ //#region src/chat/model-lineup.ts
2170
+ const DATE_DASHED = /-(\d{4})-(\d{2})-(\d{2})(?=$|[-:])/;
2171
+ const SIZE = /^(?:\d+(?:\.\d+)?[bkm]|a\d+(?:\.\d+)?b|e\d+b|\d+x\d+b)$/;
2172
+ const SNAPSHOT = /^\d{4}$|^\d{6,8}$/;
2173
+ const NUMBER = /^\d+(?:\.\d+)*$/;
2174
+ /** Zero-padded short number (`001`, `05`): a revision or a date part, never a version. */
2175
+ const PADDED = /^0\d{1,2}$/;
2176
+ /** Bedrock's `-v1` / `-v2:0` revision suffix. Only a revision once a version was read. */
2177
+ const REVISION = /^v\d+$/;
2178
+ /** `v4.1`, `k2.5`, `r1`, `o3`, `qwen3.5`: letters then a version. */
2179
+ const LETTERS_VERSION = /^([a-z]+)(\d+(?:\.\d+)*)$/;
2180
+ /** `4o`: a version then letters. */
2181
+ const VERSION_LETTERS = /^(\d+(?:\.\d+)*)([a-z]+)$/;
2182
+ /** Words that mark a pre-release or an alias of the same model, not a different one. */
2183
+ const NOISE = /* @__PURE__ */ new Set([
2184
+ "preview",
2185
+ "latest",
2186
+ "exp",
2187
+ "experimental"
2188
+ ]);
2189
+ function parseModelId(id) {
2190
+ let rest = id.toLowerCase();
2191
+ let variant = "";
2192
+ const colon = rest.indexOf(":");
2193
+ if (colon >= 0) {
2194
+ variant = rest.slice(colon + 1);
2195
+ rest = rest.slice(0, colon);
2196
+ }
2197
+ let vendor = "";
2198
+ const slash = rest.lastIndexOf("/");
2199
+ if (slash >= 0) {
2200
+ vendor = rest.slice(0, slash);
2201
+ rest = rest.slice(slash + 1);
2202
+ }
2203
+ let snapshot = "";
2204
+ const dated = DATE_DASHED.exec(rest);
2205
+ if (dated) {
2206
+ snapshot = `${dated[1]}${dated[2]}${dated[3]}`;
2207
+ rest = rest.slice(0, dated.index) + rest.slice(dated.index + dated[0].length);
2208
+ }
2209
+ const words = [];
2210
+ const version = [];
2211
+ const sizes = [];
2212
+ const snapParts = [];
2213
+ let afterNoise = false;
2214
+ const pushVersion = (v) => {
2215
+ for (const n of v.split(".")) version.push(Number(n));
2216
+ };
2217
+ for (const token of rest.split(/[-_\s]+/).filter(Boolean)) {
2218
+ if (NOISE.has(token)) {
2219
+ afterNoise = true;
2220
+ continue;
2221
+ }
2222
+ if (SIZE.test(token)) {
2223
+ sizes.push(token);
2224
+ continue;
2225
+ }
2226
+ if ((words.length > 0 || version.length > 0) && (SNAPSHOT.test(token) || afterNoise && NUMBER.test(token) || version.length > 0 && (PADDED.test(token) || REVISION.test(token)))) {
2227
+ snapParts.push(token);
2228
+ continue;
2229
+ }
2230
+ if (NUMBER.test(token)) {
2231
+ pushVersion(token);
2232
+ continue;
2233
+ }
2234
+ const lv = LETTERS_VERSION.exec(token);
2235
+ if (lv) {
2236
+ words.push(lv[1]);
2237
+ pushVersion(lv[2]);
2238
+ continue;
2239
+ }
2240
+ const vl = VERSION_LETTERS.exec(token);
2241
+ if (vl) {
2242
+ pushVersion(vl[1]);
2243
+ words.push(vl[2]);
2244
+ continue;
2245
+ }
2246
+ words.push(token);
2247
+ }
2248
+ if (!snapshot && snapParts.length > 0) snapshot = snapParts.length === 2 && /^\d{2}$/.test(snapParts[0]) && /^\d{4}$/.test(snapParts[1]) ? `${snapParts[1]}${snapParts[0]}` : snapParts.join("");
2249
+ return {
2250
+ vendor,
2251
+ family: words.join("-"),
2252
+ version,
2253
+ size: sizes.join("-"),
2254
+ snapshot,
2255
+ variant
2256
+ };
2257
+ }
2258
+ /** Numeric, segment by segment; a longer version wins a tie on the shared prefix (`5.5` > `5`). */
2259
+ function compareModelVersion(a, b) {
2260
+ const n = Math.max(a.length, b.length);
2261
+ for (let i = 0; i < n; i++) {
2262
+ const x = a[i];
2263
+ const y = b[i];
2264
+ if (x === void 0) return -1;
2265
+ if (y === void 0) return 1;
2266
+ if (x !== y) return x - y;
2267
+ }
2268
+ return 0;
2269
+ }
3197
2270
  /**
3198
- * Split a prompt buffer into word-sized atomic segments suitable for a
3199
- * flex-row + flex-wrap renderer (TUI) or a `display: inline` flow with
3200
- * inline-block chips (GUI). Each chip becomes one segment (atomic —
3201
- * never broken across rows); each plain run is split into "word +
3202
- * trailing space" units so wraps land at clean word boundaries.
3203
- *
3204
- * Robust to:
3205
- * - Overlapping refs — sorted by start; later refs that overlap are
3206
- * dropped via the first-wins rule.
3207
- * - Out-of-bounds refs — dropped entirely when `end > text.length` or
3208
- * `start >= text.length`. Partial clipping would silently truncate
3209
- * a chip's label; the caller is in a better position to surface the
3210
- * mismatch (typically a stale `refs` array referencing a previous text).
3211
- * - Whitespace-only plain runs — emitted as their own plain segment
3212
- * so chip-adjacent-to-chip cases keep the original spacing.
3213
- *
3214
- * Word splitter rationale: `\S+\s*` keeps trailing whitespace attached
3215
- * to its preceding word so wrap boundaries land between words (cleanly).
3216
- * A leading-whitespace-only segment is captured by `\s+` so we don't
3217
- * drop it entirely when the plain run starts with a space.
2271
+ * Among ids naming the same model (family, version and size equal), the one
2272
+ * a picker should show: the plain alias over a dated snapshot, the newest
2273
+ * snapshot among snapshots, no routing variant over a variant, then the
2274
+ * shorter id. Negative when `a` is preferred.
3218
2275
  */
3219
- function splitPromptSegments(text, refs) {
3220
- const sorted = [...refs].filter((r) => r.end > r.start && r.start < text.length && r.end <= text.length).sort((a, b) => a.start - b.start);
3221
- const out = [];
3222
- let cursor = 0;
3223
- for (const ref of sorted) {
3224
- if (ref.start < cursor) continue;
3225
- if (ref.start > cursor) {
3226
- const matches = text.slice(cursor, ref.start).match(/\S+\s*|\s+/g) ?? [];
3227
- for (const m of matches) out.push({
3228
- kind: "plain",
3229
- text: m
3230
- });
3231
- }
3232
- out.push({
3233
- kind: "chip",
3234
- text: text.slice(ref.start, ref.end),
3235
- providerId: ref.providerId
3236
- });
3237
- cursor = ref.end;
2276
+ function preferAlias(a, b) {
2277
+ const sa = a.parts.snapshot;
2278
+ const sb = b.parts.snapshot;
2279
+ if (!sa !== !sb) return sa ? 1 : -1;
2280
+ if (sa !== sb) return sa < sb ? 1 : -1;
2281
+ if (!a.parts.variant !== !b.parts.variant) return a.parts.variant ? 1 : -1;
2282
+ return a.id.length - b.id.length || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
2283
+ }
2284
+ const lineupKey = (e) => `${e.providerKey}\u0000${e.modelId}`;
2285
+ /**
2286
+ * Keys (`provider\0model`) of the current models: per provider and family,
2287
+ * the newest version, one entry per size. `keep` entries (the active model,
2288
+ * fallback chain, off-catalog ids) are always included.
2289
+ */
2290
+ function currentModelKeys(entries, keep = []) {
2291
+ const groups = /* @__PURE__ */ new Map();
2292
+ for (const entry of entries) {
2293
+ const parts = parseModelId(entry.modelId);
2294
+ const group = `${entry.providerKey}\u0000${parts.vendor}\u0000${parts.family}`;
2295
+ const list = groups.get(group);
2296
+ const item = {
2297
+ id: entry.modelId,
2298
+ key: lineupKey(entry),
2299
+ parts
2300
+ };
2301
+ if (list) list.push(item);
2302
+ else groups.set(group, [item]);
3238
2303
  }
3239
- if (cursor < text.length) {
3240
- const matches = text.slice(cursor).match(/\S+\s*|\s+/g) ?? [];
3241
- for (const m of matches) out.push({
3242
- kind: "plain",
3243
- text: m
3244
- });
2304
+ const kept = new Set(keep.map(lineupKey));
2305
+ for (const list of groups.values()) {
2306
+ let newest = [];
2307
+ for (const item of list) if (compareModelVersion(item.parts.version, newest) > 0) newest = item.parts.version;
2308
+ const bySize = /* @__PURE__ */ new Map();
2309
+ for (const item of list) {
2310
+ if (compareModelVersion(item.parts.version, newest) !== 0) continue;
2311
+ const best = bySize.get(item.parts.size);
2312
+ if (!best || preferAlias(item, best) < 0) bySize.set(item.parts.size, item);
2313
+ }
2314
+ for (const item of bySize.values()) kept.add(item.key);
3245
2315
  }
3246
- return out;
2316
+ return kept;
2317
+ }
2318
+ /** `entries` in their original order, current models only (see {@link currentModelKeys}). */
2319
+ function currentModels(entries, keep = []) {
2320
+ const kept = currentModelKeys(entries, keep);
2321
+ return entries.filter((entry) => kept.has(lineupKey(entry)));
3247
2322
  }
3248
2323
  //#endregion
3249
2324
  //#region src/chat/provider-usage-format.ts
@@ -3328,11 +2403,192 @@ function formatBalance(credits) {
3328
2403
  return credits.total === null ? remaining : `${remaining} / ${formatCredits(credits.total)}`;
3329
2404
  }
3330
2405
  //#endregion
2406
+ //#region src/chat/spawn-tree.ts
2407
+ /** Worker id → the worker that spawned it, from nested spawn markers. */
2408
+ function spawnParents(rows) {
2409
+ const parents = /* @__PURE__ */ new Map();
2410
+ for (const row of rows) {
2411
+ const parent = row.spawn?.parentChildId;
2412
+ if (row.childId !== void 0 && parent !== void 0 && (row.kind === "spawn-start" || row.kind === "spawn-end")) parents.set(row.childId, parent);
2413
+ }
2414
+ return parents;
2415
+ }
2416
+ /**
2417
+ * The worker `childId`'s rows belong under: its outermost ancestor that is
2418
+ * itself one of the `present` workers (the ones owning rows in this view).
2419
+ * A view that already re-based its owner's rows (a worker opened in a fold)
2420
+ * leaves that owner out of `present`, so the owner's direct children become
2421
+ * the lanes there. `childId` itself when no ancestor is present.
2422
+ */
2423
+ function spawnLane(childId, parents, present) {
2424
+ let lane = childId;
2425
+ const seen = /* @__PURE__ */ new Set([lane]);
2426
+ for (let parent = parents.get(lane); parent !== void 0 && present.has(parent) && !seen.has(parent); parent = parents.get(lane)) {
2427
+ lane = parent;
2428
+ seen.add(lane);
2429
+ }
2430
+ return lane;
2431
+ }
2432
+ /**
2433
+ * Lane resolver over one view's rows: precomputes the parent map and the
2434
+ * present-worker set, then maps a row's `childId` to its lane.
2435
+ */
2436
+ function spawnLaneResolver(rows) {
2437
+ const parents = spawnParents(rows);
2438
+ if (parents.size === 0) return (childId) => childId;
2439
+ const present = /* @__PURE__ */ new Set();
2440
+ for (const row of rows) if (row.childId !== void 0) present.add(row.childId);
2441
+ return (childId) => spawnLane(childId, parents, present);
2442
+ }
2443
+ //#endregion
3331
2444
  //#region src/chat/streaming-pure.ts
3332
2445
  const PARENT_OWNER = "parent";
3333
2446
  function ownerOf(event) {
3334
2447
  return event.childId ?? PARENT_OWNER;
3335
2448
  }
2449
+ /** Is this event owned by a subagent (rather than the top-level agent)? */
2450
+ function isChildOwned(event) {
2451
+ return event.childId !== void 0 || (event.depth ?? 0) > 0;
2452
+ }
2453
+ /**
2454
+ * Index of the event `owner`'s next delta may coalesce into, or `-1` when the
2455
+ * owner has no open lane at the tail.
2456
+ *
2457
+ * Walks backwards over events owned by OTHER subagents — concurrent siblings
2458
+ * whose rows carry no ordering relation to this owner's output, and which a
2459
+ * per-owner drain interleaves at ~60Hz — and stops at the first event that is
2460
+ * either this owner's or a real ordering boundary (parent output, user
2461
+ * prompts, banners). The caller decides whether the event found is mergeable
2462
+ * for its kind. A tail-only check splits every concurrent child's message
2463
+ * into fragments braided with its siblings'.
2464
+ */
2465
+ function mergeTargetIndex(events, owner) {
2466
+ for (let i = events.length - 1; i >= 0; i--) {
2467
+ const event = events[i];
2468
+ if (ownerOf(event) === owner) return i;
2469
+ if (!isChildOwned(event)) return -1;
2470
+ }
2471
+ return -1;
2472
+ }
2473
+ /**
2474
+ * Default segment bound for a growing streaming markdown event. Coalescing
2475
+ * copies the ENTIRE accumulated string into fresh state on every drain tick,
2476
+ * so one long streamed message costs O(L²) aggregate — past this threshold
2477
+ * the event is split and the copy re-bounded. The split reuses
2478
+ * {@link chunkMarkdownForRender} (blank-line boundaries outside fences,
2479
+ * unsafe-continuation guard), so the finalized segments are byte-identical
2480
+ * to the chunks the renderer would have produced inside a single event, and
2481
+ * the one-row markdown/markdown margin stands in for the consumed blank line
2482
+ * exactly like the renderer's inter-chunk gap. When no safe boundary exists
2483
+ * (one giant fence/list) the event keeps growing until one arrives.
2484
+ */
2485
+ const STREAMING_MARKDOWN_SEGMENT_CHARS = 65536;
2486
+ /**
2487
+ * Backoff for over-threshold events `chunkMarkdownForRender` could not
2488
+ * split (one giant fence/list): records the text length at the last failed
2489
+ * attempt, keyed on the streaming event object (re-keyed onto each tick's
2490
+ * replacement). Without it a single unsplittable segment re-runs the full
2491
+ * O(L) chunk walk on every drain tick. Growth is geometric (retry at +25%),
2492
+ * so total re-chunk work stays O(L) amortized.
2493
+ */
2494
+ const unsplittableAtLength = /* @__PURE__ */ new WeakMap();
2495
+ const SEGMENT_SPLIT_RETRY_GROWTH = 1.25;
2496
+ /**
2497
+ * Stamp owner (parent vs subagent) + depth + optional `turnId` onto a
2498
+ * freshly-minted event so consumers can identify the producer and the
2499
+ * source turn. Parent-owned events leave `childId` / `depth` off; `depth` is
2500
+ * also left off when the source never named one.
2501
+ */
2502
+ function tagEvent(evt, owner, depth, turnId) {
2503
+ const withTurn = turnId ? {
2504
+ ...evt,
2505
+ turnId
2506
+ } : evt;
2507
+ if (owner === PARENT_OWNER) return withTurn;
2508
+ return {
2509
+ ...withTurn,
2510
+ childId: owner,
2511
+ ...depth !== void 0 ? { depth } : {}
2512
+ };
2513
+ }
2514
+ /** Coalesce a markdown delta into `owner`'s open streaming block, or open one. */
2515
+ function appendMarkdownDelta(prev, delta, owner, depth, turnId, options = {}) {
2516
+ const segmentChars = options.segmentChars ?? 65536;
2517
+ const at = mergeTargetIndex(prev, owner);
2518
+ const target = at >= 0 ? prev[at] : void 0;
2519
+ if (target && target.kind === "markdown" && target.streaming) {
2520
+ const merged = target.text + delta;
2521
+ const next = prev.slice();
2522
+ if (merged.length > segmentChars) {
2523
+ const lastFailed = unsplittableAtLength.get(target);
2524
+ if (lastFailed === void 0 || merged.length > lastFailed * SEGMENT_SPLIT_RETRY_GROWTH) {
2525
+ const chunks = chunkMarkdownForRender(merged);
2526
+ if (chunks.length > 1) {
2527
+ const segments = chunks.map((chunk, i) => i === chunks.length - 1 ? {
2528
+ ...target,
2529
+ text: chunk
2530
+ } : {
2531
+ ...target,
2532
+ text: chunk,
2533
+ streaming: false
2534
+ });
2535
+ next.splice(at, 1, ...segments);
2536
+ return next;
2537
+ }
2538
+ const grown = {
2539
+ ...target,
2540
+ text: merged
2541
+ };
2542
+ unsplittableAtLength.set(grown, merged.length);
2543
+ next[at] = grown;
2544
+ return next;
2545
+ }
2546
+ const grown = {
2547
+ ...target,
2548
+ text: merged
2549
+ };
2550
+ unsplittableAtLength.set(grown, lastFailed);
2551
+ next[at] = grown;
2552
+ return next;
2553
+ }
2554
+ next[at] = {
2555
+ ...target,
2556
+ text: merged
2557
+ };
2558
+ return next;
2559
+ }
2560
+ return [...prev, tagEvent({
2561
+ kind: "markdown",
2562
+ text: delta,
2563
+ streaming: true
2564
+ }, owner, depth, turnId)];
2565
+ }
2566
+ /** Coalesce a thinking delta into `owner`'s open lane, one row per line. */
2567
+ function appendThinkingLines(prev, delta, owner, depth, turnId, options = {}) {
2568
+ const lines = delta.split("\n");
2569
+ const at = mergeTargetIndex(prev, owner);
2570
+ const target = at >= 0 ? prev[at] : void 0;
2571
+ const row = (text) => tagEvent({
2572
+ kind: "thinking",
2573
+ text
2574
+ }, owner, depth, turnId);
2575
+ if (target && target.kind === "thinking") {
2576
+ const next = prev.slice();
2577
+ next[at] = {
2578
+ ...target,
2579
+ text: target.text + lines[0]
2580
+ };
2581
+ if (lines.length > 1) next.splice(at + 1, 0, ...lines.slice(1).map(row));
2582
+ return next;
2583
+ }
2584
+ const result = [...prev];
2585
+ if (lines[0] || lines.length > 1) {
2586
+ const first = row(lines[0]);
2587
+ result.push(options.onLaneOpen ? options.onLaneOpen(first) : first);
2588
+ }
2589
+ for (let i = 1; i < lines.length; i++) result.push(row(lines[i]));
2590
+ return result;
2591
+ }
3336
2592
  /** Flip any trailing streaming markdown blocks (any owner) to finalized. */
3337
2593
  function finalizeStreamingMarkdown(events) {
3338
2594
  let changed = false;
@@ -3417,10 +2673,8 @@ function hiddenMarkerEvent(count) {
3417
2673
  };
3418
2674
  }
3419
2675
  /**
3420
- * Reveal one older page while retaining everything currently visible.
3421
- * The default transcript is memory-bounded; memory grows only when the user
3422
- * deliberately scrolls into the marker. The cut expands to a turn boundary
3423
- * so tool-call/result groups are never split.
2676
+ * Swap one turn's rows for its rebuilt ones, in place; `null` when the turn
2677
+ * is not on show.
3424
2678
  */
3425
2679
  function replaceTranscriptTurnEvents(displayed, rebuilt, turnId) {
3426
2680
  const replacements = rebuilt.filter((event) => event.turnId === turnId);
@@ -3433,6 +2687,26 @@ function replaceTranscriptTurnEvents(displayed, rebuilt, turnId) {
3433
2687
  });
3434
2688
  return inserted ? patched : null;
3435
2689
  }
2690
+ /**
2691
+ * A run in the transcript: everything from one of the user's own prompts
2692
+ * up to the next one. Revealing and capping move in whole runs, so the top
2693
+ * of the transcript always opens on a prompt, not halfway through what the
2694
+ * agent did for it.
2695
+ */
2696
+ function isRunStart(e) {
2697
+ return e !== void 0 && isTopLevelPrompt(e);
2698
+ }
2699
+ /** A run longer than this many reveal pages on its own is revealed in pieces of that size. */
2700
+ const WHOLE_RUN_REVEAL_PAGES = 5;
2701
+ /**
2702
+ * Reveal the earlier history while keeping everything on show: the whole
2703
+ * run right before it, and the runs before that while they fit `pageSize`
2704
+ * events together (several small runs at once, never part of one). A run
2705
+ * longer than {@link WHOLE_RUN_REVEAL_PAGES} pages on its own comes in
2706
+ * pieces that size, cut at a turn boundary so a tool call keeps its result.
2707
+ * The default transcript is memory-bounded; memory grows only when the user
2708
+ * deliberately scrolls into the marker.
2709
+ */
3436
2710
  function revealEarlierTranscriptEvents(all, displayed, pageSize = 600) {
3437
2711
  const hidden = hiddenEarlierTranscriptCount(displayed);
3438
2712
  if (hidden <= 0) return displayed;
@@ -3440,12 +2714,63 @@ function revealEarlierTranscriptEvents(all, displayed, pageSize = 600) {
3440
2714
  const anchorTurnId = tail.find((e) => e.turnId !== void 0)?.turnId;
3441
2715
  const anchorIndex = anchorTurnId !== void 0 ? all.findIndex((e) => e.turnId === anchorTurnId) : -1;
3442
2716
  const seam = anchorIndex >= 0 ? anchorIndex : Math.min(hidden, all.length);
3443
- let from = Math.max(0, seam - Math.max(1, pageSize));
3444
- while (from > 0 && all[from]?.turnId !== void 0 && all[from]?.turnId === all[from - 1]?.turnId) from--;
2717
+ if (seam <= 0) return displayed;
2718
+ const page = Math.max(1, pageSize);
2719
+ /** Where the run holding `all[end - 1]` starts (0 when no prompt precedes it). */
2720
+ const runStart = (end) => {
2721
+ let i = end - 1;
2722
+ while (i > 0 && !isRunStart(all[i])) i--;
2723
+ return i;
2724
+ };
2725
+ let from = runStart(seam);
2726
+ if (seam - from > page * WHOLE_RUN_REVEAL_PAGES) {
2727
+ from = seam - page * WHOLE_RUN_REVEAL_PAGES;
2728
+ while (from < seam - 1 && all[from]?.turnId !== void 0 && all[from]?.turnId === all[from - 1]?.turnId) from++;
2729
+ } else while (from > 0) {
2730
+ const previous = runStart(from);
2731
+ if (seam - previous > page) break;
2732
+ from = previous;
2733
+ }
3445
2734
  const out = [...all.slice(from, seam), ...tail];
3446
2735
  if (from > 0) out.unshift(hiddenMarkerEvent(from));
3447
2736
  return out;
3448
2737
  }
2738
+ /** The user's own prompt, not a subagent's. */
2739
+ function isTopLevelPrompt(e) {
2740
+ return e.kind === "user-prompt" && !e.depth && (!e.childId || e.childId === "parent");
2741
+ }
2742
+ /** Index of the `n`-th last top-level user prompt at or after `from` (the earliest one when fewer); -1 with none. */
2743
+ function nthLastPrompt(events, n, from) {
2744
+ let seen = 0;
2745
+ let at = -1;
2746
+ for (let i = events.length - 1; i >= from; i--) {
2747
+ const e = events[i];
2748
+ if (isTopLevelPrompt(e)) {
2749
+ at = i;
2750
+ if (++seen >= n) break;
2751
+ }
2752
+ }
2753
+ return at;
2754
+ }
2755
+ /**
2756
+ * A transcript's end, with `earlier` events of the whole replay before it:
2757
+ * the hidden-count marker in front, as a cap would have left it.
2758
+ */
2759
+ function withEarlierHidden(events, earlier) {
2760
+ return earlier > 0 ? [hiddenMarkerEvent(earlier), ...events] : events;
2761
+ }
2762
+ /**
2763
+ * Whether the end of a transcript (from a turn boundary) is enough for
2764
+ * {@link capTranscriptEvents} at the default bounds: capping it behind its
2765
+ * hidden-count marker cuts exactly where capping the whole transcript
2766
+ * would. Over the cap, and holding the prompts the cut reaches back to.
2767
+ */
2768
+ function coversTranscriptCap(events) {
2769
+ if (events.length <= 1500) return false;
2770
+ let prompts = 0;
2771
+ for (const e of events) if (isTopLevelPrompt(e) && ++prompts >= 10) return true;
2772
+ return false;
2773
+ }
3449
2774
  /**
3450
2775
  * Bound a transcript event array for display. Identity below the cap
3451
2776
  * (no re-render churn on the hot append path); above it, drops the
@@ -3469,7 +2794,14 @@ function capTranscriptEvents(events, max = MAX_TRANSCRIPT_EVENTS, trimTo = TRANS
3469
2794
  }
3470
2795
  let cut = events.length - keep;
3471
2796
  if (cut <= start) return events;
2797
+ const promptAt = nthLastPrompt(events, 10, start);
2798
+ if (promptAt >= 0 && promptAt < cut) cut = Math.max(promptAt, events.length - Math.floor((max + keep) / 2), start + 1);
3472
2799
  const turnAtCut = (i) => events[i]?.turnId;
2800
+ if (!isRunStart(events[cut])) {
2801
+ let next = cut + 1;
2802
+ while (next < events.length && !isRunStart(events[next])) next++;
2803
+ if (next < events.length) cut = next;
2804
+ }
3473
2805
  while (cut < events.length) {
3474
2806
  const turn = turnAtCut(cut);
3475
2807
  if (turn === void 0 || turn !== turnAtCut(cut - 1)) break;
@@ -3512,7 +2844,14 @@ const TOOL_DISPLAY = {
3512
2844
  displayName: "Read",
3513
2845
  format: (input) => {
3514
2846
  const path = stringField(input, "path");
3515
- if (!path) return null;
2847
+ if (!path) {
2848
+ const paths = readFileInputPaths(input);
2849
+ if (paths.length === 0) return null;
2850
+ return {
2851
+ target: paths[0],
2852
+ meta: paths.length > 1 ? [`+${paths.length - 1} more`] : []
2853
+ };
2854
+ }
3516
2855
  const meta = [];
3517
2856
  const offset = numberField(input, "offset");
3518
2857
  const limit = numberField(input, "limit");
@@ -3568,14 +2907,16 @@ const TOOL_DISPLAY = {
3568
2907
  }
3569
2908
  },
3570
2909
  shell: {
3571
- displayName: (input) => input?.run_in_background === true ? "Shell (background)" : "Shell",
2910
+ displayName: (input) => {
2911
+ const description = input ? stringField(input, "description")?.trim() : void 0;
2912
+ if (description) return previewLine(description, 120);
2913
+ return input?.run_in_background === true ? "Shell (background)" : "Shell";
2914
+ },
3572
2915
  format: (input) => {
3573
2916
  const command = stringField(input, "command");
3574
2917
  if (!command) return null;
3575
- const description = stringField(input, "description");
3576
- const line = { target: truncate(command, 200) };
3577
- if (description && description.trim() !== "") line.meta = [truncate(description, 100)];
3578
- return line;
2918
+ if (stringField(input, "description")?.trim()) return input.run_in_background === true ? { meta: ["background"] } : {};
2919
+ return { target: previewLine(command, 200) };
3579
2920
  }
3580
2921
  },
3581
2922
  shell_kill: {
@@ -3645,11 +2986,103 @@ const TOOL_DISPLAY = {
3645
2986
  if (!task) return null;
3646
2987
  const resume = stringField(input, "resume");
3647
2988
  return {
3648
- target: truncate(task, 200),
2989
+ target: previewLine(task, 200),
3649
2990
  ...resume ? { meta: [`resumed ${resume}`] } : {}
3650
2991
  };
3651
2992
  }
3652
2993
  },
2994
+ codemode: {
2995
+ displayName: (input) => {
2996
+ const description = input ? stringField(input, "description")?.trim() : void 0;
2997
+ return description ? previewLine(description, 120) : "Codemode";
2998
+ },
2999
+ format: (input) => {
3000
+ const description = stringField(input, "description")?.trim();
3001
+ const code = stringField(input, "code");
3002
+ const lines = code ? code.split("\n").filter((line) => line.trim() !== "" && !line.trimStart().startsWith("// @options:")) : [];
3003
+ const meta = lines.length > 0 ? [`${lines.length} line${lines.length === 1 ? "" : "s"}`] : [];
3004
+ if (description) return { meta };
3005
+ if (lines.length === 0) return null;
3006
+ return {
3007
+ target: previewLine(lines[0].trim(), 120),
3008
+ meta
3009
+ };
3010
+ }
3011
+ },
3012
+ browser_open: {
3013
+ displayName: "Browser",
3014
+ format: (input) => ({ target: stringField(input, "url") ?? "open" })
3015
+ },
3016
+ browser_navigate: {
3017
+ displayName: "Navigate",
3018
+ format: (input) => ({ target: stringField(input, "url") ?? stringField(input, "action") ?? "" })
3019
+ },
3020
+ browser_snapshot: {
3021
+ displayName: "Snapshot",
3022
+ format: () => ({ target: "page" })
3023
+ },
3024
+ browser_screenshot: {
3025
+ displayName: "Screenshot",
3026
+ format: (input) => ({ target: stringField(input, "ref") ?? "viewport" })
3027
+ },
3028
+ browser_click: {
3029
+ displayName: "Click",
3030
+ format: (input) => ({ target: browserTarget(input) })
3031
+ },
3032
+ browser_type: {
3033
+ displayName: "Type",
3034
+ format: (input) => {
3035
+ const text = stringField(input, "text") ?? "";
3036
+ const where = browserTarget(input);
3037
+ return {
3038
+ target: previewLine(JSON.stringify(text), 80),
3039
+ ...where ? { meta: [`into ${where}`] } : {}
3040
+ };
3041
+ }
3042
+ },
3043
+ browser_press: {
3044
+ displayName: "Press",
3045
+ format: (input) => {
3046
+ return { target: [...Array.isArray(input.modifiers) ? input.modifiers.map(String) : [], stringField(input, "key") ?? ""].join("+") };
3047
+ }
3048
+ },
3049
+ browser_scroll: {
3050
+ displayName: "Scroll",
3051
+ format: (input) => ({ target: `${numberField(input, "dx") ?? 0}, ${numberField(input, "dy") ?? 0}` })
3052
+ },
3053
+ browser_wait_for: {
3054
+ displayName: "Wait for",
3055
+ format: (input) => ({ target: stringField(input, "text") ?? stringField(input, "selector") ?? stringField(input, "urlIncludes") ?? "" })
3056
+ },
3057
+ browser_evaluate: {
3058
+ displayName: "Evaluate",
3059
+ format: (input) => ({ target: previewLine(stringField(input, "expression") ?? "", 120) })
3060
+ },
3061
+ browser_console: {
3062
+ displayName: "Console",
3063
+ format: (input) => ({ target: stringField(input, "level") ?? "all" })
3064
+ },
3065
+ terminal_list: {
3066
+ displayName: "Terminals",
3067
+ format: () => ({ target: "list" })
3068
+ },
3069
+ terminal_read: {
3070
+ displayName: "Read terminal",
3071
+ format: (input) => ({ target: stringField(input, "id") ?? "" })
3072
+ },
3073
+ terminal_write: {
3074
+ displayName: "Type in terminal",
3075
+ format: (input) => {
3076
+ const text = stringField(input, "text");
3077
+ const keys = Array.isArray(input.keys) ? input.keys.map(String).filter(Boolean) : [];
3078
+ const typed = [text ? previewLine(JSON.stringify(text), 80) : "", ...keys].filter(Boolean).join(" + ");
3079
+ const id = stringField(input, "id");
3080
+ return {
3081
+ target: typed || (id ?? ""),
3082
+ ...id && typed ? { meta: [id] } : {}
3083
+ };
3084
+ }
3085
+ },
3653
3086
  tool_search: {
3654
3087
  displayName: "Search tools",
3655
3088
  format: (input) => {
@@ -3697,7 +3130,7 @@ const TOOL_DISPLAY = {
3697
3130
  if (!name || !script) return null;
3698
3131
  const meta = [`skill ${name}`];
3699
3132
  const args = Array.isArray(input.args) ? input.args : null;
3700
- if (args && args.length > 0) meta.push(truncate(args.map(String).join(" "), 80));
3133
+ if (args && args.length > 0) meta.push(previewLine(args.map(String).join(" "), 80));
3701
3134
  return {
3702
3135
  target: script,
3703
3136
  meta
@@ -3817,6 +3250,35 @@ function displayNameFor(name, input) {
3817
3250
  return sentenceCase(name.startsWith("mcp_") ? name.slice(4) : name);
3818
3251
  }
3819
3252
  /**
3253
+ * The source code a tool call runs, for renderers that show it highlighted
3254
+ * instead of the raw JSON input: Codemode's script as JavaScript, a `shell`
3255
+ * / `bash` command as bash (the command alone: the label carries the
3256
+ * description, the rest is plumbing). `null` for
3257
+ * every other tool and for calls without the code field. `language` is a
3258
+ * fence-style name both highlighters accept (Shiki in the GUI, OpenTUI's
3259
+ * tree-sitter in the TUI).
3260
+ */
3261
+ function toolInputCode(name, input) {
3262
+ if (!input) return null;
3263
+ if (name === "codemode") {
3264
+ const code = stringField(input, "code");
3265
+ if (!code || code.trim() === "") return null;
3266
+ return {
3267
+ code: code.replace(/^\n+|\s+$/g, ""),
3268
+ language: "javascript"
3269
+ };
3270
+ }
3271
+ if (name === "shell" || name === "bash") {
3272
+ const command = stringField(input, "command");
3273
+ if (!command || command.trim() === "") return null;
3274
+ return {
3275
+ code: command.replace(/^\n+|\s+$/g, ""),
3276
+ language: "bash"
3277
+ };
3278
+ }
3279
+ return null;
3280
+ }
3281
+ /**
3820
3282
  * Run a tool's curated formatter and return the result, or `null` when
3821
3283
  * no formatter is registered / the input shape doesn't match. Renderer
3822
3284
  * decides what to do with `null` — typically: show `↳ <displayName>`
@@ -3831,6 +3293,12 @@ function formatToolCall(name, input) {
3831
3293
  return null;
3832
3294
  }
3833
3295
  }
3296
+ /** A browser tool's target as written: ref, selector, or point. */
3297
+ function browserTarget(input) {
3298
+ const x = numberField(input, "x");
3299
+ const y = numberField(input, "y");
3300
+ return stringField(input, "ref") ?? stringField(input, "selector") ?? (x !== void 0 && y !== void 0 ? `${x}, ${y}` : "");
3301
+ }
3834
3302
  function stringField(input, key) {
3835
3303
  const v = input[key];
3836
3304
  return typeof v === "string" && v.length > 0 ? v : void 0;
@@ -3846,139 +3314,7 @@ function sentenceCase(s) {
3846
3314
  words[0] = (words[0][0]?.toUpperCase() ?? "") + words[0].slice(1);
3847
3315
  return words.join(" ");
3848
3316
  }
3849
- /**
3850
- * Collapse internal whitespace (including newlines) to single spaces
3851
- * and clip to `max` columns with a trailing `…`. The whitespace
3852
- * normalisation is the load-bearing bit — tool input strings like a
3853
- * shell heredoc or a multi-line Python `-c` script otherwise render
3854
- * across several rows in the transcript even though the `↳ Tool …`
3855
- * line is meant to be a single-line scannable summary.
3856
- */
3857
- function truncate(s, max) {
3858
- const clean = s.replace(/\s+/g, " ").trim();
3859
- return clean.length <= max ? clean : `${clean.slice(0, max - 1)}…`;
3860
- }
3861
- function formatBytes(bytes) {
3862
- if (bytes < 1024) return `${bytes} B`;
3863
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
3864
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
3865
- }
3866
- //#endregion
3867
- //#region src/chat/turn-operations.ts
3868
- /**
3869
- * Fork — keep every turn up to and including `turnId`, then strip any
3870
- * `tool_call` blocks left without a matching `tool_result` in the slice.
3871
- *
3872
- * Semantics:
3873
- * - Include the selected turn ("branch from HERE" mental model — the
3874
- * user wants the selected message to be the latest in the fork).
3875
- * - If the selected turn is an assistant turn with unresolved
3876
- * `tool_call` blocks (their `tool_result`s live in turns AFTER the
3877
- * slice), strip those calls. Otherwise the fork would post an
3878
- * assistant turn with no matching tool results, breaking the next
3879
- * provider call.
3880
- * - Drop turns that become empty (all blocks stripped).
3881
- *
3882
- * Returns `null` when `turnId` doesn't exist in `turns` — caller should
3883
- * surface a "turn not found" error rather than silently no-op.
3884
- */
3885
- function truncateTurnsAt(turns, turnId) {
3886
- const idx = turns.findIndex((t) => t.id === turnId);
3887
- if (idx === -1) return null;
3888
- return stripOrphanToolBlocks(turns.slice(0, idx + 1));
3889
- }
3890
- /**
3891
- * Delete — remove the turn with `turnId` and any tool blocks left
3892
- * orphaned by the removal. Returns `null` when `turnId` doesn't exist.
3893
- *
3894
- * Strategy:
3895
- * 1. Drop the target turn.
3896
- * 2. Scan the remaining turns for `tool_call`s without a matching
3897
- * `tool_result` (orphaned by removing the user turn that carried
3898
- * the result), and `tool_result`s without a matching `tool_call`
3899
- * (orphaned by removing the assistant turn that issued the call).
3900
- * Strip both sides.
3901
- * 3. Drop turns whose content is now empty.
3902
- *
3903
- * This guarantees the resulting history is protocol-clean — a follow-up
3904
- * `agent.run()` against the modified session can post turns without the
3905
- * provider rejecting the history.
3906
- */
3907
- function deleteTurnSafely(turns, turnId) {
3908
- const idx = turns.findIndex((t) => t.id === turnId);
3909
- if (idx === -1) return null;
3910
- return stripOrphanToolBlocks([...turns.slice(0, idx), ...turns.slice(idx + 1)]);
3911
- }
3912
- /**
3913
- * Walk a turn list and remove any tool blocks whose counterpart is
3914
- * missing. Drops turns left empty. Used by `truncateTurnsAt` (which can
3915
- * leave `tool_call`s orphaned when their results are past the cut) and
3916
- * `deleteTurnSafely` (which can orphan either side of a pair).
3917
- *
3918
- * Pure / total: returns a new array; never throws.
3919
- */
3920
- function stripOrphanToolBlocks(turns) {
3921
- const callIds = /* @__PURE__ */ new Set();
3922
- const resultIds = /* @__PURE__ */ new Set();
3923
- for (const turn of turns) for (const block of turn.content) if (block.type === "tool_call") callIds.add(block.id);
3924
- else if (block.type === "tool_result") resultIds.add(block.callId);
3925
- const result = [];
3926
- for (const turn of turns) {
3927
- const filtered = [];
3928
- for (const block of turn.content) {
3929
- if (block.type === "tool_call") {
3930
- if (!resultIds.has(block.id)) continue;
3931
- } else if (block.type === "tool_result") {
3932
- if (!callIds.has(block.callId)) continue;
3933
- }
3934
- filtered.push(block);
3935
- }
3936
- if (filtered.length === 0) continue;
3937
- result.push(filtered.length === turn.content.length ? turn : {
3938
- ...turn,
3939
- content: filtered
3940
- });
3941
- }
3942
- return result;
3943
- }
3944
- /**
3945
- * Serialize a turn's content to a clean text representation suited for
3946
- * the clipboard. Joins text + thinking blocks verbatim; tool calls and
3947
- * tool results get bracketed labels so the user can paste a readable
3948
- * record of what happened without losing structure.
3949
- *
3950
- * Empty turns return `''`.
3951
- */
3952
- function turnAsText(turn) {
3953
- const parts = [];
3954
- for (const block of turn.content) if (block.type === "text" && block.text.trim()) parts.push(block.text);
3955
- else if (block.type === "thinking" && block.text.trim()) parts.push(`[thinking]\n${block.text}`);
3956
- else if (block.type === "tool_call") parts.push(`[tool call · ${block.name}]\n${stringifyArgs(block.input)}`);
3957
- else if (block.type === "tool_result") parts.push(`[tool result]\n${typeof block.output === "string" ? block.output : JSON.stringify(block.output, null, 2)}`);
3958
- else if (block.type === "compact-summary") parts.push(`[compaction summary · ${block.replacesTurnIds.length} turn${block.replacesTurnIds.length === 1 ? "" : "s"}]\n${block.summary}`);
3959
- return parts.join("\n\n");
3960
- }
3961
- function stringifyArgs(input) {
3962
- try {
3963
- return JSON.stringify(input, null, 2);
3964
- } catch {
3965
- return String(input);
3966
- }
3967
- }
3968
- /**
3969
- * Count turns before / after the one identified by `turnId` in the
3970
- * given list. Returns `null` when the id is missing. Used to label the
3971
- * turn-details modal with `N before · M after`.
3972
- */
3973
- function countNeighbors(turnIds, turnId) {
3974
- const idx = turnIds.indexOf(turnId);
3975
- if (idx === -1) return null;
3976
- return {
3977
- before: idx,
3978
- after: turnIds.length - 1 - idx
3979
- };
3980
- }
3981
3317
  //#endregion
3982
- export { pickChainPrimary as $, fitRunReceiptCells as $t, indexOfEntry as A, createChatsCompletionProvider as An, selectableTurnIds as At, cacheBreakNoticeText as B, artifactsFromTurns as Bt, resetLabel as C, parseEditOutcomesFromResult as Cn, isUserBashEvent as Ct, splitPromptSegments as D, summarizeOutcomes as Dn, promptTurnRefs as Dt, usageTone as E, stripEditOutcomesAnnotation as En, promptHistoryFromEvents as Et, codeTokenHasClosingFence as F, MAIN_SESSION_TITLE as Ft, DAEMON_HEALTH_GLYPH as G, renderSessionEventXml as Gt, liveSessionCost as H, orchestratedBy as Ht, normalizeMarkdownCodeFences as I, ORCHESTRATED_BY_KEY as It, chainEntryKey as J, sessionEventLabel as Jt, DAEMON_HEALTH_STYLE as K, sessionDirLabel as Kt, splitMarkdownCodeBlocks as L, SESSION_EVENT_STATUS_STYLE as Lt, MARKDOWN_RENDER_CHUNK_TARGET_BYTES as M, streamEventSearchText as Mt, chunkMarkdownForRender as N, turnSelectionOwnership as Nt, buildModelCatalog as O, CHATS_TRIGGER as On, retagUserPromptEvents as Ot, closeDanglingCodeFence as P, MAIN_PLAYBOOK_FRAGMENT as Pt, filterChainFallbacks as Q, RUN_RECEIPT_GLYPH as Qt, stripHtmlCommentsOutsideCode as R, SESSION_ROLE_KEY as Rt, mostConstrainedWindow as S, mergeApprovalAndBodyOutcomes as Sn, isTurnHighlighted as St, updatedLabel as T, rewriteMultiEditHeader as Tn, isVisibleDuringSearch as Tt, retryLabel as U, parseSessionEventXml as Ut, compactingLabel as V, isMainSession as Vt, streamingLabel as W, renderProjectsBriefing as Wt, chainsEqual as X, shortSessionId as Xt, chainPositionOf as Y, sessionEventMeta as Yt, chainsEqualKeyed as Z, truncateTitle as Zt, turnContextSize as _, splitLines as _n, buildLinearRamp as _t, TOOL_DISPLAY as a, formatTokensPerSecond as an, sanitizeModelChain as at, formatCredits as b, buildEditOutcomesAnnotation as bn, EDIT_TOOL_NAMES as bt, MAX_TRANSCRIPT_EVENTS as c, applyEditPayload as cn, SKILLS_TRIGGER as ct, finalizeStreamingMarkdown as d, clipDiffForRender as dn, FILES_TRIGGER as dt, formatCostUsd as en, pickPrimaryKeyed as et, finalizeStreamingMarkdownForOwner as f, computeInlineDiff as fn, createFilesCompletionProvider as ft, revealEarlierTranscriptEvents as g, previewEditPayload as gn, blendHsl as gt, replaceTranscriptTurnEvents as h, filetypeFromPath as hn, createCommandsCompletionProvider as ht, turnAsText as i, formatTokenCount as in, restoreChainFallbacks as it, parseCustomModelQuery as j, uniqueChatIdsFromReferences as jn, selectionAfterTurnRemoval as jt, filterModelCatalog as k, chatReferenceTokensFromText as kn, retagUserPromptEventsFromRefs as kt, TRANSCRIPT_TRIM_TO_EVENTS as l, buildContextualDiff as ln, createSkillsCompletionProvider as lt, ownerOf as m, extractEditPayload as mn, COMMANDS_TRIGGER as mt, deleteTurnSafely as n, formatRunReceiptCells as nn, providerKeySetChanged as nt, displayNameFor as o, hasRunReceipt as on, toggleChainEntry as ot, hiddenEarlierTranscriptCount as p, computeLineDiff as pn, uniqueFilesFromReferences as pt, buildRunModel as q, sessionEventExcerpt as qt, truncateTurnsAt as r, formatRunReceiptLine as rn, reorderKeyed as rt, formatToolCall as s, runReceiptFromRun as sn, toggleKeyed as st, countNeighbors as t, formatRunDuration as tn, positionKeyed as tt, capTranscriptEvents as u, buildUnifiedDiff as un, uniqueSkillNamesFromReferences as ut, activeSnapshotFor as v, summarizeEditPayload as vn, BASH_CALL_ID_PREFIX as vt, showsAccount as w, resolveApprovalForPayload as wn, isVisible as wt, formatUsagePercent as x, maskToOutcomeKinds as xn, isEditErrorResult as xt, formatBalance as y, tokenize as yn, CACHE_NOTICE_LABEL as yt, cacheBreakChip as z, WORKER_REPORT_FRAGMENT as zt };
3318
+ export { chunkMarkdownForRender as $, MachineErrorCode as $t, showsAccount as A, BROWSER_TEXT_MAX_CHARS as At, indexOfEntry as B, liveSessionCost as Bt, spawnParents as C, TODO_STATUS_GLYPHS as Cn, BROWSER_ACTION_TIMEOUT_MS as Ct, formatUsagePercent as D, BROWSER_OPS as Dt, formatCredits as E, BROWSER_OPEN_MOUNT_MS as Et, currentModels as F, createChatsCompletionProvider as Ft, mcpFailureLine as G, daemonGate as Gt, MCP_KEY_REFUSED_HINT as H, runStatusLabel as Ht, parseModelId as I, uniqueChatIdsFromReferences as It, mcpFailureEvent as J, isKillSwitchEngaged as Jt, MCP_STATUS_LABEL as K, defineFeatureGate as Kt, buildModelCatalog as L, cacheBreakChip as Lt, usageTone as M, BrowserOpError as Mt, compareModelVersion as N, CHATS_TRIGGER as Nt, mostConstrainedWindow as O, BROWSER_OUTLINE_MAX_ELEMENTS as Ot, currentModelKeys as P, chatReferenceTokensFromText as Pt, MARKDOWN_RENDER_CHUNK_TARGET_BYTES as Q, MACHINE_STREAM_CHUNK_BYTES as Qt, customModelTargets as R, cacheBreakNoticeText as Rt, spawnLaneResolver as S, accentColor as Sn, TerminalError as St, formatBalance as T, BROWSER_MAX_WAIT_MS as Tt, classifyMcpTest as U, streamingLabel as Ut, parseCustomModelQuery as V, retryLabel as Vt, isMcpAuthRefusal as W, FEATURE_GATES as Wt, enabledMcpsWith as X, MACHINE_IDEMPOTENT_METHODS as Xt, reduceMcpAuth as Y, remoteHostsGate as Yt, enabledMcpsWithout as Z, MACHINE_PROTOCOL_VERSION as Zt, replaceTranscriptTurnEvents as _, retagUserPromptEventsFromRefs as _n, TERMINAL_HISTORY_MAX_CHARS as _t, MAX_TRANSCRIPT_EVENTS as a, BASH_CALL_ID_PREFIX as an, DAEMON_HEALTH_GLYPH as at, withEarlierHidden as b, streamEventSearchText as bn, TERMINAL_READ_MAX_CHARS as bt, appendMarkdownDelta as c, isEditErrorResult as cn, createSkillsCompletionProvider as ct, coversTranscriptCap as d, isVisible as dn, createFilesCompletionProvider as dt, isValidMachineName as en, closeDanglingCodeFence as et, finalizeStreamingMarkdown as f, isVisibleDuringSearch as fn, uniqueFilesFromReferences as ft, ownerOf as g, retagUserPromptEvents as gn, buildLinearRamp as gt, mergeTargetIndex as h, promptTurnRefs as hn, blendHsl as ht, toolInputCode as i, toMachineName as in, stripHtmlCommentsOutsideCode as it, updatedLabel as j, BROWSER_WAIT_TIMEOUT_MS as jt, resetLabel as k, BROWSER_REQUEST_SLACK_MS as kt, appendThinkingLines as l, isTurnHighlighted as ln, uniqueSkillNamesFromReferences as lt, hiddenEarlierTranscriptCount as m, promptHistoryFromEvents as mn, createCommandsCompletionProvider as mt, displayNameFor as n, machineKind as nn, normalizeMarkdownCodeFences as nt, STREAMING_MARKDOWN_SEGMENT_CHARS as o, CACHE_NOTICE_LABEL as on, DAEMON_HEALTH_STYLE as ot, finalizeStreamingMarkdownForOwner as p, messageSelectionOwnership as pn, COMMANDS_TRIGGER as pt, getMcpAuthStatus as q, engagedKillSwitches as qt, formatToolCall as r, parseMachineKind as rn, splitMarkdownCodeBlocks as rt, TRANSCRIPT_TRIM_TO_EVENTS as s, EDIT_TOOL_NAMES as sn, SKILLS_TRIGGER as st, TOOL_DISPLAY as t, machineCredentialKey as tn, codeTokenHasClosingFence as tt, capTranscriptEvents as u, isUserBashEvent as un, FILES_TRIGGER as ut, revealEarlierTranscriptEvents as v, selectableTurnIds as vn, TERMINAL_MAX_PER_SESSION as vt, activeSnapshotFor as w, BROWSER_EVALUATE_MAX_BYTES as wt, spawnLane as x, turnSelectionOwnership as xn, TERMINAL_TEXT_MAX_LINES as xt, turnContextSize as y, selectionAfterTurnRemoval as yn, TERMINAL_OUTPUT_WINDOW as yt, filterModelCatalog as z, compactingLabel as zt };
3983
3319
 
3984
- //# sourceMappingURL=turn-operations-DrbJlpq9.js.map
3320
+ //# sourceMappingURL=tool-formatters-IQFDrdrs.js.map