@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,299 @@
1
+ import { Fragment, useMemo, useState, useSyncExternalStore, type ReactNode } from "react";
2
+ import { Link } from "react-router";
3
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import type { ExtraDirSource, ExtraDirsInfo, TurnChanges, TurnChangesInfo, TurnFile, TurnFileDiff } from "@shared/project.ts";
5
+ import type { SessionListItem } from "@shared/protocol.ts";
6
+ import type { Item } from "../transcript.ts";
7
+ import { api, ApiError } from "../api.ts";
8
+ import { fromHunks, lineDiff } from "../diff.ts";
9
+ import { DiffView } from "./Items.tsx";
10
+ import { Modal } from "./Modal.tsx";
11
+ import { Button, cx } from "../ui/Button.tsx";
12
+ import { SwitchRow, Tag, monoInputCls } from "../ui/Form.tsx";
13
+ import { BranchIcon, ChevronRightIcon, FolderIcon, PlusSmIcon } from "../ui/icons.tsx";
14
+
15
+ // ---------- files changed by each turn ----------
16
+
17
+ const FILE_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
18
+
19
+ /**
20
+ * Per-turn change bars, placed at the end of each turn (just before the next turn's prompt,
21
+ * or at the end of the transcript). Refetched whenever an edit finishes or a turn ends.
22
+ */
23
+ export function useTurnChangeSlots(pid: string, sid: string, items: Item[], running: boolean) {
24
+ const edits = items.filter((it) => it.kind === "tool" && FILE_TOOLS.has(it.name) && it.result).length;
25
+ const results = items.filter((it) => it.kind === "result").length;
26
+ const q = useQuery({
27
+ queryKey: ["turn-changes", pid, sid, edits, results, running],
28
+ queryFn: () => api<TurnChangesInfo>(`/api/projects/${pid}/sessions/${sid}/turn-changes`),
29
+ placeholderData: (prev) => prev,
30
+ });
31
+ return useMemo(() => {
32
+ const byPrompt = new Map<string, TurnChanges[]>();
33
+ for (const t of q.data?.turns ?? []) byPrompt.set(t.prompt, [...(byPrompt.get(t.prompt) ?? []), t]);
34
+ const before = new Map<string, ReactNode>();
35
+ let open: TurnChanges[] = [];
36
+ const render = (turns: TurnChanges[]) =>
37
+ turns.length ? (
38
+ <>
39
+ {turns.map((t) => (
40
+ <TurnChangesBar key={t.id} pid={pid} sid={sid} turn={t} />
41
+ ))}
42
+ </>
43
+ ) : null;
44
+ for (const it of items) {
45
+ if (it.kind !== "user") continue;
46
+ // A turn ends where the next prompt (any user message, even one that changed nothing) starts.
47
+ if (open.length) before.set(it.key, render(open));
48
+ open = byPrompt.get(it.key) ?? [];
49
+ }
50
+ return { before: (key: string) => before.get(key) ?? null, tail: render(open) };
51
+ }, [q.data, items, pid, sid]);
52
+ }
53
+
54
+ /** Render items with the per-turn change bars in between. */
55
+ export function WithTurnChanges({ items, slots, render }: { items: Item[]; slots: ReturnType<typeof useTurnChangeSlots>; render: (it: Item) => ReactNode }) {
56
+ return (
57
+ <>
58
+ {items.map((it) => (
59
+ <Fragment key={it.key}>
60
+ {slots.before(it.key)}
61
+ {render(it)}
62
+ </Fragment>
63
+ ))}
64
+ {slots.tail}
65
+ </>
66
+ );
67
+ }
68
+
69
+ const STATUS_STYLE: Record<TurnFile["status"], string> = { A: "text-ok", M: "text-accent", D: "text-danger" };
70
+ const STATUS_LABEL: Record<TurnFile["status"], string> = { A: "新增", M: "修改", D: "删除" };
71
+
72
+ function TurnChangesBar({ pid, sid, turn }: { pid: string; sid: string; turn: TurnChanges }) {
73
+ const [open, setOpen] = useState(false);
74
+ const [file, setFile] = useState<string>();
75
+ const adds = turn.files.reduce((n, f) => n + f.additions, 0);
76
+ const dels = turn.files.reduce((n, f) => n + f.deletions, 0);
77
+ return (
78
+ <div className="rounded-lg border border-border bg-surface/60 text-sm" data-testid="turn-changes" data-turn={turn.id}>
79
+ <button className="flex w-full min-w-0 items-center gap-2 px-3 py-2 text-left text-muted" onClick={() => setOpen((v) => !v)} aria-expanded={open}>
80
+ <ChevronRightIcon size={12} className={cx("shrink-0 transition-transform", open && "rotate-90")} />
81
+ <span className="text-text">本轮改动:{turn.files.length} 个文件</span>
82
+ <span className="text-xs">
83
+ <span className="text-ok">+{adds}</span> <span className="text-danger">-{dels}</span>
84
+ </span>
85
+ </button>
86
+ {open && (
87
+ <div className="border-t border-border">
88
+ <ul className="py-1">
89
+ {turn.files.map((f) => (
90
+ <li key={f.path}>
91
+ <button
92
+ className={`flex w-full min-w-0 items-center gap-2 px-3 py-2 text-left md:py-1.5 ${file === f.path ? "bg-surface-2" : "hover:bg-surface-2/70"}`}
93
+ onClick={() => setFile(file === f.path ? undefined : f.path)}
94
+ aria-expanded={file === f.path}
95
+ data-testid="turn-file"
96
+ data-path={f.path}
97
+ >
98
+ <span className={`w-4 shrink-0 font-mono text-xs font-semibold ${STATUS_STYLE[f.status]}`} title={STATUS_LABEL[f.status]}>
99
+ {f.status}
100
+ </span>
101
+ <span className="min-w-0 flex-1 truncate font-mono text-[13px]" title={f.path}>
102
+ {f.path}
103
+ </span>
104
+ {f.unchanged ? (
105
+ <span className="shrink-0 text-xs text-muted">无净变化</span>
106
+ ) : (
107
+ <span className="shrink-0 text-xs">
108
+ <span className="text-ok">+{f.additions}</span> <span className="text-danger">-{f.deletions}</span>
109
+ </span>
110
+ )}
111
+ </button>
112
+ {file === f.path && <TurnFileDiffView pid={pid} sid={sid} turn={turn.id} path={f.path} />}
113
+ </li>
114
+ ))}
115
+ </ul>
116
+ <p className="border-t border-border px-3 py-1.5 text-xs text-muted">
117
+ 根据 CLI 的代码检查点和本轮的 Edit/Write 调用统计;Bash 命令和子 agent 改动的文件追踪不到,不在此列。
118
+ </p>
119
+ </div>
120
+ )}
121
+ </div>
122
+ );
123
+ }
124
+
125
+ function TurnFileDiffView({ pid, sid, turn, path }: { pid: string; sid: string; turn: string; path: string }) {
126
+ const diff = useQuery({
127
+ queryKey: ["turn-diff", pid, sid, turn, path],
128
+ queryFn: () => api<TurnFileDiff>(`/api/projects/${pid}/sessions/${sid}/turn-changes/diff?${new URLSearchParams({ turn, path })}`),
129
+ });
130
+ const rows = useMemo(() => {
131
+ const d = diff.data;
132
+ if (!d || d.binary) return [];
133
+ if (d.patches) return fromHunks(d.patches);
134
+ return lineDiff(d.before ?? "", d.after ?? "");
135
+ }, [diff.data]);
136
+ if (diff.isPending) return <p className="px-3 py-2 text-xs text-muted">加载中…</p>;
137
+ if (diff.isError) return <p className="px-3 py-2 text-xs text-danger">{diff.error instanceof ApiError ? diff.error.message : "读取失败"}</p>;
138
+ if (diff.data.binary) return <p className="px-3 py-2 text-xs text-muted">二进制文件,无法显示差异。</p>;
139
+ return (
140
+ <div className="border-y border-border" data-testid="turn-diff">
141
+ {diff.data.patches && <p className="bg-surface-2/60 px-3 py-1 text-xs text-muted">没有检查点,按本轮各次编辑的补丁显示。</p>}
142
+ {diff.data.truncated && <p className="bg-surface-2/60 px-3 py-1 text-xs text-muted">文件太大,只比较了前 1 MiB。</p>}
143
+ <DiffView rows={rows} />
144
+ </div>
145
+ );
146
+ }
147
+
148
+ // ---------- extra directories (/add-dir) ----------
149
+
150
+ let dirsDialogFor: string | undefined;
151
+ const dirsListeners = new Set<() => void>();
152
+ const setDirsDialog = (sid: string | undefined) => {
153
+ dirsDialogFor = sid;
154
+ for (const l of dirsListeners) l();
155
+ };
156
+ /** Open the extra-directories dialog of a session (e.g. from the session menu). */
157
+ export const openDirsDialog = (sid: string) => setDirsDialog(sid);
158
+
159
+ function useDirsDialog(sid: string) {
160
+ return useSyncExternalStore(
161
+ (l) => {
162
+ dirsListeners.add(l);
163
+ return () => dirsListeners.delete(l);
164
+ },
165
+ () => dirsDialogFor === sid,
166
+ );
167
+ }
168
+
169
+ function useExtraDirs(pid: string, sid: string) {
170
+ return useQuery({ queryKey: ["dirs", pid, sid], queryFn: () => api<ExtraDirsInfo>(`/api/projects/${pid}/sessions/${sid}/dirs`) });
171
+ }
172
+
173
+ /** `/add-dir <path>` from the composer. */
174
+ export function useAddDir(pid: string, sid: string) {
175
+ const qc = useQueryClient();
176
+ return async (path: string, remember = false) => {
177
+ const info = await api<ExtraDirsInfo>(`/api/projects/${pid}/sessions/${sid}/dirs`, { method: "POST", json: { path, remember } });
178
+ qc.setQueryData(["dirs", pid, sid], info);
179
+ };
180
+ }
181
+
182
+ const SOURCE_LABEL: Record<ExtraDirSource, string> = { session: "本会话", local: "已记住", project: "项目设置", user: "用户设置" };
183
+
184
+ /**
185
+ * Under the session header: the worktree the session runs in, and the directories outside the
186
+ * project it may access. Hidden when there is neither.
187
+ */
188
+ export function SessionContextBar({ pid, sid, meta }: { pid: string; sid: string; meta?: SessionListItem }) {
189
+ const dirs = useExtraDirs(pid, sid);
190
+ const open = useDirsDialog(sid);
191
+ const list = dirs.data?.dirs ?? [];
192
+ if (!meta?.worktree && list.length === 0 && !open) return null;
193
+ return (
194
+ <div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 border-b border-border px-4 py-1.5 text-xs text-muted md:px-6" data-testid="session-context">
195
+ {meta?.worktree && (
196
+ <Link to={`/p/${pid}/worktrees`} className="flex min-w-0 items-center gap-1 hover:text-accent" title={meta.cwd} data-testid="session-worktree">
197
+ <BranchIcon size={12} className="shrink-0" />
198
+ <span className="truncate">
199
+ worktree <span className="font-mono text-text">{meta.worktree}</span>
200
+ </span>
201
+ </Link>
202
+ )}
203
+ {list.length > 0 && (
204
+ <button className="flex min-w-0 items-center gap-1 hover:text-accent" onClick={() => setDirsDialog(sid)} data-testid="extra-dirs-button">
205
+ <FolderIcon size={12} className="shrink-0" />
206
+ <span className="truncate">额外目录 {list.length}</span>
207
+ </button>
208
+ )}
209
+ {open && <ExtraDirsDialog pid={pid} sid={sid} info={dirs.data} onClose={() => setDirsDialog(undefined)} />}
210
+ </div>
211
+ );
212
+ }
213
+
214
+ function ExtraDirsDialog({ pid, sid, info, onClose }: { pid: string; sid: string; info?: ExtraDirsInfo; onClose: () => void }) {
215
+ const qc = useQueryClient();
216
+ const addDir = useAddDir(pid, sid);
217
+ const [path, setPath] = useState("");
218
+ const [remember, setRemember] = useState(false);
219
+ const add = useMutation({
220
+ mutationFn: () => addDir(path, remember),
221
+ onSuccess: () => setPath(""),
222
+ });
223
+ const remove = useMutation({
224
+ mutationFn: (dir: string) => api<ExtraDirsInfo>(`/api/projects/${pid}/sessions/${sid}/dirs?${new URLSearchParams({ path: dir })}`, { method: "DELETE" }),
225
+ onSuccess: (data) => qc.setQueryData(["dirs", pid, sid], data),
226
+ });
227
+ return (
228
+ <Modal title="额外目录" onClose={onClose} testId="dirs-dialog">
229
+ <p className="text-sm text-muted">
230
+ 除了会话的工作目录{info && <span className="font-mono"> {info.cwd}</span>},Claude 还可以读写这些目录(相当于 CLI 的 /add-dir)。运行中的会话立即生效。
231
+ </p>
232
+ <ul className="mt-3 divide-y divide-border rounded-lg border border-border bg-surface empty:hidden" aria-label="额外目录列表">
233
+ {info?.dirs.map((d) => {
234
+ const removable = d.sources.includes("session") || d.sources.includes("local");
235
+ return (
236
+ <li key={d.path} className="flex min-w-0 items-center gap-2 py-2 pl-3 pr-1.5" data-testid="extra-dir" data-path={d.path}>
237
+ <FolderIcon className="shrink-0 text-muted" />
238
+ <div className="min-w-0 flex-1">
239
+ <div className={cx("truncate font-mono text-[13px]", !d.exists && "text-danger line-through")} title={d.path}>
240
+ {d.path}
241
+ </div>
242
+ <div className="mt-1 flex flex-wrap gap-1">
243
+ {d.sources.map((s) => (
244
+ <Tag key={s} tone={s === "session" ? "accent" : "neutral"}>
245
+ {SOURCE_LABEL[s]}
246
+ </Tag>
247
+ ))}
248
+ {!d.exists && <Tag tone="danger">目录不存在</Tag>}
249
+ </div>
250
+ </div>
251
+ {removable ? (
252
+ <Button variant="ghost" size="sm" className="text-danger hover:text-danger" onClick={() => remove.mutate(d.path)} disabled={remove.isPending}>
253
+ 移除
254
+ </Button>
255
+ ) : (
256
+ <span className="shrink-0 px-2 text-xs text-muted" title="在设置文件里,请在设置页修改">
257
+ 只读
258
+ </span>
259
+ )}
260
+ </li>
261
+ );
262
+ })}
263
+ </ul>
264
+ {info && info.dirs.length === 0 && <p className="mt-3 rounded-lg border border-dashed border-border px-3 py-4 text-center text-sm text-muted">还没有额外目录。在下面填一个绝对路径即可添加。</p>}
265
+ <form
266
+ className="mt-4 space-y-2"
267
+ onSubmit={(e) => {
268
+ e.preventDefault();
269
+ if (path.trim()) add.mutate();
270
+ }}
271
+ >
272
+ <input aria-label="目录路径" placeholder="/home/you/other-repo" value={path} onChange={(e) => setPath(e.target.value)} className={monoInputCls} />
273
+ <SwitchRow
274
+ checked={remember}
275
+ onChange={setRemember}
276
+ label="记住这个目录"
277
+ desc={
278
+ <>
279
+ 写进 <span className="font-mono">.claude/settings.local.json</span>,以后这个目录下的会话(包括 CLI)都能访问
280
+ </>
281
+ }
282
+ />
283
+ {(add.error || remove.error) && (
284
+ <p role="alert" className="text-xs text-danger">
285
+ {((add.error ?? remove.error) as Error).message}
286
+ </p>
287
+ )}
288
+ <div className="flex justify-end gap-2 border-t border-border pt-3">
289
+ <Button variant="ghost" onClick={onClose}>
290
+ 完成
291
+ </Button>
292
+ <Button type="submit" variant="primary" icon={<PlusSmIcon />} disabled={!path.trim()} loading={add.isPending}>
293
+ 添加
294
+ </Button>
295
+ </div>
296
+ </form>
297
+ </Modal>
298
+ );
299
+ }
@@ -0,0 +1,47 @@
1
+ import type { ReactNode } from "react";
2
+ import { Link, useLocation } from "react-router";
3
+ import { cx } from "../ui/Button.tsx";
4
+ import { BranchIcon, ChatIcon, DiffIcon, FileIcon } from "../ui/icons.tsx";
5
+ import { projectFilesUrl, useProjectFilesState } from "../project-files-state.ts";
6
+
7
+ const tabs = (pid: string, file: string) => [
8
+ { to: `/p/${pid}`, base: `/p/${pid}`, label: "项目概览", end: true, icon: <ChatIcon /> },
9
+ { to: projectFilesUrl(pid, file), base: `/p/${pid}/files`, label: "文件", end: false, icon: <FileIcon /> },
10
+ { to: `/p/${pid}/changes`, base: `/p/${pid}/changes`, label: "变更", end: false, icon: <DiffIcon /> },
11
+ { to: `/p/${pid}/worktrees`, base: `/p/${pid}/worktrees`, label: "工作区", end: false, icon: <BranchIcon /> },
12
+ ];
13
+ const activeTab = (pathname: string, tab: { base: string; end: boolean }) => pathname === tab.base || !tab.end && pathname.startsWith(`${tab.base}/`);
14
+
15
+ /** Project navigation, kept in the main page instead of duplicated in the sidebar. */
16
+ export function ProjectTabs({ projectId, children }: { projectId: string; children?: ReactNode }) {
17
+ const { pathname } = useLocation();
18
+ const state = useProjectFilesState(projectId);
19
+ return (
20
+ <header className="flex min-w-0 items-center gap-2 border-b border-border px-2 py-1.5 md:px-4">
21
+ <nav aria-label="项目页面" className="flex min-w-0 flex-1 gap-0.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
22
+ {tabs(projectId, state.file).map((t) => (
23
+ <Link
24
+ key={t.base}
25
+ to={t.to}
26
+ aria-current={activeTab(pathname, t) ? "page" : undefined}
27
+ className={cx(
28
+ "flex h-10 shrink-0 items-center gap-1.5 rounded-md px-3 text-sm transition-colors md:h-8",
29
+ activeTab(pathname, t) ? "bg-surface-2 font-medium text-text" : "text-muted hover:bg-surface-2/70 hover:text-text",
30
+ )}
31
+ >
32
+ <span aria-hidden className="hidden sm:inline">
33
+ {t.icon}
34
+ </span>
35
+ {t.label}
36
+ </Link>
37
+ ))}
38
+ </nav>
39
+ {children && <div className="flex shrink-0 items-center gap-1.5">{children}</div>}
40
+ </header>
41
+ );
42
+ }
43
+
44
+ export const encodePath = (p: string) => p.split("/").map(encodeURIComponent).join("/");
45
+
46
+ export const fileApi = (pid: string, endpoint: string, params: Record<string, string>) =>
47
+ `/api/projects/${pid}/${endpoint}?${new URLSearchParams(params).toString()}`;
@@ -0,0 +1,271 @@
1
+ import { useState } from "react";
2
+ import { Link, useNavigate } from "react-router";
3
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import type { SessionFileInfo, SessionListItem } from "@shared/protocol.ts";
5
+ import { api, ApiError } from "../api.ts";
6
+ import { setDraft } from "../drafts.ts";
7
+ import { Menu, MenuItem, Modal } from "./Modal.tsx";
8
+ import { CopyButton } from "./CopyButton.tsx";
9
+ import { Button } from "../ui/Button.tsx";
10
+ import { RadioCards } from "../ui/Controls.tsx";
11
+ import { copyText } from "../clipboard.ts";
12
+ import { openDirsDialog } from "./ProjectExtras.tsx";
13
+ import { SessionChannelsDialog } from "./SessionChannels.tsx";
14
+
15
+ type RewindMode = "conversation" | "code" | "both";
16
+ type FilesPreview = { canRewind: boolean; error?: string; filesChanged?: string[]; insertions?: number; deletions?: number };
17
+
18
+ function useInvalidateSessions(pid: string) {
19
+ const qc = useQueryClient();
20
+ return () => qc.invalidateQueries({ queryKey: ["sessions", pid] });
21
+ }
22
+
23
+ /** Header menu: rename, pin, archive, fork, export, delete. */
24
+ export function SessionMenu({ pid, sid, meta }: { pid: string; sid: string; meta?: SessionListItem }) {
25
+ const navigate = useNavigate();
26
+ const invalidate = useInvalidateSessions(pid);
27
+ const [renaming, setRenaming] = useState(false);
28
+ const [deleting, setDeleting] = useState(false);
29
+ const [channels, setChannels] = useState(false);
30
+ const [info, setInfo] = useState(false);
31
+ const patch = (body: object) => api(`/api/projects/${pid}/sessions/${sid}`, { method: "PATCH", json: body }).then(invalidate);
32
+ const fork = useMutation({
33
+ mutationFn: () => api<{ id: string }>(`/api/projects/${pid}/sessions/${sid}/fork`, { method: "POST", json: {} }),
34
+ onSuccess: ({ id }) => {
35
+ invalidate();
36
+ navigate(`/p/${pid}/s/${id}`);
37
+ },
38
+ });
39
+
40
+ return (
41
+ <>
42
+ <Menu label="会话操作" testId="session-menu">
43
+ <MenuItem onClick={() => setRenaming(true)}>重命名</MenuItem>
44
+ <MenuItem onClick={() => patch({ pinned: !meta?.pinned })}>{meta?.pinned ? "取消置顶" : "置顶"}</MenuItem>
45
+ <MenuItem onClick={() => patch({ archived: !meta?.archived })}>{meta?.archived ? "取消归档" : "归档"}</MenuItem>
46
+ <MenuItem onClick={() => fork.mutate()}>Fork 整个会话</MenuItem>
47
+ <MenuItem onClick={() => openDirsDialog(sid)}>额外目录…</MenuItem>
48
+ <MenuItem onClick={() => setChannels(true)}>推送与订阅…</MenuItem>
49
+ <MenuItem onClick={() => window.open(`/api/projects/${pid}/sessions/${sid}/export`, "_blank")}>导出对话</MenuItem>
50
+ <MenuItem onClick={() => void copyText(sid)}>复制会话 ID</MenuItem>
51
+ <MenuItem onClick={() => setInfo(true)}>会话文件…</MenuItem>
52
+ <MenuItem onClick={() => void copyText(`claude --resume ${sid}`)}>复制 CLI 续聊命令</MenuItem>
53
+ <MenuItem danger onClick={() => setDeleting(true)}>
54
+ 删除
55
+ </MenuItem>
56
+ </Menu>
57
+ {renaming && <RenameDialog current={meta?.title ?? ""} onClose={() => setRenaming(false)} onSave={(title) => patch({ title }).then(() => setRenaming(false))} />}
58
+ {channels && <SessionChannelsDialog key={sid} pid={pid} sid={sid} onClose={() => setChannels(false)} />}
59
+ {info && <SessionFileDialog pid={pid} sid={sid} onClose={() => setInfo(false)} />}
60
+ {deleting && (
61
+ <Modal title="删除会话?" onClose={() => setDeleting(false)} testId="delete-dialog">
62
+ <p className="text-sm text-muted">会话文件会从磁盘删除,CLI 里也将看不到它。这个操作无法撤销。</p>
63
+ <div className="mt-5 flex justify-end gap-2">
64
+ <Button variant="ghost" onClick={() => setDeleting(false)}>
65
+ 取消
66
+ </Button>
67
+ <Button
68
+ variant="danger"
69
+ onClick={async () => {
70
+ await api(`/api/projects/${pid}/sessions/${sid}`, { method: "DELETE" });
71
+ invalidate();
72
+ navigate(`/p/${pid}`);
73
+ }}
74
+ >
75
+ 确认删除
76
+ </Button>
77
+ </div>
78
+ </Modal>
79
+ )}
80
+ </>
81
+ );
82
+ }
83
+
84
+ function RenameDialog({ current, onClose, onSave }: { current: string; onClose: () => void; onSave: (t: string) => void }) {
85
+ const [title, setTitle] = useState(current);
86
+ return (
87
+ <Modal title="重命名会话" onClose={onClose}>
88
+ <form
89
+ onSubmit={(e) => {
90
+ e.preventDefault();
91
+ if (title.trim()) onSave(title.trim());
92
+ }}
93
+ >
94
+ <input
95
+ aria-label="会话标题"
96
+ autoFocus
97
+ value={title}
98
+ onChange={(e) => setTitle(e.target.value)}
99
+ className="h-10 w-full rounded-md border border-border bg-surface px-3 text-base outline-none focus:border-accent/60 focus:ring-2 focus:ring-accent/20 md:h-9 md:text-sm"
100
+ />
101
+ <div className="mt-4 flex justify-end gap-2">
102
+ <Button variant="ghost" onClick={onClose}>
103
+ 取消
104
+ </Button>
105
+ <Button type="submit" variant="primary">
106
+ 保存
107
+ </Button>
108
+ </div>
109
+ </form>
110
+ </Modal>
111
+ );
112
+ }
113
+
114
+ /** Session ID, working directory and transcript path, each copyable. */
115
+ function SessionFileDialog({ pid, sid, onClose }: { pid: string; sid: string; onClose: () => void }) {
116
+ const info = useQuery({
117
+ queryKey: ["session-info", pid, sid],
118
+ queryFn: () => api<SessionFileInfo>(`/api/projects/${pid}/sessions/${sid}/info`),
119
+ staleTime: 0,
120
+ });
121
+ return (
122
+ <Modal title="会话文件" onClose={onClose} testId="session-info-dialog">
123
+ <dl className="space-y-4 text-sm">
124
+ <InfoRow label="会话 ID" value={sid} testId="session-info-id" />
125
+ {info.isPending ? (
126
+ <p className="text-xs text-muted">正在读取会话文件…</p>
127
+ ) : info.error ? (
128
+ <p role="alert" className="text-xs text-danger">读取失败:{info.error instanceof Error ? info.error.message : String(info.error)}</p>
129
+ ) : (
130
+ <>
131
+ <InfoRow label="工作目录" value={info.data.cwd} testId="session-info-cwd" />
132
+ <InfoRow label="会话文件" value={info.data.file} empty="还没有写入磁盘。发送第一条消息后,CLI 会创建这个文件。" testId="session-info-file" />
133
+ </>
134
+ )}
135
+ </dl>
136
+ <p className="mt-5 text-xs leading-relaxed text-muted">会话文件是 JSONL 格式,每行一条消息,可以用文本编辑器直接查看或备份。</p>
137
+ </Modal>
138
+ );
139
+ }
140
+
141
+ function InfoRow({ label, value, empty, testId }: { label: string; value: string | null; empty?: string; testId: string }) {
142
+ return (
143
+ <div>
144
+ <dt className="mb-1 text-xs text-muted">{label}</dt>
145
+ {value ? (
146
+ <dd className="flex items-start gap-2">
147
+ <code className="min-w-0 flex-1 break-all rounded-md bg-surface-2 px-2 py-1.5 font-mono text-xs" data-testid={testId}>{value}</code>
148
+ <CopyButton text={value} label={`复制${label}`} className="shrink-0" />
149
+ </dd>
150
+ ) : (
151
+ <dd className="text-xs text-muted" data-testid={testId}>{empty}</dd>
152
+ )}
153
+ </div>
154
+ );
155
+ }
156
+
157
+ /** "Forked from …" link back to the origin session. */
158
+ export function Lineage({ pid, meta, sessions }: { pid: string; meta?: SessionListItem; sessions?: SessionListItem[] }) {
159
+ if (!meta?.forkedFrom) return null;
160
+ const origin = sessions?.find((s) => s.id === meta.forkedFrom);
161
+ return (
162
+ <Link to={`/p/${pid}/s/${meta.forkedFrom}`} className="truncate text-xs text-muted hover:text-accent" data-testid="lineage">
163
+ ⑂ 分支自:{origin?.title || meta.forkedFrom.slice(0, 8)}
164
+ </Link>
165
+ );
166
+ }
167
+
168
+ /** Actions under a user message: fork from before it, or rewind to it. */
169
+ export function UserMessageActions({ pid, sid, uuid }: { pid: string; sid: string; uuid: string }) {
170
+ const navigate = useNavigate();
171
+ const invalidate = useInvalidateSessions(pid);
172
+ const [rewinding, setRewinding] = useState(false);
173
+ const [error, setError] = useState<string | null>(null);
174
+ async function fork() {
175
+ try {
176
+ const { id } = await api<{ id: string }>(`/api/projects/${pid}/sessions/${sid}/fork`, { method: "POST", json: { beforeUuid: uuid } });
177
+ invalidate();
178
+ navigate(`/p/${pid}/s/${id}`);
179
+ } catch (e) {
180
+ setError(e instanceof ApiError ? e.message : "fork 失败");
181
+ }
182
+ }
183
+ return (
184
+ <div className="flex justify-end">
185
+ <Menu label="消息操作" testId="message-menu">
186
+ <MenuItem onClick={fork}>从这条消息之前 fork</MenuItem>
187
+ <MenuItem onClick={() => setRewinding(true)}>回退到这里…</MenuItem>
188
+ </Menu>
189
+ {error && <span className="text-xs text-danger">{error}</span>}
190
+ {rewinding && <RewindDialog pid={pid} sid={sid} uuid={uuid} onClose={() => setRewinding(false)} />}
191
+ </div>
192
+ );
193
+ }
194
+
195
+ const MODES: { value: RewindMode; title: string; detail: string }[] = [
196
+ {
197
+ value: "conversation",
198
+ title: "只回退对话",
199
+ detail: "这条消息和它之后的对话从当前会话移除,消息内容放回输入框,可以修改后重新发送。项目文件不变。原来的对话仍保存在会话文件里,可以 fork 找回。",
200
+ },
201
+ {
202
+ value: "code",
203
+ title: "只回退代码",
204
+ detail: "把 Claude 从这条消息起用 Edit/Write 改过的文件恢复到发送前的状态。对话不变。用 Bash 命令改动的文件不会恢复。",
205
+ },
206
+ { value: "both", title: "对话和代码都回退", detail: "先恢复文件,再回退对话。" },
207
+ ];
208
+
209
+ function RewindDialog({ pid, sid, uuid, onClose }: { pid: string; sid: string; uuid: string; onClose: () => void }) {
210
+ const navigate = useNavigate();
211
+ const qc = useQueryClient();
212
+ const [mode, setMode] = useState<RewindMode>("conversation");
213
+ const [error, setError] = useState<string | null>(null);
214
+ const [busy, setBusy] = useState(false);
215
+ const preview = useQuery({
216
+ queryKey: ["rewind-preview", sid, uuid],
217
+ queryFn: () => api<{ files?: FilesPreview }>(`/api/projects/${pid}/sessions/${sid}/rewind`, { method: "POST", json: { userUuid: uuid, mode: "code", dryRun: true } }),
218
+ enabled: mode !== "conversation",
219
+ staleTime: 0,
220
+ });
221
+ const files = preview.data?.files;
222
+
223
+ async function confirm() {
224
+ setBusy(true);
225
+ setError(null);
226
+ try {
227
+ const r = await api<{ text: string; rewindId?: string; files?: FilesPreview; newSessionId?: string }>(`/api/projects/${pid}/sessions/${sid}/rewind`, {
228
+ method: "POST",
229
+ json: { userUuid: uuid, mode },
230
+ });
231
+ if (r.files && !r.files.canRewind) throw new ApiError(400, r.files.error || "无法回退代码");
232
+ qc.invalidateQueries({ queryKey: ["sessions", pid] });
233
+ if (mode !== "code") {
234
+ const target = r.newSessionId ?? sid;
235
+ setDraft(target, r.text, r.rewindId);
236
+ if (r.newSessionId) navigate(`/p/${pid}/s/${r.newSessionId}`);
237
+ }
238
+ onClose();
239
+ } catch (e) {
240
+ setError(e instanceof ApiError ? e.message : "回退失败");
241
+ } finally {
242
+ setBusy(false);
243
+ }
244
+ }
245
+
246
+ return (
247
+ <Modal title="回退到这条消息" onClose={onClose} testId="rewind-dialog">
248
+ <RadioCards label="回退方式" value={mode} onChange={setMode} options={MODES.map((m) => ({ value: m.value, title: m.title, detail: m.detail }))} />
249
+ {mode !== "conversation" && (
250
+ <div className="mt-3 rounded-lg bg-surface-2 px-3 py-2 text-xs" data-testid="rewind-preview">
251
+ {preview.isPending
252
+ ? "正在计算会恢复哪些文件…"
253
+ : files?.canRewind
254
+ ? files.filesChanged?.length
255
+ ? `将恢复 ${files.filesChanged.length} 个文件(+${files.insertions ?? 0} −${files.deletions ?? 0}):${files.filesChanged.join("、")}`
256
+ : "没有需要恢复的文件。"
257
+ : `无法回退代码:${files?.error ?? "没有检查点"}`}
258
+ </div>
259
+ )}
260
+ {error && <p className="mt-3 text-sm text-danger">{error}</p>}
261
+ <div className="mt-5 flex justify-end gap-2">
262
+ <Button variant="ghost" onClick={onClose}>
263
+ 取消
264
+ </Button>
265
+ <Button variant="primary" loading={busy} onClick={confirm}>
266
+ 回退
267
+ </Button>
268
+ </div>
269
+ </Modal>
270
+ );
271
+ }