kanna-code 0.20.0 → 0.21.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 (326) hide show
  1. package/dist/client/assets/abap-BdImnpbu.js +1 -0
  2. package/dist/client/assets/actionscript-3-CoDkCxhg.js +1 -0
  3. package/dist/client/assets/ada-bCR0ucgS.js +1 -0
  4. package/dist/client/assets/andromeeda-C4gqWexZ.js +1 -0
  5. package/dist/client/assets/angular-html-CU67Zn6k.js +1 -0
  6. package/dist/client/assets/angular-ts-BwZT4LLn.js +1 -0
  7. package/dist/client/assets/apache-Pmp26Uib.js +1 -0
  8. package/dist/client/assets/apex-D8_7TLub.js +1 -0
  9. package/dist/client/assets/apl-dKokRX4l.js +1 -0
  10. package/dist/client/assets/applescript-Co6uUVPk.js +1 -0
  11. package/dist/client/assets/ara-BRHolxvo.js +1 -0
  12. package/dist/client/assets/asciidoc-Ve4PFQV2.js +1 -0
  13. package/dist/client/assets/asm-D_Q5rh1f.js +1 -0
  14. package/dist/client/assets/astro-CbQHKStN.js +1 -0
  15. package/dist/client/assets/aurora-x-D-2ljcwZ.js +1 -0
  16. package/dist/client/assets/awk-DMzUqQB5.js +1 -0
  17. package/dist/client/assets/ayu-dark-DYE7WIF3.js +1 -0
  18. package/dist/client/assets/ayu-light-BA47KaF1.js +1 -0
  19. package/dist/client/assets/ayu-mirage-32ctXXKs.js +1 -0
  20. package/dist/client/assets/ballerina-BFfxhgS-.js +1 -0
  21. package/dist/client/assets/bat-BkioyH1T.js +1 -0
  22. package/dist/client/assets/beancount-k_qm7-4y.js +1 -0
  23. package/dist/client/assets/berry-uYugtg8r.js +1 -0
  24. package/dist/client/assets/bibtex-CHM0blh-.js +1 -0
  25. package/dist/client/assets/bicep-Bmn6On1c.js +1 -0
  26. package/dist/client/assets/bird2-DPOp833l.js +1 -0
  27. package/dist/client/assets/blade-D4QpJJKB.js +1 -0
  28. package/dist/client/assets/bsl-BO_Y6i37.js +1 -0
  29. package/dist/client/assets/c-BIGW1oBm.js +1 -0
  30. package/dist/client/assets/c3-eo99z4R2.js +1 -0
  31. package/dist/client/assets/cadence-Bv_4Rxtq.js +1 -0
  32. package/dist/client/assets/cairo-KRGpt6FW.js +1 -0
  33. package/dist/client/assets/catppuccin-frappe-DFWUc33u.js +1 -0
  34. package/dist/client/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
  35. package/dist/client/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
  36. package/dist/client/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
  37. package/dist/client/assets/clarity-D53aC0YG.js +1 -0
  38. package/dist/client/assets/clojure-P80f7IUj.js +1 -0
  39. package/dist/client/assets/cmake-D1j8_8rp.js +1 -0
  40. package/dist/client/assets/cobol-nwyudZeR.js +1 -0
  41. package/dist/client/assets/codeowners-Bp6g37R7.js +1 -0
  42. package/dist/client/assets/codeql-DsOJ9woJ.js +1 -0
  43. package/dist/client/assets/coffee-Ch7k5sss.js +1 -0
  44. package/dist/client/assets/common-lisp-Cg-RD9OK.js +1 -0
  45. package/dist/client/assets/coq-DkFqJrB1.js +1 -0
  46. package/dist/client/assets/cpp-CofmeUqb.js +1 -0
  47. package/dist/client/assets/crystal-tKQVLTB8.js +1 -0
  48. package/dist/client/assets/csharp-COcwbKMJ.js +1 -0
  49. package/dist/client/assets/css-DPfMkruS.js +1 -0
  50. package/dist/client/assets/csv-fuZLfV_i.js +1 -0
  51. package/dist/client/assets/cue-D82EKSYY.js +1 -0
  52. package/dist/client/assets/cypher-COkxafJQ.js +1 -0
  53. package/dist/client/assets/d-85-TOEBH.js +1 -0
  54. package/dist/client/assets/dark-plus-C3mMm8J8.js +1 -0
  55. package/dist/client/assets/dart-CF10PKvl.js +1 -0
  56. package/dist/client/assets/dax-CEL-wOlO.js +1 -0
  57. package/dist/client/assets/desktop-BmXAJ9_W.js +1 -0
  58. package/dist/client/assets/diff-D97Zzqfu.js +1 -0
  59. package/dist/client/assets/docker-BcOcwvcX.js +1 -0
  60. package/dist/client/assets/dotenv-Da5cRb03.js +1 -0
  61. package/dist/client/assets/dracula-BzJJZx-M.js +1 -0
  62. package/dist/client/assets/dracula-soft-BXkSAIEj.js +1 -0
  63. package/dist/client/assets/dream-maker-BtqSS_iP.js +1 -0
  64. package/dist/client/assets/edge-BkV0erSs.js +1 -0
  65. package/dist/client/assets/elixir-CDX3lj18.js +1 -0
  66. package/dist/client/assets/elm-DbKCFpqz.js +1 -0
  67. package/dist/client/assets/emacs-lisp-C9XAeP06.js +1 -0
  68. package/dist/client/assets/erb-B12qg9BL.js +1 -0
  69. package/dist/client/assets/erlang-DsQrWhSR.js +1 -0
  70. package/dist/client/assets/everforest-dark-BgDCqdQA.js +1 -0
  71. package/dist/client/assets/everforest-light-C8M2exoo.js +1 -0
  72. package/dist/client/assets/fennel-BYunw83y.js +1 -0
  73. package/dist/client/assets/fish-BvzEVeQv.js +1 -0
  74. package/dist/client/assets/fluent-C4IJs8-o.js +1 -0
  75. package/dist/client/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  76. package/dist/client/assets/fortran-free-form-BxgE0vQu.js +1 -0
  77. package/dist/client/assets/fsharp-CXgrBDvD.js +1 -0
  78. package/dist/client/assets/gdresource-BOOCDP_w.js +1 -0
  79. package/dist/client/assets/gdscript-C5YyOfLZ.js +1 -0
  80. package/dist/client/assets/gdshader-DkwncUOv.js +1 -0
  81. package/dist/client/assets/genie-D0YGMca9.js +1 -0
  82. package/dist/client/assets/gherkin-DyxjwDmM.js +1 -0
  83. package/dist/client/assets/git-commit-F4YmCXRG.js +1 -0
  84. package/dist/client/assets/git-rebase-r7XF79zn.js +1 -0
  85. package/dist/client/assets/github-dark-DHJKELXO.js +1 -0
  86. package/dist/client/assets/github-dark-default-Cuk6v7N8.js +1 -0
  87. package/dist/client/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  88. package/dist/client/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  89. package/dist/client/assets/github-light-DAi9KRSo.js +1 -0
  90. package/dist/client/assets/github-light-default-D7oLnXFd.js +1 -0
  91. package/dist/client/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  92. package/dist/client/assets/gleam-BspZqrRM.js +1 -0
  93. package/dist/client/assets/glimmer-js-Rg0-pVw9.js +1 -0
  94. package/dist/client/assets/glimmer-ts-U6CK756n.js +1 -0
  95. package/dist/client/assets/glsl-DplSGwfg.js +1 -0
  96. package/dist/client/assets/gn-n2N0HUVH.js +1 -0
  97. package/dist/client/assets/gnuplot-DdkO51Og.js +1 -0
  98. package/dist/client/assets/go-CxLEBnE3.js +1 -0
  99. package/dist/client/assets/graphql-ChdNCCLP.js +1 -0
  100. package/dist/client/assets/groovy-gcz8RCvz.js +1 -0
  101. package/dist/client/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  102. package/dist/client/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  103. package/dist/client/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  104. package/dist/client/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  105. package/dist/client/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  106. package/dist/client/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  107. package/dist/client/assets/hack-CaT9iCJl.js +1 -0
  108. package/dist/client/assets/haml-B8DHNrY2.js +1 -0
  109. package/dist/client/assets/handlebars-BL8al0AC.js +1 -0
  110. package/dist/client/assets/haskell-Df6bDoY_.js +1 -0
  111. package/dist/client/assets/haxe-CzTSHFRz.js +1 -0
  112. package/dist/client/assets/hcl-BWvSN4gD.js +1 -0
  113. package/dist/client/assets/hjson-D5-asLiD.js +1 -0
  114. package/dist/client/assets/hlsl-D3lLCCz7.js +1 -0
  115. package/dist/client/assets/horizon-BUw7H-hv.js +1 -0
  116. package/dist/client/assets/horizon-bright-Cn-bp-IR.js +1 -0
  117. package/dist/client/assets/houston-DnULxvSX.js +1 -0
  118. package/dist/client/assets/html-GMplVEZG.js +1 -0
  119. package/dist/client/assets/html-derivative-BFtXZ54Q.js +1 -0
  120. package/dist/client/assets/http-jrhK8wxY.js +1 -0
  121. package/dist/client/assets/hurl-irOxFIW8.js +1 -0
  122. package/dist/client/assets/hxml-Bvhsp5Yf.js +1 -0
  123. package/dist/client/assets/hy-DFXneXwc.js +1 -0
  124. package/dist/client/assets/imba-DGztddWO.js +1 -0
  125. package/dist/client/assets/index-B-8fHyaU.css +32 -0
  126. package/dist/client/assets/index-DrUC3AGz.js +2502 -0
  127. package/dist/client/assets/ini-BEwlwnbL.js +1 -0
  128. package/dist/client/assets/java-CylS5w8V.js +1 -0
  129. package/dist/client/assets/javascript-wDzz0qaB.js +1 -0
  130. package/dist/client/assets/jinja-4LBKfQ-Z.js +1 -0
  131. package/dist/client/assets/jison-wvAkD_A8.js +1 -0
  132. package/dist/client/assets/json-Cp-IABpG.js +1 -0
  133. package/dist/client/assets/json5-C9tS-k6U.js +1 -0
  134. package/dist/client/assets/jsonc-Des-eS-w.js +1 -0
  135. package/dist/client/assets/jsonl-DcaNXYhu.js +1 -0
  136. package/dist/client/assets/jsonnet-DFQXde-d.js +1 -0
  137. package/dist/client/assets/jssm-C2t-YnRu.js +1 -0
  138. package/dist/client/assets/jsx-g9-lgVsj.js +1 -0
  139. package/dist/client/assets/julia-CxzCAyBv.js +1 -0
  140. package/dist/client/assets/just-Cw27pwNe.js +1 -0
  141. package/dist/client/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  142. package/dist/client/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  143. package/dist/client/assets/kanagawa-wave-DWedfzmr.js +1 -0
  144. package/dist/client/assets/kdl-DV7GczEv.js +1 -0
  145. package/dist/client/assets/kotlin-BdnUsdx6.js +1 -0
  146. package/dist/client/assets/kusto-DZf3V79B.js +1 -0
  147. package/dist/client/assets/laserwave-DUszq2jm.js +1 -0
  148. package/dist/client/assets/latex-CWtU0Tv5.js +1 -0
  149. package/dist/client/assets/lean-BZvkOJ9d.js +1 -0
  150. package/dist/client/assets/less-B1dDrJ26.js +1 -0
  151. package/dist/client/assets/light-plus-B7mTdjB0.js +1 -0
  152. package/dist/client/assets/liquid-DYVedYrR.js +1 -0
  153. package/dist/client/assets/llvm-DjAJT7YJ.js +1 -0
  154. package/dist/client/assets/log-2UxHyX5q.js +1 -0
  155. package/dist/client/assets/logo-BtOb2qkB.js +1 -0
  156. package/dist/client/assets/lua-BaeVxFsk.js +1 -0
  157. package/dist/client/assets/luau-C-HG3fhB.js +1 -0
  158. package/dist/client/assets/make-CHLpvVh8.js +1 -0
  159. package/dist/client/assets/markdown-Cvjx9yec.js +1 -0
  160. package/dist/client/assets/marko-CnJfTvn9.js +1 -0
  161. package/dist/client/assets/material-theme-D5KoaKCx.js +1 -0
  162. package/dist/client/assets/material-theme-darker-BfHTSMKl.js +1 -0
  163. package/dist/client/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  164. package/dist/client/assets/material-theme-ocean-CyktbL80.js +1 -0
  165. package/dist/client/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  166. package/dist/client/assets/matlab-D7o27uSR.js +1 -0
  167. package/dist/client/assets/mdc-BMNejdWA.js +1 -0
  168. package/dist/client/assets/mdx-Cmh6b_Ma.js +1 -0
  169. package/dist/client/assets/mermaid-mWjccvbQ.js +1 -0
  170. package/dist/client/assets/min-dark-CafNBF8u.js +1 -0
  171. package/dist/client/assets/min-light-CTRr51gU.js +1 -0
  172. package/dist/client/assets/mipsasm-CKIfxQSi.js +1 -0
  173. package/dist/client/assets/mojo-rZm6bMo-.js +1 -0
  174. package/dist/client/assets/monokai-D4h5O-jR.js +1 -0
  175. package/dist/client/assets/moonbit-_H4v1dQx.js +1 -0
  176. package/dist/client/assets/move-IF9eRakj.js +1 -0
  177. package/dist/client/assets/narrat-DRg8JJMk.js +1 -0
  178. package/dist/client/assets/nextflow-Zz6hmt5N.js +1 -0
  179. package/dist/client/assets/nextflow-groovy-BeH2EWoN.js +1 -0
  180. package/dist/client/assets/nginx-BpAMiNFr.js +1 -0
  181. package/dist/client/assets/night-owl-C39BiMTA.js +1 -0
  182. package/dist/client/assets/night-owl-light-CMTm3GFP.js +1 -0
  183. package/dist/client/assets/nim-CVrawwO9.js +1 -0
  184. package/dist/client/assets/nix-CwoSXNpI.js +1 -0
  185. package/dist/client/assets/nord-Ddv68eIx.js +1 -0
  186. package/dist/client/assets/nushell-Cz2AlsmD.js +1 -0
  187. package/dist/client/assets/objective-c-DXmwc3jG.js +1 -0
  188. package/dist/client/assets/objective-cpp-CLxacb5B.js +1 -0
  189. package/dist/client/assets/ocaml-C0hk2d4L.js +1 -0
  190. package/dist/client/assets/odin-BBf5iR-q.js +1 -0
  191. package/dist/client/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  192. package/dist/client/assets/one-light-C3Wv6jpd.js +1 -0
  193. package/dist/client/assets/openscad-C4EeE6gA.js +1 -0
  194. package/dist/client/assets/pascal-D93ZcfNL.js +1 -0
  195. package/dist/client/assets/perl-C0TMdlhV.js +1 -0
  196. package/dist/client/assets/php-Dhbhpdrm.js +1 -0
  197. package/dist/client/assets/pierre-dark-DF2SEV7i.js +1 -0
  198. package/dist/client/assets/pierre-light-DOlZxES8.js +1 -0
  199. package/dist/client/assets/pkl-u5AG7uiY.js +1 -0
  200. package/dist/client/assets/plastic-3e1v2bzS.js +1 -0
  201. package/dist/client/assets/plsql-ChMvpjG-.js +1 -0
  202. package/dist/client/assets/po-BTJTHyun.js +1 -0
  203. package/dist/client/assets/poimandres-CS3Unz2-.js +1 -0
  204. package/dist/client/assets/polar-C0HS_06l.js +1 -0
  205. package/dist/client/assets/postcss-CXtECtnM.js +1 -0
  206. package/dist/client/assets/powerquery-CEu0bR-o.js +1 -0
  207. package/dist/client/assets/powershell-Dpen1YoG.js +1 -0
  208. package/dist/client/assets/prisma-Dd19v3D-.js +1 -0
  209. package/dist/client/assets/prolog-CbFg5uaA.js +1 -0
  210. package/dist/client/assets/proto-C7zT0LnQ.js +1 -0
  211. package/dist/client/assets/pug-CGlum2m_.js +1 -0
  212. package/dist/client/assets/puppet-BMWR74SV.js +1 -0
  213. package/dist/client/assets/purescript-CklMAg4u.js +1 -0
  214. package/dist/client/assets/python-B6aJPvgy.js +1 -0
  215. package/dist/client/assets/qml-3beO22l8.js +1 -0
  216. package/dist/client/assets/qmldir-C8lEn-DE.js +1 -0
  217. package/dist/client/assets/qss-IeuSbFQv.js +1 -0
  218. package/dist/client/assets/r-Dspwwk_N.js +1 -0
  219. package/dist/client/assets/racket-BqYA7rlc.js +1 -0
  220. package/dist/client/assets/raku-DXvB9xmW.js +1 -0
  221. package/dist/client/assets/razor-Uh8Bk_45.js +1 -0
  222. package/dist/client/assets/red-bN70gL4F.js +1 -0
  223. package/dist/client/assets/reg-C-SQnVFl.js +1 -0
  224. package/dist/client/assets/regexp-CDVJQ6XC.js +1 -0
  225. package/dist/client/assets/rel-C3B-1QV4.js +1 -0
  226. package/dist/client/assets/riscv-BM1_JUlF.js +1 -0
  227. package/dist/client/assets/ron-D8l8udqQ.js +1 -0
  228. package/dist/client/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
  229. package/dist/client/assets/rose-pine-moon-D4_iv3hh.js +1 -0
  230. package/dist/client/assets/rose-pine-qdsjHGoJ.js +1 -0
  231. package/dist/client/assets/rosmsg-BJDFO7_C.js +1 -0
  232. package/dist/client/assets/rst-BrH8l1NY.js +1 -0
  233. package/dist/client/assets/ruby-Dw2BHqvy.js +1 -0
  234. package/dist/client/assets/rust-B1yitclQ.js +1 -0
  235. package/dist/client/assets/sas-cz2c8ADy.js +1 -0
  236. package/dist/client/assets/sass-Cj5Yp3dK.js +1 -0
  237. package/dist/client/assets/scala-C151Ov-r.js +1 -0
  238. package/dist/client/assets/scheme-C98Dy4si.js +1 -0
  239. package/dist/client/assets/scss-OYdSNvt2.js +1 -0
  240. package/dist/client/assets/sdbl-DVxCFoDh.js +1 -0
  241. package/dist/client/assets/shaderlab-Dg9Lc6iA.js +1 -0
  242. package/dist/client/assets/shellscript-Yzrsuije.js +1 -0
  243. package/dist/client/assets/shellsession-BADoaaVG.js +1 -0
  244. package/dist/client/assets/slack-dark-BthQWCQV.js +1 -0
  245. package/dist/client/assets/slack-ochin-DqwNpetd.js +1 -0
  246. package/dist/client/assets/smalltalk-BERRCDM3.js +1 -0
  247. package/dist/client/assets/snazzy-light-Bw305WKR.js +1 -0
  248. package/dist/client/assets/solarized-dark-DXbdFlpD.js +1 -0
  249. package/dist/client/assets/solarized-light-L9t79GZl.js +1 -0
  250. package/dist/client/assets/solidity-rGO070M0.js +1 -0
  251. package/dist/client/assets/soy-Brmx7dQM.js +1 -0
  252. package/dist/client/assets/sparql-rVzFXLq3.js +1 -0
  253. package/dist/client/assets/splunk-BtCnVYZw.js +1 -0
  254. package/dist/client/assets/sql-BLtJtn59.js +1 -0
  255. package/dist/client/assets/ssh-config-_ykCGR6B.js +1 -0
  256. package/dist/client/assets/stata-BH5u7GGu.js +1 -0
  257. package/dist/client/assets/stylus-BEDo0Tqx.js +1 -0
  258. package/dist/client/assets/surrealql-Bq5Q-fJD.js +1 -0
  259. package/dist/client/assets/svelte-C_ipcX3V.js +1 -0
  260. package/dist/client/assets/swift-D82vCrfD.js +1 -0
  261. package/dist/client/assets/synthwave-84-CbfX1IO0.js +1 -0
  262. package/dist/client/assets/system-verilog-CnnmHF94.js +1 -0
  263. package/dist/client/assets/systemd-4A_iFExJ.js +1 -0
  264. package/dist/client/assets/talonscript-CkByrt1z.js +1 -0
  265. package/dist/client/assets/tasl-QIJgUcNo.js +1 -0
  266. package/dist/client/assets/tcl-dwOrl1Do.js +1 -0
  267. package/dist/client/assets/templ-P3uqSqPl.js +1 -0
  268. package/dist/client/assets/terraform-BETggiCN.js +1 -0
  269. package/dist/client/assets/tex-idrVyKtj.js +1 -0
  270. package/dist/client/assets/tokyo-night-hegEt444.js +1 -0
  271. package/dist/client/assets/toml-vGWfd6FD.js +1 -0
  272. package/dist/client/assets/ts-tags-zn1MmPIZ.js +1 -0
  273. package/dist/client/assets/tsv-B_m7g4N7.js +1 -0
  274. package/dist/client/assets/tsx-COt5Ahok.js +1 -0
  275. package/dist/client/assets/turtle-BsS91CYL.js +1 -0
  276. package/dist/client/assets/twig-DNn4PbVi.js +1 -0
  277. package/dist/client/assets/typescript-BPQ3VLAy.js +1 -0
  278. package/dist/client/assets/typespec-BGHnOYBU.js +1 -0
  279. package/dist/client/assets/typst-DHCkPAjA.js +1 -0
  280. package/dist/client/assets/v-BcVCzyr7.js +1 -0
  281. package/dist/client/assets/vala-CsfeWuGM.js +1 -0
  282. package/dist/client/assets/vb-D17OF-Vu.js +1 -0
  283. package/dist/client/assets/verilog-BQ8w6xss.js +1 -0
  284. package/dist/client/assets/vesper-DU1UobuO.js +1 -0
  285. package/dist/client/assets/vhdl-CeAyd5Ju.js +1 -0
  286. package/dist/client/assets/viml-CJc9bBzg.js +1 -0
  287. package/dist/client/assets/vitesse-black-Bkuqu6BP.js +1 -0
  288. package/dist/client/assets/vitesse-dark-D0r3Knsf.js +1 -0
  289. package/dist/client/assets/vitesse-light-CVO1_9PV.js +1 -0
  290. package/dist/client/assets/vue-DN_0RTcg.js +1 -0
  291. package/dist/client/assets/vue-html-AaS7Mt5G.js +1 -0
  292. package/dist/client/assets/vue-vine-CQOfvN7w.js +1 -0
  293. package/dist/client/assets/vyper-CDx5xZoG.js +1 -0
  294. package/dist/client/assets/wasm-CG6Dc4jp.js +1 -0
  295. package/dist/client/assets/wasm-MzD3tlZU.js +1 -0
  296. package/dist/client/assets/wenyan-BV7otONQ.js +1 -0
  297. package/dist/client/assets/wgsl-Dx-B1_4e.js +1 -0
  298. package/dist/client/assets/wikitext-BhOHFoWU.js +1 -0
  299. package/dist/client/assets/wit-5i3qLPDT.js +1 -0
  300. package/dist/client/assets/wolfram-lXgVvXCa.js +1 -0
  301. package/dist/client/assets/xml-sdJ4AIDG.js +1 -0
  302. package/dist/client/assets/xsl-CtQFsRM5.js +1 -0
  303. package/dist/client/assets/yaml-Buea-lGh.js +1 -0
  304. package/dist/client/assets/zenscript-DVFEvuxE.js +1 -0
  305. package/dist/client/assets/zig-VOosw3JB.js +1 -0
  306. package/dist/client/index.html +2 -2
  307. package/package.json +2 -1
  308. package/src/server/agent.test.ts +45 -1
  309. package/src/server/agent.ts +141 -8
  310. package/src/server/codex-app-server-protocol.ts +28 -0
  311. package/src/server/codex-app-server.test.ts +88 -0
  312. package/src/server/codex-app-server.ts +71 -1
  313. package/src/server/diff-store.test.ts +126 -0
  314. package/src/server/diff-store.ts +365 -0
  315. package/src/server/generate-commit-message.test.ts +59 -0
  316. package/src/server/generate-commit-message.ts +121 -0
  317. package/src/server/read-models.test.ts +1 -1
  318. package/src/server/read-models.ts +4 -1
  319. package/src/server/server.ts +60 -1
  320. package/src/server/terminal-manager.test.ts +37 -4
  321. package/src/server/uploads.ts +4 -0
  322. package/src/server/ws-router.ts +118 -7
  323. package/src/shared/protocol.ts +3 -0
  324. package/src/shared/types.ts +50 -0
  325. package/dist/client/assets/index-Cdjl9mRd.js +0 -606
  326. package/dist/client/assets/index-D6aACRxb.css +0 -32
@@ -0,0 +1,126 @@
1
+ import { afterEach, describe, expect, test } from "bun:test"
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises"
3
+ import { tmpdir } from "node:os"
4
+ import path from "node:path"
5
+ import { DiffStore } from "./diff-store"
6
+
7
+ async function run(command: string[], cwd: string) {
8
+ const process = Bun.spawn(command, {
9
+ cwd,
10
+ stdout: "pipe",
11
+ stderr: "pipe",
12
+ })
13
+ const [stdout, stderr, exitCode] = await Promise.all([
14
+ new Response(process.stdout).text(),
15
+ new Response(process.stderr).text(),
16
+ process.exited,
17
+ ])
18
+
19
+ if (exitCode !== 0) {
20
+ throw new Error(stderr || stdout || `Command failed: ${command.join(" ")}`)
21
+ }
22
+
23
+ return stdout
24
+ }
25
+
26
+ async function createRepo() {
27
+ const root = await mkdtemp(path.join(tmpdir(), "kanna-diff-store-"))
28
+ await run(["git", "init"], root)
29
+ await run(["git", "config", "user.email", "kanna@example.com"], root)
30
+ await run(["git", "config", "user.name", "Kanna"], root)
31
+ return root
32
+ }
33
+
34
+ const tempDirs: string[] = []
35
+
36
+ describe("DiffStore", () => {
37
+ afterEach(async () => {
38
+ await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })))
39
+ })
40
+
41
+ test("returns current worktree diffs for modified files", async () => {
42
+ const repoRoot = await createRepo()
43
+ tempDirs.push(repoRoot)
44
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
45
+ await run(["git", "add", "."], repoRoot)
46
+ await run(["git", "commit", "-m", "init"], repoRoot)
47
+ await writeFile(path.join(repoRoot, "app.txt"), "changed\n", "utf8")
48
+
49
+ const store = new DiffStore(repoRoot)
50
+ await store.initialize()
51
+ await store.refreshSnapshot("chat-1", repoRoot)
52
+
53
+ const snapshot = store.getSnapshot("chat-1")
54
+ expect(snapshot.status).toBe("ready")
55
+ expect(snapshot.files).toHaveLength(1)
56
+ expect(snapshot.files[0]?.path).toBe("app.txt")
57
+ expect(snapshot.files[0]?.patch).toContain("-base")
58
+ expect(snapshot.files[0]?.patch).toContain("+changed")
59
+ })
60
+
61
+ test("returns no_repo outside a git repository", async () => {
62
+ const root = await mkdtemp(path.join(tmpdir(), "kanna-no-repo-"))
63
+ tempDirs.push(root)
64
+
65
+ const store = new DiffStore(root)
66
+ await store.initialize()
67
+ await store.refreshSnapshot("chat-1", root)
68
+
69
+ expect(store.getSnapshot("chat-1")).toEqual({
70
+ status: "no_repo",
71
+ branchName: undefined,
72
+ files: [],
73
+ })
74
+ })
75
+
76
+ test("commits only the selected files and refreshes the snapshot", async () => {
77
+ const repoRoot = await createRepo()
78
+ tempDirs.push(repoRoot)
79
+ await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
80
+ await writeFile(path.join(repoRoot, "notes.txt"), "keep\n", "utf8")
81
+ await run(["git", "add", "."], repoRoot)
82
+ await run(["git", "commit", "-m", "init"], repoRoot)
83
+
84
+ await writeFile(path.join(repoRoot, "app.txt"), "changed\n", "utf8")
85
+ await writeFile(path.join(repoRoot, "notes.txt"), "changed too\n", "utf8")
86
+
87
+ const store = new DiffStore(repoRoot)
88
+ await store.initialize()
89
+ await store.refreshSnapshot("chat-1", repoRoot)
90
+
91
+ await store.commitFiles({
92
+ chatId: "chat-1",
93
+ projectPath: repoRoot,
94
+ paths: ["app.txt"],
95
+ summary: "Update app",
96
+ description: "Only app changes",
97
+ })
98
+
99
+ const snapshot = store.getSnapshot("chat-1")
100
+ expect(snapshot.status).toBe("ready")
101
+ expect(snapshot.files).toHaveLength(1)
102
+ expect(snapshot.files[0]?.path).toBe("notes.txt")
103
+
104
+ const lastMessage = (await run(["git", "log", "-1", "--pretty=%B"], repoRoot)).trim()
105
+ expect(lastMessage).toBe("Update app\n\nOnly app changes")
106
+ })
107
+
108
+ test("detects renamed files", async () => {
109
+ const repoRoot = await createRepo()
110
+ tempDirs.push(repoRoot)
111
+ await writeFile(path.join(repoRoot, "before.txt"), "same\n", "utf8")
112
+ await run(["git", "add", "."], repoRoot)
113
+ await run(["git", "commit", "-m", "init"], repoRoot)
114
+ await run(["git", "mv", "before.txt", "after.txt"], repoRoot)
115
+
116
+ const store = new DiffStore(repoRoot)
117
+ await store.initialize()
118
+ await store.refreshSnapshot("chat-1", repoRoot)
119
+
120
+ const snapshot = store.getSnapshot("chat-1")
121
+ expect(snapshot.status).toBe("ready")
122
+ expect(snapshot.files).toHaveLength(1)
123
+ expect(snapshot.files[0]?.path).toBe("after.txt")
124
+ expect(snapshot.files[0]?.changeType).toBe("renamed")
125
+ })
126
+ })
@@ -0,0 +1,365 @@
1
+ import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"
2
+ import { tmpdir } from "node:os"
3
+ import path from "node:path"
4
+ import type { ChatDiffFile, ChatDiffSnapshot } from "../shared/types"
5
+ import { generateCommitMessageDetailed } from "./generate-commit-message"
6
+ import { inferProjectFileContentType } from "./uploads"
7
+
8
+ interface StoredChatDiffState {
9
+ status: ChatDiffSnapshot["status"]
10
+ branchName?: string
11
+ files: ChatDiffFile[]
12
+ }
13
+
14
+ function createEmptyState(): StoredChatDiffState {
15
+ return {
16
+ status: "unknown",
17
+ branchName: undefined,
18
+ files: [],
19
+ }
20
+ }
21
+
22
+ function snapshotsEqual(left: StoredChatDiffState | undefined, right: StoredChatDiffState) {
23
+ if (!left) {
24
+ return right.status === "unknown" && right.files.length === 0
25
+ }
26
+ if (left.status !== right.status) return false
27
+ if (left.branchName !== right.branchName) return false
28
+ if (left.files.length !== right.files.length) return false
29
+ return left.files.every((file, index) => {
30
+ const other = right.files[index]
31
+ return Boolean(other)
32
+ && file.path === other.path
33
+ && file.changeType === other.changeType
34
+ && file.patch === other.patch
35
+ })
36
+ }
37
+
38
+ interface DirtyPathEntry {
39
+ path: string
40
+ previousPath?: string
41
+ changeType: ChatDiffFile["changeType"]
42
+ }
43
+
44
+ async function fileExists(filePath: string) {
45
+ try {
46
+ await stat(filePath)
47
+ return true
48
+ } catch {
49
+ return false
50
+ }
51
+ }
52
+
53
+ async function runGit(args: string[], cwd: string) {
54
+ const process = Bun.spawn(["git", "-C", cwd, ...args], {
55
+ stdout: "pipe",
56
+ stderr: "pipe",
57
+ })
58
+ const [stdout, stderr, exitCode] = await Promise.all([
59
+ new Response(process.stdout).text(),
60
+ new Response(process.stderr).text(),
61
+ process.exited,
62
+ ])
63
+
64
+ return {
65
+ stdout,
66
+ stderr,
67
+ exitCode,
68
+ }
69
+ }
70
+
71
+ async function resolveRepo(projectPath: string): Promise<{ repoRoot: string; baseCommit: string | null } | null> {
72
+ const topLevel = await runGit(["rev-parse", "--show-toplevel"], projectPath)
73
+ if (topLevel.exitCode !== 0) {
74
+ return null
75
+ }
76
+
77
+ const repoRoot = topLevel.stdout.trim()
78
+ const head = await runGit(["rev-parse", "--verify", "HEAD"], repoRoot)
79
+ return {
80
+ repoRoot,
81
+ baseCommit: head.exitCode === 0 ? head.stdout.trim() : null,
82
+ }
83
+ }
84
+
85
+ async function getBranchName(repoRoot: string) {
86
+ const symbolicRef = await runGit(["symbolic-ref", "--quiet", "--short", "HEAD"], repoRoot)
87
+ if (symbolicRef.exitCode === 0) {
88
+ return symbolicRef.stdout.trim()
89
+ }
90
+
91
+ const revParse = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], repoRoot)
92
+ if (revParse.exitCode === 0) {
93
+ return revParse.stdout.trim()
94
+ }
95
+
96
+ return undefined
97
+ }
98
+
99
+ function parseStatusPaths(output: string): DirtyPathEntry[] {
100
+ const entries: DirtyPathEntry[] = []
101
+ for (const rawLine of output.split(/\r?\n/u)) {
102
+ const line = rawLine.trimEnd()
103
+ if (line.length < 4) continue
104
+ const statusCode = line.slice(0, 2)
105
+ const value = line.slice(3)
106
+ if (!value) continue
107
+ const isRename = statusCode.includes("R")
108
+ const isDelete = statusCode.includes("D")
109
+ const isAdd = statusCode.includes("A") || statusCode === "??"
110
+ const changeType: ChatDiffFile["changeType"] = isRename
111
+ ? "renamed"
112
+ : isDelete
113
+ ? "deleted"
114
+ : isAdd
115
+ ? "added"
116
+ : "modified"
117
+
118
+ if (isRename && value.includes(" -> ")) {
119
+ const [previousPath, nextPath] = value.split(" -> ")
120
+ if (nextPath) {
121
+ entries.push({
122
+ path: nextPath,
123
+ previousPath: previousPath || undefined,
124
+ changeType,
125
+ })
126
+ }
127
+ continue
128
+ }
129
+
130
+ entries.push({
131
+ path: value,
132
+ changeType,
133
+ })
134
+ }
135
+ return entries.sort((left, right) => left.path.localeCompare(right.path))
136
+ }
137
+
138
+ async function listDirtyPaths(repoRoot: string) {
139
+ const status = await runGit(["status", "--short", "--untracked-files=all"], repoRoot)
140
+ if (status.exitCode !== 0) {
141
+ throw new Error(status.stderr.trim() || "Failed to read git status")
142
+ }
143
+
144
+ const paths = parseStatusPaths(status.stdout)
145
+ return paths
146
+ }
147
+
148
+ async function readWorktreeFile(repoRoot: string, relativePath: string): Promise<string | null> {
149
+ const absolutePath = path.join(repoRoot, relativePath)
150
+ if (!(await fileExists(absolutePath))) {
151
+ return null
152
+ }
153
+
154
+ return await readFile(absolutePath, "utf8")
155
+ }
156
+
157
+ async function readBaseFile(repoRoot: string, baseCommit: string | null, relativePath: string): Promise<string | null> {
158
+ if (!baseCommit) {
159
+ return null
160
+ }
161
+
162
+ const result = await runGit(["show", `${baseCommit}:${relativePath}`], repoRoot)
163
+ if (result.exitCode !== 0) {
164
+ return null
165
+ }
166
+ return result.stdout
167
+ }
168
+
169
+ async function createPatch(beforePathLabel: string, afterPathLabel: string, beforeText: string | null, afterText: string | null) {
170
+ const tempDir = await mkdtemp(path.join(tmpdir(), "kanna-diff-"))
171
+ const beforePath = path.join(tempDir, "before")
172
+ const afterPath = path.join(tempDir, "after")
173
+
174
+ try {
175
+ await writeFile(beforePath, beforeText ?? "", "utf8")
176
+ await writeFile(afterPath, afterText ?? "", "utf8")
177
+
178
+ const result = await runGit(
179
+ [
180
+ "diff",
181
+ "--no-index",
182
+ "--no-ext-diff",
183
+ "--text",
184
+ "--unified=3",
185
+ "--src-prefix=a/",
186
+ "--dst-prefix=b/",
187
+ "before",
188
+ "after",
189
+ ],
190
+ tempDir
191
+ )
192
+
193
+ if (result.exitCode !== 0 && result.exitCode !== 1) {
194
+ throw new Error(result.stderr.trim() || `Failed to build patch for ${afterPathLabel}`)
195
+ }
196
+
197
+ return result.stdout
198
+ .replace("diff --git a/before b/after", `diff --git a/${beforePathLabel} b/${afterPathLabel}`)
199
+ .replace("--- a/before", `--- a/${beforePathLabel}`)
200
+ .replace("+++ b/after", `+++ b/${afterPathLabel}`)
201
+ } finally {
202
+ await rm(tempDir, { recursive: true, force: true })
203
+ }
204
+ }
205
+
206
+ async function computeCurrentFiles(repoRoot: string, baseCommit: string | null): Promise<ChatDiffFile[]> {
207
+ const currentDirtyPaths = await listDirtyPaths(repoRoot)
208
+ const files: ChatDiffFile[] = []
209
+
210
+ for (const entry of currentDirtyPaths) {
211
+ const relativePath = entry.path
212
+ const beforePath = entry.previousPath ?? relativePath
213
+ const beforeText = await readBaseFile(repoRoot, baseCommit, beforePath)
214
+ const afterText = await readWorktreeFile(repoRoot, relativePath)
215
+ const absolutePath = path.join(repoRoot, relativePath)
216
+ const fileInfo = await stat(absolutePath).catch(() => null)
217
+ const file = fileInfo?.isFile() ? Bun.file(absolutePath) : null
218
+ const mimeType = file ? inferProjectFileContentType(relativePath, file.type) : undefined
219
+ const size = fileInfo?.isFile() ? fileInfo.size : undefined
220
+
221
+ if (beforeText === afterText && entry.changeType !== "renamed") {
222
+ continue
223
+ }
224
+
225
+ const patch = await createPatch(beforePath, relativePath, beforeText, afterText)
226
+ files.push({
227
+ path: relativePath,
228
+ changeType: entry.changeType,
229
+ patch,
230
+ mimeType,
231
+ size,
232
+ })
233
+ }
234
+
235
+ return files
236
+ }
237
+
238
+ function normalizeRepoRelativePath(inputPath: string) {
239
+ const normalized = path.posix.normalize(inputPath.replaceAll("\\", "/")).replace(/^\.\/+/u, "")
240
+ if (!normalized || normalized === "." || normalized.startsWith("../") || normalized.includes("/../") || path.posix.isAbsolute(normalized)) {
241
+ throw new Error(`Invalid diff path: ${inputPath}`)
242
+ }
243
+ return normalized
244
+ }
245
+
246
+ export class DiffStore {
247
+ private readonly states = new Map<string, StoredChatDiffState>()
248
+
249
+ constructor(_: string) {}
250
+
251
+ async initialize() {}
252
+
253
+ getSnapshot(chatId: string): ChatDiffSnapshot {
254
+ const state = this.states.get(chatId) ?? createEmptyState()
255
+ return {
256
+ status: state.status,
257
+ branchName: state.branchName,
258
+ files: [...state.files],
259
+ }
260
+ }
261
+
262
+ async refreshSnapshot(chatId: string, projectPath: string) {
263
+ const repo = await resolveRepo(projectPath)
264
+ if (!repo) {
265
+ const nextState = {
266
+ status: "no_repo",
267
+ branchName: undefined,
268
+ files: [],
269
+ } satisfies StoredChatDiffState
270
+ const changed = !snapshotsEqual(this.states.get(chatId), nextState)
271
+ this.states.set(chatId, nextState)
272
+ return changed
273
+ }
274
+
275
+ const files = await computeCurrentFiles(repo.repoRoot, repo.baseCommit)
276
+ const branchName = await getBranchName(repo.repoRoot)
277
+ const nextState = {
278
+ status: "ready",
279
+ branchName,
280
+ files,
281
+ } satisfies StoredChatDiffState
282
+ const changed = !snapshotsEqual(this.states.get(chatId), nextState)
283
+ this.states.set(chatId, nextState)
284
+ return changed
285
+ }
286
+
287
+ async generateCommitMessage(args: {
288
+ projectPath: string
289
+ paths: string[]
290
+ }) {
291
+ const normalizedPaths = [...new Set(args.paths.map(normalizeRepoRelativePath))]
292
+ if (normalizedPaths.length === 0) {
293
+ throw new Error("Select at least one file")
294
+ }
295
+
296
+ const repo = await resolveRepo(args.projectPath)
297
+ if (!repo) {
298
+ throw new Error("Project is not in a git repository")
299
+ }
300
+
301
+ const files = await computeCurrentFiles(repo.repoRoot, repo.baseCommit)
302
+ const selectedFiles = normalizedPaths.map((selectedPath) => {
303
+ const file = files.find((candidate) => candidate.path === selectedPath)
304
+ if (!file) {
305
+ throw new Error(`File is no longer changed: ${selectedPath}`)
306
+ }
307
+ return file
308
+ })
309
+
310
+ const branchName = await getBranchName(repo.repoRoot)
311
+ return await generateCommitMessageDetailed({
312
+ cwd: repo.repoRoot,
313
+ branchName,
314
+ files: selectedFiles,
315
+ })
316
+ }
317
+
318
+ async commitFiles(args: {
319
+ chatId: string
320
+ projectPath: string
321
+ paths: string[]
322
+ summary: string
323
+ description?: string
324
+ }) {
325
+ const summary = args.summary.trim()
326
+ const description = args.description?.trim()
327
+ if (!summary) {
328
+ throw new Error("Commit summary is required")
329
+ }
330
+
331
+ const normalizedPaths = [...new Set(args.paths.map(normalizeRepoRelativePath))]
332
+ if (normalizedPaths.length === 0) {
333
+ throw new Error("Select at least one file to commit")
334
+ }
335
+
336
+ const repo = await resolveRepo(args.projectPath)
337
+ if (!repo) {
338
+ throw new Error("Project is not in a git repository")
339
+ }
340
+
341
+ const currentDirtyPaths = new Set((await listDirtyPaths(repo.repoRoot)).map((entry) => entry.path))
342
+ const missingPaths = normalizedPaths.filter((relativePath) => !currentDirtyPaths.has(relativePath))
343
+ if (missingPaths.length > 0) {
344
+ throw new Error(`File is no longer changed: ${missingPaths[0]}`)
345
+ }
346
+
347
+ const addResult = await runGit(["add", "--", ...normalizedPaths], repo.repoRoot)
348
+ if (addResult.exitCode !== 0) {
349
+ throw new Error(addResult.stderr.trim() || "Failed to stage selected files")
350
+ }
351
+
352
+ const commitArgs = ["commit", "--only", "-m", summary]
353
+ if (description) {
354
+ commitArgs.push("-m", description)
355
+ }
356
+ commitArgs.push("--", ...normalizedPaths)
357
+
358
+ const commitResult = await runGit(commitArgs, repo.repoRoot)
359
+ if (commitResult.exitCode !== 0) {
360
+ throw new Error(commitResult.stderr.trim() || commitResult.stdout.trim() || "Failed to commit selected files")
361
+ }
362
+
363
+ return await this.refreshSnapshot(args.chatId, args.projectPath)
364
+ }
365
+ }
@@ -0,0 +1,59 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { QuickResponseAdapter } from "./quick-response"
3
+ import { generateCommitMessageDetailed } from "./generate-commit-message"
4
+
5
+ describe("generateCommitMessageDetailed", () => {
6
+ test("returns sanitized generated subject and body", async () => {
7
+ const result = await generateCommitMessageDetailed(
8
+ {
9
+ cwd: "/tmp/project",
10
+ branchName: "feature/test",
11
+ files: [{
12
+ path: "app.ts",
13
+ changeType: "modified",
14
+ patch: "diff --git a/app.ts b/app.ts\n--- a/app.ts\n+++ b/app.ts\n@@\n-old\n+new\n",
15
+ }],
16
+ },
17
+ new QuickResponseAdapter({
18
+ runClaudeStructured: async () => ({
19
+ subject: " Add login UI.\nextra line",
20
+ body: " - wire form\n- add validation ",
21
+ }),
22
+ })
23
+ )
24
+
25
+ expect(result).toEqual({
26
+ subject: "Add login UI",
27
+ body: "- wire form\n- add validation",
28
+ usedFallback: false,
29
+ failureMessage: null,
30
+ })
31
+ })
32
+
33
+ test("falls back when providers fail", async () => {
34
+ const result = await generateCommitMessageDetailed(
35
+ {
36
+ cwd: "/tmp/project",
37
+ branchName: "feature/test",
38
+ files: [{
39
+ path: "src/feature.ts",
40
+ changeType: "modified",
41
+ patch: "diff --git a/src/feature.ts b/src/feature.ts\n",
42
+ }],
43
+ },
44
+ new QuickResponseAdapter({
45
+ runClaudeStructured: async () => {
46
+ throw new Error("Not authenticated")
47
+ },
48
+ runCodexStructured: async () => null,
49
+ })
50
+ )
51
+
52
+ expect(result).toEqual({
53
+ subject: "Update feature.ts",
54
+ body: "",
55
+ usedFallback: true,
56
+ failureMessage: "claude failed commit message generation: Not authenticated; codex returned no result for commit message generation",
57
+ })
58
+ })
59
+ })
@@ -0,0 +1,121 @@
1
+ import path from "node:path"
2
+ import type { ChatDiffFile } from "../shared/types"
3
+ import { QuickResponseAdapter } from "./quick-response"
4
+
5
+ const COMMIT_MESSAGE_SCHEMA = {
6
+ type: "object",
7
+ properties: {
8
+ subject: { type: "string" },
9
+ body: { type: "string" },
10
+ },
11
+ required: ["subject", "body"],
12
+ additionalProperties: false,
13
+ } as const
14
+
15
+ export interface GenerateCommitMessageResult {
16
+ subject: string
17
+ body: string
18
+ usedFallback: boolean
19
+ failureMessage: string | null
20
+ }
21
+
22
+ function summarizeFailures(failures: Array<{ provider: "claude" | "codex"; reason: string }>) {
23
+ if (failures.length === 0) return null
24
+ return failures.map((failure) => failure.reason).join("; ")
25
+ }
26
+
27
+ function limitText(value: string, maxLength: number) {
28
+ return value.length <= maxLength ? value : `${value.slice(0, maxLength).trimEnd()}\n...[truncated]`
29
+ }
30
+
31
+ function sanitizeSubject(value: unknown): string | null {
32
+ if (typeof value !== "string") return null
33
+ const normalized = (value.split(/\r?\n/u)[0] ?? "")
34
+ .replace(/\s+/g, " ")
35
+ .trim()
36
+ .replace(/[.]+$/u, "")
37
+ .slice(0, 72)
38
+ .trim()
39
+ return normalized.length > 0 ? normalized : null
40
+ }
41
+
42
+ function sanitizeBody(value: unknown): string {
43
+ if (typeof value !== "string") return ""
44
+ return value.trim()
45
+ }
46
+
47
+ function fallbackSubject(files: ChatDiffFile[]) {
48
+ if (files.length === 1) {
49
+ const fileName = path.posix.basename(files[0]?.path ?? "file")
50
+ const normalized = `Update ${fileName}`.replace(/\s+/g, " ").trim()
51
+ return normalized.slice(0, 72).trim()
52
+ }
53
+
54
+ return `Update ${files.length} files`
55
+ }
56
+
57
+ function buildCommitMessagePrompt(args: {
58
+ branchName?: string
59
+ files: ChatDiffFile[]
60
+ }) {
61
+ const fileList = args.files.map((file) => `${file.changeType}: ${file.path}`).join("\n")
62
+ const combinedPatch = args.files.map((file) => file.patch).join("\n\n")
63
+
64
+ return [
65
+ "Generate a concise git commit message for the selected changes.",
66
+ "Return JSON with keys: subject, body.",
67
+ "Rules:",
68
+ "- subject must be imperative, under 72 chars, and have no trailing period",
69
+ "- body may be an empty string or short bullet points",
70
+ "- capture the primary user-visible or developer-visible change",
71
+ "",
72
+ `Branch: ${args.branchName ?? "current branch"}`,
73
+ "",
74
+ "Selected files:",
75
+ limitText(fileList, 6_000),
76
+ "",
77
+ "Selected patch:",
78
+ limitText(combinedPatch, 40_000),
79
+ ].join("\n")
80
+ }
81
+
82
+ export async function generateCommitMessageDetailed(
83
+ args: {
84
+ cwd: string
85
+ branchName?: string
86
+ files: ChatDiffFile[]
87
+ },
88
+ adapter = new QuickResponseAdapter()
89
+ ): Promise<GenerateCommitMessageResult> {
90
+ const result = await adapter.generateStructuredWithDiagnostics<{ subject: string; body: string }>({
91
+ cwd: args.cwd,
92
+ task: "commit message generation",
93
+ prompt: buildCommitMessagePrompt(args),
94
+ schema: COMMIT_MESSAGE_SCHEMA,
95
+ parse: (value) => {
96
+ const output = value && typeof value === "object" ? value as { subject?: unknown; body?: unknown } : {}
97
+ const subject = sanitizeSubject(output.subject)
98
+ if (!subject) return null
99
+ return {
100
+ subject,
101
+ body: sanitizeBody(output.body),
102
+ }
103
+ },
104
+ })
105
+
106
+ if (result.value) {
107
+ return {
108
+ subject: result.value.subject,
109
+ body: result.value.body,
110
+ usedFallback: false,
111
+ failureMessage: null,
112
+ }
113
+ }
114
+
115
+ return {
116
+ subject: fallbackSubject(args.files),
117
+ body: "",
118
+ usedFallback: true,
119
+ failureMessage: summarizeFailures(result.failures),
120
+ }
121
+ }
@@ -54,7 +54,7 @@ describe("read models", () => {
54
54
  lastTurnOutcome: null,
55
55
  })
56
56
 
57
- const chat = deriveChatSnapshot(state, new Map(), new Set(), "chat-1", () => [])
57
+ const chat = deriveChatSnapshot(state, new Map(), new Set(), "chat-1", () => [], () => ({ status: "unknown", files: [] }))
58
58
  expect(chat?.runtime.provider).toBe("claude")
59
59
  expect(chat?.availableProviders.length).toBeGreaterThan(1)
60
60
  expect(chat?.availableProviders.find((provider) => provider.id === "codex")?.models.map((model) => model.id)).toEqual([
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ ChatDiffSnapshot,
2
3
  ChatRuntime,
3
4
  ChatSnapshot,
4
5
  KannaStatus,
@@ -100,7 +101,8 @@ export function deriveChatSnapshot(
100
101
  activeStatuses: Map<string, KannaStatus>,
101
102
  drainingChatIds: Set<string>,
102
103
  chatId: string,
103
- getMessages: (chatId: string) => ChatSnapshot["messages"]
104
+ getMessages: (chatId: string) => ChatSnapshot["messages"],
105
+ getDiffs: (chatId: string) => ChatDiffSnapshot
104
106
  ): ChatSnapshot | null {
105
107
  const chat = state.chatsById.get(chatId)
106
108
  if (!chat || chat.deletedAt) return null
@@ -122,6 +124,7 @@ export function deriveChatSnapshot(
122
124
  return {
123
125
  runtime,
124
126
  messages: getMessages(chat.id),
127
+ diffs: getDiffs(chat.id),
125
128
  availableProviders: [...SERVER_PROVIDERS],
126
129
  }
127
130
  }