@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,1421 @@
1
+ import { appendFile, mkdir, rename, rm, writeFile } from "node:fs/promises"
2
+ import { existsSync, readFileSync as readFileSyncImmediate } from "node:fs"
3
+ import { homedir } from "node:os"
4
+ import path from "node:path"
5
+ import { getDataDir, LOG_PREFIX } from "../shared/branding"
6
+ import type { AgentProvider, ChatHistoryPage, ChatHistorySnapshot, QueuedChatMessage, SlashCommand, TranscriptEntry } from "../shared/types"
7
+ import { STORE_VERSION } from "../shared/types"
8
+ import type { AutoContinueEvent } from "./auto-continue/events"
9
+ import {
10
+ type ChatEvent,
11
+ type ProjectEvent,
12
+ type QueuedMessageEvent,
13
+ type SnapshotFile,
14
+ type StoreEvent,
15
+ type StoreState,
16
+ type TurnEvent,
17
+ cloneTranscriptEntries,
18
+ createEmptyState,
19
+ } from "./events"
20
+ import { resolveLocalPath } from "./paths"
21
+ import type { CloudflareTunnelEvent } from "./cloudflare-tunnel/events"
22
+
23
+ const COMPACTION_THRESHOLD_BYTES = 2 * 1024 * 1024
24
+ const STALE_EMPTY_CHAT_MAX_AGE_MS = 30 * 60 * 1000
25
+ const SIDEBAR_PROJECT_ORDER_FILE = "sidebar-order.json"
26
+
27
+ function normalizeSidebarProjectOrder(value: unknown) {
28
+ if (!Array.isArray(value)) {
29
+ return []
30
+ }
31
+
32
+ const seen = new Set<string>()
33
+ const projectIds: string[] = []
34
+ for (const entry of value) {
35
+ if (typeof entry !== "string") continue
36
+ const projectId = entry.trim()
37
+ if (!projectId || seen.has(projectId)) continue
38
+ seen.add(projectId)
39
+ projectIds.push(projectId)
40
+ }
41
+
42
+ return projectIds
43
+ }
44
+
45
+ function isSendToStartingProfilingEnabled() {
46
+ return process.env.KANNA_PROFILE_SEND_TO_STARTING === "1"
47
+ }
48
+
49
+ function logSendToStartingProfile(stage: string, details?: Record<string, unknown>) {
50
+ if (!isSendToStartingProfilingEnabled()) {
51
+ return
52
+ }
53
+
54
+ console.log("[kanna/send->starting][server]", JSON.stringify({
55
+ stage,
56
+ ...details,
57
+ }))
58
+ }
59
+
60
+ interface LegacyTranscriptStats {
61
+ hasLegacyData: boolean
62
+ sources: Array<"snapshot" | "messages_log">
63
+ chatCount: number
64
+ entryCount: number
65
+ }
66
+
67
+ interface TranscriptPageResult {
68
+ entries: TranscriptEntry[]
69
+ hasOlder: boolean
70
+ olderCursor: string | null
71
+ }
72
+
73
+ interface ParsedReplayEvent {
74
+ event: StoreEvent
75
+ sourceIndex: number
76
+ lineIndex: number
77
+ }
78
+
79
+ function getReplayEventPriority(event: StoreEvent) {
80
+ const discriminator = "type" in event ? event.type : event.kind
81
+ switch (discriminator) {
82
+ case "project_opened":
83
+ case "project_removed":
84
+ case "sidebar_project_order_set":
85
+ return 0
86
+ case "chat_created":
87
+ return 1
88
+ case "chat_renamed":
89
+ case "chat_provider_set":
90
+ case "chat_plan_mode_set":
91
+ return 2
92
+ case "message_appended":
93
+ return 3
94
+ case "queued_message_enqueued":
95
+ case "queued_message_removed":
96
+ return 4
97
+ case "turn_started":
98
+ return 5
99
+ case "session_token_set":
100
+ case "session_commands_loaded":
101
+ return 6
102
+ case "pending_fork_session_token_set":
103
+ return 6
104
+ case "turn_cancelled":
105
+ return 7
106
+ case "turn_finished":
107
+ case "turn_failed":
108
+ return 8
109
+ case "chat_read_state_set":
110
+ case "chat_source_hash_set":
111
+ return 9
112
+ case "chat_deleted":
113
+ case "chat_archived":
114
+ case "chat_unarchived":
115
+ return 10
116
+ case "auto_continue_proposed":
117
+ case "auto_continue_accepted":
118
+ case "auto_continue_rescheduled":
119
+ case "auto_continue_cancelled":
120
+ case "auto_continue_fired":
121
+ return 11
122
+ }
123
+ }
124
+
125
+ function encodeHistoryCursor(index: number) {
126
+ return `idx:${index}`
127
+ }
128
+
129
+ function decodeCursor(cursor: string) {
130
+ if (cursor.startsWith("idx:")) {
131
+ const value = Number.parseInt(cursor.slice("idx:".length), 10)
132
+ if (!Number.isInteger(value) || value < 0) {
133
+ throw new Error("Invalid history cursor")
134
+ }
135
+ return value
136
+ }
137
+
138
+ throw new Error("Invalid history cursor")
139
+ }
140
+
141
+ function slashCommandsEqual(a: SlashCommand[], b: SlashCommand[]) {
142
+ if (a.length !== b.length) return false
143
+ for (let i = 0; i < a.length; i += 1) {
144
+ const ai = a[i]
145
+ const bi = b[i]
146
+ if (ai.name !== bi.name || ai.description !== bi.description || ai.argumentHint !== bi.argumentHint) {
147
+ return false
148
+ }
149
+ }
150
+ return true
151
+ }
152
+
153
+ function getHistorySnapshot(page: TranscriptPageResult, recentLimit: number): ChatHistorySnapshot {
154
+ return {
155
+ hasOlder: page.hasOlder,
156
+ olderCursor: page.olderCursor,
157
+ recentLimit,
158
+ }
159
+ }
160
+
161
+ function getForkedChatTitle(title: string) {
162
+ const trimmed = title.trim()
163
+ if (!trimmed) return "Fork: New Chat"
164
+ return trimmed.startsWith("Fork: ") ? trimmed : `Fork: ${trimmed}`
165
+ }
166
+
167
+ export class EventStore {
168
+ readonly dataDir: string
169
+ readonly state: StoreState = createEmptyState()
170
+ private writeChain = Promise.resolve()
171
+ private storageReset = false
172
+ private readonly snapshotPath: string
173
+ private readonly projectsLogPath: string
174
+ private readonly chatsLogPath: string
175
+ private readonly messagesLogPath: string
176
+ private readonly queuedMessagesLogPath: string
177
+ private readonly turnsLogPath: string
178
+ private readonly schedulesLogPath: string
179
+ private readonly tunnelLogPath: string
180
+ private readonly transcriptsDir: string
181
+ private readonly sidebarProjectOrderPath: string
182
+ private legacyMessagesByChatId = new Map<string, TranscriptEntry[]>()
183
+ private legacySidebarProjectOrder: string[] = []
184
+ private sidebarProjectOrder: string[] = []
185
+ private snapshotHasLegacyMessages = false
186
+ private cachedTranscript: { chatId: string; entries: TranscriptEntry[] } | null = null
187
+ private readonly tunnelEventsByChatId = new Map<string, CloudflareTunnelEvent[]>()
188
+
189
+ constructor(dataDir = getDataDir(homedir())) {
190
+ this.dataDir = dataDir
191
+ this.snapshotPath = path.join(this.dataDir, "snapshot.json")
192
+ this.projectsLogPath = path.join(this.dataDir, "projects.jsonl")
193
+ this.chatsLogPath = path.join(this.dataDir, "chats.jsonl")
194
+ this.messagesLogPath = path.join(this.dataDir, "messages.jsonl")
195
+ this.queuedMessagesLogPath = path.join(this.dataDir, "queued-messages.jsonl")
196
+ this.turnsLogPath = path.join(this.dataDir, "turns.jsonl")
197
+ this.schedulesLogPath = path.join(this.dataDir, "schedules.jsonl")
198
+ this.tunnelLogPath = path.join(this.dataDir, "tunnels.jsonl")
199
+ this.transcriptsDir = path.join(this.dataDir, "transcripts")
200
+ this.sidebarProjectOrderPath = path.join(this.dataDir, SIDEBAR_PROJECT_ORDER_FILE)
201
+ }
202
+
203
+ async initialize() {
204
+ await mkdir(this.dataDir, { recursive: true })
205
+ await mkdir(this.transcriptsDir, { recursive: true })
206
+ await this.ensureFile(this.projectsLogPath)
207
+ await this.ensureFile(this.chatsLogPath)
208
+ await this.ensureFile(this.messagesLogPath)
209
+ await this.ensureFile(this.queuedMessagesLogPath)
210
+ await this.ensureFile(this.turnsLogPath)
211
+ await this.ensureFile(this.schedulesLogPath)
212
+ await this.ensureFile(this.tunnelLogPath)
213
+ await this.loadSnapshot()
214
+ await this.replayLogs()
215
+ await this.loadTunnelEvents()
216
+ await this.loadSidebarProjectOrder()
217
+ if (!(await this.hasLegacyTranscriptData()) && await this.shouldCompact()) {
218
+ await this.compact()
219
+ }
220
+ }
221
+
222
+ private async ensureFile(filePath: string) {
223
+ const file = Bun.file(filePath)
224
+ if (!(await file.exists())) {
225
+ await Bun.write(filePath, "")
226
+ }
227
+ }
228
+
229
+ private async clearStorage() {
230
+ if (this.storageReset) return
231
+ this.storageReset = true
232
+ this.resetState()
233
+ this.clearLegacyTranscriptState()
234
+ await Promise.all([
235
+ Bun.write(this.snapshotPath, ""),
236
+ Bun.write(this.projectsLogPath, ""),
237
+ Bun.write(this.chatsLogPath, ""),
238
+ Bun.write(this.messagesLogPath, ""),
239
+ Bun.write(this.queuedMessagesLogPath, ""),
240
+ Bun.write(this.turnsLogPath, ""),
241
+ Bun.write(this.schedulesLogPath, ""),
242
+ Bun.write(this.tunnelLogPath, ""),
243
+ ])
244
+ }
245
+
246
+ private async loadSnapshot() {
247
+ const file = Bun.file(this.snapshotPath)
248
+ if (!(await file.exists())) return
249
+
250
+ try {
251
+ const text = await file.text()
252
+ if (!text.trim()) return
253
+ const parsed = JSON.parse(text) as SnapshotFile
254
+ if (parsed.v !== STORE_VERSION) {
255
+ console.warn(`${LOG_PREFIX} Resetting local chat history for store version ${STORE_VERSION}`)
256
+ await this.clearStorage()
257
+ return
258
+ }
259
+ for (const project of parsed.projects) {
260
+ this.state.projectsById.set(project.id, { ...project })
261
+ this.state.projectIdsByPath.set(project.localPath, project.id)
262
+ }
263
+ for (const chat of parsed.chats) {
264
+ this.state.chatsById.set(chat.id, {
265
+ ...chat,
266
+ unread: chat.unread ?? false,
267
+ pendingForkSessionToken: chat.pendingForkSessionToken ?? null,
268
+ })
269
+ }
270
+ this.legacySidebarProjectOrder = normalizeSidebarProjectOrder(parsed.sidebarProjectOrder)
271
+ if (parsed.queuedMessages?.length) {
272
+ for (const queuedSet of parsed.queuedMessages) {
273
+ this.state.queuedMessagesByChatId.set(queuedSet.chatId, queuedSet.entries.map((entry) => ({
274
+ ...entry,
275
+ attachments: [...entry.attachments],
276
+ })))
277
+ }
278
+ }
279
+ if (parsed.messages?.length) {
280
+ this.snapshotHasLegacyMessages = true
281
+ for (const messageSet of parsed.messages) {
282
+ this.legacyMessagesByChatId.set(messageSet.chatId, cloneTranscriptEntries(messageSet.entries))
283
+ }
284
+ }
285
+ if (parsed.autoContinueEvents?.length) {
286
+ for (const entry of parsed.autoContinueEvents) {
287
+ this.state.autoContinueEventsByChatId.set(entry.chatId, [...entry.events])
288
+ }
289
+ }
290
+ } catch (error) {
291
+ console.warn(`${LOG_PREFIX} Failed to load snapshot, resetting local history:`, error)
292
+ await this.clearStorage()
293
+ }
294
+ }
295
+
296
+ private resetState() {
297
+ this.state.projectsById.clear()
298
+ this.state.projectIdsByPath.clear()
299
+ this.state.chatsById.clear()
300
+ this.state.queuedMessagesByChatId.clear()
301
+ this.state.sidebarProjectOrder = []
302
+ this.state.autoContinueEventsByChatId.clear()
303
+ this.tunnelEventsByChatId.clear()
304
+ this.sidebarProjectOrder = []
305
+ this.legacySidebarProjectOrder = []
306
+ this.cachedTranscript = null
307
+ }
308
+
309
+ private clearLegacyTranscriptState() {
310
+ this.legacyMessagesByChatId.clear()
311
+ this.snapshotHasLegacyMessages = false
312
+ }
313
+
314
+ private async loadSidebarProjectOrder() {
315
+ const file = Bun.file(this.sidebarProjectOrderPath)
316
+ if (await file.exists()) {
317
+ try {
318
+ const text = await file.text()
319
+ if (!text.trim()) {
320
+ this.sidebarProjectOrder = []
321
+ return
322
+ }
323
+ this.sidebarProjectOrder = normalizeSidebarProjectOrder(JSON.parse(text))
324
+ } catch (error) {
325
+ console.warn(`${LOG_PREFIX} Failed to load ${SIDEBAR_PROJECT_ORDER_FILE}, ignoring saved order:`, error)
326
+ this.sidebarProjectOrder = []
327
+ }
328
+ return
329
+ }
330
+
331
+ const legacySidebarProjectOrder = await this.loadLegacySidebarProjectOrder()
332
+ this.sidebarProjectOrder = legacySidebarProjectOrder
333
+ if (legacySidebarProjectOrder.length > 0) {
334
+ await this.writeSidebarProjectOrderFile(legacySidebarProjectOrder)
335
+ }
336
+ }
337
+
338
+ private async loadLegacySidebarProjectOrder() {
339
+ const fromProjectsLog = await this.readLegacySidebarProjectOrderFromProjectsLog()
340
+ if (fromProjectsLog.length > 0) {
341
+ return fromProjectsLog
342
+ }
343
+ return [...this.legacySidebarProjectOrder]
344
+ }
345
+
346
+ private async readLegacySidebarProjectOrderFromProjectsLog() {
347
+ const file = Bun.file(this.projectsLogPath)
348
+ if (!(await file.exists())) return []
349
+
350
+ const text = await file.text()
351
+ if (!text.trim()) return []
352
+
353
+ const lines = text.split("\n")
354
+ let lastNonEmpty = -1
355
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
356
+ if (lines[index].trim()) {
357
+ lastNonEmpty = index
358
+ break
359
+ }
360
+ }
361
+
362
+ let projectIds: string[] = []
363
+ for (let index = 0; index < lines.length; index += 1) {
364
+ const line = lines[index].trim()
365
+ if (!line) continue
366
+ try {
367
+ const event = JSON.parse(line) as {
368
+ v?: number
369
+ type?: string
370
+ projectIds?: unknown
371
+ }
372
+ if (event.v !== STORE_VERSION || event.type !== "sidebar_project_order_set") {
373
+ continue
374
+ }
375
+ projectIds = normalizeSidebarProjectOrder(event.projectIds)
376
+ } catch (error) {
377
+ if (index === lastNonEmpty) {
378
+ console.warn(`${LOG_PREFIX} Ignoring corrupt trailing line in ${path.basename(this.projectsLogPath)} while migrating sidebar order`)
379
+ return projectIds
380
+ }
381
+ console.warn(`${LOG_PREFIX} Failed to migrate sidebar order from ${path.basename(this.projectsLogPath)}:`, error)
382
+ return []
383
+ }
384
+ }
385
+
386
+ return projectIds
387
+ }
388
+
389
+ private async writeSidebarProjectOrderFile(projectIds: string[]) {
390
+ await mkdir(this.dataDir, { recursive: true })
391
+ await writeFile(this.sidebarProjectOrderPath, `${JSON.stringify(projectIds, null, 2)}\n`, "utf8")
392
+ }
393
+
394
+ private async replayLogs() {
395
+ if (this.storageReset) return
396
+ const replayEvents = [
397
+ ...await this.loadReplayEvents(this.projectsLogPath, 0),
398
+ ...await this.loadReplayEvents(this.chatsLogPath, 1),
399
+ ...await this.loadReplayEvents(this.messagesLogPath, 2),
400
+ ...await this.loadReplayEvents(this.queuedMessagesLogPath, 3),
401
+ ...await this.loadReplayEvents(this.turnsLogPath, 4),
402
+ ...await this.loadReplayEvents(this.schedulesLogPath, 5),
403
+ ]
404
+ if (this.storageReset) return
405
+
406
+ replayEvents
407
+ .sort((left, right) => (
408
+ left.event.timestamp - right.event.timestamp
409
+ || getReplayEventPriority(left.event) - getReplayEventPriority(right.event)
410
+ || left.sourceIndex - right.sourceIndex
411
+ || left.lineIndex - right.lineIndex
412
+ ))
413
+ .forEach(({ event }) => {
414
+ this.applyEvent(event)
415
+ })
416
+ }
417
+
418
+ private async loadReplayEvents(filePath: string, sourceIndex: number): Promise<ParsedReplayEvent[]> {
419
+ const file = Bun.file(filePath)
420
+ if (!(await file.exists())) return []
421
+ const text = await file.text()
422
+ if (!text.trim()) return []
423
+
424
+ const parsedEvents: ParsedReplayEvent[] = []
425
+ const lines = text.split("\n")
426
+ let lastNonEmpty = -1
427
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
428
+ if (lines[index].trim()) {
429
+ lastNonEmpty = index
430
+ break
431
+ }
432
+ }
433
+
434
+ for (let index = 0; index < lines.length; index += 1) {
435
+ const line = lines[index].trim()
436
+ if (!line) continue
437
+ try {
438
+ const event = JSON.parse(line) as Partial<StoreEvent>
439
+ if (event.v !== STORE_VERSION) {
440
+ console.warn(`${LOG_PREFIX} Resetting local history from incompatible event log`)
441
+ await this.clearStorage()
442
+ return []
443
+ }
444
+ if ((event as { type?: unknown }).type === "sidebar_project_order_set") {
445
+ continue
446
+ }
447
+ parsedEvents.push({
448
+ event: event as StoreEvent,
449
+ sourceIndex,
450
+ lineIndex: index,
451
+ })
452
+ } catch (error) {
453
+ if (index === lastNonEmpty) {
454
+ console.warn(`${LOG_PREFIX} Ignoring corrupt trailing line in ${path.basename(filePath)}`)
455
+ return parsedEvents
456
+ }
457
+ console.warn(`${LOG_PREFIX} Failed to replay ${path.basename(filePath)}, resetting local history:`, error)
458
+ await this.clearStorage()
459
+ return []
460
+ }
461
+ }
462
+
463
+ return parsedEvents
464
+ }
465
+
466
+ private applyEvent(event: StoreEvent) {
467
+ if ("kind" in event) {
468
+ this.applyAutoContinueEvent(event)
469
+ return
470
+ }
471
+ const e = event as Exclude<StoreEvent, AutoContinueEvent>
472
+ switch (e.type) {
473
+ case "project_opened": {
474
+ const localPath = resolveLocalPath(e.localPath)
475
+ const project = {
476
+ id: e.projectId,
477
+ localPath,
478
+ title: e.title,
479
+ createdAt: e.timestamp,
480
+ updatedAt: e.timestamp,
481
+ }
482
+ this.state.projectsById.set(project.id, project)
483
+ this.state.projectIdsByPath.set(localPath, project.id)
484
+ break
485
+ }
486
+ case "project_removed": {
487
+ const project = this.state.projectsById.get(e.projectId)
488
+ if (!project) break
489
+ project.deletedAt = e.timestamp
490
+ project.updatedAt = e.timestamp
491
+ this.state.projectIdsByPath.delete(project.localPath)
492
+ break
493
+ }
494
+ case "sidebar_project_order_set": {
495
+ this.state.sidebarProjectOrder = [...e.projectIds]
496
+ break
497
+ }
498
+ case "chat_created": {
499
+ const chat = {
500
+ id: e.chatId,
501
+ projectId: e.projectId,
502
+ title: e.title,
503
+ createdAt: e.timestamp,
504
+ updatedAt: e.timestamp,
505
+ unread: false,
506
+ provider: null,
507
+ planMode: false,
508
+ sessionToken: null,
509
+ sourceHash: null,
510
+ pendingForkSessionToken: null,
511
+ hasMessages: false,
512
+ lastTurnOutcome: null,
513
+ }
514
+ this.state.chatsById.set(chat.id, chat)
515
+ break
516
+ }
517
+ case "chat_renamed": {
518
+ const chat = this.state.chatsById.get(e.chatId)
519
+ if (!chat) break
520
+ chat.title = e.title
521
+ chat.updatedAt = e.timestamp
522
+ break
523
+ }
524
+ case "chat_deleted": {
525
+ const chat = this.state.chatsById.get(e.chatId)
526
+ if (!chat) break
527
+ chat.deletedAt = e.timestamp
528
+ chat.updatedAt = e.timestamp
529
+ this.state.queuedMessagesByChatId.delete(e.chatId)
530
+ this.state.autoContinueEventsByChatId.delete(e.chatId)
531
+ break
532
+ }
533
+ case "chat_archived": {
534
+ const chat = this.state.chatsById.get(e.chatId)
535
+ if (!chat) break
536
+ chat.archivedAt = e.timestamp
537
+ chat.updatedAt = e.timestamp
538
+ break
539
+ }
540
+ case "chat_unarchived": {
541
+ const chat = this.state.chatsById.get(e.chatId)
542
+ if (!chat) break
543
+ delete chat.archivedAt
544
+ chat.updatedAt = e.timestamp
545
+ break
546
+ }
547
+ case "chat_provider_set": {
548
+ const chat = this.state.chatsById.get(e.chatId)
549
+ if (!chat) break
550
+ chat.provider = e.provider
551
+ chat.updatedAt = e.timestamp
552
+ break
553
+ }
554
+ case "chat_plan_mode_set": {
555
+ const chat = this.state.chatsById.get(e.chatId)
556
+ if (!chat) break
557
+ chat.planMode = e.planMode
558
+ chat.updatedAt = e.timestamp
559
+ break
560
+ }
561
+ case "chat_read_state_set": {
562
+ const chat = this.state.chatsById.get(e.chatId)
563
+ if (!chat) break
564
+ chat.unread = e.unread
565
+ chat.updatedAt = e.timestamp
566
+ break
567
+ }
568
+ case "chat_source_hash_set": {
569
+ const chat = this.state.chatsById.get(e.chatId)
570
+ if (!chat) break
571
+ chat.sourceHash = e.sourceHash
572
+ chat.updatedAt = e.timestamp
573
+ break
574
+ }
575
+ case "message_appended": {
576
+ this.applyMessageMetadata(e.chatId, e.entry)
577
+ const existing = this.legacyMessagesByChatId.get(e.chatId) ?? []
578
+ existing.push({ ...e.entry })
579
+ this.legacyMessagesByChatId.set(e.chatId, existing)
580
+ break
581
+ }
582
+ case "queued_message_enqueued": {
583
+ const existing = this.state.queuedMessagesByChatId.get(e.chatId) ?? []
584
+ existing.push({
585
+ ...e.message,
586
+ attachments: [...e.message.attachments],
587
+ })
588
+ this.state.queuedMessagesByChatId.set(e.chatId, existing)
589
+ const chat = this.state.chatsById.get(e.chatId)
590
+ if (chat) {
591
+ chat.updatedAt = e.timestamp
592
+ }
593
+ break
594
+ }
595
+ case "queued_message_removed": {
596
+ const existing = this.state.queuedMessagesByChatId.get(e.chatId) ?? []
597
+ const next = existing.filter((entry) => entry.id !== e.queuedMessageId)
598
+ if (next.length > 0) {
599
+ this.state.queuedMessagesByChatId.set(e.chatId, next)
600
+ } else {
601
+ this.state.queuedMessagesByChatId.delete(e.chatId)
602
+ }
603
+ const chat = this.state.chatsById.get(e.chatId)
604
+ if (chat) {
605
+ chat.updatedAt = e.timestamp
606
+ }
607
+ break
608
+ }
609
+ case "turn_started": {
610
+ const chat = this.state.chatsById.get(e.chatId)
611
+ if (!chat) break
612
+ chat.updatedAt = e.timestamp
613
+ break
614
+ }
615
+ case "turn_finished": {
616
+ const chat = this.state.chatsById.get(e.chatId)
617
+ if (!chat) break
618
+ chat.updatedAt = e.timestamp
619
+ chat.unread = true
620
+ chat.lastTurnOutcome = "success"
621
+ break
622
+ }
623
+ case "turn_failed": {
624
+ const chat = this.state.chatsById.get(e.chatId)
625
+ if (!chat) break
626
+ chat.updatedAt = e.timestamp
627
+ chat.unread = true
628
+ chat.lastTurnOutcome = "failed"
629
+ break
630
+ }
631
+ case "turn_cancelled": {
632
+ const chat = this.state.chatsById.get(e.chatId)
633
+ if (!chat) break
634
+ chat.updatedAt = e.timestamp
635
+ chat.lastTurnOutcome = "cancelled"
636
+ break
637
+ }
638
+ case "session_token_set": {
639
+ const chat = this.state.chatsById.get(e.chatId)
640
+ if (!chat) break
641
+ chat.sessionToken = e.sessionToken
642
+ chat.updatedAt = e.timestamp
643
+ break
644
+ }
645
+ case "session_commands_loaded": {
646
+ const chat = this.state.chatsById.get(e.chatId)
647
+ if (!chat) break
648
+ chat.slashCommands = e.commands.map((c) => ({ ...c }))
649
+ chat.updatedAt = e.timestamp
650
+ break
651
+ }
652
+ case "pending_fork_session_token_set": {
653
+ const chat = this.state.chatsById.get(e.chatId)
654
+ if (!chat) break
655
+ chat.pendingForkSessionToken = e.pendingForkSessionToken
656
+ chat.updatedAt = e.timestamp
657
+ break
658
+ }
659
+ }
660
+ }
661
+
662
+ private applyAutoContinueEvent(event: AutoContinueEvent) {
663
+ const existing = this.state.autoContinueEventsByChatId.get(event.chatId) ?? []
664
+ existing.push(event)
665
+ this.state.autoContinueEventsByChatId.set(event.chatId, existing)
666
+ }
667
+
668
+ private applyMessageMetadata(chatId: string, entry: TranscriptEntry) {
669
+ const chat = this.state.chatsById.get(chatId)
670
+ if (!chat) return
671
+ chat.hasMessages = true
672
+ if (entry.kind === "user_prompt") {
673
+ chat.lastMessageAt = entry.createdAt
674
+ }
675
+ chat.updatedAt = Math.max(chat.updatedAt, entry.createdAt)
676
+ }
677
+
678
+ private append<TEvent extends StoreEvent>(filePath: string, event: TEvent) {
679
+ const payload = `${JSON.stringify(event)}\n`
680
+ this.writeChain = this.writeChain.then(async () => {
681
+ await appendFile(filePath, payload, "utf8")
682
+ this.applyEvent(event)
683
+ })
684
+ return this.writeChain
685
+ }
686
+
687
+ private transcriptPath(chatId: string) {
688
+ return path.join(this.transcriptsDir, `${chatId}.jsonl`)
689
+ }
690
+
691
+ private loadTranscriptFromDisk(chatId: string) {
692
+ const transcriptPath = this.transcriptPath(chatId)
693
+ if (!existsSync(transcriptPath)) {
694
+ return []
695
+ }
696
+
697
+ const text = readFileSyncImmediate(transcriptPath, "utf8")
698
+ if (!text.trim()) return []
699
+
700
+ const entries: TranscriptEntry[] = []
701
+ for (const rawLine of text.split("\n")) {
702
+ const line = rawLine.trim()
703
+ if (!line) continue
704
+ entries.push(JSON.parse(line) as TranscriptEntry)
705
+ }
706
+ return entries
707
+ }
708
+
709
+ async openProject(localPath: string, title?: string) {
710
+ const normalized = resolveLocalPath(localPath)
711
+ const existingId = this.state.projectIdsByPath.get(normalized)
712
+ if (existingId) {
713
+ const existing = this.state.projectsById.get(existingId)
714
+ if (existing && !existing.deletedAt) {
715
+ return existing
716
+ }
717
+ }
718
+
719
+ const hiddenProject = [...this.state.projectsById.values()]
720
+ .find((project) => project.localPath === normalized && project.deletedAt)
721
+ const projectId = hiddenProject?.id ?? crypto.randomUUID()
722
+ const event: ProjectEvent = {
723
+ v: STORE_VERSION,
724
+ type: "project_opened",
725
+ timestamp: Date.now(),
726
+ projectId,
727
+ localPath: normalized,
728
+ title: title?.trim() || path.basename(normalized) || normalized,
729
+ }
730
+ await this.append(this.projectsLogPath, event)
731
+ return this.state.projectsById.get(projectId)!
732
+ }
733
+
734
+ async removeProject(projectId: string) {
735
+ const project = this.getProject(projectId)
736
+ if (!project) {
737
+ throw new Error("Project not found")
738
+ }
739
+
740
+ const event: ProjectEvent = {
741
+ v: STORE_VERSION,
742
+ type: "project_removed",
743
+ timestamp: Date.now(),
744
+ projectId,
745
+ }
746
+ await this.append(this.projectsLogPath, event)
747
+ }
748
+
749
+ async setSidebarProjectOrder(projectIds: string[]) {
750
+ const validProjectIds = projectIds.filter((projectId) => {
751
+ const project = this.state.projectsById.get(projectId)
752
+ return Boolean(project && !project.deletedAt)
753
+ })
754
+
755
+ const uniqueProjectIds = [...new Set(validProjectIds)]
756
+ const current = this.sidebarProjectOrder
757
+ if (
758
+ uniqueProjectIds.length === current.length
759
+ && uniqueProjectIds.every((projectId, index) => current[index] === projectId)
760
+ ) {
761
+ return
762
+ }
763
+
764
+ this.writeChain = this.writeChain.then(async () => {
765
+ await this.writeSidebarProjectOrderFile(uniqueProjectIds)
766
+ this.sidebarProjectOrder = [...uniqueProjectIds]
767
+ })
768
+ return this.writeChain
769
+ }
770
+
771
+ async createChat(projectId: string) {
772
+ const project = this.state.projectsById.get(projectId)
773
+ if (!project || project.deletedAt) {
774
+ throw new Error("Project not found")
775
+ }
776
+ const chatId = crypto.randomUUID()
777
+ const event: ChatEvent = {
778
+ v: STORE_VERSION,
779
+ type: "chat_created",
780
+ timestamp: Date.now(),
781
+ chatId,
782
+ projectId,
783
+ title: "New Chat",
784
+ }
785
+ await this.append(this.chatsLogPath, event)
786
+ return this.state.chatsById.get(chatId)!
787
+ }
788
+
789
+ async forkChat(sourceChatId: string) {
790
+ const sourceChat = this.requireChat(sourceChatId)
791
+ const sourceSessionToken = sourceChat.sessionToken ?? sourceChat.pendingForkSessionToken ?? null
792
+ if (!sourceChat.provider || !sourceSessionToken) {
793
+ throw new Error("Chat cannot be forked")
794
+ }
795
+
796
+ const chatId = crypto.randomUUID()
797
+ const createdAt = Date.now()
798
+ const createEvent: ChatEvent = {
799
+ v: STORE_VERSION,
800
+ type: "chat_created",
801
+ timestamp: createdAt,
802
+ chatId,
803
+ projectId: sourceChat.projectId,
804
+ title: getForkedChatTitle(sourceChat.title),
805
+ }
806
+ await this.append(this.chatsLogPath, createEvent)
807
+ await this.setChatProvider(chatId, sourceChat.provider)
808
+ await this.setPlanMode(chatId, sourceChat.planMode)
809
+ await this.setPendingForkSessionToken(chatId, sourceSessionToken)
810
+
811
+ const sourceEntries = this.getMessages(sourceChatId)
812
+ if (sourceEntries.length > 0) {
813
+ const transcriptPath = this.transcriptPath(chatId)
814
+ const payload = sourceEntries.map((entry) => JSON.stringify(entry)).join("\n")
815
+ this.writeChain = this.writeChain.then(async () => {
816
+ await mkdir(this.transcriptsDir, { recursive: true })
817
+ await writeFile(transcriptPath, `${payload}\n`, "utf8")
818
+ const chat = this.state.chatsById.get(chatId)
819
+ if (chat) {
820
+ chat.hasMessages = true
821
+ chat.updatedAt = Math.max(chat.updatedAt, createdAt)
822
+ }
823
+ if (this.cachedTranscript?.chatId === chatId) {
824
+ this.cachedTranscript = { chatId, entries: cloneTranscriptEntries(sourceEntries) }
825
+ }
826
+ })
827
+ await this.writeChain
828
+ }
829
+
830
+ return this.state.chatsById.get(chatId)!
831
+ }
832
+
833
+ async renameChat(chatId: string, title: string) {
834
+ const trimmed = title.trim()
835
+ if (!trimmed) return
836
+ const chat = this.requireChat(chatId)
837
+ if (chat.title === trimmed) return
838
+ const event: ChatEvent = {
839
+ v: STORE_VERSION,
840
+ type: "chat_renamed",
841
+ timestamp: Date.now(),
842
+ chatId,
843
+ title: trimmed,
844
+ }
845
+ await this.append(this.chatsLogPath, event)
846
+ }
847
+
848
+ async deleteChat(chatId: string) {
849
+ this.requireChat(chatId)
850
+ const event: ChatEvent = {
851
+ v: STORE_VERSION,
852
+ type: "chat_deleted",
853
+ timestamp: Date.now(),
854
+ chatId,
855
+ }
856
+ await this.append(this.chatsLogPath, event)
857
+ }
858
+
859
+ async archiveChat(chatId: string) {
860
+ this.requireChat(chatId)
861
+ const event: ChatEvent = {
862
+ v: STORE_VERSION,
863
+ type: "chat_archived",
864
+ timestamp: Date.now(),
865
+ chatId,
866
+ }
867
+ await this.append(this.chatsLogPath, event)
868
+ }
869
+
870
+ async unarchiveChat(chatId: string) {
871
+ this.requireChat(chatId)
872
+ const event: ChatEvent = {
873
+ v: STORE_VERSION,
874
+ type: "chat_unarchived",
875
+ timestamp: Date.now(),
876
+ chatId,
877
+ }
878
+ await this.append(this.chatsLogPath, event)
879
+ }
880
+
881
+ async pruneStaleEmptyChats(args?: {
882
+ now?: number
883
+ maxAgeMs?: number
884
+ activeChatIds?: Iterable<string>
885
+ protectedChatIds?: Iterable<string>
886
+ }) {
887
+ const now = args?.now ?? Date.now()
888
+ const maxAgeMs = args?.maxAgeMs ?? STALE_EMPTY_CHAT_MAX_AGE_MS
889
+ const protectedChatIds = new Set([
890
+ ...(args?.activeChatIds ?? []),
891
+ ...(args?.protectedChatIds ?? []),
892
+ ])
893
+ const prunedChatIds: string[] = []
894
+
895
+ for (const chat of this.state.chatsById.values()) {
896
+ if (chat.deletedAt || chat.archivedAt || protectedChatIds.has(chat.id)) continue
897
+ if (now - chat.createdAt < maxAgeMs) continue
898
+ if (chat.hasMessages) continue
899
+ if (this.getMessages(chat.id).length > 0) {
900
+ chat.hasMessages = true
901
+ continue
902
+ }
903
+
904
+ const event: ChatEvent = {
905
+ v: STORE_VERSION,
906
+ type: "chat_deleted",
907
+ timestamp: now,
908
+ chatId: chat.id,
909
+ }
910
+ await this.append(this.chatsLogPath, event)
911
+
912
+ const transcriptPath = this.transcriptPath(chat.id)
913
+ await rm(transcriptPath, { force: true })
914
+ if (this.cachedTranscript?.chatId === chat.id) {
915
+ this.cachedTranscript = null
916
+ }
917
+
918
+ prunedChatIds.push(chat.id)
919
+ }
920
+
921
+ return prunedChatIds
922
+ }
923
+
924
+ async setChatProvider(chatId: string, provider: AgentProvider) {
925
+ const chat = this.requireChat(chatId)
926
+ if (chat.provider === provider) return
927
+ const event: ChatEvent = {
928
+ v: STORE_VERSION,
929
+ type: "chat_provider_set",
930
+ timestamp: Date.now(),
931
+ chatId,
932
+ provider,
933
+ }
934
+ await this.append(this.chatsLogPath, event)
935
+ }
936
+
937
+ async setPlanMode(chatId: string, planMode: boolean) {
938
+ const chat = this.requireChat(chatId)
939
+ if (chat.planMode === planMode) return
940
+ const event: ChatEvent = {
941
+ v: STORE_VERSION,
942
+ type: "chat_plan_mode_set",
943
+ timestamp: Date.now(),
944
+ chatId,
945
+ planMode,
946
+ }
947
+ await this.append(this.chatsLogPath, event)
948
+ }
949
+
950
+ async setChatReadState(chatId: string, unread: boolean) {
951
+ const chat = this.requireChat(chatId)
952
+ if (chat.unread === unread) return
953
+ const event: ChatEvent = {
954
+ v: STORE_VERSION,
955
+ type: "chat_read_state_set",
956
+ timestamp: Date.now(),
957
+ chatId,
958
+ unread,
959
+ }
960
+ await this.append(this.chatsLogPath, event)
961
+ }
962
+
963
+ async appendMessage(chatId: string, entry: TranscriptEntry) {
964
+ this.requireChat(chatId)
965
+ const payload = `${JSON.stringify(entry)}\n`
966
+ const transcriptPath = this.transcriptPath(chatId)
967
+ const queuedAt = performance.now()
968
+ this.writeChain = this.writeChain.then(async () => {
969
+ const startedAt = performance.now()
970
+ const queueDelayMs = Number((startedAt - queuedAt).toFixed(1))
971
+ await mkdir(this.transcriptsDir, { recursive: true })
972
+ const beforeAppendAt = performance.now()
973
+ await appendFile(transcriptPath, payload, "utf8")
974
+ const afterAppendAt = performance.now()
975
+ this.applyMessageMetadata(chatId, entry)
976
+ if (this.cachedTranscript?.chatId === chatId) {
977
+ this.cachedTranscript.entries.push({ ...entry })
978
+ }
979
+ logSendToStartingProfile("event_store.append_message", {
980
+ chatId,
981
+ entryId: entry._id,
982
+ kind: entry.kind,
983
+ payloadBytes: payload.length,
984
+ queueDelayMs,
985
+ appendMs: Number((afterAppendAt - beforeAppendAt).toFixed(1)),
986
+ totalMs: Number((afterAppendAt - queuedAt).toFixed(1)),
987
+ })
988
+ })
989
+ return this.writeChain
990
+ }
991
+
992
+ async enqueueMessage(chatId: string, message: Omit<QueuedChatMessage, "id" | "createdAt"> & Partial<Pick<QueuedChatMessage, "id" | "createdAt">>) {
993
+ this.requireChat(chatId)
994
+ const queuedMessage: QueuedChatMessage = {
995
+ id: message.id ?? crypto.randomUUID(),
996
+ content: message.content,
997
+ attachments: [...(message.attachments ?? [])],
998
+ createdAt: message.createdAt ?? Date.now(),
999
+ provider: message.provider,
1000
+ model: message.model,
1001
+ modelOptions: message.modelOptions,
1002
+ planMode: message.planMode,
1003
+ autoContinue: message.autoContinue,
1004
+ }
1005
+ const event: QueuedMessageEvent = {
1006
+ v: STORE_VERSION,
1007
+ type: "queued_message_enqueued",
1008
+ timestamp: queuedMessage.createdAt,
1009
+ chatId,
1010
+ message: queuedMessage,
1011
+ }
1012
+ await this.append(this.queuedMessagesLogPath, event)
1013
+ return queuedMessage
1014
+ }
1015
+
1016
+ async removeQueuedMessage(chatId: string, queuedMessageId: string) {
1017
+ this.requireChat(chatId)
1018
+ const existing = this.getQueuedMessages(chatId)
1019
+ if (!existing.some((entry) => entry.id === queuedMessageId)) {
1020
+ throw new Error("Queued message not found")
1021
+ }
1022
+ const event: QueuedMessageEvent = {
1023
+ v: STORE_VERSION,
1024
+ type: "queued_message_removed",
1025
+ timestamp: Date.now(),
1026
+ chatId,
1027
+ queuedMessageId,
1028
+ }
1029
+ await this.append(this.queuedMessagesLogPath, event)
1030
+ }
1031
+
1032
+ async recordTurnStarted(chatId: string) {
1033
+ this.requireChat(chatId)
1034
+ const event: TurnEvent = {
1035
+ v: STORE_VERSION,
1036
+ type: "turn_started",
1037
+ timestamp: Date.now(),
1038
+ chatId,
1039
+ }
1040
+ await this.append(this.turnsLogPath, event)
1041
+ }
1042
+
1043
+ async recordTurnFinished(chatId: string) {
1044
+ this.requireChat(chatId)
1045
+ const event: TurnEvent = {
1046
+ v: STORE_VERSION,
1047
+ type: "turn_finished",
1048
+ timestamp: Date.now(),
1049
+ chatId,
1050
+ }
1051
+ await this.append(this.turnsLogPath, event)
1052
+ }
1053
+
1054
+ async recordTurnFailed(chatId: string, error: string) {
1055
+ this.requireChat(chatId)
1056
+ const event: TurnEvent = {
1057
+ v: STORE_VERSION,
1058
+ type: "turn_failed",
1059
+ timestamp: Date.now(),
1060
+ chatId,
1061
+ error,
1062
+ }
1063
+ await this.append(this.turnsLogPath, event)
1064
+ }
1065
+
1066
+ async recordTurnCancelled(chatId: string) {
1067
+ this.requireChat(chatId)
1068
+ const event: TurnEvent = {
1069
+ v: STORE_VERSION,
1070
+ type: "turn_cancelled",
1071
+ timestamp: Date.now(),
1072
+ chatId,
1073
+ }
1074
+ await this.append(this.turnsLogPath, event)
1075
+ }
1076
+
1077
+ async setSessionToken(chatId: string, sessionToken: string | null) {
1078
+ const chat = this.requireChat(chatId)
1079
+ if (chat.sessionToken === sessionToken) return
1080
+ const event: TurnEvent = {
1081
+ v: STORE_VERSION,
1082
+ type: "session_token_set",
1083
+ timestamp: Date.now(),
1084
+ chatId,
1085
+ sessionToken,
1086
+ }
1087
+ await this.append(this.turnsLogPath, event)
1088
+ }
1089
+
1090
+ async recordSessionCommandsLoaded(chatId: string, commands: SlashCommand[]) {
1091
+ const chat = this.requireChat(chatId)
1092
+ const normalized = commands.map((c) => ({
1093
+ name: c.name,
1094
+ description: c.description,
1095
+ argumentHint: c.argumentHint,
1096
+ }))
1097
+ if (chat.slashCommands && slashCommandsEqual(chat.slashCommands, normalized)) {
1098
+ return
1099
+ }
1100
+ const event: TurnEvent = {
1101
+ v: STORE_VERSION,
1102
+ type: "session_commands_loaded",
1103
+ timestamp: Date.now(),
1104
+ chatId,
1105
+ commands: normalized,
1106
+ }
1107
+ await this.append(this.turnsLogPath, event)
1108
+ }
1109
+
1110
+ async setPendingForkSessionToken(chatId: string, pendingForkSessionToken: string | null) {
1111
+ const chat = this.requireChat(chatId)
1112
+ if ((chat.pendingForkSessionToken ?? null) === pendingForkSessionToken) return
1113
+ const event: TurnEvent = {
1114
+ v: STORE_VERSION,
1115
+ type: "pending_fork_session_token_set",
1116
+ timestamp: Date.now(),
1117
+ chatId,
1118
+ pendingForkSessionToken,
1119
+ }
1120
+ await this.append(this.turnsLogPath, event)
1121
+ }
1122
+
1123
+ async setSourceHash(chatId: string, sourceHash: string | null) {
1124
+ const chat = this.requireChat(chatId)
1125
+ if (chat.sourceHash === sourceHash) return
1126
+ const event: ChatEvent = {
1127
+ v: STORE_VERSION,
1128
+ type: "chat_source_hash_set",
1129
+ timestamp: Date.now(),
1130
+ chatId,
1131
+ sourceHash,
1132
+ }
1133
+ await this.append(this.chatsLogPath, event)
1134
+ }
1135
+
1136
+ getProject(projectId: string) {
1137
+ const project = this.state.projectsById.get(projectId)
1138
+ if (!project || project.deletedAt) return null
1139
+ return project
1140
+ }
1141
+
1142
+ requireChat(chatId: string) {
1143
+ const chat = this.state.chatsById.get(chatId)
1144
+ if (!chat || chat.deletedAt) {
1145
+ throw new Error("Chat not found")
1146
+ }
1147
+ return chat
1148
+ }
1149
+
1150
+ getChat(chatId: string) {
1151
+ const chat = this.state.chatsById.get(chatId)
1152
+ if (!chat || chat.deletedAt) return null
1153
+ return chat
1154
+ }
1155
+
1156
+ getSidebarProjectOrder() {
1157
+ return [...this.sidebarProjectOrder]
1158
+ }
1159
+
1160
+ private getMessagesPageFromEntries(entries: TranscriptEntry[], limit: number, beforeIndex?: number): TranscriptPageResult {
1161
+ if (entries.length === 0) {
1162
+ return { entries: [], hasOlder: false, olderCursor: null }
1163
+ }
1164
+
1165
+ const endIndex = beforeIndex === undefined ? entries.length : Math.max(0, Math.min(beforeIndex, entries.length))
1166
+ const startIndex = Math.max(0, endIndex - limit)
1167
+ return {
1168
+ entries: cloneTranscriptEntries(entries.slice(startIndex, endIndex)),
1169
+ hasOlder: startIndex > 0,
1170
+ olderCursor: startIndex > 0 ? encodeHistoryCursor(startIndex) : null,
1171
+ }
1172
+ }
1173
+
1174
+ getMessages(chatId: string) {
1175
+ if (this.cachedTranscript?.chatId === chatId) {
1176
+ return cloneTranscriptEntries(this.cachedTranscript.entries)
1177
+ }
1178
+
1179
+ const legacyEntries = this.legacyMessagesByChatId.get(chatId)
1180
+ if (legacyEntries) {
1181
+ this.cachedTranscript = { chatId, entries: cloneTranscriptEntries(legacyEntries) }
1182
+ return cloneTranscriptEntries(this.cachedTranscript.entries)
1183
+ }
1184
+
1185
+ const entries = this.loadTranscriptFromDisk(chatId)
1186
+ this.cachedTranscript = { chatId, entries }
1187
+ return cloneTranscriptEntries(entries)
1188
+ }
1189
+
1190
+ getQueuedMessages(chatId: string) {
1191
+ const entries = this.state.queuedMessagesByChatId.get(chatId) ?? []
1192
+ return entries.map((entry) => ({
1193
+ ...entry,
1194
+ attachments: [...entry.attachments],
1195
+ }))
1196
+ }
1197
+
1198
+ getQueuedMessage(chatId: string, queuedMessageId: string) {
1199
+ return this.getQueuedMessages(chatId).find((entry) => entry.id === queuedMessageId) ?? null
1200
+ }
1201
+
1202
+ getRecentMessagesPage(chatId: string, limit: number): ChatHistoryPage {
1203
+ if (limit <= 0) {
1204
+ return { messages: [], hasOlder: false, olderCursor: null }
1205
+ }
1206
+
1207
+ const entries = this.getMessages(chatId)
1208
+ const page = this.getMessagesPageFromEntries(entries, limit)
1209
+
1210
+ return {
1211
+ messages: page.entries,
1212
+ hasOlder: page.hasOlder,
1213
+ olderCursor: page.olderCursor,
1214
+ }
1215
+ }
1216
+
1217
+ getMessagesPageBefore(chatId: string, beforeCursor: string, limit: number): ChatHistoryPage {
1218
+ if (limit <= 0) {
1219
+ return { messages: [], hasOlder: false, olderCursor: null }
1220
+ }
1221
+
1222
+ const beforeIndex = decodeCursor(beforeCursor)
1223
+ const entries = this.getMessages(chatId)
1224
+ const page = this.getMessagesPageFromEntries(entries, limit, beforeIndex)
1225
+
1226
+ return {
1227
+ messages: page.entries,
1228
+ hasOlder: page.hasOlder,
1229
+ olderCursor: page.olderCursor,
1230
+ }
1231
+ }
1232
+
1233
+ getRecentChatHistory(chatId: string, recentLimit: number) {
1234
+ const page = this.getRecentMessagesPage(chatId, recentLimit)
1235
+ return {
1236
+ messages: page.messages,
1237
+ history: getHistorySnapshot({
1238
+ entries: page.messages,
1239
+ hasOlder: page.hasOlder,
1240
+ olderCursor: page.olderCursor,
1241
+ }, recentLimit),
1242
+ }
1243
+ }
1244
+
1245
+ listProjects() {
1246
+ return [...this.state.projectsById.values()].filter((project) => !project.deletedAt)
1247
+ }
1248
+
1249
+ listChatsByProject(projectId: string) {
1250
+ return [...this.state.chatsById.values()]
1251
+ .filter((chat) => chat.projectId === projectId && !chat.deletedAt && !chat.archivedAt)
1252
+ .sort((a, b) => (b.lastMessageAt ?? b.updatedAt) - (a.lastMessageAt ?? a.updatedAt))
1253
+ }
1254
+
1255
+ getChatCount(projectId: string) {
1256
+ return this.listChatsByProject(projectId).length
1257
+ }
1258
+
1259
+ async getLegacyTranscriptStats(): Promise<LegacyTranscriptStats> {
1260
+ const messagesLogSize = await Bun.file(this.messagesLogPath).size
1261
+ const sources: LegacyTranscriptStats["sources"] = []
1262
+ if (this.snapshotHasLegacyMessages) {
1263
+ sources.push("snapshot")
1264
+ }
1265
+ if (messagesLogSize > 0) {
1266
+ sources.push("messages_log")
1267
+ }
1268
+
1269
+ let entryCount = 0
1270
+ for (const entries of this.legacyMessagesByChatId.values()) {
1271
+ entryCount += entries.length
1272
+ }
1273
+
1274
+ return {
1275
+ hasLegacyData: sources.length > 0 || this.legacyMessagesByChatId.size > 0,
1276
+ sources,
1277
+ chatCount: this.legacyMessagesByChatId.size,
1278
+ entryCount,
1279
+ }
1280
+ }
1281
+
1282
+ async hasLegacyTranscriptData() {
1283
+ return (await this.getLegacyTranscriptStats()).hasLegacyData
1284
+ }
1285
+
1286
+ private createSnapshot(): SnapshotFile {
1287
+ return {
1288
+ v: STORE_VERSION,
1289
+ generatedAt: Date.now(),
1290
+ projects: this.listProjects().map((project) => ({ ...project })),
1291
+ chats: [...this.state.chatsById.values()]
1292
+ .filter((chat) => !chat.deletedAt)
1293
+ .map((chat) => ({ ...chat })),
1294
+ queuedMessages: [...this.state.queuedMessagesByChatId.entries()]
1295
+ .map(([chatId, entries]) => ({
1296
+ chatId,
1297
+ entries: entries.map((entry) => ({
1298
+ ...entry,
1299
+ attachments: [...entry.attachments],
1300
+ })),
1301
+ })),
1302
+ autoContinueEvents: [...this.state.autoContinueEventsByChatId.entries()].map(([chatId, events]) => ({
1303
+ chatId,
1304
+ events: [...events],
1305
+ })),
1306
+ }
1307
+ }
1308
+
1309
+ async compact() {
1310
+ const snapshot = this.createSnapshot()
1311
+ await Bun.write(this.snapshotPath, JSON.stringify(snapshot, null, 2))
1312
+ await Promise.all([
1313
+ Bun.write(this.projectsLogPath, ""),
1314
+ Bun.write(this.chatsLogPath, ""),
1315
+ Bun.write(this.messagesLogPath, ""),
1316
+ Bun.write(this.queuedMessagesLogPath, ""),
1317
+ Bun.write(this.turnsLogPath, ""),
1318
+ Bun.write(this.schedulesLogPath, ""),
1319
+ // tunnels.jsonl is NOT compacted into the snapshot — it's left as-is
1320
+ // so that active tunnel state survives server restarts.
1321
+ ])
1322
+ }
1323
+
1324
+ async migrateLegacyTranscripts(onProgress?: (message: string) => void) {
1325
+ const stats = await this.getLegacyTranscriptStats()
1326
+ if (!stats.hasLegacyData) return false
1327
+
1328
+ const sourceSummary = stats.sources.map((source) => source === "messages_log" ? "messages.jsonl" : "snapshot.json").join(", ")
1329
+ onProgress?.(`${LOG_PREFIX} transcript migration detected: ${stats.chatCount} chats, ${stats.entryCount} entries from ${sourceSummary}`)
1330
+
1331
+ const messageSets = [...this.legacyMessagesByChatId.entries()]
1332
+ onProgress?.(`${LOG_PREFIX} transcript migration: writing ${messageSets.length} per-chat transcript files`)
1333
+
1334
+ await mkdir(this.transcriptsDir, { recursive: true })
1335
+ const logEveryChat = messageSets.length <= 10
1336
+ for (let index = 0; index < messageSets.length; index += 1) {
1337
+ const [chatId, entries] = messageSets[index]
1338
+ const transcriptPath = this.transcriptPath(chatId)
1339
+ const tempPath = `${transcriptPath}.tmp`
1340
+ const payload = entries.map((entry) => JSON.stringify(entry)).join("\n")
1341
+ await writeFile(tempPath, payload ? `${payload}\n` : "", "utf8")
1342
+ await rename(tempPath, transcriptPath)
1343
+ if (logEveryChat || (index + 1) % 25 === 0 || index === messageSets.length - 1) {
1344
+ onProgress?.(`${LOG_PREFIX} transcript migration: ${index + 1}/${messageSets.length} chats`)
1345
+ }
1346
+ }
1347
+
1348
+ this.clearLegacyTranscriptState()
1349
+ await this.compact()
1350
+ this.cachedTranscript = null
1351
+ onProgress?.(`${LOG_PREFIX} transcript migration complete`)
1352
+ return true
1353
+ }
1354
+
1355
+ private async shouldCompact() {
1356
+ const sizes = await Promise.all([
1357
+ Bun.file(this.projectsLogPath).size,
1358
+ Bun.file(this.chatsLogPath).size,
1359
+ Bun.file(this.messagesLogPath).size,
1360
+ Bun.file(this.queuedMessagesLogPath).size,
1361
+ Bun.file(this.turnsLogPath).size,
1362
+ Bun.file(this.schedulesLogPath).size,
1363
+ ])
1364
+ return sizes.reduce((total, size) => total + size, 0) >= COMPACTION_THRESHOLD_BYTES
1365
+ }
1366
+
1367
+ async appendAutoContinueEvent(event: AutoContinueEvent) {
1368
+ return this.append(this.schedulesLogPath, event)
1369
+ }
1370
+
1371
+ getAutoContinueEvents(chatId: string): AutoContinueEvent[] {
1372
+ const list = this.state.autoContinueEventsByChatId.get(chatId)
1373
+ return list ? [...list] : []
1374
+ }
1375
+
1376
+ listAutoContinueChats(): string[] {
1377
+ return [...this.state.autoContinueEventsByChatId.keys()]
1378
+ }
1379
+
1380
+ async appendTunnelEvent(event: CloudflareTunnelEvent): Promise<void> {
1381
+ const payload = `${JSON.stringify(event)}\n`
1382
+ this.writeChain = this.writeChain.then(async () => {
1383
+ await appendFile(this.tunnelLogPath, payload, "utf8")
1384
+ this.applyTunnelEvent(event)
1385
+ })
1386
+ await this.writeChain
1387
+ }
1388
+
1389
+ getTunnelEvents(chatId: string): CloudflareTunnelEvent[] {
1390
+ const list = this.tunnelEventsByChatId.get(chatId)
1391
+ return list ? [...list] : []
1392
+ }
1393
+
1394
+ listTunnelChats(): string[] {
1395
+ return [...this.tunnelEventsByChatId.keys()]
1396
+ }
1397
+
1398
+ private applyTunnelEvent(event: CloudflareTunnelEvent): void {
1399
+ const existing = this.tunnelEventsByChatId.get(event.chatId) ?? []
1400
+ existing.push(event)
1401
+ this.tunnelEventsByChatId.set(event.chatId, existing)
1402
+ }
1403
+
1404
+ private async loadTunnelEvents(): Promise<void> {
1405
+ const file = Bun.file(this.tunnelLogPath)
1406
+ if (!(await file.exists())) return
1407
+ const text = await file.text()
1408
+ if (!text.trim()) return
1409
+
1410
+ for (const rawLine of text.split("\n")) {
1411
+ const line = rawLine.trim()
1412
+ if (!line) continue
1413
+ try {
1414
+ const event = JSON.parse(line) as CloudflareTunnelEvent
1415
+ this.applyTunnelEvent(event)
1416
+ } catch {
1417
+ console.warn(`${LOG_PREFIX} Ignoring malformed line in tunnels.jsonl`)
1418
+ }
1419
+ }
1420
+ }
1421
+ }