@iowarp/clio-coder 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (988) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +50 -53
  3. package/dist/acp-FPR54DGL.js +138 -0
  4. package/dist/{agents-Q4MYPMUW.js → agents-OGPIHPJH.js} +33 -25
  5. package/dist/assets/codewiki.json +1 -0
  6. package/dist/assets/grammars/notices/@vscode__tree-sitter-wasm/LICENSE +21 -0
  7. package/dist/assets/grammars/notices/@vscode__tree-sitter-wasm/cgmanifest.json +16 -0
  8. package/dist/assets/grammars/notices/tree-sitter-wasms/LICENSE +24 -0
  9. package/dist/assets/grammars/tree-sitter-c-sharp.wasm +0 -0
  10. package/dist/assets/grammars/tree-sitter-c.wasm +0 -0
  11. package/dist/assets/grammars/tree-sitter-cpp.wasm +0 -0
  12. package/dist/assets/grammars/tree-sitter-go.wasm +0 -0
  13. package/dist/assets/grammars/tree-sitter-java.wasm +0 -0
  14. package/dist/assets/grammars/tree-sitter-javascript.wasm +0 -0
  15. package/dist/assets/grammars/tree-sitter-python.wasm +0 -0
  16. package/dist/assets/grammars/tree-sitter-ruby.wasm +0 -0
  17. package/dist/assets/grammars/tree-sitter-rust.wasm +0 -0
  18. package/dist/assets/grammars/tree-sitter-tsx.wasm +0 -0
  19. package/dist/assets/grammars/tree-sitter-typescript.wasm +0 -0
  20. package/dist/assets/grammars/tree-sitter.wasm +0 -0
  21. package/dist/{auth-O6HYIJ6J.js → auth-IC3K6NIZ.js} +22 -15
  22. package/dist/{chunk-KKKPTZLM.js → chunk-2STGEDFV.js} +8 -1
  23. package/dist/chunk-3I7MS7N2.js +40 -0
  24. package/dist/chunk-3R73A4XB.js +69 -0
  25. package/dist/{chunk-26BZQOAD.js → chunk-474KN5II.js} +86 -115
  26. package/dist/{chunk-NI4DEJMC.js → chunk-4QKXUHSR.js} +26 -3
  27. package/dist/{chunk-X3BR7HWV.js → chunk-5WIGXA4T.js} +7 -1
  28. package/dist/{chunk-TFSSEXL6.js → chunk-673JJUWJ.js} +14 -4
  29. package/dist/{chunk-GNGDQYDU.js → chunk-7SS2CTV2.js} +27922 -1249
  30. package/dist/{chunk-7RBKDI66.js → chunk-ASND7OZK.js} +104 -12
  31. package/dist/{chunk-X3NE4WVW.js → chunk-BNAZZHFG.js} +7 -1
  32. package/dist/{chunk-RQDWMVRB.js → chunk-BSU2YIWB.js} +9 -3
  33. package/dist/{chunk-CLJ5HLUD.js → chunk-C4PTHK7P.js} +24 -5
  34. package/dist/{chunk-QV47YRF4.js → chunk-CGKSTWHD.js} +7 -1
  35. package/dist/{chunk-BYEU76JP.js → chunk-DKGKUHFA.js} +34 -9
  36. package/dist/chunk-EYOKLTMF.js +170 -0
  37. package/dist/{chunk-PS4PFJQP.js → chunk-FCIH3BIZ.js} +15258 -14130
  38. package/dist/{chunk-2J63S4SF.js → chunk-FST4FYJB.js} +82 -14
  39. package/dist/{chunk-3DANZDGR.js → chunk-G34LV2PF.js} +34 -9
  40. package/dist/{chunk-BBUH4VAA.js → chunk-GAEBEQVI.js} +34 -21
  41. package/dist/chunk-GB6QRBXN.js +8543 -0
  42. package/dist/chunk-GEP36Y4X.js +12796 -0
  43. package/dist/{chunk-TKHQ4DGZ.js → chunk-H6F6BYOH.js} +496 -121
  44. package/dist/{chunk-UGYAX5YI.js → chunk-HQQID6OA.js} +10 -5
  45. package/dist/chunk-IHXBNWMM.js +137 -0
  46. package/dist/{chunk-ME6DNWIU.js → chunk-IS3ONKU3.js} +7 -1
  47. package/dist/{chunk-DXQNI4PC.js → chunk-IWHMRKLL.js} +8 -1
  48. package/dist/chunk-K2ITRMHZ.js +642 -0
  49. package/dist/{chunk-KBR4MZZR.js → chunk-K5XEMXTI.js} +7 -1
  50. package/dist/{chunk-AMFR5YA3.js → chunk-KKNLWXI6.js} +24 -10
  51. package/dist/{chunk-JWHCJDCI.js → chunk-KY56HMHH.js} +145 -61
  52. package/dist/{chunk-E3NYWENM.js → chunk-LU4TK2PR.js} +52 -7
  53. package/dist/{chunk-UWIJNAOB.js → chunk-M5T5VO65.js} +109 -42
  54. package/dist/{chunk-JPOWPFCU.js → chunk-MAR7Y6HW.js} +38 -16
  55. package/dist/chunk-N6F52NLF.js +6951 -0
  56. package/dist/{chunk-UHTSULZS.js → chunk-NMOX6HFD.js} +20 -7
  57. package/dist/{chunk-WD6AJM35.js → chunk-OLBBMFRD.js} +30 -13
  58. package/dist/{chunk-VOO7NYPP.js → chunk-PFEFKVGL.js} +22 -6
  59. package/dist/{chunk-6CWDTGUC.js → chunk-PIUMUEMV.js} +7 -1
  60. package/dist/chunk-PV4JUBVJ.js +462 -0
  61. package/dist/{chunk-527KG6XR.js → chunk-Q3RUPKEJ.js} +22 -4
  62. package/dist/{chunk-D5YD55AR.js → chunk-Q5WJOSJ7.js} +11 -1
  63. package/dist/{chunk-5N2FG33Q.js → chunk-R346GLFC.js} +7 -1
  64. package/dist/{chunk-4UQA7NCT.js → chunk-R5KLMSBV.js} +7 -2
  65. package/dist/{chunk-PIDUD6M2.js → chunk-RDLVBZEO.js} +11 -6
  66. package/dist/{chunk-O4EJEDHO.js → chunk-RPTR2H26.js} +40 -31
  67. package/dist/{chunk-HBU5MTAM.js → chunk-SVNXH4L2.js} +7 -1
  68. package/dist/{chunk-XNISANGE.js → chunk-TEKV33Q5.js} +122 -57
  69. package/dist/chunk-TXOTCRLG.js +15 -0
  70. package/dist/{chunk-VPAWTYLY.js → chunk-WEPFGWHJ.js} +7 -1
  71. package/dist/{chunk-HMYNFFY4.js → chunk-XPLRXC72.js} +7 -2
  72. package/dist/{chunk-UU3R62TT.js → chunk-XYWBQRDM.js} +13 -4
  73. package/dist/{chunk-7BHLZB3A.js → chunk-ZPY3JZ5E.js} +32 -10
  74. package/dist/cli/index.js +39 -33
  75. package/dist/clio-BZVGEUFJ.js +58 -0
  76. package/dist/{components-JVHMUBEB.js → components-DMAOEKFB.js} +26 -9
  77. package/dist/{config-ZFCDBMDC.js → config-IRUQ7SE4.js} +42 -31
  78. package/dist/configure-S7S6F6CL.js +32 -0
  79. package/dist/{context-CDXTP2MP.js → context-3KWFLHJG.js} +17 -11
  80. package/dist/{context-E3KIFVXI.js → context-5RADCKTR.js} +33 -28
  81. package/dist/{context-clear-3F4PLXOS.js → context-clear-7TSNPAAI.js} +36 -28
  82. package/dist/{context-index-Q7YSYTR3.js → context-index-W4RLWOQH.js} +10 -4
  83. package/dist/{docs-YIETIWZI.js → docs-5AWSPS37.js} +16 -10
  84. package/dist/doctor-UC5NAJYQ.js +77 -0
  85. package/dist/{eval-B3KZZESM.js → eval-U6TJHRLX.js} +77 -24
  86. package/dist/{evidence-V67CHM35.js → evidence-YEGUW4L3.js} +34 -26
  87. package/dist/{evolve-YDZSUQYA.js → evolve-TXARCTPG.js} +39 -26
  88. package/dist/{extensions-SRG7XCAH.js → extensions-OZFJ3A3G.js} +13 -7
  89. package/dist/{fleet-CA2CRTVG.js → fleet-6G3DHNYE.js} +45 -37
  90. package/dist/fleet-preflight-DSNT37JK.js +23 -0
  91. package/dist/{init-2OZDJE2D.js → init-KZ5QTF6M.js} +42 -34
  92. package/dist/{memory-3PIQQAKX.js → memory-73ESV5YC.js} +35 -27
  93. package/dist/{models-DY35XI7Y.js → models-A4PVNWJK.js} +34 -23
  94. package/dist/{paths-5OMXW7Z4.js → paths-C4H6IV77.js} +12 -6
  95. package/dist/preload-6WVMHX3A.js +13 -0
  96. package/dist/{reset-PIFYNOS3.js → reset-BGW6OGMV.js} +17 -11
  97. package/dist/{run-3VSPP24F.js → run-YTPEYQOH.js} +60 -48
  98. package/dist/{share-D36RQCXM.js → share-YIFFV4NQ.js} +16 -10
  99. package/dist/{skills-F2MRLELY.js → skills-2V6RA3OQ.js} +18 -11
  100. package/dist/{skills-eval-E2ZTW4PL.js → skills-eval-S2TVJO4F.js} +45 -36
  101. package/dist/{targets-DZMEZAH4.js → targets-TYXLPB23.js} +61 -59
  102. package/dist/{trace-7NYCUI2J.js → trace-GGOJ6Q6Z.js} +9 -3
  103. package/dist/{uninstall-AD3JWHBB.js → uninstall-LLLT4F4W.js} +14 -8
  104. package/dist/{upgrade-WYYBKGDY.js → upgrade-33G2LMM5.js} +45 -23
  105. package/dist/{usage-ULIDAGFF.js → usage-ZAFSXKKG.js} +35 -27
  106. package/dist/version-KGEG7EPO.js +21 -0
  107. package/dist/{wiki-generate-PKFIX6OB.js → wiki-generate-NUQCVOQ3.js} +55 -45
  108. package/dist/worker/entry.js +101 -43
  109. package/docs/README.md +12 -9
  110. package/docs/acp.md +28 -5
  111. package/docs/alcf-provider.md +1 -1
  112. package/docs/architecture.md +23 -2
  113. package/docs/artifact-placement.md +101 -0
  114. package/docs/artifact-versions.md +1 -1
  115. package/docs/built-in-agents.md +4 -2
  116. package/docs/capacity-and-scheduling.md +1 -1
  117. package/docs/commands-and-modes.md +125 -41
  118. package/docs/configuration-and-targets.md +88 -18
  119. package/docs/context-engine.md +6 -2
  120. package/docs/development-pipeline.md +77 -0
  121. package/docs/documentation-coverage.md +4 -4
  122. package/docs/documentation-guide.md +3 -3
  123. package/docs/environment-variables.md +2 -1
  124. package/docs/eval-runner.md +3 -3
  125. package/docs/evals-internal.md +2 -2
  126. package/docs/evidence-and-memory.md +6 -2
  127. package/docs/evolution.md +1 -1
  128. package/docs/exit-codes-and-output.md +1 -1
  129. package/docs/extensions-and-sharing.md +18 -11
  130. package/docs/fleet-demo-runbook.md +22 -23
  131. package/docs/fleet-dispatch.md +111 -12
  132. package/docs/glossary.md +107 -15
  133. package/docs/html/agents_blueprint.html +3 -3
  134. package/docs/html/alcf_blueprint.html +3 -3
  135. package/docs/html/architecture_blueprint.html +3 -3
  136. package/docs/html/commands_blueprint.html +187 -42
  137. package/docs/html/configuration_blueprint.html +7 -7
  138. package/docs/html/context_blueprint.html +3 -3
  139. package/docs/html/documentation_blueprint.html +1 -1
  140. package/docs/html/environment_blueprint.html +3 -3
  141. package/docs/html/eval_blueprint.html +4 -4
  142. package/docs/html/evals_internal_blueprint.html +3 -3
  143. package/docs/html/evolution_blueprint.html +3 -3
  144. package/docs/html/extensions_blueprint.html +3 -3
  145. package/docs/html/fleet_dispatch_blueprint.html +8 -8
  146. package/docs/html/index.html +5 -5
  147. package/docs/html/lifecycle_blueprint.html +4 -4
  148. package/docs/html/memory_blueprint.html +3 -3
  149. package/docs/html/middleware_blueprint.html +3 -3
  150. package/docs/html/models_blueprint.html +2 -2
  151. package/docs/html/observability_blueprint.html +2 -2
  152. package/docs/html/provider_adapter_blueprint.html +3 -3
  153. package/docs/html/safety_blueprint.html +3 -3
  154. package/docs/html/skills_blueprint.html +2 -2
  155. package/docs/html/soak_blueprint.html +3 -3
  156. package/docs/html/tool_usage_blueprint.html +4 -4
  157. package/docs/html/tools_blueprint.html +2 -2
  158. package/docs/html/trace_blueprint.html +3 -3
  159. package/docs/html/tui_design_blueprint.html +100 -40
  160. package/docs/html/validation_blueprint.html +1 -1
  161. package/docs/html/worker_dispatch_blueprint.html +3 -3
  162. package/docs/installation-and-lifecycle.md +62 -11
  163. package/docs/middleware-and-components.md +1 -1
  164. package/docs/model-catalog.md +2 -2
  165. package/docs/observability.md +3 -3
  166. package/docs/proactive-memory.md +12 -12
  167. package/docs/prompt-envelope-and-tools.md +2 -2
  168. package/docs/provider-adapter-cookbook.md +1 -1
  169. package/docs/release-cut-checklist.md +4 -0
  170. package/docs/safety-model.md +21 -5
  171. package/docs/scientific-validation.md +3 -3
  172. package/docs/session-lifecycle.md +2 -2
  173. package/docs/skills-marketplace.md +1 -1
  174. package/docs/time-conventions.md +101 -0
  175. package/docs/tool-usage.md +5 -4
  176. package/docs/trace-store.md +1 -1
  177. package/docs/troubleshooting.md +1 -1
  178. package/docs/tui-design.md +83 -27
  179. package/docs/worker-dispatch-mechanics.md +3 -3
  180. package/package.json +16 -16
  181. package/skills/README.md +5 -6
  182. package/skills/git/file-ticket/SKILL.md +107 -0
  183. package/skills/git/file-ticket/evals.md +54 -0
  184. package/skills/git/fix-issue/SKILL.md +106 -0
  185. package/skills/git/fix-issue/evals.md +71 -0
  186. package/skills/git/ship/SKILL.md +92 -0
  187. package/skills/git/ship/evals.md +72 -0
  188. package/skills/skill-marketplace.json +12 -20
  189. package/src/cli/acp.ts +88 -0
  190. package/src/cli/agents.ts +60 -0
  191. package/src/cli/args.ts +280 -0
  192. package/src/cli/argv.ts +170 -0
  193. package/src/cli/ask.ts +34 -0
  194. package/src/cli/auth.ts +468 -0
  195. package/src/cli/bootstrap-generate.ts +498 -0
  196. package/src/cli/clio.ts +34 -0
  197. package/src/cli/components.ts +195 -0
  198. package/src/cli/config-inspect.ts +330 -0
  199. package/src/cli/config.ts +72 -0
  200. package/src/cli/configure-interop.ts +67 -0
  201. package/src/cli/configure-layout.ts +172 -0
  202. package/src/cli/configure.ts +1487 -0
  203. package/src/cli/context-clear.ts +76 -0
  204. package/src/cli/context-index.ts +102 -0
  205. package/src/cli/context.ts +316 -0
  206. package/src/cli/default-target.ts +63 -0
  207. package/src/cli/docs.ts +340 -0
  208. package/src/cli/doctor.ts +54 -0
  209. package/src/cli/eval.ts +315 -0
  210. package/src/cli/evidence.ts +222 -0
  211. package/src/cli/evolve.ts +219 -0
  212. package/src/cli/extensions.ts +205 -0
  213. package/src/cli/fleet.ts +827 -0
  214. package/src/cli/index.ts +309 -0
  215. package/src/cli/init.ts +219 -0
  216. package/src/cli/initial-message.ts +47 -0
  217. package/src/cli/internal-dispatch.ts +55 -0
  218. package/src/cli/memory.ts +180 -0
  219. package/src/cli/models.ts +257 -0
  220. package/src/cli/modes/json-stream.ts +85 -0
  221. package/src/cli/modes/jsonl.ts +9 -0
  222. package/src/cli/modes/print.ts +589 -0
  223. package/src/cli/oauth-manual-input.ts +72 -0
  224. package/src/cli/oauth-select.ts +36 -0
  225. package/src/cli/output-guard.ts +48 -0
  226. package/src/cli/paths.ts +44 -0
  227. package/src/cli/provider-target.ts +113 -0
  228. package/src/cli/removal.ts +72 -0
  229. package/src/cli/reset.ts +253 -0
  230. package/src/cli/run.ts +497 -0
  231. package/src/cli/share.ts +243 -0
  232. package/src/cli/shared.ts +60 -0
  233. package/src/cli/skills-eval.ts +1225 -0
  234. package/src/cli/skills.ts +470 -0
  235. package/src/cli/steer-channel.ts +86 -0
  236. package/src/cli/targets.ts +1150 -0
  237. package/src/cli/text-layout.ts +102 -0
  238. package/src/cli/trace.ts +274 -0
  239. package/src/cli/uninstall.ts +405 -0
  240. package/src/cli/upgrade.ts +270 -0
  241. package/src/cli/usage.ts +869 -0
  242. package/src/cli/validate-model.ts +28 -0
  243. package/src/cli/version.ts +7 -0
  244. package/src/cli/wiki-generate.ts +509 -0
  245. package/src/core/artifact-paths.ts +43 -0
  246. package/src/core/bash-exec.ts +242 -0
  247. package/src/core/boot-trace.ts +37 -0
  248. package/src/core/bus-events.ts +728 -0
  249. package/src/core/bus-trace.ts +53 -0
  250. package/src/core/c-header-language.ts +265 -0
  251. package/src/core/clio-repo.ts +78 -0
  252. package/src/core/config.ts +1593 -0
  253. package/src/core/context-floor.ts +33 -0
  254. package/src/core/defaults.ts +625 -0
  255. package/src/core/domain-loader.ts +170 -0
  256. package/src/core/event-bus.ts +94 -0
  257. package/src/core/file-references.ts +282 -0
  258. package/src/core/guardrails.ts +185 -0
  259. package/src/core/headless-permission.ts +21 -0
  260. package/src/core/init.ts +223 -0
  261. package/src/core/json-payload.ts +58 -0
  262. package/src/core/package-root.ts +54 -0
  263. package/src/core/path-canonical.ts +37 -0
  264. package/src/core/process-identity.ts +41 -0
  265. package/src/core/recent-models.ts +91 -0
  266. package/src/core/release.ts +2 -0
  267. package/src/core/residency-protection.ts +56 -0
  268. package/src/core/resolve-config-value.ts +88 -0
  269. package/src/core/response-schema.ts +279 -0
  270. package/src/core/run-overrides.ts +77 -0
  271. package/src/core/safe-exec.ts +193 -0
  272. package/src/core/safe-id.ts +37 -0
  273. package/src/core/safe-resource-write.ts +162 -0
  274. package/src/core/session-routing.ts +339 -0
  275. package/src/core/settings-layers.ts +220 -0
  276. package/src/core/shared-bus.ts +8 -0
  277. package/src/core/shell-quote.ts +26 -0
  278. package/src/core/skill-activation.ts +190 -0
  279. package/src/core/startup-timer.ts +37 -0
  280. package/src/core/state-file-lock.ts +207 -0
  281. package/src/core/termination.ts +202 -0
  282. package/src/core/timers.ts +38 -0
  283. package/src/core/tool-names.ts +65 -0
  284. package/src/core/verification-scripts.ts +13 -0
  285. package/src/core/workspace-files.ts +492 -0
  286. package/src/core/xdg.ts +177 -0
  287. package/src/domains/agents/builtins/architect.md +4 -4
  288. package/src/domains/agents/builtins/coder.md +3 -2
  289. package/src/domains/agents/builtins/debugger.md +1 -1
  290. package/src/domains/agents/builtins/documenter.md +1 -1
  291. package/src/domains/agents/builtins/git-master.md +3 -3
  292. package/src/domains/agents/builtins/provenance.md +1 -1
  293. package/src/domains/agents/builtins/researcher.md +1 -1
  294. package/src/domains/agents/builtins/scout.md +1 -1
  295. package/src/domains/agents/builtins/tester.md +1 -1
  296. package/src/domains/agents/builtins/verifier.md +1 -1
  297. package/src/domains/agents/builtins/wiki-writer.md +1 -1
  298. package/src/domains/agents/catalog.ts +172 -0
  299. package/src/domains/agents/contract.ts +16 -0
  300. package/src/domains/agents/extension.ts +110 -0
  301. package/src/domains/agents/fleet-commands.ts +154 -0
  302. package/src/domains/agents/fleet-contract.ts +797 -0
  303. package/src/domains/agents/frontmatter.ts +40 -0
  304. package/src/domains/agents/index.ts +102 -0
  305. package/src/domains/agents/manifest.ts +6 -0
  306. package/src/domains/agents/recipe-schema.ts +202 -0
  307. package/src/domains/agents/recipe.ts +125 -0
  308. package/src/domains/agents/registry.ts +111 -0
  309. package/src/domains/agents/result-contract.ts +1265 -0
  310. package/src/domains/agents/spec.ts +371 -0
  311. package/src/domains/agents/write-boundary.ts +109 -0
  312. package/src/domains/components/diff.ts +99 -0
  313. package/src/domains/components/hash.ts +21 -0
  314. package/src/domains/components/index.ts +3 -0
  315. package/src/domains/components/scan.ts +304 -0
  316. package/src/domains/components/snapshot.ts +108 -0
  317. package/src/domains/components/types.ts +82 -0
  318. package/src/domains/config/agent-namespace.ts +13 -0
  319. package/src/domains/config/classify.ts +94 -0
  320. package/src/domains/config/contract.ts +22 -0
  321. package/src/domains/config/extension.ts +136 -0
  322. package/src/domains/config/index.ts +13 -0
  323. package/src/domains/config/keybindings.ts +164 -0
  324. package/src/domains/config/manifest.ts +6 -0
  325. package/src/domains/config/watcher.ts +41 -0
  326. package/src/domains/context/adoption.ts +880 -0
  327. package/src/domains/context/bootstrap-prompt.ts +273 -0
  328. package/src/domains/context/bootstrap.ts +1409 -0
  329. package/src/domains/context/clear.ts +86 -0
  330. package/src/domains/context/clio-md.ts +327 -0
  331. package/src/domains/context/codewiki/cmake.ts +356 -0
  332. package/src/domains/context/codewiki/cooperative.ts +46 -0
  333. package/src/domains/context/codewiki/digest.ts +135 -0
  334. package/src/domains/context/codewiki/grammar-assets.ts +33 -0
  335. package/src/domains/context/codewiki/indexer.ts +1614 -0
  336. package/src/domains/context/codewiki/tree-sitter.ts +771 -0
  337. package/src/domains/context/contract.ts +62 -0
  338. package/src/domains/context/excluded-dirs.ts +1 -0
  339. package/src/domains/context/extension.ts +407 -0
  340. package/src/domains/context/fingerprint.ts +196 -0
  341. package/src/domains/context/index.ts +164 -0
  342. package/src/domains/context/init-options.ts +69 -0
  343. package/src/domains/context/manifest.ts +6 -0
  344. package/src/domains/context/operator-profile.ts +157 -0
  345. package/src/domains/context/project-metadata.ts +510 -0
  346. package/src/domains/context/project-rules.ts +227 -0
  347. package/src/domains/context/prompt-context.ts +62 -0
  348. package/src/domains/context/refresh.ts +170 -0
  349. package/src/domains/context/sibling-files.ts +10 -0
  350. package/src/domains/context/state.ts +237 -0
  351. package/src/domains/context/wiki/assemble.ts +303 -0
  352. package/src/domains/context/wiki/frontmatter.ts +210 -0
  353. package/src/domains/context/wiki/generate.ts +590 -0
  354. package/src/domains/context/wiki/layout.ts +111 -0
  355. package/src/domains/context/wiki/meta.ts +221 -0
  356. package/src/domains/context/wiki/plan-store.ts +217 -0
  357. package/src/domains/context/wiki/plan.ts +250 -0
  358. package/src/domains/context/wiki/prompts.ts +270 -0
  359. package/src/domains/context/wiki/staleness.ts +198 -0
  360. package/src/domains/dispatch/active-route-planner.ts +168 -0
  361. package/src/domains/dispatch/admission-queue.ts +150 -0
  362. package/src/domains/dispatch/admission.ts +334 -0
  363. package/src/domains/dispatch/agent-candidates.ts +307 -0
  364. package/src/domains/dispatch/agent-ledger-hub.ts +71 -0
  365. package/src/domains/dispatch/agent-ledger-store.ts +257 -0
  366. package/src/domains/dispatch/agent-ledger.ts +212 -0
  367. package/src/domains/dispatch/agent-plan-adapter.ts +85 -0
  368. package/src/domains/dispatch/assignment-events.ts +132 -0
  369. package/src/domains/dispatch/assignment-reconcile.ts +76 -0
  370. package/src/domains/dispatch/assignment-store.ts +162 -0
  371. package/src/domains/dispatch/assignment.ts +278 -0
  372. package/src/domains/dispatch/backoff.ts +59 -0
  373. package/src/domains/dispatch/batch-store.ts +134 -0
  374. package/src/domains/dispatch/batch-tracker.ts +66 -0
  375. package/src/domains/dispatch/capability-match.ts +155 -0
  376. package/src/domains/dispatch/capacity-lease.ts +413 -0
  377. package/src/domains/dispatch/code-step-store.ts +52 -0
  378. package/src/domains/dispatch/code-step.ts +368 -0
  379. package/src/domains/dispatch/contract.ts +278 -0
  380. package/src/domains/dispatch/dedup.ts +133 -0
  381. package/src/domains/dispatch/event-pump.ts +251 -0
  382. package/src/domains/dispatch/execution-handoff.ts +29 -0
  383. package/src/domains/dispatch/execution-plan.ts +328 -0
  384. package/src/domains/dispatch/execution-role.ts +277 -0
  385. package/src/domains/dispatch/execution-scheduler.ts +583 -0
  386. package/src/domains/dispatch/extension.ts +5869 -0
  387. package/src/domains/dispatch/failure-classification.ts +141 -0
  388. package/src/domains/dispatch/fleet-plan.ts +225 -0
  389. package/src/domains/dispatch/fleet-preflight.ts +394 -0
  390. package/src/domains/dispatch/gate-decisions.ts +1040 -0
  391. package/src/domains/dispatch/gate-role-prompts.ts +58 -0
  392. package/src/domains/dispatch/heartbeat.ts +27 -0
  393. package/src/domains/dispatch/index.ts +137 -0
  394. package/src/domains/dispatch/joint-route-adapter.ts +114 -0
  395. package/src/domains/dispatch/joint-route-resolver.ts +283 -0
  396. package/src/domains/dispatch/manifest.ts +6 -0
  397. package/src/domains/dispatch/orphan-recovery.ts +224 -0
  398. package/src/domains/dispatch/outcome.ts +109 -0
  399. package/src/domains/dispatch/phase-timing.ts +47 -0
  400. package/src/domains/dispatch/placement.ts +202 -0
  401. package/src/domains/dispatch/receipt-findings.ts +255 -0
  402. package/src/domains/dispatch/receipt-integrity.ts +378 -0
  403. package/src/domains/dispatch/recovery-candidates.ts +74 -0
  404. package/src/domains/dispatch/reproducibility.ts +48 -0
  405. package/src/domains/dispatch/reservation-store.ts +501 -0
  406. package/src/domains/dispatch/route-approval.ts +177 -0
  407. package/src/domains/dispatch/route-candidates.ts +57 -0
  408. package/src/domains/dispatch/route-decision.ts +1079 -0
  409. package/src/domains/dispatch/route-facts.ts +187 -0
  410. package/src/domains/dispatch/route-history.ts +238 -0
  411. package/src/domains/dispatch/route-observer.ts +488 -0
  412. package/src/domains/dispatch/route-policy.ts +327 -0
  413. package/src/domains/dispatch/route-quality.ts +256 -0
  414. package/src/domains/dispatch/route-readiness.ts +120 -0
  415. package/src/domains/dispatch/routing-intent.ts +287 -0
  416. package/src/domains/dispatch/run-identity.ts +56 -0
  417. package/src/domains/dispatch/scout-transition.ts +130 -0
  418. package/src/domains/dispatch/state.ts +277 -0
  419. package/src/domains/dispatch/tool-stats.ts +132 -0
  420. package/src/domains/dispatch/transport.ts +209 -0
  421. package/src/domains/dispatch/types.ts +785 -0
  422. package/src/domains/dispatch/validation-grounding.ts +154 -0
  423. package/src/domains/dispatch/validation.ts +721 -0
  424. package/src/domains/dispatch/worker-protocol.ts +275 -0
  425. package/src/domains/dispatch/worker-spawn.ts +566 -0
  426. package/src/domains/dispatch/write-boundary-enforcer.ts +97 -0
  427. package/src/domains/dispatch/write-boundary.ts +426 -0
  428. package/src/domains/eval/artifacts/redact.ts +42 -0
  429. package/src/domains/eval/artifacts/store.ts +173 -0
  430. package/src/domains/eval/compare/compare.ts +33 -0
  431. package/src/domains/eval/compare/gates.ts +66 -0
  432. package/src/domains/eval/compare/thresholds.ts +92 -0
  433. package/src/domains/eval/harness-metrics.ts +62 -0
  434. package/src/domains/eval/index.ts +66 -0
  435. package/src/domains/eval/metrics/chaos-stream.ts +93 -0
  436. package/src/domains/eval/metrics/context.ts +34 -0
  437. package/src/domains/eval/metrics/coverage.ts +21 -0
  438. package/src/domains/eval/metrics/evidence.ts +182 -0
  439. package/src/domains/eval/metrics/fleet-loop-stream.ts +196 -0
  440. package/src/domains/eval/metrics/invariants.ts +736 -0
  441. package/src/domains/eval/metrics/latency.ts +4 -0
  442. package/src/domains/eval/metrics/token-stream.ts +139 -0
  443. package/src/domains/eval/metrics/tokens.ts +51 -0
  444. package/src/domains/eval/metrics/tool-calls.ts +7 -0
  445. package/src/domains/eval/proactive-memory.ts +279 -0
  446. package/src/domains/eval/provenance.ts +35 -0
  447. package/src/domains/eval/report.ts +234 -0
  448. package/src/domains/eval/reports/json.ts +5 -0
  449. package/src/domains/eval/reports/junit.ts +23 -0
  450. package/src/domains/eval/reports/markdown.ts +20 -0
  451. package/src/domains/eval/reports/swe-jsonl.ts +16 -0
  452. package/src/domains/eval/reports/text.ts +25 -0
  453. package/src/domains/eval/run-compare.ts +274 -0
  454. package/src/domains/eval/runner.ts +244 -0
  455. package/src/domains/eval/runners/clio-run.ts +148 -0
  456. package/src/domains/eval/runners/context-index.ts +56 -0
  457. package/src/domains/eval/runners/context-init.ts +203 -0
  458. package/src/domains/eval/runners/external-command.ts +350 -0
  459. package/src/domains/eval/runtime-paths.ts +171 -0
  460. package/src/domains/eval/schema/artifact.ts +75 -0
  461. package/src/domains/eval/schema/suite.ts +235 -0
  462. package/src/domains/eval/schema/validate.ts +339 -0
  463. package/src/domains/eval/store.ts +392 -0
  464. package/src/domains/eval/suites/load.ts +74 -0
  465. package/src/domains/eval/suites/matrix.ts +19 -0
  466. package/src/domains/eval/suites/resolve.ts +44 -0
  467. package/src/domains/eval/suites/run.ts +325 -0
  468. package/src/domains/eval/task-file.ts +186 -0
  469. package/src/domains/eval/types.ts +141 -0
  470. package/src/domains/eval/verifiers/command.ts +27 -0
  471. package/src/domains/eval/verifiers/file-exists.ts +6 -0
  472. package/src/domains/eval/verifiers/patch.ts +22 -0
  473. package/src/domains/eval/workspaces/git.ts +41 -0
  474. package/src/domains/eval/workspaces/local.ts +17 -0
  475. package/src/domains/eval/workspaces/temp-copy.ts +29 -0
  476. package/src/domains/evidence/build.ts +1714 -0
  477. package/src/domains/evidence/eval.ts +605 -0
  478. package/src/domains/evidence/failure-attribution.ts +54 -0
  479. package/src/domains/evidence/finish-contract-map.ts +42 -0
  480. package/src/domains/evidence/index.ts +55 -0
  481. package/src/domains/evidence/provenance.ts +200 -0
  482. package/src/domains/evidence/redact.ts +81 -0
  483. package/src/domains/evidence/store.ts +291 -0
  484. package/src/domains/evidence/types.ts +325 -0
  485. package/src/domains/evolution/SELF_EDIT_GATE.md +91 -0
  486. package/src/domains/evolution/index.ts +26 -0
  487. package/src/domains/evolution/manifest.ts +154 -0
  488. package/src/domains/evolution/validate.ts +304 -0
  489. package/src/domains/extensions/contract.ts +22 -0
  490. package/src/domains/extensions/discovery.ts +193 -0
  491. package/src/domains/extensions/extension.ts +46 -0
  492. package/src/domains/extensions/index.ts +34 -0
  493. package/src/domains/extensions/manager.ts +30 -0
  494. package/src/domains/extensions/manifest.ts +6 -0
  495. package/src/domains/extensions/resources.ts +38 -0
  496. package/src/domains/extensions/state.ts +224 -0
  497. package/src/domains/extensions/types.ts +83 -0
  498. package/src/domains/interop/consent.ts +185 -0
  499. package/src/domains/interop/contract.ts +29 -0
  500. package/src/domains/interop/detect.ts +173 -0
  501. package/src/domains/interop/extension.ts +59 -0
  502. package/src/domains/interop/index.ts +34 -0
  503. package/src/domains/interop/manifest.ts +6 -0
  504. package/src/domains/interop/registry.ts +139 -0
  505. package/src/domains/interop/state.ts +92 -0
  506. package/src/domains/interop/types.ts +109 -0
  507. package/src/domains/lifecycle/contract.ts +11 -0
  508. package/src/domains/lifecycle/doctor.ts +536 -0
  509. package/src/domains/lifecycle/extension.ts +23 -0
  510. package/src/domains/lifecycle/index.ts +21 -0
  511. package/src/domains/lifecycle/install-method.ts +51 -0
  512. package/src/domains/lifecycle/manifest.ts +6 -0
  513. package/src/domains/lifecycle/migrations/index.ts +94 -0
  514. package/src/domains/lifecycle/state.ts +93 -0
  515. package/src/domains/lifecycle/upgrade-notice.ts +35 -0
  516. package/src/domains/lifecycle/version.ts +35 -0
  517. package/src/domains/memory/index.ts +128 -0
  518. package/src/domains/memory/operations.ts +143 -0
  519. package/src/domains/memory/prompt-section.ts +86 -0
  520. package/src/domains/memory/proposal.ts +180 -0
  521. package/src/domains/memory/store.ts +163 -0
  522. package/src/domains/memory/task-bank.ts +229 -0
  523. package/src/domains/memory/task-memory-handoff.ts +222 -0
  524. package/src/domains/memory/task-memory-policy.ts +602 -0
  525. package/src/domains/memory/task-memory-status.ts +58 -0
  526. package/src/domains/memory/task-memory-telemetry.ts +346 -0
  527. package/src/domains/memory/task-memory-trace.ts +89 -0
  528. package/src/domains/memory/types.ts +74 -0
  529. package/src/domains/memory/validate.ts +297 -0
  530. package/src/domains/middleware/budget.ts +216 -0
  531. package/src/domains/middleware/contract.ts +35 -0
  532. package/src/domains/middleware/dispatch-nudge.ts +469 -0
  533. package/src/domains/middleware/extension.ts +129 -0
  534. package/src/domains/middleware/hook-receipts.ts +70 -0
  535. package/src/domains/middleware/hooks-io.ts +174 -0
  536. package/src/domains/middleware/hooks.ts +432 -0
  537. package/src/domains/middleware/index.ts +154 -0
  538. package/src/domains/middleware/manifest.ts +6 -0
  539. package/src/domains/middleware/memory-intervention.ts +701 -0
  540. package/src/domains/middleware/rules.ts +42 -0
  541. package/src/domains/middleware/runtime.ts +333 -0
  542. package/src/domains/middleware/skills-reminder.ts +187 -0
  543. package/src/domains/middleware/snapshot.ts +71 -0
  544. package/src/domains/middleware/stalled-turn.ts +71 -0
  545. package/src/domains/middleware/task-board-reminder.ts +87 -0
  546. package/src/domains/middleware/task-nudge.ts +77 -0
  547. package/src/domains/middleware/tool-choice-control.ts +42 -0
  548. package/src/domains/middleware/types.ts +131 -0
  549. package/src/domains/middleware/validate.ts +329 -0
  550. package/src/domains/observability/accountability.ts +67 -0
  551. package/src/domains/observability/contract.ts +168 -0
  552. package/src/domains/observability/cost.ts +206 -0
  553. package/src/domains/observability/evidence-index.ts +123 -0
  554. package/src/domains/observability/extension.ts +281 -0
  555. package/src/domains/observability/index.ts +78 -0
  556. package/src/domains/observability/manifest.ts +10 -0
  557. package/src/domains/observability/metrics.ts +37 -0
  558. package/src/domains/observability/projection.ts +477 -0
  559. package/src/domains/observability/telemetry.ts +89 -0
  560. package/src/domains/observability/trace-store.ts +1454 -0
  561. package/src/domains/prompts/compiler.ts +556 -0
  562. package/src/domains/prompts/contract.ts +30 -0
  563. package/src/domains/prompts/extension.ts +304 -0
  564. package/src/domains/prompts/fragment-loader.ts +139 -0
  565. package/src/domains/prompts/fragments/identity/clio.md +3 -2
  566. package/src/domains/prompts/fragments/identity/self-awareness.md +63 -0
  567. package/src/domains/prompts/fragments/operating/contract.md +10 -1
  568. package/src/domains/prompts/hash.ts +73 -0
  569. package/src/domains/prompts/index.ts +32 -0
  570. package/src/domains/prompts/manifest.ts +8 -0
  571. package/src/domains/prompts/memory-intervention.ts +57 -0
  572. package/src/domains/prompts/preload.ts +82 -0
  573. package/src/domains/providers/auth/api-key.ts +47 -0
  574. package/src/domains/providers/auth/backend-file.ts +69 -0
  575. package/src/domains/providers/auth/backend-memory.ts +17 -0
  576. package/src/domains/providers/auth/index.ts +37 -0
  577. package/src/domains/providers/auth/oauth.ts +36 -0
  578. package/src/domains/providers/auth/storage.ts +667 -0
  579. package/src/domains/providers/capabilities.ts +50 -0
  580. package/src/domains/providers/catalog.ts +162 -0
  581. package/src/domains/providers/contract.ts +151 -0
  582. package/src/domains/providers/credentials.ts +20 -0
  583. package/src/domains/providers/eligibility.ts +24 -0
  584. package/src/domains/providers/errors.ts +64 -0
  585. package/src/domains/providers/extension.ts +536 -0
  586. package/src/domains/providers/index.ts +142 -0
  587. package/src/domains/providers/knowledge-base-path.ts +102 -0
  588. package/src/domains/providers/manifest.ts +6 -0
  589. package/src/domains/providers/model-capabilities.ts +136 -0
  590. package/src/domains/providers/model-discovery.ts +144 -0
  591. package/src/domains/providers/model-family.ts +32 -0
  592. package/src/domains/providers/model-runtime-capabilities.ts +712 -0
  593. package/src/domains/providers/models/local-models/clio-local-coding-targets.yaml +2 -2
  594. package/src/domains/providers/plugins.ts +59 -0
  595. package/src/domains/providers/probe/fingerprint.ts +44 -0
  596. package/src/domains/providers/probe/http.ts +114 -0
  597. package/src/domains/providers/probe/reasoning.ts +111 -0
  598. package/src/domains/providers/registry.ts +166 -0
  599. package/src/domains/providers/resolver.ts +156 -0
  600. package/src/domains/providers/runtime-resolution.ts +720 -0
  601. package/src/domains/providers/runtimes/antigravity/antigravity-code.ts +90 -0
  602. package/src/domains/providers/runtimes/builtins.ts +78 -0
  603. package/src/domains/providers/runtimes/claude/claude-code.ts +38 -0
  604. package/src/domains/providers/runtimes/claude/claude-sdk.ts +38 -0
  605. package/src/domains/providers/runtimes/claude/common.ts +55 -0
  606. package/src/domains/providers/runtimes/cloud/alcf.ts +171 -0
  607. package/src/domains/providers/runtimes/cloud/anthropic-max.ts +71 -0
  608. package/src/domains/providers/runtimes/cloud/anthropic.ts +43 -0
  609. package/src/domains/providers/runtimes/cloud/bedrock.ts +35 -0
  610. package/src/domains/providers/runtimes/cloud/deepseek.ts +50 -0
  611. package/src/domains/providers/runtimes/cloud/google.ts +41 -0
  612. package/src/domains/providers/runtimes/cloud/groq.ts +46 -0
  613. package/src/domains/providers/runtimes/cloud/mistral.ts +46 -0
  614. package/src/domains/providers/runtimes/cloud/openai-codex.ts +49 -0
  615. package/src/domains/providers/runtimes/cloud/openai.ts +46 -0
  616. package/src/domains/providers/runtimes/cloud/openrouter.ts +111 -0
  617. package/src/domains/providers/runtimes/common/lmstudio-logger.ts +32 -0
  618. package/src/domains/providers/runtimes/common/local-synth.ts +165 -0
  619. package/src/domains/providers/runtimes/common/probe-helpers.ts +474 -0
  620. package/src/domains/providers/runtimes/local-native/lemonade-anthropic.ts +27 -0
  621. package/src/domains/providers/runtimes/local-native/lemonade-openai.ts +26 -0
  622. package/src/domains/providers/runtimes/local-native/llamacpp-anthropic.ts +88 -0
  623. package/src/domains/providers/runtimes/local-native/llamacpp-completion.ts +207 -0
  624. package/src/domains/providers/runtimes/local-native/llamacpp-embed.ts +180 -0
  625. package/src/domains/providers/runtimes/local-native/llamacpp-rerank.ts +126 -0
  626. package/src/domains/providers/runtimes/local-native/llamacpp.ts +98 -0
  627. package/src/domains/providers/runtimes/local-native/lmstudio-native.ts +491 -0
  628. package/src/domains/providers/runtimes/local-native/ollama-native.ts +117 -0
  629. package/src/domains/providers/runtimes/local-native/sglang.ts +26 -0
  630. package/src/domains/providers/runtimes/local-native/vllm.ts +27 -0
  631. package/src/domains/providers/runtimes/protocol/anthropic-compat.ts +104 -0
  632. package/src/domains/providers/runtimes/protocol/anthropic-messages.ts +28 -0
  633. package/src/domains/providers/runtimes/protocol/bedrock.ts +26 -0
  634. package/src/domains/providers/runtimes/protocol/google.ts +27 -0
  635. package/src/domains/providers/runtimes/protocol/openai-compat.ts +145 -0
  636. package/src/domains/providers/support.ts +238 -0
  637. package/src/domains/providers/thinking-control-policy.ts +85 -0
  638. package/src/domains/providers/types/capability-flags.ts +114 -0
  639. package/src/domains/providers/types/cost-provenance.ts +7 -0
  640. package/src/domains/providers/types/inference.ts +41 -0
  641. package/src/domains/providers/types/knowledge-base.ts +156 -0
  642. package/src/domains/providers/types/local-model-quirks.ts +218 -0
  643. package/src/domains/providers/types/runtime-descriptor.ts +170 -0
  644. package/src/domains/providers/types/target-descriptor.ts +38 -0
  645. package/src/domains/resources/collision.ts +92 -0
  646. package/src/domains/resources/common-loader.ts +140 -0
  647. package/src/domains/resources/contract.ts +25 -0
  648. package/src/domains/resources/extension.ts +61 -0
  649. package/src/domains/resources/index.ts +74 -0
  650. package/src/domains/resources/loader.ts +86 -0
  651. package/src/domains/resources/manifest.ts +6 -0
  652. package/src/domains/resources/prompts/loader.ts +232 -0
  653. package/src/domains/resources/prompts/substitute.ts +65 -0
  654. package/src/domains/resources/skills/content-hash.ts +105 -0
  655. package/src/domains/resources/skills/evals.ts +175 -0
  656. package/src/domains/resources/skills/install.ts +410 -0
  657. package/src/domains/resources/skills/loader.ts +978 -0
  658. package/src/domains/resources/skills/marketplace.ts +255 -0
  659. package/src/domains/resources/skills/provenance-pin.ts +115 -0
  660. package/src/domains/safety/action-classifier.ts +268 -0
  661. package/src/domains/safety/approval-axis.ts +23 -0
  662. package/src/domains/safety/audit.ts +487 -0
  663. package/src/domains/safety/autonomy.ts +174 -0
  664. package/src/domains/safety/call-target.ts +59 -0
  665. package/src/domains/safety/contract.ts +65 -0
  666. package/src/domains/safety/damage-control.ts +44 -0
  667. package/src/domains/safety/default-path-policy.ts +117 -0
  668. package/src/domains/safety/extension.ts +383 -0
  669. package/src/domains/safety/finish-contract-registration.ts +128 -0
  670. package/src/domains/safety/finish-contract.ts +554 -0
  671. package/src/domains/safety/index.ts +44 -0
  672. package/src/domains/safety/loop-detector.ts +143 -0
  673. package/src/domains/safety/manifest.ts +6 -0
  674. package/src/domains/safety/path-policy.ts +164 -0
  675. package/src/domains/safety/policy-engine.ts +994 -0
  676. package/src/domains/safety/project-policy.ts +391 -0
  677. package/src/domains/safety/protected-artifacts-registration.ts +189 -0
  678. package/src/domains/safety/protected-artifacts.ts +993 -0
  679. package/src/domains/safety/rejection-feedback.ts +102 -0
  680. package/src/domains/safety/rigor.ts +72 -0
  681. package/src/domains/safety/rule-compiler.ts +48 -0
  682. package/src/domains/safety/rule-pack-loader.ts +92 -0
  683. package/src/domains/safety/run-effects.ts +197 -0
  684. package/src/domains/safety/scope.ts +61 -0
  685. package/src/domains/scheduling/budget.ts +35 -0
  686. package/src/domains/scheduling/cluster.ts +175 -0
  687. package/src/domains/scheduling/contract.ts +30 -0
  688. package/src/domains/scheduling/extension.ts +91 -0
  689. package/src/domains/scheduling/index.ts +12 -0
  690. package/src/domains/scheduling/manifest.ts +6 -0
  691. package/src/domains/session/archive-readers.ts +208 -0
  692. package/src/domains/session/checkpoint.ts +38 -0
  693. package/src/domains/session/compaction/auto.ts +89 -0
  694. package/src/domains/session/compaction/branch-summary.ts +186 -0
  695. package/src/domains/session/compaction/compact.ts +493 -0
  696. package/src/domains/session/compaction/cut-point.ts +171 -0
  697. package/src/domains/session/compaction/defaults.ts +34 -0
  698. package/src/domains/session/compaction/mask-observations.ts +265 -0
  699. package/src/domains/session/compaction/session-entries.ts +18 -0
  700. package/src/domains/session/compaction/tokens.ts +147 -0
  701. package/src/domains/session/context-accounting.ts +583 -0
  702. package/src/domains/session/context-ledger.ts +348 -0
  703. package/src/domains/session/contract.ts +144 -0
  704. package/src/domains/session/cwd-fallback.ts +65 -0
  705. package/src/domains/session/entries.ts +509 -0
  706. package/src/domains/session/extension.ts +322 -0
  707. package/src/domains/session/history.ts +172 -0
  708. package/src/domains/session/index.ts +84 -0
  709. package/src/domains/session/manager.ts +128 -0
  710. package/src/domains/session/manifest.ts +6 -0
  711. package/src/domains/session/migrations/index.ts +27 -0
  712. package/src/domains/session/prompt-manifest.ts +173 -0
  713. package/src/domains/session/protected-artifact-journal.ts +241 -0
  714. package/src/domains/session/protected-artifacts.ts +90 -0
  715. package/src/domains/session/retry.ts +238 -0
  716. package/src/domains/session/task-board.ts +375 -0
  717. package/src/domains/session/tree/active-path.ts +98 -0
  718. package/src/domains/session/tree/fork.ts +160 -0
  719. package/src/domains/session/tree/manager.ts +128 -0
  720. package/src/domains/session/tree/navigator.ts +177 -0
  721. package/src/domains/session/tree/preview.ts +259 -0
  722. package/src/domains/session/usage.ts +148 -0
  723. package/src/domains/session/workspace/git-probe.ts +161 -0
  724. package/src/domains/session/workspace/index.ts +4 -0
  725. package/src/domains/session/workspace/project-type.ts +217 -0
  726. package/src/domains/session/workspace/snapshot.ts +74 -0
  727. package/src/domains/share/archive.ts +684 -0
  728. package/src/domains/share/contract.ts +9 -0
  729. package/src/domains/share/extension.ts +26 -0
  730. package/src/domains/share/index.ts +28 -0
  731. package/src/domains/share/manifest.ts +6 -0
  732. package/src/engine/acp/adapter.ts +397 -0
  733. package/src/engine/acp/errors.ts +32 -0
  734. package/src/engine/acp/event-mapper.ts +154 -0
  735. package/src/engine/acp/server.ts +656 -0
  736. package/src/engine/acp/tool-mediator.ts +686 -0
  737. package/src/engine/acp/transport.ts +654 -0
  738. package/src/engine/acp/types.ts +170 -0
  739. package/src/engine/agent.ts +29 -0
  740. package/src/engine/ai.ts +202 -0
  741. package/src/engine/alcf-oauth.ts +198 -0
  742. package/src/engine/antigravity/subprocess-runtime.ts +205 -0
  743. package/src/engine/apis/degraded-inference.ts +95 -0
  744. package/src/engine/apis/index.ts +20 -0
  745. package/src/engine/apis/llamacpp-residency.ts +252 -0
  746. package/src/engine/apis/lmstudio-native.ts +1438 -0
  747. package/src/engine/apis/lmstudio-residency.ts +92 -0
  748. package/src/engine/apis/ollama-native.ts +507 -0
  749. package/src/engine/apis/openai-completions.ts +716 -0
  750. package/src/engine/apis/output-budget.ts +99 -0
  751. package/src/engine/apis/residency-lock.ts +29 -0
  752. package/src/engine/apis/residency.ts +640 -0
  753. package/src/engine/apis/resident-models.ts +42 -0
  754. package/src/engine/apis/sampling-overrides.ts +38 -0
  755. package/src/engine/apis/thinking-replay.ts +11 -0
  756. package/src/engine/claude/sdk-runtime.ts +685 -0
  757. package/src/engine/claude/session-id.ts +5 -0
  758. package/src/engine/claude/subprocess-runtime.ts +316 -0
  759. package/src/engine/claude/tool-safety.ts +400 -0
  760. package/src/engine/external-subprocess.ts +23 -0
  761. package/src/engine/harmony-response.ts +165 -0
  762. package/src/engine/loop-guard.ts +1127 -0
  763. package/src/engine/oauth.ts +192 -0
  764. package/src/engine/pi-mono-names.ts +13 -0
  765. package/src/engine/provider-payload.ts +315 -0
  766. package/src/engine/select-list.ts +53 -0
  767. package/src/engine/session.ts +834 -0
  768. package/src/engine/strip-tokenizer-sentinels.ts +125 -0
  769. package/src/engine/tui.ts +103 -0
  770. package/src/engine/types.ts +66 -0
  771. package/src/engine/worker-events.ts +100 -0
  772. package/src/engine/worker-runtime.ts +1022 -0
  773. package/src/engine/worker-tools.ts +203 -0
  774. package/src/entry/orchestrator.ts +1873 -0
  775. package/src/interactive/application-controller.ts +346 -0
  776. package/src/interactive/bus-notices.ts +265 -0
  777. package/src/interactive/chat-loop-messages.ts +626 -0
  778. package/src/interactive/chat-loop-policy.ts +18 -0
  779. package/src/interactive/chat-loop.ts +865 -0
  780. package/src/interactive/chat-panel.ts +1694 -0
  781. package/src/interactive/chat-renderer.ts +1038 -0
  782. package/src/interactive/clio-editor.ts +166 -0
  783. package/src/interactive/command-fallbacks.ts +23 -0
  784. package/src/interactive/command-output.ts +85 -0
  785. package/src/interactive/context-activity.ts +203 -0
  786. package/src/interactive/context-meter.ts +223 -0
  787. package/src/interactive/context-overlay.ts +220 -0
  788. package/src/interactive/cost-overlay.ts +279 -0
  789. package/src/interactive/dispatch-board.ts +1319 -0
  790. package/src/interactive/dispatch-steering.ts +86 -0
  791. package/src/interactive/editor-bash.ts +46 -0
  792. package/src/interactive/editor-steer.ts +51 -0
  793. package/src/interactive/editor-submit.ts +303 -0
  794. package/src/interactive/external-editor.ts +67 -0
  795. package/src/interactive/follow-up-queue-panel.ts +75 -0
  796. package/src/interactive/footer/dashboard.ts +534 -0
  797. package/src/interactive/footer/notifications.ts +275 -0
  798. package/src/interactive/footer/widgets.ts +1246 -0
  799. package/src/interactive/footer-panel.ts +187 -0
  800. package/src/interactive/format-time.ts +85 -0
  801. package/src/interactive/index.ts +11 -0
  802. package/src/interactive/interactive-application.ts +755 -0
  803. package/src/interactive/interactive-event-projection.ts +380 -0
  804. package/src/interactive/interactive-input-runtime.ts +261 -0
  805. package/src/interactive/interactive-presentation.ts +452 -0
  806. package/src/interactive/interactive-shell.ts +134 -0
  807. package/src/interactive/interactive-slash-runtime.ts +492 -0
  808. package/src/interactive/interactive-subscriptions.ts +111 -0
  809. package/src/interactive/interactive-tickers.ts +152 -0
  810. package/src/interactive/keybinding-manager.ts +496 -0
  811. package/src/interactive/layout.ts +20 -0
  812. package/src/interactive/leader-key.ts +143 -0
  813. package/src/interactive/loop-guard-interrupt.ts +101 -0
  814. package/src/interactive/memory-overlay.ts +280 -0
  815. package/src/interactive/overlay-ask-user-lifecycle.ts +110 -0
  816. package/src/interactive/overlay-auth-lifecycle.ts +308 -0
  817. package/src/interactive/overlay-frame.ts +502 -0
  818. package/src/interactive/overlay-general-openers.ts +217 -0
  819. package/src/interactive/overlay-key-routing.ts +213 -0
  820. package/src/interactive/overlay-lifecycle.ts +413 -0
  821. package/src/interactive/overlay-model-selectors.ts +127 -0
  822. package/src/interactive/overlay-permission-lifecycle.ts +344 -0
  823. package/src/interactive/overlay-resource-openers.ts +112 -0
  824. package/src/interactive/overlay-session-lifecycle.ts +270 -0
  825. package/src/interactive/overlay-transitions.ts +57 -0
  826. package/src/interactive/overlays/agents.ts +69 -0
  827. package/src/interactive/overlays/ask-user.ts +921 -0
  828. package/src/interactive/overlays/auth-dialog.ts +221 -0
  829. package/src/interactive/overlays/context-reset.ts +108 -0
  830. package/src/interactive/overlays/cwd-fallback.ts +141 -0
  831. package/src/interactive/overlays/extensions.ts +54 -0
  832. package/src/interactive/overlays/help-reference.ts +140 -0
  833. package/src/interactive/overlays/interop.ts +139 -0
  834. package/src/interactive/overlays/keybinding-detail.ts +90 -0
  835. package/src/interactive/overlays/list-overlay.ts +606 -0
  836. package/src/interactive/overlays/message-picker.ts +155 -0
  837. package/src/interactive/overlays/model-selector.ts +1091 -0
  838. package/src/interactive/overlays/prompts.ts +72 -0
  839. package/src/interactive/overlays/session-selector-search.ts +31 -0
  840. package/src/interactive/overlays/session-selector.ts +266 -0
  841. package/src/interactive/overlays/settings.ts +3363 -0
  842. package/src/interactive/overlays/skills-hub.ts +237 -0
  843. package/src/interactive/overlays/tree-selector.ts +387 -0
  844. package/src/interactive/permission-overlay.ts +170 -0
  845. package/src/interactive/render-trace.ts +125 -0
  846. package/src/interactive/renderers/branch-summary.ts +86 -0
  847. package/src/interactive/renderers/code-ink.ts +420 -0
  848. package/src/interactive/renderers/compaction-summary.ts +112 -0
  849. package/src/interactive/renderers/diff.ts +126 -0
  850. package/src/interactive/renderers/notice.ts +28 -0
  851. package/src/interactive/renderers/retry-status.ts +31 -0
  852. package/src/interactive/renderers/structured.ts +89 -0
  853. package/src/interactive/renderers/tool-execution.ts +1104 -0
  854. package/src/interactive/renderers/worker-entry.ts +264 -0
  855. package/src/interactive/session-last-turn.ts +186 -0
  856. package/src/interactive/session-transcript.ts +105 -0
  857. package/src/interactive/session-usage-reseed.ts +74 -0
  858. package/src/interactive/slash-autocomplete.ts +364 -0
  859. package/src/interactive/slash-commands.ts +1319 -0
  860. package/src/interactive/slash-spec.ts +453 -0
  861. package/src/interactive/status/controller.ts +256 -0
  862. package/src/interactive/status/index.ts +23 -0
  863. package/src/interactive/status/state-machine.ts +530 -0
  864. package/src/interactive/status/summary.ts +170 -0
  865. package/src/interactive/status/types.ts +121 -0
  866. package/src/interactive/status/verbs.ts +163 -0
  867. package/src/interactive/status/watchdog.ts +24 -0
  868. package/src/interactive/tasks-overlay.ts +186 -0
  869. package/src/interactive/theme/components.ts +55 -0
  870. package/src/interactive/theme/glyphs.ts +70 -0
  871. package/src/interactive/theme/index.ts +15 -0
  872. package/src/interactive/theme/labels.ts +88 -0
  873. package/src/interactive/theme/rules.ts +138 -0
  874. package/src/interactive/theme/segments.ts +88 -0
  875. package/src/interactive/theme/tokens.ts +121 -0
  876. package/src/interactive/thinking-level-policy.ts +6 -0
  877. package/src/interactive/tool-prose-loop.ts +122 -0
  878. package/src/interactive/tool-prose-registration.ts +48 -0
  879. package/src/interactive/turn-context.ts +854 -0
  880. package/src/interactive/turn-middleware.ts +283 -0
  881. package/src/interactive/turn-persistence.ts +441 -0
  882. package/src/interactive/turn-queues.ts +169 -0
  883. package/src/interactive/turn-recovery.ts +306 -0
  884. package/src/interactive/turn-runtime.ts +802 -0
  885. package/src/interactive/turn-state.ts +123 -0
  886. package/src/interactive/view/artifacts.ts +1342 -0
  887. package/src/interactive/view/view-overlay.ts +732 -0
  888. package/src/interactive/welcome-dashboard.ts +536 -0
  889. package/src/interactive/worker-receipts.ts +94 -0
  890. package/src/interactive/worker-replay.ts +111 -0
  891. package/src/interactive/worker-share.ts +100 -0
  892. package/src/interactive/worker-stream.ts +442 -0
  893. package/src/interactive/workspace-facts.ts +141 -0
  894. package/src/tools/agent-tools.ts +382 -0
  895. package/src/tools/artifact.ts +103 -0
  896. package/src/tools/ask-user.ts +656 -0
  897. package/src/tools/bash.ts +176 -0
  898. package/src/tools/bootstrap.ts +474 -0
  899. package/src/tools/codewiki/code-nav.ts +563 -0
  900. package/src/tools/codewiki/shared.ts +115 -0
  901. package/src/tools/compete-worktrees.ts +898 -0
  902. package/src/tools/context/docs-engine.ts +619 -0
  903. package/src/tools/context/index.ts +435 -0
  904. package/src/tools/credential-present.ts +151 -0
  905. package/src/tools/dispatch-arguments.ts +228 -0
  906. package/src/tools/dispatch-plan.ts +741 -0
  907. package/src/tools/dispatch-scout.ts +461 -0
  908. package/src/tools/dispatch.ts +3338 -0
  909. package/src/tools/edit-diff.ts +534 -0
  910. package/src/tools/edit.ts +128 -0
  911. package/src/tools/executables.ts +22 -0
  912. package/src/tools/file-mutation-queue.ts +35 -0
  913. package/src/tools/find.ts +292 -0
  914. package/src/tools/grep.ts +495 -0
  915. package/src/tools/ignore-policy.ts +169 -0
  916. package/src/tools/ledger.ts +206 -0
  917. package/src/tools/ls.ts +110 -0
  918. package/src/tools/monitor.ts +797 -0
  919. package/src/tools/network-policy.ts +29 -0
  920. package/src/tools/observation.ts +421 -0
  921. package/src/tools/observers.ts +99 -0
  922. package/src/tools/path-utils.ts +79 -0
  923. package/src/tools/policy.ts +154 -0
  924. package/src/tools/profiles.ts +108 -0
  925. package/src/tools/read.ts +173 -0
  926. package/src/tools/registry.ts +1157 -0
  927. package/src/tools/result-shaping.ts +204 -0
  928. package/src/tools/safe-exec.ts +128 -0
  929. package/src/tools/spawn-hygiene.ts +182 -0
  930. package/src/tools/steer.ts +88 -0
  931. package/src/tools/string-enum.ts +15 -0
  932. package/src/tools/tasks.ts +168 -0
  933. package/src/tools/truncate-utf8.ts +17 -0
  934. package/src/tools/truncate.ts +202 -0
  935. package/src/tools/verify/frontend.ts +685 -0
  936. package/src/tools/verify/index.ts +66 -0
  937. package/src/tools/verify/scripts.ts +108 -0
  938. package/src/tools/web-fetch.ts +777 -0
  939. package/src/tools/worker-evidence.ts +215 -0
  940. package/src/tools/write.ts +52 -0
  941. package/src/utils/exif-orientation.ts +192 -0
  942. package/src/utils/git.ts +24 -0
  943. package/src/utils/image-resize.ts +148 -0
  944. package/src/utils/photon.ts +103 -0
  945. package/src/worker/control-lane.ts +22 -0
  946. package/src/worker/entry.ts +257 -0
  947. package/src/worker/event-projection.ts +56 -0
  948. package/src/worker/heartbeat.ts +24 -0
  949. package/src/worker/ledger-mirror.ts +95 -0
  950. package/src/worker/ndjson.ts +40 -0
  951. package/src/worker/protocol.ts +529 -0
  952. package/src/worker/resource-facts.ts +79 -0
  953. package/src/worker/runtime-registry.ts +12 -0
  954. package/src/worker/spec-contract.ts +654 -0
  955. package/src/worker/stdin-demux.ts +316 -0
  956. package/dist/acp-UMLFVA3F.js +0 -92
  957. package/dist/chunk-262G75JS.js +0 -35
  958. package/dist/chunk-5LDRNKX2.js +0 -1063
  959. package/dist/chunk-67MTHP2E.js +0 -135
  960. package/dist/chunk-GOTUR54M.js +0 -9
  961. package/dist/chunk-TPOCL34A.js +0 -2876
  962. package/dist/chunk-XV4ZJ6ZM.js +0 -3177
  963. package/dist/clio-KIQ5SNDS.js +0 -53
  964. package/dist/configure-G4E3A2PG.js +0 -27
  965. package/dist/doctor-M5HJJZOL.js +0 -61
  966. package/dist/domains/agents/builtins/architect.md +0 -33
  967. package/dist/domains/agents/builtins/coder.md +0 -31
  968. package/dist/domains/agents/builtins/context-bootstrap.md +0 -38
  969. package/dist/domains/agents/builtins/debugger.md +0 -30
  970. package/dist/domains/agents/builtins/documenter.md +0 -31
  971. package/dist/domains/agents/builtins/git-master.md +0 -30
  972. package/dist/domains/agents/builtins/provenance.md +0 -30
  973. package/dist/domains/agents/builtins/researcher.md +0 -71
  974. package/dist/domains/agents/builtins/scout.md +0 -42
  975. package/dist/domains/agents/builtins/tester.md +0 -31
  976. package/dist/domains/agents/builtins/verifier.md +0 -30
  977. package/dist/domains/agents/builtins/wiki-writer.md +0 -41
  978. package/dist/fleet-preflight-CLIAX7YR.js +0 -21
  979. package/dist/preload-KZVHET2B.js +0 -11
  980. package/dist/version-ROZ6CZKH.js +0 -16
  981. package/skills/git/commit-crafting/SKILL.md +0 -79
  982. package/skills/git/commit-crafting/evals.md +0 -92
  983. package/skills/git/create-pr/SKILL.md +0 -116
  984. package/skills/git/create-pr/evals.md +0 -114
  985. package/skills/git/investigate-issue/SKILL.md +0 -139
  986. package/skills/git/investigate-issue/evals.md +0 -94
  987. package/skills/git/review-changes/SKILL.md +0 -103
  988. package/skills/git/review-changes/evals.md +0 -85
@@ -0,0 +1,1694 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import type { OutputVerbosity } from "../core/defaults.js";
3
+ import { SKILL_SUGGESTION_PREFIX } from "../core/skill-activation.js";
4
+ import { rawDurationMs } from "../core/timers.js";
5
+ import { estimateReasoningTextTokens, extractReasoningTokens } from "../domains/session/context-accounting.js";
6
+ import { type Component, Markdown, truncateToWidth, wrapTextWithAnsi } from "../engine/tui.js";
7
+ import type { AgentMessage } from "../engine/types.js";
8
+ import type { ChatLoopEvent, RetryStatusPayload } from "./chat-loop.js";
9
+ import { extractText, isSelfExplainingAbort } from "./chat-loop-messages.js";
10
+ import { codeInk } from "./renderers/code-ink.js";
11
+ import { styleTaggedNotice } from "./renderers/notice.js";
12
+ import { formatRetryStatus } from "./renderers/retry-status.js";
13
+ import {
14
+ classifyResourceRead,
15
+ previewResult,
16
+ renderToolAwaitingApproval,
17
+ renderToolCallHeader,
18
+ renderToolExecution,
19
+ renderToolRunningStatus,
20
+ renderToolStreamingExecution,
21
+ renderToolSubline,
22
+ unwrapResultEnvelope,
23
+ } from "./renderers/tool-execution.js";
24
+ import { renderWorkerEntryLines } from "./renderers/worker-entry.js";
25
+ import { INLINE_STATUS_INDENT_COLS, type StatusPhase, type VerbRender } from "./status/index.js";
26
+ import { clioTheme, fgSequence, GLYPH, markdownTheme, SGR_DIM, SGR_RESET } from "./theme/index.js";
27
+ import { type WorkerEntryState, workerAskedByModel } from "./worker-stream.js";
28
+
29
+ // Fenced code reaches the screen through pi-tui's Markdown component, which
30
+ // exposes the MarkdownTheme.highlightCode hook: it hands over the raw fence
31
+ // text plus its language tag before pi-tui draws the fence borders and indent.
32
+ // Wiring code ink through that hook colors only the ink, so the fence frame,
33
+ // indentation, and width behavior stay pi-tui's and nothing post-processes
34
+ // already-rendered output.
35
+ const CHAT_MARKDOWN_THEME = markdownTheme(clioTheme(), (code, lang) => codeInk(lang, code.split("\n")));
36
+
37
+ // Prefix and rail SGR constants, previously re-exported by the deleted
38
+ // palette.ts. Composing them from fgSequence/GLYPH here yields byte-identical
39
+ // sequences to what palette.js produced, so the transcript renders unchanged.
40
+ const RESET = SGR_RESET;
41
+ const DIM = SGR_DIM;
42
+ const TEAL = fgSequence("accent");
43
+ const BLUE_REASON = fgSequence("reason");
44
+ const GREEN_OK = fgSequence("success");
45
+ const AMBER = fgSequence("warning");
46
+ const RED_CRIT = fgSequence("error");
47
+ const AGENT_GLYPH = GLYPH.agent;
48
+ const USER_GLYPH = GLYPH.user;
49
+
50
+ /**
51
+ * An assistant turn is a sequence of text and tool segments interleaved in
52
+ * pi-agent-core event order. pi-agent-core emits: `message_start` →
53
+ * `text_delta`+ → `message_end` → `tool_execution_*` → (next) `message_start`
54
+ * → `text_delta`+ → `message_end`, so tool calls always sit BETWEEN the
55
+ * assistant's pre-tool narration and the post-tool summary. Storing a flat
56
+ * `text` buffer + `tools[]` array (pre-refactor) collapsed that order: all
57
+ * text across the turn concatenated into one line with every tool block
58
+ * appended at the end. The segment list preserves the stream order instead.
59
+ *
60
+ * Each text segment tracks whether it has been finalized by a `message_end`.
61
+ * Streaming deltas render as plain lines; only finalized text is piped
62
+ * through the Markdown renderer. Partial markdown (unclosed fence, half-typed
63
+ * bullet) would otherwise paint garbage at ~60 fps under streaming.
64
+ */
65
+ export type ReasoningTokenProvenance = "provider" | "estimated" | "mixed";
66
+
67
+ export interface ChatPanelTurnUsage {
68
+ inputTokens: number;
69
+ outputTokens: number;
70
+ cacheReadTokens: number;
71
+ cacheWriteTokens: number;
72
+ reasoningTokens?: number;
73
+ reasoningTokenProvenance?: ReasoningTokenProvenance;
74
+ /** Model calls the totals were summed over; absent when the count is unknown. */
75
+ modelCalls?: number;
76
+ }
77
+
78
+ export interface ChatPanelRenderMetrics {
79
+ durationMs: number;
80
+ cacheHit: boolean;
81
+ entriesRendered: number;
82
+ }
83
+
84
+ type TextSegment = {
85
+ kind: "text";
86
+ text: string;
87
+ finalized: boolean;
88
+ /**
89
+ * Lazy pi-tui Markdown instance owned by the segment. Markdown caches its
90
+ * output by (text, width) internally, so reusing the instance keeps the
91
+ * per-frame cost O(1) for stable segments and lets the active entry
92
+ * invalidate only the tail segment's cache on re-canonicalization.
93
+ */
94
+ md?: Markdown;
95
+ /**
96
+ * Wrapped output for every source line except the last, plus the width and
97
+ * source-line count it was built at. A streaming segment only ever grows at
98
+ * its tail: source lines before the last one are terminated by a newline and
99
+ * can never change, yet every frame re-wrapped all of them. On a 16k-char
100
+ * answer that was 5-22 ms per frame to reproduce identical rows.
101
+ */
102
+ wrapCache?: { width: number; completedLines: number; lines: string[] };
103
+ /**
104
+ * A protocol suggestion line and the answer prose beneath it, when the model
105
+ * opened its reply with both in one segment (which is what the skills prompt
106
+ * asks for). The answer half is a segment of its own so it keeps its own
107
+ * Markdown and wrap caches; it is cached here rather than rebuilt per frame.
108
+ */
109
+ suggestionSplit?: { suggestion: string; answer: TextSegment };
110
+ };
111
+ type ToolSegment = {
112
+ kind: "tool";
113
+ id: string;
114
+ name: string;
115
+ args: unknown;
116
+ /** Final result from `tool_execution_end`; undefined while the call is in flight. */
117
+ result?: unknown;
118
+ /** True once `tool_execution_end` has landed (success or error). */
119
+ finished: boolean;
120
+ /**
121
+ * True when the segment was force-settled without its own end event
122
+ * (blocked at admission, aborted mid-batch, id reused). A late
123
+ * `tool_execution_end` may upgrade such a segment with the call's true
124
+ * result; a segment finished by its own end event is never overwritten.
125
+ * The explicit `| undefined` allows the upgrade path to clear the flag
126
+ * under `exactOptionalPropertyTypes: true`.
127
+ */
128
+ settledWithoutResult?: boolean | undefined;
129
+ /** True when the finished result was an error. Meaningful only after `finished`. */
130
+ isError: boolean;
131
+ /** When true, render the full structured block instead of the collapsed subline. */
132
+ expanded: boolean;
133
+ /** Wall-clock start time captured by the chat panel for live duration display. */
134
+ startedAtMs?: number;
135
+ /** Completed call duration in milliseconds (event-supplied or measured locally). */
136
+ durationMs?: number;
137
+ /** Persisted result summary (bytes, truncated, offloadPath, observation) from the chat loop. */
138
+ resultSummary?: Record<string, unknown> | undefined;
139
+ /**
140
+ * Latest cumulative partial output from `tool_execution_update`. Cleared
141
+ * back to `undefined` on `tool_execution_end` so the finished `result`
142
+ * takes over. Only consumed when `!finished && expanded`. The explicit
143
+ * `| undefined` is required under `exactOptionalPropertyTypes: true` so
144
+ * the clear path can re-assign `undefined` without a `delete`.
145
+ */
146
+ partialOutput?: string | undefined;
147
+ /**
148
+ * True while the call is parked at the permission gate. Set/cleared by
149
+ * `tool_approval_state` events and cleared by any settle so a denied or
150
+ * resumed call never keeps the awaiting-approval styling. Meaningful only
151
+ * while `!finished`.
152
+ */
153
+ awaitingApproval?: boolean | undefined;
154
+ settlement?: "blocked" | "aborted" | "orphaned" | undefined;
155
+ /**
156
+ * The admission verdict's short reason, present only on a settlement the
157
+ * registry actually rejected. A blocked row without it states that something
158
+ * was refused and leaves the operator no way to learn why.
159
+ */
160
+ blockReason?: string | undefined;
161
+ };
162
+ /**
163
+ * A turn's terminal-error marker (`[error] ...`, `[aborted] ...`,
164
+ * `[stopped: length] ...`) carried as its own segment so it renders in the
165
+ * error token instead of being piped through Markdown as plain prose. Kept
166
+ * distinct from streamed text: the error text is not model output, it is Clio
167
+ * reporting why the turn ended.
168
+ */
169
+ type ErrorSegment = {
170
+ kind: "error";
171
+ text: string;
172
+ };
173
+ type AssistantSegment = TextSegment | ToolSegment | ErrorSegment;
174
+ type ReplayBlockRenderer = (width: number) => string[];
175
+ type AssistantStatusLine = { phase: StatusPhase; verb: string; toneHint: VerbRender["toneHint"] };
176
+
177
+ type TranscriptEntry =
178
+ | { role: "user"; text: string }
179
+ | { role: "retryStatus"; status: RetryStatusPayload }
180
+ | {
181
+ role: "assistant";
182
+ segments: AssistantSegment[];
183
+ /**
184
+ * Raw thinking content from `thinking_delta` events plus
185
+ * `thinking` blocks captured on `message_end`. Renders live while
186
+ * the turn is pending: a folded `Thinking (N tokens)…` marker by
187
+ * default, or the tail of the reasoning down a dim `│ ` rail if
188
+ * expanded via `toggleLastThinking()` (Ctrl+T). Once the turn
189
+ * settles it collapses to a static `Thinking...` marker (folded) or
190
+ * a head-anchored rail (expanded), mirroring the pi-coding-agent
191
+ * reference which streams thinking from the partial message.
192
+ */
193
+ thinking: string;
194
+ /**
195
+ * Whether the thinking block renders as the full body (true) or
196
+ * the one-line dim marker (false/undefined). Toggled by
197
+ * `toggleLastThinking()`. New thinking inherits the panel-level
198
+ * visibility mode until Ctrl+T toggles it again.
199
+ */
200
+ expandedThinking?: boolean;
201
+ pending: boolean;
202
+ statusLine?: AssistantStatusLine | null | undefined;
203
+ isError: boolean;
204
+ turnUsage?: ChatPanelTurnUsage;
205
+ }
206
+ /**
207
+ * A dispatched worker's attributed block. The panel owns only the fold
208
+ * state; `state` is the live object the worker-stream reducer mutates, so a
209
+ * streaming delta reaches the screen without copying the entry per frame.
210
+ * The panel is told when that happened through `applyWorkerState`.
211
+ */
212
+ | { role: "worker"; state: WorkerEntryState; folded: boolean }
213
+ | { role: "replayBlock"; renderBlock: ReplayBlockRenderer };
214
+
215
+ type WorkerTranscriptEntry = Extract<TranscriptEntry, { role: "worker" }>;
216
+
217
+ export interface ChatPanel extends Component {
218
+ appendUser(text: string): void;
219
+ appendReplayBlock(renderBlock: ReplayBlockRenderer): void;
220
+ applyEvent(event: ChatLoopEvent): void;
221
+ /**
222
+ * Place or refresh a worker's block. The first call for an assignment
223
+ * inserts the entry (agent origin nests under the tool segment named by
224
+ * `state.parentToolCallId`, everything else appends); later calls only
225
+ * invalidate the render, because the reducer mutates the same state object.
226
+ */
227
+ applyWorkerState(state: WorkerEntryState): void;
228
+ /**
229
+ * Every worker block on the transcript, oldest first, live or replayed.
230
+ * `/share` selects from this rather than from the reducer's routing table,
231
+ * so what the operator can share is exactly what the operator can see.
232
+ */
233
+ workerStates(): ReadonlyArray<WorkerEntryState>;
234
+ setStatusLine(line: AssistantStatusLine | null): void;
235
+ toggleLastToolExpanded(): boolean;
236
+ toggleAllToolsExpanded(): boolean;
237
+ /**
238
+ * Force every tool segment into its collapsed one-line form. Replay
239
+ * (`rehydrateChatPanelFromTurns`) calls this so a resumed or forked
240
+ * transcript reproduces the settled live view, where tools are collapsed to
241
+ * their ledger summary rather than the expanded body. Idempotent.
242
+ */
243
+ collapseAllTools(): void;
244
+ /**
245
+ * Flip thinking-bearing assistant turns between the one-line dim marker
246
+ * and the full rail-prefixed body. The target visibility is panel-level
247
+ * sticky state, then applied to current thinking history so Ctrl+T behaves
248
+ * like a transcript-level thinking visibility toggle.
249
+ */
250
+ toggleLastThinking(): boolean;
251
+ toggleAllThinking(): boolean;
252
+ /** Toggle whether expanded live tool bodies include cumulative partial output. */
253
+ toggleLiveToolOutput(): boolean;
254
+ /** Clears the visible transcript. /new uses this after rotating the session. */
255
+ reset(): void;
256
+ }
257
+
258
+ export interface ChatPanelOptions {
259
+ /**
260
+ * Resolves the user-visible key string for the `clio.tool.expand`
261
+ * action, which folds and unfolds the newest tool call or worker block.
262
+ * Returning a non-empty string surfaces a dim ` (<key>)` hint on the one
263
+ * surface the key would act on: the latest finished collapsed tool subline,
264
+ * or the newest folded worker card. Returning undefined or an empty string
265
+ * suppresses the hint. Called per render so live keybinding changes flow
266
+ * through.
267
+ */
268
+ getToolExpandKey?: () => string | undefined;
269
+ /** Live transcript detail mode. Settings changes take effect on the next frame. */
270
+ getOutputVerbosity?: () => OutputVerbosity;
271
+ /** Receives measured panel render cost; no FPS claim is made by the panel. */
272
+ onRenderMetrics?: (metrics: ChatPanelRenderMetrics) => void;
273
+ /** Clock injection for deterministic duration tests. Defaults to Date.now. */
274
+ now?: () => number;
275
+ /**
276
+ * Render every expanded tool body in full, without the live view's
277
+ * middle-elision or character truncation. `/export` builds a throwaway panel
278
+ * with this set so the written transcript reproduces the complete tool output
279
+ * instead of the terminal's bounded view.
280
+ */
281
+ unboundedToolBodies?: boolean;
282
+ }
283
+
284
+ /**
285
+ * An abort reaches the registry as a rejection, so it arrives here as a block
286
+ * whose reason names it (`run aborted before the operator decided`). Matching
287
+ * that is safe in a way that matching the tool's own output is not: this string
288
+ * is composed by Clio, never by the command that ran.
289
+ */
290
+ const ABORTED_REASON_RE = /\babort(?:ed)?\b/i;
291
+
292
+ /**
293
+ * Classify a finished tool call from the registry's admission verdict, which
294
+ * the turn runtime stamps onto the event as `outcome`. Only a call the registry
295
+ * refused settles as blocked; a call that executed and failed is an ordinary
296
+ * error and carries no settlement. Events with no verdict (replayed history, a
297
+ * surface that resolves tools without telemetry) settle as nothing rather than
298
+ * guessing.
299
+ */
300
+ function toolSettlement(event: {
301
+ outcome?: unknown;
302
+ blockReason?: unknown;
303
+ }): { settlement: "blocked" | "aborted"; reason?: string } | undefined {
304
+ if (event.outcome !== "blocked") return undefined;
305
+ const reason = typeof event.blockReason === "string" && event.blockReason.trim().length > 0 ? event.blockReason : null;
306
+ const settlement = reason !== null && ABORTED_REASON_RE.test(reason) ? "aborted" : "blocked";
307
+ return reason === null ? { settlement } : { settlement, reason };
308
+ }
309
+
310
+ function extractAssistantText(message: unknown): string {
311
+ if (!message || typeof message !== "object" || !("role" in message) || message.role !== "assistant") return "";
312
+ if (!("content" in message) || !Array.isArray(message.content)) return "";
313
+ return message.content
314
+ .filter((item): item is { type: "text"; text: string } => item?.type === "text" && typeof item.text === "string")
315
+ .map((item) => item.text)
316
+ .join("");
317
+ }
318
+
319
+ function extractAssistantThinking(message: unknown): string {
320
+ if (!message || typeof message !== "object" || !("role" in message) || message.role !== "assistant") return "";
321
+ if (!("content" in message) || !Array.isArray(message.content)) return "";
322
+ return message.content
323
+ .filter(
324
+ (item): item is { type: "thinking"; thinking: string } =>
325
+ item?.type === "thinking" && typeof item.thinking === "string",
326
+ )
327
+ .map((item) => item.thinking)
328
+ .join("");
329
+ }
330
+
331
+ function finiteToken(value: unknown): number {
332
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
333
+ }
334
+
335
+ function assistantUsage(message: unknown): ChatPanelTurnUsage | undefined {
336
+ if (!message || typeof message !== "object" || (message as { role?: unknown }).role !== "assistant") return undefined;
337
+ const record = message as Record<string, unknown>;
338
+ const usage = record.usage && typeof record.usage === "object" ? (record.usage as Record<string, unknown>) : undefined;
339
+ const thinking = extractAssistantThinking(message);
340
+ const reportedReasoning = usage ? extractReasoningTokens(usage) : null;
341
+ const estimatedReasoning = reportedReasoning === null ? estimateReasoningTextTokens(thinking) : null;
342
+ const inputTokens = finiteToken(usage?.input);
343
+ const outputTokens = finiteToken(usage?.output);
344
+ const cacheReadTokens = finiteToken(usage?.cacheRead);
345
+ const cacheWriteTokens = finiteToken(usage?.cacheWrite);
346
+ if (
347
+ inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens === 0 &&
348
+ reportedReasoning === null &&
349
+ estimatedReasoning === null
350
+ ) {
351
+ return undefined;
352
+ }
353
+ const turnUsage: ChatPanelTurnUsage = { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, modelCalls: 1 };
354
+ if (reportedReasoning !== null) {
355
+ turnUsage.reasoningTokens = reportedReasoning;
356
+ turnUsage.reasoningTokenProvenance = "provider";
357
+ } else if (estimatedReasoning !== null) {
358
+ turnUsage.reasoningTokens = estimatedReasoning;
359
+ turnUsage.reasoningTokenProvenance = "estimated";
360
+ }
361
+ return turnUsage;
362
+ }
363
+
364
+ function aggregateAssistantUsage(messages: unknown): ChatPanelTurnUsage | undefined {
365
+ if (!Array.isArray(messages)) return undefined;
366
+ const total: ChatPanelTurnUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
367
+ let found = false;
368
+ let provider = false;
369
+ let estimated = false;
370
+ for (const message of messages) {
371
+ const usage = assistantUsage(message);
372
+ if (!usage) continue;
373
+ found = true;
374
+ total.modelCalls = (total.modelCalls ?? 0) + (usage.modelCalls ?? 1);
375
+ total.inputTokens += usage.inputTokens;
376
+ total.outputTokens += usage.outputTokens;
377
+ total.cacheReadTokens += usage.cacheReadTokens;
378
+ total.cacheWriteTokens += usage.cacheWriteTokens;
379
+ if (usage.reasoningTokens !== undefined) {
380
+ total.reasoningTokens = (total.reasoningTokens ?? 0) + usage.reasoningTokens;
381
+ if (usage.reasoningTokenProvenance === "provider") provider = true;
382
+ else estimated = true;
383
+ }
384
+ }
385
+ if (!found) return undefined;
386
+ if (provider || estimated)
387
+ total.reasoningTokenProvenance = provider && estimated ? "mixed" : provider ? "provider" : "estimated";
388
+ return total;
389
+ }
390
+
391
+ function extractAssistantTerminalError(message: unknown): string {
392
+ if (!message || typeof message !== "object" || !("role" in message) || message.role !== "assistant") return "";
393
+ const stopReason = (message as { stopReason?: unknown }).stopReason;
394
+ if (stopReason !== "error" && stopReason !== "aborted" && stopReason !== "length") return "";
395
+ if (stopReason === "length") {
396
+ return "[stopped: length] Model target hit its generation/output limit before a complete response. This is not a safety denial. Continue with a shorter answer or lower thinking; use /context compact if the context meter is also near full.";
397
+ }
398
+ const raw = (message as { errorMessage?: unknown }).errorMessage;
399
+ if (isSelfExplainingAbort({ stopReason, errorMessage: raw, text: extractText(message as AgentMessage) })) return "";
400
+ const reason = typeof raw === "string" && raw.length > 0 ? raw : "unknown error";
401
+ return stopReason === "aborted" ? `[aborted] ${reason}` : `[error] ${reason}`;
402
+ }
403
+
404
+ function scopeTerminalErrorAfterSuccessfulTool(
405
+ entry: Extract<TranscriptEntry, { role: "assistant" }>,
406
+ terminalError: string,
407
+ ): string {
408
+ if (!terminalError.startsWith("[error] ")) return terminalError;
409
+ const successfulTools = entry.segments.filter(
410
+ (segment): segment is ToolSegment => segment.kind === "tool" && segment.finished && !segment.isError,
411
+ );
412
+ if (successfulTools.length === 0) return terminalError;
413
+
414
+ const reason = terminalError.slice("[error] ".length);
415
+ const timedOut = /\b(?:timed?\s*out|timeout)\b/i.test(reason);
416
+ const modelFailure = timedOut
417
+ ? "main model response timed out after successful tool result"
418
+ : `main model response failed after successful tool result: ${reason}`;
419
+ const detachedDispatchSucceeded = successfulTools.some(
420
+ (segment) =>
421
+ segment.name === "dispatch" &&
422
+ segment.args !== null &&
423
+ typeof segment.args === "object" &&
424
+ (segment.args as { detach?: unknown }).detach === true,
425
+ );
426
+ return `[error] ${modelFailure}${detachedDispatchSucceeded ? "; detached runs continue" : ""}`;
427
+ }
428
+
429
+ function hasVisibleOutput(entry: Extract<TranscriptEntry, { role: "assistant" }>): boolean {
430
+ for (const seg of entry.segments) {
431
+ if (seg.kind === "tool") return true;
432
+ if (seg.kind === "text" && seg.text.trim().length > 0) return true;
433
+ if (seg.kind === "error" && seg.text.trim().length > 0) return true;
434
+ }
435
+ return false;
436
+ }
437
+
438
+ /** Index of the newest assistant entry, optionally restricted to ones that rendered something. */
439
+ function lastAssistantIndex(
440
+ transcript: ReadonlyArray<TranscriptEntry>,
441
+ options: { withOutput?: boolean } = {},
442
+ ): number | null {
443
+ for (let index = transcript.length - 1; index >= 0; index -= 1) {
444
+ const entry = transcript[index];
445
+ if (entry?.role !== "assistant") continue;
446
+ if (options.withOutput === true && !hasVisibleOutput(entry)) continue;
447
+ return index;
448
+ }
449
+ return null;
450
+ }
451
+
452
+ function hasStreamingText(entry: Extract<TranscriptEntry, { role: "assistant" }>): boolean {
453
+ const tail = entry.segments[entry.segments.length - 1];
454
+ return tail?.kind === "text" && !tail.finalized && tail.text.trim().length > 0;
455
+ }
456
+
457
+ /**
458
+ * Split a reply that opens with the skill-suggestion protocol line into that
459
+ * line and the answer beneath it.
460
+ *
461
+ * The prompt asks the model to *begin its reply* with the suggestion, so the
462
+ * usual shape is one segment holding both. Classifying that whole segment as
463
+ * advisory left the turn with no voice glyph at all: the suggestion did not
464
+ * claim it and the answer never got the chance. Returns null when the segment
465
+ * is only a suggestion, which stays advisory in full.
466
+ */
467
+ function skillSuggestionSplit(seg: TextSegment): { suggestion: string; answer: TextSegment } | null {
468
+ if (!seg.text.startsWith(SKILL_SUGGESTION_PREFIX)) return null;
469
+ const breakIndex = seg.text.indexOf("\n");
470
+ if (breakIndex < 0) return null;
471
+ // A model that puts a blank line between the suggestion and the answer left
472
+ // the remainder opening with a newline, so the first rendered row was empty
473
+ // and took the glyph the answer's own text row was owed.
474
+ const answerText = seg.text.slice(breakIndex + 1).replace(/^\n+/, "");
475
+ if (answerText.trim().length === 0) return null;
476
+ const split = seg.suggestionSplit ?? {
477
+ suggestion: "",
478
+ answer: { kind: "text", text: answerText, finalized: seg.finalized } as TextSegment,
479
+ };
480
+ seg.suggestionSplit = split;
481
+ split.suggestion = seg.text.slice(0, breakIndex);
482
+ const answer = split.answer;
483
+ if (answer.text === answerText && answer.finalized === seg.finalized) return split;
484
+ // The answer half follows the same cache rules as any other segment: the
485
+ // streaming wrap cache assumes append-only text, so a rewrite or a
486
+ // finalization drops it and a delta keeps it.
487
+ const appendOnly = !seg.finalized && answer.finalized === seg.finalized && answerText.startsWith(answer.text);
488
+ answer.text = answerText;
489
+ answer.finalized = seg.finalized;
490
+ if (!appendOnly) delete answer.wrapCache;
491
+ if (answer.md) answer.md.setText(answerText);
492
+ return split;
493
+ }
494
+
495
+ function renderTextSegmentLines(seg: TextSegment, width: number): string[] {
496
+ if (!seg.finalized) {
497
+ const source = seg.text.split("\n");
498
+ // The final element is the live tail; everything before it is newline-
499
+ // terminated and frozen.
500
+ const completedCount = source.length - 1;
501
+ const cache = seg.wrapCache;
502
+ const reusable = cache !== undefined && cache.width === width && cache.completedLines <= completedCount;
503
+ const completed = reusable ? cache.lines.slice() : [];
504
+ for (let i = reusable ? cache.completedLines : 0; i < completedCount; i += 1) {
505
+ for (const line of wrapTextWithAnsi(source[i] ?? "", width)) completed.push(line);
506
+ }
507
+ seg.wrapCache = { width, completedLines: completedCount, lines: completed };
508
+ const wrapped = completed.slice();
509
+ for (const line of wrapTextWithAnsi(source[completedCount] ?? "", width)) wrapped.push(line);
510
+ return wrapped;
511
+ }
512
+ if (!seg.md) {
513
+ seg.md = new Markdown(seg.text, 0, 0, CHAT_MARKDOWN_THEME);
514
+ }
515
+ // pi-tui Markdown right-pads lines to the render width. If a long streaming
516
+ // reply has already scrolled, flipping the finalized segment from unpadded
517
+ // plain text to padded Markdown changes historical rows and forces a full
518
+ // redraw on terminals that cannot clear scrollback. Trim only that render
519
+ // padding so finalized prose remains byte-stable with the streamed shape.
520
+ return seg.md.render(width).map((line) => line.replace(/ +$/, ""));
521
+ }
522
+
523
+ /**
524
+ * Render a terminal-error segment in the error token. Terminal markers such as
525
+ * `[error] ...`, `[aborted] ...`, and `[stopped: length] ...` render as red
526
+ * message text rather than plain markdown, so a failed turn is visibly a
527
+ * failure. Each source line wraps to width and carries the error color.
528
+ */
529
+ function renderErrorSegmentLines(seg: ErrorSegment, width: number): string[] {
530
+ const out: string[] = [];
531
+ for (const line of seg.text.split("\n")) {
532
+ for (const wrapped of wrapTextWithAnsi(line, width)) {
533
+ out.push(`${RED_CRIT}${wrapped}${RESET}`);
534
+ }
535
+ }
536
+ return out;
537
+ }
538
+
539
+ const CLIO_PREFIX = `${TEAL}${AGENT_GLYPH}${RESET} `;
540
+ const CLIO_PREFIX_ERROR = `${RED_CRIT}${AGENT_GLYPH}${RESET} `;
541
+ const USER_PREFIX = `${TEAL}${USER_GLYPH}${RESET} `;
542
+ const PROSE_GUTTER = " ";
543
+ const PROSE_GUTTER_WIDTH = 2;
544
+
545
+ /**
546
+ * Give transcript prose a fixed two-cell ownership gutter. Callers render the
547
+ * content at `width - 2`, then this function spends those reserved cells on
548
+ * either the turn glyph or a hanging indent. Tool ledgers never pass through
549
+ * here, so their existing full-width grammar remains untouched.
550
+ */
551
+ function hangProseLines(lines: string[], firstPrefix?: string): string[] {
552
+ return lines.map((line, index) => `${index === 0 && firstPrefix !== undefined ? firstPrefix : PROSE_GUTTER}${line}`);
553
+ }
554
+
555
+ /**
556
+ * Static marker used when thinking is folded. This matches pi-coding-agent's
557
+ * hidden-thinking presentation and avoids previewing reasoning content.
558
+ */
559
+ const THINKING_HIDDEN_LABEL = `Thinking${GLYPH.ellipsis}`;
560
+ const THINKING_LINE_LIMIT = 12;
561
+ const REASONING_CHARS_PER_TOKEN = 4;
562
+
563
+ function estimateThinkingTokens(thinking: string): number {
564
+ return estimateReasoningTextTokens(thinking) ?? Math.max(1, Math.round(thinking.length / REASONING_CHARS_PER_TOKEN));
565
+ }
566
+
567
+ function reasoningDisplay(usage: ChatPanelTurnUsage | undefined, thinking: string): { tokens: number; marker: string } {
568
+ if (usage?.reasoningTokens !== undefined) {
569
+ return {
570
+ tokens: usage.reasoningTokens,
571
+ marker: usage.reasoningTokenProvenance === "provider" ? "provider-reported" : "mixed/estimated",
572
+ };
573
+ }
574
+ return { tokens: estimateThinkingTokens(thinking), marker: "estimated" };
575
+ }
576
+
577
+ /**
578
+ * Render the assistant turn's thinking block. Collapsed (default) returns a
579
+ * single dim `Thinking...` marker. Expanded returns the full body dimmed and
580
+ * prefixed with a dim `│ ` rail, capped at `THINKING_LINE_LIMIT` lines with a
581
+ * tail `... N more lines hidden` overflow message. Mirrors the tool toggle's
582
+ * lab-notebook minimalism: no colored glyphs, no boxes.
583
+ */
584
+ function renderThinkingLines(
585
+ thinking: string,
586
+ expanded: boolean,
587
+ width: number,
588
+ streaming: boolean,
589
+ usage?: ChatPanelTurnUsage,
590
+ ): string[] {
591
+ if (thinking.length === 0) return [];
592
+ const dimWrap = (s: string): string => `${DIM}${s}${RESET}`;
593
+ if (!expanded) {
594
+ const lineBudget = Math.max(1, width);
595
+ const display = reasoningDisplay(usage, thinking);
596
+ const label = streaming
597
+ ? `Thinking (${display.tokens} tokens)${display.marker === "provider-reported" ? "" : " ≈ estimated"}…`
598
+ : THINKING_HIDDEN_LABEL;
599
+ return [dimWrap(truncateToWidth(label, lineBudget, GLYPH.ellipsis, false))];
600
+ }
601
+ const splitLines = thinking.split("\n");
602
+ let visible: string[];
603
+ if (streaming) {
604
+ if (splitLines.length > THINKING_LINE_LIMIT) {
605
+ const hiddenCount = splitLines.length - THINKING_LINE_LIMIT;
606
+ visible = [`… ${hiddenCount} earlier lines hidden`, ...splitLines.slice(-THINKING_LINE_LIMIT)];
607
+ } else {
608
+ visible = splitLines;
609
+ }
610
+ } else {
611
+ visible =
612
+ splitLines.length > THINKING_LINE_LIMIT
613
+ ? [...splitLines.slice(0, THINKING_LINE_LIMIT), `... ${splitLines.length - THINKING_LINE_LIMIT} more lines hidden`]
614
+ : splitLines;
615
+ }
616
+ const out: string[] = [];
617
+ const bodyWidth = Math.max(1, width - 2);
618
+ for (const raw of visible) {
619
+ const wrappedLines = raw.length === 0 ? [""] : wrapTextWithAnsi(raw, bodyWidth);
620
+ for (const wrapped of wrappedLines) {
621
+ out.push(`${BLUE_REASON}│ ${RESET}${DIM}${wrapped}${RESET}`);
622
+ }
623
+ }
624
+ return out;
625
+ }
626
+
627
+ /**
628
+ * `in` is the sum of every model call the turn made, not the size of one
629
+ * prompt. A long agentic turn makes dozens of calls that each resend a growing
630
+ * context, so the total runs far past the model's context window and reads as
631
+ * one impossible request unless the call count is beside it. One live turn
632
+ * reported 717676 input tokens against a 500k window; it was 65 calls of about
633
+ * 11k, which the preceding one-call turns had already shown.
634
+ */
635
+ function renderTurnUsageLine(usage: ChatPanelTurnUsage, width: number, verbosity: OutputVerbosity): string[] {
636
+ if (verbosity === "minimal") return [];
637
+ if (verbosity === "default") {
638
+ const receipt = truncateToWidth(
639
+ ` turn · in ${usage.inputTokens} · out ${usage.outputTokens}`,
640
+ width,
641
+ GLYPH.ellipsis,
642
+ false,
643
+ );
644
+ return [`${DIM}${receipt}${RESET}`];
645
+ }
646
+ const calls = usage.modelCalls !== undefined && usage.modelCalls > 1 ? ` over ${usage.modelCalls} calls` : "";
647
+ // The label stays separated from the count in both provenances. Deriving the
648
+ // separator from the `≈` marker glued them together whenever the provider
649
+ // reported a total, which is the common case, and rendered `reason0 provider`.
650
+ // The field is named for reasoning tokens rather than `reason`, which the
651
+ // memory step rows already use for a fixed decision vocabulary.
652
+ //
653
+ // A turn that spent no reasoning tokens states nothing by naming the
654
+ // provenance of zero, and at narrow widths `reasoning 0 provider` orphaned
655
+ // the word `provider` on its own line. Zero suppresses the whole suffix, the
656
+ // same rule the caveat below already follows.
657
+ const reason =
658
+ usage.reasoningTokens !== undefined && usage.reasoningTokens > 0
659
+ ? ` reasoning ${usage.reasoningTokenProvenance === "provider" ? `${usage.reasoningTokens} provider` : `≈${usage.reasoningTokens} estimated`}`
660
+ : "";
661
+ const cache =
662
+ usage.cacheReadTokens > 0 || usage.cacheWriteTokens > 0
663
+ ? ` cache ${usage.cacheReadTokens}/${usage.cacheWriteTokens}`
664
+ : "";
665
+ // The caveat is about reasoning text the panel displayed. A turn that spent
666
+ // no reasoning tokens displayed none, so appending it there warned about
667
+ // something absent and cost a wrapped line per turn at narrow widths.
668
+ const caveat =
669
+ usage.reasoningTokens !== undefined && usage.reasoningTokens > 0
670
+ ? " · reasoning text is a UI excerpt, not a verification"
671
+ : "";
672
+ return wrapTextWithAnsi(
673
+ `${DIM} turn · in ${usage.inputTokens}${calls} · out ${usage.outputTokens}${cache}${reason}${caveat}${RESET}`,
674
+ width,
675
+ );
676
+ }
677
+
678
+ /** The indent resolveInlineVerb budgets against when it fits the verb to the terminal. */
679
+ const STATUS_INDENT = " ".repeat(INLINE_STATUS_INDENT_COLS);
680
+
681
+ function styleStatusVerb(text: string, toneHint: VerbRender["toneHint"]): string {
682
+ if (toneHint === "error") return `${RED_CRIT}${text}${RESET}`;
683
+ if (toneHint === "warn") return `${AMBER}${text}${RESET}`;
684
+ if (toneHint === "ok") return `${GREEN_OK}${text}${RESET}`;
685
+ return `${DIM}${text}${RESET}`;
686
+ }
687
+
688
+ function renderToolSegmentLines(
689
+ seg: ToolSegment,
690
+ width: number,
691
+ expandKey: string | undefined,
692
+ latestHintToolId: string | null,
693
+ nowMs: number,
694
+ unboundedToolBodies: boolean,
695
+ verbosity: OutputVerbosity,
696
+ liveToolOutput: boolean,
697
+ ): string[] {
698
+ const hintKey = seg.id === latestHintToolId ? expandKey : undefined;
699
+ const expanded = verbosity === "verbose" || (verbosity !== "minimal" && seg.expanded);
700
+ const elapsedMs = seg.startedAtMs !== undefined ? Math.max(0, rawDurationMs(seg.startedAtMs, nowMs)) : undefined;
701
+ // A parked call is not executing: the awaiting-approval line replaces the
702
+ // counting elapsed spinner in both collapsed and expanded form (there is no
703
+ // body or partial output to expand while the call sits at the gate).
704
+ if (!seg.finished && seg.awaitingApproval === true) {
705
+ return renderToolAwaitingApproval({ toolCallId: seg.id, toolName: seg.name, args: seg.args }, width);
706
+ }
707
+ if (!expanded) {
708
+ return renderToolSubline(
709
+ seg.finished
710
+ ? {
711
+ toolCallId: seg.id,
712
+ toolName: seg.name,
713
+ args: seg.args,
714
+ result: seg.result,
715
+ isError: seg.isError,
716
+ durationMs: seg.durationMs,
717
+ resultSummary: seg.resultSummary,
718
+ outcome: seg.settlement,
719
+ blockReason: seg.blockReason,
720
+ }
721
+ : { toolCallId: seg.id, toolName: seg.name, args: seg.args, elapsedMs },
722
+ width,
723
+ hintKey,
724
+ );
725
+ }
726
+ if (!seg.finished) {
727
+ if (liveToolOutput && seg.partialOutput !== undefined) {
728
+ return renderToolStreamingExecution(
729
+ { toolCallId: seg.id, toolName: seg.name, args: seg.args, elapsedMs },
730
+ width,
731
+ seg.partialOutput,
732
+ );
733
+ }
734
+ const call = { toolCallId: seg.id, toolName: seg.name, args: seg.args, elapsedMs };
735
+ return liveToolOutput ? renderToolCallHeader(call, width) : renderToolRunningStatus(call, width);
736
+ }
737
+ return renderToolExecution(
738
+ {
739
+ toolCallId: seg.id,
740
+ toolName: seg.name,
741
+ args: seg.args,
742
+ result: seg.result,
743
+ isError: seg.isError,
744
+ durationMs: seg.durationMs,
745
+ resultSummary: seg.resultSummary,
746
+ outcome: seg.settlement,
747
+ blockReason: seg.blockReason,
748
+ },
749
+ width,
750
+ { unbounded: unboundedToolBodies },
751
+ );
752
+ }
753
+
754
+ /**
755
+ * Whether a worker block draws its one-line card this frame. `/output verbose`
756
+ * opens every block and `/output minimal` folds every block, so the entry's own
757
+ * fold decides only in between.
758
+ */
759
+ function workerEntryFolded(entry: WorkerTranscriptEntry, verbosity: OutputVerbosity): boolean {
760
+ if (verbosity === "verbose") return false;
761
+ if (verbosity === "minimal") return true;
762
+ return entry.folded;
763
+ }
764
+
765
+ function renderEntryLines(
766
+ entry: TranscriptEntry,
767
+ width: number,
768
+ expandKey: string | undefined,
769
+ latestHintToolId: string | null,
770
+ latestFoldedWorkerId: string | null,
771
+ nowMs: number,
772
+ unboundedToolBodies: boolean,
773
+ verbosity: OutputVerbosity,
774
+ liveToolOutput: boolean,
775
+ ): string[] {
776
+ if (entry.role === "replayBlock") {
777
+ return entry.renderBlock(width);
778
+ }
779
+ if (entry.role === "user") {
780
+ const contentWidth = Math.max(1, width - PROSE_GUTTER_WIDTH);
781
+ const lines: string[] = [];
782
+ for (const sourceLine of entry.text.split("\n")) lines.push(...wrapTextWithAnsi(sourceLine, contentWidth));
783
+ return hangProseLines(lines, USER_PREFIX);
784
+ }
785
+ if (entry.role === "retryStatus") {
786
+ return wrapTextWithAnsi(formatRetryStatus(entry.status), width);
787
+ }
788
+ if (entry.role === "worker") {
789
+ return renderWorkerEntryLines(entry.state, width, {
790
+ folded: workerEntryFolded(entry, verbosity),
791
+ ...(expandKey !== undefined && entry.state.assignmentId === latestFoldedWorkerId ? { expandKey } : {}),
792
+ unbounded: unboundedToolBodies,
793
+ });
794
+ }
795
+ // A settled assistant entry that rendered nothing at all contributes nothing.
796
+ // A mid-turn notice splits the transcript, so the events after it open a
797
+ // fresh entry that a stopped turn never fills; that entry used to reach the
798
+ // tail below and print a lone agent bubble under the notice.
799
+ if (
800
+ !entry.pending &&
801
+ entry.thinking.length === 0 &&
802
+ entry.turnUsage === undefined &&
803
+ !hasVisibleOutput(entry) &&
804
+ entry.segments.length === 0
805
+ ) {
806
+ return [];
807
+ }
808
+ const lines: string[] = [];
809
+ // Thinking renders BEFORE assistant text/tool segments so the folded marker
810
+ // or expanded rail sits above the response, matching the order the
811
+ // pi-coding-agent reference uses. It streams live while `pending === true`
812
+ // (folded shows a dynamic token count; expanded tail-anchors the tail) and
813
+ // collapses to a static marker / head-anchored rail once the turn settles.
814
+ // The generic "thinking" status verb is suppressed while this marker is
815
+ // active so only one indicator shows (see `shouldRenderStatus` below).
816
+ if (entry.thinking.length > 0) {
817
+ lines.push(
818
+ ...renderThinkingLines(
819
+ entry.thinking,
820
+ verbosity === "verbose" || (verbosity !== "minimal" && entry.expandedThinking === true),
821
+ width,
822
+ entry.pending,
823
+ entry.turnUsage,
824
+ ),
825
+ );
826
+ }
827
+ const clioPrefix = entry.isError ? CLIO_PREFIX_ERROR : CLIO_PREFIX;
828
+ const proseWidth = Math.max(1, width - PROSE_GUTTER_WIDTH);
829
+ let labeled = false;
830
+ for (const seg of entry.segments) {
831
+ if (seg.kind === "tool") {
832
+ lines.push(
833
+ ...renderToolSegmentLines(
834
+ seg,
835
+ width,
836
+ expandKey,
837
+ latestHintToolId,
838
+ nowMs,
839
+ unboundedToolBodies,
840
+ verbosity,
841
+ liveToolOutput,
842
+ ),
843
+ );
844
+ continue;
845
+ }
846
+ // Text and error segments share the reply-prefix bookkeeping: the first
847
+ // substantive one carries the agent glyph and every later one hangs plain.
848
+ // A leading skill-suggestion protocol line is advisory rather than the
849
+ // answer, so it renders in place without claiming the turn's voice glyph.
850
+ if (seg.kind === "text" && seg.text.length === 0) continue;
851
+ const split = seg.kind === "text" ? skillSuggestionSplit(seg) : null;
852
+ if (split) {
853
+ // The suggestion line hangs plain and the answer beneath it is ordinary
854
+ // prose, so it claims the glyph when nothing else has.
855
+ lines.push(...hangProseLines(wrapTextWithAnsi(split.suggestion, proseWidth)));
856
+ const answerLines = renderTextSegmentLines(split.answer, proseWidth);
857
+ if (answerLines.length === 0) continue;
858
+ lines.push(...(labeled ? hangProseLines(answerLines) : hangProseLines(answerLines, clioPrefix)));
859
+ labeled = true;
860
+ continue;
861
+ }
862
+ const rendered =
863
+ seg.kind === "text" ? renderTextSegmentLines(seg, proseWidth) : renderErrorSegmentLines(seg, proseWidth);
864
+ if (rendered.length === 0) continue;
865
+ const isSkillSuggestion = seg.kind === "text" && seg.text.startsWith(SKILL_SUGGESTION_PREFIX);
866
+ if (!labeled && !isSkillSuggestion) {
867
+ lines.push(...hangProseLines(rendered, clioPrefix));
868
+ labeled = true;
869
+ } else {
870
+ lines.push(...hangProseLines(rendered));
871
+ }
872
+ }
873
+ if (entry.turnUsage && !entry.pending) lines.push(...renderTurnUsageLine(entry.turnUsage, width, verbosity));
874
+ const shouldRenderStatus =
875
+ entry.pending &&
876
+ entry.statusLine !== null &&
877
+ entry.statusLine !== undefined &&
878
+ !(entry.statusLine.phase === "writing" && hasStreamingText(entry)) &&
879
+ !(entry.statusLine.phase === "thinking" && entry.thinking.length > 0);
880
+ if (!labeled && !hasVisibleOutput(entry)) {
881
+ lines.push(clioPrefix.trimEnd());
882
+ if (shouldRenderStatus) {
883
+ lines.push(
884
+ `${STATUS_INDENT}${styleStatusVerb(entry.statusLine?.verb ?? "", entry.statusLine?.toneHint ?? "muted")}`,
885
+ );
886
+ }
887
+ } else if (shouldRenderStatus) {
888
+ lines.push(`${STATUS_INDENT}${styleStatusVerb(entry.statusLine?.verb ?? "", entry.statusLine?.toneHint ?? "muted")}`);
889
+ }
890
+ return lines;
891
+ }
892
+
893
+ export function createChatPanel(options: ChatPanelOptions = {}): ChatPanel {
894
+ const transcript: TranscriptEntry[] = [];
895
+ /** Assignment to its placed block, in placement order, so a streaming delta is O(1) to route. */
896
+ const workerEntries = new Map<string, WorkerTranscriptEntry>();
897
+ let dirty = true;
898
+ let cachedWidth: number | undefined;
899
+ let cachedLines: string[] = [];
900
+ let cachedExpandKey: string | undefined;
901
+ let cachedVerbosity: OutputVerbosity | undefined;
902
+ let cachedLiveToolOutput: boolean | undefined;
903
+ let cachedTick = 0;
904
+ /**
905
+ * Did the last executed render put a counting elapsed line on screen? It is
906
+ * what decides whether the render key carries a time tick at all, so a
907
+ * transcript with nothing running keeps the old mutation-only invalidation.
908
+ */
909
+ let renderedRunningTool = false;
910
+ const entryRenderCache = new Map<TranscriptEntry, { key: string; lines: string[] }>();
911
+ /**
912
+ * The cache follows the transcript instead of stopping at a fixed 256
913
+ * entries, which re-rendered everything past the cap on every dirty frame
914
+ * (10 ms/frame at 800 entries). The ceiling bounds worst-case memory at
915
+ * roughly 4096 rendered entries; past it, the excess only re-renders on
916
+ * full-rebuild events (width change, expand-all), which the frozen prefix
917
+ * below makes rare rather than per-frame.
918
+ */
919
+ const MIN_ENTRY_RENDER_CACHE = 256;
920
+ const MAX_ENTRY_RENDER_CACHE = 4096;
921
+ const entryCacheCapacity = (): number =>
922
+ Math.max(MIN_ENTRY_RENDER_CACHE, Math.min(MAX_ENTRY_RENDER_CACHE, transcript.length));
923
+ /**
924
+ * Windowed-tail build (F19 on pi-tui 0.83 terms): the lines of every settled
925
+ * leading entry are baked into one frozen prefix, so a dirty frame re-renders
926
+ * only the live tail and re-emits the prefix by reference. pi-tui still
927
+ * receives the full line array, deliberately: the 0.83 renderer keeps every
928
+ * line in `previousLines` and full-redraws (clearing scrollback) when the
929
+ * head shrinks, so a real shrinking window needs the 0.84 ScrollView
930
+ * architecture. The freeze is dropped whenever a frozen entry is invalidated
931
+ * or the render key changes.
932
+ */
933
+ let frozen: { lines: string[]; through: number; key: string } | null = null;
934
+ let thinkingExpanded = false;
935
+ let liveToolOutput = true;
936
+ const unboundedToolBodies = options.unboundedToolBodies === true;
937
+
938
+ const markDirty = (): void => {
939
+ dirty = true;
940
+ };
941
+ const invalidateEntryCache = (entry: TranscriptEntry): void => {
942
+ entryRenderCache.delete(entry);
943
+ if (frozen !== null && transcript.indexOf(entry) < frozen.through) frozen = null;
944
+ };
945
+ /** Full drop of both render caches; used by the toggle paths that touch many entries. */
946
+ const clearRenderCaches = (): void => {
947
+ entryRenderCache.clear();
948
+ frozen = null;
949
+ };
950
+ /**
951
+ * A mutation is about to land on the tail entry without an explicit
952
+ * invalidation. If the freeze extends over the whole transcript, that tail
953
+ * entry is frozen and the freeze must go.
954
+ */
955
+ const unfreezeTail = (): void => {
956
+ if (frozen !== null && frozen.through >= transcript.length) frozen = null;
957
+ };
958
+
959
+ const resolveExpandKey = (): string | undefined => {
960
+ const key = options.getToolExpandKey?.();
961
+ if (typeof key !== "string" || key.length === 0) return undefined;
962
+ return key;
963
+ };
964
+
965
+ const now = (): number => options.now?.() ?? Date.now();
966
+
967
+ /**
968
+ * Force an in-flight tool segment to a settled error line. A call blocked at
969
+ * admission (loop guard, safety) or one whose `tool_execution_end` never
970
+ * arrives (aborted mid-batch, a model that reuses a tool-call id) would
971
+ * otherwise stay a counting `· N.Ns` running line forever. Settling it with
972
+ * its OWN elapsed gives it the same visual grammar as any other error
973
+ * (`✗ · <ms>`), so a blocked call reads like the failure it is.
974
+ */
975
+ const settleUnfinishedToolSegment = (seg: ToolSegment, settlement: "aborted" | "orphaned" = "orphaned"): void => {
976
+ if (seg.finished) return;
977
+ seg.finished = true;
978
+ seg.isError = true;
979
+ seg.settledWithoutResult = true;
980
+ if (seg.durationMs === undefined && seg.startedAtMs !== undefined) {
981
+ seg.durationMs = Math.max(1, now() - seg.startedAtMs);
982
+ }
983
+ if (seg.result === undefined)
984
+ seg.result = "(no result: the call did not complete; execution was aborted, blocked, or orphaned)";
985
+ seg.settlement = settlement;
986
+ seg.partialOutput = undefined;
987
+ seg.awaitingApproval = undefined;
988
+ };
989
+
990
+ /**
991
+ * Locate the most recent tool segment with this call id anywhere in the
992
+ * transcript. A mid-turn notice entry (safety-net block, approval parked,
993
+ * context-engine notice) splits the transcript, so an in-flight call's
994
+ * segment can live in an earlier assistant entry than the tail. Unfinished
995
+ * segments win over finished ones so an id the model reuses binds to the
996
+ * live call, not the settled one. Among finished segments only a
997
+ * force-settled one (no end event of its own) is returned: a late true
998
+ * result may upgrade the synthetic settle, but a segment that finished
999
+ * with its own result is never rewritten after the fact.
1000
+ */
1001
+ const findToolSegmentOwner = (toolCallId: string): { segment: ToolSegment; entry: TranscriptEntry } | undefined => {
1002
+ let settledMatch: { segment: ToolSegment; entry: TranscriptEntry } | undefined;
1003
+ for (let entryIndex = transcript.length - 1; entryIndex >= 0; entryIndex -= 1) {
1004
+ const entry = transcript[entryIndex];
1005
+ if (entry?.role !== "assistant") continue;
1006
+ for (let segIndex = entry.segments.length - 1; segIndex >= 0; segIndex -= 1) {
1007
+ const seg = entry.segments[segIndex];
1008
+ if (seg?.kind !== "tool" || seg.id !== toolCallId) continue;
1009
+ if (!seg.finished) return { segment: seg, entry };
1010
+ if (seg.settledWithoutResult === true) settledMatch ??= { segment: seg, entry };
1011
+ }
1012
+ }
1013
+ return settledMatch;
1014
+ };
1015
+
1016
+ const ensureAssistant = (): Extract<TranscriptEntry, { role: "assistant" }> => {
1017
+ const last = transcript[transcript.length - 1];
1018
+ if (last && last.role === "assistant") {
1019
+ // Callers mutate the returned entry (pending, thinking, segments)
1020
+ // without always invalidating; a fully-frozen transcript would keep
1021
+ // serving the settled render of this tail entry.
1022
+ unfreezeTail();
1023
+ return last;
1024
+ }
1025
+ const entry: Extract<TranscriptEntry, { role: "assistant" }> = {
1026
+ role: "assistant",
1027
+ segments: [],
1028
+ thinking: "",
1029
+ expandedThinking: thinkingExpanded,
1030
+ pending: false,
1031
+ isError: false,
1032
+ };
1033
+ transcript.push(entry);
1034
+ return entry;
1035
+ };
1036
+
1037
+ const appendTextDelta = (entry: Extract<TranscriptEntry, { role: "assistant" }>, delta: string): void => {
1038
+ if (delta.length === 0) return;
1039
+ invalidateEntryCache(entry);
1040
+ const tail = entry.segments[entry.segments.length - 1];
1041
+ if (tail && tail.kind === "text" && !tail.finalized) {
1042
+ tail.text += delta;
1043
+ return;
1044
+ }
1045
+ entry.segments.push({ kind: "text", text: delta, finalized: false });
1046
+ };
1047
+
1048
+ /**
1049
+ * Canonicalize a streamed text segment from a completed assistant message.
1050
+ * When streaming produced a prefix of the final text (the common case),
1051
+ * the tail segment is overwritten and flipped to finalized so the next
1052
+ * render pipes it through Markdown. When the message arrived fully formed
1053
+ * with no deltas (non-streaming test path, synthetic notices), a fresh
1054
+ * finalized text segment is appended after any tool segments that may
1055
+ * have landed in this turn already. `replaceTail` forces the overwrite for
1056
+ * messages the chat loop rewrote after streaming (locked-turn markup
1057
+ * sanitation): the streamed tail is dead text there, not a prefix.
1058
+ */
1059
+ const canonicalizeMessageText = (
1060
+ entry: Extract<TranscriptEntry, { role: "assistant" }>,
1061
+ text: string,
1062
+ replaceTail = false,
1063
+ ): void => {
1064
+ if (text.length === 0) return;
1065
+ const tail = entry.segments[entry.segments.length - 1];
1066
+ if (tail?.kind === "text" && !tail.finalized && (replaceTail || text.startsWith(tail.text))) {
1067
+ tail.text = text;
1068
+ tail.finalized = true;
1069
+ // The streaming wrap cache assumes append-only text. This is the one
1070
+ // path that rewrites it wholesale, and finalized segments render through
1071
+ // Markdown instead, so the cache is dead here either way.
1072
+ delete tail.wrapCache;
1073
+ if (tail.md) tail.md.setText(text);
1074
+ return;
1075
+ }
1076
+ entry.segments.push({ kind: "text", text, finalized: true });
1077
+ };
1078
+
1079
+ /**
1080
+ * Append the turn's terminal-error marker as its own error segment so the
1081
+ * render path styles it in the error token rather than piping it through
1082
+ * Markdown as prose. Guards against a duplicate when the same marker arrives
1083
+ * twice for one settled turn.
1084
+ */
1085
+ const appendErrorSegment = (entry: Extract<TranscriptEntry, { role: "assistant" }>, text: string): void => {
1086
+ const tail = entry.segments[entry.segments.length - 1];
1087
+ if (tail?.kind === "error" && tail.text === text) return;
1088
+ entry.segments.push({ kind: "error", text });
1089
+ };
1090
+
1091
+ /**
1092
+ * Who advertises the fold key this frame: the newest folded worker card, or
1093
+ * the newest finished collapsed tool subline with no worker card behind it.
1094
+ * One surface at most, because the key reaches the newest foldable thing of
1095
+ * either kind, and a chord shown anywhere else would open something the
1096
+ * operator was not looking at. An already-open newest card advertises
1097
+ * nothing, since folding it again needs no invitation.
1098
+ */
1099
+ const expandHintOwner = (): { toolId: string | null; workerId: string | null } => {
1100
+ let workerMayOwn = true;
1101
+ for (let entryIndex = transcript.length - 1; entryIndex >= 0; entryIndex -= 1) {
1102
+ const entry = transcript[entryIndex];
1103
+ if (entry?.role === "worker") {
1104
+ return { toolId: null, workerId: workerMayOwn && entry.folded ? entry.state.assignmentId : null };
1105
+ }
1106
+ if (entry?.role !== "assistant") continue;
1107
+ for (let segIndex = entry.segments.length - 1; segIndex >= 0; segIndex -= 1) {
1108
+ const seg = entry.segments[segIndex];
1109
+ if (seg?.kind !== "tool") continue;
1110
+ if (seg.finished && !seg.expanded) return { toolId: seg.id, workerId: null };
1111
+ workerMayOwn = false;
1112
+ }
1113
+ }
1114
+ return { toolId: null, workerId: null };
1115
+ };
1116
+
1117
+ /**
1118
+ * Where a newly seen worker block belongs. An agent-origin run nests under
1119
+ * the assistant entry holding the tool call that spawned it, behind any
1120
+ * sibling blocks the same call already placed, so a fan-out reads top to
1121
+ * bottom in spawn order. Everything else, including a run whose parent call
1122
+ * is no longer in the transcript (a detached collect landing turns later),
1123
+ * appends at the tail. null means "append".
1124
+ */
1125
+ const workerInsertionIndex = (state: WorkerEntryState): number | null => {
1126
+ const parentToolCallId = state.parentToolCallId;
1127
+ if (parentToolCallId === undefined) return null;
1128
+ const owner = findToolSegmentOwner(parentToolCallId);
1129
+ if (owner === undefined) return null;
1130
+ const parentIndex = transcript.indexOf(owner.entry);
1131
+ if (parentIndex < 0) return null;
1132
+ let index = parentIndex + 1;
1133
+ while (transcript[index]?.role === "worker") index += 1;
1134
+ return index >= transcript.length ? null : index;
1135
+ };
1136
+
1137
+ /** True when the entry owns the tool the expand hint currently points at. */
1138
+ const entryContainsHint = (entry: TranscriptEntry, latestHintToolId: string | null): boolean =>
1139
+ latestHintToolId !== null &&
1140
+ entry.role === "assistant" &&
1141
+ entry.segments.some((segment) => segment.kind === "tool" && segment.id === latestHintToolId);
1142
+
1143
+ /** True when the entry renders at least one counting elapsed line this frame. */
1144
+ const entryHasRunningTool = (entry: TranscriptEntry): boolean =>
1145
+ entry.role === "assistant" &&
1146
+ entry.segments.some((segment) => segment.kind === "tool" && !segment.finished && segment.startedAtMs !== undefined);
1147
+
1148
+ /**
1149
+ * Settled entries whose render is a pure function of the base key. A live
1150
+ * worker block is excluded for the same reason a running tool is: the
1151
+ * reducer mutates its state object in place, so a cached render would keep
1152
+ * serving the answer as it looked several deltas ago.
1153
+ */
1154
+ const entryIsStable = (entry: TranscriptEntry): boolean =>
1155
+ entry.role === "user" ||
1156
+ entry.role === "replayBlock" ||
1157
+ (entry.role === "worker" && !entry.state.pending) ||
1158
+ (entry.role === "assistant" &&
1159
+ !entry.pending &&
1160
+ !entry.segments.some((segment) => segment.kind === "tool" && !segment.finished));
1161
+
1162
+ const render = (width: number): string[] => {
1163
+ const startedAt = performance.now();
1164
+ const expandKey = resolveExpandKey();
1165
+ const verbosity = options.getOutputVerbosity?.() ?? "default";
1166
+ const nowMs = now();
1167
+ // `dirty` is set on mutation and never on a tick, so without time in the
1168
+ // key a running tool's elapsed counter advanced only when something
1169
+ // unrelated invalidated the panel. The tick is the same 100 ms bucket
1170
+ // dispatch-board.ts uses for running rows, taken off the injectable
1171
+ // clock so a fixed clock still produces byte-stable output, and it is
1172
+ // pinned to 0 whenever nothing is counting so a settled transcript
1173
+ // re-renders no more often than it did before.
1174
+ const tick = renderedRunningTool ? Math.floor(nowMs / 100) : 0;
1175
+ // The hit guard runs before any transcript scan. expandHintOwner
1176
+ // walks the whole transcript when the newest tool is expanded or absent, and
1177
+ // it is not part of the panel-level key, so computing it above the guard cost
1178
+ // a full scan per frame for a value the early return discards.
1179
+ if (
1180
+ !dirty &&
1181
+ cachedWidth === width &&
1182
+ cachedExpandKey === expandKey &&
1183
+ cachedVerbosity === verbosity &&
1184
+ cachedLiveToolOutput === liveToolOutput &&
1185
+ cachedTick === tick
1186
+ ) {
1187
+ options.onRenderMetrics?.({ durationMs: performance.now() - startedAt, cacheHit: true, entriesRendered: 0 });
1188
+ return cachedLines;
1189
+ }
1190
+ const { toolId: latestHintToolId, workerId: latestFoldedWorkerId } = expandHintOwner();
1191
+ // The hint id is deliberately NOT part of the shared key: it changes on
1192
+ // every finished collapsed tool, and keying every entry on it re-rendered
1193
+ // the entire transcript per tool completion. Only the entry that contains
1194
+ // the hint tool renders differently, so only that entry's key carries it.
1195
+ // The tick stays out of the entry key: a settled entry renders the same
1196
+ // bytes at every tick, and keying it on time would drop the entry cache
1197
+ // and the frozen prefix ten times a second. Only the panel-level guard
1198
+ // above is time-keyed, so a tick re-renders the live tail and nothing else.
1199
+ const baseKey = `${width}|${expandKey ?? ""}|${verbosity}|${liveToolOutput}`;
1200
+ const capacity = entryCacheCapacity();
1201
+ if (frozen !== null && frozen.key !== baseKey) frozen = null;
1202
+ const out: string[] = frozen === null ? [] : frozen.lines.slice();
1203
+ const startIndex = frozen === null ? 0 : frozen.through;
1204
+ // The freeze extends over the contiguous run of stable, hint-free leading
1205
+ // entries; it is captured after the loop from what this frame rendered.
1206
+ let freezeThrough = startIndex;
1207
+ let freezeLineCount = out.length;
1208
+ let freezeOpen = frozen !== null || startIndex === 0;
1209
+ let entriesRendered = 0;
1210
+ // Only entries at or past `startIndex` can hold a running tool: the frozen
1211
+ // prefix is by construction a run of stable entries, and stable means no
1212
+ // unfinished tool segment.
1213
+ let sawRunningTool = false;
1214
+ for (let i = startIndex; i < transcript.length; i += 1) {
1215
+ const entry = transcript[i];
1216
+ if (!entry) continue;
1217
+ if (!sawRunningTool && entryHasRunningTool(entry)) sawRunningTool = true;
1218
+ // Folded worker cards are a list, not a series of blocks: a fan-out of
1219
+ // five scouts costs five rows, which is what makes the folded default
1220
+ // worth having. Anything else keeps the blank line between entries.
1221
+ const previous = i > 0 ? transcript[i - 1] : undefined;
1222
+ const stacksOnPrevious =
1223
+ entry.role === "worker" &&
1224
+ previous?.role === "worker" &&
1225
+ workerEntryFolded(entry, verbosity) &&
1226
+ workerEntryFolded(previous, verbosity);
1227
+ if (i > 0 && !stacksOnPrevious) out.push("");
1228
+ const containsHint =
1229
+ entryContainsHint(entry, latestHintToolId) ||
1230
+ (entry.role === "worker" && entry.state.assignmentId === latestFoldedWorkerId);
1231
+ const entryKey = containsHint ? `${baseKey}|hint:${latestHintToolId}|${latestFoldedWorkerId}` : baseKey;
1232
+ const cached = entryRenderCache.get(entry);
1233
+ const cacheable = i >= transcript.length - capacity && entry.role !== "replayBlock" && entryIsStable(entry);
1234
+ if (cacheable && cached?.key === entryKey) {
1235
+ // A spread here is slower than a loop for large arrays and blows the
1236
+ // stack outright for a single entry that renders enough lines.
1237
+ for (const line of cached.lines) out.push(line);
1238
+ } else {
1239
+ entriesRendered += 1;
1240
+ const renderedEntry = renderEntryLines(
1241
+ entry,
1242
+ width,
1243
+ expandKey,
1244
+ latestHintToolId,
1245
+ latestFoldedWorkerId,
1246
+ nowMs,
1247
+ unboundedToolBodies,
1248
+ verbosity,
1249
+ liveToolOutput,
1250
+ );
1251
+ for (const line of renderedEntry) out.push(line);
1252
+ if (cacheable) {
1253
+ entryRenderCache.set(entry, { key: entryKey, lines: renderedEntry });
1254
+ while (entryRenderCache.size > capacity) {
1255
+ const oldest = entryRenderCache.keys().next().value;
1256
+ if (oldest === undefined) break;
1257
+ entryRenderCache.delete(oldest);
1258
+ }
1259
+ }
1260
+ }
1261
+ if (freezeOpen && i === freezeThrough && entryIsStable(entry) && !containsHint) {
1262
+ freezeThrough = i + 1;
1263
+ freezeLineCount = out.length;
1264
+ } else {
1265
+ freezeOpen = false;
1266
+ }
1267
+ }
1268
+ frozen = freezeThrough > 0 ? { lines: out.slice(0, freezeLineCount), through: freezeThrough, key: baseKey } : null;
1269
+ cachedLines = out;
1270
+ cachedWidth = width;
1271
+ cachedExpandKey = expandKey;
1272
+ cachedVerbosity = verbosity;
1273
+ cachedLiveToolOutput = liveToolOutput;
1274
+ cachedTick = tick;
1275
+ renderedRunningTool = sawRunningTool;
1276
+ dirty = false;
1277
+ options.onRenderMetrics?.({ durationMs: performance.now() - startedAt, cacheHit: false, entriesRendered });
1278
+ return out;
1279
+ };
1280
+
1281
+ return {
1282
+ appendUser(text: string): void {
1283
+ transcript.push({ role: "user", text });
1284
+ markDirty();
1285
+ },
1286
+ appendReplayBlock(renderBlock: ReplayBlockRenderer): void {
1287
+ transcript.push({ role: "replayBlock", renderBlock });
1288
+ markDirty();
1289
+ },
1290
+ applyWorkerState(state: WorkerEntryState): void {
1291
+ const existing = workerEntries.get(state.assignmentId);
1292
+ if (existing !== undefined) {
1293
+ // The reducer mutated the same state object this entry already holds,
1294
+ // so nothing is re-linked; only the cached render is now stale.
1295
+ invalidateEntryCache(existing);
1296
+ markDirty();
1297
+ return;
1298
+ }
1299
+ const entry: WorkerTranscriptEntry = { role: "worker", state, folded: workerAskedByModel(state) };
1300
+ workerEntries.set(state.assignmentId, entry);
1301
+ const at = workerInsertionIndex(state);
1302
+ if (at === null) {
1303
+ transcript.push(entry);
1304
+ } else {
1305
+ transcript.splice(at, 0, entry);
1306
+ // A frozen prefix is a run of indices. Inserting inside it renumbers
1307
+ // every entry behind the cut, so the freeze has to go.
1308
+ if (frozen !== null && at < frozen.through) frozen = null;
1309
+ }
1310
+ markDirty();
1311
+ },
1312
+ workerStates(): ReadonlyArray<WorkerEntryState> {
1313
+ return [...workerEntries.values()].map((entry) => entry.state);
1314
+ },
1315
+ toggleLastToolExpanded(): boolean {
1316
+ // Ctrl+O owns the newest foldable thing, whichever kind it is. A worker
1317
+ // block the operator just watched land is what they mean by "expand
1318
+ // that", not the tool call two screens up that spawned it.
1319
+ for (let entryIndex = transcript.length - 1; entryIndex >= 0; entryIndex -= 1) {
1320
+ const entry = transcript[entryIndex];
1321
+ if (entry?.role === "worker") {
1322
+ entry.folded = !entry.folded;
1323
+ clearRenderCaches();
1324
+ markDirty();
1325
+ return true;
1326
+ }
1327
+ if (entry?.role !== "assistant") continue;
1328
+ for (let segIndex = entry.segments.length - 1; segIndex >= 0; segIndex -= 1) {
1329
+ const seg = entry.segments[segIndex];
1330
+ if (seg?.kind !== "tool") continue;
1331
+ seg.expanded = !seg.expanded;
1332
+ clearRenderCaches();
1333
+ markDirty();
1334
+ return true;
1335
+ }
1336
+ }
1337
+ return false;
1338
+ },
1339
+ toggleAllToolsExpanded(): boolean {
1340
+ const tools: ToolSegment[] = [];
1341
+ const workers: WorkerTranscriptEntry[] = [];
1342
+ for (const entry of transcript) {
1343
+ if (entry.role === "worker") {
1344
+ workers.push(entry);
1345
+ continue;
1346
+ }
1347
+ if (entry.role !== "assistant") continue;
1348
+ for (const seg of entry.segments) {
1349
+ if (seg.kind === "tool") tools.push(seg);
1350
+ }
1351
+ }
1352
+ if (tools.length === 0 && workers.length === 0) return false;
1353
+ const expand = tools.some((seg) => !seg.expanded) || workers.some((entry) => entry.folded);
1354
+ for (const seg of tools) seg.expanded = expand;
1355
+ for (const entry of workers) entry.folded = !expand;
1356
+ clearRenderCaches();
1357
+ markDirty();
1358
+ return true;
1359
+ },
1360
+ collapseAllTools(): void {
1361
+ for (const entry of transcript) {
1362
+ if (entry.role === "worker") {
1363
+ // The settled view for a worker is its origin default, not
1364
+ // universally folded: the operator's own run is the one block
1365
+ // /run exists to show them.
1366
+ entry.folded = workerAskedByModel(entry.state);
1367
+ continue;
1368
+ }
1369
+ if (entry.role !== "assistant") continue;
1370
+ for (const seg of entry.segments) {
1371
+ if (seg.kind === "tool") seg.expanded = false;
1372
+ }
1373
+ }
1374
+ clearRenderCaches();
1375
+ markDirty();
1376
+ },
1377
+ toggleLastThinking(): boolean {
1378
+ for (let entryIndex = transcript.length - 1; entryIndex >= 0; entryIndex -= 1) {
1379
+ const entry = transcript[entryIndex];
1380
+ if (entry?.role !== "assistant") continue;
1381
+ if (entry.thinking.length === 0) continue;
1382
+ entry.expandedThinking = entry.expandedThinking !== true;
1383
+ thinkingExpanded = entry.expandedThinking === true;
1384
+ clearRenderCaches();
1385
+ markDirty();
1386
+ return true;
1387
+ }
1388
+ thinkingExpanded = !thinkingExpanded;
1389
+ return true;
1390
+ },
1391
+ toggleAllThinking(): boolean {
1392
+ const entries: Array<Extract<TranscriptEntry, { role: "assistant" }>> = [];
1393
+ for (const entry of transcript) {
1394
+ if (entry.role === "assistant" && entry.thinking.length > 0) entries.push(entry);
1395
+ }
1396
+ if (entries.length === 0) {
1397
+ thinkingExpanded = !thinkingExpanded;
1398
+ return true;
1399
+ }
1400
+ const expand = entries.some((entry) => entry.expandedThinking !== true);
1401
+ for (const entry of entries) entry.expandedThinking = expand;
1402
+ thinkingExpanded = expand;
1403
+ clearRenderCaches();
1404
+ markDirty();
1405
+ return true;
1406
+ },
1407
+ toggleLiveToolOutput(): boolean {
1408
+ liveToolOutput = !liveToolOutput;
1409
+ markDirty();
1410
+ return liveToolOutput;
1411
+ },
1412
+ reset(): void {
1413
+ transcript.length = 0;
1414
+ workerEntries.clear();
1415
+ clearRenderCaches();
1416
+ markDirty();
1417
+ },
1418
+ applyEvent(event: ChatLoopEvent): void {
1419
+ if (event.type === "agent_status") {
1420
+ return;
1421
+ }
1422
+ if (event.type === "notice") {
1423
+ // Transcript notices are first-class advisory lines, not assistant
1424
+ // messages: render with the bracketed-tag treatment replay lines get.
1425
+ if (event.surface !== "transcript") return;
1426
+ const text = event.text;
1427
+ transcript.push({
1428
+ role: "replayBlock",
1429
+ renderBlock: (width) => wrapTextWithAnsi(styleTaggedNotice(text), width),
1430
+ });
1431
+ markDirty();
1432
+ return;
1433
+ }
1434
+ if (event.type === "queued_user_turn") {
1435
+ // A queued steer or follow-up the engine just injected. Rendering it
1436
+ // here, at injection time, keeps the transcript in the order the
1437
+ // model saw: enqueue time shows the text only in the queue panel.
1438
+ transcript.push({ role: "user", text: event.text });
1439
+ markDirty();
1440
+ return;
1441
+ }
1442
+ if (event.type === "text_delta") {
1443
+ const assistant = ensureAssistant();
1444
+ assistant.pending = true;
1445
+ appendTextDelta(assistant, event.delta);
1446
+ markDirty();
1447
+ return;
1448
+ }
1449
+ if (event.type === "thinking_delta") {
1450
+ // Capture for downstream consumers but never render inline.
1451
+ const assistant = ensureAssistant();
1452
+ assistant.pending = true;
1453
+ assistant.thinking += event.delta;
1454
+ assistant.expandedThinking = thinkingExpanded;
1455
+ markDirty();
1456
+ return;
1457
+ }
1458
+ if (event.type === "message_start" && event.message.role === "assistant") {
1459
+ ensureAssistant().pending = true;
1460
+ markDirty();
1461
+ return;
1462
+ }
1463
+ if (event.type === "tool_execution_start") {
1464
+ // A model that reuses a tool-call id across calls would leave the
1465
+ // prior same-id segment unsettled (its end matches the first segment
1466
+ // on lookup). Settle any such orphan now, wherever it lives, so it
1467
+ // does not linger as a counting running line while this call runs.
1468
+ for (const entry of transcript) {
1469
+ if (entry.role !== "assistant") continue;
1470
+ for (const seg of entry.segments) {
1471
+ if (seg.kind === "tool" && seg.id === event.toolCallId && !seg.finished) settleUnfinishedToolSegment(seg);
1472
+ }
1473
+ }
1474
+ const assistant = ensureAssistant();
1475
+ // Compact resource reads (SKILL.md, CLIO-CODER.md, AGENTS.md, docs/) stay
1476
+ // collapsed to one labeled line until explicitly expanded.
1477
+ const expanded = assistant.pending === false && classifyResourceRead(event.toolName, event.args) === null;
1478
+ assistant.pending = true;
1479
+ assistant.segments.push({
1480
+ kind: "tool",
1481
+ id: event.toolCallId,
1482
+ name: event.toolName,
1483
+ args: event.args,
1484
+ finished: false,
1485
+ isError: false,
1486
+ expanded,
1487
+ startedAtMs: now(),
1488
+ });
1489
+ markDirty();
1490
+ return;
1491
+ }
1492
+ if (event.type === "tool_approval_state") {
1493
+ // Only the entry that owns the segment can change. Clearing the whole
1494
+ // 256-entry cache re-rendered every settled turn in the transcript;
1495
+ // on a 400-turn session that was 26.7 ms per event, and
1496
+ // tool_execution_update fires on every output tick of a running command.
1497
+ const owner = findToolSegmentOwner(event.toolCallId);
1498
+ if (owner) invalidateEntryCache(owner.entry);
1499
+ const tool = owner?.segment;
1500
+ if (tool && !tool.finished) {
1501
+ tool.awaitingApproval = event.state === "awaiting-approval" ? true : undefined;
1502
+ markDirty();
1503
+ }
1504
+ return;
1505
+ }
1506
+ if (event.type === "tool_execution_update") {
1507
+ // pi-agent emits `partialResult` as a cumulative tool-result envelope
1508
+ // (the bash tool concatenates its rolling tail buffer on every tick).
1509
+ // Unwrap with the same helper the finished-result path uses, then
1510
+ // REPLACE `partialOutput` rather than appending: the upstream
1511
+ // semantics are cumulative, so appending would double-print every
1512
+ // snapshot. Render dispatch picks up the new buffer on the next
1513
+ // frame via `renderToolSegmentLines`.
1514
+ const owner = findToolSegmentOwner(event.toolCallId);
1515
+ if (owner) invalidateEntryCache(owner.entry);
1516
+ const tool = owner?.segment;
1517
+ if (tool && !tool.finished) {
1518
+ const unwrapped = unwrapResultEnvelope(event.partialResult);
1519
+ tool.partialOutput = typeof unwrapped === "string" ? unwrapped : previewResult(unwrapped);
1520
+ }
1521
+ markDirty();
1522
+ return;
1523
+ }
1524
+ if (event.type === "tool_execution_end") {
1525
+ const owner = findToolSegmentOwner(event.toolCallId);
1526
+ if (owner) invalidateEntryCache(owner.entry);
1527
+ const tool = owner?.segment;
1528
+ if (tool) {
1529
+ tool.result = event.result;
1530
+ tool.isError = event.isError;
1531
+ // The chat loop enriches tool_execution_end with durationMs, the
1532
+ // persisted resultSummary (bytes, truncated, offloadPath,
1533
+ // observation counts), and the registry's admission verdict;
1534
+ // carry them so the ledger line and replay render identical facts.
1535
+ const enriched = event as {
1536
+ durationMs?: unknown;
1537
+ resultSummary?: unknown;
1538
+ outcome?: unknown;
1539
+ blockReason?: unknown;
1540
+ };
1541
+ // Settlement is that verdict, never an inference from result text.
1542
+ // The text of a tool result is the tool's own output: `node --test`
1543
+ // prints `cancelled 0` on every run and a linter can print
1544
+ // "blocked" for its own reasons, so matching those words there
1545
+ // labelled ordinary command failures as permission blocks and
1546
+ // suppressed the output that would have explained them.
1547
+ const settled = toolSettlement(enriched);
1548
+ tool.settlement = settled?.settlement;
1549
+ tool.blockReason = settled?.reason;
1550
+ tool.finished = true;
1551
+ // The true result replaces a synthetic settle; from here on the
1552
+ // segment is model-finished and immutable to later end events.
1553
+ tool.settledWithoutResult = undefined;
1554
+ if (typeof enriched.durationMs === "number" && Number.isFinite(enriched.durationMs)) {
1555
+ tool.durationMs = enriched.durationMs;
1556
+ } else if (tool.startedAtMs !== undefined) {
1557
+ const elapsed = Math.max(0, now() - tool.startedAtMs);
1558
+ if (elapsed > 0) tool.durationMs = elapsed;
1559
+ }
1560
+ if (
1561
+ enriched.resultSummary !== null &&
1562
+ typeof enriched.resultSummary === "object" &&
1563
+ !Array.isArray(enriched.resultSummary)
1564
+ ) {
1565
+ tool.resultSummary = enriched.resultSummary as Record<string, unknown>;
1566
+ }
1567
+ // Drop the streaming buffer once the final result has landed; the
1568
+ // expanded render switches to `renderToolExecution` and stays
1569
+ // stable instead of churning through partial-frame layout. A
1570
+ // denied park settles here too, so the awaiting styling must go.
1571
+ tool.partialOutput = undefined;
1572
+ tool.awaitingApproval = undefined;
1573
+ }
1574
+ markDirty();
1575
+ return;
1576
+ }
1577
+ if (event.type === "message_end") {
1578
+ const text = extractAssistantText(event.message);
1579
+ const thinking = extractAssistantThinking(event.message);
1580
+ const extractedTerminalError = extractAssistantTerminalError(event.message);
1581
+ const current = transcript[transcript.length - 1];
1582
+ const terminalError =
1583
+ current?.role === "assistant"
1584
+ ? scopeTerminalErrorAfterSuccessfulTool(current, extractedTerminalError)
1585
+ : extractedTerminalError;
1586
+ const usage = assistantUsage(event.message);
1587
+ if (text.length === 0 && thinking.length === 0 && terminalError.length === 0 && usage === undefined) return;
1588
+ const assistant = ensureAssistant();
1589
+ // message_end rewrites exactly one entry: the assistant it lands on.
1590
+ invalidateEntryCache(assistant);
1591
+ if (usage !== undefined) assistant.turnUsage = usage;
1592
+ if (terminalError.length > 0) assistant.isError = true;
1593
+ if (thinking.length > 0) {
1594
+ assistant.thinking = thinking;
1595
+ assistant.expandedThinking = thinkingExpanded;
1596
+ }
1597
+ // The chat loop marks messages it sanitized after streaming (dead
1598
+ // tool-call markup on a synthesis-locked turn); the streamed tail
1599
+ // must be replaced, not kept alongside a duplicate segment.
1600
+ const sanitized = (event as { lockedSynthesisSanitized?: unknown }).lockedSynthesisSanitized === true;
1601
+ if (text.length > 0) canonicalizeMessageText(assistant, text, sanitized);
1602
+ if (terminalError.length > 0) appendErrorSegment(assistant, terminalError);
1603
+ markDirty();
1604
+ return;
1605
+ }
1606
+ if (event.type === "retry_status") {
1607
+ const last = transcript[transcript.length - 1];
1608
+ if (last?.role === "retryStatus" && last.status.attempt === event.status.attempt) {
1609
+ last.status = event.status;
1610
+ } else {
1611
+ transcript.push({ role: "retryStatus", status: event.status });
1612
+ }
1613
+ markDirty();
1614
+ return;
1615
+ }
1616
+ if (event.type === "agent_end") {
1617
+ // agent_end can touch many entries, but it names every one it touches:
1618
+ // the usage caption's target, the later entries whose caption it
1619
+ // removes, and any entry it settles or un-pends below. Each is
1620
+ // invalidated at the point of mutation instead of dropping the whole
1621
+ // cache and re-rendering settled history.
1622
+ const runUsage = aggregateAssistantUsage(event.messages);
1623
+ if (runUsage !== undefined) {
1624
+ // The usage line is a caption on rendered output, so it goes on the
1625
+ // last entry that rendered any. A mid-turn notice splits entries, and
1626
+ // a turn that stops after one leaves an empty tail entry behind: the
1627
+ // run total landed there while the entry above kept its own
1628
+ // message_end line, so one turn printed the identical line twice, once
1629
+ // on each side of the notice. Entries after the caption rendered
1630
+ // nothing and must not carry a second copy.
1631
+ const index = lastAssistantIndex(transcript, { withOutput: true }) ?? lastAssistantIndex(transcript);
1632
+ const target = index === null ? undefined : transcript[index];
1633
+ if (target?.role === "assistant") {
1634
+ invalidateEntryCache(target);
1635
+ target.turnUsage = runUsage;
1636
+ }
1637
+ for (let after = (index ?? -1) + 1; after < transcript.length; after += 1) {
1638
+ const later = transcript[after];
1639
+ if (later?.role !== "assistant") continue;
1640
+ if (later.turnUsage !== undefined) invalidateEntryCache(later);
1641
+ delete later.turnUsage;
1642
+ }
1643
+ }
1644
+ // The run is over: no tool can still be executing anywhere in the
1645
+ // transcript, not just in the tail entry (a mid-turn notice splits
1646
+ // entries). Settle any tool segment whose `tool_execution_end` never
1647
+ // arrived (blocked at admission, or cut off by an abort) so the
1648
+ // ledger never leaves a running line counting past the turn's end,
1649
+ // and clear `pending` everywhere so no earlier entry keeps rendering
1650
+ // live thinking or status.
1651
+ const runWasAborted =
1652
+ Array.isArray(event.messages) &&
1653
+ event.messages.some(
1654
+ (message) =>
1655
+ message && typeof message === "object" && (message as { stopReason?: unknown }).stopReason === "aborted",
1656
+ );
1657
+ for (const entry of transcript) {
1658
+ if (entry.role !== "assistant") continue;
1659
+ for (const seg of entry.segments) {
1660
+ if (seg.kind === "tool" && !seg.finished) {
1661
+ invalidateEntryCache(entry);
1662
+ settleUnfinishedToolSegment(seg, runWasAborted ? "aborted" : "orphaned");
1663
+ }
1664
+ }
1665
+ if (entry.pending) {
1666
+ invalidateEntryCache(entry);
1667
+ entry.pending = false;
1668
+ entry.statusLine = null;
1669
+ }
1670
+ }
1671
+ markDirty();
1672
+ }
1673
+ },
1674
+ setStatusLine(line): void {
1675
+ if (line) {
1676
+ const assistant = ensureAssistant();
1677
+ assistant.pending = true;
1678
+ assistant.statusLine = line;
1679
+ markDirty();
1680
+ return;
1681
+ }
1682
+ const last = transcript[transcript.length - 1];
1683
+ if (last && last.role === "assistant") {
1684
+ unfreezeTail();
1685
+ last.statusLine = null;
1686
+ markDirty();
1687
+ }
1688
+ },
1689
+ render,
1690
+ invalidate(): void {
1691
+ markDirty();
1692
+ },
1693
+ };
1694
+ }