@seqyuan/annodex 0.1.12 → 0.1.13

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 (478) hide show
  1. package/.next/BUILD_ID +1 -0
  2. package/.next/app-path-routes-manifest.json +39 -0
  3. package/.next/build-manifest.json +20 -0
  4. package/.next/diagnostics/build-diagnostics.json +6 -0
  5. package/.next/diagnostics/framework.json +1 -0
  6. package/.next/export-marker.json +6 -0
  7. package/.next/images-manifest.json +68 -0
  8. package/.next/next-minimal-server.js.nft.json +1 -0
  9. package/.next/next-server.js.nft.json +1 -0
  10. package/.next/package.json +1 -0
  11. package/.next/prerender-manifest.json +109 -0
  12. package/.next/react-loadable-manifest.json +2320 -0
  13. package/.next/required-server-files.js +343 -0
  14. package/.next/required-server-files.json +343 -0
  15. package/.next/routes-manifest.json +286 -0
  16. package/.next/server/app/_global-error/page.js +32 -0
  17. package/.next/server/app/_global-error/page.js.nft.json +1 -0
  18. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -0
  19. package/.next/server/app/_global-error.html +1 -0
  20. package/.next/server/app/_global-error.meta +16 -0
  21. package/.next/server/app/_global-error.rsc +14 -0
  22. package/.next/server/app/_global-error.segments/_full.segment.rsc +14 -0
  23. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +5 -0
  24. package/.next/server/app/_global-error.segments/_global-error.segment.rsc +5 -0
  25. package/.next/server/app/_global-error.segments/_head.segment.rsc +5 -0
  26. package/.next/server/app/_global-error.segments/_index.segment.rsc +5 -0
  27. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -0
  28. package/.next/server/app/_not-found/page.js +2 -0
  29. package/.next/server/app/_not-found/page.js.nft.json +1 -0
  30. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  31. package/.next/server/app/_not-found.html +1 -0
  32. package/.next/server/app/_not-found.meta +16 -0
  33. package/.next/server/app/_not-found.rsc +18 -0
  34. package/.next/server/app/_not-found.segments/_full.segment.rsc +18 -0
  35. package/.next/server/app/_not-found.segments/_head.segment.rsc +6 -0
  36. package/.next/server/app/_not-found.segments/_index.segment.rsc +5 -0
  37. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -0
  38. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +5 -0
  39. package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -0
  40. package/.next/server/app/api/agent/[id]/events/route.js +3 -0
  41. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -0
  42. package/.next/server/app/api/agent/[id]/events/route_client-reference-manifest.js +1 -0
  43. package/.next/server/app/api/agent/[id]/route.js +1 -0
  44. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -0
  45. package/.next/server/app/api/agent/[id]/route_client-reference-manifest.js +1 -0
  46. package/.next/server/app/api/agent/new/route.js +1 -0
  47. package/.next/server/app/api/agent/new/route.js.nft.json +1 -0
  48. package/.next/server/app/api/agent/new/route_client-reference-manifest.js +1 -0
  49. package/.next/server/app/api/auth/all-providers/route.js +1 -0
  50. package/.next/server/app/api/auth/all-providers/route.js.nft.json +1 -0
  51. package/.next/server/app/api/auth/all-providers/route_client-reference-manifest.js +1 -0
  52. package/.next/server/app/api/auth/api-key/[provider]/route.js +1 -0
  53. package/.next/server/app/api/auth/api-key/[provider]/route.js.nft.json +1 -0
  54. package/.next/server/app/api/auth/api-key/[provider]/route_client-reference-manifest.js +1 -0
  55. package/.next/server/app/api/auth/login/[provider]/route.js +1 -0
  56. package/.next/server/app/api/auth/login/[provider]/route.js.nft.json +1 -0
  57. package/.next/server/app/api/auth/login/[provider]/route_client-reference-manifest.js +1 -0
  58. package/.next/server/app/api/auth/login/route.js +1 -0
  59. package/.next/server/app/api/auth/login/route.js.nft.json +1 -0
  60. package/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -0
  61. package/.next/server/app/api/auth/logout/[provider]/route.js +1 -0
  62. package/.next/server/app/api/auth/logout/[provider]/route.js.nft.json +1 -0
  63. package/.next/server/app/api/auth/logout/[provider]/route_client-reference-manifest.js +1 -0
  64. package/.next/server/app/api/auth/providers/route.js +1 -0
  65. package/.next/server/app/api/auth/providers/route.js.nft.json +1 -0
  66. package/.next/server/app/api/auth/providers/route_client-reference-manifest.js +1 -0
  67. package/.next/server/app/api/auth/status/route.js +1 -0
  68. package/.next/server/app/api/auth/status/route.js.nft.json +1 -0
  69. package/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -0
  70. package/.next/server/app/api/default-cwd/route.js +1 -0
  71. package/.next/server/app/api/default-cwd/route.js.nft.json +1 -0
  72. package/.next/server/app/api/default-cwd/route_client-reference-manifest.js +1 -0
  73. package/.next/server/app/api/files/[...path]/route.js +4 -0
  74. package/.next/server/app/api/files/[...path]/route.js.nft.json +1 -0
  75. package/.next/server/app/api/files/[...path]/route_client-reference-manifest.js +1 -0
  76. package/.next/server/app/api/harness/route.js +1 -0
  77. package/.next/server/app/api/harness/route.js.nft.json +1 -0
  78. package/.next/server/app/api/harness/route_client-reference-manifest.js +1 -0
  79. package/.next/server/app/api/home/route.js +1 -0
  80. package/.next/server/app/api/home/route.js.nft.json +1 -0
  81. package/.next/server/app/api/home/route_client-reference-manifest.js +1 -0
  82. package/.next/server/app/api/internal/runtime/route.js +1 -0
  83. package/.next/server/app/api/internal/runtime/route.js.nft.json +1 -0
  84. package/.next/server/app/api/internal/runtime/route_client-reference-manifest.js +1 -0
  85. package/.next/server/app/api/models/route.js +1 -0
  86. package/.next/server/app/api/models/route.js.nft.json +1 -0
  87. package/.next/server/app/api/models/route_client-reference-manifest.js +1 -0
  88. package/.next/server/app/api/models-config/discover/route.js +1 -0
  89. package/.next/server/app/api/models-config/discover/route.js.nft.json +1 -0
  90. package/.next/server/app/api/models-config/discover/route_client-reference-manifest.js +1 -0
  91. package/.next/server/app/api/models-config/route.js +1 -0
  92. package/.next/server/app/api/models-config/route.js.nft.json +1 -0
  93. package/.next/server/app/api/models-config/route_client-reference-manifest.js +1 -0
  94. package/.next/server/app/api/models-config/test/route.js +1 -0
  95. package/.next/server/app/api/models-config/test/route.js.nft.json +1 -0
  96. package/.next/server/app/api/models-config/test/route_client-reference-manifest.js +1 -0
  97. package/.next/server/app/api/projects/browse/route.js +1 -0
  98. package/.next/server/app/api/projects/browse/route.js.nft.json +1 -0
  99. package/.next/server/app/api/projects/browse/route_client-reference-manifest.js +1 -0
  100. package/.next/server/app/api/projects/route.js +1 -0
  101. package/.next/server/app/api/projects/route.js.nft.json +1 -0
  102. package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -0
  103. package/.next/server/app/api/reports/[id]/route.js +10 -0
  104. package/.next/server/app/api/reports/[id]/route.js.nft.json +1 -0
  105. package/.next/server/app/api/reports/[id]/route_client-reference-manifest.js +1 -0
  106. package/.next/server/app/api/search/route.js +1 -0
  107. package/.next/server/app/api/search/route.js.nft.json +1 -0
  108. package/.next/server/app/api/search/route_client-reference-manifest.js +1 -0
  109. package/.next/server/app/api/sessions/[id]/context/route.js +1 -0
  110. package/.next/server/app/api/sessions/[id]/context/route.js.nft.json +1 -0
  111. package/.next/server/app/api/sessions/[id]/context/route_client-reference-manifest.js +1 -0
  112. package/.next/server/app/api/sessions/[id]/route.js +1 -0
  113. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -0
  114. package/.next/server/app/api/sessions/[id]/route_client-reference-manifest.js +1 -0
  115. package/.next/server/app/api/sessions/new/route.js +1 -0
  116. package/.next/server/app/api/sessions/new/route.js.nft.json +1 -0
  117. package/.next/server/app/api/sessions/new/route_client-reference-manifest.js +1 -0
  118. package/.next/server/app/api/sessions/route.js +1 -0
  119. package/.next/server/app/api/sessions/route.js.nft.json +1 -0
  120. package/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -0
  121. package/.next/server/app/api/settings/route.js +1 -0
  122. package/.next/server/app/api/settings/route.js.nft.json +1 -0
  123. package/.next/server/app/api/settings/route_client-reference-manifest.js +1 -0
  124. package/.next/server/app/api/skills/install/route.js +5 -0
  125. package/.next/server/app/api/skills/install/route.js.nft.json +1 -0
  126. package/.next/server/app/api/skills/install/route_client-reference-manifest.js +1 -0
  127. package/.next/server/app/api/skills/route.js +6 -0
  128. package/.next/server/app/api/skills/route.js.nft.json +1 -0
  129. package/.next/server/app/api/skills/route_client-reference-manifest.js +1 -0
  130. package/.next/server/app/api/skills/search/route.js +1 -0
  131. package/.next/server/app/api/skills/search/route.js.nft.json +1 -0
  132. package/.next/server/app/api/skills/search/route_client-reference-manifest.js +1 -0
  133. package/.next/server/app/api/soul/route.js +1 -0
  134. package/.next/server/app/api/soul/route.js.nft.json +1 -0
  135. package/.next/server/app/api/soul/route_client-reference-manifest.js +1 -0
  136. package/.next/server/app/api/version/route.js +1 -0
  137. package/.next/server/app/api/version/route.js.nft.json +1 -0
  138. package/.next/server/app/api/version/route_client-reference-manifest.js +1 -0
  139. package/.next/server/app/index.html +1 -0
  140. package/.next/server/app/index.meta +14 -0
  141. package/.next/server/app/index.rsc +17 -0
  142. package/.next/server/app/index.segments/__PAGE__.segment.rsc +6 -0
  143. package/.next/server/app/index.segments/_full.segment.rsc +17 -0
  144. package/.next/server/app/index.segments/_head.segment.rsc +6 -0
  145. package/.next/server/app/index.segments/_index.segment.rsc +5 -0
  146. package/.next/server/app/index.segments/_tree.segment.rsc +4 -0
  147. package/.next/server/app/login/page.js +2 -0
  148. package/.next/server/app/login/page.js.nft.json +1 -0
  149. package/.next/server/app/login/page_client-reference-manifest.js +1 -0
  150. package/.next/server/app/login.html +1 -0
  151. package/.next/server/app/login.meta +15 -0
  152. package/.next/server/app/login.rsc +22 -0
  153. package/.next/server/app/login.segments/_full.segment.rsc +22 -0
  154. package/.next/server/app/login.segments/_head.segment.rsc +6 -0
  155. package/.next/server/app/login.segments/_index.segment.rsc +5 -0
  156. package/.next/server/app/login.segments/_tree.segment.rsc +4 -0
  157. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +9 -0
  158. package/.next/server/app/login.segments/login.segment.rsc +5 -0
  159. package/.next/server/app/page.js +261 -0
  160. package/.next/server/app/page.js.nft.json +1 -0
  161. package/.next/server/app/page_client-reference-manifest.js +1 -0
  162. package/.next/server/app-paths-manifest.json +39 -0
  163. package/.next/server/chunks/1048.js +1 -0
  164. package/.next/server/chunks/1367.js +77 -0
  165. package/.next/server/chunks/1381.js +1 -0
  166. package/.next/server/chunks/165.js +1 -0
  167. package/.next/server/chunks/1681.js +215 -0
  168. package/.next/server/chunks/1688.js +45 -0
  169. package/.next/server/chunks/1703.js +79 -0
  170. package/.next/server/chunks/1712.js +43 -0
  171. package/.next/server/chunks/1813.js +1 -0
  172. package/.next/server/chunks/2325.js +80 -0
  173. package/.next/server/chunks/258.js +1 -0
  174. package/.next/server/chunks/2671.js +287 -0
  175. package/.next/server/chunks/2778.js +1 -0
  176. package/.next/server/chunks/2943.js +1 -0
  177. package/.next/server/chunks/3031.js +226 -0
  178. package/.next/server/chunks/3181.js +1 -0
  179. package/.next/server/chunks/3493.js +1 -0
  180. package/.next/server/chunks/3672.js +1 -0
  181. package/.next/server/chunks/3701.js +104 -0
  182. package/.next/server/chunks/4013.js +1 -0
  183. package/.next/server/chunks/402.js +2 -0
  184. package/.next/server/chunks/4035.js +80 -0
  185. package/.next/server/chunks/4248.js +153 -0
  186. package/.next/server/chunks/4367.js +1 -0
  187. package/.next/server/chunks/4406.js +141 -0
  188. package/.next/server/chunks/4741.js +18 -0
  189. package/.next/server/chunks/4768.js +1 -0
  190. package/.next/server/chunks/4858.js +148 -0
  191. package/.next/server/chunks/4980.js +1 -0
  192. package/.next/server/chunks/5155.js +5 -0
  193. package/.next/server/chunks/5293.js +166 -0
  194. package/.next/server/chunks/5399.js +8 -0
  195. package/.next/server/chunks/5409.js +1 -0
  196. package/.next/server/chunks/5797.js +93 -0
  197. package/.next/server/chunks/5851.js +36 -0
  198. package/.next/server/chunks/6206.js +1 -0
  199. package/.next/server/chunks/6296.js +1 -0
  200. package/.next/server/chunks/63.js +45 -0
  201. package/.next/server/chunks/6346.js +1 -0
  202. package/.next/server/chunks/6406.js +23 -0
  203. package/.next/server/chunks/642.js +1 -0
  204. package/.next/server/chunks/6429.js +50 -0
  205. package/.next/server/chunks/6729.js +64 -0
  206. package/.next/server/chunks/6907.js +115 -0
  207. package/.next/server/chunks/6980.js +1 -0
  208. package/.next/server/chunks/7073.js +24 -0
  209. package/.next/server/chunks/7233.js +24 -0
  210. package/.next/server/chunks/7307.js +1 -0
  211. package/.next/server/chunks/7362.js +9 -0
  212. package/.next/server/chunks/7567.js +29 -0
  213. package/.next/server/chunks/7765.js +1 -0
  214. package/.next/server/chunks/7890.js +1 -0
  215. package/.next/server/chunks/8065.js +1 -0
  216. package/.next/server/chunks/8238.js +34 -0
  217. package/.next/server/chunks/8276.js +1 -0
  218. package/.next/server/chunks/8336.js +1 -0
  219. package/.next/server/chunks/8477.js +3 -0
  220. package/.next/server/chunks/8490.js +1 -0
  221. package/.next/server/chunks/8916.js +1 -0
  222. package/.next/server/chunks/9280.js +252 -0
  223. package/.next/server/chunks/9315.js +1 -0
  224. package/.next/server/chunks/9537.js +90 -0
  225. package/.next/server/chunks/966.js +1 -0
  226. package/.next/server/chunks/9818.js +21 -0
  227. package/.next/server/chunks/static/media/pdf.worker.min.c476e1a0.mjs +6 -0
  228. package/.next/server/functions-config-manifest.json +16 -0
  229. package/.next/server/interception-route-rewrite-manifest.js +1 -0
  230. package/.next/server/middleware-build-manifest.js +1 -0
  231. package/.next/server/middleware-manifest.json +6 -0
  232. package/.next/server/middleware-react-loadable-manifest.js +1 -0
  233. package/.next/server/middleware.js +18 -0
  234. package/.next/server/middleware.js.nft.json +1 -0
  235. package/.next/server/next-font-manifest.js +1 -0
  236. package/.next/server/next-font-manifest.json +1 -0
  237. package/.next/server/pages/404.html +1 -0
  238. package/.next/server/pages/500.html +1 -0
  239. package/.next/server/pages-manifest.json +4 -0
  240. package/.next/server/prefetch-hints.json +1 -0
  241. package/.next/server/server-reference-manifest.js +1 -0
  242. package/.next/server/server-reference-manifest.json +1 -0
  243. package/.next/server/webpack-runtime.js +1 -0
  244. package/.next/static/chunks/0b9a0da7.9075af772487e743.js +62 -0
  245. package/.next/static/chunks/1413.922d232de90c0c41.js +115 -0
  246. package/.next/static/chunks/1643.467a526a1f24f54d.js +24 -0
  247. package/.next/static/chunks/1852.5543122f11aa7fed.js +1 -0
  248. package/.next/static/chunks/1960.b1e26436d7a5f586.js +1 -0
  249. package/.next/static/chunks/2170a4aa.4213bb2183c9cdf9.js +1 -0
  250. package/.next/static/chunks/2274.6cd173f80a1405a2.js +21 -0
  251. package/.next/static/chunks/2419.347fdfe3c170854d.js +166 -0
  252. package/.next/static/chunks/2619.9aac8983f30c7c8a.js +1 -0
  253. package/.next/static/chunks/2623.d20fabd8e18197c6.js +287 -0
  254. package/.next/static/chunks/2729.f5365061a849d659.js +34 -0
  255. package/.next/static/chunks/2821.934bcf60fbdc28c6.js +1 -0
  256. package/.next/static/chunks/2918becc.abff2ece1de37bc1.js +153 -0
  257. package/.next/static/chunks/2947.114e51cb06d1c01a.js +23 -0
  258. package/.next/static/chunks/3079.4c511fa1144e3adf.js +79 -0
  259. package/.next/static/chunks/3274.208ca44844cd7d95.js +148 -0
  260. package/.next/static/chunks/3308.465a94263d04bfea.js +73 -0
  261. package/.next/static/chunks/3325.e4bfe1ca657f3b5b.js +80 -0
  262. package/.next/static/chunks/3506.2a7eaa08b9f55337.js +90 -0
  263. package/.next/static/chunks/363642f4-043c1475ab9af70e.js +1 -0
  264. package/.next/static/chunks/3794-123fdf632563f469.js +32 -0
  265. package/.next/static/chunks/3837.a755ccfe6f9c1c1c.js +5 -0
  266. package/.next/static/chunks/394.91597771688df6d0.js +1 -0
  267. package/.next/static/chunks/3997.1009c06025691712.js +1 -0
  268. package/.next/static/chunks/4453.91a357dc43c21745.js +1 -0
  269. package/.next/static/chunks/4491.44fdf20580ac72bd.js +24 -0
  270. package/.next/static/chunks/4829.cf1d50e43e6d9db5.js +1 -0
  271. package/.next/static/chunks/498.fe1d9da9ecad6c36.js +1 -0
  272. package/.next/static/chunks/4bd1b696-e356ca5ba0218e27.js +1 -0
  273. package/.next/static/chunks/5019.b5a1a2b8daf17525.js +1 -0
  274. package/.next/static/chunks/5034.8f16c3fa3ce75411.js +1 -0
  275. package/.next/static/chunks/5074.d16651da01ec4e02.js +1 -0
  276. package/.next/static/chunks/51fb665c.0950e1b79671348d.js +45 -0
  277. package/.next/static/chunks/532.5956ed631aff722b.js +9 -0
  278. package/.next/static/chunks/5326.69460442bdcd6cd3.js +1 -0
  279. package/.next/static/chunks/5403.ff110bf5bf600758.js +64 -0
  280. package/.next/static/chunks/547.902a733488cfe3f7.js +77 -0
  281. package/.next/static/chunks/5567.540d7fc108ad6ee5.js +215 -0
  282. package/.next/static/chunks/5590.ef62922166d308b4.js +1 -0
  283. package/.next/static/chunks/5690.9d6eb1edb1399995.js +1 -0
  284. package/.next/static/chunks/5749.25faee4a1e55b854.js +226 -0
  285. package/.next/static/chunks/58bb9007.1ccb6bba34b4c635.js +80 -0
  286. package/.next/static/chunks/6121.f3f43f1896ea0cd9.js +1 -0
  287. package/.next/static/chunks/6600.583c88eef37aa524.js +1 -0
  288. package/.next/static/chunks/6696.a41aec266e657d54.js +141 -0
  289. package/.next/static/chunks/6922.42148793782d2fe7.js +1 -0
  290. package/.next/static/chunks/7006.e191611ffc2b9528.js +43 -0
  291. package/.next/static/chunks/7343.9fbb58204d8ac681.js +1 -0
  292. package/.next/static/chunks/73972abe.25a4cffa03b2bcef.js +119 -0
  293. package/.next/static/chunks/7547.58bda8a2aabba0d4.js +93 -0
  294. package/.next/static/chunks/7648.4ae2f183b4db0353.js +1 -0
  295. package/.next/static/chunks/7874.8db6929b94cdf697.js +1 -0
  296. package/.next/static/chunks/7959.1f20a35df316216a.js +104 -0
  297. package/.next/static/chunks/83.85d62d7fc9850b75.js +29 -0
  298. package/.next/static/chunks/8436.cab94b59cca0a8ff.js +1 -0
  299. package/.next/static/chunks/8451.ff6ff72b57dc52e1.js +1 -0
  300. package/.next/static/chunks/8489.45f22859734f514f.js +36 -0
  301. package/.next/static/chunks/8568.f85d8b36fc9a9037.js +1 -0
  302. package/.next/static/chunks/8771-3e14b6810486df1f.js +1 -0
  303. package/.next/static/chunks/8863.be51033a67436277.js +1 -0
  304. package/.next/static/chunks/90542734.dc1a2723e4f6affb.js +1 -0
  305. package/.next/static/chunks/9500.1488aec06ee78127.js +1 -0
  306. package/.next/static/chunks/9633.155548b5fca6e580.js +1 -0
  307. package/.next/static/chunks/9779.673004a62d70e36a.js +1 -0
  308. package/.next/static/chunks/app/_global-error/page-cc518af6b1ffb191.js +1 -0
  309. package/.next/static/chunks/app/_not-found/page-c72daab99269beff.js +1 -0
  310. package/.next/static/chunks/app/api/agent/[id]/events/route-cc518af6b1ffb191.js +1 -0
  311. package/.next/static/chunks/app/api/agent/[id]/route-cc518af6b1ffb191.js +1 -0
  312. package/.next/static/chunks/app/api/agent/new/route-cc518af6b1ffb191.js +1 -0
  313. package/.next/static/chunks/app/api/auth/all-providers/route-cc518af6b1ffb191.js +1 -0
  314. package/.next/static/chunks/app/api/auth/api-key/[provider]/route-cc518af6b1ffb191.js +1 -0
  315. package/.next/static/chunks/app/api/auth/login/[provider]/route-cc518af6b1ffb191.js +1 -0
  316. package/.next/static/chunks/app/api/auth/login/route-cc518af6b1ffb191.js +1 -0
  317. package/.next/static/chunks/app/api/auth/logout/[provider]/route-cc518af6b1ffb191.js +1 -0
  318. package/.next/static/chunks/app/api/auth/providers/route-cc518af6b1ffb191.js +1 -0
  319. package/.next/static/chunks/app/api/auth/status/route-cc518af6b1ffb191.js +1 -0
  320. package/.next/static/chunks/app/api/default-cwd/route-cc518af6b1ffb191.js +1 -0
  321. package/.next/static/chunks/app/api/files/[...path]/route-cc518af6b1ffb191.js +1 -0
  322. package/.next/static/chunks/app/api/harness/route-cc518af6b1ffb191.js +1 -0
  323. package/.next/static/chunks/app/api/home/route-cc518af6b1ffb191.js +1 -0
  324. package/.next/static/chunks/app/api/internal/runtime/route-cc518af6b1ffb191.js +1 -0
  325. package/.next/static/chunks/app/api/models/route-cc518af6b1ffb191.js +1 -0
  326. package/.next/static/chunks/app/api/models-config/discover/route-cc518af6b1ffb191.js +1 -0
  327. package/.next/static/chunks/app/api/models-config/route-cc518af6b1ffb191.js +1 -0
  328. package/.next/static/chunks/app/api/models-config/test/route-cc518af6b1ffb191.js +1 -0
  329. package/.next/static/chunks/app/api/projects/browse/route-cc518af6b1ffb191.js +1 -0
  330. package/.next/static/chunks/app/api/projects/route-cc518af6b1ffb191.js +1 -0
  331. package/.next/static/chunks/app/api/reports/[id]/route-cc518af6b1ffb191.js +1 -0
  332. package/.next/static/chunks/app/api/search/route-cc518af6b1ffb191.js +1 -0
  333. package/.next/static/chunks/app/api/sessions/[id]/context/route-cc518af6b1ffb191.js +1 -0
  334. package/.next/static/chunks/app/api/sessions/[id]/route-cc518af6b1ffb191.js +1 -0
  335. package/.next/static/chunks/app/api/sessions/new/route-cc518af6b1ffb191.js +1 -0
  336. package/.next/static/chunks/app/api/sessions/route-cc518af6b1ffb191.js +1 -0
  337. package/.next/static/chunks/app/api/settings/route-cc518af6b1ffb191.js +1 -0
  338. package/.next/static/chunks/app/api/skills/install/route-cc518af6b1ffb191.js +1 -0
  339. package/.next/static/chunks/app/api/skills/route-cc518af6b1ffb191.js +1 -0
  340. package/.next/static/chunks/app/api/skills/search/route-cc518af6b1ffb191.js +1 -0
  341. package/.next/static/chunks/app/api/soul/route-cc518af6b1ffb191.js +1 -0
  342. package/.next/static/chunks/app/api/version/route-cc518af6b1ffb191.js +1 -0
  343. package/.next/static/chunks/app/layout-be148b7ae915b22a.js +1 -0
  344. package/.next/static/chunks/app/login/page-ebf0e6de99062783.js +1 -0
  345. package/.next/static/chunks/app/page-0594cb7a3cbb0211.js +260 -0
  346. package/.next/static/chunks/d3ac728e.7964f816a1ca64e5.js +1 -0
  347. package/.next/static/chunks/framework-711ef29bc66f648c.js +1 -0
  348. package/.next/static/chunks/main-app-45a0f19af99d61b6.js +1 -0
  349. package/.next/static/chunks/main-f74964b7ae52493e.js +5 -0
  350. package/.next/static/chunks/next/dist/client/components/builtin/app-error-cc518af6b1ffb191.js +1 -0
  351. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-cc518af6b1ffb191.js +1 -0
  352. package/.next/static/chunks/next/dist/client/components/builtin/global-error-9bfa08b9491621f2.js +1 -0
  353. package/.next/static/chunks/next/dist/client/components/builtin/not-found-cc518af6b1ffb191.js +1 -0
  354. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-cc518af6b1ffb191.js +1 -0
  355. package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  356. package/.next/static/chunks/webpack-fcf4a889ecbd753c.js +1 -0
  357. package/.next/static/css/45029451a1d7255d.css +3 -0
  358. package/.next/static/media/15605e25b523335c-s.woff2 +0 -0
  359. package/.next/static/media/1a3dce5cfb5f7760-s.woff2 +0 -0
  360. package/.next/static/media/1cdd02902f937a18-s.woff2 +0 -0
  361. package/.next/static/media/4c4b3b30b6bcb2be-s.woff2 +0 -0
  362. package/.next/static/media/641a7b8a5800ee0e-s.woff2 +0 -0
  363. package/.next/static/media/7deddc85b7ffd1dc-s.p.woff2 +0 -0
  364. package/.next/static/media/ec14413c594b3356-s.p.woff2 +0 -0
  365. package/.next/static/media/pdf.worker.min.29aaf158.mjs +6 -0
  366. package/.next/static/nmfQlwnkhzbPmuiCmhW-_/_buildManifest.js +1 -0
  367. package/.next/static/nmfQlwnkhzbPmuiCmhW-_/_ssgManifest.js +1 -0
  368. package/package.json +21 -21
  369. package/app/api/agent/[id]/events/route.ts +0 -94
  370. package/app/api/agent/[id]/route.ts +0 -83
  371. package/app/api/agent/new/route.ts +0 -53
  372. package/app/api/auth/all-providers/route.ts +0 -21
  373. package/app/api/auth/api-key/[provider]/route.ts +0 -7
  374. package/app/api/auth/login/[provider]/route.ts +0 -7
  375. package/app/api/auth/login/route.ts +0 -22
  376. package/app/api/auth/logout/[provider]/route.ts +0 -7
  377. package/app/api/auth/providers/route.ts +0 -15
  378. package/app/api/auth/status/route.ts +0 -6
  379. package/app/api/default-cwd/route.ts +0 -22
  380. package/app/api/files/[...path]/route.ts +0 -621
  381. package/app/api/harness/route.ts +0 -47
  382. package/app/api/home/route.ts +0 -6
  383. package/app/api/internal/runtime/route.ts +0 -26
  384. package/app/api/models/route.ts +0 -67
  385. package/app/api/models-config/discover/route.ts +0 -42
  386. package/app/api/models-config/route.ts +0 -152
  387. package/app/api/models-config/test/route.ts +0 -154
  388. package/app/api/projects/browse/route.ts +0 -51
  389. package/app/api/projects/route.ts +0 -83
  390. package/app/api/reports/[id]/route.ts +0 -108
  391. package/app/api/search/route.ts +0 -122
  392. package/app/api/sessions/[id]/context/route.ts +0 -23
  393. package/app/api/sessions/[id]/route.ts +0 -124
  394. package/app/api/sessions/new/route.ts +0 -5
  395. package/app/api/sessions/route.ts +0 -16
  396. package/app/api/settings/route.ts +0 -51
  397. package/app/api/skills/install/route.ts +0 -249
  398. package/app/api/skills/route.ts +0 -161
  399. package/app/api/skills/search/route.ts +0 -121
  400. package/app/api/soul/route.ts +0 -47
  401. package/app/api/version/route.ts +0 -55
  402. package/app/globals.css +0 -736
  403. package/app/layout.tsx +0 -40
  404. package/app/login/page.tsx +0 -133
  405. package/app/page.tsx +0 -10
  406. package/components/AppShell.tsx +0 -1058
  407. package/components/ChatInput.tsx +0 -1103
  408. package/components/ChatMinimap.tsx +0 -381
  409. package/components/ChatWindow.tsx +0 -576
  410. package/components/CodeMirrorEditor.tsx +0 -137
  411. package/components/ConversationSearch.tsx +0 -369
  412. package/components/DataTableViewer.tsx +0 -248
  413. package/components/FileExplorer.tsx +0 -758
  414. package/components/FileIcons.tsx +0 -241
  415. package/components/FileViewer.tsx +0 -1273
  416. package/components/GlobalFileEditor.tsx +0 -98
  417. package/components/MarkdownRenderer.tsx +0 -331
  418. package/components/MermaidDiagram.tsx +0 -80
  419. package/components/MessageView.tsx +0 -1141
  420. package/components/ModelsConfig.tsx +0 -1991
  421. package/components/ProjectContext.tsx +0 -252
  422. package/components/ProjectFolderPicker.tsx +0 -202
  423. package/components/ProjectsConfig.tsx +0 -288
  424. package/components/ProviderIcons.tsx +0 -91
  425. package/components/ReportPanel.tsx +0 -237
  426. package/components/ResizeHandle.tsx +0 -105
  427. package/components/SessionSidebar.tsx +0 -1464
  428. package/components/SettingsDialog.tsx +0 -287
  429. package/components/SkillsConfig.tsx +0 -1093
  430. package/components/SubagentPanel.tsx +0 -191
  431. package/components/TabBar.tsx +0 -115
  432. package/components/ToolPanel.tsx +0 -131
  433. package/components/WidgetRenderer.tsx +0 -505
  434. package/components/viewers/DocumentToolbar.tsx +0 -78
  435. package/components/viewers/DocxViewer.tsx +0 -97
  436. package/components/viewers/PdfViewer.tsx +0 -206
  437. package/components/viewers/PptxViewer.tsx +0 -240
  438. package/components/viewers/XlsxViewer.tsx +0 -143
  439. package/hooks/useAgentSession.ts +0 -710
  440. package/hooks/useAudio.ts +0 -50
  441. package/hooks/useDragDrop.ts +0 -52
  442. package/hooks/useResizable.ts +0 -60
  443. package/hooks/useTheme.ts +0 -85
  444. package/lib/agent-client.ts +0 -39
  445. package/lib/annodex-config.ts +0 -556
  446. package/lib/auth-token.ts +0 -74
  447. package/lib/auth.ts +0 -90
  448. package/lib/brand.ts +0 -5
  449. package/lib/code-theme.ts +0 -32
  450. package/lib/codex-compat-proxy.ts +0 -1603
  451. package/lib/codex-home.ts +0 -6
  452. package/lib/codex-server.ts +0 -796
  453. package/lib/codex-session.ts +0 -590
  454. package/lib/codex-usage.ts +0 -213
  455. package/lib/file-paths.ts +0 -34
  456. package/lib/model-discovery.ts +0 -379
  457. package/lib/normalize.ts +0 -30
  458. package/lib/npx.ts +0 -87
  459. package/lib/pi-types.ts +0 -49
  460. package/lib/projects.ts +0 -269
  461. package/lib/provider-api.ts +0 -88
  462. package/lib/report-prompt.ts +0 -61
  463. package/lib/report-store.ts +0 -597
  464. package/lib/report-update-parser.ts +0 -66
  465. package/lib/rpc-manager.ts +0 -668
  466. package/lib/runtime-state.ts +0 -117
  467. package/lib/session-reader.ts +0 -903
  468. package/lib/session-runtime.ts +0 -105
  469. package/lib/subagent-progress.ts +0 -279
  470. package/lib/types.ts +0 -241
  471. package/lib/widget-export.ts +0 -318
  472. package/lib/widget-guidelines.ts +0 -288
  473. package/lib/widget-prompt.ts +0 -76
  474. package/lib/widget-utils.ts +0 -523
  475. package/postcss.config.mjs +0 -8
  476. package/proxy.ts +0 -64
  477. package/scripts/postinstall.cjs +0 -25
  478. package/tsconfig.json +0 -41
@@ -1,1991 +0,0 @@
1
- "use client";
2
-
3
- import { useState, useEffect, useCallback, useRef } from "react";
4
- import { inferProviderApi, isProviderApi, type ProviderApi } from "@/lib/provider-api";
5
- // Color icons (have their own fill colors — no background needed)
6
- import {
7
- AnthropicIcon, OpenAIIcon, GoogleIcon, DeepSeekIcon, GroqIcon, MistralIcon,
8
- MoonshotIcon, MinimaxIcon, FireworksIcon, HuggingFaceIcon, CerebrasIcon,
9
- OpenRouterIcon, XAIIcon, CloudflareIcon, VercelIcon, GithubCopilotIcon,
10
- AwsIcon, AzureIcon, KimiIcon, QwenIcon, ZhipuIcon, CohereIcon,
11
- PerplexityIcon, TogetherIcon, GrokIcon,
12
- } from "@/components/ProviderIcons";
13
-
14
- type IconComponent = React.ComponentType<{ size?: number | string; style?: React.CSSProperties }>;
15
-
16
- // hasColor=true → Color icon (self-colored SVG, no wrapper)
17
- // hasColor=false → Mono icon (rendered with currentColor, inherits theme text color)
18
- const PROVIDER_ICONS: Record<string, { Icon: IconComponent; hasColor: boolean }> = {
19
- "anthropic": { Icon: AnthropicIcon, hasColor: false },
20
- "openai": { Icon: OpenAIIcon, hasColor: false },
21
- "openai-codex": { Icon: OpenAIIcon, hasColor: false },
22
- "google": { Icon: GoogleIcon, hasColor: true },
23
- "google-vertex": { Icon: GoogleIcon, hasColor: true },
24
- "deepseek": { Icon: DeepSeekIcon, hasColor: true },
25
- "groq": { Icon: GroqIcon, hasColor: false },
26
- "mistral": { Icon: MistralIcon, hasColor: true },
27
- "moonshotai": { Icon: MoonshotIcon, hasColor: false },
28
- "moonshotai-cn": { Icon: MoonshotIcon, hasColor: false },
29
- "moonshot": { Icon: MoonshotIcon, hasColor: false },
30
- "minimax": { Icon: MinimaxIcon, hasColor: true },
31
- "minimax-cn": { Icon: MinimaxIcon, hasColor: true },
32
- "fireworks": { Icon: FireworksIcon, hasColor: true },
33
- "huggingface": { Icon: HuggingFaceIcon, hasColor: true },
34
- "cerebras": { Icon: CerebrasIcon, hasColor: true },
35
- "openrouter": { Icon: OpenRouterIcon, hasColor: false },
36
- "xai": { Icon: XAIIcon, hasColor: false },
37
- "cloudflare-ai-gateway": { Icon: CloudflareIcon, hasColor: true },
38
- "cloudflare-workers-ai": { Icon: CloudflareIcon, hasColor: true },
39
- "vercel-ai-gateway": { Icon: VercelIcon, hasColor: false },
40
- "github-copilot": { Icon: GithubCopilotIcon, hasColor: false },
41
- "amazon-bedrock": { Icon: AwsIcon, hasColor: true },
42
- "azure-openai-responses": { Icon: AzureIcon, hasColor: true },
43
- "kimi-coding": { Icon: KimiIcon, hasColor: true },
44
- "qwen": { Icon: QwenIcon, hasColor: true },
45
- "zai": { Icon: ZhipuIcon, hasColor: true },
46
- "cohere": { Icon: CohereIcon, hasColor: true },
47
- "perplexity": { Icon: PerplexityIcon, hasColor: true },
48
- "together": { Icon: TogetherIcon, hasColor: true },
49
- "grok": { Icon: GrokIcon, hasColor: false },
50
- };
51
-
52
- // ── Types ─────────────────────────────────────────────────────────────────────
53
-
54
- interface OAuthProvider {
55
- id: string;
56
- name: string;
57
- usesCallbackServer: boolean;
58
- loggedIn: boolean;
59
- }
60
-
61
- interface ApiKeyProvider {
62
- id: string;
63
- displayName: string;
64
- configured: boolean;
65
- source?: string;
66
- modelCount: number;
67
- }
68
-
69
- type OAuthLoginState =
70
- | { phase: "idle" }
71
- | { phase: "connecting" }
72
- | { phase: "auth"; url: string; instructions: string | null; token: string }
73
- | { phase: "device_code"; userCode: string; verificationUri: string; intervalSeconds: number | null; expiresInSeconds: number | null }
74
- | { phase: "prompt"; message: string; placeholder: string | null; token: string }
75
- | { phase: "select"; message: string; options: { id: string; label: string }[]; token: string }
76
- | { phase: "progress"; message: string }
77
- | { phase: "success" }
78
- | { phase: "error"; message: string };
79
-
80
- interface ModelEntry {
81
- id: string;
82
- name?: string;
83
- api?: ProviderApi;
84
- reasoning?: boolean;
85
- thinkingLevelMap?: Record<string, string | null>;
86
- input?: string[];
87
- contextWindow?: number;
88
- maxTokens?: number;
89
- cost?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number };
90
- compat?: Record<string, unknown>;
91
- }
92
-
93
- interface DiscoveredModel {
94
- id: string;
95
- name: string;
96
- ownedBy: string;
97
- input?: string[];
98
- contextWindow?: number;
99
- maxTokens?: number;
100
- }
101
-
102
- interface ProviderEntry {
103
- baseUrl?: string;
104
- api?: ProviderApi;
105
- apiKey?: string;
106
- requiresOpenAiAuth?: boolean;
107
- source?: string;
108
- sourceProviderId?: string;
109
- headers?: Record<string, string>;
110
- compat?: Record<string, unknown>;
111
- models?: ModelEntry[];
112
- modelOverrides?: Record<string, unknown>;
113
- }
114
-
115
- interface ModelsJson {
116
- providers?: Record<string, ProviderEntry>;
117
- defaultProvider?: string;
118
- defaultModel?: string;
119
- }
120
-
121
- type ModelTestState =
122
- | { phase: "idle" }
123
- | { phase: "testing" }
124
- | { phase: "success"; latencyMs?: number; status?: number; responseText?: string }
125
- | { phase: "error"; message: string; latencyMs?: number; status?: number };
126
-
127
- type Selection =
128
- | { type: "provider"; name: string }
129
- | { type: "model"; providerName: string; index: number }
130
- | { type: "oauth"; providerId: string }
131
- | { type: "apikey"; providerId: string };
132
-
133
- type DiscoverState =
134
- | { phase: "idle" }
135
- | { phase: "loading" }
136
- | { phase: "success"; endpoint: string; models: DiscoveredModel[]; piModels: ModelEntry[] }
137
- | { phase: "error"; message: string };
138
-
139
- const API_OPTIONS: Array<{ value: ProviderApi; label: string; description: string }> = [
140
- {
141
- value: "openai-responses",
142
- label: "OpenAI Responses",
143
- description: "OpenAI / deeprouter GPT relay",
144
- },
145
- {
146
- value: "openai-completions",
147
- label: "Chat Completions",
148
- description: "DeepSeek / Kimi / Qwen via compat proxy",
149
- },
150
- ];
151
-
152
- // ── Form field helpers ────────────────────────────────────────────────────────
153
-
154
- function Field({ label, children }: { label: string; children: React.ReactNode }) {
155
- return (
156
- <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
157
- <label style={{ fontSize: 11, color: "var(--text-muted)", fontWeight: 500 }}>{label}</label>
158
- {children}
159
- </div>
160
- );
161
- }
162
-
163
- const inputStyle = {
164
- padding: "6px 9px",
165
- background: "var(--bg-panel)",
166
- border: "1px solid var(--border)",
167
- borderRadius: 5,
168
- color: "var(--text)",
169
- fontSize: 12,
170
- outline: "none",
171
- width: "100%",
172
- boxSizing: "border-box" as const,
173
- };
174
-
175
- function TextInput({
176
- value,
177
- onChange,
178
- placeholder,
179
- mono,
180
- onKeyDown,
181
- style,
182
- }: {
183
- value: string;
184
- onChange: (v: string) => void;
185
- placeholder?: string;
186
- mono?: boolean;
187
- onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
188
- style?: React.CSSProperties;
189
- }) {
190
- return <input value={value} onChange={(e) => onChange(e.target.value)} onKeyDown={onKeyDown} placeholder={placeholder}
191
- style={{ ...inputStyle, fontFamily: mono ? "var(--font-mono)" : "inherit", ...style }} />;
192
- }
193
-
194
- function SecretTextInput({
195
- value,
196
- onChange,
197
- placeholder,
198
- mono,
199
- onKeyDown,
200
- autoComplete = "off",
201
- spellCheck = false,
202
- style,
203
- }: {
204
- value: string;
205
- onChange: (v: string) => void;
206
- placeholder?: string;
207
- mono?: boolean;
208
- onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
209
- autoComplete?: string;
210
- spellCheck?: boolean;
211
- style?: React.CSSProperties;
212
- }) {
213
- const [visible, setVisible] = useState(false);
214
-
215
- useEffect(() => {
216
- if (!value) setVisible(false);
217
- }, [value]);
218
-
219
- return (
220
- <div style={{ position: "relative", width: "100%", ...style }}>
221
- <input
222
- type={visible ? "text" : "password"}
223
- value={value}
224
- onChange={(e) => onChange(e.target.value)}
225
- onKeyDown={onKeyDown}
226
- placeholder={placeholder}
227
- style={{ ...inputStyle, paddingRight: 34, fontFamily: mono ? "var(--font-mono)" : "inherit" }}
228
- autoComplete={autoComplete}
229
- spellCheck={spellCheck}
230
- />
231
- <button
232
- type="button"
233
- onClick={() => setVisible((v) => !v)}
234
- aria-label={visible ? "Hide API key" : "Show API key"}
235
- title={visible ? "Hide API key" : "Show API key"}
236
- style={{
237
- position: "absolute",
238
- right: 5,
239
- top: "50%",
240
- transform: "translateY(-50%)",
241
- width: 24,
242
- height: 24,
243
- padding: 0,
244
- border: "none",
245
- background: "transparent",
246
- color: "var(--text-dim)",
247
- cursor: "pointer",
248
- display: "flex",
249
- alignItems: "center",
250
- justifyContent: "center",
251
- }}
252
- >
253
- {visible ? (
254
- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
255
- <path d="M17.94 17.94A10.94 10.94 0 0 1 12 20C7 20 2.73 16.89 1 12a18.45 18.45 0 0 1 5.06-6.94" />
256
- <path d="M9.9 4.24A10.94 10.94 0 0 1 12 4c5 0 9.27 3.11 11 8a18.5 18.5 0 0 1-2.16 3.19" />
257
- <path d="M14.12 14.12A3 3 0 0 1 9.88 9.88" />
258
- <path d="M1 1l22 22" />
259
- </svg>
260
- ) : (
261
- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
262
- <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12Z" />
263
- <circle cx="12" cy="12" r="3" />
264
- </svg>
265
- )}
266
- </button>
267
- </div>
268
- );
269
- }
270
-
271
- function NumInput({ value, onChange, placeholder }: { value: string; onChange: (v: string) => void; placeholder?: string }) {
272
- return <input type="number" value={value} onChange={(e) => onChange(e.target.value)} placeholder={placeholder} style={inputStyle} />;
273
- }
274
-
275
- function ApiSelect({ value, onChange, required }: { value: string; onChange: (v: ProviderApi | "") => void; required?: boolean }) {
276
- return (
277
- <select value={value} onChange={(e) => {
278
- const next = e.target.value;
279
- onChange(isProviderApi(next) ? next : "");
280
- }}
281
- style={{ ...inputStyle, color: value ? "var(--text)" : "var(--text-dim)" }}>
282
- {!required && <option value="">— inherit / none —</option>}
283
- {API_OPTIONS.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
284
- </select>
285
- );
286
- }
287
-
288
- function ProtocolHint({ api }: { api?: ProviderApi | "" }) {
289
- const option = API_OPTIONS.find((item) => item.value === api);
290
- return (
291
- <span style={{ fontSize: 10, color: "var(--text-dim)", marginTop: 2 }}>
292
- {option ? option.description : "Leave empty to inherit the provider protocol."}
293
- </span>
294
- );
295
- }
296
-
297
- function Check({ label, checked, onChange }: { label: string; checked: boolean; onChange: (v: boolean) => void }) {
298
- return (
299
- <label style={{ display: "flex", alignItems: "center", gap: 6, cursor: "pointer", fontSize: 12, color: "var(--text-muted)" }}>
300
- <input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)}
301
- style={{ width: 13, height: 13, accentColor: "var(--accent)", cursor: "pointer" }} />
302
- {label}
303
- </label>
304
- );
305
- }
306
-
307
- function SectionTitle({ children }: { children: React.ReactNode }) {
308
- return <div style={{ fontSize: 11, fontWeight: 600, color: "var(--text-dim)", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 2 }}>{children}</div>;
309
- }
310
-
311
- // ── Provider detail ───────────────────────────────────────────────────────────
312
-
313
- function ProviderDetail({ name, provider, onChange, onRename, onDelete }: {
314
- name: string; provider: ProviderEntry;
315
- onChange: (p: ProviderEntry) => void; onRename: (n: string) => void; onDelete: () => void;
316
- }) {
317
- const [editingName, setEditingName] = useState(name);
318
- const [quickModelId, setQuickModelId] = useState("");
319
- const readOnly = provider.source === "codex";
320
- useEffect(() => setEditingName(name), [name]);
321
- const set = <K extends keyof ProviderEntry>(k: K, v: ProviderEntry[K]) => onChange({ ...provider, [k]: v });
322
- const addQuickModel = () => {
323
- if (readOnly) return;
324
- const id = quickModelId.trim();
325
- if (!id) return;
326
- const models = provider.models ?? [];
327
- if (models.some((model) => model.id === id)) {
328
- setQuickModelId("");
329
- return;
330
- }
331
- onChange({
332
- ...provider,
333
- models: [...models, { id, name: id }],
334
- });
335
- setQuickModelId("");
336
- };
337
-
338
- return (
339
- <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
340
- <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
341
- <SectionTitle>Provider</SectionTitle>
342
- <button onClick={() => !readOnly && onDelete()} disabled={readOnly}
343
- style={{ padding: "3px 8px", background: "none", border: "1px solid rgba(239,68,68,0.3)", borderRadius: 4, color: "#ef4444", cursor: readOnly ? "not-allowed" : "pointer", fontSize: 11, opacity: readOnly ? 0.45 : 1 }}>
344
- Delete
345
- </button>
346
- </div>
347
-
348
- {readOnly && (
349
- <div style={{ padding: "7px 10px", borderRadius: 6, border: "1px solid var(--border)", background: "var(--bg-panel)", color: "var(--text-dim)", fontSize: 12 }}>
350
- Loaded from ~/.codex/config.toml and auth.json. Add a UI provider to override it.
351
- </div>
352
- )}
353
-
354
- <Field label="Provider name">
355
- <TextInput value={editingName} onChange={(v) => !readOnly && setEditingName(v)} placeholder="provider-name" mono />
356
- {!readOnly && editingName !== name && editingName.trim() && (
357
- <button onClick={() => onRename(editingName.trim())}
358
- style={{ marginTop: 4, padding: "3px 10px", background: "var(--accent)", border: "none", borderRadius: 4, color: "#fff", cursor: "pointer", fontSize: 11, alignSelf: "flex-start" }}>
359
- Rename
360
- </button>
361
- )}
362
- </Field>
363
-
364
- <Field label="Base URL">
365
- <TextInput value={provider.baseUrl ?? ""} onChange={(v) => !readOnly && set("baseUrl", v || undefined)}
366
- placeholder="https://api.example.com/v1" mono />
367
- </Field>
368
-
369
- <Field label="API Key">
370
- <SecretTextInput value={provider.apiKey ?? ""} onChange={(v) => !readOnly && set("apiKey", v || undefined)}
371
- placeholder="ENV_VAR_NAME, !shell-command, or literal key" mono />
372
- <span style={{ fontSize: 10, color: "var(--text-dim)", marginTop: 2 }}>
373
- Prefix with <code style={{ fontFamily: "var(--font-mono)" }}>!</code> to run a shell command, or use an env var name
374
- </span>
375
- </Field>
376
-
377
- <Field label="Protocol">
378
- <ApiSelect value={provider.api ?? inferProviderApi(provider.baseUrl, name)} onChange={(v) => !readOnly && set("api", v || undefined)} required />
379
- <ProtocolHint api={provider.api ?? inferProviderApi(provider.baseUrl, name)} />
380
- </Field>
381
-
382
- <Field label="Model ID">
383
- <div style={{ display: "flex", gap: 6 }}>
384
- <TextInput
385
- value={quickModelId}
386
- onChange={(v) => !readOnly && setQuickModelId(v)}
387
- onKeyDown={(event) => {
388
- if (event.key === "Enter" && !readOnly) addQuickModel();
389
- }}
390
- placeholder="gpt-5.5"
391
- mono
392
- style={{ flex: 1 }}
393
- />
394
- <button
395
- type="button"
396
- onClick={addQuickModel}
397
- disabled={readOnly || !quickModelId.trim()}
398
- style={{
399
- padding: "0 12px",
400
- border: "none",
401
- borderRadius: 5,
402
- background: !readOnly && quickModelId.trim() ? "var(--accent)" : "var(--bg-panel)",
403
- color: !readOnly && quickModelId.trim() ? "#fff" : "var(--text-dim)",
404
- cursor: !readOnly && quickModelId.trim() ? "pointer" : "not-allowed",
405
- fontSize: 12,
406
- fontWeight: 600,
407
- flexShrink: 0,
408
- }}
409
- >
410
- Add
411
- </button>
412
- </div>
413
- <span style={{ fontSize: 10, color: "var(--text-dim)", marginTop: 2 }}>
414
- Fetching the model list is optional. Add the model ID your provider supports, then save.
415
- </span>
416
- </Field>
417
- </div>
418
- );
419
- }
420
-
421
- // ── ThinkingLevelMap editor ───────────────────────────────────────────────────
422
-
423
- const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
424
- type ThinkingLevel = typeof THINKING_LEVELS[number];
425
-
426
- const LEVEL_COLORS: Record<ThinkingLevel, string> = {
427
- off: "var(--text-dim)",
428
- minimal: "#6b7280",
429
- low: "#60a5fa",
430
- medium: "#a78bfa",
431
- high: "#f472b6",
432
- xhigh: "#fb923c",
433
- };
434
-
435
- function ThinkingLevelMapEditor({
436
- value,
437
- onChange,
438
- }: {
439
- value: Record<string, string | null> | undefined;
440
- onChange: (v: Record<string, string | null> | undefined) => void;
441
- }) {
442
- const map = value ?? {};
443
-
444
- const setLevel = (level: ThinkingLevel, entry: string | null | "omit") => {
445
- const next = { ...map };
446
- if (entry === "omit") {
447
- delete next[level];
448
- } else {
449
- next[level] = entry;
450
- }
451
- onChange(Object.keys(next).length ? next : undefined);
452
- };
453
-
454
- return (
455
- <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
456
- {THINKING_LEVELS.map((level) => {
457
- const raw = map[level];
458
- const state: "omit" | "null" | "string" =
459
- !(level in map) ? "omit" : raw === null ? "null" : "string";
460
- const strVal = typeof raw === "string" ? raw : "";
461
- const color = LEVEL_COLORS[level];
462
-
463
- const btnBase: React.CSSProperties = {
464
- padding: "4px 10px",
465
- fontSize: 10,
466
- border: "none",
467
- cursor: "pointer",
468
- fontWeight: 400,
469
- transition: "background 0.1s, color 0.1s",
470
- whiteSpace: "nowrap",
471
- background: "var(--bg-panel)",
472
- color: "var(--text-dim)",
473
- };
474
- const btnActive: React.CSSProperties = {
475
- background: "var(--accent)",
476
- color: "#fff",
477
- fontWeight: 600,
478
- };
479
- const btnActiveDisabled: React.CSSProperties = {
480
- background: "#ef4444",
481
- color: "#fff",
482
- fontWeight: 600,
483
- };
484
-
485
- return (
486
- <div
487
- key={level}
488
- style={{
489
- display: "flex",
490
- alignItems: "center",
491
- gap: 8,
492
- padding: "5px 4px",
493
- borderRadius: 6,
494
- background: "transparent",
495
- border: "1px solid transparent",
496
- }}
497
- >
498
- {/* Level badge */}
499
- <div style={{ display: "flex", alignItems: "center", gap: 5, width: 68, flexShrink: 0 }}>
500
- <span style={{ width: 6, height: 6, borderRadius: "50%", background: color, flexShrink: 0, opacity: state === "null" ? 0.3 : 1 }} />
501
- <span style={{
502
- fontSize: 11,
503
- fontFamily: "var(--font-mono)",
504
- color: state === "null" ? "var(--text-dim)" : "var(--text-muted)",
505
- textDecoration: state === "null" ? "line-through" : "none",
506
- }}>
507
- {level}
508
- </span>
509
- </div>
510
-
511
- {/* Default + Disabled buttons */}
512
- <div style={{ display: "flex", borderRadius: 5, border: "1px solid var(--border)", overflow: "hidden", flexShrink: 0 }}>
513
- <button
514
- onClick={() => setLevel(level, "omit")}
515
- style={{ ...btnBase, ...(state === "omit" ? btnActive : {}) }}
516
- >
517
- Default
518
- </button>
519
- <button
520
- onClick={() => setLevel(level, null)}
521
- style={{ ...btnBase, borderLeft: "1px solid var(--border)", ...(state === "null" ? btnActiveDisabled : {}) }}
522
- >
523
- Disabled
524
- </button>
525
- </div>
526
-
527
- {/* Custom button + input fused */}
528
- <div style={{ display: "flex", borderRadius: 5, border: `1px solid ${state === "string" ? "var(--accent)" : "var(--border)"}`, overflow: "hidden", transition: "border-color 0.1s" }}>
529
- <button
530
- onClick={() => setLevel(level, strVal || level)}
531
- style={{ ...btnBase, ...(state === "string" ? btnActive : {}), borderRight: "1px solid var(--border)", flexShrink: 0 }}
532
- >
533
- Custom
534
- </button>
535
- <input
536
- value={strVal}
537
- onChange={(e) => setLevel(level, e.target.value)}
538
- onFocus={() => { if (state !== "string") setLevel(level, strVal || level); }}
539
- placeholder={level}
540
- maxLength={10}
541
- style={{
542
- width: "12ch",
543
- background: state === "string" ? "var(--bg)" : "var(--bg-panel)",
544
- border: "none",
545
- outline: "none",
546
- color: state === "string" ? "var(--text)" : "var(--text-dim)",
547
- fontFamily: "var(--font-mono)",
548
- fontSize: 11,
549
- padding: "4px 7px",
550
- transition: "background 0.1s, color 0.1s",
551
- }}
552
- />
553
- </div>
554
- </div>
555
- );
556
- })}
557
- </div>
558
- );
559
- }
560
-
561
- // ── Model detail ──────────────────────────────────────────────────────────────
562
-
563
- const DEEPSEEK_COMPAT = {
564
- thinkingFormat: "deepseek",
565
- requiresReasoningContentOnAssistantMessages: true,
566
- } as const;
567
-
568
- function hasDeepseekCompat(model: ModelEntry): boolean {
569
- return model.compat?.thinkingFormat === "deepseek";
570
- }
571
-
572
- function setDeepseekCompat(model: ModelEntry, enabled: boolean): ModelEntry {
573
- if (enabled) {
574
- return { ...model, compat: { ...(model.compat ?? {}), ...DEEPSEEK_COMPAT } };
575
- }
576
- if (!model.compat) return model;
577
- const rest = { ...model.compat };
578
- delete rest.thinkingFormat;
579
- delete rest.requiresReasoningContentOnAssistantMessages;
580
- return { ...model, compat: Object.keys(rest).length ? rest : undefined };
581
- }
582
-
583
- function ModelDetail({
584
- providerName,
585
- provider,
586
- model,
587
- onChange,
588
- onDelete,
589
- }: {
590
- providerName: string;
591
- provider: ProviderEntry;
592
- model: ModelEntry;
593
- onChange: (m: ModelEntry) => void;
594
- onDelete: () => void;
595
- }) {
596
- const [testState, setTestState] = useState<ModelTestState>({ phase: "idle" });
597
- const set = <K extends keyof ModelEntry>(k: K, v: ModelEntry[K]) => onChange({ ...model, [k]: v });
598
- const costVal = (k: keyof NonNullable<ModelEntry["cost"]>) => model.cost?.[k] !== undefined ? String(model.cost[k]) : "";
599
- const setCost = (k: keyof NonNullable<ModelEntry["cost"]>, v: string) => {
600
- const n = parseFloat(v);
601
- onChange({ ...model, cost: { ...(model.cost ?? {}), [k]: isNaN(n) ? undefined : n } });
602
- };
603
- const testSummary = (() => {
604
- if (testState.phase === "idle") return null;
605
- if (testState.phase === "testing") return "Testing model connection...";
606
- const meta = [
607
- testState.latencyMs !== undefined ? `${testState.latencyMs}ms` : null,
608
- testState.status !== undefined ? `HTTP ${testState.status}` : null,
609
- ].filter(Boolean);
610
- if (testState.phase === "success") {
611
- return ["Connected", ...meta, testState.responseText || null].filter(Boolean).join(" · ");
612
- }
613
- return ["Failed", ...meta, testState.message].filter(Boolean).join(" · ");
614
- })();
615
-
616
- useEffect(() => {
617
- setTestState({ phase: "idle" });
618
- }, [providerName, provider.baseUrl, provider.api, provider.apiKey, model.id, model.api]);
619
-
620
- const handleTest = useCallback(async () => {
621
- if (!model.id.trim() || testState.phase === "testing") return;
622
- setTestState({ phase: "testing" });
623
- try {
624
- const res = await fetch("/api/models-config/test", {
625
- method: "POST",
626
- headers: { "Content-Type": "application/json" },
627
- body: JSON.stringify({ providerName, provider, model }),
628
- });
629
- const d = await res.json() as {
630
- ok?: boolean;
631
- error?: string;
632
- latencyMs?: number;
633
- status?: number;
634
- responseText?: string;
635
- };
636
- if (!res.ok || !d.ok) {
637
- setTestState({
638
- phase: "error",
639
- message: d.error ?? `HTTP ${res.status}`,
640
- latencyMs: d.latencyMs,
641
- status: d.status,
642
- });
643
- return;
644
- }
645
- setTestState({
646
- phase: "success",
647
- latencyMs: d.latencyMs,
648
- status: d.status,
649
- responseText: d.responseText,
650
- });
651
- } catch (e) {
652
- setTestState({ phase: "error", message: e instanceof Error ? e.message : String(e) });
653
- }
654
- }, [model, provider, providerName, testState.phase]);
655
-
656
- return (
657
- <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
658
- <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
659
- <SectionTitle>Model</SectionTitle>
660
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
661
- {testSummary && (
662
- <span
663
- title={testSummary}
664
- style={{
665
- maxWidth: 260,
666
- height: 24,
667
- padding: "0 8px",
668
- border: `1px solid ${testState.phase === "error" ? "#fecaca" : testState.phase === "success" ? "#bbf7d0" : "var(--border)"}`,
669
- borderRadius: 4,
670
- background: testState.phase === "error" ? "#fee2e2" : testState.phase === "success" ? "#dcfce7" : "#e5e7eb",
671
- color: "#111827",
672
- fontSize: 11,
673
- display: "inline-flex",
674
- alignItems: "center",
675
- whiteSpace: "nowrap",
676
- overflow: "hidden",
677
- textOverflow: "ellipsis",
678
- boxSizing: "border-box",
679
- }}
680
- >
681
- {testSummary}
682
- </span>
683
- )}
684
- <button
685
- onClick={handleTest}
686
- disabled={!model.id.trim() || testState.phase === "testing"}
687
- title="Test model connection"
688
- style={{
689
- height: 24,
690
- padding: "0 8px",
691
- background: testState.phase === "success" ? "#16a34a" : "none",
692
- border: `1px solid ${testState.phase === "success" ? "#16a34a" : "var(--border)"}`,
693
- borderRadius: 4,
694
- color: testState.phase === "success" ? "#fff" : (!model.id.trim() || testState.phase === "testing") ? "var(--text-dim)" : "var(--text-muted)",
695
- cursor: (!model.id.trim() || testState.phase === "testing") ? "not-allowed" : "pointer",
696
- fontSize: 11,
697
- display: "inline-flex",
698
- alignItems: "center",
699
- justifyContent: "center",
700
- boxSizing: "border-box",
701
- gap: 5,
702
- }}
703
- >
704
- {testState.phase === "success" && (
705
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
706
- <polyline points="20 6 9 17 4 12" />
707
- </svg>
708
- )}
709
- {testState.phase === "testing" ? "Testing…" : testState.phase === "success" ? "OK" : "Test"}
710
- </button>
711
- <button onClick={onDelete}
712
- style={{ height: 24, padding: "0 8px", background: "none", border: "1px solid rgba(239,68,68,0.3)", borderRadius: 4, color: "#ef4444", cursor: "pointer", fontSize: 11, boxSizing: "border-box" }}>
713
- Remove
714
- </button>
715
- </div>
716
- </div>
717
-
718
- <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
719
- <Field label="ID *"><TextInput value={model.id} onChange={(v) => set("id", v)} placeholder="model-id" mono /></Field>
720
- <Field label="Name"><TextInput value={model.name ?? ""} onChange={(v) => set("name", v || undefined)} placeholder="Display name" /></Field>
721
- </div>
722
-
723
- <Field label="API override">
724
- <ApiSelect value={model.api ?? ""} onChange={(v) => set("api", v || undefined)} />
725
- <ProtocolHint api={model.api ?? ""} />
726
- </Field>
727
-
728
- <div style={{ display: "flex", gap: 20, flexWrap: "wrap" }}>
729
- <Check label="Reasoning / thinking" checked={model.reasoning ?? false} onChange={(v) => set("reasoning", v || undefined)} />
730
- <Check label="Image input" checked={model.input?.includes("image") ?? false}
731
- onChange={(v) => set("input", v ? ["text", "image"] : undefined)} />
732
- </div>
733
-
734
- {model.reasoning && (
735
- <>
736
- <Check
737
- label="DeepSeek thinking compat"
738
- checked={hasDeepseekCompat(model)}
739
- onChange={(v) => onChange(setDeepseekCompat(model, v))}
740
- />
741
- <div>
742
- <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 8 }}>
743
- <SectionTitle>Thinking level map</SectionTitle>
744
- {model.thinkingLevelMap && (
745
- <button
746
- onClick={() => set("thinkingLevelMap", undefined)}
747
- style={{ fontSize: 10, padding: "2px 7px", background: "none", border: "1px solid var(--border)", borderRadius: 4, color: "var(--text-dim)", cursor: "pointer" }}
748
- >
749
- clear all
750
- </button>
751
- )}
752
- </div>
753
- <ThinkingLevelMapEditor
754
- value={model.thinkingLevelMap}
755
- onChange={(v) => set("thinkingLevelMap", v)}
756
- />
757
- </div>
758
- </>
759
- )}
760
-
761
- <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
762
- <Field label="Context window (tokens)">
763
- <NumInput value={model.contextWindow !== undefined ? String(model.contextWindow) : ""}
764
- onChange={(v) => set("contextWindow", v ? parseInt(v) : undefined)} placeholder="128000" />
765
- </Field>
766
- <Field label="Max output tokens">
767
- <NumInput value={model.maxTokens !== undefined ? String(model.maxTokens) : ""}
768
- onChange={(v) => set("maxTokens", v ? parseInt(v) : undefined)} placeholder="16384" />
769
- </Field>
770
- </div>
771
-
772
- <div>
773
- <SectionTitle>Cost (per million tokens)</SectionTitle>
774
- <div style={{ marginTop: 8, display: "grid", gridTemplateColumns: "1fr 1fr 1fr 1fr", gap: 8 }}>
775
- {(["input", "output", "cacheRead", "cacheWrite"] as const).map((k) => (
776
- <Field key={k} label={k}>
777
- <NumInput value={costVal(k)} onChange={(v) => setCost(k, v)} placeholder="0" />
778
- </Field>
779
- ))}
780
- </div>
781
- </div>
782
- </div>
783
- );
784
- }
785
-
786
- // ── OAuth detail ──────────────────────────────────────────────────────────────
787
-
788
- function OAuthDetail({ provider, onRefresh }: { provider: OAuthProvider; onRefresh: () => void }) {
789
- const [loginState, setLoginState] = useState<OAuthLoginState>({ phase: "idle" });
790
- const [inputValue, setInputValue] = useState("");
791
- const eventSourceRef = useRef<EventSource | null>(null);
792
- const inputRef = useRef<HTMLInputElement>(null);
793
-
794
- useEffect(() => {
795
- if (loginState.phase === "auth" || loginState.phase === "prompt") {
796
- setTimeout(() => inputRef.current?.focus(), 50);
797
- }
798
- }, [loginState.phase]);
799
-
800
- // Reset state when provider changes
801
- useEffect(() => {
802
- setLoginState({ phase: "idle" });
803
- setInputValue("");
804
- eventSourceRef.current?.close();
805
- eventSourceRef.current = null;
806
- }, [provider.id]);
807
-
808
- useEffect(() => {
809
- return () => { eventSourceRef.current?.close(); };
810
- }, []);
811
-
812
- const handleLogin = useCallback(() => {
813
- eventSourceRef.current?.close();
814
- setLoginState({ phase: "connecting" });
815
- setInputValue("");
816
-
817
- const es = new EventSource(`/api/auth/login/${encodeURIComponent(provider.id)}`);
818
- eventSourceRef.current = es;
819
-
820
- es.onmessage = (e) => {
821
- const data = JSON.parse(e.data) as {
822
- type: string; url?: string; instructions?: string | null;
823
- token?: string; message?: string; placeholder?: string | null;
824
- userCode?: string; verificationUri?: string; intervalSeconds?: number | null; expiresInSeconds?: number | null;
825
- options?: { id: string; label: string }[];
826
- };
827
- if (data.type === "auth") {
828
- setLoginState({ phase: "auth", url: data.url!, instructions: data.instructions ?? null, token: data.token! });
829
- window.open(data.url!, "_blank", "noopener,noreferrer");
830
- } else if (data.type === "device_code") {
831
- setLoginState({
832
- phase: "device_code",
833
- userCode: data.userCode!,
834
- verificationUri: data.verificationUri!,
835
- intervalSeconds: data.intervalSeconds ?? null,
836
- expiresInSeconds: data.expiresInSeconds ?? null,
837
- });
838
- window.open(data.verificationUri!, "_blank", "noopener,noreferrer");
839
- } else if (data.type === "prompt_request") {
840
- setLoginState({ phase: "prompt", message: data.message!, placeholder: data.placeholder ?? null, token: data.token! });
841
- } else if (data.type === "select_request") {
842
- setLoginState({ phase: "select", message: data.message!, options: data.options ?? [], token: data.token! });
843
- } else if (data.type === "progress") {
844
- setLoginState({ phase: "progress", message: data.message! });
845
- } else if (data.type === "success") {
846
- es.close();
847
- setLoginState({ phase: "success" });
848
- onRefresh();
849
- } else if (data.type === "error") {
850
- es.close();
851
- setLoginState({ phase: "error", message: data.message! });
852
- } else if (data.type === "cancelled") {
853
- es.close();
854
- setLoginState({ phase: "idle" });
855
- }
856
- };
857
- es.onerror = () => {
858
- es.close();
859
- setLoginState((prev) => prev.phase === "success" ? prev : { phase: "error", message: "Connection lost" });
860
- };
861
- }, [provider.id, onRefresh]);
862
-
863
- const handleLogout = useCallback(async () => {
864
- await fetch(`/api/auth/logout/${encodeURIComponent(provider.id)}`, { method: "POST" });
865
- setLoginState({ phase: "idle" });
866
- onRefresh();
867
- }, [provider.id, onRefresh]);
868
-
869
- const submitCode = useCallback(async (token: string, code: string) => {
870
- if (!code.trim()) return;
871
- setLoginState({ phase: "progress", message: "Verifying…" });
872
- try {
873
- const res = await fetch(`/api/auth/login/${encodeURIComponent(provider.id)}`, {
874
- method: "POST",
875
- headers: { "Content-Type": "application/json" },
876
- body: JSON.stringify({ token, code: code.trim() }),
877
- });
878
- if (!res.ok) {
879
- const d = await res.json().catch(() => ({})) as { error?: string };
880
- setLoginState({ phase: "error", message: d.error ?? `Server error ${res.status}` });
881
- return;
882
- }
883
- setInputValue("");
884
- // Success path: SSE stream will emit "success" and update state
885
- } catch (e) {
886
- setLoginState({ phase: "error", message: e instanceof Error ? e.message : "Network error" });
887
- }
888
- }, [provider.id]);
889
-
890
- const submitSelection = useCallback(async (token: string, value: string) => {
891
- setLoginState({ phase: "progress", message: "Continuing…" });
892
- try {
893
- const res = await fetch(`/api/auth/login/${encodeURIComponent(provider.id)}`, {
894
- method: "POST",
895
- headers: { "Content-Type": "application/json" },
896
- body: JSON.stringify({ token, code: value }),
897
- });
898
- if (!res.ok) {
899
- const d = await res.json().catch(() => ({})) as { error?: string };
900
- setLoginState({ phase: "error", message: d.error ?? `Server error ${res.status}` });
901
- }
902
- } catch (e) {
903
- setLoginState({ phase: "error", message: e instanceof Error ? e.message : "Network error" });
904
- }
905
- }, [provider.id]);
906
-
907
- const isWorking = loginState.phase === "connecting" || loginState.phase === "progress" ||
908
- loginState.phase === "auth" || loginState.phase === "device_code" ||
909
- loginState.phase === "prompt" || loginState.phase === "select";
910
-
911
- return (
912
- <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
913
- <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
914
- <SectionTitle>Subscription</SectionTitle>
915
- <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
916
- <span style={{ width: 7, height: 7, borderRadius: "50%", background: provider.loggedIn ? "#4ade80" : "var(--border)", display: "inline-block" }} />
917
- <span style={{ fontSize: 11, color: provider.loggedIn ? "#4ade80" : "var(--text-dim)" }}>
918
- {provider.loggedIn ? "connected" : "not connected"}
919
- </span>
920
- </div>
921
- </div>
922
-
923
- {/* Status */}
924
- <div style={{ minHeight: 48 }}>
925
- {loginState.phase === "idle" && (
926
- <p style={{ margin: 0, fontSize: 12, color: "var(--text-muted)", lineHeight: 1.5 }}>
927
- {provider.loggedIn ? "Already connected. You can re-login or disconnect." : `Connect your ${provider.name} account.`}
928
- </p>
929
- )}
930
- {loginState.phase === "connecting" && (
931
- <p style={{ margin: 0, fontSize: 12, color: "var(--text-muted)" }}>Opening browser…</p>
932
- )}
933
- {loginState.phase === "select" && (
934
- <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
935
- <p style={{ margin: 0, fontSize: 12, color: "var(--text-muted)", lineHeight: 1.5 }}>
936
- {loginState.message}
937
- </p>
938
- <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
939
- {loginState.options.map((option) => (
940
- <button
941
- key={option.id}
942
- onClick={() => submitSelection(loginState.token, option.id)}
943
- style={{ padding: "6px 9px", background: "var(--bg)", border: "1px solid var(--border)", borderRadius: 5, color: "var(--text)", cursor: "pointer", fontSize: 12, textAlign: "left" }}
944
- >
945
- {option.label}
946
- </button>
947
- ))}
948
- </div>
949
- </div>
950
- )}
951
- {(loginState.phase === "auth" || loginState.phase === "prompt") && (
952
- <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
953
- <p style={{ margin: 0, fontSize: 12, color: "var(--text-muted)", lineHeight: 1.5 }}>
954
- {loginState.phase === "auth"
955
- ? "Complete sign-in in the browser, then copy the redirect URL from the address bar and paste it below."
956
- : loginState.message}
957
- </p>
958
- {loginState.phase === "auth" && (
959
- <p style={{ margin: 0, fontSize: 11, color: "var(--text-dim)", lineHeight: 1.5 }}>
960
- If the browser window did not open,{" "}
961
- <a href={loginState.url} target="_blank" rel="noopener noreferrer" style={{ color: "var(--accent)", wordBreak: "break-all" }}>
962
- click here to open the login page
963
- </a>
964
- .
965
- </p>
966
- )}
967
- <div style={{ display: "flex", gap: 6 }}>
968
- <input
969
- ref={inputRef}
970
- value={inputValue}
971
- onChange={(e) => setInputValue(e.target.value)}
972
- onKeyDown={(e) => { if (e.key === "Enter") submitCode(loginState.token, inputValue); }}
973
- placeholder={loginState.phase === "auth" ? "http://localhost:1455/auth/callback?code=…" : (loginState.placeholder ?? "Enter value…")}
974
- style={{ flex: 1, padding: "6px 9px", background: "var(--bg)", border: "1px solid var(--border)", borderRadius: 5, color: "var(--text)", fontSize: 12, outline: "none", fontFamily: "var(--font-mono)", boxSizing: "border-box" }}
975
- />
976
- <button
977
- onClick={() => submitCode(loginState.token, inputValue)}
978
- disabled={!inputValue.trim()}
979
- style={{ padding: "6px 12px", background: inputValue.trim() ? "var(--accent)" : "var(--bg-panel)", border: "none", borderRadius: 5, color: inputValue.trim() ? "#fff" : "var(--text-dim)", cursor: inputValue.trim() ? "pointer" : "not-allowed", fontSize: 12, fontWeight: 600, flexShrink: 0 }}
980
- >
981
- Submit
982
- </button>
983
- </div>
984
- </div>
985
- )}
986
- {loginState.phase === "device_code" && (
987
- <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
988
- <p style={{ margin: 0, fontSize: 12, color: "var(--text-muted)", lineHeight: 1.5 }}>
989
- Open the verification page and enter this code:
990
- </p>
991
- <div style={{ padding: "8px 10px", background: "var(--bg)", border: "1px solid var(--border)", borderRadius: 5, color: "var(--text)", fontSize: 16, fontWeight: 700, fontFamily: "var(--font-mono)", letterSpacing: 0 }}>
992
- {loginState.userCode}
993
- </div>
994
- <p style={{ margin: 0, fontSize: 11, color: "var(--text-dim)", lineHeight: 1.5 }}>
995
- <a href={loginState.verificationUri} target="_blank" rel="noopener noreferrer" style={{ color: "var(--accent)", wordBreak: "break-all" }}>
996
- {loginState.verificationUri}
997
- </a>
998
- {loginState.expiresInSeconds ? ` Expires in ${Math.ceil(loginState.expiresInSeconds / 60)} minutes.` : ""}
999
- </p>
1000
- </div>
1001
- )}
1002
- {loginState.phase === "progress" && (
1003
- <p style={{ margin: 0, fontSize: 12, color: "var(--text-muted)" }}>{loginState.message}</p>
1004
- )}
1005
- {loginState.phase === "success" && (
1006
- <p style={{ margin: 0, fontSize: 12, color: "#4ade80" }}>Connected successfully.</p>
1007
- )}
1008
- {loginState.phase === "error" && (
1009
- <p style={{ margin: 0, fontSize: 12, color: "#f87171" }}>{loginState.message}</p>
1010
- )}
1011
- </div>
1012
-
1013
- {/* Actions */}
1014
- <div style={{ display: "flex", gap: 8 }}>
1015
- {isWorking ? (
1016
- <button
1017
- onClick={() => { eventSourceRef.current?.close(); setLoginState({ phase: "idle" }); }}
1018
- style={{ padding: "5px 12px", background: "none", border: "1px solid var(--border)", borderRadius: 5, color: "var(--text-muted)", cursor: "pointer", fontSize: 12 }}
1019
- >
1020
- Cancel
1021
- </button>
1022
- ) : (
1023
- <>
1024
- <button
1025
- onClick={handleLogin}
1026
- style={{ padding: "5px 14px", background: "var(--accent)", border: "none", borderRadius: 5, color: "#fff", cursor: "pointer", fontSize: 12, fontWeight: 600 }}
1027
- >
1028
- {provider.loggedIn ? "Re-login" : "Login"}
1029
- </button>
1030
- {provider.loggedIn && (
1031
- <button
1032
- onClick={handleLogout}
1033
- style={{ padding: "5px 12px", background: "none", border: "1px solid rgba(239,68,68,0.3)", borderRadius: 5, color: "#ef4444", cursor: "pointer", fontSize: 12 }}
1034
- >
1035
- Disconnect
1036
- </button>
1037
- )}
1038
- </>
1039
- )}
1040
- </div>
1041
- </div>
1042
- );
1043
- }
1044
-
1045
- // ── API Key detail ────────────────────────────────────────────────────────────
1046
-
1047
- function ApiKeyDetail({ provider, onRefresh }: { provider: ApiKeyProvider; onRefresh: () => void }) {
1048
- const [apiKey, setApiKey] = useState("");
1049
- const [saving, setSaving] = useState(false);
1050
- const [removing, setRemoving] = useState(false);
1051
- const [error, setError] = useState<string | null>(null);
1052
- const [savedOk, setSavedOk] = useState(false);
1053
-
1054
- // Reset state when provider changes
1055
- useEffect(() => {
1056
- setApiKey("");
1057
- setError(null);
1058
- setSavedOk(false);
1059
- }, [provider.id]);
1060
-
1061
- const handleSave = useCallback(async () => {
1062
- if (!apiKey.trim()) return;
1063
- setSaving(true);
1064
- setError(null);
1065
- setSavedOk(false);
1066
- try {
1067
- const res = await fetch(`/api/auth/api-key/${encodeURIComponent(provider.id)}`, {
1068
- method: "POST",
1069
- headers: { "Content-Type": "application/json" },
1070
- body: JSON.stringify({ apiKey: apiKey.trim() }),
1071
- });
1072
- const d = await res.json() as { success?: boolean; error?: string };
1073
- if (!res.ok || d.error) {
1074
- setError(d.error ?? `HTTP ${res.status}`);
1075
- } else {
1076
- setApiKey("");
1077
- setSavedOk(true);
1078
- setTimeout(() => setSavedOk(false), 2000);
1079
- onRefresh();
1080
- }
1081
- } catch (e) {
1082
- setError(String(e));
1083
- } finally {
1084
- setSaving(false);
1085
- }
1086
- }, [apiKey, provider.id, onRefresh]);
1087
-
1088
- const handleRemove = useCallback(async () => {
1089
- setRemoving(true);
1090
- setError(null);
1091
- try {
1092
- const res = await fetch(`/api/auth/api-key/${encodeURIComponent(provider.id)}`, { method: "DELETE" });
1093
- const d = await res.json() as { success?: boolean; error?: string };
1094
- if (!res.ok || d.error) setError(d.error ?? `HTTP ${res.status}`);
1095
- else onRefresh();
1096
- } catch (e) {
1097
- setError(String(e));
1098
- } finally {
1099
- setRemoving(false);
1100
- }
1101
- }, [provider.id, onRefresh]);
1102
-
1103
- return (
1104
- <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
1105
- <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
1106
- <SectionTitle>API Key</SectionTitle>
1107
- <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
1108
- <span style={{ width: 7, height: 7, borderRadius: "50%", background: provider.configured ? "#4ade80" : "var(--border)", display: "inline-block" }} />
1109
- <span style={{ fontSize: 11, color: provider.configured ? "#4ade80" : "var(--text-dim)" }}>
1110
- {provider.configured ? "configured" : "not configured"}
1111
- </span>
1112
- </div>
1113
- </div>
1114
-
1115
- <p style={{ margin: 0, fontSize: 12, color: "var(--text-muted)", lineHeight: 1.5 }}>
1116
- {provider.configured
1117
- ? `API key is stored. Enter a new key below to replace it, or disconnect to remove it.`
1118
- : `Enter your ${provider.displayName} API key to enable ${provider.modelCount} model${provider.modelCount !== 1 ? "s" : ""}.`}
1119
- </p>
1120
-
1121
- <Field label="API Key">
1122
- <div style={{ display: "flex", gap: 6 }}>
1123
- <SecretTextInput
1124
- value={apiKey}
1125
- onChange={setApiKey}
1126
- onKeyDown={(e) => { if (e.key === "Enter" && apiKey.trim()) handleSave(); }}
1127
- placeholder={provider.configured ? "Enter new key to replace…" : "sk-…"}
1128
- style={{ flex: 1 }}
1129
- autoComplete="off"
1130
- spellCheck={false}
1131
- mono
1132
- />
1133
- <button
1134
- onClick={handleSave}
1135
- disabled={saving || !apiKey.trim() || savedOk}
1136
- style={{
1137
- padding: "6px 12px",
1138
- background: savedOk ? "#16a34a" : apiKey.trim() ? "var(--accent)" : "var(--bg-panel)",
1139
- border: "none", borderRadius: 5,
1140
- color: (apiKey.trim() || savedOk) ? "#fff" : "var(--text-dim)",
1141
- cursor: (saving || !apiKey.trim() || savedOk) ? "not-allowed" : "pointer",
1142
- fontSize: 12, fontWeight: 600, flexShrink: 0,
1143
- display: "flex", alignItems: "center", gap: 5,
1144
- }}
1145
- >
1146
- {savedOk && (
1147
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
1148
- <polyline points="20 6 9 17 4 12" />
1149
- </svg>
1150
- )}
1151
- {savedOk ? "Saved" : saving ? "Saving…" : "Save"}
1152
- </button>
1153
- </div>
1154
- </Field>
1155
-
1156
- {error && <p style={{ margin: 0, fontSize: 12, color: "#f87171" }}>{error}</p>}
1157
-
1158
- {provider.configured && (
1159
- <button
1160
- onClick={handleRemove}
1161
- disabled={removing}
1162
- style={{
1163
- alignSelf: "flex-start", padding: "5px 12px",
1164
- background: "none", border: "1px solid rgba(239,68,68,0.3)",
1165
- borderRadius: 5, color: "#ef4444",
1166
- cursor: removing ? "not-allowed" : "pointer", fontSize: 12,
1167
- }}
1168
- >
1169
- {removing ? "Removing…" : "Disconnect"}
1170
- </button>
1171
- )}
1172
- </div>
1173
- );
1174
- }
1175
-
1176
- // ── Provider icon ─────────────────────────────────────────────────────────────
1177
-
1178
- function ProviderIcon({ id, size }: { id: string; size: number }) {
1179
- const pi = PROVIDER_ICONS[id];
1180
- if (!pi) return null;
1181
- // Color icons: self-colored SVG, no wrapper needed
1182
- if (pi.hasColor) return <pi.Icon size={size} />;
1183
- // Mono icons: use currentColor so they adapt to light/dark theme
1184
- return <pi.Icon size={size} style={{ color: "var(--text-muted)" }} />;
1185
- }
1186
-
1187
- // ── Add provider picker ───────────────────────────────────────────────────────
1188
-
1189
- interface AddProviderPickerProps {
1190
- oauthProviders: OAuthProvider[];
1191
- apiKeyProviders: ApiKeyProvider[];
1192
- onSelectOAuth: (id: string) => void;
1193
- onSelectApiKey: (id: string) => void;
1194
- onAddCustom: () => void;
1195
- onDiscover: () => void;
1196
- onClose: () => void;
1197
- }
1198
-
1199
- function AddProviderPicker({
1200
- oauthProviders, apiKeyProviders,
1201
- onSelectOAuth, onSelectApiKey, onAddCustom, onDiscover, onClose,
1202
- }: AddProviderPickerProps) {
1203
- const [search, setSearch] = useState("");
1204
- const inputRef = useRef<HTMLInputElement>(null);
1205
-
1206
- useEffect(() => { setTimeout(() => inputRef.current?.focus(), 30); }, []);
1207
-
1208
- const q = search.trim().toLowerCase();
1209
-
1210
- const availableOAuth = oauthProviders.filter((p) => !p.loggedIn && (!q || p.name.toLowerCase().includes(q)));
1211
- const availableApiKey = apiKeyProviders.filter((p) => !p.configured && (!q || p.displayName.toLowerCase().includes(q) || p.id.toLowerCase().includes(q)));
1212
- const showCustom = !q || "custom".includes(q) || "openai-compatible".includes(q) || "deepseek".includes(q) || "deeprouter".includes(q);
1213
- const showDiscover = !q || "discover".includes(q) || "base url".includes(q) || "model import".includes(q);
1214
-
1215
- const totalCount = availableOAuth.length + availableApiKey.length + (showCustom ? 1 : 0) + (showDiscover ? 1 : 0);
1216
-
1217
- const cardStyle: React.CSSProperties = {
1218
- display: "flex", flexDirection: "row", alignItems: "center", gap: 8,
1219
- padding: "10px 12px",
1220
- background: "var(--bg-panel)",
1221
- border: "1px solid var(--border)",
1222
- borderRadius: 7,
1223
- boxSizing: "border-box",
1224
- cursor: "pointer",
1225
- minWidth: 0,
1226
- textAlign: "left",
1227
- transition: "border-color 0.12s, background 0.12s",
1228
- width: "100%",
1229
- };
1230
-
1231
-
1232
-
1233
- return (
1234
- <div
1235
- style={{ position: "fixed", inset: 0, zIndex: 1100, background: "rgba(0,0,0,0.4)", display: "flex", alignItems: "center", justifyContent: "center" }}
1236
- onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
1237
- >
1238
- <div style={{ width: 820, maxWidth: "calc(100vw - 32px)", maxHeight: "min(72vh, calc(100vh - 32px))", background: "var(--bg)", border: "1px solid var(--border)", borderRadius: 10, display: "flex", flexDirection: "column", boxShadow: "0 8px 32px rgba(0,0,0,0.22)", overflow: "hidden" }}>
1239
- {/* Search */}
1240
- <div style={{ padding: "10px 14px", borderBottom: "1px solid var(--border)", flexShrink: 0, display: "flex", alignItems: "center", gap: 8 }}>
1241
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ color: "var(--text-dim)", flexShrink: 0 }}>
1242
- <circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
1243
- </svg>
1244
- <input
1245
- ref={inputRef}
1246
- value={search}
1247
- onChange={(e) => setSearch(e.target.value)}
1248
- onKeyDown={(e) => { if (e.key === "Escape") onClose(); }}
1249
- placeholder="Search providers…"
1250
- style={{ flex: 1, background: "none", border: "none", outline: "none", color: "var(--text)", fontSize: 13, boxSizing: "border-box" }}
1251
- />
1252
- </div>
1253
-
1254
- {/* Card grid */}
1255
- <div style={{ flex: 1, overflowY: "auto", padding: 14 }}>
1256
- {totalCount === 0 ? (
1257
- <div style={{ padding: "20px 0", fontSize: 12, color: "var(--text-dim)", textAlign: "center" }}>No providers match</div>
1258
- ) : (
1259
- <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(min(240px, 100%), 1fr))", gap: 8 }}>
1260
- {showCustom && (
1261
- <div style={{ gridColumn: "1 / -1", fontSize: 10, fontWeight: 600, color: "var(--text-dim)", textTransform: "uppercase", letterSpacing: "0.07em" }}>Custom</div>
1262
- )}
1263
- {showCustom && (
1264
- <button
1265
- onClick={() => { onAddCustom(); onClose(); }}
1266
- style={cardStyle}
1267
- onMouseEnter={(e) => { e.currentTarget.style.borderColor = "var(--accent)"; e.currentTarget.style.background = "var(--bg-hover)"; }}
1268
- onMouseLeave={(e) => { e.currentTarget.style.borderColor = "var(--border)"; e.currentTarget.style.background = "var(--bg-panel)"; }}
1269
- >
1270
- <div style={{ flex: 1, minWidth: 0 }}>
1271
- <div style={{ fontSize: 12, fontWeight: 600, color: "var(--text)", lineHeight: 1.3, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>OpenAI-compatible endpoint</div>
1272
- <div style={{ fontSize: 10, color: "var(--text-dim)", marginTop: 2 }}>DeepSeek, deeprouter, Kimi, Qwen</div>
1273
- </div>
1274
- <span style={{ width: 26, height: 26, borderRadius: 5, background: "var(--bg-hover)", border: "1px dashed var(--border)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
1275
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ color: "var(--text-dim)" }}>
1276
- <line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
1277
- </svg>
1278
- </span>
1279
- </button>
1280
- )}
1281
- {showDiscover && (
1282
- <button
1283
- onClick={() => { onDiscover(); onClose(); }}
1284
- style={cardStyle}
1285
- onMouseEnter={(e) => { e.currentTarget.style.borderColor = "var(--accent)"; e.currentTarget.style.background = "var(--bg-hover)"; }}
1286
- onMouseLeave={(e) => { e.currentTarget.style.borderColor = "var(--border)"; e.currentTarget.style.background = "var(--bg-panel)"; }}
1287
- >
1288
- <div style={{ flex: 1, minWidth: 0 }}>
1289
- <div style={{ fontSize: 12, fontWeight: 600, color: "var(--text)", lineHeight: 1.3, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>Discover models from Base URL</div>
1290
- <div style={{ fontSize: 10, color: "var(--text-dim)", marginTop: 2 }}>Optional OpenAI-compatible / Ollama catalog</div>
1291
- </div>
1292
- <span style={{ width: 26, height: 26, borderRadius: 5, background: "var(--bg-hover)", border: "1px solid var(--border)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
1293
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ color: "var(--text-muted)" }}>
1294
- <circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
1295
- </svg>
1296
- </span>
1297
- </button>
1298
- )}
1299
-
1300
- {availableOAuth.length > 0 && (
1301
- <div style={{ gridColumn: "1 / -1", paddingTop: showCustom ? 6 : 0, fontSize: 10, fontWeight: 600, color: "var(--text-dim)", textTransform: "uppercase", letterSpacing: "0.07em" }}>Subscriptions</div>
1302
- )}
1303
- {availableOAuth.map((p) => (
1304
- <button key={p.id} onClick={() => { onSelectOAuth(p.id); onClose(); }}
1305
- style={cardStyle}
1306
- onMouseEnter={(e) => { e.currentTarget.style.borderColor = "var(--accent)"; e.currentTarget.style.background = "var(--bg-hover)"; }}
1307
- onMouseLeave={(e) => { e.currentTarget.style.borderColor = "var(--border)"; e.currentTarget.style.background = "var(--bg-panel)"; }}
1308
- >
1309
- <div style={{ flex: 1, minWidth: 0 }}>
1310
- <div style={{ fontSize: 12, fontWeight: 600, color: "var(--text)", lineHeight: 1.3, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{p.name}</div>
1311
- <div style={{ fontSize: 10, color: "var(--text-dim)", marginTop: 2 }}>OAuth</div>
1312
- </div>
1313
- <ProviderIcon id={p.id} size={28} />
1314
- </button>
1315
- ))}
1316
-
1317
- {availableApiKey.length > 0 && (
1318
- <div style={{ gridColumn: "1 / -1", paddingTop: availableOAuth.length > 0 ? 6 : 0, fontSize: 10, fontWeight: 600, color: "var(--text-dim)", textTransform: "uppercase", letterSpacing: "0.07em" }}>API Key</div>
1319
- )}
1320
- {availableApiKey.map((p) => (
1321
- <button key={p.id} onClick={() => { onSelectApiKey(p.id); onClose(); }}
1322
- style={cardStyle}
1323
- onMouseEnter={(e) => { e.currentTarget.style.borderColor = "var(--accent)"; e.currentTarget.style.background = "var(--bg-hover)"; }}
1324
- onMouseLeave={(e) => { e.currentTarget.style.borderColor = "var(--border)"; e.currentTarget.style.background = "var(--bg-panel)"; }}
1325
- >
1326
- <div style={{ flex: 1, minWidth: 0 }}>
1327
- <div style={{ fontSize: 12, fontWeight: 600, color: "var(--text)", lineHeight: 1.3, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{p.displayName}</div>
1328
- <div style={{ fontSize: 10, color: "var(--text-dim)", marginTop: 2 }}>{p.modelCount} models</div>
1329
- </div>
1330
- <ProviderIcon id={p.id} size={28} />
1331
- </button>
1332
- ))}
1333
-
1334
- </div>
1335
- )}
1336
- </div>
1337
- </div>
1338
- </div>
1339
- );
1340
- }
1341
-
1342
- // ── Discover models from Base URL ─────────────────────────────────────────────
1343
-
1344
- function DiscoverModelsDialog({
1345
- existingProviders,
1346
- onImport,
1347
- onClose,
1348
- }: {
1349
- existingProviders: Record<string, ProviderEntry>;
1350
- onImport: (providerName: string, provider: ProviderEntry, selectedCount: number) => void;
1351
- onClose: () => void;
1352
- }) {
1353
- const [baseUrl, setBaseUrl] = useState("");
1354
- const [apiKey, setApiKey] = useState("");
1355
- const [providerName, setProviderName] = useState("custom-provider");
1356
- const [selected, setSelected] = useState<Set<string>>(new Set());
1357
- const [state, setState] = useState<DiscoverState>({ phase: "idle" });
1358
- const discover = useCallback(async () => {
1359
- const trimmedBaseUrl = baseUrl.trim();
1360
- if (!trimmedBaseUrl || state.phase === "loading") return;
1361
- setState({ phase: "loading" });
1362
- try {
1363
- const res = await fetch("/api/models-config/discover", {
1364
- method: "POST",
1365
- headers: { "Content-Type": "application/json" },
1366
- body: JSON.stringify({ baseUrl: trimmedBaseUrl, apiKey, provider: providerName, enrich: false }),
1367
- });
1368
- const d = await res.json() as {
1369
- ok?: boolean;
1370
- error?: string;
1371
- endpoint?: string;
1372
- models?: DiscoveredModel[];
1373
- piModels?: ModelEntry[];
1374
- };
1375
- if (!res.ok || !d.ok || !d.models || !d.piModels || !d.endpoint) {
1376
- setState({ phase: "error", message: d.error ?? `HTTP ${res.status}` });
1377
- return;
1378
- }
1379
- setState({ phase: "success", endpoint: d.endpoint, models: d.models, piModels: d.piModels });
1380
- setSelected(new Set(d.piModels.map((m) => m.id)));
1381
- } catch (error) {
1382
- setState({ phase: "error", message: error instanceof Error ? error.message : String(error) });
1383
- }
1384
- }, [apiKey, baseUrl, providerName, state.phase]);
1385
-
1386
- const success = state.phase === "success" ? state : null;
1387
- const selectedCount = selected.size;
1388
- const canImport = Boolean(success && providerName.trim() && selectedCount > 0);
1389
-
1390
- const importSelected = () => {
1391
- if (!success || !canImport) return;
1392
- const normalizedBaseUrl = baseUrl.trim().replace(/\/+$/, "");
1393
- const trimmedApiKey = apiKey.trim();
1394
- const matched = Object.entries(existingProviders).find(([, provider]) =>
1395
- (provider.baseUrl ?? "").replace(/\/+$/, "") === normalizedBaseUrl &&
1396
- (provider.apiKey ?? "") === trimmedApiKey
1397
- );
1398
- const name = matched?.[0] ?? providerName.trim();
1399
- const existingProvider = existingProviders[name];
1400
- const byId = new Map((existingProvider?.models ?? []).map((model) => [model.id, model]));
1401
- let addedCount = 0;
1402
- for (const model of success.piModels) {
1403
- if (selected.has(model.id) && !byId.has(model.id)) {
1404
- byId.set(model.id, model);
1405
- addedCount += 1;
1406
- }
1407
- }
1408
- onImport(name, {
1409
- ...(existingProvider ?? {}),
1410
- baseUrl: normalizedBaseUrl,
1411
- api: existingProvider?.api ?? inferProviderApi(normalizedBaseUrl, name),
1412
- ...(trimmedApiKey ? { apiKey: trimmedApiKey } : {}),
1413
- models: Array.from(byId.values()),
1414
- }, addedCount);
1415
- onClose();
1416
- };
1417
-
1418
- const toggleAll = () => {
1419
- if (!success) return;
1420
- if (selected.size === success.piModels.length) {
1421
- setSelected(new Set());
1422
- return;
1423
- }
1424
- setSelected(new Set(success.piModels.map((m) => m.id)));
1425
- };
1426
-
1427
- const toggleModel = (id: string) => {
1428
- setSelected((prev) => {
1429
- const next = new Set(prev);
1430
- if (next.has(id)) next.delete(id);
1431
- else next.add(id);
1432
- return next;
1433
- });
1434
- };
1435
-
1436
- return (
1437
- <div
1438
- style={{ position: "fixed", inset: 0, zIndex: 1100, background: "rgba(0,0,0,0.4)", display: "flex", alignItems: "center", justifyContent: "center" }}
1439
- onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
1440
- >
1441
- <div style={{ width: 880, maxWidth: "calc(100vw - 32px)", height: "min(78vh, calc(100vh - 32px))", background: "var(--bg)", border: "1px solid var(--border)", borderRadius: 10, display: "flex", flexDirection: "column", boxShadow: "0 8px 32px rgba(0,0,0,0.22)", overflow: "hidden" }}>
1442
- <div style={{ padding: "12px 16px", borderBottom: "1px solid var(--border)", display: "flex", alignItems: "center", justifyContent: "space-between", flexShrink: 0 }}>
1443
- <div>
1444
- <div style={{ fontSize: 14, fontWeight: 700, color: "var(--text)" }}>Discover models</div>
1445
- <div style={{ fontSize: 11, color: "var(--text-dim)", marginTop: 2 }}>Optional: fetch a model catalog from a Base URL and import selected models.</div>
1446
- </div>
1447
- <button onClick={onClose} style={{ background: "none", border: "none", color: "var(--text-muted)", cursor: "pointer", fontSize: 20, lineHeight: 1, padding: "2px 6px" }}>×</button>
1448
- </div>
1449
-
1450
- <div style={{ padding: 16, borderBottom: "1px solid var(--border)", display: "grid", gridTemplateColumns: "1.4fr 1fr 0.9fr auto", gap: 10, alignItems: "end", flexShrink: 0 }}>
1451
- <Field label="Base URL">
1452
- <TextInput value={baseUrl} onChange={setBaseUrl} placeholder="https://api.example.com" mono />
1453
- </Field>
1454
- <Field label="API Key">
1455
- <SecretTextInput value={apiKey} onChange={setApiKey} placeholder="optional for local providers" mono />
1456
- </Field>
1457
- <Field label="Provider name">
1458
- <TextInput value={providerName} onChange={setProviderName} placeholder="provider-name" mono />
1459
- </Field>
1460
- <button
1461
- onClick={discover}
1462
- disabled={!baseUrl.trim() || state.phase === "loading"}
1463
- style={{
1464
- height: 31,
1465
- padding: "0 14px",
1466
- background: baseUrl.trim() && state.phase !== "loading" ? "var(--accent)" : "var(--bg-panel)",
1467
- border: "1px solid var(--border)",
1468
- borderRadius: 5,
1469
- color: baseUrl.trim() && state.phase !== "loading" ? "#fff" : "var(--text-dim)",
1470
- cursor: baseUrl.trim() && state.phase !== "loading" ? "pointer" : "not-allowed",
1471
- fontSize: 12,
1472
- fontWeight: 600,
1473
- whiteSpace: "nowrap",
1474
- }}
1475
- >
1476
- {state.phase === "loading" ? "Fetching..." : "Fetch models"}
1477
- </button>
1478
- </div>
1479
-
1480
- <div style={{ flex: 1, overflow: "hidden", display: "flex", flexDirection: "column" }}>
1481
- {state.phase === "idle" && (
1482
- <div style={{ height: "100%", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--text-dim)", fontSize: 13 }}>
1483
- Enter a Base URL to discover available models.
1484
- </div>
1485
- )}
1486
- {state.phase === "loading" && (
1487
- <div style={{ height: "100%", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--text-muted)", fontSize: 13 }}>
1488
- Fetching model catalog...
1489
- </div>
1490
- )}
1491
- {state.phase === "error" && (
1492
- <div style={{ padding: 18, color: "#f87171", fontSize: 12, lineHeight: 1.6 }}>
1493
- {state.message}
1494
- </div>
1495
- )}
1496
- {success && (
1497
- <>
1498
- <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 16px", borderBottom: "1px solid var(--border)", flexShrink: 0 }}>
1499
- <span style={{ fontSize: 11, color: "var(--text-dim)", flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
1500
- {success.piModels.length} models from {success.endpoint}
1501
- </span>
1502
- <button
1503
- onClick={toggleAll}
1504
- style={{ padding: "2px 8px", fontSize: 11, background: "var(--bg-hover)", border: "1px solid var(--border)", borderRadius: 5, color: "var(--text-muted)", cursor: "pointer" }}
1505
- >
1506
- {selected.size === success.piModels.length ? "Clear" : "Select all"}
1507
- </button>
1508
- </div>
1509
- <div style={{ flex: 1, overflow: "auto" }}>
1510
- {success.piModels.map((model, index) => {
1511
- const remote = success.models[index];
1512
- const checked = selected.has(model.id);
1513
- return (
1514
- <label
1515
- key={model.id}
1516
- style={{
1517
- display: "grid",
1518
- gridTemplateColumns: "24px minmax(0, 1fr) 90px 90px 90px 70px",
1519
- gap: 10,
1520
- alignItems: "center",
1521
- padding: "8px 16px",
1522
- borderBottom: "1px solid rgba(127,127,127,0.12)",
1523
- background: checked ? "var(--bg-subtle)" : "transparent",
1524
- cursor: "pointer",
1525
- }}
1526
- >
1527
- <input
1528
- type="checkbox"
1529
- checked={checked}
1530
- onChange={() => toggleModel(model.id)}
1531
- style={{ width: 14, height: 14, accentColor: "var(--accent)" }}
1532
- />
1533
- <div style={{ minWidth: 0 }}>
1534
- <div style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{model.id}</div>
1535
- <div style={{ fontSize: 10, color: "var(--text-dim)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{remote?.ownedBy ?? "unknown"}</div>
1536
- </div>
1537
- <span style={{ fontSize: 11, color: "var(--text-muted)", fontFamily: "var(--font-mono)" }}>{model.contextWindow}</span>
1538
- <span style={{ fontSize: 11, color: "var(--text-muted)", fontFamily: "var(--font-mono)" }}>{model.maxTokens}</span>
1539
- <span style={{ fontSize: 10, color: model.reasoning ? "var(--accent)" : "var(--text-dim)" }}>{model.reasoning ? "reasoning" : ""}</span>
1540
- <span style={{ fontSize: 10, color: model.input?.includes("image") ? "var(--accent)" : "var(--text-dim)" }}>{model.input?.includes("image") ? "image" : ""}</span>
1541
- </label>
1542
- );
1543
- })}
1544
- </div>
1545
- </>
1546
- )}
1547
- </div>
1548
-
1549
- <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, padding: "10px 16px", borderTop: "1px solid var(--border)", flexShrink: 0 }}>
1550
- <span style={{ fontSize: 11, color: "var(--text-dim)" }}>{selectedCount} selected</span>
1551
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
1552
- <button onClick={onClose} style={{ padding: "6px 14px", background: "none", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text-muted)", cursor: "pointer", fontSize: 13 }}>
1553
- Cancel
1554
- </button>
1555
- <button
1556
- onClick={importSelected}
1557
- disabled={!canImport}
1558
- style={{
1559
- padding: "6px 16px",
1560
- background: canImport ? "var(--accent)" : "var(--bg-panel)",
1561
- border: "none",
1562
- borderRadius: 6,
1563
- color: canImport ? "#fff" : "var(--text-dim)",
1564
- cursor: canImport ? "pointer" : "not-allowed",
1565
- fontSize: 13,
1566
- fontWeight: 600,
1567
- }}
1568
- >
1569
- Import selected
1570
- </button>
1571
- </div>
1572
- </div>
1573
- </div>
1574
- </div>
1575
- );
1576
- }
1577
-
1578
- // ── Main component ────────────────────────────────────────────────────────────
1579
-
1580
- export function ModelsConfig({ onClose, embedded = false }: { onClose: () => void; embedded?: boolean }) {
1581
- const [config, setConfig] = useState<ModelsJson>({ providers: {} });
1582
- const [loading, setLoading] = useState(true);
1583
- const [saving, setSaving] = useState(false);
1584
- const [saveError, setSaveError] = useState<string | null>(null);
1585
- const [saveNotice, setSaveNotice] = useState<string | null>(null);
1586
- const [savedOk, setSavedOk] = useState(false);
1587
- const [selection, setSelection] = useState<Selection | null>(null);
1588
- const [oauthProviders, setOauthProviders] = useState<OAuthProvider[]>([]);
1589
- const [apiKeyProviders, setApiKeyProviders] = useState<ApiKeyProvider[]>([]);
1590
- const [pickerOpen, setPickerOpen] = useState(false);
1591
- const [discoverOpen, setDiscoverOpen] = useState(false);
1592
-
1593
- const loadOAuthProviders = useCallback(() => {
1594
- fetch("/api/auth/providers")
1595
- .then((r) => r.json())
1596
- .then((d: { providers: OAuthProvider[] }) => setOauthProviders(d.providers))
1597
- .catch(() => {});
1598
- }, []);
1599
-
1600
- const loadApiKeyProviders = useCallback(() => {
1601
- fetch("/api/auth/all-providers")
1602
- .then((r) => r.json())
1603
- .then((d: { providers: ApiKeyProvider[] }) => setApiKeyProviders(d.providers))
1604
- .catch(() => {});
1605
- }, []);
1606
-
1607
- useEffect(() => {
1608
- fetch("/api/models-config")
1609
- .then((r) => r.json())
1610
- .then((d: ModelsJson) => {
1611
- const normalized = d.providers ? d : { ...d, providers: {} };
1612
- setConfig(normalized);
1613
- const keys = Object.keys(normalized.providers ?? {});
1614
- if (keys.length > 0) setSelection({ type: "provider", name: keys[0] });
1615
- })
1616
- .catch(() => setConfig({ providers: {} }))
1617
- .finally(() => setLoading(false));
1618
- loadOAuthProviders();
1619
- loadApiKeyProviders();
1620
- }, [loadOAuthProviders, loadApiKeyProviders]);
1621
-
1622
- const addCustomProvider = useCallback(() => {
1623
- let finalName = "custom";
1624
- let n = 1;
1625
- while (config.providers?.[finalName]) finalName = `custom-${n++}`;
1626
- setConfig((prev) => ({ ...prev, providers: { ...(prev.providers ?? {}), [finalName]: { baseUrl: "" } } }));
1627
- setSelection({ type: "provider", name: finalName });
1628
- }, [config.providers]);
1629
-
1630
- const updateProvider = useCallback((name: string, p: ProviderEntry) => {
1631
- setConfig((prev) => ({ ...prev, providers: { ...(prev.providers ?? {}), [name]: p } }));
1632
- }, []);
1633
-
1634
- const renameProvider = useCallback((oldName: string, newName: string) => {
1635
- setConfig((prev) => {
1636
- const entries = Object.entries(prev.providers ?? {});
1637
- const idx = entries.findIndex(([k]) => k === oldName);
1638
- if (idx === -1) return prev;
1639
- entries[idx] = [newName, entries[idx][1]];
1640
- return { ...prev, providers: Object.fromEntries(entries) };
1641
- });
1642
- setSelection((prev) => {
1643
- if (!prev) return prev;
1644
- if (prev.type === "provider" && prev.name === oldName) return { type: "provider", name: newName };
1645
- if (prev.type === "model" && prev.providerName === oldName) return { ...prev, providerName: newName };
1646
- return prev;
1647
- });
1648
- }, []);
1649
-
1650
- const deleteProvider = useCallback((name: string) => {
1651
- setConfig((prev) => {
1652
- const providers = { ...(prev.providers ?? {}) };
1653
- delete providers[name];
1654
- return { ...prev, providers };
1655
- });
1656
- setConfig((prev) => {
1657
- const remaining = Object.keys(prev.providers ?? {});
1658
- setSelection(remaining.length > 0 ? { type: "provider", name: remaining[0] } : null);
1659
- return prev;
1660
- });
1661
- }, []);
1662
-
1663
- const addModel = useCallback((providerName: string) => {
1664
- setConfig((prev) => {
1665
- const provider = prev.providers?.[providerName] ?? {};
1666
- const models = [...(provider.models ?? []), { id: "" }];
1667
- return { ...prev, providers: { ...(prev.providers ?? {}), [providerName]: { ...provider, models } } };
1668
- });
1669
- setConfig((prev) => {
1670
- const idx = (prev.providers?.[providerName]?.models?.length ?? 1) - 1;
1671
- setSelection({ type: "model", providerName, index: idx });
1672
- return prev;
1673
- });
1674
- }, []);
1675
-
1676
- const updateModel = useCallback((providerName: string, index: number, m: ModelEntry) => {
1677
- setConfig((prev) => {
1678
- const provider = prev.providers?.[providerName] ?? {};
1679
- const models = [...(provider.models ?? [])];
1680
- models[index] = m;
1681
- return { ...prev, providers: { ...(prev.providers ?? {}), [providerName]: { ...provider, models } } };
1682
- });
1683
- }, []);
1684
-
1685
- const removeModel = useCallback((providerName: string, index: number) => {
1686
- setConfig((prev) => {
1687
- const provider = prev.providers?.[providerName] ?? {};
1688
- const models = [...(provider.models ?? [])];
1689
- models.splice(index, 1);
1690
- return { ...prev, providers: { ...(prev.providers ?? {}), [providerName]: { ...provider, models: models.length ? models : undefined } } };
1691
- });
1692
- setSelection({ type: "provider", name: providerName });
1693
- }, []);
1694
-
1695
- const importDiscoveredProvider = useCallback((providerName: string, provider: ProviderEntry, selectedCount: number) => {
1696
- setConfig((prev) => ({
1697
- ...prev,
1698
- providers: {
1699
- ...(prev.providers ?? {}),
1700
- [providerName]: provider,
1701
- },
1702
- }));
1703
- setSelection({ type: "provider", name: providerName });
1704
- setSavedOk(false);
1705
- setSaveError(null);
1706
- setSaveNotice(`${selectedCount} model${selectedCount === 1 ? "" : "s"} imported. Click Save to write providers.json.`);
1707
- }, []);
1708
-
1709
- const handleSave = useCallback(async () => {
1710
- setSaving(true);
1711
- setSaveError(null);
1712
- setSaveNotice(null);
1713
- setSavedOk(false);
1714
- try {
1715
- const res = await fetch("/api/models-config", {
1716
- method: "PUT",
1717
- headers: { "Content-Type": "application/json" },
1718
- body: JSON.stringify({
1719
- providers: config.providers ?? {},
1720
- }),
1721
- });
1722
- const d = await res.json() as { success?: boolean; error?: string };
1723
- if (!res.ok || d.error) setSaveError(d.error ?? `HTTP ${res.status}`);
1724
- else { setSavedOk(true); setTimeout(() => setSavedOk(false), 2000); }
1725
- } catch (e) {
1726
- setSaveError(String(e));
1727
- } finally {
1728
- setSaving(false);
1729
- }
1730
- }, [config]);
1731
-
1732
- const providers = Object.entries(config.providers ?? {});
1733
- const activeOAuth = oauthProviders.filter((p) => p.loggedIn);
1734
- const activeApiKey = apiKeyProviders.filter((p) => p.configured);
1735
-
1736
- // Resolve current detail
1737
- const detailContent = (() => {
1738
- if (!selection) return null;
1739
- if (selection.type === "oauth") {
1740
- const p = oauthProviders.find((p) => p.id === selection.providerId);
1741
- if (!p) return null;
1742
- return <OAuthDetail key={p.id} provider={p} onRefresh={loadOAuthProviders} />;
1743
- }
1744
- if (selection.type === "apikey") {
1745
- const p = apiKeyProviders.find((p) => p.id === selection.providerId);
1746
- if (!p) return null;
1747
- return <ApiKeyDetail key={p.id} provider={p} onRefresh={loadApiKeyProviders} />;
1748
- }
1749
- if (selection.type === "provider") {
1750
- const provider = config.providers?.[selection.name];
1751
- if (!provider) return null;
1752
- return (
1753
- <ProviderDetail
1754
- key={selection.name}
1755
- name={selection.name}
1756
- provider={provider}
1757
- onChange={(p) => updateProvider(selection.name, p)}
1758
- onRename={(n) => renameProvider(selection.name, n)}
1759
- onDelete={() => deleteProvider(selection.name)}
1760
- />
1761
- );
1762
- }
1763
- const provider = config.providers?.[selection.providerName];
1764
- const model = provider?.models?.[selection.index];
1765
- if (!model) return null;
1766
- return (
1767
- <ModelDetail
1768
- key={`${selection.providerName}-${selection.index}`}
1769
- providerName={selection.providerName}
1770
- provider={provider}
1771
- model={model}
1772
- onChange={(m) => updateModel(selection.providerName, selection.index, m)}
1773
- onDelete={() => removeModel(selection.providerName, selection.index)}
1774
- />
1775
- );
1776
- })();
1777
-
1778
- return (
1779
- <>
1780
- <div
1781
- style={embedded
1782
- ? { height: "100%", minHeight: 0, display: "flex", alignItems: "stretch", justifyContent: "stretch" }
1783
- : { position: "fixed", inset: 0, zIndex: 1000, background: "rgba(0,0,0,0.35)", display: "flex", alignItems: "center", justifyContent: "center" }}
1784
- onClick={(e) => { if (!embedded && e.target === e.currentTarget) onClose(); }}>
1785
- <div style={{
1786
- width: embedded ? "100%" : 860,
1787
- height: embedded ? "100%" : "78vh",
1788
- background: "var(--bg)",
1789
- border: embedded ? "none" : "1px solid var(--border)",
1790
- borderRadius: embedded ? 0 : 10,
1791
- display: "flex",
1792
- flexDirection: "column",
1793
- boxShadow: embedded ? "none" : "0 8px 32px rgba(0,0,0,0.18)",
1794
- overflow: "hidden",
1795
- }}>
1796
-
1797
- {/* Header */}
1798
- {!embedded && <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "12px 18px", borderBottom: "1px solid var(--border)", flexShrink: 0 }}>
1799
- <div style={{ display: "flex", alignItems: "baseline", gap: 10 }}>
1800
- <span style={{ fontSize: 15, fontWeight: 700, color: "var(--text)" }}>Models</span>
1801
- <code style={{ fontSize: 11, color: "var(--text-muted)", fontFamily: "var(--font-mono)" }}>~/.config/annodex/providers.json</code>
1802
- </div>
1803
- <button onClick={onClose} style={{ background: "none", border: "none", color: "var(--text-muted)", cursor: "pointer", fontSize: 20, lineHeight: 1, padding: "2px 6px" }}>×</button>
1804
- </div>}
1805
-
1806
- {/* Body */}
1807
- <div style={{ flex: 1, display: "flex", overflow: "hidden" }}>
1808
-
1809
- {/* Left: tree */}
1810
- <div style={{ width: 210, borderRight: "1px solid var(--border)", display: "flex", flexDirection: "column", flexShrink: 0, background: "var(--bg-panel)" }}>
1811
- <div style={{ flex: 1, overflowY: "auto", padding: "8px 6px" }}>
1812
- {/* Active OAuth subscriptions */}
1813
- {activeOAuth.map((p) => {
1814
- const isSelected = selection?.type === "oauth" && selection.providerId === p.id;
1815
- return (
1816
- <div
1817
- key={p.id}
1818
- onClick={() => setSelection({ type: "oauth", providerId: p.id })}
1819
- style={{ display: "flex", alignItems: "center", gap: 7, padding: "5px 8px", borderRadius: 5, cursor: "pointer", background: isSelected ? "var(--bg-selected)" : "none" }}
1820
- onMouseEnter={(e) => { if (!isSelected) e.currentTarget.style.background = "var(--bg-hover)"; }}
1821
- onMouseLeave={(e) => { if (!isSelected) e.currentTarget.style.background = "none"; }}
1822
- >
1823
- <ProviderIcon id={p.id} size={16} />
1824
- <span style={{ fontSize: 12, color: "var(--text)", flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{p.name}</span>
1825
- </div>
1826
- );
1827
- })}
1828
-
1829
- {/* Active API key providers */}
1830
- {activeApiKey.map((p) => {
1831
- const isSelected = selection?.type === "apikey" && selection.providerId === p.id;
1832
- return (
1833
- <div
1834
- key={p.id}
1835
- onClick={() => setSelection({ type: "apikey", providerId: p.id })}
1836
- style={{ display: "flex", alignItems: "center", gap: 7, padding: "5px 8px", borderRadius: 5, cursor: "pointer", background: isSelected ? "var(--bg-selected)" : "none" }}
1837
- onMouseEnter={(e) => { if (!isSelected) e.currentTarget.style.background = "var(--bg-hover)"; }}
1838
- onMouseLeave={(e) => { if (!isSelected) e.currentTarget.style.background = "none"; }}
1839
- >
1840
- <ProviderIcon id={p.id} size={16} />
1841
- <span style={{ fontSize: 12, color: "var(--text)", flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{p.displayName}</span>
1842
- </div>
1843
- );
1844
- })}
1845
-
1846
- {/* Divider before custom providers, only when there are active managed providers */}
1847
- {(activeOAuth.length > 0 || activeApiKey.length > 0) && providers.length > 0 && (
1848
- <div style={{ margin: "4px 8px", borderTop: "1px solid var(--border)" }} />
1849
- )}
1850
-
1851
- {/* Custom providers */}
1852
- {loading ? (
1853
- <div style={{ padding: "10px 8px", fontSize: 12, color: "var(--text-muted)" }}>Loading…</div>
1854
- ) : providers.map(([pName, pData]) => {
1855
- const isProviderSelected = selection?.type === "provider" && selection.name === pName;
1856
- const models = pData.models ?? [];
1857
- return (
1858
- <div key={pName} style={{ marginBottom: 2 }}>
1859
- {/* Provider row */}
1860
- <div
1861
- onClick={() => setSelection({ type: "provider", name: pName })}
1862
- style={{ display: "flex", alignItems: "center", gap: 6, padding: "7px 8px", borderRadius: 5, cursor: "pointer", background: isProviderSelected ? "var(--bg-selected)" : "none" }}
1863
- onMouseEnter={(e) => { if (!isProviderSelected) e.currentTarget.style.background = "var(--bg-hover)"; }}
1864
- onMouseLeave={(e) => { if (!isProviderSelected) e.currentTarget.style.background = "none"; }}
1865
- >
1866
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ color: "var(--text-dim)", flexShrink: 0 }}>
1867
- <rect x="4" y="4" width="16" height="16" rx="2" /><rect x="9" y="9" width="6" height="6" />
1868
- <line x1="9" y1="1" x2="9" y2="4" /><line x1="15" y1="1" x2="15" y2="4" />
1869
- <line x1="9" y1="20" x2="9" y2="23" /><line x1="15" y1="20" x2="15" y2="23" />
1870
- <line x1="20" y1="9" x2="23" y2="9" /><line x1="20" y1="14" x2="23" y2="14" />
1871
- <line x1="1" y1="9" x2="4" y2="9" /><line x1="1" y1="14" x2="4" y2="14" />
1872
- </svg>
1873
- <span style={{ fontSize: 12, fontWeight: isProviderSelected ? 600 : 400, color: "var(--text)", fontFamily: "var(--font-mono)", flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
1874
- {pName}
1875
- </span>
1876
- </div>
1877
-
1878
- {/* Model rows */}
1879
- {models.map((m, i) => {
1880
- const isModelSelected = selection?.type === "model" && selection.providerName === pName && selection.index === i;
1881
- return (
1882
- <div
1883
- key={i}
1884
- onClick={() => setSelection({ type: "model", providerName: pName, index: i })}
1885
- style={{ display: "flex", alignItems: "center", gap: 6, padding: "5px 8px 5px 26px", borderRadius: 5, cursor: "pointer", background: isModelSelected ? "var(--bg-selected)" : "none" }}
1886
- onMouseEnter={(e) => { if (!isModelSelected) e.currentTarget.style.background = "var(--bg-hover)"; }}
1887
- onMouseLeave={(e) => { if (!isModelSelected) e.currentTarget.style.background = "none"; }}
1888
- >
1889
- <span style={{ fontSize: 11, fontFamily: "var(--font-mono)", color: m.id ? "var(--text-muted)" : "var(--text-dim)", flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
1890
- {m.id || "new model"}
1891
- </span>
1892
- {m.reasoning && (
1893
- <span style={{ fontSize: 9, padding: "1px 4px", background: "rgba(99,102,241,0.12)", color: "rgba(99,102,241,0.8)", borderRadius: 3, flexShrink: 0 }}>T</span>
1894
- )}
1895
- </div>
1896
- );
1897
- })}
1898
-
1899
- {/* Add model button */}
1900
- <div
1901
- onClick={(e) => { e.stopPropagation(); addModel(pName); }}
1902
- style={{ display: "flex", alignItems: "center", gap: 4, padding: "4px 8px 4px 26px", borderRadius: 5, cursor: "pointer", color: "var(--text-dim)" }}
1903
- onMouseEnter={(e) => { e.currentTarget.style.color = "var(--accent)"; e.currentTarget.style.background = "var(--bg-hover)"; }}
1904
- onMouseLeave={(e) => { e.currentTarget.style.color = "var(--text-dim)"; e.currentTarget.style.background = "none"; }}
1905
- >
1906
- <span style={{ fontSize: 11 }}>+ model</span>
1907
- </div>
1908
- </div>
1909
- );
1910
- })}
1911
- </div>
1912
-
1913
- {/* Add provider */}
1914
- <div style={{ borderTop: "1px solid var(--border)", padding: "8px 6px" }}>
1915
- <button onClick={() => setPickerOpen(true)} style={{
1916
- display: "flex", alignItems: "center", justifyContent: "center", gap: 5,
1917
- width: "100%", padding: "6px 0", background: "none", border: "1px dashed var(--border)", borderRadius: 5,
1918
- color: "var(--text-muted)", cursor: "pointer", fontSize: 12,
1919
- }}
1920
- onMouseEnter={(e) => { e.currentTarget.style.borderColor = "var(--accent)"; e.currentTarget.style.color = "var(--accent)"; }}
1921
- onMouseLeave={(e) => { e.currentTarget.style.borderColor = "var(--border)"; e.currentTarget.style.color = "var(--text-muted)"; }}
1922
- >
1923
- + Add provider
1924
- </button>
1925
- </div>
1926
- </div>
1927
-
1928
- {/* Right: detail */}
1929
- <div style={{ flex: 1, overflowY: "auto", padding: 20 }}>
1930
- {loading ? null : detailContent ?? (
1931
- <div style={{ height: "100%", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--text-dim)", fontSize: 13 }}>
1932
- Select a provider or model
1933
- </div>
1934
- )}
1935
- </div>
1936
- </div>
1937
-
1938
- {/* Footer */}
1939
- <div style={{ display: "flex", alignItems: "center", justifyContent: "flex-end", gap: 10, padding: "10px 18px", borderTop: "1px solid var(--border)", flexShrink: 0 }}>
1940
- {saveError ? (
1941
- <span style={{ fontSize: 12, color: "#f87171", flex: 1 }}>{saveError}</span>
1942
- ) : saveNotice ? (
1943
- <span style={{ fontSize: 12, color: "var(--text-muted)", flex: 1 }}>{saveNotice}</span>
1944
- ) : null}
1945
- <button onClick={onClose} style={{ padding: "6px 14px", background: "none", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text-muted)", cursor: "pointer", fontSize: 13 }}>
1946
- Cancel
1947
- </button>
1948
- <button onClick={handleSave} disabled={saving || savedOk} style={{
1949
- position: "relative",
1950
- padding: "6px 16px",
1951
- minWidth: 92,
1952
- background: savedOk ? "#16a34a" : saving ? "var(--bg-panel)" : "var(--accent)",
1953
- border: "none", borderRadius: 6,
1954
- color: savedOk ? "#fff" : saving ? "var(--text-muted)" : "#fff",
1955
- cursor: (saving || savedOk) ? "default" : "pointer", fontSize: 13, fontWeight: 600,
1956
- display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 6,
1957
- transition: "background-color 0.2s ease, color 0.2s ease",
1958
- animation: savedOk ? "saved-pop 0.45s ease" : undefined,
1959
- }}>
1960
- {savedOk && (
1961
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"
1962
- style={{ strokeDasharray: 18, animation: "saved-check-draw 0.35s ease forwards", flexShrink: 0 }}>
1963
- <polyline points="20 6 9 17 4 12" />
1964
- </svg>
1965
- )}
1966
- <span>{savedOk ? "Saved" : saving ? "Saving…" : "Save"}</span>
1967
- </button>
1968
- </div>
1969
- </div>
1970
- </div>
1971
- {pickerOpen && (
1972
- <AddProviderPicker
1973
- oauthProviders={oauthProviders}
1974
- apiKeyProviders={apiKeyProviders}
1975
- onSelectOAuth={(id) => setSelection({ type: "oauth", providerId: id })}
1976
- onSelectApiKey={(id) => setSelection({ type: "apikey", providerId: id })}
1977
- onAddCustom={addCustomProvider}
1978
- onDiscover={() => setDiscoverOpen(true)}
1979
- onClose={() => setPickerOpen(false)}
1980
- />
1981
- )}
1982
- {discoverOpen && (
1983
- <DiscoverModelsDialog
1984
- existingProviders={config.providers ?? {}}
1985
- onImport={importDiscoveredProvider}
1986
- onClose={() => setDiscoverOpen(false)}
1987
- />
1988
- )}
1989
- </>
1990
- );
1991
- }