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