@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,1465 @@
1
+ import type { ServerWebSocket } from "bun"
2
+ import { PROTOCOL_VERSION } from "../shared/types"
3
+ import type { ClientEnvelope, ServerEnvelope, SubscriptionTopic } from "../shared/protocol"
4
+ import { isClientEnvelope } from "../shared/protocol"
5
+ import type { AgentCoordinator } from "./agent"
6
+ import type { AnalyticsReporter } from "./analytics"
7
+ import { NoopAnalyticsReporter } from "./analytics"
8
+ import type { AppSettingsManager } from "./app-settings"
9
+ import type { DiscoveredProject } from "./discovery"
10
+ import { DiffStore } from "./diff-store"
11
+ import { EventStore } from "./event-store"
12
+ import { openExternal } from "./external-open"
13
+ import { KeybindingsManager } from "./keybindings"
14
+ import { ensureProjectDirectory, resolveLocalPath } from "./paths"
15
+ import { writeStandaloneTranscriptExport } from "./standalone-export"
16
+ import { TerminalManager } from "./terminal-manager"
17
+ import type { UpdateManager } from "./update-manager"
18
+ import { deriveChatSnapshot, deriveLocalProjectsSnapshot, deriveSidebarData } from "./read-models"
19
+ import { CLOUDFLARE_TUNNEL_DEFAULTS } from "../shared/types"
20
+ import type { AppSettingsPatch, AppSettingsSnapshot, LlmProviderSnapshot, LlmProviderValidationResult } from "../shared/types"
21
+ import { importClaudeSessions } from "./claude-session-importer"
22
+ import type { TunnelGateway } from "./cloudflare-tunnel/gateway"
23
+
24
+ const DEFAULT_CHAT_RECENT_LIMIT = 200
25
+
26
+ function isSendToStartingProfilingEnabled() {
27
+ return process.env.KANNA_PROFILE_SEND_TO_STARTING === "1"
28
+ }
29
+
30
+ function logSendToStartingProfile(
31
+ traceId: string | null | undefined,
32
+ startedAt: number | null | undefined,
33
+ stage: string,
34
+ details?: Record<string, unknown>
35
+ ) {
36
+ if (!traceId || startedAt === undefined || startedAt === null || !isSendToStartingProfilingEnabled()) {
37
+ return
38
+ }
39
+
40
+ console.log("[kanna/send->starting][server]", JSON.stringify({
41
+ traceId,
42
+ stage,
43
+ elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
44
+ ...details,
45
+ }))
46
+ }
47
+
48
+ function countSubscriptionsByTopic(ws: ServerWebSocket<ClientState>) {
49
+ let sidebar = 0
50
+ let chat = 0
51
+ let projectGit = 0
52
+ let localProjects = 0
53
+ let update = 0
54
+ let keybindings = 0
55
+ let appSettings = 0
56
+ let terminal = 0
57
+
58
+ for (const topic of ws.data.subscriptions.values()) {
59
+ switch (topic.type) {
60
+ case "sidebar":
61
+ sidebar += 1
62
+ break
63
+ case "chat":
64
+ chat += 1
65
+ break
66
+ case "project-git":
67
+ projectGit += 1
68
+ break
69
+ case "local-projects":
70
+ localProjects += 1
71
+ break
72
+ case "update":
73
+ update += 1
74
+ break
75
+ case "keybindings":
76
+ keybindings += 1
77
+ break
78
+ case "app-settings":
79
+ appSettings += 1
80
+ break
81
+ case "terminal":
82
+ terminal += 1
83
+ break
84
+ }
85
+ }
86
+
87
+ return {
88
+ total: ws.data.subscriptions.size,
89
+ sidebar,
90
+ chat,
91
+ projectGit,
92
+ localProjects,
93
+ update,
94
+ keybindings,
95
+ appSettings,
96
+ terminal,
97
+ }
98
+ }
99
+
100
+ export interface ClientState {
101
+ subscriptions: Map<string, SubscriptionTopic>
102
+ snapshotSignatures: Map<string, string>
103
+ protectedDraftChatIds?: Set<string>
104
+ }
105
+
106
+ interface CreateWsRouterArgs {
107
+ store: EventStore
108
+ diffStore?: Pick<DiffStore, "getProjectSnapshot" | "refreshSnapshot" | "initializeGit" | "getGitHubPublishInfo" | "checkGitHubRepoAvailability" | "publishToGitHub" | "listBranches" | "previewMergeBranch" | "mergeBranch" | "syncBranch" | "checkoutBranch" | "createBranch" | "generateCommitMessage" | "commitFiles" | "discardFile" | "ignoreFile" | "readPatch">
109
+ agent: AgentCoordinator
110
+ terminals: TerminalManager
111
+ keybindings: KeybindingsManager
112
+ appSettings?: Pick<AppSettingsManager, "getSnapshot" | "write"> & Partial<Pick<AppSettingsManager, "setCloudflareTunnel" | "writePatch" | "onChange">>
113
+ analytics?: AnalyticsReporter
114
+ tunnelGateway?: TunnelGateway
115
+ llmProvider?: {
116
+ read: () => Promise<LlmProviderSnapshot>
117
+ write: (value: Pick<LlmProviderSnapshot, "provider" | "apiKey" | "model" | "baseUrl">) => Promise<LlmProviderSnapshot>
118
+ validate: (value: Pick<LlmProviderSnapshot, "provider" | "apiKey" | "model" | "baseUrl">) => Promise<LlmProviderValidationResult>
119
+ }
120
+ refreshDiscovery: () => Promise<DiscoveredProject[]>
121
+ getDiscoveredProjects: () => DiscoveredProject[]
122
+ machineDisplayName: string
123
+ updateManager: UpdateManager | null
124
+ }
125
+
126
+ interface SnapshotBroadcastFilter {
127
+ includeSidebar?: boolean
128
+ includeLocalProjects?: boolean
129
+ includeUpdate?: boolean
130
+ includeKeybindings?: boolean
131
+ includeAppSettings?: boolean
132
+ chatIds?: Set<string>
133
+ projectIds?: Set<string>
134
+ terminalIds?: Set<string>
135
+ }
136
+
137
+ interface SnapshotComputationCache {
138
+ sidebar?: {
139
+ data: ReturnType<typeof deriveSidebarData>
140
+ signature: string
141
+ }
142
+ }
143
+
144
+ function getSidebarProjectOrder(store: EventStore) {
145
+ return typeof store.getSidebarProjectOrder === "function"
146
+ ? store.getSidebarProjectOrder()
147
+ : []
148
+ }
149
+
150
+ function send(ws: ServerWebSocket<ClientState>, message: ServerEnvelope) {
151
+ const payload = JSON.stringify(message)
152
+ ws.send(payload)
153
+ return payload.length
154
+ }
155
+
156
+ function ensureSnapshotSignatures(ws: ServerWebSocket<ClientState>) {
157
+ if (!ws.data.snapshotSignatures) {
158
+ ws.data.snapshotSignatures = new Map()
159
+ }
160
+
161
+ return ws.data.snapshotSignatures
162
+ }
163
+
164
+ export function createWsRouter({
165
+ store,
166
+ diffStore,
167
+ agent,
168
+ terminals,
169
+ keybindings,
170
+ appSettings,
171
+ analytics,
172
+ tunnelGateway,
173
+ llmProvider,
174
+ refreshDiscovery,
175
+ getDiscoveredProjects,
176
+ machineDisplayName,
177
+ updateManager,
178
+ }: CreateWsRouterArgs) {
179
+ const sockets = new Set<ServerWebSocket<ClientState>>()
180
+ let pendingBroadcastTimer: ReturnType<typeof setTimeout> | null = null
181
+ let pendingBroadcastAll = false
182
+ const pendingBroadcastChatIds = new Set<string>()
183
+ const resolvedDiffStore = diffStore ?? {
184
+ getProjectSnapshot: () => ({ status: "unknown", branchName: undefined, defaultBranchName: undefined, hasOriginRemote: undefined, originRepoSlug: undefined, hasUpstream: undefined, aheadCount: undefined, behindCount: undefined, lastFetchedAt: undefined, files: [] as const, branchHistory: { entries: [] as const } }),
185
+ refreshSnapshot: async () => false,
186
+ initializeGit: async () => ({ ok: true, branchName: undefined, snapshotChanged: false }),
187
+ getGitHubPublishInfo: async () => ({ ghInstalled: false, authenticated: false, activeAccountLogin: undefined, owners: [], suggestedRepoName: "my-repo" }),
188
+ checkGitHubRepoAvailability: async () => ({ available: false, message: "Unavailable" }),
189
+ publishToGitHub: async () => ({ ok: false, title: "Publish failed", message: "Unavailable", snapshotChanged: false }),
190
+ listBranches: async () => ({ recent: [], local: [], remote: [], pullRequests: [], pullRequestsStatus: "unavailable" as const }),
191
+ previewMergeBranch: async () => ({ currentBranchName: undefined, targetBranchName: "", targetDisplayName: "", status: "error" as const, commitCount: 0, hasConflicts: false, message: "Merge preview unavailable." }),
192
+ mergeBranch: async () => ({ ok: false as const, title: "Merge failed", message: "Merge unavailable.", snapshotChanged: false }),
193
+ syncBranch: async () => ({ ok: true, action: "fetch" as const, branchName: undefined, snapshotChanged: false }),
194
+ checkoutBranch: async () => ({ ok: true, branchName: undefined, snapshotChanged: false }),
195
+ createBranch: async () => ({ ok: true, branchName: "main", snapshotChanged: false }),
196
+ generateCommitMessage: async () => ({ subject: "Update selected files", body: "", usedFallback: true, failureMessage: null }),
197
+ commitFiles: async () => ({ ok: true, mode: "commit_only" as const, branchName: undefined, pushed: false, snapshotChanged: false }),
198
+ discardFile: async () => ({ snapshotChanged: false }),
199
+ ignoreFile: async () => ({ snapshotChanged: false }),
200
+ readPatch: async () => ({ patch: "" }),
201
+ }
202
+ const resolvedLlmProvider = llmProvider ?? {
203
+ read: async () => ({
204
+ provider: "openai" as const,
205
+ apiKey: "",
206
+ model: "gpt-5.4-mini",
207
+ baseUrl: "",
208
+ resolvedBaseUrl: "https://api.openai.com/v1",
209
+ enabled: false,
210
+ warning: null,
211
+ filePathDisplay: "~/.kanna/llm-provider.json",
212
+ }),
213
+ write: async ({ provider, apiKey, model, baseUrl }: {
214
+ provider: "openai" | "openrouter" | "custom"
215
+ apiKey: string
216
+ model: string
217
+ baseUrl: string
218
+ }) => ({
219
+ provider,
220
+ apiKey,
221
+ model,
222
+ baseUrl,
223
+ resolvedBaseUrl: provider === "openrouter"
224
+ ? "https://openrouter.ai/api/v1"
225
+ : provider === "custom"
226
+ ? baseUrl
227
+ : "https://api.openai.com/v1",
228
+ enabled: false,
229
+ warning: null,
230
+ filePathDisplay: "~/.kanna/llm-provider.json",
231
+ }),
232
+ validate: async () => ({
233
+ ok: false,
234
+ error: {
235
+ type: "config_error",
236
+ message: "LLM provider validation unavailable.",
237
+ },
238
+ }),
239
+ }
240
+ let fallbackAppSettingsSnapshot: AppSettingsSnapshot = {
241
+ analyticsEnabled: true,
242
+ browserSettingsMigrated: false,
243
+ theme: "system",
244
+ chatSoundPreference: "always",
245
+ chatSoundId: "funk",
246
+ terminal: {
247
+ scrollbackLines: 1_000,
248
+ minColumnWidth: 450,
249
+ },
250
+ editor: {
251
+ preset: "cursor",
252
+ commandTemplate: "cursor {path}",
253
+ },
254
+ defaultProvider: "last_used",
255
+ providerDefaults: {
256
+ claude: {
257
+ model: "claude-opus-4-7",
258
+ modelOptions: {
259
+ reasoningEffort: "high",
260
+ contextWindow: "200k",
261
+ },
262
+ planMode: false,
263
+ },
264
+ codex: {
265
+ model: "gpt-5.5",
266
+ modelOptions: {
267
+ reasoningEffort: "high",
268
+ fastMode: false,
269
+ },
270
+ planMode: false,
271
+ },
272
+ },
273
+ warning: null,
274
+ filePathDisplay: "~/.kanna/data/settings.json",
275
+ cloudflareTunnel: CLOUDFLARE_TUNNEL_DEFAULTS,
276
+ }
277
+ const mergeAppSettingsPatch = (snapshot: AppSettingsSnapshot, patch: AppSettingsPatch): AppSettingsSnapshot => ({
278
+ ...snapshot,
279
+ ...patch,
280
+ terminal: {
281
+ ...snapshot.terminal,
282
+ ...patch.terminal,
283
+ },
284
+ editor: {
285
+ ...snapshot.editor,
286
+ ...patch.editor,
287
+ },
288
+ providerDefaults: {
289
+ claude: {
290
+ ...snapshot.providerDefaults.claude,
291
+ ...patch.providerDefaults?.claude,
292
+ modelOptions: {
293
+ ...snapshot.providerDefaults.claude.modelOptions,
294
+ ...patch.providerDefaults?.claude?.modelOptions,
295
+ },
296
+ },
297
+ codex: {
298
+ ...snapshot.providerDefaults.codex,
299
+ ...patch.providerDefaults?.codex,
300
+ modelOptions: {
301
+ ...snapshot.providerDefaults.codex.modelOptions,
302
+ ...patch.providerDefaults?.codex?.modelOptions,
303
+ },
304
+ },
305
+ },
306
+ cloudflareTunnel: {
307
+ ...snapshot.cloudflareTunnel,
308
+ ...patch.cloudflareTunnel,
309
+ },
310
+ })
311
+ const resolvedAppSettings = {
312
+ getSnapshot: () => appSettings?.getSnapshot() ?? fallbackAppSettingsSnapshot,
313
+ write: async (value: { analyticsEnabled: boolean }) => {
314
+ if (appSettings) return await appSettings.write(value)
315
+ fallbackAppSettingsSnapshot = { ...fallbackAppSettingsSnapshot, analyticsEnabled: value.analyticsEnabled }
316
+ return fallbackAppSettingsSnapshot
317
+ },
318
+ writePatch: async (patch: AppSettingsPatch) => {
319
+ if (appSettings?.writePatch) return await appSettings.writePatch(patch)
320
+ if (appSettings && patch.analyticsEnabled !== undefined && Object.keys(patch).length === 1) {
321
+ return await appSettings.write({ analyticsEnabled: patch.analyticsEnabled })
322
+ }
323
+ fallbackAppSettingsSnapshot = mergeAppSettingsPatch(appSettings?.getSnapshot() ?? fallbackAppSettingsSnapshot, patch)
324
+ return fallbackAppSettingsSnapshot
325
+ },
326
+ setCloudflareTunnel: async (patch: Partial<AppSettingsSnapshot["cloudflareTunnel"]>) => {
327
+ if (appSettings?.setCloudflareTunnel) return await appSettings.setCloudflareTunnel(patch)
328
+ fallbackAppSettingsSnapshot = mergeAppSettingsPatch(appSettings?.getSnapshot() ?? fallbackAppSettingsSnapshot, { cloudflareTunnel: patch })
329
+ return fallbackAppSettingsSnapshot
330
+ },
331
+ onChange: (listener: (snapshot: AppSettingsSnapshot) => void) => appSettings?.onChange?.(listener) ?? (() => {}),
332
+ }
333
+ const resolvedAnalytics = analytics ?? NoopAnalyticsReporter
334
+
335
+ function getProtectedChatIds() {
336
+ const activeStatuses = agent.getActiveStatuses()
337
+ const drainingChatIds = typeof agent.getDrainingChatIds === "function"
338
+ ? agent.getDrainingChatIds()
339
+ : new Set<string>()
340
+ return new Set([
341
+ ...activeStatuses.keys(),
342
+ ...drainingChatIds.values(),
343
+ ])
344
+ }
345
+
346
+ function getProtectedDraftChatIds(extraSockets?: Iterable<ServerWebSocket<ClientState>>) {
347
+ const protectedChatIds = new Set<string>()
348
+
349
+ for (const socket of sockets) {
350
+ for (const chatId of socket.data.protectedDraftChatIds ?? []) {
351
+ protectedChatIds.add(chatId)
352
+ }
353
+ }
354
+
355
+ for (const socket of extraSockets ?? []) {
356
+ for (const chatId of socket.data.protectedDraftChatIds ?? []) {
357
+ protectedChatIds.add(chatId)
358
+ }
359
+ }
360
+
361
+ return protectedChatIds
362
+ }
363
+
364
+ async function maybePruneStaleEmptyChats(extraSockets?: Iterable<ServerWebSocket<ClientState>>) {
365
+ const startedAt = performance.now()
366
+ const activeChatIds = getProtectedChatIds()
367
+ const protectedDraftChatIds = getProtectedDraftChatIds(extraSockets)
368
+ const prunedChatIds = await store.pruneStaleEmptyChats?.({
369
+ activeChatIds,
370
+ protectedChatIds: protectedDraftChatIds,
371
+ })
372
+ if (isSendToStartingProfilingEnabled()) {
373
+ console.log("[kanna/send->starting][server]", JSON.stringify({
374
+ stage: "ws.prune_stale_empty_chats",
375
+ elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
376
+ activeChatCount: activeChatIds.size,
377
+ protectedDraftChatCount: protectedDraftChatIds.size,
378
+ prunedCount: prunedChatIds?.length ?? 0,
379
+ totalChatCount: store.state.chatsById.size,
380
+ totalProjectCount: store.state.projectsById.size,
381
+ }))
382
+ }
383
+ }
384
+
385
+ function shouldIncludeTopic(topic: SubscriptionTopic, filter?: SnapshotBroadcastFilter) {
386
+ if (!filter) {
387
+ return true
388
+ }
389
+
390
+ if (topic.type === "sidebar") {
391
+ return Boolean(filter.includeSidebar)
392
+ }
393
+ if (topic.type === "local-projects") {
394
+ return Boolean(filter.includeLocalProjects)
395
+ }
396
+ if (topic.type === "update") {
397
+ return Boolean(filter.includeUpdate)
398
+ }
399
+ if (topic.type === "keybindings") {
400
+ return Boolean(filter.includeKeybindings)
401
+ }
402
+ if (topic.type === "app-settings") {
403
+ return Boolean(filter.includeAppSettings)
404
+ }
405
+ if (topic.type === "chat") {
406
+ return filter.chatIds?.has(topic.chatId) ?? false
407
+ }
408
+ if (topic.type === "project-git") {
409
+ return filter.projectIds?.has(topic.projectId) ?? false
410
+ }
411
+ if (topic.type === "terminal") {
412
+ return filter.terminalIds?.has(topic.terminalId) ?? false
413
+ }
414
+
415
+ return true
416
+ }
417
+
418
+ function getSidebarSnapshotCacheEntry(cache?: SnapshotComputationCache) {
419
+ if (cache?.sidebar) {
420
+ return cache.sidebar
421
+ }
422
+
423
+ const startedAt = performance.now()
424
+ const data = deriveSidebarData(store.state, agent.getActiveStatuses(), {
425
+ sidebarProjectOrder: getSidebarProjectOrder(store),
426
+ drainingChatIds: agent.getDrainingChatIds(),
427
+ })
428
+ if (isSendToStartingProfilingEnabled()) {
429
+ const totalChats = data.projectGroups.reduce((count, group) => count + group.chats.length, 0)
430
+ console.log("[kanna/send->starting][server]", JSON.stringify({
431
+ stage: "ws.sidebar_snapshot_built",
432
+ elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
433
+ projectGroupCount: data.projectGroups.length,
434
+ chatCount: totalChats,
435
+ totalChatCount: store.state.chatsById.size,
436
+ totalProjectCount: store.state.projectsById.size,
437
+ }))
438
+ }
439
+
440
+ const sidebar = {
441
+ data,
442
+ signature: JSON.stringify({
443
+ type: "sidebar" as const,
444
+ data,
445
+ }),
446
+ }
447
+
448
+ if (cache) {
449
+ cache.sidebar = sidebar
450
+ }
451
+
452
+ return sidebar
453
+ }
454
+
455
+ function createEnvelope(id: string, topic: SubscriptionTopic, cache?: SnapshotComputationCache): ServerEnvelope {
456
+ if (topic.type === "sidebar") {
457
+ const sidebar = getSidebarSnapshotCacheEntry(cache)
458
+ return {
459
+ v: PROTOCOL_VERSION,
460
+ type: "snapshot",
461
+ id,
462
+ snapshot: {
463
+ type: "sidebar",
464
+ data: sidebar.data,
465
+ },
466
+ }
467
+ }
468
+
469
+ if (topic.type === "local-projects") {
470
+ const discoveredProjects = getDiscoveredProjects()
471
+ const data = deriveLocalProjectsSnapshot(store.state, discoveredProjects, machineDisplayName)
472
+
473
+ return {
474
+ v: PROTOCOL_VERSION,
475
+ type: "snapshot",
476
+ id,
477
+ snapshot: {
478
+ type: "local-projects",
479
+ data,
480
+ },
481
+ }
482
+ }
483
+
484
+ if (topic.type === "keybindings") {
485
+ return {
486
+ v: PROTOCOL_VERSION,
487
+ type: "snapshot",
488
+ id,
489
+ snapshot: {
490
+ type: "keybindings",
491
+ data: keybindings.getSnapshot(),
492
+ },
493
+ }
494
+ }
495
+
496
+ if (topic.type === "app-settings") {
497
+ return {
498
+ v: PROTOCOL_VERSION,
499
+ type: "snapshot",
500
+ id,
501
+ snapshot: {
502
+ type: "app-settings",
503
+ data: resolvedAppSettings.getSnapshot(),
504
+ },
505
+ }
506
+ }
507
+
508
+ if (topic.type === "update") {
509
+ return {
510
+ v: PROTOCOL_VERSION,
511
+ type: "snapshot",
512
+ id,
513
+ snapshot: {
514
+ type: "update",
515
+ data: updateManager?.getSnapshot() ?? {
516
+ currentVersion: "unknown",
517
+ latestVersion: null,
518
+ status: "idle",
519
+ updateAvailable: false,
520
+ lastCheckedAt: null,
521
+ error: null,
522
+ installAction: "restart",
523
+ reloadRequestedAt: null,
524
+ },
525
+ },
526
+ }
527
+ }
528
+
529
+ if (topic.type === "terminal") {
530
+ return {
531
+ v: PROTOCOL_VERSION,
532
+ type: "snapshot",
533
+ id,
534
+ snapshot: {
535
+ type: "terminal",
536
+ data: terminals.getSnapshot(topic.terminalId),
537
+ },
538
+ }
539
+ }
540
+
541
+ if (topic.type === "project-git") {
542
+ return {
543
+ v: PROTOCOL_VERSION,
544
+ type: "snapshot",
545
+ id,
546
+ snapshot: {
547
+ type: "project-git",
548
+ data: store.getProject(topic.projectId)
549
+ ? resolvedDiffStore.getProjectSnapshot(topic.projectId)
550
+ : null,
551
+ },
552
+ }
553
+ }
554
+
555
+ return {
556
+ v: PROTOCOL_VERSION,
557
+ type: "snapshot",
558
+ id,
559
+ snapshot: {
560
+ type: "chat",
561
+ data: deriveChatSnapshot(
562
+ store.state,
563
+ agent.getActiveStatuses(),
564
+ agent.getDrainingChatIds(),
565
+ agent.getSlashCommandsLoadingChatIds(),
566
+ topic.chatId,
567
+ (chatId) => store.getRecentChatHistory(chatId, topic.recentLimit ?? DEFAULT_CHAT_RECENT_LIMIT),
568
+ (chatId) => store.getTunnelEvents(chatId)
569
+ ),
570
+ },
571
+ }
572
+ }
573
+
574
+ async function pushSnapshots(
575
+ ws: ServerWebSocket<ClientState>,
576
+ options?: { skipPrune?: boolean; filter?: SnapshotBroadcastFilter; cache?: SnapshotComputationCache }
577
+ ) {
578
+ const pushStartedAt = performance.now()
579
+ if (!options?.skipPrune) {
580
+ await maybePruneStaleEmptyChats([ws])
581
+ }
582
+ const snapshotSignatures = ensureSnapshotSignatures(ws)
583
+ let sentCount = 0
584
+ let skippedCount = 0
585
+ for (const [id, topic] of ws.data.subscriptions.entries()) {
586
+ if (!shouldIncludeTopic(topic, options?.filter)) {
587
+ continue
588
+ }
589
+ const envelopeStartedAt = performance.now()
590
+ const envelope = createEnvelope(id, topic, options?.cache)
591
+ const createdAt = performance.now()
592
+ if (envelope.type !== "snapshot") continue
593
+ const signature = topic.type === "sidebar"
594
+ ? getSidebarSnapshotCacheEntry(options?.cache).signature
595
+ : JSON.stringify(envelope.snapshot)
596
+ const signatureReadyAt = topic.type === "sidebar" ? createdAt : performance.now()
597
+ if (snapshotSignatures.get(id) === signature) {
598
+ skippedCount += 1
599
+ continue
600
+ }
601
+ snapshotSignatures.set(id, signature)
602
+ if (topic.type === "chat" && envelope.snapshot.type === "chat" && envelope.snapshot.data?.runtime.status === "starting") {
603
+ const profile = agent.getActiveTurnProfile(topic.chatId)
604
+ logSendToStartingProfile(profile?.traceId, profile?.startedAt, "ws.snapshot_sent", {
605
+ chatId: topic.chatId,
606
+ status: envelope.snapshot.data.runtime.status,
607
+ messageCount: envelope.snapshot.data.messages.length,
608
+ buildMs: Number((createdAt - envelopeStartedAt).toFixed(1)),
609
+ signatureMs: Number((signatureReadyAt - createdAt).toFixed(1)),
610
+ signatureBytes: signature.length,
611
+ })
612
+ }
613
+ const payloadBytes = send(ws, envelope)
614
+ sentCount += 1
615
+ if (topic.type === "chat" && envelope.snapshot.type === "chat" && envelope.snapshot.data?.runtime.status === "starting") {
616
+ const profile = agent.getActiveTurnProfile(topic.chatId)
617
+ logSendToStartingProfile(profile?.traceId, profile?.startedAt, "ws.snapshot_send_completed", {
618
+ chatId: topic.chatId,
619
+ payloadBytes,
620
+ })
621
+ }
622
+ }
623
+ if (isSendToStartingProfilingEnabled()) {
624
+ console.log("[kanna/send->starting][server]", JSON.stringify({
625
+ stage: "ws.push_snapshots_completed",
626
+ elapsedMs: Number((performance.now() - pushStartedAt).toFixed(1)),
627
+ skipPrune: Boolean(options?.skipPrune),
628
+ sentCount,
629
+ skippedCount,
630
+ ...countSubscriptionsByTopic(ws),
631
+ }))
632
+ }
633
+ }
634
+
635
+ async function broadcastSnapshots() {
636
+ const startedAt = performance.now()
637
+ let socketCount = 0
638
+ const cache: SnapshotComputationCache = {}
639
+ for (const ws of sockets) {
640
+ socketCount += 1
641
+ await pushSnapshots(ws, { skipPrune: true, cache })
642
+ }
643
+ if (isSendToStartingProfilingEnabled()) {
644
+ console.log("[kanna/send->starting][server]", JSON.stringify({
645
+ stage: "ws.broadcast_snapshots_completed",
646
+ elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
647
+ pruneMs: 0,
648
+ socketCount,
649
+ totalChatCount: store.state.chatsById.size,
650
+ totalProjectCount: store.state.projectsById.size,
651
+ }))
652
+ }
653
+ }
654
+
655
+ async function broadcastFilteredSnapshots(filter: SnapshotBroadcastFilter) {
656
+ const startedAt = performance.now()
657
+ let socketCount = 0
658
+ const cache: SnapshotComputationCache = {}
659
+ for (const ws of sockets) {
660
+ socketCount += 1
661
+ await pushSnapshots(ws, { skipPrune: true, filter, cache })
662
+ }
663
+ if (isSendToStartingProfilingEnabled()) {
664
+ console.log("[kanna/send->starting][server]", JSON.stringify({
665
+ stage: "ws.broadcast_filtered_snapshots_completed",
666
+ elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
667
+ socketCount,
668
+ includeSidebar: Boolean(filter.includeSidebar),
669
+ chatCount: filter.chatIds?.size ?? 0,
670
+ projectCount: filter.projectIds?.size ?? 0,
671
+ }))
672
+ }
673
+ }
674
+
675
+ function scheduleBroadcast() {
676
+ pendingBroadcastAll = true
677
+ pendingBroadcastChatIds.clear()
678
+ if (pendingBroadcastTimer) {
679
+ return
680
+ }
681
+ pendingBroadcastTimer = setTimeout(() => {
682
+ pendingBroadcastTimer = null
683
+ const shouldBroadcastAll = pendingBroadcastAll
684
+ const chatIds = new Set(pendingBroadcastChatIds)
685
+ pendingBroadcastAll = false
686
+ pendingBroadcastChatIds.clear()
687
+ if (shouldBroadcastAll) {
688
+ void broadcastSnapshots()
689
+ return
690
+ }
691
+ if (chatIds.size > 0) {
692
+ void broadcastFilteredSnapshots({
693
+ includeSidebar: true,
694
+ chatIds,
695
+ })
696
+ }
697
+ }, 16)
698
+ }
699
+
700
+ function scheduleChatStateBroadcast(chatId: string) {
701
+ if (!pendingBroadcastAll) {
702
+ pendingBroadcastChatIds.add(chatId)
703
+ }
704
+ if (pendingBroadcastTimer) {
705
+ return
706
+ }
707
+ pendingBroadcastTimer = setTimeout(() => {
708
+ pendingBroadcastTimer = null
709
+ const shouldBroadcastAll = pendingBroadcastAll
710
+ const chatIds = new Set(pendingBroadcastChatIds)
711
+ pendingBroadcastAll = false
712
+ pendingBroadcastChatIds.clear()
713
+ if (shouldBroadcastAll) {
714
+ void broadcastSnapshots()
715
+ return
716
+ }
717
+ if (chatIds.size > 0) {
718
+ void broadcastFilteredSnapshots({
719
+ includeSidebar: true,
720
+ chatIds,
721
+ })
722
+ }
723
+ }, 16)
724
+ }
725
+
726
+ async function broadcastChatAndSidebar(chatId: string) {
727
+ await broadcastFilteredSnapshots({
728
+ includeSidebar: true,
729
+ chatIds: new Set([chatId]),
730
+ })
731
+ }
732
+
733
+ async function broadcastChatStateImmediately(chatId: string) {
734
+ await broadcastChatAndSidebar(chatId)
735
+ }
736
+
737
+ function broadcastError(message: string) {
738
+ for (const ws of sockets) {
739
+ send(ws, {
740
+ v: PROTOCOL_VERSION,
741
+ type: "error",
742
+ message,
743
+ })
744
+ }
745
+ }
746
+
747
+ function pushTerminalSnapshot(terminalId: string) {
748
+ for (const ws of sockets) {
749
+ const snapshotSignatures = ensureSnapshotSignatures(ws)
750
+ for (const [id, topic] of ws.data.subscriptions.entries()) {
751
+ if (topic.type !== "terminal" || topic.terminalId !== terminalId) continue
752
+ const envelope = createEnvelope(id, topic)
753
+ if (envelope.type !== "snapshot") continue
754
+ const signature = JSON.stringify(envelope.snapshot)
755
+ if (snapshotSignatures.get(id) === signature) continue
756
+ snapshotSignatures.set(id, signature)
757
+ send(ws, envelope)
758
+ }
759
+ }
760
+ }
761
+
762
+ function pushTerminalEvent(terminalId: string, event: Extract<ServerEnvelope, { type: "event" }>["event"]) {
763
+ for (const ws of sockets) {
764
+ for (const [id, topic] of ws.data.subscriptions.entries()) {
765
+ if (topic.type !== "terminal" || topic.terminalId !== terminalId) continue
766
+ send(ws, {
767
+ v: PROTOCOL_VERSION,
768
+ type: "event",
769
+ id,
770
+ event,
771
+ })
772
+ }
773
+ }
774
+ }
775
+
776
+ const disposeTerminalEvents = terminals.onEvent((event) => {
777
+ pushTerminalEvent(event.terminalId, event)
778
+ })
779
+
780
+ const disposeKeybindingEvents = keybindings.onChange(() => {
781
+ for (const ws of sockets) {
782
+ const snapshotSignatures = ensureSnapshotSignatures(ws)
783
+ for (const [id, topic] of ws.data.subscriptions.entries()) {
784
+ if (topic.type !== "keybindings") continue
785
+ const envelope = createEnvelope(id, topic)
786
+ if (envelope.type !== "snapshot") continue
787
+ const signature = JSON.stringify(envelope.snapshot)
788
+ if (snapshotSignatures.get(id) === signature) continue
789
+ snapshotSignatures.set(id, signature)
790
+ send(ws, envelope)
791
+ }
792
+ }
793
+ })
794
+
795
+ const disposeAppSettingsEvents = resolvedAppSettings.onChange(() => {
796
+ for (const ws of sockets) {
797
+ const snapshotSignatures = ensureSnapshotSignatures(ws)
798
+ for (const [id, topic] of ws.data.subscriptions.entries()) {
799
+ if (topic.type !== "app-settings") continue
800
+ const envelope = createEnvelope(id, topic)
801
+ if (envelope.type !== "snapshot") continue
802
+ const signature = JSON.stringify(envelope.snapshot)
803
+ if (snapshotSignatures.get(id) === signature) continue
804
+ snapshotSignatures.set(id, signature)
805
+ send(ws, envelope)
806
+ }
807
+ }
808
+ })
809
+
810
+ const disposeUpdateEvents = updateManager?.onChange(() => {
811
+ for (const ws of sockets) {
812
+ const snapshotSignatures = ensureSnapshotSignatures(ws)
813
+ for (const [id, topic] of ws.data.subscriptions.entries()) {
814
+ if (topic.type !== "update") continue
815
+ const envelope = createEnvelope(id, topic)
816
+ if (envelope.type !== "snapshot") continue
817
+ const signature = JSON.stringify(envelope.snapshot)
818
+ if (snapshotSignatures.get(id) === signature) continue
819
+ snapshotSignatures.set(id, signature)
820
+ send(ws, envelope)
821
+ }
822
+ }
823
+ }) ?? (() => {})
824
+
825
+ agent.setBackgroundErrorReporter?.(broadcastError)
826
+
827
+ function resolveChatProject(chatId: string) {
828
+ const chat = store.getChat(chatId)
829
+ if (!chat) throw new Error("Chat not found")
830
+ const project = store.getProject(chat.projectId)
831
+ if (!project) throw new Error("Project not found")
832
+ return { chat, project }
833
+ }
834
+
835
+ async function handleCommand(ws: ServerWebSocket<ClientState>, message: Extract<ClientEnvelope, { type: "command" }>) {
836
+ const { command, id } = message
837
+ try {
838
+ switch (command.type) {
839
+ case "system.ping": {
840
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
841
+ return
842
+ }
843
+ case "update.check": {
844
+ const snapshot = updateManager
845
+ ? await updateManager.checkForUpdates({ force: command.force })
846
+ : {
847
+ currentVersion: "unknown",
848
+ latestVersion: null,
849
+ status: "error",
850
+ updateAvailable: false,
851
+ lastCheckedAt: Date.now(),
852
+ error: "Update manager unavailable.",
853
+ installAction: "restart",
854
+ reloadRequestedAt: null,
855
+ }
856
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
857
+ return
858
+ }
859
+ case "update.install": {
860
+ if (!updateManager) {
861
+ throw new Error("Update manager unavailable.")
862
+ }
863
+ const result = await updateManager.installUpdate()
864
+ send(ws, {
865
+ v: PROTOCOL_VERSION,
866
+ type: "ack",
867
+ id,
868
+ result,
869
+ })
870
+ return
871
+ }
872
+ case "update.reload": {
873
+ if (!updateManager) {
874
+ throw new Error("Update manager unavailable.")
875
+ }
876
+ const result = await updateManager.forceReload()
877
+ send(ws, {
878
+ v: PROTOCOL_VERSION,
879
+ type: "ack",
880
+ id,
881
+ result,
882
+ })
883
+ return
884
+ }
885
+ case "settings.readKeybindings": {
886
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: keybindings.getSnapshot() })
887
+ return
888
+ }
889
+ case "settings.writeKeybindings": {
890
+ const snapshot = await keybindings.write(command.bindings)
891
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
892
+ return
893
+ }
894
+ case "settings.readAppSettings": {
895
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: resolvedAppSettings.getSnapshot() })
896
+ return
897
+ }
898
+ case "settings.writeAppSettings": {
899
+ const previousAnalyticsEnabled = resolvedAppSettings.getSnapshot().analyticsEnabled
900
+ if (previousAnalyticsEnabled && !command.analyticsEnabled) {
901
+ resolvedAnalytics.track("analytics_disabled")
902
+ }
903
+ const snapshot = await resolvedAppSettings.write({ analyticsEnabled: command.analyticsEnabled })
904
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
905
+ if (!previousAnalyticsEnabled && command.analyticsEnabled) {
906
+ resolvedAnalytics.track("analytics_enabled")
907
+ }
908
+ return
909
+ }
910
+ case "appSettings.setCloudflareTunnel": {
911
+ await resolvedAppSettings.setCloudflareTunnel(command.patch)
912
+ const snapshot = resolvedAppSettings.getSnapshot()
913
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
914
+ return
915
+ }
916
+ case "settings.writeAppSettingsPatch": {
917
+ const previousAnalyticsEnabled = resolvedAppSettings.getSnapshot().analyticsEnabled
918
+ const snapshot = await resolvedAppSettings.writePatch(command.patch)
919
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
920
+ if (command.patch.analyticsEnabled !== undefined && previousAnalyticsEnabled && !snapshot.analyticsEnabled) {
921
+ resolvedAnalytics.track("analytics_disabled")
922
+ }
923
+ if (command.patch.analyticsEnabled !== undefined && !previousAnalyticsEnabled && snapshot.analyticsEnabled) {
924
+ resolvedAnalytics.track("analytics_enabled")
925
+ }
926
+ return
927
+ }
928
+ case "settings.readLlmProvider": {
929
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: await resolvedLlmProvider.read() })
930
+ return
931
+ }
932
+ case "settings.writeLlmProvider": {
933
+ const snapshot = await resolvedLlmProvider.write({
934
+ provider: command.provider,
935
+ apiKey: command.apiKey,
936
+ model: command.model,
937
+ baseUrl: command.baseUrl,
938
+ })
939
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
940
+ return
941
+ }
942
+ case "settings.validateLlmProvider": {
943
+ const result = await resolvedLlmProvider.validate({
944
+ provider: command.provider,
945
+ apiKey: command.apiKey,
946
+ model: command.model,
947
+ baseUrl: command.baseUrl,
948
+ })
949
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
950
+ return
951
+ }
952
+ case "project.open": {
953
+ await ensureProjectDirectory(command.localPath)
954
+ const normalizedPath = resolveLocalPath(command.localPath)
955
+ const existingProjectId = store.state.projectIdsByPath.get(normalizedPath)
956
+ const project = await store.openProject(command.localPath)
957
+ await refreshDiscovery()
958
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { projectId: project.id } })
959
+ if (!existingProjectId) {
960
+ resolvedAnalytics.track("project_opened")
961
+ }
962
+ break
963
+ }
964
+ case "project.create": {
965
+ await ensureProjectDirectory(command.localPath)
966
+ const normalizedPath = resolveLocalPath(command.localPath)
967
+ const existingProjectId = store.state.projectIdsByPath.get(normalizedPath)
968
+ const project = await store.openProject(command.localPath, command.title)
969
+ await refreshDiscovery()
970
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { projectId: project.id } })
971
+ if (!existingProjectId) {
972
+ resolvedAnalytics.track("project_opened")
973
+ resolvedAnalytics.track("project_created")
974
+ }
975
+ break
976
+ }
977
+ case "sessions.importClaude": {
978
+ const result = await importClaudeSessions({ store })
979
+ if (result.newProjects > 0) {
980
+ await refreshDiscovery()
981
+ }
982
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
983
+ await broadcastFilteredSnapshots({ includeSidebar: true })
984
+ break
985
+ }
986
+ case "project.remove": {
987
+ await store.removeProject(command.projectId)
988
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
989
+ resolvedAnalytics.track("project_removed")
990
+ break
991
+ }
992
+ case "sidebar.reorderProjectGroups": {
993
+ await store.setSidebarProjectOrder(command.projectIds)
994
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
995
+ await broadcastFilteredSnapshots({ includeSidebar: true })
996
+ return
997
+ }
998
+ case "project.readDiffPatch": {
999
+ const project = store.getProject(command.projectId)
1000
+ if (!project) {
1001
+ throw new Error("Project not found")
1002
+ }
1003
+ const result = await resolvedDiffStore.readPatch({
1004
+ projectPath: project.localPath,
1005
+ path: command.path,
1006
+ })
1007
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1008
+ return
1009
+ }
1010
+ case "system.openExternal": {
1011
+ await openExternal(command)
1012
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1013
+ break
1014
+ }
1015
+ case "chat.create": {
1016
+ const chat = await store.createChat(command.projectId)
1017
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { chatId: chat.id } })
1018
+ resolvedAnalytics.track("chat_created")
1019
+ await broadcastChatAndSidebar(chat.id)
1020
+ return
1021
+ }
1022
+ case "chat.fork": {
1023
+ const result = await agent.forkChat(command.chatId)
1024
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1025
+ await broadcastFilteredSnapshots({ includeSidebar: true })
1026
+ return
1027
+ }
1028
+ case "chat.rename": {
1029
+ await store.renameChat(command.chatId, command.title)
1030
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1031
+ await broadcastChatAndSidebar(command.chatId)
1032
+ return
1033
+ }
1034
+ case "chat.archive": {
1035
+ await store.archiveChat(command.chatId)
1036
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1037
+ await broadcastFilteredSnapshots({ includeSidebar: true })
1038
+ return
1039
+ }
1040
+ case "chat.unarchive": {
1041
+ await store.unarchiveChat(command.chatId)
1042
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1043
+ await broadcastChatAndSidebar(command.chatId)
1044
+ return
1045
+ }
1046
+ case "chat.delete": {
1047
+ await agent.cancel(command.chatId)
1048
+ for (const scheduleId of agent.listLiveSchedules(command.chatId)) {
1049
+ await agent.cancelAutoContinue(command.chatId, scheduleId, "chat_deleted")
1050
+ }
1051
+ await agent.closeChat(command.chatId)
1052
+ await store.deleteChat(command.chatId)
1053
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1054
+ resolvedAnalytics.track("chat_deleted")
1055
+ await broadcastFilteredSnapshots({ includeSidebar: true })
1056
+ return
1057
+ }
1058
+ case "autoContinue.accept": {
1059
+ await agent.acceptAutoContinue(command.chatId, command.scheduleId, command.scheduledAt)
1060
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1061
+ await broadcastChatAndSidebar(command.chatId)
1062
+ return
1063
+ }
1064
+ case "autoContinue.reschedule": {
1065
+ await agent.rescheduleAutoContinue(command.chatId, command.scheduleId, command.scheduledAt)
1066
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1067
+ await broadcastChatAndSidebar(command.chatId)
1068
+ return
1069
+ }
1070
+ case "autoContinue.cancel": {
1071
+ await agent.cancelAutoContinue(command.chatId, command.scheduleId, "user")
1072
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1073
+ await broadcastChatAndSidebar(command.chatId)
1074
+ return
1075
+ }
1076
+ case "tunnel.accept": {
1077
+ if (tunnelGateway) {
1078
+ await tunnelGateway.accept(command.chatId, command.tunnelId)
1079
+ }
1080
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1081
+ await broadcastChatAndSidebar(command.chatId)
1082
+ return
1083
+ }
1084
+ case "tunnel.stop": {
1085
+ if (tunnelGateway) {
1086
+ await tunnelGateway.stop(command.chatId, command.tunnelId)
1087
+ }
1088
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1089
+ await broadcastChatAndSidebar(command.chatId)
1090
+ return
1091
+ }
1092
+ case "tunnel.retry": {
1093
+ if (tunnelGateway) {
1094
+ await tunnelGateway.retry(command.chatId, command.tunnelId)
1095
+ }
1096
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1097
+ await broadcastChatAndSidebar(command.chatId)
1098
+ return
1099
+ }
1100
+ case "chat.markRead": {
1101
+ await store.setChatReadState(command.chatId, false)
1102
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1103
+ await broadcastChatAndSidebar(command.chatId)
1104
+ return
1105
+ }
1106
+ case "chat.setDraftProtection": {
1107
+ ws.data.protectedDraftChatIds = new Set(command.chatIds)
1108
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1109
+ break
1110
+ }
1111
+ case "chat.send": {
1112
+ const result = await agent.send(command)
1113
+ const profile = command.clientTraceId && result.chatId
1114
+ ? agent.getActiveTurnProfile(result.chatId)
1115
+ : null
1116
+ logSendToStartingProfile(profile?.traceId ?? command.clientTraceId, profile?.startedAt, "ws.chat_send_ack", {
1117
+ chatId: result.chatId ?? null,
1118
+ })
1119
+ const payloadBytes = send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1120
+ logSendToStartingProfile(profile?.traceId ?? command.clientTraceId, profile?.startedAt, "ws.chat_send_ack_completed", {
1121
+ chatId: result.chatId ?? null,
1122
+ payloadBytes,
1123
+ })
1124
+ return
1125
+ }
1126
+ case "chat.refreshDiffs": {
1127
+ const { project } = resolveChatProject(command.chatId)
1128
+ const changed = await resolvedDiffStore.refreshSnapshot(project.id, project.localPath)
1129
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1130
+ if (changed) {
1131
+ void broadcastSnapshots()
1132
+ }
1133
+ return
1134
+ }
1135
+ case "chat.initGit": {
1136
+ const { project } = resolveChatProject(command.chatId)
1137
+ const result = await resolvedDiffStore.initializeGit({
1138
+ projectId: project.id,
1139
+ projectPath: project.localPath,
1140
+ })
1141
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1142
+ if (result.snapshotChanged) {
1143
+ void broadcastSnapshots()
1144
+ }
1145
+ return
1146
+ }
1147
+ case "chat.getGitHubPublishInfo": {
1148
+ const { project } = resolveChatProject(command.chatId)
1149
+ const result = await resolvedDiffStore.getGitHubPublishInfo({
1150
+ projectPath: project.localPath,
1151
+ })
1152
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1153
+ return
1154
+ }
1155
+ case "chat.checkGitHubRepoAvailability": {
1156
+ const result = await resolvedDiffStore.checkGitHubRepoAvailability({
1157
+ owner: command.owner,
1158
+ name: command.name,
1159
+ })
1160
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1161
+ return
1162
+ }
1163
+ case "chat.publishToGitHub": {
1164
+ const { project } = resolveChatProject(command.chatId)
1165
+ const result = await resolvedDiffStore.publishToGitHub({
1166
+ projectId: project.id,
1167
+ projectPath: project.localPath,
1168
+ owner: command.owner,
1169
+ name: command.name,
1170
+ visibility: command.visibility,
1171
+ description: command.description,
1172
+ })
1173
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1174
+ if (result.snapshotChanged) {
1175
+ void broadcastSnapshots()
1176
+ }
1177
+ return
1178
+ }
1179
+ case "chat.listBranches": {
1180
+ const { project } = resolveChatProject(command.chatId)
1181
+ const result = await resolvedDiffStore.listBranches({
1182
+ projectPath: project.localPath,
1183
+ })
1184
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1185
+ return
1186
+ }
1187
+ case "chat.previewMergeBranch": {
1188
+ const { project } = resolveChatProject(command.chatId)
1189
+ const result = await resolvedDiffStore.previewMergeBranch({
1190
+ projectPath: project.localPath,
1191
+ branch: command.branch,
1192
+ })
1193
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1194
+ return
1195
+ }
1196
+ case "chat.mergeBranch": {
1197
+ const { project } = resolveChatProject(command.chatId)
1198
+ const result = await resolvedDiffStore.mergeBranch({
1199
+ projectId: project.id,
1200
+ projectPath: project.localPath,
1201
+ branch: command.branch,
1202
+ })
1203
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1204
+ if (result.snapshotChanged) {
1205
+ void broadcastSnapshots()
1206
+ }
1207
+ return
1208
+ }
1209
+ case "chat.checkoutBranch": {
1210
+ const { project } = resolveChatProject(command.chatId)
1211
+ const result = await resolvedDiffStore.checkoutBranch({
1212
+ projectId: project.id,
1213
+ projectPath: project.localPath,
1214
+ branch: command.branch,
1215
+ bringChanges: command.bringChanges,
1216
+ })
1217
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1218
+ if (result.snapshotChanged) {
1219
+ void broadcastSnapshots()
1220
+ }
1221
+ return
1222
+ }
1223
+ case "chat.syncBranch": {
1224
+ const { project } = resolveChatProject(command.chatId)
1225
+ const result = await resolvedDiffStore.syncBranch({
1226
+ projectId: project.id,
1227
+ projectPath: project.localPath,
1228
+ action: command.action,
1229
+ })
1230
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1231
+ if (result.snapshotChanged) {
1232
+ void broadcastSnapshots()
1233
+ }
1234
+ return
1235
+ }
1236
+ case "chat.createBranch": {
1237
+ const { project } = resolveChatProject(command.chatId)
1238
+ const result = await resolvedDiffStore.createBranch({
1239
+ projectId: project.id,
1240
+ projectPath: project.localPath,
1241
+ name: command.name,
1242
+ baseBranchName: command.baseBranchName,
1243
+ })
1244
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1245
+ if (result.snapshotChanged) {
1246
+ void broadcastSnapshots()
1247
+ }
1248
+ return
1249
+ }
1250
+ case "chat.generateCommitMessage": {
1251
+ const { project } = resolveChatProject(command.chatId)
1252
+ const result = await resolvedDiffStore.generateCommitMessage({
1253
+ projectPath: project.localPath,
1254
+ paths: command.paths,
1255
+ })
1256
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1257
+ return
1258
+ }
1259
+ case "chat.commitDiffs": {
1260
+ const { project } = resolveChatProject(command.chatId)
1261
+ const result = await resolvedDiffStore.commitFiles({
1262
+ projectId: project.id,
1263
+ projectPath: project.localPath,
1264
+ paths: command.paths,
1265
+ summary: command.summary,
1266
+ description: command.description,
1267
+ mode: command.mode,
1268
+ })
1269
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1270
+ if (result.snapshotChanged) {
1271
+ void broadcastSnapshots()
1272
+ }
1273
+ return
1274
+ }
1275
+ case "chat.discardDiffFile": {
1276
+ const { project } = resolveChatProject(command.chatId)
1277
+ const result = await resolvedDiffStore.discardFile({
1278
+ projectId: project.id,
1279
+ projectPath: project.localPath,
1280
+ path: command.path,
1281
+ })
1282
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1283
+ if (result.snapshotChanged) {
1284
+ void broadcastSnapshots()
1285
+ }
1286
+ return
1287
+ }
1288
+ case "chat.ignoreDiffFile": {
1289
+ const { project } = resolveChatProject(command.chatId)
1290
+ const result = await resolvedDiffStore.ignoreFile({
1291
+ projectId: project.id,
1292
+ projectPath: project.localPath,
1293
+ path: command.path,
1294
+ })
1295
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1296
+ if (result.snapshotChanged) {
1297
+ void broadcastSnapshots()
1298
+ }
1299
+ return
1300
+ }
1301
+ case "chat.cancel": {
1302
+ await agent.cancel(command.chatId)
1303
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1304
+ return
1305
+ }
1306
+ case "chat.stopDraining": {
1307
+ await agent.stopDraining(command.chatId)
1308
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1309
+ return
1310
+ }
1311
+ case "chat.exportStandalone": {
1312
+ const { chat, project } = resolveChatProject(command.chatId)
1313
+ const result = await writeStandaloneTranscriptExport({
1314
+ chatId: chat.id,
1315
+ title: chat.title,
1316
+ localPath: project.localPath,
1317
+ theme: command.theme,
1318
+ attachmentMode: command.attachmentMode,
1319
+ messages: store.getMessages(command.chatId),
1320
+ })
1321
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1322
+ return
1323
+ }
1324
+ case "chat.loadHistory": {
1325
+ const chat = store.getChat(command.chatId)
1326
+ if (!chat) throw new Error("Chat not found")
1327
+ const page = store.getMessagesPageBefore(command.chatId, command.beforeCursor, command.limit)
1328
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: page })
1329
+ return
1330
+ }
1331
+ case "chat.respondTool": {
1332
+ await agent.respondTool(command)
1333
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1334
+ return
1335
+ }
1336
+ case "message.enqueue": {
1337
+ const result = await agent.enqueue(command)
1338
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
1339
+ await broadcastChatAndSidebar(command.chatId)
1340
+ return
1341
+ }
1342
+ case "message.steer": {
1343
+ await agent.steer(command)
1344
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1345
+ await broadcastChatAndSidebar(command.chatId)
1346
+ return
1347
+ }
1348
+ case "message.dequeue": {
1349
+ await agent.dequeue(command)
1350
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1351
+ await broadcastChatAndSidebar(command.chatId)
1352
+ return
1353
+ }
1354
+ case "terminal.create": {
1355
+ const project = store.getProject(command.projectId)
1356
+ if (!project) {
1357
+ throw new Error("Project not found")
1358
+ }
1359
+ const snapshot = terminals.createTerminal({
1360
+ projectPath: project.localPath,
1361
+ terminalId: command.terminalId,
1362
+ cols: command.cols,
1363
+ rows: command.rows,
1364
+ scrollback: command.scrollback,
1365
+ })
1366
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
1367
+ return
1368
+ }
1369
+ case "terminal.input": {
1370
+ terminals.write(command.terminalId, command.data)
1371
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1372
+ return
1373
+ }
1374
+ case "terminal.resize": {
1375
+ terminals.resize(command.terminalId, command.cols, command.rows)
1376
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1377
+ return
1378
+ }
1379
+ case "terminal.close": {
1380
+ terminals.close(command.terminalId)
1381
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
1382
+ pushTerminalSnapshot(command.terminalId)
1383
+ return
1384
+ }
1385
+ }
1386
+
1387
+ await broadcastSnapshots()
1388
+ } catch (error) {
1389
+ const messageText = error instanceof Error ? error.message : String(error)
1390
+ console.error("[ws-router] command failed", {
1391
+ id,
1392
+ type: command.type,
1393
+ message: messageText,
1394
+ })
1395
+ send(ws, { v: PROTOCOL_VERSION, type: "error", id, message: messageText })
1396
+ }
1397
+ }
1398
+
1399
+ return {
1400
+ handleOpen(ws: ServerWebSocket<ClientState>) {
1401
+ sockets.add(ws)
1402
+ },
1403
+ handleClose(ws: ServerWebSocket<ClientState>) {
1404
+ sockets.delete(ws)
1405
+ },
1406
+ broadcastSnapshots,
1407
+ broadcastChatStateImmediately,
1408
+ scheduleBroadcast,
1409
+ scheduleChatStateBroadcast,
1410
+ pruneStaleEmptyChats: () => maybePruneStaleEmptyChats(),
1411
+ async handleMessage(ws: ServerWebSocket<ClientState>, raw: string | Buffer | ArrayBuffer | Uint8Array) {
1412
+ let parsed: unknown
1413
+ try {
1414
+ parsed = JSON.parse(String(raw))
1415
+ } catch {
1416
+ send(ws, { v: PROTOCOL_VERSION, type: "error", message: "Invalid JSON" })
1417
+ return
1418
+ }
1419
+
1420
+ if (!isClientEnvelope(parsed)) {
1421
+ send(ws, { v: PROTOCOL_VERSION, type: "error", message: "Invalid envelope" })
1422
+ return
1423
+ }
1424
+
1425
+ if (parsed.type === "subscribe") {
1426
+ const snapshotSignatures = ensureSnapshotSignatures(ws)
1427
+ ws.data.subscriptions.set(parsed.id, parsed.topic)
1428
+ snapshotSignatures.delete(parsed.id)
1429
+ if (parsed.topic.type === "chat") {
1430
+ void agent.ensureSlashCommandsLoaded(parsed.topic.chatId)
1431
+ }
1432
+ if (parsed.topic.type === "local-projects") {
1433
+ void refreshDiscovery().then(() => {
1434
+ if (ws.data.subscriptions.has(parsed.id)) {
1435
+ void pushSnapshots(ws, { skipPrune: true })
1436
+ }
1437
+ })
1438
+ return
1439
+ }
1440
+ await pushSnapshots(ws, { skipPrune: true })
1441
+ return
1442
+ }
1443
+
1444
+ if (parsed.type === "unsubscribe") {
1445
+ const snapshotSignatures = ensureSnapshotSignatures(ws)
1446
+ ws.data.subscriptions.delete(parsed.id)
1447
+ snapshotSignatures.delete(parsed.id)
1448
+ send(ws, { v: PROTOCOL_VERSION, type: "ack", id: parsed.id })
1449
+ return
1450
+ }
1451
+
1452
+ await handleCommand(ws, parsed)
1453
+ },
1454
+ dispose() {
1455
+ if (pendingBroadcastTimer) {
1456
+ clearTimeout(pendingBroadcastTimer)
1457
+ }
1458
+ agent.setBackgroundErrorReporter?.(null)
1459
+ disposeTerminalEvents()
1460
+ disposeKeybindingEvents()
1461
+ disposeAppSettingsEvents()
1462
+ disposeUpdateEvents()
1463
+ },
1464
+ }
1465
+ }