@geminilight/mindos 0.6.63 → 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 (268) hide show
  1. package/_standalone/.mindos-build-version +1 -1
  2. package/_standalone/.next/BUILD_ID +1 -1
  3. package/_standalone/.next/app-path-routes-manifest.json +21 -21
  4. package/_standalone/.next/build-manifest.json +2 -2
  5. package/_standalone/.next/cache/.previewinfo +1 -1
  6. package/_standalone/.next/cache/.rscinfo +1 -1
  7. package/_standalone/.next/cache/config.json +3 -3
  8. package/_standalone/.next/prerender-manifest.json +3 -3
  9. package/_standalone/.next/server/app/.well-known/agent-card.json/route_client-reference-manifest.js +1 -1
  10. package/_standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/_standalone/.next/server/app/_global-error.html +2 -2
  12. package/_standalone/.next/server/app/_global-error.rsc +1 -1
  13. package/_standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/_standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/_standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  16. package/_standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  17. package/_standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  18. package/_standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  19. package/_standalone/.next/server/app/_not-found/page.js +1 -1
  20. package/_standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  21. package/_standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/_standalone/.next/server/app/agents/[agentKey]/page.js +1 -1
  23. package/_standalone/.next/server/app/agents/[agentKey]/page.js.nft.json +1 -1
  24. package/_standalone/.next/server/app/agents/[agentKey]/page_client-reference-manifest.js +1 -1
  25. package/_standalone/.next/server/app/agents/page.js +1 -1
  26. package/_standalone/.next/server/app/agents/page.js.nft.json +1 -1
  27. package/_standalone/.next/server/app/agents/page_client-reference-manifest.js +1 -1
  28. package/_standalone/.next/server/app/api/a2a/agents/route_client-reference-manifest.js +1 -1
  29. package/_standalone/.next/server/app/api/a2a/delegations/route_client-reference-manifest.js +1 -1
  30. package/_standalone/.next/server/app/api/a2a/discover/route_client-reference-manifest.js +1 -1
  31. package/_standalone/.next/server/app/api/a2a/route_client-reference-manifest.js +1 -1
  32. package/_standalone/.next/server/app/api/acp/config/route_client-reference-manifest.js +1 -1
  33. package/_standalone/.next/server/app/api/acp/detect/route_client-reference-manifest.js +1 -1
  34. package/_standalone/.next/server/app/api/acp/install/route_client-reference-manifest.js +1 -1
  35. package/_standalone/.next/server/app/api/acp/registry/route_client-reference-manifest.js +1 -1
  36. package/_standalone/.next/server/app/api/acp/session/route_client-reference-manifest.js +1 -1
  37. package/_standalone/.next/server/app/api/agent-activity/route_client-reference-manifest.js +1 -1
  38. package/_standalone/.next/server/app/api/agents/copy-skill/route.js.nft.json +1 -1
  39. package/_standalone/.next/server/app/api/agents/copy-skill/route_client-reference-manifest.js +1 -1
  40. package/_standalone/.next/server/app/api/agents/custom/detect/route_client-reference-manifest.js +1 -1
  41. package/_standalone/.next/server/app/api/agents/custom/route_client-reference-manifest.js +1 -1
  42. package/_standalone/.next/server/app/api/ask/route.js +48 -42
  43. package/_standalone/.next/server/app/api/ask/route.js.nft.json +1 -1
  44. package/_standalone/.next/server/app/api/ask/route_client-reference-manifest.js +1 -1
  45. package/_standalone/.next/server/app/api/ask-sessions/route_client-reference-manifest.js +1 -1
  46. package/_standalone/.next/server/app/api/auth/route_client-reference-manifest.js +1 -1
  47. package/_standalone/.next/server/app/api/backlinks/route.js.nft.json +1 -1
  48. package/_standalone/.next/server/app/api/backlinks/route_client-reference-manifest.js +1 -1
  49. package/_standalone/.next/server/app/api/bootstrap/route.js.nft.json +1 -1
  50. package/_standalone/.next/server/app/api/bootstrap/route_client-reference-manifest.js +1 -1
  51. package/_standalone/.next/server/app/api/changes/route.js.nft.json +1 -1
  52. package/_standalone/.next/server/app/api/changes/route_client-reference-manifest.js +1 -1
  53. package/_standalone/.next/server/app/api/export/route.js.nft.json +1 -1
  54. package/_standalone/.next/server/app/api/export/route_client-reference-manifest.js +1 -1
  55. package/_standalone/.next/server/app/api/extract-pdf/route_client-reference-manifest.js +1 -1
  56. package/_standalone/.next/server/app/api/file/import/route.js +1 -1
  57. package/_standalone/.next/server/app/api/file/import/route.js.nft.json +1 -1
  58. package/_standalone/.next/server/app/api/file/import/route_client-reference-manifest.js +1 -1
  59. package/_standalone/.next/server/app/api/file/raw/route.js.nft.json +1 -1
  60. package/_standalone/.next/server/app/api/file/raw/route_client-reference-manifest.js +1 -1
  61. package/_standalone/.next/server/app/api/file/route.js.nft.json +1 -1
  62. package/_standalone/.next/server/app/api/file/route_client-reference-manifest.js +1 -1
  63. package/_standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  64. package/_standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  65. package/_standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  66. package/_standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  67. package/_standalone/.next/server/app/api/graph/route.js.nft.json +1 -1
  68. package/_standalone/.next/server/app/api/graph/route_client-reference-manifest.js +1 -1
  69. package/_standalone/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  70. package/_standalone/.next/server/app/api/inbox/route.js.nft.json +1 -1
  71. package/_standalone/.next/server/app/api/inbox/route_client-reference-manifest.js +1 -1
  72. package/_standalone/.next/server/app/api/init/route.js.nft.json +1 -1
  73. package/_standalone/.next/server/app/api/init/route_client-reference-manifest.js +1 -1
  74. package/_standalone/.next/server/app/api/mcp/agents/route.js +1 -1
  75. package/_standalone/.next/server/app/api/mcp/agents/route.js.nft.json +1 -1
  76. package/_standalone/.next/server/app/api/mcp/agents/route_client-reference-manifest.js +1 -1
  77. package/_standalone/.next/server/app/api/mcp/install/route_client-reference-manifest.js +1 -1
  78. package/_standalone/.next/server/app/api/mcp/install-skill/route_client-reference-manifest.js +1 -1
  79. package/_standalone/.next/server/app/api/mcp/restart/route_client-reference-manifest.js +1 -1
  80. package/_standalone/.next/server/app/api/mcp/status/route_client-reference-manifest.js +1 -1
  81. package/_standalone/.next/server/app/api/mcp/uninstall/route_client-reference-manifest.js +1 -1
  82. package/_standalone/.next/server/app/api/monitoring/route.js.nft.json +1 -1
  83. package/_standalone/.next/server/app/api/monitoring/route_client-reference-manifest.js +1 -1
  84. package/_standalone/.next/server/app/api/recent-files/route.js.nft.json +1 -1
  85. package/_standalone/.next/server/app/api/recent-files/route_client-reference-manifest.js +1 -1
  86. package/_standalone/.next/server/app/api/restart/route_client-reference-manifest.js +1 -1
  87. package/_standalone/.next/server/app/api/search/route.js.nft.json +1 -1
  88. package/_standalone/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
  89. package/_standalone/.next/server/app/api/settings/list-models/route.js +1 -1
  90. package/_standalone/.next/server/app/api/settings/list-models/route_client-reference-manifest.js +1 -1
  91. package/_standalone/.next/server/app/api/settings/reset-token/route_client-reference-manifest.js +1 -1
  92. package/_standalone/.next/server/app/api/settings/route.js +1 -1
  93. package/_standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  94. package/_standalone/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
  95. package/_standalone/.next/server/app/api/settings/test-key/route.js +1 -1
  96. package/_standalone/.next/server/app/api/settings/test-key/route_client-reference-manifest.js +1 -1
  97. package/_standalone/.next/server/app/api/setup/check-path/route_client-reference-manifest.js +1 -1
  98. package/_standalone/.next/server/app/api/setup/check-port/route_client-reference-manifest.js +1 -1
  99. package/_standalone/.next/server/app/api/setup/generate-token/route_client-reference-manifest.js +1 -1
  100. package/_standalone/.next/server/app/api/setup/ls/route_client-reference-manifest.js +1 -1
  101. package/_standalone/.next/server/app/api/setup/route.js +1 -1
  102. package/_standalone/.next/server/app/api/setup/route_client-reference-manifest.js +1 -1
  103. package/_standalone/.next/server/app/api/skills/route.js +1 -1
  104. package/_standalone/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  105. package/_standalone/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
  106. package/_standalone/.next/server/app/api/tree-version/route.js.nft.json +1 -1
  107. package/_standalone/.next/server/app/api/tree-version/route_client-reference-manifest.js +1 -1
  108. package/_standalone/.next/server/app/api/uninstall/route_client-reference-manifest.js +1 -1
  109. package/_standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  110. package/_standalone/.next/server/app/api/update-check/route_client-reference-manifest.js +1 -1
  111. package/_standalone/.next/server/app/api/update-status/route_client-reference-manifest.js +1 -1
  112. package/_standalone/.next/server/app/api/workflows/route.js.nft.json +1 -1
  113. package/_standalone/.next/server/app/api/workflows/route_client-reference-manifest.js +1 -1
  114. package/_standalone/.next/server/app/changes/page.js +1 -1
  115. package/_standalone/.next/server/app/changes/page.js.nft.json +1 -1
  116. package/_standalone/.next/server/app/changes/page_client-reference-manifest.js +1 -1
  117. package/_standalone/.next/server/app/echo/[segment]/page.js +2 -2
  118. package/_standalone/.next/server/app/echo/[segment]/page.js.nft.json +1 -1
  119. package/_standalone/.next/server/app/echo/[segment]/page_client-reference-manifest.js +1 -1
  120. package/_standalone/.next/server/app/echo/page.js +1 -1
  121. package/_standalone/.next/server/app/echo/page.js.nft.json +1 -1
  122. package/_standalone/.next/server/app/echo/page_client-reference-manifest.js +1 -1
  123. package/_standalone/.next/server/app/explore/page.js +1 -1
  124. package/_standalone/.next/server/app/explore/page.js.nft.json +1 -1
  125. package/_standalone/.next/server/app/explore/page_client-reference-manifest.js +1 -1
  126. package/_standalone/.next/server/app/help/page.js +1 -1
  127. package/_standalone/.next/server/app/help/page.js.nft.json +1 -1
  128. package/_standalone/.next/server/app/help/page_client-reference-manifest.js +1 -1
  129. package/_standalone/.next/server/app/inbox/history/page.js +1 -1
  130. package/_standalone/.next/server/app/inbox/history/page.js.nft.json +1 -1
  131. package/_standalone/.next/server/app/inbox/history/page_client-reference-manifest.js +1 -1
  132. package/_standalone/.next/server/app/login/page.js +1 -1
  133. package/_standalone/.next/server/app/login/page.js.nft.json +1 -1
  134. package/_standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
  135. package/_standalone/.next/server/app/page.js +1 -1
  136. package/_standalone/.next/server/app/page.js.nft.json +1 -1
  137. package/_standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  138. package/_standalone/.next/server/app/setup/page.js +2 -2
  139. package/_standalone/.next/server/app/setup/page.js.nft.json +1 -1
  140. package/_standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  141. package/_standalone/.next/server/app/trash/page.js +3 -3
  142. package/_standalone/.next/server/app/trash/page.js.nft.json +1 -1
  143. package/_standalone/.next/server/app/trash/page_client-reference-manifest.js +1 -1
  144. package/_standalone/.next/server/app/view/[...path]/page.js +2 -2
  145. package/_standalone/.next/server/app/view/[...path]/page.js.nft.json +1 -1
  146. package/_standalone/.next/server/app/view/[...path]/page_client-reference-manifest.js +1 -1
  147. package/_standalone/.next/server/app/wiki/page.js +1 -1
  148. package/_standalone/.next/server/app/wiki/page.js.nft.json +1 -1
  149. package/_standalone/.next/server/app/wiki/page_client-reference-manifest.js +1 -1
  150. package/_standalone/.next/server/app-paths-manifest.json +21 -21
  151. package/_standalone/.next/server/chunks/122.js +222 -0
  152. package/_standalone/.next/server/chunks/3113.js +52 -0
  153. package/_standalone/.next/server/chunks/6539.js +1 -1
  154. package/_standalone/.next/server/chunks/8388.js +2 -2
  155. package/_standalone/.next/server/chunks/953.js +3 -3
  156. package/_standalone/.next/server/chunks/9787.js +2 -0
  157. package/_standalone/.next/server/pages/500.html +2 -2
  158. package/_standalone/.next/server/server-reference-manifest.js +1 -1
  159. package/_standalone/.next/server/server-reference-manifest.json +1 -1
  160. package/_standalone/.next/static/chunks/1001-99da82ec8d8c136f.js +1 -0
  161. package/_standalone/.next/static/chunks/5149-4d828886dda479fa.js +1 -0
  162. package/_standalone/.next/static/chunks/{5581-82e5db227f8e9393.js → 5581-c671163a2fe1b312.js} +2 -2
  163. package/_standalone/.next/static/chunks/6636-53238eff89503f03.js +6 -0
  164. package/_standalone/.next/static/chunks/6757-1c1a89720fdda8f0.js +1 -0
  165. package/_standalone/.next/static/chunks/7129-20e9d2463a9da646.js +1 -0
  166. package/_standalone/.next/static/chunks/{3674-be69a8b858ceacdd.js → 7294-cac25d97869afadc.js} +1 -1
  167. package/_standalone/.next/static/chunks/8225-21e5cebc3731ddf0.js +1 -0
  168. package/_standalone/.next/static/chunks/8520-b51810e66293ceb8.js +22 -0
  169. package/_standalone/.next/static/chunks/9207-dc9c31b351a2ed78.js +1 -0
  170. package/_standalone/.next/static/chunks/app/agents/[agentKey]/{page-b0dabe793500383d.js → page-2f5cf97e03dc1cc9.js} +1 -1
  171. package/_standalone/.next/static/chunks/app/agents/{page-1f1ac330c8177cf6.js → page-50eac58d511dcc6e.js} +1 -1
  172. package/_standalone/.next/static/chunks/app/echo/[segment]/page-2a00f4686adf3885.js +11 -0
  173. package/_standalone/.next/static/chunks/app/{layout-50a6b1164ee98ab9.js → layout-2cb7a6602d2e5d5f.js} +62 -58
  174. package/_standalone/.next/static/chunks/app/{page-73802bd31d7f6c9f.js → page-5ab911b2226f6ff7.js} +1 -1
  175. package/_standalone/.next/static/chunks/app/setup/page-907b7c57fad2292b.js +1 -0
  176. package/_standalone/.next/static/chunks/app/trash/page-11a511b065ea84c2.js +1 -0
  177. package/_standalone/.next/static/chunks/app/view/[...path]/{page-808f39963bf04715.js → page-26e47dd4c533a58c.js} +2 -2
  178. package/_standalone/.next/static/css/67e7918f5ed7d147.css +1 -0
  179. package/_standalone/.next/trace +65 -65
  180. package/_standalone/__tests__/api/ask-attachments.test.ts +194 -0
  181. package/_standalone/__tests__/api/settings.test.ts +16 -12
  182. package/_standalone/__tests__/api/setup.test.ts +11 -9
  183. package/_standalone/__tests__/api/test-key.test.ts +0 -10
  184. package/_standalone/__tests__/components/UpdateToast.test.ts +344 -0
  185. package/_standalone/__tests__/core/context.test.ts +48 -426
  186. package/_standalone/__tests__/lib/pi-skills.test.ts +4 -4
  187. package/_standalone/__tests__/lib/settings-ai-client.test.ts +32 -12
  188. package/_standalone/__tests__/setup.ts +5 -5
  189. package/_standalone/components/ask/AskContent.tsx +70 -40
  190. package/_standalone/components/ask/AskHeader.tsx +8 -1
  191. package/_standalone/components/ask/MessageList.tsx +37 -3
  192. package/_standalone/components/ask/ProviderModelCapsule.tsx +51 -129
  193. package/_standalone/components/settings/AiTab.tsx +270 -347
  194. package/_standalone/components/settings/CustomProviderFields.tsx +121 -0
  195. package/_standalone/components/settings/CustomProvidersCard.tsx +2 -2
  196. package/_standalone/components/settings/KnowledgeTab.tsx +6 -20
  197. package/_standalone/components/settings/McpAgentInstall.tsx +7 -2
  198. package/_standalone/components/settings/Primitives.tsx +48 -104
  199. package/_standalone/components/settings/ProviderModal.tsx +38 -221
  200. package/_standalone/components/settings/SettingsContent.tsx +5 -12
  201. package/_standalone/components/settings/TestButton.tsx +64 -0
  202. package/_standalone/components/settings/types.ts +3 -12
  203. package/_standalone/components/settings/useCustomProviderForm.ts +132 -0
  204. package/_standalone/components/setup/StepAI.tsx +3 -3
  205. package/_standalone/components/shared/ModelInput.tsx +18 -4
  206. package/_standalone/components/shared/ProviderSelect.tsx +126 -134
  207. package/_standalone/hooks/useAskChat.ts +97 -13
  208. package/_standalone/hooks/useAskPanel.ts +17 -1
  209. package/_standalone/lib/settings-ai-client.ts +17 -8
  210. package/_standalone/tsconfig.tsbuildinfo +1 -1
  211. package/app/app/api/ask/route.ts +124 -44
  212. package/app/app/api/mcp/agents/route.ts +3 -3
  213. package/app/app/api/settings/list-models/route.ts +15 -26
  214. package/app/app/api/settings/route.ts +14 -59
  215. package/app/app/api/settings/test-key/route.ts +47 -12
  216. package/app/app/api/setup/route.ts +36 -18
  217. package/app/app/api/skills/route.ts +1 -1
  218. package/app/app/layout.tsx +5 -3
  219. package/app/components/HomeContent.tsx +11 -0
  220. package/app/components/UpdateToast.tsx +255 -0
  221. package/app/components/ask/AskContent.tsx +70 -40
  222. package/app/components/ask/AskHeader.tsx +8 -1
  223. package/app/components/ask/MessageList.tsx +37 -3
  224. package/app/components/ask/ProviderModelCapsule.tsx +51 -129
  225. package/app/components/settings/AiTab.tsx +270 -347
  226. package/app/components/settings/CustomProviderFields.tsx +121 -0
  227. package/app/components/settings/CustomProvidersCard.tsx +2 -2
  228. package/app/components/settings/KnowledgeTab.tsx +6 -20
  229. package/app/components/settings/McpAgentInstall.tsx +7 -2
  230. package/app/components/settings/Primitives.tsx +48 -104
  231. package/app/components/settings/ProviderModal.tsx +38 -221
  232. package/app/components/settings/SettingsContent.tsx +5 -12
  233. package/app/components/settings/TestButton.tsx +64 -0
  234. package/app/components/settings/types.ts +3 -12
  235. package/app/components/settings/useCustomProviderForm.ts +132 -0
  236. package/app/components/setup/StepAI.tsx +3 -3
  237. package/app/components/shared/ModelInput.tsx +18 -4
  238. package/app/components/shared/ProviderSelect.tsx +126 -134
  239. package/app/hooks/useAskChat.ts +97 -13
  240. package/app/hooks/useAskPanel.ts +17 -1
  241. package/app/lib/agent/context.ts +65 -0
  242. package/app/lib/agent/providers.ts +25 -0
  243. package/app/lib/agent/tools.ts +1 -1
  244. package/app/lib/custom-endpoints.ts +129 -29
  245. package/app/lib/i18n/modules/settings.ts +20 -0
  246. package/app/lib/pi-integration/skills.ts +16 -4
  247. package/app/lib/settings-ai-client.ts +17 -8
  248. package/app/lib/settings.ts +64 -90
  249. package/app/lib/types.ts +4 -0
  250. package/package.json +1 -1
  251. package/_standalone/.next/server/chunks/530.js +0 -218
  252. package/_standalone/.next/server/chunks/9007.js +0 -2
  253. package/_standalone/.next/server/chunks/9137.js +0 -52
  254. package/_standalone/.next/static/chunks/1309-373ade1b40aea186.js +0 -1
  255. package/_standalone/.next/static/chunks/3165-9189a38fd9ebf6f2.js +0 -1
  256. package/_standalone/.next/static/chunks/4587-5d06728133fff222.js +0 -1
  257. package/_standalone/.next/static/chunks/6261-5ce86db54b19ae46.js +0 -1
  258. package/_standalone/.next/static/chunks/6636-9bbc90fb3b8731fe.js +0 -6
  259. package/_standalone/.next/static/chunks/7637-904b0a381dc3ec02.js +0 -1
  260. package/_standalone/.next/static/chunks/8520-84e607f33c409f91.js +0 -22
  261. package/_standalone/.next/static/chunks/9207-9a4a1a1ede4f8e6e.js +0 -1
  262. package/_standalone/.next/static/chunks/app/echo/[segment]/page-bc5e104eb7ae6327.js +0 -11
  263. package/_standalone/.next/static/chunks/app/setup/page-79acb0baf38184c6.js +0 -1
  264. package/_standalone/.next/static/chunks/app/trash/page-d040db56863da504.js +0 -1
  265. package/_standalone/.next/static/css/1287672978833d07.css +0 -1
  266. package/_standalone/lib/agent/context.ts +0 -403
  267. /package/_standalone/.next/static/{X86rF8dKEO0InosOw4a2_ → eIlwbGas1iRGonlPyEwj7}/_buildManifest.js +0 -0
  268. /package/_standalone/.next/static/{X86rF8dKEO0InosOw4a2_ → eIlwbGas1iRGonlPyEwj7}/_ssgManifest.js +0 -0
@@ -3,7 +3,7 @@
3
3
  import { useEffect, useLayoutEffect, useRef, useState, useCallback, useMemo } from 'react';
4
4
  import { Send, StopCircle, X, Plus, FileText, ImageIcon } from 'lucide-react';
5
5
  import { useLocale } from '@/lib/stores/locale-store';
6
- import type { AskMode } from '@/lib/types';
6
+ import type { AskMode, Message } from '@/lib/types';
7
7
  import ModeCapsule, { getPersistedMode } from '@/components/ask/ModeCapsule';
8
8
  import { useAskSession } from '@/hooks/useAskSession';
9
9
  import { useFileUpload } from '@/hooks/useFileUpload';
@@ -74,9 +74,11 @@ interface AskContentProps {
74
74
  askMode?: 'panel' | 'popup';
75
75
  /** Switch between panel ↔ popup */
76
76
  onModeSwitch?: () => void;
77
+ /** Navigate from fullscreen to right-side panel mode */
78
+ onDockToPanel?: () => void;
77
79
  }
78
80
 
79
- export default function AskContent({ visible, currentFile, initialMessage, initialAcpAgent, onFirstMessage, variant, onClose, maximized, onMaximize, askMode, onModeSwitch }: AskContentProps) {
81
+ export default function AskContent({ visible, currentFile, initialMessage, initialAcpAgent, onFirstMessage, variant, onClose, maximized, onMaximize, askMode, onModeSwitch, onDockToPanel }: AskContentProps) {
80
82
  const isPanel = variant === 'panel';
81
83
  const isHome = variant === 'home';
82
84
 
@@ -103,7 +105,7 @@ export default function AskContent({ visible, currentFile, initialMessage, initi
103
105
  const selectedAcpAgentRef = useRef(selectedAcpAgent);
104
106
  selectedAcpAgentRef.current = selectedAcpAgent;
105
107
  const [chatMode, setChatMode] = useState<AskMode>('agent');
106
- const [providerOverride, setProviderOverride] = useState<ProviderId | `cp_${string}` | null>(null);
108
+ const [providerOverride, setProviderOverride] = useState<ProviderId | `p_${string}` | null>(null);
107
109
  const [modelOverride, setModelOverride] = useState<string | null>(null);
108
110
 
109
111
  useEffect(() => {
@@ -136,7 +138,30 @@ export default function AskContent({ visible, currentFile, initialMessage, initi
136
138
  setSelectedSkill(null);
137
139
  setSelectedAcpAgent(null);
138
140
  setAttachedFiles(currentFile ? [currentFile] : []);
139
- }, [currentFile]);
141
+ upload.clearAttachments();
142
+ }, [currentFile, upload]);
143
+
144
+
145
+ const handleRestoreInput = useCallback((userMessage: Message) => {
146
+ setInput(userMessage.content);
147
+ // Restore images if they exist
148
+ if (userMessage.images && userMessage.images.length > 0) {
149
+ // Reconstruct the images state from the message images
150
+ imageUpload.clearImages();
151
+ // Note: we can't directly set images without going through the upload flow
152
+ // So we just clear them for now - in practice, images are usually small content
153
+ // and the user can re-add them if needed
154
+ }
155
+ if (userMessage.attachedFiles) setAttachedFiles(userMessage.attachedFiles);
156
+ // Restore skill selection if it was set
157
+ if (userMessage.skillName) {
158
+ // The skill is already in the slash command system, just mark as selected
159
+ // This will be handled through the UI state
160
+ slash.resetSlash(); // Clear any active slash query
161
+ }
162
+ // Focus back to input
163
+ setTimeout(() => inputRef.current?.focus(), 50);
164
+ }, [imageUpload, slash]);
140
165
 
141
166
  const chatRefs = useRef({ inputValueRef, mentionRef, slashRef, imageUploadRef, sessionRef, uploadRef, selectedSkillRef, selectedAcpAgentRef, attachedFilesRef });
142
167
  const chat = useAskChat({
@@ -148,6 +173,7 @@ export default function AskContent({ visible, currentFile, initialMessage, initi
148
173
  refs: chatRefs.current,
149
174
  errorLabels: { noResponse: t.ask.errorNoResponse, stopped: t.ask.stopped },
150
175
  resetInputState,
176
+ onRestoreInput: handleRestoreInput,
151
177
  });
152
178
  const { isLoading, loadingPhase, reconnectAttempt, reconnectMaxRef } = chat;
153
179
  const handleSubmit = chat.submit;
@@ -482,7 +508,7 @@ export default function AskContent({ visible, currentFile, initialMessage, initi
482
508
  }), [t, reconnectAttempt]);
483
509
 
484
510
  return (
485
- <>
511
+ <div className="flex min-h-0 w-full flex-col h-full">
486
512
  {/* Header — home variant shows session switcher + new/history/fullscreen buttons */}
487
513
  <AskHeader
488
514
  isPanel={isPanel || isHome}
@@ -495,6 +521,7 @@ export default function AskContent({ visible, currentFile, initialMessage, initi
495
521
  askMode={isHome ? undefined : askMode}
496
522
  onModeSwitch={isHome ? undefined : onModeSwitch}
497
523
  onClose={isHome ? undefined : onClose}
524
+ onDockToPanel={maximized ? onDockToPanel : undefined}
498
525
  sessions={session.sessions}
499
526
  activeSessionId={session.activeSessionId}
500
527
  onLoadSession={handleLoadSession}
@@ -522,32 +549,33 @@ export default function AskContent({ visible, currentFile, initialMessage, initi
522
549
  />
523
550
  )}
524
551
 
525
- {/* Messages */}
526
552
  {/* Messages — home variant hides empty state (suggestions rendered externally) */}
527
- {!isHome && (
528
- <MessageList
529
- messages={session.messages}
530
- isLoading={isLoading}
531
- loadingPhase={loadingPhase}
532
- emptyPrompt={t.ask.emptyPrompt}
533
- emptyHint={t.ask.emptyHint}
534
- suggestions={t.ask.suggestions}
535
- onSuggestionClick={setInput}
536
- labels={messageLabels}
537
- />
538
- )}
539
- {isHome && session.messages.length > 0 && (
540
- <MessageList
541
- messages={session.messages}
542
- isLoading={isLoading}
543
- loadingPhase={loadingPhase}
544
- emptyPrompt={t.ask.emptyPrompt}
545
- emptyHint={t.ask.emptyHint}
546
- suggestions={[]}
547
- onSuggestionClick={setInput}
548
- labels={messageLabels}
549
- />
550
- )}
553
+ <div className="flex-1 min-h-0 flex flex-col">
554
+ {!isHome && (
555
+ <MessageList
556
+ messages={session.messages}
557
+ isLoading={isLoading}
558
+ loadingPhase={loadingPhase}
559
+ emptyPrompt={t.ask.emptyPrompt}
560
+ emptyHint={t.ask.emptyHint}
561
+ suggestions={t.ask.suggestions}
562
+ onSuggestionClick={setInput}
563
+ labels={messageLabels}
564
+ />
565
+ )}
566
+ {isHome && session.messages.length > 0 && (
567
+ <MessageList
568
+ messages={session.messages}
569
+ isLoading={isLoading}
570
+ loadingPhase={loadingPhase}
571
+ emptyPrompt={t.ask.emptyPrompt}
572
+ emptyHint={t.ask.emptyHint}
573
+ suggestions={[]}
574
+ onSuggestionClick={setInput}
575
+ labels={messageLabels}
576
+ />
577
+ )}
578
+ </div>
551
579
 
552
580
  {/* Popovers — flex children so they stay within overflow boundary (absolute positioning would be clipped by RightAskPanel's overflow-hidden) */}
553
581
  {mention.mentionQuery !== null && mention.mentionResults.length > 0 && (
@@ -573,7 +601,7 @@ export default function AskContent({ visible, currentFile, initialMessage, initi
573
601
  )}
574
602
 
575
603
  {/* Composer card — unified input area */}
576
- <div className="shrink-0 px-3 pb-2.5 pt-1">
604
+ <div className={cn('shrink-0', isHome ? 'px-2 pb-2 pt-0.5' : 'px-3 pb-2.5 pt-1')}>
577
605
  <div
578
606
  className={cn(
579
607
  'rounded-xl bg-muted/40 transition-all focus-within:bg-muted/60',
@@ -621,7 +649,7 @@ export default function AskContent({ visible, currentFile, initialMessage, initi
621
649
  <form
622
650
  ref={formRef}
623
651
  onSubmit={handleSubmit}
624
- className="flex items-end gap-1.5 px-3 py-2"
652
+ className={cn('flex items-end gap-1.5', isHome ? 'px-2 py-1.5' : 'px-3 py-2')}
625
653
  >
626
654
  {/* + attach button with mini menu */}
627
655
  <div className="relative shrink-0">
@@ -690,7 +718,7 @@ export default function AskContent({ visible, currentFile, initialMessage, initi
690
718
  onPaste={handlePaste}
691
719
  placeholder={t.ask.placeholder}
692
720
  rows={1}
693
- className="min-w-0 flex-1 resize-none overflow-y-hidden bg-transparent py-2 text-sm leading-relaxed text-foreground placeholder:text-muted-foreground/50 outline-none focus-visible:ring-0"
721
+ className={cn('min-w-0 flex-1 resize-none overflow-y-hidden bg-transparent py-2 leading-relaxed text-foreground placeholder:text-muted-foreground/50 outline-none focus-visible:ring-0', isHome ? 'text-xs' : 'text-sm')}
694
722
  />
695
723
 
696
724
  {isLoading ? (
@@ -705,8 +733,8 @@ export default function AskContent({ visible, currentFile, initialMessage, initi
705
733
  </form>
706
734
 
707
735
  {/* Mode + Agent + Provider selector row + keyboard hint */}
708
- <div className="flex items-center justify-between px-3 pb-2 pt-1.5 border-t border-border/10">
709
- <div className="flex items-center gap-2">
736
+ <div className={cn('flex items-center justify-between border-t border-border/10', isPanel ? 'px-2 pb-1.5 pt-1 gap-1' : 'px-3 pb-2 pt-1.5')}>
737
+ <div className={cn('flex items-center flex-wrap', isPanel ? 'gap-1' : 'gap-2')}>
710
738
  <ModeCapsule mode={chatMode} onChange={setChatMode} disabled={isLoading} />
711
739
  {mounted && acpDetection.installedAgents.length > 0 && (
712
740
  <AgentSelectorCapsule
@@ -729,13 +757,15 @@ export default function AskContent({ visible, currentFile, initialMessage, initi
729
757
  />
730
758
  )}
731
759
  </div>
732
- {/* Keyboard hint */}
733
- <span className="hidden md:inline text-2xs text-muted-foreground/40 select-none">
734
- <kbd className="font-mono">Enter</kbd> {t.ask.send} · <kbd className="font-mono">Shift+Enter</kbd> {t.ask.newlineHint}
735
- </span>
760
+ {/* Keyboard hint — hidden in panel (too narrow) and home (compact) */}
761
+ {!isPanel && !isHome && (
762
+ <span className="hidden md:inline text-2xs text-muted-foreground/40 select-none shrink-0">
763
+ <kbd className="font-mono">Enter</kbd> {t.ask.send} · <kbd className="font-mono">Shift+Enter</kbd> {t.ask.newlineHint}
764
+ </span>
765
+ )}
736
766
  </div>
737
767
  </div>
738
768
  </div>
739
- </>
769
+ </div>
740
770
  );
741
771
  }
@@ -16,6 +16,8 @@ interface AskHeaderProps {
16
16
  askMode?: 'panel' | 'popup';
17
17
  onModeSwitch?: () => void;
18
18
  onClose?: () => void;
19
+ /** Navigate from fullscreen to right-side panel mode */
20
+ onDockToPanel?: () => void;
19
21
  hideTitle?: boolean;
20
22
  /** Session switching — inline in header when >=2 sessions */
21
23
  sessions?: ChatSession[];
@@ -28,7 +30,7 @@ interface AskHeaderProps {
28
30
 
29
31
  export default memo(function AskHeader({
30
32
  isPanel, showHistory, onToggleHistory, onReset, isLoading,
31
- maximized, onMaximize, askMode, onModeSwitch, onClose, hideTitle,
33
+ maximized, onMaximize, askMode, onModeSwitch, onClose, onDockToPanel, hideTitle,
32
34
  sessions, activeSessionId, onLoadSession, onDeleteSession, onRenameSession, onTogglePinSession,
33
35
  }: AskHeaderProps) {
34
36
  const { t } = useLocale();
@@ -244,6 +246,11 @@ export default memo(function AskHeader({
244
246
  {maximized ? <Minimize2 size={iconSize} /> : <Maximize2 size={iconSize} />}
245
247
  </button>
246
248
  )}
249
+ {onDockToPanel && (
250
+ <button type="button" onClick={(e) => { e.stopPropagation(); onDockToPanel(); }} className="p-2 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={t.hints.dockToSide ?? 'Dock to side panel'}>
251
+ <PanelRight size={iconSize} />
252
+ </button>
253
+ )}
247
254
  {onModeSwitch && (
248
255
  <button type="button" onClick={(e) => { e.stopPropagation(); onModeSwitch(); }} className="p-2 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={askMode === 'popup' ? t.hints.dockToSide : t.hints.openAsPopup}>
249
256
  {askMode === 'popup' ? <PanelRight size={iconSize} /> : <AppWindow size={iconSize} />}
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useRef, useEffect, memo, useState, useCallback } from 'react';
4
- import { Sparkles, Loader2, AlertCircle, Wrench, WifiOff, Zap, Copy, Check, ArrowDown, FolderInput, Search, PenLine, Lightbulb } from 'lucide-react';
4
+ import { Sparkles, Loader2, AlertCircle, Wrench, WifiOff, Zap, Copy, Check, ArrowDown, FolderInput, Search, PenLine, Lightbulb, FileText, Paperclip } from 'lucide-react';
5
5
  import ReactMarkdown from 'react-markdown';
6
6
  import remarkGfm from 'remark-gfm';
7
7
  import type { Message, ImagePart } from '@/lib/types';
@@ -35,10 +35,17 @@ function CopyMessageButton({ text, label }: { text: string; label?: string }) {
35
35
  );
36
36
  }
37
37
 
38
- function UserMessageContent({ content, skillName, images }: { content: string; skillName?: string; images?: ImagePart[] }) {
38
+ function UserMessageContent({ content, skillName, images, attachedFiles, uploadedFileNames }: { content: string; skillName?: string; images?: ImagePart[]; attachedFiles?: string[]; uploadedFileNames?: string[] }) {
39
39
  const resolved = skillName ?? content.match(SKILL_PREFIX_RE)?.[1];
40
40
  const prefixMatch = content.match(SKILL_PREFIX_RE);
41
41
  const rest = prefixMatch ? content.slice(prefixMatch[0].length) : content;
42
+
43
+ // Deduplicate: uploaded files already shown shouldn't repeat as attached
44
+ const uploadedSet = new Set(uploadedFileNames ?? []);
45
+ const dedupedAttached = attachedFiles?.filter(fp => !uploadedSet.has(fp.split('/').pop() ?? fp));
46
+ const hasContext = (dedupedAttached && dedupedAttached.length > 0)
47
+ || (uploadedFileNames && uploadedFileNames.length > 0);
48
+
42
49
  return (
43
50
  <>
44
51
  {/* Images */}
@@ -68,6 +75,33 @@ function UserMessageContent({ content, skillName, images }: { content: string; s
68
75
  </span>
69
76
  )}
70
77
  {resolved ? rest : content}
78
+ {/* File context chips */}
79
+ {hasContext && (
80
+ <div className="mt-2 pt-1.5 border-t border-white/15 flex flex-wrap gap-1 whitespace-normal" role="list" aria-label="Attached files">
81
+ {dedupedAttached?.map(fp => (
82
+ <span
83
+ key={fp}
84
+ role="listitem"
85
+ className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] bg-white/10 text-white/80 min-w-0"
86
+ title={fp}
87
+ >
88
+ <FileText size={9} className="shrink-0 opacity-70" />
89
+ <span className="truncate max-w-[120px]">{fp.split('/').pop()}</span>
90
+ </span>
91
+ ))}
92
+ {uploadedFileNames?.map(name => (
93
+ <span
94
+ key={name}
95
+ role="listitem"
96
+ className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] bg-white/10 text-white/80 min-w-0"
97
+ title={name}
98
+ >
99
+ <Paperclip size={9} className="shrink-0 opacity-70" />
100
+ <span className="truncate max-w-[120px]">{name}</span>
101
+ </span>
102
+ ))}
103
+ </div>
104
+ )}
71
105
  </>
72
106
  );
73
107
  }
@@ -253,7 +287,7 @@ export default memo(function MessageList({
253
287
  <div
254
288
  className="max-w-[85%] px-3.5 py-2.5 rounded-2xl rounded-br-lg text-sm leading-relaxed whitespace-pre-wrap bg-[var(--amber)] text-[var(--amber-foreground)] shadow-sm shadow-[var(--amber)]/10"
255
289
  >
256
- <UserMessageContent content={m.content} skillName={m.skillName} images={m.images} />
290
+ <UserMessageContent content={m.content} skillName={m.skillName} images={m.images} attachedFiles={m.attachedFiles} uploadedFileNames={m.uploadedFileNames} />
257
291
  </div>
258
292
  ) : m.content.startsWith('__error__') ? (
259
293
  <div className="max-w-[85%] px-3.5 py-3 rounded-2xl rounded-bl-md border border-error/30 bg-error/10 text-sm shadow-sm">
@@ -7,15 +7,13 @@ 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';
14
- import { type CustomProvider, isCustomProviderId, findCustomProvider } from '@/lib/custom-endpoints';
12
+ import { type Provider, isProviderEntryId, findProvider } from '@/lib/custom-endpoints';
15
13
 
16
14
  const STORAGE_KEY = 'mindos-provider-model';
17
15
 
18
- type ProviderSelection = ProviderId | `cp_${string}` | null;
16
+ type ProviderSelection = ProviderId | `p_${string}` | null;
19
17
 
20
18
  interface ProviderModelCapsuleProps {
21
19
  providerValue: ProviderSelection;
@@ -27,10 +25,9 @@ interface ProviderModelCapsuleProps {
27
25
 
28
26
  interface SettingsData {
29
27
  ai?: {
30
- provider?: string;
31
- providers?: Record<string, { apiKey?: string; model?: string; baseUrl?: string }>;
28
+ activeProvider?: string;
29
+ providers?: Provider[];
32
30
  };
33
- customProviders?: CustomProvider[];
34
31
  envOverrides?: Record<string, boolean>;
35
32
  }
36
33
 
@@ -42,11 +39,11 @@ export function getPersistedProviderModel(): { provider: ProviderSelection; mode
42
39
  const raw = localStorage.getItem(STORAGE_KEY);
43
40
  if (!raw) {
44
41
  const old = localStorage.getItem('mindos-provider-override');
45
- if (old && (isProviderId(old) || isCustomProviderId(old))) return { provider: old as any, model: null };
42
+ if (old && (isProviderId(old) || isProviderEntryId(old))) return { provider: old as any, model: null };
46
43
  return { provider: null, model: null };
47
44
  }
48
45
  const parsed = JSON.parse(raw);
49
- const provider = parsed?.provider && (isProviderId(parsed.provider) || isCustomProviderId(parsed.provider))
46
+ const provider = parsed?.provider && (isProviderId(parsed.provider) || isProviderEntryId(parsed.provider))
50
47
  ? parsed.provider : null;
51
48
  const model = typeof parsed?.model === 'string' ? parsed.model : null;
52
49
  return { provider, model };
@@ -63,24 +60,8 @@ function persistProviderModel(provider: ProviderSelection, model: string | null)
63
60
 
64
61
  /* ── Configured providers ── */
65
62
 
66
- function getConfiguredProviders(data: SettingsData): (ProviderId | `cp_${string}`)[] {
67
- const result: (ProviderId | `cp_${string}`)[] = [];
68
- const providers = data.ai?.providers ?? {};
69
- const customProviders = data.customProviders ?? [];
70
- const env = data.envOverrides ?? {};
71
- for (const id of ALL_PROVIDER_IDS) {
72
- const preset = PROVIDER_PRESETS[id];
73
- const hasKey = providers[id]?.apiKey === '***set***';
74
- const envVar = getApiKeyEnvVar(id);
75
- const hasEnv = envVar ? !!env[envVar] : false;
76
- if (hasKey || hasEnv) { result.push(id); }
77
- else if (preset.apiKeyFallback) {
78
- const cfg = providers[id];
79
- if (data.ai?.provider === id || (cfg && (cfg.model || cfg.baseUrl))) result.push(id);
80
- }
81
- }
82
- for (const cp of customProviders) result.push(cp.id as `cp_${string}`);
83
- return result;
63
+ function getConfiguredProviders(data: SettingsData): string[] {
64
+ return (data.ai?.providers ?? []).map(p => p.id);
84
65
  }
85
66
 
86
67
  /* ── Component ── */
@@ -101,7 +82,7 @@ export default function ProviderModelCapsule({
101
82
  // Flyout state
102
83
  const providerPanelRef = useRef<HTMLDivElement>(null);
103
84
  const hoveredRowRef = useRef<HTMLDivElement>(null);
104
- const [hoveredProvider, setHoveredProvider] = useState<ProviderId | `cp_${string}` | null>(null);
85
+ const [hoveredProvider, setHoveredProvider] = useState<string | null>(null);
105
86
  const [flyoutStyle, setFlyoutStyle] = useState<React.CSSProperties>({});
106
87
  const [expandedModels, setExpandedModels] = useState<string[] | null>(null);
107
88
  const [modelsLoading, setModelsLoading] = useState(false);
@@ -151,8 +132,7 @@ export default function ProviderModelCapsule({
151
132
  return () => { cancelled = true; document.removeEventListener('visibilitychange', onVisible); window.removeEventListener('mindos:settings-changed', onChange); };
152
133
  }, []);
153
134
 
154
- const defaultProvider = (settingsData?.ai?.provider && isProviderId(settingsData.ai.provider))
155
- ? settingsData.ai.provider as ProviderId : 'anthropic';
135
+ const defaultProvider = settingsData?.ai?.activeProvider || '';
156
136
 
157
137
  const configuredProviders = useMemo(
158
138
  () => settingsData ? getConfiguredProviders(settingsData) : [],
@@ -170,14 +150,12 @@ export default function ProviderModelCapsule({
170
150
 
171
151
  // Resolve active display
172
152
  const activeProvider = providerValue ?? defaultProvider;
173
- const isCustomActive = isCustomProviderId(String(activeProvider));
174
- const customProvider = isCustomActive ? findCustomProvider(settingsData?.customProviders ?? [], String(activeProvider)) : null;
175
- const activePreset = !isCustomActive ? PROVIDER_PRESETS[activeProvider as ProviderId] : null;
176
- const defaultModel = customProvider?.model
177
- || settingsData?.ai?.providers?.[activeProvider as ProviderId]?.model
153
+ const activeEntry = findProvider(settingsData?.ai?.providers ?? [], String(activeProvider));
154
+ const activePreset = activeEntry ? PROVIDER_PRESETS[activeEntry.protocol] : null;
155
+ const defaultModel = activeEntry?.model
178
156
  || activePreset?.defaultModel || '';
179
157
  const displayModel = modelValue || defaultModel;
180
- const displayName = customProvider?.name
158
+ const displayName = activeEntry?.name
181
159
  || (activePreset ? (locale === 'zh' ? activePreset.nameZh : activePreset.name) : String(activeProvider));
182
160
 
183
161
  /* ── Dropdown positioning ── */
@@ -244,7 +222,7 @@ export default function ProviderModelCapsule({
244
222
  }, [open, hoveredProvider]);
245
223
 
246
224
  /* ── Model fetching ── */
247
- const fetchModels = useCallback(async (providerId: ProviderId | `cp_${string}`, force = false) => {
225
+ const fetchModels = useCallback(async (providerId: string, force = false) => {
248
226
  if (!force && modelsCacheRef.current[providerId]) {
249
227
  setExpandedModels(modelsCacheRef.current[providerId]);
250
228
  setModelsLoading(false);
@@ -254,14 +232,9 @@ export default function ProviderModelCapsule({
254
232
  setModelsLoading(true); setModelsError(''); setExpandedModels(null);
255
233
  const version = ++fetchVersionRef.current;
256
234
  try {
257
- const isCustom = isCustomProviderId(String(providerId));
258
- const body: Record<string, string> = isCustom
259
- ? { customProviderId: providerId }
260
- : { provider: providerId };
261
-
262
235
  const res = await fetch('/api/settings/list-models', {
263
236
  method: 'POST', headers: { 'Content-Type': 'application/json' },
264
- body: JSON.stringify(body),
237
+ body: JSON.stringify({ provider: providerId }),
265
238
  });
266
239
  if (version !== fetchVersionRef.current) return;
267
240
  const json = await res.json();
@@ -277,19 +250,13 @@ export default function ProviderModelCapsule({
277
250
  }
278
251
  }, []);
279
252
 
280
- const canCustomProviderExpand = useCallback((cpId: string) => {
281
- if (!isCustomProviderId(cpId)) return false;
282
- const cp = findCustomProvider(settingsData?.customProviders ?? [], cpId);
283
- return !!cp; // All custom providers can now expand (they use their baseProviderId)
284
- }, [settingsData]);
285
-
286
253
  // Determine if a provider can show model flyout
287
- const canProviderExpand = useCallback((id: ProviderId | `cp_${string}`) => {
288
- if (isCustomProviderId(String(id))) {
289
- return canCustomProviderExpand(String(id));
290
- }
291
- return PROVIDER_PRESETS[id as ProviderId]?.supportsListModels ?? false;
292
- }, [canCustomProviderExpand]);
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]);
293
260
 
294
261
  // Compute flyout position: anchored to right edge of provider panel, aligned to hovered row
295
262
  const computeFlyoutPosition = useCallback(() => {
@@ -323,7 +290,7 @@ export default function ProviderModelCapsule({
323
290
  }, []);
324
291
 
325
292
  // Open flyout for a provider (debounced to prevent flicker)
326
- const openFlyout = useCallback((providerId: ProviderId | `cp_${string}`) => {
293
+ const openFlyout = useCallback((providerId: string) => {
327
294
  cancelCloseTimer();
328
295
  if (openTimerRef.current) clearTimeout(openTimerRef.current);
329
296
  openTimerRef.current = setTimeout(() => {
@@ -378,7 +345,7 @@ export default function ProviderModelCapsule({
378
345
  if (e.key === 'ArrowDown') { e.preventDefault(); setModelHighlight(i => (i + 1) % filteredModels.length); }
379
346
  else if (e.key === 'ArrowUp') { e.preventDefault(); setModelHighlight(i => (i - 1 + filteredModels.length) % filteredModels.length); }
380
347
  else if (e.key === 'Enter' && modelHighlight >= 0 && modelHighlight < filteredModels.length && hoveredProvider) {
381
- e.preventDefault(); handleSelectModel(hoveredProvider, filteredModels[modelHighlight]);
348
+ e.preventDefault(); handleSelectModel(hoveredProvider as ProviderSelection, filteredModels[modelHighlight]);
382
349
  } else if (e.key === 'Escape') { e.preventDefault(); setHoveredProvider(null); setModelSearch(''); }
383
350
  }, [filteredModels, modelHighlight, hoveredProvider, handleSelectModel]);
384
351
 
@@ -391,19 +358,22 @@ export default function ProviderModelCapsule({
391
358
  /* ── Guards ── */
392
359
  if (!settingsData || configuredProviders.length === 0) return null;
393
360
 
394
- const modelShort = (displayModel || '').length > 24
395
- ? (displayModel || '').slice(0, 22) + '...' : displayModel;
396
- const builtInIds = configuredProviders.filter(id => !isCustomProviderId(String(id)));
397
- const customIds = configuredProviders.filter(id => isCustomProviderId(String(id)));
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;
398
369
  const hasModelOverride = !!(modelValue && modelValue !== defaultModel);
399
370
 
400
371
  /* ── Render: flyout (right panel) — positioned absolutely via portal ── */
401
372
  const renderFlyout = () => {
402
373
  if (!hoveredProvider) return null;
403
- const isCustom = isCustomProviderId(String(hoveredProvider));
404
- const preset = !isCustom ? PROVIDER_PRESETS[hoveredProvider as ProviderId] : null;
405
- const customProvider = isCustom ? findCustomProvider(settingsData?.customProviders ?? [], String(hoveredProvider)) : null;
406
- const displayName = customProvider?.name || (preset ? (locale === 'zh' ? preset.nameZh : preset.name) : String(hoveredProvider));
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));
407
377
 
408
378
  return createPortal(
409
379
  <div
@@ -462,11 +432,11 @@ export default function ProviderModelCapsule({
462
432
  )}
463
433
  {filteredModels.map((m, i) => {
464
434
  const isModelSelected = providerValue === hoveredProvider && modelValue === m;
465
- const defModel = settingsData?.ai?.providers?.[hoveredProvider]?.model || preset?.defaultModel;
435
+ const defModel = hovEntry?.model || preset?.defaultModel;
466
436
  return (
467
437
  <button
468
438
  key={m} type="button" data-model-item
469
- onClick={() => handleSelectModel(hoveredProvider, m)}
439
+ onClick={() => handleSelectModel(hoveredProvider as ProviderSelection, m)}
470
440
  className={`w-full text-left px-2 py-1 text-2xs rounded transition-colors flex items-center gap-1 ${
471
441
  isModelSelected ? 'bg-[var(--amber)]/12 text-foreground font-medium'
472
442
  : i === modelHighlight ? 'bg-accent' : 'hover:bg-accent/60'
@@ -502,26 +472,28 @@ export default function ProviderModelCapsule({
502
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"
503
473
  style={{ maxHeight: '70vh', overflowY: 'auto' }}
504
474
  >
505
- {builtInIds.map((id) => {
506
- const preset = PROVIDER_PRESETS[id as ProviderId];
507
- const provName = locale === 'zh' ? preset.nameZh : preset.name;
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;
508
480
  const provModel = modelValue && providerValue === id ? modelValue
509
- : settingsData?.ai?.providers?.[id as ProviderId]?.model || preset.defaultModel;
481
+ : entry.model || preset?.defaultModel || '';
510
482
  const isSelected = providerValue === id || (!providerValue && defaultProvider === id);
511
483
  const isHovered = hoveredProvider === id;
512
- const canExpand = canProviderExpand(id as ProviderId);
484
+ const canExpand = canProviderExpand(id);
513
485
 
514
486
  return (
515
487
  <div
516
488
  key={id}
517
489
  ref={isHovered ? hoveredRowRef : undefined}
518
- onMouseEnter={() => canExpand ? openFlyout(id as ProviderId) : closeFlyoutImmediate()}
490
+ onMouseEnter={() => canExpand ? openFlyout(id) : closeFlyoutImmediate()}
519
491
  onMouseLeave={startCloseTimer}
520
492
  >
521
493
  <div className={`flex w-full items-center text-xs transition-colors ${isHovered ? 'bg-accent/60' : 'hover:bg-muted/60'}`}>
522
494
  <button
523
495
  type="button" role="option" aria-selected={isSelected}
524
- onClick={() => handleSelectProvider(id as ProviderId)}
496
+ onClick={() => handleSelectProvider(id as ProviderSelection)}
525
497
  className="flex flex-1 items-center gap-2 px-3 py-1.5 min-w-0"
526
498
  >
527
499
  <div className="flex-1 min-w-0 truncate">
@@ -538,57 +510,7 @@ export default function ProviderModelCapsule({
538
510
  onClick={(e) => {
539
511
  e.stopPropagation();
540
512
  if (hoveredProvider === id) closeFlyoutImmediate();
541
- else openFlyout(id as ProviderId);
542
- }}
543
- className={`shrink-0 px-1.5 py-1.5 mr-1 rounded transition-colors ${
544
- isHovered ? 'text-foreground' : 'text-muted-foreground/40 hover:text-muted-foreground'
545
- }`}
546
- title={t.ask?.selectModel ?? 'Select model'}
547
- >
548
- <ChevronRight size={11} />
549
- </button>
550
- )}
551
- </div>
552
- </div>
553
- );
554
- })}
555
-
556
- {customIds.map((id) => {
557
- const cp = findCustomProvider(settingsData?.customProviders ?? [], String(id));
558
- if (!cp) return null;
559
- const cpModel = modelValue && providerValue === id ? modelValue : cp.model;
560
- const isSelected = providerValue === id;
561
- const isHovered = hoveredProvider === id;
562
- const canExpand = canProviderExpand(id as `cp_${string}`);
563
-
564
- return (
565
- <div
566
- key={id}
567
- ref={isHovered ? hoveredRowRef : undefined}
568
- onMouseEnter={() => canExpand ? openFlyout(id as `cp_${string}`) : closeFlyoutImmediate()}
569
- onMouseLeave={startCloseTimer}
570
- >
571
- <div className={`flex w-full items-center text-xs transition-colors ${isHovered ? 'bg-accent/60' : 'hover:bg-muted/60'}`}>
572
- <button
573
- type="button" role="option" aria-selected={isSelected}
574
- onClick={() => handleSelectProvider(id)}
575
- className="flex flex-1 items-center gap-2 px-3 py-1.5 min-w-0"
576
- >
577
- <div className="flex-1 min-w-0 truncate">
578
- <span className={`text-xs ${isSelected ? 'font-medium text-foreground' : 'text-foreground/80'}`}>
579
- {cp.name}
580
- </span>
581
- <span className="text-2xs text-muted-foreground ml-1.5">{cpModel}</span>
582
- </div>
583
- {isSelected && <Check size={11} className="shrink-0 text-[var(--amber)]" />}
584
- </button>
585
- {canExpand && (
586
- <button
587
- type="button"
588
- onClick={(e) => {
589
- e.stopPropagation();
590
- if (hoveredProvider === id) closeFlyoutImmediate();
591
- else openFlyout(id as `cp_${string}`);
513
+ else openFlyout(id);
592
514
  }}
593
515
  className={`shrink-0 px-1.5 py-1.5 mr-1 rounded transition-colors ${
594
516
  isHovered ? 'text-foreground' : 'text-muted-foreground/40 hover:text-muted-foreground'
@@ -620,7 +542,7 @@ export default function ProviderModelCapsule({
620
542
  disabled={disabled}
621
543
  className={`
622
544
  inline-flex items-center gap-1 rounded-full px-2.5 py-0.5
623
- text-2xs font-medium transition-colors select-none
545
+ text-2xs font-medium transition-colors select-none max-w-[260px]
624
546
  border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
625
547
  disabled:opacity-40 disabled:cursor-not-allowed
626
548
  ${providerValue || hasModelOverride
@@ -628,13 +550,13 @@ export default function ProviderModelCapsule({
628
550
  : 'bg-muted/50 border-border/50 text-muted-foreground hover:bg-muted hover:text-foreground'
629
551
  }
630
552
  `}
631
- title={t.ask?.providerCapsule ?? 'Provider'}
553
+ title={capsuleTooltip}
632
554
  aria-expanded={open}
633
555
  aria-haspopup="listbox"
634
556
  >
635
557
  <Cpu size={11} className="shrink-0" />
636
- <span className="truncate max-w-[160px]">
637
- {displayName}
558
+ <span className="truncate">
559
+ {providerDisplay}
638
560
  <span className="text-muted-foreground"> · </span>
639
561
  <span className={hasModelOverride ? 'text-[var(--amber)]' : 'text-muted-foreground'}>{modelShort}</span>
640
562
  </span>