@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,729 @@
1
+ "use client";
2
+
3
+ /**
4
+ * 输入卡(对齐 dsh ui-conversation InputBar 结构):
5
+ * 22px 圆角胶囊 + elevation-soft;按钮行 = +命令 | 模型 | 圆环 | 停止/发送圆钮。
6
+ * 运行中:Enter 按设置插话 / 排队,Ctrl(⌘)+Enter 用另一种;停止按钮会把排队消息取回输入框(pi 终端同款);
7
+ * 排队中的消息列在输入卡上方,↑(空输入框时)取回编辑。Enter 发送 / Shift+Enter 换行。
8
+ */
9
+ import { useEffect, useRef, useState } from "react";
10
+ import {
11
+ IconFileOutline16,
12
+ IconSendArrowUp14,
13
+ IconStopFill16,
14
+ IconTerminalOutline14,
15
+ } from "@/components/icons";
16
+ import { ContextMeter } from "@/components/ContextMeter";
17
+ import { ModelSelector, type ModelChoice } from "@/components/ModelSelector";
18
+ import { useI18n } from "@/i18n";
19
+ import { otherBehavior, useEnterBehavior } from "@/lib/enter-behavior";
20
+ import type { ImageAttachment } from "@/lib/types";
21
+
22
+ export interface SlashCommand {
23
+ name: string;
24
+ desc: string;
25
+ /** builtin = Web 自己处理;skill / template / extension = 原样发给 pi,由 SDK 展开或执行 */
26
+ kind: "builtin" | "skill" | "template" | "extension";
27
+ /** 提示模板的参数提示(frontmatter argument-hint) */
28
+ argumentHint?: string;
29
+ }
30
+
31
+ export interface ChatDraft {
32
+ text: string;
33
+ images: ImageAttachment[];
34
+ uploads: Array<{ name: string; path: string; size: number }>;
35
+ adopted?: SlashCommand | null;
36
+ }
37
+
38
+ export type ChatDraftUpdate = ChatDraft | ((previous: ChatDraft) => ChatDraft);
39
+ export const EMPTY_CHAT_DRAFT: ChatDraft = { text: "", images: [], uploads: [] };
40
+
41
+ type SendResult = { success: boolean; error?: string };
42
+ const MAX_UPLOAD_FILES = 16;
43
+
44
+ /** abort / clearQueue 命令的返回:服务端清出来的排队文本,放回输入框用 */
45
+ type QueueResult = { success?: boolean; data?: { steering?: string[]; followUp?: string[] } };
46
+
47
+ export function ChatInput({
48
+ disabled,
49
+ isStreaming,
50
+ contextPercent,
51
+ contextTokens,
52
+ contextWindow,
53
+ contextSource,
54
+ contextVisible = true,
55
+ model,
56
+ thinkingLevel,
57
+ thinkingLevels,
58
+ models,
59
+ providerNames,
60
+ authByProvider,
61
+ queue,
62
+ commands,
63
+ onCommand,
64
+ onSend,
65
+ onSteer,
66
+ onFollowUp,
67
+ onAbort,
68
+ onSelectModel,
69
+ onSelectLevel,
70
+ onClearQueue,
71
+ draft,
72
+ initialDraft,
73
+ onDraftChange,
74
+ onUploadError,
75
+ onUploadProgress,
76
+ uploadFailure,
77
+ pendingUploadCount = 0,
78
+ pendingSend = false,
79
+ onSendPendingChange,
80
+ }: {
81
+ disabled?: boolean;
82
+ isStreaming: boolean;
83
+ contextPercent: number | null;
84
+ contextTokens: number | null;
85
+ contextWindow: number | null;
86
+ /** 上下文分段数据源:ContextMeter 弹窗打开时才做字符统计 */
87
+ contextSource?: { systemChars: number; messages: { content: Array<{ type: string; text?: string; thinking?: string }> }[] };
88
+ /** 还没有对话时隐藏上下文计量 */
89
+ contextVisible?: boolean;
90
+ model?: { provider: string; id: string; name: string };
91
+ thinkingLevel?: string;
92
+ thinkingLevels: string[];
93
+ models: ModelChoice[];
94
+ providerNames: Record<string, string>;
95
+ authByProvider: Record<string, boolean>;
96
+ queue: { steering: string[]; followUp: string[] };
97
+ commands?: SlashCommand[];
98
+ onCommand?: (name: string, args: string) => void;
99
+ onSend: (text: string, images: ImageAttachment[]) => Promise<SendResult> | SendResult;
100
+ onSteer: (text: string, images: ImageAttachment[]) => Promise<SendResult> | SendResult;
101
+ onFollowUp?: (text: string, images: ImageAttachment[]) => Promise<SendResult> | SendResult;
102
+ /** 停止:服务端先清空队列再中止,并返回被清掉的文本,这里把它们放回输入框 */
103
+ onAbort: () => Promise<QueueResult | void> | void;
104
+ onSelectModel: (provider: string, id: string) => void;
105
+ onSelectLevel: (level: string) => void;
106
+ /** 清空队列并返回被清掉的文本(取回编辑 / 丢弃都走它) */
107
+ onClearQueue?: () => Promise<QueueResult | void> | void;
108
+ /** Parent-owned, session-scoped draft. Async work always updates this same target. */
109
+ draft?: ChatDraft;
110
+ initialDraft?: ChatDraft;
111
+ onDraftChange?: (update: ChatDraftUpdate) => void;
112
+ onUploadError?: (message: string) => void;
113
+ onUploadProgress?: (delta: 1 | -1) => void;
114
+ uploadFailure?: string;
115
+ pendingUploadCount?: number;
116
+ pendingSend?: boolean;
117
+ onSendPendingChange?: (pending: boolean) => void;
118
+ }) {
119
+ const [localDraft, setLocalDraft] = useState<ChatDraft>(initialDraft ?? EMPTY_CHAT_DRAFT);
120
+ const currentDraft = draft ?? localDraft;
121
+ const draftRef = useRef(currentDraft);
122
+ draftRef.current = currentDraft;
123
+ const changeDraft = (update: ChatDraftUpdate) => {
124
+ const next = typeof update === "function" ? update(draftRef.current) : update;
125
+ draftRef.current = next;
126
+ if (draft === undefined) setLocalDraft(next);
127
+ onDraftChange?.(update);
128
+ };
129
+ const { text, images, uploads } = currentDraft;
130
+ const adopted = currentDraft.adopted ?? null;
131
+ const setText = (value: string | ((previous: string) => string)) => changeDraft((previous) => ({ ...previous, text: typeof value === "function" ? value(previous.text) : value }));
132
+ const setAdopted = (value: SlashCommand | null) => changeDraft((previous) => ({ ...previous, adopted: value }));
133
+ const pendingReads = useRef<Promise<void>[]>([]);
134
+ const inFlightUploads = useRef(0);
135
+ const mountedRef = useRef(true);
136
+ const sendingRef = useRef(false);
137
+ const [sending, setSending] = useState(false);
138
+ const [attachmentError, setAttachmentError] = useState("");
139
+ const [uploadCount, setUploadCount] = useState(0);
140
+ const uploadBusy = uploadCount > 0 || pendingUploadCount > 0;
141
+ const sendBusy = sending || pendingSend;
142
+ const [dragActive, setDragActive] = useState(false);
143
+ const dragDepth = useRef(0);
144
+ const [modelOpen, setModelOpen] = useState(false);
145
+ const [cmdIdx, setCmdIdx] = useState(0);
146
+ const [cmdDismissed, setCmdDismissed] = useState(false);
147
+ const taRef = useRef<HTMLTextAreaElement>(null);
148
+ const wrapRef = useRef<HTMLDivElement>(null);
149
+ const { t } = useI18n();
150
+ const isBlocked = !model?.id;
151
+ const enterBehavior = useEnterBehavior();
152
+ const queuedCount = queue.steering.length + queue.followUp.length;
153
+ const [restoreTick, setRestoreTick] = useState(0);
154
+
155
+ useEffect(() => {
156
+ mountedRef.current = true;
157
+ return () => { mountedRef.current = false; };
158
+ }, []);
159
+ useEffect(() => {
160
+ setAttachmentError(uploadFailure ?? "");
161
+ }, [uploadFailure]);
162
+ useEffect(() => {
163
+ autoSize();
164
+ }, [text]);
165
+
166
+ // 斜杠命令模式:文本以 / 开头且未输入参数空格
167
+ const slash = text.startsWith("/") ? text.slice(1) : "";
168
+ const hasArgs = slash.includes(" ");
169
+ const typedCommandMode = !!commands?.length && text.startsWith("/") && !cmdDismissed && !hasArgs;
170
+ const filteredCommands = (commands ?? []).filter((c) => c.name.toLowerCase().startsWith(slash.split(" ")[0].toLowerCase()));
171
+ const visibleCommands = filteredCommands;
172
+ const commandMode = typedCommandMode;
173
+ // 命令菜单滚动:.popover 的 overflow:hidden 会压掉 overflow-y-auto(unlayered CSS 优先),
174
+ // 内联 overflowY 强制可滚;键盘导航时把选中项滚进视野
175
+ const cmdMenuRef = useRef<HTMLDivElement>(null);
176
+ useEffect(() => {
177
+ cmdMenuRef.current?.querySelector('button[aria-selected="true"]')?.scrollIntoView({ block: "nearest" });
178
+ }, [cmdIdx, visibleCommands.length]);
179
+ const effectiveIdx = Math.min(cmdIdx, Math.max(0, visibleCommands.length - 1));
180
+
181
+ const runEntry = (entry: SlashCommand) => {
182
+ const args = slash.slice(entry.name.length).trim();
183
+ if (entry.kind === "builtin" && entry.name === "model") {
184
+ setModelOpen(true);
185
+ setText("");
186
+ setCmdDismissed(false);
187
+ return;
188
+ }
189
+ if (entry.kind === "builtin") {
190
+ onCommand?.(entry.name, args);
191
+ } else if (entry.kind === "template" && entry.argumentHint && !args) {
192
+ // 模板要参数:把 /name 留在输入框里等用户补参数,而不是直接发空参数
193
+ setText(`/${entry.name} `);
194
+ setCmdDismissed(true);
195
+ requestAnimationFrame(() => taRef.current?.focus());
196
+ return;
197
+ } else {
198
+ // 技能 / 模板 / 扩展命令:交给 pi 展开执行(SDK 的 prompt() 会识别 /skill:x、/template、扩展命令)
199
+ doSend(undefined, `/${entry.name}${args ? ` ${args}` : ""}`);
200
+ return;
201
+ }
202
+ setText("");
203
+ setCmdIdx(0);
204
+ setCmdDismissed(false);
205
+ };
206
+
207
+ // 自动高度:上限 14 行(约 336px)
208
+ const autoSize = () => {
209
+ const ta = taRef.current;
210
+ if (!ta) return;
211
+ ta.style.height = "auto";
212
+ ta.style.height = `${Math.min(ta.scrollHeight, 336)}px`;
213
+ };
214
+
215
+ const doSend = (override?: "steer" | "followUp", forcedText?: string) => {
216
+ if (disabled || isBlocked || sendingRef.current || pendingSend || uploadBusy) return;
217
+ sendingRef.current = true;
218
+ setSending(true);
219
+ onSendPendingChange?.(true);
220
+ const raw = (forcedText ?? draftRef.current.text).trim();
221
+ const trimmed = adopted ? `/${adopted.name}${raw ? ` ${raw}` : ""}` : raw;
222
+ void (async () => {
223
+ try {
224
+ if (pendingReads.current.length) await Promise.allSettled(pendingReads.current);
225
+ const submitted = draftRef.current;
226
+ const attachments = [...submitted.images];
227
+ const files = [...submitted.uploads];
228
+ if (!trimmed && attachments.length === 0 && files.length === 0) return;
229
+ if (trimmed.startsWith("/") && attachments.length === 0 && files.length === 0) {
230
+ const [head, ...rest] = trimmed.slice(1).split(/\s+/);
231
+ const builtin = (commands ?? []).find((c) => c.kind === "builtin" && c.name.toLowerCase() === head.toLowerCase());
232
+ if (builtin) {
233
+ if (builtin.name === "model") setModelOpen(true);
234
+ else onCommand?.(builtin.name, rest.join(" "));
235
+ changeDraft((previous) => ({ ...previous, text: "", adopted: null }));
236
+ setCmdDismissed(false);
237
+ return;
238
+ }
239
+ }
240
+ const compose = (base: string) =>
241
+ files.length ? `${base}${base ? "\n\n" : ""}${files.map((f) => t.attachmentLine.replace("{path}", f.path).replace("{size}", fmtUploadSize(f.size))).join("\n")}` : base;
242
+ let result: SendResult;
243
+ if (isStreaming) {
244
+ const mode = override ?? (enterBehavior === "steer" ? "steer" : "followUp");
245
+ if (mode === "followUp" && onFollowUp) result = await onFollowUp(compose(trimmed), attachments);
246
+ else result = await onSteer(compose(trimmed), attachments);
247
+ } else result = await onSend(compose(trimmed), attachments);
248
+ if (!result?.success) {
249
+ if (forcedText) setText(forcedText);
250
+ if (result?.error && mountedRef.current) setAttachmentError(result.error);
251
+ return;
252
+ }
253
+ // Acceptance, not completion of streaming, releases the composer. Only remove
254
+ // this submitted snapshot, including after unmount/session migration.
255
+ changeDraft((previous) => ({
256
+ ...previous,
257
+ text: previous.text === submitted.text ? "" : previous.text,
258
+ adopted: previous.adopted === submitted.adopted ? null : previous.adopted,
259
+ images: previous.images.filter((image) => !attachments.includes(image)),
260
+ uploads: previous.uploads.filter((file) => !files.some((sent) => sent.path === file.path)),
261
+ }));
262
+ } catch (error) {
263
+ if (forcedText) setText(forcedText);
264
+ if (mountedRef.current) setAttachmentError(error instanceof Error ? error.message : String(error));
265
+ } finally {
266
+ sendingRef.current = false;
267
+ onSendPendingChange?.(false);
268
+ if (mountedRef.current) setSending(false);
269
+ }
270
+ })();
271
+ };
272
+
273
+ /** 把服务端清出来的排队文本放回输入框(接在已有草稿前面;pi 终端 Esc / Alt+Up、Claude Code ↑ 同款) */
274
+ const restoreQueued = (r: QueueResult | void) => {
275
+ const texts = [...(r?.data?.steering ?? []), ...(r?.data?.followUp ?? [])];
276
+ if (!texts.length) return;
277
+ setText((prev) => [texts.join("\n\n"), prev].filter((s) => s.trim()).join("\n\n"));
278
+ setCmdDismissed(true);
279
+ setRestoreTick((n) => n + 1);
280
+ };
281
+ useEffect(() => {
282
+ if (!restoreTick) return;
283
+ autoSize();
284
+ taRef.current?.focus();
285
+ // eslint-disable-next-line react-hooks/exhaustive-deps
286
+ }, [restoreTick]);
287
+ const stopAndRestore = async () => restoreQueued(await onAbort());
288
+ const pullQueue = async () => restoreQueued(await onClearQueue?.());
289
+
290
+ const pickFiles = (files: FileList | File[] | null) => {
291
+ if (!files || disabled || isBlocked || sendingRef.current || pendingSend) return;
292
+ for (const f of Array.from(files)) {
293
+ if (["image/jpeg", "image/png", "image/webp", "image/gif"].includes(f.type)) {
294
+ if (f.size > 20 * 1024 * 1024) {
295
+ setAttachmentError(t.imageTooLarge);
296
+ continue;
297
+ }
298
+ const read = new Promise<void>((resolve) => {
299
+ const reader = new FileReader();
300
+ reader.onload = () => {
301
+ const dataUrl = String(reader.result);
302
+ const image: ImageAttachment = { type: "image", data: dataUrl.slice(dataUrl.indexOf(",") + 1), mimeType: f.type };
303
+ changeDraft((previous) => previous.images.length >= 20 ? previous : { ...previous, images: [...previous.images, image] });
304
+ resolve();
305
+ };
306
+ reader.onerror = () => resolve();
307
+ reader.onabort = () => resolve();
308
+ reader.readAsDataURL(f);
309
+ });
310
+ pendingReads.current.push(read);
311
+ void read.finally(() => { pendingReads.current = pendingReads.current.filter((pending) => pending !== read); });
312
+ } else {
313
+ void uploadFile(f);
314
+ }
315
+ }
316
+ };
317
+
318
+ const uploadFile = async (f: File): Promise<boolean> => {
319
+ if (f.size > 100 * 1024 * 1024) {
320
+ setAttachmentError(t.uploadTooLarge);
321
+ return false;
322
+ }
323
+ if (draftRef.current.uploads.length + Math.max(inFlightUploads.current, pendingUploadCount) >= MAX_UPLOAD_FILES) {
324
+ setAttachmentError(t.uploadCountLimit);
325
+ return false;
326
+ }
327
+ inFlightUploads.current += 1;
328
+ onUploadProgress?.(1);
329
+ setUploadCount((count) => count + 1);
330
+ setAttachmentError("");
331
+ try {
332
+ const r = await fetch(`/api/files?action=upload&name=${encodeURIComponent(f.name)}`, {
333
+ method: "POST",
334
+ headers: { "Content-Type": "application/octet-stream", "x-upload-size": String(f.size) },
335
+ body: f,
336
+ });
337
+ const j = await r.json();
338
+ if (!r.ok || !j.success) throw new Error(j.error || "upload failed");
339
+ if (j.data?.size !== f.size) throw new Error("upload size mismatch");
340
+ const uploaded = { name: j.data.name, path: j.data.path, size: j.data.size };
341
+ changeDraft((previous) => previous.uploads.some((file) => file.path === uploaded.path)
342
+ ? previous
343
+ : { ...previous, uploads: [...previous.uploads, uploaded] });
344
+ return true;
345
+ } catch (e) {
346
+ const message = `${t.uploadFailed}:${e instanceof Error ? e.message : String(e)}`;
347
+ onUploadError?.(message);
348
+ if (mountedRef.current) setAttachmentError(message);
349
+ return false;
350
+ } finally {
351
+ inFlightUploads.current -= 1;
352
+ onUploadProgress?.(-1);
353
+ if (mountedRef.current) setUploadCount((count) => Math.max(0, count - 1));
354
+ }
355
+ };
356
+
357
+ const pickFilesRef = useRef(pickFiles);
358
+ pickFilesRef.current = pickFiles;
359
+ // 全窗口拖放(照 dsh ComposerAttachments):拖文件悬停时整窗高亮,任意位置松开即接收
360
+ useEffect(() => {
361
+ const withFiles = (e: DragEvent) => e.dataTransfer !== null && e.dataTransfer.types.includes("Files");
362
+ const reset = () => {
363
+ dragDepth.current = 0;
364
+ setDragActive(false);
365
+ };
366
+ const onDragEnter = (e: DragEvent) => {
367
+ if (!withFiles(e)) return;
368
+ e.preventDefault();
369
+ dragDepth.current += 1;
370
+ setDragActive(true);
371
+ };
372
+ const onDragOver = (e: DragEvent) => {
373
+ if (!withFiles(e)) return;
374
+ e.preventDefault();
375
+ };
376
+ const onDragLeave = (e: DragEvent) => {
377
+ if (!withFiles(e)) return;
378
+ dragDepth.current = Math.max(0, dragDepth.current - 1);
379
+ if (dragDepth.current === 0) setDragActive(false);
380
+ };
381
+ const onDrop = (e: DragEvent) => {
382
+ if (e.dataTransfer === null) return;
383
+ e.preventDefault();
384
+ reset();
385
+ pickFilesRef.current(e.dataTransfer.files);
386
+ };
387
+ document.addEventListener("dragenter", onDragEnter);
388
+ document.addEventListener("dragover", onDragOver);
389
+ document.addEventListener("dragleave", onDragLeave);
390
+ document.addEventListener("drop", onDrop);
391
+ window.addEventListener("dragend", reset);
392
+ return () => {
393
+ document.removeEventListener("dragenter", onDragEnter);
394
+ document.removeEventListener("dragover", onDragOver);
395
+ document.removeEventListener("dragleave", onDragLeave);
396
+ document.removeEventListener("drop", onDrop);
397
+ window.removeEventListener("dragend", reset);
398
+ };
399
+ // eslint-disable-next-line react-hooks/exhaustive-deps
400
+ }, []);
401
+
402
+ const hasDraft = text.trim().length > 0 || images.length > 0 || uploads.length > 0;
403
+
404
+ return (
405
+ <div ref={wrapRef} className="relative w-full" data-testid="composer">
406
+ {/* 排队中的消息(Claude Code 同款:列在输入卡上方;↑ 取回编辑,或直接丢弃) */}
407
+ {queuedCount > 0 && (
408
+ <div className="pw-queue">
409
+ {queue.steering.map((q, i) => (
410
+ <div key={`s${i}`} className="pw-queue-row" title={q}>
411
+ <span className="pw-queue-tag">{t.modeSteer}</span>
412
+ <span className="pw-queue-text">{q}</span>
413
+ </div>
414
+ ))}
415
+ {queue.followUp.map((q, i) => (
416
+ <div key={`f${i}`} className="pw-queue-row" title={q}>
417
+ <span className="pw-queue-tag">{t.modeQueue}</span>
418
+ <span className="pw-queue-text">{q}</span>
419
+ </div>
420
+ ))}
421
+ <div className="pw-queue-foot">
422
+ <button type="button" onClick={() => void pullQueue()}>{t.queueEdit}</button>
423
+ <button type="button" onClick={() => void onClearQueue?.()}>{t.queueDrop}</button>
424
+ </div>
425
+ </div>
426
+ )}
427
+ {attachmentError && (
428
+ <div className="mb-2 px-2" role="alert" style={{ fontSize: 12, color: "var(--dsw-danger)" }}>
429
+ {attachmentError}
430
+ </div>
431
+ )}
432
+ {/* 斜杠命令菜单(dsh:悬浮于输入卡上方,同宽) */}
433
+ {commandMode && visibleCommands.length > 0 && (
434
+ <div ref={cmdMenuRef} className="popover absolute bottom-full left-0 right-0 z-50 mb-2 max-h-[340px] overflow-y-auto py-1.5" role="listbox" aria-label={t.slashCatalog} style={{ overflowY: "auto" }}>
435
+ <div className="px-4 pb-1 pt-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
436
+ {t.slashCatalog}
437
+ </div>
438
+ {visibleCommands.map((c, i) => (
439
+ <button
440
+ key={`${c.kind}:${c.name}`}
441
+ role="option"
442
+ aria-selected={i === effectiveIdx}
443
+ className="flex w-full items-baseline gap-3 px-4 py-2 text-left transition-colors"
444
+ style={{ background: i === effectiveIdx ? "var(--dsw-hover)" : "transparent" }}
445
+ onMouseEnter={() => setCmdIdx(i)}
446
+ onClick={() => runEntry(c)}
447
+ >
448
+ <span style={{ fontSize: 14, fontWeight: 500, color: "var(--dsw-label-primary)", flex: "none" }}>{c.name}</span>
449
+ {c.kind !== "builtin" && (
450
+ <span className="rounded px-1.5 py-0.5" style={{ fontSize: 10.5, color: "var(--dsw-label-caption)", background: "var(--dsw-selector)", flex: "none" }}>
451
+ {c.kind === "skill" ? t.skillType : c.kind === "template" ? t.promptType : t.extensionCmdType}
452
+ </span>
453
+ )}
454
+ {c.argumentHint && (
455
+ <span style={{ fontSize: 12, fontFamily: "var(--font-mono)", color: "var(--dsw-label-caption)", flex: "none" }}>{c.argumentHint}</span>
456
+ )}
457
+ <span className="truncate" style={{ fontSize: 13, color: "var(--dsw-label-tertiary)" }}>
458
+ {c.desc}
459
+ </span>
460
+ </button>
461
+ ))}
462
+ </div>
463
+ )}
464
+
465
+ {/* 附件轨:图片 */}
466
+ {images.length > 0 && (
467
+ <div className="mb-2 flex flex-wrap gap-2 px-1">
468
+ {images.map((img, i) => (
469
+ <div key={i} className="relative">
470
+ <img
471
+ src={`data:${img.mimeType};base64,${img.data}`}
472
+ alt=""
473
+ className="h-14 w-14 rounded-lg object-cover"
474
+ style={{ border: "0.5px solid var(--dsw-border-l2)" }}
475
+ />
476
+ <button
477
+ className="absolute -right-1.5 -top-1.5 flex h-5 w-5 items-center justify-center rounded-full text-[10px]"
478
+ style={{ background: "var(--dsw-label-primary)", color: "var(--dsw-bg-base)" }}
479
+ disabled={sendBusy}
480
+ onClick={() => changeDraft((previous) => ({ ...previous, images: previous.images.filter((_, index) => index !== i) }))}
481
+ >
482
+
483
+ </button>
484
+ </div>
485
+ ))}
486
+ </div>
487
+ )}
488
+
489
+ {/* 附件轨:上传的普通文件(压缩包/文档等,路径引用) */}
490
+ {uploads.length > 0 && (
491
+ <div className="mb-2 flex flex-wrap gap-2 px-1">
492
+ {uploads.map((f, i) => (
493
+ <div
494
+ key={f.path}
495
+ className="flex items-center gap-2 rounded-lg px-2.5 py-1.5"
496
+ style={{ border: "0.5px solid var(--dsw-border-l2)", background: "var(--dsw-hover)" }}
497
+ title={f.path}
498
+ >
499
+ <IconFileOutline16 size={14} style={{ flex: "none", color: "var(--dsw-label-tertiary)" }} />
500
+ <span className="max-w-[180px] truncate" style={{ fontSize: 12.5, color: "var(--dsw-label-secondary)" }}>{f.name}</span>
501
+ <span style={{ fontSize: 11, color: "var(--dsw-label-caption)" }}>{fmtUploadSize(f.size)}</span>
502
+ <button
503
+ className="flex h-5 w-5 items-center justify-center rounded-full text-[10px]"
504
+ style={{ color: "var(--dsw-label-caption)" }}
505
+ disabled={sendBusy}
506
+ onClick={() => changeDraft((previous) => ({ ...previous, uploads: previous.uploads.filter((_, index) => index !== i) }))}
507
+ aria-label="remove"
508
+ >
509
+
510
+ </button>
511
+ </div>
512
+ ))}
513
+ </div>
514
+ )}
515
+
516
+ {/* 卡片(玻璃态)—— 拖放由 document 级监听统一接管 */}
517
+ <div
518
+ className="relative w-full"
519
+ style={{
520
+ borderRadius: 22,
521
+ background: "var(--dsw-input-major)",
522
+ boxShadow: "var(--dsw-elevation-soft)",
523
+ border: "0.5px solid var(--dsw-border-l1)",
524
+ }}
525
+ >
526
+ <div className="px-4 pt-3 flex items-start gap-1.5">
527
+ {adopted && (
528
+ <button
529
+ type="button"
530
+ className="flex-none flex items-center gap-1 self-start"
531
+ title={`${t.delete} (Backspace)`}
532
+ style={{
533
+ fontSize: "var(--piweb-chat-font-size, var(--dsh-content-font-size))",
534
+ lineHeight: "var(--piweb-chat-line-height, 1.55)",
535
+ padding: 0,
536
+ background: "transparent",
537
+ border: "none",
538
+ color: "var(--dsw-label-primary)",
539
+ }}
540
+ onClick={() => {
541
+ setAdopted(null);
542
+ requestAnimationFrame(() => taRef.current?.focus());
543
+ }}
544
+ >
545
+ <IconTerminalOutline14 size={14} style={{ color: "var(--dsw-accent)", flex: "none" }} />
546
+ <span style={{ color: "var(--dsw-accent)", fontWeight: 600 }}>/{adopted.name}</span>
547
+ </button>
548
+ )}
549
+ <textarea
550
+ ref={taRef}
551
+ value={text}
552
+ disabled={disabled || isBlocked || sendBusy}
553
+ rows={1}
554
+ placeholder={adopted ? (adopted.argumentHint ?? t.inputPlaceholder) : !disabled && isBlocked ? t.blockedComposer : t.inputPlaceholder}
555
+ suppressHydrationWarning
556
+ className="min-w-0 flex-1 resize-none"
557
+ style={{ fontSize: "var(--piweb-chat-font-size, var(--dsh-content-font-size))", lineHeight: 1.55 }}
558
+ onChange={(e) => {
559
+ setText(e.target.value);
560
+ setCmdDismissed(false);
561
+ setCmdIdx(0);
562
+ autoSize();
563
+ }}
564
+ onPaste={(e) => {
565
+ const files = Array.from(e.clipboardData.files);
566
+ if (files.length) {
567
+ e.preventDefault();
568
+ pickFiles(e.clipboardData.files);
569
+ }
570
+ }}
571
+ onKeyDown={(e) => {
572
+ if (adopted && text === "" && (e.key === "Backspace" || e.key === "Escape")) {
573
+ // 空参数时退格/Esc:摘掉命令胶囊
574
+ e.preventDefault();
575
+ setAdopted(null);
576
+ return;
577
+ }
578
+ if (commandMode && visibleCommands.length > 0) {
579
+ if (e.key === "ArrowDown") {
580
+ e.preventDefault();
581
+ setCmdIdx((i) => Math.min(i + 1, visibleCommands.length - 1));
582
+ return;
583
+ }
584
+ if (e.key === "ArrowUp") {
585
+ e.preventDefault();
586
+ setCmdIdx((i) => Math.max(i - 1, 0));
587
+ return;
588
+ }
589
+ if (e.key === "Escape") {
590
+ e.preventDefault();
591
+ setCmdDismissed(true);
592
+ return;
593
+ }
594
+ if (e.key === "Enter" && !e.nativeEvent.isComposing) {
595
+ e.preventDefault();
596
+ runEntry(visibleCommands[effectiveIdx]);
597
+ return;
598
+ }
599
+ if (e.key === "Tab") {
600
+ // 采纳:把命令渲染成胶囊留在输入框,参数接着补(对齐其他 Agent 平台)
601
+ e.preventDefault();
602
+ const entry = visibleCommands[effectiveIdx];
603
+ if (!entry) return;
604
+ setAdopted(entry);
605
+ setText("");
606
+ setCmdIdx(0);
607
+ requestAnimationFrame(() => {
608
+ autoSize();
609
+ taRef.current?.focus();
610
+ });
611
+ return;
612
+ }
613
+ }
614
+ if (e.key === "ArrowUp" && text === "" && queuedCount > 0) {
615
+ e.preventDefault();
616
+ void pullQueue();
617
+ return;
618
+ }
619
+ if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
620
+ e.preventDefault();
621
+ // Ctrl / ⌘ + Enter:用设置之外的另一种方式发(只在运行中有区别)
622
+ const alt = e.ctrlKey || e.metaKey;
623
+ doSend(isStreaming && alt ? (otherBehavior(enterBehavior) === "steer" ? "steer" : "followUp") : undefined);
624
+ }
625
+ }}
626
+ />
627
+ </div>
628
+
629
+ {/* 按钮行:左侧只保留命令入口;图片通过拖放或粘贴添加。 */}
630
+ <div
631
+ className="flex items-center gap-2"
632
+ style={{
633
+ paddingLeft: 8,
634
+ paddingRight: 12,
635
+ paddingBottom: 8,
636
+ paddingTop: 4,
637
+ height: 40,
638
+ containerType: "inline-size",
639
+ }}
640
+ >
641
+ {/* 命令菜单通过输入 / 触发(dsh 同款),不再提供 + 启动按钮 */}
642
+ <div className="flex-1" />
643
+
644
+ {/* 模型芯片(右组,dsh 分组菜单;/model 命令可受控打开) */}
645
+ <ModelSelector
646
+ model={model}
647
+ thinkingLevel={thinkingLevel}
648
+ thinkingLevels={thinkingLevels}
649
+ models={models}
650
+ providerNames={providerNames}
651
+ authByProvider={authByProvider}
652
+ onSelectModel={onSelectModel}
653
+ onSelectLevel={onSelectLevel}
654
+ open={modelOpen}
655
+ onOpenChange={setModelOpen}
656
+ />
657
+
658
+ {/* 上下文圆环 */}
659
+ {contextVisible && (
660
+ <ContextMeter
661
+ percent={contextPercent}
662
+ tokens={contextTokens}
663
+ contextWindow={contextWindow}
664
+ source={contextSource}
665
+ />
666
+ )}
667
+
668
+ {/* 停止 / 发送圆钮:运行中没草稿 → 停止;运行中有草稿 → 幽灵停止钮 + 发送(发送即按设置插话 / 排队) */}
669
+ {isStreaming && !hasDraft ? (
670
+ <button className="btn-primary-circle" title={t.stop} onClick={() => void stopAndRestore()}>
671
+ <IconStopFill16 size={16} />
672
+ </button>
673
+ ) : (
674
+ <>
675
+ {isStreaming && (
676
+ <button className="btn-ghost-circle" title={t.stop} onClick={() => void stopAndRestore()}>
677
+ <IconStopFill16 size={14} />
678
+ </button>
679
+ )}
680
+ <button
681
+ className="btn-primary-circle"
682
+ title={isStreaming ? (enterBehavior === "steer" ? t.sendSteer : t.sendQueue) : t.send}
683
+ disabled={!hasDraft || disabled || isBlocked || sendBusy || uploadBusy}
684
+ onClick={() => doSend()}
685
+ >
686
+ <IconSendArrowUp14 size={16} />
687
+ </button>
688
+ </>
689
+ )}
690
+ </div>
691
+ </div>
692
+
693
+ {/* 全窗口拖放遮罩(dsh:拖文件悬停时高亮,松开即添加) */}
694
+ {dragActive && (
695
+ <div
696
+ className="pointer-events-none fixed inset-0 z-[95] flex items-center justify-center"
697
+ style={{ background: "color-mix(in srgb, var(--dsw-bg-base) 65%, transparent)" }}
698
+ >
699
+ <div
700
+ className="flex flex-col items-center gap-3 rounded-3xl px-12 py-10"
701
+ style={{
702
+ border: "2px dashed var(--dsw-accent)",
703
+ background: "var(--dsw-glass-popover)",
704
+ boxShadow: "var(--dsw-elevation-prominent)",
705
+ }}
706
+ >
707
+ <IconFileOutline16 size={32} style={{ color: "var(--dsw-accent)" }} />
708
+ <div style={{ fontSize: 15, fontWeight: 500, color: "var(--dsw-label-primary)" }}>
709
+ {uploadBusy ? t.uploading : t.dropToAttach}
710
+ </div>
711
+ <div style={{ fontSize: 12, color: "var(--dsw-label-caption)" }}>{t.dropToAttachHint}</div>
712
+ <div
713
+ className="rounded-full px-3 py-1"
714
+ style={{ fontSize: 11.5, color: "var(--dsw-label-secondary)", background: "var(--dsw-hover)", lineHeight: "18px" }}
715
+ >
716
+ {t.dropLimits}
717
+ </div>
718
+ </div>
719
+ </div>
720
+ )}
721
+ </div>
722
+ );
723
+ }
724
+
725
+ function fmtUploadSize(bytes: number): string {
726
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
727
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)}KB`;
728
+ return `${bytes}B`;
729
+ }