@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.
- package/LICENSE +22 -0
- package/README.md +355 -0
- package/bin/kanna +9 -0
- package/dist/client/apple-touch-icon.png +0 -0
- 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/bricolage-grotesque-latin-ext-wght-normal-CcLUaPy7.woff2 +0 -0
- package/dist/client/assets/bricolage-grotesque-latin-wght-normal-DLoelf7F.woff2 +0 -0
- package/dist/client/assets/bricolage-grotesque-vietnamese-wght-normal-BUzh504Q.woff2 +0 -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-Do7324M0.css +32 -0
- package/dist/client/assets/index-ktE9DLCD.js +2620 -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/chat-sounds/Blow.mp3 +0 -0
- package/dist/client/chat-sounds/Bottle.mp3 +0 -0
- package/dist/client/chat-sounds/Frog.mp3 +0 -0
- package/dist/client/chat-sounds/Funk.mp3 +0 -0
- package/dist/client/chat-sounds/Glass.mp3 +0 -0
- package/dist/client/chat-sounds/Ping.mp3 +0 -0
- package/dist/client/chat-sounds/Pop.mp3 +0 -0
- package/dist/client/chat-sounds/Purr.mp3 +0 -0
- package/dist/client/chat-sounds/Tink.mp3 +0 -0
- package/dist/client/editor-icons/cursor.png +0 -0
- package/dist/client/editor-icons/custom.png +0 -0
- package/dist/client/editor-icons/default-app.png +0 -0
- package/dist/client/editor-icons/finder.png +0 -0
- package/dist/client/editor-icons/preview.png +0 -0
- package/dist/client/editor-icons/terminal.png +0 -0
- package/dist/client/editor-icons/windsurf.png +0 -0
- package/dist/client/editor-icons/xcode.png +0 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/fonts/body-medium.woff2 +0 -0
- package/dist/client/fonts/body-regular-italic.woff2 +0 -0
- package/dist/client/fonts/body-regular.woff2 +0 -0
- package/dist/client/fonts/body-semibold.woff2 +0 -0
- package/dist/client/icon-192.png +0 -0
- package/dist/client/icon-512.png +0 -0
- package/dist/client/icon-maskable-512.png +0 -0
- package/dist/client/icon.svg +4 -0
- package/dist/client/index.html +34 -0
- package/dist/client/manifest.webmanifest +46 -0
- package/dist/client/screenshot-light.png +0 -0
- package/dist/client/screenshot.png +0 -0
- package/dist/export-viewer/assets/bricolage-grotesque-latin-ext-wght-normal-CcLUaPy7.woff2 +0 -0
- package/dist/export-viewer/assets/bricolage-grotesque-latin-wght-normal-DLoelf7F.woff2 +0 -0
- package/dist/export-viewer/assets/bricolage-grotesque-vietnamese-wght-normal-BUzh504Q.woff2 +0 -0
- package/dist/export-viewer/assets/index-D1qUumZR.js +410 -0
- package/dist/export-viewer/assets/index-gG2nMW51.css +1 -0
- package/dist/export-viewer/editor-icons/cursor.png +0 -0
- package/dist/export-viewer/editor-icons/custom.png +0 -0
- package/dist/export-viewer/editor-icons/default-app.png +0 -0
- package/dist/export-viewer/editor-icons/finder.png +0 -0
- package/dist/export-viewer/editor-icons/preview.png +0 -0
- package/dist/export-viewer/editor-icons/terminal.png +0 -0
- package/dist/export-viewer/editor-icons/windsurf.png +0 -0
- package/dist/export-viewer/editor-icons/xcode.png +0 -0
- package/dist/export-viewer/fonts/body-medium.woff2 +0 -0
- package/dist/export-viewer/fonts/body-regular-italic.woff2 +0 -0
- package/dist/export-viewer/fonts/body-regular.woff2 +0 -0
- package/dist/export-viewer/fonts/body-semibold.woff2 +0 -0
- package/dist/export-viewer/index.html +14 -0
- package/package.json +99 -0
- package/src/server/__fixtures__/claude-session-empty.jsonl +0 -0
- package/src/server/__fixtures__/claude-session-malformed.jsonl +3 -0
- package/src/server/__fixtures__/claude-session-valid.jsonl +6 -0
- package/src/server/agent.test.ts +2369 -0
- package/src/server/agent.ts +1927 -0
- package/src/server/analytics.test.ts +313 -0
- package/src/server/analytics.ts +131 -0
- package/src/server/app-settings.test.ts +233 -0
- package/src/server/app-settings.ts +548 -0
- package/src/server/auth.test.ts +329 -0
- package/src/server/auth.ts +204 -0
- package/src/server/auto-continue/e2e.test.ts +215 -0
- package/src/server/auto-continue/events.test.ts +30 -0
- package/src/server/auto-continue/events.ts +35 -0
- package/src/server/auto-continue/limit-detector.test.ts +153 -0
- package/src/server/auto-continue/limit-detector.ts +159 -0
- package/src/server/auto-continue/read-model.test.ts +109 -0
- package/src/server/auto-continue/read-model.ts +83 -0
- package/src/server/auto-continue/schedule-manager.test.ts +155 -0
- package/src/server/auto-continue/schedule-manager.ts +116 -0
- package/src/server/claude-session-importer.test.ts +214 -0
- package/src/server/claude-session-importer.ts +187 -0
- package/src/server/claude-session-mapper.test.ts +88 -0
- package/src/server/claude-session-mapper.ts +106 -0
- package/src/server/claude-session-parser.test.ts +38 -0
- package/src/server/claude-session-parser.ts +67 -0
- package/src/server/claude-session-scanner.test.ts +49 -0
- package/src/server/claude-session-scanner.ts +24 -0
- package/src/server/claude-session-types.ts +61 -0
- package/src/server/cli-runtime.test.ts +523 -0
- package/src/server/cli-runtime.ts +405 -0
- package/src/server/cli-supervisor.ts +102 -0
- package/src/server/cli.ts +64 -0
- package/src/server/cloudflare-tunnel/agent-integration.test.ts +76 -0
- package/src/server/cloudflare-tunnel/agent-integration.ts +55 -0
- package/src/server/cloudflare-tunnel/detector.test.ts +72 -0
- package/src/server/cloudflare-tunnel/detector.ts +44 -0
- package/src/server/cloudflare-tunnel/e2e.test.ts +194 -0
- package/src/server/cloudflare-tunnel/events.test.ts +43 -0
- package/src/server/cloudflare-tunnel/events.ts +31 -0
- package/src/server/cloudflare-tunnel/gateway.ts +143 -0
- package/src/server/cloudflare-tunnel/lifecycle.test.ts +48 -0
- package/src/server/cloudflare-tunnel/lifecycle.ts +62 -0
- package/src/server/cloudflare-tunnel/read-model.test.ts +69 -0
- package/src/server/cloudflare-tunnel/read-model.ts +80 -0
- package/src/server/cloudflare-tunnel/tunnel-manager.test.ts +116 -0
- package/src/server/cloudflare-tunnel/tunnel-manager.ts +165 -0
- package/src/server/codex-app-server-protocol.ts +487 -0
- package/src/server/codex-app-server.test.ts +1816 -0
- package/src/server/codex-app-server.ts +1475 -0
- package/src/server/diff-store.test.ts +737 -0
- package/src/server/diff-store.ts +2199 -0
- package/src/server/discovery.test.ts +211 -0
- package/src/server/discovery.ts +301 -0
- package/src/server/event-store.test.ts +797 -0
- package/src/server/event-store.ts +1421 -0
- package/src/server/events.ts +217 -0
- package/src/server/external-open.test.ts +112 -0
- package/src/server/external-open.ts +345 -0
- package/src/server/generate-commit-message.test.ts +79 -0
- package/src/server/generate-commit-message.ts +126 -0
- package/src/server/generate-title.ts +76 -0
- package/src/server/harness-types.ts +19 -0
- package/src/server/keybindings.test.ts +144 -0
- package/src/server/keybindings.ts +178 -0
- package/src/server/llm-provider.test.ts +134 -0
- package/src/server/llm-provider.ts +207 -0
- package/src/server/machine-name.ts +22 -0
- package/src/server/paths-route.test.ts +64 -0
- package/src/server/paths.ts +35 -0
- package/src/server/process-utils.test.ts +12 -0
- package/src/server/process-utils.ts +47 -0
- package/src/server/project-paths.test.ts +95 -0
- package/src/server/project-paths.ts +191 -0
- package/src/server/provider-catalog.test.ts +69 -0
- package/src/server/provider-catalog.ts +87 -0
- package/src/server/quick-response.test.ts +440 -0
- package/src/server/quick-response.ts +300 -0
- package/src/server/read-models.test.ts +509 -0
- package/src/server/read-models.ts +230 -0
- package/src/server/restart.test.ts +27 -0
- package/src/server/restart.ts +30 -0
- package/src/server/server.ts +616 -0
- package/src/server/share.test.ts +180 -0
- package/src/server/share.ts +150 -0
- package/src/server/standalone-export.test.ts +224 -0
- package/src/server/standalone-export.ts +419 -0
- package/src/server/terminal-manager.test.ts +315 -0
- package/src/server/terminal-manager.ts +350 -0
- package/src/server/test-helpers/async-event-queue.ts +52 -0
- package/src/server/test-helpers/wait-for.ts +14 -0
- package/src/server/title-generation.live.test.ts +44 -0
- package/src/server/update-manager.test.ts +158 -0
- package/src/server/update-manager.ts +222 -0
- package/src/server/update-strategy.test.ts +237 -0
- package/src/server/update-strategy.ts +241 -0
- package/src/server/uploads.test.ts +292 -0
- package/src/server/uploads.ts +131 -0
- package/src/server/ws-router.test.ts +2292 -0
- package/src/server/ws-router.ts +1465 -0
- package/src/shared/analytics.ts +30 -0
- package/src/shared/branding.test.ts +31 -0
- package/src/shared/branding.ts +77 -0
- package/src/shared/dev-ports.test.ts +113 -0
- package/src/shared/dev-ports.ts +134 -0
- package/src/shared/ports.ts +2 -0
- package/src/shared/protocol.ts +257 -0
- package/src/shared/share.ts +27 -0
- package/src/shared/tools.test.ts +164 -0
- package/src/shared/tools.ts +327 -0
- package/src/shared/types.test.ts +25 -0
- package/src/shared/types.ts +1088 -0
|
@@ -0,0 +1,1927 @@
|
|
|
1
|
+
import { query, type CanUseTool, type PermissionResult, type Query, type SDKUserMessage } from "@anthropic-ai/claude-agent-sdk"
|
|
2
|
+
import type {
|
|
3
|
+
AgentProvider,
|
|
4
|
+
ChatAttachment,
|
|
5
|
+
ContextWindowUsageSnapshot,
|
|
6
|
+
ModelOptions,
|
|
7
|
+
NormalizedToolCall,
|
|
8
|
+
PendingToolSnapshot,
|
|
9
|
+
KannaStatus,
|
|
10
|
+
QueuedChatMessage,
|
|
11
|
+
SlashCommand,
|
|
12
|
+
TranscriptEntry,
|
|
13
|
+
} from "../shared/types"
|
|
14
|
+
import { normalizeToolCall } from "../shared/tools"
|
|
15
|
+
import type { ClientCommand } from "../shared/protocol"
|
|
16
|
+
import { EventStore } from "./event-store"
|
|
17
|
+
import type { AnalyticsReporter } from "./analytics"
|
|
18
|
+
import { NoopAnalyticsReporter } from "./analytics"
|
|
19
|
+
import { CodexAppServerManager } from "./codex-app-server"
|
|
20
|
+
import { type GenerateChatTitleResult, generateTitleForChatDetailed } from "./generate-title"
|
|
21
|
+
import type { HarnessEvent, HarnessToolRequest, HarnessTurn } from "./harness-types"
|
|
22
|
+
import {
|
|
23
|
+
codexServiceTierFromModelOptions,
|
|
24
|
+
getServerProviderCatalog,
|
|
25
|
+
normalizeClaudeModelOptions,
|
|
26
|
+
normalizeCodexModelOptions,
|
|
27
|
+
normalizeServerModel,
|
|
28
|
+
} from "./provider-catalog"
|
|
29
|
+
import { resolveClaudeApiModelId } from "../shared/types"
|
|
30
|
+
import { fallbackTitleFromMessage } from "./generate-title"
|
|
31
|
+
import { AUTO_CONTINUE_EVENT_VERSION, type AutoContinueEvent } from "./auto-continue/events"
|
|
32
|
+
import { ClaudeLimitDetector, CodexLimitDetector, type LimitDetection, type LimitDetector } from "./auto-continue/limit-detector"
|
|
33
|
+
import type { ScheduleManager } from "./auto-continue/schedule-manager"
|
|
34
|
+
import { deriveChatSchedules } from "./auto-continue/read-model"
|
|
35
|
+
import type { TunnelGateway } from "./cloudflare-tunnel/gateway"
|
|
36
|
+
|
|
37
|
+
const CLAUDE_TOOLSET = [
|
|
38
|
+
"Skill",
|
|
39
|
+
"WebFetch",
|
|
40
|
+
"WebSearch",
|
|
41
|
+
"Task",
|
|
42
|
+
"TaskOutput",
|
|
43
|
+
"Bash",
|
|
44
|
+
"Glob",
|
|
45
|
+
"Grep",
|
|
46
|
+
"Read",
|
|
47
|
+
"Edit",
|
|
48
|
+
"Write",
|
|
49
|
+
"TodoWrite",
|
|
50
|
+
"KillShell",
|
|
51
|
+
"AskUserQuestion",
|
|
52
|
+
"EnterPlanMode",
|
|
53
|
+
"ExitPlanMode",
|
|
54
|
+
] as const
|
|
55
|
+
|
|
56
|
+
interface PendingToolRequest {
|
|
57
|
+
toolUseId: string
|
|
58
|
+
tool: NormalizedToolCall & { toolKind: "ask_user_question" | "exit_plan_mode" }
|
|
59
|
+
resolve: (result: unknown) => void
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface ActiveTurn {
|
|
63
|
+
chatId: string
|
|
64
|
+
provider: AgentProvider
|
|
65
|
+
turn: HarnessTurn
|
|
66
|
+
claudePromptSeq?: number
|
|
67
|
+
model: string
|
|
68
|
+
effort?: string
|
|
69
|
+
serviceTier?: "fast"
|
|
70
|
+
planMode: boolean
|
|
71
|
+
status: KannaStatus
|
|
72
|
+
pendingTool: PendingToolRequest | null
|
|
73
|
+
postToolFollowUp: { content: string; planMode: boolean } | null
|
|
74
|
+
hasFinalResult: boolean
|
|
75
|
+
cancelRequested: boolean
|
|
76
|
+
cancelRecorded: boolean
|
|
77
|
+
clientTraceId?: string
|
|
78
|
+
profilingStartedAt?: number
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface ClaudeSessionHandle {
|
|
82
|
+
provider: "claude"
|
|
83
|
+
stream: AsyncIterable<HarnessEvent>
|
|
84
|
+
getAccountInfo?: () => Promise<any>
|
|
85
|
+
interrupt: () => Promise<void>
|
|
86
|
+
close: () => void
|
|
87
|
+
sendPrompt: (content: string) => Promise<void>
|
|
88
|
+
setModel: (model: string) => Promise<void>
|
|
89
|
+
setPermissionMode: (planMode: boolean) => Promise<void>
|
|
90
|
+
getSupportedCommands: () => Promise<SlashCommand[]>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface ClaudeSessionState {
|
|
94
|
+
id: string
|
|
95
|
+
chatId: string
|
|
96
|
+
session: ClaudeSessionHandle
|
|
97
|
+
localPath: string
|
|
98
|
+
model: string
|
|
99
|
+
effort?: string
|
|
100
|
+
planMode: boolean
|
|
101
|
+
sessionToken: string | null
|
|
102
|
+
accountInfoLoaded: boolean
|
|
103
|
+
nextPromptSeq: number
|
|
104
|
+
pendingPromptSeqs: number[]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface AgentCoordinatorArgs {
|
|
108
|
+
store: EventStore
|
|
109
|
+
onStateChange: (chatId?: string, options?: { immediate?: boolean }) => void
|
|
110
|
+
analytics?: AnalyticsReporter
|
|
111
|
+
codexManager?: CodexAppServerManager
|
|
112
|
+
generateTitle?: (messageContent: string, cwd: string) => Promise<GenerateChatTitleResult>
|
|
113
|
+
tunnelGateway?: TunnelGateway
|
|
114
|
+
startClaudeSession?: (args: {
|
|
115
|
+
localPath: string
|
|
116
|
+
model: string
|
|
117
|
+
effort?: string
|
|
118
|
+
planMode: boolean
|
|
119
|
+
sessionToken: string | null
|
|
120
|
+
forkSession: boolean
|
|
121
|
+
onToolRequest: (request: HarnessToolRequest) => Promise<unknown>
|
|
122
|
+
}) => Promise<ClaudeSessionHandle>
|
|
123
|
+
claudeLimitDetector?: LimitDetector
|
|
124
|
+
codexLimitDetector?: LimitDetector
|
|
125
|
+
scheduleManager?: ScheduleManager
|
|
126
|
+
getAutoResumePreference?: () => boolean
|
|
127
|
+
throwOnClaudeSessionStart?: boolean
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface SendToStartingProfile {
|
|
131
|
+
traceId: string
|
|
132
|
+
startedAt: number
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isClaudeSteerLoggingEnabled() {
|
|
136
|
+
return process.env.KANNA_LOG_CLAUDE_STEER === "1"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function logClaudeSteer(stage: string, details?: Record<string, unknown>) {
|
|
140
|
+
if (!isClaudeSteerLoggingEnabled()) return
|
|
141
|
+
console.log("[kanna/claude-steer]", JSON.stringify({
|
|
142
|
+
stage,
|
|
143
|
+
...details,
|
|
144
|
+
}))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const STEERED_MESSAGE_PREFIX = `<system-message>
|
|
148
|
+
The user would like to inform you of something while you continue to work. Acknowledge receipt immediately with a text response, then continue with the task at hand, incorporating the user's feedback if needed.
|
|
149
|
+
</system-message>`
|
|
150
|
+
|
|
151
|
+
interface SendMessageOptions {
|
|
152
|
+
provider?: AgentProvider
|
|
153
|
+
model?: string
|
|
154
|
+
modelOptions?: ModelOptions
|
|
155
|
+
effort?: string
|
|
156
|
+
planMode?: boolean
|
|
157
|
+
autoContinue?: { scheduleId: string }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function timestamped<T extends Omit<TranscriptEntry, "_id" | "createdAt">>(
|
|
161
|
+
entry: T,
|
|
162
|
+
createdAt = Date.now()
|
|
163
|
+
): TranscriptEntry {
|
|
164
|
+
return {
|
|
165
|
+
_id: crypto.randomUUID(),
|
|
166
|
+
createdAt,
|
|
167
|
+
...entry,
|
|
168
|
+
} as TranscriptEntry
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function stringFromUnknown(value: unknown) {
|
|
172
|
+
if (typeof value === "string") return value
|
|
173
|
+
try {
|
|
174
|
+
return JSON.stringify(value, null, 2)
|
|
175
|
+
} catch {
|
|
176
|
+
return String(value)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function buildSteeredMessageContent(content: string) {
|
|
181
|
+
return content.trim().length > 0
|
|
182
|
+
? `${STEERED_MESSAGE_PREFIX}\n\n${content}`
|
|
183
|
+
: STEERED_MESSAGE_PREFIX
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
187
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function stringifyToolResultContent(content: unknown): string {
|
|
191
|
+
if (typeof content === "string") return content
|
|
192
|
+
if (Array.isArray(content)) {
|
|
193
|
+
return content
|
|
194
|
+
.map((item) => {
|
|
195
|
+
if (item && typeof item === "object") {
|
|
196
|
+
const r = item as Record<string, unknown>
|
|
197
|
+
return typeof r.text === "string" ? r.text : ""
|
|
198
|
+
}
|
|
199
|
+
return ""
|
|
200
|
+
})
|
|
201
|
+
.join("")
|
|
202
|
+
}
|
|
203
|
+
return ""
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function asNumber(value: unknown): number | undefined {
|
|
207
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function escapeXmlAttribute(value: string) {
|
|
211
|
+
return value
|
|
212
|
+
.replaceAll("&", "&")
|
|
213
|
+
.replaceAll("\"", """)
|
|
214
|
+
.replaceAll("<", "<")
|
|
215
|
+
.replaceAll(">", ">")
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isSendToStartingProfilingEnabled() {
|
|
219
|
+
return process.env.KANNA_PROFILE_SEND_TO_STARTING === "1"
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function elapsedProfileMs(startedAt: number) {
|
|
223
|
+
return Number((performance.now() - startedAt).toFixed(1))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function logSendToStartingProfile(
|
|
227
|
+
profile: SendToStartingProfile | null | undefined,
|
|
228
|
+
stage: string,
|
|
229
|
+
details?: Record<string, unknown>
|
|
230
|
+
) {
|
|
231
|
+
if (!profile || !isSendToStartingProfilingEnabled()) {
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log("[kanna/send->starting][server]", JSON.stringify({
|
|
236
|
+
traceId: profile.traceId,
|
|
237
|
+
stage,
|
|
238
|
+
elapsedMs: elapsedProfileMs(profile.startedAt),
|
|
239
|
+
...details,
|
|
240
|
+
}))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function buildAttachmentHintText(attachments: ChatAttachment[]) {
|
|
244
|
+
if (attachments.length === 0) return ""
|
|
245
|
+
|
|
246
|
+
const lines = attachments.map((attachment) => (
|
|
247
|
+
`<attachment kind="${escapeXmlAttribute(attachment.kind)}" mime_type="${escapeXmlAttribute(attachment.mimeType)}" path="${escapeXmlAttribute(attachment.absolutePath)}" project_path="${escapeXmlAttribute(attachment.relativePath)}" size_bytes="${attachment.size}" display_name="${escapeXmlAttribute(attachment.displayName)}" />`
|
|
248
|
+
))
|
|
249
|
+
|
|
250
|
+
return [
|
|
251
|
+
"<kanna-attachments>",
|
|
252
|
+
...lines,
|
|
253
|
+
"</kanna-attachments>",
|
|
254
|
+
].join("\n")
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function buildPromptText(content: string, attachments: ChatAttachment[]) {
|
|
258
|
+
const attachmentHint = buildAttachmentHintText(attachments)
|
|
259
|
+
if (!attachmentHint) {
|
|
260
|
+
return content.trim()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const trimmed = content.trim()
|
|
264
|
+
return [
|
|
265
|
+
trimmed || "Please inspect the attached files.",
|
|
266
|
+
attachmentHint,
|
|
267
|
+
].join("\n\n").trim()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function discardedToolResult(
|
|
271
|
+
tool: NormalizedToolCall & { toolKind: "ask_user_question" | "exit_plan_mode" }
|
|
272
|
+
) {
|
|
273
|
+
if (tool.toolKind === "ask_user_question") {
|
|
274
|
+
return {
|
|
275
|
+
discarded: true,
|
|
276
|
+
answers: {},
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
discarded: true,
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function normalizeClaudeUsageSnapshot(
|
|
286
|
+
value: unknown,
|
|
287
|
+
maxTokens?: number,
|
|
288
|
+
): ContextWindowUsageSnapshot | null {
|
|
289
|
+
const usage = asRecord(value)
|
|
290
|
+
if (!usage) return null
|
|
291
|
+
|
|
292
|
+
const directInputTokens = asNumber(usage.input_tokens) ?? asNumber(usage.inputTokens) ?? 0
|
|
293
|
+
const cacheCreationInputTokens =
|
|
294
|
+
asNumber(usage.cache_creation_input_tokens) ?? asNumber(usage.cacheCreationInputTokens) ?? 0
|
|
295
|
+
const cacheReadInputTokens =
|
|
296
|
+
asNumber(usage.cache_read_input_tokens) ?? asNumber(usage.cacheReadInputTokens) ?? 0
|
|
297
|
+
const outputTokens = asNumber(usage.output_tokens) ?? asNumber(usage.outputTokens) ?? 0
|
|
298
|
+
const reasoningOutputTokens =
|
|
299
|
+
asNumber(usage.reasoning_output_tokens) ?? asNumber(usage.reasoningOutputTokens)
|
|
300
|
+
const toolUses = asNumber(usage.tool_uses) ?? asNumber(usage.toolUses)
|
|
301
|
+
const durationMs = asNumber(usage.duration_ms) ?? asNumber(usage.durationMs)
|
|
302
|
+
|
|
303
|
+
const inputTokens = directInputTokens + cacheCreationInputTokens + cacheReadInputTokens
|
|
304
|
+
const usedTokens = inputTokens + outputTokens
|
|
305
|
+
if (usedTokens <= 0) {
|
|
306
|
+
return null
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
usedTokens,
|
|
311
|
+
inputTokens,
|
|
312
|
+
...(cacheReadInputTokens > 0 ? { cachedInputTokens: cacheReadInputTokens } : {}),
|
|
313
|
+
...(outputTokens > 0 ? { outputTokens } : {}),
|
|
314
|
+
...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
|
|
315
|
+
lastUsedTokens: usedTokens,
|
|
316
|
+
lastInputTokens: inputTokens,
|
|
317
|
+
...(cacheReadInputTokens > 0 ? { lastCachedInputTokens: cacheReadInputTokens } : {}),
|
|
318
|
+
...(outputTokens > 0 ? { lastOutputTokens: outputTokens } : {}),
|
|
319
|
+
...(reasoningOutputTokens !== undefined ? { lastReasoningOutputTokens: reasoningOutputTokens } : {}),
|
|
320
|
+
...(toolUses !== undefined ? { toolUses } : {}),
|
|
321
|
+
...(durationMs !== undefined ? { durationMs } : {}),
|
|
322
|
+
...(typeof maxTokens === "number" && maxTokens > 0 ? { maxTokens } : {}),
|
|
323
|
+
compactsAutomatically: false,
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function maxClaudeContextWindowFromModelUsage(modelUsage: unknown): number | undefined {
|
|
328
|
+
const record = asRecord(modelUsage)
|
|
329
|
+
if (!record) return undefined
|
|
330
|
+
|
|
331
|
+
let maxContextWindow: number | undefined
|
|
332
|
+
for (const value of Object.values(record)) {
|
|
333
|
+
const usage = asRecord(value)
|
|
334
|
+
const contextWindow = asNumber(usage?.contextWindow) ?? asNumber(usage?.context_window)
|
|
335
|
+
if (contextWindow === undefined) continue
|
|
336
|
+
maxContextWindow = Math.max(maxContextWindow ?? 0, contextWindow)
|
|
337
|
+
}
|
|
338
|
+
return maxContextWindow
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function getClaudeAssistantMessageUsageId(message: any): string | null {
|
|
342
|
+
if (typeof message?.message?.id === "string" && message.message.id) {
|
|
343
|
+
return message.message.id
|
|
344
|
+
}
|
|
345
|
+
if (typeof message?.uuid === "string" && message.uuid) {
|
|
346
|
+
return message.uuid
|
|
347
|
+
}
|
|
348
|
+
return null
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function normalizeClaudeStreamMessage(message: any): TranscriptEntry[] {
|
|
352
|
+
const debugRaw = JSON.stringify(message)
|
|
353
|
+
const messageId = typeof message.uuid === "string" ? message.uuid : undefined
|
|
354
|
+
|
|
355
|
+
if (message.type === "system" && message.subtype === "init") {
|
|
356
|
+
return [
|
|
357
|
+
timestamped({
|
|
358
|
+
kind: "system_init",
|
|
359
|
+
messageId,
|
|
360
|
+
provider: "claude",
|
|
361
|
+
model: typeof message.model === "string" ? message.model : "unknown",
|
|
362
|
+
tools: Array.isArray(message.tools) ? message.tools : [],
|
|
363
|
+
agents: Array.isArray(message.agents) ? message.agents : [],
|
|
364
|
+
slashCommands: Array.isArray(message.slash_commands)
|
|
365
|
+
? message.slash_commands.filter((entry: string) => !entry.startsWith("._"))
|
|
366
|
+
: [],
|
|
367
|
+
mcpServers: Array.isArray(message.mcp_servers) ? message.mcp_servers : [],
|
|
368
|
+
debugRaw,
|
|
369
|
+
}),
|
|
370
|
+
]
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (message.type === "assistant" && Array.isArray(message.message?.content)) {
|
|
374
|
+
const entries: TranscriptEntry[] = []
|
|
375
|
+
for (const content of message.message.content) {
|
|
376
|
+
if (content.type === "text" && typeof content.text === "string") {
|
|
377
|
+
entries.push(timestamped({
|
|
378
|
+
kind: "assistant_text",
|
|
379
|
+
messageId,
|
|
380
|
+
text: content.text,
|
|
381
|
+
debugRaw,
|
|
382
|
+
}))
|
|
383
|
+
}
|
|
384
|
+
if (content.type === "tool_use" && typeof content.name === "string" && typeof content.id === "string") {
|
|
385
|
+
entries.push(timestamped({
|
|
386
|
+
kind: "tool_call",
|
|
387
|
+
messageId,
|
|
388
|
+
tool: normalizeToolCall({
|
|
389
|
+
toolName: content.name,
|
|
390
|
+
toolId: content.id,
|
|
391
|
+
input: (content.input ?? {}) as Record<string, unknown>,
|
|
392
|
+
}),
|
|
393
|
+
debugRaw,
|
|
394
|
+
}))
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return entries
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (message.type === "user" && Array.isArray(message.message?.content)) {
|
|
401
|
+
const entries: TranscriptEntry[] = []
|
|
402
|
+
for (const content of message.message.content) {
|
|
403
|
+
if (content.type === "tool_result" && typeof content.tool_use_id === "string") {
|
|
404
|
+
entries.push(timestamped({
|
|
405
|
+
kind: "tool_result",
|
|
406
|
+
messageId,
|
|
407
|
+
toolId: content.tool_use_id,
|
|
408
|
+
content: content.content,
|
|
409
|
+
isError: Boolean(content.is_error),
|
|
410
|
+
debugRaw,
|
|
411
|
+
}))
|
|
412
|
+
}
|
|
413
|
+
if (message.message.role === "user" && typeof message.message.content === "string") {
|
|
414
|
+
entries.push(timestamped({
|
|
415
|
+
kind: "compact_summary",
|
|
416
|
+
messageId,
|
|
417
|
+
summary: message.message.content,
|
|
418
|
+
debugRaw,
|
|
419
|
+
}))
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return entries
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (message.type === "result") {
|
|
426
|
+
if (message.subtype === "cancelled") {
|
|
427
|
+
return [timestamped({ kind: "interrupted", messageId, debugRaw })]
|
|
428
|
+
}
|
|
429
|
+
return [
|
|
430
|
+
timestamped({
|
|
431
|
+
kind: "result",
|
|
432
|
+
messageId,
|
|
433
|
+
subtype: message.is_error ? "error" : "success",
|
|
434
|
+
isError: Boolean(message.is_error),
|
|
435
|
+
durationMs: typeof message.duration_ms === "number" ? message.duration_ms : 0,
|
|
436
|
+
result: typeof message.result === "string" ? message.result : stringFromUnknown(message.result),
|
|
437
|
+
costUsd: typeof message.total_cost_usd === "number" ? message.total_cost_usd : undefined,
|
|
438
|
+
debugRaw,
|
|
439
|
+
}),
|
|
440
|
+
]
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (message.type === "system" && message.subtype === "status" && typeof message.status === "string") {
|
|
444
|
+
return [timestamped({ kind: "status", messageId, status: message.status, debugRaw })]
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (message.type === "system" && message.subtype === "compact_boundary") {
|
|
448
|
+
return [timestamped({ kind: "compact_boundary", messageId, debugRaw })]
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (message.type === "system" && message.subtype === "context_cleared") {
|
|
452
|
+
return [timestamped({ kind: "context_cleared", messageId, debugRaw })]
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (
|
|
456
|
+
message.type === "user" &&
|
|
457
|
+
message.message?.role === "user" &&
|
|
458
|
+
typeof message.message.content === "string" &&
|
|
459
|
+
message.message.content.startsWith("This session is being continued")
|
|
460
|
+
) {
|
|
461
|
+
return [timestamped({ kind: "compact_summary", messageId, summary: message.message.content, debugRaw })]
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return []
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function* createClaudeHarnessStream(q: Query): AsyncGenerator<HarnessEvent> {
|
|
468
|
+
let seenAssistantUsageIds = new Set<string>()
|
|
469
|
+
let latestUsageSnapshot: ContextWindowUsageSnapshot | null = null
|
|
470
|
+
let lastKnownContextWindow: number | undefined
|
|
471
|
+
|
|
472
|
+
for await (const sdkMessage of q as AsyncIterable<any>) {
|
|
473
|
+
const sessionToken = typeof sdkMessage.session_id === "string" ? sdkMessage.session_id : null
|
|
474
|
+
if (sessionToken) {
|
|
475
|
+
yield { type: "session_token", sessionToken }
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (sdkMessage?.type === "assistant") {
|
|
479
|
+
const usageId = getClaudeAssistantMessageUsageId(sdkMessage)
|
|
480
|
+
const usageSnapshot = normalizeClaudeUsageSnapshot(sdkMessage.usage, lastKnownContextWindow)
|
|
481
|
+
if (usageId && usageSnapshot && !seenAssistantUsageIds.has(usageId)) {
|
|
482
|
+
seenAssistantUsageIds.add(usageId)
|
|
483
|
+
latestUsageSnapshot = usageSnapshot
|
|
484
|
+
yield {
|
|
485
|
+
type: "transcript",
|
|
486
|
+
entry: timestamped({
|
|
487
|
+
kind: "context_window_updated",
|
|
488
|
+
usage: usageSnapshot,
|
|
489
|
+
}),
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (sdkMessage?.type === "result") {
|
|
495
|
+
const resultContextWindow = maxClaudeContextWindowFromModelUsage(sdkMessage.modelUsage)
|
|
496
|
+
if (resultContextWindow !== undefined) {
|
|
497
|
+
lastKnownContextWindow = resultContextWindow
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const accumulatedUsage = normalizeClaudeUsageSnapshot(
|
|
501
|
+
sdkMessage.usage,
|
|
502
|
+
resultContextWindow ?? lastKnownContextWindow,
|
|
503
|
+
)
|
|
504
|
+
const finalUsage = latestUsageSnapshot
|
|
505
|
+
? {
|
|
506
|
+
...latestUsageSnapshot,
|
|
507
|
+
...(typeof (resultContextWindow ?? lastKnownContextWindow) === "number"
|
|
508
|
+
? { maxTokens: resultContextWindow ?? lastKnownContextWindow }
|
|
509
|
+
: {}),
|
|
510
|
+
...(accumulatedUsage && accumulatedUsage.usedTokens > latestUsageSnapshot.usedTokens
|
|
511
|
+
? { totalProcessedTokens: accumulatedUsage.usedTokens }
|
|
512
|
+
: {}),
|
|
513
|
+
}
|
|
514
|
+
: accumulatedUsage
|
|
515
|
+
|
|
516
|
+
if (finalUsage) {
|
|
517
|
+
yield {
|
|
518
|
+
type: "transcript",
|
|
519
|
+
entry: timestamped({
|
|
520
|
+
kind: "context_window_updated",
|
|
521
|
+
usage: finalUsage,
|
|
522
|
+
}),
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
seenAssistantUsageIds = new Set<string>()
|
|
527
|
+
latestUsageSnapshot = null
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
for (const entry of normalizeClaudeStreamMessage(sdkMessage)) {
|
|
531
|
+
yield { type: "transcript", entry }
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
class AsyncMessageQueue<T> implements AsyncIterable<T> {
|
|
537
|
+
private readonly values: T[] = []
|
|
538
|
+
private readonly waiters: Array<(result: IteratorResult<T>) => void> = []
|
|
539
|
+
private closed = false
|
|
540
|
+
|
|
541
|
+
push(value: T) {
|
|
542
|
+
if (this.closed) {
|
|
543
|
+
throw new Error("Cannot push to a closed queue")
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const waiter = this.waiters.shift()
|
|
547
|
+
if (waiter) {
|
|
548
|
+
waiter({ done: false, value })
|
|
549
|
+
return
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
this.values.push(value)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
close() {
|
|
556
|
+
if (this.closed) return
|
|
557
|
+
this.closed = true
|
|
558
|
+
while (this.waiters.length > 0) {
|
|
559
|
+
const waiter = this.waiters.shift()
|
|
560
|
+
waiter?.({ done: true, value: undefined as never })
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
[Symbol.asyncIterator](): AsyncIterator<T> {
|
|
565
|
+
return {
|
|
566
|
+
next: async () => {
|
|
567
|
+
if (this.values.length > 0) {
|
|
568
|
+
return { done: false, value: this.values.shift() as T }
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (this.closed) {
|
|
572
|
+
return { done: true, value: undefined as never }
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return await new Promise<IteratorResult<T>>((resolve) => {
|
|
576
|
+
this.waiters.push(resolve)
|
|
577
|
+
})
|
|
578
|
+
},
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async function startClaudeSession(args: {
|
|
584
|
+
localPath: string
|
|
585
|
+
model: string
|
|
586
|
+
effort?: string
|
|
587
|
+
planMode: boolean
|
|
588
|
+
sessionToken: string | null
|
|
589
|
+
forkSession: boolean
|
|
590
|
+
onToolRequest: (request: HarnessToolRequest) => Promise<unknown>
|
|
591
|
+
}): Promise<ClaudeSessionHandle> {
|
|
592
|
+
const canUseTool: CanUseTool = async (toolName, input, options) => {
|
|
593
|
+
if (toolName !== "AskUserQuestion" && toolName !== "ExitPlanMode") {
|
|
594
|
+
return {
|
|
595
|
+
behavior: "allow",
|
|
596
|
+
updatedInput: input,
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const tool = normalizeToolCall({
|
|
601
|
+
toolName,
|
|
602
|
+
toolId: options.toolUseID,
|
|
603
|
+
input: (input ?? {}) as Record<string, unknown>,
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
if (tool.toolKind !== "ask_user_question" && tool.toolKind !== "exit_plan_mode") {
|
|
607
|
+
return {
|
|
608
|
+
behavior: "deny",
|
|
609
|
+
message: "Unsupported tool request",
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const result = await args.onToolRequest({ tool })
|
|
614
|
+
|
|
615
|
+
if (tool.toolKind === "ask_user_question") {
|
|
616
|
+
const record = result && typeof result === "object" ? result as Record<string, unknown> : {}
|
|
617
|
+
return {
|
|
618
|
+
behavior: "allow",
|
|
619
|
+
updatedInput: {
|
|
620
|
+
...(tool.rawInput ?? {}),
|
|
621
|
+
questions: record.questions ?? tool.input.questions,
|
|
622
|
+
answers: record.answers ?? result,
|
|
623
|
+
},
|
|
624
|
+
} satisfies PermissionResult
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const record = result && typeof result === "object" ? result as Record<string, unknown> : {}
|
|
628
|
+
const confirmed = Boolean(record.confirmed)
|
|
629
|
+
if (confirmed) {
|
|
630
|
+
return {
|
|
631
|
+
behavior: "allow",
|
|
632
|
+
updatedInput: {
|
|
633
|
+
...(tool.rawInput ?? {}),
|
|
634
|
+
...record,
|
|
635
|
+
},
|
|
636
|
+
} satisfies PermissionResult
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
behavior: "deny",
|
|
641
|
+
message: typeof record.message === "string"
|
|
642
|
+
? `User wants to suggest edits to the plan: ${record.message}`
|
|
643
|
+
: "User wants to suggest edits to the plan before approving.",
|
|
644
|
+
} satisfies PermissionResult
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const promptQueue = new AsyncMessageQueue<SDKUserMessage>()
|
|
648
|
+
|
|
649
|
+
const q = query({
|
|
650
|
+
prompt: promptQueue,
|
|
651
|
+
options: {
|
|
652
|
+
cwd: args.localPath,
|
|
653
|
+
model: args.model,
|
|
654
|
+
effort: args.effort as "low" | "medium" | "high" | "max" | undefined,
|
|
655
|
+
resume: args.sessionToken ?? undefined,
|
|
656
|
+
forkSession: args.forkSession,
|
|
657
|
+
permissionMode: args.planMode ? "plan" : "acceptEdits",
|
|
658
|
+
canUseTool,
|
|
659
|
+
tools: [...CLAUDE_TOOLSET],
|
|
660
|
+
settingSources: ["user", "project", "local"],
|
|
661
|
+
env: (() => { const { CLAUDECODE: _, ...env } = process.env; return env })(),
|
|
662
|
+
},
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
provider: "claude",
|
|
667
|
+
stream: createClaudeHarnessStream(q),
|
|
668
|
+
getAccountInfo: async () => {
|
|
669
|
+
try {
|
|
670
|
+
return await q.accountInfo()
|
|
671
|
+
} catch {
|
|
672
|
+
return null
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
interrupt: async () => {
|
|
676
|
+
await q.interrupt()
|
|
677
|
+
},
|
|
678
|
+
sendPrompt: async (content: string) => {
|
|
679
|
+
promptQueue.push({
|
|
680
|
+
type: "user",
|
|
681
|
+
message: {
|
|
682
|
+
role: "user",
|
|
683
|
+
content,
|
|
684
|
+
},
|
|
685
|
+
parent_tool_use_id: null,
|
|
686
|
+
session_id: args.sessionToken ?? "",
|
|
687
|
+
})
|
|
688
|
+
},
|
|
689
|
+
setModel: async (model: string) => {
|
|
690
|
+
await q.setModel(model)
|
|
691
|
+
},
|
|
692
|
+
setPermissionMode: async (planMode: boolean) => {
|
|
693
|
+
await q.setPermissionMode(planMode ? "plan" : "acceptEdits")
|
|
694
|
+
},
|
|
695
|
+
getSupportedCommands: async () => {
|
|
696
|
+
try {
|
|
697
|
+
return await q.supportedCommands()
|
|
698
|
+
} catch (error) {
|
|
699
|
+
console.warn("[kanna/claude] supportedCommands failed", error)
|
|
700
|
+
return []
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
close: () => {
|
|
704
|
+
promptQueue.close()
|
|
705
|
+
q.close()
|
|
706
|
+
},
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export class AgentCoordinator {
|
|
711
|
+
private readonly store: EventStore
|
|
712
|
+
private readonly onStateChange: (chatId?: string, options?: { immediate?: boolean }) => void
|
|
713
|
+
private readonly analytics: AnalyticsReporter
|
|
714
|
+
private readonly codexManager: CodexAppServerManager
|
|
715
|
+
private readonly generateTitle: (messageContent: string, cwd: string) => Promise<GenerateChatTitleResult>
|
|
716
|
+
private readonly startClaudeSessionFn: NonNullable<AgentCoordinatorArgs["startClaudeSession"]>
|
|
717
|
+
private reportBackgroundError: ((message: string) => void) | null = null
|
|
718
|
+
readonly activeTurns = new Map<string, ActiveTurn>()
|
|
719
|
+
readonly drainingStreams = new Map<string, { turn: HarnessTurn }>()
|
|
720
|
+
readonly claudeSessions = new Map<string, ClaudeSessionState>()
|
|
721
|
+
private readonly slashCommandsInFlight = new Set<string>()
|
|
722
|
+
private readonly claudeLimitDetector: LimitDetector
|
|
723
|
+
private readonly codexLimitDetector: LimitDetector
|
|
724
|
+
private readonly scheduleManager: ScheduleManager | null
|
|
725
|
+
private readonly getAutoResumePreference: () => boolean
|
|
726
|
+
private readonly throwOnClaudeSessionStart: boolean
|
|
727
|
+
private readonly autoResumeByChat = new Map<string, boolean>()
|
|
728
|
+
private readonly tunnelGateway: TunnelGateway | null
|
|
729
|
+
private readonly pendingBashCalls = new Map<string, { command: string; chatId: string }>()
|
|
730
|
+
|
|
731
|
+
constructor(args: AgentCoordinatorArgs) {
|
|
732
|
+
this.store = args.store
|
|
733
|
+
this.onStateChange = args.onStateChange
|
|
734
|
+
this.analytics = args.analytics ?? NoopAnalyticsReporter
|
|
735
|
+
this.codexManager = args.codexManager ?? new CodexAppServerManager()
|
|
736
|
+
this.generateTitle = args.generateTitle ?? generateTitleForChatDetailed
|
|
737
|
+
this.startClaudeSessionFn = args.startClaudeSession ?? startClaudeSession
|
|
738
|
+
this.claudeLimitDetector = args.claudeLimitDetector ?? new ClaudeLimitDetector()
|
|
739
|
+
this.codexLimitDetector = args.codexLimitDetector ?? new CodexLimitDetector()
|
|
740
|
+
this.scheduleManager = args.scheduleManager ?? null
|
|
741
|
+
this.getAutoResumePreference = args.getAutoResumePreference ?? (() => false)
|
|
742
|
+
this.throwOnClaudeSessionStart = args.throwOnClaudeSessionStart ?? false
|
|
743
|
+
this.tunnelGateway = args.tunnelGateway ?? null
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
setBackgroundErrorReporter(report: ((message: string) => void) | null) {
|
|
747
|
+
this.reportBackgroundError = report
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
getActiveStatuses() {
|
|
751
|
+
const statuses = new Map<string, KannaStatus>()
|
|
752
|
+
for (const [chatId, turn] of this.activeTurns.entries()) {
|
|
753
|
+
statuses.set(chatId, turn.status)
|
|
754
|
+
}
|
|
755
|
+
return statuses
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
getPendingTool(chatId: string): PendingToolSnapshot | null {
|
|
759
|
+
const pending = this.activeTurns.get(chatId)?.pendingTool
|
|
760
|
+
if (!pending) return null
|
|
761
|
+
return { toolUseId: pending.toolUseId, toolKind: pending.tool.toolKind }
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
getDrainingChatIds(): Set<string> {
|
|
765
|
+
return new Set(this.drainingStreams.keys())
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
getSlashCommandsLoadingChatIds(): Set<string> {
|
|
769
|
+
return new Set(this.slashCommandsInFlight)
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
private emitStateChange(chatId?: string, options?: { immediate?: boolean }) {
|
|
773
|
+
this.onStateChange(chatId, options)
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
private trackBashToolEntry(chatId: string, entry: TranscriptEntry): void {
|
|
777
|
+
if (!this.tunnelGateway) return
|
|
778
|
+
|
|
779
|
+
if (entry.kind === "tool_call" && entry.tool.toolKind === "bash") {
|
|
780
|
+
const command = entry.tool.input.command ?? ""
|
|
781
|
+
this.pendingBashCalls.set(entry.tool.toolId, { command, chatId })
|
|
782
|
+
return
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (entry.kind === "tool_result") {
|
|
786
|
+
const pending = this.pendingBashCalls.get(entry.toolId)
|
|
787
|
+
if (!pending) return
|
|
788
|
+
this.pendingBashCalls.delete(entry.toolId)
|
|
789
|
+
const stdout = stringifyToolResultContent(entry.content)
|
|
790
|
+
void this.tunnelGateway.handleBashResult({
|
|
791
|
+
command: pending.command,
|
|
792
|
+
stdout,
|
|
793
|
+
chatId: pending.chatId,
|
|
794
|
+
sourcePid: null,
|
|
795
|
+
})
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
getActiveTurnProfile(chatId: string): SendToStartingProfile | null {
|
|
800
|
+
const active = this.activeTurns.get(chatId)
|
|
801
|
+
if (!active?.clientTraceId || active.profilingStartedAt === undefined) {
|
|
802
|
+
return null
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return {
|
|
806
|
+
traceId: active.clientTraceId,
|
|
807
|
+
startedAt: active.profilingStartedAt,
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async stopDraining(chatId: string) {
|
|
812
|
+
const draining = this.drainingStreams.get(chatId)
|
|
813
|
+
if (!draining) return
|
|
814
|
+
draining.turn.close()
|
|
815
|
+
this.drainingStreams.delete(chatId)
|
|
816
|
+
this.emitStateChange(chatId)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
async ensureSlashCommandsLoaded(chatId: string): Promise<void> {
|
|
820
|
+
const chat = this.store.getChat(chatId)
|
|
821
|
+
if (!chat) return
|
|
822
|
+
if (chat.provider === "codex") return
|
|
823
|
+
if (chat.slashCommands && chat.slashCommands.length > 0) return
|
|
824
|
+
if (this.slashCommandsInFlight.has(chatId)) return
|
|
825
|
+
|
|
826
|
+
const project = this.store.getProject(chat.projectId)
|
|
827
|
+
if (!project) return
|
|
828
|
+
|
|
829
|
+
this.slashCommandsInFlight.add(chatId)
|
|
830
|
+
this.emitStateChange(chatId)
|
|
831
|
+
try {
|
|
832
|
+
let commands: SlashCommand[]
|
|
833
|
+
const existing = this.claudeSessions.get(chatId)
|
|
834
|
+
if (existing) {
|
|
835
|
+
commands = await existing.session.getSupportedCommands()
|
|
836
|
+
} else {
|
|
837
|
+
const defaultModel = normalizeServerModel("claude")
|
|
838
|
+
const defaultOptions = normalizeClaudeModelOptions(defaultModel)
|
|
839
|
+
const ephemeral = await this.startClaudeSessionFn({
|
|
840
|
+
localPath: project.localPath,
|
|
841
|
+
model: resolveClaudeApiModelId(defaultModel, defaultOptions.contextWindow),
|
|
842
|
+
effort: defaultOptions.reasoningEffort,
|
|
843
|
+
planMode: chat.planMode ?? false,
|
|
844
|
+
sessionToken: chat.sessionToken ?? null,
|
|
845
|
+
forkSession: false,
|
|
846
|
+
onToolRequest: async () => null,
|
|
847
|
+
})
|
|
848
|
+
try {
|
|
849
|
+
commands = await ephemeral.getSupportedCommands()
|
|
850
|
+
} finally {
|
|
851
|
+
ephemeral.close()
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
await this.store.recordSessionCommandsLoaded(chatId, commands)
|
|
855
|
+
this.emitStateChange(chatId)
|
|
856
|
+
} catch (error) {
|
|
857
|
+
console.warn("[kanna/agent] ensureSlashCommandsLoaded failed", error)
|
|
858
|
+
} finally {
|
|
859
|
+
this.slashCommandsInFlight.delete(chatId)
|
|
860
|
+
this.emitStateChange(chatId)
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
async closeChat(chatId: string) {
|
|
865
|
+
await this.stopDraining(chatId)
|
|
866
|
+
const claudeSession = this.claudeSessions.get(chatId)
|
|
867
|
+
if (claudeSession) {
|
|
868
|
+
claudeSession.session.close()
|
|
869
|
+
this.claudeSessions.delete(chatId)
|
|
870
|
+
}
|
|
871
|
+
this.autoResumeByChat.delete(chatId)
|
|
872
|
+
this.emitStateChange(chatId)
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
private resolveProvider(options: SendMessageOptions, currentProvider: AgentProvider | null) {
|
|
876
|
+
if (currentProvider) return currentProvider
|
|
877
|
+
return options.provider ?? "claude"
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
private getProviderSettings(provider: AgentProvider, options: SendMessageOptions) {
|
|
881
|
+
const catalog = getServerProviderCatalog(provider)
|
|
882
|
+
if (provider === "claude") {
|
|
883
|
+
const model = normalizeServerModel(provider, options.model)
|
|
884
|
+
const modelOptions = normalizeClaudeModelOptions(model, options.modelOptions, options.effort)
|
|
885
|
+
return {
|
|
886
|
+
model: resolveClaudeApiModelId(model, modelOptions.contextWindow),
|
|
887
|
+
effort: modelOptions.reasoningEffort,
|
|
888
|
+
serviceTier: undefined,
|
|
889
|
+
planMode: catalog.supportsPlanMode ? Boolean(options.planMode) : false,
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const modelOptions = normalizeCodexModelOptions(options.modelOptions, options.effort)
|
|
894
|
+
return {
|
|
895
|
+
model: normalizeServerModel(provider, options.model),
|
|
896
|
+
effort: modelOptions.reasoningEffort,
|
|
897
|
+
serviceTier: codexServiceTierFromModelOptions(modelOptions),
|
|
898
|
+
planMode: catalog.supportsPlanMode ? Boolean(options.planMode) : false,
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
private async enqueueMessage(chatId: string, content: string, attachments: ChatAttachment[], options?: SendMessageOptions) {
|
|
903
|
+
const queued = await this.store.enqueueMessage(chatId, {
|
|
904
|
+
content,
|
|
905
|
+
attachments,
|
|
906
|
+
provider: options?.provider,
|
|
907
|
+
model: options?.model,
|
|
908
|
+
modelOptions: options?.modelOptions,
|
|
909
|
+
planMode: options?.planMode,
|
|
910
|
+
autoContinue: options?.autoContinue,
|
|
911
|
+
})
|
|
912
|
+
this.emitStateChange(chatId)
|
|
913
|
+
return queued
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
private async dequeueAndStartQueuedMessage(chatId: string, queuedMessage: QueuedChatMessage, options?: { steered?: boolean }) {
|
|
917
|
+
await this.store.removeQueuedMessage(chatId, queuedMessage.id)
|
|
918
|
+
const chat = this.store.requireChat(chatId)
|
|
919
|
+
const provider = this.resolveProvider(queuedMessage, chat.provider)
|
|
920
|
+
const settings = this.getProviderSettings(provider, queuedMessage)
|
|
921
|
+
await this.startTurnForChat({
|
|
922
|
+
chatId,
|
|
923
|
+
provider,
|
|
924
|
+
content: options?.steered ? buildSteeredMessageContent(queuedMessage.content) : queuedMessage.content,
|
|
925
|
+
attachments: queuedMessage.attachments,
|
|
926
|
+
model: settings.model,
|
|
927
|
+
effort: settings.effort,
|
|
928
|
+
serviceTier: settings.serviceTier,
|
|
929
|
+
planMode: settings.planMode,
|
|
930
|
+
appendUserPrompt: true,
|
|
931
|
+
steered: options?.steered,
|
|
932
|
+
autoContinue: queuedMessage.autoContinue,
|
|
933
|
+
})
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
private async maybeStartNextQueuedMessage(chatId: string) {
|
|
937
|
+
if (this.activeTurns.has(chatId)) return false
|
|
938
|
+
const nextQueuedMessage = typeof this.store.getQueuedMessages === "function"
|
|
939
|
+
? this.store.getQueuedMessages(chatId)[0]
|
|
940
|
+
: undefined
|
|
941
|
+
if (!nextQueuedMessage) return false
|
|
942
|
+
await this.dequeueAndStartQueuedMessage(chatId, nextQueuedMessage)
|
|
943
|
+
return true
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
private async startTurnForChat(args: {
|
|
947
|
+
chatId: string
|
|
948
|
+
provider: AgentProvider
|
|
949
|
+
content: string
|
|
950
|
+
attachments: ChatAttachment[]
|
|
951
|
+
model: string
|
|
952
|
+
effort?: string
|
|
953
|
+
serviceTier?: "fast"
|
|
954
|
+
planMode: boolean
|
|
955
|
+
appendUserPrompt: boolean
|
|
956
|
+
steered?: boolean
|
|
957
|
+
autoContinue?: { scheduleId: string }
|
|
958
|
+
profile?: SendToStartingProfile | null
|
|
959
|
+
}) {
|
|
960
|
+
logSendToStartingProfile(args.profile, "start_turn.begin", {
|
|
961
|
+
chatId: args.chatId,
|
|
962
|
+
provider: args.provider,
|
|
963
|
+
appendUserPrompt: args.appendUserPrompt,
|
|
964
|
+
planMode: args.planMode,
|
|
965
|
+
})
|
|
966
|
+
|
|
967
|
+
// Close any lingering draining stream before starting a new turn.
|
|
968
|
+
const draining = this.drainingStreams.get(args.chatId)
|
|
969
|
+
if (draining) {
|
|
970
|
+
draining.turn.close()
|
|
971
|
+
this.drainingStreams.delete(args.chatId)
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const chat = this.store.requireChat(args.chatId)
|
|
975
|
+
if (this.activeTurns.has(args.chatId)) {
|
|
976
|
+
throw new Error("Chat is already running")
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (!chat.provider) {
|
|
980
|
+
await this.store.setChatProvider(args.chatId, args.provider)
|
|
981
|
+
logSendToStartingProfile(args.profile, "start_turn.provider_set", {
|
|
982
|
+
chatId: args.chatId,
|
|
983
|
+
provider: args.provider,
|
|
984
|
+
})
|
|
985
|
+
}
|
|
986
|
+
await this.store.setPlanMode(args.chatId, args.planMode)
|
|
987
|
+
logSendToStartingProfile(args.profile, "start_turn.plan_mode_set", {
|
|
988
|
+
chatId: args.chatId,
|
|
989
|
+
planMode: args.planMode,
|
|
990
|
+
})
|
|
991
|
+
|
|
992
|
+
const existingMessages = this.store.getMessages(args.chatId)
|
|
993
|
+
const shouldGenerateTitle = args.appendUserPrompt && chat.title === "New Chat" && existingMessages.length === 0
|
|
994
|
+
const optimisticTitle = shouldGenerateTitle ? fallbackTitleFromMessage(args.content) : null
|
|
995
|
+
|
|
996
|
+
if (optimisticTitle) {
|
|
997
|
+
await this.store.renameChat(args.chatId, optimisticTitle)
|
|
998
|
+
logSendToStartingProfile(args.profile, "start_turn.optimistic_title_set", {
|
|
999
|
+
chatId: args.chatId,
|
|
1000
|
+
title: optimisticTitle,
|
|
1001
|
+
})
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const project = this.store.getProject(chat.projectId)
|
|
1005
|
+
if (!project) {
|
|
1006
|
+
throw new Error("Project not found")
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (args.appendUserPrompt) {
|
|
1010
|
+
const userPromptEntry = timestamped(
|
|
1011
|
+
{ kind: "user_prompt", content: args.content, attachments: args.attachments, steered: args.steered, autoContinue: args.autoContinue },
|
|
1012
|
+
Date.now()
|
|
1013
|
+
)
|
|
1014
|
+
await this.store.appendMessage(args.chatId, userPromptEntry)
|
|
1015
|
+
logSendToStartingProfile(args.profile, "start_turn.user_prompt_appended", {
|
|
1016
|
+
chatId: args.chatId,
|
|
1017
|
+
entryId: userPromptEntry._id,
|
|
1018
|
+
})
|
|
1019
|
+
}
|
|
1020
|
+
await this.store.recordTurnStarted(args.chatId)
|
|
1021
|
+
logSendToStartingProfile(args.profile, "start_turn.turn_started_recorded", {
|
|
1022
|
+
chatId: args.chatId,
|
|
1023
|
+
})
|
|
1024
|
+
|
|
1025
|
+
if (shouldGenerateTitle) {
|
|
1026
|
+
void this.generateTitleInBackground(args.chatId, args.content, project.localPath, optimisticTitle ?? "New Chat")
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const onToolRequest = async (request: HarnessToolRequest): Promise<unknown> => {
|
|
1030
|
+
const active = this.activeTurns.get(args.chatId)
|
|
1031
|
+
if (!active) {
|
|
1032
|
+
throw new Error("Chat turn ended unexpectedly")
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
active.status = "waiting_for_user"
|
|
1036
|
+
this.emitStateChange(args.chatId)
|
|
1037
|
+
|
|
1038
|
+
return await new Promise<unknown>((resolve) => {
|
|
1039
|
+
active.pendingTool = {
|
|
1040
|
+
toolUseId: request.tool.toolId,
|
|
1041
|
+
tool: request.tool,
|
|
1042
|
+
resolve,
|
|
1043
|
+
}
|
|
1044
|
+
})
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
let turn: HarnessTurn
|
|
1048
|
+
if (args.provider === "claude") {
|
|
1049
|
+
logSendToStartingProfile(args.profile, "start_turn.provider_boot.begin", {
|
|
1050
|
+
chatId: args.chatId,
|
|
1051
|
+
provider: args.provider,
|
|
1052
|
+
model: args.model,
|
|
1053
|
+
})
|
|
1054
|
+
turn = await this.startClaudeTurn({
|
|
1055
|
+
chatId: args.chatId,
|
|
1056
|
+
localPath: project.localPath,
|
|
1057
|
+
model: args.model,
|
|
1058
|
+
effort: args.effort,
|
|
1059
|
+
planMode: args.planMode,
|
|
1060
|
+
sessionToken: chat.pendingForkSessionToken ?? chat.sessionToken,
|
|
1061
|
+
forkSession: Boolean(chat.pendingForkSessionToken),
|
|
1062
|
+
onToolRequest,
|
|
1063
|
+
})
|
|
1064
|
+
logSendToStartingProfile(args.profile, "start_turn.provider_boot.ready", {
|
|
1065
|
+
chatId: args.chatId,
|
|
1066
|
+
provider: args.provider,
|
|
1067
|
+
model: args.model,
|
|
1068
|
+
})
|
|
1069
|
+
} else {
|
|
1070
|
+
logSendToStartingProfile(args.profile, "start_turn.provider_boot.begin", {
|
|
1071
|
+
chatId: args.chatId,
|
|
1072
|
+
provider: args.provider,
|
|
1073
|
+
model: args.model,
|
|
1074
|
+
})
|
|
1075
|
+
const sessionToken = await this.codexManager.startSession({
|
|
1076
|
+
chatId: args.chatId,
|
|
1077
|
+
cwd: project.localPath,
|
|
1078
|
+
model: args.model,
|
|
1079
|
+
serviceTier: args.serviceTier,
|
|
1080
|
+
sessionToken: chat.sessionToken,
|
|
1081
|
+
pendingForkSessionToken: chat.pendingForkSessionToken,
|
|
1082
|
+
})
|
|
1083
|
+
if (chat.pendingForkSessionToken && sessionToken) {
|
|
1084
|
+
await this.store.setPendingForkSessionToken(args.chatId, null)
|
|
1085
|
+
}
|
|
1086
|
+
logSendToStartingProfile(args.profile, "start_turn.session_ready", {
|
|
1087
|
+
chatId: args.chatId,
|
|
1088
|
+
provider: args.provider,
|
|
1089
|
+
model: args.model,
|
|
1090
|
+
})
|
|
1091
|
+
turn = await this.codexManager.startTurn({
|
|
1092
|
+
chatId: args.chatId,
|
|
1093
|
+
content: buildPromptText(args.content, args.attachments),
|
|
1094
|
+
model: args.model,
|
|
1095
|
+
effort: args.effort as any,
|
|
1096
|
+
serviceTier: args.serviceTier,
|
|
1097
|
+
planMode: args.planMode,
|
|
1098
|
+
onToolRequest,
|
|
1099
|
+
})
|
|
1100
|
+
logSendToStartingProfile(args.profile, "start_turn.provider_boot.ready", {
|
|
1101
|
+
chatId: args.chatId,
|
|
1102
|
+
provider: args.provider,
|
|
1103
|
+
model: args.model,
|
|
1104
|
+
})
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const active: ActiveTurn = {
|
|
1108
|
+
chatId: args.chatId,
|
|
1109
|
+
provider: args.provider,
|
|
1110
|
+
turn,
|
|
1111
|
+
model: args.model,
|
|
1112
|
+
effort: args.effort,
|
|
1113
|
+
serviceTier: args.serviceTier,
|
|
1114
|
+
planMode: args.planMode,
|
|
1115
|
+
status: args.provider === "claude" ? "running" : "starting",
|
|
1116
|
+
pendingTool: null,
|
|
1117
|
+
postToolFollowUp: null,
|
|
1118
|
+
hasFinalResult: false,
|
|
1119
|
+
cancelRequested: false,
|
|
1120
|
+
cancelRecorded: false,
|
|
1121
|
+
clientTraceId: args.profile?.traceId,
|
|
1122
|
+
profilingStartedAt: args.profile?.startedAt,
|
|
1123
|
+
}
|
|
1124
|
+
this.activeTurns.set(args.chatId, active)
|
|
1125
|
+
logSendToStartingProfile(args.profile, "start_turn.active_turn_registered", {
|
|
1126
|
+
chatId: args.chatId,
|
|
1127
|
+
status: active.status,
|
|
1128
|
+
})
|
|
1129
|
+
this.emitStateChange(args.chatId, { immediate: active.status === "starting" })
|
|
1130
|
+
logSendToStartingProfile(args.profile, "start_turn.state_change_emitted", {
|
|
1131
|
+
chatId: args.chatId,
|
|
1132
|
+
status: active.status,
|
|
1133
|
+
})
|
|
1134
|
+
|
|
1135
|
+
if (turn.getAccountInfo) {
|
|
1136
|
+
void turn.getAccountInfo()
|
|
1137
|
+
.then(async (accountInfo) => {
|
|
1138
|
+
if (!accountInfo) return
|
|
1139
|
+
if (args.provider === "claude") {
|
|
1140
|
+
const session = this.claudeSessions.get(args.chatId)
|
|
1141
|
+
if (session) {
|
|
1142
|
+
if (session.accountInfoLoaded) return
|
|
1143
|
+
session.accountInfoLoaded = true
|
|
1144
|
+
} else {
|
|
1145
|
+
return
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
await this.store.appendMessage(args.chatId, timestamped({ kind: "account_info", accountInfo }))
|
|
1149
|
+
this.emitStateChange(args.chatId)
|
|
1150
|
+
})
|
|
1151
|
+
.catch(() => undefined)
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (args.provider === "claude") {
|
|
1155
|
+
const session = this.claudeSessions.get(args.chatId)
|
|
1156
|
+
if (!session) {
|
|
1157
|
+
throw new Error("Claude session was not initialized")
|
|
1158
|
+
}
|
|
1159
|
+
const promptSeq = session.nextPromptSeq + 1
|
|
1160
|
+
session.nextPromptSeq = promptSeq
|
|
1161
|
+
session.pendingPromptSeqs.push(promptSeq)
|
|
1162
|
+
active.claudePromptSeq = promptSeq
|
|
1163
|
+
logClaudeSteer("claude_prompt_sent", {
|
|
1164
|
+
chatId: args.chatId,
|
|
1165
|
+
sessionId: session.id,
|
|
1166
|
+
promptSeq,
|
|
1167
|
+
activeStatus: active.status,
|
|
1168
|
+
contentPreview: args.content.slice(0, 160),
|
|
1169
|
+
pendingPromptSeqs: [...session.pendingPromptSeqs],
|
|
1170
|
+
})
|
|
1171
|
+
await session.session.sendPrompt(buildPromptText(args.content, args.attachments))
|
|
1172
|
+
logSendToStartingProfile(args.profile, "start_turn.claude_prompt_sent", {
|
|
1173
|
+
chatId: args.chatId,
|
|
1174
|
+
})
|
|
1175
|
+
return
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
void this.runTurn(active)
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
private async startClaudeTurn(args: {
|
|
1182
|
+
chatId: string
|
|
1183
|
+
localPath: string
|
|
1184
|
+
model: string
|
|
1185
|
+
effort?: string
|
|
1186
|
+
planMode: boolean
|
|
1187
|
+
sessionToken: string | null
|
|
1188
|
+
forkSession: boolean
|
|
1189
|
+
onToolRequest: (request: HarnessToolRequest) => Promise<unknown>
|
|
1190
|
+
}): Promise<HarnessTurn> {
|
|
1191
|
+
let session = this.claudeSessions.get(args.chatId)
|
|
1192
|
+
|
|
1193
|
+
if (!session || session.localPath !== args.localPath || session.effort !== args.effort || args.forkSession) {
|
|
1194
|
+
if (session) {
|
|
1195
|
+
session.session.close()
|
|
1196
|
+
this.claudeSessions.delete(args.chatId)
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
const started = await this.startClaudeSessionFn({
|
|
1200
|
+
localPath: args.localPath,
|
|
1201
|
+
model: args.model,
|
|
1202
|
+
effort: args.effort,
|
|
1203
|
+
planMode: args.planMode,
|
|
1204
|
+
sessionToken: args.sessionToken,
|
|
1205
|
+
forkSession: args.forkSession,
|
|
1206
|
+
onToolRequest: args.onToolRequest,
|
|
1207
|
+
})
|
|
1208
|
+
|
|
1209
|
+
session = {
|
|
1210
|
+
id: crypto.randomUUID(),
|
|
1211
|
+
chatId: args.chatId,
|
|
1212
|
+
session: started,
|
|
1213
|
+
localPath: args.localPath,
|
|
1214
|
+
model: args.model,
|
|
1215
|
+
effort: args.effort,
|
|
1216
|
+
planMode: args.planMode,
|
|
1217
|
+
sessionToken: args.sessionToken,
|
|
1218
|
+
accountInfoLoaded: false,
|
|
1219
|
+
nextPromptSeq: 0,
|
|
1220
|
+
pendingPromptSeqs: [],
|
|
1221
|
+
}
|
|
1222
|
+
this.claudeSessions.set(args.chatId, session)
|
|
1223
|
+
void this.runClaudeSession(session)
|
|
1224
|
+
void (async () => {
|
|
1225
|
+
try {
|
|
1226
|
+
const commands = await started.getSupportedCommands()
|
|
1227
|
+
await this.store.recordSessionCommandsLoaded(args.chatId, commands)
|
|
1228
|
+
this.emitStateChange(args.chatId)
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
console.warn("[kanna/agent] failed to load slash commands", error)
|
|
1231
|
+
}
|
|
1232
|
+
})()
|
|
1233
|
+
} else {
|
|
1234
|
+
if (session.model !== args.model) {
|
|
1235
|
+
await session.session.setModel(args.model)
|
|
1236
|
+
session.model = args.model
|
|
1237
|
+
}
|
|
1238
|
+
if (session.planMode !== args.planMode) {
|
|
1239
|
+
await session.session.setPermissionMode(args.planMode)
|
|
1240
|
+
session.planMode = args.planMode
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
return {
|
|
1245
|
+
provider: "claude",
|
|
1246
|
+
stream: {
|
|
1247
|
+
async *[Symbol.asyncIterator]() {},
|
|
1248
|
+
},
|
|
1249
|
+
getAccountInfo: session.session.getAccountInfo,
|
|
1250
|
+
interrupt: session.session.interrupt,
|
|
1251
|
+
close: () => {},
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
async send(command: Extract<ClientCommand, { type: "chat.send" }>) {
|
|
1256
|
+
const profile = command.clientTraceId
|
|
1257
|
+
? { traceId: command.clientTraceId, startedAt: performance.now() }
|
|
1258
|
+
: null
|
|
1259
|
+
let chatId = command.chatId
|
|
1260
|
+
|
|
1261
|
+
logSendToStartingProfile(profile, "chat_send.received", {
|
|
1262
|
+
existingChatId: command.chatId ?? null,
|
|
1263
|
+
projectId: command.projectId ?? null,
|
|
1264
|
+
})
|
|
1265
|
+
|
|
1266
|
+
if (!chatId) {
|
|
1267
|
+
if (!command.projectId) {
|
|
1268
|
+
throw new Error("Missing projectId for new chat")
|
|
1269
|
+
}
|
|
1270
|
+
const created = await this.store.createChat(command.projectId)
|
|
1271
|
+
chatId = created.id
|
|
1272
|
+
this.analytics.track("chat_created")
|
|
1273
|
+
logSendToStartingProfile(profile, "chat_send.chat_created", {
|
|
1274
|
+
chatId,
|
|
1275
|
+
projectId: command.projectId,
|
|
1276
|
+
})
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
if (typeof command.autoResumeOnRateLimit === "boolean" && chatId) {
|
|
1280
|
+
this.autoResumeByChat.set(chatId, command.autoResumeOnRateLimit)
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
const chat = this.store.requireChat(chatId)
|
|
1284
|
+
if (this.activeTurns.has(chatId)) {
|
|
1285
|
+
this.analytics.track("message_sent")
|
|
1286
|
+
const queuedMessage = await this.enqueueMessage(chatId, command.content, command.attachments ?? [], {
|
|
1287
|
+
provider: command.provider,
|
|
1288
|
+
model: command.model,
|
|
1289
|
+
modelOptions: command.modelOptions,
|
|
1290
|
+
effort: command.effort,
|
|
1291
|
+
planMode: command.planMode,
|
|
1292
|
+
})
|
|
1293
|
+
return { chatId, queuedMessageId: queuedMessage.id, queued: true as const }
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
const provider = this.resolveProvider(command, chat.provider)
|
|
1297
|
+
const settings = this.getProviderSettings(provider, command)
|
|
1298
|
+
this.analytics.track("message_sent")
|
|
1299
|
+
await this.startTurnForChat({
|
|
1300
|
+
chatId,
|
|
1301
|
+
provider,
|
|
1302
|
+
content: command.content,
|
|
1303
|
+
attachments: command.attachments ?? [],
|
|
1304
|
+
model: settings.model,
|
|
1305
|
+
effort: settings.effort,
|
|
1306
|
+
serviceTier: settings.serviceTier,
|
|
1307
|
+
planMode: settings.planMode,
|
|
1308
|
+
appendUserPrompt: true,
|
|
1309
|
+
profile,
|
|
1310
|
+
})
|
|
1311
|
+
|
|
1312
|
+
logSendToStartingProfile(profile, "chat_send.ready_for_ack", {
|
|
1313
|
+
chatId,
|
|
1314
|
+
provider,
|
|
1315
|
+
model: settings.model,
|
|
1316
|
+
})
|
|
1317
|
+
|
|
1318
|
+
return { chatId }
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
async enqueue(command: Extract<ClientCommand, { type: "message.enqueue" }>) {
|
|
1322
|
+
if (typeof command.autoResumeOnRateLimit === "boolean") {
|
|
1323
|
+
this.autoResumeByChat.set(command.chatId, command.autoResumeOnRateLimit)
|
|
1324
|
+
}
|
|
1325
|
+
this.analytics.track("message_sent")
|
|
1326
|
+
const queuedMessage = await this.enqueueMessage(command.chatId, command.content, command.attachments ?? [], {
|
|
1327
|
+
provider: command.provider,
|
|
1328
|
+
model: command.model,
|
|
1329
|
+
modelOptions: command.modelOptions,
|
|
1330
|
+
planMode: command.planMode,
|
|
1331
|
+
})
|
|
1332
|
+
return { queuedMessageId: queuedMessage.id }
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
async steer(command: Extract<ClientCommand, { type: "message.steer" }>) {
|
|
1336
|
+
const queuedMessage = this.store.getQueuedMessage(command.chatId, command.queuedMessageId)
|
|
1337
|
+
if (!queuedMessage) {
|
|
1338
|
+
throw new Error("Queued message not found")
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
logClaudeSteer("steer_requested", {
|
|
1342
|
+
chatId: command.chatId,
|
|
1343
|
+
queuedMessageId: command.queuedMessageId,
|
|
1344
|
+
activeTurn: this.activeTurns.has(command.chatId),
|
|
1345
|
+
queuedMessagePreview: queuedMessage.content.slice(0, 160),
|
|
1346
|
+
})
|
|
1347
|
+
|
|
1348
|
+
if (this.activeTurns.has(command.chatId)) {
|
|
1349
|
+
await this.cancel(command.chatId, { hideInterrupted: true })
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
logClaudeSteer("steer_after_cancel", {
|
|
1353
|
+
chatId: command.chatId,
|
|
1354
|
+
stillActive: this.activeTurns.has(command.chatId),
|
|
1355
|
+
})
|
|
1356
|
+
|
|
1357
|
+
if (this.activeTurns.has(command.chatId)) {
|
|
1358
|
+
throw new Error("Chat is still running")
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
await this.dequeueAndStartQueuedMessage(command.chatId, queuedMessage, { steered: true })
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
async dequeue(command: Extract<ClientCommand, { type: "message.dequeue" }>) {
|
|
1365
|
+
const queuedMessage = this.store.getQueuedMessage(command.chatId, command.queuedMessageId)
|
|
1366
|
+
if (!queuedMessage) {
|
|
1367
|
+
throw new Error("Queued message not found")
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
await this.store.removeQueuedMessage(command.chatId, command.queuedMessageId)
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
async forkChat(chatId: string) {
|
|
1374
|
+
const chat = this.store.requireChat(chatId)
|
|
1375
|
+
if (this.activeTurns.has(chatId) || this.drainingStreams.has(chatId)) {
|
|
1376
|
+
throw new Error("Chat must be idle before forking")
|
|
1377
|
+
}
|
|
1378
|
+
if (!chat.provider) {
|
|
1379
|
+
throw new Error("Chat must have a provider before forking")
|
|
1380
|
+
}
|
|
1381
|
+
if (!chat.sessionToken && !chat.pendingForkSessionToken) {
|
|
1382
|
+
throw new Error("Chat has no session to fork")
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
const forked = await this.store.forkChat(chatId)
|
|
1386
|
+
this.analytics.track("chat_created")
|
|
1387
|
+
return { chatId: forked.id }
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
private async runClaudeSession(session: ClaudeSessionState) {
|
|
1391
|
+
try {
|
|
1392
|
+
let simulateLimit = this.throwOnClaudeSessionStart
|
|
1393
|
+
for await (const event of session.session.stream) {
|
|
1394
|
+
if (simulateLimit) {
|
|
1395
|
+
simulateLimit = false
|
|
1396
|
+
throw new Error("simulated rate limit")
|
|
1397
|
+
}
|
|
1398
|
+
if (event.type === "session_token" && event.sessionToken) {
|
|
1399
|
+
session.sessionToken = event.sessionToken
|
|
1400
|
+
await this.store.setSessionToken(session.chatId, event.sessionToken)
|
|
1401
|
+
this.emitStateChange(session.chatId)
|
|
1402
|
+
continue
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
if (!event.entry) continue
|
|
1406
|
+
await this.store.appendMessage(session.chatId, event.entry)
|
|
1407
|
+
this.trackBashToolEntry(session.chatId, event.entry)
|
|
1408
|
+
const active = this.activeTurns.get(session.chatId)
|
|
1409
|
+
if (event.entry.kind === "system_init" && active) {
|
|
1410
|
+
active.status = "running"
|
|
1411
|
+
const chat = this.store.getChat(session.chatId)
|
|
1412
|
+
if (
|
|
1413
|
+
chat?.pendingForkSessionToken
|
|
1414
|
+
&& session.sessionToken
|
|
1415
|
+
&& session.sessionToken !== chat.pendingForkSessionToken
|
|
1416
|
+
) {
|
|
1417
|
+
await this.store.setPendingForkSessionToken(session.chatId, null)
|
|
1418
|
+
}
|
|
1419
|
+
logClaudeSteer("claude_event_system_init", {
|
|
1420
|
+
chatId: session.chatId,
|
|
1421
|
+
sessionId: session.id,
|
|
1422
|
+
activePromptSeq: active.claudePromptSeq ?? null,
|
|
1423
|
+
pendingPromptSeqs: [...session.pendingPromptSeqs],
|
|
1424
|
+
})
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
const completedClaudePromptSeq = event.entry.kind === "result" || event.entry.kind === "interrupted"
|
|
1428
|
+
? (session.pendingPromptSeqs.shift() ?? null)
|
|
1429
|
+
: null
|
|
1430
|
+
|
|
1431
|
+
logClaudeSteer("claude_event", {
|
|
1432
|
+
chatId: session.chatId,
|
|
1433
|
+
sessionId: session.id,
|
|
1434
|
+
entryKind: event.entry.kind,
|
|
1435
|
+
activePromptSeq: active?.claudePromptSeq ?? null,
|
|
1436
|
+
completedPromptSeq: completedClaudePromptSeq,
|
|
1437
|
+
activeStatus: active?.status ?? null,
|
|
1438
|
+
pendingPromptSeqs: [...session.pendingPromptSeqs],
|
|
1439
|
+
})
|
|
1440
|
+
|
|
1441
|
+
if (event.entry.kind === "result" && active && completedClaudePromptSeq === (active.claudePromptSeq ?? null)) {
|
|
1442
|
+
active.hasFinalResult = true
|
|
1443
|
+
if (event.entry.isError) {
|
|
1444
|
+
const resultText = event.entry.result || "Turn failed"
|
|
1445
|
+
const detection = this.claudeLimitDetector.detectFromResultText?.(session.chatId, resultText) ?? null
|
|
1446
|
+
let handled = false
|
|
1447
|
+
if (detection) {
|
|
1448
|
+
handled = await this.handleLimitDetection(session.chatId, detection)
|
|
1449
|
+
}
|
|
1450
|
+
if (handled) {
|
|
1451
|
+
await this.store.recordTurnFailed(session.chatId, "rate_limit")
|
|
1452
|
+
} else {
|
|
1453
|
+
await this.store.recordTurnFailed(session.chatId, resultText)
|
|
1454
|
+
}
|
|
1455
|
+
} else if (!active.cancelRequested) {
|
|
1456
|
+
await this.store.recordTurnFinished(session.chatId)
|
|
1457
|
+
}
|
|
1458
|
+
this.activeTurns.delete(session.chatId)
|
|
1459
|
+
if (!active.cancelRequested) {
|
|
1460
|
+
await this.maybeStartNextQueuedMessage(session.chatId)
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
this.emitStateChange(session.chatId)
|
|
1465
|
+
}
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
const active = this.activeTurns.get(session.chatId)
|
|
1468
|
+
if (active && !active.cancelRequested) {
|
|
1469
|
+
const handled = await this.handleLimitError(session.chatId, this.claudeLimitDetector, error)
|
|
1470
|
+
if (!handled) {
|
|
1471
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1472
|
+
await this.store.appendMessage(
|
|
1473
|
+
session.chatId,
|
|
1474
|
+
timestamped({
|
|
1475
|
+
kind: "result",
|
|
1476
|
+
subtype: "error",
|
|
1477
|
+
isError: true,
|
|
1478
|
+
durationMs: 0,
|
|
1479
|
+
result: message,
|
|
1480
|
+
})
|
|
1481
|
+
)
|
|
1482
|
+
await this.store.recordTurnFailed(session.chatId, message)
|
|
1483
|
+
} else {
|
|
1484
|
+
await this.store.recordTurnFailed(session.chatId, "rate_limit")
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
} finally {
|
|
1488
|
+
this.claudeSessions.delete(session.chatId)
|
|
1489
|
+
const active = this.activeTurns.get(session.chatId)
|
|
1490
|
+
if (active?.provider === "claude") {
|
|
1491
|
+
if (active.cancelRequested && !active.cancelRecorded) {
|
|
1492
|
+
await this.store.recordTurnCancelled(session.chatId)
|
|
1493
|
+
}
|
|
1494
|
+
this.activeTurns.delete(session.chatId)
|
|
1495
|
+
}
|
|
1496
|
+
session.session.close()
|
|
1497
|
+
this.emitStateChange(session.chatId)
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
private async generateTitleInBackground(chatId: string, messageContent: string, cwd: string, expectedCurrentTitle: string) {
|
|
1502
|
+
try {
|
|
1503
|
+
const result = await this.generateTitle(messageContent, cwd)
|
|
1504
|
+
if (result.failureMessage) {
|
|
1505
|
+
this.reportBackgroundError?.(
|
|
1506
|
+
`[title-generation] chat ${chatId} failed provider title generation: ${result.failureMessage}`
|
|
1507
|
+
)
|
|
1508
|
+
}
|
|
1509
|
+
if (!result.title || result.usedFallback) return
|
|
1510
|
+
|
|
1511
|
+
const chat = this.store.requireChat(chatId)
|
|
1512
|
+
if (chat.title !== expectedCurrentTitle) return
|
|
1513
|
+
|
|
1514
|
+
await this.store.renameChat(chatId, result.title)
|
|
1515
|
+
this.emitStateChange(chatId)
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1518
|
+
this.reportBackgroundError?.(
|
|
1519
|
+
`[title-generation] chat ${chatId} failed background title generation: ${message}`
|
|
1520
|
+
)
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
private async runTurn(active: ActiveTurn) {
|
|
1525
|
+
try {
|
|
1526
|
+
for await (const event of active.turn.stream) {
|
|
1527
|
+
// Once cancelled, stop processing further stream events.
|
|
1528
|
+
// cancel() already removed us from activeTurns and notified the UI.
|
|
1529
|
+
if (active.cancelRequested) break
|
|
1530
|
+
|
|
1531
|
+
if (event.type === "session_token" && event.sessionToken) {
|
|
1532
|
+
await this.store.setSessionToken(active.chatId, event.sessionToken)
|
|
1533
|
+
const chat = this.store.getChat(active.chatId)
|
|
1534
|
+
if (
|
|
1535
|
+
chat?.pendingForkSessionToken
|
|
1536
|
+
&& event.sessionToken !== chat.pendingForkSessionToken
|
|
1537
|
+
) {
|
|
1538
|
+
await this.store.setPendingForkSessionToken(active.chatId, null)
|
|
1539
|
+
}
|
|
1540
|
+
this.emitStateChange(active.chatId)
|
|
1541
|
+
continue
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
if (!event.entry) continue
|
|
1545
|
+
await this.store.appendMessage(active.chatId, event.entry)
|
|
1546
|
+
this.trackBashToolEntry(active.chatId, event.entry)
|
|
1547
|
+
|
|
1548
|
+
if (event.entry.kind === "system_init") {
|
|
1549
|
+
active.status = "running"
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
if (event.entry.kind === "result") {
|
|
1553
|
+
active.hasFinalResult = true
|
|
1554
|
+
if (event.entry.isError) {
|
|
1555
|
+
await this.store.recordTurnFailed(active.chatId, event.entry.result || "Turn failed")
|
|
1556
|
+
} else if (!active.cancelRequested) {
|
|
1557
|
+
await this.store.recordTurnFinished(active.chatId)
|
|
1558
|
+
}
|
|
1559
|
+
// Remove from activeTurns as soon as the result arrives so the UI
|
|
1560
|
+
// transitions to idle immediately. The stream may still be open
|
|
1561
|
+
// (e.g. background tasks), but the user should be able to send
|
|
1562
|
+
// new messages without having to hit stop first.
|
|
1563
|
+
this.activeTurns.delete(active.chatId)
|
|
1564
|
+
// Track the still-open stream so the UI can show a draining
|
|
1565
|
+
// indicator and the user can stop background tasks.
|
|
1566
|
+
this.drainingStreams.set(active.chatId, { turn: active.turn })
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
this.emitStateChange(active.chatId)
|
|
1570
|
+
}
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
if (!active.cancelRequested) {
|
|
1573
|
+
const handled = await this.handleLimitError(active.chatId, this.codexLimitDetector, error)
|
|
1574
|
+
if (!handled) {
|
|
1575
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1576
|
+
await this.store.appendMessage(
|
|
1577
|
+
active.chatId,
|
|
1578
|
+
timestamped({
|
|
1579
|
+
kind: "result",
|
|
1580
|
+
subtype: "error",
|
|
1581
|
+
isError: true,
|
|
1582
|
+
durationMs: 0,
|
|
1583
|
+
result: message,
|
|
1584
|
+
})
|
|
1585
|
+
)
|
|
1586
|
+
await this.store.recordTurnFailed(active.chatId, message)
|
|
1587
|
+
} else {
|
|
1588
|
+
await this.store.recordTurnFailed(active.chatId, "rate_limit")
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
} finally {
|
|
1592
|
+
if (active.cancelRequested && !active.cancelRecorded) {
|
|
1593
|
+
await this.store.recordTurnCancelled(active.chatId)
|
|
1594
|
+
}
|
|
1595
|
+
active.turn.close()
|
|
1596
|
+
// Only remove if we're still the active turn for this chat.
|
|
1597
|
+
// We may have already been removed by result handling or cancel(),
|
|
1598
|
+
// and a new turn may have started for the same chatId.
|
|
1599
|
+
if (this.activeTurns.get(active.chatId) === active) {
|
|
1600
|
+
this.activeTurns.delete(active.chatId)
|
|
1601
|
+
}
|
|
1602
|
+
// Stream has fully ended — no longer draining.
|
|
1603
|
+
this.drainingStreams.delete(active.chatId)
|
|
1604
|
+
this.emitStateChange(active.chatId)
|
|
1605
|
+
|
|
1606
|
+
if (active.postToolFollowUp && !active.cancelRequested) {
|
|
1607
|
+
try {
|
|
1608
|
+
await this.startTurnForChat({
|
|
1609
|
+
chatId: active.chatId,
|
|
1610
|
+
provider: active.provider,
|
|
1611
|
+
content: active.postToolFollowUp.content,
|
|
1612
|
+
attachments: [],
|
|
1613
|
+
model: active.model,
|
|
1614
|
+
effort: active.effort,
|
|
1615
|
+
serviceTier: active.serviceTier,
|
|
1616
|
+
planMode: active.postToolFollowUp.planMode,
|
|
1617
|
+
appendUserPrompt: false,
|
|
1618
|
+
})
|
|
1619
|
+
} catch (error) {
|
|
1620
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1621
|
+
await this.store.appendMessage(
|
|
1622
|
+
active.chatId,
|
|
1623
|
+
timestamped({
|
|
1624
|
+
kind: "result",
|
|
1625
|
+
subtype: "error",
|
|
1626
|
+
isError: true,
|
|
1627
|
+
durationMs: 0,
|
|
1628
|
+
result: message,
|
|
1629
|
+
})
|
|
1630
|
+
)
|
|
1631
|
+
await this.store.recordTurnFailed(active.chatId, message)
|
|
1632
|
+
this.emitStateChange(active.chatId)
|
|
1633
|
+
}
|
|
1634
|
+
} else if (!active.cancelRequested) {
|
|
1635
|
+
try {
|
|
1636
|
+
await this.maybeStartNextQueuedMessage(active.chatId)
|
|
1637
|
+
} catch (error) {
|
|
1638
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1639
|
+
await this.store.appendMessage(
|
|
1640
|
+
active.chatId,
|
|
1641
|
+
timestamped({
|
|
1642
|
+
kind: "result",
|
|
1643
|
+
subtype: "error",
|
|
1644
|
+
isError: true,
|
|
1645
|
+
durationMs: 0,
|
|
1646
|
+
result: message,
|
|
1647
|
+
})
|
|
1648
|
+
)
|
|
1649
|
+
await this.store.recordTurnFailed(active.chatId, message)
|
|
1650
|
+
this.emitStateChange(active.chatId)
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
private resolveAutoResumeFor(chatId: string): boolean {
|
|
1657
|
+
const cached = this.autoResumeByChat.get(chatId)
|
|
1658
|
+
if (typeof cached === "boolean") return cached
|
|
1659
|
+
return this.getAutoResumePreference()
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
private async emitAutoContinueEvent(event: AutoContinueEvent): Promise<void> {
|
|
1663
|
+
await this.store.appendAutoContinueEvent(event)
|
|
1664
|
+
this.scheduleManager?.onEvent(event)
|
|
1665
|
+
this.emitStateChange(event.chatId)
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
private getChatSchedule(chatId: string, scheduleId: string) {
|
|
1669
|
+
const events = this.store.getAutoContinueEvents(chatId)
|
|
1670
|
+
return deriveChatSchedules(events, chatId).schedules[scheduleId]
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
private requireFuture(scheduledAt: number): void {
|
|
1674
|
+
if (scheduledAt <= Date.now()) throw new Error("scheduledAt must be in the future")
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
private async handleLimitError(chatId: string, detector: LimitDetector, error: unknown): Promise<boolean> {
|
|
1678
|
+
const detection = detector.detect(chatId, error)
|
|
1679
|
+
if (!detection) return false
|
|
1680
|
+
return this.handleLimitDetection(chatId, detection)
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
private async handleLimitDetection(chatId: string, detection: LimitDetection): Promise<boolean> {
|
|
1684
|
+
const live = deriveChatSchedules(this.store.getAutoContinueEvents(chatId), chatId).liveScheduleId
|
|
1685
|
+
if (live !== null) return true
|
|
1686
|
+
|
|
1687
|
+
const now = Date.now()
|
|
1688
|
+
const scheduleId = crypto.randomUUID()
|
|
1689
|
+
const base = { v: AUTO_CONTINUE_EVENT_VERSION, timestamp: now, chatId, scheduleId }
|
|
1690
|
+
|
|
1691
|
+
const event: AutoContinueEvent = this.resolveAutoResumeFor(chatId)
|
|
1692
|
+
? {
|
|
1693
|
+
...base,
|
|
1694
|
+
kind: "auto_continue_accepted",
|
|
1695
|
+
scheduledAt: detection.resetAt,
|
|
1696
|
+
tz: detection.tz,
|
|
1697
|
+
source: "auto_setting",
|
|
1698
|
+
resetAt: detection.resetAt,
|
|
1699
|
+
detectedAt: now,
|
|
1700
|
+
}
|
|
1701
|
+
: {
|
|
1702
|
+
...base,
|
|
1703
|
+
kind: "auto_continue_proposed",
|
|
1704
|
+
detectedAt: now,
|
|
1705
|
+
resetAt: detection.resetAt,
|
|
1706
|
+
tz: detection.tz,
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
await this.emitAutoContinueEvent(event)
|
|
1710
|
+
await this.store.appendMessage(chatId, timestamped({
|
|
1711
|
+
kind: "auto_continue_prompt",
|
|
1712
|
+
scheduleId,
|
|
1713
|
+
}))
|
|
1714
|
+
|
|
1715
|
+
return true
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
async fireAutoContinue(chatId: string, scheduleId: string) {
|
|
1719
|
+
if (!this.store.getChat(chatId)) return
|
|
1720
|
+
|
|
1721
|
+
const event: AutoContinueEvent = {
|
|
1722
|
+
v: AUTO_CONTINUE_EVENT_VERSION,
|
|
1723
|
+
kind: "auto_continue_fired",
|
|
1724
|
+
timestamp: Date.now(),
|
|
1725
|
+
chatId,
|
|
1726
|
+
scheduleId,
|
|
1727
|
+
}
|
|
1728
|
+
try {
|
|
1729
|
+
await this.store.appendAutoContinueEvent(event)
|
|
1730
|
+
await this.enqueueMessage(chatId, "continue", [], { autoContinue: { scheduleId } })
|
|
1731
|
+
await this.maybeStartNextQueuedMessage(chatId)
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1734
|
+
await this.store.appendMessage(chatId, timestamped({
|
|
1735
|
+
kind: "result",
|
|
1736
|
+
subtype: "error",
|
|
1737
|
+
isError: true,
|
|
1738
|
+
durationMs: 0,
|
|
1739
|
+
result: `Auto-continue failed: ${message}`,
|
|
1740
|
+
}))
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
this.emitStateChange(chatId)
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
async acceptAutoContinue(chatId: string, scheduleId: string, scheduledAt: number): Promise<void> {
|
|
1747
|
+
const schedule = this.getChatSchedule(chatId, scheduleId)
|
|
1748
|
+
if (!schedule) throw new Error("Schedule not found")
|
|
1749
|
+
if (schedule.state !== "proposed") throw new Error("Schedule not pending")
|
|
1750
|
+
this.requireFuture(scheduledAt)
|
|
1751
|
+
|
|
1752
|
+
await this.emitAutoContinueEvent({
|
|
1753
|
+
v: AUTO_CONTINUE_EVENT_VERSION,
|
|
1754
|
+
kind: "auto_continue_accepted",
|
|
1755
|
+
timestamp: Date.now(),
|
|
1756
|
+
chatId,
|
|
1757
|
+
scheduleId,
|
|
1758
|
+
scheduledAt,
|
|
1759
|
+
tz: schedule.tz,
|
|
1760
|
+
source: "user",
|
|
1761
|
+
resetAt: schedule.resetAt,
|
|
1762
|
+
detectedAt: schedule.detectedAt,
|
|
1763
|
+
})
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
async rescheduleAutoContinue(chatId: string, scheduleId: string, scheduledAt: number): Promise<void> {
|
|
1767
|
+
const schedule = this.getChatSchedule(chatId, scheduleId)
|
|
1768
|
+
if (!schedule || schedule.state !== "scheduled") throw new Error("Schedule not active")
|
|
1769
|
+
this.requireFuture(scheduledAt)
|
|
1770
|
+
|
|
1771
|
+
await this.emitAutoContinueEvent({
|
|
1772
|
+
v: AUTO_CONTINUE_EVENT_VERSION,
|
|
1773
|
+
kind: "auto_continue_rescheduled",
|
|
1774
|
+
timestamp: Date.now(),
|
|
1775
|
+
chatId,
|
|
1776
|
+
scheduleId,
|
|
1777
|
+
scheduledAt,
|
|
1778
|
+
})
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
async cancelAutoContinue(chatId: string, scheduleId: string, reason: "user" | "chat_deleted"): Promise<void> {
|
|
1782
|
+
const schedule = this.getChatSchedule(chatId, scheduleId)
|
|
1783
|
+
if (!schedule) return
|
|
1784
|
+
if (schedule.state !== "proposed" && schedule.state !== "scheduled") return
|
|
1785
|
+
|
|
1786
|
+
await this.emitAutoContinueEvent({
|
|
1787
|
+
v: AUTO_CONTINUE_EVENT_VERSION,
|
|
1788
|
+
kind: "auto_continue_cancelled",
|
|
1789
|
+
timestamp: Date.now(),
|
|
1790
|
+
chatId,
|
|
1791
|
+
scheduleId,
|
|
1792
|
+
reason,
|
|
1793
|
+
})
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
listLiveSchedules(chatId: string): string[] {
|
|
1797
|
+
const { schedules } = deriveChatSchedules(this.store.getAutoContinueEvents(chatId), chatId)
|
|
1798
|
+
return Object.values(schedules)
|
|
1799
|
+
.filter((s) => s.state === "proposed" || s.state === "scheduled")
|
|
1800
|
+
.map((s) => s.scheduleId)
|
|
1801
|
+
.sort()
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
async cancel(chatId: string, options?: { hideInterrupted?: boolean }) {
|
|
1805
|
+
// Also clean up any draining stream for this chat.
|
|
1806
|
+
const draining = this.drainingStreams.get(chatId)
|
|
1807
|
+
if (draining) {
|
|
1808
|
+
draining.turn.close()
|
|
1809
|
+
this.drainingStreams.delete(chatId)
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
const active = this.activeTurns.get(chatId)
|
|
1813
|
+
if (!active) return
|
|
1814
|
+
|
|
1815
|
+
logClaudeSteer("cancel_requested", {
|
|
1816
|
+
chatId,
|
|
1817
|
+
provider: active.provider,
|
|
1818
|
+
activePromptSeq: active.claudePromptSeq ?? null,
|
|
1819
|
+
})
|
|
1820
|
+
|
|
1821
|
+
// Guard against concurrent cancel() calls — only the first one does work.
|
|
1822
|
+
if (active.cancelRequested) return
|
|
1823
|
+
active.cancelRequested = true
|
|
1824
|
+
|
|
1825
|
+
const pendingTool = active.pendingTool
|
|
1826
|
+
active.pendingTool = null
|
|
1827
|
+
|
|
1828
|
+
if (pendingTool) {
|
|
1829
|
+
const result = discardedToolResult(pendingTool.tool)
|
|
1830
|
+
await this.store.appendMessage(
|
|
1831
|
+
chatId,
|
|
1832
|
+
timestamped({
|
|
1833
|
+
kind: "tool_result",
|
|
1834
|
+
toolId: pendingTool.toolUseId,
|
|
1835
|
+
content: result,
|
|
1836
|
+
})
|
|
1837
|
+
)
|
|
1838
|
+
if (active.provider === "codex" && pendingTool.tool.toolKind === "exit_plan_mode") {
|
|
1839
|
+
pendingTool.resolve(result)
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
await this.store.appendMessage(chatId, timestamped({ kind: "interrupted", hidden: options?.hideInterrupted }))
|
|
1844
|
+
await this.store.recordTurnCancelled(chatId)
|
|
1845
|
+
active.cancelRecorded = true
|
|
1846
|
+
active.hasFinalResult = true
|
|
1847
|
+
|
|
1848
|
+
// Remove from activeTurns immediately so the UI reflects the cancellation
|
|
1849
|
+
// right away, rather than waiting for interrupt() which may hang.
|
|
1850
|
+
this.activeTurns.delete(chatId)
|
|
1851
|
+
this.emitStateChange(chatId)
|
|
1852
|
+
logClaudeSteer("cancel_active_turn_deleted", {
|
|
1853
|
+
chatId,
|
|
1854
|
+
provider: active.provider,
|
|
1855
|
+
activePromptSeq: active.claudePromptSeq ?? null,
|
|
1856
|
+
})
|
|
1857
|
+
|
|
1858
|
+
// Now attempt to interrupt/close the underlying stream in the background.
|
|
1859
|
+
// This is best-effort — the turn is already removed from active state above,
|
|
1860
|
+
// and runTurn()'s finally block will also call close().
|
|
1861
|
+
try {
|
|
1862
|
+
await Promise.race([
|
|
1863
|
+
active.turn.interrupt(),
|
|
1864
|
+
new Promise((resolve) => setTimeout(resolve, 5_000)),
|
|
1865
|
+
])
|
|
1866
|
+
} catch {
|
|
1867
|
+
// interrupt() failed — force close
|
|
1868
|
+
}
|
|
1869
|
+
active.turn.close()
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
async respondTool(command: Extract<ClientCommand, { type: "chat.respondTool" }>) {
|
|
1873
|
+
const active = this.activeTurns.get(command.chatId)
|
|
1874
|
+
if (!active || !active.pendingTool) {
|
|
1875
|
+
throw new Error("No pending tool request")
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
const pending = active.pendingTool
|
|
1879
|
+
if (pending.toolUseId !== command.toolUseId) {
|
|
1880
|
+
throw new Error("Tool response does not match active request")
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
await this.store.appendMessage(
|
|
1884
|
+
command.chatId,
|
|
1885
|
+
timestamped({
|
|
1886
|
+
kind: "tool_result",
|
|
1887
|
+
toolId: command.toolUseId,
|
|
1888
|
+
content: command.result,
|
|
1889
|
+
})
|
|
1890
|
+
)
|
|
1891
|
+
|
|
1892
|
+
active.pendingTool = null
|
|
1893
|
+
active.status = "running"
|
|
1894
|
+
|
|
1895
|
+
if (pending.tool.toolKind === "exit_plan_mode") {
|
|
1896
|
+
const result = (command.result ?? {}) as {
|
|
1897
|
+
confirmed?: boolean
|
|
1898
|
+
clearContext?: boolean
|
|
1899
|
+
message?: string
|
|
1900
|
+
}
|
|
1901
|
+
if (result.confirmed && result.clearContext) {
|
|
1902
|
+
await this.store.setSessionToken(command.chatId, null)
|
|
1903
|
+
await this.store.appendMessage(command.chatId, timestamped({ kind: "context_cleared" }))
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
if (active.provider === "codex") {
|
|
1907
|
+
active.postToolFollowUp = result.confirmed
|
|
1908
|
+
? {
|
|
1909
|
+
content: result.message
|
|
1910
|
+
? `Proceed with the approved plan. Additional guidance: ${result.message}`
|
|
1911
|
+
: "Proceed with the approved plan.",
|
|
1912
|
+
planMode: false,
|
|
1913
|
+
}
|
|
1914
|
+
: {
|
|
1915
|
+
content: result.message
|
|
1916
|
+
? `Revise the plan using this feedback: ${result.message}`
|
|
1917
|
+
: "Revise the plan using this feedback.",
|
|
1918
|
+
planMode: true,
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
pending.resolve(command.result)
|
|
1924
|
+
|
|
1925
|
+
this.emitStateChange(command.chatId)
|
|
1926
|
+
}
|
|
1927
|
+
}
|