@rexvane/piweb 0.3.21 → 0.3.23

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 (343) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +9 -6
  3. package/.next/build-manifest.json +5 -5
  4. package/.next/next-minimal-server.js.nft.json +1 -1
  5. package/.next/next-server.js.nft.json +1 -1
  6. package/.next/prerender-manifest.json +7 -7
  7. package/.next/react-loadable-manifest.json +18 -11
  8. package/.next/required-server-files.js +16 -1
  9. package/.next/required-server-files.json +16 -1
  10. package/.next/routes-manifest.json +20 -0
  11. package/.next/server/app/_global-error/page.js +2 -2
  12. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  13. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_global-error.html +1 -1
  15. package/.next/server/app/_global-error.meta +8 -0
  16. package/.next/server/app/_global-error.rsc +15 -11
  17. package/.next/server/app/_global-error.segments/_full.segment.rsc +15 -11
  18. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +20 -16
  19. package/.next/server/app/_global-error.segments/_tree.segment.rsc +3 -1
  20. package/.next/server/app/_not-found/page.js +1 -1
  21. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  22. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/_not-found.html +1 -1
  24. package/.next/server/app/_not-found.meta +8 -0
  25. package/.next/server/app/_not-found.rsc +17 -13
  26. package/.next/server/app/_not-found.segments/_full.segment.rsc +17 -13
  27. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +24 -20
  28. package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -2
  29. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  30. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  31. package/.next/server/app/api/agent/[id]/route.js +1 -1
  32. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  33. package/.next/server/app/api/agent/[id]/side-chat/route.js +5 -0
  34. package/.next/server/app/api/agent/[id]/side-chat/route.js.nft.json +1 -0
  35. package/.next/server/app/api/agent/[id]/side-chat/route_client-reference-manifest.js +1 -0
  36. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  37. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  38. package/.next/server/app/api/agent/new/route.js +1 -1
  39. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  40. package/.next/server/app/api/config/route.js +1 -1
  41. package/.next/server/app/api/config/route.js.nft.json +1 -1
  42. package/.next/server/app/api/files/route.js +1 -1
  43. package/.next/server/app/api/files/route.js.nft.json +1 -1
  44. package/.next/server/app/api/fs/route.js +1 -1
  45. package/.next/server/app/api/fs/route.js.nft.json +1 -1
  46. package/.next/server/app/api/git/route.js +4 -4
  47. package/.next/server/app/api/git/route.js.nft.json +1 -1
  48. package/.next/server/app/api/github/route.js +5 -0
  49. package/.next/server/app/api/github/route.js.nft.json +1 -0
  50. package/.next/server/app/api/github/route_client-reference-manifest.js +1 -0
  51. package/.next/server/app/api/growth/route.js +1 -1
  52. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  53. package/.next/server/app/api/health/route.js +1 -1
  54. package/.next/server/app/api/health/route.js.nft.json +1 -1
  55. package/.next/server/app/api/models/defaults/route.js +1 -0
  56. package/.next/server/app/api/models/defaults/route.js.nft.json +1 -0
  57. package/.next/server/app/api/models/defaults/route_client-reference-manifest.js +1 -0
  58. package/.next/server/app/api/models/route.js +1 -1
  59. package/.next/server/app/api/models/route.js.nft.json +1 -1
  60. package/.next/server/app/api/pi-settings/route.js +1 -1
  61. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  62. package/.next/server/app/api/plugins/route.js +1 -1
  63. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  64. package/.next/server/app/api/prompts/route.js +1 -1
  65. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  66. package/.next/server/app/api/remote/route.js +4 -4
  67. package/.next/server/app/api/remote/route.js.nft.json +1 -1
  68. package/.next/server/app/api/security/route.js +1 -1
  69. package/.next/server/app/api/security/route.js.nft.json +1 -1
  70. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  71. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  72. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  73. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  74. package/.next/server/app/api/sessions/import/route.js +2 -2
  75. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  76. package/.next/server/app/api/sessions/route.js +1 -1
  77. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  78. package/.next/server/app/api/skills/route.js +2 -2
  79. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  80. package/.next/server/app/api/update/route.js +2 -2
  81. package/.next/server/app/api/update/route.js.nft.json +1 -1
  82. package/.next/server/app/api/version/route.js +1 -1
  83. package/.next/server/app/api/version/route.js.nft.json +1 -1
  84. package/.next/server/app/api/web-auth/route.js +2 -2
  85. package/.next/server/app/api/web-auth/route.js.nft.json +1 -1
  86. package/.next/server/app/api/workspaces/route.js +1 -1
  87. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  88. package/.next/server/app/icon.svg/route.js +1 -1
  89. package/.next/server/app/icon.svg/route.js.nft.json +1 -1
  90. package/.next/server/app/icon.svg.meta +1 -1
  91. package/.next/server/app/index.html +1 -1
  92. package/.next/server/app/index.meta +8 -0
  93. package/.next/server/app/index.rsc +20 -13
  94. package/.next/server/app/index.segments/__PAGE__.segment.rsc +26 -19
  95. package/.next/server/app/index.segments/_full.segment.rsc +20 -13
  96. package/.next/server/app/index.segments/_tree.segment.rsc +4 -2
  97. package/.next/server/app/login/page.js +1 -1
  98. package/.next/server/app/login/page.js.nft.json +1 -1
  99. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  100. package/.next/server/app/login.html +1 -1
  101. package/.next/server/app/login.meta +8 -0
  102. package/.next/server/app/login.rsc +23 -17
  103. package/.next/server/app/login.segments/_full.segment.rsc +23 -17
  104. package/.next/server/app/login.segments/_tree.segment.rsc +4 -2
  105. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +30 -24
  106. package/.next/server/app/page.js +1 -1
  107. package/.next/server/app/page.js.nft.json +1 -1
  108. package/.next/server/app/page_client-reference-manifest.js +1 -1
  109. package/.next/server/app-paths-manifest.json +9 -6
  110. package/.next/server/chunks/1813.js +23 -0
  111. package/.next/server/chunks/2430.js +377 -0
  112. package/.next/server/chunks/2970.js +305 -0
  113. package/.next/server/chunks/3421.js +1 -0
  114. package/.next/server/chunks/3924.js +6 -0
  115. package/.next/server/chunks/4807.js +1 -0
  116. package/.next/server/chunks/538.js +1 -1
  117. package/.next/server/chunks/5393.js +1 -0
  118. package/.next/server/chunks/5802.js +2 -0
  119. package/.next/server/chunks/6229.js +1 -0
  120. package/.next/server/chunks/7496.js +1 -0
  121. package/.next/server/chunks/7662.js +2 -0
  122. package/.next/server/chunks/8686.js +1 -0
  123. package/.next/server/chunks/{593.js → 974.js} +1 -1
  124. package/.next/server/chunks/{920.js → 9920.js} +2 -2
  125. package/.next/server/functions-config-manifest.json +2 -1
  126. package/.next/server/instrumentation.js +1 -1
  127. package/.next/server/instrumentation.js.nft.json +1 -1
  128. package/.next/server/middleware-build-manifest.js +1 -1
  129. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  130. package/.next/server/middleware.js +185 -23
  131. package/.next/server/middleware.js.nft.json +1 -1
  132. package/.next/server/pages/404.html +1 -1
  133. package/.next/server/pages/500.html +1 -1
  134. package/.next/server/preview-props.json +5 -0
  135. package/.next/server/server-reference-manifest.json +1 -1
  136. package/.next/server/webpack-runtime.js +1 -1
  137. package/.next/static/chunks/2716.44fdf1b21454b961.js +1 -0
  138. package/.next/static/chunks/316-698d10eb465bd8ef.js +1 -0
  139. package/.next/static/chunks/3794-0978865cda773008.js +52 -0
  140. package/.next/static/chunks/4673.e261707dce391be7.js +1 -0
  141. package/.next/static/chunks/4686-0cd87513f9ea3538.js +1 -0
  142. package/.next/static/chunks/4778.eddd3a5813f85824.js +1 -0
  143. package/.next/static/chunks/4bd1b696-e199e788e58acf79.js +1 -0
  144. package/.next/static/chunks/{5173.91ddb803785009f8.js → 5173.dfd1cbb795b9a21c.js} +1 -1
  145. package/.next/static/chunks/618.949981da99be7182.js +4 -0
  146. package/.next/static/chunks/7411.5f79671208209b13.js +1 -0
  147. package/.next/static/chunks/app/_global-error/page-cbaaee0d7aacb85a.js +1 -0
  148. package/.next/static/chunks/app/api/agent/[id]/events/route-cbaaee0d7aacb85a.js +1 -0
  149. package/.next/static/chunks/app/api/agent/[id]/route-cbaaee0d7aacb85a.js +1 -0
  150. package/.next/static/chunks/app/api/agent/[id]/side-chat/route-cbaaee0d7aacb85a.js +1 -0
  151. package/.next/static/chunks/app/api/agent/[id]/tree/route-cbaaee0d7aacb85a.js +1 -0
  152. package/.next/static/chunks/app/api/agent/new/route-cbaaee0d7aacb85a.js +1 -0
  153. package/.next/static/chunks/app/api/config/route-cbaaee0d7aacb85a.js +1 -0
  154. package/.next/static/chunks/app/api/files/route-cbaaee0d7aacb85a.js +1 -0
  155. package/.next/static/chunks/app/api/fs/route-cbaaee0d7aacb85a.js +1 -0
  156. package/.next/static/chunks/app/api/git/route-cbaaee0d7aacb85a.js +1 -0
  157. package/.next/static/chunks/app/api/github/route-cbaaee0d7aacb85a.js +1 -0
  158. package/.next/static/chunks/app/api/growth/route-cbaaee0d7aacb85a.js +1 -0
  159. package/.next/static/chunks/app/api/health/route-cbaaee0d7aacb85a.js +1 -0
  160. package/.next/static/chunks/app/api/models/defaults/route-cbaaee0d7aacb85a.js +1 -0
  161. package/.next/static/chunks/app/api/models/route-cbaaee0d7aacb85a.js +1 -0
  162. package/.next/static/chunks/app/api/pi-settings/route-cbaaee0d7aacb85a.js +1 -0
  163. package/.next/static/chunks/app/api/plugins/route-cbaaee0d7aacb85a.js +1 -0
  164. package/.next/static/chunks/app/api/prompts/route-cbaaee0d7aacb85a.js +1 -0
  165. package/.next/static/chunks/app/api/remote/route-cbaaee0d7aacb85a.js +1 -0
  166. package/.next/static/chunks/app/api/security/route-cbaaee0d7aacb85a.js +1 -0
  167. package/.next/static/chunks/app/api/sessions/[id]/export/route-cbaaee0d7aacb85a.js +1 -0
  168. package/.next/static/chunks/app/api/sessions/[id]/route-cbaaee0d7aacb85a.js +1 -0
  169. package/.next/static/chunks/app/api/sessions/import/route-cbaaee0d7aacb85a.js +1 -0
  170. package/.next/static/chunks/app/api/sessions/route-cbaaee0d7aacb85a.js +1 -0
  171. package/.next/static/chunks/app/api/skills/route-cbaaee0d7aacb85a.js +1 -0
  172. package/.next/static/chunks/app/api/update/route-cbaaee0d7aacb85a.js +1 -0
  173. package/.next/static/chunks/app/api/version/route-cbaaee0d7aacb85a.js +1 -0
  174. package/.next/static/chunks/app/api/web-auth/route-cbaaee0d7aacb85a.js +1 -0
  175. package/.next/static/chunks/app/api/workspaces/route-cbaaee0d7aacb85a.js +1 -0
  176. package/.next/static/chunks/app/page-dc3cb41d91c7432e.js +1 -0
  177. package/.next/static/chunks/framework-47b723d27f4a4963.js +1 -0
  178. package/.next/static/chunks/main-5403f0764579e5fe.js +5 -0
  179. package/.next/static/chunks/next/dist/client/components/builtin/app-error-cbaaee0d7aacb85a.js +1 -0
  180. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-cbaaee0d7aacb85a.js +1 -0
  181. package/.next/static/chunks/next/dist/client/components/builtin/not-found-cbaaee0d7aacb85a.js +1 -0
  182. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-cbaaee0d7aacb85a.js +1 -0
  183. package/.next/static/chunks/webpack-2ac3d76b81895f5b.js +1 -0
  184. package/.next/static/css/9fc559c2c4ad1d0f.css +1 -0
  185. package/.next/static/css/c6e2227c36ec2fd6.css +1 -0
  186. package/.next/static/css/{2959bd6b626b7ffe.css → f15a853af8b5127f.css} +1 -1
  187. package/.next/static/hYPL1-dmhKFjLS3f5yoqN/_buildManifest.js +1 -0
  188. package/.next/types/app/api/agent/[id]/events/route.ts +3 -1
  189. package/.next/types/app/api/agent/[id]/route.ts +3 -1
  190. package/.next/types/app/api/agent/[id]/side-chat/route.ts +355 -0
  191. package/.next/types/app/api/agent/[id]/tree/route.ts +3 -1
  192. package/.next/types/app/api/agent/new/route.ts +3 -1
  193. package/.next/types/app/api/config/route.ts +3 -1
  194. package/.next/types/app/api/files/route.ts +3 -1
  195. package/.next/types/app/api/fs/route.ts +3 -1
  196. package/.next/types/app/api/git/route.ts +3 -1
  197. package/.next/types/app/api/github/route.ts +355 -0
  198. package/.next/types/app/api/growth/route.ts +3 -1
  199. package/.next/types/app/api/health/route.ts +3 -1
  200. package/.next/types/app/api/models/defaults/route.ts +355 -0
  201. package/.next/types/app/api/models/route.ts +3 -1
  202. package/.next/types/app/api/pi-settings/route.ts +3 -1
  203. package/.next/types/app/api/plugins/route.ts +3 -1
  204. package/.next/types/app/api/prompts/route.ts +3 -1
  205. package/.next/types/app/api/remote/route.ts +3 -1
  206. package/.next/types/app/api/security/route.ts +3 -1
  207. package/.next/types/app/api/sessions/[id]/export/route.ts +3 -1
  208. package/.next/types/app/api/sessions/[id]/route.ts +3 -1
  209. package/.next/types/app/api/sessions/import/route.ts +3 -1
  210. package/.next/types/app/api/sessions/route.ts +3 -1
  211. package/.next/types/app/api/skills/route.ts +3 -1
  212. package/.next/types/app/api/update/route.ts +3 -1
  213. package/.next/types/app/api/version/route.ts +3 -1
  214. package/.next/types/app/api/web-auth/route.ts +3 -1
  215. package/.next/types/app/api/workspaces/route.ts +3 -1
  216. package/.next/types/app/layout.ts +4 -1
  217. package/.next/types/app/login/page.ts +4 -1
  218. package/.next/types/app/page.ts +4 -1
  219. package/.next/types/cache-life.d.ts +11 -43
  220. package/.next/types/routes.d.ts +4 -1
  221. package/.next/types/validator.ts +86 -2
  222. package/next.config.ts +2 -0
  223. package/package.json +8 -2
  224. package/scripts/release.mjs +3 -7
  225. package/scripts/runtime/check-installed.mjs +66 -0
  226. package/scripts/runtime/pi-packages.mjs +51 -0
  227. package/scripts/verify-package.mjs +29 -7
  228. package/src/app/api/agent/[id]/side-chat/route.ts +77 -0
  229. package/src/app/api/files/route.ts +5 -2
  230. package/src/app/api/github/route.ts +26 -0
  231. package/src/app/api/models/defaults/route.ts +18 -0
  232. package/src/app/api/models/route.ts +24 -1
  233. package/src/app/globals.css +1748 -0
  234. package/src/components/AppShell.tsx +145 -43
  235. package/src/components/ChatInput.tsx +264 -79
  236. package/src/components/ChatWindow.tsx +13 -23
  237. package/src/components/ContextMeter.tsx +213 -144
  238. package/src/components/DrawerPanel.tsx +1 -1
  239. package/src/components/FileViewer.tsx +89 -8
  240. package/src/components/GitPanel.tsx +110 -40
  241. package/src/components/GrowthTree.tsx +30 -5
  242. package/src/components/ModelSelector.tsx +21 -5
  243. package/src/components/ProjectPanel.tsx +55 -30
  244. package/src/components/PromptPanel.tsx +2 -4
  245. package/src/components/ProviderSetupModal.module.css +20 -0
  246. package/src/components/ProviderSetupModal.tsx +134 -29
  247. package/src/components/SettingsPanel.tsx +252 -27
  248. package/src/components/TrajectoryView.tsx +4 -6
  249. package/src/components/chat/reading-anchor.ts +81 -0
  250. package/src/components/composer/ComposerImages.tsx +326 -0
  251. package/src/components/side-chat/SideChatPanel.module.css +131 -0
  252. package/src/components/side-chat/SideChatPanel.tsx +189 -0
  253. package/src/components/side-chat/icons.tsx +8 -0
  254. package/src/components/side-chat/request-history.ts +29 -0
  255. package/src/components/side-chat/store.ts +248 -0
  256. package/src/hooks/useFileViewer.ts +6 -4
  257. package/src/hooks/useGrowth.ts +3 -0
  258. package/src/hooks/usePiWeb.ts +12 -2
  259. package/src/hooks/useWorkspaceFiles.ts +93 -0
  260. package/src/i18n.tsx +150 -2
  261. package/src/lib/agent-manager.ts +135 -104
  262. package/src/lib/files-service.ts +166 -3
  263. package/src/lib/format.ts +56 -0
  264. package/src/lib/git-service.ts +45 -13
  265. package/src/lib/github-identity.ts +245 -0
  266. package/src/lib/growth-service.ts +6 -2
  267. package/src/lib/hero-model-memory.ts +28 -0
  268. package/src/lib/model-defaults.ts +105 -0
  269. package/src/lib/model-draft.ts +257 -9
  270. package/src/lib/model-endpoint.ts +23 -0
  271. package/src/lib/model-files.ts +33 -0
  272. package/src/lib/models-service.ts +240 -78
  273. package/src/lib/path-security.ts +24 -32
  274. package/src/lib/pi.ts +27 -2
  275. package/src/lib/process-format.ts +3 -2
  276. package/src/lib/session/metrics.ts +31 -0
  277. package/src/lib/session-reader.ts +1 -1
  278. package/src/lib/side-chat/context.ts +123 -0
  279. package/src/lib/side-chat/service.ts +191 -0
  280. package/src/lib/side-chat/types.ts +61 -0
  281. package/src/lib/thinking-level.ts +39 -0
  282. package/src/lib/trajectory.ts +17 -3
  283. package/src/lib/types.ts +12 -1
  284. package/src/lib/update-service.ts +4 -6
  285. package/src/lib/workspace-index.ts +270 -0
  286. package/.next/server/chunks/229.js +0 -1
  287. package/.next/server/chunks/393.js +0 -1
  288. package/.next/server/chunks/421.js +0 -1
  289. package/.next/server/chunks/430.js +0 -193
  290. package/.next/server/chunks/496.js +0 -1
  291. package/.next/server/chunks/662.js +0 -2
  292. package/.next/server/chunks/686.js +0 -1
  293. package/.next/server/chunks/741.js +0 -165
  294. package/.next/server/chunks/802.js +0 -2
  295. package/.next/server/chunks/807.js +0 -1
  296. package/.next/server/chunks/813.js +0 -1
  297. package/.next/server/chunks/924.js +0 -6
  298. package/.next/static/chunks/2716.7ad073038e94a802.js +0 -1
  299. package/.next/static/chunks/316-5ea443ee6b512aa9.js +0 -1
  300. package/.next/static/chunks/3794-3679f1c05160977c.js +0 -52
  301. package/.next/static/chunks/4673.8a5a9e6855350e34.js +0 -1
  302. package/.next/static/chunks/4686-882531aef1d2e250.js +0 -1
  303. package/.next/static/chunks/4778.305dc6abb9889627.js +0 -1
  304. package/.next/static/chunks/4bd1b696-93bf506b236d6248.js +0 -1
  305. package/.next/static/chunks/618.ee5d5bf1c647e483.js +0 -4
  306. package/.next/static/chunks/839.bfd2db36483faa3e.js +0 -1
  307. package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +0 -1
  308. package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +0 -1
  309. package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +0 -1
  310. package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +0 -1
  311. package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +0 -1
  312. package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +0 -1
  313. package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +0 -1
  314. package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +0 -1
  315. package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +0 -1
  316. package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +0 -1
  317. package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +0 -1
  318. package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +0 -1
  319. package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +0 -1
  320. package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +0 -1
  321. package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +0 -1
  322. package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +0 -1
  323. package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +0 -1
  324. package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +0 -1
  325. package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +0 -1
  326. package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +0 -1
  327. package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +0 -1
  328. package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +0 -1
  329. package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +0 -1
  330. package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +0 -1
  331. package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +0 -1
  332. package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +0 -1
  333. package/.next/static/chunks/app/page-c04af72e549695bd.js +0 -1
  334. package/.next/static/chunks/framework-c4e651fe89edfa91.js +0 -1
  335. package/.next/static/chunks/main-6159b22a8cb0b7c5.js +0 -5
  336. package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +0 -1
  337. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +0 -1
  338. package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +0 -1
  339. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +0 -1
  340. package/.next/static/chunks/webpack-9b4bb574e5817654.js +0 -1
  341. package/.next/static/css/ea27b9e30f76303f.css +0 -1
  342. package/.next/static/sNX5GQkOWuK5YEID1hT-C/_buildManifest.js +0 -1
  343. /package/.next/static/{sNX5GQkOWuK5YEID1hT-C → hYPL1-dmhKFjLS3f5yoqN}/_ssgManifest.js +0 -0
@@ -23,6 +23,7 @@ import {
23
23
  IconShieldOutline16,
24
24
  IconSkillOutline16,
25
25
  IconTrashOutline16,
26
+ IconWarningOutline16,
26
27
  } from "@/components/icons";
27
28
  import { ProviderBrand } from "@/components/ProviderBrand";
28
29
  import {
@@ -35,6 +36,7 @@ import { RemoteSection } from "@/components/RemoteSection";
35
36
  import { CopyCommand } from "@/components/update/CopyCommand";
36
37
  import { checkUpdate, loadUpdates, runUpdate, useUpdates, type UpdateTarget } from "@/components/update/update-store";
37
38
  import { useI18n } from "@/i18n";
39
+ import { fmtTokens } from "@/lib/format";
38
40
  import { applyPebrelTheme, loadPebrelTheme, loadThemeMode, type PebrelTheme, type ThemeMode } from "@/lib/theme";
39
41
  import { applyFontChoice, CUSTOM_PREFIX, FONT_PRESETS, loadFontChoice } from "@/lib/font";
40
42
  import {
@@ -47,7 +49,22 @@ import {
47
49
  type StreamSettings,
48
50
  type StreamStyle,
49
51
  } from "@/lib/chat/stream-settings";
50
- import { modelDraftFromConfig, serializeProviderDraft, validateModelDrafts, type ModelDraft } from "@/lib/model-draft";
52
+ import {
53
+ customModelGaps,
54
+ fillModelFields,
55
+ formatCapacity,
56
+ isInertProviderConfig,
57
+ modelDraftFromCatalog,
58
+ modelDraftFromConfig,
59
+ modelDraftsFromProviderConfig,
60
+ parseCapacity,
61
+ PI_MODEL_FALLBACKS,
62
+ serializeProviderDraft,
63
+ validateModelDrafts,
64
+ type CustomModelGap,
65
+ type ModelDraft,
66
+ type ModelFieldFill,
67
+ } from "@/lib/model-draft";
51
68
  import { customApiOptions } from "@/lib/provider-display";
52
69
  import { quotaWindowKind, supportsUsageProbe, type BalanceAmount, type BalanceDetailKey, type ProviderUsage, type QuotaWindow, type XaiUsage } from "@/lib/provider-usage";
53
70
  import type { ProviderView } from "@/lib/models-service";
@@ -871,12 +888,167 @@ function FontFamilyPicker({ scope }: { scope: "ui" | "mono" }) {
871
888
 
872
889
  // ---------- Models ----------
873
890
 
891
+ type AuditRow = { contextWindow: string; maxTokens: string; vision: "" | "image" | "text" };
892
+ const EMPTY_AUDIT_ROW: AuditRow = { contextWindow: "", maxTokens: "", vision: "" };
893
+
894
+ /**
895
+ * 模型配置体检:models[] 里有字段没写的模型,pi 会按 PI_MODEL_FALLBACKS / 纯文本处理。
896
+ * 不替用户猜——空着的格子由用户填(支持 256K / 1M 写法),保存时只补空字段,已写的值不改。
897
+ */
898
+ function ModelFieldAudit({ gaps, providerLabel, busy, onSave }: {
899
+ gaps: CustomModelGap[];
900
+ providerLabel: (id: string) => string;
901
+ busy: boolean;
902
+ /** 保存成功返回 true(之后清空填写) */
903
+ onSave: (fills: ModelFieldFill[]) => Promise<boolean>;
904
+ }) {
905
+ const { t } = useI18n();
906
+ const [rows, setRows] = useState<Record<string, AuditRow>>({});
907
+ const keyOf = (gap: CustomModelGap) => `${gap.provider}\u0000${gap.index}`;
908
+ const rowOf = (gap: CustomModelGap) => rows[keyOf(gap)] ?? EMPTY_AUDIT_ROW;
909
+ const edit = (gap: CustomModelGap, patch: Partial<AuditRow>) =>
910
+ setRows((current) => ({ ...current, [keyOf(gap)]: { ...(current[keyOf(gap)] ?? EMPTY_AUDIT_ROW), ...patch } }));
911
+ const visionGaps = gaps.filter((gap) => gap.missing.includes("input"));
912
+ const markVision = (vision: "image" | "text") =>
913
+ setRows((current) => {
914
+ const next = { ...current };
915
+ for (const gap of visionGaps) next[keyOf(gap)] = { ...(next[keyOf(gap)] ?? EMPTY_AUDIT_ROW), vision };
916
+ return next;
917
+ });
918
+
919
+ // 每行填的文本 → 要写的值;有一格格式不对就不让保存(那一格标红)
920
+ let invalid = false;
921
+ const fills: ModelFieldFill[] = [];
922
+ for (const gap of gaps) {
923
+ const row = rows[keyOf(gap)];
924
+ if (!row) continue;
925
+ const contextWindow = gap.missing.includes("contextWindow") ? parseCapacity(row.contextWindow) : undefined;
926
+ const maxTokens = gap.missing.includes("maxTokens") ? parseCapacity(row.maxTokens) : undefined;
927
+ if (Number.isNaN(contextWindow) || Number.isNaN(maxTokens)) {
928
+ invalid = true;
929
+ continue;
930
+ }
931
+ const vision = gap.missing.includes("input") && row.vision ? row.vision === "image" : undefined;
932
+ if (contextWindow === undefined && maxTokens === undefined && vision === undefined) continue;
933
+ fills.push({ provider: gap.provider, index: gap.index, id: gap.id, contextWindow, maxTokens, vision });
934
+ }
935
+
936
+ const save = async () => {
937
+ if (await onSave(fills)) setRows({});
938
+ };
939
+
940
+ const capacityCell = (gap: CustomModelGap, field: "contextWindow" | "maxTokens", label: string) => {
941
+ if (!gap.missing.includes(field)) {
942
+ return (
943
+ <span className="truncate" style={{ fontFamily: "var(--font-mono)", color: "var(--dsw-label-secondary)" }}>
944
+ <span className="sm:hidden" style={{ color: "var(--dsw-label-caption)" }}>{label} </span>
945
+ {formatCapacity(gap[field]) || "—"}
946
+ </span>
947
+ );
948
+ }
949
+ const text = rowOf(gap)[field];
950
+ return (
951
+ <input
952
+ className="pw-audit-field"
953
+ style={{ fontFamily: "var(--font-mono)" }}
954
+ value={text}
955
+ onChange={(event) => edit(gap, { [field]: event.target.value })}
956
+ placeholder={label}
957
+ aria-label={`${gap.id} ${label}`}
958
+ aria-invalid={Number.isNaN(parseCapacity(text)) || undefined}
959
+ disabled={busy}
960
+ />
961
+ );
962
+ };
963
+
964
+ return (
965
+ <section className="flex flex-col gap-3 rounded-2xl p-4" style={{ border: "0.5px solid var(--dsw-border-l3)", background: "var(--dsw-hover)" }} data-testid="model-audit">
966
+ <div className="flex items-start gap-2.5">
967
+ <IconWarningOutline16 size={16} style={{ flex: "none", marginTop: 1, color: "var(--dsw-warn)" }} />
968
+ <div className="min-w-0 flex-1">
969
+ <div style={{ fontSize: 13.5, fontWeight: 600 }}>{t.modelAuditTitle.replace("{n}", String(gaps.length))}</div>
970
+ <div className="mt-1" style={{ fontSize: 12, color: "var(--dsw-label-caption)", lineHeight: 1.65 }}>
971
+ {t.modelAuditDesc
972
+ .replace("{ctx}", formatCapacity(PI_MODEL_FALLBACKS.contextWindow))
973
+ .replace("{out}", formatCapacity(PI_MODEL_FALLBACKS.maxTokens))}
974
+ </div>
975
+ </div>
976
+ </div>
977
+
978
+ <div style={{ fontSize: 12.5 }} role="table" aria-label={t.modelAuditTitle.replace("{n}", String(gaps.length))}>
979
+ <div role="row" className="hidden gap-x-2 px-1 pb-1.5 sm:grid sm:grid-cols-[minmax(0,1fr)_92px_92px_108px]" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
980
+ <span role="columnheader">{t.modelAuditColModel}</span>
981
+ <span role="columnheader">{t.modelAuditColContext}</span>
982
+ <span role="columnheader">{t.modelAuditColOutput}</span>
983
+ <span role="columnheader">{t.modelAuditColVision}</span>
984
+ </div>
985
+ <div className="max-h-[320px] overflow-y-auto" style={{ borderTop: "0.5px solid var(--dsw-border-l2)" }}>
986
+ {gaps.map((gap) => (
987
+ <div
988
+ key={keyOf(gap)}
989
+ role="row"
990
+ className="grid grid-cols-3 items-center gap-x-2 gap-y-1.5 px-1 py-2 sm:grid-cols-[minmax(0,1fr)_92px_92px_108px]"
991
+ style={{ borderBottom: "0.5px solid var(--dsw-border-l1)" }}
992
+ >
993
+ <div role="cell" className="col-span-3 min-w-0 sm:col-span-1" title={`${gap.provider} / ${gap.id}`}>
994
+ <div className="truncate" style={{ fontWeight: 500 }}>{gap.name ?? gap.id}</div>
995
+ <div className="truncate" style={{ fontSize: 11.5, fontFamily: "var(--font-mono)", color: "var(--dsw-label-caption)" }}>
996
+ {providerLabel(gap.provider)} / {gap.id}
997
+ </div>
998
+ </div>
999
+ <div role="cell" className="min-w-0">{capacityCell(gap, "contextWindow", t.modelAuditColContext)}</div>
1000
+ <div role="cell" className="min-w-0">{capacityCell(gap, "maxTokens", t.modelAuditColOutput)}</div>
1001
+ <div role="cell" className="min-w-0">
1002
+ {/* 输入类型:文本 pi 总是接受(勾上不可点),图片这一勾就是 models.json 的 input 里加 image */}
1003
+ {gap.missing.includes("input") || true ? (
1004
+ <div className="flex items-center gap-3" role="group" aria-label={`${gap.id} ${t.modelAuditColVision}`}>
1005
+ <label className="flex cursor-default items-center gap-1" style={{ color: "var(--dsw-label-tertiary)" }} title={t.visionInputTextHint}>
1006
+ <input type="checkbox" checked disabled readOnly />
1007
+ {t.visionInputText}
1008
+ </label>
1009
+ <label className="flex items-center gap-1" style={{ color: "var(--dsw-label-secondary)" }} title={t.visionInputImageHint}>
1010
+ <input
1011
+ type="checkbox"
1012
+ checked={gap.missing.includes("input") ? rowOf(gap).vision === "image" : gap.vision === true}
1013
+ disabled={busy || !gap.missing.includes("input")}
1014
+ onChange={(event) => edit(gap, { vision: event.target.checked ? "image" : "text" })}
1015
+ aria-label={`${gap.id} ${t.visionInputImage}`}
1016
+ />
1017
+ {t.visionInputImage}
1018
+ </label>
1019
+ </div>
1020
+ ) : null}
1021
+ </div>
1022
+ </div>
1023
+ ))}
1024
+ </div>
1025
+ </div>
1026
+
1027
+ <div className="flex flex-wrap items-center gap-2">
1028
+ {visionGaps.length > 0 ? (
1029
+ <>
1030
+ <span style={{ fontSize: 12, color: "var(--dsw-label-caption)" }}>{t.modelAuditVisionQuick}</span>
1031
+ <button className="btn-outline" style={{ height: 28, padding: "0 11px", fontSize: 12.5 }} disabled={busy} onClick={() => markVision("image")}>{t.modelAuditVisionOn}</button>
1032
+ <button className="btn-outline" style={{ height: 28, padding: "0 11px", fontSize: 12.5 }} disabled={busy} onClick={() => markVision("text")}>{t.modelAuditVisionOff}</button>
1033
+ </>
1034
+ ) : null}
1035
+ <span className="flex-1" />
1036
+ <button className="btn-primary-white" style={{ height: 32 }} disabled={busy || invalid || fills.length === 0} onClick={() => void save()}>
1037
+ {busy ? t.saving : fills.length ? t.modelAuditSave.replace("{n}", String(fills.length)) : t.modelAuditSaveIdle}
1038
+ </button>
1039
+ </div>
1040
+ </section>
1041
+ );
1042
+ }
1043
+
874
1044
  function ModelsSection() {
875
1045
  const { t } = useI18n();
876
1046
  const [providers, setProviders] = useState<ProviderView[]>([]);
877
1047
  const [custom, setCustom] = useState<{ providers: Record<string, any>; [key: string]: unknown }>({ providers: {} });
878
1048
  const [customRevision, setCustomRevision] = useState<string | null>(null);
879
1049
  const [secretProviderIds, setSecretProviderIds] = useState<Set<string>>(new Set());
1050
+ // 内置 provider → 目录里的模型 ID:编辑时同 ID 的行写 modelOverrides,「获取可用模型」里不再可选
1051
+ const [catalogModelIds, setCatalogModelIds] = useState<Record<string, string[]>>({});
880
1052
  const [editing, setEditing] = useState<string | null>(null);
881
1053
  const [keyDraft, setKeyDraft] = useState("");
882
1054
  const [adding, setAdding] = useState<"builtin" | "custom" | null>(null);
@@ -955,6 +1127,7 @@ function ModelsSection() {
955
1127
  throw new Error("invalid model configuration");
956
1128
  }
957
1129
  setProviders(j.data.providers);
1130
+ setCatalogModelIds(j.data.catalogModelIds ?? {});
958
1131
  setSecretProviderIds(new Set(j.data.customProviders?.secretProviderIds ?? []));
959
1132
  setCustom({ ...parsed, providers: parsed.providers ?? {} });
960
1133
  setCustomRevision(typeof j.data.customProviders?.revision === "string" ? j.data.customProviders.revision : null);
@@ -1137,6 +1310,10 @@ function ModelsSection() {
1137
1310
  }
1138
1311
  };
1139
1312
 
1313
+ // 体检:直接从已载入的配置算(不另发请求),每次保存后 load() 重新载入就自动刷新
1314
+ const gaps = useMemo(() => customModelGaps(custom.providers ?? {}), [custom]);
1315
+ const [auditBusy, setAuditBusy] = useState(false);
1316
+
1140
1317
  const call = async (body: Record<string, unknown>) => {
1141
1318
  const j = await request(body);
1142
1319
  notify(j.success, j.success && j.data?.requiresSessionReopen ? t.modelConfigReopen : j.error);
@@ -1181,6 +1358,23 @@ function ModelsSection() {
1181
1358
  return call({ action: "saveCustomProviders", revision: customRevision, content: JSON.stringify({ ...custom, providers: content }, null, 2) });
1182
1359
  };
1183
1360
 
1361
+ /** 体检卡片的保存:只补空字段,走与编辑 provider 同一条写入(revision 乐观锁、密钥由服务端从原文件恢复) */
1362
+ const saveAuditFills = async (fills: ModelFieldFill[]): Promise<boolean> => {
1363
+ const { providers: next, filled } = fillModelFields(custom.providers ?? {}, fills);
1364
+ if (!filled) return true;
1365
+ setAuditBusy(true);
1366
+ try {
1367
+ const result = await saveCustom(next);
1368
+ // 失败(典型是 revision 冲突:写入期间 pi 自己动过 models.json)也必须刷新本地副本与 revision。
1369
+ // 否则本地一直拿着旧 revision,之后每次保存都撞同一个 409——用户看到的就是「反复填、永远不成」。
1370
+ // call() 只在成功时 load(),这里把失败那一半补上。
1371
+ if (!result.success) await load();
1372
+ return Boolean(result.success);
1373
+ } finally {
1374
+ setAuditBusy(false);
1375
+ }
1376
+ };
1377
+
1184
1378
  const providerName = (id: string) => providers.find((p) => p.id === id)?.name ?? id;
1185
1379
  const credentialSource = (provider: ProviderView) => {
1186
1380
  const source = provider.authSource;
@@ -1210,10 +1404,28 @@ function ModelsSection() {
1210
1404
  setCName(typeof override?.name === "string" ? override.name : "");
1211
1405
  setCModels((override?.models ?? []).map(modelDraftFromConfig));
1212
1406
  } else {
1213
- // 内置 provider:从 models.json 覆盖层初始化(若有)
1407
+ // 内置 provider:从 models.json 覆盖层初始化(若有),models[] 定义和 modelOverrides 覆盖都作为行
1214
1408
  setBBaseUrl(typeof override?.baseUrl === "string" ? override.baseUrl : "");
1215
- setBModels((override?.models ?? []).map(modelDraftFromConfig));
1409
+ setBModels(modelDraftsFromProviderConfig(override));
1410
+ }
1411
+ };
1412
+
1413
+ const catalogIdsOf = (providerId: string): ReadonlySet<string> => new Set(catalogModelIds[providerId] ?? []);
1414
+
1415
+ /**
1416
+ * 内置 provider 的条目写回:什么都没覆盖时删掉条目(SDK 不认空条目,会让整份 models.json 存不进去)。
1417
+ * 前端拿到的配置不含 apiKey,存了密钥的条目不能当成空的删掉。
1418
+ * 返回 null = models.json 不用动(原本就没有条目、这次也没覆盖任何东西,比如只填了密钥)。
1419
+ */
1420
+ const withBuiltinEntry = (providerId: string, config: Record<string, unknown>): Record<string, any> | null => {
1421
+ const next = { ...custom.providers };
1422
+ if (isInertProviderConfig(config) && !secretProviderIds.has(providerId)) {
1423
+ if (!Object.prototype.hasOwnProperty.call(next, providerId)) return null;
1424
+ delete next[providerId];
1425
+ } else {
1426
+ next[providerId] = config;
1216
1427
  }
1428
+ return next;
1217
1429
  };
1218
1430
 
1219
1431
  const saveCustomEdit = async () => {
@@ -1247,12 +1459,12 @@ function ModelsSection() {
1247
1459
  if (invalid) return;
1248
1460
  setEditBusy(true);
1249
1461
  try {
1250
- const next = { ...custom.providers };
1251
- next[provider.id] = serializeProviderDraft(next[provider.id] ?? {}, {
1462
+ const config = serializeProviderDraft(custom.providers[provider.id] ?? {}, {
1252
1463
  baseUrl: bBaseUrl,
1253
1464
  models: bModels,
1254
- }, provider.apis[0] ?? "openai-completions");
1255
- const j = await saveCustom(next);
1465
+ }, provider.apis[0] ?? "openai-completions", catalogIdsOf(provider.id));
1466
+ const next = withBuiltinEntry(provider.id, config);
1467
+ const j = next ? await saveCustom(next) : { success: true };
1256
1468
  if (j.success && keyDraft.trim()) {
1257
1469
  void call({ action: "setKey", providerId: provider.id, apiKey: keyDraft.trim() });
1258
1470
  setOauthDone(null);
@@ -1283,8 +1495,9 @@ function ModelsSection() {
1283
1495
  };
1284
1496
 
1285
1497
  const saveBuiltinSetup = async ({ providerId, apiKey, config }: BuiltinProviderSetup) => {
1286
- const next = { ...custom.providers, [providerId]: { ...custom.providers[providerId], ...config } };
1287
- const result = await saveCustom(next);
1498
+ const next = withBuiltinEntry(providerId, { ...custom.providers[providerId], ...config });
1499
+ // 只填了密钥:models.json 不需要条目,直接去存密钥
1500
+ const result = next ? await saveCustom(next) : { success: true };
1288
1501
  if (result.success && apiKey) {
1289
1502
  // 多步认证(Vertex/Bedrock/Cloudflare 等)的后续提示经登录弹窗应答;
1290
1503
  // 不能 await setKey —— 它会挂起到全部提示答完。
@@ -1467,7 +1680,7 @@ function ModelsSection() {
1467
1680
  });
1468
1681
  const result = await response.json();
1469
1682
  if (!result.success) throw new Error(result.error || t.toastError);
1470
- return (result.data.models ?? []).map((model: { id: string; name?: string }) => ({ id: model.id, name: model.name ?? "" }));
1683
+ return (result.data.models ?? []).map(modelDraftFromCatalog);
1471
1684
  }}
1472
1685
  discoverDisabled={!cBaseUrl.trim()}
1473
1686
  />
@@ -1514,6 +1727,7 @@ function ModelsSection() {
1514
1727
  setModels={setBModels}
1515
1728
  updateModel={(index, patch) => setBModels((current) => current.map((model, at) => (at === index ? { ...model, ...patch } : model)))}
1516
1729
  inheritedCount={p.modelCount}
1730
+ catalogIds={catalogIdsOf(p.id)}
1517
1731
  discover={async (allowPrivate) => {
1518
1732
  const response = await fetch("/api/models", {
1519
1733
  method: "POST",
@@ -1523,7 +1737,7 @@ function ModelsSection() {
1523
1737
  });
1524
1738
  const result = await response.json();
1525
1739
  if (!result.success) throw new Error(result.error || t.toastError);
1526
- return (result.data.models ?? []).map((model: { id: string; name?: string }) => ({ id: model.id, name: model.name ?? "" }));
1740
+ return (result.data.models ?? []).map(modelDraftFromCatalog);
1527
1741
  }}
1528
1742
  discoverDisabled={!(bBaseUrl.trim() || p.baseUrl)}
1529
1743
  />
@@ -1566,6 +1780,11 @@ function ModelsSection() {
1566
1780
  })}
1567
1781
  </div>
1568
1782
 
1783
+ {/* 模型配置体检:没写的字段 pi 会按缺省处理;由用户填,不替他猜 */}
1784
+ {gaps.length > 0 ? (
1785
+ <ModelFieldAudit gaps={gaps} providerLabel={providerName} busy={auditBusy || customRevision === null} onSave={saveAuditFills} />
1786
+ ) : null}
1787
+
1569
1788
  {/* 两个添加按钮 */}
1570
1789
  <div className="flex gap-3">
1571
1790
  <button
@@ -1594,6 +1813,7 @@ function ModelsSection() {
1594
1813
  <ProviderSetupModal
1595
1814
  mode={adding}
1596
1815
  providers={addable}
1816
+ catalogModelIds={catalogModelIds}
1597
1817
  existingIds={providers.map((provider) => provider.id)}
1598
1818
  onClose={() => setAdding(null)}
1599
1819
  onSaveBuiltin={saveBuiltinSetup}
@@ -1720,12 +1940,13 @@ function ModelsSection() {
1720
1940
 
1721
1941
  </div>
1722
1942
  );
1723
- }const fieldCls = "w-full rounded-xl px-3 py-2";
1943
+ }
1944
+
1945
+ const fieldCls = "w-full rounded-xl px-3 py-2";
1724
1946
 
1725
- /** 配额展示:token 数转 M(53,000,000 → 53M),小数值原样 */
1947
+ /** 配额展示:token 数转换单位(53,000,000 → 53.00M),与统计条 / 上下文弹窗口径一致 */
1726
1948
  function fmtQuotaTokens(value: number): string {
1727
- if (value >= 1_000_000) return `${Math.round(value / 100_000) / 10}M`;
1728
- return value.toLocaleString();
1949
+ return fmtTokens(value);
1729
1950
  }
1730
1951
 
1731
1952
  /** 套餐值来自服务端声明(如 "pro"/"plus"),首字母大写后直接跟在标签后显示。 */
@@ -1994,7 +2215,8 @@ function SkillsSection({ cwd, onOpenFileContent }: { cwd: string; onOpenFileCont
1994
2215
 
1995
2216
  // ---------- Plugins ----------
1996
2217
 
1997
- function PluginsSection({ cwd }: { cwd: string }) {
2218
+ /** 导出仅供测试(其余 section 不导出) */
2219
+ export function PluginsSection({ cwd }: { cwd: string }) {
1998
2220
  const { t, lang } = useI18n();
1999
2221
  const confirmUninstall = (source: string) => window.confirm(t.confirmUninstallPackage.replace("{source}", source));
2000
2222
  const [tab, setTab] = useState<"config" | "list">("config");
@@ -2111,13 +2333,19 @@ function PluginsSection({ cwd }: { cwd: string }) {
2111
2333
  })),
2112
2334
  ].filter((x) => !query || x.name.toLowerCase().includes(query));
2113
2335
 
2114
- const badge = (
2336
+ /** 扩展卡片的启用状态:每张卡只在头部显示这一处,展开区不重复 */
2337
+ const stateBadge = (disabled: boolean) => (
2115
2338
  <span
2116
2339
  className="flex flex-none items-center gap-1.5 rounded-md px-1.5 py-0.5"
2117
- style={{ fontSize: 10.5, background: "rgba(34,197,94,.12)", color: "var(--dsw-success)" }}
2340
+ style={{
2341
+ fontSize: 10.5,
2342
+ ...(disabled
2343
+ ? { background: "var(--dsw-selector)", color: "var(--dsw-label-tertiary)" }
2344
+ : { background: "rgba(34,197,94,.12)", color: "var(--dsw-success)" }),
2345
+ }}
2118
2346
  >
2119
- <span className="h-1.5 w-1.5 rounded-full" style={{ background: "var(--dsw-success)" }} />
2120
- {t.enabled}
2347
+ <span className="h-1.5 w-1.5 rounded-full" style={{ background: disabled ? "var(--dsw-label-tertiary)" : "var(--dsw-success)" }} />
2348
+ {disabled ? t.disabled : t.enabled}
2121
2349
  </span>
2122
2350
  );
2123
2351
 
@@ -2234,9 +2462,7 @@ function PluginsSection({ cwd }: { cwd: string }) {
2234
2462
  {e.error && (
2235
2463
  <span className="rounded px-1.5 py-0.5" style={{ fontSize: 10.5, color: "var(--dsw-danger)", background: "var(--pw-danger-soft, rgba(236,19,19,.12))" }}>{t.extensionLoadError}</span>
2236
2464
  )}
2237
- {e.disabled && !e.error && (
2238
- <span className="rounded px-1.5 py-0.5" style={{ fontSize: 10.5, color: "var(--dsw-label-tertiary)", background: "var(--dsw-selector)" }}>{t.disabled}</span>
2239
- )}
2465
+ {!e.error && stateBadge(e.disabled === true)}
2240
2466
  </div>
2241
2467
  <div className="truncate" style={{ fontSize: 12, color: e.error ? "var(--dsw-danger)" : "var(--dsw-label-caption)" }} title={e.error ?? e.path}>
2242
2468
  {e.error ?? e.path}
@@ -2258,7 +2484,6 @@ function PluginsSection({ cwd }: { cwd: string }) {
2258
2484
  >
2259
2485
  {scopeLabel(scope)}
2260
2486
  </span>
2261
- {badge}
2262
2487
  </div>
2263
2488
  <div className="break-all" style={{ fontFamily: "var(--font-mono)", fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
2264
2489
  {e.path}
@@ -2341,7 +2566,7 @@ function PluginsSection({ cwd }: { cwd: string }) {
2341
2566
  <span className="h-3 w-3 flex-none animate-spin rounded-full border border-[var(--dsw-label-tertiary)] border-t-transparent" />
2342
2567
  ) : !isDisabled ? (
2343
2568
  <span
2344
- className="h-2 w-2 flex-none rounded-full"
2569
+ className="h-2 w-2 flex-none rounded-full"
2345
2570
  style={{ background: "#22c55e", boxShadow: "0 0 5px rgba(34, 197, 94, 0.4)" }}
2346
2571
  />
2347
2572
  ) : null}
@@ -2355,8 +2580,8 @@ function PluginsSection({ cwd }: { cwd: string }) {
2355
2580
  >
2356
2581
  {isDisabled ? t.disabled : t.enabled}
2357
2582
  </span>
2358
- <IconChevronDown14
2359
- size={13}
2583
+ <IconChevronDown14
2584
+ size={13}
2360
2585
  style={{
2361
2586
  color: "var(--dsw-label-tertiary)",
2362
2587
  transform: isMenuOpen ? "rotate(180deg)" : "none",
@@ -3,6 +3,7 @@
3
3
  import { useMemo, useState } from "react";
4
4
  import { IconCloseOutline14, IconSearchOutline16 } from "@/components/icons";
5
5
  import { useI18n } from "@/i18n";
6
+ import { fmtTokens } from "@/lib/format";
6
7
  import type { TrajEntry, TrajTokens } from "@/lib/types";
7
8
  import styles from "./TrajectoryAnalyzer.module.css";
8
9
 
@@ -26,10 +27,7 @@ const KIND_NAME: Record<TrajEntry["kind"], string> = {
26
27
  };
27
28
 
28
29
  function fmtTok(value?: number): string {
29
- if (value == null) return "—";
30
- if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
31
- if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`;
32
- return String(value);
30
+ return value == null ? "—" : fmtTokens(value);
33
31
  }
34
32
 
35
33
  function fmtTime(ts: number): string {
@@ -45,8 +43,8 @@ function durationOf(entry: TrajEntry): number | undefined {
45
43
 
46
44
  function fmtDur(ms?: number): string {
47
45
  if (ms == null) return "—";
48
- if (ms >= 60_000) return `${(ms / 60_000).toFixed(1)}m`;
49
- if (ms >= 1_000) return `${(ms / 1_000).toFixed(ms >= 10_000 ? 0 : 1)}s`;
46
+ if (ms >= 60_000) return `${(ms / 60_000).toFixed(2)}m`;
47
+ if (ms >= 1_000) return `${(ms / 1_000).toFixed(2)}s`;
50
48
  return `${Math.round(ms)}ms`;
51
49
  }
52
50
 
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, type RefObject } from "react";
4
+
5
+ const BEFORE_LAYOUT = "piweb:before-chat-layout";
6
+
7
+ /** Call before changing the columns, while the currently visible text still has its old geometry. */
8
+ export function preserveChatReadingPosition(): void {
9
+ window.dispatchEvent(new Event(BEFORE_LAYOUT));
10
+ }
11
+
12
+ interface ReadingAnchor {
13
+ element?: Element;
14
+ range?: Range;
15
+ top: number;
16
+ bottom: boolean;
17
+ width: number;
18
+ capturedAt: number;
19
+ }
20
+
21
+ export function useChatReadingAnchor(rootRef: RefObject<HTMLElement | null>, scrollRef: RefObject<HTMLElement | null>, shouldPin: () => boolean): void {
22
+ const anchor = useRef<ReadingAnchor | null>(null);
23
+ useEffect(() => {
24
+ const root = rootRef.current;
25
+ const scroller = scrollRef.current;
26
+ if (!root || !scroller) return;
27
+ let frame = 0;
28
+ const restore = () => {
29
+ const saved = anchor.current;
30
+ anchor.current = null;
31
+ if (!saved || Date.now() - saved.capturedAt > 500 || root.getBoundingClientRect().width === saved.width) return;
32
+ if (saved.bottom) {
33
+ scroller.scrollTop = scroller.scrollHeight;
34
+ return;
35
+ }
36
+ const rect = saved.range?.startContainer.isConnected ? saved.range.getBoundingClientRect() : saved.element?.isConnected ? saved.element.getBoundingClientRect() : null;
37
+ if (rect) scroller.scrollTop += rect.top - scroller.getBoundingClientRect().top - saved.top;
38
+ };
39
+ const capture = () => {
40
+ if (frame) cancelAnimationFrame(frame);
41
+ // A new drag frame may arrive before the previous restore frame. Finish it first.
42
+ restore();
43
+ const geometry = { width: root.getBoundingClientRect().width, capturedAt: Date.now() };
44
+ if (shouldPin()) {
45
+ anchor.current = { top: 0, bottom: true, ...geometry };
46
+ return;
47
+ }
48
+ const viewport = scroller.getBoundingClientRect();
49
+ const element = [...scroller.querySelectorAll("p, pre, li, h1, h2, h3, h4, table, [data-role='user']")].find((node) => {
50
+ const rect = node.getBoundingClientRect();
51
+ return rect.bottom > viewport.top + 8 && rect.top < viewport.bottom;
52
+ });
53
+ if (!element) return;
54
+ const rect = element.getBoundingClientRect();
55
+ const x = Math.max(viewport.left + 8, rect.left + 8);
56
+ const y = Math.max(viewport.top + 8, rect.top + 4);
57
+ const range = document.caretRangeFromPoint?.(x, y);
58
+ if (range && scroller.contains(range.startContainer) && range.startContainer.nodeType === Node.TEXT_NODE && range.startOffset < (range.startContainer.textContent?.length ?? 0)) {
59
+ range.setEnd(range.startContainer, range.startOffset + 1);
60
+ anchor.current = { element, range, top: range.getBoundingClientRect().top - viewport.top, bottom: false, ...geometry };
61
+ } else {
62
+ anchor.current = { element, top: rect.top - viewport.top, bottom: false, ...geometry };
63
+ }
64
+ };
65
+ const observer = new ResizeObserver(() => {
66
+ if (!anchor.current) return;
67
+ if (frame) cancelAnimationFrame(frame);
68
+ frame = requestAnimationFrame(() => {
69
+ frame = 0;
70
+ restore();
71
+ });
72
+ });
73
+ observer.observe(root);
74
+ window.addEventListener(BEFORE_LAYOUT, capture);
75
+ return () => {
76
+ observer.disconnect();
77
+ window.removeEventListener(BEFORE_LAYOUT, capture);
78
+ if (frame) cancelAnimationFrame(frame);
79
+ };
80
+ }, [rootRef, scrollRef, shouldPin]);
81
+ }