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.
Files changed (506) hide show
  1. package/README.md +259 -0
  2. package/dist/bin/claude-remote-cli.js +390 -0
  3. package/dist/bin/relay-ide.js +390 -0
  4. package/dist/frontend/assets/abap-BdImnpbu.js +1 -0
  5. package/dist/frontend/assets/actionscript-3-CoDkCxhg.js +1 -0
  6. package/dist/frontend/assets/ada-bCR0ucgS.js +1 -0
  7. package/dist/frontend/assets/andromeeda-C4gqWexZ.js +1 -0
  8. package/dist/frontend/assets/angular-html-DA-rfuFy.js +1 -0
  9. package/dist/frontend/assets/angular-ts-BrjP3tb8.js +1 -0
  10. package/dist/frontend/assets/apache-Pmp26Uib.js +1 -0
  11. package/dist/frontend/assets/apex-D8_7TLub.js +1 -0
  12. package/dist/frontend/assets/apl-CORt7UWP.js +1 -0
  13. package/dist/frontend/assets/applescript-Co6uUVPk.js +1 -0
  14. package/dist/frontend/assets/ara-BRHolxvo.js +1 -0
  15. package/dist/frontend/assets/asciidoc-Ve4PFQV2.js +1 -0
  16. package/dist/frontend/assets/asm-D_Q5rh1f.js +1 -0
  17. package/dist/frontend/assets/astro-HNnZUWAn.js +1 -0
  18. package/dist/frontend/assets/aurora-x-D-2ljcwZ.js +1 -0
  19. package/dist/frontend/assets/awk-DMzUqQB5.js +1 -0
  20. package/dist/frontend/assets/ayu-dark-DYE7WIF3.js +1 -0
  21. package/dist/frontend/assets/ayu-light-BA47KaF1.js +1 -0
  22. package/dist/frontend/assets/ayu-mirage-32ctXXKs.js +1 -0
  23. package/dist/frontend/assets/ballerina-BFfxhgS-.js +1 -0
  24. package/dist/frontend/assets/bat-BkioyH1T.js +1 -0
  25. package/dist/frontend/assets/beancount-k_qm7-4y.js +1 -0
  26. package/dist/frontend/assets/berry-uYugtg8r.js +1 -0
  27. package/dist/frontend/assets/bibtex-CHM0blh-.js +1 -0
  28. package/dist/frontend/assets/bicep-Bmn6On1c.js +1 -0
  29. package/dist/frontend/assets/bird2-BIv1doCn.js +1 -0
  30. package/dist/frontend/assets/blade-BjGOyj-B.js +1 -0
  31. package/dist/frontend/assets/bsl-BO_Y6i37.js +1 -0
  32. package/dist/frontend/assets/c-BIGW1oBm.js +1 -0
  33. package/dist/frontend/assets/c3-eo99z4R2.js +1 -0
  34. package/dist/frontend/assets/cadence-Bv_4Rxtq.js +1 -0
  35. package/dist/frontend/assets/cairo-KRGpt6FW.js +1 -0
  36. package/dist/frontend/assets/catppuccin-frappe-DFWUc33u.js +1 -0
  37. package/dist/frontend/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
  38. package/dist/frontend/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
  39. package/dist/frontend/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
  40. package/dist/frontend/assets/clarity-D53aC0YG.js +1 -0
  41. package/dist/frontend/assets/clojure-P80f7IUj.js +1 -0
  42. package/dist/frontend/assets/cmake-D1j8_8rp.js +1 -0
  43. package/dist/frontend/assets/cobol-nBiQ_Alo.js +1 -0
  44. package/dist/frontend/assets/codeowners-Bp6g37R7.js +1 -0
  45. package/dist/frontend/assets/codeql-DsOJ9woJ.js +1 -0
  46. package/dist/frontend/assets/coffee-Ch7k5sss.js +1 -0
  47. package/dist/frontend/assets/common-lisp-Cg-RD9OK.js +1 -0
  48. package/dist/frontend/assets/coq-DkFqJrB1.js +1 -0
  49. package/dist/frontend/assets/cpp-CofmeUqb.js +1 -0
  50. package/dist/frontend/assets/crystal-DNxU26gB.js +1 -0
  51. package/dist/frontend/assets/csharp-COcwbKMJ.js +1 -0
  52. package/dist/frontend/assets/css-CLj8gQPS.js +1 -0
  53. package/dist/frontend/assets/csv-fuZLfV_i.js +1 -0
  54. package/dist/frontend/assets/cue-D82EKSYY.js +1 -0
  55. package/dist/frontend/assets/cypher-COkxafJQ.js +1 -0
  56. package/dist/frontend/assets/d-85-TOEBH.js +1 -0
  57. package/dist/frontend/assets/dark-plus-C3mMm8J8.js +1 -0
  58. package/dist/frontend/assets/dart-bE4Kk8sk.js +1 -0
  59. package/dist/frontend/assets/dax-CEL-wOlO.js +1 -0
  60. package/dist/frontend/assets/desktop-BmXAJ9_W.js +1 -0
  61. package/dist/frontend/assets/diff-D97Zzqfu.js +1 -0
  62. package/dist/frontend/assets/docker-BcOcwvcX.js +1 -0
  63. package/dist/frontend/assets/dotenv-Da5cRb03.js +1 -0
  64. package/dist/frontend/assets/dracula-BzJJZx-M.js +1 -0
  65. package/dist/frontend/assets/dracula-soft-BXkSAIEj.js +1 -0
  66. package/dist/frontend/assets/dream-maker-BtqSS_iP.js +1 -0
  67. package/dist/frontend/assets/edge-FbVlp4U3.js +1 -0
  68. package/dist/frontend/assets/elixir-CkH2-t6x.js +1 -0
  69. package/dist/frontend/assets/elm-DbKCFpqz.js +1 -0
  70. package/dist/frontend/assets/emacs-lisp-CXvaQtF9.js +1 -0
  71. package/dist/frontend/assets/erb-BYCe7drp.js +1 -0
  72. package/dist/frontend/assets/erlang-DsQrWhSR.js +1 -0
  73. package/dist/frontend/assets/everforest-dark-BgDCqdQA.js +1 -0
  74. package/dist/frontend/assets/everforest-light-C8M2exoo.js +1 -0
  75. package/dist/frontend/assets/fennel-BYunw83y.js +1 -0
  76. package/dist/frontend/assets/fish-BvzEVeQv.js +1 -0
  77. package/dist/frontend/assets/fluent-C4IJs8-o.js +1 -0
  78. package/dist/frontend/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  79. package/dist/frontend/assets/fortran-free-form-BxgE0vQu.js +1 -0
  80. package/dist/frontend/assets/fsharp-CXgrBDvD.js +1 -0
  81. package/dist/frontend/assets/gdresource-BOOCDP_w.js +1 -0
  82. package/dist/frontend/assets/gdscript-C5YyOfLZ.js +1 -0
  83. package/dist/frontend/assets/gdshader-DkwncUOv.js +1 -0
  84. package/dist/frontend/assets/genie-D0YGMca9.js +1 -0
  85. package/dist/frontend/assets/gherkin-DyxjwDmM.js +1 -0
  86. package/dist/frontend/assets/git-commit-F4YmCXRG.js +1 -0
  87. package/dist/frontend/assets/git-rebase-r7XF79zn.js +1 -0
  88. package/dist/frontend/assets/github-dark-DHJKELXO.js +1 -0
  89. package/dist/frontend/assets/github-dark-default-Cuk6v7N8.js +1 -0
  90. package/dist/frontend/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  91. package/dist/frontend/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  92. package/dist/frontend/assets/github-light-DAi9KRSo.js +1 -0
  93. package/dist/frontend/assets/github-light-default-D7oLnXFd.js +1 -0
  94. package/dist/frontend/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  95. package/dist/frontend/assets/gleam-BspZqrRM.js +1 -0
  96. package/dist/frontend/assets/glimmer-js-ByusRIyA.js +1 -0
  97. package/dist/frontend/assets/glimmer-ts-BfAWNZQY.js +1 -0
  98. package/dist/frontend/assets/glsl-DplSGwfg.js +1 -0
  99. package/dist/frontend/assets/gn-n2N0HUVH.js +1 -0
  100. package/dist/frontend/assets/gnuplot-DdkO51Og.js +1 -0
  101. package/dist/frontend/assets/go-C27-OAKa.js +1 -0
  102. package/dist/frontend/assets/graphql-ChdNCCLP.js +1 -0
  103. package/dist/frontend/assets/groovy-gcz8RCvz.js +1 -0
  104. package/dist/frontend/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  105. package/dist/frontend/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  106. package/dist/frontend/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  107. package/dist/frontend/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  108. package/dist/frontend/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  109. package/dist/frontend/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  110. package/dist/frontend/assets/hack-i7_Ulhet.js +1 -0
  111. package/dist/frontend/assets/haml-D5jkg6IW.js +1 -0
  112. package/dist/frontend/assets/handlebars-BpdQsYii.js +1 -0
  113. package/dist/frontend/assets/haskell-Df6bDoY_.js +1 -0
  114. package/dist/frontend/assets/haxe-CzTSHFRz.js +1 -0
  115. package/dist/frontend/assets/hcl-BWvSN4gD.js +1 -0
  116. package/dist/frontend/assets/hjson-D5-asLiD.js +1 -0
  117. package/dist/frontend/assets/hlsl-D3lLCCz7.js +1 -0
  118. package/dist/frontend/assets/horizon-BUw7H-hv.js +1 -0
  119. package/dist/frontend/assets/horizon-bright-CUuTKBJd.js +1 -0
  120. package/dist/frontend/assets/houston-DnULxvSX.js +1 -0
  121. package/dist/frontend/assets/html-derivative-DlHx6ybY.js +1 -0
  122. package/dist/frontend/assets/html-pp8916En.js +1 -0
  123. package/dist/frontend/assets/http-jrhK8wxY.js +1 -0
  124. package/dist/frontend/assets/hurl-irOxFIW8.js +1 -0
  125. package/dist/frontend/assets/hxml-Bvhsp5Yf.js +1 -0
  126. package/dist/frontend/assets/hy-DFXneXwc.js +1 -0
  127. package/dist/frontend/assets/imba-DGztddWO.js +1 -0
  128. package/dist/frontend/assets/ini-BEwlwnbL.js +1 -0
  129. package/dist/frontend/assets/java-CylS5w8V.js +1 -0
  130. package/dist/frontend/assets/javascript-wDzz0qaB.js +1 -0
  131. package/dist/frontend/assets/jinja-f2NsQr07.js +1 -0
  132. package/dist/frontend/assets/jison-wvAkD_A8.js +1 -0
  133. package/dist/frontend/assets/json-Cp-IABpG.js +1 -0
  134. package/dist/frontend/assets/json5-C9tS-k6U.js +1 -0
  135. package/dist/frontend/assets/jsonc-Des-eS-w.js +1 -0
  136. package/dist/frontend/assets/jsonl-DcaNXYhu.js +1 -0
  137. package/dist/frontend/assets/jsonnet-DFQXde-d.js +1 -0
  138. package/dist/frontend/assets/jssm-C2t-YnRu.js +1 -0
  139. package/dist/frontend/assets/jsx-g9-lgVsj.js +1 -0
  140. package/dist/frontend/assets/julia-CxzCAyBv.js +1 -0
  141. package/dist/frontend/assets/just-VxiPbLrw.js +1 -0
  142. package/dist/frontend/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  143. package/dist/frontend/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  144. package/dist/frontend/assets/kanagawa-wave-DWedfzmr.js +1 -0
  145. package/dist/frontend/assets/kdl-DV7GczEv.js +1 -0
  146. package/dist/frontend/assets/kotlin-BdnUsdx6.js +1 -0
  147. package/dist/frontend/assets/kusto-wEQ09or8.js +1 -0
  148. package/dist/frontend/assets/laserwave-DUszq2jm.js +1 -0
  149. package/dist/frontend/assets/latex-CWtU0Tv5.js +1 -0
  150. package/dist/frontend/assets/lean-BZvkOJ9d.js +1 -0
  151. package/dist/frontend/assets/less-B1dDrJ26.js +1 -0
  152. package/dist/frontend/assets/light-plus-B7mTdjB0.js +1 -0
  153. package/dist/frontend/assets/liquid-C0sCDyMI.js +1 -0
  154. package/dist/frontend/assets/llvm-DjAJT7YJ.js +1 -0
  155. package/dist/frontend/assets/log-2UxHyX5q.js +1 -0
  156. package/dist/frontend/assets/logo-BtOb2qkB.js +1 -0
  157. package/dist/frontend/assets/lua-BaeVxFsk.js +1 -0
  158. package/dist/frontend/assets/luau-C-HG3fhB.js +1 -0
  159. package/dist/frontend/assets/main-CL5_Wlhv.css +32 -0
  160. package/dist/frontend/assets/main-Czet4Z1x.js +371 -0
  161. package/dist/frontend/assets/make-CHLpvVh8.js +1 -0
  162. package/dist/frontend/assets/markdown-Cvjx9yec.js +1 -0
  163. package/dist/frontend/assets/marko-DjSrsDqO.js +1 -0
  164. package/dist/frontend/assets/material-theme-D5KoaKCx.js +1 -0
  165. package/dist/frontend/assets/material-theme-darker-BfHTSMKl.js +1 -0
  166. package/dist/frontend/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  167. package/dist/frontend/assets/material-theme-ocean-CyktbL80.js +1 -0
  168. package/dist/frontend/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  169. package/dist/frontend/assets/matlab-D7o27uSR.js +1 -0
  170. package/dist/frontend/assets/mdc-DTYItulj.js +1 -0
  171. package/dist/frontend/assets/mdx-Cmh6b_Ma.js +1 -0
  172. package/dist/frontend/assets/mermaid-mWjccvbQ.js +1 -0
  173. package/dist/frontend/assets/min-dark-CafNBF8u.js +1 -0
  174. package/dist/frontend/assets/min-light-CTRr51gU.js +1 -0
  175. package/dist/frontend/assets/mipsasm-CKIfxQSi.js +1 -0
  176. package/dist/frontend/assets/mojo-rZm6bMo-.js +1 -0
  177. package/dist/frontend/assets/monokai-D4h5O-jR.js +1 -0
  178. package/dist/frontend/assets/moonbit-_H4v1dQx.js +1 -0
  179. package/dist/frontend/assets/move-IF9eRakj.js +1 -0
  180. package/dist/frontend/assets/narrat-DRg8JJMk.js +1 -0
  181. package/dist/frontend/assets/nextflow-C-mBbutL.js +1 -0
  182. package/dist/frontend/assets/nextflow-groovy-vE_lwT2v.js +1 -0
  183. package/dist/frontend/assets/nginx-BpAMiNFr.js +1 -0
  184. package/dist/frontend/assets/night-owl-C39BiMTA.js +1 -0
  185. package/dist/frontend/assets/night-owl-light-CMTm3GFP.js +1 -0
  186. package/dist/frontend/assets/nim-BIad80T-.js +1 -0
  187. package/dist/frontend/assets/nix-CwoSXNpI.js +1 -0
  188. package/dist/frontend/assets/nord-Ddv68eIx.js +1 -0
  189. package/dist/frontend/assets/nushell-Cz2AlsmD.js +1 -0
  190. package/dist/frontend/assets/objective-c-DXmwc3jG.js +1 -0
  191. package/dist/frontend/assets/objective-cpp-CLxacb5B.js +1 -0
  192. package/dist/frontend/assets/ocaml-C0hk2d4L.js +1 -0
  193. package/dist/frontend/assets/odin-BBf5iR-q.js +1 -0
  194. package/dist/frontend/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  195. package/dist/frontend/assets/one-light-C3Wv6jpd.js +1 -0
  196. package/dist/frontend/assets/openscad-C4EeE6gA.js +1 -0
  197. package/dist/frontend/assets/pascal-D93ZcfNL.js +1 -0
  198. package/dist/frontend/assets/perl-NvoQZIq0.js +1 -0
  199. package/dist/frontend/assets/php-R6g_5hLQ.js +1 -0
  200. package/dist/frontend/assets/pkl-u5AG7uiY.js +1 -0
  201. package/dist/frontend/assets/plastic-3e1v2bzS.js +1 -0
  202. package/dist/frontend/assets/plsql-ChMvpjG-.js +1 -0
  203. package/dist/frontend/assets/po-BTJTHyun.js +1 -0
  204. package/dist/frontend/assets/poimandres-CS3Unz2-.js +1 -0
  205. package/dist/frontend/assets/polar-C0HS_06l.js +1 -0
  206. package/dist/frontend/assets/postcss-CXtECtnM.js +1 -0
  207. package/dist/frontend/assets/powerquery-CEu0bR-o.js +1 -0
  208. package/dist/frontend/assets/powershell-Dpen1YoG.js +1 -0
  209. package/dist/frontend/assets/prisma-Dd19v3D-.js +1 -0
  210. package/dist/frontend/assets/prolog-CbFg5uaA.js +1 -0
  211. package/dist/frontend/assets/proto-C7zT0LnQ.js +1 -0
  212. package/dist/frontend/assets/pug-DKIMFp6K.js +1 -0
  213. package/dist/frontend/assets/puppet-BMWR74SV.js +1 -0
  214. package/dist/frontend/assets/purescript-CklMAg4u.js +1 -0
  215. package/dist/frontend/assets/python-B6aJPvgy.js +1 -0
  216. package/dist/frontend/assets/qml-3beO22l8.js +1 -0
  217. package/dist/frontend/assets/qmldir-C8lEn-DE.js +1 -0
  218. package/dist/frontend/assets/qss-IeuSbFQv.js +1 -0
  219. package/dist/frontend/assets/r-Dspwwk_N.js +1 -0
  220. package/dist/frontend/assets/racket-BqYA7rlc.js +1 -0
  221. package/dist/frontend/assets/raku-DXvB9xmW.js +1 -0
  222. package/dist/frontend/assets/razor-BDqjjVU7.js +1 -0
  223. package/dist/frontend/assets/red-bN70gL4F.js +1 -0
  224. package/dist/frontend/assets/reg-C-SQnVFl.js +1 -0
  225. package/dist/frontend/assets/regexp-CDVJQ6XC.js +1 -0
  226. package/dist/frontend/assets/rel-C3B-1QV4.js +1 -0
  227. package/dist/frontend/assets/riscv-BM1_JUlF.js +1 -0
  228. package/dist/frontend/assets/ron-D8l8udqQ.js +1 -0
  229. package/dist/frontend/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
  230. package/dist/frontend/assets/rose-pine-moon-D4_iv3hh.js +1 -0
  231. package/dist/frontend/assets/rose-pine-qdsjHGoJ.js +1 -0
  232. package/dist/frontend/assets/rosmsg-BJDFO7_C.js +1 -0
  233. package/dist/frontend/assets/rst-CRjBmOyv.js +1 -0
  234. package/dist/frontend/assets/ruby-Wjq7vjNf.js +1 -0
  235. package/dist/frontend/assets/rust-B1yitclQ.js +1 -0
  236. package/dist/frontend/assets/sas-cz2c8ADy.js +1 -0
  237. package/dist/frontend/assets/sass-Cj5Yp3dK.js +1 -0
  238. package/dist/frontend/assets/scala-C151Ov-r.js +1 -0
  239. package/dist/frontend/assets/scheme-C98Dy4si.js +1 -0
  240. package/dist/frontend/assets/scss-D5BDwBP9.js +1 -0
  241. package/dist/frontend/assets/sdbl-DVxCFoDh.js +1 -0
  242. package/dist/frontend/assets/shaderlab-Dg9Lc6iA.js +1 -0
  243. package/dist/frontend/assets/shellscript-Yzrsuije.js +1 -0
  244. package/dist/frontend/assets/shellsession-BADoaaVG.js +1 -0
  245. package/dist/frontend/assets/slack-dark-BthQWCQV.js +1 -0
  246. package/dist/frontend/assets/slack-ochin-DqwNpetd.js +1 -0
  247. package/dist/frontend/assets/smalltalk-BERRCDM3.js +1 -0
  248. package/dist/frontend/assets/snazzy-light-Bw305WKR.js +1 -0
  249. package/dist/frontend/assets/solarized-dark-DXbdFlpD.js +1 -0
  250. package/dist/frontend/assets/solarized-light-L9t79GZl.js +1 -0
  251. package/dist/frontend/assets/solidity-rGO070M0.js +1 -0
  252. package/dist/frontend/assets/soy-8wufbnw4.js +1 -0
  253. package/dist/frontend/assets/sparql-rVzFXLq3.js +1 -0
  254. package/dist/frontend/assets/splunk-BtCnVYZw.js +1 -0
  255. package/dist/frontend/assets/sql-BLtJtn59.js +1 -0
  256. package/dist/frontend/assets/ssh-config-_ykCGR6B.js +1 -0
  257. package/dist/frontend/assets/stata-BH5u7GGu.js +1 -0
  258. package/dist/frontend/assets/stylus-BEDo0Tqx.js +1 -0
  259. package/dist/frontend/assets/surrealql-Bq5Q-fJD.js +1 -0
  260. package/dist/frontend/assets/svelte-Cy7k_4gC.js +1 -0
  261. package/dist/frontend/assets/swift-D82vCrfD.js +1 -0
  262. package/dist/frontend/assets/synthwave-84-CbfX1IO0.js +1 -0
  263. package/dist/frontend/assets/system-verilog-CnnmHF94.js +1 -0
  264. package/dist/frontend/assets/systemd-4A_iFExJ.js +1 -0
  265. package/dist/frontend/assets/talonscript-CkByrt1z.js +1 -0
  266. package/dist/frontend/assets/tasl-QIJgUcNo.js +1 -0
  267. package/dist/frontend/assets/tcl-dwOrl1Do.js +1 -0
  268. package/dist/frontend/assets/templ-DhtptRzy.js +1 -0
  269. package/dist/frontend/assets/terraform-BETggiCN.js +1 -0
  270. package/dist/frontend/assets/tex-idrVyKtj.js +1 -0
  271. package/dist/frontend/assets/tokyo-night-hegEt444.js +1 -0
  272. package/dist/frontend/assets/toml-vGWfd6FD.js +1 -0
  273. package/dist/frontend/assets/ts-tags-DQrlYJgV.js +1 -0
  274. package/dist/frontend/assets/tsv-B_m7g4N7.js +1 -0
  275. package/dist/frontend/assets/tsx-COt5Ahok.js +1 -0
  276. package/dist/frontend/assets/turtle-BsS91CYL.js +1 -0
  277. package/dist/frontend/assets/twig-xg9kU7Mw.js +1 -0
  278. package/dist/frontend/assets/typescript-BPQ3VLAy.js +1 -0
  279. package/dist/frontend/assets/typespec-CAFt9gP4.js +1 -0
  280. package/dist/frontend/assets/typst-DHCkPAjA.js +1 -0
  281. package/dist/frontend/assets/v-BcVCzyr7.js +1 -0
  282. package/dist/frontend/assets/vala-CsfeWuGM.js +1 -0
  283. package/dist/frontend/assets/vb-D17OF-Vu.js +1 -0
  284. package/dist/frontend/assets/verilog-BQ8w6xss.js +1 -0
  285. package/dist/frontend/assets/vesper-DU1UobuO.js +1 -0
  286. package/dist/frontend/assets/vhdl-CeAyd5Ju.js +1 -0
  287. package/dist/frontend/assets/viml-CJc9bBzg.js +1 -0
  288. package/dist/frontend/assets/vitesse-black-Bkuqu6BP.js +1 -0
  289. package/dist/frontend/assets/vitesse-dark-D0r3Knsf.js +1 -0
  290. package/dist/frontend/assets/vitesse-light-CVO1_9PV.js +1 -0
  291. package/dist/frontend/assets/vue-D2xRrEX4.js +1 -0
  292. package/dist/frontend/assets/vue-html-AaS7Mt5G.js +1 -0
  293. package/dist/frontend/assets/vue-vine-BoDAl6tE.js +1 -0
  294. package/dist/frontend/assets/vyper-CDx5xZoG.js +1 -0
  295. package/dist/frontend/assets/wasm-CG6Dc4jp.js +1 -0
  296. package/dist/frontend/assets/wasm-MzD3tlZU.js +1 -0
  297. package/dist/frontend/assets/wenyan-BV7otONQ.js +1 -0
  298. package/dist/frontend/assets/wgsl-Dx-B1_4e.js +1 -0
  299. package/dist/frontend/assets/wikitext-BhOHFoWU.js +1 -0
  300. package/dist/frontend/assets/wit-5i3qLPDT.js +1 -0
  301. package/dist/frontend/assets/wolfram-lXgVvXCa.js +1 -0
  302. package/dist/frontend/assets/xml-sdJ4AIDG.js +1 -0
  303. package/dist/frontend/assets/xsl-CtQFsRM5.js +1 -0
  304. package/dist/frontend/assets/yaml-Buea-lGh.js +1 -0
  305. package/dist/frontend/assets/zenscript-DVFEvuxE.js +1 -0
  306. package/dist/frontend/assets/zig-VOosw3JB.js +1 -0
  307. package/dist/frontend/icon-192.png +0 -0
  308. package/dist/frontend/icon-512.png +0 -0
  309. package/dist/frontend/icon.svg +8 -0
  310. package/dist/frontend/index.html +30 -0
  311. package/dist/frontend/manifest.json +25 -0
  312. package/dist/frontend/sw.js +66 -0
  313. package/dist/server/agent-events.js +39 -0
  314. package/dist/server/analytics.js +885 -0
  315. package/dist/server/auth.js +65 -0
  316. package/dist/server/belayer/executor.js +200 -0
  317. package/dist/server/belayer/intake.js +27 -0
  318. package/dist/server/belayer/pipeline.js +97 -0
  319. package/dist/server/belayer/pr-lifecycle.js +69 -0
  320. package/dist/server/belayer/prompts.js +154 -0
  321. package/dist/server/belayer/types.js +23 -0
  322. package/dist/server/branch-linker.js +137 -0
  323. package/dist/server/browser-content.js +145 -0
  324. package/dist/server/clipboard.js +63 -0
  325. package/dist/server/codex-hooks-adapter.js +93 -0
  326. package/dist/server/config.js +325 -0
  327. package/dist/server/gh-routes.js +163 -0
  328. package/dist/server/gh.js +276 -0
  329. package/dist/server/git-routes.js +154 -0
  330. package/dist/server/git.js +694 -0
  331. package/dist/server/github-app.js +218 -0
  332. package/dist/server/github-graphql.js +178 -0
  333. package/dist/server/hooks.js +373 -0
  334. package/dist/server/index.js +1549 -0
  335. package/dist/server/integration-github.js +137 -0
  336. package/dist/server/integration-jira.js +210 -0
  337. package/dist/server/integration-linear.js +176 -0
  338. package/dist/server/logger.js +18 -0
  339. package/dist/server/mobile-input-pipeline.js +129 -0
  340. package/dist/server/opencode-relay.js +53 -0
  341. package/dist/server/org-dashboard.js +241 -0
  342. package/dist/server/output-parsers/claude-parser.js +56 -0
  343. package/dist/server/output-parsers/codex-parser.js +13 -0
  344. package/dist/server/output-parsers/index.js +14 -0
  345. package/dist/server/output-parsers/null-parser.js +12 -0
  346. package/dist/server/output-parsers/opencode-parser.js +77 -0
  347. package/dist/server/pty-handler.js +586 -0
  348. package/dist/server/push.js +84 -0
  349. package/dist/server/review-poller.js +237 -0
  350. package/dist/server/sdk-handler.js +539 -0
  351. package/dist/server/service.js +189 -0
  352. package/dist/server/sessions.js +638 -0
  353. package/dist/server/telemetry.js +236 -0
  354. package/dist/server/ticket-transitions.js +166 -0
  355. package/dist/server/types.js +146 -0
  356. package/dist/server/utils.js +23 -0
  357. package/dist/server/watcher.js +661 -0
  358. package/dist/server/webhook-manager.js +547 -0
  359. package/dist/server/webhooks.js +73 -0
  360. package/dist/server/workspace-groups.js +363 -0
  361. package/dist/server/workspaces.js +1207 -0
  362. package/dist/server/ws.js +192 -0
  363. package/dist/test/EmptyState.spec.js +51 -0
  364. package/dist/test/action-coverage.test.js +139 -0
  365. package/dist/test/actions/registry.test.js +59 -0
  366. package/dist/test/actions/shortcuts.test.js +79 -0
  367. package/dist/test/agent-events.test.js +151 -0
  368. package/dist/test/analytics.test.js +158 -0
  369. package/dist/test/attention.test.js +91 -0
  370. package/dist/test/auth.test.js +105 -0
  371. package/dist/test/backend-state.test.js +47 -0
  372. package/dist/test/belayer-executor.test.js +33 -0
  373. package/dist/test/belayer-intake.test.js +44 -0
  374. package/dist/test/belayer-pipeline.test.js +113 -0
  375. package/dist/test/belayer-pr-lifecycle.test.js +26 -0
  376. package/dist/test/belayer-prompts.test.js +60 -0
  377. package/dist/test/belayer-types.test.js +69 -0
  378. package/dist/test/bin/claude-remote-cli.js +214 -0
  379. package/dist/test/boot-state.test.js +133 -0
  380. package/dist/test/branch-lifecycle.test.js +75 -0
  381. package/dist/test/branch-linker.test.js +236 -0
  382. package/dist/test/branch-rename.test.js +45 -0
  383. package/dist/test/branch-watcher.test.js +115 -0
  384. package/dist/test/browser-cli.test.js +91 -0
  385. package/dist/test/browser-content.test.js +93 -0
  386. package/dist/test/browser-tabs-ui.test.js +39 -0
  387. package/dist/test/changed-files-api.test.js +140 -0
  388. package/dist/test/clipboard.test.js +12 -0
  389. package/dist/test/codex-hooks-adapter.test.js +237 -0
  390. package/dist/test/components/EmptyState.spec.js +51 -0
  391. package/dist/test/components/ErrorToast.spec.js +65 -0
  392. package/dist/test/components/TuiCheckbox.spec.js +120 -0
  393. package/dist/test/components/TuiInput.spec.js +186 -0
  394. package/dist/test/components/leaf-component-migration.spec.js +104 -0
  395. package/dist/test/config-freshness.test.js +63 -0
  396. package/dist/test/config.test.js +813 -0
  397. package/dist/test/diff-summary.test.js +98 -0
  398. package/dist/test/display-state.test.js +179 -0
  399. package/dist/test/event-message-types.test.js +32 -0
  400. package/dist/test/file-tree-utils.test.js +167 -0
  401. package/dist/test/framework-types.test.js +183 -0
  402. package/dist/test/frameworks-api.test.js +93 -0
  403. package/dist/test/frontend/src/lib/pr-state.js +114 -0
  404. package/dist/test/fs-browse.test.js +246 -0
  405. package/dist/test/fuzzy-scorer.test.js +145 -0
  406. package/dist/test/gh-routes.test.js +156 -0
  407. package/dist/test/git-changed-files.test.js +152 -0
  408. package/dist/test/git-routes.test.js +146 -0
  409. package/dist/test/git-utils.test.js +68 -0
  410. package/dist/test/git-watcher.test.js +110 -0
  411. package/dist/test/git.test.js +140 -0
  412. package/dist/test/github-app.test.js +455 -0
  413. package/dist/test/github-graphql.test.js +301 -0
  414. package/dist/test/greetings.test.js +83 -0
  415. package/dist/test/hooks-agent-event.test.js +412 -0
  416. package/dist/test/hooks.test.js +149 -0
  417. package/dist/test/integration-github.test.js +220 -0
  418. package/dist/test/integration-jira.test.js +238 -0
  419. package/dist/test/integration-linear.test.js +293 -0
  420. package/dist/test/mobile-input.test.js +235 -0
  421. package/dist/test/opencode-relay.test.js +107 -0
  422. package/dist/test/org-dashboard.test.js +349 -0
  423. package/dist/test/output-parser.test.js +217 -0
  424. package/dist/test/paths.test.js +32 -0
  425. package/dist/test/pr-state.test.js +407 -0
  426. package/dist/test/pr-status.test.js +82 -0
  427. package/dist/test/presets.test.js +242 -0
  428. package/dist/test/pty-handler-multi-agent.test.js +149 -0
  429. package/dist/test/pty-handler.test.js +146 -0
  430. package/dist/test/pull-requests.test.js +78 -0
  431. package/dist/test/review-poller.test.js +349 -0
  432. package/dist/test/server/analytics.js +121 -0
  433. package/dist/test/server/auth.js +63 -0
  434. package/dist/test/server/branch-linker.js +124 -0
  435. package/dist/test/server/clipboard.js +56 -0
  436. package/dist/test/server/config.js +137 -0
  437. package/dist/test/server/git.js +308 -0
  438. package/dist/test/server/hooks.js +196 -0
  439. package/dist/test/server/index.js +1124 -0
  440. package/dist/test/server/integration-github.js +117 -0
  441. package/dist/test/server/integration-jira.js +164 -0
  442. package/dist/test/server/integration-linear.js +176 -0
  443. package/dist/test/server/mobile-input-pipeline.js +123 -0
  444. package/dist/test/server/org-dashboard.js +184 -0
  445. package/dist/test/server/output-parsers/claude-parser.js +54 -0
  446. package/dist/test/server/output-parsers/codex-parser.js +13 -0
  447. package/dist/test/server/output-parsers/index.js +7 -0
  448. package/dist/test/server/pty-handler.js +310 -0
  449. package/dist/test/server/push.js +80 -0
  450. package/dist/test/server/review-poller.js +218 -0
  451. package/dist/test/server/service.js +169 -0
  452. package/dist/test/server/sessions.js +434 -0
  453. package/dist/test/server/ticket-transitions.js +216 -0
  454. package/dist/test/server/types.js +20 -0
  455. package/dist/test/server/utils.js +22 -0
  456. package/dist/test/server/watcher.js +139 -0
  457. package/dist/test/server/workspaces.js +657 -0
  458. package/dist/test/server/ws.js +152 -0
  459. package/dist/test/server-startup.test.js +62 -0
  460. package/dist/test/service.test.js +43 -0
  461. package/dist/test/session-analytics-api.test.js +123 -0
  462. package/dist/test/session-analytics.test.js +425 -0
  463. package/dist/test/session-intent.test.js +249 -0
  464. package/dist/test/sessions.test.js +1152 -0
  465. package/dist/test/sidebar-items.test.js +164 -0
  466. package/dist/test/stores/boot-state-store.test.js +165 -0
  467. package/dist/test/stores/sessions-logic.test.js +191 -0
  468. package/dist/test/stores/toasts-store.test.js +66 -0
  469. package/dist/test/stores/ui-store.test.js +203 -0
  470. package/dist/test/stores/unread-store.test.js +97 -0
  471. package/dist/test/telemetry-api.test.js +54 -0
  472. package/dist/test/telemetry-sync.test.js +68 -0
  473. package/dist/test/telemetry.test.js +295 -0
  474. package/dist/test/terminal-zoom.test.js +102 -0
  475. package/dist/test/test/analytics.test.js +152 -0
  476. package/dist/test/test/auth.test.js +95 -0
  477. package/dist/test/test/branch-linker.test.js +231 -0
  478. package/dist/test/test/branch-rename.test.js +45 -0
  479. package/dist/test/test/clipboard.test.js +12 -0
  480. package/dist/test/test/config.test.js +281 -0
  481. package/dist/test/test/fs-browse.test.js +202 -0
  482. package/dist/test/test/git.test.js +67 -0
  483. package/dist/test/test/hooks.test.js +139 -0
  484. package/dist/test/test/integration-github.test.js +203 -0
  485. package/dist/test/test/integration-jira.test.js +294 -0
  486. package/dist/test/test/integration-linear.test.js +293 -0
  487. package/dist/test/test/mobile-input.test.js +193 -0
  488. package/dist/test/test/org-dashboard.test.js +240 -0
  489. package/dist/test/test/output-parser.test.js +95 -0
  490. package/dist/test/test/paths.test.js +32 -0
  491. package/dist/test/test/pr-state.test.js +220 -0
  492. package/dist/test/test/pull-requests.test.js +67 -0
  493. package/dist/test/test/review-poller.test.js +235 -0
  494. package/dist/test/test/service.test.js +43 -0
  495. package/dist/test/test/sessions.test.js +750 -0
  496. package/dist/test/test/ticket-transitions.test.js +130 -0
  497. package/dist/test/test/version.test.js +34 -0
  498. package/dist/test/test/worktrees.test.js +256 -0
  499. package/dist/test/ticket-transitions.test.js +312 -0
  500. package/dist/test/unread.test.js +23 -0
  501. package/dist/test/version.test.js +34 -0
  502. package/dist/test/webhook-manager.test.js +484 -0
  503. package/dist/test/webhooks.test.js +208 -0
  504. package/dist/test/workspace-groups.test.js +377 -0
  505. package/dist/test/worktrees.test.js +531 -0
  506. 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
+ });