relay-ide 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +259 -0
- package/dist/bin/claude-remote-cli.js +390 -0
- package/dist/bin/relay-ide.js +390 -0
- package/dist/frontend/assets/abap-BdImnpbu.js +1 -0
- package/dist/frontend/assets/actionscript-3-CoDkCxhg.js +1 -0
- package/dist/frontend/assets/ada-bCR0ucgS.js +1 -0
- package/dist/frontend/assets/andromeeda-C4gqWexZ.js +1 -0
- package/dist/frontend/assets/angular-html-DA-rfuFy.js +1 -0
- package/dist/frontend/assets/angular-ts-BrjP3tb8.js +1 -0
- package/dist/frontend/assets/apache-Pmp26Uib.js +1 -0
- package/dist/frontend/assets/apex-D8_7TLub.js +1 -0
- package/dist/frontend/assets/apl-CORt7UWP.js +1 -0
- package/dist/frontend/assets/applescript-Co6uUVPk.js +1 -0
- package/dist/frontend/assets/ara-BRHolxvo.js +1 -0
- package/dist/frontend/assets/asciidoc-Ve4PFQV2.js +1 -0
- package/dist/frontend/assets/asm-D_Q5rh1f.js +1 -0
- package/dist/frontend/assets/astro-HNnZUWAn.js +1 -0
- package/dist/frontend/assets/aurora-x-D-2ljcwZ.js +1 -0
- package/dist/frontend/assets/awk-DMzUqQB5.js +1 -0
- package/dist/frontend/assets/ayu-dark-DYE7WIF3.js +1 -0
- package/dist/frontend/assets/ayu-light-BA47KaF1.js +1 -0
- package/dist/frontend/assets/ayu-mirage-32ctXXKs.js +1 -0
- package/dist/frontend/assets/ballerina-BFfxhgS-.js +1 -0
- package/dist/frontend/assets/bat-BkioyH1T.js +1 -0
- package/dist/frontend/assets/beancount-k_qm7-4y.js +1 -0
- package/dist/frontend/assets/berry-uYugtg8r.js +1 -0
- package/dist/frontend/assets/bibtex-CHM0blh-.js +1 -0
- package/dist/frontend/assets/bicep-Bmn6On1c.js +1 -0
- package/dist/frontend/assets/bird2-BIv1doCn.js +1 -0
- package/dist/frontend/assets/blade-BjGOyj-B.js +1 -0
- package/dist/frontend/assets/bsl-BO_Y6i37.js +1 -0
- package/dist/frontend/assets/c-BIGW1oBm.js +1 -0
- package/dist/frontend/assets/c3-eo99z4R2.js +1 -0
- package/dist/frontend/assets/cadence-Bv_4Rxtq.js +1 -0
- package/dist/frontend/assets/cairo-KRGpt6FW.js +1 -0
- package/dist/frontend/assets/catppuccin-frappe-DFWUc33u.js +1 -0
- package/dist/frontend/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
- package/dist/frontend/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
- package/dist/frontend/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
- package/dist/frontend/assets/clarity-D53aC0YG.js +1 -0
- package/dist/frontend/assets/clojure-P80f7IUj.js +1 -0
- package/dist/frontend/assets/cmake-D1j8_8rp.js +1 -0
- package/dist/frontend/assets/cobol-nBiQ_Alo.js +1 -0
- package/dist/frontend/assets/codeowners-Bp6g37R7.js +1 -0
- package/dist/frontend/assets/codeql-DsOJ9woJ.js +1 -0
- package/dist/frontend/assets/coffee-Ch7k5sss.js +1 -0
- package/dist/frontend/assets/common-lisp-Cg-RD9OK.js +1 -0
- package/dist/frontend/assets/coq-DkFqJrB1.js +1 -0
- package/dist/frontend/assets/cpp-CofmeUqb.js +1 -0
- package/dist/frontend/assets/crystal-DNxU26gB.js +1 -0
- package/dist/frontend/assets/csharp-COcwbKMJ.js +1 -0
- package/dist/frontend/assets/css-CLj8gQPS.js +1 -0
- package/dist/frontend/assets/csv-fuZLfV_i.js +1 -0
- package/dist/frontend/assets/cue-D82EKSYY.js +1 -0
- package/dist/frontend/assets/cypher-COkxafJQ.js +1 -0
- package/dist/frontend/assets/d-85-TOEBH.js +1 -0
- package/dist/frontend/assets/dark-plus-C3mMm8J8.js +1 -0
- package/dist/frontend/assets/dart-bE4Kk8sk.js +1 -0
- package/dist/frontend/assets/dax-CEL-wOlO.js +1 -0
- package/dist/frontend/assets/desktop-BmXAJ9_W.js +1 -0
- package/dist/frontend/assets/diff-D97Zzqfu.js +1 -0
- package/dist/frontend/assets/docker-BcOcwvcX.js +1 -0
- package/dist/frontend/assets/dotenv-Da5cRb03.js +1 -0
- package/dist/frontend/assets/dracula-BzJJZx-M.js +1 -0
- package/dist/frontend/assets/dracula-soft-BXkSAIEj.js +1 -0
- package/dist/frontend/assets/dream-maker-BtqSS_iP.js +1 -0
- package/dist/frontend/assets/edge-FbVlp4U3.js +1 -0
- package/dist/frontend/assets/elixir-CkH2-t6x.js +1 -0
- package/dist/frontend/assets/elm-DbKCFpqz.js +1 -0
- package/dist/frontend/assets/emacs-lisp-CXvaQtF9.js +1 -0
- package/dist/frontend/assets/erb-BYCe7drp.js +1 -0
- package/dist/frontend/assets/erlang-DsQrWhSR.js +1 -0
- package/dist/frontend/assets/everforest-dark-BgDCqdQA.js +1 -0
- package/dist/frontend/assets/everforest-light-C8M2exoo.js +1 -0
- package/dist/frontend/assets/fennel-BYunw83y.js +1 -0
- package/dist/frontend/assets/fish-BvzEVeQv.js +1 -0
- package/dist/frontend/assets/fluent-C4IJs8-o.js +1 -0
- package/dist/frontend/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
- package/dist/frontend/assets/fortran-free-form-BxgE0vQu.js +1 -0
- package/dist/frontend/assets/fsharp-CXgrBDvD.js +1 -0
- package/dist/frontend/assets/gdresource-BOOCDP_w.js +1 -0
- package/dist/frontend/assets/gdscript-C5YyOfLZ.js +1 -0
- package/dist/frontend/assets/gdshader-DkwncUOv.js +1 -0
- package/dist/frontend/assets/genie-D0YGMca9.js +1 -0
- package/dist/frontend/assets/gherkin-DyxjwDmM.js +1 -0
- package/dist/frontend/assets/git-commit-F4YmCXRG.js +1 -0
- package/dist/frontend/assets/git-rebase-r7XF79zn.js +1 -0
- package/dist/frontend/assets/github-dark-DHJKELXO.js +1 -0
- package/dist/frontend/assets/github-dark-default-Cuk6v7N8.js +1 -0
- package/dist/frontend/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- package/dist/frontend/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- package/dist/frontend/assets/github-light-DAi9KRSo.js +1 -0
- package/dist/frontend/assets/github-light-default-D7oLnXFd.js +1 -0
- package/dist/frontend/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- package/dist/frontend/assets/gleam-BspZqrRM.js +1 -0
- package/dist/frontend/assets/glimmer-js-ByusRIyA.js +1 -0
- package/dist/frontend/assets/glimmer-ts-BfAWNZQY.js +1 -0
- package/dist/frontend/assets/glsl-DplSGwfg.js +1 -0
- package/dist/frontend/assets/gn-n2N0HUVH.js +1 -0
- package/dist/frontend/assets/gnuplot-DdkO51Og.js +1 -0
- package/dist/frontend/assets/go-C27-OAKa.js +1 -0
- package/dist/frontend/assets/graphql-ChdNCCLP.js +1 -0
- package/dist/frontend/assets/groovy-gcz8RCvz.js +1 -0
- package/dist/frontend/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
- package/dist/frontend/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
- package/dist/frontend/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
- package/dist/frontend/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
- package/dist/frontend/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
- package/dist/frontend/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
- package/dist/frontend/assets/hack-i7_Ulhet.js +1 -0
- package/dist/frontend/assets/haml-D5jkg6IW.js +1 -0
- package/dist/frontend/assets/handlebars-BpdQsYii.js +1 -0
- package/dist/frontend/assets/haskell-Df6bDoY_.js +1 -0
- package/dist/frontend/assets/haxe-CzTSHFRz.js +1 -0
- package/dist/frontend/assets/hcl-BWvSN4gD.js +1 -0
- package/dist/frontend/assets/hjson-D5-asLiD.js +1 -0
- package/dist/frontend/assets/hlsl-D3lLCCz7.js +1 -0
- package/dist/frontend/assets/horizon-BUw7H-hv.js +1 -0
- package/dist/frontend/assets/horizon-bright-CUuTKBJd.js +1 -0
- package/dist/frontend/assets/houston-DnULxvSX.js +1 -0
- package/dist/frontend/assets/html-derivative-DlHx6ybY.js +1 -0
- package/dist/frontend/assets/html-pp8916En.js +1 -0
- package/dist/frontend/assets/http-jrhK8wxY.js +1 -0
- package/dist/frontend/assets/hurl-irOxFIW8.js +1 -0
- package/dist/frontend/assets/hxml-Bvhsp5Yf.js +1 -0
- package/dist/frontend/assets/hy-DFXneXwc.js +1 -0
- package/dist/frontend/assets/imba-DGztddWO.js +1 -0
- package/dist/frontend/assets/ini-BEwlwnbL.js +1 -0
- package/dist/frontend/assets/java-CylS5w8V.js +1 -0
- package/dist/frontend/assets/javascript-wDzz0qaB.js +1 -0
- package/dist/frontend/assets/jinja-f2NsQr07.js +1 -0
- package/dist/frontend/assets/jison-wvAkD_A8.js +1 -0
- package/dist/frontend/assets/json-Cp-IABpG.js +1 -0
- package/dist/frontend/assets/json5-C9tS-k6U.js +1 -0
- package/dist/frontend/assets/jsonc-Des-eS-w.js +1 -0
- package/dist/frontend/assets/jsonl-DcaNXYhu.js +1 -0
- package/dist/frontend/assets/jsonnet-DFQXde-d.js +1 -0
- package/dist/frontend/assets/jssm-C2t-YnRu.js +1 -0
- package/dist/frontend/assets/jsx-g9-lgVsj.js +1 -0
- package/dist/frontend/assets/julia-CxzCAyBv.js +1 -0
- package/dist/frontend/assets/just-VxiPbLrw.js +1 -0
- package/dist/frontend/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
- package/dist/frontend/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
- package/dist/frontend/assets/kanagawa-wave-DWedfzmr.js +1 -0
- package/dist/frontend/assets/kdl-DV7GczEv.js +1 -0
- package/dist/frontend/assets/kotlin-BdnUsdx6.js +1 -0
- package/dist/frontend/assets/kusto-wEQ09or8.js +1 -0
- package/dist/frontend/assets/laserwave-DUszq2jm.js +1 -0
- package/dist/frontend/assets/latex-CWtU0Tv5.js +1 -0
- package/dist/frontend/assets/lean-BZvkOJ9d.js +1 -0
- package/dist/frontend/assets/less-B1dDrJ26.js +1 -0
- package/dist/frontend/assets/light-plus-B7mTdjB0.js +1 -0
- package/dist/frontend/assets/liquid-C0sCDyMI.js +1 -0
- package/dist/frontend/assets/llvm-DjAJT7YJ.js +1 -0
- package/dist/frontend/assets/log-2UxHyX5q.js +1 -0
- package/dist/frontend/assets/logo-BtOb2qkB.js +1 -0
- package/dist/frontend/assets/lua-BaeVxFsk.js +1 -0
- package/dist/frontend/assets/luau-C-HG3fhB.js +1 -0
- package/dist/frontend/assets/main-CL5_Wlhv.css +32 -0
- package/dist/frontend/assets/main-Czet4Z1x.js +371 -0
- package/dist/frontend/assets/make-CHLpvVh8.js +1 -0
- package/dist/frontend/assets/markdown-Cvjx9yec.js +1 -0
- package/dist/frontend/assets/marko-DjSrsDqO.js +1 -0
- package/dist/frontend/assets/material-theme-D5KoaKCx.js +1 -0
- package/dist/frontend/assets/material-theme-darker-BfHTSMKl.js +1 -0
- package/dist/frontend/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- package/dist/frontend/assets/material-theme-ocean-CyktbL80.js +1 -0
- package/dist/frontend/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- package/dist/frontend/assets/matlab-D7o27uSR.js +1 -0
- package/dist/frontend/assets/mdc-DTYItulj.js +1 -0
- package/dist/frontend/assets/mdx-Cmh6b_Ma.js +1 -0
- package/dist/frontend/assets/mermaid-mWjccvbQ.js +1 -0
- package/dist/frontend/assets/min-dark-CafNBF8u.js +1 -0
- package/dist/frontend/assets/min-light-CTRr51gU.js +1 -0
- package/dist/frontend/assets/mipsasm-CKIfxQSi.js +1 -0
- package/dist/frontend/assets/mojo-rZm6bMo-.js +1 -0
- package/dist/frontend/assets/monokai-D4h5O-jR.js +1 -0
- package/dist/frontend/assets/moonbit-_H4v1dQx.js +1 -0
- package/dist/frontend/assets/move-IF9eRakj.js +1 -0
- package/dist/frontend/assets/narrat-DRg8JJMk.js +1 -0
- package/dist/frontend/assets/nextflow-C-mBbutL.js +1 -0
- package/dist/frontend/assets/nextflow-groovy-vE_lwT2v.js +1 -0
- package/dist/frontend/assets/nginx-BpAMiNFr.js +1 -0
- package/dist/frontend/assets/night-owl-C39BiMTA.js +1 -0
- package/dist/frontend/assets/night-owl-light-CMTm3GFP.js +1 -0
- package/dist/frontend/assets/nim-BIad80T-.js +1 -0
- package/dist/frontend/assets/nix-CwoSXNpI.js +1 -0
- package/dist/frontend/assets/nord-Ddv68eIx.js +1 -0
- package/dist/frontend/assets/nushell-Cz2AlsmD.js +1 -0
- package/dist/frontend/assets/objective-c-DXmwc3jG.js +1 -0
- package/dist/frontend/assets/objective-cpp-CLxacb5B.js +1 -0
- package/dist/frontend/assets/ocaml-C0hk2d4L.js +1 -0
- package/dist/frontend/assets/odin-BBf5iR-q.js +1 -0
- package/dist/frontend/assets/one-dark-pro-DVMEJ2y_.js +1 -0
- package/dist/frontend/assets/one-light-C3Wv6jpd.js +1 -0
- package/dist/frontend/assets/openscad-C4EeE6gA.js +1 -0
- package/dist/frontend/assets/pascal-D93ZcfNL.js +1 -0
- package/dist/frontend/assets/perl-NvoQZIq0.js +1 -0
- package/dist/frontend/assets/php-R6g_5hLQ.js +1 -0
- package/dist/frontend/assets/pkl-u5AG7uiY.js +1 -0
- package/dist/frontend/assets/plastic-3e1v2bzS.js +1 -0
- package/dist/frontend/assets/plsql-ChMvpjG-.js +1 -0
- package/dist/frontend/assets/po-BTJTHyun.js +1 -0
- package/dist/frontend/assets/poimandres-CS3Unz2-.js +1 -0
- package/dist/frontend/assets/polar-C0HS_06l.js +1 -0
- package/dist/frontend/assets/postcss-CXtECtnM.js +1 -0
- package/dist/frontend/assets/powerquery-CEu0bR-o.js +1 -0
- package/dist/frontend/assets/powershell-Dpen1YoG.js +1 -0
- package/dist/frontend/assets/prisma-Dd19v3D-.js +1 -0
- package/dist/frontend/assets/prolog-CbFg5uaA.js +1 -0
- package/dist/frontend/assets/proto-C7zT0LnQ.js +1 -0
- package/dist/frontend/assets/pug-DKIMFp6K.js +1 -0
- package/dist/frontend/assets/puppet-BMWR74SV.js +1 -0
- package/dist/frontend/assets/purescript-CklMAg4u.js +1 -0
- package/dist/frontend/assets/python-B6aJPvgy.js +1 -0
- package/dist/frontend/assets/qml-3beO22l8.js +1 -0
- package/dist/frontend/assets/qmldir-C8lEn-DE.js +1 -0
- package/dist/frontend/assets/qss-IeuSbFQv.js +1 -0
- package/dist/frontend/assets/r-Dspwwk_N.js +1 -0
- package/dist/frontend/assets/racket-BqYA7rlc.js +1 -0
- package/dist/frontend/assets/raku-DXvB9xmW.js +1 -0
- package/dist/frontend/assets/razor-BDqjjVU7.js +1 -0
- package/dist/frontend/assets/red-bN70gL4F.js +1 -0
- package/dist/frontend/assets/reg-C-SQnVFl.js +1 -0
- package/dist/frontend/assets/regexp-CDVJQ6XC.js +1 -0
- package/dist/frontend/assets/rel-C3B-1QV4.js +1 -0
- package/dist/frontend/assets/riscv-BM1_JUlF.js +1 -0
- package/dist/frontend/assets/ron-D8l8udqQ.js +1 -0
- package/dist/frontend/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
- package/dist/frontend/assets/rose-pine-moon-D4_iv3hh.js +1 -0
- package/dist/frontend/assets/rose-pine-qdsjHGoJ.js +1 -0
- package/dist/frontend/assets/rosmsg-BJDFO7_C.js +1 -0
- package/dist/frontend/assets/rst-CRjBmOyv.js +1 -0
- package/dist/frontend/assets/ruby-Wjq7vjNf.js +1 -0
- package/dist/frontend/assets/rust-B1yitclQ.js +1 -0
- package/dist/frontend/assets/sas-cz2c8ADy.js +1 -0
- package/dist/frontend/assets/sass-Cj5Yp3dK.js +1 -0
- package/dist/frontend/assets/scala-C151Ov-r.js +1 -0
- package/dist/frontend/assets/scheme-C98Dy4si.js +1 -0
- package/dist/frontend/assets/scss-D5BDwBP9.js +1 -0
- package/dist/frontend/assets/sdbl-DVxCFoDh.js +1 -0
- package/dist/frontend/assets/shaderlab-Dg9Lc6iA.js +1 -0
- package/dist/frontend/assets/shellscript-Yzrsuije.js +1 -0
- package/dist/frontend/assets/shellsession-BADoaaVG.js +1 -0
- package/dist/frontend/assets/slack-dark-BthQWCQV.js +1 -0
- package/dist/frontend/assets/slack-ochin-DqwNpetd.js +1 -0
- package/dist/frontend/assets/smalltalk-BERRCDM3.js +1 -0
- package/dist/frontend/assets/snazzy-light-Bw305WKR.js +1 -0
- package/dist/frontend/assets/solarized-dark-DXbdFlpD.js +1 -0
- package/dist/frontend/assets/solarized-light-L9t79GZl.js +1 -0
- package/dist/frontend/assets/solidity-rGO070M0.js +1 -0
- package/dist/frontend/assets/soy-8wufbnw4.js +1 -0
- package/dist/frontend/assets/sparql-rVzFXLq3.js +1 -0
- package/dist/frontend/assets/splunk-BtCnVYZw.js +1 -0
- package/dist/frontend/assets/sql-BLtJtn59.js +1 -0
- package/dist/frontend/assets/ssh-config-_ykCGR6B.js +1 -0
- package/dist/frontend/assets/stata-BH5u7GGu.js +1 -0
- package/dist/frontend/assets/stylus-BEDo0Tqx.js +1 -0
- package/dist/frontend/assets/surrealql-Bq5Q-fJD.js +1 -0
- package/dist/frontend/assets/svelte-Cy7k_4gC.js +1 -0
- package/dist/frontend/assets/swift-D82vCrfD.js +1 -0
- package/dist/frontend/assets/synthwave-84-CbfX1IO0.js +1 -0
- package/dist/frontend/assets/system-verilog-CnnmHF94.js +1 -0
- package/dist/frontend/assets/systemd-4A_iFExJ.js +1 -0
- package/dist/frontend/assets/talonscript-CkByrt1z.js +1 -0
- package/dist/frontend/assets/tasl-QIJgUcNo.js +1 -0
- package/dist/frontend/assets/tcl-dwOrl1Do.js +1 -0
- package/dist/frontend/assets/templ-DhtptRzy.js +1 -0
- package/dist/frontend/assets/terraform-BETggiCN.js +1 -0
- package/dist/frontend/assets/tex-idrVyKtj.js +1 -0
- package/dist/frontend/assets/tokyo-night-hegEt444.js +1 -0
- package/dist/frontend/assets/toml-vGWfd6FD.js +1 -0
- package/dist/frontend/assets/ts-tags-DQrlYJgV.js +1 -0
- package/dist/frontend/assets/tsv-B_m7g4N7.js +1 -0
- package/dist/frontend/assets/tsx-COt5Ahok.js +1 -0
- package/dist/frontend/assets/turtle-BsS91CYL.js +1 -0
- package/dist/frontend/assets/twig-xg9kU7Mw.js +1 -0
- package/dist/frontend/assets/typescript-BPQ3VLAy.js +1 -0
- package/dist/frontend/assets/typespec-CAFt9gP4.js +1 -0
- package/dist/frontend/assets/typst-DHCkPAjA.js +1 -0
- package/dist/frontend/assets/v-BcVCzyr7.js +1 -0
- package/dist/frontend/assets/vala-CsfeWuGM.js +1 -0
- package/dist/frontend/assets/vb-D17OF-Vu.js +1 -0
- package/dist/frontend/assets/verilog-BQ8w6xss.js +1 -0
- package/dist/frontend/assets/vesper-DU1UobuO.js +1 -0
- package/dist/frontend/assets/vhdl-CeAyd5Ju.js +1 -0
- package/dist/frontend/assets/viml-CJc9bBzg.js +1 -0
- package/dist/frontend/assets/vitesse-black-Bkuqu6BP.js +1 -0
- package/dist/frontend/assets/vitesse-dark-D0r3Knsf.js +1 -0
- package/dist/frontend/assets/vitesse-light-CVO1_9PV.js +1 -0
- package/dist/frontend/assets/vue-D2xRrEX4.js +1 -0
- package/dist/frontend/assets/vue-html-AaS7Mt5G.js +1 -0
- package/dist/frontend/assets/vue-vine-BoDAl6tE.js +1 -0
- package/dist/frontend/assets/vyper-CDx5xZoG.js +1 -0
- package/dist/frontend/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/frontend/assets/wasm-MzD3tlZU.js +1 -0
- package/dist/frontend/assets/wenyan-BV7otONQ.js +1 -0
- package/dist/frontend/assets/wgsl-Dx-B1_4e.js +1 -0
- package/dist/frontend/assets/wikitext-BhOHFoWU.js +1 -0
- package/dist/frontend/assets/wit-5i3qLPDT.js +1 -0
- package/dist/frontend/assets/wolfram-lXgVvXCa.js +1 -0
- package/dist/frontend/assets/xml-sdJ4AIDG.js +1 -0
- package/dist/frontend/assets/xsl-CtQFsRM5.js +1 -0
- package/dist/frontend/assets/yaml-Buea-lGh.js +1 -0
- package/dist/frontend/assets/zenscript-DVFEvuxE.js +1 -0
- package/dist/frontend/assets/zig-VOosw3JB.js +1 -0
- package/dist/frontend/icon-192.png +0 -0
- package/dist/frontend/icon-512.png +0 -0
- package/dist/frontend/icon.svg +8 -0
- package/dist/frontend/index.html +30 -0
- package/dist/frontend/manifest.json +25 -0
- package/dist/frontend/sw.js +66 -0
- package/dist/server/agent-events.js +39 -0
- package/dist/server/analytics.js +885 -0
- package/dist/server/auth.js +65 -0
- package/dist/server/belayer/executor.js +200 -0
- package/dist/server/belayer/intake.js +27 -0
- package/dist/server/belayer/pipeline.js +97 -0
- package/dist/server/belayer/pr-lifecycle.js +69 -0
- package/dist/server/belayer/prompts.js +154 -0
- package/dist/server/belayer/types.js +23 -0
- package/dist/server/branch-linker.js +137 -0
- package/dist/server/browser-content.js +145 -0
- package/dist/server/clipboard.js +63 -0
- package/dist/server/codex-hooks-adapter.js +93 -0
- package/dist/server/config.js +325 -0
- package/dist/server/gh-routes.js +163 -0
- package/dist/server/gh.js +276 -0
- package/dist/server/git-routes.js +154 -0
- package/dist/server/git.js +694 -0
- package/dist/server/github-app.js +218 -0
- package/dist/server/github-graphql.js +178 -0
- package/dist/server/hooks.js +373 -0
- package/dist/server/index.js +1549 -0
- package/dist/server/integration-github.js +137 -0
- package/dist/server/integration-jira.js +210 -0
- package/dist/server/integration-linear.js +176 -0
- package/dist/server/logger.js +18 -0
- package/dist/server/mobile-input-pipeline.js +129 -0
- package/dist/server/opencode-relay.js +53 -0
- package/dist/server/org-dashboard.js +241 -0
- package/dist/server/output-parsers/claude-parser.js +56 -0
- package/dist/server/output-parsers/codex-parser.js +13 -0
- package/dist/server/output-parsers/index.js +14 -0
- package/dist/server/output-parsers/null-parser.js +12 -0
- package/dist/server/output-parsers/opencode-parser.js +77 -0
- package/dist/server/pty-handler.js +586 -0
- package/dist/server/push.js +84 -0
- package/dist/server/review-poller.js +237 -0
- package/dist/server/sdk-handler.js +539 -0
- package/dist/server/service.js +189 -0
- package/dist/server/sessions.js +638 -0
- package/dist/server/telemetry.js +236 -0
- package/dist/server/ticket-transitions.js +166 -0
- package/dist/server/types.js +146 -0
- package/dist/server/utils.js +23 -0
- package/dist/server/watcher.js +661 -0
- package/dist/server/webhook-manager.js +547 -0
- package/dist/server/webhooks.js +73 -0
- package/dist/server/workspace-groups.js +363 -0
- package/dist/server/workspaces.js +1207 -0
- package/dist/server/ws.js +192 -0
- package/dist/test/EmptyState.spec.js +51 -0
- package/dist/test/action-coverage.test.js +139 -0
- package/dist/test/actions/registry.test.js +59 -0
- package/dist/test/actions/shortcuts.test.js +79 -0
- package/dist/test/agent-events.test.js +151 -0
- package/dist/test/analytics.test.js +158 -0
- package/dist/test/attention.test.js +91 -0
- package/dist/test/auth.test.js +105 -0
- package/dist/test/backend-state.test.js +47 -0
- package/dist/test/belayer-executor.test.js +33 -0
- package/dist/test/belayer-intake.test.js +44 -0
- package/dist/test/belayer-pipeline.test.js +113 -0
- package/dist/test/belayer-pr-lifecycle.test.js +26 -0
- package/dist/test/belayer-prompts.test.js +60 -0
- package/dist/test/belayer-types.test.js +69 -0
- package/dist/test/bin/claude-remote-cli.js +214 -0
- package/dist/test/boot-state.test.js +133 -0
- package/dist/test/branch-lifecycle.test.js +75 -0
- package/dist/test/branch-linker.test.js +236 -0
- package/dist/test/branch-rename.test.js +45 -0
- package/dist/test/branch-watcher.test.js +115 -0
- package/dist/test/browser-cli.test.js +91 -0
- package/dist/test/browser-content.test.js +93 -0
- package/dist/test/browser-tabs-ui.test.js +39 -0
- package/dist/test/changed-files-api.test.js +140 -0
- package/dist/test/clipboard.test.js +12 -0
- package/dist/test/codex-hooks-adapter.test.js +237 -0
- package/dist/test/components/EmptyState.spec.js +51 -0
- package/dist/test/components/ErrorToast.spec.js +65 -0
- package/dist/test/components/TuiCheckbox.spec.js +120 -0
- package/dist/test/components/TuiInput.spec.js +186 -0
- package/dist/test/components/leaf-component-migration.spec.js +104 -0
- package/dist/test/config-freshness.test.js +63 -0
- package/dist/test/config.test.js +813 -0
- package/dist/test/diff-summary.test.js +98 -0
- package/dist/test/display-state.test.js +179 -0
- package/dist/test/event-message-types.test.js +32 -0
- package/dist/test/file-tree-utils.test.js +167 -0
- package/dist/test/framework-types.test.js +183 -0
- package/dist/test/frameworks-api.test.js +93 -0
- package/dist/test/frontend/src/lib/pr-state.js +114 -0
- package/dist/test/fs-browse.test.js +246 -0
- package/dist/test/fuzzy-scorer.test.js +145 -0
- package/dist/test/gh-routes.test.js +156 -0
- package/dist/test/git-changed-files.test.js +152 -0
- package/dist/test/git-routes.test.js +146 -0
- package/dist/test/git-utils.test.js +68 -0
- package/dist/test/git-watcher.test.js +110 -0
- package/dist/test/git.test.js +140 -0
- package/dist/test/github-app.test.js +455 -0
- package/dist/test/github-graphql.test.js +301 -0
- package/dist/test/greetings.test.js +83 -0
- package/dist/test/hooks-agent-event.test.js +412 -0
- package/dist/test/hooks.test.js +149 -0
- package/dist/test/integration-github.test.js +220 -0
- package/dist/test/integration-jira.test.js +238 -0
- package/dist/test/integration-linear.test.js +293 -0
- package/dist/test/mobile-input.test.js +235 -0
- package/dist/test/opencode-relay.test.js +107 -0
- package/dist/test/org-dashboard.test.js +349 -0
- package/dist/test/output-parser.test.js +217 -0
- package/dist/test/paths.test.js +32 -0
- package/dist/test/pr-state.test.js +407 -0
- package/dist/test/pr-status.test.js +82 -0
- package/dist/test/presets.test.js +242 -0
- package/dist/test/pty-handler-multi-agent.test.js +149 -0
- package/dist/test/pty-handler.test.js +146 -0
- package/dist/test/pull-requests.test.js +78 -0
- package/dist/test/review-poller.test.js +349 -0
- package/dist/test/server/analytics.js +121 -0
- package/dist/test/server/auth.js +63 -0
- package/dist/test/server/branch-linker.js +124 -0
- package/dist/test/server/clipboard.js +56 -0
- package/dist/test/server/config.js +137 -0
- package/dist/test/server/git.js +308 -0
- package/dist/test/server/hooks.js +196 -0
- package/dist/test/server/index.js +1124 -0
- package/dist/test/server/integration-github.js +117 -0
- package/dist/test/server/integration-jira.js +164 -0
- package/dist/test/server/integration-linear.js +176 -0
- package/dist/test/server/mobile-input-pipeline.js +123 -0
- package/dist/test/server/org-dashboard.js +184 -0
- package/dist/test/server/output-parsers/claude-parser.js +54 -0
- package/dist/test/server/output-parsers/codex-parser.js +13 -0
- package/dist/test/server/output-parsers/index.js +7 -0
- package/dist/test/server/pty-handler.js +310 -0
- package/dist/test/server/push.js +80 -0
- package/dist/test/server/review-poller.js +218 -0
- package/dist/test/server/service.js +169 -0
- package/dist/test/server/sessions.js +434 -0
- package/dist/test/server/ticket-transitions.js +216 -0
- package/dist/test/server/types.js +20 -0
- package/dist/test/server/utils.js +22 -0
- package/dist/test/server/watcher.js +139 -0
- package/dist/test/server/workspaces.js +657 -0
- package/dist/test/server/ws.js +152 -0
- package/dist/test/server-startup.test.js +62 -0
- package/dist/test/service.test.js +43 -0
- package/dist/test/session-analytics-api.test.js +123 -0
- package/dist/test/session-analytics.test.js +425 -0
- package/dist/test/session-intent.test.js +249 -0
- package/dist/test/sessions.test.js +1152 -0
- package/dist/test/sidebar-items.test.js +164 -0
- package/dist/test/stores/boot-state-store.test.js +165 -0
- package/dist/test/stores/sessions-logic.test.js +191 -0
- package/dist/test/stores/toasts-store.test.js +66 -0
- package/dist/test/stores/ui-store.test.js +203 -0
- package/dist/test/stores/unread-store.test.js +97 -0
- package/dist/test/telemetry-api.test.js +54 -0
- package/dist/test/telemetry-sync.test.js +68 -0
- package/dist/test/telemetry.test.js +295 -0
- package/dist/test/terminal-zoom.test.js +102 -0
- package/dist/test/test/analytics.test.js +152 -0
- package/dist/test/test/auth.test.js +95 -0
- package/dist/test/test/branch-linker.test.js +231 -0
- package/dist/test/test/branch-rename.test.js +45 -0
- package/dist/test/test/clipboard.test.js +12 -0
- package/dist/test/test/config.test.js +281 -0
- package/dist/test/test/fs-browse.test.js +202 -0
- package/dist/test/test/git.test.js +67 -0
- package/dist/test/test/hooks.test.js +139 -0
- package/dist/test/test/integration-github.test.js +203 -0
- package/dist/test/test/integration-jira.test.js +294 -0
- package/dist/test/test/integration-linear.test.js +293 -0
- package/dist/test/test/mobile-input.test.js +193 -0
- package/dist/test/test/org-dashboard.test.js +240 -0
- package/dist/test/test/output-parser.test.js +95 -0
- package/dist/test/test/paths.test.js +32 -0
- package/dist/test/test/pr-state.test.js +220 -0
- package/dist/test/test/pull-requests.test.js +67 -0
- package/dist/test/test/review-poller.test.js +235 -0
- package/dist/test/test/service.test.js +43 -0
- package/dist/test/test/sessions.test.js +750 -0
- package/dist/test/test/ticket-transitions.test.js +130 -0
- package/dist/test/test/version.test.js +34 -0
- package/dist/test/test/worktrees.test.js +256 -0
- package/dist/test/ticket-transitions.test.js +312 -0
- package/dist/test/unread.test.js +23 -0
- package/dist/test/version.test.js +34 -0
- package/dist/test/webhook-manager.test.js +484 -0
- package/dist/test/webhooks.test.js +208 -0
- package/dist/test/workspace-groups.test.js +377 -0
- package/dist/test/worktrees.test.js +531 -0
- package/package.json +88 -0
|
@@ -0,0 +1,1152 @@
|
|
|
1
|
+
import { describe, it, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import * as sessions from '../server/sessions.js';
|
|
7
|
+
import { resolveTmuxSpawn, generateTmuxSessionName, getTmuxPrefix, } from '../server/pty-handler.js';
|
|
8
|
+
import { serializeAll, restoreFromDisk } from '../server/sessions.js';
|
|
9
|
+
import { AGENT_YOLO_ARGS } from '../server/types.js';
|
|
10
|
+
// Track created session IDs so we can clean up after each test
|
|
11
|
+
const createdIds = [];
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
// Kill any remaining sessions created during tests
|
|
14
|
+
for (const id of createdIds) {
|
|
15
|
+
try {
|
|
16
|
+
const session = sessions.get(id);
|
|
17
|
+
if (session) {
|
|
18
|
+
sessions.kill(id);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Already killed or exited, ignore
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
createdIds.length = 0;
|
|
26
|
+
});
|
|
27
|
+
describe('sessions', () => {
|
|
28
|
+
it('list returns empty array initially', () => {
|
|
29
|
+
const result = sessions.list();
|
|
30
|
+
assert.ok(Array.isArray(result));
|
|
31
|
+
assert.strictEqual(result.length, 0);
|
|
32
|
+
});
|
|
33
|
+
it('create spawns PTY and adds session to registry', () => {
|
|
34
|
+
const result = sessions.create({
|
|
35
|
+
repoName: 'test-repo',
|
|
36
|
+
repoPath: '/tmp',
|
|
37
|
+
worktreePath: null,
|
|
38
|
+
cwd: '/tmp',
|
|
39
|
+
command: '/bin/echo',
|
|
40
|
+
args: ['hello'],
|
|
41
|
+
cols: 80,
|
|
42
|
+
rows: 24,
|
|
43
|
+
});
|
|
44
|
+
createdIds.push(result.id);
|
|
45
|
+
assert.ok(result.id, 'should have an id');
|
|
46
|
+
assert.strictEqual(result.repoName, 'test-repo');
|
|
47
|
+
assert.strictEqual(result.cwd, '/tmp');
|
|
48
|
+
assert.ok(typeof result.pid === 'number', 'should have a numeric pid');
|
|
49
|
+
assert.ok(result.createdAt, 'should have a createdAt timestamp');
|
|
50
|
+
assert.strictEqual('pty' in result, false, 'should not expose pty object');
|
|
51
|
+
const list = sessions.list();
|
|
52
|
+
assert.strictEqual(list.length, 1);
|
|
53
|
+
assert.strictEqual(list[0]?.id, result.id);
|
|
54
|
+
});
|
|
55
|
+
it('get returns session by id', () => {
|
|
56
|
+
const result = sessions.create({
|
|
57
|
+
repoName: 'test-repo',
|
|
58
|
+
repoPath: '/tmp',
|
|
59
|
+
worktreePath: null,
|
|
60
|
+
cwd: '/tmp',
|
|
61
|
+
command: '/bin/echo',
|
|
62
|
+
args: ['hello'],
|
|
63
|
+
});
|
|
64
|
+
createdIds.push(result.id);
|
|
65
|
+
const session = sessions.get(result.id);
|
|
66
|
+
assert.ok(session, 'should return the session');
|
|
67
|
+
assert.strictEqual(session.id, result.id);
|
|
68
|
+
assert.strictEqual(session.repoName, 'test-repo');
|
|
69
|
+
assert.strictEqual(session.mode, 'pty');
|
|
70
|
+
assert.ok(session.pty, 'get should include the pty object');
|
|
71
|
+
});
|
|
72
|
+
it('get returns undefined for nonexistent id', () => {
|
|
73
|
+
const session = sessions.get('nonexistent-id-12345');
|
|
74
|
+
assert.strictEqual(session, undefined);
|
|
75
|
+
});
|
|
76
|
+
it('kill removes session from registry', () => {
|
|
77
|
+
const result = sessions.create({
|
|
78
|
+
repoName: 'test-repo',
|
|
79
|
+
repoPath: '/tmp',
|
|
80
|
+
worktreePath: null,
|
|
81
|
+
cwd: '/tmp',
|
|
82
|
+
command: '/bin/echo',
|
|
83
|
+
args: ['hello'],
|
|
84
|
+
});
|
|
85
|
+
createdIds.push(result.id);
|
|
86
|
+
sessions.kill(result.id);
|
|
87
|
+
// Remove from tracking since it's already killed
|
|
88
|
+
createdIds.splice(createdIds.indexOf(result.id), 1);
|
|
89
|
+
const session = sessions.get(result.id);
|
|
90
|
+
assert.strictEqual(session, undefined, 'session should be removed after kill');
|
|
91
|
+
const list = sessions.list();
|
|
92
|
+
assert.ok(!list.some((s) => s.id === result.id), 'killed session should not appear in list');
|
|
93
|
+
});
|
|
94
|
+
it('kill throws for nonexistent session', () => {
|
|
95
|
+
assert.throws(() => sessions.kill('nonexistent-id'), /Session not found/);
|
|
96
|
+
});
|
|
97
|
+
it('resize throws for nonexistent session', () => {
|
|
98
|
+
assert.throws(() => sessions.resize('nonexistent-id', 100, 40), /Session not found/);
|
|
99
|
+
});
|
|
100
|
+
it('write sends data to PTY stdin', (_, done) => {
|
|
101
|
+
const result = sessions.create({
|
|
102
|
+
repoName: 'test-repo',
|
|
103
|
+
repoPath: '/tmp',
|
|
104
|
+
worktreePath: null,
|
|
105
|
+
cwd: '/tmp',
|
|
106
|
+
command: '/bin/cat',
|
|
107
|
+
args: [],
|
|
108
|
+
cols: 80,
|
|
109
|
+
rows: 24,
|
|
110
|
+
});
|
|
111
|
+
createdIds.push(result.id);
|
|
112
|
+
const session = sessions.get(result.id);
|
|
113
|
+
assert.ok(session);
|
|
114
|
+
assert.strictEqual(session.mode, 'pty');
|
|
115
|
+
const ptySession = session;
|
|
116
|
+
let output = '';
|
|
117
|
+
ptySession.pty.onData((data) => {
|
|
118
|
+
output += data;
|
|
119
|
+
if (output.includes('hello')) {
|
|
120
|
+
done();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
sessions.write(result.id, 'hello');
|
|
124
|
+
});
|
|
125
|
+
it('write throws for nonexistent session', () => {
|
|
126
|
+
assert.throws(() => sessions.write('nonexistent-id', 'data'), /Session not found/);
|
|
127
|
+
});
|
|
128
|
+
it('session starts as not idle', () => {
|
|
129
|
+
const result = sessions.create({
|
|
130
|
+
repoName: 'test-repo',
|
|
131
|
+
repoPath: '/tmp',
|
|
132
|
+
worktreePath: null,
|
|
133
|
+
cwd: '/tmp',
|
|
134
|
+
command: '/bin/cat',
|
|
135
|
+
args: [],
|
|
136
|
+
});
|
|
137
|
+
createdIds.push(result.id);
|
|
138
|
+
const session = sessions.get(result.id);
|
|
139
|
+
assert.ok(session);
|
|
140
|
+
assert.strictEqual(session.idle, false);
|
|
141
|
+
});
|
|
142
|
+
it('list includes idle field', () => {
|
|
143
|
+
const result = sessions.create({
|
|
144
|
+
repoName: 'test-repo',
|
|
145
|
+
repoPath: '/tmp',
|
|
146
|
+
worktreePath: null,
|
|
147
|
+
cwd: '/tmp',
|
|
148
|
+
command: '/bin/cat',
|
|
149
|
+
args: [],
|
|
150
|
+
});
|
|
151
|
+
createdIds.push(result.id);
|
|
152
|
+
const list = sessions.list();
|
|
153
|
+
assert.strictEqual(list.length, 1);
|
|
154
|
+
assert.strictEqual(list[0]?.idle, false);
|
|
155
|
+
});
|
|
156
|
+
it('type defaults to agent when not specified', () => {
|
|
157
|
+
const result = sessions.create({
|
|
158
|
+
repoName: 'test-repo',
|
|
159
|
+
repoPath: '/tmp',
|
|
160
|
+
worktreePath: null,
|
|
161
|
+
cwd: '/tmp',
|
|
162
|
+
command: '/bin/echo',
|
|
163
|
+
args: ['hello'],
|
|
164
|
+
});
|
|
165
|
+
createdIds.push(result.id);
|
|
166
|
+
assert.strictEqual(result.type, 'agent');
|
|
167
|
+
const session = sessions.get(result.id);
|
|
168
|
+
assert.ok(session);
|
|
169
|
+
assert.strictEqual(session.type, 'agent');
|
|
170
|
+
});
|
|
171
|
+
it('type is set to agent when specified', () => {
|
|
172
|
+
const result = sessions.create({
|
|
173
|
+
type: 'agent',
|
|
174
|
+
repoName: 'test-repo',
|
|
175
|
+
repoPath: '/tmp',
|
|
176
|
+
worktreePath: null,
|
|
177
|
+
cwd: '/tmp',
|
|
178
|
+
command: '/bin/echo',
|
|
179
|
+
args: ['hello'],
|
|
180
|
+
});
|
|
181
|
+
createdIds.push(result.id);
|
|
182
|
+
assert.strictEqual(result.type, 'agent');
|
|
183
|
+
const session = sessions.get(result.id);
|
|
184
|
+
assert.ok(session);
|
|
185
|
+
assert.strictEqual(session.type, 'agent');
|
|
186
|
+
});
|
|
187
|
+
it('list includes type field', () => {
|
|
188
|
+
const r1 = sessions.create({
|
|
189
|
+
type: 'agent',
|
|
190
|
+
repoName: 'repo-a',
|
|
191
|
+
repoPath: '/tmp/a',
|
|
192
|
+
worktreePath: null,
|
|
193
|
+
cwd: '/tmp/a',
|
|
194
|
+
command: '/bin/echo',
|
|
195
|
+
args: ['hello'],
|
|
196
|
+
});
|
|
197
|
+
createdIds.push(r1.id);
|
|
198
|
+
const r2 = sessions.create({
|
|
199
|
+
type: 'agent',
|
|
200
|
+
repoName: 'repo-b',
|
|
201
|
+
repoPath: '/tmp/b',
|
|
202
|
+
worktreePath: null,
|
|
203
|
+
cwd: '/tmp/b',
|
|
204
|
+
command: '/bin/echo',
|
|
205
|
+
args: ['hello'],
|
|
206
|
+
});
|
|
207
|
+
createdIds.push(r2.id);
|
|
208
|
+
const list = sessions.list();
|
|
209
|
+
const s1 = list.find(function (s) {
|
|
210
|
+
return s.id === r1.id;
|
|
211
|
+
});
|
|
212
|
+
const s2 = list.find(function (s) {
|
|
213
|
+
return s.id === r2.id;
|
|
214
|
+
});
|
|
215
|
+
assert.ok(s1);
|
|
216
|
+
assert.strictEqual(s1.type, 'agent');
|
|
217
|
+
assert.ok(s2);
|
|
218
|
+
assert.strictEqual(s2.type, 'agent');
|
|
219
|
+
});
|
|
220
|
+
it('list includes repoPath, worktreePath, and cwd fields', () => {
|
|
221
|
+
const result = sessions.create({
|
|
222
|
+
type: 'agent',
|
|
223
|
+
repoName: 'test-repo',
|
|
224
|
+
repoPath: '/tmp/workspace',
|
|
225
|
+
worktreePath: '/tmp/workspace/.worktrees/my-branch',
|
|
226
|
+
cwd: '/tmp/workspace/.worktrees/my-branch',
|
|
227
|
+
command: '/bin/echo',
|
|
228
|
+
args: ['hello'],
|
|
229
|
+
});
|
|
230
|
+
createdIds.push(result.id);
|
|
231
|
+
const list = sessions.list();
|
|
232
|
+
const session = list.find((s) => s.id === result.id);
|
|
233
|
+
assert.ok(session);
|
|
234
|
+
assert.strictEqual(session.repoPath, '/tmp/workspace');
|
|
235
|
+
assert.strictEqual(session.worktreePath, '/tmp/workspace/.worktrees/my-branch');
|
|
236
|
+
assert.strictEqual(session.cwd, '/tmp/workspace/.worktrees/my-branch');
|
|
237
|
+
});
|
|
238
|
+
it('branchName defaults to empty string when branchName is not provided', () => {
|
|
239
|
+
const result = sessions.create({
|
|
240
|
+
type: 'agent',
|
|
241
|
+
repoName: 'test-repo',
|
|
242
|
+
repoPath: '/tmp',
|
|
243
|
+
worktreePath: null,
|
|
244
|
+
cwd: '/tmp',
|
|
245
|
+
command: '/bin/echo',
|
|
246
|
+
args: ['hello'],
|
|
247
|
+
});
|
|
248
|
+
createdIds.push(result.id);
|
|
249
|
+
assert.strictEqual(result.branchName, '');
|
|
250
|
+
});
|
|
251
|
+
it('resolveTmuxSpawn returns correct tmux command and args', () => {
|
|
252
|
+
const result = resolveTmuxSpawn('claude', ['--continue'], 'test-session');
|
|
253
|
+
assert.deepStrictEqual(result, {
|
|
254
|
+
command: 'tmux',
|
|
255
|
+
args: [
|
|
256
|
+
'-u',
|
|
257
|
+
'new-session',
|
|
258
|
+
'-s',
|
|
259
|
+
'test-session',
|
|
260
|
+
'--',
|
|
261
|
+
'claude',
|
|
262
|
+
'--continue',
|
|
263
|
+
';',
|
|
264
|
+
'set',
|
|
265
|
+
'set-clipboard',
|
|
266
|
+
'on',
|
|
267
|
+
';',
|
|
268
|
+
'set',
|
|
269
|
+
'allow-passthrough',
|
|
270
|
+
'on',
|
|
271
|
+
';',
|
|
272
|
+
'set',
|
|
273
|
+
'mode-keys',
|
|
274
|
+
'vi',
|
|
275
|
+
],
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
it('generateTmuxSessionName has correct prefix', () => {
|
|
279
|
+
const original = process.env.NO_PIN;
|
|
280
|
+
delete process.env.NO_PIN;
|
|
281
|
+
try {
|
|
282
|
+
const name = generateTmuxSessionName('my-session', 'abcdef1234567890');
|
|
283
|
+
assert.ok(name.startsWith('crc-'), `expected crc- prefix, got: ${name}`);
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
if (original !== undefined)
|
|
287
|
+
process.env.NO_PIN = original;
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
it('generateTmuxSessionName sanitizes special characters', () => {
|
|
291
|
+
const original = process.env.NO_PIN;
|
|
292
|
+
delete process.env.NO_PIN;
|
|
293
|
+
try {
|
|
294
|
+
const name = generateTmuxSessionName('feat/auth-flow', 'abcdef1234567890');
|
|
295
|
+
assert.ok(name.startsWith('crc-feat-auth-flow-'), `expected sanitized name, got: ${name}`);
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
if (original !== undefined)
|
|
299
|
+
process.env.NO_PIN = original;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
it('generateTmuxSessionName limits display name to 30 chars', () => {
|
|
303
|
+
const original = process.env.NO_PIN;
|
|
304
|
+
delete process.env.NO_PIN;
|
|
305
|
+
try {
|
|
306
|
+
const longName = 'a-very-long-display-name-that-exceeds-thirty-characters';
|
|
307
|
+
const id = 'abcdef1234567890';
|
|
308
|
+
const name = generateTmuxSessionName(longName, id);
|
|
309
|
+
// Format is crc-<sanitized up to 30>-<8 char id>
|
|
310
|
+
// The sanitized portion should be at most 30 chars
|
|
311
|
+
const withoutPrefix = name.slice('crc-'.length);
|
|
312
|
+
const parts = withoutPrefix.split('-');
|
|
313
|
+
const idPart = parts[parts.length - 1];
|
|
314
|
+
const displayPart = withoutPrefix.slice(0, withoutPrefix.length - idPart.length - 1);
|
|
315
|
+
assert.ok(displayPart.length <= 30, `display portion should be <= 30 chars, got: ${displayPart.length}`);
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
if (original !== undefined)
|
|
319
|
+
process.env.NO_PIN = original;
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
it('generateTmuxSessionName uses 8 chars from the provided id', () => {
|
|
323
|
+
const id = 'abcdef1234567890';
|
|
324
|
+
const name = generateTmuxSessionName('my-session', id);
|
|
325
|
+
assert.ok(name.endsWith(id.slice(0, 8)), `expected name to end with ${id.slice(0, 8)}, got: ${name}`);
|
|
326
|
+
});
|
|
327
|
+
it('prod prefix (crc-) does not match dev prefix (crcd-)', () => {
|
|
328
|
+
const prodPrefix = 'crc-';
|
|
329
|
+
const devPrefix = 'crcd-';
|
|
330
|
+
assert.ok(!devPrefix.startsWith(prodPrefix), `dev prefix '${devPrefix}' must not start with prod prefix '${prodPrefix}'`);
|
|
331
|
+
assert.ok(!prodPrefix.startsWith(devPrefix), `prod prefix '${prodPrefix}' must not start with dev prefix '${devPrefix}'`);
|
|
332
|
+
});
|
|
333
|
+
it('getTmuxPrefix returns crc- when NO_PIN is not set', () => {
|
|
334
|
+
const original = process.env.NO_PIN;
|
|
335
|
+
delete process.env.NO_PIN;
|
|
336
|
+
try {
|
|
337
|
+
assert.strictEqual(getTmuxPrefix(), 'crc-');
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
if (original !== undefined)
|
|
341
|
+
process.env.NO_PIN = original;
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
it('getTmuxPrefix returns crcd- when NO_PIN is 1', () => {
|
|
345
|
+
const original = process.env.NO_PIN;
|
|
346
|
+
process.env.NO_PIN = '1';
|
|
347
|
+
try {
|
|
348
|
+
assert.strictEqual(getTmuxPrefix(), 'crcd-');
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
if (original !== undefined) {
|
|
352
|
+
process.env.NO_PIN = original;
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
delete process.env.NO_PIN;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
it('agent defaults to claude when not specified', () => {
|
|
360
|
+
const result = sessions.create({
|
|
361
|
+
repoName: 'test-repo',
|
|
362
|
+
repoPath: '/tmp',
|
|
363
|
+
worktreePath: null,
|
|
364
|
+
cwd: '/tmp',
|
|
365
|
+
command: '/bin/echo',
|
|
366
|
+
args: ['hello'],
|
|
367
|
+
});
|
|
368
|
+
createdIds.push(result.id);
|
|
369
|
+
assert.strictEqual(result.agent, 'claude');
|
|
370
|
+
});
|
|
371
|
+
it('agent is set when specified', () => {
|
|
372
|
+
const result = sessions.create({
|
|
373
|
+
repoName: 'test-repo',
|
|
374
|
+
repoPath: '/tmp',
|
|
375
|
+
worktreePath: null,
|
|
376
|
+
cwd: '/tmp',
|
|
377
|
+
agent: 'codex',
|
|
378
|
+
command: '/bin/echo',
|
|
379
|
+
args: ['hello'],
|
|
380
|
+
});
|
|
381
|
+
createdIds.push(result.id);
|
|
382
|
+
assert.strictEqual(result.agent, 'codex');
|
|
383
|
+
});
|
|
384
|
+
it('list includes agent field', () => {
|
|
385
|
+
const result = sessions.create({
|
|
386
|
+
repoName: 'test-repo',
|
|
387
|
+
repoPath: '/tmp',
|
|
388
|
+
worktreePath: null,
|
|
389
|
+
cwd: '/tmp',
|
|
390
|
+
agent: 'codex',
|
|
391
|
+
command: '/bin/echo',
|
|
392
|
+
args: ['hello'],
|
|
393
|
+
});
|
|
394
|
+
createdIds.push(result.id);
|
|
395
|
+
const list = sessions.list();
|
|
396
|
+
const session = list.find((s) => s.id === result.id);
|
|
397
|
+
assert.ok(session);
|
|
398
|
+
assert.strictEqual(session.agent, 'codex');
|
|
399
|
+
});
|
|
400
|
+
it('useTmux defaults to false when not specified', () => {
|
|
401
|
+
const result = sessions.create({
|
|
402
|
+
repoName: 'test-repo',
|
|
403
|
+
repoPath: '/tmp',
|
|
404
|
+
worktreePath: null,
|
|
405
|
+
cwd: '/tmp',
|
|
406
|
+
command: '/bin/echo',
|
|
407
|
+
args: ['hello'],
|
|
408
|
+
});
|
|
409
|
+
createdIds.push(result.id);
|
|
410
|
+
assert.strictEqual(result.useTmux, false);
|
|
411
|
+
assert.strictEqual(result.tmuxSessionName, '');
|
|
412
|
+
});
|
|
413
|
+
it('useTmux is disabled when custom command is provided even if useTmux is true', () => {
|
|
414
|
+
const result = sessions.create({
|
|
415
|
+
repoName: 'test-repo',
|
|
416
|
+
repoPath: '/tmp',
|
|
417
|
+
worktreePath: null,
|
|
418
|
+
cwd: '/tmp',
|
|
419
|
+
command: '/bin/echo',
|
|
420
|
+
args: ['hello'],
|
|
421
|
+
useTmux: true,
|
|
422
|
+
});
|
|
423
|
+
createdIds.push(result.id);
|
|
424
|
+
// Custom command sessions should never use tmux
|
|
425
|
+
assert.strictEqual(result.useTmux, false);
|
|
426
|
+
assert.strictEqual(result.tmuxSessionName, '');
|
|
427
|
+
});
|
|
428
|
+
it('list includes useTmux and tmuxSessionName fields', () => {
|
|
429
|
+
const result = sessions.create({
|
|
430
|
+
repoName: 'test-repo',
|
|
431
|
+
repoPath: '/tmp',
|
|
432
|
+
worktreePath: null,
|
|
433
|
+
cwd: '/tmp',
|
|
434
|
+
command: '/bin/echo',
|
|
435
|
+
args: ['hello'],
|
|
436
|
+
});
|
|
437
|
+
createdIds.push(result.id);
|
|
438
|
+
const list = sessions.list();
|
|
439
|
+
const session = list.find((s) => s.id === result.id);
|
|
440
|
+
assert.ok(session);
|
|
441
|
+
assert.strictEqual(session.useTmux, false);
|
|
442
|
+
assert.strictEqual(session.tmuxSessionName, '');
|
|
443
|
+
});
|
|
444
|
+
it('calls onPtyReplaced when continue-arg process fails quickly', (_, done) => {
|
|
445
|
+
const result = sessions.create({
|
|
446
|
+
repoName: 'test-repo',
|
|
447
|
+
repoPath: '/tmp',
|
|
448
|
+
worktreePath: null,
|
|
449
|
+
cwd: '/tmp',
|
|
450
|
+
command: '/bin/false',
|
|
451
|
+
args: [...(sessions.AGENT_CONTINUE_ARGS['claude'] ?? [])],
|
|
452
|
+
});
|
|
453
|
+
createdIds.push(result.id);
|
|
454
|
+
const session = sessions.get(result.id);
|
|
455
|
+
assert.ok(session);
|
|
456
|
+
assert.strictEqual(session.mode, 'pty');
|
|
457
|
+
const ptySession = session;
|
|
458
|
+
ptySession.onPtyReplacedCallbacks.push((newPty) => {
|
|
459
|
+
assert.ok(newPty, 'should receive new PTY');
|
|
460
|
+
assert.strictEqual(ptySession.pty, newPty, 'session.pty should be updated to new PTY');
|
|
461
|
+
done();
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
it('session survives after continue-arg retry', (_, done) => {
|
|
465
|
+
const result = sessions.create({
|
|
466
|
+
repoName: 'test-repo',
|
|
467
|
+
repoPath: '/tmp',
|
|
468
|
+
worktreePath: null,
|
|
469
|
+
cwd: '/tmp',
|
|
470
|
+
command: '/bin/false',
|
|
471
|
+
args: [...(sessions.AGENT_CONTINUE_ARGS['claude'] ?? [])],
|
|
472
|
+
});
|
|
473
|
+
createdIds.push(result.id);
|
|
474
|
+
const session = sessions.get(result.id);
|
|
475
|
+
assert.ok(session);
|
|
476
|
+
assert.strictEqual(session.mode, 'pty');
|
|
477
|
+
const ptySession = session;
|
|
478
|
+
ptySession.onPtyReplacedCallbacks.push(() => {
|
|
479
|
+
const stillExists = sessions.get(result.id);
|
|
480
|
+
assert.ok(stillExists, 'session should still exist after retry');
|
|
481
|
+
done();
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
it('retries when continue-arg process exits quickly with code 0 (tmux behavior)', (_, done) => {
|
|
485
|
+
const result = sessions.create({
|
|
486
|
+
repoName: 'test-repo',
|
|
487
|
+
repoPath: '/tmp',
|
|
488
|
+
worktreePath: null,
|
|
489
|
+
cwd: '/tmp',
|
|
490
|
+
command: '/bin/sh',
|
|
491
|
+
args: ['-c', 'exit 0', ...(sessions.AGENT_CONTINUE_ARGS['claude'] ?? [])],
|
|
492
|
+
});
|
|
493
|
+
createdIds.push(result.id);
|
|
494
|
+
const session = sessions.get(result.id);
|
|
495
|
+
assert.ok(session);
|
|
496
|
+
assert.strictEqual(session.mode, 'pty');
|
|
497
|
+
const ptySession = session;
|
|
498
|
+
ptySession.onPtyReplacedCallbacks.push((newPty) => {
|
|
499
|
+
assert.ok(newPty, 'should receive new PTY even with exit code 0');
|
|
500
|
+
assert.strictEqual(ptySession.pty, newPty, 'session.pty should be updated');
|
|
501
|
+
const stillExists = sessions.get(result.id);
|
|
502
|
+
assert.ok(stillExists, 'session should still exist after retry');
|
|
503
|
+
done();
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
it('create accepts a predetermined id', () => {
|
|
507
|
+
const result = sessions.create({
|
|
508
|
+
id: 'custom-id-12345678',
|
|
509
|
+
repoName: 'test-repo',
|
|
510
|
+
repoPath: '/tmp',
|
|
511
|
+
worktreePath: null,
|
|
512
|
+
cwd: '/tmp',
|
|
513
|
+
command: '/bin/echo',
|
|
514
|
+
args: ['hello'],
|
|
515
|
+
});
|
|
516
|
+
createdIds.push(result.id);
|
|
517
|
+
assert.strictEqual(result.id, 'custom-id-12345678');
|
|
518
|
+
const session = sessions.get('custom-id-12345678');
|
|
519
|
+
assert.ok(session);
|
|
520
|
+
});
|
|
521
|
+
it('create accepts initialScrollback', () => {
|
|
522
|
+
const result = sessions.create({
|
|
523
|
+
repoName: 'test-repo',
|
|
524
|
+
repoPath: '/tmp',
|
|
525
|
+
worktreePath: null,
|
|
526
|
+
cwd: '/tmp',
|
|
527
|
+
command: '/bin/echo',
|
|
528
|
+
args: ['hello'],
|
|
529
|
+
initialScrollback: ['prior output\r\n'],
|
|
530
|
+
});
|
|
531
|
+
createdIds.push(result.id);
|
|
532
|
+
const session = sessions.get(result.id);
|
|
533
|
+
assert.ok(session);
|
|
534
|
+
assert.strictEqual(session.mode, 'pty');
|
|
535
|
+
assert.ok(session.scrollback.length >= 1);
|
|
536
|
+
assert.strictEqual(session.scrollback[0], 'prior output\r\n');
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
describe('session persistence', () => {
|
|
540
|
+
let tmpDir;
|
|
541
|
+
afterEach(() => {
|
|
542
|
+
// Clean up any sessions created during tests
|
|
543
|
+
for (const s of sessions.list()) {
|
|
544
|
+
try {
|
|
545
|
+
sessions.kill(s.id);
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
/* ignore */
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// Clean up temp directory
|
|
552
|
+
if (tmpDir) {
|
|
553
|
+
try {
|
|
554
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
/* ignore */
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
function createTmpDir() {
|
|
562
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'crc-test-'));
|
|
563
|
+
return tmpDir;
|
|
564
|
+
}
|
|
565
|
+
it('serializeAll writes pending-sessions.json and scrollback files', () => {
|
|
566
|
+
const configDir = createTmpDir();
|
|
567
|
+
const s = sessions.create({
|
|
568
|
+
repoName: 'test-repo',
|
|
569
|
+
repoPath: '/tmp',
|
|
570
|
+
worktreePath: null,
|
|
571
|
+
cwd: '/tmp',
|
|
572
|
+
command: '/bin/cat',
|
|
573
|
+
args: [],
|
|
574
|
+
});
|
|
575
|
+
// Manually push some scrollback
|
|
576
|
+
const session = sessions.get(s.id);
|
|
577
|
+
assert.ok(session);
|
|
578
|
+
assert.strictEqual(session.mode, 'pty');
|
|
579
|
+
session.scrollback.push('hello world');
|
|
580
|
+
serializeAll(configDir);
|
|
581
|
+
// Check pending-sessions.json
|
|
582
|
+
const pendingPath = path.join(configDir, 'pending-sessions.json');
|
|
583
|
+
assert.ok(fs.existsSync(pendingPath), 'pending-sessions.json should exist');
|
|
584
|
+
const pending = JSON.parse(fs.readFileSync(pendingPath, 'utf-8'));
|
|
585
|
+
assert.strictEqual(pending.version, 4);
|
|
586
|
+
assert.ok(pending.timestamp);
|
|
587
|
+
assert.strictEqual(pending.sessions.length, 1);
|
|
588
|
+
assert.strictEqual(pending.sessions[0].id, s.id);
|
|
589
|
+
assert.strictEqual(pending.sessions[0].cwd, '/tmp');
|
|
590
|
+
assert.strictEqual(pending.sessions[0].repoPath, '/tmp');
|
|
591
|
+
// Check scrollback file
|
|
592
|
+
const scrollbackPath = path.join(configDir, 'scrollback', s.id + '.buf');
|
|
593
|
+
assert.ok(fs.existsSync(scrollbackPath), 'scrollback file should exist');
|
|
594
|
+
const scrollbackData = fs.readFileSync(scrollbackPath, 'utf-8');
|
|
595
|
+
assert.ok(scrollbackData.includes('hello world'));
|
|
596
|
+
});
|
|
597
|
+
it('restoreFromDisk restores sessions with original IDs', async () => {
|
|
598
|
+
const configDir = createTmpDir();
|
|
599
|
+
// Create and serialize a session
|
|
600
|
+
const s = sessions.create({
|
|
601
|
+
repoName: 'test-repo',
|
|
602
|
+
repoPath: '/tmp',
|
|
603
|
+
worktreePath: null,
|
|
604
|
+
cwd: '/tmp',
|
|
605
|
+
command: '/bin/cat',
|
|
606
|
+
args: [],
|
|
607
|
+
displayName: 'my-session',
|
|
608
|
+
});
|
|
609
|
+
const originalId = s.id;
|
|
610
|
+
const session = sessions.get(originalId);
|
|
611
|
+
assert.ok(session);
|
|
612
|
+
assert.strictEqual(session.mode, 'pty');
|
|
613
|
+
session.scrollback.push('saved output');
|
|
614
|
+
serializeAll(configDir);
|
|
615
|
+
// Kill the original session
|
|
616
|
+
sessions.kill(originalId);
|
|
617
|
+
assert.strictEqual(sessions.get(originalId), undefined);
|
|
618
|
+
// Restore
|
|
619
|
+
const restored = await restoreFromDisk(configDir);
|
|
620
|
+
assert.strictEqual(restored, 1);
|
|
621
|
+
// Verify session exists with original ID
|
|
622
|
+
const restoredSession = sessions.get(originalId);
|
|
623
|
+
assert.ok(restoredSession, 'restored session should exist');
|
|
624
|
+
assert.strictEqual(restoredSession.cwd, '/tmp');
|
|
625
|
+
assert.strictEqual(restoredSession.repoPath, '/tmp');
|
|
626
|
+
assert.strictEqual(restoredSession.displayName, 'my-session');
|
|
627
|
+
// Scrollback should be restored
|
|
628
|
+
assert.strictEqual(restoredSession.mode, 'pty');
|
|
629
|
+
assert.ok(restoredSession.scrollback.length >= 1);
|
|
630
|
+
assert.strictEqual(restoredSession.scrollback[0], 'saved output');
|
|
631
|
+
// pending-sessions.json should be cleaned up
|
|
632
|
+
assert.ok(!fs.existsSync(path.join(configDir, 'pending-sessions.json')));
|
|
633
|
+
});
|
|
634
|
+
it('restoreFromDisk ignores stale files (>5 min old)', async () => {
|
|
635
|
+
const configDir = createTmpDir();
|
|
636
|
+
// Write a stale pending file
|
|
637
|
+
const staleTime = new Date(Date.now() - 6 * 60 * 1000).toISOString();
|
|
638
|
+
const pending = {
|
|
639
|
+
version: 3,
|
|
640
|
+
timestamp: staleTime,
|
|
641
|
+
sessions: [
|
|
642
|
+
{
|
|
643
|
+
id: 'stale-id',
|
|
644
|
+
type: 'agent',
|
|
645
|
+
agent: 'claude',
|
|
646
|
+
workspacePath: '/tmp',
|
|
647
|
+
worktreePath: null,
|
|
648
|
+
cwd: '/tmp',
|
|
649
|
+
repoName: 'test',
|
|
650
|
+
branchName: '',
|
|
651
|
+
displayName: 'test',
|
|
652
|
+
createdAt: staleTime,
|
|
653
|
+
lastActivity: staleTime,
|
|
654
|
+
useTmux: false,
|
|
655
|
+
tmuxSessionName: '',
|
|
656
|
+
customCommand: null,
|
|
657
|
+
},
|
|
658
|
+
],
|
|
659
|
+
};
|
|
660
|
+
fs.writeFileSync(path.join(configDir, 'pending-sessions.json'), JSON.stringify(pending));
|
|
661
|
+
const restored = await restoreFromDisk(configDir);
|
|
662
|
+
assert.strictEqual(restored, 0, 'should not restore stale sessions');
|
|
663
|
+
assert.ok(!fs.existsSync(path.join(configDir, 'pending-sessions.json')), 'stale file should be deleted');
|
|
664
|
+
});
|
|
665
|
+
it('restoreFromDisk handles missing scrollback gracefully', async () => {
|
|
666
|
+
const configDir = createTmpDir();
|
|
667
|
+
// Create a session, serialize, then delete scrollback file
|
|
668
|
+
const s = sessions.create({
|
|
669
|
+
repoName: 'test-repo',
|
|
670
|
+
repoPath: '/tmp',
|
|
671
|
+
worktreePath: null,
|
|
672
|
+
cwd: '/tmp',
|
|
673
|
+
command: '/bin/cat',
|
|
674
|
+
args: [],
|
|
675
|
+
});
|
|
676
|
+
serializeAll(configDir);
|
|
677
|
+
sessions.kill(s.id);
|
|
678
|
+
// Delete scrollback file
|
|
679
|
+
const scrollbackPath = path.join(configDir, 'scrollback', s.id + '.buf');
|
|
680
|
+
try {
|
|
681
|
+
fs.unlinkSync(scrollbackPath);
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
/* ignore */
|
|
685
|
+
}
|
|
686
|
+
const restored = await restoreFromDisk(configDir);
|
|
687
|
+
assert.strictEqual(restored, 1, 'should still restore without scrollback');
|
|
688
|
+
});
|
|
689
|
+
it('restoreFromDisk returns 0 when no pending file exists', async () => {
|
|
690
|
+
const configDir = createTmpDir();
|
|
691
|
+
const restored = await restoreFromDisk(configDir);
|
|
692
|
+
assert.strictEqual(restored, 0);
|
|
693
|
+
});
|
|
694
|
+
it('restoreFromDisk preserves tmuxSessionName for tmux sessions', async () => {
|
|
695
|
+
const configDir = createTmpDir();
|
|
696
|
+
// Write a pending file with a tmux session
|
|
697
|
+
const pending = {
|
|
698
|
+
version: 3,
|
|
699
|
+
timestamp: new Date().toISOString(),
|
|
700
|
+
sessions: [
|
|
701
|
+
{
|
|
702
|
+
id: 'tmux-test-id',
|
|
703
|
+
type: 'agent',
|
|
704
|
+
agent: 'claude',
|
|
705
|
+
workspacePath: '/tmp',
|
|
706
|
+
worktreePath: null,
|
|
707
|
+
cwd: '/tmp',
|
|
708
|
+
repoName: 'test-repo',
|
|
709
|
+
branchName: 'my-branch',
|
|
710
|
+
displayName: 'my-session',
|
|
711
|
+
createdAt: new Date().toISOString(),
|
|
712
|
+
lastActivity: new Date().toISOString(),
|
|
713
|
+
useTmux: true,
|
|
714
|
+
tmuxSessionName: 'crc-my-session-tmux-tes',
|
|
715
|
+
customCommand: '/bin/cat', // Use /bin/cat to avoid spawning real claude binary in test
|
|
716
|
+
},
|
|
717
|
+
],
|
|
718
|
+
};
|
|
719
|
+
fs.writeFileSync(path.join(configDir, 'pending-sessions.json'), JSON.stringify(pending));
|
|
720
|
+
const restored = await restoreFromDisk(configDir);
|
|
721
|
+
assert.strictEqual(restored, 1);
|
|
722
|
+
const session = sessions.get('tmux-test-id');
|
|
723
|
+
assert.ok(session, 'restored session should exist');
|
|
724
|
+
assert.strictEqual(session.mode, 'pty');
|
|
725
|
+
assert.strictEqual(session.tmuxSessionName, 'crc-my-session-tmux-tes', 'tmuxSessionName should be preserved from serialized data');
|
|
726
|
+
});
|
|
727
|
+
it('restored session remains in list after PTY exits (disconnected status)', async () => {
|
|
728
|
+
const configDir = createTmpDir();
|
|
729
|
+
const pending = {
|
|
730
|
+
version: 3,
|
|
731
|
+
timestamp: new Date().toISOString(),
|
|
732
|
+
sessions: [
|
|
733
|
+
{
|
|
734
|
+
id: 'restore-exit-test',
|
|
735
|
+
type: 'agent',
|
|
736
|
+
agent: 'claude',
|
|
737
|
+
workspacePath: '/tmp',
|
|
738
|
+
worktreePath: null,
|
|
739
|
+
cwd: '/tmp',
|
|
740
|
+
repoName: 'test-repo',
|
|
741
|
+
branchName: 'my-branch',
|
|
742
|
+
displayName: 'restored-session',
|
|
743
|
+
createdAt: new Date().toISOString(),
|
|
744
|
+
lastActivity: new Date().toISOString(),
|
|
745
|
+
useTmux: false,
|
|
746
|
+
tmuxSessionName: '',
|
|
747
|
+
customCommand: '/bin/false',
|
|
748
|
+
},
|
|
749
|
+
],
|
|
750
|
+
};
|
|
751
|
+
fs.writeFileSync(path.join(configDir, 'pending-sessions.json'), JSON.stringify(pending));
|
|
752
|
+
await restoreFromDisk(configDir);
|
|
753
|
+
// Wait for PTY to exit
|
|
754
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
755
|
+
// Session should still be in the list with disconnected status
|
|
756
|
+
const list = sessions.list();
|
|
757
|
+
const found = list.find((s) => s.id === 'restore-exit-test');
|
|
758
|
+
assert.ok(found, 'restored session should remain in list after PTY exit');
|
|
759
|
+
assert.strictEqual(found.status, 'disconnected');
|
|
760
|
+
});
|
|
761
|
+
it('full serialize-restore round trip preserves all session fields including tmuxSessionName', async () => {
|
|
762
|
+
const configDir = createTmpDir();
|
|
763
|
+
// Create sessions of different types
|
|
764
|
+
const agentSession = sessions.create({
|
|
765
|
+
type: 'agent',
|
|
766
|
+
repoName: 'my-repo',
|
|
767
|
+
repoPath: '/tmp/repo',
|
|
768
|
+
worktreePath: null,
|
|
769
|
+
cwd: '/tmp/repo',
|
|
770
|
+
command: '/bin/cat',
|
|
771
|
+
args: [],
|
|
772
|
+
displayName: 'My Agent',
|
|
773
|
+
});
|
|
774
|
+
const terminal = sessions.create({
|
|
775
|
+
type: 'terminal',
|
|
776
|
+
repoPath: '/tmp',
|
|
777
|
+
worktreePath: null,
|
|
778
|
+
cwd: '/tmp',
|
|
779
|
+
command: '/bin/sh',
|
|
780
|
+
args: [],
|
|
781
|
+
displayName: 'Terminal 1',
|
|
782
|
+
});
|
|
783
|
+
// Serialize all
|
|
784
|
+
serializeAll(configDir);
|
|
785
|
+
// Kill originals
|
|
786
|
+
sessions.kill(agentSession.id);
|
|
787
|
+
sessions.kill(terminal.id);
|
|
788
|
+
assert.strictEqual(sessions.list().length, 0);
|
|
789
|
+
// Also inject a tmux-style session into the pending file to test tmuxSessionName round-trip.
|
|
790
|
+
// Use customCommand so restore spawns that instead of claude --continue (which would exit instantly).
|
|
791
|
+
const pendingPath = path.join(configDir, 'pending-sessions.json');
|
|
792
|
+
const pending = JSON.parse(fs.readFileSync(pendingPath, 'utf-8'));
|
|
793
|
+
pending.sessions.push({
|
|
794
|
+
id: 'tmux-roundtrip-id',
|
|
795
|
+
type: 'agent',
|
|
796
|
+
agent: 'claude',
|
|
797
|
+
workspacePath: '/tmp',
|
|
798
|
+
worktreePath: null,
|
|
799
|
+
cwd: '/tmp',
|
|
800
|
+
repoName: 'tmux-repo',
|
|
801
|
+
branchName: 'feat/tmux',
|
|
802
|
+
displayName: 'Tmux Session',
|
|
803
|
+
createdAt: new Date().toISOString(),
|
|
804
|
+
lastActivity: new Date().toISOString(),
|
|
805
|
+
useTmux: true,
|
|
806
|
+
tmuxSessionName: 'crc-tmux-session-tmux-rou',
|
|
807
|
+
customCommand: '/bin/cat',
|
|
808
|
+
});
|
|
809
|
+
fs.writeFileSync(pendingPath, JSON.stringify(pending));
|
|
810
|
+
// Restore
|
|
811
|
+
const restored = await restoreFromDisk(configDir);
|
|
812
|
+
assert.strictEqual(restored, 3);
|
|
813
|
+
// Verify all sessions exist
|
|
814
|
+
const list = sessions.list();
|
|
815
|
+
assert.strictEqual(list.length, 3);
|
|
816
|
+
const restoredAgent = list.find((s) => s.id === agentSession.id);
|
|
817
|
+
assert.ok(restoredAgent);
|
|
818
|
+
assert.strictEqual(restoredAgent.type, 'agent');
|
|
819
|
+
assert.strictEqual(restoredAgent.displayName, 'My Agent');
|
|
820
|
+
assert.strictEqual(restoredAgent.status, 'active');
|
|
821
|
+
const restoredTerminal = list.find((s) => s.id === terminal.id);
|
|
822
|
+
assert.ok(restoredTerminal);
|
|
823
|
+
assert.strictEqual(restoredTerminal.type, 'terminal');
|
|
824
|
+
assert.strictEqual(restoredTerminal.displayName, 'Terminal 1');
|
|
825
|
+
// Verify tmux session name survived the round trip
|
|
826
|
+
const restoredTmux = sessions.get('tmux-roundtrip-id');
|
|
827
|
+
assert.ok(restoredTmux);
|
|
828
|
+
assert.strictEqual(restoredTmux.mode, 'pty');
|
|
829
|
+
assert.strictEqual(restoredTmux.tmuxSessionName, 'crc-tmux-session-tmux-rou');
|
|
830
|
+
assert.strictEqual(restoredTmux.displayName, 'Tmux Session');
|
|
831
|
+
});
|
|
832
|
+
it('serialize/restore preserves yolo flag', async () => {
|
|
833
|
+
const configDir = createTmpDir();
|
|
834
|
+
const s = sessions.create({
|
|
835
|
+
repoName: 'test-repo',
|
|
836
|
+
repoPath: '/tmp',
|
|
837
|
+
worktreePath: null,
|
|
838
|
+
cwd: '/tmp',
|
|
839
|
+
command: '/bin/cat',
|
|
840
|
+
args: [],
|
|
841
|
+
yolo: true,
|
|
842
|
+
});
|
|
843
|
+
const session = sessions.get(s.id);
|
|
844
|
+
assert.ok(session);
|
|
845
|
+
assert.strictEqual(session.yolo, true);
|
|
846
|
+
serializeAll(configDir);
|
|
847
|
+
sessions.kill(s.id);
|
|
848
|
+
// Verify yolo is in the serialized JSON
|
|
849
|
+
const pending = JSON.parse(fs.readFileSync(path.join(configDir, 'pending-sessions.json'), 'utf-8'));
|
|
850
|
+
assert.strictEqual(pending.version, 4);
|
|
851
|
+
assert.strictEqual(pending.sessions[0].yolo, true);
|
|
852
|
+
await restoreFromDisk(configDir);
|
|
853
|
+
const restored = sessions.get(s.id);
|
|
854
|
+
assert.ok(restored);
|
|
855
|
+
assert.strictEqual(restored.yolo, true);
|
|
856
|
+
});
|
|
857
|
+
it('maps codex yolo to no-approval workspace-write args', () => {
|
|
858
|
+
assert.deepStrictEqual(AGENT_YOLO_ARGS.codex, [
|
|
859
|
+
'--ask-for-approval',
|
|
860
|
+
'never',
|
|
861
|
+
'--sandbox',
|
|
862
|
+
'workspace-write',
|
|
863
|
+
]);
|
|
864
|
+
});
|
|
865
|
+
it('serialize/restore preserves claudeArgs', async () => {
|
|
866
|
+
const configDir = createTmpDir();
|
|
867
|
+
const s = sessions.create({
|
|
868
|
+
repoName: 'test-repo',
|
|
869
|
+
repoPath: '/tmp',
|
|
870
|
+
worktreePath: null,
|
|
871
|
+
cwd: '/tmp',
|
|
872
|
+
command: '/bin/cat',
|
|
873
|
+
args: [],
|
|
874
|
+
claudeArgs: ['--model', 'opus', '--verbose'],
|
|
875
|
+
});
|
|
876
|
+
const session = sessions.get(s.id);
|
|
877
|
+
assert.ok(session);
|
|
878
|
+
assert.deepStrictEqual(session.claudeArgs, [
|
|
879
|
+
'--model',
|
|
880
|
+
'opus',
|
|
881
|
+
'--verbose',
|
|
882
|
+
]);
|
|
883
|
+
serializeAll(configDir);
|
|
884
|
+
sessions.kill(s.id);
|
|
885
|
+
await restoreFromDisk(configDir);
|
|
886
|
+
const restored = sessions.get(s.id);
|
|
887
|
+
assert.ok(restored);
|
|
888
|
+
assert.deepStrictEqual(restored.claudeArgs, [
|
|
889
|
+
'--model',
|
|
890
|
+
'opus',
|
|
891
|
+
'--verbose',
|
|
892
|
+
]);
|
|
893
|
+
});
|
|
894
|
+
it('serialize/restore preserves hookToken and hooksActive', async () => {
|
|
895
|
+
const configDir = createTmpDir();
|
|
896
|
+
const s = sessions.create({
|
|
897
|
+
repoName: 'test-repo',
|
|
898
|
+
repoPath: '/tmp',
|
|
899
|
+
worktreePath: null,
|
|
900
|
+
cwd: '/tmp',
|
|
901
|
+
command: '/bin/cat',
|
|
902
|
+
args: [],
|
|
903
|
+
});
|
|
904
|
+
// Manually set hookToken and hooksActive (simulating a session that had hooks injected)
|
|
905
|
+
const session = sessions.get(s.id);
|
|
906
|
+
assert.ok(session);
|
|
907
|
+
session.hookToken = 'abc123deadbeef';
|
|
908
|
+
session.hooksActive = true;
|
|
909
|
+
serializeAll(configDir);
|
|
910
|
+
// Verify hookToken is in the serialized JSON
|
|
911
|
+
const pending = JSON.parse(fs.readFileSync(path.join(configDir, 'pending-sessions.json'), 'utf-8'));
|
|
912
|
+
assert.strictEqual(pending.sessions[0].hookToken, 'abc123deadbeef');
|
|
913
|
+
assert.strictEqual(pending.sessions[0].hooksActive, true);
|
|
914
|
+
sessions.kill(s.id);
|
|
915
|
+
await restoreFromDisk(configDir);
|
|
916
|
+
const restored = sessions.get(s.id);
|
|
917
|
+
assert.ok(restored);
|
|
918
|
+
assert.strictEqual(restored.hookToken, 'abc123deadbeef');
|
|
919
|
+
assert.strictEqual(restored.hooksActive, true);
|
|
920
|
+
});
|
|
921
|
+
it('serialize/restore preserves needsBranchRename and branchRenamePrompt', async () => {
|
|
922
|
+
const configDir = createTmpDir();
|
|
923
|
+
const s = sessions.create({
|
|
924
|
+
repoName: 'test-repo',
|
|
925
|
+
repoPath: '/tmp',
|
|
926
|
+
worktreePath: null,
|
|
927
|
+
cwd: '/tmp',
|
|
928
|
+
command: '/bin/cat',
|
|
929
|
+
args: [],
|
|
930
|
+
needsBranchRename: true,
|
|
931
|
+
branchRenamePrompt: 'Name this feature branch:',
|
|
932
|
+
});
|
|
933
|
+
const session = sessions.get(s.id);
|
|
934
|
+
assert.ok(session);
|
|
935
|
+
assert.strictEqual(session.needsBranchRename, true);
|
|
936
|
+
serializeAll(configDir);
|
|
937
|
+
sessions.kill(s.id);
|
|
938
|
+
await restoreFromDisk(configDir);
|
|
939
|
+
const restored = sessions.get(s.id);
|
|
940
|
+
assert.ok(restored);
|
|
941
|
+
assert.strictEqual(restored.needsBranchRename, true);
|
|
942
|
+
assert.strictEqual(restored.branchRenamePrompt, 'Name this feature branch:');
|
|
943
|
+
});
|
|
944
|
+
it('restoreFromDisk handles v1/v2 pending files (v2→v3 migration)', async () => {
|
|
945
|
+
const configDir = createTmpDir();
|
|
946
|
+
// Write a v2 format pending file with old fields: type: 'repo', repoPath, root
|
|
947
|
+
const v2Timestamp = new Date().toISOString();
|
|
948
|
+
const pending = {
|
|
949
|
+
version: 2,
|
|
950
|
+
timestamp: v2Timestamp,
|
|
951
|
+
sessions: [
|
|
952
|
+
{
|
|
953
|
+
id: 'v2-migration-test',
|
|
954
|
+
type: 'repo',
|
|
955
|
+
agent: 'claude',
|
|
956
|
+
root: '',
|
|
957
|
+
repoName: 'test-repo',
|
|
958
|
+
repoPath: '/tmp/my-repo',
|
|
959
|
+
worktreeName: '',
|
|
960
|
+
branchName: 'main',
|
|
961
|
+
displayName: 'v2-session',
|
|
962
|
+
createdAt: v2Timestamp,
|
|
963
|
+
lastActivity: v2Timestamp,
|
|
964
|
+
useTmux: false,
|
|
965
|
+
tmuxSessionName: '',
|
|
966
|
+
customCommand: '/bin/cat',
|
|
967
|
+
cwd: '/tmp/my-repo',
|
|
968
|
+
},
|
|
969
|
+
],
|
|
970
|
+
};
|
|
971
|
+
fs.writeFileSync(path.join(configDir, 'pending-sessions.json'), JSON.stringify(pending));
|
|
972
|
+
const restored = await restoreFromDisk(configDir);
|
|
973
|
+
assert.strictEqual(restored, 1);
|
|
974
|
+
const session = sessions.get('v2-migration-test');
|
|
975
|
+
assert.ok(session, 'restored session should exist');
|
|
976
|
+
// type should be migrated from 'repo' to 'agent'
|
|
977
|
+
assert.strictEqual(session.type, 'agent', 'type should be migrated to agent');
|
|
978
|
+
// cwd should equal the old repoPath
|
|
979
|
+
assert.strictEqual(session.cwd, '/tmp/my-repo', 'cwd should be set from old repoPath');
|
|
980
|
+
// repoPath should be derived from cwd (no configured workspaces, so falls back to cwd)
|
|
981
|
+
assert.strictEqual(session.repoPath, '/tmp/my-repo', 'repoPath should be derived');
|
|
982
|
+
// worktreePath should be null since cwd === repoPath
|
|
983
|
+
assert.strictEqual(session.worktreePath, null, 'worktreePath should be null for main repo sessions');
|
|
984
|
+
});
|
|
985
|
+
it('restoreFromDisk handles v3 pending files (v3→v4 migration: workspacePath→repoPath)', async () => {
|
|
986
|
+
const configDir = createTmpDir();
|
|
987
|
+
const v3Timestamp = new Date().toISOString();
|
|
988
|
+
const pending = {
|
|
989
|
+
version: 3,
|
|
990
|
+
timestamp: v3Timestamp,
|
|
991
|
+
sessions: [
|
|
992
|
+
{
|
|
993
|
+
id: 'v3-migration-test',
|
|
994
|
+
type: 'agent',
|
|
995
|
+
agent: 'claude',
|
|
996
|
+
workspacePath: '/tmp/my-v3-repo',
|
|
997
|
+
worktreePath: null,
|
|
998
|
+
cwd: '/tmp/my-v3-repo',
|
|
999
|
+
repoName: 'v3-repo',
|
|
1000
|
+
branchName: 'main',
|
|
1001
|
+
displayName: 'v3-session',
|
|
1002
|
+
createdAt: v3Timestamp,
|
|
1003
|
+
lastActivity: v3Timestamp,
|
|
1004
|
+
useTmux: false,
|
|
1005
|
+
tmuxSessionName: '',
|
|
1006
|
+
customCommand: '/bin/cat',
|
|
1007
|
+
},
|
|
1008
|
+
],
|
|
1009
|
+
};
|
|
1010
|
+
fs.writeFileSync(path.join(configDir, 'pending-sessions.json'), JSON.stringify(pending));
|
|
1011
|
+
const restored = await restoreFromDisk(configDir);
|
|
1012
|
+
assert.strictEqual(restored, 1);
|
|
1013
|
+
const session = sessions.get('v3-migration-test');
|
|
1014
|
+
assert.ok(session, 'restored session should exist');
|
|
1015
|
+
// repoPath should be migrated from v3 workspacePath
|
|
1016
|
+
assert.strictEqual(session.repoPath, '/tmp/my-v3-repo', 'repoPath should be set from v3 workspacePath');
|
|
1017
|
+
assert.strictEqual(session.cwd, '/tmp/my-v3-repo', 'cwd should be preserved');
|
|
1018
|
+
createdIds.push('v3-migration-test');
|
|
1019
|
+
});
|
|
1020
|
+
it('serializeAll writes version 4 in pending-sessions.json', () => {
|
|
1021
|
+
const configDir = createTmpDir();
|
|
1022
|
+
const s = sessions.create({
|
|
1023
|
+
repoName: 'test-repo',
|
|
1024
|
+
repoPath: '/tmp',
|
|
1025
|
+
worktreePath: null,
|
|
1026
|
+
cwd: '/tmp',
|
|
1027
|
+
command: '/bin/echo',
|
|
1028
|
+
args: ['hello'],
|
|
1029
|
+
});
|
|
1030
|
+
createdIds.push(s.id);
|
|
1031
|
+
serializeAll(configDir);
|
|
1032
|
+
const pendingPath = path.join(configDir, 'pending-sessions.json');
|
|
1033
|
+
assert.ok(fs.existsSync(pendingPath), 'pending-sessions.json should exist');
|
|
1034
|
+
const pending = JSON.parse(fs.readFileSync(pendingPath, 'utf-8'));
|
|
1035
|
+
assert.strictEqual(pending.version, 4, 'serializeAll should write version 4');
|
|
1036
|
+
assert.strictEqual(pending.sessions[0].repoPath, '/tmp', 'repoPath field should be present');
|
|
1037
|
+
assert.ok(!('workspacePath' in pending.sessions[0]), 'workspacePath field should not be present in v4');
|
|
1038
|
+
});
|
|
1039
|
+
it('serializeAll captures session state before kill', () => {
|
|
1040
|
+
const configDir = createTmpDir();
|
|
1041
|
+
const s = sessions.create({
|
|
1042
|
+
repoName: 'test-repo',
|
|
1043
|
+
repoPath: '/tmp',
|
|
1044
|
+
worktreePath: null,
|
|
1045
|
+
cwd: '/tmp',
|
|
1046
|
+
command: '/bin/cat',
|
|
1047
|
+
args: [],
|
|
1048
|
+
displayName: 'before-kill',
|
|
1049
|
+
});
|
|
1050
|
+
const session = sessions.get(s.id);
|
|
1051
|
+
assert.ok(session);
|
|
1052
|
+
assert.strictEqual(session.mode, 'pty');
|
|
1053
|
+
session.scrollback.push('important output');
|
|
1054
|
+
serializeAll(configDir);
|
|
1055
|
+
// Kill after serialize (mimics gracefulShutdown sequence)
|
|
1056
|
+
sessions.kill(s.id);
|
|
1057
|
+
// Verify data is on disk
|
|
1058
|
+
const pendingPath = path.join(configDir, 'pending-sessions.json');
|
|
1059
|
+
assert.ok(fs.existsSync(pendingPath));
|
|
1060
|
+
const pending = JSON.parse(fs.readFileSync(pendingPath, 'utf-8'));
|
|
1061
|
+
assert.strictEqual(pending.sessions.length, 1);
|
|
1062
|
+
assert.strictEqual(pending.sessions[0].displayName, 'before-kill');
|
|
1063
|
+
});
|
|
1064
|
+
it('restoreFromDisk uses framework continueArgs for claude (--continue)', async () => {
|
|
1065
|
+
const configDir = createTmpDir();
|
|
1066
|
+
// Write a v4 pending file with a non-tmux claude agent session
|
|
1067
|
+
const pending = {
|
|
1068
|
+
version: 4,
|
|
1069
|
+
timestamp: new Date().toISOString(),
|
|
1070
|
+
sessions: [
|
|
1071
|
+
{
|
|
1072
|
+
id: 'framework-continue-claude',
|
|
1073
|
+
type: 'agent',
|
|
1074
|
+
agent: 'claude',
|
|
1075
|
+
repoPath: '/tmp',
|
|
1076
|
+
worktreePath: null,
|
|
1077
|
+
cwd: '/tmp',
|
|
1078
|
+
repoName: 'test-repo',
|
|
1079
|
+
branchName: 'main',
|
|
1080
|
+
displayName: 'claude-session',
|
|
1081
|
+
createdAt: new Date().toISOString(),
|
|
1082
|
+
lastActivity: new Date().toISOString(),
|
|
1083
|
+
useTmux: false,
|
|
1084
|
+
tmuxSessionName: '',
|
|
1085
|
+
customCommand: '/bin/cat', // use /bin/cat so session doesn't error out
|
|
1086
|
+
yolo: false,
|
|
1087
|
+
claudeArgs: [],
|
|
1088
|
+
},
|
|
1089
|
+
],
|
|
1090
|
+
};
|
|
1091
|
+
fs.writeFileSync(path.join(configDir, 'pending-sessions.json'), JSON.stringify(pending));
|
|
1092
|
+
const restored = await restoreFromDisk(configDir);
|
|
1093
|
+
assert.strictEqual(restored, 1);
|
|
1094
|
+
const session = sessions.get('framework-continue-claude');
|
|
1095
|
+
assert.ok(session, 'restored session should exist');
|
|
1096
|
+
// The session should have been created successfully (continueArgs from framework)
|
|
1097
|
+
assert.strictEqual(session.agent, 'claude');
|
|
1098
|
+
});
|
|
1099
|
+
it('restoreFromDisk uses framework continueArgs for codex (resume --last)', async () => {
|
|
1100
|
+
const configDir = createTmpDir();
|
|
1101
|
+
// Write a v4 pending file with a non-tmux codex agent session
|
|
1102
|
+
const pending = {
|
|
1103
|
+
version: 4,
|
|
1104
|
+
timestamp: new Date().toISOString(),
|
|
1105
|
+
sessions: [
|
|
1106
|
+
{
|
|
1107
|
+
id: 'framework-continue-codex',
|
|
1108
|
+
type: 'agent',
|
|
1109
|
+
agent: 'codex',
|
|
1110
|
+
repoPath: '/tmp',
|
|
1111
|
+
worktreePath: null,
|
|
1112
|
+
cwd: '/tmp',
|
|
1113
|
+
repoName: 'test-repo',
|
|
1114
|
+
branchName: 'main',
|
|
1115
|
+
displayName: 'codex-session',
|
|
1116
|
+
createdAt: new Date().toISOString(),
|
|
1117
|
+
lastActivity: new Date().toISOString(),
|
|
1118
|
+
useTmux: false,
|
|
1119
|
+
tmuxSessionName: '',
|
|
1120
|
+
customCommand: '/bin/cat',
|
|
1121
|
+
yolo: false,
|
|
1122
|
+
claudeArgs: [],
|
|
1123
|
+
},
|
|
1124
|
+
],
|
|
1125
|
+
};
|
|
1126
|
+
fs.writeFileSync(path.join(configDir, 'pending-sessions.json'), JSON.stringify(pending));
|
|
1127
|
+
const restored = await restoreFromDisk(configDir);
|
|
1128
|
+
assert.strictEqual(restored, 1);
|
|
1129
|
+
const session = sessions.get('framework-continue-codex');
|
|
1130
|
+
assert.ok(session, 'restored session should exist');
|
|
1131
|
+
assert.strictEqual(session.agent, 'codex');
|
|
1132
|
+
});
|
|
1133
|
+
it('serializeAll preserves claudeArgs for backward compat', () => {
|
|
1134
|
+
const configDir = createTmpDir();
|
|
1135
|
+
const s = sessions.create({
|
|
1136
|
+
repoName: 'test-repo',
|
|
1137
|
+
repoPath: '/tmp',
|
|
1138
|
+
worktreePath: null,
|
|
1139
|
+
cwd: '/tmp',
|
|
1140
|
+
command: '/bin/cat',
|
|
1141
|
+
args: [],
|
|
1142
|
+
claudeArgs: ['--model', 'opus'],
|
|
1143
|
+
});
|
|
1144
|
+
serializeAll(configDir);
|
|
1145
|
+
sessions.kill(s.id);
|
|
1146
|
+
const pendingPath = path.join(configDir, 'pending-sessions.json');
|
|
1147
|
+
const pending = JSON.parse(fs.readFileSync(pendingPath, 'utf-8'));
|
|
1148
|
+
assert.strictEqual(pending.sessions.length, 1);
|
|
1149
|
+
// claudeArgs should still be there for backward compat
|
|
1150
|
+
assert.deepStrictEqual(pending.sessions[0].claudeArgs, ['--model', 'opus']);
|
|
1151
|
+
});
|
|
1152
|
+
});
|