@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,552 @@
1
+ import { uuid } from "../uuid.ts";
2
+ import { useEffect, useMemo, useRef, useState, type ClipboardEvent, type DragEvent, type KeyboardEvent } from "react";
3
+ import type { CommandInfo, SessionState } from "@shared/protocol.ts";
4
+ import { sendClient } from "../live.ts";
5
+ import { useCommands } from "../hooks.ts";
6
+ import { CloseIcon, ImageIcon, SendIcon, StopIcon } from "./icons.tsx";
7
+ import { RefreshIcon } from "../ui/icons.tsx";
8
+ import { addDraftImages, clearDraft, editDraft, getDraft, useDrafts, type Attached } from "../drafts.ts";
9
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
10
+ import { useNavigate } from "react-router";
11
+ import type { FileMatch } from "@shared/files.ts";
12
+ import { api } from "../api.ts";
13
+ import { useConfirm } from "./Confirmation.tsx";
14
+ import { useClearGoal, useCompactSession } from "../session-confirmations.ts";
15
+ import { consumeAnnotations, formatAnnotatedMessage, snapshotAnnotations, useAnnotations } from "../annotations.ts";
16
+ import { ComposerAnnotations } from "./ComposerAnnotations.tsx";
17
+
18
+ const isTouch = typeof window !== "undefined" && window.matchMedia("(pointer: coarse)").matches;
19
+
20
+ export type ComposerProps = {
21
+ projectId: string;
22
+ sessionId: string;
23
+ state: SessionState;
24
+ /** A task list was restored, but this CLI process has not confirmed it yet. */
25
+ cronsStale?: boolean;
26
+ /** `/btw <question>` — ask a side question in a fork. */
27
+ onBtw?: (question: string) => Promise<void>;
28
+ /** `/clear` — start over in a new session. */
29
+ onClear?: () => Promise<boolean>;
30
+ /** `/add-dir <path>` — let the session access a directory outside the project. */
31
+ onAddDir?: (path: string) => Promise<void>;
32
+ };
33
+
34
+ export function Composer({ projectId, sessionId, state, cronsStale, onBtw, onClear, onAddDir }: ComposerProps) {
35
+ const confirm = useConfirm();
36
+ const compact = useCompactSession(projectId, sessionId);
37
+ const clearGoal = useClearGoal(projectId, sessionId);
38
+ const submitting = useRef(false);
39
+ const navigate = useNavigate();
40
+ const queryClient = useQueryClient();
41
+ const [initialDraft] = useState(() => getDraft(sessionId));
42
+ const draft = useDrafts((s) => s.drafts[sessionId]) ?? initialDraft;
43
+ const hasAnnotations = useAnnotations((s) => s.items.some((item) => item.projectId === projectId && item.targetSessionId === sessionId));
44
+ const { text, images, processing } = draft;
45
+ const setText = (text: string) => editDraft(sessionId, { text });
46
+ const setImages = (fn: (images: Attached[]) => Attached[]) => editDraft(sessionId, { images: fn(getDraft(sessionId).images) });
47
+ const [error, setError] = useState<string>();
48
+ const [refreshRequested, setRefreshRequested] = useState(false);
49
+ const [dragging, setDragging] = useState(false);
50
+ const ref = useRef<HTMLTextAreaElement>(null);
51
+ const fileRef = useRef<HTMLInputElement>(null);
52
+ const running = state !== "idle";
53
+ const commands = useCommands(projectId);
54
+ const [menuIndex, setMenuIndex] = useState(0);
55
+ const [menuDismissed, setMenuDismissed] = useState(false);
56
+ const slashQuery = hasAnnotations ? undefined : /^\/(\S*)$/.exec(text)?.[1];
57
+ const matches = useMemo(() => (slashQuery === undefined ? [] : filterCommands(commands.data ?? [], slashQuery)), [commands.data, slashQuery]);
58
+ const menuOpen = slashQuery !== undefined && !menuDismissed && matches.length > 0;
59
+ useEffect(() => setMenuIndex(0), [slashQuery]);
60
+
61
+ // The refresh is one internal turn; if the server never confirms it, let the user try again.
62
+ useEffect(() => {
63
+ if (!cronsStale) setRefreshRequested(false);
64
+ }, [cronsStale]);
65
+ useEffect(() => {
66
+ if (!refreshRequested) return;
67
+ const timer = setTimeout(() => setRefreshRequested(false), 30_000);
68
+ return () => clearTimeout(timer);
69
+ }, [refreshRequested]);
70
+
71
+ // @file references: complete the word under the caret against project file names.
72
+ const [caret, setCaret] = useState(0);
73
+ const mention = /(?:^|\s)@([^\s@]*)$/.exec(text.slice(0, caret));
74
+ const mentionQuery = mention?.[1];
75
+ const fileSearch = useQuery({
76
+ queryKey: ["mention", projectId, mentionQuery],
77
+ queryFn: () =>
78
+ api<{ matches: FileMatch[] }>(`/api/projects/${projectId}/search/files?limit=20&q=${encodeURIComponent(mentionQuery ?? "")}`).then((r) => r.matches),
79
+ enabled: mentionQuery !== undefined,
80
+ staleTime: 30_000,
81
+ placeholderData: (prev) => prev,
82
+ });
83
+ const fileMatches = mentionQuery !== undefined && !menuDismissed ? (fileSearch.data ?? []) : [];
84
+ const fileMenuOpen = !menuOpen && fileMatches.length > 0;
85
+ useEffect(() => setMenuIndex(0), [mentionQuery]);
86
+
87
+ function chooseFile(f: FileMatch) {
88
+ const before = text.slice(0, caret).replace(/@[^\s@]*$/, `@${f.path} `);
89
+ const next = before + text.slice(caret);
90
+ setText(next);
91
+ setMenuDismissed(false);
92
+ requestAnimationFrame(() => {
93
+ const el = ref.current;
94
+ if (!el) return;
95
+ el.focus();
96
+ el.setSelectionRange(before.length, before.length);
97
+ setCaret(before.length);
98
+ autosize(el);
99
+ });
100
+ }
101
+
102
+ function reset(revision: number) {
103
+ clearDraft(sessionId, revision);
104
+ setError(undefined);
105
+ requestAnimationFrame(() => autosize(ref.current));
106
+ }
107
+
108
+ async function submit() {
109
+ if (submitting.current) return;
110
+ submitting.current = true;
111
+ try { await submitDraft(); }
112
+ catch (err) { setError(err instanceof Error ? err.message : String(err)); }
113
+ finally { submitting.current = false; }
114
+ }
115
+
116
+ async function submitDraft() {
117
+ const submittedDraft = getDraft(sessionId);
118
+ if (submittedDraft.processing) return;
119
+ const { text, images, revision } = submittedDraft;
120
+ const annotations = snapshotAnnotations(projectId, sessionId);
121
+ const value = text.trim();
122
+ if (!value && images.length === 0 && annotations.length === 0) return;
123
+ setError(undefined);
124
+ const resetSubmitted = () => reset(revision);
125
+ const sendMessage = () => {
126
+ sendClient({
127
+ t: "send",
128
+ sessionId,
129
+ projectId,
130
+ uuid: uuid(),
131
+ text: annotations.length ? formatAnnotatedMessage(value, annotations) : value,
132
+ ...(images.length ? { images: images.map(({ mediaType, data }) => ({ mediaType, data })) } : {}),
133
+ });
134
+ // sendClient synchronously sends or queues the payload; it has no per-message acknowledgement.
135
+ // Consume only the submitted revisions so edits made during a send keep their drafts.
136
+ if (annotations.length) consumeAnnotations(annotations);
137
+ resetSubmitted();
138
+ };
139
+ if (annotations.length) return sendMessage();
140
+ if (value.startsWith("!") && images.length === 0) {
141
+ const command = value.slice(1).trim();
142
+ if (!command) return;
143
+ sendClient({ t: "bash", sessionId, projectId, uuid: uuid(), command });
144
+ return resetSubmitted();
145
+ }
146
+ const btw = /^\/btw(?:\s+([\s\S]*))?$/.exec(value);
147
+ if (btw && onBtw) {
148
+ const question = btw[1]?.trim() ?? "";
149
+ try {
150
+ await onBtw(question);
151
+ resetSubmitted();
152
+ } catch (err) {
153
+ setError(err instanceof Error ? err.message : String(err));
154
+ }
155
+ return;
156
+ }
157
+ const addDir = /^\/add-dir(?:\s+([\s\S]*))?$/.exec(value);
158
+ if (addDir && onAddDir) {
159
+ const dir = addDir[1]?.trim();
160
+ if (!dir) return setError("用法:/add-dir 绝对路径");
161
+ try {
162
+ await onAddDir(dir);
163
+ resetSubmitted();
164
+ } catch (err) {
165
+ setError(err instanceof Error ? err.message : String(err));
166
+ }
167
+ return;
168
+ }
169
+ // Terminal-menu commands the web handles itself.
170
+ const cd = /^\/cd\s+(\S[\s\S]*)$/.exec(value);
171
+ if (cd) {
172
+ try {
173
+ const r = await api<{ projectId: string }>(`/api/projects/${projectId}/sessions/${sessionId}/cd`, { method: "POST", json: { path: cd[1]!.trim() } });
174
+ resetSubmitted();
175
+ queryClient.invalidateQueries({ queryKey: ["projects"] });
176
+ navigate(`/p/${r.projectId}/s/${sessionId}`);
177
+ } catch (err) {
178
+ setError(err instanceof Error ? err.message : String(err));
179
+ }
180
+ return;
181
+ }
182
+ const feedback = /^\/(?:feedback|bug)(?:\s+([\s\S]*))?$/.exec(value);
183
+ if (feedback) {
184
+ const description = feedback[1]?.trim();
185
+ if (!description) return setError("用法:/feedback 你想反馈的内容(提交前可选择是否附带会话记录)");
186
+ const answer = await confirm({
187
+ title: "提交反馈给 Anthropic?",
188
+ description,
189
+ confirmLabel: "提交反馈",
190
+ checkbox: { label: "附带本会话记录(可能包含代码和敏感内容)", defaultChecked: false },
191
+ });
192
+ if (!answer.confirmed) return;
193
+ try {
194
+ await api(`/api/projects/${projectId}/sessions/${sessionId}/feedback`, { method: "POST", json: { description, attachTranscript: answer.checked } });
195
+ resetSubmitted();
196
+ setError("反馈已提交,谢谢。");
197
+ } catch (err) {
198
+ setError(err instanceof Error ? err.message : String(err));
199
+ }
200
+ return;
201
+ }
202
+ if (value === "/release-notes") {
203
+ window.open("https://code.claude.com/docs/en/changelog", "_blank", "noopener");
204
+ return resetSubmitted();
205
+ }
206
+ if (value === "/clear" && onClear) {
207
+ try {
208
+ if (await onClear()) resetSubmitted();
209
+ } catch (error) {
210
+ setError(error instanceof Error ? error.message : String(error));
211
+ }
212
+ return;
213
+ }
214
+ const compactCommand = /^\/compact(?:\s+([\s\S]*))?$/.exec(value);
215
+ if (compactCommand) {
216
+ if (await compact(compactCommand[1], images.map(({ mediaType, data }) => ({ mediaType, data })))) resetSubmitted();
217
+ return;
218
+ }
219
+ if (/^\/goal\s+clear$/.test(value)) {
220
+ if (await clearGoal()) resetSubmitted();
221
+ return;
222
+ }
223
+ sendMessage();
224
+ }
225
+
226
+ function choose(c: CommandInfo) {
227
+ setText(`/${c.name} `);
228
+ setMenuDismissed(false);
229
+ requestAnimationFrame(() => {
230
+ const el = ref.current;
231
+ if (!el) return;
232
+ el.focus();
233
+ el.setSelectionRange(el.value.length, el.value.length);
234
+ autosize(el);
235
+ });
236
+ }
237
+
238
+ function onKeyDown(e: KeyboardEvent<HTMLTextAreaElement>) {
239
+ if (e.nativeEvent.isComposing) return;
240
+ if (fileMenuOpen) {
241
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
242
+ e.preventDefault();
243
+ const d = e.key === "ArrowDown" ? 1 : -1;
244
+ setMenuIndex((i) => (i + d + fileMatches.length) % fileMatches.length);
245
+ return;
246
+ }
247
+ if (e.key === "Enter" || e.key === "Tab") {
248
+ const f = fileMatches[menuIndex];
249
+ if (f) {
250
+ e.preventDefault();
251
+ chooseFile(f);
252
+ return;
253
+ }
254
+ }
255
+ if (e.key === "Escape") {
256
+ e.preventDefault();
257
+ setMenuDismissed(true);
258
+ return;
259
+ }
260
+ }
261
+ if (menuOpen) {
262
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
263
+ e.preventDefault();
264
+ const d = e.key === "ArrowDown" ? 1 : -1;
265
+ setMenuIndex((i) => (i + d + matches.length) % matches.length);
266
+ return;
267
+ }
268
+ if (e.key === "Enter" || e.key === "Tab") {
269
+ const c = matches[menuIndex];
270
+ // An exact, complete command name sends right away on Enter.
271
+ if (c && !(e.key === "Enter" && slashQuery === c.name && !c.argumentHint)) {
272
+ e.preventDefault();
273
+ choose(c);
274
+ return;
275
+ }
276
+ }
277
+ if (e.key === "Escape") {
278
+ e.preventDefault();
279
+ setMenuDismissed(true);
280
+ return;
281
+ }
282
+ }
283
+ if (e.key === "Enter" && !e.shiftKey && !isTouch) {
284
+ e.preventDefault();
285
+ void submit();
286
+ }
287
+ }
288
+
289
+ async function addFiles(files: Iterable<File>) {
290
+ const list = [...files].filter((f) => f.type.startsWith("image/"));
291
+ if (!list.length) return false;
292
+ try {
293
+ await addDraftImages(sessionId, list.length, () => Promise.all(list.map(prepareImage)));
294
+ } catch {
295
+ setError("图片读取失败");
296
+ }
297
+ return true;
298
+ }
299
+
300
+ function onPaste(e: ClipboardEvent<HTMLTextAreaElement>) {
301
+ const files = [...e.clipboardData.files].filter((f) => f.type.startsWith("image/"));
302
+ if (files.length) {
303
+ e.preventDefault();
304
+ void addFiles(files);
305
+ }
306
+ }
307
+
308
+ function onDrop(e: DragEvent) {
309
+ setDragging(false);
310
+ if (e.dataTransfer.files.length) {
311
+ e.preventDefault();
312
+ void addFiles(e.dataTransfer.files);
313
+ }
314
+ }
315
+
316
+ const bangMode = !hasAnnotations && text.trimStart().startsWith("!");
317
+
318
+ return (
319
+ <div className="relative">
320
+ {fileMenuOpen && <FileMenu items={fileMatches} active={menuIndex} onHover={setMenuIndex} onChoose={chooseFile} />}
321
+ {menuOpen && (
322
+ <CommandMenu
323
+ items={matches}
324
+ active={menuIndex}
325
+ onHover={setMenuIndex}
326
+ onChoose={choose}
327
+ />
328
+ )}
329
+ <div
330
+ className={`rounded-2xl border bg-surface shadow-sm focus-within:border-accent/60 ${dragging ? "border-accent" : bangMode ? "border-accent/60" : "border-border"}`}
331
+ onDragOver={(e) => {
332
+ if ([...e.dataTransfer.types].includes("Files")) {
333
+ e.preventDefault();
334
+ setDragging(true);
335
+ }
336
+ }}
337
+ onDragLeave={() => setDragging(false)}
338
+ onDrop={onDrop}
339
+ data-testid="composer"
340
+ data-session-id={sessionId}
341
+ >
342
+ <ComposerAnnotations projectId={projectId} sessionId={sessionId} />
343
+ {images.length > 0 && (
344
+ <div className="flex flex-wrap gap-2 px-3 pt-3" data-testid="attachments">
345
+ {images.map((img) => (
346
+ <div key={img.id} className="relative">
347
+ <img src={img.url} alt="待发送的图片" className="size-16 rounded-lg border border-border object-cover" />
348
+ <button
349
+ className="absolute -right-2 -top-2 flex size-6 items-center justify-center rounded-full border border-border bg-surface text-muted shadow-sm hover:text-text"
350
+ onClick={() => setImages((cur) => cur.filter((x) => x.id !== img.id))}
351
+ aria-label="移除图片"
352
+ >
353
+ <CloseIcon size={12} />
354
+ </button>
355
+ </div>
356
+ ))}
357
+ </div>
358
+ )}
359
+ <textarea
360
+ ref={ref}
361
+ aria-label="消息"
362
+ rows={1}
363
+ value={text}
364
+ placeholder={running ? "继续输入,消息会排进下一轮…" : "给 Claude 发消息(/ 命令,! 执行命令)"}
365
+ onSelect={(e) => setCaret(e.currentTarget.selectionStart)}
366
+ onChange={(e) => {
367
+ setText(e.target.value);
368
+ setCaret(e.target.selectionStart);
369
+ setMenuDismissed(false);
370
+ autosize(e.target);
371
+ }}
372
+ onKeyDown={onKeyDown}
373
+ onPaste={onPaste}
374
+ className={`block max-h-[40dvh] w-full resize-none bg-transparent px-4 pt-3 text-base outline-none ${bangMode ? "font-mono" : ""}`}
375
+ />
376
+ <div className="flex items-center gap-2 px-2 pb-2">
377
+ <input
378
+ ref={fileRef}
379
+ type="file"
380
+ accept="image/*"
381
+ multiple
382
+ hidden
383
+ data-testid="image-input"
384
+ onChange={(e) => {
385
+ if (e.target.files) void addFiles(e.target.files);
386
+ e.target.value = "";
387
+ }}
388
+ />
389
+ <button
390
+ onClick={() => fileRef.current?.click()}
391
+ className="flex size-9 items-center justify-center rounded-full text-muted hover:bg-surface-2 hover:text-text"
392
+ aria-label="添加图片"
393
+ title="添加图片"
394
+ >
395
+ <ImageIcon />
396
+ </button>
397
+ {cronsStale && (
398
+ <button
399
+ type="button"
400
+ onClick={() => {
401
+ if (refreshRequested) return;
402
+ setRefreshRequested(true);
403
+ sendClient({ t: "refreshCrons", sessionId, projectId });
404
+ }}
405
+ disabled={running || refreshRequested}
406
+ className="flex size-9 items-center justify-center rounded-full text-muted hover:bg-surface-2 hover:text-text disabled:opacity-40"
407
+ aria-label="刷新定时任务"
408
+ title="重新确认定时任务(会发送一条刷新消息)"
409
+ data-testid="refresh-crons"
410
+ >
411
+ <RefreshIcon size={18} className={refreshRequested ? "animate-spin" : undefined} />
412
+ </button>
413
+ )}
414
+ <span className="min-w-0 flex-1 truncate text-xs text-muted">
415
+ {error ? <span className="text-danger" role="alert">{error}</span> : processing ? "正在处理图片…" : bangMode ? "在项目目录执行命令,输出会带进对话" : null}
416
+ </span>
417
+ {running && (
418
+ <button
419
+ onClick={() => sendClient({ t: "interrupt", sessionId })}
420
+ className="flex size-9 items-center justify-center rounded-full border border-border text-text hover:bg-surface-2"
421
+ aria-label="中断"
422
+ title="中断"
423
+ >
424
+ <StopIcon />
425
+ </button>
426
+ )}
427
+ <button
428
+ onClick={() => void submit()}
429
+ disabled={processing > 0 || (!text.trim() && images.length === 0 && !hasAnnotations)}
430
+ className="flex size-9 items-center justify-center rounded-full bg-accent text-accent-text disabled:opacity-40"
431
+ aria-label="发送"
432
+ title="发送"
433
+ >
434
+ <SendIcon />
435
+ </button>
436
+ </div>
437
+ </div>
438
+ </div>
439
+ );
440
+ }
441
+
442
+ function FileMenu({ items, active, onHover, onChoose }: { items: FileMatch[]; active: number; onHover: (i: number) => void; onChoose: (f: FileMatch) => void }) {
443
+ return (
444
+ <div role="listbox" aria-label="文件" className="absolute inset-x-0 bottom-full z-30 mb-2 max-h-[45dvh] overflow-y-auto overscroll-contain rounded-xl border border-border bg-surface py-1 shadow-lg">
445
+ {items.map((f, i) => (
446
+ <button
447
+ key={f.path}
448
+ role="option"
449
+ aria-selected={i === active}
450
+ onMouseDown={(e) => e.preventDefault()}
451
+ onMouseEnter={() => onHover(i)}
452
+ onClick={() => onChoose(f)}
453
+ className={`block w-full truncate px-3 py-2 text-left font-mono text-sm ${i === active ? "bg-surface-2" : ""}`}
454
+ >
455
+ @{f.path}
456
+ </button>
457
+ ))}
458
+ </div>
459
+ );
460
+ }
461
+
462
+ function CommandMenu({ items, active, onHover, onChoose }: { items: CommandInfo[]; active: number; onHover: (i: number) => void; onChoose: (c: CommandInfo) => void }) {
463
+ const listRef = useRef<HTMLDivElement>(null);
464
+ useEffect(() => {
465
+ listRef.current?.querySelector(`[data-index="${active}"]`)?.scrollIntoView({ block: "nearest" });
466
+ }, [active]);
467
+ return (
468
+ <div
469
+ ref={listRef}
470
+ role="listbox"
471
+ aria-label="命令"
472
+ className="absolute inset-x-0 bottom-full z-30 mb-2 max-h-[45dvh] overflow-y-auto overscroll-contain rounded-xl border border-border bg-surface py-1 shadow-lg"
473
+ >
474
+ {items.map((c, i) => (
475
+ <button
476
+ key={c.name}
477
+ data-index={i}
478
+ role="option"
479
+ aria-selected={i === active}
480
+ // Keep focus in the textarea so the keyboard stays open on phones.
481
+ onMouseDown={(e) => e.preventDefault()}
482
+ onMouseEnter={() => onHover(i)}
483
+ onClick={() => onChoose(c)}
484
+ className={`block w-full px-3 py-2 text-left ${i === active ? "bg-surface-2" : ""}`}
485
+ >
486
+ <div className="flex min-w-0 items-baseline gap-2">
487
+ <span className="shrink-0 font-mono text-sm text-text">/{c.name}</span>
488
+ {c.argumentHint && <span className="min-w-0 truncate font-mono text-xs text-muted">{c.argumentHint}</span>}
489
+ </div>
490
+ {c.description && <div className="line-clamp-2 text-xs text-muted">{c.description}</div>}
491
+ </button>
492
+ ))}
493
+ </div>
494
+ );
495
+ }
496
+
497
+ /** Prefix matches on the name first, then substring matches, then description/alias matches. */
498
+ function filterCommands(all: CommandInfo[], q: string): CommandInfo[] {
499
+ const s = q.toLowerCase();
500
+ if (!s) return all;
501
+ const rank = (c: CommandInfo) => {
502
+ const n = c.name.toLowerCase();
503
+ if (n.startsWith(s)) return 0;
504
+ if (c.aliases?.some((a) => a.toLowerCase().startsWith(s))) return 1;
505
+ if (n.includes(s)) return 2;
506
+ if (c.description.toLowerCase().includes(s)) return 3;
507
+ return -1;
508
+ };
509
+ return all
510
+ .map((c) => ({ c, r: rank(c) }))
511
+ .filter((x) => x.r >= 0)
512
+ .sort((a, b) => a.r - b.r || a.c.name.length - b.c.name.length)
513
+ .map((x) => x.c);
514
+ }
515
+
516
+ const MAX_SIDE = 2000;
517
+ const MAX_BYTES = 3_500_000;
518
+ const API_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
519
+
520
+ /** Read an image as base64; downscale big photos (the API caps images at ~5 MB). */
521
+ async function prepareImage(file: File): Promise<Attached> {
522
+ const dataUrl = await new Promise<string>((resolve, reject) => {
523
+ const r = new FileReader();
524
+ r.onload = () => resolve(String(r.result));
525
+ r.onerror = () => reject(r.error);
526
+ r.readAsDataURL(file);
527
+ });
528
+ const img = await new Promise<HTMLImageElement>((resolve, reject) => {
529
+ const el = new Image();
530
+ el.onload = () => resolve(el);
531
+ el.onerror = reject;
532
+ el.src = dataUrl;
533
+ });
534
+ let url = dataUrl;
535
+ let mediaType = file.type;
536
+ if (!API_TYPES.has(file.type) || file.size > MAX_BYTES || Math.max(img.naturalWidth, img.naturalHeight) > MAX_SIDE) {
537
+ const scale = Math.min(1, MAX_SIDE / Math.max(img.naturalWidth, img.naturalHeight));
538
+ const canvas = document.createElement("canvas");
539
+ canvas.width = Math.round(img.naturalWidth * scale);
540
+ canvas.height = Math.round(img.naturalHeight * scale);
541
+ canvas.getContext("2d")!.drawImage(img, 0, 0, canvas.width, canvas.height);
542
+ url = canvas.toDataURL("image/jpeg", 0.85);
543
+ mediaType = "image/jpeg";
544
+ }
545
+ return { id: uuid(), url, mediaType, data: url.slice(url.indexOf(",") + 1) };
546
+ }
547
+
548
+ function autosize(el: HTMLTextAreaElement | null) {
549
+ if (!el) return;
550
+ el.style.height = "auto";
551
+ el.style.height = `${el.scrollHeight}px`;
552
+ }
@@ -0,0 +1,45 @@
1
+ import { useEffect, useState } from "react";
2
+ import { deleteAnnotation, ensureAnnotationsLoaded, useAnnotations, type DraftAnnotation } from "../annotations.ts";
3
+ import { Button } from "../ui/Button.tsx";
4
+ import { AnnotationEditor, AnnotationSourceLink } from "./AnnotationEditor.tsx";
5
+
6
+ /** Draft annotations belong to a project and one target session, independently of the text composer. */
7
+ export function ComposerAnnotations({ projectId, sessionId }: { projectId: string; sessionId: string }) {
8
+ const allItems = useAnnotations((state) => state.items);
9
+ const storageError = useAnnotations((state) => state.storageError);
10
+ const items = allItems.filter((item) => item.projectId === projectId && item.targetSessionId === sessionId);
11
+ const [editingId, setEditingId] = useState<string | null>(null);
12
+ const editing = items.find((item) => item.id === editingId);
13
+ useEffect(() => { ensureAnnotationsLoaded(); }, []);
14
+ useEffect(() => { setEditingId(null); }, [projectId, sessionId]);
15
+
16
+ if (!items.length && !storageError) return null;
17
+ return (
18
+ <div className="mb-2 min-w-0 text-sm" data-testid="composer-annotations" data-annotation-exclude>
19
+ {storageError && <p role="alert" data-testid="annotation-storage-error" className="mb-2 rounded-md border border-danger/30 bg-danger/5 px-3 py-2 text-xs text-danger">{storageError}</p>}
20
+ {items.length > 0 && <details className="rounded-lg border border-accent/25 bg-accent/5">
21
+ <summary className="cursor-pointer select-none px-3 py-2 text-xs font-medium text-accent">批注({items.length})<span className="ml-2 font-normal text-muted">随下一条消息发送</span></summary>
22
+ <ul className="max-h-64 space-y-2 overflow-y-auto border-t border-accent/15 p-2" data-testid="annotation-list">
23
+ {items.map((item) => <AnnotationItem key={item.id} item={item} onEdit={() => setEditingId(item.id)} />)}
24
+ </ul>
25
+ </details>}
26
+ {editing && <AnnotationEditor projectId={projectId} sessionId={sessionId} source={editing.source} quote={editing.quote} annotation={editing} onClose={() => setEditingId(null)} />}
27
+ </div>
28
+ );
29
+ }
30
+
31
+ function AnnotationItem({ item, onEdit }: { item: DraftAnnotation; onEdit: () => void }) {
32
+ return <li className="min-w-0 rounded-md border border-border bg-bg px-3 py-2" data-annotation-id={item.id}>
33
+ <div className="flex min-w-0 items-start gap-2">
34
+ <div className="min-w-0 flex-1">
35
+ {item.source.kind === "file" && <div className="mb-1.5 flex min-w-0"><AnnotationSourceLink projectId={item.projectId} source={item.source} /></div>}
36
+ <blockquote className="max-h-24 overflow-auto whitespace-pre-wrap break-words border-l-2 border-border pl-2 font-mono text-xs text-muted">{item.quote}</blockquote>
37
+ </div>
38
+ <div className="flex shrink-0 items-center gap-1">
39
+ <Button variant="ghost" size="sm" onClick={onEdit} data-testid="annotation-edit">编辑</Button>
40
+ <Button variant="ghost" size="sm" onClick={() => deleteAnnotation(item.id)} data-testid="annotation-delete" className="hover:text-danger">删除</Button>
41
+ </div>
42
+ </div>
43
+ <p className="mt-2 whitespace-pre-wrap break-words text-sm">{item.comment}</p>
44
+ </li>;
45
+ }
@@ -0,0 +1,91 @@
1
+ import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import { Modal } from "./Modal.tsx";
4
+ import { Button } from "../ui/Button.tsx";
5
+
6
+ type Confirmation = {
7
+ title: string;
8
+ description: ReactNode;
9
+ confirmLabel?: string;
10
+ cancelLabel?: string;
11
+ danger?: boolean;
12
+ checkbox?: { label: string; defaultChecked: boolean };
13
+ };
14
+ type Answer = { confirmed: boolean; checked: boolean };
15
+ type Request = Confirmation & { id: number; resolve: (answer: Answer) => void; returnFocus: HTMLElement | null };
16
+ const Context = createContext<((options: Confirmation) => Promise<Answer>) | null>(null);
17
+
18
+ /** Keep one confirmation open at a time; a second click cannot queue another action. */
19
+ export function ConfirmationProvider({ children }: { children: ReactNode }) {
20
+ const [request, setRequest] = useState<Request | null>(null);
21
+ const pending = useRef<Request | null>(null);
22
+ const sequence = useRef(0);
23
+ const confirm = useCallback((options: Confirmation) => {
24
+ if (pending.current) return Promise.resolve({ confirmed: false, checked: false });
25
+ return new Promise<Answer>((resolve) => {
26
+ const request = { ...options, id: ++sequence.current, resolve, returnFocus: document.activeElement instanceof HTMLElement ? document.activeElement : null };
27
+ pending.current = request;
28
+ setRequest(request);
29
+ });
30
+ }, []);
31
+ const finish = useCallback((answer: Answer) => {
32
+ const request = pending.current;
33
+ if (!request) return;
34
+ pending.current = null;
35
+ setRequest(null);
36
+ request.resolve(answer);
37
+ requestAnimationFrame(() => request.returnFocus?.isConnected && request.returnFocus.focus());
38
+ }, []);
39
+ useEffect(() => () => {
40
+ pending.current?.resolve({ confirmed: false, checked: false });
41
+ pending.current = null;
42
+ }, []);
43
+
44
+ return (
45
+ <Context.Provider value={confirm}>
46
+ {children}
47
+ {request && createPortal(<ConfirmationDialog key={request.id} request={request} finish={finish} />, document.body)}
48
+ </Context.Provider>
49
+ );
50
+ }
51
+
52
+ function ConfirmationDialog({ request, finish }: { request: Request; finish: (answer: Answer) => void }) {
53
+ const [checked, setChecked] = useState(request.checkbox?.defaultChecked ?? false);
54
+ const root = useRef<HTMLDivElement>(null);
55
+ const cancel = () => finish({ confirmed: false, checked });
56
+ useEffect(() => {
57
+ root.current?.querySelector<HTMLButtonElement>("button")?.focus();
58
+ }, []);
59
+ return (
60
+ <Modal title={request.title} onClose={cancel} testId="confirmation-dialog">
61
+ <div ref={root} onKeyDown={(event) => {
62
+ if (event.key !== "Tab") return;
63
+ const controls = root.current?.querySelectorAll<HTMLElement>("button, input");
64
+ const first = controls?.[0];
65
+ const last = controls?.[controls.length - 1];
66
+ if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
67
+ if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }
68
+ }}>
69
+ <div className="whitespace-pre-wrap break-words text-sm leading-relaxed text-muted">{request.description}</div>
70
+ {request.checkbox && (
71
+ <label className="mt-4 flex items-center gap-2 text-sm">
72
+ <input type="checkbox" checked={checked} onChange={(event) => setChecked(event.target.checked)} className="size-4 accent-accent" />
73
+ {request.checkbox.label}
74
+ </label>
75
+ )}
76
+ <div className="mt-5 flex justify-end gap-2">
77
+ <Button variant="ghost" onClick={cancel}>{request.cancelLabel ?? "取消"}</Button>
78
+ <Button variant={request.danger ? "danger" : "primary"} onClick={() => finish({ confirmed: true, checked })}>
79
+ {request.confirmLabel ?? "确认"}
80
+ </Button>
81
+ </div>
82
+ </div>
83
+ </Modal>
84
+ );
85
+ }
86
+
87
+ export function useConfirm() {
88
+ const confirm = useContext(Context);
89
+ if (!confirm) throw new Error("ConfirmationProvider is missing");
90
+ return confirm;
91
+ }