@rexvane/piweb 0.3.1

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 (535) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +128 -0
  3. package/assets/showcase.png +0 -0
  4. package/bin/pi.js +50 -0
  5. package/bin/piweb.js +12 -0
  6. package/next.config.ts +21 -0
  7. package/package.json +88 -0
  8. package/postcss.config.mjs +7 -0
  9. package/public/file-icons/3d.svg +1 -0
  10. package/public/file-icons/LICENSE +8 -0
  11. package/public/file-icons/adobe-illustrator.svg +1 -0
  12. package/public/file-icons/adobe-photoshop.svg +1 -0
  13. package/public/file-icons/agent.svg +1 -0
  14. package/public/file-icons/asciidoc.svg +1 -0
  15. package/public/file-icons/assembly.svg +1 -0
  16. package/public/file-icons/astro.svg +1 -0
  17. package/public/file-icons/audio.svg +1 -0
  18. package/public/file-icons/authors.svg +1 -0
  19. package/public/file-icons/babel.svg +1 -0
  20. package/public/file-icons/bibliography.svg +1 -0
  21. package/public/file-icons/bicep.svg +1 -0
  22. package/public/file-icons/blender.svg +1 -0
  23. package/public/file-icons/bun.svg +1 -0
  24. package/public/file-icons/c.svg +1 -0
  25. package/public/file-icons/certificate.svg +1 -0
  26. package/public/file-icons/changelog.svg +1 -0
  27. package/public/file-icons/clojure.svg +1 -0
  28. package/public/file-icons/cmake.svg +1 -0
  29. package/public/file-icons/codeowners.svg +1 -0
  30. package/public/file-icons/commitlint.svg +1 -0
  31. package/public/file-icons/conduct.svg +1 -0
  32. package/public/file-icons/console.svg +1 -0
  33. package/public/file-icons/contributing.svg +1 -0
  34. package/public/file-icons/cpp.svg +1 -0
  35. package/public/file-icons/csharp.svg +1 -0
  36. package/public/file-icons/css.svg +1 -0
  37. package/public/file-icons/cursor.svg +1 -0
  38. package/public/file-icons/dart.svg +1 -0
  39. package/public/file-icons/database.svg +1 -0
  40. package/public/file-icons/deno.svg +1 -0
  41. package/public/file-icons/diff.svg +1 -0
  42. package/public/file-icons/docker.svg +1 -0
  43. package/public/file-icons/document.svg +1 -0
  44. package/public/file-icons/editorconfig.svg +1 -0
  45. package/public/file-icons/ejs.svg +1 -0
  46. package/public/file-icons/elixir.svg +1 -0
  47. package/public/file-icons/elm.svg +1 -0
  48. package/public/file-icons/email.svg +1 -0
  49. package/public/file-icons/erlang.svg +1 -0
  50. package/public/file-icons/eslint.svg +1 -0
  51. package/public/file-icons/favicon.svg +1 -0
  52. package/public/file-icons/file.svg +1 -0
  53. package/public/file-icons/folder-admin-open.svg +1 -0
  54. package/public/file-icons/folder-admin.svg +1 -0
  55. package/public/file-icons/folder-android-open.svg +1 -0
  56. package/public/file-icons/folder-android.svg +1 -0
  57. package/public/file-icons/folder-api-open.svg +1 -0
  58. package/public/file-icons/folder-api.svg +1 -0
  59. package/public/file-icons/folder-app-open.svg +1 -0
  60. package/public/file-icons/folder-app.svg +1 -0
  61. package/public/file-icons/folder-archive-open.svg +1 -0
  62. package/public/file-icons/folder-archive.svg +1 -0
  63. package/public/file-icons/folder-atom-open.svg +1 -0
  64. package/public/file-icons/folder-atom.svg +1 -0
  65. package/public/file-icons/folder-audio-open.svg +1 -0
  66. package/public/file-icons/folder-audio.svg +1 -0
  67. package/public/file-icons/folder-aws-open.svg +1 -0
  68. package/public/file-icons/folder-aws.svg +1 -0
  69. package/public/file-icons/folder-backup-open.svg +1 -0
  70. package/public/file-icons/folder-backup.svg +1 -0
  71. package/public/file-icons/folder-benchmark-open.svg +1 -0
  72. package/public/file-icons/folder-benchmark.svg +1 -0
  73. package/public/file-icons/folder-cart-open.svg +1 -0
  74. package/public/file-icons/folder-cart.svg +1 -0
  75. package/public/file-icons/folder-ci-open.svg +1 -0
  76. package/public/file-icons/folder-ci.svg +1 -0
  77. package/public/file-icons/folder-circleci-open.svg +1 -0
  78. package/public/file-icons/folder-circleci.svg +1 -0
  79. package/public/file-icons/folder-class-open.svg +1 -0
  80. package/public/file-icons/folder-class.svg +1 -0
  81. package/public/file-icons/folder-client-open.svg +1 -0
  82. package/public/file-icons/folder-client.svg +1 -0
  83. package/public/file-icons/folder-command-open.svg +1 -0
  84. package/public/file-icons/folder-command.svg +1 -0
  85. package/public/file-icons/folder-components-open.svg +1 -0
  86. package/public/file-icons/folder-components.svg +1 -0
  87. package/public/file-icons/folder-config-open.svg +1 -0
  88. package/public/file-icons/folder-config.svg +1 -0
  89. package/public/file-icons/folder-console-open.svg +1 -0
  90. package/public/file-icons/folder-console.svg +1 -0
  91. package/public/file-icons/folder-content-open.svg +1 -0
  92. package/public/file-icons/folder-content.svg +1 -0
  93. package/public/file-icons/folder-context-open.svg +1 -0
  94. package/public/file-icons/folder-context.svg +1 -0
  95. package/public/file-icons/folder-contract-open.svg +1 -0
  96. package/public/file-icons/folder-contract.svg +1 -0
  97. package/public/file-icons/folder-controller-open.svg +1 -0
  98. package/public/file-icons/folder-controller.svg +1 -0
  99. package/public/file-icons/folder-core-open.svg +1 -0
  100. package/public/file-icons/folder-core.svg +1 -0
  101. package/public/file-icons/folder-coverage-open.svg +1 -0
  102. package/public/file-icons/folder-coverage.svg +1 -0
  103. package/public/file-icons/folder-css-open.svg +1 -0
  104. package/public/file-icons/folder-css.svg +1 -0
  105. package/public/file-icons/folder-database-open.svg +1 -0
  106. package/public/file-icons/folder-database.svg +1 -0
  107. package/public/file-icons/folder-debug-open.svg +1 -0
  108. package/public/file-icons/folder-debug.svg +1 -0
  109. package/public/file-icons/folder-deprecated-open.svg +1 -0
  110. package/public/file-icons/folder-deprecated.svg +1 -0
  111. package/public/file-icons/folder-desktop-open.svg +1 -0
  112. package/public/file-icons/folder-desktop.svg +1 -0
  113. package/public/file-icons/folder-dist-open.svg +1 -0
  114. package/public/file-icons/folder-dist.svg +1 -0
  115. package/public/file-icons/folder-docker-open.svg +1 -0
  116. package/public/file-icons/folder-docker.svg +1 -0
  117. package/public/file-icons/folder-docs-open.svg +1 -0
  118. package/public/file-icons/folder-docs.svg +1 -0
  119. package/public/file-icons/folder-element-open.svg +1 -0
  120. package/public/file-icons/folder-element.svg +1 -0
  121. package/public/file-icons/folder-environment-open.svg +1 -0
  122. package/public/file-icons/folder-environment.svg +1 -0
  123. package/public/file-icons/folder-event-open.svg +1 -0
  124. package/public/file-icons/folder-event.svg +1 -0
  125. package/public/file-icons/folder-examples-open.svg +1 -0
  126. package/public/file-icons/folder-examples.svg +1 -0
  127. package/public/file-icons/folder-features-open.svg +1 -0
  128. package/public/file-icons/folder-features.svg +1 -0
  129. package/public/file-icons/folder-font-open.svg +1 -0
  130. package/public/file-icons/folder-font.svg +1 -0
  131. package/public/file-icons/folder-functions-open.svg +1 -0
  132. package/public/file-icons/folder-functions.svg +1 -0
  133. package/public/file-icons/folder-generator-open.svg +1 -0
  134. package/public/file-icons/folder-generator.svg +1 -0
  135. package/public/file-icons/folder-git-open.svg +1 -0
  136. package/public/file-icons/folder-git.svg +1 -0
  137. package/public/file-icons/folder-github-open.svg +1 -0
  138. package/public/file-icons/folder-github.svg +1 -0
  139. package/public/file-icons/folder-gitlab-open.svg +1 -0
  140. package/public/file-icons/folder-gitlab.svg +1 -0
  141. package/public/file-icons/folder-graphql-open.svg +1 -0
  142. package/public/file-icons/folder-graphql.svg +1 -0
  143. package/public/file-icons/folder-helper-open.svg +1 -0
  144. package/public/file-icons/folder-helper.svg +1 -0
  145. package/public/file-icons/folder-hook-open.svg +1 -0
  146. package/public/file-icons/folder-hook.svg +1 -0
  147. package/public/file-icons/folder-husky-open.svg +1 -0
  148. package/public/file-icons/folder-husky.svg +1 -0
  149. package/public/file-icons/folder-i18n-open.svg +1 -0
  150. package/public/file-icons/folder-i18n.svg +1 -0
  151. package/public/file-icons/folder-images-open.svg +1 -0
  152. package/public/file-icons/folder-images.svg +1 -0
  153. package/public/file-icons/folder-include-open.svg +1 -0
  154. package/public/file-icons/folder-include.svg +1 -0
  155. package/public/file-icons/folder-intellij-open.svg +1 -0
  156. package/public/file-icons/folder-intellij.svg +1 -0
  157. package/public/file-icons/folder-interface-open.svg +1 -0
  158. package/public/file-icons/folder-interface.svg +1 -0
  159. package/public/file-icons/folder-ios-open.svg +1 -0
  160. package/public/file-icons/folder-ios.svg +1 -0
  161. package/public/file-icons/folder-java-open.svg +1 -0
  162. package/public/file-icons/folder-java.svg +1 -0
  163. package/public/file-icons/folder-job-open.svg +1 -0
  164. package/public/file-icons/folder-job.svg +1 -0
  165. package/public/file-icons/folder-jupyter-open.svg +1 -0
  166. package/public/file-icons/folder-jupyter.svg +1 -0
  167. package/public/file-icons/folder-keys-open.svg +1 -0
  168. package/public/file-icons/folder-keys.svg +1 -0
  169. package/public/file-icons/folder-kubernetes-open.svg +1 -0
  170. package/public/file-icons/folder-kubernetes.svg +1 -0
  171. package/public/file-icons/folder-layout-open.svg +1 -0
  172. package/public/file-icons/folder-layout.svg +1 -0
  173. package/public/file-icons/folder-less-open.svg +1 -0
  174. package/public/file-icons/folder-less.svg +1 -0
  175. package/public/file-icons/folder-lib-open.svg +1 -0
  176. package/public/file-icons/folder-lib.svg +1 -0
  177. package/public/file-icons/folder-log-open.svg +1 -0
  178. package/public/file-icons/folder-log.svg +1 -0
  179. package/public/file-icons/folder-messages-open.svg +1 -0
  180. package/public/file-icons/folder-messages.svg +1 -0
  181. package/public/file-icons/folder-meta-open.svg +1 -0
  182. package/public/file-icons/folder-meta.svg +1 -0
  183. package/public/file-icons/folder-middleware-open.svg +1 -0
  184. package/public/file-icons/folder-middleware.svg +1 -0
  185. package/public/file-icons/folder-migrations-open.svg +1 -0
  186. package/public/file-icons/folder-migrations.svg +1 -0
  187. package/public/file-icons/folder-mobile-open.svg +1 -0
  188. package/public/file-icons/folder-mobile.svg +1 -0
  189. package/public/file-icons/folder-mock-open.svg +1 -0
  190. package/public/file-icons/folder-mock.svg +1 -0
  191. package/public/file-icons/folder-molecule-open.svg +1 -0
  192. package/public/file-icons/folder-molecule.svg +1 -0
  193. package/public/file-icons/folder-next-open.svg +1 -0
  194. package/public/file-icons/folder-next.svg +1 -0
  195. package/public/file-icons/folder-node-open.svg +1 -0
  196. package/public/file-icons/folder-node.svg +1 -0
  197. package/public/file-icons/folder-nuxt-open.svg +1 -0
  198. package/public/file-icons/folder-nuxt.svg +1 -0
  199. package/public/file-icons/folder-open.svg +1 -0
  200. package/public/file-icons/folder-organism-open.svg +1 -0
  201. package/public/file-icons/folder-organism.svg +1 -0
  202. package/public/file-icons/folder-packages-open.svg +1 -0
  203. package/public/file-icons/folder-packages.svg +1 -0
  204. package/public/file-icons/folder-plugin-open.svg +1 -0
  205. package/public/file-icons/folder-plugin.svg +1 -0
  206. package/public/file-icons/folder-prisma-open.svg +1 -0
  207. package/public/file-icons/folder-prisma.svg +1 -0
  208. package/public/file-icons/folder-prompts-open.svg +1 -0
  209. package/public/file-icons/folder-prompts.svg +1 -0
  210. package/public/file-icons/folder-proto-open.svg +1 -0
  211. package/public/file-icons/folder-proto.svg +1 -0
  212. package/public/file-icons/folder-public-open.svg +1 -0
  213. package/public/file-icons/folder-public.svg +1 -0
  214. package/public/file-icons/folder-python-open.svg +1 -0
  215. package/public/file-icons/folder-python.svg +1 -0
  216. package/public/file-icons/folder-queue-open.svg +1 -0
  217. package/public/file-icons/folder-queue.svg +1 -0
  218. package/public/file-icons/folder-resource-open.svg +1 -0
  219. package/public/file-icons/folder-resource.svg +1 -0
  220. package/public/file-icons/folder-review-open.svg +1 -0
  221. package/public/file-icons/folder-review.svg +1 -0
  222. package/public/file-icons/folder-robot-open.svg +1 -0
  223. package/public/file-icons/folder-robot.svg +1 -0
  224. package/public/file-icons/folder-routes-open.svg +1 -0
  225. package/public/file-icons/folder-routes.svg +1 -0
  226. package/public/file-icons/folder-rust-open.svg +1 -0
  227. package/public/file-icons/folder-rust.svg +1 -0
  228. package/public/file-icons/folder-sass-open.svg +1 -0
  229. package/public/file-icons/folder-sass.svg +1 -0
  230. package/public/file-icons/folder-scripts-open.svg +1 -0
  231. package/public/file-icons/folder-scripts.svg +1 -0
  232. package/public/file-icons/folder-secure-open.svg +1 -0
  233. package/public/file-icons/folder-secure.svg +1 -0
  234. package/public/file-icons/folder-seeders-open.svg +1 -0
  235. package/public/file-icons/folder-seeders.svg +1 -0
  236. package/public/file-icons/folder-server-open.svg +1 -0
  237. package/public/file-icons/folder-server.svg +1 -0
  238. package/public/file-icons/folder-shared-open.svg +1 -0
  239. package/public/file-icons/folder-shared.svg +1 -0
  240. package/public/file-icons/folder-skills-open.svg +1 -0
  241. package/public/file-icons/folder-skills.svg +1 -0
  242. package/public/file-icons/folder-src-open.svg +1 -0
  243. package/public/file-icons/folder-src.svg +1 -0
  244. package/public/file-icons/folder-store-open.svg +1 -0
  245. package/public/file-icons/folder-store.svg +1 -0
  246. package/public/file-icons/folder-storybook-open.svg +1 -0
  247. package/public/file-icons/folder-storybook.svg +1 -0
  248. package/public/file-icons/folder-supabase-open.svg +1 -0
  249. package/public/file-icons/folder-supabase.svg +1 -0
  250. package/public/file-icons/folder-svelte-open.svg +1 -0
  251. package/public/file-icons/folder-svelte.svg +1 -0
  252. package/public/file-icons/folder-target-open.svg +1 -0
  253. package/public/file-icons/folder-target.svg +1 -0
  254. package/public/file-icons/folder-tasks-open.svg +1 -0
  255. package/public/file-icons/folder-tasks.svg +1 -0
  256. package/public/file-icons/folder-temp-open.svg +1 -0
  257. package/public/file-icons/folder-temp.svg +1 -0
  258. package/public/file-icons/folder-template-open.svg +1 -0
  259. package/public/file-icons/folder-template.svg +1 -0
  260. package/public/file-icons/folder-terraform-open.svg +1 -0
  261. package/public/file-icons/folder-terraform.svg +1 -0
  262. package/public/file-icons/folder-test-open.svg +1 -0
  263. package/public/file-icons/folder-test.svg +1 -0
  264. package/public/file-icons/folder-theme-open.svg +1 -0
  265. package/public/file-icons/folder-theme.svg +1 -0
  266. package/public/file-icons/folder-tools-open.svg +1 -0
  267. package/public/file-icons/folder-tools.svg +1 -0
  268. package/public/file-icons/folder-turborepo-open.svg +1 -0
  269. package/public/file-icons/folder-turborepo.svg +1 -0
  270. package/public/file-icons/folder-typescript-open.svg +1 -0
  271. package/public/file-icons/folder-typescript.svg +1 -0
  272. package/public/file-icons/folder-ui-open.svg +1 -0
  273. package/public/file-icons/folder-ui.svg +1 -0
  274. package/public/file-icons/folder-utils-open.svg +1 -0
  275. package/public/file-icons/folder-utils.svg +1 -0
  276. package/public/file-icons/folder-video-open.svg +1 -0
  277. package/public/file-icons/folder-video.svg +1 -0
  278. package/public/file-icons/folder-views-open.svg +1 -0
  279. package/public/file-icons/folder-views.svg +1 -0
  280. package/public/file-icons/folder-vscode-open.svg +1 -0
  281. package/public/file-icons/folder-vscode.svg +1 -0
  282. package/public/file-icons/folder.svg +1 -0
  283. package/public/file-icons/font.svg +1 -0
  284. package/public/file-icons/fsharp.svg +1 -0
  285. package/public/file-icons/gemfile.svg +1 -0
  286. package/public/file-icons/git.svg +1 -0
  287. package/public/file-icons/go-mod.svg +1 -0
  288. package/public/file-icons/go.svg +1 -0
  289. package/public/file-icons/gradle.svg +1 -0
  290. package/public/file-icons/graphql.svg +1 -0
  291. package/public/file-icons/groovy.svg +1 -0
  292. package/public/file-icons/h.svg +1 -0
  293. package/public/file-icons/handlebars.svg +1 -0
  294. package/public/file-icons/haskell.svg +1 -0
  295. package/public/file-icons/hcl.svg +1 -0
  296. package/public/file-icons/heroku.svg +1 -0
  297. package/public/file-icons/hpp.svg +1 -0
  298. package/public/file-icons/html.svg +1 -0
  299. package/public/file-icons/husky.svg +1 -0
  300. package/public/file-icons/i18n.svg +1 -0
  301. package/public/file-icons/image.svg +1 -0
  302. package/public/file-icons/java.svg +1 -0
  303. package/public/file-icons/javascript.svg +1 -0
  304. package/public/file-icons/jenkins.svg +1 -0
  305. package/public/file-icons/jest.svg +1 -0
  306. package/public/file-icons/jsconfig.svg +1 -0
  307. package/public/file-icons/json.svg +1 -0
  308. package/public/file-icons/julia.svg +1 -0
  309. package/public/file-icons/jupyter.svg +1 -0
  310. package/public/file-icons/just.svg +1 -0
  311. package/public/file-icons/key.svg +1 -0
  312. package/public/file-icons/kotlin.svg +1 -0
  313. package/public/file-icons/lerna.svg +1 -0
  314. package/public/file-icons/less.svg +1 -0
  315. package/public/file-icons/license.svg +1 -0
  316. package/public/file-icons/lintstaged.svg +1 -0
  317. package/public/file-icons/liquid.svg +1 -0
  318. package/public/file-icons/lock.svg +1 -0
  319. package/public/file-icons/log.svg +1 -0
  320. package/public/file-icons/lua.svg +1 -0
  321. package/public/file-icons/makefile.svg +1 -0
  322. package/public/file-icons/markdown.svg +1 -0
  323. package/public/file-icons/maven.svg +1 -0
  324. package/public/file-icons/mdx.svg +1 -0
  325. package/public/file-icons/netlify.svg +1 -0
  326. package/public/file-icons/next.svg +1 -0
  327. package/public/file-icons/nim.svg +1 -0
  328. package/public/file-icons/nix.svg +1 -0
  329. package/public/file-icons/nodejs.svg +1 -0
  330. package/public/file-icons/nodemon.svg +1 -0
  331. package/public/file-icons/npm.svg +1 -0
  332. package/public/file-icons/nx.svg +1 -0
  333. package/public/file-icons/objective-c.svg +1 -0
  334. package/public/file-icons/objective-cpp.svg +1 -0
  335. package/public/file-icons/ocaml.svg +1 -0
  336. package/public/file-icons/pdf.svg +1 -0
  337. package/public/file-icons/php.svg +1 -0
  338. package/public/file-icons/pm2-ecosystem.svg +1 -0
  339. package/public/file-icons/pnpm.svg +1 -0
  340. package/public/file-icons/poetry.svg +1 -0
  341. package/public/file-icons/postcss.svg +1 -0
  342. package/public/file-icons/powerpoint.svg +1 -0
  343. package/public/file-icons/powershell.svg +1 -0
  344. package/public/file-icons/prettier.svg +1 -0
  345. package/public/file-icons/prisma.svg +1 -0
  346. package/public/file-icons/prolog.svg +1 -0
  347. package/public/file-icons/proto.svg +1 -0
  348. package/public/file-icons/pug.svg +1 -0
  349. package/public/file-icons/python-misc.svg +1 -0
  350. package/public/file-icons/python.svg +1 -0
  351. package/public/file-icons/r.svg +1 -0
  352. package/public/file-icons/razor.svg +1 -0
  353. package/public/file-icons/react.svg +1 -0
  354. package/public/file-icons/react_ts.svg +1 -0
  355. package/public/file-icons/readme.svg +1 -0
  356. package/public/file-icons/renovate.svg +1 -0
  357. package/public/file-icons/robots.svg +1 -0
  358. package/public/file-icons/rollup.svg +1 -0
  359. package/public/file-icons/ruby.svg +1 -0
  360. package/public/file-icons/rust.svg +1 -0
  361. package/public/file-icons/sass.svg +1 -0
  362. package/public/file-icons/scala.svg +1 -0
  363. package/public/file-icons/semantic-release.svg +1 -0
  364. package/public/file-icons/settings.svg +1 -0
  365. package/public/file-icons/shader.svg +1 -0
  366. package/public/file-icons/sml.svg +1 -0
  367. package/public/file-icons/solidity.svg +1 -0
  368. package/public/file-icons/stylus.svg +1 -0
  369. package/public/file-icons/svelte.svg +1 -0
  370. package/public/file-icons/svg.svg +1 -0
  371. package/public/file-icons/swift.svg +1 -0
  372. package/public/file-icons/table.svg +1 -0
  373. package/public/file-icons/tailwindcss.svg +1 -0
  374. package/public/file-icons/taskfile.svg +1 -0
  375. package/public/file-icons/terraform.svg +1 -0
  376. package/public/file-icons/test-js.svg +1 -0
  377. package/public/file-icons/test-jsx.svg +1 -0
  378. package/public/file-icons/test-ts.svg +1 -0
  379. package/public/file-icons/tex.svg +1 -0
  380. package/public/file-icons/toml.svg +1 -0
  381. package/public/file-icons/travis.svg +1 -0
  382. package/public/file-icons/tsconfig.svg +1 -0
  383. package/public/file-icons/tune.svg +1 -0
  384. package/public/file-icons/turborepo.svg +1 -0
  385. package/public/file-icons/typescript-def.svg +1 -0
  386. package/public/file-icons/typescript.svg +1 -0
  387. package/public/file-icons/unity.svg +1 -0
  388. package/public/file-icons/uv.svg +1 -0
  389. package/public/file-icons/vala.svg +1 -0
  390. package/public/file-icons/vercel.svg +1 -0
  391. package/public/file-icons/video.svg +1 -0
  392. package/public/file-icons/vim.svg +1 -0
  393. package/public/file-icons/vite.svg +1 -0
  394. package/public/file-icons/vitest.svg +1 -0
  395. package/public/file-icons/vlang.svg +1 -0
  396. package/public/file-icons/vue.svg +1 -0
  397. package/public/file-icons/webassembly.svg +1 -0
  398. package/public/file-icons/webpack.svg +1 -0
  399. package/public/file-icons/word.svg +1 -0
  400. package/public/file-icons/xml.svg +1 -0
  401. package/public/file-icons/yaml.svg +1 -0
  402. package/public/file-icons/yarn.svg +1 -0
  403. package/public/file-icons/zig.svg +1 -0
  404. package/public/file-icons/zip.svg +1 -0
  405. package/public/provider-icons/antgroup-color.svg +1 -0
  406. package/public/provider-icons/anthropic.svg +1 -0
  407. package/public/provider-icons/azure-color.svg +1 -0
  408. package/public/provider-icons/baseten.svg +1 -0
  409. package/public/provider-icons/bedrock-color.svg +1 -0
  410. package/public/provider-icons/cerebras-color.svg +1 -0
  411. package/public/provider-icons/cloudflare-color.svg +1 -0
  412. package/public/provider-icons/deepseek-color.svg +1 -0
  413. package/public/provider-icons/fireworks-color.svg +1 -0
  414. package/public/provider-icons/githubcopilot.svg +1 -0
  415. package/public/provider-icons/google-color.svg +1 -0
  416. package/public/provider-icons/googlecloud-color.svg +1 -0
  417. package/public/provider-icons/groq.svg +1 -0
  418. package/public/provider-icons/huggingface-color.svg +1 -0
  419. package/public/provider-icons/kimi-color.svg +1 -0
  420. package/public/provider-icons/minimax-color.svg +1 -0
  421. package/public/provider-icons/mistral-color.svg +1 -0
  422. package/public/provider-icons/moonshot.svg +1 -0
  423. package/public/provider-icons/nvidia-color.svg +1 -0
  424. package/public/provider-icons/openai.svg +1 -0
  425. package/public/provider-icons/opencode.svg +1 -0
  426. package/public/provider-icons/openrouter-color.svg +1 -0
  427. package/public/provider-icons/qwen-color.svg +1 -0
  428. package/public/provider-icons/together-color.svg +1 -0
  429. package/public/provider-icons/vercel.svg +1 -0
  430. package/public/provider-icons/xai.svg +1 -0
  431. package/public/provider-icons/xiaomimimo.svg +1 -0
  432. package/public/provider-icons/zai.svg +1 -0
  433. package/scripts/build-output.mjs +131 -0
  434. package/scripts/dev.mjs +18 -0
  435. package/scripts/entrypoint.mjs +8 -0
  436. package/scripts/install-build.mjs +134 -0
  437. package/scripts/launcher.mjs +249 -0
  438. package/scripts/pick-folder.ps1 +110 -0
  439. package/scripts/postinstall.mjs +12 -0
  440. package/scripts/process-runner.mjs +60 -0
  441. package/scripts/release.mjs +172 -0
  442. package/scripts/sync-file-icons.mjs +118 -0
  443. package/scripts/sync-provider-icons.mjs +41 -0
  444. package/src/app/api/agent/[id]/events/route.ts +81 -0
  445. package/src/app/api/agent/[id]/route.ts +24 -0
  446. package/src/app/api/agent/[id]/tree/route.ts +49 -0
  447. package/src/app/api/agent/new/route.ts +19 -0
  448. package/src/app/api/config/route.ts +22 -0
  449. package/src/app/api/files/route.ts +63 -0
  450. package/src/app/api/git/route.ts +32 -0
  451. package/src/app/api/growth/route.ts +50 -0
  452. package/src/app/api/health/route.ts +7 -0
  453. package/src/app/api/models/route.ts +129 -0
  454. package/src/app/api/pi-settings/route.ts +22 -0
  455. package/src/app/api/plugins/route.ts +79 -0
  456. package/src/app/api/security/route.ts +41 -0
  457. package/src/app/api/sessions/[id]/export/route.ts +25 -0
  458. package/src/app/api/sessions/[id]/route.ts +37 -0
  459. package/src/app/api/sessions/route.ts +23 -0
  460. package/src/app/api/skills/route.ts +39 -0
  461. package/src/app/api/update/route.ts +33 -0
  462. package/src/app/api/version/route.ts +8 -0
  463. package/src/app/api/web-auth/route.ts +80 -0
  464. package/src/app/api/workspaces/route.ts +78 -0
  465. package/src/app/globals.css +2039 -0
  466. package/src/app/icon.svg +21 -0
  467. package/src/app/layout.tsx +29 -0
  468. package/src/app/login/page.tsx +111 -0
  469. package/src/app/page.tsx +10 -0
  470. package/src/components/AppShell.tsx +1171 -0
  471. package/src/components/ChatInput.tsx +729 -0
  472. package/src/components/ChatWindow.tsx +1320 -0
  473. package/src/components/ContextMeter.tsx +131 -0
  474. package/src/components/DiffView.tsx +139 -0
  475. package/src/components/ExtensionUI.tsx +181 -0
  476. package/src/components/FileViewer.tsx +555 -0
  477. package/src/components/FullDiff.tsx +220 -0
  478. package/src/components/GitPanel.tsx +357 -0
  479. package/src/components/GrowthTree.tsx +317 -0
  480. package/src/components/ModelSelector.tsx +286 -0
  481. package/src/components/OutlineRail.tsx +166 -0
  482. package/src/components/PiMark.tsx +79 -0
  483. package/src/components/ProjectPanel.tsx +309 -0
  484. package/src/components/ProviderBrand.tsx +28 -0
  485. package/src/components/ProviderSetupModal.module.css +560 -0
  486. package/src/components/ProviderSetupModal.tsx +559 -0
  487. package/src/components/SessionSidebar.tsx +1066 -0
  488. package/src/components/SettingsPanel.tsx +2216 -0
  489. package/src/components/TrajectoryAnalyzer.module.css +225 -0
  490. package/src/components/TrajectoryView.tsx +289 -0
  491. package/src/components/icons.tsx +443 -0
  492. package/src/hooks/useFileViewer.ts +58 -0
  493. package/src/hooks/useGrowth.ts +635 -0
  494. package/src/hooks/usePiWeb.ts +900 -0
  495. package/src/i18n.tsx +1043 -0
  496. package/src/lib/agent-manager.ts +1348 -0
  497. package/src/lib/app-root.ts +4 -0
  498. package/src/lib/auth.ts +51 -0
  499. package/src/lib/command-validation.ts +116 -0
  500. package/src/lib/enter-behavior.ts +40 -0
  501. package/src/lib/extension-ui.ts +134 -0
  502. package/src/lib/file-icons.json +557 -0
  503. package/src/lib/file-icons.ts +39 -0
  504. package/src/lib/files-service.ts +280 -0
  505. package/src/lib/git-service.ts +255 -0
  506. package/src/lib/growth-rounds.ts +84 -0
  507. package/src/lib/growth-service.ts +586 -0
  508. package/src/lib/growth-tracker.ts +328 -0
  509. package/src/lib/growth-tree.ts +245 -0
  510. package/src/lib/growth-turns.ts +23 -0
  511. package/src/lib/highlight.ts +59 -0
  512. package/src/lib/model-draft.ts +103 -0
  513. package/src/lib/models-service.ts +852 -0
  514. package/src/lib/path-security.ts +101 -0
  515. package/src/lib/pi-settings.ts +115 -0
  516. package/src/lib/pi.ts +367 -0
  517. package/src/lib/plugins-service.ts +312 -0
  518. package/src/lib/process-format.ts +261 -0
  519. package/src/lib/provider-display.ts +57 -0
  520. package/src/lib/security-service.ts +52 -0
  521. package/src/lib/semver.ts +39 -0
  522. package/src/lib/session-reader.ts +204 -0
  523. package/src/lib/settings-write-lock.ts +45 -0
  524. package/src/lib/skills-service.ts +97 -0
  525. package/src/lib/text-sanitize.ts +20 -0
  526. package/src/lib/theme.ts +137 -0
  527. package/src/lib/trajectory.ts +294 -0
  528. package/src/lib/types.ts +300 -0
  529. package/src/lib/update-service.ts +129 -0
  530. package/src/lib/version.ts +6 -0
  531. package/src/lib/web-auth-shared.ts +35 -0
  532. package/src/lib/web-auth.ts +53 -0
  533. package/src/lib/workspace-store.ts +261 -0
  534. package/src/proxy.ts +53 -0
  535. package/tsconfig.json +49 -0
@@ -0,0 +1,559 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import {
5
+ IconCheckOutline14,
6
+ IconChevronDown14,
7
+ IconCloseOutline14,
8
+ IconPlusOutline16,
9
+ IconSearchOutline16,
10
+ IconTrashOutline16,
11
+ } from "@/components/icons";
12
+ import { ProviderBrand } from "@/components/ProviderBrand";
13
+ import { useI18n } from "@/i18n";
14
+ import { CUSTOM_PROVIDER_ID_PATTERN, customApiOptions } from "@/lib/provider-display";
15
+ import { formatCapacity, parseCapacity, serializeModelDraft, validateModelDrafts, type ModelDraft } from "@/lib/model-draft";
16
+ import type { ProviderView } from "@/lib/models-service";
17
+ import styles from "./ProviderSetupModal.module.css";
18
+
19
+ export { formatCapacity, parseCapacity, serializeModelDraft, validateModelDrafts, type ModelDraft } from "@/lib/model-draft";
20
+
21
+ export interface BuiltinProviderSetup {
22
+ providerId: string;
23
+ apiKey: string;
24
+ config: Record<string, unknown>;
25
+ }
26
+
27
+ export interface CustomProviderSetup {
28
+ providerId: string;
29
+ config: Record<string, unknown>;
30
+ }
31
+
32
+ type SubmitResult = { success: boolean; error?: string };
33
+
34
+ export function ProviderSetupModal({
35
+ mode,
36
+ providers,
37
+ existingIds,
38
+ onClose,
39
+ onSaveBuiltin,
40
+ onSaveCustom,
41
+ onOAuthLogin,
42
+ }: {
43
+ mode: "builtin" | "custom";
44
+ providers: ProviderView[];
45
+ existingIds: string[];
46
+ onClose: () => void;
47
+ onSaveBuiltin: (value: BuiltinProviderSetup) => Promise<SubmitResult>;
48
+ onSaveCustom: (value: CustomProviderSetup) => Promise<SubmitResult>;
49
+ /** 内置提供商支持 OAuth 时的订阅登录路径;未传则不显示认证方式选择 */
50
+ onOAuthLogin?: (providerId: string) => Promise<SubmitResult>;
51
+ }) {
52
+ const { t } = useI18n();
53
+ const [selectedId, setSelectedId] = useState(providers[0]?.id ?? "");
54
+ const [pickerOpen, setPickerOpen] = useState(false);
55
+ const [search, setSearch] = useState("");
56
+ const [authMethod, setAuthMethod] = useState<"api_key" | "oauth">("api_key");
57
+ const [apiKey, setApiKey] = useState("");
58
+ const [advanced, setAdvanced] = useState(false);
59
+ const [baseUrl, setBaseUrl] = useState("");
60
+ const [providerId, setProviderId] = useState("");
61
+ const [displayName, setDisplayName] = useState("");
62
+ const allApis = customApiOptions(providers.flatMap((provider) => provider.apis));
63
+ const [api, setApi] = useState(allApis.includes("openai-completions") ? "openai-completions" : (allApis[0] ?? "openai-completions"));
64
+ const [models, setModels] = useState<ModelDraft[]>([]);
65
+ const [saving, setSaving] = useState(false);
66
+ const [error, setError] = useState("");
67
+ const pickerRef = useRef<HTMLDivElement>(null);
68
+ const selected = providers.find((provider) => provider.id === selectedId);
69
+ const filtered = providers.filter((provider) => {
70
+ const needle = search.trim().toLocaleLowerCase();
71
+ return !needle || provider.name.toLocaleLowerCase().includes(needle) || provider.id.toLocaleLowerCase().includes(needle);
72
+ });
73
+
74
+ useEffect(() => {
75
+ const handleKeyDown = (event: KeyboardEvent) => {
76
+ if (event.key !== "Escape") return;
77
+ event.preventDefault();
78
+ event.stopPropagation();
79
+ if (pickerOpen) setPickerOpen(false);
80
+ else onClose();
81
+ };
82
+ document.addEventListener("keydown", handleKeyDown, true);
83
+ return () => document.removeEventListener("keydown", handleKeyDown, true);
84
+ }, [onClose, pickerOpen]);
85
+
86
+ useEffect(() => {
87
+ if (!pickerOpen) return;
88
+ const handlePointer = (event: MouseEvent) => {
89
+ if (!pickerRef.current?.contains(event.target as Node)) setPickerOpen(false);
90
+ };
91
+ document.addEventListener("mousedown", handlePointer);
92
+ return () => document.removeEventListener("mousedown", handlePointer);
93
+ }, [pickerOpen]);
94
+
95
+ const updateModel = (index: number, patch: Partial<ModelDraft>) => {
96
+ setModels((current) => current.map((model, modelIndex) => (modelIndex === index ? { ...model, ...patch } : model)));
97
+ };
98
+
99
+ const invalidModels = validateModelDrafts(models);
100
+ const normalizedId = providerId.trim();
101
+ const customIdValid = CUSTOM_PROVIDER_ID_PATTERN.test(normalizedId);
102
+ const customCanSave = customIdValid && !existingIds.includes(normalizedId) && Boolean(baseUrl.trim()) && models.length > 0 && !invalidModels;
103
+ const builtinCanSave = Boolean(selected) && (authMethod === "oauth" || !invalidModels);
104
+ const savingRef = useRef(false);
105
+
106
+ const submit = async () => {
107
+ if (savingRef.current || (mode === "builtin" ? !builtinCanSave : !customCanSave)) return;
108
+ savingRef.current = true;
109
+ setSaving(true);
110
+ setError("");
111
+ try {
112
+ let result: SubmitResult;
113
+ if (mode === "builtin" && selected) {
114
+ if (authMethod === "oauth" && onOAuthLogin) {
115
+ result = await onOAuthLogin(selected.id);
116
+ } else {
117
+ const config: Record<string, unknown> = {};
118
+ if (baseUrl.trim()) config.baseUrl = baseUrl.trim();
119
+ if (models.length) {
120
+ config.api = selected.apis[0] ?? "openai-completions";
121
+ config.models = models.map(serializeModelDraft);
122
+ }
123
+ result = await onSaveBuiltin({ providerId: selected.id, apiKey: apiKey.trim(), config });
124
+ }
125
+ } else {
126
+ result = await onSaveCustom({
127
+ providerId: normalizedId,
128
+ config: {
129
+ name: displayName.trim() || normalizedId,
130
+ baseUrl: baseUrl.trim(),
131
+ api,
132
+ ...(apiKey.trim() ? { apiKey: apiKey.trim() } : {}),
133
+ models: models.map(serializeModelDraft),
134
+ },
135
+ });
136
+ }
137
+ if (result.success) onClose();
138
+ else setError(result.error || t.toastError);
139
+ } catch (reason) {
140
+ setError(reason instanceof Error ? reason.message : t.toastError);
141
+ } finally {
142
+ savingRef.current = false;
143
+ setSaving(false);
144
+ }
145
+ };
146
+
147
+ /** 供 ModelCatalog 的候选对话框调用:用表单当前值(含未保存的 key)探测远端目录 */
148
+ const discoverAvailableModels = async (): Promise<ModelDraft[]> => {
149
+ if (!baseUrl.trim()) throw new Error(t.apiAddress);
150
+ const response = await fetch("/api/models", {
151
+ method: "POST",
152
+ headers: { "Content-Type": "application/json" },
153
+ // builtin 模式带 providerId:表单 key 为空时后端回退到已存密钥 / OAuth 令牌
154
+ body: JSON.stringify({ action: "discoverModels", baseUrl: baseUrl.trim(), api: mode === "builtin" ? selected?.apis[0] : api, apiKey: apiKey.trim(), providerId: mode === "builtin" ? selected?.id : undefined }),
155
+ });
156
+ const result = await response.json();
157
+ if (!response.ok || !result.success) throw new Error(result.error || t.toastError);
158
+ return (result.data.models ?? []).map((model: { id: string; name?: string }) => ({ id: model.id, name: model.name ?? "" }));
159
+ };
160
+
161
+ return (
162
+ <div
163
+ className={styles.mask}
164
+ data-provider-modal="true"
165
+ onMouseDown={(event) => {
166
+ if (event.target === event.currentTarget) onClose();
167
+ }}
168
+ >
169
+ <section className={`${styles.dialog} ${mode === "builtin" ? styles.builtinDialog : ""}`} role="dialog" aria-modal="true" aria-labelledby="provider-setup-title">
170
+ <header className={styles.header}>
171
+ <h2 id="provider-setup-title" className={styles.title}>
172
+ {mode === "builtin" ? t.addProvider : t.customProviderTitle}
173
+ </h2>
174
+ <button className="icon-btn" title={t.close} aria-label={t.close} onClick={onClose}>
175
+ <IconCloseOutline14 size={17} />
176
+ </button>
177
+ </header>
178
+
179
+ <div className={`${styles.body} ${mode === "builtin" ? styles.builtinBody : ""}`}>
180
+ {mode === "builtin" ? (
181
+ <>
182
+ <div className={styles.field}>
183
+ <label className={styles.label}>{t.providerLabel}</label>
184
+ <div className={styles.picker} ref={pickerRef}>
185
+ <button className={styles.providerTrigger} data-open={pickerOpen} onClick={() => setPickerOpen((open) => !open)}>
186
+ {selected ? <ProviderBrand id={selected.id} name={selected.name} size={34} /> : null}
187
+ <span className={styles.providerText}>
188
+ <span className={styles.providerName}>{selected?.name ?? t.selectProvider}</span>
189
+ {selected ? <span className={styles.providerMeta}>{selected.id}</span> : null}
190
+ </span>
191
+ <IconChevronDown14 size={15} />
192
+ </button>
193
+ {pickerOpen ? (
194
+ <div className={styles.menu}>
195
+ <div className={styles.searchWrap}>
196
+ <span className={styles.searchIcon}><IconSearchOutline16 size={15} /></span>
197
+ <input autoFocus className={styles.search} value={search} onChange={(event) => setSearch(event.target.value)} placeholder={t.searchProviders} />
198
+ </div>
199
+ <div className={styles.providerList}>
200
+ {filtered.map((provider) => (
201
+ <button
202
+ key={provider.id}
203
+ className={styles.providerRow}
204
+ data-selected={provider.id === selectedId}
205
+ onClick={() => {
206
+ setSelectedId(provider.id);
207
+ setAuthMethod("api_key");
208
+ setApiKey("");
209
+ setApi(provider.apis[0] ?? "openai-completions");
210
+ setBaseUrl("");
211
+ setModels([]);
212
+ setPickerOpen(false);
213
+ }}
214
+ >
215
+ <ProviderBrand id={provider.id} name={provider.name} size={38} />
216
+ <span className={styles.providerText}>
217
+ <span className={styles.providerName}>{provider.name}</span>
218
+ <span className={styles.providerMeta}>{provider.id} · {t.modelCount.replace("{count}", String(provider.modelCount))}</span>
219
+ </span>
220
+ {provider.id === selectedId ? <span className={styles.selectedMark}><IconCheckOutline14 size={16} /></span> : null}
221
+ </button>
222
+ ))}
223
+ {filtered.length === 0 ? <div className={styles.empty}>{t.noProviderMatches}</div> : null}
224
+ </div>
225
+ </div>
226
+ ) : null}
227
+ </div>
228
+ </div>
229
+
230
+ {selected?.authTypes.includes("oauth") && onOAuthLogin ? (
231
+ <div className={styles.field}>
232
+ <label className={styles.label}>{t.authMethod}</label>
233
+ <select className={styles.select} value={authMethod} onChange={(event) => setAuthMethod(event.target.value as "api_key" | "oauth")}>
234
+ <option value="api_key">{t.authMethodApiKey}</option>
235
+ <option value="oauth">{t.oauthLogin}</option>
236
+ </select>
237
+ </div>
238
+ ) : null}
239
+
240
+ {authMethod === "api_key" ? (
241
+ <div className={styles.field}>
242
+ <label className={styles.label}>{selected?.apiKeyLabel ?? t.apiKey}</label>
243
+ <input className={styles.input} type="password" value={apiKey} onChange={(event) => setApiKey(event.target.value)} placeholder={t.apiKeyOptional} />
244
+ </div>
245
+ ) : null}
246
+
247
+ <div className={styles.advanced}>
248
+ <button className={styles.advancedButton} data-open={advanced} onClick={() => setAdvanced((open) => !open)}>
249
+ <IconChevronDown14 size={15} />
250
+ {t.customSettings}
251
+ </button>
252
+ {advanced ? (
253
+ <div>
254
+ <div className={styles.field}>
255
+ <label className={styles.label}>{t.apiAddress}</label>
256
+ <input className={styles.input} value={baseUrl} onChange={(event) => setBaseUrl(event.target.value)} placeholder={selected?.baseUrl || t.providerDefault} />
257
+ </div>
258
+ <ModelCatalog models={models} setModels={setModels} updateModel={updateModel} inheritedCount={selected?.modelCount ?? 0} discover={discoverAvailableModels} discoverDisabled={!baseUrl.trim()} />
259
+ </div>
260
+ ) : null}
261
+ </div>
262
+ </>
263
+ ) : (
264
+ <>
265
+ <div className={styles.field}>
266
+ <label className={styles.label}>{t.providerId}</label>
267
+ <input className={styles.input} autoFocus value={providerId} onChange={(event) => setProviderId(event.target.value.toLocaleLowerCase())} placeholder="acme-gateway" />
268
+ <div className={styles.hint}>{t.providerIdHint}</div>
269
+ {normalizedId && !customIdValid ? <div className={styles.hint} style={{ color: "var(--dsw-danger)" }}>{t.providerIdInvalid}</div> : null}
270
+ {normalizedId && existingIds.includes(normalizedId) ? <div className={styles.hint} style={{ color: "var(--dsw-danger)" }}>{t.providerIdTaken}</div> : null}
271
+ </div>
272
+ <div className={styles.field}>
273
+ <label className={styles.label}>{t.displayName}</label>
274
+ <input className={styles.input} value={displayName} onChange={(event) => setDisplayName(event.target.value)} placeholder={t.displayName} />
275
+ </div>
276
+ <div className={styles.field}>
277
+ <label className={styles.label}>{t.apiAddress}</label>
278
+ <input className={styles.input} value={baseUrl} onChange={(event) => setBaseUrl(event.target.value)} placeholder="https://gateway.example/v1" />
279
+ </div>
280
+ <div className={styles.field}>
281
+ <label className={styles.label}>{t.apiProtocol}</label>
282
+ <select className={styles.select} value={api} onChange={(event) => setApi(event.target.value)}>
283
+ {allApis.map((value) => <option key={value} value={value}>{value}</option>)}
284
+ </select>
285
+ </div>
286
+ <div className={styles.field}>
287
+ <label className={styles.label}>{t.apiKey}</label>
288
+ <input className={styles.input} type="password" value={apiKey} onChange={(event) => setApiKey(event.target.value)} placeholder={t.apiKeyOptional} />
289
+ <div className={styles.hint}>{t.customApiKeyHint}</div>
290
+ </div>
291
+ <ModelCatalog
292
+ models={models}
293
+ setModels={setModels}
294
+ updateModel={updateModel}
295
+ discover={discoverAvailableModels}
296
+ discoverDisabled={!baseUrl.trim()}
297
+ />
298
+ </>
299
+ )}
300
+ {error ? <div className={styles.error}>{error}</div> : null}
301
+ </div>
302
+
303
+ <footer className={styles.footer}>
304
+ <button className={styles.footerButton} onClick={onClose}>{t.cancel}</button>
305
+ <button
306
+ className={`${styles.footerButton} ${styles.primary}`}
307
+ disabled={saving || (mode === "builtin" ? !builtinCanSave : !customCanSave)}
308
+ onClick={submit}
309
+ >
310
+ {saving ? t.saving : mode === "builtin" ? t.save : t.createProvider}
311
+ </button>
312
+ </footer>
313
+ </section>
314
+ </div>
315
+ );
316
+ }
317
+
318
+ /**
319
+ * 行式模型目录(对齐 dsh ModelListEditor):
320
+ * 每行 = ID + 名称,折叠展开两个容量字段(K/M 写法);
321
+ * 「从提供商获取」产出候选勾选对话框,采纳不覆盖已配置行。
322
+ */
323
+ export function ModelCatalog({
324
+ models,
325
+ setModels,
326
+ updateModel,
327
+ inheritedCount,
328
+ discover,
329
+ discoverDisabled,
330
+ }: {
331
+ models: ModelDraft[];
332
+ setModels: React.Dispatch<React.SetStateAction<ModelDraft[]>>;
333
+ updateModel: (index: number, patch: Partial<ModelDraft>) => void;
334
+ inheritedCount?: number;
335
+ /** 用表单当前值探测远端目录;抛错时错误显示在目录内 */
336
+ discover?: () => Promise<ModelDraft[]>;
337
+ discoverDisabled?: boolean;
338
+ }) {
339
+ const { t } = useI18n();
340
+ const [expanded, setExpanded] = useState<ReadonlySet<number>>(new Set());
341
+ const [discovering, setDiscovering] = useState(false);
342
+ const [discoverError, setDiscoverError] = useState("");
343
+ // 候选对话框:null = 关闭
344
+ const [candidates, setCandidates] = useState<ModelDraft[] | null>(null);
345
+ const [picked, setPicked] = useState<ReadonlySet<string>>(new Set());
346
+ // 容量逐字段文本缓冲,避免输入中被 K/M 格式化打断
347
+ const [capacityText, setCapacityText] = useState<Record<string, string>>({});
348
+
349
+ const toggleExpanded = (index: number) =>
350
+ setExpanded((current) => {
351
+ const next = new Set(current);
352
+ if (!next.delete(index)) next.add(index);
353
+ return next;
354
+ });
355
+
356
+ const capacityValue = (model: ModelDraft, index: number, field: "contextWindow" | "maxTokens") =>
357
+ capacityText[`${index}:${field}`] ?? formatCapacity(model[field]);
358
+
359
+ const editCapacity = (index: number, field: "contextWindow" | "maxTokens", text: string) => {
360
+ setCapacityText((current) => ({ ...current, [`${index}:${field}`]: text }));
361
+ updateModel(index, { [field]: parseCapacity(text) } as Partial<ModelDraft>);
362
+ };
363
+
364
+ const removeModel = (index: number) => {
365
+ setModels((current) => current.filter((_, modelIndex) => modelIndex !== index));
366
+ setExpanded((current) => {
367
+ const next = new Set<number>();
368
+ for (const at of current) if (at !== index) next.add(at > index ? at - 1 : at);
369
+ return next;
370
+ });
371
+ setCapacityText((current) => {
372
+ const next: Record<string, string> = {};
373
+ for (const [key, value] of Object.entries(current)) {
374
+ const at = Number(key.slice(0, key.indexOf(":")));
375
+ if (at === index) continue;
376
+ next[at > index ? `${at - 1}${key.slice(key.indexOf(":"))}` : key] = value;
377
+ }
378
+ return next;
379
+ });
380
+ };
381
+
382
+ const startDiscover = async () => {
383
+ if (!discover || discovering) return;
384
+ setDiscovering(true);
385
+ setDiscoverError("");
386
+ try {
387
+ const found = await discover();
388
+ if (!found.length) {
389
+ setDiscoverError(t.discoverEmpty);
390
+ return;
391
+ }
392
+ // 已配置的行默认不勾选:采纳永远不覆盖用户改过的内容
393
+ const known = new Set(models.map((model) => model.id.trim()).filter(Boolean));
394
+ setCandidates(found);
395
+ setPicked(new Set(found.filter((model) => !known.has(model.id.trim())).map((model) => model.id)));
396
+ } catch (reason) {
397
+ setDiscoverError(reason instanceof Error ? reason.message : t.toastError);
398
+ } finally {
399
+ setDiscovering(false);
400
+ }
401
+ };
402
+
403
+ const closeCandidates = () => {
404
+ setCandidates(null);
405
+ setPicked(new Set());
406
+ };
407
+
408
+ const adoptPicked = () => {
409
+ if (!candidates) return;
410
+ setModels((current) => {
411
+ const byId = new Map(current.map((model) => [model.id.trim(), model]));
412
+ for (const candidate of candidates) {
413
+ const id = candidate.id.trim();
414
+ if (!id || !picked.has(id) || byId.has(id)) continue;
415
+ byId.set(id, { id, name: candidate.name ?? "" });
416
+ }
417
+ return [...byId.values()];
418
+ });
419
+ closeCandidates();
420
+ };
421
+
422
+ const visibleCandidates = candidates ?? [];
423
+ const allVisiblePicked = visibleCandidates.length > 0 && visibleCandidates.every((model) => picked.has(model.id));
424
+
425
+ const togglePick = (id: string) =>
426
+ setPicked((current) => {
427
+ const next = new Set(current);
428
+ if (!next.delete(id)) next.add(id);
429
+ return next;
430
+ });
431
+
432
+ const firstInvalid = validateModelDrafts(models);
433
+
434
+ return (
435
+ <div className={styles.catalog}>
436
+ <div className={styles.catalogHeader}>
437
+ <div>
438
+ <div className={styles.label}>{t.modelCatalog}</div>
439
+ <div className={styles.catalogStatus}>
440
+ {inheritedCount ? t.usingDefaultModels.replace("{count}", String(inheritedCount)) : t.customModelsRequired}
441
+ </div>
442
+ </div>
443
+ {discover ? (
444
+ <button className={styles.discoverButton} disabled={discoverDisabled || discovering} onClick={() => void startDiscover()}>
445
+ {discovering ? t.discoveringModels : t.discoverModels}
446
+ </button>
447
+ ) : null}
448
+ </div>
449
+ {discoverError ? <div className={styles.catalogError}>{discoverError}</div> : null}
450
+ {models.length ? (
451
+ <div className={styles.modelList}>
452
+ {models.map((model, index) => {
453
+ const open = expanded.has(index);
454
+ const badCapacity = Number.isNaN(model.contextWindow) || Number.isNaN(model.maxTokens);
455
+ return (
456
+ <div className={styles.modelEntry} key={index}>
457
+ <div className={styles.modelRow}>
458
+ <button
459
+ type="button"
460
+ className={styles.rowToggle}
461
+ title={t.capacityFields}
462
+ onClick={() => toggleExpanded(index)}
463
+ >
464
+ <IconChevronDown14 size={13} style={{ transform: open ? "rotate(0deg)" : "rotate(-90deg)", transition: "transform 120ms ease" }} />
465
+ </button>
466
+ <input
467
+ className={styles.input}
468
+ style={{ borderColor: !model.id.trim() && firstInvalid?.index === index ? "var(--dsw-danger)" : undefined }}
469
+ value={model.id}
470
+ onChange={(event) => updateModel(index, { id: event.target.value })}
471
+ placeholder={t.modelId}
472
+ />
473
+ <input className={styles.input} value={model.name} onChange={(event) => updateModel(index, { name: event.target.value })} placeholder={t.displayNameOptional} />
474
+ <button className={styles.removeModel} title={t.delete} onClick={() => removeModel(index)}>
475
+ <IconTrashOutline16 size={15} />
476
+ </button>
477
+ </div>
478
+ {open ? (
479
+ <div className={styles.capacityRow}>
480
+ <label className={styles.capacityField}>
481
+ <span>{t.contextWindowLabel}</span>
482
+ <input
483
+ className={styles.input}
484
+ style={{ borderColor: Number.isNaN(model.contextWindow) ? "var(--dsw-danger)" : undefined }}
485
+ value={capacityValue(model, index, "contextWindow")}
486
+ onChange={(event) => editCapacity(index, "contextWindow", event.target.value)}
487
+ placeholder="128K"
488
+ />
489
+ </label>
490
+ <label className={styles.capacityField}>
491
+ <span>{t.maxTokensLabel}</span>
492
+ <input
493
+ className={styles.input}
494
+ style={{ borderColor: Number.isNaN(model.maxTokens) ? "var(--dsw-danger)" : undefined }}
495
+ value={capacityValue(model, index, "maxTokens")}
496
+ onChange={(event) => editCapacity(index, "maxTokens", event.target.value)}
497
+ placeholder="16K"
498
+ />
499
+ </label>
500
+ <span className={styles.capacityHint}>{t.capacityHint}</span>
501
+ </div>
502
+ ) : null}
503
+ {badCapacity ? <div className={styles.rowError}>{t.capacityInvalid.replace("{n}", String(index + 1))}</div> : null}
504
+ </div>
505
+ );
506
+ })}
507
+ </div>
508
+ ) : (
509
+ <div className={styles.modelEmpty}>{inheritedCount ? t.modelCatalogEmptyBuiltin : t.modelCatalogEmptyCustom}</div>
510
+ )}
511
+ <button className={styles.addModel} onClick={() => setModels((current) => [...current, { id: "", name: "" }])}>
512
+ <IconPlusOutline16 size={14} />
513
+ {t.addModel}
514
+ </button>
515
+
516
+ {candidates ? (
517
+ <div className={styles.candidateMask}>
518
+ <div className={styles.candidateDialog} role="dialog" aria-modal="true" aria-label={t.discoverModels}>
519
+ <div className={styles.candidateHeader}>
520
+ <span>{t.candidateTitle.replace("{count}", String(candidates.length))}</span>
521
+ </div>
522
+ <div className={styles.candidateList}>
523
+ {visibleCandidates.map((model) => (
524
+ <label className={styles.candidateRow} key={model.id}>
525
+ <input type="checkbox" checked={picked.has(model.id)} onChange={() => togglePick(model.id)} />
526
+ <span className={styles.candidateId}>{model.id}</span>
527
+ {model.name ? <span className={styles.candidateName}>{model.name}</span> : null}
528
+ </label>
529
+ ))}
530
+ {visibleCandidates.length === 0 ? <div className={styles.modelEmpty}>—</div> : null}
531
+ </div>
532
+ <div className={styles.candidateActions}>
533
+ <label className={styles.candidateAll}>
534
+ <input
535
+ type="checkbox"
536
+ checked={allVisiblePicked}
537
+ onChange={() =>
538
+ setPicked((current) => {
539
+ const next = new Set(current);
540
+ if (visibleCandidates.every((model) => next.has(model.id))) visibleCandidates.forEach((model) => next.delete(model.id));
541
+ else visibleCandidates.forEach((model) => next.add(model.id));
542
+ return next;
543
+ })
544
+ }
545
+ />
546
+ {t.candidateAll}
547
+ </label>
548
+ <span className={styles.candidatePicked}>{t.candidatePicked.replace("{n}", String(picked.size))}</span>
549
+ <button className={styles.footerButton} onClick={closeCandidates}>{t.cancel}</button>
550
+ <button className={`${styles.footerButton} ${styles.primary}`} disabled={picked.size === 0} onClick={adoptPicked}>
551
+ {t.candidateAdopt.replace("{n}", String(picked.size))}
552
+ </button>
553
+ </div>
554
+ </div>
555
+ </div>
556
+ ) : null}
557
+ </div>
558
+ );
559
+ }