@rexvane/piweb 0.3.20 → 0.3.22

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 (317) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +6 -4
  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 +12 -12
  8. package/.next/required-server-files.js +18 -2
  9. package/.next/required-server-files.json +18 -2
  10. package/.next/routes-manifest.json +12 -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 +3 -3
  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]/tree/route.js +1 -1
  34. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  35. package/.next/server/app/api/agent/new/route.js +1 -1
  36. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  37. package/.next/server/app/api/config/route.js +1 -1
  38. package/.next/server/app/api/config/route.js.nft.json +1 -1
  39. package/.next/server/app/api/files/route.js +1 -1
  40. package/.next/server/app/api/files/route.js.nft.json +1 -1
  41. package/.next/server/app/api/fs/route.js +1 -1
  42. package/.next/server/app/api/fs/route.js.nft.json +1 -1
  43. package/.next/server/app/api/git/route.js +4 -4
  44. package/.next/server/app/api/git/route.js.nft.json +1 -1
  45. package/.next/server/app/api/github/route.js +5 -0
  46. package/.next/server/app/api/github/route.js.nft.json +1 -0
  47. package/.next/server/app/api/github/route_client-reference-manifest.js +1 -0
  48. package/.next/server/app/api/growth/route.js +1 -1
  49. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  50. package/.next/server/app/api/health/route.js +1 -1
  51. package/.next/server/app/api/health/route.js.nft.json +1 -1
  52. package/.next/server/app/api/models/defaults/route.js +1 -0
  53. package/.next/server/app/api/models/defaults/route.js.nft.json +1 -0
  54. package/.next/server/app/api/models/defaults/route_client-reference-manifest.js +1 -0
  55. package/.next/server/app/api/models/route.js +1 -1
  56. package/.next/server/app/api/models/route.js.nft.json +1 -1
  57. package/.next/server/app/api/pi-settings/route.js +1 -1
  58. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  59. package/.next/server/app/api/plugins/route.js +1 -1
  60. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  61. package/.next/server/app/api/prompts/route.js +1 -1
  62. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  63. package/.next/server/app/api/remote/route.js +3 -3
  64. package/.next/server/app/api/remote/route.js.nft.json +1 -1
  65. package/.next/server/app/api/security/route.js +1 -1
  66. package/.next/server/app/api/security/route.js.nft.json +1 -1
  67. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  68. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  69. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  70. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  71. package/.next/server/app/api/sessions/import/route.js +2 -2
  72. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  73. package/.next/server/app/api/sessions/route.js +1 -1
  74. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  75. package/.next/server/app/api/skills/route.js +2 -2
  76. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  77. package/.next/server/app/api/update/route.js +2 -2
  78. package/.next/server/app/api/update/route.js.nft.json +1 -1
  79. package/.next/server/app/api/version/route.js +1 -1
  80. package/.next/server/app/api/version/route.js.nft.json +1 -1
  81. package/.next/server/app/api/web-auth/route.js +1 -1
  82. package/.next/server/app/api/web-auth/route.js.nft.json +1 -1
  83. package/.next/server/app/api/workspaces/route.js +1 -1
  84. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  85. package/.next/server/app/icon.svg/route.js +1 -1
  86. package/.next/server/app/icon.svg/route.js.nft.json +1 -1
  87. package/.next/server/app/icon.svg.meta +1 -1
  88. package/.next/server/app/index.html +1 -1
  89. package/.next/server/app/index.meta +8 -0
  90. package/.next/server/app/index.rsc +19 -13
  91. package/.next/server/app/index.segments/__PAGE__.segment.rsc +25 -19
  92. package/.next/server/app/index.segments/_full.segment.rsc +19 -13
  93. package/.next/server/app/index.segments/_tree.segment.rsc +3 -2
  94. package/.next/server/app/login/page.js +1 -1
  95. package/.next/server/app/login/page.js.nft.json +1 -1
  96. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  97. package/.next/server/app/login.html +1 -1
  98. package/.next/server/app/login.meta +8 -0
  99. package/.next/server/app/login.rsc +23 -17
  100. package/.next/server/app/login.segments/_full.segment.rsc +23 -17
  101. package/.next/server/app/login.segments/_tree.segment.rsc +4 -2
  102. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +30 -24
  103. package/.next/server/app/page.js +1 -1
  104. package/.next/server/app/page.js.nft.json +1 -1
  105. package/.next/server/app/page_client-reference-manifest.js +1 -1
  106. package/.next/server/app-paths-manifest.json +6 -4
  107. package/.next/server/chunks/229.js +1 -1
  108. package/.next/server/chunks/421.js +1 -0
  109. package/.next/server/chunks/430.js +215 -31
  110. package/.next/server/chunks/496.js +1 -1
  111. package/.next/server/chunks/538.js +1 -1
  112. package/.next/server/chunks/662.js +2 -0
  113. package/.next/server/chunks/686.js +1 -1
  114. package/.next/server/chunks/807.js +1 -1
  115. package/.next/server/chunks/813.js +23 -1
  116. package/.next/server/chunks/920.js +2 -2
  117. package/.next/server/chunks/924.js +1 -1
  118. package/.next/server/chunks/970.js +305 -0
  119. package/.next/server/instrumentation.js +1 -1
  120. package/.next/server/instrumentation.js.nft.json +1 -1
  121. package/.next/server/middleware-build-manifest.js +1 -1
  122. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  123. package/.next/server/middleware.js +185 -23
  124. package/.next/server/middleware.js.nft.json +1 -1
  125. package/.next/server/pages/404.html +1 -1
  126. package/.next/server/pages/500.html +1 -1
  127. package/.next/server/preview-props.json +5 -0
  128. package/.next/server/server-reference-manifest.json +1 -1
  129. package/.next/static/WMazYvD-gbeLsCUfUxvvM/_buildManifest.js +1 -0
  130. package/.next/static/chunks/316-698d10eb465bd8ef.js +1 -0
  131. package/.next/static/chunks/3794-0978865cda773008.js +52 -0
  132. package/.next/static/chunks/4673.4cf66cbdd8364c57.js +1 -0
  133. package/.next/static/chunks/4686-09fcd0028079006c.js +1 -0
  134. package/.next/static/chunks/4778.eddd3a5813f85824.js +1 -0
  135. package/.next/static/chunks/4bd1b696-e199e788e58acf79.js +1 -0
  136. package/.next/static/chunks/{5173.91ddb803785009f8.js → 5173.dfd1cbb795b9a21c.js} +1 -1
  137. package/.next/static/chunks/618.949981da99be7182.js +4 -0
  138. package/.next/static/chunks/7411.67366dc5032a141d.js +1 -0
  139. package/.next/static/chunks/8552.25c39ef4c995eba0.js +1 -0
  140. package/.next/static/chunks/app/_global-error/page-f977af7f858990a2.js +1 -0
  141. package/.next/static/chunks/app/api/agent/[id]/events/route-f977af7f858990a2.js +1 -0
  142. package/.next/static/chunks/app/api/agent/[id]/route-f977af7f858990a2.js +1 -0
  143. package/.next/static/chunks/app/api/agent/[id]/tree/route-f977af7f858990a2.js +1 -0
  144. package/.next/static/chunks/app/api/agent/new/route-f977af7f858990a2.js +1 -0
  145. package/.next/static/chunks/app/api/config/route-f977af7f858990a2.js +1 -0
  146. package/.next/static/chunks/app/api/files/route-f977af7f858990a2.js +1 -0
  147. package/.next/static/chunks/app/api/fs/route-f977af7f858990a2.js +1 -0
  148. package/.next/static/chunks/app/api/git/route-f977af7f858990a2.js +1 -0
  149. package/.next/static/chunks/app/api/github/route-f977af7f858990a2.js +1 -0
  150. package/.next/static/chunks/app/api/growth/route-f977af7f858990a2.js +1 -0
  151. package/.next/static/chunks/app/api/health/route-f977af7f858990a2.js +1 -0
  152. package/.next/static/chunks/app/api/models/defaults/route-f977af7f858990a2.js +1 -0
  153. package/.next/static/chunks/app/api/models/route-f977af7f858990a2.js +1 -0
  154. package/.next/static/chunks/app/api/pi-settings/route-f977af7f858990a2.js +1 -0
  155. package/.next/static/chunks/app/api/plugins/route-f977af7f858990a2.js +1 -0
  156. package/.next/static/chunks/app/api/prompts/route-f977af7f858990a2.js +1 -0
  157. package/.next/static/chunks/app/api/remote/route-f977af7f858990a2.js +1 -0
  158. package/.next/static/chunks/app/api/security/route-f977af7f858990a2.js +1 -0
  159. package/.next/static/chunks/app/api/sessions/[id]/export/route-f977af7f858990a2.js +1 -0
  160. package/.next/static/chunks/app/api/sessions/[id]/route-f977af7f858990a2.js +1 -0
  161. package/.next/static/chunks/app/api/sessions/import/route-f977af7f858990a2.js +1 -0
  162. package/.next/static/chunks/app/api/sessions/route-f977af7f858990a2.js +1 -0
  163. package/.next/static/chunks/app/api/skills/route-f977af7f858990a2.js +1 -0
  164. package/.next/static/chunks/app/api/update/route-f977af7f858990a2.js +1 -0
  165. package/.next/static/chunks/app/api/version/route-f977af7f858990a2.js +1 -0
  166. package/.next/static/chunks/app/api/web-auth/route-f977af7f858990a2.js +1 -0
  167. package/.next/static/chunks/app/api/workspaces/route-f977af7f858990a2.js +1 -0
  168. package/.next/static/chunks/app/page-fc4ed2ff29b6235e.js +1 -0
  169. package/.next/static/chunks/framework-47b723d27f4a4963.js +1 -0
  170. package/.next/static/chunks/main-5403f0764579e5fe.js +5 -0
  171. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f977af7f858990a2.js +1 -0
  172. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f977af7f858990a2.js +1 -0
  173. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f977af7f858990a2.js +1 -0
  174. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f977af7f858990a2.js +1 -0
  175. package/.next/static/chunks/webpack-22ebffc20adb44ae.js +1 -0
  176. package/.next/static/css/6671d0d1589fdc0e.css +1 -0
  177. package/.next/static/css/{2959bd6b626b7ffe.css → f15a853af8b5127f.css} +1 -1
  178. package/.next/types/app/api/agent/[id]/events/route.ts +3 -1
  179. package/.next/types/app/api/agent/[id]/route.ts +3 -1
  180. package/.next/types/app/api/agent/[id]/tree/route.ts +3 -1
  181. package/.next/types/app/api/agent/new/route.ts +3 -1
  182. package/.next/types/app/api/config/route.ts +3 -1
  183. package/.next/types/app/api/files/route.ts +3 -1
  184. package/.next/types/app/api/fs/route.ts +3 -1
  185. package/.next/types/app/api/git/route.ts +3 -1
  186. package/.next/types/app/api/github/route.ts +355 -0
  187. package/.next/types/app/api/growth/route.ts +3 -1
  188. package/.next/types/app/api/health/route.ts +3 -1
  189. package/.next/types/app/api/models/defaults/route.ts +355 -0
  190. package/.next/types/app/api/models/route.ts +3 -1
  191. package/.next/types/app/api/pi-settings/route.ts +3 -1
  192. package/.next/types/app/api/plugins/route.ts +3 -1
  193. package/.next/types/app/api/prompts/route.ts +3 -1
  194. package/.next/types/app/api/remote/route.ts +3 -1
  195. package/.next/types/app/api/security/route.ts +3 -1
  196. package/.next/types/app/api/sessions/[id]/export/route.ts +3 -1
  197. package/.next/types/app/api/sessions/[id]/route.ts +3 -1
  198. package/.next/types/app/api/sessions/import/route.ts +3 -1
  199. package/.next/types/app/api/sessions/route.ts +3 -1
  200. package/.next/types/app/api/skills/route.ts +3 -1
  201. package/.next/types/app/api/update/route.ts +3 -1
  202. package/.next/types/app/api/version/route.ts +3 -1
  203. package/.next/types/app/api/web-auth/route.ts +3 -1
  204. package/.next/types/app/api/workspaces/route.ts +3 -1
  205. package/.next/types/app/layout.ts +4 -1
  206. package/.next/types/app/login/page.ts +4 -1
  207. package/.next/types/app/page.ts +4 -1
  208. package/.next/types/cache-life.d.ts +11 -43
  209. package/.next/types/routes.d.ts +3 -1
  210. package/.next/types/validator.ts +76 -2
  211. package/README.md +14 -5
  212. package/next.config.ts +4 -2
  213. package/package.json +2 -2
  214. package/scripts/release.mjs +1 -1
  215. package/src/app/api/agent/[id]/events/route.ts +42 -13
  216. package/src/app/api/files/route.ts +5 -2
  217. package/src/app/api/github/route.ts +26 -0
  218. package/src/app/api/models/defaults/route.ts +18 -0
  219. package/src/app/api/models/route.ts +24 -1
  220. package/src/app/api/update/route.ts +18 -2
  221. package/src/app/globals.css +275 -0
  222. package/src/components/AppShell.tsx +89 -33
  223. package/src/components/ChatInput.tsx +245 -63
  224. package/src/components/ChatWindow.tsx +123 -108
  225. package/src/components/ContextMeter.tsx +204 -123
  226. package/src/components/DrawerPanel.tsx +19 -26
  227. package/src/components/ExtensionUI.tsx +8 -2
  228. package/src/components/GitPanel.tsx +110 -40
  229. package/src/components/MarkdownBlock.tsx +333 -37
  230. package/src/components/ProjectPanel.tsx +55 -30
  231. package/src/components/PromptPanel.tsx +2 -4
  232. package/src/components/ProviderSetupModal.module.css +20 -0
  233. package/src/components/ProviderSetupModal.tsx +134 -29
  234. package/src/components/SettingsPanel.tsx +365 -80
  235. package/src/components/TrajectoryView.tsx +4 -6
  236. package/src/components/chat/chat-scroll.ts +18 -0
  237. package/src/components/composer/ComposerImages.tsx +306 -0
  238. package/src/components/update/CopyCommand.tsx +34 -0
  239. package/src/components/update/UpdateNotice.tsx +130 -0
  240. package/src/components/update/update-store.ts +155 -0
  241. package/src/hooks/useGrowth.ts +3 -0
  242. package/src/hooks/usePiWeb.ts +54 -27
  243. package/src/hooks/useWorkspaceFiles.ts +93 -0
  244. package/src/i18n.tsx +182 -6
  245. package/src/instrumentation-node.ts +3 -0
  246. package/src/lib/agent-manager.ts +193 -119
  247. package/src/lib/chat/event-stream.ts +113 -0
  248. package/src/lib/chat/stream-settings.ts +192 -0
  249. package/src/lib/files-service.ts +100 -1
  250. package/src/lib/format.ts +56 -0
  251. package/src/lib/git-service.ts +45 -13
  252. package/src/lib/github-identity.ts +245 -0
  253. package/src/lib/growth-service.ts +6 -2
  254. package/src/lib/hero-model-memory.ts +28 -0
  255. package/src/lib/model-defaults.ts +105 -0
  256. package/src/lib/model-draft.ts +257 -9
  257. package/src/lib/model-endpoint.ts +23 -0
  258. package/src/lib/model-files.ts +33 -0
  259. package/src/lib/models-service.ts +240 -78
  260. package/src/lib/pi.ts +27 -2
  261. package/src/lib/process-format.ts +3 -2
  262. package/src/lib/session/metrics.ts +31 -0
  263. package/src/lib/thinking-level.ts +39 -0
  264. package/src/lib/trajectory.ts +17 -3
  265. package/src/lib/types.ts +19 -1
  266. package/src/lib/update/startup-check.ts +117 -0
  267. package/src/lib/update-service.ts +23 -3
  268. package/src/lib/workspace-index.ts +270 -0
  269. package/.next/server/chunks/131.js +0 -1
  270. package/.next/server/chunks/243.js +0 -1
  271. package/.next/server/chunks/741.js +0 -165
  272. package/.next/static/_Y3Lq0lI5RJ1tFSOnDi0L/_buildManifest.js +0 -1
  273. package/.next/static/chunks/316-5ea443ee6b512aa9.js +0 -1
  274. package/.next/static/chunks/3794-3679f1c05160977c.js +0 -52
  275. package/.next/static/chunks/4673.8a5a9e6855350e34.js +0 -1
  276. package/.next/static/chunks/4686-b1e9fa876d0ea671.js +0 -1
  277. package/.next/static/chunks/4778.e4c9ed093a413bc4.js +0 -1
  278. package/.next/static/chunks/4bd1b696-93bf506b236d6248.js +0 -1
  279. package/.next/static/chunks/618.ee5d5bf1c647e483.js +0 -4
  280. package/.next/static/chunks/839.d3fe337a9117c325.js +0 -1
  281. package/.next/static/chunks/8552.6bfe3e101250b20f.js +0 -1
  282. package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +0 -1
  283. package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +0 -1
  284. package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +0 -1
  285. package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +0 -1
  286. package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +0 -1
  287. package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +0 -1
  288. package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +0 -1
  289. package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +0 -1
  290. package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +0 -1
  291. package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +0 -1
  292. package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +0 -1
  293. package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +0 -1
  294. package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +0 -1
  295. package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +0 -1
  296. package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +0 -1
  297. package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +0 -1
  298. package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +0 -1
  299. package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +0 -1
  300. package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +0 -1
  301. package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +0 -1
  302. package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +0 -1
  303. package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +0 -1
  304. package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +0 -1
  305. package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +0 -1
  306. package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +0 -1
  307. package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +0 -1
  308. package/.next/static/chunks/app/page-2e425ef83fba0782.js +0 -1
  309. package/.next/static/chunks/framework-c4e651fe89edfa91.js +0 -1
  310. package/.next/static/chunks/main-6159b22a8cb0b7c5.js +0 -5
  311. package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +0 -1
  312. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +0 -1
  313. package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +0 -1
  314. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +0 -1
  315. package/.next/static/chunks/webpack-3614a8337843456b.js +0 -1
  316. package/.next/static/css/7391c42e1ae617f9.css +0 -1
  317. /package/.next/static/{_Y3Lq0lI5RJ1tFSOnDi0L → WMazYvD-gbeLsCUfUxvvM}/_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 {
@@ -32,10 +33,38 @@ import {
32
33
  type CustomProviderSetup,
33
34
  } from "@/components/ProviderSetupModal";
34
35
  import { RemoteSection } from "@/components/RemoteSection";
36
+ import { CopyCommand } from "@/components/update/CopyCommand";
37
+ import { checkUpdate, loadUpdates, runUpdate, useUpdates, type UpdateTarget } from "@/components/update/update-store";
35
38
  import { useI18n } from "@/i18n";
39
+ import { fmtTokens } from "@/lib/format";
36
40
  import { applyPebrelTheme, loadPebrelTheme, loadThemeMode, type PebrelTheme, type ThemeMode } from "@/lib/theme";
37
41
  import { applyFontChoice, CUSTOM_PREFIX, FONT_PRESETS, loadFontChoice } from "@/lib/font";
38
- import { modelDraftFromConfig, serializeProviderDraft, validateModelDrafts, type ModelDraft } from "@/lib/model-draft";
42
+ import {
43
+ DEFAULT_STREAM_SETTINGS,
44
+ getStreamSettings,
45
+ setStreamSettings,
46
+ STREAM_SPEED_MAX,
47
+ STREAM_SPEED_MIN,
48
+ STREAM_STYLES,
49
+ type StreamSettings,
50
+ type StreamStyle,
51
+ } from "@/lib/chat/stream-settings";
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";
39
68
  import { customApiOptions } from "@/lib/provider-display";
40
69
  import { quotaWindowKind, supportsUsageProbe, type BalanceAmount, type BalanceDetailKey, type ProviderUsage, type QuotaWindow, type XaiUsage } from "@/lib/provider-usage";
41
70
  import type { ProviderView } from "@/lib/models-service";
@@ -174,6 +203,7 @@ function GeneralSection({
174
203
  const [themeMode, setThemeMode] = useState<ThemeMode>("system");
175
204
  const [fontSize, setFontSize] = useState(14);
176
205
  const [chatFontSize, setChatFontSize] = useState(14);
206
+ const [stream, setStream] = useState<StreamSettings>(DEFAULT_STREAM_SETTINGS);
177
207
  const [enterBehavior, setEnterBehavior] = useState<EnterBehavior>("steer");
178
208
  const [piSettings, setPiSettings] = useState<{ compaction: { enabled: boolean; reserveTokens: number; keepRecentTokens: number }; retry: { enabled: boolean; maxRetries: number; baseDelayMs: number } } | null>(null);
179
209
  const [versions, setVersions] = useState<{ piWeb: string; piEngine: string } | null>(null);
@@ -222,6 +252,8 @@ function GeneralSection({
222
252
  if (!Number.isNaN(fs)) setFontSize(fs);
223
253
  const chatFs = parseInt(localStorage.getItem("piweb.chatFontSize") ?? "14", 10);
224
254
  if (!Number.isNaN(chatFs)) setChatFontSize(chatFs);
255
+ // 流式输出偏好与主题/字体同源(localStorage),同样等挂载后再读,避免水合不一致
256
+ setStream(getStreamSettings());
225
257
  setEnterBehavior(getEnterBehavior());
226
258
  void loadRemoteSettings();
227
259
  }, []);
@@ -254,6 +286,9 @@ function GeneralSection({
254
286
  document.documentElement.style.setProperty("--piweb-chat-font-size", `${v}px`);
255
287
  };
256
288
 
289
+ /** 流式输出:样式 / 速度 / 光标。改完即持久化,正在流出的回答下一帧就换(订阅在 MarkdownBlock 里) */
290
+ const pickStream = (patch: Partial<StreamSettings>) => setStream(setStreamSettings(patch));
291
+
257
292
  const pickEnter = (v: EnterBehavior) => {
258
293
  setEnterBehavior(v);
259
294
  saveEnterBehavior(v);
@@ -316,6 +351,20 @@ function GeneralSection({
316
351
  { id: "pebrel", label: t.themePebrel, light: "#F3F4F6", dark: "#0F111A", accent: "#52A8FF" },
317
352
  ];
318
353
 
354
+ const streamStyleText: Record<StreamStyle, { label: string; desc: string }> = {
355
+ smooth: { label: t.streamStyleSmooth, desc: t.streamStyleSmoothDesc },
356
+ typewriter: { label: t.streamStyleTypewriter, desc: t.streamStyleTypewriterDesc },
357
+ fade: { label: t.streamStyleFade, desc: t.streamStyleFadeDesc },
358
+ instant: { label: t.streamStyleInstant, desc: t.streamStyleInstantDesc },
359
+ };
360
+ const streamSpeedText: Record<number, string> = {
361
+ 1: t.streamSpeed1,
362
+ 2: t.streamSpeed2,
363
+ 3: t.streamSpeed3,
364
+ 4: t.streamSpeed4,
365
+ 5: t.streamSpeed5,
366
+ };
367
+
319
368
  return (
320
369
  <div className="flex flex-col gap-6">
321
370
  {(settingsLoading || settingsError) && (
@@ -405,6 +454,79 @@ function GeneralSection({
405
454
  <Row title={t.fontMonoFamily} desc={t.fontMonoFamilyDesc}>
406
455
  <FontFamilyPicker scope="mono" />
407
456
  </Row>
457
+ <RowColumn title={t.streamStyleTitle} desc={t.streamStyleDesc}>
458
+ <div className="flex w-full flex-wrap gap-2">
459
+ {STREAM_STYLES.map((id) => {
460
+ const active = stream.style === id;
461
+ return (
462
+ <button
463
+ key={id}
464
+ type="button"
465
+ className="flex flex-col items-start gap-1 text-left transition-colors"
466
+ style={{
467
+ flex: "1 1 150px",
468
+ padding: "12px 16px",
469
+ borderRadius: 14,
470
+ border: `0.5px solid ${active ? "var(--dsw-border-strong)" : "var(--dsw-border-l4)"}`,
471
+ background: active ? "var(--dsw-module-platform)" : "transparent",
472
+ color: "var(--dsw-label-primary)",
473
+ lineHeight: "18px",
474
+ }}
475
+ aria-pressed={active}
476
+ onClick={() => pickStream({ style: id })}
477
+ >
478
+ <span className="flex items-center gap-1.5" style={{ fontSize: 14 }}>
479
+ {active ? <IconCheckOutline14 size={13} /> : null}
480
+ {streamStyleText[id].label}
481
+ </span>
482
+ <span style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>{streamStyleText[id].desc}</span>
483
+ </button>
484
+ );
485
+ })}
486
+ </div>
487
+ </RowColumn>
488
+ <RowColumn title={t.streamSpeedTitle} desc={t.streamSpeedDesc}>
489
+ <div className="flex w-full flex-wrap gap-2">
490
+ {Array.from({ length: STREAM_SPEED_MAX - STREAM_SPEED_MIN + 1 }, (_, i) => STREAM_SPEED_MIN + i).map((level) => {
491
+ const active = stream.speed === level;
492
+ return (
493
+ <button
494
+ key={level}
495
+ type="button"
496
+ className="transition-colors disabled:opacity-50"
497
+ // 即时样式不排队,速度对它没有意义:置灰,免得以为调了没反应
498
+ disabled={stream.style === "instant"}
499
+ style={{
500
+ flex: "1 1 84px",
501
+ padding: "10px 8px",
502
+ borderRadius: 12,
503
+ border: `0.5px solid ${active ? "var(--dsw-border-strong)" : "var(--dsw-border-l4)"}`,
504
+ background: active ? "var(--dsw-module-platform)" : "transparent",
505
+ color: active ? "var(--dsw-label-primary)" : "var(--dsw-label-secondary)",
506
+ fontSize: 13,
507
+ }}
508
+ aria-pressed={active}
509
+ onClick={() => pickStream({ speed: level })}
510
+ >
511
+ {streamSpeedText[level]}
512
+ </button>
513
+ );
514
+ })}
515
+ </div>
516
+ </RowColumn>
517
+ <Row title={t.streamCaretTitle} desc={t.streamCaretDesc}>
518
+ <button
519
+ type="button"
520
+ className="relative h-5 w-9 flex-none rounded-full transition-colors"
521
+ style={{ background: stream.caret ? "var(--dsw-accent)" : "var(--dsw-border-l3)" }}
522
+ role="switch"
523
+ aria-checked={stream.caret}
524
+ aria-label={t.streamCaretTitle}
525
+ onClick={() => pickStream({ caret: !stream.caret })}
526
+ >
527
+ <span className="absolute top-0.5 h-4 w-4 rounded-full bg-white transition-all" style={{ left: stream.caret ? 18 : 2 }} />
528
+ </button>
529
+ </Row>
408
530
  <Row title={t.enterBehavior} desc={t.enterBehaviorDesc}>
409
531
  <SelectOption
410
532
  value={enterBehavior}
@@ -479,61 +601,20 @@ function VersionLink({ href, value }: { href: string; value?: string }) {
479
601
  );
480
602
  }
481
603
 
482
- type UpdatePhase = "idle" | "checking" | "available" | "updating" | "latest" | "updated" | "error";
483
-
484
604
  /** 检查更新 / 更新控件:npm 安装的 piweb 走 npm install -g @rexvane/piweb@latest(服务端固定参数),
485
- * Git 检出仍走 git pull + 本地构建;pi 走 npm install @latest。 */
486
- function UpdateControl({ target }: { target: "piweb" | "pi" }) {
605
+ * Git 检出仍走 git pull + 本地构建;pi 走 npm install @latest(npm 安装只换依赖、不重建)。
606
+ * 状态和右下角提示条共用(update-store):启动检查的结果一到,这里直接显示「更新到 vX」或「已是最新」。 */
607
+ function UpdateControl({ target }: { target: UpdateTarget }) {
487
608
  const { t } = useI18n();
488
- const [phase, setPhase] = useState<UpdatePhase>("idle");
489
- const [latest, setLatest] = useState("");
490
- const [command, setCommand] = useState("");
491
- const [error, setError] = useState("");
609
+ const { phase, latest, command, error, busy: sessionBusy } = useUpdates().targets[target];
492
610
  const busy = phase === "checking" || phase === "updating";
493
611
 
494
- const post = async (action: "check" | "update") => {
495
- const r = await fetch("/api/update", {
496
- method: "POST",
497
- headers: { "Content-Type": "application/json" },
498
- body: JSON.stringify({ target, action }),
499
- });
500
- return r.json();
501
- };
502
-
503
- const check = async () => {
504
- setPhase("checking");
505
- setError("");
506
- try {
507
- const j = await post("check");
508
- if (!j.success) throw new Error(j.error);
509
- if (j.data.canUpdate) {
510
- setLatest(j.data.latest);
511
- setCommand(j.data.command ?? "");
512
- setPhase("available");
513
- } else {
514
- setPhase("latest");
515
- }
516
- } catch (e: any) {
517
- setError(String(e?.message ?? e));
518
- setPhase("error");
519
- }
520
- };
521
-
522
- const run = async () => {
523
- setPhase("updating");
524
- try {
525
- const j = await post("update");
526
- if (!j.success) throw new Error(j.error);
527
- setLatest(j.data.version ?? latest);
528
- setPhase("updated");
529
- } catch (e: any) {
530
- setError(String(e?.message ?? e));
531
- setPhase("error");
532
- }
533
- };
612
+ useEffect(() => {
613
+ void loadUpdates();
614
+ }, []);
534
615
 
535
616
  if (phase === "idle" || phase === "latest" || phase === "error") {
536
- const label = phase === "latest" ? t.upToDate : phase === "error" ? t.updateFailed : t.checkUpdate;
617
+ const label = phase === "latest" ? t.upToDate : phase === "error" ? (sessionBusy ? t.updateBusy : t.updateFailed) : t.checkUpdate;
537
618
  const color = phase === "latest" ? "var(--dsw-label-tertiary)" : phase === "error" ? "var(--dsw-danger)" : "var(--dsw-label-primary)";
538
619
  return (
539
620
  <button
@@ -542,7 +623,7 @@ function UpdateControl({ target }: { target: "piweb" | "pi" }) {
542
623
  style={{ height: 26, padding: "0 10px", fontSize: 12, color }}
543
624
  title={error || undefined}
544
625
  disabled={busy}
545
- onClick={() => void check()}
626
+ onClick={() => void checkUpdate(target)}
546
627
  >
547
628
  {label}
548
629
  </button>
@@ -553,19 +634,24 @@ function UpdateControl({ target }: { target: "piweb" | "pi" }) {
553
634
  if (command) {
554
635
  return (
555
636
  <span className="flex flex-col items-end gap-0.5" style={{ maxWidth: 300 }}>
556
- <code style={{ fontSize: 11, color: "var(--dsw-label-primary)", whiteSpace: "nowrap", overflowX: "auto", maxWidth: "100%" }}>{command}</code>
637
+ <CopyCommand command={command} />
557
638
  <span style={{ fontSize: 11, color: "var(--dsw-label-tertiary)", textAlign: "right" }}>{t.updateManualHint}</span>
558
639
  </span>
559
640
  );
560
641
  }
561
642
  return (
562
- <button type="button" className="btn-primary-white" style={{ height: 26, padding: "0 12px", fontSize: 12 }} onClick={() => void run()}>
643
+ <button type="button" className="btn-primary-white" style={{ height: 26, padding: "0 12px", fontSize: 12 }} onClick={() => void runUpdate(target)}>
563
644
  {t.updateTo.replace("{v}", latest)}
564
645
  </button>
565
646
  );
566
647
  }
567
648
  if (phase === "updated") {
568
- return <span style={{ fontSize: 12, color: "var(--dsw-success)", maxWidth: 260, textAlign: "right" }}>{t.updatedTo.replace("{v}", latest)}</span>;
649
+ return (
650
+ <span className="flex flex-col items-end gap-0.5" style={{ maxWidth: 280, textAlign: "right" }}>
651
+ <span style={{ fontSize: 12, color: "var(--dsw-success)" }}>{t.updatedTo.replace("{v}", latest)}</span>
652
+ <span style={{ fontSize: 11, color: "var(--dsw-label-tertiary)" }}>{t.updateRestartHint}</span>
653
+ </span>
654
+ );
569
655
  }
570
656
  return (
571
657
  <span className="flex items-center gap-1.5" style={{ fontSize: 12, color: "var(--dsw-label-tertiary)" }}>
@@ -802,12 +888,163 @@ function FontFamilyPicker({ scope }: { scope: "ui" | "mono" }) {
802
888
 
803
889
  // ---------- Models ----------
804
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
+ {gap.missing.includes("input") ? (
1003
+ <select
1004
+ className="pw-audit-field"
1005
+ value={rowOf(gap).vision}
1006
+ onChange={(event) => edit(gap, { vision: event.target.value as AuditRow["vision"] })}
1007
+ aria-label={`${gap.id} ${t.modelAuditColVision}`}
1008
+ disabled={busy}
1009
+ >
1010
+ <option value="">{t.visionUnset}</option>
1011
+ <option value="image">{t.visionOn}</option>
1012
+ <option value="text">{t.visionOff}</option>
1013
+ </select>
1014
+ ) : (
1015
+ <span style={{ color: "var(--dsw-label-secondary)" }}>{gap.vision ? t.visionOn : t.visionOff}</span>
1016
+ )}
1017
+ </div>
1018
+ </div>
1019
+ ))}
1020
+ </div>
1021
+ </div>
1022
+
1023
+ <div className="flex flex-wrap items-center gap-2">
1024
+ {visionGaps.length > 0 ? (
1025
+ <>
1026
+ <span style={{ fontSize: 12, color: "var(--dsw-label-caption)" }}>{t.modelAuditVisionQuick}</span>
1027
+ <button className="btn-outline" style={{ height: 28, padding: "0 11px", fontSize: 12.5 }} disabled={busy} onClick={() => markVision("image")}>{t.modelAuditVisionOn}</button>
1028
+ <button className="btn-outline" style={{ height: 28, padding: "0 11px", fontSize: 12.5 }} disabled={busy} onClick={() => markVision("text")}>{t.modelAuditVisionOff}</button>
1029
+ </>
1030
+ ) : null}
1031
+ <span className="flex-1" />
1032
+ <button className="btn-primary-white" style={{ height: 32 }} disabled={busy || invalid || fills.length === 0} onClick={() => void save()}>
1033
+ {busy ? t.saving : fills.length ? t.modelAuditSave.replace("{n}", String(fills.length)) : t.modelAuditSaveIdle}
1034
+ </button>
1035
+ </div>
1036
+ </section>
1037
+ );
1038
+ }
1039
+
805
1040
  function ModelsSection() {
806
1041
  const { t } = useI18n();
807
1042
  const [providers, setProviders] = useState<ProviderView[]>([]);
808
1043
  const [custom, setCustom] = useState<{ providers: Record<string, any>; [key: string]: unknown }>({ providers: {} });
809
1044
  const [customRevision, setCustomRevision] = useState<string | null>(null);
810
1045
  const [secretProviderIds, setSecretProviderIds] = useState<Set<string>>(new Set());
1046
+ // 内置 provider → 目录里的模型 ID:编辑时同 ID 的行写 modelOverrides,「获取可用模型」里不再可选
1047
+ const [catalogModelIds, setCatalogModelIds] = useState<Record<string, string[]>>({});
811
1048
  const [editing, setEditing] = useState<string | null>(null);
812
1049
  const [keyDraft, setKeyDraft] = useState("");
813
1050
  const [adding, setAdding] = useState<"builtin" | "custom" | null>(null);
@@ -886,6 +1123,7 @@ function ModelsSection() {
886
1123
  throw new Error("invalid model configuration");
887
1124
  }
888
1125
  setProviders(j.data.providers);
1126
+ setCatalogModelIds(j.data.catalogModelIds ?? {});
889
1127
  setSecretProviderIds(new Set(j.data.customProviders?.secretProviderIds ?? []));
890
1128
  setCustom({ ...parsed, providers: parsed.providers ?? {} });
891
1129
  setCustomRevision(typeof j.data.customProviders?.revision === "string" ? j.data.customProviders.revision : null);
@@ -1068,6 +1306,10 @@ function ModelsSection() {
1068
1306
  }
1069
1307
  };
1070
1308
 
1309
+ // 体检:直接从已载入的配置算(不另发请求),每次保存后 load() 重新载入就自动刷新
1310
+ const gaps = useMemo(() => customModelGaps(custom.providers ?? {}), [custom]);
1311
+ const [auditBusy, setAuditBusy] = useState(false);
1312
+
1071
1313
  const call = async (body: Record<string, unknown>) => {
1072
1314
  const j = await request(body);
1073
1315
  notify(j.success, j.success && j.data?.requiresSessionReopen ? t.modelConfigReopen : j.error);
@@ -1112,6 +1354,18 @@ function ModelsSection() {
1112
1354
  return call({ action: "saveCustomProviders", revision: customRevision, content: JSON.stringify({ ...custom, providers: content }, null, 2) });
1113
1355
  };
1114
1356
 
1357
+ /** 体检卡片的保存:只补空字段,走与编辑 provider 同一条写入(revision 乐观锁、密钥由服务端从原文件恢复) */
1358
+ const saveAuditFills = async (fills: ModelFieldFill[]): Promise<boolean> => {
1359
+ const { providers: next, filled } = fillModelFields(custom.providers ?? {}, fills);
1360
+ if (!filled) return true;
1361
+ setAuditBusy(true);
1362
+ try {
1363
+ return Boolean((await saveCustom(next)).success);
1364
+ } finally {
1365
+ setAuditBusy(false);
1366
+ }
1367
+ };
1368
+
1115
1369
  const providerName = (id: string) => providers.find((p) => p.id === id)?.name ?? id;
1116
1370
  const credentialSource = (provider: ProviderView) => {
1117
1371
  const source = provider.authSource;
@@ -1141,10 +1395,28 @@ function ModelsSection() {
1141
1395
  setCName(typeof override?.name === "string" ? override.name : "");
1142
1396
  setCModels((override?.models ?? []).map(modelDraftFromConfig));
1143
1397
  } else {
1144
- // 内置 provider:从 models.json 覆盖层初始化(若有)
1398
+ // 内置 provider:从 models.json 覆盖层初始化(若有),models[] 定义和 modelOverrides 覆盖都作为行
1145
1399
  setBBaseUrl(typeof override?.baseUrl === "string" ? override.baseUrl : "");
1146
- setBModels((override?.models ?? []).map(modelDraftFromConfig));
1400
+ setBModels(modelDraftsFromProviderConfig(override));
1401
+ }
1402
+ };
1403
+
1404
+ const catalogIdsOf = (providerId: string): ReadonlySet<string> => new Set(catalogModelIds[providerId] ?? []);
1405
+
1406
+ /**
1407
+ * 内置 provider 的条目写回:什么都没覆盖时删掉条目(SDK 不认空条目,会让整份 models.json 存不进去)。
1408
+ * 前端拿到的配置不含 apiKey,存了密钥的条目不能当成空的删掉。
1409
+ * 返回 null = models.json 不用动(原本就没有条目、这次也没覆盖任何东西,比如只填了密钥)。
1410
+ */
1411
+ const withBuiltinEntry = (providerId: string, config: Record<string, unknown>): Record<string, any> | null => {
1412
+ const next = { ...custom.providers };
1413
+ if (isInertProviderConfig(config) && !secretProviderIds.has(providerId)) {
1414
+ if (!Object.prototype.hasOwnProperty.call(next, providerId)) return null;
1415
+ delete next[providerId];
1416
+ } else {
1417
+ next[providerId] = config;
1147
1418
  }
1419
+ return next;
1148
1420
  };
1149
1421
 
1150
1422
  const saveCustomEdit = async () => {
@@ -1178,12 +1450,12 @@ function ModelsSection() {
1178
1450
  if (invalid) return;
1179
1451
  setEditBusy(true);
1180
1452
  try {
1181
- const next = { ...custom.providers };
1182
- next[provider.id] = serializeProviderDraft(next[provider.id] ?? {}, {
1453
+ const config = serializeProviderDraft(custom.providers[provider.id] ?? {}, {
1183
1454
  baseUrl: bBaseUrl,
1184
1455
  models: bModels,
1185
- }, provider.apis[0] ?? "openai-completions");
1186
- const j = await saveCustom(next);
1456
+ }, provider.apis[0] ?? "openai-completions", catalogIdsOf(provider.id));
1457
+ const next = withBuiltinEntry(provider.id, config);
1458
+ const j = next ? await saveCustom(next) : { success: true };
1187
1459
  if (j.success && keyDraft.trim()) {
1188
1460
  void call({ action: "setKey", providerId: provider.id, apiKey: keyDraft.trim() });
1189
1461
  setOauthDone(null);
@@ -1214,8 +1486,9 @@ function ModelsSection() {
1214
1486
  };
1215
1487
 
1216
1488
  const saveBuiltinSetup = async ({ providerId, apiKey, config }: BuiltinProviderSetup) => {
1217
- const next = { ...custom.providers, [providerId]: { ...custom.providers[providerId], ...config } };
1218
- const result = await saveCustom(next);
1489
+ const next = withBuiltinEntry(providerId, { ...custom.providers[providerId], ...config });
1490
+ // 只填了密钥:models.json 不需要条目,直接去存密钥
1491
+ const result = next ? await saveCustom(next) : { success: true };
1219
1492
  if (result.success && apiKey) {
1220
1493
  // 多步认证(Vertex/Bedrock/Cloudflare 等)的后续提示经登录弹窗应答;
1221
1494
  // 不能 await setKey —— 它会挂起到全部提示答完。
@@ -1398,7 +1671,7 @@ function ModelsSection() {
1398
1671
  });
1399
1672
  const result = await response.json();
1400
1673
  if (!result.success) throw new Error(result.error || t.toastError);
1401
- return (result.data.models ?? []).map((model: { id: string; name?: string }) => ({ id: model.id, name: model.name ?? "" }));
1674
+ return (result.data.models ?? []).map(modelDraftFromCatalog);
1402
1675
  }}
1403
1676
  discoverDisabled={!cBaseUrl.trim()}
1404
1677
  />
@@ -1445,6 +1718,7 @@ function ModelsSection() {
1445
1718
  setModels={setBModels}
1446
1719
  updateModel={(index, patch) => setBModels((current) => current.map((model, at) => (at === index ? { ...model, ...patch } : model)))}
1447
1720
  inheritedCount={p.modelCount}
1721
+ catalogIds={catalogIdsOf(p.id)}
1448
1722
  discover={async (allowPrivate) => {
1449
1723
  const response = await fetch("/api/models", {
1450
1724
  method: "POST",
@@ -1454,7 +1728,7 @@ function ModelsSection() {
1454
1728
  });
1455
1729
  const result = await response.json();
1456
1730
  if (!result.success) throw new Error(result.error || t.toastError);
1457
- return (result.data.models ?? []).map((model: { id: string; name?: string }) => ({ id: model.id, name: model.name ?? "" }));
1731
+ return (result.data.models ?? []).map(modelDraftFromCatalog);
1458
1732
  }}
1459
1733
  discoverDisabled={!(bBaseUrl.trim() || p.baseUrl)}
1460
1734
  />
@@ -1497,6 +1771,11 @@ function ModelsSection() {
1497
1771
  })}
1498
1772
  </div>
1499
1773
 
1774
+ {/* 模型配置体检:没写的字段 pi 会按缺省处理;由用户填,不替他猜 */}
1775
+ {gaps.length > 0 ? (
1776
+ <ModelFieldAudit gaps={gaps} providerLabel={providerName} busy={auditBusy || customRevision === null} onSave={saveAuditFills} />
1777
+ ) : null}
1778
+
1500
1779
  {/* 两个添加按钮 */}
1501
1780
  <div className="flex gap-3">
1502
1781
  <button
@@ -1525,6 +1804,7 @@ function ModelsSection() {
1525
1804
  <ProviderSetupModal
1526
1805
  mode={adding}
1527
1806
  providers={addable}
1807
+ catalogModelIds={catalogModelIds}
1528
1808
  existingIds={providers.map((provider) => provider.id)}
1529
1809
  onClose={() => setAdding(null)}
1530
1810
  onSaveBuiltin={saveBuiltinSetup}
@@ -1651,12 +1931,13 @@ function ModelsSection() {
1651
1931
 
1652
1932
  </div>
1653
1933
  );
1654
- }const fieldCls = "w-full rounded-xl px-3 py-2";
1934
+ }
1655
1935
 
1656
- /** 配额展示:token 数转 M(53,000,000 → 53M),小数值原样 */
1936
+ const fieldCls = "w-full rounded-xl px-3 py-2";
1937
+
1938
+ /** 配额展示:token 数转换单位(53,000,000 → 53.00M),与统计条 / 上下文弹窗口径一致 */
1657
1939
  function fmtQuotaTokens(value: number): string {
1658
- if (value >= 1_000_000) return `${Math.round(value / 100_000) / 10}M`;
1659
- return value.toLocaleString();
1940
+ return fmtTokens(value);
1660
1941
  }
1661
1942
 
1662
1943
  /** 套餐值来自服务端声明(如 "pro"/"plus"),首字母大写后直接跟在标签后显示。 */
@@ -1925,7 +2206,8 @@ function SkillsSection({ cwd, onOpenFileContent }: { cwd: string; onOpenFileCont
1925
2206
 
1926
2207
  // ---------- Plugins ----------
1927
2208
 
1928
- function PluginsSection({ cwd }: { cwd: string }) {
2209
+ /** 导出仅供测试(其余 section 不导出) */
2210
+ export function PluginsSection({ cwd }: { cwd: string }) {
1929
2211
  const { t, lang } = useI18n();
1930
2212
  const confirmUninstall = (source: string) => window.confirm(t.confirmUninstallPackage.replace("{source}", source));
1931
2213
  const [tab, setTab] = useState<"config" | "list">("config");
@@ -2042,13 +2324,19 @@ function PluginsSection({ cwd }: { cwd: string }) {
2042
2324
  })),
2043
2325
  ].filter((x) => !query || x.name.toLowerCase().includes(query));
2044
2326
 
2045
- const badge = (
2327
+ /** 扩展卡片的启用状态:每张卡只在头部显示这一处,展开区不重复 */
2328
+ const stateBadge = (disabled: boolean) => (
2046
2329
  <span
2047
2330
  className="flex flex-none items-center gap-1.5 rounded-md px-1.5 py-0.5"
2048
- style={{ fontSize: 10.5, background: "rgba(34,197,94,.12)", color: "var(--dsw-success)" }}
2331
+ style={{
2332
+ fontSize: 10.5,
2333
+ ...(disabled
2334
+ ? { background: "var(--dsw-selector)", color: "var(--dsw-label-tertiary)" }
2335
+ : { background: "rgba(34,197,94,.12)", color: "var(--dsw-success)" }),
2336
+ }}
2049
2337
  >
2050
- <span className="h-1.5 w-1.5 rounded-full" style={{ background: "var(--dsw-success)" }} />
2051
- {t.enabled}
2338
+ <span className="h-1.5 w-1.5 rounded-full" style={{ background: disabled ? "var(--dsw-label-tertiary)" : "var(--dsw-success)" }} />
2339
+ {disabled ? t.disabled : t.enabled}
2052
2340
  </span>
2053
2341
  );
2054
2342
 
@@ -2165,9 +2453,7 @@ function PluginsSection({ cwd }: { cwd: string }) {
2165
2453
  {e.error && (
2166
2454
  <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>
2167
2455
  )}
2168
- {e.disabled && !e.error && (
2169
- <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>
2170
- )}
2456
+ {!e.error && stateBadge(e.disabled === true)}
2171
2457
  </div>
2172
2458
  <div className="truncate" style={{ fontSize: 12, color: e.error ? "var(--dsw-danger)" : "var(--dsw-label-caption)" }} title={e.error ?? e.path}>
2173
2459
  {e.error ?? e.path}
@@ -2189,7 +2475,6 @@ function PluginsSection({ cwd }: { cwd: string }) {
2189
2475
  >
2190
2476
  {scopeLabel(scope)}
2191
2477
  </span>
2192
- {badge}
2193
2478
  </div>
2194
2479
  <div className="break-all" style={{ fontFamily: "var(--font-mono)", fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
2195
2480
  {e.path}
@@ -2272,7 +2557,7 @@ function PluginsSection({ cwd }: { cwd: string }) {
2272
2557
  <span className="h-3 w-3 flex-none animate-spin rounded-full border border-[var(--dsw-label-tertiary)] border-t-transparent" />
2273
2558
  ) : !isDisabled ? (
2274
2559
  <span
2275
- className="h-2 w-2 flex-none rounded-full"
2560
+ className="h-2 w-2 flex-none rounded-full"
2276
2561
  style={{ background: "#22c55e", boxShadow: "0 0 5px rgba(34, 197, 94, 0.4)" }}
2277
2562
  />
2278
2563
  ) : null}
@@ -2286,8 +2571,8 @@ function PluginsSection({ cwd }: { cwd: string }) {
2286
2571
  >
2287
2572
  {isDisabled ? t.disabled : t.enabled}
2288
2573
  </span>
2289
- <IconChevronDown14
2290
- size={13}
2574
+ <IconChevronDown14
2575
+ size={13}
2291
2576
  style={{
2292
2577
  color: "var(--dsw-label-tertiary)",
2293
2578
  transform: isMenuOpen ? "rotate(180deg)" : "none",