miko-code 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (379) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +179 -0
  3. package/bin/miko +10 -0
  4. package/dist/client/antigravity.webp +0 -0
  5. package/dist/client/assets/abap-BdImnpbu.js +1 -0
  6. package/dist/client/assets/actionscript-3-CoDkCxhg.js +1 -0
  7. package/dist/client/assets/ada-bCR0ucgS.js +1 -0
  8. package/dist/client/assets/andromeeda-C4gqWexZ.js +1 -0
  9. package/dist/client/assets/angular-html-CU67Zn6k.js +1 -0
  10. package/dist/client/assets/angular-ts-BwZT4LLn.js +1 -0
  11. package/dist/client/assets/apache-Pmp26Uib.js +1 -0
  12. package/dist/client/assets/apex-D8_7TLub.js +1 -0
  13. package/dist/client/assets/apl-dKokRX4l.js +1 -0
  14. package/dist/client/assets/applescript-Co6uUVPk.js +1 -0
  15. package/dist/client/assets/ara-BRHolxvo.js +1 -0
  16. package/dist/client/assets/asciidoc-Ve4PFQV2.js +1 -0
  17. package/dist/client/assets/asm-D_Q5rh1f.js +1 -0
  18. package/dist/client/assets/astro-CbQHKStN.js +1 -0
  19. package/dist/client/assets/aurora-x-D-2ljcwZ.js +1 -0
  20. package/dist/client/assets/awk-DMzUqQB5.js +1 -0
  21. package/dist/client/assets/ayu-dark-DYE7WIF3.js +1 -0
  22. package/dist/client/assets/ayu-light-BA47KaF1.js +1 -0
  23. package/dist/client/assets/ayu-mirage-32ctXXKs.js +1 -0
  24. package/dist/client/assets/ballerina-BFfxhgS-.js +1 -0
  25. package/dist/client/assets/bat-BkioyH1T.js +1 -0
  26. package/dist/client/assets/beancount-k_qm7-4y.js +1 -0
  27. package/dist/client/assets/berry-uYugtg8r.js +1 -0
  28. package/dist/client/assets/bibtex-CHM0blh-.js +1 -0
  29. package/dist/client/assets/bicep-Bmn6On1c.js +1 -0
  30. package/dist/client/assets/bird2-DPOp833l.js +1 -0
  31. package/dist/client/assets/blade-D4QpJJKB.js +1 -0
  32. package/dist/client/assets/bsl-BO_Y6i37.js +1 -0
  33. package/dist/client/assets/c-BIGW1oBm.js +1 -0
  34. package/dist/client/assets/c3-eo99z4R2.js +1 -0
  35. package/dist/client/assets/cadence-Bv_4Rxtq.js +1 -0
  36. package/dist/client/assets/cairo-KRGpt6FW.js +1 -0
  37. package/dist/client/assets/catppuccin-frappe-DFWUc33u.js +1 -0
  38. package/dist/client/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
  39. package/dist/client/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
  40. package/dist/client/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
  41. package/dist/client/assets/clarity-D53aC0YG.js +1 -0
  42. package/dist/client/assets/clojure-P80f7IUj.js +1 -0
  43. package/dist/client/assets/cmake-D1j8_8rp.js +1 -0
  44. package/dist/client/assets/cobol-nwyudZeR.js +1 -0
  45. package/dist/client/assets/codeowners-Bp6g37R7.js +1 -0
  46. package/dist/client/assets/codeql-DsOJ9woJ.js +1 -0
  47. package/dist/client/assets/coffee-Ch7k5sss.js +1 -0
  48. package/dist/client/assets/common-lisp-Cg-RD9OK.js +1 -0
  49. package/dist/client/assets/coq-DkFqJrB1.js +1 -0
  50. package/dist/client/assets/cpp-CofmeUqb.js +1 -0
  51. package/dist/client/assets/crystal-tKQVLTB8.js +1 -0
  52. package/dist/client/assets/csharp-COcwbKMJ.js +1 -0
  53. package/dist/client/assets/css-DPfMkruS.js +1 -0
  54. package/dist/client/assets/csv-fuZLfV_i.js +1 -0
  55. package/dist/client/assets/cue-D82EKSYY.js +1 -0
  56. package/dist/client/assets/cypher-COkxafJQ.js +1 -0
  57. package/dist/client/assets/d-85-TOEBH.js +1 -0
  58. package/dist/client/assets/dark-plus-C3mMm8J8.js +1 -0
  59. package/dist/client/assets/dart-CF10PKvl.js +1 -0
  60. package/dist/client/assets/dax-CEL-wOlO.js +1 -0
  61. package/dist/client/assets/desktop-BmXAJ9_W.js +1 -0
  62. package/dist/client/assets/diff-D97Zzqfu.js +1 -0
  63. package/dist/client/assets/docker-BcOcwvcX.js +1 -0
  64. package/dist/client/assets/dotenv-Da5cRb03.js +1 -0
  65. package/dist/client/assets/dracula-BzJJZx-M.js +1 -0
  66. package/dist/client/assets/dracula-soft-BXkSAIEj.js +1 -0
  67. package/dist/client/assets/dream-maker-BtqSS_iP.js +1 -0
  68. package/dist/client/assets/edge-BkV0erSs.js +1 -0
  69. package/dist/client/assets/elixir-CDX3lj18.js +1 -0
  70. package/dist/client/assets/elm-DbKCFpqz.js +1 -0
  71. package/dist/client/assets/emacs-lisp-C9XAeP06.js +1 -0
  72. package/dist/client/assets/erb-B12qg9BL.js +1 -0
  73. package/dist/client/assets/erlang-DsQrWhSR.js +1 -0
  74. package/dist/client/assets/everforest-dark-BgDCqdQA.js +1 -0
  75. package/dist/client/assets/everforest-light-C8M2exoo.js +1 -0
  76. package/dist/client/assets/fennel-BYunw83y.js +1 -0
  77. package/dist/client/assets/fish-BvzEVeQv.js +1 -0
  78. package/dist/client/assets/fluent-C4IJs8-o.js +1 -0
  79. package/dist/client/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  80. package/dist/client/assets/fortran-free-form-BxgE0vQu.js +1 -0
  81. package/dist/client/assets/fsharp-CXgrBDvD.js +1 -0
  82. package/dist/client/assets/gdresource-BOOCDP_w.js +1 -0
  83. package/dist/client/assets/gdscript-C5YyOfLZ.js +1 -0
  84. package/dist/client/assets/gdshader-DkwncUOv.js +1 -0
  85. package/dist/client/assets/genie-D0YGMca9.js +1 -0
  86. package/dist/client/assets/gherkin-DyxjwDmM.js +1 -0
  87. package/dist/client/assets/git-commit-F4YmCXRG.js +1 -0
  88. package/dist/client/assets/git-rebase-r7XF79zn.js +1 -0
  89. package/dist/client/assets/github-dark-DHJKELXO.js +1 -0
  90. package/dist/client/assets/github-dark-default-Cuk6v7N8.js +1 -0
  91. package/dist/client/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  92. package/dist/client/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  93. package/dist/client/assets/github-light-DAi9KRSo.js +1 -0
  94. package/dist/client/assets/github-light-default-D7oLnXFd.js +1 -0
  95. package/dist/client/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  96. package/dist/client/assets/gleam-BspZqrRM.js +1 -0
  97. package/dist/client/assets/glimmer-js-Rg0-pVw9.js +1 -0
  98. package/dist/client/assets/glimmer-ts-U6CK756n.js +1 -0
  99. package/dist/client/assets/glsl-DplSGwfg.js +1 -0
  100. package/dist/client/assets/gn-n2N0HUVH.js +1 -0
  101. package/dist/client/assets/gnuplot-DdkO51Og.js +1 -0
  102. package/dist/client/assets/go-CxLEBnE3.js +1 -0
  103. package/dist/client/assets/graphql-ChdNCCLP.js +1 -0
  104. package/dist/client/assets/groovy-gcz8RCvz.js +1 -0
  105. package/dist/client/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  106. package/dist/client/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  107. package/dist/client/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  108. package/dist/client/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  109. package/dist/client/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  110. package/dist/client/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  111. package/dist/client/assets/hack-CaT9iCJl.js +1 -0
  112. package/dist/client/assets/haml-B8DHNrY2.js +1 -0
  113. package/dist/client/assets/handlebars-BL8al0AC.js +1 -0
  114. package/dist/client/assets/haskell-Df6bDoY_.js +1 -0
  115. package/dist/client/assets/haxe-CzTSHFRz.js +1 -0
  116. package/dist/client/assets/hcl-BWvSN4gD.js +1 -0
  117. package/dist/client/assets/hjson-D5-asLiD.js +1 -0
  118. package/dist/client/assets/hlsl-D3lLCCz7.js +1 -0
  119. package/dist/client/assets/horizon-BUw7H-hv.js +1 -0
  120. package/dist/client/assets/horizon-bright-Cn-bp-IR.js +1 -0
  121. package/dist/client/assets/houston-DnULxvSX.js +1 -0
  122. package/dist/client/assets/html-GMplVEZG.js +1 -0
  123. package/dist/client/assets/html-derivative-BFtXZ54Q.js +1 -0
  124. package/dist/client/assets/http-jrhK8wxY.js +1 -0
  125. package/dist/client/assets/hurl-irOxFIW8.js +1 -0
  126. package/dist/client/assets/hxml-Bvhsp5Yf.js +1 -0
  127. package/dist/client/assets/hy-DFXneXwc.js +1 -0
  128. package/dist/client/assets/imba-DGztddWO.js +1 -0
  129. package/dist/client/assets/index-C07zYq_-.css +32 -0
  130. package/dist/client/assets/index-Ce3hNHfL.js +2351 -0
  131. package/dist/client/assets/ini-BEwlwnbL.js +1 -0
  132. package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  133. package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  134. package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  135. package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  136. package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  137. package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  138. package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  139. package/dist/client/assets/java-CylS5w8V.js +1 -0
  140. package/dist/client/assets/javascript-wDzz0qaB.js +1 -0
  141. package/dist/client/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  142. package/dist/client/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  143. package/dist/client/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  144. package/dist/client/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  145. package/dist/client/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  146. package/dist/client/assets/jinja-4LBKfQ-Z.js +1 -0
  147. package/dist/client/assets/jison-wvAkD_A8.js +1 -0
  148. package/dist/client/assets/json-Cp-IABpG.js +1 -0
  149. package/dist/client/assets/json5-C9tS-k6U.js +1 -0
  150. package/dist/client/assets/jsonc-Des-eS-w.js +1 -0
  151. package/dist/client/assets/jsonl-DcaNXYhu.js +1 -0
  152. package/dist/client/assets/jsonnet-DFQXde-d.js +1 -0
  153. package/dist/client/assets/jssm-C2t-YnRu.js +1 -0
  154. package/dist/client/assets/jsx-g9-lgVsj.js +1 -0
  155. package/dist/client/assets/julia-CxzCAyBv.js +1 -0
  156. package/dist/client/assets/just-Cw27pwNe.js +1 -0
  157. package/dist/client/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  158. package/dist/client/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  159. package/dist/client/assets/kanagawa-wave-DWedfzmr.js +1 -0
  160. package/dist/client/assets/kdl-DV7GczEv.js +1 -0
  161. package/dist/client/assets/kotlin-BdnUsdx6.js +1 -0
  162. package/dist/client/assets/kusto-DZf3V79B.js +1 -0
  163. package/dist/client/assets/laserwave-DUszq2jm.js +1 -0
  164. package/dist/client/assets/latex-CWtU0Tv5.js +1 -0
  165. package/dist/client/assets/lean-BZvkOJ9d.js +1 -0
  166. package/dist/client/assets/less-B1dDrJ26.js +1 -0
  167. package/dist/client/assets/light-plus-B7mTdjB0.js +1 -0
  168. package/dist/client/assets/liquid-DYVedYrR.js +1 -0
  169. package/dist/client/assets/llvm-DjAJT7YJ.js +1 -0
  170. package/dist/client/assets/log-2UxHyX5q.js +1 -0
  171. package/dist/client/assets/logo-BtOb2qkB.js +1 -0
  172. package/dist/client/assets/lua-BaeVxFsk.js +1 -0
  173. package/dist/client/assets/luau-C-HG3fhB.js +1 -0
  174. package/dist/client/assets/make-CHLpvVh8.js +1 -0
  175. package/dist/client/assets/markdown-Cvjx9yec.js +1 -0
  176. package/dist/client/assets/marko-CnJfTvn9.js +1 -0
  177. package/dist/client/assets/material-theme-D5KoaKCx.js +1 -0
  178. package/dist/client/assets/material-theme-darker-BfHTSMKl.js +1 -0
  179. package/dist/client/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  180. package/dist/client/assets/material-theme-ocean-CyktbL80.js +1 -0
  181. package/dist/client/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  182. package/dist/client/assets/matlab-D7o27uSR.js +1 -0
  183. package/dist/client/assets/mdc-BMNejdWA.js +1 -0
  184. package/dist/client/assets/mdx-Cmh6b_Ma.js +1 -0
  185. package/dist/client/assets/mermaid-mWjccvbQ.js +1 -0
  186. package/dist/client/assets/min-dark-CafNBF8u.js +1 -0
  187. package/dist/client/assets/min-light-CTRr51gU.js +1 -0
  188. package/dist/client/assets/mipsasm-CKIfxQSi.js +1 -0
  189. package/dist/client/assets/mojo-rZm6bMo-.js +1 -0
  190. package/dist/client/assets/monokai-D4h5O-jR.js +1 -0
  191. package/dist/client/assets/moonbit-_H4v1dQx.js +1 -0
  192. package/dist/client/assets/move-IF9eRakj.js +1 -0
  193. package/dist/client/assets/narrat-DRg8JJMk.js +1 -0
  194. package/dist/client/assets/nextflow-Zz6hmt5N.js +1 -0
  195. package/dist/client/assets/nextflow-groovy-BeH2EWoN.js +1 -0
  196. package/dist/client/assets/nginx-BpAMiNFr.js +1 -0
  197. package/dist/client/assets/night-owl-C39BiMTA.js +1 -0
  198. package/dist/client/assets/night-owl-light-CMTm3GFP.js +1 -0
  199. package/dist/client/assets/nim-CVrawwO9.js +1 -0
  200. package/dist/client/assets/nix-CwoSXNpI.js +1 -0
  201. package/dist/client/assets/nord-Ddv68eIx.js +1 -0
  202. package/dist/client/assets/nushell-Cz2AlsmD.js +1 -0
  203. package/dist/client/assets/objective-c-DXmwc3jG.js +1 -0
  204. package/dist/client/assets/objective-cpp-CLxacb5B.js +1 -0
  205. package/dist/client/assets/ocaml-C0hk2d4L.js +1 -0
  206. package/dist/client/assets/odin-BBf5iR-q.js +1 -0
  207. package/dist/client/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  208. package/dist/client/assets/one-light-C3Wv6jpd.js +1 -0
  209. package/dist/client/assets/openscad-C4EeE6gA.js +1 -0
  210. package/dist/client/assets/pascal-D93ZcfNL.js +1 -0
  211. package/dist/client/assets/perl-C0TMdlhV.js +1 -0
  212. package/dist/client/assets/php-Dhbhpdrm.js +1 -0
  213. package/dist/client/assets/pierre-dark-DF2SEV7i.js +1 -0
  214. package/dist/client/assets/pierre-light-DOlZxES8.js +1 -0
  215. package/dist/client/assets/pkl-u5AG7uiY.js +1 -0
  216. package/dist/client/assets/plastic-3e1v2bzS.js +1 -0
  217. package/dist/client/assets/plsql-ChMvpjG-.js +1 -0
  218. package/dist/client/assets/po-BTJTHyun.js +1 -0
  219. package/dist/client/assets/poimandres-CS3Unz2-.js +1 -0
  220. package/dist/client/assets/polar-C0HS_06l.js +1 -0
  221. package/dist/client/assets/postcss-CXtECtnM.js +1 -0
  222. package/dist/client/assets/powerquery-CEu0bR-o.js +1 -0
  223. package/dist/client/assets/powershell-Dpen1YoG.js +1 -0
  224. package/dist/client/assets/prisma-Dd19v3D-.js +1 -0
  225. package/dist/client/assets/prolog-CbFg5uaA.js +1 -0
  226. package/dist/client/assets/proto-C7zT0LnQ.js +1 -0
  227. package/dist/client/assets/pug-CGlum2m_.js +1 -0
  228. package/dist/client/assets/puppet-BMWR74SV.js +1 -0
  229. package/dist/client/assets/purescript-CklMAg4u.js +1 -0
  230. package/dist/client/assets/python-B6aJPvgy.js +1 -0
  231. package/dist/client/assets/qml-3beO22l8.js +1 -0
  232. package/dist/client/assets/qmldir-C8lEn-DE.js +1 -0
  233. package/dist/client/assets/qss-IeuSbFQv.js +1 -0
  234. package/dist/client/assets/r-Dspwwk_N.js +1 -0
  235. package/dist/client/assets/racket-BqYA7rlc.js +1 -0
  236. package/dist/client/assets/raku-DXvB9xmW.js +1 -0
  237. package/dist/client/assets/razor-Uh8Bk_45.js +1 -0
  238. package/dist/client/assets/red-bN70gL4F.js +1 -0
  239. package/dist/client/assets/reg-C-SQnVFl.js +1 -0
  240. package/dist/client/assets/regexp-CDVJQ6XC.js +1 -0
  241. package/dist/client/assets/rel-C3B-1QV4.js +1 -0
  242. package/dist/client/assets/riscv-BM1_JUlF.js +1 -0
  243. package/dist/client/assets/ron-D8l8udqQ.js +1 -0
  244. package/dist/client/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
  245. package/dist/client/assets/rose-pine-moon-D4_iv3hh.js +1 -0
  246. package/dist/client/assets/rose-pine-qdsjHGoJ.js +1 -0
  247. package/dist/client/assets/rosmsg-BJDFO7_C.js +1 -0
  248. package/dist/client/assets/rst-BrH8l1NY.js +1 -0
  249. package/dist/client/assets/ruby-Dw2BHqvy.js +1 -0
  250. package/dist/client/assets/rust-B1yitclQ.js +1 -0
  251. package/dist/client/assets/sas-cz2c8ADy.js +1 -0
  252. package/dist/client/assets/sass-Cj5Yp3dK.js +1 -0
  253. package/dist/client/assets/scala-C151Ov-r.js +1 -0
  254. package/dist/client/assets/scheme-C98Dy4si.js +1 -0
  255. package/dist/client/assets/scratchpad-page-DcMny6uZ.js +1 -0
  256. package/dist/client/assets/scss-OYdSNvt2.js +1 -0
  257. package/dist/client/assets/sdbl-DVxCFoDh.js +1 -0
  258. package/dist/client/assets/shaderlab-Dg9Lc6iA.js +1 -0
  259. package/dist/client/assets/shellscript-Yzrsuije.js +1 -0
  260. package/dist/client/assets/shellsession-BADoaaVG.js +1 -0
  261. package/dist/client/assets/slack-dark-BthQWCQV.js +1 -0
  262. package/dist/client/assets/slack-ochin-DqwNpetd.js +1 -0
  263. package/dist/client/assets/smalltalk-BERRCDM3.js +1 -0
  264. package/dist/client/assets/snazzy-light-Bw305WKR.js +1 -0
  265. package/dist/client/assets/solarized-dark-DXbdFlpD.js +1 -0
  266. package/dist/client/assets/solarized-light-L9t79GZl.js +1 -0
  267. package/dist/client/assets/solidity-rGO070M0.js +1 -0
  268. package/dist/client/assets/soy-Brmx7dQM.js +1 -0
  269. package/dist/client/assets/sparql-rVzFXLq3.js +1 -0
  270. package/dist/client/assets/splunk-BtCnVYZw.js +1 -0
  271. package/dist/client/assets/sql-BLtJtn59.js +1 -0
  272. package/dist/client/assets/ssh-config-_ykCGR6B.js +1 -0
  273. package/dist/client/assets/stata-BH5u7GGu.js +1 -0
  274. package/dist/client/assets/stylus-BEDo0Tqx.js +1 -0
  275. package/dist/client/assets/surrealql-Bq5Q-fJD.js +1 -0
  276. package/dist/client/assets/svelte-C_ipcX3V.js +1 -0
  277. package/dist/client/assets/swift-D82vCrfD.js +1 -0
  278. package/dist/client/assets/synthwave-84-CbfX1IO0.js +1 -0
  279. package/dist/client/assets/system-verilog-CnnmHF94.js +1 -0
  280. package/dist/client/assets/systemd-4A_iFExJ.js +1 -0
  281. package/dist/client/assets/talonscript-CkByrt1z.js +1 -0
  282. package/dist/client/assets/tasl-QIJgUcNo.js +1 -0
  283. package/dist/client/assets/tcl-dwOrl1Do.js +1 -0
  284. package/dist/client/assets/templ-P3uqSqPl.js +1 -0
  285. package/dist/client/assets/terraform-BETggiCN.js +1 -0
  286. package/dist/client/assets/tex-idrVyKtj.js +1 -0
  287. package/dist/client/assets/tokyo-night-hegEt444.js +1 -0
  288. package/dist/client/assets/toml-vGWfd6FD.js +1 -0
  289. package/dist/client/assets/ts-tags-zn1MmPIZ.js +1 -0
  290. package/dist/client/assets/tsv-B_m7g4N7.js +1 -0
  291. package/dist/client/assets/tsx-COt5Ahok.js +1 -0
  292. package/dist/client/assets/turtle-BsS91CYL.js +1 -0
  293. package/dist/client/assets/twig-DNn4PbVi.js +1 -0
  294. package/dist/client/assets/typescript-BPQ3VLAy.js +1 -0
  295. package/dist/client/assets/typespec-BGHnOYBU.js +1 -0
  296. package/dist/client/assets/typst-DHCkPAjA.js +1 -0
  297. package/dist/client/assets/v-BcVCzyr7.js +1 -0
  298. package/dist/client/assets/vala-CsfeWuGM.js +1 -0
  299. package/dist/client/assets/vb-D17OF-Vu.js +1 -0
  300. package/dist/client/assets/verilog-BQ8w6xss.js +1 -0
  301. package/dist/client/assets/vesper-DU1UobuO.js +1 -0
  302. package/dist/client/assets/vhdl-CeAyd5Ju.js +1 -0
  303. package/dist/client/assets/viml-CJc9bBzg.js +1 -0
  304. package/dist/client/assets/vitesse-black-Bkuqu6BP.js +1 -0
  305. package/dist/client/assets/vitesse-dark-D0r3Knsf.js +1 -0
  306. package/dist/client/assets/vitesse-light-CVO1_9PV.js +1 -0
  307. package/dist/client/assets/vue-DN_0RTcg.js +1 -0
  308. package/dist/client/assets/vue-html-AaS7Mt5G.js +1 -0
  309. package/dist/client/assets/vue-vine-CQOfvN7w.js +1 -0
  310. package/dist/client/assets/vyper-CDx5xZoG.js +1 -0
  311. package/dist/client/assets/wasm-CG6Dc4jp.js +1 -0
  312. package/dist/client/assets/wasm-MzD3tlZU.js +1 -0
  313. package/dist/client/assets/wenyan-BV7otONQ.js +1 -0
  314. package/dist/client/assets/wgsl-Dx-B1_4e.js +1 -0
  315. package/dist/client/assets/wikitext-BhOHFoWU.js +1 -0
  316. package/dist/client/assets/wit-5i3qLPDT.js +1 -0
  317. package/dist/client/assets/wolfram-lXgVvXCa.js +1 -0
  318. package/dist/client/assets/xml-sdJ4AIDG.js +1 -0
  319. package/dist/client/assets/xsl-CtQFsRM5.js +1 -0
  320. package/dist/client/assets/yaml-Buea-lGh.js +1 -0
  321. package/dist/client/assets/zenscript-DVFEvuxE.js +1 -0
  322. package/dist/client/assets/zig-VOosw3JB.js +1 -0
  323. package/dist/client/cursor.png +0 -0
  324. package/dist/client/favicon.svg +17 -0
  325. package/dist/client/finder.png +0 -0
  326. package/dist/client/icons/claude.svg +1 -0
  327. package/dist/client/icons/openai.svg +1 -0
  328. package/dist/client/images/github.png +0 -0
  329. package/dist/client/index.html +15 -0
  330. package/dist/client/logo.svg +17 -0
  331. package/dist/client/terminal.png +0 -0
  332. package/dist/client/vscode.png +0 -0
  333. package/dist/client/warp.png +0 -0
  334. package/package.json +107 -0
  335. package/src/server/agent-instruction-attachments.ts +458 -0
  336. package/src/server/agent.ts +1879 -0
  337. package/src/server/cli-runtime.ts +418 -0
  338. package/src/server/cli-supervisor.ts +90 -0
  339. package/src/server/cli.ts +102 -0
  340. package/src/server/codex-app-server-protocol.ts +478 -0
  341. package/src/server/codex-app-server.ts +1645 -0
  342. package/src/server/data-dir-lock.ts +128 -0
  343. package/src/server/diff-store.ts +1587 -0
  344. package/src/server/durable-file.ts +74 -0
  345. package/src/server/event-store.ts +1448 -0
  346. package/src/server/event.ts +249 -0
  347. package/src/server/external-file-access.ts +48 -0
  348. package/src/server/external-open.ts +259 -0
  349. package/src/server/generate-title.ts +75 -0
  350. package/src/server/git-refresh-poller.ts +92 -0
  351. package/src/server/github-rest-client.ts +176 -0
  352. package/src/server/harness-types.ts +24 -0
  353. package/src/server/keybindings.ts +203 -0
  354. package/src/server/machine-name.ts +22 -0
  355. package/src/server/paths.ts +51 -0
  356. package/src/server/pr-manager.ts +1204 -0
  357. package/src/server/pr-refresh-poller.ts +126 -0
  358. package/src/server/process-utils.ts +18 -0
  359. package/src/server/provider-catalog.ts +90 -0
  360. package/src/server/quick-response.ts +274 -0
  361. package/src/server/read-models.ts +311 -0
  362. package/src/server/restart.ts +33 -0
  363. package/src/server/scratchpad-manager.ts +87 -0
  364. package/src/server/server.ts +759 -0
  365. package/src/server/share.ts +126 -0
  366. package/src/server/terminal-manager.ts +371 -0
  367. package/src/server/update-manager.ts +250 -0
  368. package/src/server/uploads.ts +191 -0
  369. package/src/server/workspace-file-search.ts +191 -0
  370. package/src/server/workspace-manager.ts +627 -0
  371. package/src/server/workspace-polling.ts +10 -0
  372. package/src/server/ws-router.ts +1039 -0
  373. package/src/shared/branding.ts +69 -0
  374. package/src/shared/dev-ports.ts +100 -0
  375. package/src/shared/ports.ts +2 -0
  376. package/src/shared/protocol.ts +217 -0
  377. package/src/shared/tools.ts +324 -0
  378. package/src/shared/types.ts +1220 -0
  379. package/tsconfig.json +35 -0
@@ -0,0 +1,1879 @@
1
+ import {
2
+ type CanUseTool,
3
+ type PermissionResult,
4
+ type Query,
5
+ query,
6
+ type SDKUserMessage,
7
+ type SdkBeta,
8
+ } from '@anthropic-ai/claude-agent-sdk';
9
+ import type { ClientCommand } from '../shared/protocol';
10
+ import { normalizeToolCall } from '../shared/tools';
11
+ import type {
12
+ AccountInfo,
13
+ AgentProvider,
14
+ ChatAttachment,
15
+ ClaudeContextWindow,
16
+ ContextWindowUsageSnapshot,
17
+ MikoStatus,
18
+ NormalizedToolCall,
19
+ PendingToolSnapshot,
20
+ PromptPart,
21
+ QueuedMessageSnapshot,
22
+ SlashCommandInfo,
23
+ TranscriptEntry,
24
+ } from '../shared/types';
25
+ import { CodexAppServerManager } from './codex-app-server';
26
+ import type { QueuedSessionMessageRecord, QueuedSessionSendPayload } from './event';
27
+ import type { EventStore } from './event-store';
28
+ import {
29
+ fallbackTitleFromMessage,
30
+ type GenerateSessionTitleResult,
31
+ generateTitleForSessionDetailed,
32
+ } from './generate-title';
33
+ import type { HarnessEvent, HarnessToolRequest, HarnessTurn } from './harness-types';
34
+ import {
35
+ codexServiceTierFromModelOptions,
36
+ getServerProviderCatalog,
37
+ normalizeClaudeModelOptions,
38
+ normalizeCodexModelOptions,
39
+ normalizeServerModel,
40
+ } from './provider-catalog';
41
+
42
+ const CLAUDE_TOOLSET = [
43
+ 'Skill',
44
+ 'WebFetch',
45
+ 'WebSearch',
46
+ 'Task',
47
+ 'TaskOutput',
48
+ 'Bash',
49
+ 'Glob',
50
+ 'Grep',
51
+ 'Read',
52
+ 'Edit',
53
+ 'Write',
54
+ 'TodoWrite',
55
+ 'KillShell',
56
+ 'AskUserQuestion',
57
+ 'EnterPlanMode',
58
+ 'ExitPlanMode',
59
+ ] as const;
60
+
61
+ // 1M context is a session-level beta; the model id no longer carries a `[1m]` suffix. The beta is
62
+ // only ever reached for models the catalog marks 1M-capable, since normalizeClaudeContextWindow
63
+ // downgrades `1m` to `200k` for any model without a `1m` contextWindowOption.
64
+ const CLAUDE_1M_CONTEXT_BETA: SdkBeta = 'context-1m-2025-08-07';
65
+
66
+ function claudeBetasForContextWindow(contextWindow?: ClaudeContextWindow): SdkBeta[] | undefined {
67
+ return contextWindow === '1m' ? [CLAUDE_1M_CONTEXT_BETA] : undefined;
68
+ }
69
+
70
+ interface PendingToolRequest {
71
+ toolUseId: string;
72
+ tool: NormalizedToolCall & { toolKind: 'ask_user_question' | 'exit_plan_mode' };
73
+ resolve: (result: unknown) => void;
74
+ }
75
+
76
+ function pendingToolSnapshot(pending: PendingToolRequest): PendingToolSnapshot {
77
+ const { tool, toolUseId } = pending;
78
+ if (tool.toolKind === 'exit_plan_mode') {
79
+ return {
80
+ toolUseId,
81
+ toolKind: 'exit_plan_mode',
82
+ plan: tool.input?.plan,
83
+ summary: tool.input?.summary,
84
+ };
85
+ }
86
+ return {
87
+ toolUseId,
88
+ toolKind: 'ask_user_question',
89
+ questions: tool.input?.questions ?? [],
90
+ };
91
+ }
92
+
93
+ type SendCommand = Extract<ClientCommand, { type: 'session.send' }>;
94
+
95
+ class RecordedTurnStartupError extends Error {
96
+ constructor(message: string) {
97
+ super(message);
98
+ this.name = 'RecordedTurnStartupError';
99
+ }
100
+ }
101
+
102
+ interface ActiveTurn {
103
+ sessionId: string;
104
+ provider: AgentProvider;
105
+ turn: HarnessTurn;
106
+ queuedMessageId?: string;
107
+ // The Claude session that owns this turn, so a stale session loop only tears down its own turn
108
+ // (never a replacement registered under the same sessionId after a 200k<->1M/effort/cwd switch).
109
+ claudeSession?: ClaudeSessionState;
110
+ model: string;
111
+ effort?: string;
112
+ serviceTier?: 'fast';
113
+ planMode: boolean;
114
+ status: MikoStatus;
115
+ pendingTool: PendingToolRequest | null;
116
+ postToolFollowUp: { content: string; planMode: boolean } | null;
117
+ hasFinalResult: boolean;
118
+ cancelRequested: boolean;
119
+ cancelRecorded: boolean;
120
+ settled: boolean;
121
+ }
122
+
123
+ interface ClaudeSessionHandle {
124
+ provider: 'claude';
125
+ stream: AsyncIterable<HarnessEvent>;
126
+ getAccountInfo?: () => Promise<AccountInfo | null>;
127
+ interrupt: () => Promise<void>;
128
+ close: () => void;
129
+ sendPrompt: (content: string) => Promise<void>;
130
+ setModel: (model: string) => Promise<void>;
131
+ setPermissionMode: (planMode: boolean) => Promise<void>;
132
+ getCommands: () => Promise<SlashCommandInfo[]>;
133
+ }
134
+
135
+ interface ClaudeSessionState {
136
+ sessionId: string;
137
+ session: ClaudeSessionHandle;
138
+ localPath: string;
139
+ model: string;
140
+ effort?: string;
141
+ contextWindow?: ClaudeContextWindow;
142
+ planMode: boolean;
143
+ sessionToken: string | null;
144
+ accountInfoLoaded: boolean;
145
+ }
146
+
147
+ interface AutoRenameWorkspaceBranchArgs {
148
+ workspaceId: string;
149
+ branchName: string;
150
+ expectedCurrentBranchName?: string;
151
+ }
152
+
153
+ interface AutoRenameWorkspaceBranchResult {
154
+ branchName: string;
155
+ changed: boolean;
156
+ }
157
+
158
+ interface AgentCoordinatorArgs {
159
+ store: EventStore;
160
+ onStateChange: () => void;
161
+ onTurnSettled?: (event: {
162
+ sessionId: string;
163
+ outcome: 'success' | 'failed' | 'cancelled';
164
+ }) => void | Promise<void>;
165
+ codexManager?: CodexAppServerManager;
166
+ generateTitle?: (messageContent: string) => Promise<GenerateSessionTitleResult>;
167
+ renameWorkspaceBranch?: (
168
+ args: AutoRenameWorkspaceBranchArgs,
169
+ ) => Promise<AutoRenameWorkspaceBranchResult>;
170
+ startClaudeSession?: (args: {
171
+ localPath: string;
172
+ model: string;
173
+ effort?: string;
174
+ contextWindow?: ClaudeContextWindow;
175
+ planMode: boolean;
176
+ sessionToken: string | null;
177
+ onToolRequest: (request: HarnessToolRequest) => Promise<unknown>;
178
+ }) => Promise<ClaudeSessionHandle>;
179
+ }
180
+
181
+ interface StartClaudeSessionDeps {
182
+ queryFn?: typeof query;
183
+ }
184
+
185
+ function timestamped<T extends Omit<TranscriptEntry, '_id' | 'createdAt'>>(
186
+ entry: T,
187
+ createdAt = Date.now(),
188
+ id: string = crypto.randomUUID(),
189
+ ): TranscriptEntry {
190
+ return {
191
+ _id: id,
192
+ createdAt,
193
+ ...entry,
194
+ } as TranscriptEntry;
195
+ }
196
+
197
+ function stringFromUnknown(value: unknown) {
198
+ if (typeof value === 'string') return value;
199
+ try {
200
+ return JSON.stringify(value, null, 2);
201
+ } catch {
202
+ return String(value);
203
+ }
204
+ }
205
+
206
+ function asRecord(value: unknown): Record<string, unknown> | null {
207
+ return value && typeof value === 'object' && !Array.isArray(value)
208
+ ? (value as Record<string, unknown>)
209
+ : null;
210
+ }
211
+
212
+ function asNumber(value: unknown): number | undefined {
213
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
214
+ }
215
+
216
+ function escapeXmlAttribute(value: string) {
217
+ return value
218
+ .replaceAll('&', '&amp;')
219
+ .replaceAll('"', '&quot;')
220
+ .replaceAll('<', '&lt;')
221
+ .replaceAll('>', '&gt;');
222
+ }
223
+
224
+ export function buildAttachmentHintText(attachments: ChatAttachment[]) {
225
+ if (attachments.length === 0) return '';
226
+
227
+ const lines = attachments.map(
228
+ (attachment) =>
229
+ `<attachment
230
+ kind="${escapeXmlAttribute(attachment.kind)}"
231
+ mime_type="${escapeXmlAttribute(attachment.mimeType)}"
232
+ path="${escapeXmlAttribute(attachment.absolutePath)}"
233
+ project_path="${escapeXmlAttribute(attachment.relativePath)}"
234
+ size_bytes="${attachment.size}"
235
+ display_name="${escapeXmlAttribute(attachment.displayName)}"
236
+ />`,
237
+ );
238
+
239
+ return ['<miko-attachments>', ...lines, '</miko-attachments>'].join('\n');
240
+ }
241
+
242
+ export function buildPromptText(content: string, attachments: ChatAttachment[]) {
243
+ const attachmentHint = buildAttachmentHintText(attachments);
244
+ if (!attachmentHint) {
245
+ return content.trim();
246
+ }
247
+
248
+ const trimmed = content.trim();
249
+ return [trimmed || 'Please inspect the attached files.', attachmentHint].join('\n\n').trim();
250
+ }
251
+
252
+ function discardedToolResult(
253
+ tool: NormalizedToolCall & { toolKind: 'ask_user_question' | 'exit_plan_mode' },
254
+ ) {
255
+ if (tool.toolKind === 'ask_user_question') {
256
+ return {
257
+ discarded: true,
258
+ answers: {},
259
+ };
260
+ }
261
+
262
+ return {
263
+ discarded: true,
264
+ };
265
+ }
266
+
267
+ export function normalizeClaudeUsageSnapshot(
268
+ value: unknown,
269
+ maxTokens?: number,
270
+ ): ContextWindowUsageSnapshot | null {
271
+ const usage = asRecord(value);
272
+ if (!usage) return null;
273
+
274
+ const directInputTokens = asNumber(usage.input_tokens) ?? asNumber(usage.inputTokens) ?? 0;
275
+ const cacheCreationInputTokens =
276
+ asNumber(usage.cache_creation_input_tokens) ?? asNumber(usage.cacheCreationInputTokens) ?? 0;
277
+
278
+ const cacheReadInputTokens =
279
+ asNumber(usage.cache_read_input_tokens) ?? asNumber(usage.cacheReadInputTokens) ?? 0;
280
+
281
+ const outputTokens = asNumber(usage.output_tokens) ?? asNumber(usage.outputTokens) ?? 0;
282
+ const reasoningOutputTokens =
283
+ asNumber(usage.reasoning_output_tokens) ?? asNumber(usage.reasoningOutputTokens);
284
+
285
+ const toolUses = asNumber(usage.tool_uses) ?? asNumber(usage.toolUses);
286
+ const durationMs = asNumber(usage.duration_ms) ?? asNumber(usage.durationMs);
287
+
288
+ const inputTokens = directInputTokens + cacheCreationInputTokens + cacheReadInputTokens;
289
+ const usedTokens = inputTokens + outputTokens;
290
+
291
+ if (usedTokens <= 0) {
292
+ return null;
293
+ }
294
+
295
+ return {
296
+ usedTokens,
297
+ inputTokens,
298
+ ...(cacheReadInputTokens > 0 ? { cachedInputTokens: cacheReadInputTokens } : {}),
299
+ ...(outputTokens > 0 ? { outputTokens } : {}),
300
+ ...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
301
+ lastUsedTokens: usedTokens,
302
+ lastInputTokens: inputTokens,
303
+ ...(cacheReadInputTokens > 0 ? { lastCachedInputTokens: cacheReadInputTokens } : {}),
304
+ ...(outputTokens > 0 ? { lastOutputTokens: outputTokens } : {}),
305
+ ...(reasoningOutputTokens !== undefined
306
+ ? { lastReasoningOutputTokens: reasoningOutputTokens }
307
+ : {}),
308
+ ...(toolUses !== undefined ? { toolUses } : {}),
309
+ ...(durationMs !== undefined ? { durationMs } : {}),
310
+ ...(typeof maxTokens === 'number' && maxTokens > 0 ? { maxTokens } : {}),
311
+ compactsAutomatically: false,
312
+ };
313
+ }
314
+
315
+ export function maxClaudeContextWindowFromModelUsage(modelUsage: unknown): number | undefined {
316
+ const record = asRecord(modelUsage);
317
+ if (!record) return undefined;
318
+
319
+ let maxContextWindow: number | undefined;
320
+ for (const value of Object.values(record)) {
321
+ const usage = asRecord(value);
322
+ const contextWindow = asNumber(usage?.contextWindow) ?? asNumber(usage?.context_window);
323
+
324
+ if (contextWindow === undefined) continue;
325
+ maxContextWindow = Math.max(maxContextWindow ?? 0, contextWindow);
326
+ }
327
+ return maxContextWindow;
328
+ }
329
+
330
+ function getClaudeAssistantMessageUsageId(message: unknown): string | null {
331
+ const record = asRecord(message);
332
+ const nestedMessage = asRecord(record?.message);
333
+ const nestedId = nestedMessage?.id;
334
+ if (typeof nestedId === 'string' && nestedId) return nestedId;
335
+
336
+ const uuid = record?.uuid;
337
+ if (typeof uuid === 'string' && uuid) return uuid;
338
+
339
+ return null;
340
+ }
341
+
342
+ export function normalizeClaudeStreamMessage(message: unknown): TranscriptEntry[] {
343
+ const debugRaw = JSON.stringify(message);
344
+ const record = asRecord(message) ?? {};
345
+ const nestedMessage = asRecord(record.message);
346
+ const messageId = typeof record.uuid === 'string' ? record.uuid : undefined;
347
+
348
+ if (record.type === 'system' && record.subtype === 'init') {
349
+ return [
350
+ timestamped({
351
+ kind: 'system_init',
352
+ messageId,
353
+ provider: 'claude',
354
+ model: typeof record.model === 'string' ? record.model : 'unknown',
355
+ tools: Array.isArray(record.tools) ? record.tools : [],
356
+ agents: Array.isArray(record.agents) ? record.agents : [],
357
+ slashCommands: Array.isArray(record.slash_commands)
358
+ ? record.slash_commands.filter(
359
+ (entry): entry is string => typeof entry === 'string' && !entry.startsWith('._'),
360
+ )
361
+ : [],
362
+ mcpServers: Array.isArray(record.mcp_servers) ? record.mcp_servers : [],
363
+ debugRaw,
364
+ }),
365
+ ];
366
+ }
367
+
368
+ if (record.type === 'assistant' && Array.isArray(nestedMessage?.content)) {
369
+ const entries: TranscriptEntry[] = [];
370
+ for (const rawContent of nestedMessage.content) {
371
+ const content = asRecord(rawContent);
372
+ if (content?.type === 'text' && typeof content.text === 'string') {
373
+ entries.push(
374
+ timestamped({
375
+ kind: 'assistant_text',
376
+ messageId,
377
+ text: content.text,
378
+ debugRaw,
379
+ }),
380
+ );
381
+ }
382
+
383
+ if (
384
+ content?.type === 'tool_use' &&
385
+ typeof content.name === 'string' &&
386
+ typeof content.id === 'string'
387
+ ) {
388
+ entries.push(
389
+ timestamped({
390
+ kind: 'tool_call',
391
+ messageId,
392
+ tool: normalizeToolCall({
393
+ toolName: content.name,
394
+ toolId: content.id,
395
+ input: (content.input ?? {}) as Record<string, unknown>,
396
+ }),
397
+ debugRaw,
398
+ }),
399
+ );
400
+ }
401
+ }
402
+ return entries;
403
+ }
404
+
405
+ if (record.type === 'user' && Array.isArray(nestedMessage?.content)) {
406
+ const entries: TranscriptEntry[] = [];
407
+ for (const rawContent of nestedMessage.content) {
408
+ const content = asRecord(rawContent);
409
+ if (content?.type === 'tool_result' && typeof content.tool_use_id === 'string') {
410
+ entries.push(
411
+ timestamped({
412
+ kind: 'tool_result',
413
+ messageId,
414
+ toolId: content.tool_use_id,
415
+ content: content.content,
416
+ isError: Boolean(content.is_error),
417
+ debugRaw,
418
+ }),
419
+ );
420
+ }
421
+ }
422
+ return entries;
423
+ }
424
+
425
+ if (record.type === 'result') {
426
+ if (record.subtype === 'cancelled') {
427
+ return [timestamped({ kind: 'interrupted', messageId, debugRaw })];
428
+ }
429
+
430
+ return [
431
+ timestamped({
432
+ kind: 'result',
433
+ messageId,
434
+ subtype: record.is_error ? 'error' : 'success',
435
+ isError: Boolean(record.is_error),
436
+ durationMs: typeof record.duration_ms === 'number' ? record.duration_ms : 0,
437
+ result:
438
+ typeof record.result === 'string' ? record.result : stringFromUnknown(record.result),
439
+ costUsd: typeof record.total_cost_usd === 'number' ? record.total_cost_usd : undefined,
440
+ debugRaw,
441
+ }),
442
+ ];
443
+ }
444
+
445
+ if (
446
+ record.type === 'system' &&
447
+ record.subtype === 'status' &&
448
+ typeof record.status === 'string'
449
+ ) {
450
+ return [timestamped({ kind: 'status', messageId, status: record.status, debugRaw })];
451
+ }
452
+
453
+ if (record.type === 'system' && record.subtype === 'compact_boundary') {
454
+ return [timestamped({ kind: 'compact_boundary', messageId, debugRaw })];
455
+ }
456
+
457
+ if (record.type === 'system' && record.subtype === 'context_cleared') {
458
+ return [timestamped({ kind: 'context_cleared', messageId, debugRaw })];
459
+ }
460
+
461
+ if (
462
+ record.type === 'user' &&
463
+ nestedMessage?.role === 'user' &&
464
+ typeof nestedMessage.content === 'string' &&
465
+ nestedMessage.content.startsWith('This session is being continued')
466
+ ) {
467
+ return [
468
+ timestamped({
469
+ kind: 'compact_summary',
470
+ messageId,
471
+ summary: nestedMessage.content,
472
+ debugRaw,
473
+ }),
474
+ ];
475
+ }
476
+
477
+ return [];
478
+ }
479
+
480
+ export async function* createClaudeHarnessStream(q: Query): AsyncGenerator<HarnessEvent> {
481
+ let seenAssistantUsageIds = new Set<string>();
482
+ let latestUsageSnapshot: ContextWindowUsageSnapshot | null = null;
483
+ let lastKnownContextWindow: number | undefined;
484
+
485
+ for await (const sdkMessage of q as AsyncIterable<unknown>) {
486
+ const sdkRecord = asRecord(sdkMessage) ?? {};
487
+ const sessionToken = typeof sdkRecord.session_id === 'string' ? sdkRecord.session_id : null;
488
+ if (sessionToken) {
489
+ yield { type: 'session_token', sessionToken };
490
+ }
491
+
492
+ if (sdkRecord.type === 'assistant') {
493
+ const usageId = getClaudeAssistantMessageUsageId(sdkMessage);
494
+ const usageSnapshot = normalizeClaudeUsageSnapshot(sdkRecord.usage, lastKnownContextWindow);
495
+
496
+ if (usageId && usageSnapshot && !seenAssistantUsageIds.has(usageId)) {
497
+ seenAssistantUsageIds.add(usageId);
498
+ latestUsageSnapshot = usageSnapshot;
499
+ yield {
500
+ type: 'transcript',
501
+ entry: timestamped({
502
+ kind: 'context_window_updated',
503
+ usage: usageSnapshot,
504
+ }),
505
+ };
506
+ }
507
+ }
508
+
509
+ if (sdkRecord.type === 'result') {
510
+ const resultContextWindow = maxClaudeContextWindowFromModelUsage(sdkRecord.modelUsage);
511
+ if (resultContextWindow !== undefined) {
512
+ lastKnownContextWindow = resultContextWindow;
513
+ }
514
+
515
+ const accumulatedUsage = normalizeClaudeUsageSnapshot(
516
+ sdkRecord.usage,
517
+ resultContextWindow ?? lastKnownContextWindow,
518
+ );
519
+
520
+ const finalUsage = latestUsageSnapshot
521
+ ? {
522
+ ...latestUsageSnapshot,
523
+ ...(typeof (resultContextWindow ?? lastKnownContextWindow) === 'number'
524
+ ? { maxTokens: resultContextWindow ?? lastKnownContextWindow }
525
+ : {}),
526
+ ...(accumulatedUsage && accumulatedUsage.usedTokens > latestUsageSnapshot.usedTokens
527
+ ? { totalProcessedTokens: accumulatedUsage.usedTokens }
528
+ : {}),
529
+ }
530
+ : accumulatedUsage;
531
+
532
+ if (finalUsage) {
533
+ yield {
534
+ type: 'transcript',
535
+ entry: timestamped({
536
+ kind: 'context_window_updated',
537
+ usage: finalUsage,
538
+ }),
539
+ };
540
+ }
541
+
542
+ seenAssistantUsageIds = new Set<string>();
543
+ latestUsageSnapshot = null;
544
+ }
545
+
546
+ for (const entry of normalizeClaudeStreamMessage(sdkMessage)) {
547
+ yield { type: 'transcript', entry };
548
+ }
549
+ }
550
+ }
551
+
552
+ class AsyncMessageQueue<T> implements AsyncIterable<T> {
553
+ private readonly values: T[] = [];
554
+ private readonly waiters: Array<(result: IteratorResult<T>) => void> = [];
555
+ private closed = false;
556
+
557
+ push(value: T) {
558
+ if (this.closed) {
559
+ throw new Error('Cannot push to a closed queue');
560
+ }
561
+
562
+ const waiter = this.waiters.shift();
563
+ if (waiter) {
564
+ waiter({ done: false, value });
565
+ return;
566
+ }
567
+
568
+ this.values.push(value);
569
+ }
570
+
571
+ close() {
572
+ if (this.closed) return;
573
+ this.closed = true;
574
+
575
+ while (this.waiters.length > 0) {
576
+ const waiter = this.waiters.shift();
577
+ waiter?.({ done: true, value: undefined as never });
578
+ }
579
+ }
580
+
581
+ [Symbol.asyncIterator](): AsyncIterator<T> {
582
+ return {
583
+ next: async () => {
584
+ if (this.values.length > 0) {
585
+ return { done: false, value: this.values.shift() as T };
586
+ }
587
+
588
+ if (this.closed) {
589
+ return { done: true, value: undefined as never };
590
+ }
591
+
592
+ return await new Promise<IteratorResult<T>>((resolve) => {
593
+ this.waiters.push(resolve);
594
+ });
595
+ },
596
+ };
597
+ }
598
+ }
599
+
600
+ export async function startClaudeSession(
601
+ args: {
602
+ localPath: string;
603
+ model: string;
604
+ effort?: string;
605
+ contextWindow?: ClaudeContextWindow;
606
+ planMode: boolean;
607
+ sessionToken: string | null;
608
+ onToolRequest: (request: HarnessToolRequest) => Promise<unknown>;
609
+ },
610
+ deps: StartClaudeSessionDeps = {},
611
+ ): Promise<ClaudeSessionHandle> {
612
+ const canUseTool: CanUseTool = async (toolName, input, options) => {
613
+ if (toolName !== 'AskUserQuestion' && toolName !== 'ExitPlanMode') {
614
+ return {
615
+ behavior: 'allow',
616
+ updatedInput: input,
617
+ };
618
+ }
619
+
620
+ const tool = normalizeToolCall({
621
+ toolName,
622
+ toolId: options.toolUseID,
623
+ input: (input ?? {}) as Record<string, unknown>,
624
+ });
625
+
626
+ if (tool.toolKind !== 'ask_user_question' && tool.toolKind !== 'exit_plan_mode') {
627
+ return {
628
+ behavior: 'deny',
629
+ message: 'Unsupported tool request',
630
+ };
631
+ }
632
+
633
+ const result = await args.onToolRequest({ tool });
634
+
635
+ if (tool.toolKind === 'ask_user_question') {
636
+ const record =
637
+ result && typeof result === 'object' ? (result as Record<string, unknown>) : {};
638
+
639
+ return {
640
+ behavior: 'allow',
641
+ updatedInput: {
642
+ ...(tool.rawInput ?? {}),
643
+ questions: record.questions ?? tool.input.questions,
644
+ answers: record.answers ?? result,
645
+ },
646
+ } satisfies PermissionResult;
647
+ }
648
+
649
+ const record = result && typeof result === 'object' ? (result as Record<string, unknown>) : {};
650
+ const confirmed = Boolean(record.confirmed);
651
+ if (confirmed) {
652
+ return {
653
+ behavior: 'allow',
654
+ updatedInput: {
655
+ ...(tool.rawInput ?? {}),
656
+ ...record,
657
+ },
658
+ } satisfies PermissionResult;
659
+ }
660
+
661
+ return {
662
+ behavior: 'deny',
663
+ message:
664
+ typeof record.message === 'string'
665
+ ? `User wants to suggest edits to the plan: ${record.message}`
666
+ : 'User wants to suggest edits to the plan before approving.',
667
+ } satisfies PermissionResult;
668
+ };
669
+
670
+ const promptQueue = new AsyncMessageQueue<SDKUserMessage>();
671
+
672
+ const q = (deps.queryFn ?? query)({
673
+ prompt: promptQueue,
674
+ options: {
675
+ cwd: args.localPath,
676
+ model: args.model,
677
+ effort: args.effort as 'low' | 'medium' | 'high' | 'max' | undefined,
678
+ betas: claudeBetasForContextWindow(args.contextWindow),
679
+ resume: args.sessionToken ?? undefined,
680
+ permissionMode: args.planMode ? 'plan' : 'acceptEdits',
681
+ canUseTool,
682
+ tools: [...CLAUDE_TOOLSET],
683
+ settingSources: ['user', 'project', 'local'],
684
+ env: (() => {
685
+ const { CLAUDECODE: _, ...env } = process.env;
686
+ return env;
687
+ })(),
688
+ },
689
+ });
690
+
691
+ return {
692
+ provider: 'claude',
693
+ stream: createClaudeHarnessStream(q),
694
+ getAccountInfo: async () => {
695
+ try {
696
+ return await q.accountInfo();
697
+ } catch {
698
+ return null;
699
+ }
700
+ },
701
+ interrupt: async () => {
702
+ await q.interrupt();
703
+ },
704
+ sendPrompt: async (content: string) => {
705
+ promptQueue.push({
706
+ type: 'user',
707
+ message: {
708
+ role: 'user',
709
+ content,
710
+ },
711
+ parent_tool_use_id: null,
712
+ session_id: args.sessionToken ?? '',
713
+ });
714
+ },
715
+ setModel: async (model: string) => {
716
+ await q.setModel(model);
717
+ },
718
+ setPermissionMode: async (planMode: boolean) => {
719
+ await q.setPermissionMode(planMode ? 'plan' : 'acceptEdits');
720
+ },
721
+ getCommands: async () => {
722
+ try {
723
+ const commands = await q.supportedCommands();
724
+ return commands.map((command) => ({
725
+ name: command.name,
726
+ description: command.description || undefined,
727
+ argumentHint: command.argumentHint || undefined,
728
+ }));
729
+ } catch {
730
+ return [];
731
+ }
732
+ },
733
+ close: () => {
734
+ promptQueue.close();
735
+ q.close();
736
+ },
737
+ };
738
+ }
739
+
740
+ export class AgentCoordinator {
741
+ private readonly store: EventStore;
742
+ private readonly onStateChange: () => void;
743
+ private readonly onTurnSettled: NonNullable<AgentCoordinatorArgs['onTurnSettled']> | null;
744
+ private readonly codexManager: CodexAppServerManager;
745
+ private readonly generateTitle: (messageContent: string) => Promise<GenerateSessionTitleResult>;
746
+ private readonly renameWorkspaceBranch: AgentCoordinatorArgs['renameWorkspaceBranch'];
747
+ private readonly startClaudeSessionFn: NonNullable<AgentCoordinatorArgs['startClaudeSession']>;
748
+ private reportBackgroundError: ((message: string) => void) | null = null;
749
+ // Releases uploaded files of queued messages dropped via dequeue/stop so they don't orphan in app
750
+ // data. Receives a server-resolved workspaceId (never derived from the client payload) plus the
751
+ // upload stored-names, so a forged attachment path can't steer deletion to another workspace.
752
+ private discardUploads: ((workspaceId: string, storedNames: string[]) => void) | null = null;
753
+ readonly activeTurns = new Map<string, ActiveTurn>();
754
+ readonly drainingStreams = new Map<string, { turn: HarnessTurn }>();
755
+ readonly claudeSessions = new Map<string, ClaudeSessionState>();
756
+ // Slash commands keyed by `${workspaceId}:${provider}`. Commands are workspace/provider scoped
757
+ // (filesystem + config derived), so the cache is shared across sessions in a workspace.
758
+ private readonly commandsCache = new Map<string, SlashCommandInfo[]>();
759
+ private readonly commandsInFlight = new Map<string, Promise<SlashCommandInfo[]>>();
760
+ // Sessions whose turn is mid-startup (reserved before activeTurns is registered) so concurrent
761
+ // sends are treated as busy and never race past the check.
762
+ private readonly startingSessions = new Set<string>();
763
+ private readonly drainingQueueSessions = new Set<string>();
764
+
765
+ constructor(args: AgentCoordinatorArgs) {
766
+ this.store = args.store;
767
+ this.onStateChange = args.onStateChange;
768
+ this.onTurnSettled = args.onTurnSettled ?? null;
769
+ this.codexManager = args.codexManager ?? new CodexAppServerManager();
770
+ this.generateTitle = args.generateTitle ?? generateTitleForSessionDetailed;
771
+ this.renameWorkspaceBranch = args.renameWorkspaceBranch;
772
+ this.startClaudeSessionFn = args.startClaudeSession ?? startClaudeSession;
773
+ }
774
+
775
+ setBackgroundErrorReporter(report: ((message: string) => void) | null) {
776
+ this.reportBackgroundError = report;
777
+ }
778
+
779
+ setUploadCleanup(cleanup: ((workspaceId: string, storedNames: string[]) => void) | null) {
780
+ this.discardUploads = cleanup;
781
+ }
782
+
783
+ private discardQueuedUploads(sessionId: string, messages: QueuedSessionMessageRecord[]) {
784
+ // Trust only the stored-name segment of the canonical upload ref; the workspace comes from the
785
+ // session, never from the (client-supplied) attachment path.
786
+ const storedNames = messages.flatMap((entry) =>
787
+ (entry.payload.attachments ?? []).flatMap((attachment) => {
788
+ const name = /^miko:\/\/uploads\/[^/]+\/(.+)$/.exec(attachment.relativePath)?.[1];
789
+ return name ? [name] : [];
790
+ }),
791
+ );
792
+ if (storedNames.length === 0) return;
793
+ const session = this.store.getSession(sessionId);
794
+ if (session) this.discardUploads?.(session.workspaceId, storedNames);
795
+ }
796
+
797
+ private queuedPayload(command: SendCommand): QueuedSessionSendPayload {
798
+ return {
799
+ provider: command.provider,
800
+ content: command.content,
801
+ attachments: command.attachments,
802
+ parts: command.parts,
803
+ model: command.model,
804
+ modelOptions: command.modelOptions,
805
+ effort: command.effort,
806
+ planMode: command.planMode,
807
+ };
808
+ }
809
+
810
+ private queuedCommand(message: QueuedSessionMessageRecord): SendCommand & { sessionId: string } {
811
+ return {
812
+ type: 'session.send',
813
+ sessionId: message.sessionId,
814
+ ...structuredClone(message.payload),
815
+ };
816
+ }
817
+
818
+ private async notifyTurnSettled(sessionId: string, outcome: 'success' | 'failed' | 'cancelled') {
819
+ try {
820
+ await this.onTurnSettled?.({ sessionId, outcome });
821
+ } catch (error) {
822
+ const message = error instanceof Error ? error.message : String(error);
823
+ this.reportBackgroundError?.(
824
+ `[turn-settled] session ${sessionId} failed post-turn orchestration: ${message}`,
825
+ );
826
+ }
827
+ }
828
+
829
+ private async notifyActiveTurnSettled(
830
+ active: ActiveTurn,
831
+ outcome: 'success' | 'failed' | 'cancelled',
832
+ ) {
833
+ if (active.settled) return;
834
+ active.settled = true;
835
+ if (active.queuedMessageId) {
836
+ if (outcome === 'success') {
837
+ await this.store.completeQueuedSessionMessage(active.sessionId, active.queuedMessageId);
838
+ } else {
839
+ await this.store.failQueuedSessionMessage(active.sessionId, active.queuedMessageId);
840
+ }
841
+ }
842
+ await this.notifyTurnSettled(active.sessionId, outcome);
843
+ }
844
+
845
+ private async recordTurnFailure(sessionId: string, error: unknown) {
846
+ const message = error instanceof Error ? error.message : String(error);
847
+ await this.store.appendMessage(
848
+ sessionId,
849
+ timestamped({
850
+ kind: 'result',
851
+ subtype: 'error',
852
+ isError: true,
853
+ durationMs: 0,
854
+ result: message,
855
+ }),
856
+ );
857
+ await this.store.recordTurnFailed(sessionId, message);
858
+ return message;
859
+ }
860
+
861
+ private async autoRenameWorkspaceBranchFromTitle(args: AutoRenameWorkspaceBranchArgs) {
862
+ if (!this.renameWorkspaceBranch) return null;
863
+
864
+ try {
865
+ const result = await this.renameWorkspaceBranch(args);
866
+ if (result.changed) this.onStateChange();
867
+ return result.branchName;
868
+ } catch (error) {
869
+ const message = error instanceof Error ? error.message : String(error);
870
+ this.reportBackgroundError?.(
871
+ `[branch-rename] workspace ${args.workspaceId} failed automatic branch rename: ${message}`,
872
+ );
873
+ return null;
874
+ }
875
+ }
876
+
877
+ getActiveStatuses() {
878
+ const statuses = new Map<string, MikoStatus>();
879
+ for (const [sessionId, turn] of this.activeTurns.entries()) {
880
+ statuses.set(sessionId, turn.status);
881
+ }
882
+ return statuses;
883
+ }
884
+
885
+ getPendingTool(sessionId: string): PendingToolSnapshot | null {
886
+ const pending = this.activeTurns.get(sessionId)?.pendingTool;
887
+ if (!pending) return null;
888
+ return pendingToolSnapshot(pending);
889
+ }
890
+
891
+ /**
892
+ * Slash commands for a session's workspace + provider. Served from cache when warm; otherwise a
893
+ * live session is used, or a short-lived harness is spawned just to enumerate (no turn sent, no
894
+ * message required). The provider falls back to the request hint before a turn binds one.
895
+ * Concurrent calls share one in-flight enumeration so opening/focusing never double-spawns.
896
+ */
897
+ async listCommands(sessionId: string, provider?: AgentProvider): Promise<SlashCommandInfo[]> {
898
+ const session = this.store.getSession(sessionId);
899
+ if (!session) return [];
900
+ const workspace = this.store.getWorkspace(session.workspaceId);
901
+ if (!workspace) return [];
902
+
903
+ const effectiveProvider = session.provider ?? provider ?? 'claude';
904
+ const cacheKey = `${workspace.id}:${effectiveProvider}`;
905
+
906
+ const cached = this.commandsCache.get(cacheKey);
907
+ if (cached) return cached;
908
+
909
+ const inFlight = this.commandsInFlight.get(cacheKey);
910
+ if (inFlight) return inFlight;
911
+
912
+ const promise = this.enumerateCommands(sessionId, effectiveProvider, workspace.localPath)
913
+ .then((commands) => {
914
+ this.commandsCache.set(cacheKey, commands);
915
+ return commands;
916
+ })
917
+ .catch(() => [] as SlashCommandInfo[])
918
+ .finally(() => {
919
+ this.commandsInFlight.delete(cacheKey);
920
+ });
921
+
922
+ this.commandsInFlight.set(cacheKey, promise);
923
+ return promise;
924
+ }
925
+
926
+ private async enumerateCommands(
927
+ sessionId: string,
928
+ provider: AgentProvider,
929
+ localPath: string,
930
+ ): Promise<SlashCommandInfo[]> {
931
+ if (provider === 'codex') {
932
+ return this.codexManager.enumerateSkills(localPath, normalizeServerModel('codex'));
933
+ }
934
+
935
+ const live = this.claudeSessions.get(sessionId);
936
+ if (live) return live.session.getCommands();
937
+
938
+ // Short-lived enumeration session: no prompt is ever sent and it is closed immediately.
939
+ const handle = await this.startClaudeSessionFn({
940
+ localPath,
941
+ model: normalizeServerModel('claude'),
942
+ planMode: false,
943
+ sessionToken: null,
944
+ onToolRequest: async () => ({}),
945
+ });
946
+ try {
947
+ return await handle.getCommands();
948
+ } finally {
949
+ handle.close();
950
+ }
951
+ }
952
+
953
+ getDrainingSessionIds(): Set<string> {
954
+ return new Set(this.drainingStreams.keys());
955
+ }
956
+
957
+ async stopDraining(sessionId: string) {
958
+ const draining = this.drainingStreams.get(sessionId);
959
+ if (!draining) return;
960
+
961
+ draining.turn.close();
962
+ this.drainingStreams.delete(sessionId);
963
+ this.onStateChange();
964
+ }
965
+
966
+ async closeSession(sessionId: string) {
967
+ await this.stopDraining(sessionId);
968
+ const claudeSession = this.claudeSessions.get(sessionId);
969
+ if (claudeSession) {
970
+ claudeSession.session.close();
971
+ this.claudeSessions.delete(sessionId);
972
+ }
973
+ this.onStateChange();
974
+ }
975
+
976
+ private resolveProvider(
977
+ command: Extract<ClientCommand, { type: 'session.send' }>,
978
+ currentProvider: AgentProvider | null,
979
+ ) {
980
+ if (currentProvider) return currentProvider;
981
+ return command.provider ?? 'claude';
982
+ }
983
+
984
+ private getProviderSettings(
985
+ provider: AgentProvider,
986
+ command: Extract<ClientCommand, { type: 'session.send' }>,
987
+ ) {
988
+ const catalog = getServerProviderCatalog(provider);
989
+ if (provider === 'claude') {
990
+ const model = normalizeServerModel(provider, command.model);
991
+ const modelOptions = normalizeClaudeModelOptions(model, command.modelOptions, command.effort);
992
+
993
+ return {
994
+ model,
995
+ effort: modelOptions.reasoningEffort,
996
+ contextWindow: modelOptions.contextWindow,
997
+ serviceTier: undefined,
998
+ planMode: catalog.supportsPlanMode ? Boolean(command.planMode) : false,
999
+ };
1000
+ }
1001
+
1002
+ const modelOptions = normalizeCodexModelOptions(command.modelOptions, command.effort);
1003
+ return {
1004
+ model: normalizeServerModel(provider, command.model),
1005
+ effort: modelOptions.reasoningEffort,
1006
+ contextWindow: undefined,
1007
+ serviceTier: codexServiceTierFromModelOptions(modelOptions),
1008
+ planMode: catalog.supportsPlanMode ? Boolean(command.planMode) : false,
1009
+ };
1010
+ }
1011
+
1012
+ private async startTurnForSession(args: {
1013
+ sessionId: string;
1014
+ provider: AgentProvider;
1015
+ content: string;
1016
+ attachments: ChatAttachment[];
1017
+ parts?: PromptPart[];
1018
+ model: string;
1019
+ effort?: string;
1020
+ contextWindow?: ClaudeContextWindow;
1021
+ serviceTier?: 'fast';
1022
+ planMode: boolean;
1023
+ appendUserPrompt: boolean;
1024
+ promptEntryId?: string;
1025
+ queuedMessageId?: string;
1026
+ }) {
1027
+ // Close any lingering draining stream before starting a new turn.
1028
+ const draining = this.drainingStreams.get(args.sessionId);
1029
+ if (draining) {
1030
+ draining.turn.close();
1031
+ this.drainingStreams.delete(args.sessionId);
1032
+ }
1033
+
1034
+ const session = this.store.requireSession(args.sessionId);
1035
+ if (this.activeTurns.has(args.sessionId)) {
1036
+ throw new Error('Session is already running');
1037
+ }
1038
+
1039
+ if (!session.provider) {
1040
+ await this.store.setSessionProvider(args.sessionId, args.provider);
1041
+ }
1042
+
1043
+ await this.store.setPlanMode(args.sessionId, args.planMode);
1044
+
1045
+ const workspace = this.store.getWorkspace(session.workspaceId);
1046
+ if (!workspace) {
1047
+ throw new Error('Workspace not found');
1048
+ }
1049
+
1050
+ const existingMessages = this.store.getMessages(args.sessionId);
1051
+ const shouldGenerateTitle =
1052
+ args.appendUserPrompt && session.title === 'Untitled' && existingMessages.length === 0;
1053
+
1054
+ const optimisticTitle = shouldGenerateTitle ? fallbackTitleFromMessage(args.content) : null;
1055
+ let optimisticBranchRename: Promise<string | null> = Promise.resolve(null);
1056
+
1057
+ if (optimisticTitle) {
1058
+ await this.store.renameSession(args.sessionId, optimisticTitle);
1059
+ optimisticBranchRename = this.autoRenameWorkspaceBranchFromTitle({
1060
+ workspaceId: workspace.id,
1061
+ branchName: optimisticTitle,
1062
+ expectedCurrentBranchName: workspace.branchName,
1063
+ });
1064
+ }
1065
+
1066
+ if (args.appendUserPrompt) {
1067
+ const userPromptEntry = timestamped(
1068
+ {
1069
+ kind: 'user_prompt',
1070
+ content: args.content,
1071
+ attachments: args.attachments,
1072
+ parts: args.parts,
1073
+ },
1074
+ Date.now(),
1075
+ args.promptEntryId,
1076
+ );
1077
+ await this.store.appendMessage(args.sessionId, userPromptEntry);
1078
+ }
1079
+
1080
+ await this.store.recordTurnStarted(args.sessionId);
1081
+
1082
+ if (shouldGenerateTitle) {
1083
+ void this.generateTitleInBackground(
1084
+ args.sessionId,
1085
+ args.content,
1086
+ workspace.localPath,
1087
+ optimisticTitle ?? 'Untitled',
1088
+ workspace.branchName,
1089
+ optimisticBranchRename,
1090
+ );
1091
+ }
1092
+
1093
+ const recordStartupFailure = async (error: unknown): Promise<never> => {
1094
+ if (args.provider === 'codex') {
1095
+ this.codexManager.stopSession(args.sessionId);
1096
+ }
1097
+ const message = await this.recordTurnFailure(args.sessionId, error);
1098
+ this.onStateChange();
1099
+ throw new RecordedTurnStartupError(message);
1100
+ };
1101
+
1102
+ const onToolRequest = async (request: HarnessToolRequest): Promise<unknown> => {
1103
+ const active = this.activeTurns.get(args.sessionId);
1104
+ if (!active) {
1105
+ throw new Error('Session turn ended unexpectedly');
1106
+ }
1107
+
1108
+ active.status = 'waiting_for_user';
1109
+ this.onStateChange();
1110
+
1111
+ return await new Promise<unknown>((resolve) => {
1112
+ active.pendingTool = {
1113
+ toolUseId: request.tool.toolId,
1114
+ tool: request.tool,
1115
+ resolve,
1116
+ };
1117
+ });
1118
+ };
1119
+
1120
+ let turn: HarnessTurn;
1121
+ try {
1122
+ if (args.provider === 'claude') {
1123
+ turn = await this.startClaudeTurn({
1124
+ sessionId: args.sessionId,
1125
+ localPath: workspace.localPath,
1126
+ model: args.model,
1127
+ effort: args.effort,
1128
+ contextWindow: args.contextWindow,
1129
+ planMode: args.planMode,
1130
+ sessionToken: session.sessionToken,
1131
+ onToolRequest,
1132
+ });
1133
+ } else {
1134
+ await this.codexManager.startSession({
1135
+ sessionId: args.sessionId,
1136
+ cwd: workspace.localPath,
1137
+ model: args.model,
1138
+ serviceTier: args.serviceTier,
1139
+ sessionToken: session.sessionToken,
1140
+ });
1141
+
1142
+ turn = await this.codexManager.startTurn({
1143
+ sessionId: args.sessionId,
1144
+ content: buildPromptText(args.content, args.attachments),
1145
+ model: args.model,
1146
+ effort: args.effort as Parameters<CodexAppServerManager['startTurn']>[0]['effort'],
1147
+ serviceTier: args.serviceTier,
1148
+ planMode: args.planMode,
1149
+ onToolRequest,
1150
+ });
1151
+ }
1152
+ } catch (error) {
1153
+ return await recordStartupFailure(error);
1154
+ }
1155
+
1156
+ const active: ActiveTurn = {
1157
+ sessionId: args.sessionId,
1158
+ provider: args.provider,
1159
+ turn,
1160
+ queuedMessageId: args.queuedMessageId,
1161
+ claudeSession:
1162
+ args.provider === 'claude' ? this.claudeSessions.get(args.sessionId) : undefined,
1163
+ model: args.model,
1164
+ effort: args.effort,
1165
+ serviceTier: args.serviceTier,
1166
+ planMode: args.planMode,
1167
+ status: args.provider === 'claude' ? 'running' : 'starting',
1168
+ pendingTool: null,
1169
+ postToolFollowUp: null,
1170
+ hasFinalResult: false,
1171
+ cancelRequested: false,
1172
+ cancelRecorded: false,
1173
+ settled: false,
1174
+ };
1175
+
1176
+ this.activeTurns.set(args.sessionId, active);
1177
+ this.onStateChange();
1178
+
1179
+ if (turn.getAccountInfo) {
1180
+ void turn
1181
+ .getAccountInfo()
1182
+ .then(async (accountInfo) => {
1183
+ if (!accountInfo) return;
1184
+ if (args.provider === 'claude') {
1185
+ const session = this.claudeSessions.get(args.sessionId);
1186
+ if (session) {
1187
+ if (session.accountInfoLoaded) return;
1188
+ session.accountInfoLoaded = true;
1189
+ } else {
1190
+ return;
1191
+ }
1192
+ }
1193
+ await this.store.appendMessage(
1194
+ args.sessionId,
1195
+ timestamped({ kind: 'account_info', accountInfo }),
1196
+ );
1197
+ this.onStateChange();
1198
+ })
1199
+ .catch(() => undefined);
1200
+ }
1201
+
1202
+ if (args.provider === 'claude') {
1203
+ const session = this.claudeSessions.get(args.sessionId);
1204
+ if (!session) {
1205
+ throw new Error('Claude session was not initialized');
1206
+ }
1207
+ await session.session.sendPrompt(buildPromptText(args.content, args.attachments));
1208
+ return;
1209
+ }
1210
+
1211
+ void this.runTurn(active);
1212
+ }
1213
+
1214
+ private async startClaudeTurn(args: {
1215
+ sessionId: string;
1216
+ localPath: string;
1217
+ model: string;
1218
+ effort?: string;
1219
+ contextWindow?: ClaudeContextWindow;
1220
+ planMode: boolean;
1221
+ sessionToken: string | null;
1222
+ onToolRequest: (request: HarnessToolRequest) => Promise<unknown>;
1223
+ }): Promise<HarnessTurn> {
1224
+ let session = this.claudeSessions.get(args.sessionId);
1225
+
1226
+ // The 1M context beta is fixed when the query is created, so a context-window change forces
1227
+ // a fresh session just like effort does.
1228
+ if (
1229
+ !session ||
1230
+ session.localPath !== args.localPath ||
1231
+ session.effort !== args.effort ||
1232
+ session.contextWindow !== args.contextWindow
1233
+ ) {
1234
+ if (session) {
1235
+ session.session.close();
1236
+ this.claudeSessions.delete(args.sessionId);
1237
+ }
1238
+
1239
+ const started = await this.startClaudeSessionFn({
1240
+ localPath: args.localPath,
1241
+ model: args.model,
1242
+ effort: args.effort,
1243
+ contextWindow: args.contextWindow,
1244
+ planMode: args.planMode,
1245
+ sessionToken: args.sessionToken,
1246
+ onToolRequest: args.onToolRequest,
1247
+ });
1248
+
1249
+ session = {
1250
+ sessionId: args.sessionId,
1251
+ session: started,
1252
+ localPath: args.localPath,
1253
+ model: args.model,
1254
+ effort: args.effort,
1255
+ contextWindow: args.contextWindow,
1256
+ planMode: args.planMode,
1257
+ sessionToken: args.sessionToken,
1258
+ accountInfoLoaded: false,
1259
+ };
1260
+
1261
+ this.claudeSessions.set(args.sessionId, session);
1262
+ void this.runClaudeSession(session);
1263
+ } else {
1264
+ if (session.model !== args.model) {
1265
+ await session.session.setModel(args.model);
1266
+ session.model = args.model;
1267
+ }
1268
+ if (session.planMode !== args.planMode) {
1269
+ await session.session.setPermissionMode(args.planMode);
1270
+ session.planMode = args.planMode;
1271
+ }
1272
+ }
1273
+
1274
+ return {
1275
+ provider: 'claude',
1276
+ stream: {
1277
+ async *[Symbol.asyncIterator]() {},
1278
+ },
1279
+ getAccountInfo: session.session.getAccountInfo,
1280
+ interrupt: session.session.interrupt,
1281
+ close: () => {},
1282
+ };
1283
+ }
1284
+
1285
+ async send(command: Extract<ClientCommand, { type: 'session.send' }>) {
1286
+ let sessionId = command.sessionId;
1287
+
1288
+ if (!sessionId) {
1289
+ if (!command.workspaceId) {
1290
+ throw new Error('Missing workspaceId for new session');
1291
+ }
1292
+
1293
+ const created = await this.store.createSession(command.workspaceId);
1294
+ sessionId = created.id;
1295
+ }
1296
+
1297
+ this.store.requireSession(sessionId);
1298
+
1299
+ // A turn is running/starting — or messages are already queued behind a settling turn — so queue
1300
+ // this one and let it start when the session drains, instead of rejecting or jumping the line.
1301
+ if (this.isSessionBusy(sessionId)) {
1302
+ await this.store.enqueueSessionMessage(sessionId, this.queuedPayload(command));
1303
+ this.onStateChange();
1304
+ return { sessionId };
1305
+ }
1306
+
1307
+ try {
1308
+ await this.startQueuedOrDirect({ ...command, sessionId });
1309
+ } catch (error) {
1310
+ if (!(error instanceof RecordedTurnStartupError)) throw error;
1311
+ }
1312
+ return { sessionId };
1313
+ }
1314
+
1315
+ async sendWhenIdle(
1316
+ command: Extract<ClientCommand, { type: 'session.send' }>,
1317
+ beforeStart?: () => void,
1318
+ ) {
1319
+ let sessionId = command.sessionId;
1320
+
1321
+ if (!sessionId) {
1322
+ if (!command.workspaceId) {
1323
+ throw new Error('Missing workspaceId for new session');
1324
+ }
1325
+
1326
+ const created = await this.store.createSession(command.workspaceId);
1327
+ sessionId = created.id;
1328
+ }
1329
+
1330
+ this.store.requireSession(sessionId);
1331
+
1332
+ if (this.isSessionBusy(sessionId)) {
1333
+ throw new Error('Session is busy — wait for the current turn to finish.');
1334
+ }
1335
+
1336
+ await this.startQueuedOrDirect({ ...command, sessionId }, beforeStart);
1337
+ return { sessionId };
1338
+ }
1339
+
1340
+ private async startQueuedOrDirect(
1341
+ command: SendCommand & { sessionId: string },
1342
+ beforeStart?: () => void,
1343
+ promptEntryId?: string,
1344
+ queuedMessageId?: string,
1345
+ ) {
1346
+ // Reserve synchronously (before any await) so a concurrent send/instruction sees the session as
1347
+ // busy during the async startup window — closing the check-then-start race. `activeTurns` takes
1348
+ // over once the turn is registered.
1349
+ this.startingSessions.add(command.sessionId);
1350
+ try {
1351
+ const session = this.store.requireSession(command.sessionId);
1352
+ const provider = this.resolveProvider(command, session.provider);
1353
+ const settings = this.getProviderSettings(provider, command);
1354
+
1355
+ beforeStart?.();
1356
+
1357
+ await this.startTurnForSession({
1358
+ sessionId: command.sessionId,
1359
+ provider,
1360
+ content: command.content,
1361
+ attachments: command.attachments ?? [],
1362
+ parts: command.parts,
1363
+ model: settings.model,
1364
+ effort: settings.effort,
1365
+ contextWindow: settings.contextWindow,
1366
+ serviceTier: settings.serviceTier,
1367
+ planMode: settings.planMode,
1368
+ appendUserPrompt: true,
1369
+ promptEntryId,
1370
+ queuedMessageId,
1371
+ });
1372
+ } finally {
1373
+ this.startingSessions.delete(command.sessionId);
1374
+ }
1375
+ }
1376
+
1377
+ /** Start the next queued message once a session has no active turn. No-op if busy or empty. */
1378
+ private async drainQueue(sessionId: string) {
1379
+ if (
1380
+ this.activeTurns.has(sessionId) ||
1381
+ this.startingSessions.has(sessionId) ||
1382
+ this.drainingQueueSessions.has(sessionId)
1383
+ ) {
1384
+ return;
1385
+ }
1386
+ this.drainingQueueSessions.add(sessionId);
1387
+ try {
1388
+ while (!this.activeTurns.has(sessionId) && !this.startingSessions.has(sessionId)) {
1389
+ const next = await this.store.claimNextQueuedSessionMessage(sessionId);
1390
+ if (!next) return;
1391
+ this.onStateChange();
1392
+
1393
+ try {
1394
+ await this.startQueuedOrDirect(
1395
+ this.queuedCommand(next),
1396
+ undefined,
1397
+ next.promptEntryId,
1398
+ next.id,
1399
+ );
1400
+ this.onStateChange();
1401
+ return;
1402
+ } catch (error) {
1403
+ if (!(error instanceof RecordedTurnStartupError) && this.store.getSession(sessionId)) {
1404
+ await this.recordTurnFailure(sessionId, error);
1405
+ }
1406
+ await this.store.failQueuedSessionMessage(sessionId, next.id);
1407
+ this.onStateChange();
1408
+ }
1409
+ }
1410
+ } finally {
1411
+ this.drainingQueueSessions.delete(sessionId);
1412
+ }
1413
+ }
1414
+
1415
+ private async recoverInterruptedQueueMessage(message: QueuedSessionMessageRecord) {
1416
+ const transcript = this.store.getMessages(message.sessionId);
1417
+ const promptIndex = transcript.findIndex((entry) => entry._id === message.promptEntryId);
1418
+ if (promptIndex < 0) {
1419
+ await this.store.requeueSessionMessage(message.sessionId, message.id);
1420
+ return;
1421
+ }
1422
+
1423
+ const terminalEntry = transcript
1424
+ .slice(promptIndex + 1)
1425
+ .find((entry) => entry.kind === 'result' || entry.kind === 'interrupted');
1426
+ if (terminalEntry?.kind === 'result' && !terminalEntry.isError) {
1427
+ await this.store.completeQueuedSessionMessage(message.sessionId, message.id);
1428
+ return;
1429
+ }
1430
+ if (terminalEntry) {
1431
+ await this.store.failQueuedSessionMessage(message.sessionId, message.id);
1432
+ return;
1433
+ }
1434
+
1435
+ const errorText =
1436
+ 'Miko restarted while this queued message was starting. It was not retried to avoid running the same task twice.';
1437
+ await this.store.appendMessageOnce(
1438
+ message.sessionId,
1439
+ timestamped(
1440
+ {
1441
+ kind: 'result',
1442
+ subtype: 'error',
1443
+ isError: true,
1444
+ durationMs: 0,
1445
+ result: errorText,
1446
+ },
1447
+ Date.now(),
1448
+ `queued-recovery:${message.id}`,
1449
+ ),
1450
+ );
1451
+ await this.store.recordTurnFailed(message.sessionId, errorText);
1452
+ await this.store.failQueuedSessionMessage(message.sessionId, message.id);
1453
+ }
1454
+
1455
+ async resumeQueuedMessages() {
1456
+ let draining: QueuedSessionMessageRecord[];
1457
+ try {
1458
+ draining = this.store.listDrainingSessionMessages();
1459
+ } catch (error) {
1460
+ const message = error instanceof Error ? error.message : String(error);
1461
+ this.reportBackgroundError?.(
1462
+ `[queue-recovery] Could not inspect draining messages: ${message}`,
1463
+ );
1464
+ return;
1465
+ }
1466
+
1467
+ for (const message of draining) {
1468
+ try {
1469
+ await this.recoverInterruptedQueueMessage(message);
1470
+ } catch (error) {
1471
+ const detail = error instanceof Error ? error.message : String(error);
1472
+ this.reportBackgroundError?.(
1473
+ `[queue-recovery] Session ${message.sessionId} could not recover message ${message.id}: ${detail}`,
1474
+ );
1475
+ }
1476
+ }
1477
+
1478
+ let sessionIds: string[];
1479
+ try {
1480
+ sessionIds = this.store.listSessionIdsWithQueuedMessages();
1481
+ } catch (error) {
1482
+ const message = error instanceof Error ? error.message : String(error);
1483
+ this.reportBackgroundError?.(
1484
+ `[queue-recovery] Could not inspect queued sessions: ${message}`,
1485
+ );
1486
+ return;
1487
+ }
1488
+
1489
+ if (draining.length > 0 || sessionIds.length > 0) {
1490
+ this.onStateChange();
1491
+ }
1492
+ await Promise.all(
1493
+ sessionIds.map(async (sessionId) => {
1494
+ try {
1495
+ await this.drainQueue(sessionId);
1496
+ } catch (error) {
1497
+ const message = error instanceof Error ? error.message : String(error);
1498
+ this.reportBackgroundError?.(
1499
+ `[queue-recovery] Session ${sessionId} could not resume queued messages: ${message}`,
1500
+ );
1501
+ }
1502
+ }),
1503
+ );
1504
+ }
1505
+
1506
+ /** Drop a still-queued message before it runs. */
1507
+ async dequeueMessage(sessionId: string, messageId: string) {
1508
+ const dropped = await this.store.dequeueSessionMessage(sessionId, messageId);
1509
+ if (!dropped) return;
1510
+ this.discardQueuedUploads(sessionId, [dropped]);
1511
+ this.onStateChange();
1512
+ }
1513
+
1514
+ /** A turn is running/starting or follow-ups are queued. Keeps discrete workspace actions un-queued. */
1515
+ isSessionBusy(sessionId: string): boolean {
1516
+ return (
1517
+ this.activeTurns.has(sessionId) ||
1518
+ this.startingSessions.has(sessionId) ||
1519
+ this.store.hasQueuedSessionMessages(sessionId)
1520
+ );
1521
+ }
1522
+
1523
+ getQueuedMessages(sessionId: string): QueuedMessageSnapshot[] {
1524
+ return this.store.listQueuedSessionMessages(sessionId).map((message) => ({
1525
+ id: message.id,
1526
+ content: message.payload.content,
1527
+ attachmentCount: message.payload.attachments?.length ?? 0,
1528
+ }));
1529
+ }
1530
+
1531
+ private async runClaudeSession(session: ClaudeSessionState) {
1532
+ // Set when the persistent stream throws/closes before emitting a result, so the finally can
1533
+ // drain any queued follow-up (the result branch is the only other place that drains Claude).
1534
+ let streamFailed = false;
1535
+ try {
1536
+ for await (const event of session.session.stream) {
1537
+ if (event.type === 'session_token' && event.sessionToken) {
1538
+ session.sessionToken = event.sessionToken;
1539
+ await this.store.setSessionToken(session.sessionId, event.sessionToken);
1540
+ this.onStateChange();
1541
+ continue;
1542
+ }
1543
+
1544
+ if (!event.entry) continue;
1545
+ await this.store.appendMessage(session.sessionId, event.entry);
1546
+
1547
+ const active = this.activeTurns.get(session.sessionId);
1548
+ if (event.entry.kind === 'system_init' && active) {
1549
+ active.status = 'running';
1550
+ }
1551
+
1552
+ if (event.entry.kind === 'result' && active) {
1553
+ active.hasFinalResult = true;
1554
+ if (event.entry.isError && !active.cancelRequested) {
1555
+ await this.store.recordTurnFailed(
1556
+ session.sessionId,
1557
+ event.entry.result || 'Turn failed',
1558
+ );
1559
+ await this.notifyActiveTurnSettled(active, 'failed');
1560
+ } else if (!active.cancelRequested) {
1561
+ await this.store.recordTurnFinished(session.sessionId);
1562
+ await this.notifyActiveTurnSettled(active, 'success');
1563
+ }
1564
+ this.activeTurns.delete(session.sessionId);
1565
+ // Start the next queued message on the same persistent session (skipped on cancel).
1566
+ if (!active.cancelRequested) await this.drainQueue(session.sessionId);
1567
+ }
1568
+
1569
+ this.onStateChange();
1570
+ }
1571
+ } catch (error) {
1572
+ const active = this.activeTurns.get(session.sessionId);
1573
+ if (active && !active.cancelRequested) {
1574
+ streamFailed = true;
1575
+ const message = error instanceof Error ? error.message : String(error);
1576
+ await this.store.appendMessage(
1577
+ session.sessionId,
1578
+ timestamped({
1579
+ kind: 'result',
1580
+ subtype: 'error',
1581
+ isError: true,
1582
+ durationMs: 0,
1583
+ result: message,
1584
+ }),
1585
+ );
1586
+ await this.store.recordTurnFailed(session.sessionId, message);
1587
+ await this.notifyActiveTurnSettled(active, 'failed');
1588
+ }
1589
+ } finally {
1590
+ // A 200k<->1M (or effort/cwd) switch can swap a fresh session and turn in under the same
1591
+ // sessionId. Only retire map entries this loop actually owns so the replacement survives:
1592
+ // the session entry by identity, and the active turn by its back-reference to this session.
1593
+ if (this.claudeSessions.get(session.sessionId) === session) {
1594
+ this.claudeSessions.delete(session.sessionId);
1595
+ }
1596
+ const active = this.activeTurns.get(session.sessionId);
1597
+ if (active?.provider === 'claude' && active.claudeSession === session) {
1598
+ if (active.cancelRequested && !active.cancelRecorded) {
1599
+ await this.store.recordTurnCancelled(session.sessionId);
1600
+ await this.notifyActiveTurnSettled(active, 'cancelled');
1601
+ }
1602
+ this.activeTurns.delete(session.sessionId);
1603
+ }
1604
+ session.session.close();
1605
+ this.onStateChange();
1606
+
1607
+ // Stream failed before a result: this session is torn down, so draining starts the queued
1608
+ // follow-up on a fresh session. Guarded by activeTurns inside drainQueue (no double-start
1609
+ // if a replacement turn already took over this id).
1610
+ if (streamFailed) await this.drainQueue(session.sessionId);
1611
+ }
1612
+ }
1613
+
1614
+ private async generateTitleInBackground(
1615
+ sessionId: string,
1616
+ messageContent: string,
1617
+ _cwd: string,
1618
+ expectedCurrentTitle: string,
1619
+ initialBranchName: string,
1620
+ optimisticBranchRename: Promise<string | null>,
1621
+ ) {
1622
+ try {
1623
+ const result = await this.generateTitle(messageContent);
1624
+ if (result.failureMessage) {
1625
+ this.reportBackgroundError?.(
1626
+ `[title-generation] session ${sessionId} failed provider title generation: ${result.failureMessage}`,
1627
+ );
1628
+ }
1629
+
1630
+ if (!result.title || result.usedFallback) return;
1631
+
1632
+ const session = this.store.requireSession(sessionId);
1633
+ if (session.title !== expectedCurrentTitle) return;
1634
+
1635
+ await this.store.renameSession(sessionId, result.title);
1636
+ const expectedCurrentBranchName = (await optimisticBranchRename) ?? initialBranchName;
1637
+ await this.autoRenameWorkspaceBranchFromTitle({
1638
+ workspaceId: session.workspaceId,
1639
+ branchName: result.title,
1640
+ expectedCurrentBranchName,
1641
+ });
1642
+ this.onStateChange();
1643
+ } catch (error) {
1644
+ const message = error instanceof Error ? error.message : String(error);
1645
+ this.reportBackgroundError?.(
1646
+ `[title-generation] session ${sessionId} failed background title generation: ${message}`,
1647
+ );
1648
+ }
1649
+ }
1650
+
1651
+ private async runTurn(active: ActiveTurn) {
1652
+ try {
1653
+ for await (const event of active.turn.stream) {
1654
+ // Once cancelled, stop processing further stream events.
1655
+ // cancel() already removed us from activeTurns and notified the UI.
1656
+ if (active.cancelRequested) break;
1657
+
1658
+ if (event.type === 'session_token' && event.sessionToken) {
1659
+ await this.store.setSessionToken(active.sessionId, event.sessionToken);
1660
+ this.onStateChange();
1661
+ continue;
1662
+ }
1663
+
1664
+ if (!event.entry) continue;
1665
+ await this.store.appendMessage(active.sessionId, event.entry);
1666
+
1667
+ if (event.entry.kind === 'system_init') {
1668
+ active.status = 'running';
1669
+ }
1670
+
1671
+ if (event.entry.kind === 'result') {
1672
+ active.hasFinalResult = true;
1673
+
1674
+ if (event.entry.isError && !active.cancelRequested) {
1675
+ await this.store.recordTurnFailed(
1676
+ active.sessionId,
1677
+ event.entry.result || 'Turn failed',
1678
+ );
1679
+ await this.notifyActiveTurnSettled(active, 'failed');
1680
+ } else if (!active.cancelRequested) {
1681
+ await this.store.recordTurnFinished(active.sessionId);
1682
+ await this.notifyActiveTurnSettled(active, 'success');
1683
+ }
1684
+
1685
+ // Remove from activeTurns as soon as the result arrives so the UI
1686
+ // transitions to idle immediately. The stream may still be open
1687
+ // (e.g. background tasks), but the user should be able to send
1688
+ // new messages without having to hit stop first.
1689
+ this.activeTurns.delete(active.sessionId);
1690
+
1691
+ // Track the still-open stream so the UI can show a draining
1692
+ // indicator and the user can stop background tasks.
1693
+ this.drainingStreams.set(active.sessionId, { turn: active.turn });
1694
+
1695
+ // The turn is settled from the UI's perspective even though the stream may keep
1696
+ // emitting background output. Drain now so a queued follow-up isn't stuck behind it —
1697
+ // but not when a tool-mandated follow-up is pending (it runs first, in the finally, and
1698
+ // its own settle drains the queue); draining here would preempt it.
1699
+ if (!active.cancelRequested && !active.postToolFollowUp) {
1700
+ await this.drainQueue(active.sessionId);
1701
+ }
1702
+ }
1703
+
1704
+ this.onStateChange();
1705
+ }
1706
+ } catch (error) {
1707
+ if (!active.cancelRequested) {
1708
+ await this.recordTurnFailure(active.sessionId, error);
1709
+ await this.notifyActiveTurnSettled(active, 'failed');
1710
+ }
1711
+ } finally {
1712
+ if (active.cancelRequested && !active.cancelRecorded) {
1713
+ await this.store.recordTurnCancelled(active.sessionId);
1714
+ await this.notifyActiveTurnSettled(active, 'cancelled');
1715
+ }
1716
+
1717
+ active.turn.close();
1718
+ // Only remove if we're still the active turn for this session.
1719
+ // We may have already been removed by result handling or cancel(),
1720
+ // and a new turn may have started for the same sessionId.
1721
+ if (this.activeTurns.get(active.sessionId) === active) {
1722
+ this.activeTurns.delete(active.sessionId);
1723
+ }
1724
+
1725
+ // Stream has fully ended — no longer draining.
1726
+ this.drainingStreams.delete(active.sessionId);
1727
+ this.onStateChange();
1728
+
1729
+ if (active.postToolFollowUp && !active.cancelRequested) {
1730
+ try {
1731
+ await this.startTurnForSession({
1732
+ sessionId: active.sessionId,
1733
+ provider: active.provider,
1734
+ content: active.postToolFollowUp.content,
1735
+ attachments: [],
1736
+ model: active.model,
1737
+ effort: active.effort,
1738
+ serviceTier: active.serviceTier,
1739
+ planMode: active.postToolFollowUp.planMode,
1740
+ appendUserPrompt: false,
1741
+ });
1742
+ } catch (error) {
1743
+ if (!(error instanceof RecordedTurnStartupError)) {
1744
+ await this.recordTurnFailure(active.sessionId, error);
1745
+ this.onStateChange();
1746
+ }
1747
+ await this.notifyActiveTurnSettled(active, 'failed');
1748
+ }
1749
+ }
1750
+
1751
+ // Start the next queued message (skipped if a follow-up turn already started or on cancel).
1752
+ if (!active.cancelRequested) await this.drainQueue(active.sessionId);
1753
+ }
1754
+ }
1755
+
1756
+ async cancel(sessionId: string, options: { preserveQueue?: boolean } = {}) {
1757
+ // Also clean up any draining stream for this session.
1758
+ const draining = this.drainingStreams.get(sessionId);
1759
+ if (draining) {
1760
+ draining.turn.close();
1761
+ this.drainingStreams.delete(sessionId);
1762
+ }
1763
+
1764
+ // Stop halts everything for this session: drop any queued follow-ups so they don't auto-start,
1765
+ // and release their uploaded attachments.
1766
+ const droppedQueue = options.preserveQueue
1767
+ ? []
1768
+ : await this.store.clearQueuedSessionMessages(sessionId);
1769
+ const hadQueue = droppedQueue.length > 0;
1770
+ if (hadQueue) this.discardQueuedUploads(sessionId, droppedQueue);
1771
+
1772
+ const active = this.activeTurns.get(sessionId);
1773
+ if (!active) {
1774
+ if (hadQueue) this.onStateChange();
1775
+ return;
1776
+ }
1777
+
1778
+ // Guards against double-cancel
1779
+ if (active.cancelRequested) return;
1780
+ active.cancelRequested = true;
1781
+ active.cancelRecorded = true;
1782
+
1783
+ const pendingTool = active.pendingTool;
1784
+ active.pendingTool = null;
1785
+
1786
+ if (pendingTool) {
1787
+ const result = discardedToolResult(pendingTool.tool);
1788
+ await this.store.appendMessage(
1789
+ sessionId,
1790
+ timestamped({
1791
+ kind: 'tool_result',
1792
+ toolId: pendingTool.toolUseId,
1793
+ content: result,
1794
+ }),
1795
+ );
1796
+ pendingTool.resolve(result);
1797
+ }
1798
+
1799
+ await this.store.appendMessage(sessionId, timestamped({ kind: 'interrupted' }));
1800
+ await this.store.recordTurnCancelled(sessionId);
1801
+ await this.notifyActiveTurnSettled(active, 'cancelled');
1802
+
1803
+ active.hasFinalResult = true;
1804
+
1805
+ // Remove from activeTurns immediately so the UI reflects the cancellation
1806
+ // right away, rather than waiting for interrupt() which may hang.
1807
+ this.activeTurns.delete(sessionId);
1808
+ this.onStateChange();
1809
+
1810
+ // Now attempt to interrupt/close the underlying stream in the background.
1811
+ // This is best-effort — the turn is already removed from active state above,
1812
+ // and runTurn()'s finally block will also call close().
1813
+ try {
1814
+ await Promise.race([
1815
+ active.turn.interrupt(),
1816
+ new Promise((resolve) => setTimeout(resolve, 5_000)),
1817
+ ]);
1818
+ } catch {
1819
+ // interrupt() failed — force close
1820
+ }
1821
+ active.turn.close();
1822
+ }
1823
+
1824
+ async respondTool(command: Extract<ClientCommand, { type: 'session.respondTool' }>) {
1825
+ const active = this.activeTurns.get(command.sessionId);
1826
+ if (!active?.pendingTool) {
1827
+ throw new Error('No pending tool request');
1828
+ }
1829
+
1830
+ const pending = active.pendingTool;
1831
+ if (pending.toolUseId !== command.toolUseId) {
1832
+ throw new Error('Tool response does not match active request');
1833
+ }
1834
+
1835
+ await this.store.appendMessage(
1836
+ command.sessionId,
1837
+ timestamped({
1838
+ kind: 'tool_result',
1839
+ toolId: command.toolUseId,
1840
+ content: command.result,
1841
+ }),
1842
+ );
1843
+
1844
+ active.pendingTool = null;
1845
+ active.status = 'running';
1846
+
1847
+ if (pending.tool.toolKind === 'exit_plan_mode') {
1848
+ const result = (command.result ?? {}) as {
1849
+ confirmed?: boolean;
1850
+ clearContext?: boolean;
1851
+ message?: string;
1852
+ };
1853
+
1854
+ if (result.confirmed && result.clearContext) {
1855
+ await this.store.setSessionToken(command.sessionId, null);
1856
+ await this.store.appendMessage(command.sessionId, timestamped({ kind: 'context_cleared' }));
1857
+ }
1858
+
1859
+ if (active.provider === 'codex') {
1860
+ active.postToolFollowUp = result.confirmed
1861
+ ? {
1862
+ content: result.message
1863
+ ? `Proceed with the approved plan. Additional guidance: ${result.message}`
1864
+ : 'Proceed with the approved plan.',
1865
+ planMode: false,
1866
+ }
1867
+ : {
1868
+ content: result.message
1869
+ ? `Revise the plan using this feedback: ${result.message}`
1870
+ : 'Revise the plan using this feedback.',
1871
+ planMode: true,
1872
+ };
1873
+ }
1874
+ }
1875
+
1876
+ pending.resolve(command.result);
1877
+ this.onStateChange();
1878
+ }
1879
+ }