@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
@@ -84,6 +84,12 @@ function roundColor(round: GrowthEntry): string {
84
84
  return "var(--dsw-warn)";
85
85
  }
86
86
 
87
+ /** 这一轮有没有实际改动(轮次列表据此把没改动的轮降成次要色,改过的才显示 +/− 行数) */
88
+ function hasChanges(round: GrowthEntry): boolean {
89
+ const st = round.stats;
90
+ return Boolean(st.added || st.modified || st.deleted || st.renamed || st.add || st.del);
91
+ }
92
+
87
93
  function changedLines(round: GrowthEntry): number {
88
94
  return round.stats.add + round.stats.del;
89
95
  }
@@ -175,7 +181,13 @@ export function ProjectPanel({
175
181
  };
176
182
  }, [cwd, gitRefreshKey, latestCommit]);
177
183
 
178
- const totals = useMemo(() => sumChanges(growth.changes), [growth.changes]);
184
+ const totals = useMemo(() => {
185
+ if (selected && growth.scope === "round") {
186
+ const st = selected.stats;
187
+ return { files: st.added + st.modified + st.deleted + st.renamed, add: st.add, del: st.del };
188
+ }
189
+ return growth.changesReady === false ? null : sumChanges(growth.changes);
190
+ }, [selected, growth.scope, growth.changes, growth.changesReady]);
179
191
  const piRounds = useMemo(() => rounds.filter((round) => round.kind === "round").length, [rounds]);
180
192
  const maxLines = useMemo(() => Math.max(1, ...rounds.map(changedLines)), [rounds]);
181
193
  const railRef = useRef<HTMLDivElement>(null);
@@ -304,9 +316,12 @@ export function ProjectPanel({
304
316
  <div className="mt-0.5 flex items-center gap-2" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
305
317
  <span>{fmtTime(selected.ts)}</span>
306
318
  <span>·</span>
307
- <span>{t.growthRoundFiles.replace("{n}", String(totals.files))}</span>
308
- <span style={{ color: "var(--dsw-success)" }}>+{totals.add}</span>
309
- <span style={{ color: "var(--dsw-danger)" }}>−{totals.del}</span>
319
+ {growth.scope === "session" && <span>{t.growthScopeSession}</span>}
320
+ {totals ? <>
321
+ <span>{t.growthRoundFiles.replace("{n}", String(totals.files))}</span>
322
+ <span style={{ color: "var(--dsw-success)" }}>+{totals.add}</span>
323
+ <span style={{ color: "var(--dsw-danger)" }}>−{totals.del}</span>
324
+ </> : <span>—</span>}
310
325
  {statusText && <span style={{ color: "var(--dsw-warn)" }}>{statusText}</span>}
311
326
  <span className="flex-1" />
312
327
  {onJumpToChat && selected.promptIds[0] && (
@@ -397,7 +412,7 @@ export function ProjectPanel({
397
412
  data-kind={round.kind}
398
413
  data-active={selected?.commit === round.commit || undefined}
399
414
  style={{ height: h, background: roundColor(round) }}
400
- title={`${roundTitle(round, tt)} · ${fmtTime(round.ts)} · ${t.growthRoundFiles.replace("{n}", String(round.changes.length))} +${round.stats.add} −${round.stats.del}`}
415
+ title={`${roundTitle(round, tt)} · ${fmtTime(round.ts)} · ${t.growthRoundFiles.replace("{n}", String(round.stats.added + round.stats.modified + round.stats.deleted + round.stats.renamed))} +${round.stats.add} −${round.stats.del}`}
401
416
  onClick={() => growth.select(round.commit)}
402
417
  />
403
418
  );
@@ -478,31 +493,41 @@ export function ProjectPanel({
478
493
  )}
479
494
  {/* 轮次列表:当前轮高亮,打开时自动滚到它 */}
480
495
  <div ref={roundListRef} className="min-h-0 max-h-[260px] overflow-y-auto border-t pt-1" style={{ borderColor: "var(--dsw-border-l1)" }}>
481
- {rounds.map((round) => (
482
- <button
483
- key={round.commit}
484
- type="button"
485
- role="option"
486
- aria-selected={selected?.commit === round.commit}
487
- data-selected={selected?.commit === round.commit || undefined}
488
- className="flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left transition-colors"
489
- style={{ background: selected?.commit === round.commit ? "var(--dsw-active)" : "transparent" }}
490
- onMouseEnter={(e) => {
491
- if (selected?.commit !== round.commit) e.currentTarget.style.background = "var(--dsw-hover)";
492
- }}
493
- onMouseLeave={(e) => {
494
- if (selected?.commit !== round.commit) e.currentTarget.style.background = "transparent";
495
- }}
496
- title={`${roundTitle(round, tt)} · ${fmtTime(round.ts)}`}
497
- onClick={() => {
498
- growth.select(round.commit);
499
- setRoundPickerOpen(false);
500
- }}
501
- >
502
- <span className="min-w-0 flex-1 truncate" style={{ fontSize: 12.5 }}>{roundTitle(round, tt)}</span>
503
- <span className="flex-none" style={{ fontSize: 11, color: "var(--dsw-label-caption)" }}>{fmtTime(round.ts)}</span>
504
- </button>
505
- ))}
496
+ {rounds.map((round) => {
497
+ const changed = hasChanges(round);
498
+ // 没改动的轮整行降对比度,扫视时自然被跳过;选中的那行不灰(否则像被禁用)
499
+ const dimmed = !changed && selected?.commit !== round.commit;
500
+ return (
501
+ <button
502
+ key={round.commit}
503
+ type="button"
504
+ role="option"
505
+ aria-selected={selected?.commit === round.commit}
506
+ data-selected={selected?.commit === round.commit || undefined}
507
+ className="flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left transition-colors"
508
+ style={{ background: selected?.commit === round.commit ? "var(--dsw-active)" : "transparent", ...(dimmed ? { opacity: 0.45 } : {}) }}
509
+ onMouseEnter={(e) => {
510
+ if (selected?.commit !== round.commit) e.currentTarget.style.background = "var(--dsw-hover)";
511
+ }}
512
+ onMouseLeave={(e) => {
513
+ if (selected?.commit !== round.commit) e.currentTarget.style.background = "transparent";
514
+ }}
515
+ title={`${roundTitle(round, tt)} · ${fmtTime(round.ts)}`}
516
+ onClick={() => {
517
+ growth.select(round.commit);
518
+ setRoundPickerOpen(false);
519
+ }}
520
+ >
521
+ <span className="min-w-0 flex-1 truncate" style={{ fontSize: 12.5 }}>{roundTitle(round, tt)}</span>
522
+ {changed && (round.stats.add > 0 || round.stats.del > 0) ? (
523
+ <span className="flex-none" style={{ fontSize: 11, fontFamily: "var(--font-mono)" }}>
524
+ <span style={{ color: "var(--dsw-success)" }}>+{round.stats.add}</span> <span style={{ color: "var(--dsw-danger)" }}>−{round.stats.del}</span>
525
+ </span>
526
+ ) : null}
527
+ <span className="flex-none" style={{ fontSize: 11, color: "var(--dsw-label-caption)" }}>{fmtTime(round.ts)}</span>
528
+ </button>
529
+ );
530
+ })}
506
531
  </div>
507
532
  </div>
508
533
  )}
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { useCallback, useEffect, useState } from "react";
10
10
  import { useI18n } from "@/i18n";
11
+ import { fmtBytes } from "@/lib/format";
11
12
  import { IconRefreshOutline14, IconCloseOutline14 } from "@/components/icons";
12
13
 
13
14
  export type PromptSourceKind = "system" | "append" | "agents" | "skill" | "template" | "tool" | "assembled";
@@ -45,10 +46,7 @@ export interface PromptSourcesData {
45
46
  const GROUP_KINDS: PromptSourceKind[] = ["system", "append", "agents", "skill", "template", "tool", "assembled"];
46
47
 
47
48
  function formatBytes(bytes: number | undefined): string {
48
- if (!bytes) return "";
49
- if (bytes < 1024) return `${bytes} B`;
50
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
51
- return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
49
+ return bytes ? fmtBytes(bytes) : "";
52
50
  }
53
51
 
54
52
  export function PromptPanel({
@@ -489,6 +489,12 @@
489
489
  color: var(--dsw-danger);
490
490
  padding: 4px 0;
491
491
  }
492
+ .catalogHint {
493
+ margin-top: 8px;
494
+ font-size: 11.5px;
495
+ line-height: 1.5;
496
+ color: var(--dsw-label-caption);
497
+ }
492
498
 
493
499
  /* ---- 候选选择对话框 ---- */
494
500
  .candidateMask {
@@ -542,6 +548,20 @@
542
548
  .candidateRow:hover {
543
549
  background: var(--dsw-hover);
544
550
  }
551
+ /* 已在内置目录里的候选:不可勾选 */
552
+ .candidateRow[data-disabled] {
553
+ cursor: default;
554
+ color: var(--dsw-label-tertiary);
555
+ }
556
+ .candidateRow[data-disabled]:hover {
557
+ background: transparent;
558
+ }
559
+ .candidateTag {
560
+ flex: none;
561
+ margin-left: auto;
562
+ font-size: 11px;
563
+ color: var(--dsw-label-caption);
564
+ }
545
565
  .candidateId {
546
566
  font-family: var(--font-mono);
547
567
  font-size: 12.5px;
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useRef, useState } from "react";
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
4
  import {
5
5
  IconCheckOutline14,
6
6
  IconChevronDown14,
@@ -11,8 +11,9 @@ import {
11
11
  } from "@/components/icons";
12
12
  import { ProviderBrand } from "@/components/ProviderBrand";
13
13
  import { useI18n } from "@/i18n";
14
+ import { looksPrivateEndpoint } from "@/lib/model-endpoint";
14
15
  import { CUSTOM_PROVIDER_ID_PATTERN, customApiOptions } from "@/lib/provider-display";
15
- import { formatCapacity, parseCapacity, serializeModelDraft, validateModelDrafts, type ModelDraft } from "@/lib/model-draft";
16
+ import { formatCapacity, modelDraftFromCatalog, parseCapacity, serializeModelDraft, serializeProviderDraft, validateModelDrafts, type ModelDraft } from "@/lib/model-draft";
16
17
  import type { ProviderView } from "@/lib/models-service";
17
18
  import styles from "./ProviderSetupModal.module.css";
18
19
 
@@ -34,6 +35,7 @@ type SubmitResult = { success: boolean; error?: string };
34
35
  export function ProviderSetupModal({
35
36
  mode,
36
37
  providers,
38
+ catalogModelIds,
37
39
  existingIds,
38
40
  onClose,
39
41
  onSaveBuiltin,
@@ -42,6 +44,8 @@ export function ProviderSetupModal({
42
44
  }: {
43
45
  mode: "builtin" | "custom";
44
46
  providers: ProviderView[];
47
+ /** 内置 provider → 目录里的模型 ID:同 ID 的行写 modelOverrides,「获取可用模型」里不再可选 */
48
+ catalogModelIds?: Readonly<Record<string, readonly string[]>>;
45
49
  existingIds: string[];
46
50
  onClose: () => void;
47
51
  onSaveBuiltin: (value: BuiltinProviderSetup) => Promise<SubmitResult>;
@@ -62,10 +66,18 @@ export function ProviderSetupModal({
62
66
  const allApis = customApiOptions(providers.flatMap((provider) => provider.apis));
63
67
  const [api, setApi] = useState(allApis.includes("openai-completions") ? "openai-completions" : (allApis[0] ?? "openai-completions"));
64
68
  const [models, setModels] = useState<ModelDraft[]>([]);
69
+ // 自定义 provider 的自动识别只在地址 / 密钥输入完(失焦)或换协议时触发:
70
+ // 还没有任何模型行,且 URL(+ 密钥,本机网关可省)都填了,这次失焦才算数
71
+ const autoReady = models.length === 0 && Boolean(baseUrl.trim()) && Boolean(apiKey.trim() || looksPrivateEndpoint(baseUrl));
72
+ const [autoTrigger, setAutoTrigger] = useState(0);
73
+ const requestAutoDiscover = () => {
74
+ if (autoReady) setAutoTrigger((count) => count + 1);
75
+ };
65
76
  const [saving, setSaving] = useState(false);
66
77
  const [error, setError] = useState("");
67
78
  const pickerRef = useRef<HTMLDivElement>(null);
68
79
  const selected = providers.find((provider) => provider.id === selectedId);
80
+ const catalogIds = useMemo(() => new Set(catalogModelIds?.[selectedId] ?? []), [catalogModelIds, selectedId]);
69
81
  const filtered = providers.filter((provider) => {
70
82
  const needle = search.trim().toLocaleLowerCase();
71
83
  return !needle || provider.name.toLocaleLowerCase().includes(needle) || provider.id.toLocaleLowerCase().includes(needle);
@@ -114,12 +126,8 @@ export function ProviderSetupModal({
114
126
  if (authMethod === "oauth" && onOAuthLogin) {
115
127
  result = await onOAuthLogin(selected.id);
116
128
  } else {
117
- const config: Record<string, unknown> = {};
118
- if (baseUrl.trim()) config.baseUrl = baseUrl.trim();
119
- if (models.length) {
120
- config.api = selected.apis[0] ?? "openai-completions";
121
- config.models = models.map(serializeModelDraft);
122
- }
129
+ // 与目录同 ID 的行进 modelOverrides,新 ID 才进 models[](并带上协议)
130
+ const config = serializeProviderDraft({}, { baseUrl, models }, selected.apis[0] ?? "openai-completions", catalogIds);
123
131
  result = await onSaveBuiltin({ providerId: selected.id, apiKey: apiKey.trim(), config });
124
132
  }
125
133
  } else {
@@ -130,7 +138,7 @@ export function ProviderSetupModal({
130
138
  baseUrl: baseUrl.trim(),
131
139
  api,
132
140
  ...(apiKey.trim() ? { apiKey: apiKey.trim() } : {}),
133
- models: models.map(serializeModelDraft),
141
+ models: models.map((model) => serializeModelDraft(model)),
134
142
  },
135
143
  });
136
144
  }
@@ -155,7 +163,7 @@ export function ProviderSetupModal({
155
163
  });
156
164
  const result = await response.json();
157
165
  if (!response.ok || !result.success) throw new Error(result.error || t.toastError);
158
- return (result.data.models ?? []).map((model: { id: string; name?: string }) => ({ id: model.id, name: model.name ?? "" }));
166
+ return (result.data.models ?? []).map(modelDraftFromCatalog);
159
167
  };
160
168
 
161
169
  return (
@@ -255,7 +263,7 @@ export function ProviderSetupModal({
255
263
  <label className={styles.label}>{t.apiAddress}</label>
256
264
  <input className={styles.input} value={baseUrl} onChange={(event) => setBaseUrl(event.target.value)} placeholder={selected?.baseUrl || t.providerDefault} />
257
265
  </div>
258
- <ModelCatalog models={models} setModels={setModels} updateModel={updateModel} inheritedCount={selected?.modelCount ?? 0} discover={discoverAvailableModels} discoverDisabled={!baseUrl.trim()} />
266
+ <ModelCatalog models={models} setModels={setModels} updateModel={updateModel} inheritedCount={selected?.modelCount ?? 0} catalogIds={catalogIds} discover={discoverAvailableModels} discoverDisabled={!baseUrl.trim()} />
259
267
  </div>
260
268
  ) : null}
261
269
  </div>
@@ -275,17 +283,17 @@ export function ProviderSetupModal({
275
283
  </div>
276
284
  <div className={styles.field}>
277
285
  <label className={styles.label}>{t.apiAddress}</label>
278
- <input className={styles.input} value={baseUrl} onChange={(event) => setBaseUrl(event.target.value)} placeholder="https://gateway.example/v1" />
286
+ <input className={styles.input} value={baseUrl} onChange={(event) => setBaseUrl(event.target.value)} onBlur={requestAutoDiscover} placeholder="https://gateway.example/v1" />
279
287
  </div>
280
288
  <div className={styles.field}>
281
289
  <label className={styles.label}>{t.apiProtocol}</label>
282
- <select className={styles.select} value={api} onChange={(event) => setApi(event.target.value)}>
290
+ <select className={styles.select} value={api} onChange={(event) => { setApi(event.target.value); requestAutoDiscover(); }}>
283
291
  {allApis.map((value) => <option key={value} value={value}>{value}</option>)}
284
292
  </select>
285
293
  </div>
286
294
  <div className={styles.field}>
287
295
  <label className={styles.label}>{t.apiKey}</label>
288
- <input className={styles.input} type="password" value={apiKey} onChange={(event) => setApiKey(event.target.value)} placeholder={t.apiKeyOptional} />
296
+ <input className={styles.input} type="password" value={apiKey} onChange={(event) => setApiKey(event.target.value)} onBlur={requestAutoDiscover} placeholder={t.apiKeyOptional} />
289
297
  <div className={styles.hint}>{t.customApiKeyHint}</div>
290
298
  </div>
291
299
  <ModelCatalog
@@ -294,6 +302,7 @@ export function ProviderSetupModal({
294
302
  updateModel={updateModel}
295
303
  discover={discoverAvailableModels}
296
304
  discoverDisabled={!baseUrl.trim()}
305
+ autoDiscover={autoReady ? { signature: `${baseUrl.trim()}|${api}|${apiKey.trim()}`, privateHost: looksPrivateEndpoint(baseUrl), trigger: autoTrigger } : undefined}
297
306
  />
298
307
  </>
299
308
  )}
@@ -325,16 +334,25 @@ export function ModelCatalog({
325
334
  setModels,
326
335
  updateModel,
327
336
  inheritedCount,
337
+ catalogIds,
328
338
  discover,
329
339
  discoverDisabled,
340
+ autoDiscover,
330
341
  }: {
331
342
  models: ModelDraft[];
332
343
  setModels: React.Dispatch<React.SetStateAction<ModelDraft[]>>;
333
344
  updateModel: (index: number, patch: Partial<ModelDraft>) => void;
334
345
  inheritedCount?: number;
346
+ /** 内置 provider 的目录模型 ID:候选里已在目录中的不能勾选(本来就能选),同 ID 的行只覆盖填写的字段 */
347
+ catalogIds?: ReadonlySet<string>;
335
348
  /** 用表单当前值探测远端目录(allowPrivate:用户勾选了允许访问本机/私网地址);抛错时错误显示在目录内 */
336
349
  discover?: (allowPrivate: boolean) => Promise<ModelDraft[]>;
337
350
  discoverDisabled?: boolean;
351
+ /**
352
+ * 自动识别:trigger 变了(地址 / 密钥失焦、换协议)就按当前签名(URL + 协议 + 密钥)拉一次模型目录。
353
+ * 只在「还没有任何模型行」时给出;同一签名只跑一次,清空模型行后不会自己又加回来。
354
+ */
355
+ autoDiscover?: { signature: string; privateHost: boolean; trigger: number };
338
356
  }) {
339
357
  const { t } = useI18n();
340
358
  const [expanded, setExpanded] = useState<ReadonlySet<number>>(new Set());
@@ -381,8 +399,64 @@ export function ModelCatalog({
381
399
  });
382
400
  };
383
401
 
402
+ const inCatalog = (id: string) => catalogIds?.has(id.trim()) ?? false;
403
+
404
+ // 自动识别:地址 / 密钥输入框失焦时(父组件递增 trigger)自己去拉一次目录,不用再点按钮。
405
+ // 不按「停手」计时:那样会拿半截地址和半截密钥去发请求。同一签名只跑一次;
406
+ // 表单一改(签名变了)就作废还在路上的那次,迟到的结果不会把别的地址的模型塞进来。
407
+ const auto = autoDiscover;
408
+ const autoDone = useRef("");
409
+ const autoSeq = useRef(0);
410
+ const autoPending = useRef(false);
411
+ // 处理过的最后一次 trigger:条目从无到有(如清空模型行)时 trigger 没变,不算一次新的失焦
412
+ const autoTriggerSeen = useRef(0);
413
+ const [autoStatus, setAutoStatus] = useState<"" | "loading" | "done" | "empty" | "failed">("");
414
+ const [autoFound, setAutoFound] = useState(0);
415
+ useEffect(() => {
416
+ if (!autoPending.current) return;
417
+ autoPending.current = false;
418
+ autoSeq.current += 1;
419
+ // 作废的那次不算跑过:改回原值再失焦还能重试
420
+ autoDone.current = "";
421
+ setAutoStatus("");
422
+ }, [auto?.signature]);
423
+ useEffect(() => {
424
+ if (!auto || auto.trigger === autoTriggerSeen.current) return;
425
+ autoTriggerSeen.current = auto.trigger;
426
+ if (!discover || autoDone.current === auto.signature) return;
427
+ autoDone.current = auto.signature;
428
+ autoPending.current = true;
429
+ const seq = ++autoSeq.current;
430
+ setAutoStatus("loading");
431
+ const settle = (status: "done" | "empty" | "failed") => {
432
+ if (seq !== autoSeq.current) return false;
433
+ autoPending.current = false;
434
+ setAutoStatus(status);
435
+ return true;
436
+ };
437
+ // 本机/私网地址自动允许(服务端仍会自己再判一次)
438
+ discover(allowPrivate || auto.privateHost).then(
439
+ (found) => {
440
+ if (!settle(found.length ? "done" : "empty")) return;
441
+ setAutoFound(found.length);
442
+ setModels((current) => (current.length ? current : found));
443
+ },
444
+ // 自动跑失败不弹错:按钮还在,手动点能看到具体原因
445
+ () => void settle("failed"),
446
+ );
447
+ // 只由失焦触发;discover 每次渲染都是新函数,放进依赖会让任何重渲染都重跑
448
+ // eslint-disable-next-line react-hooks/exhaustive-deps
449
+ }, [auto?.trigger]);
450
+
384
451
  const startDiscover = async () => {
385
452
  if (!discover || discovering) return;
453
+ // 手动点了就以候选对话框为准:点按钮时输入框失焦触发的那次自动识别作废
454
+ if (autoPending.current) {
455
+ autoPending.current = false;
456
+ autoSeq.current += 1;
457
+ autoDone.current = "";
458
+ setAutoStatus("");
459
+ }
386
460
  setDiscovering(true);
387
461
  setDiscoverError("");
388
462
  try {
@@ -391,10 +465,12 @@ export function ModelCatalog({
391
465
  setDiscoverError(t.discoverEmpty);
392
466
  return;
393
467
  }
394
- // 已配置的行默认不勾选:采纳永远不覆盖用户改过的内容
468
+ // 已配置的行默认不勾选:采纳永远不覆盖用户改过的内容。
469
+ // 内置目录里已有的模型本来就能选,加成行只会变成一条空覆盖:排到后面、不可勾选
395
470
  const known = new Set(models.map((model) => model.id.trim()).filter(Boolean));
396
- setCandidates(found);
397
- setPicked(new Set(found.filter((model) => !known.has(model.id.trim())).map((model) => model.id)));
471
+ const ordered = [...found.filter((model) => !inCatalog(model.id)), ...found.filter((model) => inCatalog(model.id))];
472
+ setCandidates(ordered);
473
+ setPicked(new Set(ordered.filter((model) => !known.has(model.id.trim()) && !inCatalog(model.id)).map((model) => model.id)));
398
474
  } catch (reason) {
399
475
  setDiscoverError(reason instanceof Error ? reason.message : t.toastError);
400
476
  } finally {
@@ -413,8 +489,8 @@ export function ModelCatalog({
413
489
  const byId = new Map(current.map((model) => [model.id.trim(), model]));
414
490
  for (const candidate of candidates) {
415
491
  const id = candidate.id.trim();
416
- if (!id || !picked.has(id) || byId.has(id)) continue;
417
- byId.set(id, { id, name: candidate.name ?? "" });
492
+ if (!id || !picked.has(id) || byId.has(id) || inCatalog(id)) continue;
493
+ byId.set(id, candidate);
418
494
  }
419
495
  return [...byId.values()];
420
496
  });
@@ -422,7 +498,9 @@ export function ModelCatalog({
422
498
  };
423
499
 
424
500
  const visibleCandidates = candidates ?? [];
425
- const allVisiblePicked = visibleCandidates.length > 0 && visibleCandidates.every((model) => picked.has(model.id));
501
+ // 「全选」只管能勾的那些
502
+ const selectableCandidates = visibleCandidates.filter((model) => !inCatalog(model.id));
503
+ const allVisiblePicked = selectableCandidates.length > 0 && selectableCandidates.every((model) => picked.has(model.id));
426
504
 
427
505
  const togglePick = (id: string) =>
428
506
  setPicked((current) => {
@@ -432,6 +510,7 @@ export function ModelCatalog({
432
510
  });
433
511
 
434
512
  const firstInvalid = validateModelDrafts(models);
513
+ const overridesBuiltin = models.some((model) => inCatalog(model.id));
435
514
 
436
515
  return (
437
516
  <div className={styles.catalog}>
@@ -440,6 +519,10 @@ export function ModelCatalog({
440
519
  <div className={styles.label}>{t.modelCatalog}</div>
441
520
  <div className={styles.catalogStatus}>
442
521
  {inheritedCount ? t.usingDefaultModels.replace("{count}", String(inheritedCount)) : t.customModelsRequired}
522
+ {autoStatus === "loading" ? ` · ${t.autoDiscovering}` : null}
523
+ {autoStatus === "done" ? ` · ${t.autoDiscovered.replace("{n}", String(autoFound))}` : null}
524
+ {autoStatus === "empty" ? ` · ${t.discoverEmpty}` : null}
525
+ {autoStatus === "failed" ? ` · ${t.autoDiscoverFailed}` : null}
443
526
  </div>
444
527
  </div>
445
528
  {discover ? (
@@ -509,6 +592,22 @@ export function ModelCatalog({
509
592
  placeholder="16K"
510
593
  />
511
594
  </label>
595
+ <label className={styles.capacityField} title={t.visionHint}>
596
+ <span>{t.visionLabel}</span>
597
+ <select
598
+ className={styles.input}
599
+ value={model.vision === undefined ? "unset" : model.vision ? "image" : "text"}
600
+ onChange={(event) => {
601
+ const choice = event.target.value;
602
+ // 未声明 = 不写 input:pi 按纯文本处理,体检卡片会列出来提醒
603
+ updateModel(index, { vision: choice === "unset" ? undefined : choice === "image" });
604
+ }}
605
+ >
606
+ <option value="unset">{t.visionUnset}</option>
607
+ <option value="image">{t.visionOn}</option>
608
+ <option value="text">{t.visionOff}</option>
609
+ </select>
610
+ </label>
512
611
  <span className={styles.capacityHint}>{t.capacityHint}</span>
513
612
  </div>
514
613
  ) : null}
@@ -520,6 +619,7 @@ export function ModelCatalog({
520
619
  ) : (
521
620
  <div className={styles.modelEmpty}>{inheritedCount ? t.modelCatalogEmptyBuiltin : t.modelCatalogEmptyCustom}</div>
522
621
  )}
622
+ {overridesBuiltin ? <div className={styles.catalogHint}>{t.catalogOverrideHint}</div> : null}
523
623
  <button className={styles.addModel} onClick={() => setModels((current) => [...current, { id: "", name: "" }])}>
524
624
  <IconPlusOutline16 size={14} />
525
625
  {t.addModel}
@@ -532,13 +632,17 @@ export function ModelCatalog({
532
632
  <span>{t.candidateTitle.replace("{count}", String(candidates.length))}</span>
533
633
  </div>
534
634
  <div className={styles.candidateList}>
535
- {visibleCandidates.map((model) => (
536
- <label className={styles.candidateRow} key={model.id}>
537
- <input type="checkbox" checked={picked.has(model.id)} onChange={() => togglePick(model.id)} />
538
- <span className={styles.candidateId}>{model.id}</span>
539
- {model.name ? <span className={styles.candidateName}>{model.name}</span> : null}
540
- </label>
541
- ))}
635
+ {visibleCandidates.map((model) => {
636
+ const listed = inCatalog(model.id);
637
+ return (
638
+ <label className={styles.candidateRow} data-disabled={listed || undefined} key={model.id}>
639
+ <input type="checkbox" checked={!listed && picked.has(model.id)} disabled={listed} onChange={() => togglePick(model.id)} />
640
+ <span className={styles.candidateId}>{model.id}</span>
641
+ {model.name ? <span className={styles.candidateName}>{model.name}</span> : null}
642
+ {listed ? <span className={styles.candidateTag}>{t.candidateInCatalog}</span> : null}
643
+ </label>
644
+ );
645
+ })}
542
646
  {visibleCandidates.length === 0 ? <div className={styles.modelEmpty}>—</div> : null}
543
647
  </div>
544
648
  <div className={styles.candidateActions}>
@@ -546,11 +650,12 @@ export function ModelCatalog({
546
650
  <input
547
651
  type="checkbox"
548
652
  checked={allVisiblePicked}
653
+ disabled={selectableCandidates.length === 0}
549
654
  onChange={() =>
550
655
  setPicked((current) => {
551
656
  const next = new Set(current);
552
- if (visibleCandidates.every((model) => next.has(model.id))) visibleCandidates.forEach((model) => next.delete(model.id));
553
- else visibleCandidates.forEach((model) => next.add(model.id));
657
+ if (selectableCandidates.every((model) => next.has(model.id))) selectableCandidates.forEach((model) => next.delete(model.id));
658
+ else selectableCandidates.forEach((model) => next.add(model.id));
554
659
  return next;
555
660
  })
556
661
  }