@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,227 @@
1
+ import type { ReactNode } from "react";
2
+ import { Button, cx } from "./Button.tsx";
3
+ import { MenuPopover, useMenuAction } from "./MenuPopover.tsx";
4
+ import { MoreIcon, XIcon } from "./icons.tsx";
5
+
6
+ /** An on/off switch with its label inside the same hit area. */
7
+ export function Switch({
8
+ checked,
9
+ onChange,
10
+ label,
11
+ icon,
12
+ className,
13
+ compact,
14
+ disabled,
15
+ }: {
16
+ checked: boolean;
17
+ onChange: (v: boolean) => void;
18
+ label: string;
19
+ icon?: ReactNode;
20
+ className?: string;
21
+ compact?: boolean;
22
+ disabled?: boolean;
23
+ }) {
24
+ return (
25
+ <button
26
+ type="button"
27
+ role="switch"
28
+ aria-checked={checked}
29
+ aria-label={label}
30
+ disabled={disabled}
31
+ onClick={() => onChange(!checked)}
32
+ className={cx(
33
+ "inline-flex h-10 shrink-0 items-center gap-2 rounded-md border px-2.5 text-sm transition-colors md:h-8",
34
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 disabled:cursor-wait disabled:opacity-60",
35
+ checked ? "border-accent/50 bg-accent/10 text-text" : "border-border bg-surface text-muted hover:text-text",
36
+ className,
37
+ )}
38
+ >
39
+ {icon}
40
+ {!compact && <span className="hidden sm:inline">{label}</span>}
41
+ <span className={cx("relative h-4 w-7 rounded-full transition-colors", checked ? "bg-accent" : "bg-border")} aria-hidden>
42
+ <span className={cx("absolute top-0.5 size-3 rounded-full bg-white shadow-sm transition-all", checked ? "left-3.5" : "left-0.5")} />
43
+ </span>
44
+ </button>
45
+ );
46
+ }
47
+
48
+ /** A small rounded tag; with onRemove it gets its own close button. */
49
+ export function Chip({
50
+ children,
51
+ onClick,
52
+ onRemove,
53
+ removeLabel = "移除",
54
+ active,
55
+ tone = "neutral",
56
+ icon,
57
+ testId,
58
+ checked,
59
+ disabled,
60
+ ariaLabel,
61
+ compactOnMobile,
62
+ shape = "pill",
63
+ }: {
64
+ children: ReactNode;
65
+ onClick?: () => void;
66
+ onRemove?: () => void;
67
+ removeLabel?: string;
68
+ active?: boolean;
69
+ tone?: "neutral" | "accent" | "ok" | "warn";
70
+ icon?: ReactNode;
71
+ testId?: string;
72
+ /** A toggle keeps the same pill shape without pretending to open a panel. */
73
+ checked?: boolean;
74
+ disabled?: boolean;
75
+ ariaLabel?: string;
76
+ compactOnMobile?: boolean;
77
+ shape?: "pill" | "rounded";
78
+ }) {
79
+ const tones = {
80
+ neutral: "border-border text-muted",
81
+ accent: "border-accent/40 text-text bg-accent/5",
82
+ ok: "border-ok/40 text-ok bg-ok/5",
83
+ warn: "border-amber-500/40 text-amber-700 dark:text-amber-400 bg-amber-500/5",
84
+ };
85
+ return (
86
+ <span
87
+ className={cx(
88
+ "inline-flex max-w-full items-stretch overflow-hidden border transition-colors md:h-7 md:text-xs",
89
+ shape === "rounded" ? "rounded-md" : "rounded-full",
90
+ compactOnMobile ? "h-7 text-[11px]" : "h-8 text-xs",
91
+ tones[tone],
92
+ active && "ring-2 ring-accent/25",
93
+ )}
94
+ >
95
+ <button
96
+ type="button"
97
+ data-testid={testId}
98
+ role={checked === undefined ? undefined : "switch"}
99
+ aria-checked={checked}
100
+ aria-label={ariaLabel}
101
+ aria-expanded={checked === undefined && onClick ? !!active : undefined}
102
+ disabled={disabled}
103
+ onClick={onClick}
104
+ className={cx(
105
+ "flex min-w-0 items-center hover:text-text focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent/40 disabled:cursor-wait disabled:opacity-60",
106
+ shape === "rounded" ? "gap-1 pl-1.5 md:pl-2" : compactOnMobile ? "gap-1 pl-2.5 md:gap-1.5 md:pl-3" : "gap-1.5 pl-3",
107
+ compactOnMobile && "[&>svg]:size-[13px] md:[&>svg]:size-3.5",
108
+ onRemove ? "pr-1.5" : shape === "rounded" ? "pr-1.5 md:pr-2" : compactOnMobile ? "pr-2.5 md:pr-3" : "pr-3",
109
+ )}
110
+ >
111
+ {icon}
112
+ <span className="truncate">{children}</span>
113
+ </button>
114
+ {onRemove && (
115
+ <button type="button" aria-label={removeLabel} onClick={onRemove} className="flex w-7 items-center justify-center border-l border-current/15 hover:bg-surface-2 hover:text-text">
116
+ <XIcon size={12} />
117
+ </button>
118
+ )}
119
+ </span>
120
+ );
121
+ }
122
+
123
+ /** A titled inline panel that always has a close button. */
124
+ export function Panel({
125
+ title,
126
+ onClose,
127
+ children,
128
+ actions,
129
+ testId,
130
+ className,
131
+ }: {
132
+ title: ReactNode;
133
+ onClose: () => void;
134
+ children: ReactNode;
135
+ actions?: ReactNode;
136
+ testId?: string;
137
+ className?: string;
138
+ }) {
139
+ return (
140
+ <section className={cx("rounded-xl border border-border bg-surface shadow-sm", className)} data-testid={testId}>
141
+ <header className="flex items-center gap-2 border-b border-border py-1.5 pl-3.5 pr-1.5">
142
+ <h3 className="min-w-0 flex-1 truncate font-sans text-sm font-medium">{title}</h3>
143
+ {actions}
144
+ <Button variant="ghost" size="sm" iconOnly onClick={onClose} aria-label="关闭" title="关闭">
145
+ <XIcon />
146
+ </Button>
147
+ </header>
148
+ <div className="p-3.5">{children}</div>
149
+ </section>
150
+ );
151
+ }
152
+
153
+ /** "⋯" trigger with a menu of actions. */
154
+ export function ActionMenu({ children, label = "更多操作", testId, align = "end" }: { children: (close: () => void) => ReactNode; label?: string; testId?: string; align?: "start" | "end" }) {
155
+ return (
156
+ <span data-testid={testId}>
157
+ <MenuPopover label={label} align={align} trigger={(props) => <Button {...props} variant="ghost" size="sm" iconOnly><MoreIcon /></Button>}>
158
+ {children}
159
+ </MenuPopover>
160
+ </span>
161
+ );
162
+ }
163
+
164
+ export function MenuItem({ onClick, children, danger, icon, hint, disabled }: { onClick: () => void; children: ReactNode; danger?: boolean; icon?: ReactNode; hint?: ReactNode; disabled?: boolean }) {
165
+ const close = useMenuAction();
166
+ return (
167
+ <button
168
+ type="button"
169
+ role="menuitem"
170
+ tabIndex={close ? -1 : undefined}
171
+ disabled={disabled}
172
+ onClick={() => { close?.(); onClick(); }}
173
+ className={cx("flex w-full items-center gap-2.5 px-3 py-2.5 text-left text-sm hover:bg-surface-2 focus-visible:bg-surface-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-45 sm:py-2", danger ? "text-danger" : "text-text")}
174
+ >
175
+ {icon && <span className="w-4 shrink-0 text-muted">{icon}</span>}
176
+ <span className="min-w-0 flex-1 truncate">{children}</span>
177
+ {hint && <span className="shrink-0 text-xs text-muted">{hint}</span>}
178
+ </button>
179
+ );
180
+ }
181
+
182
+ export function MenuSeparator() {
183
+ return <div className="my-1 border-t border-border" role="separator" />;
184
+ }
185
+
186
+ /** Mutually exclusive choices shown as cards with an explanation each. */
187
+ export function RadioCards<V extends string>({
188
+ label,
189
+ value,
190
+ onChange,
191
+ options,
192
+ }: {
193
+ label: string;
194
+ value: V;
195
+ onChange: (v: V) => void;
196
+ options: { value: V; title: ReactNode; detail?: ReactNode }[];
197
+ }) {
198
+ return (
199
+ <div role="radiogroup" aria-label={label} className="space-y-2">
200
+ {options.map((o) => {
201
+ const on = o.value === value;
202
+ return (
203
+ <button
204
+ key={o.value}
205
+ type="button"
206
+ role="radio"
207
+ aria-checked={on}
208
+ onClick={() => onChange(o.value)}
209
+ className={cx(
210
+ "flex w-full items-start gap-3 rounded-lg border px-3.5 py-3 text-left transition-colors",
211
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40",
212
+ on ? "border-accent/60 bg-accent/5" : "border-border hover:border-muted/40",
213
+ )}
214
+ >
215
+ <span className={cx("mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border", on ? "border-accent" : "border-muted/50")} aria-hidden>
216
+ {on && <span className="size-2 rounded-full bg-accent" />}
217
+ </span>
218
+ <span className="min-w-0">
219
+ <span className="block text-sm font-medium">{o.title}</span>
220
+ {o.detail && <span className="mt-0.5 block text-xs leading-relaxed text-muted">{o.detail}</span>}
221
+ </span>
222
+ </button>
223
+ );
224
+ })}
225
+ </div>
226
+ );
227
+ }
@@ -0,0 +1,203 @@
1
+ import { useState, type ReactNode } from "react";
2
+ import { Button, CONTROL, cx, type ButtonSize } from "./Button.tsx";
3
+
4
+ /** Text inputs share the control geometry (40px on phones, 32px on desktop). 16px text on phones avoids iOS zoom. */
5
+ export const inputCls =
6
+ "h-10 md:h-8 w-full min-w-0 rounded-md border border-border bg-surface px-3 text-base md:text-sm text-text outline-none transition-colors placeholder:text-muted/80 hover:border-muted/40 focus:border-accent/60 focus:ring-2 focus:ring-accent/20 disabled:opacity-50";
7
+ export const monoInputCls = `${inputCls} font-mono placeholder:font-sans`;
8
+ export const textareaCls =
9
+ "block w-full min-w-0 rounded-md border border-border bg-surface px-3 py-2 text-base md:text-sm text-text outline-none transition-colors placeholder:text-muted/80 hover:border-muted/40 focus:border-accent/60 focus:ring-2 focus:ring-accent/20";
10
+ export const monoTextareaCls = `${textareaCls} font-mono text-[13px] md:text-[13px] leading-relaxed placeholder:font-sans`;
11
+
12
+ /** A labelled form field. Use `group` when the control is not a native input (e.g. a Select), so the label does not forward clicks. */
13
+ export function Field({ label, children, hint, className, group }: { label: string; children: ReactNode; hint?: ReactNode; className?: string; group?: boolean }) {
14
+ const Tag = group ? "div" : "label";
15
+ return (
16
+ <Tag className={cx("block min-w-0", className)}>
17
+ <span className="mb-1.5 block text-xs font-medium text-muted">{label}</span>
18
+ {children}
19
+ {hint && <span className="mt-1 block text-xs leading-relaxed text-muted">{hint}</span>}
20
+ </Tag>
21
+ );
22
+ }
23
+
24
+ /** A bare on/off switch (no label box), for rows that already show their label. 40px hit area on phones. */
25
+ export function Toggle({ checked, onChange, label, disabled }: { checked: boolean; onChange: (v: boolean) => void; label: string; disabled?: boolean }) {
26
+ return (
27
+ <button
28
+ type="button"
29
+ role="switch"
30
+ aria-checked={checked}
31
+ aria-label={label}
32
+ disabled={disabled}
33
+ onClick={() => onChange(!checked)}
34
+ className="group inline-flex h-10 shrink-0 items-center rounded-md px-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 disabled:opacity-45 md:h-8"
35
+ >
36
+ <span className={cx("relative h-5 w-9 rounded-full transition-colors", checked ? "bg-accent" : "bg-border group-hover:bg-muted/40")} aria-hidden>
37
+ <span className={cx("absolute top-0.5 size-4 rounded-full bg-white shadow-sm transition-all", checked ? "left-[18px]" : "left-0.5")} />
38
+ </span>
39
+ </button>
40
+ );
41
+ }
42
+
43
+ /** A labelled row with a switch at the end: the whole row toggles. */
44
+ export function SwitchRow({ checked, onChange, label, desc, disabled, testId }: { checked: boolean; onChange: (v: boolean) => void; label: string; desc?: ReactNode; disabled?: boolean; testId?: string }) {
45
+ return (
46
+ <div className="flex items-center gap-3 py-1" data-testid={testId}>
47
+ <div className="min-w-0 flex-1 cursor-pointer select-none" onClick={() => !disabled && onChange(!checked)}>
48
+ <div className="text-sm">{label}</div>
49
+ {desc && <div className="text-xs leading-relaxed text-muted">{desc}</div>}
50
+ </div>
51
+ <Toggle checked={checked} onChange={onChange} label={label} disabled={disabled} />
52
+ </div>
53
+ );
54
+ }
55
+
56
+ /** Segmented single choice (radio group) — for 2–4 short options. */
57
+ export function Segmented<V extends string>({
58
+ value,
59
+ options,
60
+ onChange,
61
+ label,
62
+ size = "md",
63
+ className,
64
+ stretch,
65
+ disabled,
66
+ }: {
67
+ value: V;
68
+ options: { value: V; label: ReactNode; icon?: ReactNode }[];
69
+ onChange: (v: V) => void;
70
+ label: string;
71
+ size?: ButtonSize;
72
+ className?: string;
73
+ stretch?: boolean;
74
+ disabled?: boolean;
75
+ }) {
76
+ return (
77
+ <div role="radiogroup" aria-label={label} className={cx("inline-flex max-w-full gap-0.5 rounded-lg border border-border bg-surface-2/60 p-0.5", stretch && "flex w-full", className)}>
78
+ {options.map((o) => {
79
+ const on = o.value === value;
80
+ return (
81
+ <button
82
+ key={o.value}
83
+ type="button"
84
+ role="radio"
85
+ aria-checked={on}
86
+ disabled={disabled}
87
+ onClick={() => onChange(o.value)}
88
+ className={cx(
89
+ "inline-flex min-w-0 items-center justify-center whitespace-nowrap rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 disabled:pointer-events-none disabled:opacity-45",
90
+ CONTROL[size],
91
+ size === "md" ? "h-9 md:h-7" : "h-7 md:h-6",
92
+ stretch && "flex-1",
93
+ on ? "bg-surface font-medium text-text shadow-sm ring-1 ring-border" : "text-muted hover:text-text",
94
+ )}
95
+ >
96
+ {o.icon}
97
+ <span className="truncate">{o.label}</span>
98
+ </button>
99
+ );
100
+ })}
101
+ </div>
102
+ );
103
+ }
104
+
105
+
106
+ export type TagTone = "neutral" | "accent" | "ok" | "warn" | "danger";
107
+ const TAG_TONE: Record<TagTone, string> = {
108
+ neutral: "border-border text-muted bg-surface-2/50",
109
+ accent: "border-accent/35 text-accent bg-accent/5",
110
+ ok: "border-ok/35 text-ok bg-ok/5",
111
+ warn: "border-amber-500/40 text-amber-700 dark:text-amber-400 bg-amber-500/5",
112
+ danger: "border-danger/35 text-danger bg-danger/5",
113
+ };
114
+
115
+ /** A small, square-ish label for scope / source / status facts in lists. */
116
+ export function Tag({ children, tone = "neutral", mono, title, icon, className, ...rest }: { children: ReactNode; tone?: TagTone; mono?: boolean; title?: string; icon?: ReactNode; className?: string } & Record<`data-${string}`, string>) {
117
+ return (
118
+ <span
119
+ title={title}
120
+ className={cx("inline-flex h-5 max-w-full shrink-0 items-center gap-1 rounded border px-1.5 text-[11px] leading-none", TAG_TONE[tone], mono && "font-mono", className)}
121
+ {...rest}
122
+ >
123
+ {icon}
124
+ <span className="truncate">{children}</span>
125
+ </span>
126
+ );
127
+ }
128
+
129
+ /** Empty state: what this is, and what to do next. */
130
+ export function EmptyState({ icon, title, children, action, compact, testId }: { icon?: ReactNode; title: string; children?: ReactNode; action?: ReactNode; compact?: boolean; testId?: string }) {
131
+ return (
132
+ <div className={cx("flex flex-col items-center rounded-xl border border-dashed border-border text-center", compact ? "px-4 py-5" : "px-6 py-10")} data-testid={testId}>
133
+ {icon && <span className="mb-2 flex size-9 items-center justify-center rounded-full bg-surface-2 text-muted">{icon}</span>}
134
+ <p className="text-sm font-medium">{title}</p>
135
+ {children && <div className="mt-1 max-w-md text-xs leading-relaxed text-muted">{children}</div>}
136
+ {action && <div className="mt-3">{action}</div>}
137
+ </div>
138
+ );
139
+ }
140
+
141
+ /**
142
+ * Two-step destructive action: the first press asks inline, the second does it.
143
+ * The confirmation is a group named "确认操作" with "确认" and "取消".
144
+ */
145
+ export function ConfirmButton({
146
+ children,
147
+ confirmText,
148
+ onConfirm,
149
+ danger = true,
150
+ disabled,
151
+ label,
152
+ size = "sm",
153
+ icon,
154
+ variant,
155
+ }: {
156
+ children: ReactNode;
157
+ confirmText: string;
158
+ onConfirm: () => void;
159
+ danger?: boolean;
160
+ disabled?: boolean;
161
+ label?: string;
162
+ size?: ButtonSize;
163
+ icon?: ReactNode;
164
+ variant?: "ghost" | "secondary" | "outline-danger";
165
+ }) {
166
+ const [asking, setAsking] = useState(false);
167
+ if (!asking)
168
+ return (
169
+ <Button variant={variant ?? (danger ? "outline-danger" : "secondary")} size={size} icon={icon} onClick={() => setAsking(true)} disabled={disabled} aria-label={label}>
170
+ {children}
171
+ </Button>
172
+ );
173
+ return (
174
+ <span className="inline-flex flex-wrap items-center gap-1.5 rounded-md bg-surface-2/70 py-0.5 pl-2 pr-0.5" role="group" aria-label="确认操作">
175
+ <span className="text-xs text-muted">{confirmText}</span>
176
+ <Button
177
+ variant={danger ? "danger" : "primary"}
178
+ size={size}
179
+ autoFocus
180
+ onClick={() => {
181
+ setAsking(false);
182
+ onConfirm();
183
+ }}
184
+ >
185
+ 确认
186
+ </Button>
187
+ <Button variant="ghost" size={size} onClick={() => setAsking(false)} onKeyDown={(e) => e.key === "Escape" && setAsking(false)}>
188
+ 取消
189
+ </Button>
190
+ </span>
191
+ );
192
+ }
193
+
194
+ /** Loading line with spinner-free shimmer bars; keeps the layout from jumping. */
195
+ export function Skeleton({ rows = 3 }: { rows?: number }) {
196
+ return (
197
+ <div className="space-y-2 py-2" aria-label="加载中" role="status">
198
+ {Array.from({ length: rows }, (_, i) => (
199
+ <div key={i} className="h-9 animate-pulse rounded-md bg-surface-2/70" style={{ opacity: 1 - i * 0.2 }} />
200
+ ))}
201
+ </div>
202
+ );
203
+ }
@@ -0,0 +1,144 @@
1
+ import { createContext, useCallback, useContext, useId, useLayoutEffect, useRef, useState, type ButtonHTMLAttributes, type ReactNode, type RefObject } from "react";
2
+ import { Popover, type PopoverDismissReason } from "./Popover.tsx";
3
+
4
+ type CloseReason = "action" | "escape" | "outside" | "backdrop" | "tab" | "trigger";
5
+ type TriggerProps = Pick<ButtonHTMLAttributes<HTMLButtonElement>, "onClick" | "onKeyDown" | "onPointerDown" | "onPointerCancel" | "aria-label" | "aria-haspopup" | "aria-expanded" | "aria-controls"> & { ref: RefObject<HTMLButtonElement | null> };
6
+ const MenuAction = createContext<(() => void) | undefined>(undefined);
7
+ export const useMenuAction = () => useContext(MenuAction);
8
+
9
+ function available(element: HTMLElement) {
10
+ return !element.matches(':disabled, [aria-disabled="true"]') && !element.closest("[inert]")
11
+ && element.getClientRects().length > 0 && !["hidden", "collapse"].includes(getComputedStyle(element).visibility);
12
+ }
13
+
14
+ function menuItems(menu: HTMLElement) {
15
+ return [...menu.querySelectorAll<HTMLElement>('[role="menuitem"]')]
16
+ .filter((element) => element.closest('[role="menu"]') === menu && available(element));
17
+ }
18
+
19
+ /** Position a menu with Popover, but opt into its own focus/keyboard protocol.
20
+ * Select deliberately keeps focus on its trigger and does not use this layer. */
21
+ export function MenuPopover({ trigger, children, label, beforeOpen, align = "end", width = 220, testId }: {
22
+ trigger: (props: TriggerProps) => ReactNode;
23
+ children: ReactNode | ((close: () => void) => ReactNode);
24
+ label: string;
25
+ /** Runs before pointer focus or keyboard opening can disturb a text selection. */
26
+ beforeOpen?: () => void;
27
+ align?: "start" | "end";
28
+ width?: number;
29
+ testId?: string;
30
+ }) {
31
+ const [open, setOpen] = useState(false);
32
+ const opened = useRef(false);
33
+ const prepared = useRef(false);
34
+ const firstFocus = useRef<"first" | "last">("first");
35
+ const anchor = useRef<HTMLButtonElement>(null);
36
+ const menu = useRef<HTMLDivElement>(null);
37
+ const id = useId();
38
+ const close = useCallback((reason: CloseReason) => {
39
+ // Existing ActionMenu callers may also close from their action callback.
40
+ // Only the first close restores focus, before an action opens another view.
41
+ if (!opened.current) return;
42
+ opened.current = false;
43
+ setOpen(false);
44
+ if (reason === "action" || reason === "escape" || reason === "backdrop" || reason === "trigger") anchor.current?.focus({ preventScroll: true });
45
+ }, []);
46
+ const action = useCallback(() => close("action"), [close]);
47
+ const dismiss = useCallback((reason?: PopoverDismissReason) => close(reason ?? "outside"), [close]);
48
+ const show = (edge: "first" | "last") => {
49
+ if (!prepared.current) beforeOpen?.();
50
+ prepared.current = false;
51
+ firstFocus.current = edge;
52
+ opened.current = true;
53
+ setOpen(true);
54
+ };
55
+ return <>
56
+ {trigger({
57
+ ref: anchor, "aria-label": label, "aria-haspopup": "menu", "aria-expanded": open, "aria-controls": open ? id : undefined,
58
+ onPointerDown: (event) => {
59
+ prepared.current = false;
60
+ if (!opened.current && event.button === 0) { beforeOpen?.(); prepared.current = true; }
61
+ },
62
+ onPointerCancel: () => { prepared.current = false; },
63
+ onClick: () => { if (opened.current) close("trigger"); else show("first"); },
64
+ onKeyDown: (event) => {
65
+ if (!["Enter", " ", "ArrowDown", "ArrowUp"].includes(event.key)) return;
66
+ event.preventDefault();
67
+ prepared.current = false;
68
+ if (opened.current) {
69
+ if (event.key === "Enter" || event.key === " ") close("trigger");
70
+ else if (menu.current) focusEdge(menu.current, event.key === "ArrowUp" ? "last" : "first");
71
+ } else show(event.key === "ArrowUp" ? "last" : "first");
72
+ },
73
+ })}
74
+ <Popover open={open} onClose={dismiss} anchor={anchor} align={align} width={width} label={label} testId={testId} topmostOnly>
75
+ <MenuAction.Provider value={action}>
76
+ <MenuSurface id={id} label={label} menu={menu} anchor={anchor} firstFocus={firstFocus.current} close={close}>
77
+ {typeof children === "function" ? children(action) : children}
78
+ </MenuSurface>
79
+ </MenuAction.Provider>
80
+ </Popover>
81
+ </>;
82
+ }
83
+
84
+ function focusItem(item: HTMLElement) {
85
+ item.focus({ preventScroll: true });
86
+ item.scrollIntoView({ block: "nearest", inline: "nearest" });
87
+ }
88
+
89
+ function focusEdge(menu: HTMLElement, edge: "first" | "last") {
90
+ const items = menuItems(menu);
91
+ const item = edge === "last" ? items.at(-1) : items[0];
92
+ if (item) focusItem(item);
93
+ else menu.focus({ preventScroll: true });
94
+ }
95
+
96
+ function MenuSurface({ id, label, menu, anchor, firstFocus, close, children }: {
97
+ id: string; label: string; menu: RefObject<HTMLDivElement | null>; anchor: RefObject<HTMLButtonElement | null>;
98
+ firstFocus: "first" | "last"; close: (reason: CloseReason) => void; children: ReactNode;
99
+ }) {
100
+ useLayoutEffect(() => {
101
+ const root = menu.current;
102
+ if (!root) return;
103
+ const layers = document.querySelectorAll('[role="dialog"]');
104
+ if (layers[layers.length - 1] !== root.closest('[role="dialog"]')) return;
105
+ const active = document.activeElement;
106
+ // Preserve the current item on ordinary rerenders, but recover when it
107
+ // becomes disabled or disappears while asynchronous menu data changes.
108
+ if (!root.contains(active) || active === root && menuItems(root).length
109
+ || active instanceof HTMLElement && active !== root && !available(active)) focusEdge(root, firstFocus);
110
+ }, [menu, firstFocus, children]);
111
+ return <div ref={menu} id={id} role="menu" aria-label={label} tabIndex={-1} className="outline-none"
112
+ onClickCapture={(event) => {
113
+ const item = event.target instanceof Element ? event.target.closest<HTMLElement>('[role="menuitem"]') : null;
114
+ if (!item || !available(item)) event.stopPropagation();
115
+ }}
116
+ onKeyDown={(event) => {
117
+ const root = menu.current;
118
+ if (!root) return;
119
+ if (event.key === "Tab") {
120
+ // Portal DOM order is unrelated to the trigger. Continue from that
121
+ // trigger, wrapping only when it belongs to a parent modal's focus scope.
122
+ const trigger = anchor.current;
123
+ const modal = trigger?.closest<HTMLElement>('[role="dialog"][aria-modal="true"]');
124
+ const scope = modal ?? document.body;
125
+ const controls = [...scope.querySelectorAll<HTMLElement>('button, a[href], input:not([type="hidden"]), select, textarea, [tabindex]')]
126
+ .filter((element) => element.tabIndex >= 0 && available(element) && !element.closest('[role="menu"]'));
127
+ const index = trigger ? controls.indexOf(trigger) : -1;
128
+ const next = controls[index + (event.shiftKey ? -1 : 1)] ?? (modal ? event.shiftKey ? controls.at(-1) : controls[0] : undefined);
129
+ close("tab");
130
+ if (next) { event.preventDefault(); next.focus({ preventScroll: true }); }
131
+ else trigger?.focus({ preventScroll: true });
132
+ } else if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
133
+ event.preventDefault();
134
+ const items = menuItems(root);
135
+ if (!items.length) return;
136
+ const index = items.indexOf(document.activeElement as HTMLElement);
137
+ const next = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1
138
+ : event.key === "ArrowDown" ? (index + 1) % items.length : (index < 0 ? items.length - 1 : index - 1 + items.length) % items.length;
139
+ if (items[next]) focusItem(items[next]);
140
+ }
141
+ }}>
142
+ {children}
143
+ </div>;
144
+ }