@rexvane/piweb 0.3.21 → 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 (293) 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 +10 -10
  8. package/.next/required-server-files.js +16 -1
  9. package/.next/required-server-files.json +16 -1
  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 +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]/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 -1
  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 -2
  113. package/.next/server/chunks/686.js +1 -1
  114. package/.next/server/chunks/813.js +23 -1
  115. package/.next/server/chunks/920.js +2 -2
  116. package/.next/server/chunks/924.js +1 -1
  117. package/.next/server/chunks/970.js +305 -0
  118. package/.next/server/middleware-build-manifest.js +1 -1
  119. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  120. package/.next/server/middleware.js +185 -23
  121. package/.next/server/middleware.js.nft.json +1 -1
  122. package/.next/server/pages/404.html +1 -1
  123. package/.next/server/pages/500.html +1 -1
  124. package/.next/server/preview-props.json +5 -0
  125. package/.next/server/server-reference-manifest.json +1 -1
  126. package/.next/static/WMazYvD-gbeLsCUfUxvvM/_buildManifest.js +1 -0
  127. package/.next/static/chunks/316-698d10eb465bd8ef.js +1 -0
  128. package/.next/static/chunks/3794-0978865cda773008.js +52 -0
  129. package/.next/static/chunks/4673.4cf66cbdd8364c57.js +1 -0
  130. package/.next/static/chunks/4686-09fcd0028079006c.js +1 -0
  131. package/.next/static/chunks/4778.eddd3a5813f85824.js +1 -0
  132. package/.next/static/chunks/4bd1b696-e199e788e58acf79.js +1 -0
  133. package/.next/static/chunks/{5173.91ddb803785009f8.js → 5173.dfd1cbb795b9a21c.js} +1 -1
  134. package/.next/static/chunks/618.949981da99be7182.js +4 -0
  135. package/.next/static/chunks/7411.67366dc5032a141d.js +1 -0
  136. package/.next/static/chunks/app/_global-error/page-f977af7f858990a2.js +1 -0
  137. package/.next/static/chunks/app/api/agent/[id]/events/route-f977af7f858990a2.js +1 -0
  138. package/.next/static/chunks/app/api/agent/[id]/route-f977af7f858990a2.js +1 -0
  139. package/.next/static/chunks/app/api/agent/[id]/tree/route-f977af7f858990a2.js +1 -0
  140. package/.next/static/chunks/app/api/agent/new/route-f977af7f858990a2.js +1 -0
  141. package/.next/static/chunks/app/api/config/route-f977af7f858990a2.js +1 -0
  142. package/.next/static/chunks/app/api/files/route-f977af7f858990a2.js +1 -0
  143. package/.next/static/chunks/app/api/fs/route-f977af7f858990a2.js +1 -0
  144. package/.next/static/chunks/app/api/git/route-f977af7f858990a2.js +1 -0
  145. package/.next/static/chunks/app/api/github/route-f977af7f858990a2.js +1 -0
  146. package/.next/static/chunks/app/api/growth/route-f977af7f858990a2.js +1 -0
  147. package/.next/static/chunks/app/api/health/route-f977af7f858990a2.js +1 -0
  148. package/.next/static/chunks/app/api/models/defaults/route-f977af7f858990a2.js +1 -0
  149. package/.next/static/chunks/app/api/models/route-f977af7f858990a2.js +1 -0
  150. package/.next/static/chunks/app/api/pi-settings/route-f977af7f858990a2.js +1 -0
  151. package/.next/static/chunks/app/api/plugins/route-f977af7f858990a2.js +1 -0
  152. package/.next/static/chunks/app/api/prompts/route-f977af7f858990a2.js +1 -0
  153. package/.next/static/chunks/app/api/remote/route-f977af7f858990a2.js +1 -0
  154. package/.next/static/chunks/app/api/security/route-f977af7f858990a2.js +1 -0
  155. package/.next/static/chunks/app/api/sessions/[id]/export/route-f977af7f858990a2.js +1 -0
  156. package/.next/static/chunks/app/api/sessions/[id]/route-f977af7f858990a2.js +1 -0
  157. package/.next/static/chunks/app/api/sessions/import/route-f977af7f858990a2.js +1 -0
  158. package/.next/static/chunks/app/api/sessions/route-f977af7f858990a2.js +1 -0
  159. package/.next/static/chunks/app/api/skills/route-f977af7f858990a2.js +1 -0
  160. package/.next/static/chunks/app/api/update/route-f977af7f858990a2.js +1 -0
  161. package/.next/static/chunks/app/api/version/route-f977af7f858990a2.js +1 -0
  162. package/.next/static/chunks/app/api/web-auth/route-f977af7f858990a2.js +1 -0
  163. package/.next/static/chunks/app/api/workspaces/route-f977af7f858990a2.js +1 -0
  164. package/.next/static/chunks/app/page-fc4ed2ff29b6235e.js +1 -0
  165. package/.next/static/chunks/framework-47b723d27f4a4963.js +1 -0
  166. package/.next/static/chunks/main-5403f0764579e5fe.js +5 -0
  167. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f977af7f858990a2.js +1 -0
  168. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f977af7f858990a2.js +1 -0
  169. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f977af7f858990a2.js +1 -0
  170. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f977af7f858990a2.js +1 -0
  171. package/.next/static/chunks/webpack-22ebffc20adb44ae.js +1 -0
  172. package/.next/static/css/6671d0d1589fdc0e.css +1 -0
  173. package/.next/static/css/{2959bd6b626b7ffe.css → f15a853af8b5127f.css} +1 -1
  174. package/.next/types/app/api/agent/[id]/events/route.ts +3 -1
  175. package/.next/types/app/api/agent/[id]/route.ts +3 -1
  176. package/.next/types/app/api/agent/[id]/tree/route.ts +3 -1
  177. package/.next/types/app/api/agent/new/route.ts +3 -1
  178. package/.next/types/app/api/config/route.ts +3 -1
  179. package/.next/types/app/api/files/route.ts +3 -1
  180. package/.next/types/app/api/fs/route.ts +3 -1
  181. package/.next/types/app/api/git/route.ts +3 -1
  182. package/.next/types/app/api/github/route.ts +355 -0
  183. package/.next/types/app/api/growth/route.ts +3 -1
  184. package/.next/types/app/api/health/route.ts +3 -1
  185. package/.next/types/app/api/models/defaults/route.ts +355 -0
  186. package/.next/types/app/api/models/route.ts +3 -1
  187. package/.next/types/app/api/pi-settings/route.ts +3 -1
  188. package/.next/types/app/api/plugins/route.ts +3 -1
  189. package/.next/types/app/api/prompts/route.ts +3 -1
  190. package/.next/types/app/api/remote/route.ts +3 -1
  191. package/.next/types/app/api/security/route.ts +3 -1
  192. package/.next/types/app/api/sessions/[id]/export/route.ts +3 -1
  193. package/.next/types/app/api/sessions/[id]/route.ts +3 -1
  194. package/.next/types/app/api/sessions/import/route.ts +3 -1
  195. package/.next/types/app/api/sessions/route.ts +3 -1
  196. package/.next/types/app/api/skills/route.ts +3 -1
  197. package/.next/types/app/api/update/route.ts +3 -1
  198. package/.next/types/app/api/version/route.ts +3 -1
  199. package/.next/types/app/api/web-auth/route.ts +3 -1
  200. package/.next/types/app/api/workspaces/route.ts +3 -1
  201. package/.next/types/app/layout.ts +4 -1
  202. package/.next/types/app/login/page.ts +4 -1
  203. package/.next/types/app/page.ts +4 -1
  204. package/.next/types/cache-life.d.ts +11 -43
  205. package/.next/types/routes.d.ts +3 -1
  206. package/.next/types/validator.ts +76 -2
  207. package/package.json +2 -2
  208. package/src/app/api/files/route.ts +5 -2
  209. package/src/app/api/github/route.ts +26 -0
  210. package/src/app/api/models/defaults/route.ts +18 -0
  211. package/src/app/api/models/route.ts +24 -1
  212. package/src/app/globals.css +186 -0
  213. package/src/components/AppShell.tsx +82 -30
  214. package/src/components/ChatInput.tsx +244 -63
  215. package/src/components/ChatWindow.tsx +18 -27
  216. package/src/components/ContextMeter.tsx +193 -143
  217. package/src/components/GitPanel.tsx +110 -40
  218. package/src/components/ProjectPanel.tsx +55 -30
  219. package/src/components/PromptPanel.tsx +2 -4
  220. package/src/components/ProviderSetupModal.module.css +20 -0
  221. package/src/components/ProviderSetupModal.tsx +134 -29
  222. package/src/components/SettingsPanel.tsx +243 -27
  223. package/src/components/TrajectoryView.tsx +4 -6
  224. package/src/components/composer/ComposerImages.tsx +306 -0
  225. package/src/hooks/useGrowth.ts +3 -0
  226. package/src/hooks/usePiWeb.ts +12 -2
  227. package/src/hooks/useWorkspaceFiles.ts +93 -0
  228. package/src/i18n.tsx +108 -2
  229. package/src/lib/agent-manager.ts +135 -104
  230. package/src/lib/files-service.ts +100 -1
  231. package/src/lib/format.ts +56 -0
  232. package/src/lib/git-service.ts +45 -13
  233. package/src/lib/github-identity.ts +245 -0
  234. package/src/lib/growth-service.ts +6 -2
  235. package/src/lib/hero-model-memory.ts +28 -0
  236. package/src/lib/model-defaults.ts +105 -0
  237. package/src/lib/model-draft.ts +257 -9
  238. package/src/lib/model-endpoint.ts +23 -0
  239. package/src/lib/model-files.ts +33 -0
  240. package/src/lib/models-service.ts +240 -78
  241. package/src/lib/pi.ts +27 -2
  242. package/src/lib/process-format.ts +3 -2
  243. package/src/lib/session/metrics.ts +31 -0
  244. package/src/lib/thinking-level.ts +39 -0
  245. package/src/lib/trajectory.ts +17 -3
  246. package/src/lib/types.ts +12 -1
  247. package/src/lib/workspace-index.ts +270 -0
  248. package/.next/server/chunks/741.js +0 -165
  249. package/.next/static/chunks/316-5ea443ee6b512aa9.js +0 -1
  250. package/.next/static/chunks/3794-3679f1c05160977c.js +0 -52
  251. package/.next/static/chunks/4673.8a5a9e6855350e34.js +0 -1
  252. package/.next/static/chunks/4686-882531aef1d2e250.js +0 -1
  253. package/.next/static/chunks/4778.305dc6abb9889627.js +0 -1
  254. package/.next/static/chunks/4bd1b696-93bf506b236d6248.js +0 -1
  255. package/.next/static/chunks/618.ee5d5bf1c647e483.js +0 -4
  256. package/.next/static/chunks/839.bfd2db36483faa3e.js +0 -1
  257. package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +0 -1
  258. package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +0 -1
  259. package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +0 -1
  260. package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +0 -1
  261. package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +0 -1
  262. package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +0 -1
  263. package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +0 -1
  264. package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +0 -1
  265. package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +0 -1
  266. package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +0 -1
  267. package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +0 -1
  268. package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +0 -1
  269. package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +0 -1
  270. package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +0 -1
  271. package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +0 -1
  272. package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +0 -1
  273. package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +0 -1
  274. package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +0 -1
  275. package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +0 -1
  276. package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +0 -1
  277. package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +0 -1
  278. package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +0 -1
  279. package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +0 -1
  280. package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +0 -1
  281. package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +0 -1
  282. package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +0 -1
  283. package/.next/static/chunks/app/page-c04af72e549695bd.js +0 -1
  284. package/.next/static/chunks/framework-c4e651fe89edfa91.js +0 -1
  285. package/.next/static/chunks/main-6159b22a8cb0b7c5.js +0 -5
  286. package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +0 -1
  287. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +0 -1
  288. package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +0 -1
  289. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +0 -1
  290. package/.next/static/chunks/webpack-9b4bb574e5817654.js +0 -1
  291. package/.next/static/css/ea27b9e30f76303f.css +0 -1
  292. package/.next/static/sNX5GQkOWuK5YEID1hT-C/_buildManifest.js +0 -1
  293. /package/.next/static/{sNX5GQkOWuK5YEID1hT-C → 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 {
@@ -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,163 @@ 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
+ {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
+
874
1040
  function ModelsSection() {
875
1041
  const { t } = useI18n();
876
1042
  const [providers, setProviders] = useState<ProviderView[]>([]);
877
1043
  const [custom, setCustom] = useState<{ providers: Record<string, any>; [key: string]: unknown }>({ providers: {} });
878
1044
  const [customRevision, setCustomRevision] = useState<string | null>(null);
879
1045
  const [secretProviderIds, setSecretProviderIds] = useState<Set<string>>(new Set());
1046
+ // 内置 provider → 目录里的模型 ID:编辑时同 ID 的行写 modelOverrides,「获取可用模型」里不再可选
1047
+ const [catalogModelIds, setCatalogModelIds] = useState<Record<string, string[]>>({});
880
1048
  const [editing, setEditing] = useState<string | null>(null);
881
1049
  const [keyDraft, setKeyDraft] = useState("");
882
1050
  const [adding, setAdding] = useState<"builtin" | "custom" | null>(null);
@@ -955,6 +1123,7 @@ function ModelsSection() {
955
1123
  throw new Error("invalid model configuration");
956
1124
  }
957
1125
  setProviders(j.data.providers);
1126
+ setCatalogModelIds(j.data.catalogModelIds ?? {});
958
1127
  setSecretProviderIds(new Set(j.data.customProviders?.secretProviderIds ?? []));
959
1128
  setCustom({ ...parsed, providers: parsed.providers ?? {} });
960
1129
  setCustomRevision(typeof j.data.customProviders?.revision === "string" ? j.data.customProviders.revision : null);
@@ -1137,6 +1306,10 @@ function ModelsSection() {
1137
1306
  }
1138
1307
  };
1139
1308
 
1309
+ // 体检:直接从已载入的配置算(不另发请求),每次保存后 load() 重新载入就自动刷新
1310
+ const gaps = useMemo(() => customModelGaps(custom.providers ?? {}), [custom]);
1311
+ const [auditBusy, setAuditBusy] = useState(false);
1312
+
1140
1313
  const call = async (body: Record<string, unknown>) => {
1141
1314
  const j = await request(body);
1142
1315
  notify(j.success, j.success && j.data?.requiresSessionReopen ? t.modelConfigReopen : j.error);
@@ -1181,6 +1354,18 @@ function ModelsSection() {
1181
1354
  return call({ action: "saveCustomProviders", revision: customRevision, content: JSON.stringify({ ...custom, providers: content }, null, 2) });
1182
1355
  };
1183
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
+
1184
1369
  const providerName = (id: string) => providers.find((p) => p.id === id)?.name ?? id;
1185
1370
  const credentialSource = (provider: ProviderView) => {
1186
1371
  const source = provider.authSource;
@@ -1210,10 +1395,28 @@ function ModelsSection() {
1210
1395
  setCName(typeof override?.name === "string" ? override.name : "");
1211
1396
  setCModels((override?.models ?? []).map(modelDraftFromConfig));
1212
1397
  } else {
1213
- // 内置 provider:从 models.json 覆盖层初始化(若有)
1398
+ // 内置 provider:从 models.json 覆盖层初始化(若有),models[] 定义和 modelOverrides 覆盖都作为行
1214
1399
  setBBaseUrl(typeof override?.baseUrl === "string" ? override.baseUrl : "");
1215
- 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;
1216
1418
  }
1419
+ return next;
1217
1420
  };
1218
1421
 
1219
1422
  const saveCustomEdit = async () => {
@@ -1247,12 +1450,12 @@ function ModelsSection() {
1247
1450
  if (invalid) return;
1248
1451
  setEditBusy(true);
1249
1452
  try {
1250
- const next = { ...custom.providers };
1251
- next[provider.id] = serializeProviderDraft(next[provider.id] ?? {}, {
1453
+ const config = serializeProviderDraft(custom.providers[provider.id] ?? {}, {
1252
1454
  baseUrl: bBaseUrl,
1253
1455
  models: bModels,
1254
- }, provider.apis[0] ?? "openai-completions");
1255
- 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 };
1256
1459
  if (j.success && keyDraft.trim()) {
1257
1460
  void call({ action: "setKey", providerId: provider.id, apiKey: keyDraft.trim() });
1258
1461
  setOauthDone(null);
@@ -1283,8 +1486,9 @@ function ModelsSection() {
1283
1486
  };
1284
1487
 
1285
1488
  const saveBuiltinSetup = async ({ providerId, apiKey, config }: BuiltinProviderSetup) => {
1286
- const next = { ...custom.providers, [providerId]: { ...custom.providers[providerId], ...config } };
1287
- 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 };
1288
1492
  if (result.success && apiKey) {
1289
1493
  // 多步认证(Vertex/Bedrock/Cloudflare 等)的后续提示经登录弹窗应答;
1290
1494
  // 不能 await setKey —— 它会挂起到全部提示答完。
@@ -1467,7 +1671,7 @@ function ModelsSection() {
1467
1671
  });
1468
1672
  const result = await response.json();
1469
1673
  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 ?? "" }));
1674
+ return (result.data.models ?? []).map(modelDraftFromCatalog);
1471
1675
  }}
1472
1676
  discoverDisabled={!cBaseUrl.trim()}
1473
1677
  />
@@ -1514,6 +1718,7 @@ function ModelsSection() {
1514
1718
  setModels={setBModels}
1515
1719
  updateModel={(index, patch) => setBModels((current) => current.map((model, at) => (at === index ? { ...model, ...patch } : model)))}
1516
1720
  inheritedCount={p.modelCount}
1721
+ catalogIds={catalogIdsOf(p.id)}
1517
1722
  discover={async (allowPrivate) => {
1518
1723
  const response = await fetch("/api/models", {
1519
1724
  method: "POST",
@@ -1523,7 +1728,7 @@ function ModelsSection() {
1523
1728
  });
1524
1729
  const result = await response.json();
1525
1730
  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 ?? "" }));
1731
+ return (result.data.models ?? []).map(modelDraftFromCatalog);
1527
1732
  }}
1528
1733
  discoverDisabled={!(bBaseUrl.trim() || p.baseUrl)}
1529
1734
  />
@@ -1566,6 +1771,11 @@ function ModelsSection() {
1566
1771
  })}
1567
1772
  </div>
1568
1773
 
1774
+ {/* 模型配置体检:没写的字段 pi 会按缺省处理;由用户填,不替他猜 */}
1775
+ {gaps.length > 0 ? (
1776
+ <ModelFieldAudit gaps={gaps} providerLabel={providerName} busy={auditBusy || customRevision === null} onSave={saveAuditFills} />
1777
+ ) : null}
1778
+
1569
1779
  {/* 两个添加按钮 */}
1570
1780
  <div className="flex gap-3">
1571
1781
  <button
@@ -1594,6 +1804,7 @@ function ModelsSection() {
1594
1804
  <ProviderSetupModal
1595
1805
  mode={adding}
1596
1806
  providers={addable}
1807
+ catalogModelIds={catalogModelIds}
1597
1808
  existingIds={providers.map((provider) => provider.id)}
1598
1809
  onClose={() => setAdding(null)}
1599
1810
  onSaveBuiltin={saveBuiltinSetup}
@@ -1720,12 +1931,13 @@ function ModelsSection() {
1720
1931
 
1721
1932
  </div>
1722
1933
  );
1723
- }const fieldCls = "w-full rounded-xl px-3 py-2";
1934
+ }
1935
+
1936
+ const fieldCls = "w-full rounded-xl px-3 py-2";
1724
1937
 
1725
- /** 配额展示:token 数转 M(53,000,000 → 53M),小数值原样 */
1938
+ /** 配额展示:token 数转换单位(53,000,000 → 53.00M),与统计条 / 上下文弹窗口径一致 */
1726
1939
  function fmtQuotaTokens(value: number): string {
1727
- if (value >= 1_000_000) return `${Math.round(value / 100_000) / 10}M`;
1728
- return value.toLocaleString();
1940
+ return fmtTokens(value);
1729
1941
  }
1730
1942
 
1731
1943
  /** 套餐值来自服务端声明(如 "pro"/"plus"),首字母大写后直接跟在标签后显示。 */
@@ -1994,7 +2206,8 @@ function SkillsSection({ cwd, onOpenFileContent }: { cwd: string; onOpenFileCont
1994
2206
 
1995
2207
  // ---------- Plugins ----------
1996
2208
 
1997
- function PluginsSection({ cwd }: { cwd: string }) {
2209
+ /** 导出仅供测试(其余 section 不导出) */
2210
+ export function PluginsSection({ cwd }: { cwd: string }) {
1998
2211
  const { t, lang } = useI18n();
1999
2212
  const confirmUninstall = (source: string) => window.confirm(t.confirmUninstallPackage.replace("{source}", source));
2000
2213
  const [tab, setTab] = useState<"config" | "list">("config");
@@ -2111,13 +2324,19 @@ function PluginsSection({ cwd }: { cwd: string }) {
2111
2324
  })),
2112
2325
  ].filter((x) => !query || x.name.toLowerCase().includes(query));
2113
2326
 
2114
- const badge = (
2327
+ /** 扩展卡片的启用状态:每张卡只在头部显示这一处,展开区不重复 */
2328
+ const stateBadge = (disabled: boolean) => (
2115
2329
  <span
2116
2330
  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)" }}
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
+ }}
2118
2337
  >
2119
- <span className="h-1.5 w-1.5 rounded-full" style={{ background: "var(--dsw-success)" }} />
2120
- {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}
2121
2340
  </span>
2122
2341
  );
2123
2342
 
@@ -2234,9 +2453,7 @@ function PluginsSection({ cwd }: { cwd: string }) {
2234
2453
  {e.error && (
2235
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>
2236
2455
  )}
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
- )}
2456
+ {!e.error && stateBadge(e.disabled === true)}
2240
2457
  </div>
2241
2458
  <div className="truncate" style={{ fontSize: 12, color: e.error ? "var(--dsw-danger)" : "var(--dsw-label-caption)" }} title={e.error ?? e.path}>
2242
2459
  {e.error ?? e.path}
@@ -2258,7 +2475,6 @@ function PluginsSection({ cwd }: { cwd: string }) {
2258
2475
  >
2259
2476
  {scopeLabel(scope)}
2260
2477
  </span>
2261
- {badge}
2262
2478
  </div>
2263
2479
  <div className="break-all" style={{ fontFamily: "var(--font-mono)", fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
2264
2480
  {e.path}
@@ -2341,7 +2557,7 @@ function PluginsSection({ cwd }: { cwd: string }) {
2341
2557
  <span className="h-3 w-3 flex-none animate-spin rounded-full border border-[var(--dsw-label-tertiary)] border-t-transparent" />
2342
2558
  ) : !isDisabled ? (
2343
2559
  <span
2344
- className="h-2 w-2 flex-none rounded-full"
2560
+ className="h-2 w-2 flex-none rounded-full"
2345
2561
  style={{ background: "#22c55e", boxShadow: "0 0 5px rgba(34, 197, 94, 0.4)" }}
2346
2562
  />
2347
2563
  ) : null}
@@ -2355,8 +2571,8 @@ function PluginsSection({ cwd }: { cwd: string }) {
2355
2571
  >
2356
2572
  {isDisabled ? t.disabled : t.enabled}
2357
2573
  </span>
2358
- <IconChevronDown14
2359
- size={13}
2574
+ <IconChevronDown14
2575
+ size={13}
2360
2576
  style={{
2361
2577
  color: "var(--dsw-label-tertiary)",
2362
2578
  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