@rexvane/piweb 0.3.24 → 0.3.26

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 (285) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +11 -11
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +5 -5
  5. package/.next/react-loadable-manifest.json +13 -20
  6. package/.next/required-server-files.js +2 -1
  7. package/.next/required-server-files.json +2 -1
  8. package/.next/routes-manifest.json +6 -8
  9. package/.next/server/app/_global-error/page.js +2 -2
  10. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_global-error.html +1 -1
  12. package/.next/server/app/_global-error.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page.js +1 -1
  17. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/_not-found.html +1 -1
  19. package/.next/server/app/_not-found.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  24. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  25. package/.next/server/app/api/agent/[id]/route.js +1 -1
  26. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  27. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  28. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  29. package/.next/server/app/api/agent/new/route.js +1 -1
  30. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  31. package/.next/server/app/api/agent/resources/route.js +1 -0
  32. package/.next/server/app/api/agent/resources/route.js.nft.json +1 -0
  33. package/.next/server/app/api/agent/resources/route_client-reference-manifest.js +1 -0
  34. package/.next/server/app/api/config/route.js +1 -1
  35. package/.next/server/app/api/config/route.js.nft.json +1 -1
  36. package/.next/server/app/api/files/route.js +1 -1
  37. package/.next/server/app/api/files/route.js.nft.json +1 -1
  38. package/.next/server/app/api/fs/route.js +1 -1
  39. package/.next/server/app/api/fs/route.js.nft.json +1 -1
  40. package/.next/server/app/api/git/route.js +4 -4
  41. package/.next/server/app/api/git/route.js.nft.json +1 -1
  42. package/.next/server/app/api/github/route.js +4 -4
  43. package/.next/server/app/api/github/route.js.nft.json +1 -1
  44. package/.next/server/app/api/growth/route.js +1 -1
  45. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  46. package/.next/server/app/api/health/route.js +1 -1
  47. package/.next/server/app/api/models/defaults/route.js +1 -1
  48. package/.next/server/app/api/models/defaults/route.js.nft.json +1 -1
  49. package/.next/server/app/api/models/route.js +1 -1
  50. package/.next/server/app/api/models/route.js.nft.json +1 -1
  51. package/.next/server/app/api/pi-settings/route.js +1 -1
  52. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  53. package/.next/server/app/api/plugins/route.js +1 -1
  54. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  55. package/.next/server/app/api/prompts/route.js +1 -1
  56. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  57. package/.next/server/app/api/remote/route.js +4 -3
  58. package/.next/server/app/api/security/route.js +1 -1
  59. package/.next/server/app/api/security/route.js.nft.json +1 -1
  60. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  61. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  62. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  63. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  64. package/.next/server/app/api/sessions/import/route.js +2 -2
  65. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  66. package/.next/server/app/api/sessions/route.js +1 -1
  67. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  68. package/.next/server/app/api/skills/route.js +2 -2
  69. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  70. package/.next/server/app/api/update/route.js +2 -2
  71. package/.next/server/app/api/update/route.js.nft.json +1 -1
  72. package/.next/server/app/api/version/route.js +1 -1
  73. package/.next/server/app/api/version/route.js.nft.json +1 -1
  74. package/.next/server/app/api/web-auth/route.js +2 -2
  75. package/.next/server/app/api/workspaces/route.js +1 -1
  76. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  77. package/.next/server/app/icon.svg/route.js +1 -1
  78. package/.next/server/app/index.html +1 -1
  79. package/.next/server/app/index.rsc +6 -7
  80. package/.next/server/app/index.segments/__PAGE__.segment.rsc +6 -7
  81. package/.next/server/app/index.segments/_full.segment.rsc +6 -7
  82. package/.next/server/app/index.segments/_tree.segment.rsc +2 -3
  83. package/.next/server/app/login/page.js +1 -1
  84. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/login.html +1 -1
  86. package/.next/server/app/login.rsc +3 -3
  87. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  88. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  89. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -3
  90. package/.next/server/app/page.js +1 -1
  91. package/.next/server/app/page.js.nft.json +1 -1
  92. package/.next/server/app/page_client-reference-manifest.js +1 -1
  93. package/.next/server/app-paths-manifest.json +11 -11
  94. package/.next/server/chunks/3924.js +3 -3
  95. package/.next/server/chunks/538.js +1 -1
  96. package/.next/server/chunks/620.js +1 -0
  97. package/.next/server/chunks/6229.js +1 -1
  98. package/.next/server/chunks/7496.js +1 -1
  99. package/.next/server/chunks/7662.js +2 -2
  100. package/.next/server/chunks/8686.js +1 -1
  101. package/.next/server/functions-config-manifest.json +1 -2
  102. package/.next/server/instrumentation.js.nft.json +1 -1
  103. package/.next/server/middleware-build-manifest.js +1 -1
  104. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  105. package/.next/server/pages/404.html +1 -1
  106. package/.next/server/pages/500.html +1 -1
  107. package/.next/server/preview-props.json +3 -3
  108. package/.next/server/server-reference-manifest.json +1 -1
  109. package/.next/static/QJCtqgDKzGS2BdKABd9ch/_buildManifest.js +1 -0
  110. package/.next/static/chunks/2577.86c8538fbf05a8b2.js +1 -0
  111. package/.next/static/chunks/2716.fa561519675a3ff1.js +1 -0
  112. package/.next/static/chunks/2906.8a0e09bb47605cf1.js +1 -0
  113. package/.next/static/chunks/4673.ea36e55005c0f82f.js +1 -0
  114. package/.next/static/chunks/4686-4dc565817a160351.js +1 -0
  115. package/.next/static/chunks/4778.0d2c5d75b791d352.js +1 -0
  116. package/.next/static/chunks/{618.949981da99be7182.js → 618.9a22656b863f242b.js} +2 -2
  117. package/.next/static/chunks/app/_global-error/page-f2d93969c0bcf406.js +1 -0
  118. package/.next/static/chunks/app/api/agent/[id]/events/route-f2d93969c0bcf406.js +1 -0
  119. package/.next/static/chunks/app/api/agent/[id]/route-f2d93969c0bcf406.js +1 -0
  120. package/.next/static/chunks/app/api/agent/[id]/tree/route-f2d93969c0bcf406.js +1 -0
  121. package/.next/static/chunks/app/api/agent/new/route-f2d93969c0bcf406.js +1 -0
  122. package/.next/static/chunks/app/api/agent/resources/route-f2d93969c0bcf406.js +1 -0
  123. package/.next/static/chunks/app/api/config/route-f2d93969c0bcf406.js +1 -0
  124. package/.next/static/chunks/app/api/files/route-f2d93969c0bcf406.js +1 -0
  125. package/.next/static/chunks/app/api/fs/route-f2d93969c0bcf406.js +1 -0
  126. package/.next/static/chunks/app/api/git/route-f2d93969c0bcf406.js +1 -0
  127. package/.next/static/chunks/app/api/github/route-f2d93969c0bcf406.js +1 -0
  128. package/.next/static/chunks/app/api/growth/route-f2d93969c0bcf406.js +1 -0
  129. package/.next/static/chunks/app/api/health/route-f2d93969c0bcf406.js +1 -0
  130. package/.next/static/chunks/app/api/models/defaults/route-f2d93969c0bcf406.js +1 -0
  131. package/.next/static/chunks/app/api/models/route-f2d93969c0bcf406.js +1 -0
  132. package/.next/static/chunks/app/api/pi-settings/route-f2d93969c0bcf406.js +1 -0
  133. package/.next/static/chunks/app/api/plugins/route-f2d93969c0bcf406.js +1 -0
  134. package/.next/static/chunks/app/api/prompts/route-f2d93969c0bcf406.js +1 -0
  135. package/.next/static/chunks/app/api/remote/route-f2d93969c0bcf406.js +1 -0
  136. package/.next/static/chunks/app/api/security/route-f2d93969c0bcf406.js +1 -0
  137. package/.next/static/chunks/app/api/sessions/[id]/export/route-f2d93969c0bcf406.js +1 -0
  138. package/.next/static/chunks/app/api/sessions/[id]/route-f2d93969c0bcf406.js +1 -0
  139. package/.next/static/chunks/app/api/sessions/import/route-f2d93969c0bcf406.js +1 -0
  140. package/.next/static/chunks/app/api/sessions/route-f2d93969c0bcf406.js +1 -0
  141. package/.next/static/chunks/app/api/skills/route-f2d93969c0bcf406.js +1 -0
  142. package/.next/static/chunks/app/api/update/route-f2d93969c0bcf406.js +1 -0
  143. package/.next/static/chunks/app/api/version/route-f2d93969c0bcf406.js +1 -0
  144. package/.next/static/chunks/app/api/web-auth/route-f2d93969c0bcf406.js +1 -0
  145. package/.next/static/chunks/app/api/workspaces/route-f2d93969c0bcf406.js +1 -0
  146. package/.next/static/chunks/app/page-71f2d3b3ed864b3d.js +1 -0
  147. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f2d93969c0bcf406.js +1 -0
  148. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f2d93969c0bcf406.js +1 -0
  149. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f2d93969c0bcf406.js +1 -0
  150. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f2d93969c0bcf406.js +1 -0
  151. package/.next/static/chunks/webpack-ffc09b5fcc8aaa6d.js +1 -0
  152. package/.next/static/css/014eb1f21414e615.css +1 -0
  153. package/.next/static/css/56b129a13637fcfd.css +1 -0
  154. package/.next/static/css/7e3caf797e4cb19a.css +1 -0
  155. package/.next/types/app/api/agent/{[id]/side-chat → resources}/route.ts +3 -3
  156. package/.next/types/routes.d.ts +2 -2
  157. package/.next/types/validator.ts +9 -9
  158. package/README.md +1 -1
  159. package/next.config.ts +1 -1
  160. package/package.json +9 -9
  161. package/scripts/apply-global.mjs +87 -0
  162. package/scripts/launcher.mjs +21 -0
  163. package/scripts/testing/prepare-sdk-tools.mjs +19 -0
  164. package/src/app/api/agent/new/route.ts +2 -1
  165. package/src/app/api/agent/resources/route.ts +18 -0
  166. package/src/app/api/models/route.ts +22 -1
  167. package/src/app/api/plugins/route.ts +3 -2
  168. package/src/app/api/prompts/route.ts +3 -2
  169. package/src/app/api/remote/route.ts +22 -4
  170. package/src/app/api/sessions/[id]/export/route.ts +3 -1
  171. package/src/app/api/skills/route.ts +3 -2
  172. package/src/app/globals.css +1002 -1765
  173. package/src/components/AppShell.tsx +128 -93
  174. package/src/components/ChatInput.tsx +177 -98
  175. package/src/components/ChatWindow.tsx +189 -56
  176. package/src/components/ContextMeter.tsx +33 -9
  177. package/src/components/CustomSelect.module.css +142 -0
  178. package/src/components/CustomSelect.tsx +157 -0
  179. package/src/components/DrawerPanel.tsx +1 -1
  180. package/src/components/ExtensionUI.tsx +9 -3
  181. package/src/components/FileViewer.tsx +24 -21
  182. package/src/components/GitPanel.tsx +35 -13
  183. package/src/components/GrowthTree.tsx +6 -2
  184. package/src/components/ImageViewer.tsx +72 -0
  185. package/src/components/MarkdownBlock.tsx +67 -96
  186. package/src/components/ModelSelector.tsx +36 -39
  187. package/src/components/PiMark.tsx +76 -41
  188. package/src/components/PreviewImage.tsx +33 -0
  189. package/src/components/ProjectPanel.tsx +8 -5
  190. package/src/components/PromptPanel.tsx +85 -20
  191. package/src/components/ProviderSetupModal.module.css +12 -11
  192. package/src/components/ProviderSetupModal.tsx +107 -36
  193. package/src/components/RemoteSection.tsx +53 -2
  194. package/src/components/SessionImportSection.tsx +30 -15
  195. package/src/components/SessionSidebar.tsx +72 -62
  196. package/src/components/SettingsPanel.tsx +49 -151
  197. package/src/components/TrajectoryAnalyzer.module.css +26 -11
  198. package/src/components/TrajectoryView.tsx +11 -3
  199. package/src/components/chat/markdown-actions.tsx +23 -0
  200. package/src/components/composer/ComposerFiles.tsx +224 -0
  201. package/src/components/composer/ComposerImages.tsx +59 -127
  202. package/src/hooks/usePiWeb.ts +47 -2
  203. package/src/i18n.tsx +62 -80
  204. package/src/lib/agent-manager.ts +365 -33
  205. package/src/lib/chat/file-path.ts +54 -0
  206. package/src/lib/chat/skill-expansion.ts +51 -0
  207. package/src/lib/extension-ui.ts +25 -1
  208. package/src/lib/font.ts +37 -39
  209. package/src/lib/git-service.ts +4 -2
  210. package/src/lib/growth-turns.ts +3 -1
  211. package/src/lib/model-draft.ts +69 -2
  212. package/src/lib/models-service.ts +53 -3
  213. package/src/lib/pi.ts +2 -2
  214. package/src/lib/remote/assets.json +1 -1
  215. package/src/lib/remote/supervisor.ts +48 -5
  216. package/src/lib/security-service.ts +2 -1
  217. package/src/lib/session-reader.ts +25 -13
  218. package/src/lib/session-retention.ts +11 -8
  219. package/src/lib/skills-service.ts +2 -1
  220. package/src/lib/slash-command-token.ts +14 -0
  221. package/src/lib/trajectory.ts +111 -15
  222. package/src/lib/types.ts +28 -3
  223. package/src/lib/update-service.ts +29 -9
  224. package/src/lib/workspace-store.ts +36 -1
  225. package/.next/server/app/api/agent/[id]/side-chat/route.js +0 -5
  226. package/.next/server/app/api/agent/[id]/side-chat/route.js.nft.json +0 -1
  227. package/.next/server/app/api/agent/[id]/side-chat/route_client-reference-manifest.js +0 -1
  228. package/.next/server/chunks/3421.js +0 -1
  229. package/.next/static/chunks/2716.44fdf1b21454b961.js +0 -1
  230. package/.next/static/chunks/4673.e261707dce391be7.js +0 -1
  231. package/.next/static/chunks/4686-0cd87513f9ea3538.js +0 -1
  232. package/.next/static/chunks/4778.eddd3a5813f85824.js +0 -1
  233. package/.next/static/chunks/7411.5f79671208209b13.js +0 -1
  234. package/.next/static/chunks/8552.25c39ef4c995eba0.js +0 -1
  235. package/.next/static/chunks/app/_global-error/page-cbaaee0d7aacb85a.js +0 -1
  236. package/.next/static/chunks/app/api/agent/[id]/events/route-cbaaee0d7aacb85a.js +0 -1
  237. package/.next/static/chunks/app/api/agent/[id]/route-cbaaee0d7aacb85a.js +0 -1
  238. package/.next/static/chunks/app/api/agent/[id]/side-chat/route-cbaaee0d7aacb85a.js +0 -1
  239. package/.next/static/chunks/app/api/agent/[id]/tree/route-cbaaee0d7aacb85a.js +0 -1
  240. package/.next/static/chunks/app/api/agent/new/route-cbaaee0d7aacb85a.js +0 -1
  241. package/.next/static/chunks/app/api/config/route-cbaaee0d7aacb85a.js +0 -1
  242. package/.next/static/chunks/app/api/files/route-cbaaee0d7aacb85a.js +0 -1
  243. package/.next/static/chunks/app/api/fs/route-cbaaee0d7aacb85a.js +0 -1
  244. package/.next/static/chunks/app/api/git/route-cbaaee0d7aacb85a.js +0 -1
  245. package/.next/static/chunks/app/api/github/route-cbaaee0d7aacb85a.js +0 -1
  246. package/.next/static/chunks/app/api/growth/route-cbaaee0d7aacb85a.js +0 -1
  247. package/.next/static/chunks/app/api/health/route-cbaaee0d7aacb85a.js +0 -1
  248. package/.next/static/chunks/app/api/models/defaults/route-cbaaee0d7aacb85a.js +0 -1
  249. package/.next/static/chunks/app/api/models/route-cbaaee0d7aacb85a.js +0 -1
  250. package/.next/static/chunks/app/api/pi-settings/route-cbaaee0d7aacb85a.js +0 -1
  251. package/.next/static/chunks/app/api/plugins/route-cbaaee0d7aacb85a.js +0 -1
  252. package/.next/static/chunks/app/api/prompts/route-cbaaee0d7aacb85a.js +0 -1
  253. package/.next/static/chunks/app/api/remote/route-cbaaee0d7aacb85a.js +0 -1
  254. package/.next/static/chunks/app/api/security/route-cbaaee0d7aacb85a.js +0 -1
  255. package/.next/static/chunks/app/api/sessions/[id]/export/route-cbaaee0d7aacb85a.js +0 -1
  256. package/.next/static/chunks/app/api/sessions/[id]/route-cbaaee0d7aacb85a.js +0 -1
  257. package/.next/static/chunks/app/api/sessions/import/route-cbaaee0d7aacb85a.js +0 -1
  258. package/.next/static/chunks/app/api/sessions/route-cbaaee0d7aacb85a.js +0 -1
  259. package/.next/static/chunks/app/api/skills/route-cbaaee0d7aacb85a.js +0 -1
  260. package/.next/static/chunks/app/api/update/route-cbaaee0d7aacb85a.js +0 -1
  261. package/.next/static/chunks/app/api/version/route-cbaaee0d7aacb85a.js +0 -1
  262. package/.next/static/chunks/app/api/web-auth/route-cbaaee0d7aacb85a.js +0 -1
  263. package/.next/static/chunks/app/api/workspaces/route-cbaaee0d7aacb85a.js +0 -1
  264. package/.next/static/chunks/app/page-dc3cb41d91c7432e.js +0 -1
  265. package/.next/static/chunks/next/dist/client/components/builtin/app-error-cbaaee0d7aacb85a.js +0 -1
  266. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-cbaaee0d7aacb85a.js +0 -1
  267. package/.next/static/chunks/next/dist/client/components/builtin/not-found-cbaaee0d7aacb85a.js +0 -1
  268. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-cbaaee0d7aacb85a.js +0 -1
  269. package/.next/static/chunks/webpack-2ac3d76b81895f5b.js +0 -1
  270. package/.next/static/css/08815507594e8b8d.css +0 -1
  271. package/.next/static/css/9fc559c2c4ad1d0f.css +0 -1
  272. package/.next/static/css/c6e2227c36ec2fd6.css +0 -1
  273. package/.next/static/css/f15a853af8b5127f.css +0 -1
  274. package/.next/static/lvKCJmjFj8LuqSojRVLRF/_buildManifest.js +0 -1
  275. package/src/app/api/agent/[id]/side-chat/route.ts +0 -77
  276. package/src/components/side-chat/SideChatPanel.module.css +0 -131
  277. package/src/components/side-chat/SideChatPanel.tsx +0 -189
  278. package/src/components/side-chat/icons.tsx +0 -8
  279. package/src/components/side-chat/request-history.ts +0 -29
  280. package/src/components/side-chat/store.ts +0 -248
  281. package/src/lib/chat/stream-settings.ts +0 -192
  282. package/src/lib/side-chat/context.ts +0 -123
  283. package/src/lib/side-chat/service.ts +0 -191
  284. package/src/lib/side-chat/types.ts +0 -61
  285. /package/.next/static/{lvKCJmjFj8LuqSojRVLRF → QJCtqgDKzGS2BdKABd9ch}/_ssgManifest.js +0 -0
@@ -0,0 +1,224 @@
1
+ "use client";
2
+
3
+ /**
4
+ * 输入卡里上传的文件列表(多文件牌堆与竖直向上弧度展开):
5
+ * - 不超过 COLLAPSE_AT 个:平铺文件 chip。
6
+ * - 超过时折叠成牌堆:第一张在最上,后面露 4 层边,显示「展开 N」;
7
+ * - 点「展开」沿竖直向上呈优美弧度扇开,每张都可以单独删除或悬停预览,
8
+ * 点击「收起」平滑收回牌堆;与图片牌堆/扇形拥有一致的动效与交互质感。
9
+ */
10
+ import { useEffect, useRef, useState } from "react";
11
+ import { IconFileOutline16 } from "@/components/icons";
12
+ import { useI18n } from "@/i18n";
13
+ import { fmtBytes } from "@/lib/format";
14
+
15
+ export const COLLAPSE_AT = 4;
16
+ const FILE_CARD_W = 196;
17
+ const FILE_CARD_H = 32;
18
+ const DECK_PEEK = 4;
19
+ const DECK_LAYERS = 4;
20
+ const FAN_SIDE = 6;
21
+
22
+ export interface FileCardPose {
23
+ x: number;
24
+ y: number;
25
+ angle: number;
26
+ hidden?: boolean;
27
+ }
28
+
29
+ interface FileLayout {
30
+ cards: FileCardPose[];
31
+ width: number;
32
+ height: number;
33
+ gap: number;
34
+ }
35
+
36
+ const round = (value: number) => Math.round(value * 10) / 10;
37
+ const fmtUploadSize = (bytes?: number) => (bytes ? fmtBytes(bytes) : "");
38
+
39
+ export function fileDeckLayout(count: number): FileLayout {
40
+ const cards = Array.from({ length: count }, (_, i) => ({
41
+ x: Math.min(i, DECK_LAYERS) * DECK_PEEK,
42
+ y: 0,
43
+ angle: 0,
44
+ hidden: i >= DECK_LAYERS,
45
+ }));
46
+ return {
47
+ cards,
48
+ width: FILE_CARD_W + Math.min(Math.max(count - 1, 0), DECK_LAYERS) * DECK_PEEK,
49
+ height: FILE_CARD_H,
50
+ gap: DECK_PEEK,
51
+ };
52
+ }
53
+
54
+ export function fileFanLayout(count: number, width: number): FileLayout {
55
+ if (count <= 1) {
56
+ return { cards: [{ x: FAN_SIDE, y: 0, angle: 0 }], width: FILE_CARD_W + FAN_SIDE * 2, height: FILE_CARD_H, gap: 0 };
57
+ }
58
+ // 垂直每张抬升步长:自适应压窄,保证上方不顶出视口
59
+ const stepY = Math.min(32, Math.max(16, 220 / (count - 1)));
60
+ const spanY = (count - 1) * stepY;
61
+ const totalH = FILE_CARD_H + spanY + 14;
62
+ const cards = Array.from({ length: count }, (_, i) => {
63
+ const progress = i / (count - 1);
64
+ // y 轴:第 0 张在底部,最后一张在最上方
65
+ const y = round((totalH - FILE_CARD_H - 6) - i * stepY);
66
+ // x 轴:向上升起时呈优雅的弧度向右轻微弯出
67
+ const arcX = 18 * Math.sin(progress * Math.PI);
68
+ const x = round(FAN_SIDE + arcX + progress * 10);
69
+ // 角度:从下方的 -3° 顺滑过渡到上方的 +5°
70
+ const angle = round(-3 + progress * 8);
71
+ return { x, y, angle };
72
+ });
73
+ const maxX = Math.max(...cards.map((c) => c.x)) + FILE_CARD_W + FAN_SIDE;
74
+ return {
75
+ cards,
76
+ width: Math.min(width, Math.max(FILE_CARD_W + 32, Math.ceil(maxX))),
77
+ height: Math.ceil(totalH),
78
+ gap: round(stepY),
79
+ };
80
+ }
81
+
82
+ export interface UploadFileItem {
83
+ name: string;
84
+ path: string;
85
+ size?: number;
86
+ }
87
+
88
+ export function ComposerFiles({
89
+ files,
90
+ disabled,
91
+ onRemove,
92
+ onClear,
93
+ }: {
94
+ files: UploadFileItem[];
95
+ disabled?: boolean;
96
+ onRemove: (index: number) => void;
97
+ onClear?: () => void;
98
+ }) {
99
+ const { t } = useI18n();
100
+ const collapsible = files.length > COLLAPSE_AT;
101
+ const [expanded, setExpanded] = useState(false);
102
+ const fanned = collapsible && expanded;
103
+ const [lifted, setLifted] = useState<number | null>(null);
104
+ const railRef = useRef<HTMLDivElement>(null);
105
+ const [railWidth, setRailWidth] = useState(360);
106
+
107
+ useEffect(() => {
108
+ const rail = railRef.current;
109
+ if (!rail || !collapsible) return;
110
+ const measure = () => setRailWidth(Math.max(280, rail.clientWidth - 8));
111
+ measure();
112
+ if (typeof ResizeObserver === "undefined") return;
113
+ const observer = new ResizeObserver(measure);
114
+ observer.observe(rail);
115
+ return () => observer.disconnect();
116
+ }, [collapsible]);
117
+
118
+ const layout = fanned ? fileFanLayout(files.length, railWidth) : fileDeckLayout(files.length);
119
+
120
+ const renderFileCard = (f: UploadFileItem, i: number, pose?: FileCardPose, isDeck = false) => {
121
+ const up = fanned && lifted === i;
122
+ const delay = Math.min(fanned ? i * 14 : (files.length - 1 - i) * 10, 140);
123
+ const isHidden = !fanned && isDeck && i >= DECK_LAYERS;
124
+ if (isHidden) return null;
125
+
126
+ return (
127
+ <div
128
+ key={`${f.path}:${i}`}
129
+ className="pw-ref-chip group/ref pw-file-card"
130
+ title={f.path}
131
+ data-lifted={up || undefined}
132
+ style={
133
+ pose
134
+ ? {
135
+ position: "absolute",
136
+ left: 0,
137
+ top: 0,
138
+ width: FILE_CARD_W,
139
+ height: FILE_CARD_H,
140
+ zIndex: up ? files.length + 2 : fanned ? i + 1 : files.length - i,
141
+ transform: `translate(${pose.x}px, ${pose.y - (up ? 4 : 0)}px) rotate(${pose.angle}deg) ${up ? "scale(1.04)" : ""}`,
142
+ transitionDelay: up || lifted !== null ? "0ms" : `${delay}ms`,
143
+ }
144
+ : undefined
145
+ }
146
+ onMouseEnter={() => fanned && setLifted(i)}
147
+ onMouseLeave={() => setLifted((cur) => (cur === i ? null : cur))}
148
+ onFocus={() => fanned && setLifted(i)}
149
+ onBlur={(e) => {
150
+ if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setLifted((cur) => (cur === i ? null : cur));
151
+ }}
152
+ >
153
+ <IconFileOutline16 size={12} style={{ flex: "none", color: "var(--dsw-label-tertiary)" }} />
154
+ <span className="max-w-[110px] truncate font-mono text-[12px]">{f.name}</span>
155
+ <span style={{ flex: "none", fontSize: 10.5, color: "var(--dsw-label-caption)" }}>{fmtUploadSize(f.size)}</span>
156
+ <button
157
+ type="button"
158
+ className="pw-ref-close"
159
+ disabled={disabled}
160
+ onClick={(e) => {
161
+ e.stopPropagation();
162
+ onRemove(i);
163
+ setLifted(null);
164
+ }}
165
+ aria-label={`${t.delete} ${f.name}`}
166
+ >
167
+ ✕
168
+ </button>
169
+ </div>
170
+ );
171
+ };
172
+
173
+ return (
174
+ <div ref={railRef} className="pointer-events-auto flex flex-wrap items-end gap-2 px-1 pb-1" data-testid="composer-uploads">
175
+ {collapsible ? (
176
+ <div
177
+ className="pw-file-fan-container relative flex items-center gap-2"
178
+ style={{
179
+ position: "relative",
180
+ width: fanned ? layout.width : layout.width + 8,
181
+ height: layout.height,
182
+ transition: "width 320ms cubic-bezier(0.2, 0.8, 0.2, 1), height 320ms cubic-bezier(0.2, 0.8, 0.2, 1)",
183
+ }}
184
+ >
185
+ {files.map((f, i) => renderFileCard(f, i, layout.cards[i], true))}
186
+ </div>
187
+ ) : (
188
+ files.map((f, i) => renderFileCard(f, i))
189
+ )}
190
+
191
+ <div className="flex items-center gap-1.5 self-end pb-0.5">
192
+ {collapsible && (
193
+ <button
194
+ type="button"
195
+ className="pw-deck-action-btn"
196
+ data-testid="uploads-toggle"
197
+ aria-expanded={expanded}
198
+ onClick={() => {
199
+ setExpanded((open) => !open);
200
+ setLifted(null);
201
+ }}
202
+ >
203
+ {expanded ? t.chipCollapse : t.chipExpandMore.replace("{n}", String(files.length - COLLAPSE_AT))}
204
+ </button>
205
+ )}
206
+
207
+ {files.length > 1 && onClear && (
208
+ <button
209
+ type="button"
210
+ className="pw-deck-action-btn pw-deck-action-clear"
211
+ data-testid="uploads-clear"
212
+ disabled={disabled}
213
+ onClick={() => {
214
+ onClear();
215
+ setLifted(null);
216
+ }}
217
+ >
218
+ {t.filesClearAll}
219
+ </button>
220
+ )}
221
+ </div>
222
+ </div>
223
+ );
224
+ }
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { useEffect, useRef, useState } from "react";
16
16
  import { createPortal } from "react-dom";
17
+ import { ImageViewer } from "@/components/ImageViewer";
17
18
  import { useI18n } from "@/i18n";
18
19
  import type { ImageAttachment } from "@/lib/types";
19
20
 
@@ -61,27 +62,35 @@ export function deckLayout(count: number): Layout {
61
62
  }
62
63
 
63
64
  /**
64
- * 扇形:卡片中心落在一段圆弧上(中间最高、两侧顺着弧下垂),每张转到切线方向;
65
- * 间距按可用宽度均分、不超过 FAN_GAP_MAX,从左边开始铺(离牌堆原来的位置最近)。
65
+ * 扇形:沿竖直向上逐级延伸呈弧度展开(与多文件完全同一套竖直向上延展体验)。
66
+ * 卡片从底部逐级向上攀升,每张卡片竖直排布并沿轻微弧线微幅偏转,悬停上浮高亮。
66
67
  */
67
68
  export function fanLayout(count: number, width: number): Layout {
68
- const gap = count > 1 ? Math.min(FAN_GAP_MAX, Math.max(8, (width - CARD - FAN_SIDE * 2) / (count - 1))) : 0;
69
- const step = gap / FAN_RADIUS;
70
- const mid = (count - 1) / 2;
71
- const edge = -mid * step;
72
- // 正方形转过 θ 后比原来高出(也宽出)的那一截,最外侧两张最大
73
- const corner = (CARD / 2) * (Math.abs(Math.sin(edge)) + Math.cos(edge) - 1);
74
- const top = FAN_LIFT + corner;
69
+ if (count <= 1) {
70
+ return { cards: [{ x: FAN_SIDE, y: 0, angle: 0 }], width: CARD + FAN_SIDE * 2, height: CARD, gap: 0 };
71
+ }
72
+ // 竖直向上延伸:每张卡片向上抬升 stepY
73
+ const stepY = Math.min(36, Math.max(16, 210 / (count - 1)));
74
+ const spanY = (count - 1) * stepY;
75
+ const totalH = CARD + spanY + 14;
75
76
  const cards = Array.from({ length: count }, (_, i) => {
76
- const theta = (i - mid) * step;
77
- return {
78
- x: round(FAN_SIDE + FAN_RADIUS * (Math.sin(theta) - Math.sin(edge))),
79
- y: round(top + FAN_RADIUS * (1 - Math.cos(theta))),
80
- angle: round((theta * 180) / Math.PI),
81
- };
77
+ const progress = i / (count - 1);
78
+ // y 轴:Card 0 在底部,Card count-1 在最上方(竖直向上延伸)
79
+ const y = round((totalH - CARD - 6) - i * stepY);
80
+ // x 轴:向上延伸的同时呈柔和弧线向右微弯再回收(优雅的 C 弧)
81
+ const arcX = 22 * Math.sin(progress * Math.PI);
82
+ const x = round(FAN_SIDE + arcX + progress * 8);
83
+ // 角度:从下方的 -4° 顺滑过渡到上方的 +6°
84
+ const angle = round(-4 + progress * 10);
85
+ return { x, y, angle };
82
86
  });
83
- const span = cards.length ? cards[cards.length - 1].x + CARD + FAN_SIDE : 0;
84
- return { cards, width: Math.ceil(span), height: Math.ceil(top + FAN_RADIUS * (1 - Math.cos(edge)) + CARD + corner), gap: round(gap) };
87
+ const maxX = Math.max(...cards.map((c) => c.x)) + CARD + FAN_SIDE;
88
+ return {
89
+ cards,
90
+ width: Math.min(width, Math.max(CARD + 32, Math.ceil(maxX))),
91
+ height: Math.ceil(totalH),
92
+ gap: round(stepY),
93
+ };
85
94
  }
86
95
 
87
96
  /** 每张图一个稳定的 key:前几位 base64 对同格式的图几乎都一样(PNG 都是 iVBORw0KGgo…),不能拿来当 key */
@@ -140,7 +149,7 @@ export function ComposerImages({ images, disabled, onRemove, onClear }: { images
140
149
 
141
150
  return (
142
151
  <>
143
- <div ref={railRef} className="pointer-events-auto flex flex-wrap items-center gap-2 px-1" data-testid="composer-images">
152
+ <div ref={railRef} className="pointer-events-auto flex flex-wrap items-end gap-2 px-1 pb-1" data-testid="composer-images">
144
153
  {collapsible ? (
145
154
  <div
146
155
  className="pw-image-fan"
@@ -169,7 +178,7 @@ export function ComposerImages({ images, disabled, onRemove, onClear }: { images
169
178
  data-lifted={up || undefined}
170
179
  style={{
171
180
  zIndex: up ? images.length + 1 : images.length - i,
172
- transform: `translate(${pose.x}px, ${pose.y - (up ? FAN_LIFT : 0)}px) rotate(${pose.angle}deg)`,
181
+ transform: `translate(${pose.x}px, ${pose.y - (up ? FAN_LIFT : 0)}px) rotate(${pose.angle}deg) ${up ? "scale(1.05)" : ""}`,
173
182
  transitionDelay: up || lifted !== null ? "0ms" : `${delay}ms`,
174
183
  }}
175
184
  onMouseEnter={() => fanned && setLifted(i)}
@@ -206,35 +215,37 @@ export function ComposerImages({ images, disabled, onRemove, onClear }: { images
206
215
  </div>
207
216
  ))
208
217
  )}
209
- {images.length > 1 && onClear ? (
210
- <button
211
- type="button"
212
- className="pw-chip"
213
- data-testid="images-clear"
214
- disabled={disabled}
215
- onClick={() => {
216
- onClear();
217
- setLifted(null);
218
- setPreview(null);
219
- }}
220
- >
221
- {t.imageClearAll}
222
- </button>
223
- ) : null}
224
- {collapsible ? (
225
- <button
226
- type="button"
227
- className="pw-chip"
228
- data-testid="images-toggle"
229
- aria-expanded={fanned}
230
- onClick={() => {
231
- setExpanded((open) => !open);
232
- setLifted(null);
233
- }}
234
- >
235
- {fanned ? t.chipCollapse : t.chipExpandMore.replace("{n}", String(images.length - 1))}
236
- </button>
237
- ) : null}
218
+ <div className="flex items-center gap-1.5 self-end pb-0.5">
219
+ {collapsible ? (
220
+ <button
221
+ type="button"
222
+ className="pw-deck-action-btn"
223
+ data-testid="images-toggle"
224
+ aria-expanded={fanned}
225
+ onClick={() => {
226
+ setExpanded((open) => !open);
227
+ setLifted(null);
228
+ }}
229
+ >
230
+ {fanned ? t.chipCollapse : t.chipExpandMore.replace("{n}", String(images.length - 1))}
231
+ </button>
232
+ ) : null}
233
+ {images.length > 1 && onClear ? (
234
+ <button
235
+ type="button"
236
+ className="pw-deck-action-btn pw-deck-action-clear"
237
+ data-testid="images-clear"
238
+ disabled={disabled}
239
+ onClick={() => {
240
+ onClear();
241
+ setLifted(null);
242
+ setPreview(null);
243
+ }}
244
+ >
245
+ {t.imageClearAll}
246
+ </button>
247
+ ) : null}
248
+ </div>
238
249
  </div>
239
250
  {shown !== null
240
251
  ? createPortal(
@@ -245,82 +256,3 @@ export function ComposerImages({ images, disabled, onRemove, onClear }: { images
245
256
  </>
246
257
  );
247
258
  }
248
-
249
- /** 放大预览:手机上没法用本机程序打开图片,应用内看最实在 */
250
- function ImageViewer({
251
- images,
252
- index,
253
- disabled,
254
- onIndex,
255
- onRemove,
256
- onClose,
257
- }: {
258
- images: ImageAttachment[];
259
- index: number;
260
- disabled?: boolean;
261
- onIndex: (index: number) => void;
262
- onRemove: (index: number) => void;
263
- onClose: () => void;
264
- }) {
265
- const { t } = useI18n();
266
- const dialogRef = useRef<HTMLDivElement>(null);
267
- const closeRef = useRef<HTMLButtonElement>(null);
268
- const count = images.length;
269
- const go = (delta: number) => onIndex((index + delta + count) % count);
270
-
271
- // 打开时把焦点移进来,关掉时还给原来那个元素(还在页面上的话)
272
- useEffect(() => {
273
- const opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
274
- closeRef.current?.focus();
275
- return () => {
276
- if (opener?.isConnected) opener.focus();
277
- };
278
- }, []);
279
-
280
- // 捕获阶段处理:Esc / 方向键不再传给页面上其它监听 document 的面板;Tab 只在预览里转
281
- useEffect(() => {
282
- const onKey = (event: KeyboardEvent) => {
283
- if (event.key === "Escape") onClose();
284
- else if (event.key === "ArrowLeft" && count > 1) go(-1);
285
- else if (event.key === "ArrowRight" && count > 1) go(1);
286
- else if (event.key === "Tab") {
287
- const focusable = [...(dialogRef.current?.querySelectorAll<HTMLElement>("button:not(:disabled)") ?? [])];
288
- if (!focusable.length) return;
289
- const at = focusable.indexOf(document.activeElement as HTMLElement);
290
- const next = event.shiftKey ? (at <= 0 ? focusable.length - 1 : at - 1) : at === focusable.length - 1 ? 0 : at + 1;
291
- focusable[next].focus();
292
- } else return;
293
- event.preventDefault();
294
- event.stopPropagation();
295
- };
296
- document.addEventListener("keydown", onKey, true);
297
- return () => document.removeEventListener("keydown", onKey, true);
298
- // onClose / onIndex 来自父组件的 setState 包装,语义不变;只随张数和当前张重绑
299
- // eslint-disable-next-line react-hooks/exhaustive-deps
300
- }, [count, index]);
301
-
302
- return (
303
- <div ref={dialogRef} className="pw-lightbox" role="dialog" aria-modal="true" aria-label={t.imageViewer} onClick={onClose}>
304
- <div className="pw-lightbox-bar" onClick={(event) => event.stopPropagation()}>
305
- <span className="pw-lightbox-count">{count > 1 ? `${index + 1} / ${count}` : ""}</span>
306
- <button type="button" className="pw-lightbox-btn" disabled={disabled} onClick={() => onRemove(index)}>
307
- {t.delete}
308
- </button>
309
- <button ref={closeRef} type="button" className="pw-lightbox-btn" aria-label={t.close} onClick={onClose}>
310
- ✕
311
- </button>
312
- </div>
313
- <img src={srcOf(images[index])} alt="" className="pw-lightbox-image" onClick={(event) => event.stopPropagation()} />
314
- {count > 1 ? (
315
- <>
316
- <button type="button" className="pw-lightbox-nav" data-side="prev" aria-label={t.imagePrev} onClick={(event) => { event.stopPropagation(); go(-1); }}>
317
- ‹
318
- </button>
319
- <button type="button" className="pw-lightbox-nav" data-side="next" aria-label={t.imageNext} onClick={(event) => { event.stopPropagation(); go(1); }}>
320
- ›
321
- </button>
322
- </>
323
- ) : null}
324
- </div>
325
- );
326
- }
@@ -44,6 +44,17 @@ export interface ExtensionDialog {
44
44
  ts: number;
45
45
  }
46
46
 
47
+ /** 右下角的短通知:扩展 notify,或 PiWeb 自己的提示(带 code,渲染时按语言取文案) */
48
+ export interface PiWebNotice {
49
+ id: string;
50
+ message: string;
51
+ type: "info" | "warning" | "error";
52
+ ts: number;
53
+ /** external_reload:终端写了这个会话,已从磁盘重载;external_write:正在跑时终端也在写 */
54
+ code?: "external_reload" | "external_write";
55
+ count?: number;
56
+ }
57
+
47
58
  export interface PiWebState {
48
59
  snapshot: WebSnapshot | null;
49
60
  messages: WebMessage[];
@@ -57,7 +68,7 @@ export interface PiWebState {
57
68
  /** 扩展对话框队列(按到达顺序,一次显示一个) */
58
69
  extensionDialogs: ExtensionDialog[];
59
70
  /** 扩展通知(notify),显示几秒后消失 */
60
- extensionNotices: { id: string; message: string; type: "info" | "warning" | "error"; ts: number }[];
71
+ extensionNotices: PiWebNotice[];
61
72
  /** 扩展设的页脚状态文本(setStatus) */
62
73
  extensionStatuses: Record<string, string>;
63
74
  /** 扩展覆盖的「工作中」文案(setWorkingMessage) */
@@ -176,6 +187,17 @@ export const foldPiWebEvent = (state: PiWebState, evt: WebEvent): PiWebState =>
176
187
  return s;
177
188
  case "compaction":
178
189
  s.compaction = { phase: evt.phase, errorMessage: evt.errorMessage, ts: evt.ts };
190
+ if (evt.marker) {
191
+ // 已显示的消息全部保留;分隔卡插到保留下来的第一条前面,和刷新后的快照位置一致
192
+ const marker = evt.marker;
193
+ const msgs = s.messages.filter((message) => !(marker.id && message.id === marker.id));
194
+ const at = msgs.findIndex((message) =>
195
+ (!!evt.beforeId && message.id === evt.beforeId) ||
196
+ (!!evt.beforeStreamId && message.streamId === evt.beforeStreamId));
197
+ if (at >= 0) msgs.splice(at, 0, marker);
198
+ else msgs.push(marker);
199
+ s.messages = msgs;
200
+ }
179
201
  return s;
180
202
  case "traj": {
181
203
  if (s.snapshot) {
@@ -236,6 +258,12 @@ export const foldPiWebEvent = (state: PiWebState, evt: WebEvent): PiWebState =>
236
258
  case "extension_ui_resolved":
237
259
  s.extensionDialogs = s.extensionDialogs.filter((dialog) => dialog.id !== evt.id);
238
260
  return s;
261
+ case "external_write":
262
+ s.extensionNotices = [
263
+ ...s.extensionNotices.filter((notice) => notice.code !== "external_write").slice(-4),
264
+ { id: `external-write-${evt.ts}`, message: "", type: "warning", ts: evt.ts, code: "external_write", count: evt.entries },
265
+ ];
266
+ return s;
239
267
  case "status":
240
268
  if (s.snapshot) s.snapshot = { ...s.snapshot, isStreaming: evt.isStreaming };
241
269
  if (!evt.isStreaming) {
@@ -366,6 +394,7 @@ export function usePiWeb() {
366
394
  const [snapshotEpoch, setSnapshotEpoch] = useState(0);
367
395
  const esRef = useRef<{ close(): void } | null>(null);
368
396
  const promptRequestsRef = useRef(new Set<string>());
397
+ const shownReloadsRef = useRef(new Set<string>());
369
398
  const presetRestoreAttemptRef = useRef<string | null>(null);
370
399
  const presetRestorePromiseRef = useRef<Promise<void> | null>(null);
371
400
  const activeSessionRef = useRef(currentId);
@@ -689,6 +718,14 @@ export function usePiWeb() {
689
718
  : { ...emptyState(savedToolPreset()), connected: false });
690
719
  let retryDelayMs = 2000;
691
720
  const eventsUrl = `/api/agent/${currentId}/events`;
721
+ // 同一次重载既有 external_reload 事件、又会出现在随后若干快照里:按重载时间只提示一次
722
+ const reloadNotice = (reload?: { entries: number; at: number }): PiWebNotice | null => {
723
+ if (!reload) return null;
724
+ const key = `${currentId}:${reload.at}`;
725
+ if (shownReloadsRef.current.has(key)) return null;
726
+ shownReloadsRef.current.add(key);
727
+ return { id: `external-reload-${reload.at}`, message: "", type: "info", ts: Date.now(), code: "external_reload", count: reload.entries };
728
+ };
692
729
  const connect = () => {
693
730
  // 流式事件按帧合并:token 速率再高也只每 16ms 渲染一次(见 event-batcher)
694
731
  const batcher = createEventBatcher<WebEvent>({
@@ -707,11 +744,19 @@ export function usePiWeb() {
707
744
  // 单帧损坏(代理截断等)只丢弃该帧,不能让 EventSource 回调抛异常
708
745
  return;
709
746
  }
747
+ if (parsed.type === "external_reload") {
748
+ // 服务端发现终端写过这个会话、已从磁盘重载,旧流随即关闭:不等 EventSource 2 秒后自己重连,马上换新连接拿快照
749
+ const notice = reloadNotice(parsed);
750
+ if (notice) setState((s) => ({ ...s, extensionNotices: [...s.extensionNotices.slice(-4), notice] }));
751
+ setResyncNonce((n) => n + 1);
752
+ return;
753
+ }
710
754
  if (parsed.type === "snapshot" && parsed.snapshot) {
711
755
  // 快照是权威状态:之前缓冲的增量已经过时,直接丢弃
712
756
  batcher.cancel();
713
757
  retryDelayMs = 2000;
714
758
  const snap = parsed.snapshot as WebSnapshot;
759
+ const notice = reloadNotice(snap.externalReload);
715
760
  setSnapshotEpoch((epoch) => epoch + 1);
716
761
  setState((prev) => ({
717
762
  snapshot: snap,
@@ -724,7 +769,7 @@ export function usePiWeb() {
724
769
  retryNotice: null,
725
770
  // 服务器当前 pending 集合是权威来源,刷新可恢复、已答请求不能复活。
726
771
  extensionDialogs: extensionDialogsFromSnapshot(snap, prev.extensionDialogs),
727
- extensionNotices: prev.extensionNotices,
772
+ extensionNotices: notice ? [...prev.extensionNotices.slice(-4), notice] : prev.extensionNotices,
728
773
  extensionStatuses: snap.extensionStatuses ?? {},
729
774
  workingMessage: null,
730
775
  // 重连的快照不带生长账本;连接期间已收到的步保留,useGrowth 会按 seq 与拉取结果去重