@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,1066 @@
1
+ "use client";
2
+
3
+ /**
4
+ * 侧栏 —— 照抄 dsh ui-sidebar:
5
+ * 品牌行(mark + 字标 + 描边徽章 + 侧栏开合)、新会话按钮(h38 r12),
6
+ * 工作区标题行(搜索 / 滑杆菜单 / 添加工作区=原生文件夹选择器),
7
+ * 工作区树(▶/▼ 三角 + 文件夹 + 名称;展开行 hover 显 … 和 +;会话缩进 + 相对时间),
8
+ * 搜索模式(输入框替身标题行,空查询=工作区平铺,输入=会话平铺),
9
+ * 滑杆菜单(分组方式:按工作区/单列表;排序方式:手动排序/最近更新)。
10
+ */
11
+ import { useEffect, useMemo, useRef, useState } from "react";
12
+ import { createPortal } from "react-dom";
13
+ import { BrandPi, PiMark } from "@/components/PiMark";
14
+ import {
15
+ IconArchiveOutline20,
16
+ IconBranchOutline16,
17
+ IconCheckOutline14,
18
+ IconDownloadOutline16,
19
+ IconEditOutline16,
20
+ IconEllipsisOutline16,
21
+ IconFolderClose16,
22
+ IconFolderOpenOutline16,
23
+ IconNewChatOutline16,
24
+ IconPanelLeftOutline16,
25
+ IconPersonalizationOutline16,
26
+ IconPlusOutline16,
27
+ IconProjectAddOutline16,
28
+ IconSearchOutline16,
29
+ IconCloseOutline14,
30
+ IconSettingsOutline16,
31
+ IconTrashOutline16,
32
+ IconTriangleRightFill14,
33
+ } from "@/components/icons";
34
+ import { useI18n } from "@/i18n";
35
+ import type { SessionListItem } from "@/hooks/usePiWeb";
36
+
37
+ function relTime(iso: string, lang: "zh" | "en"): string {
38
+ const d = Date.now() - new Date(iso).getTime();
39
+ const m = Math.floor(d / 60000);
40
+ if (m < 1) return lang === "zh" ? "刚刚" : "now";
41
+ if (m < 60) return lang === "zh" ? `${m}分钟` : `${m}m`;
42
+ const h = Math.floor(m / 60);
43
+ if (h < 24) return lang === "zh" ? `${h}小时` : `${h}h`;
44
+ const days = Math.floor(h / 24);
45
+ if (days < 30) return lang === "zh" ? `${days}天` : `${days}d`;
46
+ return lang === "zh" ? `${Math.floor(days / 30)}月` : `${Math.floor(days / 30)}mo`;
47
+ }
48
+
49
+ function basename(p: string): string {
50
+ const norm = p.replace(/\\/g, "/");
51
+ return norm.split("/").filter(Boolean).pop() ?? norm;
52
+ }
53
+
54
+ function normPath(p: string): string {
55
+ if (!p) return "";
56
+ const normalized = p.replace(/\\/g, "/").replace(/\/+$/, "");
57
+ return /^[A-Za-z]:\//.test(normalized) || normalized.startsWith("//")
58
+ ? normalized.toLowerCase()
59
+ : normalized;
60
+ }
61
+
62
+ type GroupBy = "workspace" | "flat";
63
+ type OrderBy = "updated" | "manual";
64
+
65
+ interface WorkspaceGroup {
66
+ cwd: string;
67
+ sessions: SessionListItem[];
68
+ lastModified: string;
69
+ }
70
+
71
+ export function SessionSidebar({
72
+ sessions,
73
+ sessionListError,
74
+ onRetrySessionList,
75
+ archivedSessions = [],
76
+ archivedPaths = [],
77
+ addedWorkspaces,
78
+ removedWorkspaces = [],
79
+ currentPath,
80
+ groupBy,
81
+ orderBy,
82
+ setGroupBy,
83
+ setOrderBy,
84
+ collapsed,
85
+ onToggleCollapse,
86
+ onOpen,
87
+ onNew,
88
+ onNewInWorkspace,
89
+ onRename,
90
+ onFork,
91
+ onArchive,
92
+ onUnarchive,
93
+ onExport,
94
+ getWorkspaceName,
95
+ onRenameWorkspace,
96
+ onDeleteWorkspace,
97
+ onOpenSettings,
98
+ onAddWorkspace,
99
+ draftCwd,
100
+ }: {
101
+ sessions: SessionListItem[];
102
+ sessionListError?: string | null;
103
+ onRetrySessionList?: () => void;
104
+ archivedSessions?: SessionListItem[];
105
+ /** 原始归档路径(含被豁免的当前会话:dsh 合同下主列表可见但菜单显示取消归档) */
106
+ archivedPaths?: string[];
107
+ addedWorkspaces: string[];
108
+ removedWorkspaces?: string[];
109
+ currentPath: string | null;
110
+ groupBy: GroupBy;
111
+ orderBy: OrderBy;
112
+ setGroupBy: (v: GroupBy) => void;
113
+ setOrderBy: (v: OrderBy) => void;
114
+ collapsed: boolean;
115
+ onToggleCollapse: () => void;
116
+ onOpen: (path: string) => void;
117
+ onNew: () => void;
118
+ onNewInWorkspace: (cwd: string) => void;
119
+ onRename: (path: string, newName: string) => Promise<void>;
120
+ onFork?: (path: string, cwd: string) => void;
121
+ onArchive?: (path: string) => void;
122
+ onUnarchive?: (path: string) => void;
123
+ /** 下载会话日志(JSONL 导出,经 AppShell 统一编码会话 id) */
124
+ onExport?: (path: string) => void;
125
+ getWorkspaceName?: (cwd: string) => string;
126
+ onRenameWorkspace?: (cwd: string, newName: string) => Promise<void>;
127
+ onDeleteWorkspace?: (cwd: string) => Promise<void>;
128
+ onOpenSettings: () => void;
129
+ onAddWorkspace: () => void;
130
+ /** 新会话草稿所在的工作区:条目显示在该分组内,发送第一条消息才真正创建 */
131
+ draftCwd?: string | null;
132
+ }) {
133
+ const { t, lang } = useI18n();
134
+ const [q, setQ] = useState("");
135
+ const [searchOpen, setSearchOpen] = useState(false);
136
+ const [viewMenu, setViewMenu] = useState(false);
137
+ const [rowMenu, setRowMenu] = useState<string | null>(null); // 展开工作区行菜单的 cwd
138
+ const [sessionMenu, setSessionMenu] = useState<string | null>(null); // 展开会话行菜单的 session path
139
+ const [archiveOpen, setArchiveOpen] = useState(false);
140
+ const menuRef = useRef<HTMLDivElement>(null);
141
+ const sessionMenuRef = useRef<HTMLDivElement>(null);
142
+ const renameInputRef = useRef<HTMLInputElement>(null);
143
+
144
+ // 自定义弹窗状态(重命名与删除工作区)
145
+ const [renameTarget, setRenameTarget] = useState<
146
+ { type: "session"; path: string; name: string } | { type: "workspace"; cwd: string; name: string } | null
147
+ >(null);
148
+ const [renameValue, setRenameValue] = useState("");
149
+ const [renamePending, setRenamePending] = useState(false);
150
+ const [renameError, setRenameError] = useState<string | null>(null);
151
+ const [deleteWorkspaceTarget, setDeleteWorkspaceTarget] = useState<{ cwd: string; name: string } | null>(null);
152
+ const [deleteWorkspacePending, setDeleteWorkspacePending] = useState(false);
153
+ const [deleteWorkspaceError, setDeleteWorkspaceError] = useState<string | null>(null);
154
+ const [mounted, setMounted] = useState(false);
155
+ const renameWorkspaceConflict = renameTarget?.type === "workspace"
156
+ && renameValue.trim().length > 0
157
+ && addedWorkspaces.some((cwd) => (
158
+ normPath(cwd) !== normPath(renameTarget.cwd)
159
+ && (getWorkspaceName ? getWorkspaceName(cwd) : basename(cwd)) === renameValue.trim()
160
+ ));
161
+
162
+ useEffect(() => {
163
+ setMounted(true);
164
+ }, []);
165
+
166
+ useEffect(() => {
167
+ if (renameTarget) {
168
+ setRenameValue(renameTarget.name);
169
+ setRenamePending(false);
170
+ setRenameError(null);
171
+ setTimeout(() => {
172
+ renameInputRef.current?.focus();
173
+ renameInputRef.current?.select();
174
+ }, 30);
175
+ }
176
+ }, [renameTarget]);
177
+
178
+ const handleConfirmRename = async () => {
179
+ if (!renameTarget || renamePending) return;
180
+ const val = renameValue.trim();
181
+ if (!val || (renameTarget.type === "workspace" && (val === renameTarget.name || renameWorkspaceConflict))) {
182
+ if (renameWorkspaceConflict) {
183
+ setRenameError(t.workspaceNameConflict.replace("{name}", val));
184
+ }
185
+ return;
186
+ }
187
+ setRenamePending(true);
188
+ setRenameError(null);
189
+ try {
190
+ if (renameTarget.type === "session") {
191
+ await onRename(renameTarget.path, val);
192
+ } else {
193
+ await onRenameWorkspace?.(renameTarget.cwd, val);
194
+ }
195
+ setRenameTarget(null);
196
+ } catch (error) {
197
+ setRenameError(error instanceof Error ? error.message : String(error));
198
+ } finally {
199
+ setRenamePending(false);
200
+ }
201
+ };
202
+
203
+ const closeRename = () => {
204
+ if (renamePending) return;
205
+ setRenameTarget(null);
206
+ setRenameError(null);
207
+ };
208
+
209
+ const handleConfirmDeleteWorkspace = async () => {
210
+ if (!deleteWorkspaceTarget || deleteWorkspacePending) return;
211
+ const targetCwd = deleteWorkspaceTarget.cwd;
212
+ setDeleteWorkspacePending(true);
213
+ setDeleteWorkspaceError(null);
214
+ try {
215
+ await onDeleteWorkspace?.(targetCwd);
216
+ setDeleteWorkspaceTarget(null);
217
+ } catch (error) {
218
+ setDeleteWorkspaceError(error instanceof Error ? error.message : String(error));
219
+ } finally {
220
+ setDeleteWorkspacePending(false);
221
+ }
222
+ };
223
+
224
+ const currentCwd = sessions.find((s) => s.path === currentPath)?.cwd;
225
+
226
+ useEffect(() => {
227
+ if (!viewMenu && !rowMenu && !sessionMenu) return;
228
+ const h = (e: MouseEvent) => {
229
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
230
+ setViewMenu(false);
231
+ setRowMenu(null);
232
+ }
233
+ if (sessionMenuRef.current && !sessionMenuRef.current.contains(e.target as Node)) {
234
+ setSessionMenu(null);
235
+ }
236
+ };
237
+ document.addEventListener("mousedown", h);
238
+ return () => document.removeEventListener("mousedown", h);
239
+ }, [viewMenu, rowMenu, sessionMenu]);
240
+
241
+ const { workspaceGroups, ungroupedGroup } = useMemo(() => {
242
+ const removedSet = new Set((removedWorkspaces || []).map(normPath));
243
+ const activeNormSet = new Set<string>();
244
+ const activeWorkspaces: string[] = [];
245
+
246
+ for (const w of addedWorkspaces) {
247
+ const n = normPath(w);
248
+ if (!n || removedSet.has(n) || activeNormSet.has(n)) continue;
249
+ activeWorkspaces.push(w);
250
+ activeNormSet.add(n);
251
+ }
252
+
253
+ const map = new Map<string, { cwd: string; sessions: SessionListItem[] }>();
254
+ for (const w of activeWorkspaces) {
255
+ map.set(normPath(w), { cwd: w, sessions: [] });
256
+ }
257
+
258
+ const ungroupedList: SessionListItem[] = [];
259
+ for (const s of sessions) {
260
+ const sNorm = normPath(s.cwd);
261
+ if (sNorm && map.has(sNorm)) {
262
+ map.get(sNorm)!.sessions.push(s);
263
+ } else {
264
+ ungroupedList.push(s);
265
+ }
266
+ }
267
+
268
+ const groups: WorkspaceGroup[] = Array.from(map.values()).map(({ cwd, sessions: list }) => ({
269
+ cwd,
270
+ sessions: [...list].sort((a, b) => b.modified.localeCompare(a.modified)),
271
+ lastModified: list.reduce((acc, s) => (s.modified > acc ? s.modified : acc), ""),
272
+ }));
273
+
274
+ if (orderBy === "updated") {
275
+ groups.sort((a, b) => b.lastModified.localeCompare(a.lastModified));
276
+ } else {
277
+ groups.sort((a, b) => basename(a.cwd).localeCompare(basename(b.cwd)));
278
+ }
279
+
280
+ let ungrouped: WorkspaceGroup | null = null;
281
+ if (ungroupedList.length > 0) {
282
+ ungrouped = {
283
+ cwd: "__ungrouped__",
284
+ sessions: [...ungroupedList].sort((a, b) => b.modified.localeCompare(a.modified)),
285
+ lastModified: ungroupedList.reduce((acc, s) => (s.modified > acc ? s.modified : acc), ""),
286
+ };
287
+ }
288
+
289
+ return { workspaceGroups: groups, ungroupedGroup: ungrouped };
290
+ }, [sessions, addedWorkspaces, removedWorkspaces, orderBy]);
291
+
292
+ // 工作区展开状态持久化:刷新后保持收起/展开,不被自动展开逻辑覆盖
293
+ const [openSet, setOpenSet] = useState<Set<string>>(() => {
294
+ try {
295
+ const saved = JSON.parse(localStorage.getItem("piweb.openWorkspaces") ?? "[]");
296
+ if (Array.isArray(saved)) return new Set(saved.filter((x) => typeof x === "string"));
297
+ } catch {
298
+ /* ignore */
299
+ }
300
+ return new Set();
301
+ });
302
+ useEffect(() => {
303
+ try {
304
+ localStorage.setItem("piweb.openWorkspaces", JSON.stringify([...openSet]));
305
+ } catch {
306
+ /* ignore */
307
+ }
308
+ }, [openSet]);
309
+
310
+ // 仅在用户主动切换会话时展开其所在工作区一次;刷新恢复(sessionRestored 标记)
311
+ // 与后续列表轮询/手动收起都不再自动撑开
312
+ const lastExpandedFor = useRef<string | null>(null);
313
+ useEffect(() => {
314
+ if (!currentPath) {
315
+ lastExpandedFor.current = null;
316
+ return;
317
+ }
318
+ if (lastExpandedFor.current === currentPath) return;
319
+ try {
320
+ if (sessionStorage.getItem("piweb.sessionRestored")) {
321
+ sessionStorage.removeItem("piweb.sessionRestored");
322
+ lastExpandedFor.current = currentPath;
323
+ return;
324
+ }
325
+ } catch {
326
+ /* ignore */
327
+ }
328
+ const s = sessions.find((item) => item.path === currentPath);
329
+ if (!s) return; // 会话列表尚未加载,等轮询后重试
330
+ lastExpandedFor.current = currentPath;
331
+ const matchedGroup = workspaceGroups.find((g) => normPath(g.cwd) === normPath(s.cwd));
332
+ if (matchedGroup) {
333
+ setOpenSet((prev) => (prev.has(matchedGroup.cwd) ? prev : new Set([...prev, matchedGroup.cwd])));
334
+ } else if (ungroupedGroup) {
335
+ setOpenSet((prev) => (prev.has("__ungrouped__") ? prev : new Set([...prev, "__ungrouped__"])));
336
+ }
337
+ }, [currentPath, sessions, workspaceGroups, ungroupedGroup]);
338
+
339
+ // 进入草稿态时展开其所属分组一次(对齐 dsh 新建 blank 会话即定位到该工作区)
340
+ const lastDraftCwd = useRef<string | null>(null);
341
+ useEffect(() => {
342
+ if (!draftCwd) {
343
+ lastDraftCwd.current = null;
344
+ return;
345
+ }
346
+ if (lastDraftCwd.current === draftCwd) return;
347
+ lastDraftCwd.current = draftCwd;
348
+ const groupKey = workspaceGroups.find((g) => normPath(g.cwd) === normPath(draftCwd))?.cwd ?? "__ungrouped__";
349
+ setOpenSet((prev) => (prev.has(groupKey) ? prev : new Set([...prev, groupKey])));
350
+ }, [draftCwd, workspaceGroups]);
351
+
352
+ const flatSessions = useMemo(() => {
353
+ const list = [...sessions];
354
+ if (orderBy === "updated") list.sort((a, b) => b.modified.localeCompare(a.modified));
355
+ else list.sort((a, b) => a.created.localeCompare(b.created));
356
+ return list;
357
+ }, [sessions, orderBy]);
358
+
359
+ const toggleOpen = (cwd: string) => {
360
+ setOpenSet((prev) => {
361
+ const next = new Set(prev);
362
+ if (next.has(cwd)) next.delete(cwd);
363
+ else next.add(cwd);
364
+ return next;
365
+ });
366
+ };
367
+
368
+ if (collapsed) {
369
+ return (
370
+ <div
371
+ className="flex h-full flex-col items-center gap-3"
372
+ style={{ paddingLeft: 10, paddingRight: 10, paddingTop: 12, paddingBottom: 12 }}
373
+ >
374
+ {/* dsh:导轨顶部是品牌标,点击展开侧栏 */}
375
+ <button className="rail-btn" title="»" onClick={onToggleCollapse}>
376
+ <PiMark size={22} />
377
+ </button>
378
+ <button className="rail-btn" title={t.newChat} onClick={onNew}>
379
+ <IconNewChatOutline16 size={18} />
380
+ </button>
381
+ <button className="rail-btn" title={t.addWorkspace} onClick={onAddWorkspace}>
382
+ <IconProjectAddOutline16 size={17} />
383
+ </button>
384
+ <button
385
+ className="rail-btn"
386
+ title={t.search}
387
+ onClick={() => {
388
+ setSearchOpen(true);
389
+ onToggleCollapse();
390
+ }}
391
+ >
392
+ <IconSearchOutline16 size={17} />
393
+ </button>
394
+ <div className="flex-1" />
395
+ <button className="rail-btn" title={t.settings} onClick={onOpenSettings}>
396
+ <IconSettingsOutline16 size={17} />
397
+ </button>
398
+ </div>
399
+ );
400
+ }
401
+
402
+ const sessionRow = (s: SessionListItem, indented: boolean, archived = false) => {
403
+ const selected = s.path === currentPath;
404
+ // dsh 合同:被归档的当前会话仍显示在主列表,菜单按真实归档态切换
405
+ const reallyArchived = archived || archivedPaths.some((p) => normPath(p) === normPath(s.path));
406
+ return (
407
+ <div
408
+ key={s.path}
409
+ className="group relative flex items-center rounded-xl transition-colors"
410
+ style={{
411
+ background: selected ? "var(--dsw-active)" : "transparent",
412
+ paddingLeft: indented ? 38 : 10,
413
+ paddingRight: 8,
414
+ paddingTop: 6,
415
+ paddingBottom: 6,
416
+ // 选中/悬停框之间留缝,不互相粘连
417
+ marginTop: 2,
418
+ marginBottom: 2,
419
+ }}
420
+ onMouseEnter={(e) => {
421
+ if (!selected) e.currentTarget.style.background = "var(--dsw-hover)";
422
+ }}
423
+ onMouseLeave={(e) => {
424
+ if (!selected) e.currentTarget.style.background = "transparent";
425
+ }}
426
+ >
427
+ {s.streaming && <span className="state-dot running" style={{ position: "absolute", left: indented ? 26 : 6, width: 6, height: 6 }} />}
428
+ <button className="flex min-w-0 flex-1 flex-col items-start text-left" onClick={() => onOpen(s.path)}>
429
+ <span
430
+ className="w-full truncate"
431
+ style={{ fontSize: 13, color: selected ? "var(--dsw-accent)" : "var(--dsw-label-primary)" }}
432
+ >
433
+ {s.name || s.firstMessage || "(untitled)"}
434
+ </span>
435
+ {s.streaming && s.lastStep && (
436
+ <span className="w-full truncate" style={{ fontSize: 11, fontFamily: "var(--font-mono)", color: "var(--dsw-label-caption)", lineHeight: "16px" }} title={s.lastStep}>
437
+ {s.lastStep}
438
+ </span>
439
+ )}
440
+ </button>
441
+ <span
442
+ className={`flex-none pl-1 ${sessionMenu === s.path ? "hidden" : "group-hover:hidden group-focus-within:hidden"}`}
443
+ style={{ fontSize: 10.5, color: "var(--dsw-label-caption)" }}
444
+ >
445
+ {relTime(s.modified, lang)}
446
+ </span>
447
+ <span className={`flex-none items-center pl-1 ${sessionMenu === s.path ? "flex" : "hidden group-hover:flex group-focus-within:flex"}`}>
448
+ <button
449
+ className="icon-btn"
450
+ style={{ width: 22, height: 22 }}
451
+ title="..."
452
+ aria-label="session-actions"
453
+ onClick={(e) => {
454
+ e.stopPropagation();
455
+ setSessionMenu(sessionMenu === s.path ? null : s.path);
456
+ setRowMenu(null);
457
+ }}
458
+ >
459
+ <IconEllipsisOutline16 size={14} />
460
+ </button>
461
+ </span>
462
+ {sessionMenu === s.path && (
463
+ <div className="popover absolute right-1 top-8 z-50 w-36 py-1 shadow-xl" ref={sessionMenuRef}>
464
+ <MenuItem
465
+ icon={<IconEditOutline16 size={14} />}
466
+ label={t.rename}
467
+ onClick={(e) => {
468
+ e.stopPropagation();
469
+ setSessionMenu(null);
470
+ setRenameTarget({
471
+ type: "session",
472
+ path: s.path,
473
+ name: s.name || s.firstMessage || "",
474
+ });
475
+ }}
476
+ />
477
+ <MenuItem
478
+ icon={<IconDownloadOutline16 size={14} />}
479
+ label={t.cmdExport}
480
+ onClick={(e) => {
481
+ e.stopPropagation();
482
+ setSessionMenu(null);
483
+ onExport?.(s.path);
484
+ }}
485
+ />
486
+ <MenuItem
487
+ icon={<IconBranchOutline16 size={14} />}
488
+ label={t.forkSession}
489
+ onClick={(e) => {
490
+ e.stopPropagation();
491
+ setSessionMenu(null);
492
+ onFork?.(s.path, s.cwd);
493
+ }}
494
+ />
495
+ <MenuItem
496
+ icon={<IconArchiveOutline20 size={14} />}
497
+ label={reallyArchived ? t.unarchiveSession : t.archiveSession}
498
+ onClick={(e) => {
499
+ e.stopPropagation();
500
+ setSessionMenu(null);
501
+ if (reallyArchived) onUnarchive?.(s.path);
502
+ else onArchive?.(s.path);
503
+ }}
504
+ />
505
+ </div>
506
+ )}
507
+ </div>
508
+ );
509
+ };
510
+
511
+ const workspaceRow = (g: WorkspaceGroup) => {
512
+ const isUngrouped = g.cwd === "__ungrouped__";
513
+ const open = openSet.has(g.cwd);
514
+ const isCurrent = !isUngrouped && g.cwd === currentCwd;
515
+ // 草稿新会话属于哪个分组:路径匹配的工作区,否则归未分组
516
+ const draftInThisGroup = draftCwd
517
+ ? (isUngrouped
518
+ ? !workspaceGroups.some((wg) => normPath(wg.cwd) === normPath(draftCwd))
519
+ : normPath(g.cwd) === normPath(draftCwd))
520
+ : false;
521
+ const displayName = isUngrouped
522
+ ? (t.ungrouped || "未分组")
523
+ : (getWorkspaceName ? getWorkspaceName(g.cwd) : basename(g.cwd));
524
+
525
+ return (
526
+ <div key={g.cwd}>
527
+ <div
528
+ className="group relative flex items-center rounded-xl transition-colors"
529
+ style={{
530
+ background: open || isCurrent ? "var(--dsw-hover)" : "transparent",
531
+ paddingLeft: 10,
532
+ paddingRight: 6,
533
+ paddingTop: 7,
534
+ paddingBottom: 7,
535
+ // 悬停/展开框之间留缝,不互相粘连(与会话行一致)
536
+ marginTop: 2,
537
+ marginBottom: 2,
538
+ }}
539
+ onMouseEnter={(e) => {
540
+ if (!(open || isCurrent)) e.currentTarget.style.background = "var(--dsw-hover)";
541
+ }}
542
+ onMouseLeave={(e) => {
543
+ if (!(open || isCurrent)) e.currentTarget.style.background = "transparent";
544
+ }}
545
+ >
546
+ <button className="flex min-w-0 flex-1 items-center gap-2 text-left" onClick={() => toggleOpen(g.cwd)} title={displayName}>
547
+ <span
548
+ style={{
549
+ color: "var(--dsw-label-tertiary)",
550
+ display: "inline-flex",
551
+ transform: open ? "rotate(90deg)" : "none",
552
+ transition: "transform 120ms var(--ds-ease-in-out)",
553
+ flex: "none",
554
+ }}
555
+ >
556
+ <IconTriangleRightFill14 size={11} />
557
+ </span>
558
+ <span style={{ color: isCurrent ? "var(--dsw-accent)" : "var(--dsw-label-tertiary)", display: "inline-flex", flex: "none" }}>
559
+ {open ? <IconFolderOpenOutline16 size={15} /> : <IconFolderClose16 size={15} />}
560
+ </span>
561
+ <span
562
+ className="truncate"
563
+ style={{ fontSize: 13, color: isCurrent ? "var(--dsw-accent)" : "var(--dsw-label-primary)" }}
564
+ >
565
+ {displayName}
566
+ </span>
567
+ </button>
568
+ <span className="hidden flex-none items-center gap-0.5 pl-1 group-hover:flex group-focus-within:flex" style={{ color: "var(--dsw-label-tertiary)" }}>
569
+ {!isUngrouped && (
570
+ <button
571
+ className="icon-btn"
572
+ style={{ width: 22, height: 22 }}
573
+ title="..."
574
+ aria-label="workspace-actions"
575
+ onClick={() => {
576
+ setRowMenu(rowMenu === g.cwd ? null : g.cwd);
577
+ setSessionMenu(null);
578
+ }}
579
+ >
580
+ <IconEllipsisOutline16 size={14} />
581
+ </button>
582
+ )}
583
+ <button
584
+ className="icon-btn"
585
+ style={{ width: 22, height: 22 }}
586
+ title={isUngrouped ? t.newChat : t.newSessionHere}
587
+ onClick={() => (isUngrouped ? onNew() : onNewInWorkspace(g.cwd))}
588
+ >
589
+ <IconPlusOutline16 size={14} />
590
+ </button>
591
+ </span>
592
+ {!isUngrouped && rowMenu === g.cwd && (
593
+ <div className="popover absolute right-1 top-9 z-50 w-44 py-1 shadow-xl" ref={menuRef}>
594
+ <MenuItem
595
+ icon={<IconEditOutline16 size={14} />}
596
+ label={t.rename}
597
+ onClick={(e) => {
598
+ e.stopPropagation();
599
+ setRowMenu(null);
600
+ setRenameTarget({
601
+ type: "workspace",
602
+ cwd: g.cwd,
603
+ name: displayName,
604
+ });
605
+ }}
606
+ />
607
+ <MenuItem
608
+ icon={<IconTrashOutline16 size={14} />}
609
+ label={t.deleteWorkspace}
610
+ danger
611
+ onClick={(e) => {
612
+ e.stopPropagation();
613
+ setRowMenu(null);
614
+ setDeleteWorkspaceTarget({
615
+ cwd: g.cwd,
616
+ name: displayName,
617
+ });
618
+ setDeleteWorkspaceError(null);
619
+ }}
620
+ />
621
+ </div>
622
+ )}
623
+ </div>
624
+ {open && (
625
+ <>
626
+ {/* dsh 草稿新会话:显示在所属工作区分组内,发送第一条消息才真正创建,切走即消失 */}
627
+ {draftInThisGroup && (
628
+ <div
629
+ className="flex items-center rounded-xl"
630
+ style={{
631
+ background: "var(--dsw-active)",
632
+ paddingLeft: 38,
633
+ paddingRight: 8,
634
+ paddingTop: 6,
635
+ paddingBottom: 6,
636
+ marginTop: 2,
637
+ marginBottom: 2,
638
+ }}
639
+ >
640
+ <IconNewChatOutline16 size={14} style={{ color: "var(--dsw-accent)", flex: "none", marginRight: 8 }} />
641
+ <span className="truncate" style={{ fontSize: 13, color: "var(--dsw-accent)" }}>
642
+ {t.newChat}
643
+ </span>
644
+ </div>
645
+ )}
646
+ {g.sessions.map((s) => sessionRow(s, true))}
647
+ </>
648
+ )}
649
+ </div>
650
+ );
651
+ };
652
+
653
+ return (
654
+ <div className="flex h-full min-h-0 flex-col">
655
+ {/* 品牌行 */}
656
+ <div className="flex items-center gap-1 px-4 pb-2 pt-4">
657
+ <BrandPi />
658
+ <div className="flex-1" />
659
+ <button className="icon-btn" onClick={onToggleCollapse} title="«">
660
+ <IconPanelLeftOutline16 size={15} />
661
+ </button>
662
+ </div>
663
+
664
+ {/* 新会话(dsh .newSession) */}
665
+ <div className="px-3 pt-1">
666
+ <button className="btn-new-session w-full" onClick={onNew}>
667
+ <IconNewChatOutline16 size={14} />
668
+ {t.newChat}
669
+ </button>
670
+ </div>
671
+
672
+ {/* 工作区标题行 / 搜索框(互替,照 dsh) */}
673
+ {searchOpen ? (
674
+ <div className="px-3 pb-1 pt-3">
675
+ <div className="flex items-center gap-2 rounded-xl px-2.5 py-2" style={{ background: "var(--dsw-hover)" }}>
676
+ <IconSearchOutline16 size={14} style={{ color: "var(--dsw-label-caption)" }} />
677
+ <input
678
+ autoFocus
679
+ value={q}
680
+ onChange={(e) => setQ(e.target.value)}
681
+ placeholder={t.searchSessions}
682
+ className="w-full"
683
+ style={{ fontSize: 13 }}
684
+ />
685
+ <button
686
+ className="icon-btn"
687
+ style={{ width: 20, height: 20 }}
688
+ onClick={() => {
689
+ setQ("");
690
+ setSearchOpen(false);
691
+ }}
692
+ >
693
+ <IconCloseOutline14 size={13} />
694
+ </button>
695
+ </div>
696
+ </div>
697
+ ) : (
698
+ <div className="flex items-center gap-0.5 px-4 pb-1 pt-3">
699
+ <span style={{ fontSize: 13, color: "var(--dsw-label-primary)" }}>{t.workspaces}</span>
700
+ <div className="flex-1" />
701
+ <button className="icon-btn" style={{ width: 26, height: 26 }} title={t.search} onClick={() => setSearchOpen(true)}>
702
+ <IconSearchOutline16 size={14} />
703
+ </button>
704
+ <div className="relative" ref={viewMenu ? menuRef : undefined}>
705
+ <button
706
+ className="icon-btn"
707
+ style={{ width: 26, height: 26, background: viewMenu ? "var(--dsw-active)" : undefined, borderRadius: 8 }}
708
+ title={t.groupBy}
709
+ onClick={() => setViewMenu((v) => !v)}
710
+ >
711
+ <IconPersonalizationOutline16 size={14} />
712
+ </button>
713
+ {viewMenu && (
714
+ <div className="popover absolute right-0 top-8 z-50 w-56 py-2">
715
+ <div className="px-3 pb-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
716
+ {t.groupBy}
717
+ </div>
718
+ <MenuCheck label={t.byWorkspace} checked={groupBy === "workspace"} onClick={() => { setGroupBy("workspace"); setViewMenu(false); }} />
719
+ <MenuCheck label={t.flatList} checked={groupBy === "flat"} onClick={() => { setGroupBy("flat"); setViewMenu(false); }} />
720
+ <div className="my-1.5" style={{ borderTop: "0.5px solid var(--dsw-border-l2)" }} />
721
+ <div className="px-3 pb-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
722
+ {t.sortBy}
723
+ </div>
724
+ <MenuCheck label={t.manualOrder} checked={orderBy === "manual"} onClick={() => { setOrderBy("manual"); setViewMenu(false); }} />
725
+ <MenuCheck label={t.recentUpdated} checked={orderBy === "updated"} onClick={() => { setOrderBy("updated"); setViewMenu(false); }} />
726
+ </div>
727
+ )}
728
+ </div>
729
+ <button className="icon-btn" style={{ width: 26, height: 26 }} title={t.addWorkspace} onClick={onAddWorkspace}>
730
+ <IconProjectAddOutline16 size={14} />
731
+ </button>
732
+ </div>
733
+ )}
734
+
735
+ {/* 列表区 */}
736
+ <div className="min-h-0 flex-1 overflow-y-auto px-3 pb-3">
737
+ {sessionListError && (
738
+ <div role="alert" className="mb-2 rounded-xl px-3 py-2" style={{ background: "var(--dsw-hover)", color: "var(--dsw-danger)", fontSize: 12 }}>
739
+ <div>{t.sessionListLoadFailed}: {sessionListError}</div>
740
+ <button type="button" className="mt-1 underline" onClick={onRetrySessionList}>{t.retry}</button>
741
+ </div>
742
+ )}
743
+ {searchOpen && q.trim() ? (
744
+ // 搜索结果:会话平铺
745
+ flatSessions.filter(
746
+ (s) =>
747
+ s.name?.toLowerCase().includes(q.trim().toLowerCase()) ||
748
+ s.firstMessage.toLowerCase().includes(q.trim().toLowerCase()) ||
749
+ basename(s.cwd).toLowerCase().includes(q.trim().toLowerCase()),
750
+ ).length === 0 ? (
751
+ <div className="px-2 py-6 text-center" style={{ fontSize: 12.5, color: "var(--dsw-label-caption)" }}>
752
+
753
+ </div>
754
+ ) : (
755
+ flatSessions
756
+ .filter(
757
+ (s) =>
758
+ s.name?.toLowerCase().includes(q.trim().toLowerCase()) ||
759
+ s.firstMessage.toLowerCase().includes(q.trim().toLowerCase()) ||
760
+ basename(s.cwd).toLowerCase().includes(q.trim().toLowerCase()),
761
+ )
762
+ .map((s) => sessionRow(s, false))
763
+ )
764
+ ) : searchOpen ? (
765
+ // 空查询:工作区平铺(照 dsh 截图)
766
+ [...workspaceGroups, ...(ungroupedGroup ? [ungroupedGroup] : [])].map((g) => (
767
+ <div
768
+ key={g.cwd}
769
+ className="flex items-center gap-2.5 rounded-xl px-2.5 py-2 transition-colors"
770
+ onMouseEnter={(e) => (e.currentTarget.style.background = "var(--dsw-hover)")}
771
+ onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
772
+ onClick={() => {
773
+ setSearchOpen(false);
774
+ toggleOpen(g.cwd);
775
+ }}
776
+ title={g.cwd === "__ungrouped__" ? (t.ungrouped || "未分组") : g.cwd}
777
+ >
778
+ <span style={{ color: "var(--dsw-label-tertiary)", display: "inline-flex" }}>
779
+ <IconFolderClose16 size={15} />
780
+ </span>
781
+ <span className="truncate" style={{ fontSize: 13 }}>
782
+ {g.cwd === "__ungrouped__"
783
+ ? (t.ungrouped || "未分组")
784
+ : (getWorkspaceName ? getWorkspaceName(g.cwd) : basename(g.cwd))}
785
+ </span>
786
+ </div>
787
+ ))
788
+ ) : groupBy === "workspace" ? (
789
+ // 按工作区分组
790
+ workspaceGroups.length === 0 && !ungroupedGroup ? (
791
+ <div className="px-2 py-6 text-center" style={{ fontSize: 12.5, color: "var(--dsw-label-caption)" }}>
792
+ {t.noSessions}
793
+ </div>
794
+ ) : (
795
+ <>
796
+ {workspaceGroups.map(workspaceRow)}
797
+ {ungroupedGroup && workspaceRow(ungroupedGroup)}
798
+ </>
799
+ )
800
+ ) : (
801
+ // 单列表
802
+ <>
803
+ {draftCwd && (
804
+ <div
805
+ className="flex items-center rounded-xl"
806
+ style={{
807
+ background: "var(--dsw-active)",
808
+ paddingLeft: 10,
809
+ paddingRight: 8,
810
+ paddingTop: 6,
811
+ paddingBottom: 6,
812
+ marginTop: 2,
813
+ marginBottom: 2,
814
+ }}
815
+ >
816
+ <IconNewChatOutline16 size={14} style={{ color: "var(--dsw-accent)", flex: "none", marginRight: 8 }} />
817
+ <span className="truncate" style={{ fontSize: 13, color: "var(--dsw-accent)" }}>
818
+ {t.newChat}
819
+ </span>
820
+ </div>
821
+ )}
822
+ {(flatSessions.length === 0
823
+ ? [...workspaceGroups, ...(ungroupedGroup ? [ungroupedGroup] : [])].map((g) => (
824
+ <div key={g.cwd} className="flex items-center gap-2.5 rounded-xl px-2.5 py-2" title={g.cwd}>
825
+ <span style={{ color: "var(--dsw-label-tertiary)", display: "inline-flex" }}>
826
+ <IconFolderClose16 size={15} />
827
+ </span>
828
+ <span className="truncate" style={{ fontSize: 13 }}>
829
+ {g.cwd === "__ungrouped__"
830
+ ? (t.ungrouped || "未分组")
831
+ : (getWorkspaceName ? getWorkspaceName(g.cwd) : basename(g.cwd))}
832
+ </span>
833
+ </div>
834
+ ))
835
+ : flatSessions.map((s) => sessionRow(s, false)))}
836
+ </>
837
+ )}
838
+ {!searchOpen && archivedSessions.length > 0 && (
839
+ <div className="mt-2 border-t pt-2" style={{ borderColor: "var(--dsw-border-l2)" }}>
840
+ <button
841
+ type="button"
842
+ className="flex w-full items-center gap-2 rounded-xl px-2.5 py-2 text-left transition-colors"
843
+ aria-expanded={archiveOpen}
844
+ onClick={() => setArchiveOpen((open) => !open)}
845
+ onMouseEnter={(event) => (event.currentTarget.style.background = "var(--dsw-hover)")}
846
+ onMouseLeave={(event) => (event.currentTarget.style.background = "transparent")}
847
+ >
848
+ <span
849
+ style={{
850
+ display: "inline-flex",
851
+ transform: archiveOpen ? "rotate(90deg)" : "none",
852
+ transition: "transform 120ms var(--ds-ease-in-out)",
853
+ color: "var(--dsw-label-tertiary)",
854
+ }}
855
+ >
856
+ <IconTriangleRightFill14 size={11} />
857
+ </span>
858
+ <IconArchiveOutline20 size={15} style={{ color: "var(--dsw-label-tertiary)" }} />
859
+ <span className="min-w-0 flex-1 truncate" style={{ fontSize: 13 }}>{t.archivedSessions}</span>
860
+ <span style={{ fontSize: 11, color: "var(--dsw-label-caption)" }}>{archivedSessions.length}</span>
861
+ </button>
862
+ {archiveOpen && archivedSessions.map((session) => sessionRow(session, true, true))}
863
+ </div>
864
+ )}
865
+ </div>
866
+
867
+ {/* 设置固定左下角 */}
868
+ <div className="px-3 py-2.5">
869
+ <button
870
+ className="flex w-full items-center gap-2.5 rounded-xl px-4 py-2 text-left transition-colors"
871
+ style={{ color: "var(--dsw-label-secondary)" }}
872
+ onMouseEnter={(e) => (e.currentTarget.style.background = "var(--dsw-hover)")}
873
+ onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
874
+ onClick={onOpenSettings}
875
+ >
876
+ <IconSettingsOutline16 size={16} />
877
+ <span style={{ fontSize: "var(--dsh-content-font-size)" }}>{t.settings}</span>
878
+ </button>
879
+ </div>
880
+
881
+ {/* 重命名弹窗(照用户截图 media_1788745548711.png,通过 createPortal 挂载到 document.body 确保浏览器正居中) */}
882
+ {mounted && renameTarget && createPortal(
883
+ <div
884
+ className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-150"
885
+ onClick={closeRename}
886
+ >
887
+ <div
888
+ className="relative w-full max-w-[420px] rounded-[24px] bg-[#212121] p-6 text-white shadow-2xl border border-white/10"
889
+ onClick={(e) => e.stopPropagation()}
890
+ role="dialog"
891
+ aria-modal="true"
892
+ aria-labelledby="rename-dialog-title"
893
+ >
894
+ {/* 关闭按钮 */}
895
+ <button
896
+ type="button"
897
+ onClick={closeRename}
898
+ disabled={renamePending}
899
+ className="absolute right-5 top-5 rounded-full p-1 text-zinc-400 hover:text-white hover:bg-white/10 transition"
900
+ aria-label={t.close}
901
+ >
902
+ <IconCloseOutline14 size={16} />
903
+ </button>
904
+
905
+ {/* 标题 */}
906
+ <h3 id="rename-dialog-title" className="text-lg font-medium text-white pr-8">
907
+ {renameTarget.type === "session"
908
+ ? (t.renameSession || "重命名会话")
909
+ : (t.renameWorkspace || "重命名工作区")}
910
+ </h3>
911
+
912
+ {/* 输入框 */}
913
+ <div className="mt-5 mb-6">
914
+ <input
915
+ ref={renameInputRef}
916
+ type="text"
917
+ value={renameValue}
918
+ disabled={renamePending}
919
+ onChange={(e) => {
920
+ setRenameValue(e.target.value);
921
+ setRenameError(null);
922
+ }}
923
+ onKeyDown={(e) => {
924
+ if (e.key === "Enter") void handleConfirmRename();
925
+ if (e.key === "Escape") closeRename();
926
+ }}
927
+ className="w-full rounded-full bg-white/[0.05] border border-white/15 px-4 py-2.5 text-[14.5px] text-white outline-none focus:border-white/40 focus:ring-1 focus:ring-white/30 transition"
928
+ />
929
+ </div>
930
+ {renameError && (
931
+ <p className="-mt-3 mb-5 text-sm" style={{ color: "var(--dsw-danger)" }} role="alert">{renameError}</p>
932
+ )}
933
+
934
+ {/* 按钮区 */}
935
+ <div className="flex items-center justify-end gap-3">
936
+ <button
937
+ type="button"
938
+ onClick={closeRename}
939
+ disabled={renamePending}
940
+ className="rounded-full bg-[#2c2c2c] px-5 py-2 text-sm font-medium text-zinc-200 hover:bg-[#383838] transition"
941
+ >
942
+ {t.cancel}
943
+ </button>
944
+ <button
945
+ type="button"
946
+ onClick={() => void handleConfirmRename()}
947
+ disabled={renamePending || !renameValue.trim() || (renameTarget.type === "workspace" && (renameValue.trim() === renameTarget.name || renameWorkspaceConflict))}
948
+ className="rounded-full bg-white px-5 py-2 text-sm font-medium text-black hover:bg-zinc-200 transition"
949
+ >
950
+ {t.rename}
951
+ </button>
952
+ </div>
953
+ </div>
954
+ </div>,
955
+ document.body
956
+ )}
957
+
958
+ {/* 删除工作区确认弹窗(照用户截图 media_1788745235179.png 与 media_1788746156687.png,通过 createPortal 挂载到 document.body 确保浏览器正居中) */}
959
+ {mounted && deleteWorkspaceTarget && createPortal(
960
+ <div
961
+ className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-150"
962
+ onClick={() => !deleteWorkspacePending && setDeleteWorkspaceTarget(null)}
963
+ >
964
+ <div
965
+ className="relative w-full max-w-[420px] rounded-[24px] bg-[#212121] p-6 text-white shadow-2xl border border-white/10"
966
+ onClick={(e) => e.stopPropagation()}
967
+ role="dialog"
968
+ aria-modal="true"
969
+ aria-labelledby="delete-workspace-dialog-title"
970
+ >
971
+ {/* 关闭按钮 */}
972
+ <button
973
+ type="button"
974
+ onClick={() => setDeleteWorkspaceTarget(null)}
975
+ disabled={deleteWorkspacePending}
976
+ className="absolute right-5 top-5 rounded-full p-1 text-zinc-400 hover:text-white hover:bg-white/10 transition"
977
+ aria-label={t.close}
978
+ >
979
+ <IconCloseOutline14 size={16} />
980
+ </button>
981
+
982
+ {/* 标题 */}
983
+ <h3 id="delete-workspace-dialog-title" className="text-lg font-medium text-white pr-8">
984
+ {t.deleteWorkspace}
985
+ </h3>
986
+
987
+ {/* 描述文本 */}
988
+ <p className="mt-3 text-[14.5px] leading-relaxed text-zinc-300">
989
+ {(t.deleteWorkspaceConfirm || "将把“{name}”从工作区列表中移除。文件夹与会话记录会保留,其会话将显示在“未分组”下。").replace(
990
+ "{name}",
991
+ deleteWorkspaceTarget.name,
992
+ )}
993
+ </p>
994
+ {deleteWorkspacePending && (
995
+ <p className="mt-4 text-sm text-zinc-400" role="status">{t.deleteWorkspacePending}</p>
996
+ )}
997
+ {deleteWorkspaceError && (
998
+ <p className="mt-4 text-sm" style={{ color: "var(--dsw-danger)" }} role="alert">{deleteWorkspaceError}</p>
999
+ )}
1000
+
1001
+ {/* 按钮区 */}
1002
+ <div className="mt-6 flex items-center justify-end gap-3">
1003
+ <button
1004
+ type="button"
1005
+ onClick={() => setDeleteWorkspaceTarget(null)}
1006
+ disabled={deleteWorkspacePending}
1007
+ className="rounded-xl border border-transparent bg-[#2c2c2c] px-5 py-2 text-sm font-medium text-zinc-200 transition hover:border-white/25 hover:bg-[#383838]"
1008
+ >
1009
+ {t.cancel}
1010
+ </button>
1011
+ <button
1012
+ type="button"
1013
+ onClick={() => void handleConfirmDeleteWorkspace()}
1014
+ disabled={deleteWorkspacePending}
1015
+ className="rounded-xl border border-transparent bg-[#2c2c2c] px-5 py-2 text-sm font-medium text-[#f87171] transition hover:border-[#f87171]/40 hover:bg-[#383838] hover:text-[#ef4444]"
1016
+ >
1017
+ {t.deleteWorkspace}
1018
+ </button>
1019
+ </div>
1020
+ </div>
1021
+ </div>,
1022
+ document.body
1023
+ )}
1024
+ </div>
1025
+ );
1026
+ }
1027
+
1028
+ function MenuItem({
1029
+ icon,
1030
+ label,
1031
+ danger,
1032
+ onClick,
1033
+ }: {
1034
+ icon: React.ReactNode;
1035
+ label: string;
1036
+ danger?: boolean;
1037
+ onClick: (e: React.MouseEvent) => void;
1038
+ }) {
1039
+ return (
1040
+ <button
1041
+ className="mx-1 flex w-[calc(100%-8px)] items-center gap-2.5 rounded-lg px-3 py-2 text-left transition-colors"
1042
+ style={{ fontSize: 13, color: danger ? "var(--dsw-danger)" : "var(--dsw-label-primary)" }}
1043
+ onMouseEnter={(e) => (e.currentTarget.style.background = "var(--dsw-hover)")}
1044
+ onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
1045
+ onClick={onClick}
1046
+ >
1047
+ {icon}
1048
+ {label}
1049
+ </button>
1050
+ );
1051
+ }
1052
+
1053
+ function MenuCheck({ label, checked, onClick }: { label: string; checked: boolean; onClick: () => void }) {
1054
+ return (
1055
+ <button
1056
+ className="mx-1 flex w-[calc(100%-8px)] items-center justify-between rounded-lg pl-5 pr-3 py-2 text-left transition-colors"
1057
+ style={{ fontSize: 13, color: "var(--dsw-label-primary)" }}
1058
+ onMouseEnter={(e) => (e.currentTarget.style.background = "var(--dsw-hover)")}
1059
+ onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
1060
+ onClick={onClick}
1061
+ >
1062
+ {label}
1063
+ {checked && <IconCheckOutline14 size={13} />}
1064
+ </button>
1065
+ );
1066
+ }