@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,555 @@
1
+ "use client";
2
+
3
+ /**
4
+ * 文件查看器:点击项目树里的文件弹出,80vw × 80vh 居中,右上全屏 / 关闭。
5
+ * 页签行照 dsh:首个页签「文件」是同一棵项目树,每个打开的文件一个页签(可关),「+」模糊打开。
6
+ * 文件页签:面包屑 + 模式(变更 / 内容 / 渲染)+ 折叠未改动 + 上一处 / 下一处 + 引用 / 编辑器打开。
7
+ * 「变更」= 整文件行内 diff(FullDiff);「内容」= 所选步的全文(被删文件显示删除前);「渲染」= Markdown。
8
+ */
9
+ import { useEffect, useMemo, useRef, useState } from "react";
10
+ import ReactMarkdown from "react-markdown";
11
+ import rehypeHighlight from "rehype-highlight";
12
+ import remarkGfm from "remark-gfm";
13
+ import { parseUnifiedDiff, type DiffLine } from "@/components/DiffView";
14
+ import { FullDiff, type FullDiffHandle } from "@/components/FullDiff";
15
+ import { GrowthTree } from "@/components/GrowthTree";
16
+ import { IconChevronDown14, IconChevronUp14, IconCloseOutline14, IconEditOutline16, IconFileOutline16, IconPlusOutline16, IconSearchOutline16 } from "@/components/icons";
17
+ import type { GrowthApi } from "@/hooks/useGrowth";
18
+ import { useI18n } from "@/i18n";
19
+ import { fileIconSrc } from "@/lib/file-icons";
20
+ import { fuzzyScore, type TreeNode } from "@/lib/growth-tree";
21
+ import type { ViewerState } from "@/hooks/useFileViewer";
22
+ import { highlightCode, languageForPath } from "@/lib/highlight";
23
+
24
+ type Mode = "diff" | "content" | "render";
25
+
26
+ const fetchCache = new Map<string, Promise<unknown>>();
27
+ async function requestJson<T>(url: string): Promise<T> {
28
+ const r = await fetch(url);
29
+ const j = await r.json();
30
+ if (!r.ok || !j.success) {
31
+ const err = new Error(j.error || `request failed (${r.status})`) as Error & { status?: number };
32
+ err.status = r.status;
33
+ throw err;
34
+ }
35
+ return j.data as T;
36
+ }
37
+
38
+ function cachedJson<T>(url: string): Promise<T> {
39
+ let p = fetchCache.get(url) as Promise<T> | undefined;
40
+ if (!p) {
41
+ p = requestJson<T>(url);
42
+ p.catch(() => fetchCache.delete(url));
43
+ fetchCache.set(url, p);
44
+ if (fetchCache.size > 200) fetchCache.delete(fetchCache.keys().next().value as string);
45
+ }
46
+ return p;
47
+ }
48
+
49
+ interface Loaded {
50
+ key: string;
51
+ diff: DiffLine[] | null;
52
+ content: string | null;
53
+ binary: boolean;
54
+ truncated: boolean;
55
+ notice: "deleted" | "historical" | "missing" | "disk" | "nochange" | null;
56
+ error: string | null;
57
+ }
58
+
59
+ /** 代码全文:行号栏 + 高亮正文 */
60
+ function CodeBlock({ content, name }: { content: string; name: string }) {
61
+ const html = useMemo(() => highlightCode(content, languageForPath(name)), [content, name]);
62
+ const lines = content.split("\n").length;
63
+ const gutterWidth = `${String(lines).length + 1}ch`;
64
+ return (
65
+ <div className="flex min-h-0 flex-1 overflow-auto" style={{ fontFamily: "var(--font-mono)", fontSize: 12, lineHeight: "20px" }}>
66
+ <pre aria-hidden className="flex-none select-none py-2 pl-3 pr-2 text-right" style={{ width: gutterWidth, minWidth: gutterWidth, color: "var(--dsw-label-caption)", margin: 0, position: "sticky", left: 0, background: "var(--dsw-glass-modal)" }}>
67
+ {Array.from({ length: lines }, (_, i) => i + 1).join("\n")}
68
+ </pre>
69
+ {html !== null ? (
70
+ <pre className="hljs min-w-0 flex-1 py-2 pl-3 pr-4" style={{ margin: 0, background: "transparent", color: "var(--dsw-label-secondary)" }}>
71
+ <code dangerouslySetInnerHTML={{ __html: html }} />
72
+ </pre>
73
+ ) : (
74
+ <pre className="min-w-0 flex-1 py-2 pl-3 pr-4" style={{ margin: 0, color: "var(--dsw-label-secondary)" }}>{content}</pre>
75
+ )}
76
+ </div>
77
+ );
78
+ }
79
+
80
+ export function FileViewer({
81
+ state,
82
+ growth,
83
+ cwd,
84
+ onClose,
85
+ onSelectTab,
86
+ onCloseTab,
87
+ onOpen,
88
+ onReference,
89
+ onOpenEditor,
90
+ }: {
91
+ state: ViewerState;
92
+ growth: GrowthApi;
93
+ cwd: string;
94
+ onClose: () => void;
95
+ onSelectTab: (path: string | null) => void;
96
+ onCloseTab: (path: string) => void;
97
+ onOpen: (path: string, opts?: { from?: string; lazy?: boolean }) => void;
98
+ onReference?: (path: string) => void;
99
+ onOpenEditor?: (path: string) => void;
100
+ }) {
101
+ const { t } = useI18n();
102
+ const [fullscreen, setFullscreen] = useState(false);
103
+ const [mode, setMode] = useState<Mode>("diff");
104
+ const [modeTouched, setModeTouched] = useState(false);
105
+ const [folded, setFolded] = useState(true);
106
+ const [quickOpen, setQuickOpen] = useState(false);
107
+ const [quickQuery, setQuickQuery] = useState("");
108
+ const [quickIndex, setQuickIndex] = useState(0);
109
+ const [treeFilter, setTreeFilter] = useState("");
110
+ const [reveal, setReveal] = useState<{ path: string; key: number } | null>(null);
111
+ const [loaded, setLoaded] = useState<Loaded | null>(null);
112
+ const diffRef = useRef<FullDiffHandle>(null);
113
+ const rootRef = useRef<HTMLDivElement>(null);
114
+ const quickInputRef = useRef<HTMLInputElement>(null);
115
+
116
+ const activeTab = state.tabs.find((tab) => tab.path === state.active) ?? null;
117
+ const change = useMemo(() => (activeTab ? growth.changes.find((c) => c.path === activeTab.path) ?? null : null), [activeTab, growth.changes]);
118
+ const isMarkdown = Boolean(activeTab && /\.(md|mdx|markdown)$/i.test(activeTab.path));
119
+ const range = growth.range;
120
+
121
+ // 切页签:模式按是否有改动自动选,用户手动切过才保留
122
+ useEffect(() => {
123
+ setModeTouched(false);
124
+ }, [state.active]);
125
+ useEffect(() => {
126
+ if (modeTouched || !activeTab) return;
127
+ setMode(change && !activeTab.lazy ? "diff" : isMarkdown ? "render" : "content");
128
+ }, [activeTab, change, isMarkdown, modeTouched]);
129
+
130
+ // 取数据
131
+ useEffect(() => {
132
+ if (!activeTab) {
133
+ setLoaded(null);
134
+ return;
135
+ }
136
+ const tab = activeTab;
137
+ const key = `${tab.path}|${mode}|${range?.from ?? ""}|${range?.to ?? ""}|${change?.status ?? ""}`;
138
+ let alive = true;
139
+ const done = (partial: Partial<Loaded>) => {
140
+ if (alive) setLoaded({ key, diff: null, content: null, binary: false, truncated: false, notice: null, error: null, ...partial });
141
+ };
142
+ // 静态内容页签(技能文档等):调用方已带内容,不读磁盘
143
+ if (tab.staticContent !== undefined) {
144
+ done({ content: tab.staticContent });
145
+ return () => {
146
+ alive = false;
147
+ };
148
+ }
149
+ if (!cwd) {
150
+ setLoaded(null);
151
+ return;
152
+ }
153
+ const q = `cwd=${encodeURIComponent(cwd)}`;
154
+ const diskContent = async (notice: Loaded["notice"]) => {
155
+ try {
156
+ const data = await requestJson<{ content: string; binary: boolean; truncated: boolean }>(`/api/files?read=1&${q}&path=${encodeURIComponent(tab.path)}`);
157
+ done({ content: data.content, binary: data.binary, truncated: data.truncated, notice });
158
+ } catch (error) {
159
+ if ((error as { status?: number }).status !== 400) throw error;
160
+ done({ notice: "missing" });
161
+ }
162
+ };
163
+ const snapshotContent = async (tree: string, notice: Loaded["notice"]) => {
164
+ try {
165
+ const data = await cachedJson<{ content: string; binary: boolean; truncated: boolean }>(`/api/growth?${q}&tree=${tree}&path=${encodeURIComponent(tab.path)}&content=1`);
166
+ done({ content: data.content, binary: data.binary, truncated: data.truncated, notice });
167
+ } catch (error) {
168
+ if ((error as { status?: number }).status === 404) {
169
+ // 跟随中的文件可能在本轮后续工具里被删掉;从最近一次涉及它的快照取回最后内容。
170
+ const prior = [...growth.steps].reverse().find((step) =>
171
+ step.seq <= (growth.selected?.seq ?? Infinity) && step.changes.some((item) => item.path === tab.path || item.from === tab.path),
172
+ );
173
+ const priorTree = prior?.changes.some((item) => (item.status === "D" && item.path === tab.path) || (item.status === "R" && item.from === tab.path))
174
+ ? prior.parent
175
+ : prior?.tree;
176
+ if (priorTree && priorTree !== tree) {
177
+ try {
178
+ const data = await cachedJson<{ content: string; binary: boolean; truncated: boolean }>(`/api/growth?${q}&tree=${priorTree}&path=${encodeURIComponent(tab.path)}&content=1`);
179
+ done({ content: data.content, binary: data.binary, truncated: data.truncated, notice: "historical" });
180
+ return;
181
+ } catch (priorError) {
182
+ if ((priorError as { status?: number }).status !== 404) throw priorError;
183
+ }
184
+ }
185
+ await diskContent("disk");
186
+ return;
187
+ }
188
+ throw error;
189
+ }
190
+ };
191
+ void (async () => {
192
+ try {
193
+ if (tab.lazy || !range) {
194
+ // 会话还没有任何快照时整棵树都是磁盘条目,「不在快照里」的提示会误导
195
+ await diskContent(tab.lazy && growth.steps.length > 0 ? "disk" : null);
196
+ return;
197
+ }
198
+ if (mode === "diff") {
199
+ if (!change) {
200
+ await snapshotContent(range.to, "nochange");
201
+ return;
202
+ }
203
+ const data = await cachedJson<{ patch: string; binary: boolean; truncated: boolean }>(
204
+ `/api/growth?${q}&from=${range.from}&to=${range.to}&path=${encodeURIComponent(tab.path)}${tab.from ?? change.from ? `&renamedFrom=${encodeURIComponent(tab.from ?? change.from ?? "")}` : ""}`,
205
+ );
206
+ if (data.binary) {
207
+ done({ binary: true });
208
+ return;
209
+ }
210
+ const lines = parseUnifiedDiff(data.patch);
211
+ if (!lines) {
212
+ await snapshotContent(range.to, "nochange");
213
+ return;
214
+ }
215
+ done({ diff: lines, truncated: data.truncated, notice: change.status === "D" ? "deleted" : null });
216
+ return;
217
+ }
218
+ await snapshotContent(range.to, null);
219
+ } catch (error) {
220
+ done({ error: error instanceof Error ? error.message : t.viewerLoadFailed });
221
+ }
222
+ })();
223
+ return () => {
224
+ alive = false;
225
+ };
226
+ }, [activeTab, cwd, mode, range, change, growth.steps, growth.selected, growth.pending, t.viewerLoadFailed]);
227
+
228
+ // 键盘:Esc 关(先关快速打开)、Ctrl/⌘+W 关页签、Ctrl/⌘+P 快速打开、[ ] 上一处下一处
229
+ useEffect(() => {
230
+ if (!state.open) return;
231
+ const h = (e: KeyboardEvent) => {
232
+ const target = e.target as HTMLElement | null;
233
+ const typing = target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable);
234
+ if (e.key === "Escape") {
235
+ e.preventDefault();
236
+ if (quickOpen) setQuickOpen(false);
237
+ else onClose();
238
+ return;
239
+ }
240
+ const mod = e.ctrlKey || e.metaKey;
241
+ if (mod && e.key.toLowerCase() === "w") {
242
+ e.preventDefault();
243
+ if (state.active) onCloseTab(state.active);
244
+ else onClose();
245
+ return;
246
+ }
247
+ if (mod && e.key.toLowerCase() === "p") {
248
+ e.preventDefault();
249
+ setQuickOpen(true);
250
+ return;
251
+ }
252
+ if (!typing && !mod && mode === "diff") {
253
+ if (e.key === "]") diffRef.current?.nextChange();
254
+ else if (e.key === "[") diffRef.current?.prevChange();
255
+ }
256
+ };
257
+ document.addEventListener("keydown", h);
258
+ return () => document.removeEventListener("keydown", h);
259
+ }, [state.open, state.active, quickOpen, mode, onClose, onCloseTab]);
260
+
261
+ useEffect(() => {
262
+ if (quickOpen) {
263
+ setQuickQuery("");
264
+ setQuickIndex(0);
265
+ setTimeout(() => quickInputRef.current?.focus(), 0);
266
+ }
267
+ }, [quickOpen]);
268
+ useEffect(() => {
269
+ if (state.open) setTimeout(() => rootRef.current?.focus(), 0);
270
+ }, [state.open]);
271
+
272
+ const quickMatches = useMemo(() => {
273
+ if (!quickOpen) return [];
274
+ const q = quickQuery.trim();
275
+ const scored = growth.filePaths
276
+ .map((p) => ({ p, s: fuzzyScore(q, p) }))
277
+ .filter((x) => x.s >= 0)
278
+ .sort((a, b) => b.s - a.s || a.p.length - b.p.length)
279
+ .slice(0, 50);
280
+ return scored.map((x) => x.p);
281
+ }, [quickOpen, quickQuery, growth.filePaths]);
282
+
283
+ if (!state.open) return null;
284
+
285
+ const segments = activeTab ? activeTab.path.split("/") : [];
286
+ const goCrumb = (i: number) => {
287
+ const dir = segments.slice(0, i + 1).join("/");
288
+ growth.revealPath(dir);
289
+ setReveal({ path: dir, key: Date.now() });
290
+ onSelectTab(null);
291
+ };
292
+ const roundNumber = growth.selectedRound?.id ?? 0;
293
+ const stepBadge = roundNumber
294
+ ? `${t.growthRoundLabel.replace("{n}", String(roundNumber))}${change ? ` · +${change.add ?? 0} −${change.del ?? 0}` : ""}`
295
+ : "";
296
+ const isDiffMode = mode === "diff" && Boolean(loaded?.diff);
297
+ const noticeText = loaded?.notice === "deleted" ? t.viewerDeleted : loaded?.notice === "historical" ? t.viewerHistorical : loaded?.notice === "missing" ? t.viewerMissing : loaded?.notice === "disk" ? t.viewerNotInSnapshot : loaded?.notice === "nochange" ? t.viewerNoChanges : "";
298
+
299
+ return (
300
+ <div className="pw-viewer-mask modal-mask fixed inset-0 z-[105] flex items-center justify-center" onMouseDown={(e) => e.target === e.currentTarget && onClose()}>
301
+ <div
302
+ ref={rootRef}
303
+ className="pw-viewer glass-modal flex flex-col overflow-hidden outline-none"
304
+ data-fullscreen={fullscreen || undefined}
305
+ role="dialog"
306
+ aria-modal="true"
307
+ tabIndex={-1}
308
+ data-testid="file-viewer"
309
+ >
310
+ {/* 页签行 */}
311
+ <div className="pw-viewer-tabs hairline-b" onDoubleClick={(e) => e.target === e.currentTarget && setFullscreen((f) => !f)}>
312
+ <button type="button" className="pw-viewer-tab" data-active={state.active === null} onClick={() => onSelectTab(null)}>
313
+ <img className="pw-tree-icon" src="/file-icons/folder-open.svg" alt="" />
314
+ <span>{t.projectPanel}</span>
315
+ </button>
316
+ <div className="pw-viewer-tabstrip">
317
+ {state.tabs.map((tab) => (
318
+ <div
319
+ key={tab.path}
320
+ className="pw-viewer-tab"
321
+ data-active={state.active === tab.path}
322
+ onAuxClick={(e) => {
323
+ if (e.button === 1) onCloseTab(tab.path);
324
+ }}
325
+ >
326
+ <button type="button" className="pw-viewer-tab-main" title={tab.path} onClick={() => onSelectTab(tab.path)}>
327
+ <img className="pw-tree-icon" src={fileIconSrc(tab.path.split("/").pop() ?? tab.path)} alt="" />
328
+ <span className="truncate">{tab.path.split("/").pop()}</span>
329
+ </button>
330
+ <button
331
+ type="button"
332
+ className="pw-viewer-tab-close"
333
+ title={t.viewerCloseTab}
334
+ aria-label={`${t.viewerCloseTab}: ${tab.path}`}
335
+ onClick={() => onCloseTab(tab.path)}
336
+ >
337
+ <IconCloseOutline14 size={10} />
338
+ </button>
339
+ </div>
340
+ ))}
341
+ </div>
342
+ <button type="button" className="icon-btn" style={{ width: 26, height: 26 }} title={`${t.viewerQuickOpen} (Ctrl/⌘+P)`} onClick={() => setQuickOpen(true)}>
343
+ <IconPlusOutline16 size={13} />
344
+ </button>
345
+ <div className="flex-1" />
346
+ <button type="button" className="icon-btn" style={{ width: 26, height: 26 }} title={fullscreen ? t.viewerExitFullscreen : t.viewerFullscreen} onClick={() => setFullscreen((f) => !f)} data-testid="viewer-fullscreen">
347
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden>
348
+ {fullscreen ? (
349
+ <path d="M6 1v5H1M10 1v5h5M6 15v-5H1M10 15v-5h5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
350
+ ) : (
351
+ <path d="M1 6V1h5M15 6V1h-5M1 10v5h5M15 10v5h-5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
352
+ )}
353
+ </svg>
354
+ </button>
355
+ <button type="button" className="icon-btn" style={{ width: 26, height: 26 }} title={`${t.close} (Esc)`} onClick={onClose} data-testid="viewer-close">
356
+ <IconCloseOutline14 size={13} />
357
+ </button>
358
+ </div>
359
+
360
+ {activeTab ? (
361
+ <>
362
+ {/* 面包屑 + 工具 */}
363
+ <div className="pw-viewer-toolbar hairline-b">
364
+ <div className="pw-crumb min-w-0 flex-1">
365
+ {segments.map((seg, i) => (
366
+ <span key={i} className="flex items-center">
367
+ {i > 0 && <span className="pw-crumb-sep">/</span>}
368
+ {i < segments.length - 1 ? (
369
+ <button type="button" onClick={() => goCrumb(i)}>{seg}</button>
370
+ ) : (
371
+ <span style={{ color: "var(--dsw-label-primary)", fontWeight: 500 }}>{seg}</span>
372
+ )}
373
+ </span>
374
+ ))}
375
+ </div>
376
+ {stepBadge && !activeTab.lazy && (
377
+ <span className="flex-none truncate" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)", maxWidth: 220 }}>
378
+ {stepBadge}
379
+ </span>
380
+ )}
381
+ {!activeTab.lazy && (
382
+ <span className="pw-seg" role="radiogroup">
383
+ {(["diff", "content", ...(isMarkdown ? ["render" as const] : [])] as Mode[]).map((m) => (
384
+ <button
385
+ key={m}
386
+ type="button"
387
+ role="radio"
388
+ aria-checked={mode === m}
389
+ data-active={mode === m}
390
+ onClick={() => {
391
+ setModeTouched(true);
392
+ setMode(m);
393
+ }}
394
+ >
395
+ {m === "diff" ? t.viewerModeDiff : m === "content" ? t.viewerModeContent : t.viewerModeRender}
396
+ </button>
397
+ ))}
398
+ </span>
399
+ )}
400
+ {activeTab.lazy && isMarkdown && (
401
+ <span className="pw-seg" role="radiogroup">
402
+ {(["content", "render"] as Mode[]).map((m) => (
403
+ <button key={m} type="button" role="radio" aria-checked={mode === m} data-active={mode === m} onClick={() => { setModeTouched(true); setMode(m); }}>
404
+ {m === "content" ? t.viewerModeContent : t.viewerModeRender}
405
+ </button>
406
+ ))}
407
+ </span>
408
+ )}
409
+ {isDiffMode && (
410
+ <>
411
+ <button type="button" className="pw-chip" data-active={folded} onClick={() => setFolded((f) => !f)}>
412
+ {folded ? t.viewerFold : t.viewerUnfold}
413
+ </button>
414
+ <button type="button" className="icon-btn" style={{ width: 24, height: 24 }} title={`${t.viewerPrevChange} ([)`} onClick={() => diffRef.current?.prevChange()}>
415
+ <IconChevronUp14 size={12} />
416
+ </button>
417
+ <button type="button" className="icon-btn" style={{ width: 24, height: 24 }} title={`${t.viewerNextChange} (])`} onClick={() => diffRef.current?.nextChange()}>
418
+ <IconChevronDown14 size={12} />
419
+ </button>
420
+ </>
421
+ )}
422
+ {onReference && (
423
+ <button type="button" className="icon-btn" style={{ width: 24, height: 24 }} title={t.fileReference} onClick={() => onReference(activeTab.path)}>
424
+ <IconEditOutline16 size={13} />
425
+ </button>
426
+ )}
427
+ {onOpenEditor && (
428
+ <button type="button" className="icon-btn" style={{ width: 24, height: 24 }} title={t.fileOpenEditor} onClick={() => onOpenEditor(activeTab.path)}>
429
+ <IconFileOutline16 size={13} />
430
+ </button>
431
+ )}
432
+ </div>
433
+
434
+ {/* 正文 */}
435
+ {noticeText && (
436
+ <div className="pw-viewer-notice" data-kind={loaded?.notice}>
437
+ {noticeText}
438
+ </div>
439
+ )}
440
+ {loaded?.truncated && <div className="pw-viewer-notice" data-kind="warn">{mode === "diff" ? t.viewerPatchTruncated : t.filePreviewTruncated}</div>}
441
+ {!loaded || loaded.key !== `${activeTab.path}|${mode}|${range?.from ?? ""}|${range?.to ?? ""}|${change?.status ?? ""}` ? (
442
+ <div className="flex min-h-0 flex-1 items-center justify-center" style={{ fontSize: 12.5, color: "var(--dsw-label-caption)" }}>{t.panelLoading}</div>
443
+ ) : loaded.error ? (
444
+ <div className="flex min-h-0 flex-1 items-center justify-center" style={{ fontSize: 12.5, color: "var(--dsw-danger)" }}>{loaded.error}</div>
445
+ ) : loaded.notice === "missing" ? (
446
+ <div className="flex min-h-0 flex-1 items-center justify-center" style={{ fontSize: 12.5, color: "var(--dsw-label-caption)" }}>{t.viewerMissing}</div>
447
+ ) : loaded.binary ? (
448
+ <div className="flex min-h-0 flex-1 items-center justify-center" style={{ fontSize: 12.5, color: "var(--dsw-label-caption)" }}>{t.fileBinary}</div>
449
+ ) : loaded.diff ? (
450
+ <FullDiff lines={loaded.diff} language={languageForPath(activeTab.path)} folded={folded} handle={diffRef} initialChange />
451
+ ) : mode === "render" && loaded.content !== null ? (
452
+ <div className="md min-h-0 flex-1 overflow-auto px-6 py-4" style={{ fontSize: 14 }}>
453
+ <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[[rehypeHighlight, { detect: false, ignoreMissing: true }]]}>
454
+ {loaded.content}
455
+ </ReactMarkdown>
456
+ </div>
457
+ ) : loaded.content !== null ? (
458
+ <CodeBlock content={loaded.content} name={activeTab.path} />
459
+ ) : null}
460
+ </>
461
+ ) : (
462
+ <>
463
+ <div className="pw-viewer-toolbar hairline-b">
464
+ <div className="flex min-w-0 flex-1 items-center gap-1.5 rounded-lg px-2" style={{ background: "var(--dsw-hover)", height: 26, maxWidth: 360 }}>
465
+ <IconSearchOutline16 size={12} style={{ flex: "none", color: "var(--dsw-label-caption)" }} />
466
+ <input value={treeFilter} onChange={(e) => setTreeFilter(e.target.value)} placeholder={t.growthFilter} className="min-w-0 flex-1 bg-transparent" style={{ fontSize: 12 }} />
467
+ </div>
468
+ <span style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
469
+ {roundNumber ? t.growthRoundLabel.replace("{n}", String(roundNumber)) : ""}
470
+ </span>
471
+ <button type="button" className="pw-chip" onClick={growth.expandAll}>{t.growthExpandAll}</button>
472
+ <button type="button" className="pw-chip" onClick={growth.collapseAll}>{t.growthCollapseAll}</button>
473
+ </div>
474
+ <GrowthTree
475
+ tree={growth.tree}
476
+ expanded={growth.expanded}
477
+ fresh={growth.fresh}
478
+ filter={treeFilter}
479
+ selectedPath={null}
480
+ onToggleDir={growth.toggleDir}
481
+ onExpandLazy={growth.expandLazy}
482
+ onOpenFile={(node: TreeNode) => onOpen(node.path, { from: node.from, lazy: node.lazy })}
483
+ onReference={onReference}
484
+ onOpenEditor={onOpenEditor}
485
+ revealPath={reveal?.path ?? null}
486
+ revealKey={reveal?.key}
487
+ emptyText={t.viewerEmpty}
488
+ />
489
+ </>
490
+ )}
491
+
492
+ {/* 快速打开 */}
493
+ {quickOpen && (
494
+ <div className="pw-quick" onMouseDown={(e) => e.target === e.currentTarget && setQuickOpen(false)}>
495
+ <div className="pw-quick-box popover">
496
+ <div className="flex items-center gap-2 px-2" style={{ height: 34 }}>
497
+ <IconSearchOutline16 size={14} style={{ flex: "none", color: "var(--dsw-label-caption)" }} />
498
+ <input
499
+ ref={quickInputRef}
500
+ value={quickQuery}
501
+ onChange={(e) => {
502
+ setQuickQuery(e.target.value);
503
+ setQuickIndex(0);
504
+ }}
505
+ placeholder={t.viewerQuickOpen}
506
+ className="min-w-0 flex-1 bg-transparent"
507
+ style={{ fontSize: 13 }}
508
+ onKeyDown={(e) => {
509
+ if (e.key === "ArrowDown") {
510
+ e.preventDefault();
511
+ setQuickIndex((i) => Math.min(quickMatches.length - 1, i + 1));
512
+ } else if (e.key === "ArrowUp") {
513
+ e.preventDefault();
514
+ setQuickIndex((i) => Math.max(0, i - 1));
515
+ } else if (e.key === "Enter") {
516
+ e.preventDefault();
517
+ const p = quickMatches[quickIndex];
518
+ if (p) {
519
+ onOpen(p);
520
+ setQuickOpen(false);
521
+ }
522
+ }
523
+ }}
524
+ />
525
+ </div>
526
+ <div className="pw-quick-list">
527
+ {quickMatches.length === 0 ? (
528
+ <div style={{ fontSize: 12.5, color: "var(--dsw-label-caption)", padding: "8px 10px" }}>{t.viewerQuickOpenEmpty}</div>
529
+ ) : (
530
+ quickMatches.map((p, i) => (
531
+ <button
532
+ key={p}
533
+ type="button"
534
+ className="pw-quick-item"
535
+ data-active={i === quickIndex}
536
+ onMouseEnter={() => setQuickIndex(i)}
537
+ onClick={() => {
538
+ onOpen(p);
539
+ setQuickOpen(false);
540
+ }}
541
+ >
542
+ <img className="pw-tree-icon" src={fileIconSrc(p.split("/").pop() ?? p)} alt="" />
543
+ <span className="truncate">{p.split("/").pop()}</span>
544
+ <span className="min-w-0 truncate" style={{ color: "var(--dsw-label-caption)", fontSize: 11.5 }}>{p}</span>
545
+ </button>
546
+ ))
547
+ )}
548
+ </div>
549
+ </div>
550
+ </div>
551
+ )}
552
+ </div>
553
+ </div>
554
+ );
555
+ }