@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
@@ -1,62 +1,84 @@
1
1
  'use client';
2
2
 
3
3
  import { useState } from 'react';
4
- import { CheckCircle2, ChevronDown, SkipForward, Plus, Edit2, Trash2 } from 'lucide-react';
5
- import { type ProviderId, PROVIDER_PRESETS, groupedProviders } from '@/lib/agent/providers';
6
- import { type CustomProvider } from '@/lib/custom-endpoints';
4
+ import { CheckCircle2, ChevronDown, SkipForward, Plus } from 'lucide-react';
5
+ import { type ProviderId, PROVIDER_PRESETS, groupedProviders, ALL_PROVIDER_IDS } from '@/lib/agent/providers';
6
+ import { type Provider } from '@/lib/custom-endpoints';
7
7
  import { useLocale } from '@/lib/stores/locale-store';
8
8
 
9
9
  interface ProviderSelectProps {
10
- value: ProviderId | 'skip';
11
- onChange: (id: ProviderId | 'skip') => void;
10
+ value: string | 'skip';
11
+ onChange: (id: string | 'skip') => void;
12
12
  showSkip?: boolean;
13
13
  compact?: boolean;
14
+ /** @deprecated Use customProviders (unified Provider[]) instead */
14
15
  configuredProviders?: Set<ProviderId>;
15
- customProviders?: CustomProvider[];
16
- onAddCustom?: () => void;
17
- onEditCustom?: (id: string) => void;
18
- onDeleteCustom?: (id: string) => void;
16
+ customProviders?: Provider[];
17
+ onAdd?: () => void;
19
18
  }
20
19
 
21
20
  export default function ProviderSelect({
22
21
  value, onChange, showSkip = false, compact = false, configuredProviders,
23
- customProviders, onAddCustom, onEditCustom, onDeleteCustom,
22
+ customProviders, onAdd,
24
23
  }: ProviderSelectProps) {
25
24
  const { locale } = useLocale();
26
25
  const [showMore, setShowMore] = useState(false);
27
- const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
28
26
  const groups = groupedProviders();
29
27
 
30
- const renderItem = (id: ProviderId) => {
28
+ const hasConfigured = configuredProviders && configuredProviders.size > 0;
29
+ const hasCustom = customProviders && customProviders.length > 0;
30
+
31
+ // In compact settings mode: show provider list + Add button
32
+ // New model: customProviders IS the full list (unified Provider[])
33
+ // Legacy model: configuredProviders set + separate customProviders array
34
+ const useConfiguredMode = compact && (hasConfigured || hasCustom) && !showSkip;
35
+
36
+ // Legacy: Sorted configured provider IDs (for backward compat with old callers)
37
+ const configuredIds = hasConfigured
38
+ ? ALL_PROVIDER_IDS.filter(id => configuredProviders!.has(id))
39
+ : [];
40
+
41
+ // Add panel shows ALL providers as protocol templates (can add multiple of the same type)
42
+ const { primary: primaryItems, more: moreItems } = groups;
43
+
44
+ /* ── Compact tab button (for legacy builtin-only mode) ── */
45
+ const renderCompactTab = (id: ProviderId) => {
31
46
  const preset = PROVIDER_PRESETS[id];
32
47
  const displayName = locale === 'zh' ? preset.nameZh : preset.name;
33
48
  const isSelected = value === id;
34
49
  const isConfigured = configuredProviders?.has(id);
35
50
 
36
- if (compact) {
37
- return (
38
- <button
39
- key={id}
40
- type="button"
41
- onClick={() => onChange(id)}
42
- className={`flex items-center gap-2 px-3 py-2 rounded-lg border text-left transition-all text-sm ${
43
- isSelected
44
- ? 'border-[var(--amber)] bg-[var(--amber-subtle)] shadow-sm'
45
- : 'border-border/50 hover:border-border hover:bg-muted/30'
46
- }`}
47
- >
48
- <span className={`font-medium ${isSelected ? 'text-foreground' : 'text-muted-foreground'}`}>
49
- {displayName}
50
- </span>
51
- {isConfigured && !isSelected && (
52
- <CheckCircle2 size={12} className="text-success ml-auto shrink-0" />
53
- )}
54
- {isSelected && (
55
- <CheckCircle2 size={14} className="ml-auto shrink-0" style={{ color: 'var(--amber)' }} />
56
- )}
57
- </button>
58
- );
59
- }
51
+ return (
52
+ <button
53
+ key={id}
54
+ type="button"
55
+ onClick={() => onChange(id)}
56
+ className={`flex items-center gap-2 px-3 py-2 rounded-lg border text-left transition-all text-sm ${
57
+ isSelected
58
+ ? 'border-[var(--amber)] bg-[var(--amber-subtle)] shadow-sm'
59
+ : 'border-border/50 hover:border-border hover:bg-muted/30'
60
+ }`}
61
+ >
62
+ <span className={`font-medium ${isSelected ? 'text-foreground' : 'text-muted-foreground'}`}>
63
+ {displayName}
64
+ </span>
65
+ {isConfigured && !isSelected && (
66
+ <CheckCircle2 size={12} className="text-success ml-auto shrink-0" />
67
+ )}
68
+ {isSelected && (
69
+ <CheckCircle2 size={14} className="ml-auto shrink-0" style={{ color: 'var(--amber)' }} />
70
+ )}
71
+ </button>
72
+ );
73
+ };
74
+
75
+ /* ── Full card button (used in setup wizard / non-compact) ── */
76
+ const renderCard = (id: ProviderId) => {
77
+ const preset = PROVIDER_PRESETS[id];
78
+ const displayName = locale === 'zh' ? preset.nameZh : preset.name;
79
+ const description = locale === 'zh' ? preset.descriptionZh : preset.description;
80
+ const isSelected = value === id;
81
+ const isConfigured = configuredProviders?.has(id);
60
82
 
61
83
  return (
62
84
  <button
@@ -71,7 +93,10 @@ export default function ProviderSelect({
71
93
  >
72
94
  <div className="flex-1 min-w-0">
73
95
  <p className="text-sm font-medium" style={{ color: 'var(--foreground)' }}>{displayName}</p>
74
- <p className="text-xs mt-0.5" style={{ color: 'var(--muted-foreground)' }}>
96
+ {description && (
97
+ <p className="text-xs mt-0.5" style={{ color: 'var(--muted-foreground)' }}>{description}</p>
98
+ )}
99
+ <p className={`text-xs ${description ? 'mt-1' : 'mt-0.5'}`} style={{ color: 'var(--muted-foreground)' }}>
75
100
  {preset.defaultModel}
76
101
  </p>
77
102
  </div>
@@ -85,19 +110,72 @@ export default function ProviderSelect({
85
110
  );
86
111
  };
87
112
 
88
- const hasCustom = customProviders && customProviders.length > 0;
89
- const hasCustomSection = onAddCustom != null;
90
- const { primary: primaryItems, more: moreItems } = groups;
113
+ /* ════════════════════════════════════════════
114
+ * MODE 1: Provider list + Add button
115
+ * (compact settings, has providers)
116
+ * ════════════════════════════════════════════ */
117
+ if (useConfiguredMode) {
118
+ return (
119
+ <div className="space-y-2">
120
+ {/* Providers row */}
121
+ <div className="flex flex-wrap gap-2">
122
+ {/* Legacy: built-in configured providers */}
123
+ {configuredIds.map(id => renderCompactTab(id))}
124
+
125
+ {/* Unified provider list (or legacy custom providers) */}
126
+ {customProviders?.map(cp => {
127
+ const isSelected = value === cp.id;
128
+ return (
129
+ <button
130
+ key={cp.id}
131
+ type="button"
132
+ onClick={() => onChange(cp.id)}
133
+ className={`flex items-center gap-2 px-3 py-2 rounded-lg border text-left transition-all text-sm ${
134
+ isSelected
135
+ ? 'border-[var(--amber)] bg-[var(--amber-subtle)] shadow-sm'
136
+ : 'border-border/50 hover:border-border hover:bg-muted/30'
137
+ }`}
138
+ >
139
+ <span className={`font-medium ${isSelected ? 'text-foreground' : 'text-muted-foreground'}`}>
140
+ {cp.name}
141
+ </span>
142
+ {isSelected && (
143
+ <CheckCircle2 size={14} className="ml-auto shrink-0" style={{ color: 'var(--amber)' }} />
144
+ )}
145
+ </button>
146
+ );
147
+ })}
148
+
149
+ {/* Add button — opens form directly */}
150
+ {onAdd && (
151
+ <button
152
+ type="button"
153
+ onClick={onAdd}
154
+ className="flex items-center gap-1.5 px-3 py-2 rounded-lg border border-dashed border-border/50 text-sm text-muted-foreground hover:border-border hover:text-foreground transition-all"
155
+ >
156
+ <Plus size={14} />
157
+ <span>{locale === 'zh' ? '添加' : 'Add'}</span>
158
+ </button>
159
+ )}
160
+ </div>
161
+ </div>
162
+ );
163
+ }
164
+
165
+ /* ════════════════════════════════════════════
166
+ * MODE 2: Full list (setup wizard / no configured providers)
167
+ * Original behavior preserved
168
+ * ════════════════════════════════════════════ */
91
169
 
92
170
  return (
93
171
  <div className="space-y-2">
94
- {/* Primary providers — always visible */}
172
+ {/* Primary providers */}
95
173
  <div className={compact ? 'flex flex-wrap gap-2' : 'grid grid-cols-1 gap-2'}>
96
- {primaryItems.map(renderItem)}
174
+ {primaryItems.map(id => compact ? renderCompactTab(id) : renderCard(id))}
97
175
  </div>
98
176
 
99
- {/* Show more toggle */}
100
- {(moreItems.length > 0 || hasCustomSection) && (
177
+ {/* More toggle */}
178
+ {moreItems.length > 0 && (
101
179
  <>
102
180
  <button
103
181
  type="button"
@@ -108,75 +186,13 @@ export default function ProviderSelect({
108
186
  {showMore
109
187
  ? (locale === 'zh' ? '收起' : 'Show less')
110
188
  : (locale === 'zh'
111
- ? `更多${hasCustom ? ` (${moreItems.length + customProviders!.length})` : moreItems.length > 0 ? ` (${moreItems.length})` : ''}`
112
- : `More${hasCustom ? ` (${moreItems.length + customProviders!.length})` : moreItems.length > 0 ? ` (${moreItems.length})` : ''}`)}
189
+ ? `更多 (${moreItems.length})`
190
+ : `More (${moreItems.length})`)}
113
191
  </button>
114
192
 
115
193
  {showMore && (
116
- <div className="space-y-2">
117
- {/* Built-in "more" providers */}
118
- {moreItems.length > 0 && (
119
- <div className={compact ? 'flex flex-wrap gap-2' : 'grid grid-cols-1 gap-2'}>
120
- {moreItems.map(renderItem)}
121
- </div>
122
- )}
123
-
124
- {/* Custom providers */}
125
- {hasCustomSection && (
126
- <div className="space-y-2">
127
- {hasCustom && (
128
- <div className={compact ? 'flex flex-wrap gap-2' : 'grid grid-cols-1 gap-2'}>
129
- {customProviders!.map(cp => (
130
- <div
131
- key={cp.id}
132
- className={`flex items-center gap-2 rounded-lg border text-left transition-all group ${
133
- compact
134
- ? 'px-3 py-2 text-sm border-border/50 hover:border-border hover:bg-muted/30'
135
- : 'p-3 border-border/50 hover:border-border'
136
- }`}
137
- >
138
- <div className="flex-1 min-w-0">
139
- <span className={`font-medium text-muted-foreground ${compact ? 'text-sm' : 'text-sm'}`}>{cp.name}</span>
140
- {!compact && (
141
- <span className="text-xs text-muted-foreground/50 ml-2">{cp.model}</span>
142
- )}
143
- </div>
144
- <div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
145
- {onEditCustom && (
146
- <button
147
- type="button"
148
- onClick={() => onEditCustom(cp.id)}
149
- className="p-1 rounded text-muted-foreground/50 hover:text-foreground hover:bg-muted/50 transition-colors"
150
- >
151
- <Edit2 size={12} />
152
- </button>
153
- )}
154
- {onDeleteCustom && (
155
- <button
156
- type="button"
157
- onClick={() => setDeleteConfirmId(cp.id)}
158
- className="p-1 rounded text-muted-foreground/50 hover:text-destructive hover:bg-destructive/8 transition-colors"
159
- >
160
- <Trash2 size={12} />
161
- </button>
162
- )}
163
- </div>
164
- </div>
165
- ))}
166
- </div>
167
- )}
168
-
169
- {/* Add custom provider button */}
170
- <button
171
- type="button"
172
- onClick={onAddCustom}
173
- className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors py-1"
174
- >
175
- <Plus size={12} />
176
- {locale === 'zh' ? '自定义 Provider' : 'Custom Provider'}
177
- </button>
178
- </div>
179
- )}
194
+ <div className={compact ? 'flex flex-wrap gap-2' : 'grid grid-cols-1 gap-2'}>
195
+ {moreItems.map(id => compact ? renderCompactTab(id) : renderCard(id))}
180
196
  </div>
181
197
  )}
182
198
  </>
@@ -203,30 +219,6 @@ export default function ProviderSelect({
203
219
  </button>
204
220
  )}
205
221
 
206
- {/* Inline delete confirmation */}
207
- {deleteConfirmId && onDeleteCustom && (
208
- <div className="flex items-center gap-2 px-3 py-2 rounded-lg border border-destructive/20 bg-destructive/5">
209
- <span className="text-xs text-destructive flex-1">
210
- {locale === 'zh'
211
- ? `删除 "${customProviders?.find(p => p.id === deleteConfirmId)?.name}"?`
212
- : `Delete "${customProviders?.find(p => p.id === deleteConfirmId)?.name}"?`}
213
- </span>
214
- <button
215
- type="button"
216
- onClick={() => setDeleteConfirmId(null)}
217
- className="text-xs text-muted-foreground hover:text-foreground px-2 py-0.5 rounded transition-colors"
218
- >
219
- {locale === 'zh' ? '取消' : 'Cancel'}
220
- </button>
221
- <button
222
- type="button"
223
- onClick={() => { onDeleteCustom(deleteConfirmId); setDeleteConfirmId(null); }}
224
- className="text-xs text-destructive font-medium hover:bg-destructive/10 px-2 py-0.5 rounded transition-colors"
225
- >
226
- {locale === 'zh' ? '删除' : 'Delete'}
227
- </button>
228
- </div>
229
- )}
230
222
  </div>
231
223
  );
232
224
  }
@@ -28,12 +28,13 @@ export interface AskChatRefs {
28
28
  interface UseAskChatOpts {
29
29
  currentFile?: string;
30
30
  chatMode: AskMode;
31
- providerOverride: ProviderId | `cp_${string}` | null;
31
+ providerOverride: ProviderId | `p_${string}` | null;
32
32
  modelOverride: string | null;
33
33
  onFirstMessage?: () => void;
34
34
  refs: AskChatRefs;
35
35
  errorLabels: { noResponse: string; stopped: string };
36
36
  resetInputState: () => void;
37
+ onRestoreInput?: (userMessage: Message) => void;
37
38
  }
38
39
 
39
40
  export function useAskChat({
@@ -45,6 +46,7 @@ export function useAskChat({
45
46
  refs,
46
47
  errorLabels,
47
48
  resetInputState,
49
+ onRestoreInput,
48
50
  }: UseAskChatOpts) {
49
51
  const [isLoading, setIsLoading] = useState(false);
50
52
  const [loadingPhase, setLoadingPhase] = useState<LoadingPhase>('connecting');
@@ -53,10 +55,68 @@ export function useAskChat({
53
55
  const abortRef = useRef<AbortController | null>(null);
54
56
  const firstMessageFired = useRef(false);
55
57
 
56
- const stop = useCallback(() => { abortRef.current?.abort(); }, []);
58
+ // Cooldown guard: after stop+retract, briefly block re-submission so that
59
+ // the mouseup on the stop-button position doesn't accidentally trigger the
60
+ // send button that React swaps in at the same DOM position.
61
+ const submitCooldownRef = useRef(false);
62
+
63
+ // Track the pending user message so we can retract it on stop.
64
+ // `userMessageIndex` is the index of the *user* message inside the messages
65
+ // array (the assistant placeholder sits at userMessageIndex + 1).
66
+ const pendingMessageRef = useRef<{
67
+ userMessageIndex: number;
68
+ userMessage: Message;
69
+ } | null>(null);
70
+
71
+ // When true the AbortError handler in submit() skips its own setMessages
72
+ // because stop() already cleaned up the messages array.
73
+ const retractedRef = useRef(false);
74
+
75
+ const stop = useCallback(() => {
76
+ const pending = pendingMessageRef.current;
77
+
78
+ // Abort the fetch first.
79
+ abortRef.current?.abort();
80
+
81
+ if (pending) {
82
+ retractedRef.current = true;
83
+
84
+ // Always remove the user message + assistant response (empty or partial)
85
+ // from the messages array. The user clicked stop — they don't want this
86
+ // exchange in the history at all.
87
+ refs.sessionRef.current?.setMessages(prev => {
88
+ const updated = [...prev];
89
+ const idx = pending.userMessageIndex;
90
+
91
+ // Remove assistant message first (at idx + 1) — may be empty placeholder
92
+ // or a partial streamed response.
93
+ if (idx + 1 < updated.length && updated[idx + 1]?.role === 'assistant') {
94
+ updated.splice(idx + 1, 1);
95
+ }
96
+
97
+ // Remove the user message.
98
+ if (idx < updated.length && updated[idx]?.role === 'user') {
99
+ updated.splice(idx, 1);
100
+ }
101
+
102
+ return updated;
103
+ });
104
+
105
+ // Restore text (+ attachments) back into the input box.
106
+ onRestoreInput?.(pending.userMessage);
107
+
108
+ pendingMessageRef.current = null;
109
+
110
+ // Block re-submission for a short window so the browser's mouseup
111
+ // doesn't hit the send button that replaces the stop button.
112
+ submitCooldownRef.current = true;
113
+ setTimeout(() => { submitCooldownRef.current = false; }, 300);
114
+ }
115
+ }, [refs, onRestoreInput]);
57
116
 
58
117
  const submit = useCallback(async (e: React.FormEvent) => {
59
118
  e.preventDefault();
119
+ if (submitCooldownRef.current) return; // ignore accidental re-submit after stop
60
120
  const m = refs.mentionRef.current;
61
121
  const s = refs.slashRef.current;
62
122
  const img = refs.imageUploadRef.current;
@@ -70,15 +130,33 @@ export function useAskChat({
70
130
  const skill = refs.selectedSkillRef.current;
71
131
  const acpAgent = refs.selectedAcpAgentRef.current;
72
132
  const pendingImages = img.images.length > 0 ? [...img.images] : undefined;
133
+ // Only store explicitly user-chosen files (filter out auto-included currentFile)
134
+ const explicitAttached = refs.attachedFilesRef.current.filter(f => f !== currentFile);
135
+ const pendingAttachedFiles = explicitAttached.length > 0 ? explicitAttached : undefined;
136
+ const pendingUploadedNames = upl.localAttachments
137
+ .filter(f => f.status !== 'loading')
138
+ .map(f => f.name);
73
139
  const userMsg: Message = {
74
140
  role: 'user',
75
141
  content: text,
76
142
  timestamp: Date.now(),
77
143
  ...(skill && { skillName: skill.name }),
78
144
  ...(pendingImages && { images: pendingImages }),
145
+ ...(pendingAttachedFiles && { attachedFiles: pendingAttachedFiles }),
146
+ ...(pendingUploadedNames.length > 0 && { uploadedFileNames: pendingUploadedNames }),
79
147
  };
80
148
  img.clearImages();
81
149
  const requestMessages = [...sess.messages, userMsg];
150
+
151
+ // Track the user message index for potential retraction on stop.
152
+ // The user message is at requestMessages.length - 1; the assistant
153
+ // placeholder we're about to insert will be at requestMessages.length.
154
+ pendingMessageRef.current = {
155
+ userMessageIndex: requestMessages.length - 1,
156
+ userMessage: userMsg,
157
+ };
158
+ retractedRef.current = false;
159
+
82
160
  sess.setMessages([...requestMessages, { role: 'assistant', content: '', timestamp: Date.now() }]);
83
161
 
84
162
  resetInputState();
@@ -190,6 +268,8 @@ export function useAskChat({
190
268
  return updated;
191
269
  });
192
270
  }
271
+ // Successfully received response — no longer retractable.
272
+ pendingMessageRef.current = null;
193
273
  return;
194
274
  } catch (err) {
195
275
  lastError = err instanceof Error ? err : new Error(String(err));
@@ -201,18 +281,21 @@ export function useAskChat({
201
281
  if (lastError) throw lastError;
202
282
  } catch (err) {
203
283
  if ((err as Error).name === 'AbortError') {
204
- refs.sessionRef.current?.setMessages(prev => {
205
- const updated = [...prev];
206
- const lastIdx = updated.length - 1;
207
- if (lastIdx >= 0 && updated[lastIdx].role === 'assistant') {
208
- const last = updated[lastIdx];
209
- const hasContent = last.content.trim() || (last.parts && last.parts.length > 0);
210
- if (!hasContent) {
211
- updated[lastIdx] = { role: 'assistant', content: `__error__${errorLabels.stopped}` };
284
+ // If stop() already retracted the messages, skip writing __error__stopped.
285
+ if (!retractedRef.current) {
286
+ refs.sessionRef.current?.setMessages(prev => {
287
+ const updated = [...prev];
288
+ const lastIdx = updated.length - 1;
289
+ if (lastIdx >= 0 && updated[lastIdx].role === 'assistant') {
290
+ const last = updated[lastIdx];
291
+ const hasContent = last.content.trim() || (last.parts && last.parts.length > 0);
292
+ if (!hasContent) {
293
+ updated[lastIdx] = { role: 'assistant', content: `__error__${errorLabels.stopped}` };
294
+ }
212
295
  }
213
- }
214
- return updated;
215
- });
296
+ return updated;
297
+ });
298
+ }
216
299
  } else {
217
300
  const errMsg = err instanceof Error ? err.message : 'Something went wrong';
218
301
  refs.sessionRef.current?.setMessages(prev => {
@@ -233,6 +316,7 @@ export function useAskChat({
233
316
  setIsLoading(false);
234
317
  setReconnectAttempt(0);
235
318
  abortRef.current = null;
319
+ pendingMessageRef.current = null;
236
320
  }
237
321
  }, [currentFile, chatMode, providerOverride, errorLabels.noResponse, errorLabels.stopped, onFirstMessage, refs, resetInputState]);
238
322
 
@@ -33,6 +33,8 @@ export function useAskPanel(): AskPanelState {
33
33
  const [desktopAskPopupOpen, setDesktopAskPopupOpen] = useState(false);
34
34
  const [askInitialMessage, setAskInitialMessage] = useState('');
35
35
  const [askMaximized, setAskMaximized] = useState(false);
36
+ const askMaximizedRef = useRef(false);
37
+ askMaximizedRef.current = askMaximized;
36
38
  const [askOpenSource, setAskOpenSource] = useState<'user' | 'guide' | 'guide-next'>('user');
37
39
  const [askAcpAgent, setAskAcpAgent] = useState<AcpAgentSelection | null>(null);
38
40
  const prevWidthRef = useRef(RIGHT_ASK_DEFAULT_WIDTH);
@@ -60,7 +62,21 @@ export function useAskPanel(): AskPanelState {
60
62
  }
61
63
  };
62
64
  window.addEventListener('storage', onStorage);
63
- return () => window.removeEventListener('storage', onStorage);
65
+
66
+ // Listen for "dock to panel" from home page fullscreen
67
+ const onOpenPanel = () => {
68
+ setAskPanelOpen(true);
69
+ if (askMaximizedRef.current) {
70
+ setAskMaximized(false);
71
+ setAskPanelWidth(prevWidthRef.current);
72
+ }
73
+ };
74
+ window.addEventListener('mindos:open-ask-panel', onOpenPanel);
75
+
76
+ return () => {
77
+ window.removeEventListener('storage', onStorage);
78
+ window.removeEventListener('mindos:open-ask-panel', onOpenPanel);
79
+ };
64
80
  }, []);
65
81
 
66
82
  // Bridge useAskModal store → right Ask panel or popup
@@ -62,6 +62,71 @@ export function estimateTokens(messages: AgentMessage[]): number {
62
62
  // Context limits by model family
63
63
  // ---------------------------------------------------------------------------
64
64
 
65
+ /**
66
+ * Cache for Ollama model context window sizes.
67
+ * Key: "baseUrl::modelName", Value: num_ctx (token count).
68
+ * Avoids querying /api/show on every request.
69
+ */
70
+ const ollamaContextCache = new Map<string, number>();
71
+
72
+ /**
73
+ * Query Ollama's /api/show endpoint to get the actual context window (num_ctx)
74
+ * for a specific model. Returns undefined if the query fails or the model
75
+ * doesn't report its context size.
76
+ *
77
+ * Results are cached in-memory per baseUrl+model for the process lifetime.
78
+ */
79
+ export async function getOllamaContextWindow(
80
+ baseUrl: string,
81
+ modelName: string,
82
+ ): Promise<number | undefined> {
83
+ const cacheKey = `${baseUrl}::${modelName}`;
84
+ if (ollamaContextCache.has(cacheKey)) return ollamaContextCache.get(cacheKey);
85
+
86
+ // Ollama's /api/show endpoint is at the root, not under /v1
87
+ const ollamaBase = baseUrl.replace(/\/v1\/?$/, '');
88
+ try {
89
+ const resp = await fetch(`${ollamaBase}/api/show`, {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/json' },
92
+ body: JSON.stringify({ name: modelName }),
93
+ signal: AbortSignal.timeout(3000),
94
+ });
95
+ if (!resp.ok) return undefined;
96
+
97
+ const data = await resp.json() as {
98
+ model_info?: Record<string, unknown>;
99
+ parameters?: string;
100
+ };
101
+
102
+ // Method 1: model_info (structured, most reliable)
103
+ let numCtx: number | undefined;
104
+ if (data.model_info) {
105
+ // Different Ollama versions use different key names
106
+ const ctxVal = data.model_info['context_length']
107
+ ?? data.model_info['num_ctx']
108
+ ?? data.model_info['general.context_length'];
109
+ if (typeof ctxVal === 'number' && ctxVal > 0) {
110
+ numCtx = ctxVal;
111
+ }
112
+ }
113
+
114
+ // Method 2: parse from parameters string (fallback)
115
+ if (!numCtx && typeof data.parameters === 'string') {
116
+ const match = data.parameters.match(/num_ctx\s+(\d+)/);
117
+ if (match) numCtx = parseInt(match[1], 10);
118
+ }
119
+
120
+ if (numCtx && numCtx > 0) {
121
+ ollamaContextCache.set(cacheKey, numCtx);
122
+ return numCtx;
123
+ }
124
+ } catch {
125
+ // Network error, timeout, or Ollama not running — fail silently
126
+ }
127
+ return undefined;
128
+ }
129
+
65
130
  const MODEL_LIMITS: Record<string, number> = {
66
131
  'claude': 200_000,
67
132
  'gpt-4o': 128_000,