@thaut/claude-ide 0.0.0-stage → 1.0.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 (559) hide show
  1. package/bin/claude-ide.js +23 -0
  2. package/package.json +75 -4
  3. package/server/add-dirs.ts +126 -0
  4. package/server/auth.test.ts +148 -0
  5. package/server/auth.ts +235 -0
  6. package/server/bang.ts +54 -0
  7. package/server/btw-instructions.ts +19 -0
  8. package/server/btw-policy.ts +17 -0
  9. package/server/btw.ts +143 -0
  10. package/server/child-env.ts +29 -0
  11. package/server/cli-control.ts +255 -0
  12. package/server/cli-files.ts +55 -0
  13. package/server/cli-info.ts +73 -0
  14. package/server/config/account.ts +65 -0
  15. package/server/config/agents.ts +186 -0
  16. package/server/config/mcp.ts +86 -0
  17. package/server/config/memory.ts +193 -0
  18. package/server/config/paths.ts +156 -0
  19. package/server/config/plugins.ts +204 -0
  20. package/server/config/routes.ts +95 -0
  21. package/server/config/settings.ts +246 -0
  22. package/server/config.ts +46 -0
  23. package/server/db.ts +121 -0
  24. package/server/directories.test.ts +180 -0
  25. package/server/directories.ts +87 -0
  26. package/server/dist-version.ts +69 -0
  27. package/server/durable-tasks.ts +61 -0
  28. package/server/file-transfers.ts +79 -0
  29. package/server/files.test.ts +127 -0
  30. package/server/files.ts +386 -0
  31. package/server/fs-queue.ts +14 -0
  32. package/server/fsutil.ts +144 -0
  33. package/server/git.ts +86 -0
  34. package/server/goal-state.test.ts +64 -0
  35. package/server/goal-state.ts +78 -0
  36. package/server/history-pages.test.ts +619 -0
  37. package/server/history-pages.ts +513 -0
  38. package/server/http-channel-manager.ts +468 -0
  39. package/server/http-channel-pull.test.ts +314 -0
  40. package/server/http-channel-push.test.ts +364 -0
  41. package/server/http-channel-registration.test.ts +291 -0
  42. package/server/http-channel-routes.ts +73 -0
  43. package/server/http-channel-store.ts +142 -0
  44. package/server/http-channel-test-fixture.ts +103 -0
  45. package/server/ide-messages-mcp.test.ts +82 -0
  46. package/server/ide-messages-mcp.ts +47 -0
  47. package/server/idecron-mcp.ts +65 -0
  48. package/server/idecron.test.ts +79 -0
  49. package/server/idecron.ts +271 -0
  50. package/server/index.ts +336 -0
  51. package/server/live-session.test.ts +1126 -0
  52. package/server/live-session.ts +1165 -0
  53. package/server/project-routes.ts +46 -0
  54. package/server/projects.test.ts +28 -0
  55. package/server/push.ts +315 -0
  56. package/server/report-cache.ts +28 -0
  57. package/server/session-aliases.test.ts +57 -0
  58. package/server/session-aliases.ts +50 -0
  59. package/server/session-branch.ts +71 -0
  60. package/server/session-dirs.ts +189 -0
  61. package/server/session-file.test.ts +134 -0
  62. package/server/session-file.ts +113 -0
  63. package/server/session-messaging.test.ts +297 -0
  64. package/server/session-messaging.ts +165 -0
  65. package/server/sessions.ts +638 -0
  66. package/server/subagents.test.ts +43 -0
  67. package/server/subagents.ts +58 -0
  68. package/server/tool-images.test.ts +76 -0
  69. package/server/tool-images.ts +45 -0
  70. package/server/turn-changes.ts +308 -0
  71. package/server/worktree-git.ts +87 -0
  72. package/server/worktrees.ts +138 -0
  73. package/server/ws.test.ts +195 -0
  74. package/server/ws.ts +197 -0
  75. package/shared/agent-model.ts +15 -0
  76. package/shared/agent-tasks.ts +12 -0
  77. package/shared/config.ts +175 -0
  78. package/shared/directories.ts +12 -0
  79. package/shared/files.ts +51 -0
  80. package/shared/http-channels.ts +63 -0
  81. package/shared/message-window.ts +189 -0
  82. package/shared/project.ts +77 -0
  83. package/shared/protocol.ts +203 -0
  84. package/shared/task-types.ts +4 -0
  85. package/shared/todos.ts +40 -0
  86. package/shared/write-policy.ts +9 -0
  87. package/web/dist/assets/AgentView-wrTKgcJ3.js +1 -0
  88. package/web/dist/assets/AnnotationSelection-BZmNmwVW.js +7 -0
  89. package/web/dist/assets/ChangesView-G1WTzh4q.js +2 -0
  90. package/web/dist/assets/FilesView-DPgAbYt_.js +6 -0
  91. package/web/dist/assets/Form-BKTwiYyw.js +1 -0
  92. package/web/dist/assets/HomePage-Dq2apjBe.js +1 -0
  93. package/web/dist/assets/Markdown-IwP-xF29.js +67 -0
  94. package/web/dist/assets/MessageList-DD9GypSy.js +15 -0
  95. package/web/dist/assets/Modal-g2rv8I_n.js +1 -0
  96. package/web/dist/assets/NotificationSettings-DpuLO_Bv.js +1 -0
  97. package/web/dist/assets/OverviewPage-Dx6miabE.js +1 -0
  98. package/web/dist/assets/ProjectHome-CousyTLL.js +1 -0
  99. package/web/dist/assets/SessionView-CPVL7_HO.js +2 -0
  100. package/web/dist/assets/SettingsPage-D_oYSnW2.js +7 -0
  101. package/web/dist/assets/WorktreesView-C7e0HV__.js +1 -0
  102. package/web/dist/assets/abap-CLvhMVsD.js +1 -0
  103. package/web/dist/assets/actionscript-3-Cs6JJXT5.js +1 -0
  104. package/web/dist/assets/ada-X3Y2mlq2.js +1 -0
  105. package/web/dist/assets/ahk-DwADkxyd.js +1 -0
  106. package/web/dist/assets/ahk2-_6rzW9BS.js +1 -0
  107. package/web/dist/assets/andromeeda-vGVdxbeo.js +1 -0
  108. package/web/dist/assets/angular-html-Cw29lun8.js +1 -0
  109. package/web/dist/assets/angular-ts-BXz-O3SJ.js +1 -0
  110. package/web/dist/assets/apache-U0d_L8uA.js +1 -0
  111. package/web/dist/assets/apex-KFGFyKwG.js +1 -0
  112. package/web/dist/assets/apl-xBchdRhy.js +1 -0
  113. package/web/dist/assets/applescript-CCn79oCD.js +1 -0
  114. package/web/dist/assets/ara-4CJ0cIlV.js +1 -0
  115. package/web/dist/assets/asciidoc-SCjQUq34.js +1 -0
  116. package/web/dist/assets/asm-Cmm7eHzH.js +1 -0
  117. package/web/dist/assets/astro-1q6DGuN3.js +1 -0
  118. package/web/dist/assets/aurora-x-CDeNXAV0.js +1 -0
  119. package/web/dist/assets/awk-BWXHIvNe.js +1 -0
  120. package/web/dist/assets/ayu-dark-DluEY0Gj.js +1 -0
  121. package/web/dist/assets/ayu-light-C3h-C4tm.js +1 -0
  122. package/web/dist/assets/ayu-mirage-Bqwy1Gya.js +1 -0
  123. package/web/dist/assets/ballerina-B7ZEbQpA.js +1 -0
  124. package/web/dist/assets/bat-qdxAdBWQ.js +1 -0
  125. package/web/dist/assets/beancount-C2IN4Vk8.js +1 -0
  126. package/web/dist/assets/berry-DKpUyyne.js +1 -0
  127. package/web/dist/assets/bibtex-Ci_nEsc7.js +1 -0
  128. package/web/dist/assets/bicep-CUHmPFLl.js +1 -0
  129. package/web/dist/assets/bird2-CYIUgYqB.js +1 -0
  130. package/web/dist/assets/blade-Cqdv4oZN.js +1 -0
  131. package/web/dist/assets/bsl-CVED6lJ1.js +1 -0
  132. package/web/dist/assets/c-D9gIxSfx.js +1 -0
  133. package/web/dist/assets/c3-D0apG41V.js +1 -0
  134. package/web/dist/assets/cadence-CQ2zXKGN.js +1 -0
  135. package/web/dist/assets/cairo-DRAZz75c.js +1 -0
  136. package/web/dist/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
  137. package/web/dist/assets/catppuccin-latte-CtsyrtPq.js +1 -0
  138. package/web/dist/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
  139. package/web/dist/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
  140. package/web/dist/assets/chapel-CuseGurG.js +1 -0
  141. package/web/dist/assets/clarity-CtfBBspg.js +1 -0
  142. package/web/dist/assets/clojure-DqKBuwfJ.js +1 -0
  143. package/web/dist/assets/cmake-Bj61d0ZC.js +1 -0
  144. package/web/dist/assets/cobol-Bdpj3Qoh.js +1 -0
  145. package/web/dist/assets/codeowners-C8r90Shi.js +1 -0
  146. package/web/dist/assets/codeql-oeQT6MSM.js +1 -0
  147. package/web/dist/assets/coffee-Sk8pHFjs.js +1 -0
  148. package/web/dist/assets/common-lisp-Cv5bFMCO.js +1 -0
  149. package/web/dist/assets/coq-CGaSfhwR.js +1 -0
  150. package/web/dist/assets/core-BxI4V4-T.js +12 -0
  151. package/web/dist/assets/cpp-BQfBYKx3.js +1 -0
  152. package/web/dist/assets/crystal-B3j7DoOZ.js +1 -0
  153. package/web/dist/assets/csharp-dmMxLVLW.js +1 -0
  154. package/web/dist/assets/css-DpFbYmRh.js +1 -0
  155. package/web/dist/assets/csv-Dx-8-gkx.js +1 -0
  156. package/web/dist/assets/cue-CE9AQfxI.js +1 -0
  157. package/web/dist/assets/cypher-ClKdZ_lG.js +1 -0
  158. package/web/dist/assets/d-qD-0Kul2.js +1 -0
  159. package/web/dist/assets/dark-plus-Cs2F2srj.js +1 -0
  160. package/web/dist/assets/dart-CHZY9Xhz.js +1 -0
  161. package/web/dist/assets/dax-BkyTk9wS.js +1 -0
  162. package/web/dist/assets/desktop-Dlh5hvp9.js +1 -0
  163. package/web/dist/assets/diff-woXpYk--.js +1 -0
  164. package/web/dist/assets/docker-IyjqRm3v.js +1 -0
  165. package/web/dist/assets/dotenv-CuH5my32.js +1 -0
  166. package/web/dist/assets/dracula-BHWKrbxM.js +1 -0
  167. package/web/dist/assets/dracula-soft-5eyTD99u.js +1 -0
  168. package/web/dist/assets/dream-maker-DW3nJb8Q.js +1 -0
  169. package/web/dist/assets/edge-DXQWmXTB.js +1 -0
  170. package/web/dist/assets/elixir-CrDIY2Vo.js +1 -0
  171. package/web/dist/assets/elm-BslFEg8K.js +1 -0
  172. package/web/dist/assets/emacs-lisp-DR2vtWaj.js +1 -0
  173. package/web/dist/assets/engine-javascript-DJg3-Vu2.js +141 -0
  174. package/web/dist/assets/erb-CUYhg0Ws.js +1 -0
  175. package/web/dist/assets/erlang-BXdgHVfi.js +1 -0
  176. package/web/dist/assets/everforest-dark-sB-x3p7T.js +1 -0
  177. package/web/dist/assets/everforest-light-Df2xbC6M.js +1 -0
  178. package/web/dist/assets/fennel-DQxkIbk2.js +1 -0
  179. package/web/dist/assets/fish-BJitypiv.js +1 -0
  180. package/web/dist/assets/fluent-C03EYrpw.js +1 -0
  181. package/web/dist/assets/format-DrBC7w0A.js +1 -0
  182. package/web/dist/assets/fortran-fixed-form-D6mN0kjy.js +1 -0
  183. package/web/dist/assets/fortran-free-form-CYNrtFtB.js +1 -0
  184. package/web/dist/assets/fsharp-CwoEF39o.js +1 -0
  185. package/web/dist/assets/gdresource-CZkFGo3B.js +1 -0
  186. package/web/dist/assets/gdscript-DYvRD3hg.js +1 -0
  187. package/web/dist/assets/gdshader-CBce3t8t.js +1 -0
  188. package/web/dist/assets/genie-CV2tkWYe.js +1 -0
  189. package/web/dist/assets/gherkin-DExj1W_8.js +1 -0
  190. package/web/dist/assets/git-commit-dUGrvm6Q.js +1 -0
  191. package/web/dist/assets/git-rebase-4zCRqF28.js +1 -0
  192. package/web/dist/assets/github-dark-C-LZuMrd.js +1 -0
  193. package/web/dist/assets/github-dark-default-DXG-b-1a.js +1 -0
  194. package/web/dist/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
  195. package/web/dist/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
  196. package/web/dist/assets/github-light-EUqPIrTm.js +1 -0
  197. package/web/dist/assets/github-light-default-BXViO-2h.js +1 -0
  198. package/web/dist/assets/github-light-high-contrast-B68TUdTA.js +1 -0
  199. package/web/dist/assets/gleam-CSRkHgEL.js +1 -0
  200. package/web/dist/assets/glimmer-js-7adTRvH4.js +1 -0
  201. package/web/dist/assets/glimmer-ts-IdSskDYw.js +1 -0
  202. package/web/dist/assets/glsl-Qzy53KDt.js +1 -0
  203. package/web/dist/assets/gn-CAXYAgV-.js +1 -0
  204. package/web/dist/assets/gnuplot-7GGW24-e.js +1 -0
  205. package/web/dist/assets/go-rLFTqkRN.js +1 -0
  206. package/web/dist/assets/graphql-CV_puxbB.js +1 -0
  207. package/web/dist/assets/groovy-CacY0gHj.js +1 -0
  208. package/web/dist/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
  209. package/web/dist/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
  210. package/web/dist/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
  211. package/web/dist/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
  212. package/web/dist/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
  213. package/web/dist/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
  214. package/web/dist/assets/hack-C5GPjiLV.js +1 -0
  215. package/web/dist/assets/haml-BQ5YR1DQ.js +1 -0
  216. package/web/dist/assets/handlebars-DnhRK9A1.js +1 -0
  217. package/web/dist/assets/haskell-D8IpX4py.js +1 -0
  218. package/web/dist/assets/haxe-C8oCnwJJ.js +1 -0
  219. package/web/dist/assets/hcl-Dh228itO.js +1 -0
  220. package/web/dist/assets/highlight.worker-CY-ZeDAl.js +1 -0
  221. package/web/dist/assets/hjson-CxZEssPk.js +1 -0
  222. package/web/dist/assets/hlsl-Cvrh5tZx.js +1 -0
  223. package/web/dist/assets/horizon-CE9ld1lL.js +1 -0
  224. package/web/dist/assets/horizon-bright-DSNQnXHK.js +1 -0
  225. package/web/dist/assets/houston-CsvMBhTu.js +1 -0
  226. package/web/dist/assets/html-CmHiEKV9.js +1 -0
  227. package/web/dist/assets/html-derivative-BXE-rYIK.js +1 -0
  228. package/web/dist/assets/http-DPjjqtcL.js +1 -0
  229. package/web/dist/assets/hurl-AMBpSloO.js +1 -0
  230. package/web/dist/assets/hxml-D7Y24wKF.js +1 -0
  231. package/web/dist/assets/hy-CZbG8q4J.js +1 -0
  232. package/web/dist/assets/imba-DsUTQ-LC.js +1 -0
  233. package/web/dist/assets/index-DNPnl4sh.css +2 -0
  234. package/web/dist/assets/index-DQh72lEx.js +14 -0
  235. package/web/dist/assets/ini-B5eOa1yu.js +1 -0
  236. package/web/dist/assets/java-CEM7Tdw_.js +1 -0
  237. package/web/dist/assets/javascript-CU3PLowd.js +1 -0
  238. package/web/dist/assets/jinja-fR0x_9Xy.js +1 -0
  239. package/web/dist/assets/jison-BoKsxs5M.js +1 -0
  240. package/web/dist/assets/json-BM2FWsRB.js +1 -0
  241. package/web/dist/assets/json5-BR5RXkoi.js +1 -0
  242. package/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
  243. package/web/dist/assets/jsonl-CmCQp5Yx.js +1 -0
  244. package/web/dist/assets/jsonnet-CJTPZ8u_.js +1 -0
  245. package/web/dist/assets/jssm-DXw9l8Rf.js +1 -0
  246. package/web/dist/assets/jsx-B5fTecQP.js +1 -0
  247. package/web/dist/assets/julia-DGyDG39_.js +1 -0
  248. package/web/dist/assets/just-B-RyLcSR.js +1 -0
  249. package/web/dist/assets/kanagawa-dragon-Bozd7klA.js +1 -0
  250. package/web/dist/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
  251. package/web/dist/assets/kanagawa-wave-BML5mF2J.js +1 -0
  252. package/web/dist/assets/kdl-CsD5j6eV.js +1 -0
  253. package/web/dist/assets/kotlin-DhhofPvG.js +1 -0
  254. package/web/dist/assets/kusto-C7mF5XQf.js +1 -0
  255. package/web/dist/assets/langs-CciPKqaF.js +1 -0
  256. package/web/dist/assets/laserwave-C_8bwKvT.js +1 -0
  257. package/web/dist/assets/latex-CV1pQelm.js +1 -0
  258. package/web/dist/assets/lean-CewbzKMR.js +1 -0
  259. package/web/dist/assets/less-DVTAwKKz.js +1 -0
  260. package/web/dist/assets/lib-DlcVMUqT.js +1 -0
  261. package/web/dist/assets/light-plus-DVQuIRkW.js +1 -0
  262. package/web/dist/assets/liquid-9p05RSzQ.js +1 -0
  263. package/web/dist/assets/llvm-DqHOMS-s.js +1 -0
  264. package/web/dist/assets/log-BNLmms1o.js +1 -0
  265. package/web/dist/assets/logo-Cluzi2Zq.js +1 -0
  266. package/web/dist/assets/lua-CIPVh0S-.js +1 -0
  267. package/web/dist/assets/luau-VGWVLoqX.js +1 -0
  268. package/web/dist/assets/make-Dixweg8N.js +1 -0
  269. package/web/dist/assets/markdown-BYOwaDjH.js +1 -0
  270. package/web/dist/assets/marko-h2h6YUMS.js +1 -0
  271. package/web/dist/assets/material-theme-Bm3Qr25_.js +1 -0
  272. package/web/dist/assets/material-theme-darker-2IIEA8gg.js +1 -0
  273. package/web/dist/assets/material-theme-lighter-uhdI0v04.js +1 -0
  274. package/web/dist/assets/material-theme-ocean-CHQ94UKr.js +1 -0
  275. package/web/dist/assets/material-theme-palenight-B5W6OYN7.js +1 -0
  276. package/web/dist/assets/matlab-D7qyCx1q.js +1 -0
  277. package/web/dist/assets/mdc-yAyZMdoC.js +1 -0
  278. package/web/dist/assets/mdx-DQZ5AkYe.js +1 -0
  279. package/web/dist/assets/mermaid-BxpfTIzt.js +1 -0
  280. package/web/dist/assets/message-font-size-DoaZfwkT.js +1 -0
  281. package/web/dist/assets/min-dark-BSWPekZh.js +1 -0
  282. package/web/dist/assets/min-light-DDpmG2fV.js +1 -0
  283. package/web/dist/assets/mipsasm-BMqwQI7S.js +1 -0
  284. package/web/dist/assets/mojo-fg40_ntQ.js +1 -0
  285. package/web/dist/assets/monokai-BscAjiQZ.js +1 -0
  286. package/web/dist/assets/moonbit-D9W784E_.js +1 -0
  287. package/web/dist/assets/move-BYnGhZoM.js +1 -0
  288. package/web/dist/assets/narrat-_X_XdTYD.js +1 -0
  289. package/web/dist/assets/nextflow-BoTnSBQ_.js +1 -0
  290. package/web/dist/assets/nextflow-groovy-D7IRt6Pp.js +1 -0
  291. package/web/dist/assets/nginx-jWINucNT.js +1 -0
  292. package/web/dist/assets/night-owl-DhmEMT88.js +1 -0
  293. package/web/dist/assets/night-owl-light-eJ-hLW7d.js +1 -0
  294. package/web/dist/assets/nim-Dtt9DZ0t.js +1 -0
  295. package/web/dist/assets/nix-Cl_7024t.js +1 -0
  296. package/web/dist/assets/nord-Cb4Vim4T.js +1 -0
  297. package/web/dist/assets/nsis-CjNLYCLg.js +1 -0
  298. package/web/dist/assets/nushell--0fT3PI1.js +1 -0
  299. package/web/dist/assets/objective-c-D1A_Heim.js +1 -0
  300. package/web/dist/assets/objective-cpp-BsSzOQcm.js +1 -0
  301. package/web/dist/assets/ocaml-O90oeIOV.js +1 -0
  302. package/web/dist/assets/odin-B1RWQWA5.js +1 -0
  303. package/web/dist/assets/one-dark-pro-C6YyR8OD.js +1 -0
  304. package/web/dist/assets/one-light-D7Lr4KcI.js +1 -0
  305. package/web/dist/assets/openscad-BUDT5pXO.js +1 -0
  306. package/web/dist/assets/org-ClD5huOa.js +1 -0
  307. package/web/dist/assets/pascal-4ZHwLPI5.js +1 -0
  308. package/web/dist/assets/perl-BFegxyeJ.js +1 -0
  309. package/web/dist/assets/php-B-qOA3Y4.js +1 -0
  310. package/web/dist/assets/pkl-ot-7Btpt.js +1 -0
  311. package/web/dist/assets/plastic-DQwYfKfQ.js +1 -0
  312. package/web/dist/assets/plsql-DGHpHOYJ.js +1 -0
  313. package/web/dist/assets/po-BiJDBrnU.js +1 -0
  314. package/web/dist/assets/poimandres-DRFjx7u4.js +1 -0
  315. package/web/dist/assets/polar-C7UOKdEL.js +1 -0
  316. package/web/dist/assets/postcss-BXeXVLqQ.js +1 -0
  317. package/web/dist/assets/powerquery-DNMTfnFr.js +1 -0
  318. package/web/dist/assets/powershell-QtSnmXcu.js +1 -0
  319. package/web/dist/assets/prisma-DMgNhnYS.js +1 -0
  320. package/web/dist/assets/prolog-iXnhIJG7.js +1 -0
  321. package/web/dist/assets/proto-DB4EqR-F.js +1 -0
  322. package/web/dist/assets/pug-Cq0NBPu-.js +1 -0
  323. package/web/dist/assets/puppet-CDv2pdJW.js +1 -0
  324. package/web/dist/assets/purescript-9MfHhQsQ.js +1 -0
  325. package/web/dist/assets/python-gzcpVVnB.js +1 -0
  326. package/web/dist/assets/qml-CYUsa87k.js +1 -0
  327. package/web/dist/assets/qmldir-DCQb3MpD.js +1 -0
  328. package/web/dist/assets/qss-Fe1Jh2GI.js +1 -0
  329. package/web/dist/assets/r-4xJhgbOf.js +1 -0
  330. package/web/dist/assets/racket-DcIDlBhZ.js +1 -0
  331. package/web/dist/assets/raku-B3gFvitq.js +1 -0
  332. package/web/dist/assets/razor-BlHaOHx8.js +1 -0
  333. package/web/dist/assets/rbs-C0MpOrj4.js +1 -0
  334. package/web/dist/assets/react-B4UGpcWC.js +1 -0
  335. package/web/dist/assets/red-CYlAq2b0.js +1 -0
  336. package/web/dist/assets/reg-CRGYupPL.js +1 -0
  337. package/web/dist/assets/regexp-BUSxjv2o.js +1 -0
  338. package/web/dist/assets/rel-BtDbiS_P.js +1 -0
  339. package/web/dist/assets/riscv-Ckw8ddFX.js +1 -0
  340. package/web/dist/assets/ron-VUp2lXgN.js +1 -0
  341. package/web/dist/assets/rose-pine-aMsaMD1D.js +1 -0
  342. package/web/dist/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
  343. package/web/dist/assets/rose-pine-moon-DO-qXg-I.js +1 -0
  344. package/web/dist/assets/rosmsg-CAekHB0j.js +1 -0
  345. package/web/dist/assets/rst-zjfguEh7.js +1 -0
  346. package/web/dist/assets/ruby-4oR9waBK.js +1 -0
  347. package/web/dist/assets/rust-DpiIf5JO.js +1 -0
  348. package/web/dist/assets/sas-Do4V_2i9.js +1 -0
  349. package/web/dist/assets/sass-DXrisJhu.js +1 -0
  350. package/web/dist/assets/scala-BiFt_7mu.js +1 -0
  351. package/web/dist/assets/scheme-DQCgrYNe.js +1 -0
  352. package/web/dist/assets/scss-CW6JPtag.js +1 -0
  353. package/web/dist/assets/sdbl-bTVj8UrX.js +1 -0
  354. package/web/dist/assets/shaderlab-C4rnI7x0.js +1 -0
  355. package/web/dist/assets/shellscript-zBmOMFJh.js +1 -0
  356. package/web/dist/assets/shellsession-DxGUTWaf.js +1 -0
  357. package/web/dist/assets/slack-dark-DnToyrRv.js +1 -0
  358. package/web/dist/assets/slack-ochin-B2OO5cIa.js +1 -0
  359. package/web/dist/assets/smalltalk-BHpQb5nM.js +1 -0
  360. package/web/dist/assets/smithy-Dgaz7aur.js +1 -0
  361. package/web/dist/assets/snazzy-light-4G7pJPwS.js +1 -0
  362. package/web/dist/assets/solarized-dark-b97czJOk.js +1 -0
  363. package/web/dist/assets/solarized-light-DFPxpIYQ.js +1 -0
  364. package/web/dist/assets/solidity-Bj7E1mRn.js +1 -0
  365. package/web/dist/assets/soy-DVtw20Yn.js +1 -0
  366. package/web/dist/assets/sparql-C0ia5K60.js +1 -0
  367. package/web/dist/assets/splunk-BC2Px7Mm.js +1 -0
  368. package/web/dist/assets/sql-DGCM-jag.js +1 -0
  369. package/web/dist/assets/ssh-config-BgfXC-Er.js +1 -0
  370. package/web/dist/assets/stata-DfuMOXna.js +1 -0
  371. package/web/dist/assets/stylus-B6D30XZt.js +1 -0
  372. package/web/dist/assets/surrealql-CBoRWxfR.js +1 -0
  373. package/web/dist/assets/svelte-g_uCUem5.js +1 -0
  374. package/web/dist/assets/swift-CyEgAFGc.js +1 -0
  375. package/web/dist/assets/synthwave-84-nFMaYfgc.js +1 -0
  376. package/web/dist/assets/system-verilog-Dh5ihWbf.js +1 -0
  377. package/web/dist/assets/systemd-BxMlprV5.js +1 -0
  378. package/web/dist/assets/talonscript-CohzipZa.js +1 -0
  379. package/web/dist/assets/tasl-DMoTqEGO.js +1 -0
  380. package/web/dist/assets/tcl-CZd0xW_V.js +1 -0
  381. package/web/dist/assets/templ-N5C1hJ4Y.js +1 -0
  382. package/web/dist/assets/terraform-DswuEJGm.js +1 -0
  383. package/web/dist/assets/tex-mx6xDwuK.js +1 -0
  384. package/web/dist/assets/themes-CNElGaXw.js +1 -0
  385. package/web/dist/assets/tokyo-night-oM2G3aXe.js +1 -0
  386. package/web/dist/assets/toml-CcmNWLt0.js +1 -0
  387. package/web/dist/assets/ts-tags-C-j71FRb.js +1 -0
  388. package/web/dist/assets/tsv-sltzmVWM.js +1 -0
  389. package/web/dist/assets/tsx-z04y7qR9.js +1 -0
  390. package/web/dist/assets/turtle-ByJddavk.js +1 -0
  391. package/web/dist/assets/twig-Bz57G_nl.js +1 -0
  392. package/web/dist/assets/typescript-RMqVTmzR.js +1 -0
  393. package/web/dist/assets/typespec-BGhR2SqG.js +1 -0
  394. package/web/dist/assets/typst-BgkqA2Pg.js +1 -0
  395. package/web/dist/assets/v-DBj4K9d_.js +1 -0
  396. package/web/dist/assets/vala-zf12oZj6.js +1 -0
  397. package/web/dist/assets/vb-DOwzaHZ0.js +1 -0
  398. package/web/dist/assets/verilog-B2gwz-sJ.js +1 -0
  399. package/web/dist/assets/vesper-DdrHHSXu.js +1 -0
  400. package/web/dist/assets/vhdl-BroJfC0k.js +1 -0
  401. package/web/dist/assets/viml-DvXPmvsu.js +1 -0
  402. package/web/dist/assets/vitesse-black-fwtXNY1n.js +1 -0
  403. package/web/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
  404. package/web/dist/assets/vitesse-light-VbXTXTou.js +1 -0
  405. package/web/dist/assets/vue-CQgvQytb.js +1 -0
  406. package/web/dist/assets/vue-html-Dhl6fh9l.js +1 -0
  407. package/web/dist/assets/vue-vine-Dw_uU59K.js +1 -0
  408. package/web/dist/assets/vyper-CgoNMtux.js +1 -0
  409. package/web/dist/assets/wasm-ByWQv1Qj.js +1 -0
  410. package/web/dist/assets/wenyan-C8pVoKbM.js +1 -0
  411. package/web/dist/assets/wgsl-BsKzXJz4.js +1 -0
  412. package/web/dist/assets/wikitext-ClFFjSW2.js +1 -0
  413. package/web/dist/assets/wit-DdvCle-K.js +1 -0
  414. package/web/dist/assets/wolfram-DLL8P-h_.js +1 -0
  415. package/web/dist/assets/xml-B49klYQ3.js +1 -0
  416. package/web/dist/assets/xsl-D7tnD3qq.js +1 -0
  417. package/web/dist/assets/yaml-DNQ5_xkL.js +1 -0
  418. package/web/dist/assets/zenscript-BnlCZFoB.js +1 -0
  419. package/web/dist/assets/zig-CMLA9XwU.js +1 -0
  420. package/web/dist/icons/apple-touch-icon.png +0 -0
  421. package/web/dist/icons/badge-96.png +0 -0
  422. package/web/dist/icons/icon-192.png +0 -0
  423. package/web/dist/icons/icon-512.png +0 -0
  424. package/web/dist/icons/icon.svg +4 -0
  425. package/web/dist/icons/maskable-192.png +0 -0
  426. package/web/dist/icons/maskable-512.png +0 -0
  427. package/web/dist/index.html +30 -0
  428. package/web/dist/manifest.webmanifest +20 -0
  429. package/web/dist/sw.js +176 -0
  430. package/web/dist/version.json +1 -0
  431. package/web/src/App.tsx +97 -0
  432. package/web/src/agent-history-flow.test.ts +191 -0
  433. package/web/src/agent-history-flow.ts +58 -0
  434. package/web/src/agent-owners.ts +30 -0
  435. package/web/src/annotation-selection.test.ts +36 -0
  436. package/web/src/annotation-selection.ts +123 -0
  437. package/web/src/annotations.test.ts +279 -0
  438. package/web/src/annotations.ts +191 -0
  439. package/web/src/api.ts +27 -0
  440. package/web/src/auth-events.ts +11 -0
  441. package/web/src/auth-lifecycle.test.ts +105 -0
  442. package/web/src/clipboard.ts +32 -0
  443. package/web/src/components/ActivityNotifier.tsx +22 -0
  444. package/web/src/components/AnnotationEditor.tsx +135 -0
  445. package/web/src/components/AnnotationSelection.tsx +122 -0
  446. package/web/src/components/Automation.tsx +390 -0
  447. package/web/src/components/CodeView.tsx +121 -0
  448. package/web/src/components/Composer.tsx +552 -0
  449. package/web/src/components/ComposerAnnotations.tsx +45 -0
  450. package/web/src/components/Confirmation.tsx +91 -0
  451. package/web/src/components/CopyButton.tsx +23 -0
  452. package/web/src/components/DiffView.tsx +94 -0
  453. package/web/src/components/DirectoryPicker.tsx +230 -0
  454. package/web/src/components/FileCopyMenu.tsx +58 -0
  455. package/web/src/components/FileEntryActions.tsx +95 -0
  456. package/web/src/components/ImagePreview.tsx +68 -0
  457. package/web/src/components/Items.tsx +608 -0
  458. package/web/src/components/Markdown.tsx +73 -0
  459. package/web/src/components/MessageList.tsx +458 -0
  460. package/web/src/components/Modal.tsx +72 -0
  461. package/web/src/components/NotificationEntry.tsx +31 -0
  462. package/web/src/components/NotificationSettings.tsx +226 -0
  463. package/web/src/components/NotificationToasts.tsx +59 -0
  464. package/web/src/components/OpenTabs.tsx +112 -0
  465. package/web/src/components/Pending.tsx +175 -0
  466. package/web/src/components/ProjectExtras.tsx +299 -0
  467. package/web/src/components/ProjectNav.tsx +47 -0
  468. package/web/src/components/SessionActions.tsx +271 -0
  469. package/web/src/components/SessionChannels.tsx +288 -0
  470. package/web/src/components/SessionExtras.tsx +263 -0
  471. package/web/src/components/SettingsLink.tsx +21 -0
  472. package/web/src/components/Shell.tsx +320 -0
  473. package/web/src/components/SvgPreview.tsx +18 -0
  474. package/web/src/components/TodoPanel.tsx +37 -0
  475. package/web/src/components/VersionNotice.tsx +40 -0
  476. package/web/src/components/icons.tsx +43 -0
  477. package/web/src/diff.ts +62 -0
  478. package/web/src/drafts.test.ts +80 -0
  479. package/web/src/drafts.ts +64 -0
  480. package/web/src/format.ts +5 -0
  481. package/web/src/highlight.worker.ts +49 -0
  482. package/web/src/hooks.ts +59 -0
  483. package/web/src/item-view-state.test.ts +51 -0
  484. package/web/src/item-view-state.ts +43 -0
  485. package/web/src/live-history-flow.test.ts +77 -0
  486. package/web/src/live-merge.test.ts +64 -0
  487. package/web/src/live-merge.ts +50 -0
  488. package/web/src/live-message-hydration.test.ts +253 -0
  489. package/web/src/live.ts +404 -0
  490. package/web/src/main.tsx +27 -0
  491. package/web/src/markdown-assets.test.ts +70 -0
  492. package/web/src/markdown-assets.ts +78 -0
  493. package/web/src/markdown-html.test.ts +84 -0
  494. package/web/src/markdown-html.ts +60 -0
  495. package/web/src/markdown-svg.test.ts +37 -0
  496. package/web/src/markdown-svg.ts +42 -0
  497. package/web/src/markdown-tables.test.ts +34 -0
  498. package/web/src/markdown-tables.ts +17 -0
  499. package/web/src/message-bodies.ts +117 -0
  500. package/web/src/message-cache.test.ts +208 -0
  501. package/web/src/message-cache.ts +158 -0
  502. package/web/src/message-font-size.test.ts +41 -0
  503. package/web/src/message-font-size.ts +57 -0
  504. package/web/src/message-time.test.ts +32 -0
  505. package/web/src/message-time.ts +26 -0
  506. package/web/src/notify.ts +132 -0
  507. package/web/src/open-tabs.test.ts +69 -0
  508. package/web/src/open-tabs.ts +110 -0
  509. package/web/src/pages/AgentView.tsx +138 -0
  510. package/web/src/pages/ChangesView.tsx +193 -0
  511. package/web/src/pages/FilesView.tsx +406 -0
  512. package/web/src/pages/HomePage.tsx +96 -0
  513. package/web/src/pages/LoginPage.tsx +56 -0
  514. package/web/src/pages/OverviewPage.tsx +124 -0
  515. package/web/src/pages/ProjectHome.tsx +136 -0
  516. package/web/src/pages/SessionView.tsx +238 -0
  517. package/web/src/pages/WorktreesView.tsx +197 -0
  518. package/web/src/pages/settings/AccountPage.tsx +149 -0
  519. package/web/src/pages/settings/AgentsPage.tsx +182 -0
  520. package/web/src/pages/settings/ConfigPage.tsx +181 -0
  521. package/web/src/pages/settings/GeneralPage.tsx +27 -0
  522. package/web/src/pages/settings/HooksPage.tsx +244 -0
  523. package/web/src/pages/settings/McpPage.tsx +310 -0
  524. package/web/src/pages/settings/MemoryPage.tsx +309 -0
  525. package/web/src/pages/settings/PermissionsPage.tsx +160 -0
  526. package/web/src/pages/settings/PluginsPage.tsx +251 -0
  527. package/web/src/pages/settings/SettingsPage.tsx +141 -0
  528. package/web/src/pages/settings/SkillsPage.tsx +142 -0
  529. package/web/src/pages/settings/ui.tsx +165 -0
  530. package/web/src/preference-document.ts +133 -0
  531. package/web/src/project-actions.ts +18 -0
  532. package/web/src/project-files-state.test.ts +49 -0
  533. package/web/src/project-files-state.ts +64 -0
  534. package/web/src/project-images.ts +13 -0
  535. package/web/src/pwa.ts +33 -0
  536. package/web/src/queryClient.ts +5 -0
  537. package/web/src/reasoning-preference.test.ts +39 -0
  538. package/web/src/reasoning-preference.ts +42 -0
  539. package/web/src/session-confirmations.ts +36 -0
  540. package/web/src/stream-replay.test.ts +163 -0
  541. package/web/src/styles.css +231 -0
  542. package/web/src/tool-result-body.test.ts +115 -0
  543. package/web/src/transcript.test.ts +186 -0
  544. package/web/src/transcript.ts +583 -0
  545. package/web/src/turns.test.ts +338 -0
  546. package/web/src/turns.ts +178 -0
  547. package/web/src/ui/Button.tsx +60 -0
  548. package/web/src/ui/Controls.tsx +227 -0
  549. package/web/src/ui/Form.tsx +203 -0
  550. package/web/src/ui/MenuPopover.tsx +144 -0
  551. package/web/src/ui/Popover.tsx +147 -0
  552. package/web/src/ui/ResizablePane.tsx +110 -0
  553. package/web/src/ui/Select.tsx +150 -0
  554. package/web/src/ui/icons.tsx +270 -0
  555. package/web/src/unsaved.tsx +99 -0
  556. package/web/src/user-text.test.ts +60 -0
  557. package/web/src/user-text.ts +35 -0
  558. package/web/src/uuid.ts +12 -0
  559. package/README.md +0 -3
@@ -0,0 +1,138 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { listSessions } from "@anthropic-ai/claude-agent-sdk";
4
+ import type { WorktreeInfo, WorktreeList, WorktreeSession } from "../shared/project.ts";
5
+ import { db } from "./db.ts";
6
+ import { setSessionCwd } from "./session-dirs.ts";
7
+ import { HttpError, createDraftSession, sessions } from "./sessions.ts";
8
+ import { git, projectWorktrees, repoOf, worktreesDir } from "./worktree-git.ts";
9
+
10
+ /**
11
+ * git worktrees as sub-workspaces of a project (decided design, see NOTES.md "worktree"):
12
+ * the server runs `git worktree add` itself in the CLI's layout and starts sessions with the
13
+ * worktree as their cwd. Sessions in a worktree are listed with the project, tagged with the
14
+ * worktree's name, so one project keeps one session list.
15
+ */
16
+
17
+ const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
18
+
19
+ async function sessionsIn(cwd: string): Promise<WorktreeSession[]> {
20
+ const infos = await listSessions({ dir: cwd, includeWorktrees: false }).catch(() => []);
21
+ const out: WorktreeSession[] = infos.map((s) => ({ id: s.sessionId, title: s.customTitle ?? s.summary ?? "", lastModified: s.lastModified, draft: false }));
22
+ const seen = new Set(out.map((s) => s.id));
23
+ const drafts = db
24
+ .prepare("SELECT c.session_id AS id FROM session_cwd c JOIN session_view v ON v.session_id = c.session_id WHERE c.cwd = ? AND v.btw_parent IS NULL")
25
+ .all(cwd) as { id: string }[];
26
+ for (const d of drafts) if (!seen.has(d.id)) out.push({ id: d.id, title: "", lastModified: sessions.get(d.id)?.lastActivity ?? Date.now(), draft: true });
27
+ return out.sort((a, b) => b.lastModified - a.lastModified);
28
+ }
29
+
30
+ async function pathExists(p: string) {
31
+ try {
32
+ await fs.access(p);
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ export async function listWorktrees(projectPath: string): Promise<WorktreeList> {
40
+ const found = await projectWorktrees(projectPath);
41
+ if (!found) return { isRepo: false };
42
+ const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"], projectPath);
43
+ const worktrees: WorktreeInfo[] = [];
44
+ for (const w of found.worktrees) {
45
+ const exists = await pathExists(w.path);
46
+ let changes = 0;
47
+ if (exists) {
48
+ const st = await git(["status", "--porcelain", "-uall"], w.path);
49
+ changes = st.stdout.split("\n").filter(Boolean).length;
50
+ }
51
+ worktrees.push({
52
+ name: w.name,
53
+ path: w.path,
54
+ cwd: w.cwd,
55
+ branch: w.branch,
56
+ head: w.head.slice(0, 12),
57
+ dirty: changes > 0,
58
+ changes,
59
+ locked: w.locked,
60
+ exists,
61
+ sessions: exists ? await sessionsIn(w.cwd) : [],
62
+ });
63
+ }
64
+ return { isRepo: true, root: found.repo.top, branch: branch.code === 0 ? branch.stdout.trim() : null, worktrees };
65
+ }
66
+
67
+ /** Keep `.claude/worktrees/` out of the main checkout's `git status` (repo-local, no tracked file changes). */
68
+ async function excludeWorktreesDir(projectPath: string, top: string) {
69
+ const probe = await git(["check-ignore", "-q", "--no-index", "--", ".claude/worktrees/x"], top);
70
+ if (probe.code === 0) return;
71
+ const common = await git(["rev-parse", "--git-common-dir"], projectPath);
72
+ if (common.code !== 0) return;
73
+ const file = path.join(path.resolve(projectPath, common.stdout.trim()), "info", "exclude");
74
+ await fs.mkdir(path.dirname(file), { recursive: true });
75
+ const cur = (await pathExists(file)) ? await fs.readFile(file, "utf8") : "";
76
+ await fs.appendFile(file, `${cur && !cur.endsWith("\n") ? "\n" : ""}# claude-ide / claude --worktree checkouts\n/.claude/worktrees/\n`);
77
+ }
78
+
79
+ export async function createWorktree(projectPath: string, rawName: string): Promise<WorktreeInfo> {
80
+ const name = rawName.trim();
81
+ if (!NAME.test(name) || name.endsWith(".lock") || name.includes("..")) {
82
+ throw new HttpError(400, "名称只能包含字母、数字、点、下划线和连字符(最多 64 个字符)");
83
+ }
84
+ const repo = await repoOf(projectPath);
85
+ if (!repo) throw new HttpError(409, "这个项目不是 git 仓库");
86
+ if ((await git(["rev-parse", "--verify", "-q", "HEAD"], projectPath)).code !== 0) throw new HttpError(409, "仓库还没有任何提交,无法创建 worktree");
87
+ const wtPath = path.join(worktreesDir(repo.top), name);
88
+ if (await pathExists(wtPath)) throw new HttpError(409, `已经存在:${wtPath}`);
89
+ const branch = `worktree-${name}`;
90
+ const hasBranch = (await git(["rev-parse", "--verify", "-q", `refs/heads/${branch}`], projectPath)).code === 0;
91
+ await excludeWorktreesDir(projectPath, repo.top);
92
+ const r = await git(hasBranch ? ["worktree", "add", wtPath, branch] : ["worktree", "add", "-b", branch, wtPath, "HEAD"], repo.top);
93
+ if (r.code !== 0) throw new HttpError(409, r.stderr.trim() || "git worktree add 失败");
94
+ const info = (await listWorktrees(projectPath)) as Extract<WorktreeList, { isRepo: true }>;
95
+ const created = info.worktrees.find((w) => w.name === name);
96
+ if (!created) throw new HttpError(409, "worktree 已创建,但没有出现在 git worktree list 里");
97
+ sessions.notifyChanged(projectPath);
98
+ return created;
99
+ }
100
+
101
+ async function findWorktree(projectPath: string, name: string): Promise<WorktreeInfo> {
102
+ const list = await listWorktrees(projectPath);
103
+ const wt = list.isRepo ? list.worktrees.find((w) => w.name === name) : undefined;
104
+ if (!wt) throw new HttpError(404, "找不到这个 worktree");
105
+ return wt;
106
+ }
107
+
108
+ /** A new session whose cwd is the worktree. */
109
+ export async function createWorktreeSession(projectPath: string, name: string): Promise<string> {
110
+ const wt = await findWorktree(projectPath, name);
111
+ if (!wt.exists) throw new HttpError(409, "worktree 目录已不存在");
112
+ const id = createDraftSession(projectPath);
113
+ setSessionCwd(id, wt.cwd);
114
+ sessions.notifyChanged(projectPath);
115
+ return id;
116
+ }
117
+
118
+ /**
119
+ * `git worktree remove`. Uncommitted changes need `force` (the UI confirms twice). The branch is
120
+ * kept so commits made in the worktree are not lost; session transcripts stay in ~/.claude.
121
+ */
122
+ export async function removeWorktree(projectPath: string, name: string, force: boolean) {
123
+ const wt = await findWorktree(projectPath, name);
124
+ if (wt.dirty && !force) throw new HttpError(409, `worktree 里有 ${wt.changes} 个未提交的改动,确认后才能删除`);
125
+ for (const s of wt.sessions) await sessions.drop(s.id);
126
+ const repo = await repoOf(projectPath);
127
+ const cwd = repo?.top ?? projectPath;
128
+ // The CLI's --worktree leaves a lock ("claude session …") behind; the user asked to remove it.
129
+ if (wt.locked) await git(["worktree", "unlock", wt.path], cwd);
130
+ if (wt.exists) {
131
+ const r = await git(["worktree", "remove", ...(force ? ["--force"] : []), wt.path], cwd);
132
+ if (r.code !== 0) throw new HttpError(409, r.stderr.trim() || "git worktree remove 失败");
133
+ } else {
134
+ await git(["worktree", "prune"], cwd);
135
+ }
136
+ sessions.notifyChanged(projectPath);
137
+ return { ok: true, branch: wt.branch };
138
+ }
@@ -0,0 +1,195 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import type { WSContext } from "hono/ws";
7
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "cide-ws-unit-"));
8
+ process.env.CLAUDE_IDE_DATA_DIR = path.join(dir, "data");
9
+ process.env.CLAUDE_IDE_PASSWORD = "unit";
10
+ process.env.CLAUDE_CONFIG_DIR = path.join(dir, "claude");
11
+ const { openConn, closeConn, revokeConnections, handleMessage } = await import("./ws.ts");
12
+ const { db } = await import("./db.ts");
13
+ const { refreshAuth } = await import("./auth.ts");
14
+ await refreshAuth();
15
+ function socket() {
16
+ const sent: string[] = [], closed: number[] = [];
17
+ return { sent, closed, ws: { send: (message: string) => sent.push(message), close: (code: number) => closed.push(code) } as unknown as WSContext };
18
+ }
19
+
20
+ test("write preference broadcasts to every subscribed device and survives reconnect snapshots", async () => {
21
+ const { addProject, sessions, updateSession } = await import("./sessions.ts");
22
+ const project = await addProject(dir), sid = crypto.randomUUID(), token = crypto.randomUUID();
23
+ db.prepare("INSERT INTO auth_tokens VALUES (?, ?, ?)").run(token, Date.now(), Date.now());
24
+ const a = socket(), b = socket(), reconnected = socket();
25
+ const connections = [a, b, reconnected].map((client) => openConn(client.ws, token));
26
+ try {
27
+ const subscribe = (index: number) => handleMessage(connections[index]!, JSON.stringify({ t: "sub", sessionId: sid, projectId: project.id }));
28
+ await Promise.all([subscribe(0), subscribe(1)]);
29
+ await updateSession(sid, dir, { writeEnabled: false });
30
+ for (const client of [a, b]) {
31
+ const messages = client.sent.map((text) => JSON.parse(text));
32
+ assert.ok(messages.some((message) => message.t === "ev" && message.ev.k === "settings" && message.ev.writeEnabled === false));
33
+ }
34
+ await subscribe(2);
35
+ const snapshot = reconnected.sent.map((text) => JSON.parse(text)).find((message) => message.t === "snapshot");
36
+ assert.equal(snapshot.live.writeEnabled, false);
37
+ await updateSession(sid, dir, { writeEnabled: true });
38
+ for (const client of [a, b, reconnected]) {
39
+ const message = client.sent.map((text) => JSON.parse(text)).filter((message) => message.t === "ev" && message.ev.k === "settings").at(-1);
40
+ assert.equal(message.ev.writeEnabled, true);
41
+ }
42
+ } finally {
43
+ connections.forEach(closeConn);
44
+ await sessions.drop(sid);
45
+ }
46
+ });
47
+
48
+ test("revocation closes every socket for that token and ignores already queued messages", async () => {
49
+ for (const token of ["login-A", "login-B"]) db.prepare("INSERT INTO auth_tokens VALUES (?, ?, ?)").run(token, Date.now(), Date.now());
50
+ const a = socket(), a2 = socket(), b = socket();
51
+ const conn = openConn(a.ws, "login-A"), peer = openConn(a2.ws, "login-A"), other = openConn(b.ws, "login-B");
52
+ let subscriptionsRemoved = 0;
53
+ conn.subs.set("s", () => { subscriptionsRemoved++; });
54
+ db.prepare("DELETE FROM auth_tokens WHERE token=?").run("login-A");
55
+ revokeConnections("login-A");
56
+ assert.deepEqual(a.closed, [4401]); assert.deepEqual(a2.closed, [4401]);
57
+ assert.deepEqual(b.closed, []); assert.equal(other.closed, false);
58
+ assert.equal(subscriptionsRemoved, 1); assert.equal(conn.subs.size, 0);
59
+ await handleMessage(conn, JSON.stringify({ t: "ping" }));
60
+ assert.equal(a.sent.length, 0, "even a queued ping cannot be dispatched after logout");
61
+ await handleMessage(other, JSON.stringify({ t: "ping" }));
62
+ assert.deepEqual(b.sent.map((m) => JSON.parse(m)), [{ t: "pong" }]);
63
+ closeConn(conn); closeConn(peer); closeConn(other);
64
+ assert.equal(subscriptionsRemoved, 1, "close is idempotent");
65
+ });
66
+
67
+ test("handshake finishing after token revocation cannot register a new connection", () => {
68
+ const s = socket();
69
+ const conn = openConn(s.ws, "revoked");
70
+ assert.equal(conn.closed, true);
71
+ assert.deepEqual(s.closed, [4401]);
72
+ });
73
+
74
+ test("CACHE-002/004 bounded reconnect restores the active prefix and independent agent state", async () => {
75
+ const { EPOCH } = await import("./ws.ts");
76
+ const { addProject, sessions } = await import("./sessions.ts");
77
+ const { randomUUID } = await import("node:crypto");
78
+ const project = await addProject(dir);
79
+ const sid = randomUUID();
80
+ const session = await sessions.getOrCreate(sid, dir);
81
+ (session as any).handle({ type: "assistant", uuid: "call", message: { content: [{ type: "tool_use", id: "agent-tool", name: "Agent", input: {} }] } });
82
+ for (let i = 0; i < 6000; i++) (session as any).emit({ k: "delta", msgId: "stream", index: 0, kind: "text", text: `${i},`, parent: "agent-tool" });
83
+ const token = "checkpoint-login";
84
+ db.prepare("INSERT INTO auth_tokens VALUES (?, ?, ?)").run(token, Date.now(), Date.now());
85
+ const client = socket();
86
+ const conn = openConn(client.ws, token);
87
+ try {
88
+ await handleMessage(conn, JSON.stringify({ t: "sub", sessionId: sid, projectId: project.id, bounded: true, epoch: EPOCH, seq: session.seq - 1 }));
89
+ const snapshot = client.sent.map((message) => JSON.parse(message)).find((message) => message.t === "snapshot");
90
+ assert.equal(snapshot.reset, true);
91
+ assert.equal(snapshot.historyRevision, session.historyRevision);
92
+ assert.ok(snapshot.events.length <= 1500);
93
+ assert.equal(snapshot.events.find((entry: any) => entry.ev.k === "delta")?.ev.text, Array.from({ length: 6000 }, (_, i) => `${i},`).join(""));
94
+ assert.equal(snapshot.live.agentTasks.find((task: any) => task.toolUseId === "agent-tool")?.status, "running");
95
+ } finally {
96
+ closeConn(conn);
97
+ await sessions.drop(sid);
98
+ }
99
+ });
100
+
101
+
102
+ test("HTTP and WS share the view revision; actual rewind changes it without starting SDK", async () => {
103
+ const { EPOCH } = await import("./ws.ts");
104
+ const { addProject, sessions, sessionHistoryPage } = await import("./sessions.ts");
105
+ const project = await addProject(dir);
106
+ const sid = crypto.randomUUID();
107
+ const session = await sessions.getOrCreate(sid, dir);
108
+ const original = session.historyRevision;
109
+ const page = await sessionHistoryPage(sid, dir);
110
+ assert.equal(page.historyRevision, original);
111
+ assert.equal(session.isStarted, false);
112
+ const resets: any[] = [];
113
+ const unsub = session.subscribe((_seq, ev) => { if (ev.k === "reset") resets.push(ev); });
114
+ await session.rewindConversation(dir, "anchor", "draft");
115
+ assert.notEqual(session.historyRevision, original);
116
+ assert.equal(resets[0]?.historyRevision, session.historyRevision);
117
+ assert.equal((await sessionHistoryPage(sid, dir)).historyRevision, session.historyRevision);
118
+ const token = "revision-login";
119
+ db.prepare("INSERT INTO auth_tokens VALUES (?, ?, ?)").run(token, Date.now(), Date.now());
120
+ const client = socket();
121
+ const conn = openConn(client.ws, token);
122
+ try {
123
+ await handleMessage(conn, JSON.stringify({ t: "sub", sessionId: sid, projectId: project.id, bounded: true, epoch: EPOCH, seq: 0 }));
124
+ const snapshot = client.sent.map((m) => JSON.parse(m)).find((m) => m.t === "snapshot");
125
+ assert.equal(snapshot.historyRevision, session.historyRevision);
126
+ assert.equal(snapshot.reset, true);
127
+ assert.equal(session.isStarted, false);
128
+ } finally { unsub(); closeConn(conn); await sessions.drop(sid); }
129
+ });
130
+
131
+ test("each WS message rechecks token validity without relying on a logout callback", async () => {
132
+ const token = "synthetic-message-recheck";
133
+ db.prepare("INSERT INTO auth_tokens VALUES (?, ?, ?)").run(token, Date.now(), Date.now());
134
+ const client = socket(), conn = openConn(client.ws, token);
135
+ let removed = 0;
136
+ conn.subs.set("synthetic", () => removed++);
137
+ db.prepare("DELETE FROM auth_tokens WHERE token = ?").run(token);
138
+ await handleMessage(conn, JSON.stringify({ t: "ping" }));
139
+ assert.equal(conn.closed, true);
140
+ assert.deepEqual(client.closed, [4401]);
141
+ assert.equal(removed, 1);
142
+ assert.equal(client.sent.length, 0);
143
+ });
144
+
145
+ test("logout while subscription initialization awaits cannot leak a new subscription", async () => {
146
+ const { addProject, sessions } = await import("./sessions.ts");
147
+ const project = await addProject(dir), sid = crypto.randomUUID();
148
+ const session = await sessions.getOrCreate(sid, dir);
149
+ const token = "synthetic-pending-sub";
150
+ db.prepare("INSERT INTO auth_tokens VALUES (?, ?, ?)").run(token, Date.now(), Date.now());
151
+ const client = socket(), conn = openConn(client.ws, token);
152
+ const original = sessions.getOrCreate;
153
+ let release!: () => void, entered!: () => void;
154
+ const gate = new Promise<void>((resolve) => { release = resolve; });
155
+ const started = new Promise<void>((resolve) => { entered = resolve; });
156
+ sessions.getOrCreate = async () => { entered(); await gate; return session; };
157
+ try {
158
+ const pending = handleMessage(conn, JSON.stringify({ t: "sub", projectId: project.id, sessionId: sid }));
159
+ await started;
160
+ revokeConnections(token);
161
+ release();
162
+ await pending;
163
+ assert.equal(conn.closed, true);
164
+ assert.equal(conn.subs.size, 0);
165
+ assert.equal(client.sent.length, 0);
166
+ } finally {
167
+ release(); sessions.getOrCreate = original; closeConn(conn); await sessions.drop(sid);
168
+ }
169
+ });
170
+
171
+ test("password rotation actively closes every old socket and removes subscriptions", async () => {
172
+ const { config } = await import("./config.ts");
173
+ const { isTokenValid } = await import("./auth.ts");
174
+ const tokens = ["synthetic-rotation-a", "synthetic-rotation-b"];
175
+ const clients = tokens.map(() => socket());
176
+ const connections = tokens.map((token, index) => {
177
+ db.prepare("INSERT INTO auth_tokens VALUES (?, ?, ?)").run(token, Date.now(), Date.now());
178
+ return openConn(clients[index]!.ws, token);
179
+ });
180
+ let removed = 0;
181
+ for (const conn of connections) conn.subs.set("synthetic-view", () => removed++);
182
+ await fs.promises.writeFile(config.passwordFile, "synthetic-new-password\n");
183
+ assert.equal(await refreshAuth(), true);
184
+ assert.equal(removed, 2);
185
+ for (const [index, conn] of connections.entries()) {
186
+ assert.equal(conn.closed, true); assert.equal(conn.subs.size, 0);
187
+ assert.deepEqual(clients[index]!.closed, [4401]);
188
+ assert.equal(isTokenValid(tokens[index]!), false);
189
+ await handleMessage(conn, JSON.stringify({ t: "ping" }));
190
+ assert.equal(clients[index]!.sent.length, 0);
191
+ }
192
+ const pending = socket();
193
+ assert.equal(openConn(pending.ws, tokens[0]!).closed, true);
194
+ assert.deepEqual(pending.closed, [4401]);
195
+ });
package/server/ws.ts ADDED
@@ -0,0 +1,197 @@
1
+ import crypto from "node:crypto";
2
+ import type { WSContext } from "hono/ws";
3
+ import type { ClientMsg, ServerMsg } from "../shared/protocol.ts";
4
+ import { projectPath, sessions } from "./sessions.ts";
5
+ import { idecron } from "./idecron.ts";
6
+ import { isTokenValid, onAuthRevoked, validateToken } from "./auth.ts";
7
+ import { notify, setViewingCheck } from "./push.ts";
8
+ import { deployedDistVersion, onDistVersion } from "./dist-version.ts";
9
+ import { boundedReplay } from "../shared/message-window.ts";
10
+
11
+ /** Changes on every server start so stale client cursors are detected. */
12
+ export const EPOCH = crypto.randomBytes(6).toString("hex");
13
+
14
+ type Conn = { ws: WSContext; subs: Map<string, () => void>; visible: boolean; token: string; closed: boolean; versionAware?: boolean; sentVersion?: string };
15
+
16
+ const conns = new Set<Conn>();
17
+
18
+ // Credential rotation revokes devices, never the sessions/CLI processes they
19
+ // were viewing. closeConn only removes this socket and its subscriptions.
20
+ onAuthRevoked(() => {
21
+ for (const conn of conns) {
22
+ closeConn(conn);
23
+ conn.ws.close(4401, "authentication changed");
24
+ }
25
+ });
26
+
27
+ function sendVersion(conn: Conn, version: string | undefined) {
28
+ if (!version || !conn.versionAware || conn.sentVersion === version) return;
29
+ send(conn, { t: "version", version });
30
+ conn.sentVersion = version;
31
+ }
32
+ onDistVersion((version) => {
33
+ for (const conn of conns) sendVersion(conn, version);
34
+ });
35
+
36
+ function send(conn: Conn, msg: ServerMsg) {
37
+ if (conn.closed) return;
38
+ try {
39
+ conn.ws.send(JSON.stringify(msg));
40
+ } catch {
41
+ // socket already closed
42
+ }
43
+ }
44
+
45
+ sessions.onActivity((a) => {
46
+ for (const c of conns) send(c, { t: "activity", ...a });
47
+ void notify({
48
+ kind: a.kind,
49
+ title: a.title,
50
+ body: a.kind === "done" ? "已完成" : a.kind === "attention" ? "等你处理" : "出错了",
51
+ url: `/p/${a.projectId}/s/${a.sessionId}`,
52
+ tag: `${a.sessionId}:${a.kind}`,
53
+ skipIfViewing: a.sessionId,
54
+ });
55
+ });
56
+
57
+ // A device with the session open over a live socket is "looking at it": no push needed.
58
+ // "Viewing" = a visible page has the session open; a background tab still gets the push.
59
+ setViewingCheck((sessionId) => [...conns].some((c) => c.visible && c.subs.has(sessionId)));
60
+
61
+ sessions.onSessionsChanged((projectId) => {
62
+ for (const c of conns) send(c, { t: "sessions", projectId });
63
+ });
64
+
65
+ export function openConn(ws: WSContext, token: string): Conn {
66
+ const conn: Conn = { ws, subs: new Map(), visible: true, token, closed: false };
67
+ if (isTokenValid(token)) conns.add(conn);
68
+ else { conn.closed = true; ws.close(4401, "unauthorized"); }
69
+ return conn;
70
+ }
71
+
72
+ export function closeConn(conn: Conn) {
73
+ conn.closed = true;
74
+ for (const unsub of conn.subs.values()) unsub();
75
+ conn.subs.clear();
76
+ conns.delete(conn);
77
+ }
78
+
79
+ export function revokeConnections(token: string) {
80
+ for (const conn of conns) {
81
+ if (conn.token !== token) continue;
82
+ closeConn(conn);
83
+ conn.ws.close(4401, "logged out");
84
+ }
85
+ }
86
+
87
+ export async function handleMessage(conn: Conn, raw: string) {
88
+ if (conn.closed) return;
89
+ if (!await validateToken(conn.token)) {
90
+ closeConn(conn);
91
+ conn.ws.close(4401, "unauthorized");
92
+ return;
93
+ }
94
+ if (conn.closed) return;
95
+ let msg: ClientMsg;
96
+ try {
97
+ msg = JSON.parse(raw);
98
+ } catch {
99
+ return;
100
+ }
101
+ try {
102
+ await dispatch(conn, msg);
103
+ } catch (err) {
104
+ send(conn, { t: "error", message: err instanceof Error ? err.message : String(err) });
105
+ }
106
+ }
107
+
108
+ async function dispatch(conn: Conn, msg: ClientMsg) {
109
+ switch (msg.t) {
110
+ case "clientVersion":
111
+ // Older clients do not understand the new message type; only send it after opt-in.
112
+ conn.versionAware = true;
113
+ sendVersion(conn, await deployedDistVersion());
114
+ return;
115
+ case "sub": {
116
+ conn.subs.get(msg.sessionId)?.();
117
+ const s = await sessions.getOrCreate(msg.sessionId, projectPath(msg.projectId));
118
+ if (conn.closed) return;
119
+ const sameEpoch = msg.epoch === EPOCH && typeof msg.seq === "number";
120
+ const missed = sameEpoch ? s.eventsSince(msg.seq!) : undefined;
121
+ // Active block prefixes live outside the ring. Replay a fresh window when restoring
122
+ // them so their cumulative checkpoints replace the client's earlier fragments.
123
+ const checkpoints = msg.bounded ? s.streamingCheckpoints : [];
124
+ const replay = checkpoints.length ? s.events : missed ?? s.events;
125
+ const events = msg.bounded ? boundedReplay(replay, checkpoints) : replay;
126
+ const reset = !missed || checkpoints.length > 0 || events.length !== replay.length;
127
+ send(conn, {
128
+ t: "snapshot",
129
+ sessionId: msg.sessionId,
130
+ epoch: EPOCH,
131
+ seq: s.seq,
132
+ reset,
133
+ historyRevision: s.historyRevision,
134
+ live: s.snapshot(),
135
+ events,
136
+ });
137
+ const unsub = s.subscribe((seq, ev) => send(conn, { t: "ev", sessionId: msg.sessionId, seq, ev }));
138
+ conn.subs.set(msg.sessionId, unsub);
139
+ return;
140
+ }
141
+ case "unsub":
142
+ conn.subs.get(msg.sessionId)?.();
143
+ conn.subs.delete(msg.sessionId);
144
+ return;
145
+ case "send": {
146
+ const s = await sessions.getOrCreate(msg.sessionId, projectPath(msg.projectId));
147
+ if (conn.closed) return;
148
+ await s.send(msg.uuid, msg.text, msg.images);
149
+ return;
150
+ }
151
+ case "interrupt":
152
+ await sessions.get(msg.sessionId)?.interrupt();
153
+ return;
154
+ case "refreshCrons": {
155
+ const s = await sessions.getOrCreate(msg.sessionId, projectPath(msg.projectId));
156
+ if (conn.closed) return;
157
+ await s.refreshCrons();
158
+ return;
159
+ }
160
+ case "cancelIdecron":
161
+ idecron.cancel(msg.id, msg.sessionId);
162
+ return;
163
+ case "setModel": {
164
+ const s = await sessions.getOrCreate(msg.sessionId, projectPath(msg.projectId));
165
+ if (conn.closed) return;
166
+ await s.setModel(msg.model);
167
+ return;
168
+ }
169
+ case "setEffort": {
170
+ const s = await sessions.getOrCreate(msg.sessionId, projectPath(msg.projectId));
171
+ if (conn.closed) return;
172
+ await s.setEffort(msg.effort);
173
+ return;
174
+ }
175
+ case "setPlanMode": {
176
+ const s = await sessions.getOrCreate(msg.sessionId, projectPath(msg.projectId));
177
+ if (conn.closed) return;
178
+ await s.setPlanMode(msg.on);
179
+ return;
180
+ }
181
+ case "bash": {
182
+ const s = await sessions.getOrCreate(msg.sessionId, projectPath(msg.projectId));
183
+ if (conn.closed) return;
184
+ await s.runBash(msg.uuid, msg.command);
185
+ return;
186
+ }
187
+ case "ping":
188
+ send(conn, { t: "pong" });
189
+ return;
190
+ case "visibility":
191
+ conn.visible = msg.visible;
192
+ return;
193
+ case "answer":
194
+ sessions.get(msg.sessionId)?.resolvePending(msg.requestId, msg.answer);
195
+ return;
196
+ }
197
+ }
@@ -0,0 +1,15 @@
1
+ import type { SdkMessageLike } from "./protocol.ts";
2
+
3
+ /** Actual response metadata, never the parent's settings or a tool's model hint. */
4
+ export function responseModel(message: SdkMessageLike): string | undefined {
5
+ let model: unknown;
6
+ if (message.type === "assistant" && !message.error && !message.aborted) {
7
+ model = (message.message as { model?: unknown } | undefined)?.model;
8
+ } else if (message.type === "stream_event") {
9
+ const event = message.event as { type?: string; message?: { model?: unknown } } | undefined;
10
+ if (event?.type === "message_start") model = event.message?.model;
11
+ }
12
+ if (typeof model !== "string") return undefined;
13
+ const value = model.trim();
14
+ return value && value !== "<synthetic>" ? value : undefined;
15
+ }
@@ -0,0 +1,12 @@
1
+ import type { AgentTask } from "./protocol.ts";
2
+
3
+ /** SDK run IDs are ordered lexically. Arrival order can change when an older
4
+ * run reports late progress or model metadata, so it cannot establish recency. */
5
+ export function currentAgentTask(tasks: Iterable<AgentTask>, taskId: string): AgentTask | undefined {
6
+ let current: AgentTask | undefined;
7
+ for (const task of tasks) {
8
+ if (task.taskId !== taskId) continue;
9
+ if (!current?.runId || task.runId && task.runId >= current.runId) current = task;
10
+ }
11
+ return current;
12
+ }