@geminilight/mindos 0.6.61 → 0.6.65

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 (313) hide show
  1. package/_standalone/.antigravity/mcp_config.json +14 -0
  2. package/_standalone/.mindos-build-version +1 -1
  3. package/_standalone/.next/BUILD_ID +1 -1
  4. package/_standalone/.next/app-path-routes-manifest.json +23 -23
  5. package/_standalone/.next/build-manifest.json +2 -2
  6. package/_standalone/.next/cache/.previewinfo +1 -1
  7. package/_standalone/.next/cache/.rscinfo +1 -1
  8. package/_standalone/.next/cache/config.json +3 -3
  9. package/_standalone/.next/prerender-manifest.json +3 -3
  10. package/_standalone/.next/server/app/.well-known/agent-card.json/route_client-reference-manifest.js +1 -1
  11. package/_standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  12. package/_standalone/.next/server/app/_global-error.html +2 -2
  13. package/_standalone/.next/server/app/_global-error.rsc +1 -1
  14. package/_standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  15. package/_standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  16. package/_standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  17. package/_standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  18. package/_standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  19. package/_standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  20. package/_standalone/.next/server/app/_not-found/page.js +1 -1
  21. package/_standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  22. package/_standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/_standalone/.next/server/app/agents/[agentKey]/page.js +1 -1
  24. package/_standalone/.next/server/app/agents/[agentKey]/page.js.nft.json +1 -1
  25. package/_standalone/.next/server/app/agents/[agentKey]/page_client-reference-manifest.js +1 -1
  26. package/_standalone/.next/server/app/agents/page.js +1 -1
  27. package/_standalone/.next/server/app/agents/page.js.nft.json +1 -1
  28. package/_standalone/.next/server/app/agents/page_client-reference-manifest.js +1 -1
  29. package/_standalone/.next/server/app/api/a2a/agents/route_client-reference-manifest.js +1 -1
  30. package/_standalone/.next/server/app/api/a2a/delegations/route_client-reference-manifest.js +1 -1
  31. package/_standalone/.next/server/app/api/a2a/discover/route_client-reference-manifest.js +1 -1
  32. package/_standalone/.next/server/app/api/a2a/route_client-reference-manifest.js +1 -1
  33. package/_standalone/.next/server/app/api/acp/config/route_client-reference-manifest.js +1 -1
  34. package/_standalone/.next/server/app/api/acp/detect/route.js +1 -1
  35. package/_standalone/.next/server/app/api/acp/detect/route_client-reference-manifest.js +1 -1
  36. package/_standalone/.next/server/app/api/acp/install/route_client-reference-manifest.js +1 -1
  37. package/_standalone/.next/server/app/api/acp/registry/route.js +1 -1
  38. package/_standalone/.next/server/app/api/acp/registry/route_client-reference-manifest.js +1 -1
  39. package/_standalone/.next/server/app/api/acp/session/route_client-reference-manifest.js +1 -1
  40. package/_standalone/.next/server/app/api/agent-activity/route_client-reference-manifest.js +1 -1
  41. package/_standalone/.next/server/app/api/agents/copy-skill/route.js.nft.json +1 -1
  42. package/_standalone/.next/server/app/api/agents/copy-skill/route_client-reference-manifest.js +1 -1
  43. package/_standalone/.next/server/app/api/agents/custom/detect/route_client-reference-manifest.js +1 -1
  44. package/_standalone/.next/server/app/api/agents/custom/route_client-reference-manifest.js +1 -1
  45. package/_standalone/.next/server/app/api/ask/route.js +53 -47
  46. package/_standalone/.next/server/app/api/ask/route.js.nft.json +1 -1
  47. package/_standalone/.next/server/app/api/ask/route_client-reference-manifest.js +1 -1
  48. package/_standalone/.next/server/app/api/ask-sessions/route_client-reference-manifest.js +1 -1
  49. package/_standalone/.next/server/app/api/auth/route_client-reference-manifest.js +1 -1
  50. package/_standalone/.next/server/app/api/backlinks/route.js.nft.json +1 -1
  51. package/_standalone/.next/server/app/api/backlinks/route_client-reference-manifest.js +1 -1
  52. package/_standalone/.next/server/app/api/bootstrap/route.js.nft.json +1 -1
  53. package/_standalone/.next/server/app/api/bootstrap/route_client-reference-manifest.js +1 -1
  54. package/_standalone/.next/server/app/api/changes/route.js.nft.json +1 -1
  55. package/_standalone/.next/server/app/api/changes/route_client-reference-manifest.js +1 -1
  56. package/_standalone/.next/server/app/api/export/route.js.nft.json +1 -1
  57. package/_standalone/.next/server/app/api/export/route_client-reference-manifest.js +1 -1
  58. package/_standalone/.next/server/app/api/extract-pdf/route_client-reference-manifest.js +1 -1
  59. package/_standalone/.next/server/app/api/file/import/route.js +1 -1
  60. package/_standalone/.next/server/app/api/file/import/route.js.nft.json +1 -1
  61. package/_standalone/.next/server/app/api/file/import/route_client-reference-manifest.js +1 -1
  62. package/_standalone/.next/server/app/api/file/raw/route.js.nft.json +1 -1
  63. package/_standalone/.next/server/app/api/file/raw/route_client-reference-manifest.js +1 -1
  64. package/_standalone/.next/server/app/api/file/route.js.nft.json +1 -1
  65. package/_standalone/.next/server/app/api/file/route_client-reference-manifest.js +1 -1
  66. package/_standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  67. package/_standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  68. package/_standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  69. package/_standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  70. package/_standalone/.next/server/app/api/graph/route.js.nft.json +1 -1
  71. package/_standalone/.next/server/app/api/graph/route_client-reference-manifest.js +1 -1
  72. package/_standalone/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  73. package/_standalone/.next/server/app/api/inbox/route.js.nft.json +1 -1
  74. package/_standalone/.next/server/app/api/inbox/route_client-reference-manifest.js +1 -1
  75. package/_standalone/.next/server/app/api/init/route.js.nft.json +1 -1
  76. package/_standalone/.next/server/app/api/init/route_client-reference-manifest.js +1 -1
  77. package/_standalone/.next/server/app/api/mcp/agents/route.js +1 -1
  78. package/_standalone/.next/server/app/api/mcp/agents/route.js.nft.json +1 -1
  79. package/_standalone/.next/server/app/api/mcp/agents/route_client-reference-manifest.js +1 -1
  80. package/_standalone/.next/server/app/api/mcp/install/route_client-reference-manifest.js +1 -1
  81. package/_standalone/.next/server/app/api/mcp/install-skill/route_client-reference-manifest.js +1 -1
  82. package/_standalone/.next/server/app/api/mcp/restart/route_client-reference-manifest.js +1 -1
  83. package/_standalone/.next/server/app/api/mcp/status/route_client-reference-manifest.js +1 -1
  84. package/_standalone/.next/server/app/api/mcp/uninstall/route_client-reference-manifest.js +1 -1
  85. package/_standalone/.next/server/app/api/monitoring/route.js.nft.json +1 -1
  86. package/_standalone/.next/server/app/api/monitoring/route_client-reference-manifest.js +1 -1
  87. package/_standalone/.next/server/app/api/recent-files/route.js.nft.json +1 -1
  88. package/_standalone/.next/server/app/api/recent-files/route_client-reference-manifest.js +1 -1
  89. package/_standalone/.next/server/app/api/restart/route_client-reference-manifest.js +1 -1
  90. package/_standalone/.next/server/app/api/search/route.js.nft.json +1 -1
  91. package/_standalone/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
  92. package/_standalone/.next/server/app/api/settings/list-models/route.js +1 -1
  93. package/_standalone/.next/server/app/api/settings/list-models/route_client-reference-manifest.js +1 -1
  94. package/_standalone/.next/server/app/api/settings/reset-token/route_client-reference-manifest.js +1 -1
  95. package/_standalone/.next/server/app/api/settings/route.js +1 -1
  96. package/_standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  97. package/_standalone/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
  98. package/_standalone/.next/server/app/api/settings/test-key/route.js +1 -1
  99. package/_standalone/.next/server/app/api/settings/test-key/route_client-reference-manifest.js +1 -1
  100. package/_standalone/.next/server/app/api/setup/check-path/route_client-reference-manifest.js +1 -1
  101. package/_standalone/.next/server/app/api/setup/check-port/route_client-reference-manifest.js +1 -1
  102. package/_standalone/.next/server/app/api/setup/generate-token/route_client-reference-manifest.js +1 -1
  103. package/_standalone/.next/server/app/api/setup/ls/route_client-reference-manifest.js +1 -1
  104. package/_standalone/.next/server/app/api/setup/route.js +1 -1
  105. package/_standalone/.next/server/app/api/setup/route_client-reference-manifest.js +1 -1
  106. package/_standalone/.next/server/app/api/skills/route.js +1 -1
  107. package/_standalone/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  108. package/_standalone/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
  109. package/_standalone/.next/server/app/api/tree-version/route.js.nft.json +1 -1
  110. package/_standalone/.next/server/app/api/tree-version/route_client-reference-manifest.js +1 -1
  111. package/_standalone/.next/server/app/api/uninstall/route_client-reference-manifest.js +1 -1
  112. package/_standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  113. package/_standalone/.next/server/app/api/update-check/route_client-reference-manifest.js +1 -1
  114. package/_standalone/.next/server/app/api/update-status/route_client-reference-manifest.js +1 -1
  115. package/_standalone/.next/server/app/api/workflows/route.js.nft.json +1 -1
  116. package/_standalone/.next/server/app/api/workflows/route_client-reference-manifest.js +1 -1
  117. package/_standalone/.next/server/app/changes/page.js +1 -1
  118. package/_standalone/.next/server/app/changes/page.js.nft.json +1 -1
  119. package/_standalone/.next/server/app/changes/page_client-reference-manifest.js +1 -1
  120. package/_standalone/.next/server/app/echo/[segment]/page.js +2 -2
  121. package/_standalone/.next/server/app/echo/[segment]/page.js.nft.json +1 -1
  122. package/_standalone/.next/server/app/echo/[segment]/page_client-reference-manifest.js +1 -1
  123. package/_standalone/.next/server/app/echo/page.js +1 -1
  124. package/_standalone/.next/server/app/echo/page.js.nft.json +1 -1
  125. package/_standalone/.next/server/app/echo/page_client-reference-manifest.js +1 -1
  126. package/_standalone/.next/server/app/explore/page.js +1 -1
  127. package/_standalone/.next/server/app/explore/page.js.nft.json +1 -1
  128. package/_standalone/.next/server/app/explore/page_client-reference-manifest.js +1 -1
  129. package/_standalone/.next/server/app/help/page.js +1 -1
  130. package/_standalone/.next/server/app/help/page.js.nft.json +1 -1
  131. package/_standalone/.next/server/app/help/page_client-reference-manifest.js +1 -1
  132. package/_standalone/.next/server/app/inbox/history/page.js +1 -1
  133. package/_standalone/.next/server/app/inbox/history/page.js.nft.json +1 -1
  134. package/_standalone/.next/server/app/inbox/history/page_client-reference-manifest.js +1 -1
  135. package/_standalone/.next/server/app/login/page.js +1 -1
  136. package/_standalone/.next/server/app/login/page.js.nft.json +1 -1
  137. package/_standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
  138. package/_standalone/.next/server/app/page.js +1 -1
  139. package/_standalone/.next/server/app/page.js.nft.json +1 -1
  140. package/_standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  141. package/_standalone/.next/server/app/setup/page.js +2 -2
  142. package/_standalone/.next/server/app/setup/page.js.nft.json +1 -1
  143. package/_standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  144. package/_standalone/.next/server/app/trash/page.js +3 -3
  145. package/_standalone/.next/server/app/trash/page.js.nft.json +1 -1
  146. package/_standalone/.next/server/app/trash/page_client-reference-manifest.js +1 -1
  147. package/_standalone/.next/server/app/view/[...path]/page.js +2 -2
  148. package/_standalone/.next/server/app/view/[...path]/page.js.nft.json +1 -1
  149. package/_standalone/.next/server/app/view/[...path]/page_client-reference-manifest.js +1 -1
  150. package/_standalone/.next/server/app/wiki/page.js +1 -1
  151. package/_standalone/.next/server/app/wiki/page.js.nft.json +1 -1
  152. package/_standalone/.next/server/app/wiki/page_client-reference-manifest.js +1 -1
  153. package/_standalone/.next/server/app-paths-manifest.json +23 -23
  154. package/_standalone/.next/server/chunks/122.js +222 -0
  155. package/_standalone/.next/server/chunks/1550.js +1 -1
  156. package/_standalone/.next/server/chunks/1750.js +1 -1
  157. package/_standalone/.next/server/chunks/3113.js +52 -0
  158. package/_standalone/.next/server/chunks/6539.js +1 -1
  159. package/_standalone/.next/server/chunks/8388.js +3 -3
  160. package/_standalone/.next/server/chunks/953.js +3 -3
  161. package/_standalone/.next/server/pages/500.html +2 -2
  162. package/_standalone/.next/server/server-reference-manifest.js +1 -1
  163. package/_standalone/.next/server/server-reference-manifest.json +1 -1
  164. package/_standalone/.next/static/chunks/1001-99da82ec8d8c136f.js +1 -0
  165. package/_standalone/.next/static/chunks/1088-77544af0a50cb7a4.js +1 -0
  166. package/_standalone/.next/static/chunks/1467-87dde7eed498806f.js +1 -0
  167. package/_standalone/.next/static/chunks/5149-4d828886dda479fa.js +1 -0
  168. package/_standalone/.next/static/chunks/5581-c671163a2fe1b312.js +29 -0
  169. package/_standalone/.next/static/chunks/{7266-bb7be1128eccd48e.js → 5718-3837c3210a0e175f.js} +2 -2
  170. package/_standalone/.next/static/chunks/6636-53238eff89503f03.js +6 -0
  171. package/_standalone/.next/static/chunks/6757-1c1a89720fdda8f0.js +1 -0
  172. package/_standalone/.next/static/chunks/7129-20e9d2463a9da646.js +1 -0
  173. package/_standalone/.next/static/chunks/7294-cac25d97869afadc.js +1 -0
  174. package/_standalone/.next/static/chunks/8225-21e5cebc3731ddf0.js +1 -0
  175. package/_standalone/.next/static/chunks/8520-b51810e66293ceb8.js +22 -0
  176. package/_standalone/.next/static/chunks/9207-dc9c31b351a2ed78.js +1 -0
  177. package/_standalone/.next/static/chunks/app/agents/[agentKey]/page-2f5cf97e03dc1cc9.js +1 -0
  178. package/_standalone/.next/static/chunks/app/agents/page-50eac58d511dcc6e.js +1 -0
  179. package/_standalone/.next/static/chunks/app/echo/[segment]/page-2a00f4686adf3885.js +11 -0
  180. package/_standalone/.next/static/chunks/app/layout-2cb7a6602d2e5d5f.js +168 -0
  181. package/_standalone/.next/static/chunks/app/{page-6a1f8d21c12b829e.js → page-5ab911b2226f6ff7.js} +1 -1
  182. package/_standalone/.next/static/chunks/app/setup/page-907b7c57fad2292b.js +1 -0
  183. package/_standalone/.next/static/chunks/app/trash/page-11a511b065ea84c2.js +1 -0
  184. package/_standalone/.next/static/chunks/app/view/[...path]/page-26e47dd4c533a58c.js +12 -0
  185. package/_standalone/.next/static/chunks/app/wiki/page-dce495b9048022fb.js +1 -0
  186. package/_standalone/.next/static/css/67e7918f5ed7d147.css +1 -0
  187. package/_standalone/.next/trace +65 -65
  188. package/_standalone/__tests__/acp/registry.test.ts +30 -20
  189. package/_standalone/__tests__/api/ask-attachments.test.ts +194 -0
  190. package/_standalone/__tests__/api/mcp-install.test.ts +49 -2
  191. package/_standalone/__tests__/api/settings.test.ts +16 -12
  192. package/_standalone/__tests__/api/setup.test.ts +11 -9
  193. package/_standalone/__tests__/api/test-key.test.ts +0 -10
  194. package/_standalone/__tests__/components/UpdateToast.test.ts +344 -0
  195. package/_standalone/__tests__/core/context.test.ts +48 -426
  196. package/_standalone/__tests__/lib/pi-skills.test.ts +4 -4
  197. package/_standalone/__tests__/lib/settings-ai-client.test.ts +32 -12
  198. package/_standalone/__tests__/setup.ts +5 -5
  199. package/_standalone/app/globals.css +4 -4
  200. package/_standalone/components/ActivityBar.tsx +17 -6
  201. package/_standalone/components/Panel.tsx +24 -6
  202. package/_standalone/components/SidebarLayout.tsx +36 -8
  203. package/_standalone/components/agents/AgentsMcpSection.tsx +2 -2
  204. package/_standalone/components/agents/AgentsOverviewSection.tsx +5 -1
  205. package/_standalone/components/agents/AgentsPanelA2aTab.tsx +173 -113
  206. package/_standalone/components/agents/AgentsSkillsSection.tsx +2 -2
  207. package/_standalone/components/ask/AskContent.tsx +83 -44
  208. package/_standalone/components/ask/AskHeader.tsx +8 -1
  209. package/_standalone/components/ask/MessageList.tsx +37 -3
  210. package/_standalone/components/ask/ProviderModelCapsule.tsx +444 -174
  211. package/_standalone/components/home/InboxSection.tsx +25 -25
  212. package/_standalone/components/settings/AiTab.tsx +353 -298
  213. package/_standalone/components/settings/CustomProviderFields.tsx +121 -0
  214. package/_standalone/components/settings/CustomProvidersCard.tsx +154 -0
  215. package/_standalone/components/settings/KnowledgeTab.tsx +6 -20
  216. package/_standalone/components/settings/McpAgentInstall.tsx +7 -2
  217. package/_standalone/components/settings/Primitives.tsx +48 -104
  218. package/_standalone/components/settings/ProviderModal.tsx +87 -0
  219. package/_standalone/components/settings/SettingsContent.tsx +2 -5
  220. package/_standalone/components/settings/TestButton.tsx +64 -0
  221. package/_standalone/components/settings/types.ts +3 -9
  222. package/_standalone/components/settings/useCustomProviderForm.ts +132 -0
  223. package/_standalone/components/setup/StepAI.tsx +12 -5
  224. package/_standalone/components/shared/ModelInput.tsx +220 -0
  225. package/_standalone/components/shared/ProviderSelect.tsx +126 -36
  226. package/_standalone/hooks/useAskChat.ts +100 -13
  227. package/_standalone/hooks/useAskPanel.ts +17 -1
  228. package/_standalone/lib/settings-ai-client.ts +17 -8
  229. package/_standalone/tsconfig.tsbuildinfo +1 -1
  230. package/app/.antigravity/mcp_config.json +14 -0
  231. package/app/app/api/ask/route.ts +154 -45
  232. package/app/app/api/mcp/agents/route.ts +3 -3
  233. package/app/app/api/settings/list-models/route.ts +36 -9
  234. package/app/app/api/settings/route.ts +14 -42
  235. package/app/app/api/settings/test-key/route.ts +78 -2
  236. package/app/app/api/setup/route.ts +36 -18
  237. package/app/app/api/skills/route.ts +1 -1
  238. package/app/app/globals.css +4 -4
  239. package/app/app/layout.tsx +5 -3
  240. package/app/app/view/[...path]/page.tsx +5 -0
  241. package/app/components/ActivityBar.tsx +17 -6
  242. package/app/components/HomeContent.tsx +11 -0
  243. package/app/components/InboxView.tsx +656 -0
  244. package/app/components/Panel.tsx +24 -6
  245. package/app/components/SidebarLayout.tsx +36 -8
  246. package/app/components/UpdateToast.tsx +255 -0
  247. package/app/components/agents/AgentDetailContent.tsx +8 -8
  248. package/app/components/agents/AgentsMcpSection.tsx +2 -2
  249. package/app/components/agents/AgentsOverviewSection.tsx +5 -1
  250. package/app/components/agents/AgentsPanelA2aTab.tsx +173 -113
  251. package/app/components/agents/AgentsSkillsSection.tsx +2 -2
  252. package/app/components/ask/AskContent.tsx +83 -44
  253. package/app/components/ask/AskHeader.tsx +8 -1
  254. package/app/components/ask/MessageList.tsx +37 -3
  255. package/app/components/ask/ProviderModelCapsule.tsx +444 -174
  256. package/app/components/home/InboxSection.tsx +25 -25
  257. package/app/components/settings/AiTab.tsx +353 -298
  258. package/app/components/settings/CustomProviderFields.tsx +121 -0
  259. package/app/components/settings/CustomProvidersCard.tsx +154 -0
  260. package/app/components/settings/KnowledgeTab.tsx +6 -20
  261. package/app/components/settings/McpAgentInstall.tsx +7 -2
  262. package/app/components/settings/Primitives.tsx +48 -104
  263. package/app/components/settings/ProviderModal.tsx +87 -0
  264. package/app/components/settings/SettingsContent.tsx +2 -5
  265. package/app/components/settings/TestButton.tsx +64 -0
  266. package/app/components/settings/types.ts +3 -9
  267. package/app/components/settings/useCustomProviderForm.ts +132 -0
  268. package/app/components/setup/StepAI.tsx +12 -5
  269. package/app/components/shared/ModelInput.tsx +220 -0
  270. package/app/components/shared/ProviderSelect.tsx +126 -36
  271. package/app/hooks/useAskChat.ts +100 -13
  272. package/app/hooks/useAskPanel.ts +17 -1
  273. package/app/lib/acp/registry.ts +92 -10
  274. package/app/lib/agent/context.ts +65 -0
  275. package/app/lib/agent/providers.ts +25 -0
  276. package/app/lib/agent/tools.ts +1 -1
  277. package/app/lib/custom-endpoints.ts +160 -0
  278. package/app/lib/fs.ts +8 -1
  279. package/app/lib/i18n/modules/ai-chat.ts +6 -0
  280. package/app/lib/i18n/modules/knowledge.ts +16 -0
  281. package/app/lib/i18n/modules/onboarding.ts +4 -0
  282. package/app/lib/i18n/modules/settings.ts +88 -2
  283. package/app/lib/mcp-agents.ts +11 -0
  284. package/app/lib/pi-integration/skills.ts +16 -4
  285. package/app/lib/settings-ai-client.ts +17 -8
  286. package/app/lib/settings.ts +68 -72
  287. package/app/lib/types.ts +4 -0
  288. package/bin/lib/mcp-agents.js +11 -0
  289. package/bin/lib/mcp-install.js +71 -7
  290. package/package.json +1 -1
  291. package/_standalone/.next/server/chunks/530.js +0 -218
  292. package/_standalone/.next/server/chunks/8955.js +0 -52
  293. package/_standalone/.next/static/chunks/1369-7d0ac5d1564eed1e.js +0 -1
  294. package/_standalone/.next/static/chunks/3427-2e61a5df1f5e55fb.js +0 -1
  295. package/_standalone/.next/static/chunks/5581-0c700c20718bd916.js +0 -29
  296. package/_standalone/.next/static/chunks/6297-085daa21037d5f81.js +0 -1
  297. package/_standalone/.next/static/chunks/6636-9bbc90fb3b8731fe.js +0 -6
  298. package/_standalone/.next/static/chunks/7637-904b0a381dc3ec02.js +0 -1
  299. package/_standalone/.next/static/chunks/8520-76d1b05072178b43.js +0 -22
  300. package/_standalone/.next/static/chunks/8658-16ff58b75ae37fbb.js +0 -1
  301. package/_standalone/.next/static/chunks/9905-a19d379cb225246e.js +0 -1
  302. package/_standalone/.next/static/chunks/app/agents/[agentKey]/page-0ea3571c8fbae823.js +0 -1
  303. package/_standalone/.next/static/chunks/app/agents/page-66858acbcd1d4bf8.js +0 -1
  304. package/_standalone/.next/static/chunks/app/echo/[segment]/page-bf5c290fa3ccff09.js +0 -11
  305. package/_standalone/.next/static/chunks/app/layout-a5d5925b47e87cc3.js +0 -164
  306. package/_standalone/.next/static/chunks/app/setup/page-821714e7477be46c.js +0 -1
  307. package/_standalone/.next/static/chunks/app/trash/page-40bc7316806acd62.js +0 -1
  308. package/_standalone/.next/static/chunks/app/view/[...path]/page-6fbb14b8f322d0f0.js +0 -12
  309. package/_standalone/.next/static/chunks/app/wiki/page-ba36eccf4fe62cfe.js +0 -1
  310. package/_standalone/.next/static/css/b57c4eb3cc88308b.css +0 -1
  311. package/_standalone/lib/agent/context.ts +0 -403
  312. /package/_standalone/.next/static/{5GmVArEG8OX03azKICsGq → eIlwbGas1iRGonlPyEwj7}/_buildManifest.js +0 -0
  313. /package/_standalone/.next/static/{5GmVArEG8OX03azKICsGq → eIlwbGas1iRGonlPyEwj7}/_ssgManifest.js +0 -0
@@ -2,102 +2,120 @@
2
2
 
3
3
  import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
- import { Cpu, ChevronDown, Check, Settings } from 'lucide-react';
5
+ import { Cpu, ChevronDown, ChevronRight, Check, Loader2, Search, RefreshCw } from 'lucide-react';
6
6
  import { useLocale } from '@/lib/stores/locale-store';
7
7
  import {
8
8
  type ProviderId,
9
9
  PROVIDER_PRESETS,
10
- ALL_PROVIDER_IDS,
11
10
  isProviderId,
12
- getApiKeyEnvVar,
13
11
  } from '@/lib/agent/providers';
12
+ import { type Provider, isProviderEntryId, findProvider } from '@/lib/custom-endpoints';
14
13
 
15
- const STORAGE_KEY = 'mindos-provider-override';
14
+ const STORAGE_KEY = 'mindos-provider-model';
15
+
16
+ type ProviderSelection = ProviderId | `p_${string}` | null;
16
17
 
17
18
  interface ProviderModelCapsuleProps {
18
- value: ProviderId | null;
19
- onChange: (provider: ProviderId | null) => void;
19
+ providerValue: ProviderSelection;
20
+ onProviderChange: (provider: ProviderSelection) => void;
21
+ modelValue: string | null;
22
+ onModelChange: (model: string | null) => void;
20
23
  disabled?: boolean;
21
24
  }
22
25
 
23
- interface DropdownPos {
24
- top: number;
25
- left: number;
26
- direction: 'up' | 'down';
27
- }
28
-
29
26
  interface SettingsData {
30
27
  ai?: {
31
- provider?: string;
32
- providers?: Record<string, { apiKey?: string; model?: string; baseUrl?: string }>;
28
+ activeProvider?: string;
29
+ providers?: Provider[];
33
30
  };
34
31
  envOverrides?: Record<string, boolean>;
35
32
  }
36
33
 
37
- export function getPersistedProvider(): ProviderId | null {
38
- if (typeof window === 'undefined') return null;
34
+ /* ── Persistence ── */
35
+
36
+ export function getPersistedProviderModel(): { provider: ProviderSelection; model: string | null } {
37
+ if (typeof window === 'undefined') return { provider: null, model: null };
39
38
  try {
40
- const stored = localStorage.getItem(STORAGE_KEY);
41
- if (stored && isProviderId(stored)) return stored;
42
- } catch { /* localStorage unavailable */ }
43
- return null;
39
+ const raw = localStorage.getItem(STORAGE_KEY);
40
+ if (!raw) {
41
+ const old = localStorage.getItem('mindos-provider-override');
42
+ if (old && (isProviderId(old) || isProviderEntryId(old))) return { provider: old as any, model: null };
43
+ return { provider: null, model: null };
44
+ }
45
+ const parsed = JSON.parse(raw);
46
+ const provider = parsed?.provider && (isProviderId(parsed.provider) || isProviderEntryId(parsed.provider))
47
+ ? parsed.provider : null;
48
+ const model = typeof parsed?.model === 'string' ? parsed.model : null;
49
+ return { provider, model };
50
+ } catch { return { provider: null, model: null }; }
44
51
  }
45
52
 
46
- export function persistProvider(provider: ProviderId | null): void {
53
+ function persistProviderModel(provider: ProviderSelection, model: string | null): void {
47
54
  try {
48
- if (provider) {
49
- localStorage.setItem(STORAGE_KEY, provider);
50
- } else {
51
- localStorage.removeItem(STORAGE_KEY);
52
- }
53
- } catch { /* localStorage unavailable */ }
55
+ if (provider || model) localStorage.setItem(STORAGE_KEY, JSON.stringify({ provider, model }));
56
+ else localStorage.removeItem(STORAGE_KEY);
57
+ localStorage.removeItem('mindos-provider-override');
58
+ } catch {}
54
59
  }
55
60
 
56
- /**
57
- * Determine which providers are configured (have an API key set via settings or env).
58
- * Providers with apiKeyFallback (e.g. Ollama) only appear if the user has explicitly
59
- * configured them having an API key, env var, or a saved model/baseUrl entry.
60
- */
61
- function getConfiguredProviders(data: SettingsData): ProviderId[] {
62
- const result: ProviderId[] = [];
63
- const providers = data.ai?.providers ?? {};
64
- const env = data.envOverrides ?? {};
65
-
66
- for (const id of ALL_PROVIDER_IDS) {
67
- const preset = PROVIDER_PRESETS[id];
68
- const hasSettingsKey = providers[id]?.apiKey === '***set***';
69
- const envVar = getApiKeyEnvVar(id);
70
- const hasEnvKey = envVar ? !!env[envVar] : false;
71
-
72
- if (hasSettingsKey || hasEnvKey) {
73
- result.push(id);
74
- } else if (preset.apiKeyFallback) {
75
- // Local providers (Ollama etc.): only show if user has explicitly configured
76
- // them (selected as default, or saved a model/baseUrl), not just because
77
- // a fallback key exists and they haven't been touched.
78
- const isDefault = data.ai?.provider === id;
79
- const cfg = providers[id];
80
- if (isDefault || (cfg && (cfg.model || cfg.baseUrl))) {
81
- result.push(id);
82
- }
83
- }
84
- }
85
- return result;
61
+ /* ── Configured providers ── */
62
+
63
+ function getConfiguredProviders(data: SettingsData): string[] {
64
+ return (data.ai?.providers ?? []).map(p => p.id);
86
65
  }
87
66
 
67
+ /* ── Component ── */
68
+
88
69
  export default function ProviderModelCapsule({
89
- value,
90
- onChange,
91
- disabled = false,
70
+ providerValue, onProviderChange, modelValue, onModelChange, disabled = false,
92
71
  }: ProviderModelCapsuleProps) {
93
72
  const { t, locale } = useLocale();
94
73
  const [open, setOpen] = useState(false);
95
- const [pos, setPos] = useState<DropdownPos | null>(null);
96
74
  const triggerRef = useRef<HTMLButtonElement>(null);
97
- const dropdownRef = useRef<HTMLDivElement>(null);
75
+ const containerRef = useRef<HTMLDivElement>(null);
76
+ const flyoutRef = useRef<HTMLDivElement>(null);
77
+ const searchInputRef = useRef<HTMLInputElement>(null);
78
+ const modelListRef = useRef<HTMLDivElement>(null);
98
79
 
99
80
  const [settingsData, setSettingsData] = useState<SettingsData | null>(null);
100
81
 
82
+ // Flyout state
83
+ const providerPanelRef = useRef<HTMLDivElement>(null);
84
+ const hoveredRowRef = useRef<HTMLDivElement>(null);
85
+ const [hoveredProvider, setHoveredProvider] = useState<string | null>(null);
86
+ const [flyoutStyle, setFlyoutStyle] = useState<React.CSSProperties>({});
87
+ const [expandedModels, setExpandedModels] = useState<string[] | null>(null);
88
+ const [modelsLoading, setModelsLoading] = useState(false);
89
+ const [modelsError, setModelsError] = useState('');
90
+ const [modelSearch, setModelSearch] = useState('');
91
+ const [modelHighlight, setModelHighlight] = useState(-1);
92
+ const fetchVersionRef = useRef(0);
93
+ const modelsCacheRef = useRef<Record<string, string[]>>({});
94
+
95
+ // Debounced flyout close — prevents flicker when mouse crosses gap
96
+ const closeTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
97
+ const openTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
98
+
99
+ const cancelCloseTimer = useCallback(() => {
100
+ if (closeTimerRef.current) { clearTimeout(closeTimerRef.current); closeTimerRef.current = undefined; }
101
+ }, []);
102
+
103
+ const startCloseTimer = useCallback(() => {
104
+ cancelCloseTimer();
105
+ closeTimerRef.current = setTimeout(() => {
106
+ setHoveredProvider(null);
107
+ setModelSearch('');
108
+ }, 300); // 300ms grace period to cross the gap smoothly
109
+ }, [cancelCloseTimer]);
110
+
111
+ // Cleanup timers on unmount
112
+ useEffect(() => () => {
113
+ if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
114
+ if (openTimerRef.current) clearTimeout(openTimerRef.current);
115
+ if (repositionTimerRef.current) clearTimeout(repositionTimerRef.current);
116
+ }, []);
117
+
118
+ // Fetch settings
101
119
  useEffect(() => {
102
120
  let cancelled = false;
103
121
  const doFetch = () => {
@@ -108,192 +126,444 @@ export default function ProviderModelCapsule({
108
126
  };
109
127
  doFetch();
110
128
  const onVisible = () => { if (document.visibilityState === 'visible') doFetch(); };
111
- const onSettingsChanged = () => doFetch();
129
+ const onChange = () => doFetch();
112
130
  document.addEventListener('visibilitychange', onVisible);
113
- window.addEventListener('mindos:settings-changed', onSettingsChanged);
114
- return () => { cancelled = true; document.removeEventListener('visibilitychange', onVisible); window.removeEventListener('mindos:settings-changed', onSettingsChanged); };
131
+ window.addEventListener('mindos:settings-changed', onChange);
132
+ return () => { cancelled = true; document.removeEventListener('visibilitychange', onVisible); window.removeEventListener('mindos:settings-changed', onChange); };
115
133
  }, []);
116
134
 
117
- const defaultProvider = (settingsData?.ai?.provider && isProviderId(settingsData.ai.provider))
118
- ? settingsData.ai.provider as ProviderId
119
- : 'anthropic';
135
+ const defaultProvider = settingsData?.ai?.activeProvider || '';
136
+
120
137
  const configuredProviders = useMemo(
121
138
  () => settingsData ? getConfiguredProviders(settingsData) : [],
122
139
  [settingsData],
123
140
  );
124
141
 
125
- // Auto-clear stale override if the provider lost its API key
142
+ // Auto-clear stale override
126
143
  useEffect(() => {
127
- if (!settingsData || !value) return;
128
- if (!configuredProviders.includes(value)) {
129
- onChange(null);
130
- persistProvider(null);
144
+ if (!settingsData || !providerValue) return;
145
+ if (!configuredProviders.includes(providerValue)) {
146
+ onProviderChange(null); onModelChange(null);
147
+ persistProviderModel(null, null);
131
148
  }
132
- }, [settingsData, value, configuredProviders, onChange]);
149
+ }, [settingsData, providerValue, configuredProviders, onProviderChange, onModelChange]);
150
+
151
+ // Resolve active display
152
+ const activeProvider = providerValue ?? defaultProvider;
153
+ const activeEntry = findProvider(settingsData?.ai?.providers ?? [], String(activeProvider));
154
+ const activePreset = activeEntry ? PROVIDER_PRESETS[activeEntry.protocol] : null;
155
+ const defaultModel = activeEntry?.model
156
+ || activePreset?.defaultModel || '';
157
+ const displayModel = modelValue || defaultModel;
158
+ const displayName = activeEntry?.name
159
+ || (activePreset ? (locale === 'zh' ? activePreset.nameZh : activePreset.name) : String(activeProvider));
133
160
 
134
- const activeProvider = value ?? defaultProvider;
135
- const activePreset = PROVIDER_PRESETS[activeProvider];
136
- const activeModel = settingsData?.ai?.providers?.[activeProvider]?.model || activePreset.defaultModel;
137
- const displayName = locale === 'zh' ? activePreset.nameZh : activePreset.name;
161
+ /* ── Dropdown positioning ── */
162
+ const [dropdownStyle, setDropdownStyle] = useState<React.CSSProperties>({});
163
+ const repositionTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
138
164
 
139
165
  const reposition = useCallback(() => {
140
166
  if (!triggerRef.current) return;
141
167
  const rect = triggerRef.current.getBoundingClientRect();
142
- const spaceAbove = rect.top;
143
- const spaceBelow = window.innerHeight - rect.bottom;
144
- const estimatedH = 240;
145
- const direction: 'up' | 'down' = spaceAbove > spaceBelow && spaceAbove > estimatedH ? 'up' : 'down';
146
- setPos({
147
- left: rect.left,
148
- top: direction === 'up' ? rect.top - 6 : rect.bottom + 6,
149
- direction,
168
+ const goUp = rect.top > window.innerHeight - rect.bottom && rect.top > 280;
169
+ setDropdownStyle({
170
+ position: 'fixed',
171
+ left: Math.min(rect.left, window.innerWidth - 230),
172
+ ...(goUp ? { bottom: window.innerHeight - rect.top + 6 } : { top: rect.bottom + 6 }),
173
+ zIndex: 50,
150
174
  });
151
175
  }, []);
152
176
 
177
+ // Debounce repositioning to prevent jank from rapid mouse events
178
+ const debouncedReposition = useCallback(() => {
179
+ if (repositionTimerRef.current) clearTimeout(repositionTimerRef.current);
180
+ repositionTimerRef.current = setTimeout(() => {
181
+ reposition();
182
+ }, 0); // Use requestAnimationFrame-like timing
183
+ }, [reposition]);
184
+
185
+ useEffect(() => { if (open) reposition(); }, [open, reposition]);
153
186
  useEffect(() => {
154
187
  if (!open) return;
155
- reposition();
156
- }, [open, reposition]);
188
+ window.addEventListener('scroll', debouncedReposition, true);
189
+ window.addEventListener('resize', debouncedReposition);
190
+ return () => {
191
+ window.removeEventListener('scroll', debouncedReposition, true);
192
+ window.removeEventListener('resize', debouncedReposition);
193
+ if (repositionTimerRef.current) clearTimeout(repositionTimerRef.current);
194
+ };
195
+ }, [open, debouncedReposition]);
157
196
 
197
+ // Close on outside click — check trigger, provider panel, and flyout
158
198
  useEffect(() => {
159
199
  if (!open) return;
160
200
  const handler = (e: MouseEvent) => {
161
201
  const target = e.target as Node;
162
- if (
163
- triggerRef.current && !triggerRef.current.contains(target) &&
164
- dropdownRef.current && !dropdownRef.current.contains(target)
165
- ) {
166
- setOpen(false);
167
- }
202
+ if (triggerRef.current?.contains(target)) return;
203
+ if (containerRef.current?.contains(target)) return;
204
+ if (flyoutRef.current?.contains(target)) return;
205
+ setOpen(false); setHoveredProvider(null);
168
206
  };
169
207
  document.addEventListener('mousedown', handler);
170
208
  return () => document.removeEventListener('mousedown', handler);
171
209
  }, [open]);
172
210
 
211
+ // Escape key
173
212
  useEffect(() => {
174
213
  if (!open) return;
175
214
  const handler = (e: KeyboardEvent) => {
176
- if (e.key === 'Escape') setOpen(false);
215
+ if (e.key === 'Escape') {
216
+ if (hoveredProvider) { setHoveredProvider(null); setModelSearch(''); }
217
+ else setOpen(false);
218
+ }
177
219
  };
178
220
  document.addEventListener('keydown', handler);
179
221
  return () => document.removeEventListener('keydown', handler);
180
- }, [open]);
222
+ }, [open, hoveredProvider]);
181
223
 
182
- useEffect(() => {
183
- if (!open) return;
184
- window.addEventListener('scroll', reposition, true);
185
- window.addEventListener('resize', reposition);
186
- return () => {
187
- window.removeEventListener('scroll', reposition, true);
188
- window.removeEventListener('resize', reposition);
189
- };
190
- }, [open, reposition]);
224
+ /* ── Model fetching ── */
225
+ const fetchModels = useCallback(async (providerId: string, force = false) => {
226
+ if (!force && modelsCacheRef.current[providerId]) {
227
+ setExpandedModels(modelsCacheRef.current[providerId]);
228
+ setModelsLoading(false);
229
+ return;
230
+ }
231
+ if (force) delete modelsCacheRef.current[providerId];
232
+ setModelsLoading(true); setModelsError(''); setExpandedModels(null);
233
+ const version = ++fetchVersionRef.current;
234
+ try {
235
+ const res = await fetch('/api/settings/list-models', {
236
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
237
+ body: JSON.stringify({ provider: providerId }),
238
+ });
239
+ if (version !== fetchVersionRef.current) return;
240
+ const json = await res.json();
241
+ if (version !== fetchVersionRef.current) return;
242
+ if (json.ok && Array.isArray(json.models)) {
243
+ modelsCacheRef.current[providerId] = json.models;
244
+ setExpandedModels(json.models);
245
+ } else { setModelsError(json.error || 'Failed'); }
246
+ } catch {
247
+ if (version === fetchVersionRef.current) setModelsError('Network error');
248
+ } finally {
249
+ if (version === fetchVersionRef.current) setModelsLoading(false);
250
+ }
251
+ }, []);
191
252
 
192
- const handleSelect = useCallback((provider: ProviderId | null) => {
193
- onChange(provider);
194
- persistProvider(provider);
195
- setOpen(false);
196
- }, [onChange]);
253
+ // Determine if a provider can show model flyout
254
+ const canProviderExpand = useCallback((id: string) => {
255
+ const entry = findProvider(settingsData?.ai?.providers ?? [], id);
256
+ if (!entry) return false;
257
+ const preset = PROVIDER_PRESETS[entry.protocol];
258
+ return preset?.supportsListModels ?? false;
259
+ }, [settingsData]);
197
260
 
261
+ // Compute flyout position: anchored to right edge of provider panel, aligned to hovered row
262
+ const computeFlyoutPosition = useCallback(() => {
263
+ const panel = providerPanelRef.current;
264
+ const row = hoveredRowRef.current;
265
+ if (!panel || !row) return;
266
+ const panelRect = panel.getBoundingClientRect();
267
+ const rowRect = row.getBoundingClientRect();
268
+ const flyoutWidth = 220;
269
+ const flyoutMaxH = 320; // approx max height of flyout
270
+ const gap = 4;
271
+ const left = panelRect.right + gap;
272
+ // Align top to the hovered row by default
273
+ let top = rowRect.top;
274
+ // If flyout would overflow below viewport, shift upward
275
+ const spaceBelow = window.innerHeight - top;
276
+ if (spaceBelow < flyoutMaxH) {
277
+ // Align bottom of flyout to bottom of row instead
278
+ top = Math.max(8, rowRect.bottom - flyoutMaxH);
279
+ }
280
+ // If flyout would overflow right edge, flip to left side
281
+ const actualLeft = left + flyoutWidth > window.innerWidth - 8
282
+ ? panelRect.left - flyoutWidth - gap
283
+ : left;
284
+ setFlyoutStyle({
285
+ position: 'fixed',
286
+ left: actualLeft,
287
+ top,
288
+ zIndex: 51,
289
+ });
290
+ }, []);
291
+
292
+ // Open flyout for a provider (debounced to prevent flicker)
293
+ const openFlyout = useCallback((providerId: string) => {
294
+ cancelCloseTimer();
295
+ if (openTimerRef.current) clearTimeout(openTimerRef.current);
296
+ openTimerRef.current = setTimeout(() => {
297
+ if (!canProviderExpand(providerId)) {
298
+ setHoveredProvider(null); setModelSearch('');
299
+ return;
300
+ }
301
+ setHoveredProvider(providerId);
302
+ setModelSearch(''); setModelHighlight(-1); setModelsError('');
303
+ setExpandedModels(modelsCacheRef.current[providerId] ?? null);
304
+ if (!modelsCacheRef.current[providerId]) fetchModels(providerId);
305
+ // Position flyout after state update
306
+ requestAnimationFrame(() => {
307
+ computeFlyoutPosition();
308
+ setTimeout(() => searchInputRef.current?.focus(), 50);
309
+ });
310
+ }, 80);
311
+ }, [cancelCloseTimer, fetchModels, computeFlyoutPosition, canProviderExpand]);
312
+
313
+ // Close flyout for non-expandable items (immediate, no debounce)
314
+ const closeFlyoutImmediate = useCallback(() => {
315
+ cancelCloseTimer();
316
+ if (openTimerRef.current) { clearTimeout(openTimerRef.current); openTimerRef.current = undefined; }
317
+ setHoveredProvider(null); setModelSearch('');
318
+ }, [cancelCloseTimer]);
319
+
320
+ /* ── Selection handlers ── */
321
+ const handleSelectProvider = useCallback((provider: ProviderSelection) => {
322
+ onProviderChange(provider); onModelChange(null);
323
+ persistProviderModel(provider, null);
324
+ setOpen(false); setHoveredProvider(null); setModelSearch('');
325
+ }, [onProviderChange, onModelChange]);
326
+
327
+ const handleSelectModel = useCallback((provider: ProviderSelection, model: string) => {
328
+ onProviderChange(provider); onModelChange(model);
329
+ persistProviderModel(provider, model);
330
+ setOpen(false); setHoveredProvider(null); setModelSearch('');
331
+ }, [onProviderChange, onModelChange]);
332
+
333
+ /* ── Filtered models ── */
334
+ const filteredModels = useMemo(() => {
335
+ if (!expandedModels) return [];
336
+ if (!modelSearch.trim()) return expandedModels;
337
+ const q = modelSearch.toLowerCase();
338
+ return expandedModels.filter(m => m.toLowerCase().includes(q));
339
+ }, [expandedModels, modelSearch]);
340
+
341
+ useEffect(() => { setModelHighlight(-1); }, [filteredModels]);
342
+
343
+ const handleModelKeyDown = useCallback((e: React.KeyboardEvent) => {
344
+ if (!filteredModels.length) return;
345
+ if (e.key === 'ArrowDown') { e.preventDefault(); setModelHighlight(i => (i + 1) % filteredModels.length); }
346
+ else if (e.key === 'ArrowUp') { e.preventDefault(); setModelHighlight(i => (i - 1 + filteredModels.length) % filteredModels.length); }
347
+ else if (e.key === 'Enter' && modelHighlight >= 0 && modelHighlight < filteredModels.length && hoveredProvider) {
348
+ e.preventDefault(); handleSelectModel(hoveredProvider as ProviderSelection, filteredModels[modelHighlight]);
349
+ } else if (e.key === 'Escape') { e.preventDefault(); setHoveredProvider(null); setModelSearch(''); }
350
+ }, [filteredModels, modelHighlight, hoveredProvider, handleSelectModel]);
351
+
352
+ useEffect(() => {
353
+ if (modelHighlight < 0 || !modelListRef.current) return;
354
+ const items = modelListRef.current.querySelectorAll('[data-model-item]');
355
+ items[modelHighlight]?.scrollIntoView({ block: 'nearest' });
356
+ }, [modelHighlight]);
357
+
358
+ /* ── Guards ── */
198
359
  if (!settingsData || configuredProviders.length === 0) return null;
199
360
 
200
- const modelShort = activeModel.length > 24
201
- ? activeModel.slice(0, 22) + '…'
202
- : activeModel;
361
+ const modelShort = (displayModel || '').length > 20
362
+ ? (displayModel || '').slice(0, 18) + '…' : displayModel;
363
+ // For built-in providers, use shortLabel; for custom, truncate if too long
364
+ const providerDisplay = (displayName || '').length > 12
365
+ ? (displayName || '').slice(0, 10) + '…'
366
+ : (activePreset?.shortLabel || displayName);
367
+ const capsuleTooltip = `${displayName} · ${displayModel}`;
368
+ const providerIds = configuredProviders;
369
+ const hasModelOverride = !!(modelValue && modelValue !== defaultModel);
203
370
 
204
- const dropdown = open && pos ? (
205
- <div
206
- ref={dropdownRef}
207
- role="listbox"
208
- aria-label={t.ask.providerCapsule}
209
- className="fixed z-50 min-w-[200px] max-w-[280px] rounded-lg border border-border bg-card shadow-lg py-1 animate-in fade-in-0 zoom-in-95 duration-100"
210
- style={{
211
- left: pos.left,
212
- ...(pos.direction === 'up'
213
- ? { bottom: window.innerHeight - pos.top }
214
- : { top: pos.top }),
215
- }}
216
- >
217
- {/* Default (use server settings) */}
218
- <button
219
- type="button"
220
- role="option"
221
- aria-selected={value === null}
222
- onClick={() => handleSelect(null)}
223
- className="flex w-full items-center gap-2.5 px-3 py-2 text-xs text-left transition-colors hover:bg-muted"
371
+ /* ── Render: flyout (right panel) positioned absolutely via portal ── */
372
+ const renderFlyout = () => {
373
+ if (!hoveredProvider) return null;
374
+ const hovEntry = findProvider(settingsData?.ai?.providers ?? [], hoveredProvider);
375
+ const preset = hovEntry ? PROVIDER_PRESETS[hovEntry.protocol] : null;
376
+ const displayName = hovEntry?.name || (preset ? (locale === 'zh' ? preset.nameZh : preset.name) : String(hoveredProvider));
377
+
378
+ return createPortal(
379
+ <div
380
+ ref={flyoutRef}
381
+ style={flyoutStyle}
382
+ className="w-[220px] rounded-lg border border-border bg-card shadow-lg py-1 animate-in fade-in-0 zoom-in-95 duration-100"
383
+ onMouseEnter={cancelCloseTimer}
384
+ onMouseLeave={startCloseTimer}
224
385
  >
225
- <Settings size={12} className="shrink-0 text-muted-foreground" />
226
- <div className="flex-1 min-w-0">
227
- <div className="font-medium">{t.ask.providerDefault}</div>
228
- <div className="text-2xs text-muted-foreground mt-0.5">
229
- {locale === 'zh'
230
- ? PROVIDER_PRESETS[defaultProvider].nameZh
231
- : PROVIDER_PRESETS[defaultProvider].name} · {settingsData?.ai?.providers?.[defaultProvider]?.model || PROVIDER_PRESETS[defaultProvider].defaultModel}
386
+ {/* Header */}
387
+ <div className="flex items-center justify-between px-3 py-1.5 border-b border-border/40">
388
+ <span className="text-2xs font-medium text-muted-foreground">
389
+ {displayName}
390
+ </span>
391
+ <button
392
+ type="button"
393
+ onClick={() => fetchModels(hoveredProvider, true)}
394
+ disabled={modelsLoading}
395
+ className="p-1 text-muted-foreground/50 hover:text-foreground transition-colors disabled:opacity-30"
396
+ title="Refresh"
397
+ >
398
+ <RefreshCw size={10} className={modelsLoading ? 'animate-spin' : ''} />
399
+ </button>
400
+ </div>
401
+ {/* Search */}
402
+ <div className="px-2 pt-1.5 pb-1">
403
+ <div className="relative">
404
+ <Search size={10} className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground/40" />
405
+ <input
406
+ ref={searchInputRef}
407
+ type="text"
408
+ value={modelSearch}
409
+ onChange={e => setModelSearch(e.target.value)}
410
+ onKeyDown={handleModelKeyDown}
411
+ placeholder={t.ask?.searchModels ?? 'Search...'}
412
+ className="w-full text-2xs pl-6 pr-2 py-1 rounded border border-border/50 bg-transparent text-foreground placeholder:text-muted-foreground/40 focus:outline-none focus:border-[var(--amber)]/50"
413
+ autoComplete="off"
414
+ />
232
415
  </div>
233
416
  </div>
234
- {value === null && <Check size={12} className="shrink-0 text-[var(--amber)]" />}
235
- </button>
417
+ {/* List */}
418
+ <div ref={modelListRef} className="max-h-[240px] overflow-y-auto px-1 pb-1">
419
+ {modelsLoading && !expandedModels && (
420
+ <div className="flex items-center gap-1.5 px-2 py-3 text-2xs text-muted-foreground justify-center">
421
+ <Loader2 size={10} className="animate-spin" />
422
+ {t.ask?.loadingModels ?? 'Loading...'}
423
+ </div>
424
+ )}
425
+ {modelsError && !modelsLoading && (
426
+ <div className="px-2 py-3 text-2xs text-destructive text-center">{modelsError}</div>
427
+ )}
428
+ {!modelsLoading && !modelsError && filteredModels.length === 0 && expandedModels !== null && (
429
+ <div className="px-2 py-3 text-2xs text-muted-foreground text-center">
430
+ {modelSearch ? 'No matches' : 'No models'}
431
+ </div>
432
+ )}
433
+ {filteredModels.map((m, i) => {
434
+ const isModelSelected = providerValue === hoveredProvider && modelValue === m;
435
+ const defModel = hovEntry?.model || preset?.defaultModel;
436
+ return (
437
+ <button
438
+ key={m} type="button" data-model-item
439
+ onClick={() => handleSelectModel(hoveredProvider as ProviderSelection, m)}
440
+ className={`w-full text-left px-2 py-1 text-2xs rounded transition-colors flex items-center gap-1 ${
441
+ isModelSelected ? 'bg-[var(--amber)]/12 text-foreground font-medium'
442
+ : i === modelHighlight ? 'bg-accent' : 'hover:bg-accent/60'
443
+ }`}
444
+ >
445
+ {isModelSelected
446
+ ? <Check size={9} className="shrink-0 text-[var(--amber)]" />
447
+ : <span className="w-[9px] shrink-0" />}
448
+ <span className="truncate">{m}</span>
449
+ {m === defModel && !isModelSelected && (
450
+ <span className="ml-auto text-[9px] text-muted-foreground/30 shrink-0">default</span>
451
+ )}
452
+ </button>
453
+ );
454
+ })}
455
+ </div>
456
+ </div>,
457
+ document.body,
458
+ );
459
+ };
236
460
 
237
- {configuredProviders.length > 0 && (
238
- <div className="mx-2 my-1 border-t border-border/60" />
239
- )}
461
+ /* ── Render: dropdown ── */
462
+ const dropdown = open ? (
463
+ <div
464
+ ref={containerRef}
465
+ style={dropdownStyle}
466
+ >
467
+ {/* Provider list (sole child — never moves) */}
468
+ <div
469
+ ref={providerPanelRef}
470
+ role="listbox"
471
+ aria-label={t.ask?.providerCapsule ?? 'Provider'}
472
+ className="w-[220px] rounded-lg border border-border bg-card shadow-lg py-1 animate-in fade-in-0 zoom-in-95 duration-100"
473
+ style={{ maxHeight: '70vh', overflowY: 'auto' }}
474
+ >
475
+ {providerIds.map((id) => {
476
+ const entry = findProvider(settingsData?.ai?.providers ?? [], id);
477
+ if (!entry) return null;
478
+ const preset = PROVIDER_PRESETS[entry.protocol];
479
+ const provName = entry.name || (locale === 'zh' ? preset?.nameZh : preset?.name) || id;
480
+ const provModel = modelValue && providerValue === id ? modelValue
481
+ : entry.model || preset?.defaultModel || '';
482
+ const isSelected = providerValue === id || (!providerValue && defaultProvider === id);
483
+ const isHovered = hoveredProvider === id;
484
+ const canExpand = canProviderExpand(id);
240
485
 
241
- {configuredProviders.map((id) => {
242
- const preset = PROVIDER_PRESETS[id];
243
- const provName = locale === 'zh' ? preset.nameZh : preset.name;
244
- const provModel = settingsData?.ai?.providers?.[id]?.model || preset.defaultModel;
245
- const isSelected = value === id;
246
- return (
247
- <button
248
- key={id}
249
- type="button"
250
- role="option"
251
- aria-selected={isSelected}
252
- onClick={() => handleSelect(id)}
253
- className="flex w-full items-center gap-2.5 px-3 py-2 text-xs text-left transition-colors hover:bg-muted"
254
- >
255
- <Cpu size={12} className="shrink-0 text-muted-foreground" />
256
- <div className="flex-1 min-w-0">
257
- <div className="font-medium">{provName}</div>
258
- <div className="text-2xs text-muted-foreground mt-0.5 truncate">{provModel}</div>
486
+ return (
487
+ <div
488
+ key={id}
489
+ ref={isHovered ? hoveredRowRef : undefined}
490
+ onMouseEnter={() => canExpand ? openFlyout(id) : closeFlyoutImmediate()}
491
+ onMouseLeave={startCloseTimer}
492
+ >
493
+ <div className={`flex w-full items-center text-xs transition-colors ${isHovered ? 'bg-accent/60' : 'hover:bg-muted/60'}`}>
494
+ <button
495
+ type="button" role="option" aria-selected={isSelected}
496
+ onClick={() => handleSelectProvider(id as ProviderSelection)}
497
+ className="flex flex-1 items-center gap-2 px-3 py-1.5 min-w-0"
498
+ >
499
+ <div className="flex-1 min-w-0 truncate">
500
+ <span className={`text-xs ${isSelected ? 'font-medium text-foreground' : 'text-foreground/80'}`}>
501
+ {provName}
502
+ </span>
503
+ <span className="text-2xs text-muted-foreground ml-1.5">{provModel}</span>
504
+ </div>
505
+ {isSelected && <Check size={11} className="shrink-0 text-[var(--amber)]" />}
506
+ </button>
507
+ {canExpand && (
508
+ <button
509
+ type="button"
510
+ onClick={(e) => {
511
+ e.stopPropagation();
512
+ if (hoveredProvider === id) closeFlyoutImmediate();
513
+ else openFlyout(id);
514
+ }}
515
+ className={`shrink-0 px-1.5 py-1.5 mr-1 rounded transition-colors ${
516
+ isHovered ? 'text-foreground' : 'text-muted-foreground/40 hover:text-muted-foreground'
517
+ }`}
518
+ title={t.ask?.selectModel ?? 'Select model'}
519
+ >
520
+ <ChevronRight size={11} />
521
+ </button>
522
+ )}
523
+ </div>
259
524
  </div>
260
- {isSelected && <Check size={12} className="shrink-0 text-[var(--amber)]" />}
261
- </button>
262
- );
263
- })}
525
+ );
526
+ })}
527
+ </div>
264
528
  </div>
265
529
  ) : null;
266
530
 
531
+ /* ── Capsule button ── */
267
532
  return (
268
533
  <>
269
534
  <button
270
535
  ref={triggerRef}
271
536
  type="button"
272
- onClick={() => { if (!disabled) setOpen(v => !v); }}
537
+ onClick={() => {
538
+ if (disabled) return;
539
+ setOpen(v => !v);
540
+ if (open) { setHoveredProvider(null); setModelSearch(''); }
541
+ }}
273
542
  disabled={disabled}
274
543
  className={`
275
544
  inline-flex items-center gap-1 rounded-full px-2.5 py-0.5
276
- text-2xs font-medium transition-colors select-none
545
+ text-2xs font-medium transition-colors select-none max-w-[260px]
277
546
  border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
278
547
  disabled:opacity-40 disabled:cursor-not-allowed
279
- ${value
548
+ ${providerValue || hasModelOverride
280
549
  ? 'bg-[var(--amber)]/10 border-[var(--amber)]/25 text-foreground hover:bg-[var(--amber)]/15'
281
550
  : 'bg-muted/50 border-border/50 text-muted-foreground hover:bg-muted hover:text-foreground'
282
551
  }
283
552
  `}
284
- title={t.ask.providerCapsule}
553
+ title={capsuleTooltip}
285
554
  aria-expanded={open}
286
555
  aria-haspopup="listbox"
287
556
  >
288
557
  <Cpu size={11} className="shrink-0" />
289
- <span className="truncate max-w-[140px]">
290
- {displayName}
558
+ <span className="truncate">
559
+ {providerDisplay}
291
560
  <span className="text-muted-foreground"> · </span>
292
- <span className="text-muted-foreground">{modelShort}</span>
561
+ <span className={hasModelOverride ? 'text-[var(--amber)]' : 'text-muted-foreground'}>{modelShort}</span>
293
562
  </span>
294
563
  <ChevronDown size={10} className="shrink-0 text-muted-foreground" />
295
564
  </button>
296
565
  {typeof document !== 'undefined' && dropdown && createPortal(dropdown, document.body)}
566
+ {typeof document !== 'undefined' && open && renderFlyout()}
297
567
  </>
298
568
  );
299
569
  }