@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,79 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { QuickResponseAdapter } from "./quick-response"
3
+ import { generateCommitMessageDetailed } from "./generate-commit-message"
4
+
5
+ describe("generateCommitMessageDetailed", () => {
6
+ test("returns sanitized generated subject and body", async () => {
7
+ const result = await generateCommitMessageDetailed(
8
+ {
9
+ cwd: "/tmp/project",
10
+ branchName: "feature/test",
11
+ files: [{
12
+ path: "app.ts",
13
+ changeType: "modified",
14
+ patch: "diff --git a/app.ts b/app.ts\n--- a/app.ts\n+++ b/app.ts\n@@\n-old\n+new\n",
15
+ }],
16
+ },
17
+ new QuickResponseAdapter({
18
+ readLlmProvider: async () => ({
19
+ provider: "openai",
20
+ apiKey: "",
21
+ model: "",
22
+ baseUrl: "",
23
+ resolvedBaseUrl: "https://api.openai.com/v1",
24
+ enabled: false,
25
+ warning: null,
26
+ filePathDisplay: "~/.kanna/llm-provider.json",
27
+ }),
28
+ runClaudeStructured: async () => ({
29
+ subject: " Add login UI.\nextra line",
30
+ body: " - wire form\n- add validation ",
31
+ }),
32
+ })
33
+ )
34
+
35
+ expect(result).toEqual({
36
+ subject: "Add login UI",
37
+ body: "- wire form\n- add validation",
38
+ usedFallback: false,
39
+ failureMessage: null,
40
+ })
41
+ })
42
+
43
+ test("falls back when providers fail", async () => {
44
+ const result = await generateCommitMessageDetailed(
45
+ {
46
+ cwd: "/tmp/project",
47
+ branchName: "feature/test",
48
+ files: [{
49
+ path: "src/feature.ts",
50
+ changeType: "modified",
51
+ patch: "diff --git a/src/feature.ts b/src/feature.ts\n",
52
+ }],
53
+ },
54
+ new QuickResponseAdapter({
55
+ readLlmProvider: async () => ({
56
+ provider: "openai",
57
+ apiKey: "",
58
+ model: "",
59
+ baseUrl: "",
60
+ resolvedBaseUrl: "https://api.openai.com/v1",
61
+ enabled: false,
62
+ warning: null,
63
+ filePathDisplay: "~/.kanna/llm-provider.json",
64
+ }),
65
+ runClaudeStructured: async () => {
66
+ throw new Error("Not authenticated")
67
+ },
68
+ runCodexStructured: async () => null,
69
+ })
70
+ )
71
+
72
+ expect(result).toEqual({
73
+ subject: "Update feature.ts",
74
+ body: "",
75
+ usedFallback: true,
76
+ failureMessage: "claude failed commit message generation: Not authenticated; codex returned no result for commit message generation",
77
+ })
78
+ })
79
+ })
@@ -0,0 +1,126 @@
1
+ import path from "node:path"
2
+ import { QuickResponseAdapter } from "./quick-response"
3
+
4
+ interface CommitMessageFile {
5
+ path: string
6
+ changeType: "added" | "deleted" | "modified" | "renamed"
7
+ patch: string
8
+ }
9
+
10
+ const COMMIT_MESSAGE_SCHEMA = {
11
+ type: "object",
12
+ properties: {
13
+ subject: { type: "string" },
14
+ body: { type: "string" },
15
+ },
16
+ required: ["subject", "body"],
17
+ additionalProperties: false,
18
+ } as const
19
+
20
+ export interface GenerateCommitMessageResult {
21
+ subject: string
22
+ body: string
23
+ usedFallback: boolean
24
+ failureMessage: string | null
25
+ }
26
+
27
+ function summarizeFailures(failures: Array<{ provider: "openai" | "claude" | "codex"; reason: string }>) {
28
+ if (failures.length === 0) return null
29
+ return failures.map((failure) => failure.reason).join("; ")
30
+ }
31
+
32
+ function limitText(value: string, maxLength: number) {
33
+ return value.length <= maxLength ? value : `${value.slice(0, maxLength).trimEnd()}\n...[truncated]`
34
+ }
35
+
36
+ function sanitizeSubject(value: unknown): string | null {
37
+ if (typeof value !== "string") return null
38
+ const normalized = (value.split(/\r?\n/u)[0] ?? "")
39
+ .replace(/\s+/g, " ")
40
+ .trim()
41
+ .replace(/[.]+$/u, "")
42
+ .slice(0, 72)
43
+ .trim()
44
+ return normalized.length > 0 ? normalized : null
45
+ }
46
+
47
+ function sanitizeBody(value: unknown): string {
48
+ if (typeof value !== "string") return ""
49
+ return value.trim()
50
+ }
51
+
52
+ function fallbackSubject(files: CommitMessageFile[]) {
53
+ if (files.length === 1) {
54
+ const fileName = path.posix.basename(files[0]?.path ?? "file")
55
+ const normalized = `Update ${fileName}`.replace(/\s+/g, " ").trim()
56
+ return normalized.slice(0, 72).trim()
57
+ }
58
+
59
+ return `Update ${files.length} files`
60
+ }
61
+
62
+ function buildCommitMessagePrompt(args: {
63
+ branchName?: string
64
+ files: CommitMessageFile[]
65
+ }) {
66
+ const fileList = args.files.map((file) => `${file.changeType}: ${file.path}`).join("\n")
67
+ const combinedPatch = args.files.map((file) => file.patch).join("\n\n")
68
+
69
+ return [
70
+ "Generate a git commit message for the selected changes.",
71
+ "Return JSON with keys: subject, body.",
72
+ "Rules:",
73
+ "- subject must be imperative, under 72 chars, and have no trailing period",
74
+ "- body may be an empty string or 3-5 bullet points",
75
+ "- capture the primary user-visible or developer-visible change",
76
+ "",
77
+ `Branch: ${args.branchName ?? "current branch"}`,
78
+ "",
79
+ "Selected files:",
80
+ limitText(fileList, 6_000),
81
+ "",
82
+ "Selected patch:",
83
+ limitText(combinedPatch, 40_000),
84
+ ].join("\n")
85
+ }
86
+
87
+ export async function generateCommitMessageDetailed(
88
+ args: {
89
+ cwd: string
90
+ branchName?: string
91
+ files: CommitMessageFile[]
92
+ },
93
+ adapter = new QuickResponseAdapter()
94
+ ): Promise<GenerateCommitMessageResult> {
95
+ const result = await adapter.generateStructuredWithDiagnostics<{ subject: string; body: string }>({
96
+ cwd: args.cwd,
97
+ task: "commit message generation",
98
+ prompt: buildCommitMessagePrompt(args),
99
+ schema: COMMIT_MESSAGE_SCHEMA,
100
+ parse: (value) => {
101
+ const output = value && typeof value === "object" ? value as { subject?: unknown; body?: unknown } : {}
102
+ const subject = sanitizeSubject(output.subject)
103
+ if (!subject) return null
104
+ return {
105
+ subject,
106
+ body: sanitizeBody(output.body),
107
+ }
108
+ },
109
+ })
110
+
111
+ if (result.value) {
112
+ return {
113
+ subject: result.value.subject,
114
+ body: result.value.body,
115
+ usedFallback: false,
116
+ failureMessage: null,
117
+ }
118
+ }
119
+
120
+ return {
121
+ subject: fallbackSubject(args.files),
122
+ body: "",
123
+ usedFallback: true,
124
+ failureMessage: summarizeFailures(result.failures),
125
+ }
126
+ }
@@ -0,0 +1,76 @@
1
+ import { QuickResponseAdapter } from "./quick-response"
2
+
3
+ const TITLE_SCHEMA = {
4
+ type: "object",
5
+ properties: {
6
+ title: { type: "string" },
7
+ },
8
+ required: ["title"],
9
+ additionalProperties: false,
10
+ } as const
11
+
12
+ function normalizeGeneratedTitle(value: unknown): string | null {
13
+ if (typeof value !== "string") return null
14
+ const normalized = value.replace(/\s+/g, " ").trim().slice(0, 80)
15
+ if (!normalized || normalized === "New Chat") return null
16
+ return normalized
17
+ }
18
+
19
+ export function fallbackTitleFromMessage(messageContent: string): string | null {
20
+ const normalized = messageContent.replace(/\s+/g, " ").trim()
21
+ if (!normalized) return null
22
+ if (normalized.length <= 35) return normalized
23
+ return `${normalized.slice(0, 35)}...`
24
+ }
25
+
26
+ export interface GenerateChatTitleResult {
27
+ title: string | null
28
+ usedFallback: boolean
29
+ failureMessage: string | null
30
+ }
31
+
32
+ function summarizeFailures(failures: Array<{ provider: "openai" | "claude" | "codex"; reason: string }>) {
33
+ if (failures.length === 0) return null
34
+ return failures.map((failure) => failure.reason).join("; ")
35
+ }
36
+
37
+ export async function generateTitleForChat(
38
+ messageContent: string,
39
+ cwd: string,
40
+ adapter = new QuickResponseAdapter()
41
+ ): Promise<string | null> {
42
+ const result = await generateTitleForChatDetailed(messageContent, cwd, adapter)
43
+ return result.title
44
+ }
45
+
46
+ export async function generateTitleForChatDetailed(
47
+ messageContent: string,
48
+ cwd: string,
49
+ adapter = new QuickResponseAdapter()
50
+ ): Promise<GenerateChatTitleResult> {
51
+ const result = await adapter.generateStructuredWithDiagnostics<string>({
52
+ cwd,
53
+ task: "conversation title generation",
54
+ prompt: `Generate a short, descriptive title (under 30 chars) for a conversation that starts with this message.\n\n${messageContent}`,
55
+ schema: TITLE_SCHEMA,
56
+ parse: (value) => {
57
+ const output = value && typeof value === "object" ? value as { title?: unknown } : {}
58
+ return normalizeGeneratedTitle(output.title)
59
+ },
60
+ })
61
+
62
+ if (result.value) {
63
+ return {
64
+ title: result.value,
65
+ usedFallback: false,
66
+ failureMessage: null,
67
+ }
68
+ }
69
+
70
+ const fallbackTitle = fallbackTitleFromMessage(messageContent)
71
+ return {
72
+ title: fallbackTitle,
73
+ usedFallback: true,
74
+ failureMessage: summarizeFailures(result.failures),
75
+ }
76
+ }
@@ -0,0 +1,19 @@
1
+ import type { AccountInfo, AgentProvider, NormalizedToolCall, TranscriptEntry } from "../shared/types"
2
+
3
+ export interface HarnessEvent {
4
+ type: "transcript" | "session_token"
5
+ entry?: TranscriptEntry
6
+ sessionToken?: string
7
+ }
8
+
9
+ export interface HarnessToolRequest {
10
+ tool: NormalizedToolCall & { toolKind: "ask_user_question" | "exit_plan_mode" }
11
+ }
12
+
13
+ export interface HarnessTurn {
14
+ provider: AgentProvider
15
+ stream: AsyncIterable<HarnessEvent>
16
+ getAccountInfo?: () => Promise<AccountInfo | null>
17
+ interrupt: () => Promise<void>
18
+ close: () => void
19
+ }
@@ -0,0 +1,144 @@
1
+ import { afterEach, describe, expect, test } from "bun:test"
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises"
3
+ import { tmpdir } from "node:os"
4
+ import path from "node:path"
5
+ import { DEFAULT_KEYBINDINGS } from "../shared/types"
6
+ import { KeybindingsManager, normalizeKeybindings, readKeybindingsSnapshot } from "./keybindings"
7
+
8
+ let tempDirs: string[] = []
9
+ const TEST_FILE_PATH = "/tmp/kanna-test-keybindings.json"
10
+
11
+ afterEach(async () => {
12
+ await Promise.all(tempDirs.map((dir) => rm(dir, { recursive: true, force: true })))
13
+ tempDirs = []
14
+ })
15
+
16
+ async function createTempFilePath() {
17
+ const dir = await mkdtemp(path.join(tmpdir(), "kanna-keybindings-"))
18
+ tempDirs.push(dir)
19
+ return path.join(dir, "keybindings.json")
20
+ }
21
+
22
+ describe("normalizeKeybindings", () => {
23
+ test("falls back to defaults for invalid entries", () => {
24
+ const snapshot = normalizeKeybindings({
25
+ toggleEmbeddedTerminal: [],
26
+ toggleRightSidebar: "Ctrl+B",
27
+ }, TEST_FILE_PATH)
28
+
29
+ expect(snapshot.bindings).toEqual(DEFAULT_KEYBINDINGS)
30
+ expect(snapshot.warning).toContain("toggleEmbeddedTerminal")
31
+ expect(snapshot.warning).toContain("toggleRightSidebar")
32
+ })
33
+
34
+ test("keeps valid shortcut arrays", () => {
35
+ const snapshot = normalizeKeybindings({
36
+ toggleEmbeddedTerminal: [" Cmd+K ", "Ctrl+`"],
37
+ toggleRightSidebar: ["Ctrl+Shift+B"],
38
+ openInFinder: ["Cmd+Alt+F"],
39
+ openInEditor: ["Cmd+Shift+O"],
40
+ addSplitTerminal: ["Cmd+Shift+J"],
41
+ jumpToSidebarChat: ["Cmd+Alt"],
42
+ createChatInCurrentProject: ["Cmd+Alt+N"],
43
+ openAddProject: ["Cmd+Alt+O"],
44
+ }, TEST_FILE_PATH)
45
+
46
+ expect(snapshot).toEqual({
47
+ bindings: {
48
+ toggleEmbeddedTerminal: ["cmd+k", "ctrl+`"],
49
+ toggleRightSidebar: ["ctrl+shift+b"],
50
+ openInFinder: ["cmd+alt+f"],
51
+ openInEditor: ["cmd+shift+o"],
52
+ addSplitTerminal: ["cmd+shift+j"],
53
+ jumpToSidebarChat: ["cmd+alt"],
54
+ createChatInCurrentProject: ["cmd+alt+n"],
55
+ openAddProject: ["cmd+alt+o"],
56
+ },
57
+ warning: null,
58
+ filePathDisplay: TEST_FILE_PATH,
59
+ })
60
+ })
61
+ })
62
+
63
+ describe("readKeybindingsSnapshot", () => {
64
+ test("returns defaults when the file does not exist", async () => {
65
+ const filePath = await createTempFilePath()
66
+ const snapshot = await readKeybindingsSnapshot(filePath)
67
+ expect(snapshot).toEqual({
68
+ bindings: DEFAULT_KEYBINDINGS,
69
+ warning: null,
70
+ filePathDisplay: filePath,
71
+ })
72
+ })
73
+
74
+ test("returns a warning when the file contains invalid json", async () => {
75
+ const filePath = await createTempFilePath()
76
+ await writeFile(filePath, "{not-json", "utf8")
77
+
78
+ const snapshot = await readKeybindingsSnapshot(filePath)
79
+ expect(snapshot.bindings).toEqual(DEFAULT_KEYBINDINGS)
80
+ expect(snapshot.warning).toContain("invalid JSON")
81
+ })
82
+ })
83
+
84
+ describe("KeybindingsManager", () => {
85
+ test("creates the keybindings file with defaults during initialization", async () => {
86
+ const filePath = await createTempFilePath()
87
+ const manager = new KeybindingsManager(filePath)
88
+
89
+ await manager.initialize()
90
+
91
+ expect(await Bun.file(filePath).json()).toEqual(DEFAULT_KEYBINDINGS)
92
+ manager.dispose()
93
+ })
94
+
95
+ test("writes normalized bindings to disk", async () => {
96
+ const filePath = await createTempFilePath()
97
+ const manager = new KeybindingsManager(filePath)
98
+
99
+ await manager.initialize()
100
+ const snapshot = await manager.write({
101
+ toggleEmbeddedTerminal: ["Cmd+K"],
102
+ toggleRightSidebar: ["Ctrl+Shift+B"],
103
+ openInFinder: ["Cmd+Alt+F"],
104
+ openInEditor: ["Cmd+Shift+O"],
105
+ addSplitTerminal: ["Cmd+Shift+J"],
106
+ jumpToSidebarChat: ["Cmd+Alt"],
107
+ createChatInCurrentProject: ["Cmd+Alt+N"],
108
+ openAddProject: ["Cmd+Alt+O"],
109
+ })
110
+
111
+ expect(snapshot).toEqual({
112
+ bindings: {
113
+ toggleEmbeddedTerminal: ["cmd+k"],
114
+ toggleRightSidebar: ["ctrl+shift+b"],
115
+ openInFinder: ["cmd+alt+f"],
116
+ openInEditor: ["cmd+shift+o"],
117
+ addSplitTerminal: ["cmd+shift+j"],
118
+ jumpToSidebarChat: ["cmd+alt"],
119
+ createChatInCurrentProject: ["cmd+alt+n"],
120
+ openAddProject: ["cmd+alt+o"],
121
+ },
122
+ warning: null,
123
+ filePathDisplay: filePath,
124
+ })
125
+ expect(JSON.parse(await Bun.file(filePath).text())).toEqual(snapshot.bindings)
126
+
127
+ manager.dispose()
128
+ })
129
+
130
+ test("uses the runtime profile for the default keybindings path", () => {
131
+ const previous = process.env.KANNA_RUNTIME_PROFILE
132
+ process.env.KANNA_RUNTIME_PROFILE = "dev"
133
+
134
+ const manager = new KeybindingsManager()
135
+
136
+ expect(manager.filePath).toEndWith("/.kanna-dev/keybindings.json")
137
+
138
+ if (previous === undefined) {
139
+ delete process.env.KANNA_RUNTIME_PROFILE
140
+ } else {
141
+ process.env.KANNA_RUNTIME_PROFILE = previous
142
+ }
143
+ })
144
+ })
@@ -0,0 +1,178 @@
1
+ import { watch, type FSWatcher } from "node:fs"
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises"
3
+ import { homedir } from "node:os"
4
+ import path from "node:path"
5
+ import { getKeybindingsFilePath, LOG_PREFIX } from "../shared/branding"
6
+ import { DEFAULT_KEYBINDINGS, type KeybindingAction, type KeybindingsSnapshot } from "../shared/types"
7
+
8
+ const KEYBINDING_ACTIONS = Object.keys(DEFAULT_KEYBINDINGS) as KeybindingAction[]
9
+
10
+ type KeybindingsFile = Partial<Record<KeybindingAction, unknown>>
11
+
12
+ export class KeybindingsManager {
13
+ readonly filePath: string
14
+ private watcher: FSWatcher | null = null
15
+ private snapshot: KeybindingsSnapshot
16
+ private readonly listeners = new Set<(snapshot: KeybindingsSnapshot) => void>()
17
+
18
+ constructor(filePath = getKeybindingsFilePath(homedir())) {
19
+ this.filePath = filePath
20
+ this.snapshot = createDefaultSnapshot(this.filePath)
21
+ }
22
+
23
+ async initialize() {
24
+ await mkdir(path.dirname(this.filePath), { recursive: true })
25
+ const file = Bun.file(this.filePath)
26
+ if (!(await file.exists())) {
27
+ await writeFile(this.filePath, `${JSON.stringify(DEFAULT_KEYBINDINGS, null, 2)}\n`, "utf8")
28
+ }
29
+ await this.reload()
30
+ this.startWatching()
31
+ }
32
+
33
+ dispose() {
34
+ this.watcher?.close()
35
+ this.watcher = null
36
+ this.listeners.clear()
37
+ }
38
+
39
+ getSnapshot() {
40
+ return this.snapshot
41
+ }
42
+
43
+ onChange(listener: (snapshot: KeybindingsSnapshot) => void) {
44
+ this.listeners.add(listener)
45
+ return () => {
46
+ this.listeners.delete(listener)
47
+ }
48
+ }
49
+
50
+ async reload() {
51
+ const nextSnapshot = await readKeybindingsSnapshot(this.filePath)
52
+ this.setSnapshot(nextSnapshot)
53
+ }
54
+
55
+ async write(bindings: Partial<Record<KeybindingAction, string[]>>) {
56
+ const nextSnapshot = normalizeKeybindings(bindings, this.filePath)
57
+ await mkdir(path.dirname(this.filePath), { recursive: true })
58
+ await writeFile(this.filePath, `${JSON.stringify(nextSnapshot.bindings, null, 2)}\n`, "utf8")
59
+ this.setSnapshot(nextSnapshot)
60
+ return nextSnapshot
61
+ }
62
+
63
+ private setSnapshot(snapshot: KeybindingsSnapshot) {
64
+ this.snapshot = snapshot
65
+ for (const listener of this.listeners) {
66
+ listener(snapshot)
67
+ }
68
+ }
69
+
70
+ private startWatching() {
71
+ this.watcher?.close()
72
+ try {
73
+ this.watcher = watch(path.dirname(this.filePath), { persistent: false }, (_eventType, filename) => {
74
+ if (filename && filename !== path.basename(this.filePath)) {
75
+ return
76
+ }
77
+ void this.reload().catch((error: unknown) => {
78
+ console.warn(`${LOG_PREFIX} Failed to reload keybindings:`, error)
79
+ })
80
+ })
81
+ } catch (error) {
82
+ console.warn(`${LOG_PREFIX} Failed to watch keybindings file:`, error)
83
+ this.watcher = null
84
+ }
85
+ }
86
+ }
87
+
88
+ export async function readKeybindingsSnapshot(filePath: string) {
89
+ try {
90
+ const text = await readFile(filePath, "utf8")
91
+ if (!text.trim()) {
92
+ return createDefaultSnapshot(filePath, "Keybindings file was empty. Using defaults.")
93
+ }
94
+ const parsed = JSON.parse(text) as KeybindingsFile
95
+ return normalizeKeybindings(parsed, filePath)
96
+ } catch (error) {
97
+ if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
98
+ return createDefaultSnapshot(filePath)
99
+ }
100
+
101
+ if (error instanceof SyntaxError) {
102
+ return createDefaultSnapshot(filePath, "Keybindings file is invalid JSON. Using defaults.")
103
+ }
104
+
105
+ throw error
106
+ }
107
+ }
108
+
109
+ export function normalizeKeybindings(value: KeybindingsFile | null | undefined, filePath = getKeybindingsFilePath(homedir())): KeybindingsSnapshot {
110
+ const warnings: string[] = []
111
+ const source = value && typeof value === "object" && !Array.isArray(value)
112
+ ? value
113
+ : null
114
+
115
+ if (!source) {
116
+ return createDefaultSnapshot(filePath, "Keybindings file must contain a JSON object. Using defaults.")
117
+ }
118
+
119
+ const bindings = {} as Record<KeybindingAction, string[]>
120
+ for (const action of KEYBINDING_ACTIONS) {
121
+ const rawValue = source[action]
122
+ if (!Array.isArray(rawValue)) {
123
+ bindings[action] = [...DEFAULT_KEYBINDINGS[action]]
124
+ if (rawValue !== undefined) {
125
+ warnings.push(`${action} must be an array of shortcut strings`)
126
+ }
127
+ continue
128
+ }
129
+
130
+ const normalized = rawValue
131
+ .filter((entry): entry is string => typeof entry === "string")
132
+ .map((entry) => entry.trim())
133
+ .map((entry) => entry.toLowerCase())
134
+ .filter(Boolean)
135
+
136
+ if (normalized.length === 0) {
137
+ bindings[action] = [...DEFAULT_KEYBINDINGS[action]]
138
+ if (rawValue.length > 0 || source[action] !== undefined) {
139
+ warnings.push(`${action} did not contain any valid shortcut strings`)
140
+ }
141
+ continue
142
+ }
143
+
144
+ bindings[action] = normalized
145
+ }
146
+
147
+ return {
148
+ bindings,
149
+ warning: warnings.length > 0 ? `Some keybindings were reset to defaults: ${warnings.join("; ")}` : null,
150
+ filePathDisplay: formatDisplayPath(filePath),
151
+ }
152
+ }
153
+
154
+ function createDefaultSnapshot(filePath: string, warning: string | null = null): KeybindingsSnapshot {
155
+ return {
156
+ bindings: {
157
+ toggleEmbeddedTerminal: [...DEFAULT_KEYBINDINGS.toggleEmbeddedTerminal],
158
+ toggleRightSidebar: [...DEFAULT_KEYBINDINGS.toggleRightSidebar],
159
+ openInFinder: [...DEFAULT_KEYBINDINGS.openInFinder],
160
+ openInEditor: [...DEFAULT_KEYBINDINGS.openInEditor],
161
+ addSplitTerminal: [...DEFAULT_KEYBINDINGS.addSplitTerminal],
162
+ jumpToSidebarChat: [...DEFAULT_KEYBINDINGS.jumpToSidebarChat],
163
+ createChatInCurrentProject: [...DEFAULT_KEYBINDINGS.createChatInCurrentProject],
164
+ openAddProject: [...DEFAULT_KEYBINDINGS.openAddProject],
165
+ },
166
+ warning,
167
+ filePathDisplay: formatDisplayPath(filePath),
168
+ }
169
+ }
170
+
171
+ function formatDisplayPath(filePath: string) {
172
+ const homePath = homedir()
173
+ if (filePath === homePath) return "~"
174
+ if (filePath.startsWith(`${homePath}${path.sep}`)) {
175
+ return `~${filePath.slice(homePath.length)}`
176
+ }
177
+ return filePath
178
+ }