@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,142 @@
1
+ import { useState } from "react";
2
+ import { useSearchParams } from "react-router";
3
+ import type { SkillInfo } from "@shared/config.ts";
4
+ import { Markdown } from "../../components/Markdown.tsx";
5
+ import { Button, cx } from "../../ui/Button.tsx";
6
+ import { ArrowLeftIcon, SearchIcon, SparkIcon } from "../../ui/icons.tsx";
7
+ import { Card, Empty, EmptyState, ErrorLine, Loading, PageTitle, SplitView, Tag, inputCls, useCfg, useProjectParam } from "./ui.tsx";
8
+
9
+ const SOURCE_LABEL: Record<string, string> = {
10
+ user: "用户级",
11
+ project: "项目级",
12
+ plugin: "插件提供",
13
+ "user-command": "用户级命令(.claude/commands)",
14
+ "project-command": "项目级命令(.claude/commands)",
15
+ };
16
+
17
+ export function SkillsPage() {
18
+ const pid = useProjectParam();
19
+ const skills = useCfg<SkillInfo[]>("skills", pid);
20
+ const [sp, setSp] = useSearchParams();
21
+ const [q, setQ] = useState("");
22
+ const selected = sp.get("f");
23
+ const select = (p: string | null) => {
24
+ const n = new URLSearchParams(sp);
25
+ if (p) n.set("f", p);
26
+ else n.delete("f");
27
+ setSp(n);
28
+ };
29
+ const all = skills.data ?? [];
30
+ const match = (s: SkillInfo) => !q || `${s.name} ${s.description}`.toLowerCase().includes(q.toLowerCase());
31
+ const groups = Object.keys(SOURCE_LABEL).map((src) => ({ src, items: all.filter((s) => s.source === src && match(s)) }));
32
+ const current = all.find((s) => s.path === selected);
33
+
34
+ const list = (
35
+ <div className="space-y-4">
36
+ {all.length > 6 && (
37
+ <span className="relative block">
38
+ <SearchIcon className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-muted" />
39
+ <input aria-label="筛选技能" placeholder={`在 ${all.length} 个技能里筛选`} value={q} onChange={(e) => setQ(e.target.value)} onKeyDown={(e) => e.key === "Escape" && setQ("")} className={`${inputCls} pl-8`} />
40
+ </span>
41
+ )}
42
+ {groups
43
+ .filter((g) => g.items.length)
44
+ .map((g) => (
45
+ <Card key={g.src} title={SOURCE_LABEL[g.src]} count={g.items.length} flush>
46
+ <ul className="p-1">
47
+ {g.items.map((s) => (
48
+ <li key={s.path ?? s.name}>
49
+ <button
50
+ className={cx(
51
+ "block w-full rounded-md px-3 py-2.5 text-left transition-colors disabled:cursor-default",
52
+ s.path && s.path === selected ? "bg-accent/10" : "hover:bg-surface-2",
53
+ )}
54
+ disabled={!s.path}
55
+ onClick={() => s.path && select(s.path)}
56
+ aria-current={s.path === selected ? "true" : undefined}
57
+ data-testid="skill"
58
+ data-name={s.name}
59
+ >
60
+ <div className="flex flex-wrap items-center gap-1.5">
61
+ <span className="font-mono text-sm font-medium">/{s.name}</span>
62
+ {s.plugin && <Tag>{s.plugin}</Tag>}
63
+ {s.state && s.state !== "on" && <Tag tone="warn">{s.state}</Tag>}
64
+ </div>
65
+ {s.description && <p className="mt-0.5 line-clamp-2 text-xs leading-relaxed text-muted">{s.description}</p>}
66
+ </button>
67
+ </li>
68
+ ))}
69
+ </ul>
70
+ </Card>
71
+ ))}
72
+ {q && groups.every((g) => !g.items.length) && <Empty title="没有匹配的技能">换个关键词试试。</Empty>}
73
+ </div>
74
+ );
75
+
76
+ return (
77
+ <div>
78
+ <PageTitle title="技能" desc="用户级 ~/.claude/skills、项目级 .claude/skills 和插件带来的技能。在对话里用 / 调用,Claude 也会按说明自动使用。" />
79
+ {skills.isPending && <Loading />}
80
+ <ErrorLine error={skills.error} />
81
+ {skills.data &&
82
+ (all.length === 0 ? (
83
+ <EmptyState icon={<SparkIcon size={16} />} title="还没有技能">
84
+ 在 <code className="font-mono">~/.claude/skills/名称/SKILL.md</code> 或项目的 <code className="font-mono">.claude/skills/</code> 下放一个 SKILL.md,或在“插件”页安装带技能的插件。
85
+ </EmptyState>
86
+ ) : (
87
+ <SplitView
88
+ showDetail={!!selected}
89
+ listWidth="lg:w-96"
90
+ list={list}
91
+ detail={
92
+ selected ? (
93
+ <SkillFile key={selected} pid={pid} path={selected} skill={current} onClose={() => select(null)} />
94
+ ) : (
95
+ <EmptyState icon={<SparkIcon size={16} />} title="选择一个技能查看 SKILL.md">
96
+ 共 {all.length} 个技能。
97
+ </EmptyState>
98
+ )
99
+ }
100
+ />
101
+ ))}
102
+ </div>
103
+ );
104
+ }
105
+
106
+ function SkillFile({ pid, path, skill, onClose }: { pid?: string; path: string; skill?: SkillInfo; onClose: () => void }) {
107
+ const file = useCfg<{ path: string; content: string }>("skills/file", pid, { params: { path } });
108
+ const body = file.data?.content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "") ?? "";
109
+ const front = file.data?.content.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1];
110
+ const fields = front
111
+ ?.split("\n")
112
+ .map((l) => /^([\w-]+):\s*(.*)$/.exec(l))
113
+ .filter((m): m is RegExpExecArray => !!m && m[1] !== "description");
114
+ return (
115
+ <Card
116
+ icon={<SparkIcon />}
117
+ title={skill ? `/${skill.name}` : (path.split("/").slice(-2, -1)[0] ?? "SKILL.md")}
118
+ sub={path}
119
+ actions={
120
+ <Button variant="ghost" size="sm" icon={<ArrowLeftIcon />} onClick={onClose} aria-label="返回列表">
121
+ <span className="lg:hidden">返回</span>
122
+ <span className="hidden lg:inline">关闭</span>
123
+ </Button>
124
+ }
125
+ >
126
+ {file.isPending && <Loading rows={5} />}
127
+ <ErrorLine error={file.error} />
128
+ {skill?.description && <p className="mb-3 text-sm leading-relaxed text-muted">{skill.description}</p>}
129
+ {fields && fields.length > 0 && (
130
+ <dl className="mb-4 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 rounded-lg bg-surface-2/60 px-3 py-2 text-xs">
131
+ {fields.map((m) => (
132
+ <div key={m[1]} className="contents">
133
+ <dt className="font-mono text-muted">{m[1]}</dt>
134
+ <dd className="break-all font-mono">{m[2] || "—"}</dd>
135
+ </div>
136
+ ))}
137
+ </dl>
138
+ )}
139
+ {file.data && <Markdown text={body} />}
140
+ </Card>
141
+ );
142
+ }
@@ -0,0 +1,165 @@
1
+ import type { ReactNode } from "react";
2
+ import { useSearchParams } from "react-router";
3
+ import { useMutation, useQuery, useQueryClient, type QueryKey } from "@tanstack/react-query";
4
+ import { SCOPE_LABEL, type SettingsScope } from "@shared/config.ts";
5
+ import { api, ApiError } from "../../api.ts";
6
+ import { cx } from "../../ui/Button.tsx";
7
+ import { Select } from "../../ui/Select.tsx";
8
+ import { EmptyState, Skeleton, Tag, type TagTone } from "../../ui/Form.tsx";
9
+ import { AlertIcon, FolderIcon } from "../../ui/icons.tsx";
10
+
11
+ export { ConfirmButton, Field, inputCls, monoInputCls, monoTextareaCls, textareaCls, Toggle, Tag, EmptyState } from "../../ui/Form.tsx";
12
+
13
+ /** The project whose project-level config is shown (`?p=<pid>`); undefined = user level only. */
14
+ export function useProjectParam(): string | undefined {
15
+ const [sp] = useSearchParams();
16
+ return sp.get("p") || undefined;
17
+ }
18
+
19
+ export function cfgUrl(path: string, pid: string | undefined, params: Record<string, string> = {}) {
20
+ const sp = new URLSearchParams({ ...(pid ? { project: pid } : {}), ...params });
21
+ const s = sp.toString();
22
+ return `/api/config/${path}${s ? `?${s}` : ""}`;
23
+ }
24
+
25
+ export function useCfg<T>(path: string, pid: string | undefined, opts: { enabled?: boolean; params?: Record<string, string> } = {}) {
26
+ return useQuery({
27
+ queryKey: ["cfg", path, pid ?? "", opts.params ?? {}],
28
+ queryFn: () => api<T>(cfgUrl(path, pid, opts.params)),
29
+ enabled: opts.enabled ?? true,
30
+ staleTime: 5_000,
31
+ });
32
+ }
33
+
34
+ /** A mutation against /api/config that refreshes the given config listings afterwards. */
35
+ export function useCfgMutation<V, R = unknown>(fn: (v: V) => Promise<R>, invalidate: string[], onSuccess?: (r: R, v: V) => void) {
36
+ const qc = useQueryClient();
37
+ return useMutation({
38
+ mutationFn: fn,
39
+ onSuccess: (r, v) => {
40
+ for (const p of invalidate) qc.invalidateQueries({ queryKey: ["cfg", p] as QueryKey });
41
+ onSuccess?.(r, v);
42
+ },
43
+ });
44
+ }
45
+
46
+ export const errText = (e: unknown) => (e instanceof ApiError ? e.message : e instanceof Error ? e.message : e ? String(e) : "");
47
+
48
+ export function ErrorLine({ error }: { error: unknown }) {
49
+ if (!error) return null;
50
+ return (
51
+ <p role="alert" className="my-2 flex items-start gap-2 whitespace-pre-wrap break-words rounded-md border border-danger/30 bg-danger/5 px-3 py-2 text-sm text-danger">
52
+ <AlertIcon className="mt-0.5 shrink-0" />
53
+ <span className="min-w-0">{errText(error)}</span>
54
+ </p>
55
+ );
56
+ }
57
+
58
+ export function PageTitle({ title, desc, children }: { title: string; desc?: ReactNode; children?: ReactNode }) {
59
+ return (
60
+ <div className="mb-5 flex flex-wrap items-end gap-x-4 gap-y-3">
61
+ <div className="min-w-0 flex-1 basis-64">
62
+ <h2 className="text-2xl">{title}</h2>
63
+ {desc && <p className="mt-1 text-sm leading-relaxed text-muted">{desc}</p>}
64
+ </div>
65
+ {children && <div className="flex flex-wrap items-center gap-2">{children}</div>}
66
+ </div>
67
+ );
68
+ }
69
+
70
+ export function Card({
71
+ title,
72
+ sub,
73
+ children,
74
+ actions,
75
+ testId,
76
+ icon,
77
+ count,
78
+ flush,
79
+ className,
80
+ }: {
81
+ title?: ReactNode;
82
+ sub?: ReactNode;
83
+ children: ReactNode;
84
+ actions?: ReactNode;
85
+ testId?: string;
86
+ icon?: ReactNode;
87
+ count?: number;
88
+ /** No body padding (for lists that draw their own row padding). */
89
+ flush?: boolean;
90
+ className?: string;
91
+ }) {
92
+ return (
93
+ <section className={cx("mb-4 min-w-0 rounded-xl border border-border bg-surface shadow-sm", className)} data-testid={testId}>
94
+ {(title || actions) && (
95
+ <header className="flex flex-wrap items-center gap-x-2 gap-y-1.5 border-b border-border py-2 pl-4 pr-2">
96
+ <div className="min-w-0 flex-1 basis-40">
97
+ {title && (
98
+ <h3 className="flex items-center gap-2 text-base">
99
+ {icon && <span className="text-muted">{icon}</span>}
100
+ <span className="min-w-0 truncate">{title}</span>
101
+ {count !== undefined && <span className="rounded-full bg-surface-2 px-1.5 font-sans text-[11px] font-medium tabular-nums text-muted">{count}</span>}
102
+ </h3>
103
+ )}
104
+ {sub && <p className="truncate font-mono text-[11px] text-muted" title={typeof sub === "string" ? sub : undefined}>{sub}</p>}
105
+ </div>
106
+ {actions && <div className="flex flex-wrap items-center gap-1.5">{actions}</div>}
107
+ </header>
108
+ )}
109
+ <div className={flush ? "" : "px-4 py-3"}>{children}</div>
110
+ </section>
111
+ );
112
+ }
113
+
114
+ const SCOPE_TONE: Record<string, TagTone> = { user: "neutral", project: "accent", local: "warn" };
115
+
116
+ export function ScopeBadge({ scope }: { scope: string }) {
117
+ const label = SCOPE_LABEL[scope as SettingsScope] ?? scope;
118
+ return <Tag tone={SCOPE_TONE[scope] ?? "neutral"}>{label}</Tag>;
119
+ }
120
+
121
+ const SCOPE_HINT: Record<SettingsScope, string> = {
122
+ user: "~/.claude,所有项目",
123
+ project: ".claude/settings.json,随仓库共享",
124
+ local: ".claude/settings.local.json,只在本机",
125
+ };
126
+
127
+ export function ScopeSelect({ value, onChange, scopes, label = "层级", className }: { value: string; onChange: (v: SettingsScope) => void; scopes: SettingsScope[]; label?: string; className?: string }) {
128
+ return (
129
+ <Select<SettingsScope>
130
+ label={label}
131
+ value={value as SettingsScope}
132
+ onChange={onChange}
133
+ className={cx("w-full", className)}
134
+ options={scopes.map((s) => ({ value: s, label: SCOPE_LABEL[s], text: SCOPE_LABEL[s], hint: SCOPE_HINT[s] }))}
135
+ />
136
+ );
137
+ }
138
+
139
+ export function Empty({ children, title = "这里还没有内容", icon, action }: { children?: ReactNode; title?: string; icon?: ReactNode; action?: ReactNode }) {
140
+ return <EmptyState compact title={title} icon={icon} action={action}>{children}</EmptyState>;
141
+ }
142
+
143
+ export function Loading({ rows = 3 }: { rows?: number }) {
144
+ return <Skeleton rows={rows} />;
145
+ }
146
+
147
+ /** Note for project-level sections when no project is selected. */
148
+ export function NeedProject() {
149
+ return (
150
+ <p className="mb-4 flex items-center gap-2 rounded-lg border border-dashed border-border px-3 py-2.5 text-sm text-muted">
151
+ <FolderIcon className="shrink-0" />
152
+ 在页面顶部选择一个项目,即可查看和修改项目级配置。
153
+ </p>
154
+ );
155
+ }
156
+
157
+ /** Two-column work area: a list on the left and the selected item on the right (stacked on phones). */
158
+ export function SplitView({ list, detail, showDetail, listWidth = "lg:w-80" }: { list: ReactNode; detail: ReactNode; showDetail: boolean; listWidth?: string }) {
159
+ return (
160
+ <div className="lg:flex lg:items-start lg:gap-5">
161
+ <div className={cx(showDetail && "hidden lg:block", "min-w-0 lg:shrink-0", listWidth)}>{list}</div>
162
+ <div className={cx(!showDetail && "hidden lg:block", "min-w-0 lg:sticky lg:top-4 lg:flex-1")}>{detail}</div>
163
+ </div>
164
+ );
165
+ }
@@ -0,0 +1,133 @@
1
+ export type PreferenceStorage = Pick<Storage, "getItem" | "setItem">;
2
+ type SavedDocument<T> = { key: string; revision: number; value: T };
3
+ const DATABASE = "claude-ide:preferences", STORE = "documents", SIGNAL = "claude-ide:preferences:changed";
4
+ let database: Promise<IDBDatabase> | undefined;
5
+
6
+ function openDatabase() {
7
+ return database ??= new Promise<IDBDatabase>((resolve, reject) => {
8
+ const request = indexedDB.open(DATABASE, 1);
9
+ let abandoned = false;
10
+ request.onupgradeneeded = () => { request.result.createObjectStore(STORE, { keyPath: "key" }); };
11
+ request.onerror = () => { database = undefined; reject(request.error); };
12
+ request.onblocked = () => { abandoned = true; database = undefined; reject(new Error("Preference database is blocked")); };
13
+ request.onsuccess = () => {
14
+ const db = request.result;
15
+ if (abandoned) { db.close(); return; }
16
+ db.onversionchange = () => { db.close(); database = undefined; };
17
+ resolve(db);
18
+ };
19
+ });
20
+ }
21
+
22
+ /** Read, migrate and change inside one read/write transaction shared by all windows. */
23
+ async function transact<T>(key: string, seed: T, normalize: (raw: unknown) => T, change?: (current: T) => T): Promise<SavedDocument<T>> {
24
+ const db = await openDatabase();
25
+ return new Promise((resolve, reject) => {
26
+ const transaction = db.transaction(STORE, "readwrite"), store = transaction.objectStore(STORE);
27
+ const request = store.get(key);
28
+ let result: SavedDocument<T>, failure: unknown;
29
+ request.onsuccess = () => {
30
+ try {
31
+ const previous = request.result as SavedDocument<T> | undefined;
32
+ const current = normalize(previous?.value ?? seed), next = change ? change(current) : current;
33
+ const changed = !previous || JSON.stringify(previous.value) !== JSON.stringify(next);
34
+ result = { key, revision: (previous?.revision ?? 0) + (changed ? 1 : 0), value: next };
35
+ if (changed) store.put(result);
36
+ } catch (error) { failure = error; transaction.abort(); }
37
+ };
38
+ transaction.oncomplete = () => resolve(result);
39
+ transaction.onabort = () => reject(failure ?? transaction.error ?? new Error("Preference transaction aborted"));
40
+ transaction.onerror = () => { /* onabort reports the final transaction failure. */ };
41
+ });
42
+ }
43
+
44
+ let channel: BroadcastChannel | undefined;
45
+ const watchers = new Map<string, Set<() => void>>();
46
+ let watching = false;
47
+ function watch(key: string, refresh: () => void) {
48
+ if (!watching) {
49
+ watching = true;
50
+ const changed = (key: unknown) => { if (typeof key === "string") watchers.get(key)?.forEach((refresh) => refresh()); };
51
+ try { channel = new BroadcastChannel(SIGNAL); channel.onmessage = (event) => changed(event.data); } catch { /* Storage events also work on ordinary HTTP origins. */ }
52
+ window.addEventListener("storage", (event) => {
53
+ if (event.key !== SIGNAL || !event.newValue) return;
54
+ try { changed(JSON.parse(event.newValue).key); } catch { /* Ignore unrelated or malformed signals. */ }
55
+ });
56
+ }
57
+ const listeners = watchers.get(key) ?? new Set();
58
+ listeners.add(refresh); watchers.set(key, listeners);
59
+ }
60
+ function signal(key: string) {
61
+ try {
62
+ if (channel) channel.postMessage(key);
63
+ else localStorage.setItem(SIGNAL, JSON.stringify({ key, nonce: `${Date.now()}:${Math.random()}` }));
64
+ } catch { /* A notification failure cannot undo a committed transaction. */ }
65
+ }
66
+
67
+ /** Optimistic UI over an atomic document. Pending pure operations are rebased on
68
+ * each committed value; remote refreshes wait for local writes to finish. An
69
+ * explicitly supplied synchronous storage is useful for isolated store tests. */
70
+ export function createPreferenceDocument<T>(key: string, empty: T, normalize: (raw: unknown) => T, suppliedStorage?: PreferenceStorage) {
71
+ let storage = suppliedStorage;
72
+ try { storage ??= typeof localStorage === "undefined" ? undefined : localStorage; } catch { /* In-memory fallback. */ }
73
+ const readLegacy = () => {
74
+ try { const raw = storage?.getItem(key); return raw ? normalize(JSON.parse(raw)) : undefined; } catch { return undefined; }
75
+ };
76
+ let base = readLegacy() ?? empty, snapshot = base, revision = -1;
77
+ let immediate = suppliedStorage !== undefined || typeof window === "undefined" || typeof indexedDB === "undefined";
78
+ let writing = false, refreshWanted = false, failedWrite = false;
79
+ const seed = base, pending: ((current: T) => T)[] = [], listeners = new Set<() => void>();
80
+ const publish = () => {
81
+ const next = pending.reduce((current, change) => change(current), base);
82
+ if (JSON.stringify(next) === JSON.stringify(snapshot)) return;
83
+ snapshot = next; listeners.forEach((notify) => notify());
84
+ };
85
+ const persistFallback = () => {
86
+ try { storage?.setItem(key, JSON.stringify(base)); failedWrite = false; } catch { failedWrite = true; }
87
+ };
88
+ const fallback = () => {
89
+ base = snapshot; pending.length = 0; immediate = true; persistFallback();
90
+ };
91
+ const refresh = async () => {
92
+ if (immediate) { if (!failedWrite) { base = readLegacy() ?? base; publish(); } return; }
93
+ if (writing || pending.length) { refreshWanted = true; return; }
94
+ try {
95
+ const result = await transact(key, seed, normalize);
96
+ if (immediate) return;
97
+ if (writing || pending.length) { refreshWanted = true; return; }
98
+ if (result.revision >= revision) { revision = result.revision; base = result.value; publish(); }
99
+ } catch { fallback(); }
100
+ };
101
+ const drain = async () => {
102
+ if (writing) return;
103
+ writing = true;
104
+ try {
105
+ while (pending.length) {
106
+ const result = await transact(key, seed, normalize, pending[0]);
107
+ if (immediate) return;
108
+ pending.shift(); base = result.value; revision = result.revision;
109
+ publish(); signal(key);
110
+ }
111
+ } catch {
112
+ // Preserve all edits already shown if persistence becomes unavailable.
113
+ fallback();
114
+ } finally {
115
+ writing = false;
116
+ if (refreshWanted) { refreshWanted = false; void refresh(); }
117
+ }
118
+ };
119
+ if (!immediate) { watch(key, () => { void refresh(); }); void refresh(); }
120
+ return {
121
+ get: () => snapshot,
122
+ subscribe(notify: () => void) { listeners.add(notify); return () => { listeners.delete(notify); }; },
123
+ change(change: (current: T) => T) {
124
+ if (immediate) {
125
+ if (!failedWrite) base = readLegacy() ?? base;
126
+ const next = change(base);
127
+ if (JSON.stringify(next) === JSON.stringify(base)) { publish(); return; }
128
+ base = next; persistFallback(); publish();
129
+ } else { pending.push(change); publish(); void drain(); }
130
+ },
131
+ refresh,
132
+ };
133
+ }
@@ -0,0 +1,18 @@
1
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import type { ProjectInfo } from "@shared/protocol.ts";
3
+ import { api } from "./api.ts";
4
+ import { useConfirm } from "./components/Confirmation.tsx";
5
+
6
+ export function useRemoveProject(onRemoved?: (project: ProjectInfo) => void) {
7
+ const qc = useQueryClient();
8
+ const confirm = useConfirm();
9
+ return useMutation({
10
+ mutationFn: async (project: ProjectInfo) => {
11
+ if (!(await confirm({ title: "移除项目?", description: `将“${project.name}”从 Claude IDE 项目列表移除。目录和会话文件都会保留,可以重新添加。`, confirmLabel: "移除项目" })).confirmed) return;
12
+ await api(`/api/projects/${project.id}`, { method: "DELETE" });
13
+ qc.setQueryData<ProjectInfo[]>(["projects"], (items) => items?.filter((item) => item.id !== project.id));
14
+ onRemoved?.(project);
15
+ await Promise.all([qc.invalidateQueries({ queryKey: ["projects"] }), qc.invalidateQueries({ queryKey: ["overview"] })]);
16
+ },
17
+ });
18
+ }
@@ -0,0 +1,49 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createProjectFilesPreferences, projectFilesUrl, PROJECT_FILES_STORAGE_KEY } from "./project-files-state.ts";
4
+
5
+ test("project file state survives reconstruction and remains isolated between projects", () => {
6
+ const saved = new Map<string, string>();
7
+ const storage = { getItem: (key: string) => saved.get(key) ?? null, setItem: (key: string, value: string) => { saved.set(key, value); } };
8
+ const state = createProjectFilesPreferences(storage);
9
+ state.set("a", { file: "src/a.md", expanded: ["src", "assets"], source: true, search: "needle", searchMode: "content" });
10
+ state.set("b", { file: "README.md" });
11
+ const reopened = createProjectFilesPreferences(storage);
12
+ assert.deepEqual(reopened.get("a"), state.get("a"));
13
+ assert.equal(reopened.get("b").file, "README.md");
14
+ assert.equal(reopened.get("b").source, false);
15
+ reopened.set("a", { expanded: [] });
16
+ assert.deepEqual(reopened.get("a").expanded, [], "manual collapsing is retained independently of the selected file");
17
+ assert.equal(reopened.get("a").file, "src/a.md");
18
+ });
19
+
20
+ test("saved file links encode literal names and reject invalid navigation paths", () => {
21
+ const state = createProjectFilesPreferences({ getItem: () => JSON.stringify({ version: 1, projects: [
22
+ ["a", { file: "../escape", expanded: ["src", "src", "/outside"], searchMode: "bad" }],
23
+ ] }), setItem: () => {} });
24
+ assert.equal(state.get("a").file, "");
25
+ assert.deepEqual(state.get("a").expanded, ["src"]);
26
+ assert.equal(projectFilesUrl("p", "目录/a #1?.md"), "/p/p/files/%E7%9B%AE%E5%BD%95/a%20%231%3F.md");
27
+ assert.equal(projectFilesUrl("p", ""), "/p/p/files");
28
+ });
29
+
30
+ test("unavailable storage keeps in-memory state, with a bounded project catalog", () => {
31
+ const state = createProjectFilesPreferences({ getItem: () => { throw new Error("blocked"); }, setItem: () => { throw new Error("full"); } });
32
+ for (let i = 0; i < 55; i++) state.set(`p${i}`, { file: `${i}.txt` });
33
+ assert.equal(state.get("p0").file, "");
34
+ assert.equal(state.get("p54").file, "54.txt");
35
+ let persisted = "";
36
+ createProjectFilesPreferences({ getItem: () => null, setItem: (key, value) => { assert.equal(key, PROJECT_FILES_STORAGE_KEY); persisted = value; } }).set("p", { expanded: Array.from({ length: 600 }, (_, i) => `dir${i}`) });
37
+ assert.equal(JSON.parse(persisted).projects[0][1].expanded.length, 512);
38
+ });
39
+
40
+ test("stale preference instances preserve other projects and unrelated fields", () => {
41
+ const saved = new Map<string, string>();
42
+ const storage = { getItem: (key: string) => saved.get(key) ?? null, setItem: (key: string, value: string) => { saved.set(key, value); } };
43
+ const a = createProjectFilesPreferences(storage), b = createProjectFilesPreferences(storage);
44
+ a.set("a", { file: "a.md", source: true }); b.set("b", { file: "b.md" });
45
+ b.set("a", { search: "needle" }); a.set("a", (state) => ({ expanded: [...state.expanded, "src"] }));
46
+ const reopened = createProjectFilesPreferences(storage);
47
+ assert.deepEqual(reopened.get("a"), { file: "a.md", source: true, search: "needle", expanded: ["src"], searchMode: "name" });
48
+ assert.equal(reopened.get("b").file, "b.md");
49
+ });
@@ -0,0 +1,64 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import { createPreferenceDocument, type PreferenceStorage } from "./preference-document.ts";
3
+
4
+ export type ProjectFilesState = { file: string; expanded: string[]; source: boolean; search: string; searchMode: "name" | "content" };
5
+ export const PROJECT_FILES_STORAGE_KEY = "claude-ide:project-files:v1";
6
+ const empty: ProjectFilesState = { file: "", expanded: [], source: false, search: "", searchMode: "name" };
7
+ const relative = (value: unknown): value is string => typeof value === "string" && value.length <= 4096
8
+ && !value.startsWith("/") && !value.includes("\0") && !value.split("/").some((part) => part === ".." || part === ".");
9
+ type FilesState = { version: 1; projects: [string, ProjectFilesState][] };
10
+ function normalize(value: Partial<ProjectFilesState>): ProjectFilesState {
11
+ return {
12
+ file: relative(value.file) ? value.file : "",
13
+ expanded: Array.isArray(value.expanded) ? [...new Set(value.expanded.filter((item) => relative(item) && item))].slice(-512) : [],
14
+ source: value.source === true,
15
+ search: typeof value.search === "string" ? value.search.slice(0, 1000) : "",
16
+ searchMode: value.searchMode === "content" ? "content" : "name",
17
+ };
18
+ }
19
+
20
+ /** Small project UI state, independent of mounted pages and session caches. */
21
+ function normalizeDocument(raw: unknown): FilesState {
22
+ const projects = new Map<string, ProjectFilesState>();
23
+ const saved = raw as Partial<FilesState> | null;
24
+ if (saved?.version === 1 && Array.isArray(saved.projects)) for (const entry of saved.projects.slice(-50)) {
25
+ if (Array.isArray(entry) && typeof entry[0] === "string" && entry[1] && typeof entry[1] === "object") projects.set(entry[0], normalize(entry[1]));
26
+ }
27
+ return { version: 1, projects: [...projects] };
28
+ }
29
+
30
+ export function createProjectFilesPreferences(storage?: PreferenceStorage) {
31
+ const document = createPreferenceDocument<FilesState>(PROJECT_FILES_STORAGE_KEY, { version: 1, projects: [] }, normalizeDocument, storage);
32
+ const get = (state: FilesState, pid: string) => state.projects.find(([id]) => id === pid)?.[1] ?? empty;
33
+ return {
34
+ get: (pid: string) => get(document.get(), pid),
35
+ subscribe: document.subscribe,
36
+ refresh: document.refresh,
37
+ set(pid: string, patch: Partial<ProjectFilesState> | ((state: ProjectFilesState) => Partial<ProjectFilesState>)) {
38
+ document.change((state) => {
39
+ const current = get(state, pid);
40
+ const next = normalize({ ...current, ...(typeof patch === "function" ? patch(current) : patch) });
41
+ if (JSON.stringify(current) === JSON.stringify(next)) return state;
42
+ const projects = new Map(state.projects);
43
+ projects.delete(pid); projects.set(pid, next);
44
+ while (projects.size > 50) projects.delete(projects.keys().next().value!);
45
+ return { version: 1, projects: [...projects] };
46
+ });
47
+ },
48
+ };
49
+ }
50
+
51
+ const preferences = createProjectFilesPreferences();
52
+ export const setProjectFilesState = preferences.set;
53
+ export function useProjectFilesState(pid: string) {
54
+ return useSyncExternalStore(preferences.subscribe, () => preferences.get(pid), () => empty);
55
+ }
56
+ export function rememberProjectFile(pid: string, file: string) {
57
+ if (!relative(file) || !file) return;
58
+ preferences.set(pid, (state) => {
59
+ if (state.file === file) return {};
60
+ const parts = file.split("/").slice(0, -1);
61
+ return { file, expanded: [...state.expanded, ...parts.map((_, index) => parts.slice(0, index + 1).join("/"))] };
62
+ });
63
+ }
64
+ export const projectFilesUrl = (pid: string, file: string) => `/p/${pid}/files${file ? `/${file.split("/").map(encodeURIComponent).join("/")}` : ""}`;
@@ -0,0 +1,13 @@
1
+ import { useSyncExternalStore } from "react";
2
+
3
+ const versions = new Map<string, number>(), listeners = new Set<() => void>();
4
+ const subscribe = (listener: () => void) => { listeners.add(listener); return () => { listeners.delete(listener); }; };
5
+
6
+ /** A fresh URL makes the browser revalidate images after an upload. No image bytes are retained. */
7
+ export function refreshProjectImages(pid: string) {
8
+ versions.set(pid, (versions.get(pid) ?? 0) + 1);
9
+ listeners.forEach((listener) => listener());
10
+ }
11
+ export function useProjectImageVersion(pid: string) {
12
+ return useSyncExternalStore(subscribe, () => versions.get(pid) ?? 0, () => 0);
13
+ }
package/web/src/pwa.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Service worker registration. Browsers only allow service workers (and therefore Web Push) in a
3
+ * secure context — https or localhost. Over plain http on the LAN we skip quietly.
4
+ */
5
+
6
+ let registration: Promise<ServiceWorkerRegistration | undefined> | undefined;
7
+
8
+ export function swAvailable(): boolean {
9
+ return typeof window !== "undefined" && window.isSecureContext && "serviceWorker" in navigator;
10
+ }
11
+
12
+ /** Registers /sw.js once (production builds only; the dev server has no sw.js). Never throws. */
13
+ export function registerServiceWorker(): Promise<ServiceWorkerRegistration | undefined> {
14
+ if (registration) return registration;
15
+ if (!import.meta.env.PROD || !swAvailable()) return (registration = Promise.resolve(undefined));
16
+ registration = navigator.serviceWorker.register("/sw.js", { scope: "/" }).catch((err: unknown) => {
17
+ console.warn("service worker registration failed", err);
18
+ return undefined;
19
+ });
20
+ return registration;
21
+ }
22
+
23
+ /** Web Push needs a service worker, PushManager and the Notification API (iOS: only once added to the home screen). */
24
+ export function pushAvailable(): boolean {
25
+ return swAvailable() && "PushManager" in window && "Notification" in window;
26
+ }
27
+
28
+ /** The active push subscription of this device, if any. */
29
+ export async function currentPushSubscription(): Promise<PushSubscription | null> {
30
+ if (!pushAvailable()) return null;
31
+ const reg = await registerServiceWorker();
32
+ return (await reg?.pushManager.getSubscription().catch(() => null)) ?? null;
33
+ }
@@ -0,0 +1,5 @@
1
+ import { QueryClient } from "@tanstack/react-query";
2
+
3
+ export const queryClient = new QueryClient({
4
+ defaultOptions: { queries: { retry: false, refetchOnWindowFocus: false } },
5
+ });