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