@cuongtran001/kanna 0.39.2

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 (473) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +355 -0
  3. package/bin/kanna +9 -0
  4. package/dist/client/apple-touch-icon.png +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/bricolage-grotesque-latin-ext-wght-normal-CcLUaPy7.woff2 +0 -0
  33. package/dist/client/assets/bricolage-grotesque-latin-wght-normal-DLoelf7F.woff2 +0 -0
  34. package/dist/client/assets/bricolage-grotesque-vietnamese-wght-normal-BUzh504Q.woff2 +0 -0
  35. package/dist/client/assets/bsl-BO_Y6i37.js +1 -0
  36. package/dist/client/assets/c-BIGW1oBm.js +1 -0
  37. package/dist/client/assets/c3-eo99z4R2.js +1 -0
  38. package/dist/client/assets/cadence-Bv_4Rxtq.js +1 -0
  39. package/dist/client/assets/cairo-KRGpt6FW.js +1 -0
  40. package/dist/client/assets/catppuccin-frappe-DFWUc33u.js +1 -0
  41. package/dist/client/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
  42. package/dist/client/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
  43. package/dist/client/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
  44. package/dist/client/assets/clarity-D53aC0YG.js +1 -0
  45. package/dist/client/assets/clojure-P80f7IUj.js +1 -0
  46. package/dist/client/assets/cmake-D1j8_8rp.js +1 -0
  47. package/dist/client/assets/cobol-nwyudZeR.js +1 -0
  48. package/dist/client/assets/codeowners-Bp6g37R7.js +1 -0
  49. package/dist/client/assets/codeql-DsOJ9woJ.js +1 -0
  50. package/dist/client/assets/coffee-Ch7k5sss.js +1 -0
  51. package/dist/client/assets/common-lisp-Cg-RD9OK.js +1 -0
  52. package/dist/client/assets/coq-DkFqJrB1.js +1 -0
  53. package/dist/client/assets/cpp-CofmeUqb.js +1 -0
  54. package/dist/client/assets/crystal-tKQVLTB8.js +1 -0
  55. package/dist/client/assets/csharp-COcwbKMJ.js +1 -0
  56. package/dist/client/assets/css-DPfMkruS.js +1 -0
  57. package/dist/client/assets/csv-fuZLfV_i.js +1 -0
  58. package/dist/client/assets/cue-D82EKSYY.js +1 -0
  59. package/dist/client/assets/cypher-COkxafJQ.js +1 -0
  60. package/dist/client/assets/d-85-TOEBH.js +1 -0
  61. package/dist/client/assets/dark-plus-C3mMm8J8.js +1 -0
  62. package/dist/client/assets/dart-CF10PKvl.js +1 -0
  63. package/dist/client/assets/dax-CEL-wOlO.js +1 -0
  64. package/dist/client/assets/desktop-BmXAJ9_W.js +1 -0
  65. package/dist/client/assets/diff-D97Zzqfu.js +1 -0
  66. package/dist/client/assets/docker-BcOcwvcX.js +1 -0
  67. package/dist/client/assets/dotenv-Da5cRb03.js +1 -0
  68. package/dist/client/assets/dracula-BzJJZx-M.js +1 -0
  69. package/dist/client/assets/dracula-soft-BXkSAIEj.js +1 -0
  70. package/dist/client/assets/dream-maker-BtqSS_iP.js +1 -0
  71. package/dist/client/assets/edge-BkV0erSs.js +1 -0
  72. package/dist/client/assets/elixir-CDX3lj18.js +1 -0
  73. package/dist/client/assets/elm-DbKCFpqz.js +1 -0
  74. package/dist/client/assets/emacs-lisp-C9XAeP06.js +1 -0
  75. package/dist/client/assets/erb-B12qg9BL.js +1 -0
  76. package/dist/client/assets/erlang-DsQrWhSR.js +1 -0
  77. package/dist/client/assets/everforest-dark-BgDCqdQA.js +1 -0
  78. package/dist/client/assets/everforest-light-C8M2exoo.js +1 -0
  79. package/dist/client/assets/fennel-BYunw83y.js +1 -0
  80. package/dist/client/assets/fish-BvzEVeQv.js +1 -0
  81. package/dist/client/assets/fluent-C4IJs8-o.js +1 -0
  82. package/dist/client/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  83. package/dist/client/assets/fortran-free-form-BxgE0vQu.js +1 -0
  84. package/dist/client/assets/fsharp-CXgrBDvD.js +1 -0
  85. package/dist/client/assets/gdresource-BOOCDP_w.js +1 -0
  86. package/dist/client/assets/gdscript-C5YyOfLZ.js +1 -0
  87. package/dist/client/assets/gdshader-DkwncUOv.js +1 -0
  88. package/dist/client/assets/genie-D0YGMca9.js +1 -0
  89. package/dist/client/assets/gherkin-DyxjwDmM.js +1 -0
  90. package/dist/client/assets/git-commit-F4YmCXRG.js +1 -0
  91. package/dist/client/assets/git-rebase-r7XF79zn.js +1 -0
  92. package/dist/client/assets/github-dark-DHJKELXO.js +1 -0
  93. package/dist/client/assets/github-dark-default-Cuk6v7N8.js +1 -0
  94. package/dist/client/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  95. package/dist/client/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  96. package/dist/client/assets/github-light-DAi9KRSo.js +1 -0
  97. package/dist/client/assets/github-light-default-D7oLnXFd.js +1 -0
  98. package/dist/client/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  99. package/dist/client/assets/gleam-BspZqrRM.js +1 -0
  100. package/dist/client/assets/glimmer-js-Rg0-pVw9.js +1 -0
  101. package/dist/client/assets/glimmer-ts-U6CK756n.js +1 -0
  102. package/dist/client/assets/glsl-DplSGwfg.js +1 -0
  103. package/dist/client/assets/gn-n2N0HUVH.js +1 -0
  104. package/dist/client/assets/gnuplot-DdkO51Og.js +1 -0
  105. package/dist/client/assets/go-CxLEBnE3.js +1 -0
  106. package/dist/client/assets/graphql-ChdNCCLP.js +1 -0
  107. package/dist/client/assets/groovy-gcz8RCvz.js +1 -0
  108. package/dist/client/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  109. package/dist/client/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  110. package/dist/client/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  111. package/dist/client/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  112. package/dist/client/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  113. package/dist/client/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  114. package/dist/client/assets/hack-CaT9iCJl.js +1 -0
  115. package/dist/client/assets/haml-B8DHNrY2.js +1 -0
  116. package/dist/client/assets/handlebars-BL8al0AC.js +1 -0
  117. package/dist/client/assets/haskell-Df6bDoY_.js +1 -0
  118. package/dist/client/assets/haxe-CzTSHFRz.js +1 -0
  119. package/dist/client/assets/hcl-BWvSN4gD.js +1 -0
  120. package/dist/client/assets/hjson-D5-asLiD.js +1 -0
  121. package/dist/client/assets/hlsl-D3lLCCz7.js +1 -0
  122. package/dist/client/assets/horizon-BUw7H-hv.js +1 -0
  123. package/dist/client/assets/horizon-bright-Cn-bp-IR.js +1 -0
  124. package/dist/client/assets/houston-DnULxvSX.js +1 -0
  125. package/dist/client/assets/html-GMplVEZG.js +1 -0
  126. package/dist/client/assets/html-derivative-BFtXZ54Q.js +1 -0
  127. package/dist/client/assets/http-jrhK8wxY.js +1 -0
  128. package/dist/client/assets/hurl-irOxFIW8.js +1 -0
  129. package/dist/client/assets/hxml-Bvhsp5Yf.js +1 -0
  130. package/dist/client/assets/hy-DFXneXwc.js +1 -0
  131. package/dist/client/assets/imba-DGztddWO.js +1 -0
  132. package/dist/client/assets/index-Do7324M0.css +32 -0
  133. package/dist/client/assets/index-ktE9DLCD.js +2620 -0
  134. package/dist/client/assets/ini-BEwlwnbL.js +1 -0
  135. package/dist/client/assets/java-CylS5w8V.js +1 -0
  136. package/dist/client/assets/javascript-wDzz0qaB.js +1 -0
  137. package/dist/client/assets/jinja-4LBKfQ-Z.js +1 -0
  138. package/dist/client/assets/jison-wvAkD_A8.js +1 -0
  139. package/dist/client/assets/json-Cp-IABpG.js +1 -0
  140. package/dist/client/assets/json5-C9tS-k6U.js +1 -0
  141. package/dist/client/assets/jsonc-Des-eS-w.js +1 -0
  142. package/dist/client/assets/jsonl-DcaNXYhu.js +1 -0
  143. package/dist/client/assets/jsonnet-DFQXde-d.js +1 -0
  144. package/dist/client/assets/jssm-C2t-YnRu.js +1 -0
  145. package/dist/client/assets/jsx-g9-lgVsj.js +1 -0
  146. package/dist/client/assets/julia-CxzCAyBv.js +1 -0
  147. package/dist/client/assets/just-Cw27pwNe.js +1 -0
  148. package/dist/client/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  149. package/dist/client/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  150. package/dist/client/assets/kanagawa-wave-DWedfzmr.js +1 -0
  151. package/dist/client/assets/kdl-DV7GczEv.js +1 -0
  152. package/dist/client/assets/kotlin-BdnUsdx6.js +1 -0
  153. package/dist/client/assets/kusto-DZf3V79B.js +1 -0
  154. package/dist/client/assets/laserwave-DUszq2jm.js +1 -0
  155. package/dist/client/assets/latex-CWtU0Tv5.js +1 -0
  156. package/dist/client/assets/lean-BZvkOJ9d.js +1 -0
  157. package/dist/client/assets/less-B1dDrJ26.js +1 -0
  158. package/dist/client/assets/light-plus-B7mTdjB0.js +1 -0
  159. package/dist/client/assets/liquid-DYVedYrR.js +1 -0
  160. package/dist/client/assets/llvm-DjAJT7YJ.js +1 -0
  161. package/dist/client/assets/log-2UxHyX5q.js +1 -0
  162. package/dist/client/assets/logo-BtOb2qkB.js +1 -0
  163. package/dist/client/assets/lua-BaeVxFsk.js +1 -0
  164. package/dist/client/assets/luau-C-HG3fhB.js +1 -0
  165. package/dist/client/assets/make-CHLpvVh8.js +1 -0
  166. package/dist/client/assets/markdown-Cvjx9yec.js +1 -0
  167. package/dist/client/assets/marko-CnJfTvn9.js +1 -0
  168. package/dist/client/assets/material-theme-D5KoaKCx.js +1 -0
  169. package/dist/client/assets/material-theme-darker-BfHTSMKl.js +1 -0
  170. package/dist/client/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  171. package/dist/client/assets/material-theme-ocean-CyktbL80.js +1 -0
  172. package/dist/client/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  173. package/dist/client/assets/matlab-D7o27uSR.js +1 -0
  174. package/dist/client/assets/mdc-BMNejdWA.js +1 -0
  175. package/dist/client/assets/mdx-Cmh6b_Ma.js +1 -0
  176. package/dist/client/assets/mermaid-mWjccvbQ.js +1 -0
  177. package/dist/client/assets/min-dark-CafNBF8u.js +1 -0
  178. package/dist/client/assets/min-light-CTRr51gU.js +1 -0
  179. package/dist/client/assets/mipsasm-CKIfxQSi.js +1 -0
  180. package/dist/client/assets/mojo-rZm6bMo-.js +1 -0
  181. package/dist/client/assets/monokai-D4h5O-jR.js +1 -0
  182. package/dist/client/assets/moonbit-_H4v1dQx.js +1 -0
  183. package/dist/client/assets/move-IF9eRakj.js +1 -0
  184. package/dist/client/assets/narrat-DRg8JJMk.js +1 -0
  185. package/dist/client/assets/nextflow-Zz6hmt5N.js +1 -0
  186. package/dist/client/assets/nextflow-groovy-BeH2EWoN.js +1 -0
  187. package/dist/client/assets/nginx-BpAMiNFr.js +1 -0
  188. package/dist/client/assets/night-owl-C39BiMTA.js +1 -0
  189. package/dist/client/assets/night-owl-light-CMTm3GFP.js +1 -0
  190. package/dist/client/assets/nim-CVrawwO9.js +1 -0
  191. package/dist/client/assets/nix-CwoSXNpI.js +1 -0
  192. package/dist/client/assets/nord-Ddv68eIx.js +1 -0
  193. package/dist/client/assets/nushell-Cz2AlsmD.js +1 -0
  194. package/dist/client/assets/objective-c-DXmwc3jG.js +1 -0
  195. package/dist/client/assets/objective-cpp-CLxacb5B.js +1 -0
  196. package/dist/client/assets/ocaml-C0hk2d4L.js +1 -0
  197. package/dist/client/assets/odin-BBf5iR-q.js +1 -0
  198. package/dist/client/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  199. package/dist/client/assets/one-light-C3Wv6jpd.js +1 -0
  200. package/dist/client/assets/openscad-C4EeE6gA.js +1 -0
  201. package/dist/client/assets/pascal-D93ZcfNL.js +1 -0
  202. package/dist/client/assets/perl-C0TMdlhV.js +1 -0
  203. package/dist/client/assets/php-Dhbhpdrm.js +1 -0
  204. package/dist/client/assets/pierre-dark-DF2SEV7i.js +1 -0
  205. package/dist/client/assets/pierre-light-DOlZxES8.js +1 -0
  206. package/dist/client/assets/pkl-u5AG7uiY.js +1 -0
  207. package/dist/client/assets/plastic-3e1v2bzS.js +1 -0
  208. package/dist/client/assets/plsql-ChMvpjG-.js +1 -0
  209. package/dist/client/assets/po-BTJTHyun.js +1 -0
  210. package/dist/client/assets/poimandres-CS3Unz2-.js +1 -0
  211. package/dist/client/assets/polar-C0HS_06l.js +1 -0
  212. package/dist/client/assets/postcss-CXtECtnM.js +1 -0
  213. package/dist/client/assets/powerquery-CEu0bR-o.js +1 -0
  214. package/dist/client/assets/powershell-Dpen1YoG.js +1 -0
  215. package/dist/client/assets/prisma-Dd19v3D-.js +1 -0
  216. package/dist/client/assets/prolog-CbFg5uaA.js +1 -0
  217. package/dist/client/assets/proto-C7zT0LnQ.js +1 -0
  218. package/dist/client/assets/pug-CGlum2m_.js +1 -0
  219. package/dist/client/assets/puppet-BMWR74SV.js +1 -0
  220. package/dist/client/assets/purescript-CklMAg4u.js +1 -0
  221. package/dist/client/assets/python-B6aJPvgy.js +1 -0
  222. package/dist/client/assets/qml-3beO22l8.js +1 -0
  223. package/dist/client/assets/qmldir-C8lEn-DE.js +1 -0
  224. package/dist/client/assets/qss-IeuSbFQv.js +1 -0
  225. package/dist/client/assets/r-Dspwwk_N.js +1 -0
  226. package/dist/client/assets/racket-BqYA7rlc.js +1 -0
  227. package/dist/client/assets/raku-DXvB9xmW.js +1 -0
  228. package/dist/client/assets/razor-Uh8Bk_45.js +1 -0
  229. package/dist/client/assets/red-bN70gL4F.js +1 -0
  230. package/dist/client/assets/reg-C-SQnVFl.js +1 -0
  231. package/dist/client/assets/regexp-CDVJQ6XC.js +1 -0
  232. package/dist/client/assets/rel-C3B-1QV4.js +1 -0
  233. package/dist/client/assets/riscv-BM1_JUlF.js +1 -0
  234. package/dist/client/assets/ron-D8l8udqQ.js +1 -0
  235. package/dist/client/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
  236. package/dist/client/assets/rose-pine-moon-D4_iv3hh.js +1 -0
  237. package/dist/client/assets/rose-pine-qdsjHGoJ.js +1 -0
  238. package/dist/client/assets/rosmsg-BJDFO7_C.js +1 -0
  239. package/dist/client/assets/rst-BrH8l1NY.js +1 -0
  240. package/dist/client/assets/ruby-Dw2BHqvy.js +1 -0
  241. package/dist/client/assets/rust-B1yitclQ.js +1 -0
  242. package/dist/client/assets/sas-cz2c8ADy.js +1 -0
  243. package/dist/client/assets/sass-Cj5Yp3dK.js +1 -0
  244. package/dist/client/assets/scala-C151Ov-r.js +1 -0
  245. package/dist/client/assets/scheme-C98Dy4si.js +1 -0
  246. package/dist/client/assets/scss-OYdSNvt2.js +1 -0
  247. package/dist/client/assets/sdbl-DVxCFoDh.js +1 -0
  248. package/dist/client/assets/shaderlab-Dg9Lc6iA.js +1 -0
  249. package/dist/client/assets/shellscript-Yzrsuije.js +1 -0
  250. package/dist/client/assets/shellsession-BADoaaVG.js +1 -0
  251. package/dist/client/assets/slack-dark-BthQWCQV.js +1 -0
  252. package/dist/client/assets/slack-ochin-DqwNpetd.js +1 -0
  253. package/dist/client/assets/smalltalk-BERRCDM3.js +1 -0
  254. package/dist/client/assets/snazzy-light-Bw305WKR.js +1 -0
  255. package/dist/client/assets/solarized-dark-DXbdFlpD.js +1 -0
  256. package/dist/client/assets/solarized-light-L9t79GZl.js +1 -0
  257. package/dist/client/assets/solidity-rGO070M0.js +1 -0
  258. package/dist/client/assets/soy-Brmx7dQM.js +1 -0
  259. package/dist/client/assets/sparql-rVzFXLq3.js +1 -0
  260. package/dist/client/assets/splunk-BtCnVYZw.js +1 -0
  261. package/dist/client/assets/sql-BLtJtn59.js +1 -0
  262. package/dist/client/assets/ssh-config-_ykCGR6B.js +1 -0
  263. package/dist/client/assets/stata-BH5u7GGu.js +1 -0
  264. package/dist/client/assets/stylus-BEDo0Tqx.js +1 -0
  265. package/dist/client/assets/surrealql-Bq5Q-fJD.js +1 -0
  266. package/dist/client/assets/svelte-C_ipcX3V.js +1 -0
  267. package/dist/client/assets/swift-D82vCrfD.js +1 -0
  268. package/dist/client/assets/synthwave-84-CbfX1IO0.js +1 -0
  269. package/dist/client/assets/system-verilog-CnnmHF94.js +1 -0
  270. package/dist/client/assets/systemd-4A_iFExJ.js +1 -0
  271. package/dist/client/assets/talonscript-CkByrt1z.js +1 -0
  272. package/dist/client/assets/tasl-QIJgUcNo.js +1 -0
  273. package/dist/client/assets/tcl-dwOrl1Do.js +1 -0
  274. package/dist/client/assets/templ-P3uqSqPl.js +1 -0
  275. package/dist/client/assets/terraform-BETggiCN.js +1 -0
  276. package/dist/client/assets/tex-idrVyKtj.js +1 -0
  277. package/dist/client/assets/tokyo-night-hegEt444.js +1 -0
  278. package/dist/client/assets/toml-vGWfd6FD.js +1 -0
  279. package/dist/client/assets/ts-tags-zn1MmPIZ.js +1 -0
  280. package/dist/client/assets/tsv-B_m7g4N7.js +1 -0
  281. package/dist/client/assets/tsx-COt5Ahok.js +1 -0
  282. package/dist/client/assets/turtle-BsS91CYL.js +1 -0
  283. package/dist/client/assets/twig-DNn4PbVi.js +1 -0
  284. package/dist/client/assets/typescript-BPQ3VLAy.js +1 -0
  285. package/dist/client/assets/typespec-BGHnOYBU.js +1 -0
  286. package/dist/client/assets/typst-DHCkPAjA.js +1 -0
  287. package/dist/client/assets/v-BcVCzyr7.js +1 -0
  288. package/dist/client/assets/vala-CsfeWuGM.js +1 -0
  289. package/dist/client/assets/vb-D17OF-Vu.js +1 -0
  290. package/dist/client/assets/verilog-BQ8w6xss.js +1 -0
  291. package/dist/client/assets/vesper-DU1UobuO.js +1 -0
  292. package/dist/client/assets/vhdl-CeAyd5Ju.js +1 -0
  293. package/dist/client/assets/viml-CJc9bBzg.js +1 -0
  294. package/dist/client/assets/vitesse-black-Bkuqu6BP.js +1 -0
  295. package/dist/client/assets/vitesse-dark-D0r3Knsf.js +1 -0
  296. package/dist/client/assets/vitesse-light-CVO1_9PV.js +1 -0
  297. package/dist/client/assets/vue-DN_0RTcg.js +1 -0
  298. package/dist/client/assets/vue-html-AaS7Mt5G.js +1 -0
  299. package/dist/client/assets/vue-vine-CQOfvN7w.js +1 -0
  300. package/dist/client/assets/vyper-CDx5xZoG.js +1 -0
  301. package/dist/client/assets/wasm-CG6Dc4jp.js +1 -0
  302. package/dist/client/assets/wasm-MzD3tlZU.js +1 -0
  303. package/dist/client/assets/wenyan-BV7otONQ.js +1 -0
  304. package/dist/client/assets/wgsl-Dx-B1_4e.js +1 -0
  305. package/dist/client/assets/wikitext-BhOHFoWU.js +1 -0
  306. package/dist/client/assets/wit-5i3qLPDT.js +1 -0
  307. package/dist/client/assets/wolfram-lXgVvXCa.js +1 -0
  308. package/dist/client/assets/xml-sdJ4AIDG.js +1 -0
  309. package/dist/client/assets/xsl-CtQFsRM5.js +1 -0
  310. package/dist/client/assets/yaml-Buea-lGh.js +1 -0
  311. package/dist/client/assets/zenscript-DVFEvuxE.js +1 -0
  312. package/dist/client/assets/zig-VOosw3JB.js +1 -0
  313. package/dist/client/chat-sounds/Blow.mp3 +0 -0
  314. package/dist/client/chat-sounds/Bottle.mp3 +0 -0
  315. package/dist/client/chat-sounds/Frog.mp3 +0 -0
  316. package/dist/client/chat-sounds/Funk.mp3 +0 -0
  317. package/dist/client/chat-sounds/Glass.mp3 +0 -0
  318. package/dist/client/chat-sounds/Ping.mp3 +0 -0
  319. package/dist/client/chat-sounds/Pop.mp3 +0 -0
  320. package/dist/client/chat-sounds/Purr.mp3 +0 -0
  321. package/dist/client/chat-sounds/Tink.mp3 +0 -0
  322. package/dist/client/editor-icons/cursor.png +0 -0
  323. package/dist/client/editor-icons/custom.png +0 -0
  324. package/dist/client/editor-icons/default-app.png +0 -0
  325. package/dist/client/editor-icons/finder.png +0 -0
  326. package/dist/client/editor-icons/preview.png +0 -0
  327. package/dist/client/editor-icons/terminal.png +0 -0
  328. package/dist/client/editor-icons/windsurf.png +0 -0
  329. package/dist/client/editor-icons/xcode.png +0 -0
  330. package/dist/client/favicon.png +0 -0
  331. package/dist/client/fonts/body-medium.woff2 +0 -0
  332. package/dist/client/fonts/body-regular-italic.woff2 +0 -0
  333. package/dist/client/fonts/body-regular.woff2 +0 -0
  334. package/dist/client/fonts/body-semibold.woff2 +0 -0
  335. package/dist/client/icon-192.png +0 -0
  336. package/dist/client/icon-512.png +0 -0
  337. package/dist/client/icon-maskable-512.png +0 -0
  338. package/dist/client/icon.svg +4 -0
  339. package/dist/client/index.html +34 -0
  340. package/dist/client/manifest.webmanifest +46 -0
  341. package/dist/client/screenshot-light.png +0 -0
  342. package/dist/client/screenshot.png +0 -0
  343. package/dist/export-viewer/assets/bricolage-grotesque-latin-ext-wght-normal-CcLUaPy7.woff2 +0 -0
  344. package/dist/export-viewer/assets/bricolage-grotesque-latin-wght-normal-DLoelf7F.woff2 +0 -0
  345. package/dist/export-viewer/assets/bricolage-grotesque-vietnamese-wght-normal-BUzh504Q.woff2 +0 -0
  346. package/dist/export-viewer/assets/index-D1qUumZR.js +410 -0
  347. package/dist/export-viewer/assets/index-gG2nMW51.css +1 -0
  348. package/dist/export-viewer/editor-icons/cursor.png +0 -0
  349. package/dist/export-viewer/editor-icons/custom.png +0 -0
  350. package/dist/export-viewer/editor-icons/default-app.png +0 -0
  351. package/dist/export-viewer/editor-icons/finder.png +0 -0
  352. package/dist/export-viewer/editor-icons/preview.png +0 -0
  353. package/dist/export-viewer/editor-icons/terminal.png +0 -0
  354. package/dist/export-viewer/editor-icons/windsurf.png +0 -0
  355. package/dist/export-viewer/editor-icons/xcode.png +0 -0
  356. package/dist/export-viewer/fonts/body-medium.woff2 +0 -0
  357. package/dist/export-viewer/fonts/body-regular-italic.woff2 +0 -0
  358. package/dist/export-viewer/fonts/body-regular.woff2 +0 -0
  359. package/dist/export-viewer/fonts/body-semibold.woff2 +0 -0
  360. package/dist/export-viewer/index.html +14 -0
  361. package/package.json +99 -0
  362. package/src/server/__fixtures__/claude-session-empty.jsonl +0 -0
  363. package/src/server/__fixtures__/claude-session-malformed.jsonl +3 -0
  364. package/src/server/__fixtures__/claude-session-valid.jsonl +6 -0
  365. package/src/server/agent.test.ts +2369 -0
  366. package/src/server/agent.ts +1927 -0
  367. package/src/server/analytics.test.ts +313 -0
  368. package/src/server/analytics.ts +131 -0
  369. package/src/server/app-settings.test.ts +233 -0
  370. package/src/server/app-settings.ts +548 -0
  371. package/src/server/auth.test.ts +329 -0
  372. package/src/server/auth.ts +204 -0
  373. package/src/server/auto-continue/e2e.test.ts +215 -0
  374. package/src/server/auto-continue/events.test.ts +30 -0
  375. package/src/server/auto-continue/events.ts +35 -0
  376. package/src/server/auto-continue/limit-detector.test.ts +153 -0
  377. package/src/server/auto-continue/limit-detector.ts +159 -0
  378. package/src/server/auto-continue/read-model.test.ts +109 -0
  379. package/src/server/auto-continue/read-model.ts +83 -0
  380. package/src/server/auto-continue/schedule-manager.test.ts +155 -0
  381. package/src/server/auto-continue/schedule-manager.ts +116 -0
  382. package/src/server/claude-session-importer.test.ts +214 -0
  383. package/src/server/claude-session-importer.ts +187 -0
  384. package/src/server/claude-session-mapper.test.ts +88 -0
  385. package/src/server/claude-session-mapper.ts +106 -0
  386. package/src/server/claude-session-parser.test.ts +38 -0
  387. package/src/server/claude-session-parser.ts +67 -0
  388. package/src/server/claude-session-scanner.test.ts +49 -0
  389. package/src/server/claude-session-scanner.ts +24 -0
  390. package/src/server/claude-session-types.ts +61 -0
  391. package/src/server/cli-runtime.test.ts +523 -0
  392. package/src/server/cli-runtime.ts +405 -0
  393. package/src/server/cli-supervisor.ts +102 -0
  394. package/src/server/cli.ts +64 -0
  395. package/src/server/cloudflare-tunnel/agent-integration.test.ts +76 -0
  396. package/src/server/cloudflare-tunnel/agent-integration.ts +55 -0
  397. package/src/server/cloudflare-tunnel/detector.test.ts +72 -0
  398. package/src/server/cloudflare-tunnel/detector.ts +44 -0
  399. package/src/server/cloudflare-tunnel/e2e.test.ts +194 -0
  400. package/src/server/cloudflare-tunnel/events.test.ts +43 -0
  401. package/src/server/cloudflare-tunnel/events.ts +31 -0
  402. package/src/server/cloudflare-tunnel/gateway.ts +143 -0
  403. package/src/server/cloudflare-tunnel/lifecycle.test.ts +48 -0
  404. package/src/server/cloudflare-tunnel/lifecycle.ts +62 -0
  405. package/src/server/cloudflare-tunnel/read-model.test.ts +69 -0
  406. package/src/server/cloudflare-tunnel/read-model.ts +80 -0
  407. package/src/server/cloudflare-tunnel/tunnel-manager.test.ts +116 -0
  408. package/src/server/cloudflare-tunnel/tunnel-manager.ts +165 -0
  409. package/src/server/codex-app-server-protocol.ts +487 -0
  410. package/src/server/codex-app-server.test.ts +1816 -0
  411. package/src/server/codex-app-server.ts +1475 -0
  412. package/src/server/diff-store.test.ts +737 -0
  413. package/src/server/diff-store.ts +2199 -0
  414. package/src/server/discovery.test.ts +211 -0
  415. package/src/server/discovery.ts +301 -0
  416. package/src/server/event-store.test.ts +797 -0
  417. package/src/server/event-store.ts +1421 -0
  418. package/src/server/events.ts +217 -0
  419. package/src/server/external-open.test.ts +112 -0
  420. package/src/server/external-open.ts +345 -0
  421. package/src/server/generate-commit-message.test.ts +79 -0
  422. package/src/server/generate-commit-message.ts +126 -0
  423. package/src/server/generate-title.ts +76 -0
  424. package/src/server/harness-types.ts +19 -0
  425. package/src/server/keybindings.test.ts +144 -0
  426. package/src/server/keybindings.ts +178 -0
  427. package/src/server/llm-provider.test.ts +134 -0
  428. package/src/server/llm-provider.ts +207 -0
  429. package/src/server/machine-name.ts +22 -0
  430. package/src/server/paths-route.test.ts +64 -0
  431. package/src/server/paths.ts +35 -0
  432. package/src/server/process-utils.test.ts +12 -0
  433. package/src/server/process-utils.ts +47 -0
  434. package/src/server/project-paths.test.ts +95 -0
  435. package/src/server/project-paths.ts +191 -0
  436. package/src/server/provider-catalog.test.ts +69 -0
  437. package/src/server/provider-catalog.ts +87 -0
  438. package/src/server/quick-response.test.ts +440 -0
  439. package/src/server/quick-response.ts +300 -0
  440. package/src/server/read-models.test.ts +509 -0
  441. package/src/server/read-models.ts +230 -0
  442. package/src/server/restart.test.ts +27 -0
  443. package/src/server/restart.ts +30 -0
  444. package/src/server/server.ts +616 -0
  445. package/src/server/share.test.ts +180 -0
  446. package/src/server/share.ts +150 -0
  447. package/src/server/standalone-export.test.ts +224 -0
  448. package/src/server/standalone-export.ts +419 -0
  449. package/src/server/terminal-manager.test.ts +315 -0
  450. package/src/server/terminal-manager.ts +350 -0
  451. package/src/server/test-helpers/async-event-queue.ts +52 -0
  452. package/src/server/test-helpers/wait-for.ts +14 -0
  453. package/src/server/title-generation.live.test.ts +44 -0
  454. package/src/server/update-manager.test.ts +158 -0
  455. package/src/server/update-manager.ts +222 -0
  456. package/src/server/update-strategy.test.ts +237 -0
  457. package/src/server/update-strategy.ts +241 -0
  458. package/src/server/uploads.test.ts +292 -0
  459. package/src/server/uploads.ts +131 -0
  460. package/src/server/ws-router.test.ts +2292 -0
  461. package/src/server/ws-router.ts +1465 -0
  462. package/src/shared/analytics.ts +30 -0
  463. package/src/shared/branding.test.ts +31 -0
  464. package/src/shared/branding.ts +77 -0
  465. package/src/shared/dev-ports.test.ts +113 -0
  466. package/src/shared/dev-ports.ts +134 -0
  467. package/src/shared/ports.ts +2 -0
  468. package/src/shared/protocol.ts +257 -0
  469. package/src/shared/share.ts +27 -0
  470. package/src/shared/tools.test.ts +164 -0
  471. package/src/shared/tools.ts +327 -0
  472. package/src/shared/types.test.ts +25 -0
  473. package/src/shared/types.ts +1088 -0
@@ -0,0 +1,797 @@
1
+ import { afterEach, describe, expect, test } from "bun:test"
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
3
+ import { existsSync } from "node:fs"
4
+ import { join } from "node:path"
5
+ import { tmpdir } from "node:os"
6
+ import type { TranscriptEntry } from "../shared/types"
7
+ import type { SnapshotFile } from "./events"
8
+ import type { AutoContinueEvent } from "./auto-continue/events"
9
+ import { EventStore } from "./event-store"
10
+
11
+ const originalRuntimeProfile = process.env.KANNA_RUNTIME_PROFILE
12
+ const tempDirs: string[] = []
13
+
14
+ afterEach(async () => {
15
+ if (originalRuntimeProfile === undefined) {
16
+ delete process.env.KANNA_RUNTIME_PROFILE
17
+ } else {
18
+ process.env.KANNA_RUNTIME_PROFILE = originalRuntimeProfile
19
+ }
20
+
21
+ await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })))
22
+ })
23
+
24
+ async function createTempDataDir() {
25
+ const dir = await mkdtemp(join(tmpdir(), "kanna-event-store-"))
26
+ tempDirs.push(dir)
27
+ return dir
28
+ }
29
+
30
+ function entry(kind: "user_prompt" | "assistant_text", createdAt: number, extra: Record<string, unknown> = {}): TranscriptEntry {
31
+ const base = { _id: `${kind}-${createdAt}`, createdAt }
32
+ if (kind === "user_prompt") {
33
+ return { ...base, kind, content: String(extra.content ?? "") }
34
+ }
35
+ return { ...base, kind, text: String(extra.content ?? extra.text ?? "") }
36
+ }
37
+
38
+ describe("EventStore", () => {
39
+ test("uses the runtime profile for the default data dir", () => {
40
+ process.env.KANNA_RUNTIME_PROFILE = "dev"
41
+
42
+ const store = new EventStore()
43
+
44
+ expect(store.dataDir).toEndWith("/.kanna-dev/data")
45
+ })
46
+
47
+ test("migrates legacy snapshot and messages log transcripts into per-chat files", async () => {
48
+ const dataDir = await createTempDataDir()
49
+ const snapshotPath = join(dataDir, "snapshot.json")
50
+ const messagesLogPath = join(dataDir, "messages.jsonl")
51
+ const chatId = "chat-1"
52
+
53
+ const snapshot: SnapshotFile = {
54
+ v: 3,
55
+ generatedAt: 10,
56
+ projects: [{
57
+ id: "project-1",
58
+ localPath: "/tmp/project",
59
+ title: "Project",
60
+ createdAt: 1,
61
+ updatedAt: 5,
62
+ }],
63
+ chats: [{
64
+ id: chatId,
65
+ projectId: "project-1",
66
+ title: "Chat",
67
+ createdAt: 1,
68
+ updatedAt: 5,
69
+ unread: false,
70
+ provider: null,
71
+ planMode: false,
72
+ sessionToken: null,
73
+ sourceHash: null,
74
+ lastTurnOutcome: null,
75
+ }],
76
+ messages: [{
77
+ chatId,
78
+ entries: [
79
+ entry("user_prompt", 100, { content: "hello" }),
80
+ ],
81
+ }],
82
+ }
83
+
84
+ await writeFile(snapshotPath, JSON.stringify(snapshot, null, 2), "utf8")
85
+ await writeFile(messagesLogPath, `${JSON.stringify({
86
+ v: 3,
87
+ type: "message_appended",
88
+ timestamp: 101,
89
+ chatId,
90
+ entry: entry("assistant_text", 101, { content: "world" }),
91
+ })}\n`, "utf8")
92
+
93
+ const store = new EventStore(dataDir)
94
+ await store.initialize()
95
+
96
+ const progress: string[] = []
97
+ const migrated = await store.migrateLegacyTranscripts((message) => {
98
+ progress.push(message)
99
+ })
100
+
101
+ expect(migrated).toBe(true)
102
+ expect(progress.some((message) => message.includes("transcript migration detected"))).toBe(true)
103
+ expect(progress.at(-1)).toContain("transcript migration complete")
104
+ expect(store.getMessages(chatId)).toEqual([
105
+ entry("user_prompt", 100, { content: "hello" }),
106
+ entry("assistant_text", 101, { text: "world" }),
107
+ ])
108
+
109
+ const migratedSnapshot = JSON.parse(await readFile(snapshotPath, "utf8")) as SnapshotFile
110
+ expect(migratedSnapshot.messages).toBeUndefined()
111
+ expect(await readFile(messagesLogPath, "utf8")).toBe("")
112
+ expect(await readFile(join(dataDir, "transcripts", `${chatId}.jsonl`), "utf8")).toContain('"kind":"assistant_text"')
113
+ })
114
+
115
+ test("appends new transcript entries only to the per-chat transcript file", async () => {
116
+ const dataDir = await createTempDataDir()
117
+ const store = new EventStore(dataDir)
118
+ await store.initialize()
119
+
120
+ const project = await store.openProject("/tmp/project")
121
+ const chat = await store.createChat(project.id)
122
+ await store.appendMessage(chat.id, entry("user_prompt", 200, { content: "hello" }))
123
+ await store.appendMessage(chat.id, entry("assistant_text", 201, { content: "world" }))
124
+ await store.compact()
125
+
126
+ expect(store.getMessages(chat.id)).toEqual([
127
+ entry("user_prompt", 200, { content: "hello" }),
128
+ entry("assistant_text", 201, { text: "world" }),
129
+ ])
130
+ expect(await readFile(join(dataDir, "messages.jsonl"), "utf8")).toBe("")
131
+
132
+ const snapshot = JSON.parse(await readFile(join(dataDir, "snapshot.json"), "utf8")) as SnapshotFile
133
+ expect(snapshot.messages).toBeUndefined()
134
+ expect(existsSync(join(dataDir, "transcripts", `${chat.id}.jsonl`))).toBe(true)
135
+ })
136
+
137
+ test("pages recent transcript history and older entries by cursor", async () => {
138
+ const dataDir = await createTempDataDir()
139
+ const store = new EventStore(dataDir)
140
+ await store.initialize()
141
+
142
+ const project = await store.openProject("/tmp/project")
143
+ const chat = await store.createChat(project.id)
144
+
145
+ for (let index = 1; index <= 5; index += 1) {
146
+ await store.appendMessage(chat.id, entry(index % 2 === 0 ? "assistant_text" : "user_prompt", 200 + index, {
147
+ content: `message-${index}`,
148
+ }))
149
+ }
150
+
151
+ const recentPage = store.getRecentMessagesPage(chat.id, 2)
152
+ expect(recentPage.messages.map((message) => message._id)).toEqual(["assistant_text-204", "user_prompt-205"])
153
+ expect(recentPage.hasOlder).toBe(true)
154
+ expect(recentPage.olderCursor).not.toBeNull()
155
+
156
+ const olderPage = store.getMessagesPageBefore(chat.id, recentPage.olderCursor!, 2)
157
+ expect(olderPage.messages.map((message) => message._id)).toEqual(["assistant_text-202", "user_prompt-203"])
158
+ expect(olderPage.hasOlder).toBe(true)
159
+ expect(olderPage.olderCursor).not.toBeNull()
160
+
161
+ const oldestPage = store.getMessagesPageBefore(chat.id, olderPage.olderCursor!, 2)
162
+ expect(oldestPage.messages.map((message) => message._id)).toEqual(["user_prompt-201"])
163
+ expect(oldestPage.hasOlder).toBe(false)
164
+ expect(oldestPage.olderCursor).toBeNull()
165
+ })
166
+
167
+ test("persists queued messages across restart and removes promoted entries", async () => {
168
+ const dataDir = await createTempDataDir()
169
+ const store = new EventStore(dataDir)
170
+ await store.initialize()
171
+
172
+ const project = await store.openProject("/tmp/project")
173
+ const chat = await store.createChat(project.id)
174
+
175
+ const first = await store.enqueueMessage(chat.id, {
176
+ content: "first queued",
177
+ attachments: [],
178
+ provider: "codex",
179
+ model: "gpt-5.4",
180
+ planMode: false,
181
+ })
182
+ const second = await store.enqueueMessage(chat.id, {
183
+ content: "second queued",
184
+ attachments: [],
185
+ provider: "claude",
186
+ model: "claude-sonnet-4-6",
187
+ planMode: true,
188
+ })
189
+
190
+ expect(store.getQueuedMessages(chat.id).map((message) => message.content)).toEqual([
191
+ "first queued",
192
+ "second queued",
193
+ ])
194
+
195
+ const reloaded = new EventStore(dataDir)
196
+ await reloaded.initialize()
197
+ expect(reloaded.getQueuedMessages(chat.id).map((message) => message.content)).toEqual([
198
+ "first queued",
199
+ "second queued",
200
+ ])
201
+
202
+ await reloaded.removeQueuedMessage(chat.id, first.id)
203
+ expect(reloaded.getQueuedMessages(chat.id).map((message) => message.id)).toEqual([second.id])
204
+ })
205
+
206
+ test("marks chats unread on completed turns and clears unread when marked read", async () => {
207
+ const dataDir = await createTempDataDir()
208
+ const store = new EventStore(dataDir)
209
+ await store.initialize()
210
+
211
+ const project = await store.openProject("/tmp/project")
212
+ const chat = await store.createChat(project.id)
213
+
214
+ expect(store.getChat(chat.id)?.unread).toBe(false)
215
+
216
+ await store.recordTurnFinished(chat.id)
217
+ expect(store.getChat(chat.id)?.unread).toBe(true)
218
+
219
+ await store.setChatReadState(chat.id, false)
220
+ expect(store.getChat(chat.id)?.unread).toBe(false)
221
+
222
+ await store.recordTurnFailed(chat.id, "boom")
223
+ expect(store.getChat(chat.id)?.unread).toBe(true)
224
+
225
+ await store.recordTurnCancelled(chat.id)
226
+ expect(store.getChat(chat.id)?.unread).toBe(true)
227
+
228
+ await store.compact()
229
+
230
+ const reloaded = new EventStore(dataDir)
231
+ await reloaded.initialize()
232
+ expect(reloaded.getChat(chat.id)?.unread).toBe(true)
233
+ })
234
+
235
+ test("preserves read state after a finished turn across restart", async () => {
236
+ const dataDir = await createTempDataDir()
237
+ const store = new EventStore(dataDir)
238
+ await store.initialize()
239
+
240
+ const project = await store.openProject("/tmp/project")
241
+ const chat = await store.createChat(project.id)
242
+
243
+ await store.recordTurnFinished(chat.id)
244
+ await store.setChatReadState(chat.id, false)
245
+
246
+ expect(store.getChat(chat.id)?.unread).toBe(false)
247
+
248
+ const reloaded = new EventStore(dataDir)
249
+ await reloaded.initialize()
250
+
251
+ expect(reloaded.getChat(chat.id)?.unread).toBe(false)
252
+ })
253
+
254
+ test("preserves read state after a failed turn across restart", async () => {
255
+ const dataDir = await createTempDataDir()
256
+ const store = new EventStore(dataDir)
257
+ await store.initialize()
258
+
259
+ const project = await store.openProject("/tmp/project")
260
+ const chat = await store.createChat(project.id)
261
+
262
+ await store.recordTurnFailed(chat.id, "boom")
263
+ await store.setChatReadState(chat.id, false)
264
+
265
+ expect(store.getChat(chat.id)?.unread).toBe(false)
266
+
267
+ const reloaded = new EventStore(dataDir)
268
+ await reloaded.initialize()
269
+
270
+ expect(reloaded.getChat(chat.id)?.unread).toBe(false)
271
+ })
272
+
273
+ test("prefers mark-read over turn completion when replay timestamps tie", async () => {
274
+ const dataDir = await createTempDataDir()
275
+ const chatsLogPath = join(dataDir, "chats.jsonl")
276
+ const turnsLogPath = join(dataDir, "turns.jsonl")
277
+ const projectId = "project-1"
278
+ const chatId = "chat-1"
279
+ const timestamp = 100
280
+
281
+ await writeFile(chatsLogPath, [
282
+ JSON.stringify({
283
+ v: 3,
284
+ type: "chat_created",
285
+ timestamp,
286
+ chatId,
287
+ projectId,
288
+ title: "Chat",
289
+ }),
290
+ JSON.stringify({
291
+ v: 3,
292
+ type: "chat_read_state_set",
293
+ timestamp,
294
+ chatId,
295
+ unread: false,
296
+ }),
297
+ "",
298
+ ].join("\n"), "utf8")
299
+ await writeFile(turnsLogPath, [
300
+ JSON.stringify({
301
+ v: 3,
302
+ type: "turn_finished",
303
+ timestamp,
304
+ chatId,
305
+ }),
306
+ "",
307
+ ].join("\n"), "utf8")
308
+
309
+ const store = new EventStore(dataDir)
310
+ await store.initialize()
311
+
312
+ expect(store.getChat(chatId)?.unread).toBe(false)
313
+ })
314
+
315
+ test("loads chats without unread from older snapshots as read", async () => {
316
+ const dataDir = await createTempDataDir()
317
+ const snapshotPath = join(dataDir, "snapshot.json")
318
+
319
+ const snapshot: SnapshotFile = {
320
+ v: 3,
321
+ generatedAt: 10,
322
+ projects: [{
323
+ id: "project-1",
324
+ localPath: "/tmp/project",
325
+ title: "Project",
326
+ createdAt: 1,
327
+ updatedAt: 5,
328
+ }],
329
+ chats: [{
330
+ id: "chat-1",
331
+ projectId: "project-1",
332
+ title: "Chat",
333
+ createdAt: 1,
334
+ updatedAt: 5,
335
+ unread: false,
336
+ provider: null,
337
+ planMode: false,
338
+ sessionToken: null,
339
+ sourceHash: null,
340
+ lastTurnOutcome: null,
341
+ }],
342
+ }
343
+
344
+ await writeFile(snapshotPath, JSON.stringify(snapshot, null, 2), "utf8")
345
+
346
+ const store = new EventStore(dataDir)
347
+ await store.initialize()
348
+
349
+ expect(store.getChat("chat-1")?.unread).toBe(false)
350
+ })
351
+
352
+ test("persists sidebar project order across restart and compaction", async () => {
353
+ const dataDir = await createTempDataDir()
354
+ const store = new EventStore(dataDir)
355
+ await store.initialize()
356
+
357
+ const first = await store.openProject("/tmp/project-a")
358
+ const second = await store.openProject("/tmp/project-b")
359
+
360
+ await store.setSidebarProjectOrder([second.id, first.id])
361
+ expect(store.getSidebarProjectOrder()).toEqual([second.id, first.id])
362
+ expect(JSON.parse(await readFile(join(dataDir, "sidebar-order.json"), "utf8"))).toEqual([second.id, first.id])
363
+
364
+ await store.compact()
365
+
366
+ const snapshot = JSON.parse(await readFile(join(dataDir, "snapshot.json"), "utf8")) as SnapshotFile
367
+ expect(snapshot.sidebarProjectOrder).toBeUndefined()
368
+
369
+ const reloaded = new EventStore(dataDir)
370
+ await reloaded.initialize()
371
+ expect(reloaded.getSidebarProjectOrder()).toEqual([second.id, first.id])
372
+ })
373
+
374
+ test("migrates legacy sidebar project order from existing snapshots and project logs", async () => {
375
+ const dataDir = await createTempDataDir()
376
+ const snapshotPath = join(dataDir, "snapshot.json")
377
+ const projectsLogPath = join(dataDir, "projects.jsonl")
378
+
379
+ const snapshot = {
380
+ v: 3,
381
+ generatedAt: 10,
382
+ projects: [
383
+ {
384
+ id: "project-1",
385
+ localPath: "/tmp/project-a",
386
+ title: "Project A",
387
+ createdAt: 1,
388
+ updatedAt: 1,
389
+ },
390
+ {
391
+ id: "project-2",
392
+ localPath: "/tmp/project-b",
393
+ title: "Project B",
394
+ createdAt: 2,
395
+ updatedAt: 2,
396
+ },
397
+ ],
398
+ chats: [],
399
+ sidebarProjectOrder: ["project-1"],
400
+ }
401
+
402
+ await writeFile(snapshotPath, JSON.stringify(snapshot, null, 2), "utf8")
403
+ await writeFile(projectsLogPath, [
404
+ JSON.stringify({
405
+ v: 3,
406
+ type: "sidebar_project_order_set",
407
+ timestamp: 20,
408
+ projectIds: ["project-2", "project-1"],
409
+ }),
410
+ "",
411
+ ].join("\n"), "utf8")
412
+
413
+ const store = new EventStore(dataDir)
414
+ await store.initialize()
415
+
416
+ expect(store.getSidebarProjectOrder()).toEqual(["project-2", "project-1"])
417
+ expect(JSON.parse(await readFile(join(dataDir, "sidebar-order.json"), "utf8"))).toEqual(["project-2", "project-1"])
418
+ })
419
+
420
+ test("ignores an invalid sidebar order file without resetting store state", async () => {
421
+ const dataDir = await createTempDataDir()
422
+ await writeFile(join(dataDir, "sidebar-order.json"), "{not-json", "utf8")
423
+
424
+ const originalWarn = console.warn
425
+ console.warn = () => {}
426
+ try {
427
+ const store = new EventStore(dataDir)
428
+ await store.initialize()
429
+
430
+ const project = await store.openProject("/tmp/project")
431
+
432
+ const reloaded = new EventStore(dataDir)
433
+ await reloaded.initialize()
434
+
435
+ expect(reloaded.getProject(project.id)?.localPath).toBe("/tmp/project")
436
+ expect(reloaded.getSidebarProjectOrder()).toEqual([])
437
+ } finally {
438
+ console.warn = originalWarn
439
+ }
440
+ })
441
+
442
+ test("prunes stale empty chats after thirty minutes", async () => {
443
+ const dataDir = await createTempDataDir()
444
+ const store = new EventStore(dataDir)
445
+ await store.initialize()
446
+
447
+ const project = await store.openProject("/tmp/project")
448
+ const chat = await store.createChat(project.id)
449
+ const staleNow = chat.createdAt + 30 * 60 * 1000
450
+
451
+ const pruned = await store.pruneStaleEmptyChats({ now: staleNow })
452
+
453
+ expect(pruned).toEqual([chat.id])
454
+ expect(store.getChat(chat.id)).toBeNull()
455
+ })
456
+
457
+ test("does not prune recent empty chats", async () => {
458
+ const dataDir = await createTempDataDir()
459
+ const store = new EventStore(dataDir)
460
+ await store.initialize()
461
+
462
+ const project = await store.openProject("/tmp/project")
463
+ const chat = await store.createChat(project.id)
464
+ const pruned = await store.pruneStaleEmptyChats({ now: chat.createdAt + 30 * 60 * 1000 - 1 })
465
+
466
+ expect(pruned).toEqual([])
467
+ expect(store.getChat(chat.id)?.id).toBe(chat.id)
468
+ })
469
+
470
+ test("does not prune chats once they have transcript messages", async () => {
471
+ const dataDir = await createTempDataDir()
472
+ const store = new EventStore(dataDir)
473
+ await store.initialize()
474
+
475
+ const project = await store.openProject("/tmp/project")
476
+ const chat = await store.createChat(project.id)
477
+ await store.appendMessage(chat.id, entry("user_prompt", chat.createdAt + 1, { content: "hello" }))
478
+
479
+ const pruned = await store.pruneStaleEmptyChats({ now: chat.createdAt + 30 * 60 * 1000 })
480
+
481
+ expect(pruned).toEqual([])
482
+ expect(store.getChat(chat.id)?.id).toBe(chat.id)
483
+ })
484
+
485
+ test("does not prune stale chats that are currently active", async () => {
486
+ const dataDir = await createTempDataDir()
487
+ const store = new EventStore(dataDir)
488
+ await store.initialize()
489
+
490
+ const project = await store.openProject("/tmp/project")
491
+ const chat = await store.createChat(project.id)
492
+
493
+ const pruned = await store.pruneStaleEmptyChats({
494
+ now: chat.createdAt + 30 * 60 * 1000,
495
+ activeChatIds: [chat.id],
496
+ })
497
+
498
+ expect(pruned).toEqual([])
499
+ expect(store.getChat(chat.id)?.id).toBe(chat.id)
500
+ })
501
+
502
+ test("does not prune stale chats with protected draft state", async () => {
503
+ const dataDir = await createTempDataDir()
504
+ const store = new EventStore(dataDir)
505
+ await store.initialize()
506
+
507
+ const project = await store.openProject("/tmp/project")
508
+ const chat = await store.createChat(project.id)
509
+
510
+ const pruned = await store.pruneStaleEmptyChats({
511
+ now: chat.createdAt + 30 * 60 * 1000,
512
+ protectedChatIds: [chat.id],
513
+ })
514
+
515
+ expect(pruned).toEqual([])
516
+ expect(store.getChat(chat.id)?.id).toBe(chat.id)
517
+ })
518
+
519
+ test("forks a chat with copied transcript and pending fork session token", async () => {
520
+ const dataDir = await createTempDataDir()
521
+ const store = new EventStore(dataDir)
522
+ await store.initialize()
523
+
524
+ const project = await store.openProject("/tmp/project")
525
+ const source = await store.createChat(project.id)
526
+ await store.setChatProvider(source.id, "claude")
527
+ await store.setPlanMode(source.id, true)
528
+ await store.setSessionToken(source.id, "session-1")
529
+ await store.appendMessage(source.id, entry("user_prompt", source.createdAt + 1, { content: "analyze this" }))
530
+ await store.appendMessage(source.id, entry("assistant_text", source.createdAt + 2, { text: "done" }))
531
+
532
+ const forked = await store.forkChat(source.id)
533
+
534
+ expect(forked.id).not.toBe(source.id)
535
+ expect(forked.title).toBe("Fork: New Chat")
536
+ expect(forked.provider).toBe("claude")
537
+ expect(forked.planMode).toBe(true)
538
+ expect(forked.sessionToken).toBeNull()
539
+ expect(forked.pendingForkSessionToken).toBe("session-1")
540
+ expect(forked.lastTurnOutcome).toBeNull()
541
+ expect(forked.lastMessageAt).toBeUndefined()
542
+ expect(store.getMessages(forked.id)).toEqual(store.getMessages(source.id))
543
+ })
544
+
545
+ test("reopening a removed project restores its existing chats", async () => {
546
+ const dataDir = await createTempDataDir()
547
+ const store = new EventStore(dataDir)
548
+ await store.initialize()
549
+
550
+ const project = await store.openProject("/tmp/project")
551
+ const chat = await store.createChat(project.id)
552
+
553
+ await store.removeProject(project.id)
554
+ expect(store.getProject(project.id)).toBeNull()
555
+
556
+ const reopened = await store.openProject("/tmp/project")
557
+
558
+ expect(reopened.id).toBe(project.id)
559
+ expect(store.listChatsByProject(reopened.id).map((entry) => entry.id)).toEqual([chat.id])
560
+ })
561
+
562
+ test("archives chats without deleting their transcript", async () => {
563
+ const dataDir = await createTempDataDir()
564
+ const store = new EventStore(dataDir)
565
+ await store.initialize()
566
+
567
+ const project = await store.openProject("/tmp/project")
568
+ const chat = await store.createChat(project.id)
569
+ await store.appendMessage(chat.id, entry("user_prompt", chat.createdAt + 1, { content: "keep this" }))
570
+
571
+ await store.archiveChat(chat.id)
572
+
573
+ expect(store.getChat(chat.id)?.archivedAt).toBeNumber()
574
+ expect(store.listChatsByProject(project.id)).toEqual([])
575
+ expect(store.getMessages(chat.id).map((message) => message.kind)).toEqual(["user_prompt"])
576
+
577
+ await store.unarchiveChat(chat.id)
578
+
579
+ expect(store.getChat(chat.id)?.archivedAt).toBeUndefined()
580
+ expect(store.listChatsByProject(project.id).map((entry) => entry.id)).toEqual([chat.id])
581
+ })
582
+ })
583
+
584
+ describe("recordSessionCommandsLoaded", () => {
585
+ test("stores latest commands on chat record", async () => {
586
+ const dataDir = await createTempDataDir()
587
+ const store = new EventStore(dataDir)
588
+ await store.initialize()
589
+ const project = await store.openProject("/tmp/project")
590
+ const chat = await store.createChat(project.id)
591
+
592
+ await store.recordSessionCommandsLoaded(chat.id, [
593
+ { name: "review", description: "Review PR", argumentHint: "<pr>" },
594
+ ])
595
+
596
+ expect(store.getChat(chat.id)?.slashCommands).toEqual([
597
+ { name: "review", description: "Review PR", argumentHint: "<pr>" },
598
+ ])
599
+ })
600
+
601
+ test("replaces commands on subsequent load", async () => {
602
+ const dataDir = await createTempDataDir()
603
+ const store = new EventStore(dataDir)
604
+ await store.initialize()
605
+ const project = await store.openProject("/tmp/project")
606
+ const chat = await store.createChat(project.id)
607
+
608
+ await store.recordSessionCommandsLoaded(chat.id, [
609
+ { name: "a", description: "", argumentHint: "" },
610
+ ])
611
+ await store.recordSessionCommandsLoaded(chat.id, [
612
+ { name: "b", description: "", argumentHint: "" },
613
+ ])
614
+
615
+ expect(store.getChat(chat.id)?.slashCommands).toEqual([
616
+ { name: "b", description: "", argumentHint: "" },
617
+ ])
618
+ })
619
+
620
+ test("skips redundant writes when commands are unchanged", async () => {
621
+ const dataDir = await createTempDataDir()
622
+ const store = new EventStore(dataDir)
623
+ await store.initialize()
624
+ const project = await store.openProject("/tmp/project")
625
+ const chat = await store.createChat(project.id)
626
+ const turnsLogPath = join(dataDir, "turns.jsonl")
627
+
628
+ const commands = [{ name: "review", description: "Review", argumentHint: "<pr>" }]
629
+ await store.recordSessionCommandsLoaded(chat.id, commands)
630
+ const afterFirst = (await readFile(turnsLogPath, "utf8")).trim().split("\n").length
631
+
632
+ await store.recordSessionCommandsLoaded(chat.id, [...commands.map((c) => ({ ...c }))])
633
+ const afterSecond = (await readFile(turnsLogPath, "utf8")).trim().split("\n").length
634
+
635
+ expect(afterSecond).toBe(afterFirst)
636
+ })
637
+
638
+ test("compaction + reload preserves slashCommands on chat records", async () => {
639
+ const dataDir = await createTempDataDir()
640
+ const store = new EventStore(dataDir)
641
+ await store.initialize()
642
+ const project = await store.openProject("/tmp/project")
643
+ const chat = await store.createChat(project.id)
644
+
645
+ const commands = [
646
+ { name: "review", description: "Review PR", argumentHint: "<pr>" },
647
+ { name: "help", description: "Show help", argumentHint: "" },
648
+ ]
649
+ await store.recordSessionCommandsLoaded(chat.id, commands)
650
+ await store.compact()
651
+
652
+ const reloaded = new EventStore(dataDir)
653
+ await reloaded.initialize()
654
+
655
+ expect(reloaded.getChat(chat.id)?.slashCommands).toEqual(commands)
656
+ })
657
+ })
658
+
659
+ describe("EventStore auto-continue schedules", () => {
660
+ test("appends and replays AutoContinueEvent sequence", async () => {
661
+ const dataDir = await createTempDataDir()
662
+ const store = new EventStore(dataDir)
663
+ await store.initialize()
664
+ const project = await store.openProject("/tmp/p1")
665
+ const chat = await store.createChat(project.id)
666
+
667
+ const proposed: AutoContinueEvent = {
668
+ v: 3,
669
+ kind: "auto_continue_proposed",
670
+ timestamp: 1_000,
671
+ chatId: chat.id,
672
+ scheduleId: "s1",
673
+ detectedAt: 1_000,
674
+ resetAt: 2_000,
675
+ tz: "Asia/Saigon",
676
+
677
+ }
678
+ const accepted: AutoContinueEvent = {
679
+ v: 3,
680
+ kind: "auto_continue_accepted",
681
+ timestamp: 1_100,
682
+ chatId: chat.id,
683
+ scheduleId: "s1",
684
+ scheduledAt: 2_000,
685
+ tz: "Asia/Saigon",
686
+ source: "user",
687
+ resetAt: 2_000,
688
+ detectedAt: 1_000,
689
+ }
690
+ await store.appendAutoContinueEvent(proposed)
691
+ await store.appendAutoContinueEvent(accepted)
692
+
693
+ const rehydrated = new EventStore(dataDir)
694
+ await rehydrated.initialize()
695
+ const events = rehydrated.getAutoContinueEvents(chat.id)
696
+ expect(events).toHaveLength(2)
697
+ expect(events[0].kind).toBe("auto_continue_proposed")
698
+ expect(events[1].kind).toBe("auto_continue_accepted")
699
+ })
700
+
701
+ test("snapshot compaction retains auto-continue events", async () => {
702
+ const dataDir = await createTempDataDir()
703
+ const store = new EventStore(dataDir)
704
+ await store.initialize()
705
+ const project = await store.openProject("/tmp/p1")
706
+ const chat = await store.createChat(project.id)
707
+
708
+ await store.appendAutoContinueEvent({
709
+ v: 3,
710
+ kind: "auto_continue_proposed",
711
+ timestamp: 1_000,
712
+ chatId: chat.id,
713
+ scheduleId: "s1",
714
+ detectedAt: 1_000,
715
+ resetAt: 2_000,
716
+ tz: "Asia/Saigon",
717
+
718
+ })
719
+ await store.compact()
720
+
721
+ const rehydrated = new EventStore(dataDir)
722
+ await rehydrated.initialize()
723
+ expect(rehydrated.getAutoContinueEvents(chat.id)).toHaveLength(1)
724
+ })
725
+ })
726
+
727
+ describe("EventStore tunnel events", () => {
728
+ test("appends two tunnel events and retrieves them in order by chatId", async () => {
729
+ const dataDir = await createTempDataDir()
730
+ const store = new EventStore(dataDir)
731
+ await store.initialize()
732
+ const project = await store.openProject("/tmp/p-tunnel")
733
+ const chat = await store.createChat(project.id)
734
+
735
+ const proposed = {
736
+ v: 1 as const,
737
+ kind: "tunnel_proposed" as const,
738
+ timestamp: 1_000,
739
+ chatId: chat.id,
740
+ tunnelId: "t1",
741
+ port: 5173,
742
+ sourcePid: null,
743
+ }
744
+ const accepted = {
745
+ v: 1 as const,
746
+ kind: "tunnel_accepted" as const,
747
+ timestamp: 2_000,
748
+ chatId: chat.id,
749
+ tunnelId: "t1",
750
+ source: "user" as const,
751
+ }
752
+
753
+ await store.appendTunnelEvent(proposed)
754
+ await store.appendTunnelEvent(accepted)
755
+
756
+ const events = store.getTunnelEvents(chat.id)
757
+ expect(events).toHaveLength(2)
758
+ expect(events[0].kind).toBe("tunnel_proposed")
759
+ expect(events[1].kind).toBe("tunnel_accepted")
760
+ })
761
+
762
+ test("persists tunnel events across store restart", async () => {
763
+ const dataDir = await createTempDataDir()
764
+ const store = new EventStore(dataDir)
765
+ await store.initialize()
766
+ const project = await store.openProject("/tmp/p-tunnel2")
767
+ const chat = await store.createChat(project.id)
768
+
769
+ await store.appendTunnelEvent({
770
+ v: 1 as const,
771
+ kind: "tunnel_proposed" as const,
772
+ timestamp: 1_000,
773
+ chatId: chat.id,
774
+ tunnelId: "t2",
775
+ port: 3000,
776
+ sourcePid: 42,
777
+ })
778
+
779
+ const rehydrated = new EventStore(dataDir)
780
+ await rehydrated.initialize()
781
+ const events = rehydrated.getTunnelEvents(chat.id)
782
+ expect(events).toHaveLength(1)
783
+ if (events[0].kind === "tunnel_proposed") {
784
+ expect(events[0].port).toBe(3000)
785
+ expect(events[0].sourcePid).toBe(42)
786
+ } else {
787
+ throw new Error("expected tunnel_proposed")
788
+ }
789
+ })
790
+
791
+ test("returns empty array for unknown chatId", async () => {
792
+ const dataDir = await createTempDataDir()
793
+ const store = new EventStore(dataDir)
794
+ await store.initialize()
795
+ expect(store.getTunnelEvents("nonexistent")).toEqual([])
796
+ })
797
+ })