@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,43 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { listSessionSubagents, subagentMessages, subagentHistoryPage, subagentHistoryMessage } from "./subagents.ts";
7
+
8
+ test("nested SDK-discovered agents retain metadata, paged history and full message reads (CACHE-006)", async (t) => {
9
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "ide-subagents-"));
10
+ const original = process.env.CLAUDE_CONFIG_DIR;
11
+ process.env.CLAUDE_CONFIG_DIR = root;
12
+ t.after(async () => {
13
+ if (original === undefined) delete process.env.CLAUDE_CONFIG_DIR;
14
+ else process.env.CLAUDE_CONFIG_DIR = original;
15
+ await fs.rm(root, { recursive: true, force: true });
16
+ });
17
+ const projectPath = "/tmp/ide-nested-agent-fixture";
18
+ const sessionId = "12345678-1234-1234-1234-123456789abc";
19
+ const project = path.join(root, "projects", projectPath.replace(/[^a-zA-Z0-9]/g, "-"));
20
+ const agents = path.join(project, sessionId, "subagents");
21
+ const nested = path.join(agents, "parent", "children");
22
+ await fs.mkdir(nested, { recursive: true });
23
+ await fs.writeFile(path.join(project, `${sessionId}.jsonl`), JSON.stringify({ type: "user", uuid: "main", parentUuid: null, sessionId, cwd: projectPath, message: { role: "user", content: "main" } }) + "\n");
24
+ const records = Array.from({ length: 110 }, (_, i) => ({ type: "user", uuid: `u${i}`, parentUuid: i ? `u${i - 1}` : null, sessionId, isSidechain: true, message: { role: "user", content: `agent message ${i}` } }));
25
+ await fs.writeFile(path.join(nested, "agent-child.jsonl"), records.map((r) => JSON.stringify(r)).join("\n") + "\n");
26
+ const metadata = { toolUseId: "toolu_nested", agentType: "general-purpose", description: "nested task" };
27
+ await fs.writeFile(path.join(nested, "agent-child.meta.json"), JSON.stringify(metadata));
28
+ const directReply = { type: "assistant", uuid: "direct-reply", parentUuid: "u0", sessionId, isSidechain: true, message: { role: "assistant", id: "api-reply", model: "saved-agent-model", content: [{ type: "text", text: "done" }] } };
29
+ await fs.writeFile(path.join(agents, "agent-direct.jsonl"), [records[0], directReply].map((r) => JSON.stringify(r)).join("\n") + "\n");
30
+ await fs.writeFile(path.join(agents, "agent-direct.meta.json"), JSON.stringify({ toolUseId: "toolu_direct" }));
31
+
32
+ const listed = await listSessionSubagents(projectPath, sessionId);
33
+ assert.deepEqual(listed.find((a) => a.agentId === "child"), { agentId: "child", ...metadata });
34
+ assert.equal(listed.find((a) => a.agentId === "direct")?.toolUseId, "toolu_direct");
35
+ const sdk = await subagentMessages(projectPath, sessionId, "child");
36
+ const latest = await subagentHistoryPage(projectPath, sessionId, "child", {});
37
+ const older = await subagentHistoryPage(projectPath, sessionId, "child", { cursor: latest.older });
38
+ assert.deepEqual([...older.items, ...latest.items].map((m) => m.uuid), sdk.map((m) => m.uuid));
39
+ assert.equal((await subagentHistoryMessage(projectPath, sessionId, "child", "u0")).uuid, "u0");
40
+ const directHistory = await subagentHistoryPage(projectPath, sessionId, "direct", {});
41
+ assert.deepEqual(directHistory.items.map((m) => m.uuid), ["u0", "direct-reply"]);
42
+ assert.equal((directHistory.items[1]?.message as { model?: string }).model, "saved-agent-model", "paged history retains actual response model metadata");
43
+ });
@@ -0,0 +1,58 @@
1
+ import fs from "node:fs/promises";
2
+ import type { Dirent } from "node:fs";
3
+ import path from "node:path";
4
+ import { getSubagentMessages, listSubagents } from "@anthropic-ai/claude-agent-sdk";
5
+ import { historyPage, historyMessage, historyReadPath, type PageOptions } from "./history-pages.ts";
6
+ import { sessionFileDir } from "./cli-files.ts";
7
+
8
+ export type SubagentInfo = { agentId: string; toolUseId?: string; agentType?: string; description?: string };
9
+
10
+ export async function listSessionSubagents(projectPath: string, sessionId: string): Promise<SubagentInfo[]> {
11
+ const ids = await listSubagents(sessionId, { dir: projectPath }).catch(() => [] as string[]);
12
+ return Promise.all(ids.map(async (agentId) => {
13
+ const metaFile = (await agentFile(projectPath, sessionId, agentId))?.replace(/\.jsonl$/, ".meta.json");
14
+ try {
15
+ const meta = JSON.parse(await fs.readFile(metaFile!, "utf8"));
16
+ return { agentId, toolUseId: meta.toolUseId, agentType: meta.agentType, description: meta.description };
17
+ } catch {
18
+ return { agentId };
19
+ }
20
+ }));
21
+ }
22
+
23
+ export async function subagentMessages(projectPath: string, sessionId: string, agentId: string) {
24
+ if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) throw new Error("bad agent id");
25
+ return getSubagentMessages(sessionId, agentId, { dir: projectPath });
26
+ }
27
+
28
+ async function agentFile(projectPath: string, sessionId: string, agentId: string): Promise<string | undefined> {
29
+ if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) throw new Error("bad agent id");
30
+ if (!/^[a-fA-F0-9-]{36}$/.test(sessionId)) return undefined;
31
+ const dir = await sessionFileDir(projectPath, sessionId);
32
+ if (!dir) return undefined;
33
+ const name = `agent-${agentId}.jsonl`;
34
+ // The SDK lists nested transcript directories as well as direct children.
35
+ async function find(directory: string): Promise<string | undefined> {
36
+ let children: Dirent[];
37
+ try { children = await fs.readdir(directory, { withFileTypes: true }); } catch { return undefined; }
38
+ for (const child of children) {
39
+ if (child.isFile() && child.name === name) return path.join(directory, child.name);
40
+ if (child.isDirectory()) {
41
+ const found = await find(path.join(directory, child.name));
42
+ if (found) return found;
43
+ }
44
+ }
45
+ return undefined;
46
+ }
47
+ return find(path.join(dir, sessionId, "subagents"));
48
+ }
49
+ export async function subagentHistoryPage(projectPath: string, sessionId: string, agentId: string, options: PageOptions) {
50
+ return historyPage(await agentFile(projectPath, sessionId, agentId), { ...options, sidechain: true });
51
+ }
52
+ export async function subagentHistoryMessage(projectPath: string, sessionId: string, agentId: string, uuid: string) {
53
+ return historyMessage(await agentFile(projectPath, sessionId, agentId), uuid, undefined, true);
54
+ }
55
+
56
+ export async function subagentReadPath(projectPath: string, sessionId: string, agentId: string, toolId: string) {
57
+ return historyReadPath(await agentFile(projectPath, sessionId, agentId), toolId, undefined, true);
58
+ }
@@ -0,0 +1,76 @@
1
+ import { after, 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 { channelFixture } from "./http-channel-test-fixture.ts";
7
+
8
+ const fixture = await channelFixture();
9
+ const outside = fs.mkdtempSync(path.join(os.tmpdir(), "ide-read-images-"));
10
+ after(async () => { await fixture.close(); fs.rmSync(outside, { recursive: true, force: true }); });
11
+ const { registerToolImageRoutes } = await import("./tool-images.ts");
12
+ registerToolImageRoutes(fixture.app);
13
+ const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==", "base64");
14
+ const dir = path.join(process.env.CLAUDE_CONFIG_DIR!, "projects", fixture.tmp.replace(/[^a-zA-Z0-9]/g, "-"));
15
+ fs.mkdirSync(dir, { recursive: true });
16
+ function transcript(sid: string, calls: { id: string; name?: string; file: string }[], agent?: string) {
17
+ const records = [
18
+ { type: "user", uuid: "prompt", parentUuid: null, sessionId: sid, cwd: outside, isSidechain: !!agent, message: { role: "user", content: "read" } },
19
+ { type: "assistant", uuid: "read-owner", parentUuid: "prompt", sessionId: sid, cwd: outside, isSidechain: !!agent,
20
+ message: { role: "assistant", id: "api-read", content: calls.map((call) => ({ type: "tool_use", id: call.id, name: call.name ?? "Read", input: { file_path: call.file } })) } },
21
+ ];
22
+ const file = agent ? path.join(dir, sid, "subagents", `agent-${agent}.jsonl`) : path.join(dir, `${sid}.jsonl`);
23
+ fs.mkdirSync(path.dirname(file), { recursive: true });
24
+ fs.writeFileSync(file, records.map((record) => JSON.stringify(record)).join("\n") + "\n");
25
+ }
26
+ const read = (sid: string, tool: string, query?: Record<string, string>) => fixture.app.request(
27
+ `/api/projects/${fixture.project.id}/sessions/${sid}/tools/${tool}/image?${new URLSearchParams(query)}`);
28
+
29
+ test("Read images outside the project are served temporarily from their recorded path and cwd", async () => {
30
+ const sid = fixture.newSession();
31
+ const image = path.join(outside, "crop.png");
32
+ fs.writeFileSync(image, png);
33
+ transcript(sid, [{ id: "absolute", file: image }, { id: "relative", file: "crop.png" }]);
34
+ for (const tool of ["absolute", "relative"]) {
35
+ const response = await read(sid, tool, { path: "/not/a/client/controlled/path.png" });
36
+ assert.equal(response.status, 200);
37
+ assert.equal(response.headers.get("content-type"), "image/png");
38
+ assert.equal(response.headers.get("cache-control"), "no-store");
39
+ assert.equal(response.headers.get("x-content-type-options"), "nosniff");
40
+ assert.deepEqual(Buffer.from(await response.arrayBuffer()), png);
41
+ }
42
+ fs.unlinkSync(image);
43
+ assert.equal((await read(sid, "absolute")).status, 404, "deleted temporary images are not retained");
44
+ });
45
+
46
+ test("image requests stay in the selected Read scope and reject unavailable targets", async () => {
47
+ const sid = fixture.newSession();
48
+ fs.writeFileSync(path.join(outside, "valid.png"), png);
49
+ fs.writeFileSync(path.join(outside, "text.txt"), "not an image");
50
+ const huge = path.join(outside, "huge.png"), fd = fs.openSync(huge, "w");
51
+ try { fs.ftruncateSync(fd, 20 * 1024 * 1024 + 1); } finally { fs.closeSync(fd); }
52
+ fs.mkdirSync(path.join(outside, "directory.png"));
53
+ transcript(sid, [
54
+ { id: "read", file: path.join(outside, "valid.png") },
55
+ { id: "edit", name: "Edit", file: path.join(outside, "valid.png") },
56
+ { id: "text", file: path.join(outside, "text.txt") },
57
+ { id: "huge", file: huge }, { id: "dir", file: path.join(outside, "directory.png") },
58
+ ]);
59
+ for (const tool of ["edit", "text", "huge", "dir", "unknown"]) assert.equal((await read(sid, tool)).status, 404, tool);
60
+ fixture.db.prepare("UPDATE session_view SET rewind_anchor='prompt' WHERE session_id=?").run(sid);
61
+ assert.equal((await read(sid, "read")).status, 404, "discarded calls do not grant access");
62
+ });
63
+
64
+ test("subagent previews use their own transcript and SVG remains sandboxed", async () => {
65
+ const sid = fixture.newSession();
66
+ const svg = path.join(outside, "view.svg");
67
+ fs.writeFileSync(svg, '<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>');
68
+ transcript(sid, []);
69
+ transcript(sid, [{ id: "child-read", file: svg }], "child");
70
+ assert.equal((await read(sid, "child-read")).status, 404);
71
+ const response = await read(sid, "child-read", { agent: "child" });
72
+ assert.equal(response.status, 200);
73
+ assert.equal(response.headers.get("content-type"), "image/svg+xml");
74
+ assert.match(response.headers.get("content-security-policy")!, /sandbox/);
75
+ assert.equal((await read(sid, "child-read", { agent: "../child" })).status, 404);
76
+ });
@@ -0,0 +1,45 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { Hono } from "hono";
4
+ import { IMAGE_TYPES } from "./files.ts";
5
+ import { FsHttpError, route } from "./fsutil.ts";
6
+ import { projectPath, sessionReadPath } from "./sessions.ts";
7
+ import { historyCwd } from "./session-dirs.ts";
8
+ import { subagentReadPath } from "./subagents.ts";
9
+
10
+ const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
11
+
12
+ /** Best-effort views of existing Read targets. No copies, base64 or durable cache. */
13
+ export function registerToolImageRoutes(app: Hono) {
14
+ app.get("/api/projects/:pid/sessions/:sid/tools/:tid/image", route(async (c) => {
15
+ c.header("cache-control", "no-store");
16
+ const project = projectPath(c.req.param("pid")!);
17
+ const sid = c.req.param("sid")!, toolId = c.req.param("tid")!, agent = c.req.query("agent");
18
+ if (agent !== undefined && !/^[a-zA-Z0-9_-]+$/.test(agent)) throw new FsHttpError(404, "图片不存在");
19
+ const target = agent ? await subagentReadPath(project, sid, agent, toolId) : await sessionReadPath(sid, project, toolId);
20
+ const file = path.resolve(target.cwd ?? await historyCwd(project, sid), target.path);
21
+ const mime = IMAGE_TYPES[path.extname(file).toLowerCase()];
22
+ if (!mime) throw new FsHttpError(404, "图片不存在");
23
+ let handle: fs.promises.FileHandle | undefined;
24
+ try {
25
+ // Non-blocking open also lets us reject named pipes without waiting on a writer.
26
+ handle = await fs.promises.open(file, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK);
27
+ const stat = await handle.stat();
28
+ if (!stat.isFile() || !stat.size || stat.size > MAX_IMAGE_BYTES) throw new FsHttpError(404, "图片不可用");
29
+ const bytes = Buffer.alloc(stat.size);
30
+ let offset = 0;
31
+ while (offset < bytes.length) {
32
+ const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, offset);
33
+ if (!bytesRead) throw new FsHttpError(404, "图片不可用");
34
+ offset += bytesRead;
35
+ }
36
+ return new Response(new Uint8Array(bytes), { headers: {
37
+ "content-type": mime, "content-length": String(bytes.length), "cache-control": "no-store",
38
+ "x-content-type-options": "nosniff",
39
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox",
40
+ } });
41
+ } catch {
42
+ throw new FsHttpError(404, "图片不可用");
43
+ } finally { await handle?.close(); }
44
+ }));
45
+ }
@@ -0,0 +1,308 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { Hunk, TurnChanges, TurnChangesInfo, TurnFile, TurnFileDiff, TurnFileStatus } from "../shared/project.ts";
4
+ import { lineDiff } from "../web/src/diff.ts";
5
+ import { claudeDir, forkedFrom, sessionFileDir } from "./cli-files.ts";
6
+ import { sessionCwd } from "./session-dirs.ts";
7
+ import { HttpError } from "./sessions.ts";
8
+
9
+ /**
10
+ * Which files each turn changed, from the CLI's own records (see NOTES.md "SDK:fork 与回退"):
11
+ * - `file-history-snapshot{messageId, snapshot.trackedFileBackups}` is written when a prompt is
12
+ * sent: the state of every already-tracked file at the start of that turn.
13
+ * - `file-history-delta{snapshotMessageId, trackingPath, backup}` is written when a turn touches
14
+ * a file for the first time: its state before the edit (`backupFileName: null` = did not exist).
15
+ * - Backups live in `~/.claude/file-history/<session>/<backupFileName>`.
16
+ * So a file's content before turn N is its delta in N or its entry in snapshot N, and after
17
+ * turn N it is its entry in the next turn's snapshot/delta, or the file on disk for the last turn.
18
+ * Edit/Write/MultiEdit/NotebookEdit calls of the turn add files the checkpoints missed; Bash
19
+ * and subagent edits are not tracked by the CLI at all.
20
+ */
21
+
22
+ const FILE_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
23
+ const MAX_BYTES = 1024 * 1024;
24
+
25
+ type Backup = { backupFileName: string | null; version?: number };
26
+ type Rec = {
27
+ type?: string;
28
+ uuid?: string;
29
+ parentUuid?: string | null;
30
+ logicalParentUuid?: string | null;
31
+ isSidechain?: boolean;
32
+ isMeta?: boolean;
33
+ isCompactSummary?: boolean;
34
+ messageId?: string;
35
+ snapshotMessageId?: string;
36
+ trackingPath?: string;
37
+ backup?: Backup;
38
+ snapshot?: { messageId?: string; trackedFileBackups?: Record<string, Backup> };
39
+ isSnapshotUpdate?: boolean;
40
+ message?: { content?: unknown };
41
+ toolUseResult?: unknown;
42
+ };
43
+
44
+ type ToolUse = { id: string; name: string; abs: string };
45
+ type ToolOutcome = { isError: boolean; patch?: Hunk[]; created?: string };
46
+ type Turn = { id: string; prompt: string; tools: ToolUse[]; snapshot: Map<string, Backup>; deltas: Map<string, Backup> };
47
+ type Parsed = { cwd: string; sessionIds: string[]; turns: Turn[]; outcomes: Map<string, ToolOutcome> };
48
+
49
+ async function readRecords(file: string): Promise<Rec[]> {
50
+ const out: Rec[] = [];
51
+ for (const line of (await fs.readFile(file, "utf8")).split("\n")) {
52
+ if (!line) continue;
53
+ try {
54
+ out.push(JSON.parse(line));
55
+ } catch {
56
+ // partial line while the CLI is writing
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+
62
+ function blocks(r: Rec): { type?: string; [k: string]: unknown }[] {
63
+ const c = r.message?.content;
64
+ if (typeof c === "string") return [{ type: "text", text: c }];
65
+ return Array.isArray(c) ? c : [];
66
+ }
67
+
68
+ /** A user record the transcript shows as a prompt (not a tool result or CLI-internal message). */
69
+ function isPrompt(r: Rec) {
70
+ if (r.type !== "user" || r.isMeta || r.isCompactSummary || r.isSidechain) return false;
71
+ return blocks(r).some((b) => b.type === "text" || b.type === "image");
72
+ }
73
+
74
+ /** The records of the current branch, oldest first (rewinds leave other branches in the file). */
75
+ function currentBranch(recs: Rec[]): Rec[] {
76
+ const byUuid = new Map<string, Rec>();
77
+ let leaf: Rec | undefined;
78
+ for (const r of recs) {
79
+ if (!r.uuid || r.isSidechain) continue;
80
+ byUuid.set(r.uuid, r);
81
+ if (r.type === "user" || r.type === "assistant" || r.type === "system") leaf = r;
82
+ }
83
+ const chain: Rec[] = [];
84
+ const seen = new Set<string>();
85
+ for (let r = leaf; r && r.uuid && !seen.has(r.uuid); ) {
86
+ seen.add(r.uuid);
87
+ chain.push(r);
88
+ const parent = r.parentUuid ?? r.logicalParentUuid;
89
+ r = parent ? byUuid.get(parent) : undefined;
90
+ }
91
+ return chain.reverse();
92
+ }
93
+
94
+ async function parse(projectPath: string, sessionId: string): Promise<Parsed> {
95
+ const dir = await sessionFileDir(projectPath, sessionId);
96
+ if (!dir) throw new HttpError(404, "会话文件不存在");
97
+ const cwd = await sessionCwd(projectPath, sessionId);
98
+ const recs = await readRecords(path.join(dir, `${sessionId}.jsonl`));
99
+ const abs = (p: string) => (path.isAbsolute(p) ? path.normalize(p) : path.join(cwd, p));
100
+
101
+ const snapshots = new Map<string, Map<string, Backup>>();
102
+ const deltas = new Map<string, Map<string, Backup>>();
103
+ for (const r of recs) {
104
+ if (r.type === "file-history-snapshot" && r.messageId) {
105
+ const cur = (r.isSnapshotUpdate && snapshots.get(r.messageId)) || new Map<string, Backup>();
106
+ for (const [p, b] of Object.entries(r.snapshot?.trackedFileBackups ?? {})) cur.set(abs(p), b);
107
+ snapshots.set(r.messageId, cur);
108
+ } else if (r.type === "file-history-delta" && r.snapshotMessageId && r.trackingPath && r.backup) {
109
+ const m = deltas.get(r.snapshotMessageId) ?? new Map<string, Backup>();
110
+ if (!m.has(abs(r.trackingPath))) m.set(abs(r.trackingPath), r.backup);
111
+ deltas.set(r.snapshotMessageId, m);
112
+ }
113
+ }
114
+
115
+ const turns: Turn[] = [];
116
+ const outcomes = new Map<string, ToolOutcome>();
117
+ let prompt: string | undefined;
118
+ let cur: Turn | undefined;
119
+ for (const r of currentBranch(recs)) {
120
+ if (isPrompt(r)) prompt = r.uuid;
121
+ if (r.uuid && snapshots.has(r.uuid)) {
122
+ cur = { id: r.uuid, prompt: prompt ?? r.uuid, tools: [], snapshot: snapshots.get(r.uuid)!, deltas: deltas.get(r.uuid) ?? new Map() };
123
+ turns.push(cur);
124
+ }
125
+ for (const b of blocks(r)) {
126
+ if (r.type === "assistant" && b.type === "tool_use" && FILE_TOOLS.has(String(b.name)) && cur) {
127
+ const input = (b.input ?? {}) as { file_path?: unknown; notebook_path?: unknown };
128
+ const p = typeof input.file_path === "string" ? input.file_path : typeof input.notebook_path === "string" ? input.notebook_path : "";
129
+ if (p) cur.tools.push({ id: String(b.id), name: String(b.name), abs: abs(p) });
130
+ } else if (r.type === "user" && b.type === "tool_result") {
131
+ const tur = r.toolUseResult as { structuredPatch?: Hunk[]; type?: string; content?: string } | undefined;
132
+ outcomes.set(String(b.tool_use_id), {
133
+ isError: !!b.is_error,
134
+ patch: Array.isArray(tur?.structuredPatch) ? tur.structuredPatch : undefined,
135
+ created: tur?.type === "create" && typeof tur.content === "string" ? tur.content : undefined,
136
+ });
137
+ }
138
+ }
139
+ }
140
+ const origin = (await forkedFrom(projectPath, sessionId))?.sessionId;
141
+ return { cwd, sessionIds: origin ? [sessionId, origin] : [sessionId], turns, outcomes };
142
+ }
143
+
144
+ type Content = string | null | undefined; // null = absent, undefined = unknown
145
+
146
+ async function readCapped(file: string): Promise<{ text: string; binary: boolean; truncated: boolean }> {
147
+ const fh = await fs.open(file, "r");
148
+ try {
149
+ const size = (await fh.stat()).size;
150
+ const buf = Buffer.alloc(Math.min(size, MAX_BYTES));
151
+ await fh.read(buf, 0, buf.length, 0);
152
+ const binary = buf.subarray(0, 8000).includes(0);
153
+ return { text: binary ? "" : buf.toString("utf8"), binary, truncated: size > MAX_BYTES };
154
+ } finally {
155
+ await fh.close();
156
+ }
157
+ }
158
+
159
+ type Loaded = { content: Content; binary: boolean; truncated: boolean };
160
+
161
+ async function exists(p: string) {
162
+ try {
163
+ await fs.access(p);
164
+ return true;
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
169
+
170
+ async function loadBackup(p: Parsed, b: Backup | undefined): Promise<Loaded> {
171
+ if (!b) return { content: undefined, binary: false, truncated: false };
172
+ if (b.backupFileName === null) return { content: null, binary: false, truncated: false };
173
+ for (const sid of p.sessionIds) {
174
+ const f = path.join(claudeDir(), "file-history", sid, b.backupFileName);
175
+ if (await exists(f)) {
176
+ const r = await readCapped(f);
177
+ return { content: r.text, binary: r.binary, truncated: r.truncated };
178
+ }
179
+ }
180
+ return { content: undefined, binary: false, truncated: false };
181
+ }
182
+
183
+ async function loadDisk(file: string): Promise<Loaded> {
184
+ try {
185
+ if (!(await fs.stat(file)).isFile()) return { content: null, binary: false, truncated: false };
186
+ const r = await readCapped(file);
187
+ return { content: r.text, binary: r.binary, truncated: r.truncated };
188
+ } catch {
189
+ return { content: null, binary: false, truncated: false };
190
+ }
191
+ }
192
+
193
+ function filesOf(t: Turn): string[] {
194
+ const set = new Set<string>([...t.deltas.keys(), ...t.tools.map((x) => x.abs)]);
195
+ const internal = claudeDir() + path.sep;
196
+ // Plan files and other CLI-internal writes are not project changes.
197
+ return [...set].filter((f) => !f.startsWith(internal));
198
+ }
199
+
200
+ /** Content of `file` before turn `i` and after it. */
201
+ async function beforeAfter(p: Parsed, i: number, file: string): Promise<{ before: Loaded; after: Loaded }> {
202
+ const t = p.turns[i]!;
203
+ const before = await loadBackup(p, t.deltas.get(file) ?? t.snapshot.get(file));
204
+ for (let j = i + 1; j < p.turns.length; j++) {
205
+ const n = p.turns[j]!;
206
+ const b = n.snapshot.get(file) ?? n.deltas.get(file);
207
+ if (b) return { before, after: await loadBackup(p, b) };
208
+ }
209
+ return { before, after: await loadDisk(file) };
210
+ }
211
+
212
+ function toolPatches(p: Parsed, t: Turn, file: string): Hunk[] {
213
+ const out: Hunk[] = [];
214
+ for (const tu of t.tools) {
215
+ if (tu.abs !== file) continue;
216
+ const o = p.outcomes.get(tu.id);
217
+ if (!o || o.isError) continue;
218
+ if (o.patch?.length) out.push(...o.patch);
219
+ else if (o.created !== undefined) out.push({ oldStart: 0, newStart: 1, lines: o.created.split("\n").map((l) => "+" + l) });
220
+ }
221
+ return out;
222
+ }
223
+
224
+ function statusOf(before: Content, after: Content, t: Turn, p: Parsed, file: string): TurnFileStatus {
225
+ if (before === null && after !== null) return "A";
226
+ if (after === null && before !== null) return "D";
227
+ if (before === undefined && t.tools.some((tu) => tu.abs === file && p.outcomes.get(tu.id)?.created !== undefined)) return "A";
228
+ return "M";
229
+ }
230
+
231
+ function display(p: Parsed, file: string) {
232
+ const rel = path.relative(p.cwd, file);
233
+ return !rel.startsWith("..") && !path.isAbsolute(rel) ? rel.split(path.sep).join("/") : file;
234
+ }
235
+
236
+ const lineCount = (s: string) => (s === "" ? 0 : s.replace(/\n$/, "").split("\n").length);
237
+
238
+ function countDiff(before: string, after: string) {
239
+ if (before === "" || after === "") return { additions: lineCount(after), deletions: lineCount(before) };
240
+ let additions = 0;
241
+ let deletions = 0;
242
+ for (const r of lineDiff(before, after, 0)) {
243
+ if (r.type === "+") additions++;
244
+ else if (r.type === "-") deletions++;
245
+ }
246
+ return { additions, deletions };
247
+ }
248
+
249
+ export async function turnChanges(projectPath: string, sessionId: string): Promise<TurnChangesInfo> {
250
+ let p: Parsed;
251
+ try {
252
+ p = await parse(projectPath, sessionId);
253
+ } catch (err) {
254
+ if (err instanceof HttpError && err.status === 404) return { turns: [] };
255
+ throw err;
256
+ }
257
+ const turns: TurnChanges[] = [];
258
+ for (const [i, t] of p.turns.entries()) {
259
+ const files: TurnFile[] = [];
260
+ for (const file of filesOf(t)) {
261
+ const tools = [...new Set(t.tools.filter((x) => x.abs === file && !p.outcomes.get(x.id)?.isError).map((x) => x.name))];
262
+ const { before, after } = await beforeAfter(p, i, file);
263
+ const checkpoint = before.content !== undefined && after.content !== undefined;
264
+ let counts = { additions: 0, deletions: 0 };
265
+ let unchanged = false;
266
+ if (checkpoint) {
267
+ unchanged = before.content === after.content;
268
+ if (!before.binary && !after.binary) counts = countDiff(before.content ?? "", after.content ?? "");
269
+ } else {
270
+ // Only failed calls touched it and nothing was checkpointed: not a change.
271
+ if (tools.length === 0) continue;
272
+ for (const h of toolPatches(p, t, file))
273
+ for (const l of h.lines) {
274
+ if (l.startsWith("+")) counts.additions++;
275
+ else if (l.startsWith("-")) counts.deletions++;
276
+ }
277
+ }
278
+ files.push({
279
+ path: display(p, file),
280
+ status: statusOf(before.content, after.content, t, p, file),
281
+ ...counts,
282
+ tools,
283
+ checkpoint,
284
+ ...(unchanged ? { unchanged } : {}),
285
+ });
286
+ }
287
+ files.sort((a, b) => a.path.localeCompare(b.path));
288
+ if (files.length) turns.push({ id: t.id, prompt: t.prompt, files });
289
+ }
290
+ return { turns };
291
+ }
292
+
293
+ export async function turnFileDiff(projectPath: string, sessionId: string, turnId: string, filePath: string): Promise<TurnFileDiff> {
294
+ const p = await parse(projectPath, sessionId);
295
+ const i = p.turns.findIndex((t) => t.id === turnId);
296
+ if (i === -1) throw new HttpError(404, "找不到这一轮");
297
+ const t = p.turns[i]!;
298
+ const file = path.isAbsolute(filePath) ? path.normalize(filePath) : path.join(p.cwd, filePath);
299
+ // Only files this turn is known to have touched can be read through here.
300
+ if (!filesOf(t).includes(file)) throw new HttpError(404, "这一轮没有改动这个文件");
301
+ const { before, after } = await beforeAfter(p, i, file);
302
+ const status = statusOf(before.content, after.content, t, p, file);
303
+ if (before.content !== undefined && after.content !== undefined) {
304
+ if (before.binary || after.binary) return { path: display(p, file), status, binary: true };
305
+ return { path: display(p, file), status, before: before.content, after: after.content, truncated: before.truncated || after.truncated };
306
+ }
307
+ return { path: display(p, file), status, patches: toolPatches(p, t, file) };
308
+ }
@@ -0,0 +1,87 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { execFile } from "node:child_process";
4
+
5
+ /**
6
+ * `git worktree` plumbing shared by the session list and the worktree routes. Kept free of
7
+ * session imports so sessions.ts can use it without an import cycle.
8
+ *
9
+ * Layout follows the CLI's `--worktree`: `<repo top>/.claude/worktrees/<name>` on branch
10
+ * `worktree-<name>`. Only worktrees in that directory belong to a project; others (made by hand
11
+ * elsewhere) are left alone.
12
+ */
13
+
14
+ export function git(args: string[], cwd: string): Promise<{ code: number; stdout: string; stderr: string }> {
15
+ return new Promise((resolve) => {
16
+ execFile(
17
+ "git",
18
+ args,
19
+ { cwd, encoding: "utf8", maxBuffer: 16 * 1024 * 1024, env: { ...process.env, GIT_TERMINAL_PROMPT: "0", LC_ALL: "C.UTF-8" } },
20
+ (err, stdout, stderr) => {
21
+ const code = err ? (typeof (err as { code?: unknown }).code === "number" ? ((err as { code: number }).code) : 1) : 0;
22
+ resolve({ code, stdout: stdout ?? "", stderr: stderr ?? String(err ?? "") });
23
+ },
24
+ );
25
+ });
26
+ }
27
+
28
+ export type RepoInfo = { top: string; prefix: string };
29
+
30
+ /** The checkout the project lives in (top) and the project's subdirectory inside it ("" or "a/b/"). */
31
+ async function pathExists(p: string) {
32
+ try {
33
+ await fs.access(p);
34
+ return true;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ export async function repoOf(projectPath: string): Promise<RepoInfo | undefined> {
41
+ if (!(await pathExists(projectPath))) return undefined;
42
+ const r = await git(["rev-parse", "--show-toplevel", "--show-prefix"], projectPath);
43
+ if (r.code !== 0) return undefined;
44
+ const [top = "", prefix = ""] = r.stdout.split("\n");
45
+ if (prefix) {
46
+ // A scratch directory ignored by an enclosing repository is not part of it.
47
+ const ign = await git(["check-ignore", "-q", "--", "."], projectPath);
48
+ if (ign.code === 0) return undefined;
49
+ }
50
+ return { top, prefix };
51
+ }
52
+
53
+ export type PorcelainWorktree = { path: string; head: string; branch: string | null; locked: boolean; prunable: boolean; bare: boolean };
54
+
55
+ export function parsePorcelain(out: string): PorcelainWorktree[] {
56
+ const list: PorcelainWorktree[] = [];
57
+ for (const block of out.split("\n\n")) {
58
+ const lines = block.split("\n").filter(Boolean);
59
+ const wt = lines.find((l) => l.startsWith("worktree "));
60
+ if (!wt) continue;
61
+ const branch = lines.find((l) => l.startsWith("branch "))?.slice(7) ?? null;
62
+ list.push({
63
+ path: wt.slice(9),
64
+ head: lines.find((l) => l.startsWith("HEAD "))?.slice(5) ?? "",
65
+ branch: branch?.replace(/^refs\/heads\//, "") ?? null,
66
+ locked: lines.some((l) => l === "locked" || l.startsWith("locked ")),
67
+ prunable: lines.some((l) => l === "prunable" || l.startsWith("prunable ")),
68
+ bare: lines.includes("bare"),
69
+ });
70
+ }
71
+ return list;
72
+ }
73
+
74
+ export const worktreesDir = (top: string) => path.join(top, ".claude", "worktrees");
75
+
76
+ /** The project's worktrees: entries of `git worktree list` under `<top>/.claude/worktrees/`. */
77
+ export async function projectWorktrees(projectPath: string): Promise<{ repo: RepoInfo; worktrees: (PorcelainWorktree & { name: string; cwd: string })[] } | undefined> {
78
+ const repo = await repoOf(projectPath);
79
+ if (!repo) return undefined;
80
+ const r = await git(["worktree", "list", "--porcelain"], projectPath);
81
+ if (r.code !== 0) return { repo, worktrees: [] };
82
+ const base = worktreesDir(repo.top) + path.sep;
83
+ const worktrees = parsePorcelain(r.stdout)
84
+ .filter((w) => w.path.startsWith(base) && !w.path.slice(base.length).includes(path.sep))
85
+ .map((w) => ({ ...w, name: w.path.slice(base.length), cwd: repo.prefix ? path.join(w.path, repo.prefix).replace(/\/$/, "") : w.path }));
86
+ return { repo, worktrees };
87
+ }