@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
@@ -1,51 +1,38 @@
1
1
  'use client';
2
2
 
3
- import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
4
- import { AlertCircle, ChevronDown, Loader2, Sparkles, Bot, Monitor, ExternalLink } from 'lucide-react';
5
- import type { AiSettings, AgentSettings, ProviderConfig, SettingsData, AiTabProps } from './types';
6
- import { Field, Select, Input, EnvBadge, ApiKeyInput, Toggle, SettingCard, SettingRow } from './Primitives';
3
+ import { useState, useRef, useCallback, useEffect } from 'react';
4
+ import { AlertCircle, Sparkles, Bot, Monitor, ExternalLink, RotateCcw, Trash2, X } from 'lucide-react';
5
+ import type { AiTabProps } from './types';
6
+ import { Field, Select, Input, PasswordInput, EnvBadge, Toggle, SettingCard, SettingRow } from './Primitives';
7
7
  import { useLocale } from '@/lib/stores/locale-store';
8
- import { type ProviderId, PROVIDER_PRESETS, isProviderId, getApiKeyEnvVar, getDefaultBaseUrl } from '@/lib/agent/providers';
8
+ import { type ProviderId, PROVIDER_PRESETS, ALL_PROVIDER_IDS, getApiKeyEnvVar, getDefaultBaseUrl } from '@/lib/agent/providers';
9
9
  import ProviderSelect from '@/components/shared/ProviderSelect';
10
-
11
- type TestState = 'idle' | 'testing' | 'ok' | 'error';
12
- type ErrorCode = 'auth_error' | 'model_not_found' | 'rate_limited' | 'network_error' | 'unknown';
13
-
14
- interface TestResult {
15
- state: TestState;
16
- latency?: number;
17
- error?: string;
18
- code?: ErrorCode;
19
- }
20
-
21
- function errorMessage(t: AiTabProps['t'], code?: ErrorCode): string {
22
- switch (code) {
23
- case 'auth_error': return t.settings.ai.testKeyAuthError;
24
- case 'model_not_found': return t.settings.ai.testKeyModelNotFound;
25
- case 'rate_limited': return t.settings.ai.testKeyRateLimited;
26
- case 'network_error': return t.settings.ai.testKeyNetworkError;
27
- default: return t.settings.ai.testKeyUnknown;
28
- }
29
- }
10
+ import ModelInput from '@/components/shared/ModelInput';
11
+ import { type Provider, generateProviderId } from '@/lib/custom-endpoints';
12
+ import { useCustomProviderForm, type TestResult } from './useCustomProviderForm';
13
+ import CustomProviderFields from './CustomProviderFields';
14
+ import { TestButton } from './TestButton';
30
15
 
31
16
  export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
32
17
  const { locale } = useLocale();
33
18
  const env = data.envOverrides ?? {};
34
- const envVal = data.envValues ?? {};
35
- const provider = data.ai.provider;
36
- const preset = isProviderId(provider) ? PROVIDER_PRESETS[provider] : null;
19
+
20
+ // ── Current provider from the unified array ──
21
+ const current = data.ai.providers.find(p => p.id === data.ai.activeProvider);
22
+ const preset = current ? PROVIDER_PRESETS[current.protocol] : null;
37
23
 
38
24
  const [testResult, setTestResult] = useState<Record<string, TestResult>>({});
25
+ const [customFormOpen, setCustomFormOpen] = useState(false);
39
26
  const okTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
40
- const prevProviderRef = useRef(provider);
27
+ const prevProviderRef = useRef(data.ai.activeProvider);
41
28
 
42
29
  useEffect(() => {
43
- if (prevProviderRef.current !== provider) {
44
- prevProviderRef.current = provider;
30
+ if (prevProviderRef.current !== data.ai.activeProvider) {
31
+ prevProviderRef.current = data.ai.activeProvider;
45
32
  setTestResult({});
46
33
  if (okTimerRef.current) { clearTimeout(okTimerRef.current); okTimerRef.current = undefined; }
47
34
  }
48
- }, [provider]);
35
+ }, [data.ai.activeProvider]);
49
36
 
50
37
  useEffect(() => () => { if (okTimerRef.current) clearTimeout(okTimerRef.current); }, []);
51
38
 
@@ -54,15 +41,17 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
54
41
  try { localStorage.setItem('mindos-reconnect-retries', String(v)); } catch (err) { console.warn("[AiTab] localStorage setItem reconnectRetries failed:", err); }
55
42
  }, [data.agent?.reconnectRetries]);
56
43
 
57
- const handleTestKey = useCallback(async (providerName: ProviderId) => {
58
- const prov = data.ai.providers?.[providerName] ?? {} as ProviderConfig;
59
- setTestResult(prev => ({ ...prev, [providerName]: { state: 'testing' } }));
44
+ // ── Test key for the current provider ──
45
+ const handleTestKey = useCallback(async () => {
46
+ if (!current) return;
47
+ const pid = current.id;
48
+ setTestResult(prev => ({ ...prev, [pid]: { state: 'testing' } }));
60
49
 
61
50
  try {
62
- const body: Record<string, string> = { provider: providerName };
63
- if (prov.apiKey) body.apiKey = prov.apiKey;
64
- if (prov.model) body.model = prov.model;
65
- if (prov.baseUrl) body.baseUrl = prov.baseUrl;
51
+ const body: Record<string, string> = { provider: current.protocol };
52
+ if (current.apiKey) body.apiKey = current.apiKey;
53
+ if (current.model) body.model = current.model;
54
+ if (current.baseUrl) body.baseUrl = current.baseUrl;
66
55
 
67
56
  const res = await fetch('/api/settings/test-key', {
68
57
  method: 'POST',
@@ -72,86 +61,87 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
72
61
  const json = await res.json();
73
62
 
74
63
  if (json.ok) {
75
- setTestResult(prev => ({ ...prev, [providerName]: { state: 'ok', latency: json.latency } }));
64
+ setTestResult(prev => ({ ...prev, [pid]: { state: 'ok', latency: json.latency } }));
76
65
  if (okTimerRef.current) clearTimeout(okTimerRef.current);
77
66
  okTimerRef.current = setTimeout(() => {
78
- setTestResult(prev => ({ ...prev, [providerName]: { state: 'idle' } }));
67
+ setTestResult(prev => ({ ...prev, [pid]: { state: 'idle' } }));
79
68
  }, 8000);
80
69
  } else {
81
70
  setTestResult(prev => ({
82
71
  ...prev,
83
- [providerName]: { state: 'error', error: json.error, code: json.code },
72
+ [pid]: { state: 'error', error: json.error, code: json.code },
84
73
  }));
85
74
  }
86
75
  } catch {
87
76
  setTestResult(prev => ({
88
77
  ...prev,
89
- [providerName]: { state: 'error', code: 'network_error', error: 'Network error' },
78
+ [pid]: { state: 'error', code: 'network_error', error: 'Network error' },
90
79
  }));
91
80
  }
92
- }, [data.ai.providers]);
81
+ }, [current]);
93
82
 
94
- const patchProvider = useCallback((name: ProviderId, patch: Partial<ProviderConfig>) => {
83
+ // ── Patch any field on the current provider (auto-save) ──
84
+ const patchProvider = useCallback((patch: Partial<Provider>) => {
85
+ if (!current) return;
95
86
  if ('apiKey' in patch) {
96
- setTestResult(prev => ({ ...prev, [name]: { state: 'idle' } }));
87
+ setTestResult(prev => ({ ...prev, [current.id]: { state: 'idle' } }));
97
88
  }
98
89
  updateAi({
99
- providers: {
100
- ...data.ai.providers,
101
- [name]: { ...data.ai.providers?.[name], ...patch },
102
- },
90
+ providers: data.ai.providers.map(p => p.id === current.id ? { ...p, ...patch } : p),
103
91
  });
104
- }, [data.ai.providers, updateAi]);
92
+ }, [current, data.ai.providers, updateAi]);
105
93
 
106
- const currentConfig = data.ai.providers?.[provider] ?? { apiKey: '', model: '', baseUrl: '' };
107
- const envKeyName = preset ? getApiKeyEnvVar(provider) : undefined;
108
- const activeApiKey = currentConfig.apiKey;
94
+ // ── Env key detection ──
95
+ const envKeyName = current ? getApiKeyEnvVar(current.protocol) : undefined;
109
96
  const activeEnvKey = envKeyName ? env[envKeyName] : false;
110
97
  const hasFallbackKey = !!preset?.apiKeyFallback;
111
- const missingApiKey = !activeApiKey && !activeEnvKey && !hasFallbackKey;
112
98
 
113
- const configuredProviders = new Set(
114
- Object.entries(data.ai.providers ?? {})
115
- .filter(([id, cfg]) => (cfg && cfg.apiKey) || PROVIDER_PRESETS[id as ProviderId]?.apiKeyFallback)
116
- .map(([id]) => id as ProviderId),
117
- );
118
-
119
- const renderTestButton = (providerName: ProviderId, hasKey: boolean, hasEnv: boolean) => {
120
- const result = testResult[providerName] ?? { state: 'idle' as TestState };
121
- const hasFallback = !!PROVIDER_PRESETS[providerName]?.apiKeyFallback;
122
- const disabled = result.state === 'testing' || (!hasKey && !hasEnv && !hasFallback);
99
+ // ── Reset provider (clear fields to defaults) ──
100
+ const resetProvider = useCallback(() => {
101
+ if (!current) return;
102
+ const defaults = PROVIDER_PRESETS[current.protocol];
103
+ setTestResult(prev => ({ ...prev, [current.id]: { state: 'idle' } }));
104
+ updateAi({
105
+ providers: data.ai.providers.map(p => p.id === current.id ? {
106
+ ...p,
107
+ apiKey: '',
108
+ model: '',
109
+ baseUrl: defaults?.fixedBaseUrl ?? '',
110
+ } : p),
111
+ });
112
+ }, [current, data.ai.providers, updateAi]);
123
113
 
124
- return (
125
- <div className="flex items-center gap-2 mt-1.5">
126
- <button
127
- type="button"
128
- disabled={disabled}
129
- title={disabled ? t.hints.testInProgressOrNoKey : undefined}
130
- onClick={() => handleTestKey(providerName)}
131
- className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg border border-border text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
132
- >
133
- {result.state === 'testing' ? (
134
- <>
135
- <Loader2 size={12} className="animate-spin" />
136
- {t.settings.ai.testKeyTesting}
137
- </>
138
- ) : (
139
- t.settings.ai.testKey
140
- )}
141
- </button>
142
- {result.state === 'ok' && result.latency != null && (
143
- <span className="text-xs text-success">
144
- {t.settings.ai.testKeyOk(result.latency)}
145
- </span>
146
- )}
147
- {result.state === 'error' && (
148
- <span className="text-xs text-error">✗ {errorMessage(t, result.code)}</span>
149
- )}
150
- </div>
151
- );
152
- };
114
+ // ── Delete provider ──
115
+ const deleteProvider = useCallback(() => {
116
+ if (!current) return;
117
+ const remaining = data.ai.providers.filter(p => p.id !== current.id);
118
+ const fallbackId = remaining.length > 0 ? remaining[0].id : '';
119
+ updateAi({
120
+ activeProvider: fallbackId,
121
+ providers: remaining,
122
+ });
123
+ setTestResult(prev => { const n = { ...prev }; delete n[current.id]; return n; });
124
+ }, [current, data.ai.providers, updateAi]);
125
+
126
+ // ── Save handler for the "Add Provider" form ──
127
+ const handleSaveNew = useCallback((formProvider: Provider) => {
128
+ // The form uses `protocol` directly now (no mapping needed)
129
+ const newProvider: Provider = {
130
+ id: formProvider.id || generateProviderId(),
131
+ name: formProvider.name,
132
+ protocol: formProvider.protocol,
133
+ apiKey: formProvider.apiKey,
134
+ model: formProvider.model,
135
+ baseUrl: formProvider.baseUrl,
136
+ };
137
+ updateAi({
138
+ activeProvider: newProvider.id,
139
+ providers: [...data.ai.providers, newProvider],
140
+ });
141
+ setCustomFormOpen(false);
142
+ }, [data.ai.providers, updateAi]);
153
143
 
154
- const displayName = preset ? (locale === 'zh' ? preset.nameZh : preset.name) : provider;
144
+ const displayName = current?.name ?? (locale === 'zh' ? '未选择' : 'No provider');
155
145
 
156
146
  return (
157
147
  <div className="space-y-4">
@@ -162,26 +152,77 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
162
152
  description={displayName}
163
153
  >
164
154
  <ProviderSelect
165
- value={provider}
166
- onChange={id => { if (id !== 'skip') updateAi({ provider: id }); }}
155
+ value={data.ai.activeProvider as ProviderId}
156
+ onChange={id => {
157
+ if (id !== 'skip') updateAi({ activeProvider: id });
158
+ setCustomFormOpen(false);
159
+ }}
167
160
  compact
168
- configuredProviders={configuredProviders}
161
+ customProviders={data.ai.providers}
162
+ onAdd={() => {
163
+ // Open form pre-filled with OpenAI defaults
164
+ const defaultId: ProviderId = 'openai';
165
+ const p = PROVIDER_PRESETS[defaultId];
166
+ const baseName = locale === 'zh' ? p.nameZh : p.name;
167
+ const names = new Set(data.ai.providers.map(cp => cp.name.toLowerCase()));
168
+ let finalName = baseName;
169
+ if (names.has(finalName.toLowerCase())) {
170
+ let n = 2;
171
+ while (names.has(`${baseName} (${n})`.toLowerCase())) n++;
172
+ finalName = `${baseName} (${n})`;
173
+ }
174
+ setCustomFormOpen(true);
175
+ }}
169
176
  />
170
177
 
171
- {/* Provider configuration fields */}
172
- {preset && (
173
- <div className="space-y-3 pt-3" style={{ borderTop: '1px solid var(--border)' }}>
174
- {/* 1. API Key — most essential, enter first */}
178
+ {/* Add new provider form */}
179
+ {customFormOpen && (
180
+ <CustomProviderForm
181
+ key="new"
182
+ onSave={handleSaveNew}
183
+ onCancel={() => setCustomFormOpen(false)}
184
+ t={t}
185
+ existingNames={data.ai.providers.map(p => p.name)}
186
+ />
187
+ )}
188
+
189
+ {/* ── Inline config fields for the selected provider ── */}
190
+ {!customFormOpen && current && (
191
+ <div className="space-y-3 pt-3 border-t border-border">
192
+ {/* Name + Protocol (inline, auto-save) */}
193
+ <div className="grid grid-cols-2 gap-3">
194
+ <Field label={locale === 'zh' ? '名称' : 'Name'}>
195
+ <Input
196
+ value={current.name}
197
+ onChange={e => patchProvider({ name: e.target.value })}
198
+ placeholder={locale === 'zh' ? '输入名称' : 'Enter name'}
199
+ />
200
+ </Field>
201
+ <Field label={locale === 'zh' ? '协议' : 'Protocol'}>
202
+ <Select
203
+ value={current.protocol}
204
+ onChange={e => patchProvider({ protocol: e.target.value as ProviderId })}
205
+ >
206
+ {ALL_PROVIDER_IDS.map(id => (
207
+ <option key={id} value={id}>
208
+ {locale === 'zh' ? PROVIDER_PRESETS[id].nameZh : PROVIDER_PRESETS[id].name}
209
+ </option>
210
+ ))}
211
+ </Select>
212
+ </Field>
213
+ </div>
214
+
215
+ {/* API Key */}
175
216
  <Field
176
217
  label={<>{t.settings.ai.apiKey} {envKeyName && <EnvBadge overridden={env[envKeyName]} />}</>}
177
- hint={activeEnvKey ? t.settings.ai.envFieldNote(envKeyName!) : hasFallbackKey ? t.settings.ai.keyOptionalHint : t.settings.ai.keyHint}
218
+ hint={preset && activeEnvKey ? t.settings.ai.envFieldNote(envKeyName!) : preset && hasFallbackKey ? t.settings.ai.keyOptionalHint : undefined}
178
219
  >
179
- <ApiKeyInput
180
- value={currentConfig.apiKey}
181
- onChange={v => patchProvider(provider, { apiKey: v })}
182
- labels={{ change: t.settings.ai.keyChange, cancel: t.settings.ai.keyCancel }}
220
+ <PasswordInput
221
+ value={current.apiKey}
222
+ onChange={v => patchProvider({ apiKey: v })}
223
+ placeholder="sk-..."
183
224
  />
184
- {preset.signupUrl && !currentConfig.apiKey && !activeEnvKey && (
225
+ {preset?.signupUrl && !current.apiKey && !activeEnvKey && (
185
226
  <a
186
227
  href={preset.signupUrl}
187
228
  target="_blank"
@@ -197,48 +238,51 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
197
238
  )}
198
239
  </Field>
199
240
 
200
- {/* 2. Base URL — before Model so "List Models" uses the correct endpoint */}
201
- {preset.supportsBaseUrl && (
202
- <Field
203
- label={t.settings.ai.baseUrl}
204
- hint={t.settings.ai.baseUrlHint}
205
- >
241
+ {/* Base URL */}
242
+ {(preset?.supportsBaseUrl || current.baseUrl) && (
243
+ <Field label="Base URL">
206
244
  <Input
207
- value={currentConfig.baseUrl ?? ''}
208
- onChange={e => patchProvider(provider, { baseUrl: e.target.value })}
209
- placeholder={preset.fixedBaseUrl || getDefaultBaseUrl(provider) || 'https://api.openai.com/v1'}
245
+ value={current.baseUrl}
246
+ onChange={e => patchProvider({ baseUrl: e.target.value })}
247
+ placeholder={preset?.fixedBaseUrl || getDefaultBaseUrl(current.protocol) || 'https://api.openai.com/v1'}
210
248
  />
211
249
  </Field>
212
250
  )}
213
251
 
214
- {/* 3. Model — after Base URL so "List Models" queries the right endpoint */}
215
- <Field label={t.settings.ai.model}>
252
+ {/* Model */}
253
+ <Field label={locale === 'zh' ? '模型' : 'Model'}>
216
254
  <ModelInput
217
- value={currentConfig.model}
218
- onChange={v => patchProvider(provider, { model: v })}
219
- placeholder={preset.defaultModel}
220
- provider={provider}
221
- apiKey={currentConfig.apiKey}
255
+ value={current.model}
256
+ onChange={v => patchProvider({ model: v })}
257
+ placeholder={preset?.defaultModel ?? ''}
258
+ provider={current.protocol}
259
+ apiKey={current.apiKey}
222
260
  envKey={!!activeEnvKey}
223
- baseUrl={currentConfig.baseUrl}
224
- supportsListModels={preset.supportsListModels}
225
- t={t}
261
+ baseUrl={current.baseUrl}
262
+ supportsListModels={!!current.baseUrl?.trim() || !!preset?.supportsListModels}
263
+ allowNoKey={!!current.baseUrl?.trim()}
264
+ browseLabel={t.settings.ai.listModels}
265
+ noModelsLabel={t.settings.ai.noModelsFound}
226
266
  />
227
267
  </Field>
228
268
 
229
- {/* 4. Test after all fields, tests the complete configuration */}
230
- {renderTestButton(provider, !!currentConfig.apiKey, !!activeEnvKey)}
269
+ {/* Test & Reset & Delete */}
270
+ <ProviderActions
271
+ provider={current.protocol}
272
+ result={testResult[current.id] ?? { state: 'idle' }}
273
+ hasKey={!!current.apiKey}
274
+ hasEnv={!!activeEnvKey}
275
+ hasConfig={!!(current.apiKey || current.model || current.baseUrl)}
276
+ onTest={handleTestKey}
277
+ onReset={resetProvider}
278
+ onDelete={deleteProvider}
279
+ t={t}
280
+ />
231
281
  </div>
232
282
  )}
233
283
 
234
- {/* Inline warnings */}
235
- {missingApiKey && (
236
- <div className="flex items-start gap-2 text-xs text-destructive/80 bg-destructive/8 border border-destructive/20 rounded-lg px-3 py-2.5">
237
- <AlertCircle size={13} className="shrink-0 mt-0.5" />
238
- <span>{t.settings.ai.noApiKey}</span>
239
- </div>
240
- )}
241
- {Object.values(env).some(Boolean) && (
284
+ {/* Env override hint — only when env vars are active */}
285
+ {!customFormOpen && Object.values(env).some(Boolean) && (
242
286
  <div className="flex items-start gap-2 text-xs text-[var(--amber)] bg-[var(--amber-subtle)] border border-[var(--amber)]/20 rounded-lg px-3 py-2.5">
243
287
  <AlertCircle size={13} className="shrink-0 mt-0.5" />
244
288
  <span>{t.settings.ai.envHint}</span>
@@ -329,193 +373,204 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
329
373
  );
330
374
  }
331
375
 
332
- /* ── Model Input with "List models" picker ── */
376
+ /* ── Provider Actions: Test + Reset + Delete ── */
333
377
 
334
- function ModelInput({
335
- value, onChange, placeholder, provider, apiKey, envKey, baseUrl, supportsListModels, t,
378
+ function ProviderActions({
379
+ provider, result, hasKey, hasEnv, hasConfig, onTest, onReset, onDelete, t,
336
380
  }: {
337
- value: string;
338
- onChange: (v: string) => void;
339
- placeholder: string;
340
381
  provider: ProviderId;
341
- apiKey: string;
342
- envKey?: boolean;
343
- baseUrl?: string;
344
- supportsListModels: boolean;
382
+ result: TestResult;
383
+ hasKey: boolean;
384
+ hasEnv: boolean;
385
+ hasConfig: boolean;
386
+ onTest: () => void;
387
+ onReset?: () => void;
388
+ onDelete?: () => void;
345
389
  t: AiTabProps['t'];
346
390
  }) {
347
- const [models, setModels] = useState<string[] | null>(null);
348
- const [loading, setLoading] = useState(false);
349
- const [open, setOpen] = useState(false);
350
- const [error, setError] = useState('');
351
- const [focused, setFocused] = useState(false);
352
- const [highlightIdx, setHighlightIdx] = useState(-1);
353
- const containerRef = useRef<HTMLDivElement>(null);
354
- const listRef = useRef<HTMLDivElement>(null);
355
- const fetchedRef = useRef(false);
356
- const fetchVersionRef = useRef(0);
357
- const loadingRef = useRef(false);
358
-
359
- const hasKey = !!apiKey || !!envKey || !!PROVIDER_PRESETS[provider]?.apiKeyFallback;
360
-
361
- // Reset fetched cache when provider/key/baseUrl changes
362
- useEffect(() => {
363
- fetchedRef.current = false;
364
- fetchVersionRef.current++;
365
- setModels(null);
366
- }, [provider, apiKey, baseUrl]);
367
-
368
- const fetchModels = useCallback(async (silent = false) => {
369
- if (loadingRef.current) return;
370
- loadingRef.current = true;
371
- setLoading(true);
372
- if (!silent) setError('');
373
- const version = fetchVersionRef.current;
374
- try {
375
- const body: Record<string, string> = { provider };
376
- if (apiKey) body.apiKey = apiKey;
377
- if (baseUrl) body.baseUrl = baseUrl;
378
-
379
- const res = await fetch('/api/settings/list-models', {
380
- method: 'POST',
381
- headers: { 'Content-Type': 'application/json' },
382
- body: JSON.stringify(body),
383
- });
384
- if (version !== fetchVersionRef.current) return;
385
- const json = await res.json();
386
- if (version !== fetchVersionRef.current) return;
387
- if (json.ok && Array.isArray(json.models)) {
388
- setModels(json.models);
389
- fetchedRef.current = true;
390
- if (!silent) setOpen(true);
391
- } else if (!silent) {
392
- setError(json.error || 'Failed to fetch models');
393
- }
394
- } catch {
395
- if (version !== fetchVersionRef.current) return;
396
- if (!silent) setError('Network error');
397
- } finally {
398
- loadingRef.current = false;
399
- setLoading(false);
391
+ const [confirmAction, setConfirmAction] = useState<'reset' | 'delete' | null>(null);
392
+ const confirmTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
393
+ const hasFallback = !!PROVIDER_PRESETS[provider]?.apiKeyFallback;
394
+ const canTest = hasKey || hasEnv || hasFallback;
395
+
396
+ useEffect(() => () => { if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current); }, []);
397
+
398
+ const startConfirm = (action: 'reset' | 'delete') => {
399
+ if (confirmAction === action) {
400
+ if (action === 'reset') onReset?.(); else onDelete?.();
401
+ setConfirmAction(null);
402
+ if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current);
403
+ } else {
404
+ setConfirmAction(action);
405
+ if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current);
406
+ confirmTimerRef.current = setTimeout(() => setConfirmAction(null), 3000);
400
407
  }
401
- }, [provider, apiKey, baseUrl]);
408
+ };
402
409
 
403
- const handleFocus = useCallback(() => {
404
- setFocused(true);
405
- if (!fetchedRef.current && supportsListModels && hasKey && !loadingRef.current) {
406
- fetchModels(true);
407
- }
408
- }, [supportsListModels, hasKey, fetchModels]);
410
+ const { locale } = useLocale();
409
411
 
410
- // Filtered models for typeahead
411
- const filtered = useMemo(() => {
412
- if (!models || !value.trim()) return models ?? [];
413
- const q = value.toLowerCase();
414
- return models.filter(m => m.toLowerCase().includes(q));
415
- }, [models, value]);
412
+ return (
413
+ <div className="space-y-2 pt-2">
414
+ <div className="flex items-center justify-between">
415
+ <TestButton result={result} disabled={!canTest} onTest={onTest} t={t} />
416
416
 
417
- // Show typeahead when focused + models loaded + user is typing (or Browse was clicked)
418
- const showDropdown = open || (focused && models !== null && value.trim().length > 0 && filtered.length > 0);
417
+ <div className="flex items-center gap-1">
418
+ {/* Reset */}
419
+ {onReset && hasConfig && (
420
+ <button
421
+ type="button"
422
+ onClick={() => startConfirm('reset')}
423
+ onBlur={() => { if (confirmAction === 'reset') { setConfirmAction(null); if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current); } }}
424
+ className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-lg transition-all duration-200 ${
425
+ confirmAction === 'reset'
426
+ ? 'bg-destructive/10 text-destructive border border-destructive/25 font-medium'
427
+ : 'text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted/50'
428
+ }`}
429
+ >
430
+ <RotateCcw size={12} />
431
+ {confirmAction === 'reset'
432
+ ? (locale === 'zh' ? '确认重置?' : 'Confirm?')
433
+ : (locale === 'zh' ? '重置' : 'Reset')}
434
+ </button>
435
+ )}
436
+ {/* Delete */}
437
+ {onDelete && (
438
+ <button
439
+ type="button"
440
+ onClick={() => startConfirm('delete')}
441
+ onBlur={() => { if (confirmAction === 'delete') { setConfirmAction(null); if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current); } }}
442
+ className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-lg transition-all duration-200 ${
443
+ confirmAction === 'delete'
444
+ ? 'bg-destructive/10 text-destructive border border-destructive/25 font-medium'
445
+ : 'text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted/50'
446
+ }`}
447
+ >
448
+ <Trash2 size={12} />
449
+ {confirmAction === 'delete'
450
+ ? (locale === 'zh' ? '确认删除?' : 'Confirm?')
451
+ : (locale === 'zh' ? '删除' : 'Delete')}
452
+ </button>
453
+ )}
454
+ </div>
455
+ </div>
456
+ </div>
457
+ );
458
+ }
419
459
 
420
- // Reset highlight when filtered list changes
421
- useEffect(() => { setHighlightIdx(-1); }, [filtered]);
460
+ /* ── Inline Custom Provider Form (uses shared hook + fields) ── */
422
461
 
423
- // Scroll highlighted item into view
424
- useEffect(() => {
425
- if (highlightIdx < 0 || !listRef.current) return;
426
- const items = listRef.current.querySelectorAll('[data-model-item]');
427
- items[highlightIdx]?.scrollIntoView({ block: 'nearest' });
428
- }, [highlightIdx]);
429
-
430
- const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
431
- const items = showDropdown ? filtered : [];
432
- if (!items.length) return;
433
-
434
- if (e.key === 'ArrowDown') {
435
- e.preventDefault();
436
- setHighlightIdx(i => (i + 1) % items.length);
437
- } else if (e.key === 'ArrowUp') {
438
- e.preventDefault();
439
- setHighlightIdx(i => (i - 1 + items.length) % items.length);
440
- } else if (e.key === 'Enter' && highlightIdx >= 0 && highlightIdx < items.length) {
441
- e.preventDefault();
442
- onChange(items[highlightIdx]);
443
- setOpen(false);
444
- setFocused(false);
445
- } else if (e.key === 'Escape') {
446
- setOpen(false);
447
- setFocused(false);
448
- }
449
- }, [showDropdown, filtered, highlightIdx, onChange]);
462
+ function CustomProviderForm({
463
+ initial, onSave, onCancel, onDelete, t, existingNames,
464
+ }: {
465
+ initial?: Provider;
466
+ onSave: (provider: Provider) => void;
467
+ onCancel: () => void;
468
+ onDelete?: () => void;
469
+ t: AiTabProps['t'];
470
+ existingNames: string[];
471
+ }) {
472
+ const { locale } = useLocale();
473
+ const form = useCustomProviderForm({ initial, onSave, locale, existingNames });
474
+ const [confirmDelete, setConfirmDelete] = useState(false);
475
+ const deleteTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
450
476
 
451
- useEffect(() => {
452
- if (!showDropdown) return;
453
- function handleClickOutside(e: MouseEvent) {
454
- if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
455
- setOpen(false);
456
- setFocused(false);
457
- }
458
- }
459
- document.addEventListener('mousedown', handleClickOutside);
460
- return () => document.removeEventListener('mousedown', handleClickOutside);
461
- }, [showDropdown]);
477
+ useEffect(() => () => { if (deleteTimerRef.current) clearTimeout(deleteTimerRef.current); }, []);
478
+
479
+ const formTitle = initial?.id
480
+ ? (locale === 'zh' ? '编辑 Provider' : 'Edit Provider')
481
+ : (locale === 'zh' ? '添加 Provider' : 'Add Provider');
462
482
 
463
- const displayList = filtered;
483
+ const missingFields: string[] = [];
484
+ if (!form.name.trim()) missingFields.push(locale === 'zh' ? '名称' : 'Name');
485
+ if (!form.baseUrl.trim()) missingFields.push(locale === 'zh' ? '接口地址' : 'Base URL');
486
+ if (!form.model.trim()) missingFields.push(locale === 'zh' ? '模型' : 'Model');
464
487
 
465
488
  return (
466
- <div ref={containerRef} className="relative">
467
- <div className="flex gap-1.5">
468
- <Input
469
- value={value}
470
- onChange={e => { onChange(e.target.value); if (!open) setFocused(true); }}
471
- onFocus={handleFocus}
472
- onKeyDown={handleKeyDown}
473
- placeholder={placeholder}
474
- className="flex-1"
475
- autoComplete="off"
476
- />
477
- {supportsListModels && (
478
- <button
479
- type="button"
480
- disabled={!hasKey || loading}
481
- onClick={() => fetchModels(false)}
482
- title={t.settings.ai.listModels}
483
- className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg border border-border text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors disabled:opacity-40 disabled:cursor-not-allowed shrink-0"
484
- >
485
- {loading ? <Loader2 size={12} className="animate-spin" /> : <ChevronDown size={12} />}
486
- {t.settings.ai.listModels}
487
- </button>
488
- )}
489
+ <div className="mt-3 rounded-lg border border-border overflow-hidden">
490
+ {/* Header */}
491
+ <div className="flex items-center justify-between px-4 py-2.5 border-b border-border">
492
+ <span className="text-sm font-medium text-foreground">{formTitle}</span>
493
+ <button
494
+ type="button"
495
+ onClick={onCancel}
496
+ className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
497
+ aria-label={locale === 'zh' ? '关闭' : 'Close'}
498
+ >
499
+ <X size={14} />
500
+ </button>
489
501
  </div>
490
- {error && <p className="text-xs text-error mt-1">{error}</p>}
491
- {showDropdown && displayList.length > 0 && (
492
- <div ref={listRef} className="absolute z-50 mt-1 w-full max-h-48 overflow-y-auto rounded-lg border border-border bg-popover shadow-lg">
493
- {displayList.map((m, i) => (
502
+
503
+ {/* Form body */}
504
+ <div className="p-4">
505
+ <CustomProviderFields form={form} t={t} locale={locale} layout="compact" />
506
+
507
+ {/* Actions */}
508
+ <div className="flex items-center gap-2 pt-4">
509
+ <TestButton result={form.testResult} disabled={!form.canSave} onTest={form.handleTest} t={t} />
510
+
511
+ {/* Delete — only when editing an existing provider */}
512
+ {onDelete && initial?.id && (
494
513
  <button
495
- key={m}
496
514
  type="button"
497
- data-model-item
498
- className={`w-full text-left px-3 py-1.5 text-sm transition-colors ${
499
- m === value ? 'bg-accent/60 font-medium'
500
- : i === highlightIdx ? 'bg-accent'
501
- : 'hover:bg-accent'
515
+ onClick={() => {
516
+ if (confirmDelete) {
517
+ onDelete();
518
+ if (deleteTimerRef.current) clearTimeout(deleteTimerRef.current);
519
+ } else {
520
+ setConfirmDelete(true);
521
+ deleteTimerRef.current = setTimeout(() => setConfirmDelete(false), 3000);
522
+ }
523
+ }}
524
+ onBlur={() => { setConfirmDelete(false); if (deleteTimerRef.current) clearTimeout(deleteTimerRef.current); }}
525
+ className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-lg transition-all duration-200 ${
526
+ confirmDelete
527
+ ? 'bg-destructive/10 text-destructive border border-destructive/25 font-medium'
528
+ : 'text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted/50'
502
529
  }`}
503
- onClick={() => { onChange(m); setOpen(false); setFocused(false); }}
504
530
  >
505
- {m}
531
+ <Trash2 size={12} />
532
+ {confirmDelete
533
+ ? (locale === 'zh' ? '确认删除?' : 'Confirm?')
534
+ : (locale === 'zh' ? '删除' : 'Delete')}
506
535
  </button>
507
- ))}
508
- </div>
509
- )}
510
- {open && displayList.length === 0 && (
511
- <div className="absolute z-50 mt-1 w-full rounded-lg border border-border bg-popover shadow-lg px-3 py-2 text-xs text-muted-foreground">
512
- {t.settings.ai.noModelsFound}
536
+ )}
537
+
538
+ <div className="flex-1">
539
+ {form.isDuplicateName && (
540
+ <span className="text-2xs text-destructive pl-2">
541
+ {locale === 'zh' ? '名称已存在' : 'Name already exists'}
542
+ </span>
543
+ )}
544
+ {!form.isDuplicateName && !form.canSave && missingFields.length > 0 && (
545
+ <span className="text-2xs text-muted-foreground/60 pl-2">
546
+ {locale === 'zh' ? `需要: ${missingFields.join('、')}` : `Required: ${missingFields.join(', ')}`}
547
+ </span>
548
+ )}
549
+ </div>
550
+
551
+ <button
552
+ type="button"
553
+ onClick={onCancel}
554
+ className="px-3 py-1.5 text-sm rounded-lg text-muted-foreground hover:text-foreground transition-colors"
555
+ >
556
+ {t.settings?.customProviders?.modal?.buttonCancel ?? 'Cancel'}
557
+ </button>
558
+ <button
559
+ type="button"
560
+ onClick={form.handleSave}
561
+ disabled={!form.canSave}
562
+ className="px-4 py-1.5 text-sm font-medium rounded-lg bg-[var(--amber)] text-[var(--amber-foreground)] hover:bg-[var(--amber)]/90 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
563
+ >
564
+ {locale === 'zh' ? '保存' : 'Save'}
565
+ </button>
513
566
  </div>
514
- )}
567
+ </div>
515
568
  </div>
516
569
  );
517
570
  }
518
571
 
572
+ /* ModelInput is now a shared component at @/components/shared/ModelInput */
573
+
519
574
  /* ── Ask AI Display Mode (localStorage-based, no server roundtrip) ── */
520
575
 
521
576
  function AskDisplayMode() {