@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,900 @@
1
+ "use client";
2
+
3
+ /**
4
+ * 中央状态:会话列表轮询、当前会话 SSE 订阅、事件折叠(快照→增量)、命令发送。
5
+ */
6
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
+ import type {
8
+ GrowthStep,
9
+ SessionSummary,
10
+ TrajEntry,
11
+ WebEvent,
12
+ WebMessage,
13
+ WebSnapshot,
14
+ ToolPreset,
15
+ } from "@/lib/types";
16
+
17
+ export interface ToolCardState {
18
+ name: string;
19
+ args: unknown;
20
+ state: "running" | "done";
21
+ partialResult?: string;
22
+ result?: string;
23
+ isError?: boolean;
24
+ encodingLoss?: boolean;
25
+ /** edit/write 的 unified patch,用于渲染红绿 diff */
26
+ patch?: string;
27
+ /** 服务端事件时间戳:运行中显示已用时长,完成后显示耗时 */
28
+ startedAt?: number;
29
+ endedAt?: number;
30
+ }
31
+
32
+ /** 扩展发来的、需要浏览器应答的对话框(对应 pi RPC 的 extension_ui_request) */
33
+ export interface ExtensionDialog {
34
+ id: string;
35
+ method: "select" | "confirm" | "input";
36
+ title: string;
37
+ message?: string;
38
+ options?: string[];
39
+ placeholder?: string;
40
+ timeout?: number;
41
+ ts: number;
42
+ }
43
+
44
+ export interface PiWebState {
45
+ snapshot: WebSnapshot | null;
46
+ messages: WebMessage[];
47
+ tools: Record<string, ToolCardState>;
48
+ toolPreset: ToolPreset;
49
+ connected: boolean;
50
+ error: string | null;
51
+ compaction: { phase: "start" | "end"; errorMessage?: string; ts: number } | null;
52
+ /** 自动重试通知(随消息流显示的折叠行,流结束清除) */
53
+ retryNotice: string | null;
54
+ /** 扩展对话框队列(按到达顺序,一次显示一个) */
55
+ extensionDialogs: ExtensionDialog[];
56
+ /** 扩展通知(notify),显示几秒后消失 */
57
+ extensionNotices: { id: string; message: string; type: "info" | "warning" | "error"; ts: number }[];
58
+ /** 扩展设的页脚状态文本(setStatus) */
59
+ extensionStatuses: Record<string, string>;
60
+ /** 扩展覆盖的「工作中」文案(setWorkingMessage) */
61
+ workingMessage: string | null;
62
+ /** 项目生长:本次连接期间收到的步(完整账本由 useGrowth 拉取后合并),以及运行中工具正在生成的路径 */
63
+ growth: { steps: GrowthStep[]; pending: string[]; error: string | null };
64
+ }
65
+
66
+ export interface SessionListItem extends SessionSummary {
67
+ streaming: boolean;
68
+ /** 正在运行的会话最后一个工具步骤(侧栏副标题) */
69
+ lastStep?: string;
70
+ }
71
+
72
+ export const foldPiWebEvent = (state: PiWebState, evt: WebEvent): PiWebState => {
73
+ const s = { ...state };
74
+ switch (evt.type) {
75
+ case "delta": {
76
+ // 新服务端按稳定流 ID 定位;旧协议仍按最近 pending 消息兼容。
77
+ const msgs = [...s.messages];
78
+ let i = evt.messageId ? msgs.findIndex((message) => message.streamId === evt.messageId || message.id === evt.messageId) : msgs.length - 1;
79
+ if (!evt.messageId) while (i >= 0 && msgs[i].role !== "assistant" && msgs[i].role !== "user") i -= 1;
80
+ if (evt.messageId && i >= 0 && msgs[i].stopReason && msgs[i].stopReason !== "pending") return s;
81
+ if (i < 0 || msgs[i].role !== "assistant" || (msgs[i].stopReason ?? "pending") !== "pending") {
82
+ msgs.push({ role: "assistant", content: [], streamId: evt.messageId, stopReason: "pending", timestamp: evt.ts });
83
+ i = msgs.length - 1;
84
+ }
85
+ {
86
+ const m = { ...msgs[i] };
87
+ const content = [...m.content];
88
+ while (content.length <= evt.contentIndex) content.push({ type: "text", text: "" });
89
+ const c: any = { ...content[evt.contentIndex] };
90
+ if (evt.kind === "text" && c.type !== "text") {
91
+ content[evt.contentIndex] = { type: "text", text: evt.delta };
92
+ } else if (evt.kind === "thinking") {
93
+ if (c.type !== "thinking") content[evt.contentIndex] = { type: "thinking", thinking: evt.delta };
94
+ else c.thinking = (c.thinking ?? "") + evt.delta, content[evt.contentIndex] = c;
95
+ } else if (c.type === "text") {
96
+ c.text = (c.text ?? "") + evt.delta;
97
+ content[evt.contentIndex] = c;
98
+ }
99
+ m.content = content;
100
+ msgs[i] = m;
101
+ }
102
+ s.messages = msgs;
103
+ return s;
104
+ }
105
+ case "message": {
106
+ const msgs = [...s.messages];
107
+ const same = msgs.findIndex((message) =>
108
+ (!!evt.message.id && message.id === evt.message.id) ||
109
+ (!!evt.message.streamId && message.streamId === evt.message.streamId));
110
+ if (same >= 0) {
111
+ // snapshot 可能已包含 final,但 SDK 的扩展 message_end 还没返回;不能丢掉权威 end。
112
+ if (evt.phase === "end") msgs[same] = { ...msgs[same], ...evt.message };
113
+ s.messages = msgs;
114
+ return s;
115
+ }
116
+ if (evt.phase === "start") {
117
+ if (evt.message.role === "user" || evt.message.role === "assistant") msgs.push(evt.message);
118
+ } else if (evt.message.role !== "toolResult") {
119
+ // 无稳定流 ID 的旧服务端:只在本轮末尾匹配 pending,不按时间戳覆盖历史消息。
120
+ let index = -1;
121
+ if (!evt.message.streamId) {
122
+ const last = msgs[msgs.length - 1];
123
+ if (evt.message.role === "assistant" && last?.role === "assistant" && (last.stopReason ?? "pending") === "pending") index = msgs.length - 1;
124
+ if (evt.message.role === "user" && last?.role === "user" && !last.id && !last.streamId) index = msgs.length - 1;
125
+ }
126
+ if (index >= 0) msgs[index] = evt.message;
127
+ else msgs.push(evt.message);
128
+ }
129
+ s.messages = msgs;
130
+ return s;
131
+ }
132
+ case "tool": {
133
+ const tools = { ...s.tools };
134
+ const prev = tools[evt.id];
135
+ tools[evt.id] = {
136
+ name: evt.name,
137
+ args: evt.args ?? prev?.args,
138
+ state: evt.state,
139
+ partialResult: evt.partialResult ?? (evt.state === "done" ? undefined : prev?.partialResult),
140
+ result: evt.result ?? prev?.result,
141
+ isError: evt.isError ?? prev?.isError,
142
+ encodingLoss: evt.encodingLoss ?? prev?.encodingLoss,
143
+ patch: evt.patch ?? prev?.patch,
144
+ startedAt: prev?.startedAt ?? evt.ts,
145
+ endedAt: evt.state === "done" ? evt.ts : prev?.endedAt,
146
+ };
147
+ s.tools = tools;
148
+ return s;
149
+ }
150
+ case "queue":
151
+ if (s.snapshot)
152
+ s.snapshot = { ...s.snapshot, queue: { steering: evt.steering, followUp: evt.followUp } };
153
+ return s;
154
+ case "model": {
155
+ if (s.snapshot) {
156
+ const snap = { ...s.snapshot };
157
+ if (evt.model) snap.model = { ...(snap.model ?? { provider: "", id: "", name: "" }), id: evt.model, name: evt.model };
158
+ if (evt.provider && snap.model) snap.model = { ...snap.model, provider: evt.provider };
159
+ if (evt.thinkingLevel) snap.thinkingLevel = evt.thinkingLevel;
160
+ if (evt.thinkingLevels) snap.thinkingLevels = evt.thinkingLevels;
161
+ s.snapshot = snap;
162
+ }
163
+ return s;
164
+ }
165
+ case "usage":
166
+ if (s.snapshot) s.snapshot = { ...s.snapshot, stats: evt.stats, contextUsage: evt.contextUsage };
167
+ return s;
168
+ case "compaction":
169
+ s.compaction = { phase: evt.phase, errorMessage: evt.errorMessage, ts: evt.ts };
170
+ return s;
171
+ case "traj": {
172
+ if (s.snapshot) {
173
+ const traj = [...s.snapshot.trajectory];
174
+ const idx = traj.findIndex((e) => e.seq === evt.entry.seq);
175
+ if (idx >= 0) traj[idx] = evt.entry;
176
+ else traj.push(evt.entry);
177
+ s.snapshot = { ...s.snapshot, trajectory: traj };
178
+ }
179
+ return s;
180
+ }
181
+ case "name":
182
+ if (s.snapshot) s.snapshot = { ...s.snapshot, name: evt.name };
183
+ return s;
184
+ case "tools":
185
+ if (evt.toolPreset) s.toolPreset = evt.toolPreset;
186
+ if (s.snapshot) s.snapshot = {
187
+ ...s.snapshot,
188
+ toolPreset: evt.toolPreset ?? s.snapshot.toolPreset,
189
+ customActiveTools: evt.customActiveTools === undefined ? s.snapshot.customActiveTools : evt.customActiveTools,
190
+ tools: { active: evt.active, all: evt.all.map((n) => ({ name: n })) },
191
+ };
192
+ return s;
193
+ case "resources":
194
+ if (s.snapshot) {
195
+ s.snapshot = {
196
+ ...s.snapshot,
197
+ skills: evt.skills,
198
+ promptTemplates: evt.promptTemplates,
199
+ extensionCommands: evt.extensionCommands,
200
+ projectTrust: evt.projectTrust,
201
+ resourceDiagnostics: evt.resourceDiagnostics,
202
+ };
203
+ }
204
+ return s;
205
+ case "extension_ui": {
206
+ if (evt.method === "select" || evt.method === "confirm" || evt.method === "input") {
207
+ if (s.extensionDialogs.some((dialog) => dialog.id === evt.id)) return s;
208
+ s.extensionDialogs = [
209
+ ...s.extensionDialogs,
210
+ { id: evt.id, method: evt.method, title: evt.title ?? "", message: evt.message, options: evt.options, placeholder: evt.placeholder, timeout: evt.timeout, ts: evt.ts },
211
+ ];
212
+ } else if (evt.method === "notify") {
213
+ s.extensionNotices = [...s.extensionNotices.slice(-4), { id: evt.id, message: evt.message ?? "", type: evt.notifyType ?? "info", ts: evt.ts }];
214
+ } else if (evt.method === "setStatus" && evt.statusKey) {
215
+ const next = { ...s.extensionStatuses };
216
+ if (evt.statusText === undefined || evt.statusText === "") delete next[evt.statusKey];
217
+ else next[evt.statusKey] = evt.statusText;
218
+ s.extensionStatuses = next;
219
+ } else if (evt.method === "setWorkingMessage") {
220
+ s.workingMessage = evt.message ?? null;
221
+ }
222
+ return s;
223
+ }
224
+ case "extension_ui_resolved":
225
+ s.extensionDialogs = s.extensionDialogs.filter((dialog) => dialog.id !== evt.id);
226
+ return s;
227
+ case "status":
228
+ if (s.snapshot) s.snapshot = { ...s.snapshot, isStreaming: evt.isStreaming };
229
+ if (!evt.isStreaming) {
230
+ s.retryNotice = null;
231
+ s.workingMessage = null;
232
+ }
233
+ return s;
234
+ case "growth": {
235
+ const steps = s.growth.steps.some((x) => x.seq === evt.step.seq) ? s.growth.steps.map((x) => (x.seq === evt.step.seq ? evt.step : x)) : [...s.growth.steps, evt.step];
236
+ s.growth = { steps, pending: [], error: null };
237
+ return s;
238
+ }
239
+ case "growth_pending":
240
+ s.growth = { ...s.growth, pending: evt.paths };
241
+ return s;
242
+ case "growth_error":
243
+ s.growth = { ...s.growth, pending: [], error: evt.message };
244
+ return s;
245
+ case "error":
246
+ // 自动重试属流程内通知:随消息流显示、流结束清除,不走 6 秒错误条
247
+ if (/^自动重试/.test(evt.message)) s.retryNotice = evt.message;
248
+ else s.error = evt.message;
249
+ return s;
250
+ default:
251
+ return s;
252
+ }
253
+ };
254
+
255
+ export function extensionDialogsFromSnapshot(snapshot: WebSnapshot, previous: ExtensionDialog[] = []): ExtensionDialog[] {
256
+ // undefined 仅用于兼容旧服务端;新快照的空数组必须清除已答/取消的旧对话框。
257
+ if (snapshot.extensionUiRequests === undefined) return previous;
258
+ const dialogs = new Map<string, ExtensionDialog>();
259
+ for (const request of snapshot.extensionUiRequests) {
260
+ if (request.method === "select" || request.method === "confirm" || request.method === "input") {
261
+ dialogs.set(request.id, { ...request, method: request.method, title: request.title ?? "" });
262
+ }
263
+ }
264
+ return [...dialogs.values()];
265
+ }
266
+
267
+ function savedToolPreset(): ToolPreset {
268
+ if (typeof window === "undefined") return "standard";
269
+ const saved = localStorage.getItem("piweb.toolPreset");
270
+ return saved === "readonly" || saved === "full" ? saved : "standard";
271
+ }
272
+
273
+ function toolsFromMessages(messages: WebMessage[], streaming = false): Record<string, ToolCardState> {
274
+ const tools: Record<string, ToolCardState> = {};
275
+ const resolved = new Set<string>();
276
+ for (const message of messages) {
277
+ for (const content of message.content) {
278
+ if (content.type === "toolCall") {
279
+ tools[content.id] = { name: content.name, args: content.arguments, state: "done" };
280
+ } else if (content.type === "toolResult" && content.toolCallId && tools[content.toolCallId]) {
281
+ resolved.add(content.toolCallId);
282
+ tools[content.toolCallId] = {
283
+ ...tools[content.toolCallId],
284
+ result: content.text,
285
+ isError: content.isError,
286
+ encodingLoss: content.encodingLoss,
287
+ patch: content.patch,
288
+ };
289
+ }
290
+ }
291
+ }
292
+ // 正在流式时还没有结果的调用就是正在执行(中途打开页面拿到的快照),别显示成"已完成 · 无输出"
293
+ if (streaming) {
294
+ for (const [id, tool] of Object.entries(tools)) {
295
+ if (!resolved.has(id)) tools[id] = { ...tool, state: "running", startedAt: Date.now() };
296
+ }
297
+ }
298
+ return tools;
299
+ }
300
+
301
+ const emptyState = (toolPreset: ToolPreset = "standard"): PiWebState => ({
302
+ snapshot: null,
303
+ messages: [],
304
+ tools: {},
305
+ toolPreset,
306
+ connected: false,
307
+ error: null,
308
+ compaction: null,
309
+ retryNotice: null,
310
+ extensionDialogs: [],
311
+ extensionNotices: [],
312
+ extensionStatuses: {},
313
+ workingMessage: null,
314
+ growth: { steps: [], pending: [], error: null },
315
+ });
316
+
317
+ function pathKey(value: string): string {
318
+ const normalized = value.replace(/\\/g, "/").replace(/\/+$/, "");
319
+ return /^[A-Za-z]:\//.test(normalized) || normalized.startsWith("//")
320
+ ? normalized.toLowerCase()
321
+ : normalized;
322
+ }
323
+
324
+ export function usePiWeb() {
325
+ const [sessions, setSessions] = useState<SessionListItem[]>([]);
326
+ const [sessionListError, setSessionListError] = useState<string | null>(null);
327
+ const [sessionListRetryNonce, setSessionListRetryNonce] = useState(0);
328
+ const [currentId, setCurrentId] = useState<string | null>(null);
329
+ const [currentPath, setCurrentPath] = useState<string | null>(null);
330
+ const [state, setState] = useState<PiWebState>(emptyState);
331
+ const [models, setModels] = useState<{ providers: any[]; models: any[] } | null>(null);
332
+ const [addedWorkspaces, setAddedWorkspaces] = useState<string[]>([]);
333
+ const [removedWorkspaces, setRemovedWorkspaces] = useState<string[]>([]);
334
+ const [groupBy, setGroupByState] = useState<"workspace" | "flat">("workspace");
335
+ const [orderBy, setOrderByState] = useState<"updated" | "manual">("updated");
336
+ const [resyncNonce, setResyncNonce] = useState(0);
337
+ const [snapshotEpoch, setSnapshotEpoch] = useState(0);
338
+ const esRef = useRef<EventSource | null>(null);
339
+ const promptRequestsRef = useRef(new Set<string>());
340
+ const presetRestoreAttemptRef = useRef<string | null>(null);
341
+ const presetRestorePromiseRef = useRef<Promise<void> | null>(null);
342
+ const activeSessionRef = useRef(currentId);
343
+ activeSessionRef.current = currentId;
344
+ const subscribedSessionRef = useRef<string | null>(null);
345
+ const retrySessionList = useCallback(() => setSessionListRetryNonce((value) => value + 1), []);
346
+ const clearCompaction = useCallback(() => setState((current) => ({ ...current, compaction: null })), []);
347
+ useEffect(() => {
348
+ const notice = state.compaction;
349
+ if (!notice || notice.phase !== "end") return;
350
+ const timer = setTimeout(() => {
351
+ setState((current) => current.compaction?.ts === notice.ts ? { ...current, compaction: null } : current);
352
+ }, 8_000);
353
+ return () => clearTimeout(timer);
354
+ }, [state.compaction]);
355
+
356
+ const idOf = useCallback((path: string) => {
357
+ const bytes = new TextEncoder().encode(path);
358
+ let bin = "";
359
+ for (const b of bytes) bin += String.fromCharCode(b);
360
+ return encodeURIComponent(btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""));
361
+ }, []);
362
+
363
+ // 视图偏好持久化
364
+ useEffect(() => {
365
+ const g = localStorage.getItem("piweb.groupBy");
366
+ if (g === "flat" || g === "workspace") setGroupByState(g);
367
+ const o = localStorage.getItem("piweb.orderBy");
368
+ if (o === "updated" || o === "manual") setOrderByState(o);
369
+ }, []);
370
+ const setGroupBy = useCallback((v: "workspace" | "flat") => {
371
+ setGroupByState(v);
372
+ localStorage.setItem("piweb.groupBy", v);
373
+ }, []);
374
+ const setOrderBy = useCallback((v: "updated" | "manual") => {
375
+ setOrderByState(v);
376
+ localStorage.setItem("piweb.orderBy", v);
377
+ }, []);
378
+
379
+ const [workspaceAliases, setWorkspaceAliases] = useState<Record<string, string>>({});
380
+ const [archivedSessionPaths, setArchivedSessionPaths] = useState<string[]>([]);
381
+
382
+ // 手动添加的工作区与别名、归档
383
+ const refreshWorkspaces = useCallback(async () => {
384
+ try {
385
+ const r = await fetch("/api/workspaces");
386
+ const j = await r.json();
387
+ if (!r.ok || !j.success) throw new Error(j.error || `request failed (${r.status})`);
388
+ if (j.success) {
389
+ if (Array.isArray(j.data.workspaces)) setAddedWorkspaces(j.data.workspaces);
390
+ if (Array.isArray(j.data.removedWorkspaces)) setRemovedWorkspaces(j.data.removedWorkspaces);
391
+ if (j.data.aliases && typeof j.data.aliases === "object") setWorkspaceAliases(j.data.aliases);
392
+ if (Array.isArray(j.data.archivedSessions)) setArchivedSessionPaths(j.data.archivedSessions);
393
+ }
394
+ } catch (error) {
395
+ const message = error instanceof Error ? error.message : "failed to load workspaces";
396
+ setState((current) => ({ ...current, error: message }));
397
+ }
398
+ }, []);
399
+ useEffect(() => {
400
+ refreshWorkspaces();
401
+ }, [refreshWorkspaces]);
402
+
403
+ /** 会话改名成功后先把列表里的名字改掉,不等下一次轮询(轮询 3 秒 + 服务端 1.2 秒列表缓存) */
404
+ const patchSessionName = useCallback((sessionPath: string, name: string) => {
405
+ const key = pathKey(sessionPath);
406
+ setSessions((prev) => prev.map((s) => (pathKey(s.path) === key ? { ...s, name } : s)));
407
+ }, []);
408
+
409
+ const renameWorkspace = useCallback(async (dir: string, name: string) => {
410
+ try {
411
+ const r = await fetch("/api/workspaces", {
412
+ method: "POST",
413
+ headers: { "Content-Type": "application/json" },
414
+ body: JSON.stringify({ action: "rename", path: dir, name }),
415
+ });
416
+ const j = await r.json();
417
+ if (!r.ok || !j.success) throw new Error(j.error || `request failed (${r.status})`);
418
+ if (j.data?.aliases) setWorkspaceAliases(j.data.aliases);
419
+ } catch (error) {
420
+ const message = error instanceof Error ? error.message : "failed to rename workspace";
421
+ setState((current) => ({ ...current, error: message }));
422
+ throw error;
423
+ }
424
+ }, []);
425
+
426
+ const archiveSession = useCallback(async (sessionPath: string) => {
427
+ try {
428
+ const r = await fetch("/api/workspaces", {
429
+ method: "POST",
430
+ headers: { "Content-Type": "application/json" },
431
+ body: JSON.stringify({ action: "archiveSession", path: sessionPath }),
432
+ });
433
+ const j = await r.json();
434
+ if (!r.ok || !j.success) throw new Error(j.error || `request failed (${r.status})`);
435
+ if (Array.isArray(j.data?.archivedSessions)) setArchivedSessionPaths(j.data.archivedSessions);
436
+ } catch (error) {
437
+ const message = error instanceof Error ? error.message : "failed to archive session";
438
+ setState((current) => ({ ...current, error: message }));
439
+ throw error;
440
+ }
441
+ }, []);
442
+
443
+ const unarchiveSession = useCallback(async (sessionPath: string) => {
444
+ try {
445
+ const r = await fetch("/api/workspaces", {
446
+ method: "POST",
447
+ headers: { "Content-Type": "application/json" },
448
+ body: JSON.stringify({ action: "forgetSession", path: sessionPath }),
449
+ });
450
+ const j = await r.json();
451
+ if (!r.ok || !j.success) throw new Error(j.error || `request failed (${r.status})`);
452
+ if (Array.isArray(j.data?.archivedSessions)) setArchivedSessionPaths(j.data.archivedSessions);
453
+ } catch (error) {
454
+ const message = error instanceof Error ? error.message : "failed to unarchive session";
455
+ setState((current) => ({ ...current, error: message }));
456
+ throw error;
457
+ }
458
+ }, []);
459
+
460
+ const getWorkspaceName = useCallback(
461
+ (dir: string): string => {
462
+ if (!dir) return "";
463
+ const norm = pathKey(dir);
464
+ for (const [k, v] of Object.entries(workspaceAliases)) {
465
+ if (pathKey(k) === norm && v.trim()) {
466
+ return v;
467
+ }
468
+ }
469
+ return dir.split(/[\\/]/).filter(Boolean).pop() || dir;
470
+ },
471
+ [workspaceAliases],
472
+ );
473
+
474
+
475
+
476
+ /** 弹出系统原生文件夹选择器并加入工作区列表;成功返回路径 */
477
+ const addWorkspaceByPicker = useCallback(async (): Promise<string | null> => {
478
+ try {
479
+ const r = await fetch("/api/workspaces", {
480
+ method: "POST",
481
+ headers: { "Content-Type": "application/json" },
482
+ body: JSON.stringify({ action: "pick" }),
483
+ });
484
+ const j = await r.json();
485
+ if (!r.ok || !j.success) throw new Error(j.error || `request failed (${r.status})`);
486
+ if (j.success && j.data?.path) {
487
+ setAddedWorkspaces(j.data.workspaces ?? []);
488
+ await refreshWorkspaces();
489
+ return j.data.path as string;
490
+ }
491
+ return null;
492
+ } catch (error) {
493
+ const message = error instanceof Error ? error.message : "failed to add workspace";
494
+ setState((current) => ({ ...current, error: message }));
495
+ return null;
496
+ }
497
+ }, [refreshWorkspaces]);
498
+
499
+ const removeWorkspace = useCallback(
500
+ async (dir: string) => {
501
+ try {
502
+ const r = await fetch("/api/workspaces", {
503
+ method: "POST",
504
+ headers: { "Content-Type": "application/json" },
505
+ body: JSON.stringify({ action: "remove", path: dir }),
506
+ });
507
+ const j = await r.json();
508
+ if (!r.ok || !j.success) throw new Error(j.error || `request failed (${r.status})`);
509
+ if (j.data) {
510
+ if (Array.isArray(j.data.workspaces)) setAddedWorkspaces(j.data.workspaces);
511
+ if (Array.isArray(j.data.removedWorkspaces)) setRemovedWorkspaces(j.data.removedWorkspaces);
512
+ if (j.data.aliases && typeof j.data.aliases === "object") setWorkspaceAliases(j.data.aliases);
513
+ }
514
+ await refreshWorkspaces();
515
+ } catch (error) {
516
+ const message = error instanceof Error ? error.message : "failed to remove workspace";
517
+ setState((current) => ({ ...current, error: message }));
518
+ throw error;
519
+ }
520
+ },
521
+ [refreshWorkspaces],
522
+ );
523
+
524
+ // 会话列表轮询(3 秒,多标签同步);隐藏标签页暂停,单个标签内不会重叠请求。
525
+ useEffect(() => {
526
+ let alive = true;
527
+ let timer: ReturnType<typeof setTimeout> | undefined;
528
+ let controller: AbortController | undefined;
529
+ // 上一次列表的 ETag:内容没变服务端答 304,跳过解析与 setState(侧栏不会每 3 秒白白重渲染一次)
530
+ let lastEtag: string | null = null;
531
+ const schedule = () => {
532
+ if (!alive || document.visibilityState !== "visible") return;
533
+ timer = setTimeout(() => void load(false), 3000);
534
+ };
535
+ const load = async (force: boolean) => {
536
+ if (!alive || (!force && document.visibilityState !== "visible")) return;
537
+ controller = new AbortController();
538
+ try {
539
+ const r = await fetch("/api/sessions", { signal: controller.signal, headers: lastEtag ? { "If-None-Match": lastEtag } : undefined });
540
+ if (r.status === 304) {
541
+ if (alive) setSessionListError(null);
542
+ return;
543
+ }
544
+ const j = await r.json();
545
+ if (!alive) return;
546
+ if (!r.ok || !j.success) throw new Error(j.error || `request failed (${r.status})`);
547
+ setSessionListError(null);
548
+ lastEtag = r.headers.get("etag");
549
+ const running: Record<string, { streaming: boolean; lastStep?: string }> = j.data.running ?? {};
550
+ const registry = j.data.workspaceRegistry;
551
+ if (registry) {
552
+ if (Array.isArray(registry.workspaces)) setAddedWorkspaces(registry.workspaces);
553
+ if (Array.isArray(registry.removedWorkspaces)) setRemovedWorkspaces(registry.removedWorkspaces);
554
+ if (registry.aliases && typeof registry.aliases === "object") setWorkspaceAliases(registry.aliases);
555
+ if (Array.isArray(registry.archivedSessions)) setArchivedSessionPaths(registry.archivedSessions);
556
+ }
557
+ setSessions(
558
+ (j.data.sessions as SessionSummary[]).map((s) => ({ ...s, streaming: running[s.path]?.streaming === true, lastStep: running[s.path]?.lastStep })),
559
+ );
560
+ } catch (error) {
561
+ if (alive && !(error instanceof DOMException && error.name === "AbortError"))
562
+ setSessionListError(error instanceof Error ? error.message : "session list request failed");
563
+ } finally {
564
+ controller = undefined;
565
+ schedule();
566
+ }
567
+ };
568
+ const onVisibilityChange = () => {
569
+ if (timer) clearTimeout(timer);
570
+ timer = undefined;
571
+ controller?.abort();
572
+ if (document.visibilityState === "visible") void load(false);
573
+ };
574
+ document.addEventListener("visibilitychange", onVisibilityChange);
575
+ void load(true);
576
+ return () => {
577
+ alive = false;
578
+ if (timer) clearTimeout(timer);
579
+ controller?.abort();
580
+ document.removeEventListener("visibilitychange", onVisibilityChange);
581
+ };
582
+ }, [sessionListRetryNonce]);
583
+
584
+ // 模型目录
585
+ const refreshModels = useCallback(async () => {
586
+ try {
587
+ const r = await fetch("/api/models");
588
+ const j = await r.json();
589
+ if (j.success) setModels({ providers: j.data.providers, models: j.data.models });
590
+ } catch {
591
+ /* ignore */
592
+ }
593
+ }, []);
594
+ useEffect(() => {
595
+ refreshModels();
596
+ }, [refreshModels]);
597
+
598
+ // SSE 订阅当前会话(resyncNonce 变化时强制重连取新快照)
599
+ useEffect(() => {
600
+ esRef.current?.close();
601
+ esRef.current = null;
602
+ if (!currentId) {
603
+ subscribedSessionRef.current = null;
604
+ setState(emptyState(savedToolPreset()));
605
+ return;
606
+ }
607
+ const reconnectingCurrentSession = subscribedSessionRef.current === currentId;
608
+ subscribedSessionRef.current = currentId;
609
+ setState((current) => reconnectingCurrentSession
610
+ ? { ...current, connected: false }
611
+ : { ...emptyState(savedToolPreset()), connected: false });
612
+ let retryDelayMs = 2000;
613
+ const connect = () => {
614
+ const es = new EventSource(`/api/agent/${currentId}/events`);
615
+ esRef.current = es;
616
+ es.onopen = () => {
617
+ setState((s) => ({ ...s, connected: true }));
618
+ };
619
+ es.onerror = () => {
620
+ setState((s) => ({ ...s, connected: false }));
621
+ // 浏览器对非 200 响应会永久放弃 EventSource 自动重连
622
+ // (网络断连才会重试),这里手动兜底:关掉后延迟重建,
623
+ // 重建时会拿到全新快照,不依赖 Last-Event-ID 回放。
624
+ if (esRef.current !== es) return;
625
+ es.close();
626
+ esRef.current = null;
627
+ const delay = retryDelayMs;
628
+ retryDelayMs = Math.min(retryDelayMs * 2, 30_000);
629
+ retryTimer = setTimeout(() => {
630
+ if (esRef.current === null) connect();
631
+ }, delay);
632
+ };
633
+ es.onmessage = (e) => {
634
+ let parsed: any;
635
+ try {
636
+ parsed = JSON.parse(e.data);
637
+ } catch {
638
+ // 单帧损坏(代理截断等)只丢弃该帧,不能让 EventSource 回调抛异常
639
+ return;
640
+ }
641
+ if (parsed.type === "snapshot" && parsed.snapshot) {
642
+ retryDelayMs = 2000;
643
+ const snap = parsed.snapshot as WebSnapshot;
644
+ setSnapshotEpoch((epoch) => epoch + 1);
645
+ setState((prev) => ({
646
+ snapshot: snap,
647
+ messages: snap.messages,
648
+ tools: toolsFromMessages(snap.messages, snap.isStreaming),
649
+ toolPreset: snap.toolPreset,
650
+ connected: true,
651
+ error: null,
652
+ compaction: null,
653
+ retryNotice: null,
654
+ // 服务器当前 pending 集合是权威来源,刷新可恢复、已答请求不能复活。
655
+ extensionDialogs: extensionDialogsFromSnapshot(snap, prev.extensionDialogs),
656
+ extensionNotices: prev.extensionNotices,
657
+ extensionStatuses: {},
658
+ workingMessage: null,
659
+ // 重连的快照不带生长账本;连接期间已收到的步保留,useGrowth 会按 seq 与拉取结果去重
660
+ growth: { ...prev.growth, error: snap.growthError ?? null },
661
+ }));
662
+ } else {
663
+ setState((s) => foldPiWebEvent(s, parsed as WebEvent));
664
+ }
665
+ };
666
+ };
667
+ let retryTimer: ReturnType<typeof setTimeout> | undefined;
668
+ connect();
669
+ return () => {
670
+ if (retryTimer) clearTimeout(retryTimer);
671
+ esRef.current?.close();
672
+ esRef.current = null;
673
+ };
674
+ }, [currentId, resyncNonce]);
675
+
676
+ const sendCommand = useCallback(async (cmd: Record<string, unknown>, id?: string): Promise<any> => {
677
+ const target = id ?? currentId;
678
+ if (!target) {
679
+ setState((s) => ({ ...s, error: "no session" }));
680
+ return { success: false, error: "no session" };
681
+ }
682
+ const isPrompt = cmd.cmd === "prompt";
683
+ if (isPrompt && promptRequestsRef.current.has(target)) return { success: false, error: "prompt acceptance pending" };
684
+ if (isPrompt) promptRequestsRef.current.add(target);
685
+ try {
686
+ const r = await fetch(`/api/agent/${target}`, {
687
+ method: "POST",
688
+ headers: { "Content-Type": "application/json" },
689
+ body: JSON.stringify(cmd),
690
+ });
691
+ const result = await r.json();
692
+ if (!r.ok || !result.success) setState((s) => ({ ...s, error: result.error || `request failed (${r.status})` }));
693
+ return result;
694
+ } catch (error) {
695
+ const message = error instanceof Error ? error.message : "request failed";
696
+ setState((s) => ({ ...s, error: message }));
697
+ return { success: false, error: message };
698
+ } finally {
699
+ if (isPrompt) promptRequestsRef.current.delete(target);
700
+ }
701
+ }, [currentId]);
702
+
703
+ const newSession = useCallback(async (
704
+ cwd: string,
705
+ overrides: { provider?: string; modelId?: string; thinking?: string } = {},
706
+ ): Promise<string | null> => {
707
+ try {
708
+ const r = await fetch("/api/agent/new", {
709
+ method: "POST",
710
+ headers: { "Content-Type": "application/json" },
711
+ body: JSON.stringify({ cwd }),
712
+ });
713
+ const j = await r.json();
714
+ if (j.success) {
715
+ const p = j.data.sessionPath as string;
716
+ const id = idOf(p);
717
+ // 工具预设取通用设置的持久化选择;模型/思考级别由 Hero 页显式下发
718
+ const setup: Record<string, unknown>[] = [{ cmd: "setToolPreset", preset: savedToolPreset() }];
719
+ if (overrides.provider && overrides.modelId) setup.push({ cmd: "setModel", provider: overrides.provider, modelId: overrides.modelId });
720
+ if (overrides.thinking) setup.push({ cmd: "setThinkingLevel", level: overrides.thinking });
721
+ for (const command of setup) {
722
+ const configured = await sendCommand(command, id);
723
+ if (!configured.success) {
724
+ // 思考级别与模型不兼容(如非推理模型只支持 off)时降级跳过,
725
+ // 不能因此删掉整个会话让用户毫无提示。
726
+ if (command.cmd === "setThinkingLevel") continue;
727
+ await fetch(`/api/sessions/${id}`, { method: "DELETE" }).catch(() => undefined);
728
+ return null;
729
+ }
730
+ }
731
+ setCurrentPath(p);
732
+ setCurrentId(id);
733
+ try {
734
+ localStorage.setItem("piweb.currentSession", p);
735
+ } catch {
736
+ /* ignore */
737
+ }
738
+ return p;
739
+ }
740
+ setState((s) => ({ ...s, error: j.error || "failed to create session" }));
741
+ return null;
742
+ } catch (error) {
743
+ setState((s) => ({ ...s, error: error instanceof Error ? error.message : "failed to create session" }));
744
+ return null;
745
+ }
746
+ }, [idOf, sendCommand]);
747
+
748
+ const openSession = useCallback(
749
+ (path: string) => {
750
+ setCurrentPath(path);
751
+ setCurrentId(idOf(path));
752
+ // 持久化当前会话:刷新页面时恢复到这里,而不是回到新会话草稿
753
+ try {
754
+ localStorage.setItem("piweb.currentSession", path);
755
+ } catch {
756
+ /* ignore */
757
+ }
758
+ },
759
+ [idOf],
760
+ );
761
+
762
+ const closeSession = useCallback(() => {
763
+ setCurrentId(null);
764
+ setCurrentPath(null);
765
+ try {
766
+ localStorage.removeItem("piweb.currentSession");
767
+ } catch {
768
+ /* ignore */
769
+ }
770
+ }, []);
771
+
772
+ // 刷新恢复:会话列表首次加载后,若刷新前有打开的会话且仍存在,则重新打开
773
+ const restoredRef = useRef(false);
774
+ useEffect(() => {
775
+ if (restoredRef.current || !sessions.length) return;
776
+ restoredRef.current = true;
777
+ try {
778
+ const saved = localStorage.getItem("piweb.currentSession");
779
+ if (saved) {
780
+ if (sessions.some((s) => s.path === saved)) {
781
+ // 标记本次 currentPath 来自刷新恢复:侧栏据此跳过"自动展开其工作区"
782
+ sessionStorage.setItem("piweb.sessionRestored", "1");
783
+ openSession(saved);
784
+ } else localStorage.removeItem("piweb.currentSession");
785
+ }
786
+ } catch {
787
+ /* ignore */
788
+ }
789
+ }, [sessions, openSession]);
790
+
791
+ const setToolPreset = useCallback(
792
+ async (preset: ToolPreset) => {
793
+ await presetRestorePromiseRef.current;
794
+ if (activeSessionRef.current !== currentId) return;
795
+ if (currentId) {
796
+ const result = await sendCommand({ cmd: "setToolPreset", preset });
797
+ if (!result?.success) return;
798
+ }
799
+ localStorage.setItem("piweb.toolPreset", preset);
800
+ if (activeSessionRef.current === currentId) setState((s) => ({
801
+ ...s,
802
+ toolPreset: preset,
803
+ snapshot: s.snapshot ? { ...s.snapshot, toolPreset: preset } : null,
804
+ }));
805
+ },
806
+ [currentId, sendCommand],
807
+ );
808
+
809
+ // ---------- 会话预设已移除:工具档位见通用设置/工具启用情况,模型与思考级别由 Hero 页显式下发 ----------
810
+
811
+ /** 强制重连 SSE 拿新快照(navigate 等场景) */
812
+ const resync = useCallback(() => setResyncNonce((n) => n + 1), []);
813
+ /** 应答扩展对话框(select/confirm/input);cancelled=true 表示用户关掉了 */
814
+ const answerExtensionDialog = useCallback(
815
+ async (id: string, response: { value?: string; confirmed?: boolean; cancelled?: boolean }) => {
816
+ setState((s) => ({ ...s, extensionDialogs: s.extensionDialogs.filter((d) => d.id !== id) }));
817
+ const target = currentId;
818
+ const result = await sendCommand({ cmd: "extensionUiResponse", requestId: id, ...response });
819
+ if (!result?.success && activeSessionRef.current === target) resync();
820
+ },
821
+ [currentId, resync, sendCommand],
822
+ );
823
+ const dismissExtensionNotice = useCallback((id: string) => setState((s) => ({ ...s, extensionNotices: s.extensionNotices.filter((n) => n.id !== id) })), []);
824
+ const clearError = useCallback(() => setState((s) => ({ ...s, error: null })), []);
825
+ /** 供 AppShell 直接展示行内提示(如未选工作区就发送) */
826
+ const setError = useCallback((message: string) => setState((s) => ({ ...s, error: message })), []);
827
+
828
+ // 工具权限不写入 Pi JSONL。每次收到服务端快照后恢复本地选择;运行中延后到空闲。
829
+ useEffect(() => {
830
+ if (!currentId || !state.connected || !state.snapshot || state.snapshot.isStreaming || state.snapshot.customActiveTools != null) return;
831
+ const preset = savedToolPreset();
832
+ if (state.snapshot.toolPreset === preset) return;
833
+ const attemptKey = `${currentId}:${snapshotEpoch}`;
834
+ if (presetRestoreAttemptRef.current === attemptKey) return;
835
+ presetRestoreAttemptRef.current = attemptKey;
836
+ const sessionId = currentId;
837
+ const restore = sendCommand({ cmd: "setToolPreset", preset }).then((result) => {
838
+ if (result?.success && activeSessionRef.current === sessionId && savedToolPreset() === preset) setState((current) => ({
839
+ ...current,
840
+ toolPreset: preset,
841
+ snapshot: current.snapshot ? { ...current.snapshot, toolPreset: preset } : null,
842
+ }));
843
+ }).finally(() => {
844
+ if (presetRestorePromiseRef.current === restore) presetRestorePromiseRef.current = null;
845
+ });
846
+ presetRestorePromiseRef.current = restore;
847
+ }, [currentId, snapshotEpoch, state.connected, state.snapshot, sendCommand]);
848
+
849
+ // dsh sessionVisible 合同:当前会话豁免归档过滤——归档当前会话不关闭、
850
+ // 主列表保持可见可聊,已归档区也不显示它(取消归档前菜单按真实归档态切换)。
851
+ const visibleSessions = useMemo(() => {
852
+ const set = new Set(archivedSessionPaths.map(pathKey));
853
+ const currentKey = currentPath ? pathKey(currentPath) : null;
854
+ return sessions.filter((s) => !set.has(pathKey(s.path)) || (currentKey != null && pathKey(s.path) === currentKey));
855
+ }, [sessions, archivedSessionPaths, currentPath]);
856
+ const archivedSessions = useMemo(() => {
857
+ const set = new Set(archivedSessionPaths.map(pathKey));
858
+ const currentKey = currentPath ? pathKey(currentPath) : null;
859
+ return sessions.filter((s) => set.has(pathKey(s.path)) && !(currentKey != null && pathKey(s.path) === currentKey));
860
+ }, [sessions, archivedSessionPaths, currentPath]);
861
+
862
+ return {
863
+ sessions: visibleSessions,
864
+ sessionListError,
865
+ retrySessionList,
866
+ archivedSessions,
867
+ /** 原始归档路径列表(含被豁免的当前会话,供菜单显示真实归档态) */
868
+ archivedSessionPaths,
869
+ currentId,
870
+ currentPath,
871
+ state,
872
+ models,
873
+ addedWorkspaces,
874
+ removedWorkspaces,
875
+ workspaceAliases,
876
+ getWorkspaceName,
877
+ renameWorkspace,
878
+ patchSessionName,
879
+ archiveSession,
880
+ unarchiveSession,
881
+ resync,
882
+ groupBy,
883
+ orderBy,
884
+ setGroupBy,
885
+ setOrderBy,
886
+ addWorkspaceByPicker,
887
+ removeWorkspace,
888
+ refreshModels,
889
+ sendCommand,
890
+ newSession,
891
+ openSession,
892
+ closeSession,
893
+ setToolPreset,
894
+ clearError,
895
+ clearCompaction,
896
+ setError,
897
+ answerExtensionDialog,
898
+ dismissExtensionNotice,
899
+ };
900
+ }