ompchamber 2.0.2 → 3.0.0

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 (329) hide show
  1. package/README.md +7 -6
  2. package/bunfig.toml +11 -0
  3. package/dist/client/index.html +16 -5
  4. package/dist/client/index.js +1393 -0
  5. package/dist/client/static/js/index-prbmx3yj.js +5123 -0
  6. package/dist/client/static/js/index-ranztes9.css +1 -0
  7. package/index.html +54 -0
  8. package/package.json +9 -10
  9. package/public/apple-touch-icon.png +0 -0
  10. package/public/icon-192x192.png +0 -0
  11. package/public/icon.svg +60 -0
  12. package/public/manifest.webmanifest +38 -0
  13. package/src/cli/lib/commands/serve.js +9 -5
  14. package/src/cli/lib/runtime.js +18 -7
  15. package/src/client/components/common/provider-icon/glyphs.ts +118 -0
  16. package/src/client/components/common/provider-icon/index.tsx +85 -0
  17. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
  18. package/src/client/components/layout/desktop-layout/index.tsx +2 -0
  19. package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
  20. package/src/client/components/mobile/SessionSidebar.tsx +10 -0
  21. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +0 -1
  22. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -1
  23. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +3 -0
  24. package/src/client/components/settings/categories/ProviderSettings.tsx +45 -4
  25. package/src/client/components/settings/categories/UsageSettings.tsx +22 -7
  26. package/src/client/components/settings/categories/provider-settings/AddModelModal.tsx +322 -0
  27. package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +152 -56
  28. package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +52 -1
  29. package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +133 -0
  30. package/src/client/components/settings/categories/provider-settings/DialectFields.tsx +136 -0
  31. package/src/client/components/settings/categories/provider-settings/EffortLadder.tsx +89 -0
  32. package/src/client/components/settings/categories/provider-settings/Header.tsx +2 -2
  33. package/src/client/components/settings/categories/provider-settings/LoginModal.tsx +2 -2
  34. package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +22 -2
  35. package/src/client/components/settings/categories/provider-settings/PresetPicker.tsx +82 -0
  36. package/src/client/components/settings/categories/provider-settings/PriceFields.tsx +107 -0
  37. package/src/client/components/settings/categories/provider-settings/ReconnectModal.tsx +43 -22
  38. package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +2 -2
  39. package/src/client/components/settings/categories/usage-settings/LimitsSection.tsx +115 -0
  40. package/src/client/components/settings/categories/usage-settings/ProviderCard.tsx +19 -26
  41. package/src/client/components/settings/categories/usage-settings/ProviderDetail.tsx +29 -22
  42. package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +60 -48
  43. package/src/client/components/settings/categories/usage-settings/format.ts +36 -0
  44. package/src/client/components/settings/categories/usage-settings/providers.ts +52 -28
  45. package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +1 -1
  46. package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +1 -1
  47. package/src/client/components/workspace/diff-panel/Toolbar.tsx +71 -38
  48. package/src/client/components/workspace/diff-panel/index.tsx +139 -53
  49. package/src/client/components/workspace/editor/Tabs.tsx +2 -2
  50. package/src/client/components/workspace/editor/Toolbar.tsx +111 -26
  51. package/src/client/components/workspace/editor/index.tsx +27 -9
  52. package/src/client/components/workspace/model-dropdown/Item.tsx +13 -23
  53. package/src/client/components/workspace/model-dropdown/Panel.tsx +1 -1
  54. package/src/client/components/workspace/model-dropdown/Section.tsx +13 -4
  55. package/src/client/components/workspace/model-dropdown/index.tsx +7 -3
  56. package/src/client/components/workspace/usage-panel/ProviderSelect.tsx +10 -5
  57. package/src/client/components/workspace/usage-panel/index.tsx +36 -18
  58. package/src/client/data/samples/dialogue-session.ts +1 -1
  59. package/src/client/data/samples/sample-tools/other.ts +1 -1
  60. package/src/client/data/settings/provider.ts +141 -9
  61. package/src/client/hooks/editor/use-file-editor.ts +24 -27
  62. package/src/client/hooks/models/use-provider-names.ts +1 -1
  63. package/src/client/hooks/settings/providers/actions.ts +337 -0
  64. package/src/client/hooks/settings/providers/api.ts +69 -0
  65. package/src/client/hooks/settings/providers/index.ts +138 -0
  66. package/src/client/hooks/settings/providers/messages.ts +77 -0
  67. package/src/client/hooks/settings/useUsageReport.ts +8 -3
  68. package/src/client/hooks/ui/theme.ts +3 -3
  69. package/src/client/hooks/workspace/file-tabs.ts +62 -11
  70. package/src/client/stubs/elkjs.ts +6 -1
  71. package/src/server/index.ts +20 -2
  72. package/src/server/lib/assets/font-css.server.ts +86 -0
  73. package/src/server/lib/assets/fonts.server.ts +117 -0
  74. package/src/server/lib/bundler/css.ts +116 -0
  75. package/src/server/lib/lifecycle/shell-route.ts +17 -0
  76. package/src/server/lib/models/provider-listing.server.ts +257 -0
  77. package/src/server/lib/models/provider-registry.server.test.ts +122 -0
  78. package/src/server/lib/models/provider-registry.server.ts +76 -25
  79. package/src/server/lib/omp/config/models-config.ts +11 -0
  80. package/src/server/lib/omp/config/models-validation.test.ts +137 -0
  81. package/src/server/lib/omp/config/models-validation.ts +111 -2
  82. package/src/server/lib/omp/config/provider-seeds.test.ts +167 -0
  83. package/src/server/lib/omp/config/provider-seeds.ts +176 -0
  84. package/src/server/lib/omp/config/providers.ts +94 -100
  85. package/src/server/lib/omp/core/auth-credentials.ts +45 -0
  86. package/src/server/lib/omp/session/{usage.ts → usage/aggregate.ts} +1 -118
  87. package/src/server/lib/omp/session/usage/shaping.ts +130 -0
  88. package/src/server/lib/usage/deepseek.ts +4 -5
  89. package/src/server/lib/usage/kenari.ts +7 -6
  90. package/src/server/lib/usage/omp-usage.server.test.ts +126 -0
  91. package/src/server/lib/usage/omp-usage.server.ts +194 -0
  92. package/src/server/lib/usage/provider-key.ts +81 -3
  93. package/src/server/lib/usage/providers.server.test.ts +47 -0
  94. package/src/server/lib/usage/providers.server.ts +208 -0
  95. package/src/server/lib/usage/vendor-balance.server.test.ts +104 -0
  96. package/src/server/lib/usage/vendor-balance.server.ts +284 -0
  97. package/src/server/plugins/shell.server.ts +56 -0
  98. package/src/server/plugins/ssr.ts +24 -36
  99. package/src/server/plugins/static.test.ts +26 -29
  100. package/src/server/plugins/static.ts +40 -19
  101. package/src/server/routes/settings/index.ts +2 -0
  102. package/src/server/routes/settings/provider-model.ts +107 -0
  103. package/src/server/routes/settings/provider-models.ts +65 -202
  104. package/src/server/routes/settings/providers.ts +22 -7
  105. package/src/server/routes/settings/usage.ts +125 -45
  106. package/src/server/routes/telemetry/tokens.ts +2 -1
  107. package/src/shared/lib/fs/diff-parser.test.ts +116 -0
  108. package/src/shared/lib/fs/diff-parser.ts +86 -33
  109. package/src/shared/lib/models/{provider-connection.ts → provider/connection.ts} +37 -0
  110. package/src/shared/lib/models/provider/dialect.ts +179 -0
  111. package/src/shared/lib/models/provider/glyph.test.ts +116 -0
  112. package/src/shared/lib/models/provider/glyph.ts +200 -0
  113. package/src/shared/lib/models/{provider-models.ts → provider/models.ts} +91 -6
  114. package/src/shared/lib/models/provider/presets.test.ts +172 -0
  115. package/src/shared/lib/models/provider/presets.ts +83 -0
  116. package/src/shared/lib/workspace/file-tab-id.ts +29 -0
  117. package/src/shared/types/settings/provider.ts +81 -0
  118. package/src/shared/types/settings/usage.ts +88 -14
  119. package/tsconfig.json +3 -1
  120. package/dist/client/static/css/index.5f630421b7.css +0 -2
  121. package/dist/client/static/font/KaTeX_AMS-Regular.a79f1c3119.woff2 +0 -0
  122. package/dist/client/static/font/KaTeX_Caligraphic-Bold.ec17d13264.woff2 +0 -0
  123. package/dist/client/static/font/KaTeX_Caligraphic-Regular.55fac25845.woff2 +0 -0
  124. package/dist/client/static/font/KaTeX_Fraktur-Bold.d42a5579b0.woff2 +0 -0
  125. package/dist/client/static/font/KaTeX_Fraktur-Regular.d3c882a649.woff2 +0 -0
  126. package/dist/client/static/font/KaTeX_Main-Bold.c3fb5ac22f.woff2 +0 -0
  127. package/dist/client/static/font/KaTeX_Main-BoldItalic.6f2bb1dff2.woff2 +0 -0
  128. package/dist/client/static/font/KaTeX_Main-Italic.8916142bec.woff2 +0 -0
  129. package/dist/client/static/font/KaTeX_Main-Regular.0462f03bdf.woff2 +0 -0
  130. package/dist/client/static/font/KaTeX_Math-BoldItalic.572d331f69.woff2 +0 -0
  131. package/dist/client/static/font/KaTeX_Math-Italic.f28c23acad.woff2 +0 -0
  132. package/dist/client/static/font/KaTeX_SansSerif-Bold.8c5b5494b6.woff2 +0 -0
  133. package/dist/client/static/font/KaTeX_SansSerif-Italic.3b1e59b3ba.woff2 +0 -0
  134. package/dist/client/static/font/KaTeX_SansSerif-Regular.ba21ed5f84.woff2 +0 -0
  135. package/dist/client/static/font/KaTeX_Script-Regular.03e9641d6f.woff2 +0 -0
  136. package/dist/client/static/font/KaTeX_Size1-Regular.eae34984b3.woff2 +0 -0
  137. package/dist/client/static/font/KaTeX_Size2-Regular.5916a24fa3.woff2 +0 -0
  138. package/dist/client/static/font/KaTeX_Size4-Regular.10d95fd3a2.woff2 +0 -0
  139. package/dist/client/static/font/KaTeX_Typewriter-Regular.a8709e3622.woff2 +0 -0
  140. package/dist/client/static/font/fira-code-latin-400-normal.2655b10c55.woff2 +0 -0
  141. package/dist/client/static/font/fira-code-latin-500-normal.92a991f204.woff2 +0 -0
  142. package/dist/client/static/font/fira-code-latin-600-normal.43788eb7f1.woff2 +0 -0
  143. package/dist/client/static/font/fira-code-latin-700-normal.2667215d33.woff2 +0 -0
  144. package/dist/client/static/js/77.0d81c74cf0.js +0 -139
  145. package/dist/client/static/js/77.0d81c74cf0.js.LICENSE.txt +0 -8
  146. package/dist/client/static/js/async/0.8e59f2eb5b.js +0 -1
  147. package/dist/client/static/js/async/0k.5852e6b4fa.js +0 -1
  148. package/dist/client/static/js/async/0w.9ed36d5860.js +0 -1
  149. package/dist/client/static/js/async/1.f387c12b63.js +0 -1
  150. package/dist/client/static/js/async/12.d091f91c99.js +0 -1
  151. package/dist/client/static/js/async/13.8f810a8a6c.js +0 -1
  152. package/dist/client/static/js/async/19.6fe909d55a.js +0 -160
  153. package/dist/client/static/js/async/1b.23f1141a38.js +0 -1
  154. package/dist/client/static/js/async/1k.973e8e21fc.js +0 -2
  155. package/dist/client/static/js/async/1k.973e8e21fc.js.LICENSE.txt +0 -11
  156. package/dist/client/static/js/async/1p.0d5cddda96.js +0 -1
  157. package/dist/client/static/js/async/2.b589f1733b.js +0 -1
  158. package/dist/client/static/js/async/2v.96165101de.js +0 -1
  159. package/dist/client/static/js/async/3.0cacc08423.js +0 -1
  160. package/dist/client/static/js/async/37.b28b2f61cd.js +0 -1
  161. package/dist/client/static/js/async/3d.5085fb913f.js +0 -1
  162. package/dist/client/static/js/async/3j.357c80ff38.js +0 -2
  163. package/dist/client/static/js/async/3j.357c80ff38.js.LICENSE.txt +0 -11
  164. package/dist/client/static/js/async/3p.bcf32a0983.js +0 -1
  165. package/dist/client/static/js/async/3r.3a69e98ee0.js +0 -64
  166. package/dist/client/static/js/async/3u.1228460e2a.js +0 -1
  167. package/dist/client/static/js/async/3y.3530b824ad.js +0 -198
  168. package/dist/client/static/js/async/3y.3530b824ad.js.LICENSE.txt +0 -5
  169. package/dist/client/static/js/async/4.9442eeb472.js +0 -1
  170. package/dist/client/static/js/async/4e.678285ca55.js +0 -1
  171. package/dist/client/static/js/async/4k.33a0c7eeb4.js +0 -1
  172. package/dist/client/static/js/async/4m.928a1dde85.js +0 -31
  173. package/dist/client/static/js/async/4m.928a1dde85.js.LICENSE.txt +0 -11
  174. package/dist/client/static/js/async/4q.aad20dab25.js +0 -1
  175. package/dist/client/static/js/async/5.ebaf8b89cc.js +0 -36
  176. package/dist/client/static/js/async/51.95bcae98af.js +0 -2
  177. package/dist/client/static/js/async/51.95bcae98af.js.LICENSE.txt +0 -6
  178. package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
  179. package/dist/client/static/js/async/5j.d6407c4248.js +0 -287
  180. package/dist/client/static/js/async/5o.88316d4460.js +0 -62
  181. package/dist/client/static/js/async/6.f034c9b528.js +0 -1
  182. package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
  183. package/dist/client/static/js/async/6o.5b1a06a382.js +0 -1
  184. package/dist/client/static/js/async/7.e4a9288dd0.js +0 -1
  185. package/dist/client/static/js/async/73.308dff4da8.js +0 -1
  186. package/dist/client/static/js/async/7e.a1cc9bfd88.js +0 -1
  187. package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
  188. package/dist/client/static/js/async/7l.fadbbc0583.js +0 -1
  189. package/dist/client/static/js/async/8.5067439d8c.js +0 -1
  190. package/dist/client/static/js/async/8j.ea110663fb.js +0 -1
  191. package/dist/client/static/js/async/a.bce4542392.js +0 -1
  192. package/dist/client/static/js/async/aa.65d3852576.js +0 -1
  193. package/dist/client/static/js/async/ar.9303e5f2e8.js +0 -2
  194. package/dist/client/static/js/async/ar.9303e5f2e8.js.LICENSE.txt +0 -11
  195. package/dist/client/static/js/async/az.d656062b0f.js +0 -1
  196. package/dist/client/static/js/async/b.b4f6f06515.js +0 -1
  197. package/dist/client/static/js/async/bi.09688ba75c.js +0 -1
  198. package/dist/client/static/js/async/bq.586bc738d1.js +0 -1
  199. package/dist/client/static/js/async/c.797d933882.js +0 -1
  200. package/dist/client/static/js/async/c0.0af1e640d3.js +0 -24
  201. package/dist/client/static/js/async/ca.8a52cfd447.js +0 -1
  202. package/dist/client/static/js/async/cf.d34fff5d67.js +0 -1
  203. package/dist/client/static/js/async/ct.b74928f7a3.js +0 -1
  204. package/dist/client/static/js/async/cx.3cb0242a3a.js +0 -226
  205. package/dist/client/static/js/async/d.a5fc8fe1b3.js +0 -1
  206. package/dist/client/static/js/async/da.f62c70f6c8.js +0 -1
  207. package/dist/client/static/js/async/dp.1725d6432c.js +0 -1
  208. package/dist/client/static/js/async/e.93214d622b.js +0 -79
  209. package/dist/client/static/js/async/f.ab50467719.js +0 -19
  210. package/dist/client/static/js/async/f.ab50467719.js.LICENSE.txt +0 -12
  211. package/dist/client/static/js/async/f7.46971e9d6d.js +0 -2
  212. package/dist/client/static/js/async/f7.46971e9d6d.js.LICENSE.txt +0 -6
  213. package/dist/client/static/js/async/f8.bbf0dbae70.js +0 -1
  214. package/dist/client/static/js/async/g.cb81980498.js +0 -2
  215. package/dist/client/static/js/async/g.cb81980498.js.LICENSE.txt +0 -11
  216. package/dist/client/static/js/async/h.4e790471e4.js +0 -27
  217. package/dist/client/static/js/async/h1.393f066d09.js +0 -1
  218. package/dist/client/static/js/async/h3.216e9798ab.js +0 -148
  219. package/dist/client/static/js/async/hd.0da931b622.js +0 -1
  220. package/dist/client/static/js/async/hk.ba7fb04d41.js +0 -1
  221. package/dist/client/static/js/async/hn.b645041e00.js +0 -1
  222. package/dist/client/static/js/async/ho.475a5e1ae4.js +0 -1
  223. package/dist/client/static/js/async/hp.e9fe2e395a.js +0 -2
  224. package/dist/client/static/js/async/hp.e9fe2e395a.js.LICENSE.txt +0 -17
  225. package/dist/client/static/js/async/hs.e7c59d8090.js +0 -41
  226. package/dist/client/static/js/async/i.7c148e8440.js +0 -1
  227. package/dist/client/static/js/async/id.573df20f29.js +0 -1
  228. package/dist/client/static/js/async/j.ab925882d8.js +0 -1
  229. package/dist/client/static/js/async/jd.63886a1f09.js +0 -1
  230. package/dist/client/static/js/async/jg.d86ebd4013.js +0 -86
  231. package/dist/client/static/js/async/jl.874d352278.js +0 -1
  232. package/dist/client/static/js/async/jll.11313b455a.js +0 -1
  233. package/dist/client/static/js/async/js.67182759ef.js +0 -1
  234. package/dist/client/static/js/async/jsc.23b70d6f40.js +0 -1
  235. package/dist/client/static/js/async/jse.334588ff5d.js +0 -1
  236. package/dist/client/static/js/async/jz.72eb929106.js +0 -77
  237. package/dist/client/static/js/async/k.3210286401.js +0 -217
  238. package/dist/client/static/js/async/ka.c1990ecaa3.js +0 -115
  239. package/dist/client/static/js/async/katex-vendor.de6a87c308.js +0 -1
  240. package/dist/client/static/js/async/ku.29c8a59d0c.js +0 -1
  241. package/dist/client/static/js/async/ky.8dab1dade0.js +0 -1
  242. package/dist/client/static/js/async/l.12dd34e1be.js +0 -1
  243. package/dist/client/static/js/async/l9.949710adaf.js +0 -83
  244. package/dist/client/static/js/async/l9.949710adaf.js.LICENSE.txt +0 -13
  245. package/dist/client/static/js/async/ls.9d9a01026c.js +0 -104
  246. package/dist/client/static/js/async/m.eb2e6752e2.js +0 -28
  247. package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +0 -11
  248. package/dist/client/static/js/async/m3.d2b05f9604.js +0 -2
  249. package/dist/client/static/js/async/m3.d2b05f9604.js.LICENSE.txt +0 -6
  250. package/dist/client/static/js/async/ma.2c261b04dd.js +0 -2
  251. package/dist/client/static/js/async/ma.2c261b04dd.js.LICENSE.txt +0 -6
  252. package/dist/client/static/js/async/me.8ace414f80.js +0 -23
  253. package/dist/client/static/js/async/ms.98960af4eb.js +0 -67
  254. package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +0 -12
  255. package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
  256. package/dist/client/static/js/async/n.a9f1c51701.js +0 -1
  257. package/dist/client/static/js/async/na.a36cd532d6.js +0 -1
  258. package/dist/client/static/js/async/nk.fc7e58325e.js +0 -1
  259. package/dist/client/static/js/async/o.36353dfb88.js +0 -1
  260. package/dist/client/static/js/async/oc.b73042e8bd.js +0 -8
  261. package/dist/client/static/js/async/og.8eb577893a.js +0 -193
  262. package/dist/client/static/js/async/p.48ecfcd14e.js +0 -1
  263. package/dist/client/static/js/async/px.fc6877924b.js +0 -1
  264. package/dist/client/static/js/async/q.22588845a9.js +0 -21
  265. package/dist/client/static/js/async/q.22588845a9.js.LICENSE.txt +0 -6
  266. package/dist/client/static/js/async/q5.97dc1481b6.js +0 -286
  267. package/dist/client/static/js/async/qs.2a9ddb3c72.js +0 -1
  268. package/dist/client/static/js/async/r.51010ad87c.js +0 -1
  269. package/dist/client/static/js/async/rd.cb6bf495bb.js +0 -2
  270. package/dist/client/static/js/async/rd.cb6bf495bb.js.LICENSE.txt +0 -4
  271. package/dist/client/static/js/async/s.0f395cdda5.js +0 -94
  272. package/dist/client/static/js/async/t.4dca0e2ba8.js +0 -1
  273. package/dist/client/static/js/async/t9.d44f57d518.js +0 -1
  274. package/dist/client/static/js/async/ta.973534bbec.js +0 -1
  275. package/dist/client/static/js/async/tr.e878658140.js +0 -309
  276. package/dist/client/static/js/async/u.0871ac5b22.js +0 -1
  277. package/dist/client/static/js/async/ud.3065bfee31.js +0 -2
  278. package/dist/client/static/js/async/ud.3065bfee31.js.LICENSE.txt +0 -4
  279. package/dist/client/static/js/async/up.ea990da0ff.js +0 -32
  280. package/dist/client/static/js/async/uw.8890abbabc.js +0 -1
  281. package/dist/client/static/js/async/v.7d28d3c241.js +0 -34
  282. package/dist/client/static/js/async/vp.9b13bf8610.js +0 -1
  283. package/dist/client/static/js/async/vt.42416b707d.js +0 -1
  284. package/dist/client/static/js/async/w.08ac210f60.js +0 -1
  285. package/dist/client/static/js/async/wt.ad92393a90.js +0 -1
  286. package/dist/client/static/js/async/x.563f72ca6b.js +0 -1
  287. package/dist/client/static/js/async/x0.475516418f.js +0 -1
  288. package/dist/client/static/js/async/x1.da8687159b.js +0 -90
  289. package/dist/client/static/js/async/xe.da8dcc2015.js +0 -2
  290. package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +0 -11
  291. package/dist/client/static/js/async/xl.a3c562a58c.js +0 -1
  292. package/dist/client/static/js/async/xn.6510c0abe2.js +0 -2
  293. package/dist/client/static/js/async/xn.6510c0abe2.js.LICENSE.txt +0 -6
  294. package/dist/client/static/js/async/xn1.83a7784fe1.js +0 -10
  295. package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +0 -11
  296. package/dist/client/static/js/async/xnl.feb0021e50.js +0 -2
  297. package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +0 -12
  298. package/dist/client/static/js/async/y.1a9ed7a5d8.js +0 -1
  299. package/dist/client/static/js/async/y6.43c57defe4.js +0 -1
  300. package/dist/client/static/js/async/yb.8c1342401d.js +0 -1
  301. package/dist/client/static/js/async/ybq.4491f2f2dc.js +0 -1
  302. package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
  303. package/dist/client/static/js/async/yq.917ddd7ba2.js +0 -1
  304. package/dist/client/static/js/async/ys.00687cf82d.js +0 -38
  305. package/dist/client/static/js/async/yv.d99d829136.js +0 -1
  306. package/dist/client/static/js/async/z.56f61a4374.js +0 -1
  307. package/dist/client/static/js/async/zi.5fdb07b86a.js +0 -24
  308. package/dist/client/static/js/async/zj.654f37e8ff.js +0 -1
  309. package/dist/client/static/js/async/zn.966d6f1cc6.js +0 -1
  310. package/dist/client/static/js/async/zo.26213b40c0.js +0 -1
  311. package/dist/client/static/js/async/zx.e9f4105e00.js +0 -1
  312. package/dist/client/static/js/index.e6a257402a.js +0 -38
  313. package/dist/client/static/js/index.e6a257402a.js.LICENSE.txt +0 -4
  314. package/dist/client/static/js/markdown-vendor.71e43278b3.js +0 -144
  315. package/dist/client/static/js/shiki-vendor.cb8452ddcd.js +0 -1
  316. package/src/client/components/settings/categories/provider-settings/Icons.tsx +0 -35
  317. package/src/client/hooks/settings/providers.ts +0 -326
  318. package/src/server/plugins/dev-assets.ts +0 -49
  319. package/src/shared/lib/code/editor-utils.ts +0 -16
  320. package/src/shared/lib/models/provider-presets.ts +0 -29
  321. /package/dist/client/{apple-touch-icon.png → static/apple-touch-icon-dskx5sr3.png} +0 -0
  322. /package/dist/client/{icon-192x192.png → static/icon-192x192-6gn5cr2f.png} +0 -0
  323. /package/dist/client/{icon.svg → static/icon-d07z9dtz.svg} +0 -0
  324. /package/dist/client/{manifest.webmanifest → static/manifest-bnyy75kt.webmanifest} +0 -0
  325. /package/{dist/client → public}/icon-512x512.png +0 -0
  326. /package/{dist/client → public}/icon-maskable-512x512.png +0 -0
  327. /package/{dist/client → public}/sw.js +0 -0
  328. /package/src/shared/lib/models/{provider-cleanup.ts → provider/cleanup.ts} +0 -0
  329. /package/src/shared/lib/models/{provider-label.ts → provider/label.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import type { ProviderItem } from '@/shared/types';
1
+ import type { PresetProviderOption, ProviderItem } from '@/shared/types/settings/provider';
2
2
 
3
3
  export const DEFAULT_PROVIDERS_LIST: ProviderItem[] = [
4
4
  {
@@ -8,6 +8,7 @@ export const DEFAULT_PROVIDERS_LIST: ProviderItem[] = [
8
8
  icon: 'deepseek',
9
9
  status: 'connected',
10
10
  configuredIn: 'auth credentials',
11
+ credentialSource: 'chamber',
11
12
  apiKey: 'sk-ds-••••••••••••••••••••38f1',
12
13
  baseUrl: 'https://api.deepseek.com/v1',
13
14
  models: [
@@ -59,6 +60,7 @@ export const DEFAULT_PROVIDERS_LIST: ProviderItem[] = [
59
60
  icon: 'agentrouter',
60
61
  status: 'connected',
61
62
  configuredIn: 'auth credentials',
63
+ credentialSource: 'chamber',
62
64
  apiKey: 'ar-live-•••••••••••••••••••89c2',
63
65
  baseUrl: 'https://router.agent.ai/v1',
64
66
  models: [
@@ -104,6 +106,7 @@ export const DEFAULT_PROVIDERS_LIST: ProviderItem[] = [
104
106
  icon: 'commandcode',
105
107
  status: 'connected',
106
108
  configuredIn: 'auth credentials',
109
+ credentialSource: 'chamber',
107
110
  apiKey: 'cc-key-••••••••••••••••••••7a12',
108
111
  baseUrl: 'https://api.commandcode.dev/v1',
109
112
  models: Array.from({ length: 41 }, (_, i) => {
@@ -142,6 +145,7 @@ export const DEFAULT_PROVIDERS_LIST: ProviderItem[] = [
142
145
  icon: 'opencode',
143
146
  status: 'connected',
144
147
  configuredIn: 'auth credentials',
148
+ credentialSource: 'chamber',
145
149
  apiKey: 'zen-live-•••••••••••••••••••44fa',
146
150
  baseUrl: 'https://zen.opencode.ai/v1',
147
151
  models: [
@@ -161,6 +165,7 @@ export const DEFAULT_PROVIDERS_LIST: ProviderItem[] = [
161
165
  icon: 'claude',
162
166
  status: 'connected',
163
167
  configuredIn: 'auth credentials',
168
+ credentialSource: 'chamber',
164
169
  apiKey: 'sk-ant-•••••••••••••••••••••991c',
165
170
  baseUrl: 'https://api.anthropic.com/v1',
166
171
  models: [
@@ -174,12 +179,139 @@ export const DEFAULT_PROVIDERS_LIST: ProviderItem[] = [
174
179
  },
175
180
  ];
176
181
 
177
- export const PRESET_NEW_PROVIDERS = [
178
- { id: 'openai', name: 'OpenAI', slug: 'openai', icon: 'openai', defaultUrl: 'https://api.openai.com/v1' },
179
- { id: 'anthropic', name: 'Anthropic', slug: 'anthropic', icon: 'claude', defaultUrl: 'https://api.anthropic.com/v1' },
180
- { id: 'google', name: 'Google Gemini', slug: 'google', icon: 'gemini', defaultUrl: 'https://generativelanguage.googleapis.com/v1beta' },
181
- { id: 'groq', name: 'Groq Cloud', slug: 'groq', icon: 'groq', defaultUrl: 'https://api.groq.com/openai/v1' },
182
- { id: 'mistral', name: 'Mistral AI', slug: 'mistral', icon: 'mistral', defaultUrl: 'https://api.mistral.ai/v1' },
183
- { id: 'ollama', name: 'Ollama (Local)', slug: 'ollama', icon: 'ollama', defaultUrl: 'http://localhost:11434/v1' },
184
- { id: 'custom', name: 'Custom OpenAI-Compatible', slug: 'custom', icon: 'custom', defaultUrl: 'https://' },
182
+ /**
183
+ * Providers offered by the Add Provider picker. `group` is the section the
184
+ * picker renders; `api`/`auth`/`discovery` are the `models.yml` fields the
185
+ * preset seeds, so a choice that omp understands is made once here rather than
186
+ * re-derived from the URL at write time.
187
+ *
188
+ * A preset whose id names a provider omp already bundles (`bundled: true`) is
189
+ * an OVERRIDE entry: omp keeps serving its own model list for that provider and
190
+ * only the endpoint changes. That is the documented way to point a bundled
191
+ * provider at a proxy, and it is deliberately not offered for providers whose
192
+ * models the chamber would then have to duplicate.
193
+ */
194
+ export const PRESET_NEW_PROVIDERS: PresetProviderOption[] = [
195
+ {
196
+ id: 'openai', name: 'OpenAI', slug: 'openai', icon: 'openai',
197
+ defaultUrl: 'https://api.openai.com/v1', api: 'openai-responses', group: 'Frontier APIs',
198
+ },
199
+ {
200
+ id: 'anthropic', name: 'Anthropic', slug: 'anthropic', icon: 'claude',
201
+ defaultUrl: 'https://api.anthropic.com/v1', api: 'anthropic-messages', group: 'Frontier APIs',
202
+ },
203
+ {
204
+ id: 'google', name: 'Google Gemini', slug: 'google', icon: 'gemini',
205
+ defaultUrl: 'https://generativelanguage.googleapis.com/v1beta',
206
+ api: 'google-generative-ai', group: 'Frontier APIs',
207
+ },
208
+ {
209
+ id: 'groq', name: 'Groq Cloud', slug: 'groq', icon: 'groq',
210
+ defaultUrl: 'https://api.groq.com/openai/v1', api: 'openai-completions', group: 'Frontier APIs',
211
+ },
212
+ {
213
+ id: 'mistral', name: 'Mistral AI', slug: 'mistral', icon: 'mistral',
214
+ defaultUrl: 'https://api.mistral.ai/v1', api: 'openai-completions', group: 'Frontier APIs',
215
+ },
216
+ {
217
+ id: 'xai', name: 'xAI', slug: 'xai', icon: 'xai',
218
+ defaultUrl: 'https://api.x.ai/v1', api: 'openai-completions', group: 'Frontier APIs',
219
+ },
220
+ {
221
+ id: 'cerebras', name: 'Cerebras', slug: 'cerebras', icon: 'cerebras',
222
+ defaultUrl: 'https://api.cerebras.ai/v1', api: 'openai-completions', group: 'Frontier APIs',
223
+ },
224
+ {
225
+ id: 'deepseek', name: 'DeepSeek', slug: 'deepseek', icon: 'deepseek',
226
+ defaultUrl: 'https://api.deepseek.com/v1', api: 'openai-completions', group: 'Frontier APIs',
227
+ },
228
+ {
229
+ id: 'openrouter', name: 'OpenRouter', slug: 'openrouter', icon: 'openrouter',
230
+ defaultUrl: 'https://openrouter.ai/api/v1', api: 'openai-completions', group: 'Gateways',
231
+ },
232
+ {
233
+ id: 'together', name: 'Together AI', slug: 'together', icon: 'together',
234
+ defaultUrl: 'https://api.together.xyz/v1', api: 'openai-completions', group: 'Gateways',
235
+ },
236
+ {
237
+ id: 'fireworks', name: 'Fireworks', slug: 'fireworks', icon: 'fireworks',
238
+ defaultUrl: 'https://api.fireworks.ai/inference/v1', api: 'openai-completions', group: 'Gateways',
239
+ },
240
+ {
241
+ id: 'deepinfra', name: 'DeepInfra', slug: 'deepinfra', icon: 'deepinfra',
242
+ defaultUrl: 'https://api.deepinfra.com/v1/openai', api: 'openai-completions', group: 'Gateways',
243
+ },
244
+ {
245
+ id: 'siliconflow', name: 'SiliconFlow', slug: 'siliconflow', icon: 'siliconflow',
246
+ defaultUrl: 'https://api.siliconflow.com/v1', api: 'openai-completions', group: 'Gateways',
247
+ },
248
+ {
249
+ id: 'nvidia', name: 'NVIDIA NIM', slug: 'nvidia', icon: 'nvidia',
250
+ defaultUrl: 'https://integrate.api.nvidia.com/v1', api: 'openai-completions', group: 'Gateways',
251
+ },
252
+ {
253
+ id: 'novita', name: 'Novita AI', slug: 'novita', icon: 'novita',
254
+ defaultUrl: 'https://api.novita.ai/v3/openai', api: 'openai-completions', group: 'Gateways',
255
+ },
256
+ {
257
+ id: 'vercel-ai-gateway', name: 'Vercel AI Gateway', slug: 'vercel-ai-gateway', icon: 'vercel',
258
+ defaultUrl: 'https://ai-gateway.vercel.sh/v1', api: 'openai-completions', group: 'Gateways',
259
+ },
260
+ {
261
+ id: 'ollama', name: 'Ollama (Local)', slug: 'ollama', icon: 'ollama',
262
+ defaultUrl: 'http://127.0.0.1:11434', api: 'openai-responses',
263
+ auth: 'none', discovery: 'ollama', group: 'Local & self-hosted',
264
+ },
265
+ {
266
+ id: 'lm-studio', name: 'LM Studio (Local)', slug: 'lm-studio', icon: 'lmstudio',
267
+ defaultUrl: 'http://127.0.0.1:1234/v1', api: 'openai-completions',
268
+ auth: 'none', discovery: 'lm-studio', group: 'Local & self-hosted',
269
+ },
270
+ {
271
+ id: 'llama-cpp', name: 'llama.cpp (Local)', slug: 'llama.cpp', icon: 'llamacpp',
272
+ defaultUrl: 'http://127.0.0.1:8080', api: 'openai-responses',
273
+ auth: 'none', discovery: 'llama.cpp', group: 'Local & self-hosted',
274
+ },
275
+ {
276
+ id: 'vllm', name: 'vLLM (Local)', slug: 'vllm', icon: 'vllm',
277
+ defaultUrl: 'http://127.0.0.1:8000/v1', api: 'openai-completions',
278
+ auth: 'none', discovery: 'openai-models-list', group: 'Local & self-hosted',
279
+ },
280
+ {
281
+ id: 'litellm', name: 'LiteLLM Proxy', slug: 'litellm', icon: 'litellm',
282
+ defaultUrl: 'http://localhost:4000/v1', api: 'openai-completions',
283
+ discovery: 'litellm', group: 'Local & self-hosted',
284
+ },
285
+ {
286
+ id: 'azure', name: 'Azure OpenAI', slug: 'azure', icon: 'azure',
287
+ defaultUrl: 'https://YOUR-RESOURCE.openai.azure.com/openai/v1',
288
+ api: 'azure-openai-responses', bundled: true, group: 'Other dialects',
289
+ },
290
+ {
291
+ id: 'amazon-bedrock', name: 'Amazon Bedrock', slug: 'amazon-bedrock', icon: 'amazon',
292
+ defaultUrl: 'https://bedrock-runtime.us-east-1.amazonaws.com',
293
+ api: 'bedrock-converse-stream', bundled: true, group: 'Other dialects',
294
+ },
295
+ {
296
+ id: 'google-vertex', name: 'Google Vertex AI', slug: 'google-vertex', icon: 'vertexai',
297
+ defaultUrl: 'https://us-central1-aiplatform.googleapis.com/v1',
298
+ api: 'google-vertex', bundled: true, group: 'Other dialects',
299
+ },
300
+ {
301
+ id: 'openai-codex', name: 'OpenAI Codex', slug: 'openai-codex', icon: 'codex',
302
+ defaultUrl: 'https://chatgpt.com/backend-api/codex',
303
+ api: 'openai-codex-responses', bundled: true, group: 'Other dialects',
304
+ },
305
+ {
306
+ id: 'anthropic-custom', name: 'Anthropic-Compatible Proxy', slug: 'anthropic-custom', icon: 'claude',
307
+ defaultUrl: 'https://', api: 'anthropic-messages', group: 'Custom',
308
+ },
309
+ {
310
+ id: 'openai-custom', name: 'OpenAI-Compatible Gateway', slug: 'openai-custom', icon: 'openai',
311
+ defaultUrl: 'https://', api: 'openai-completions', group: 'Custom',
312
+ },
313
+ {
314
+ id: 'custom', name: 'Custom OpenAI-Compatible', slug: 'custom', icon: 'custom',
315
+ defaultUrl: 'https://', api: 'openai-completions', group: 'Custom',
316
+ },
185
317
  ];
@@ -19,10 +19,6 @@ export interface FileEditorTarget {
19
19
  }
20
20
 
21
21
  export interface UseFileEditorOptions {
22
- /** Content substituted when a read fails (desktop). Omit to report the failure (mobile). */
23
- fallback?: (name: string) => string;
24
- /** Surface read failures through `loadError` instead of silently substituting `fallback`. */
25
- reportLoadError?: boolean;
26
22
  /** Flush a pending debounced write on unmount so closing the editor cannot drop an edit. */
27
23
  flushOnUnmount?: boolean;
28
24
  /** Mark a failed write as `'error'`; otherwise the status returns to `'idle'`. */
@@ -74,8 +70,6 @@ export function useFileEditor(
74
70
  options: UseFileEditorOptions = {},
75
71
  ): UseFileEditorResult {
76
72
  const {
77
- fallback,
78
- reportLoadError = false,
79
73
  flushOnUnmount = false,
80
74
  reportSaveError = false,
81
75
  downloadMimeType = 'text/plain',
@@ -184,11 +178,9 @@ export function useFileEditor(
184
178
  }
185
179
 
186
180
  if (!target.path) {
187
- if (reportLoadError) {
188
- setLoadError('No file path supplied for this attachment.');
189
- return;
190
- }
191
- remember(fileKey, fallback ? fallback(target.name) : '');
181
+ // An attachment chip with no path has nothing to read and nothing to
182
+ // save; the message is the honest outcome.
183
+ setLoadError('No file path supplied for this attachment.');
192
184
  return;
193
185
  }
194
186
 
@@ -203,14 +195,11 @@ export function useFileEditor(
203
195
  if (target.repo && target.repo !== '.') params.set('repo', target.repo);
204
196
  fetch(`/api/fs/read?${params.toString()}`)
205
197
  .then(async (res) => {
206
- const data = await res.json().catch(() => null);
207
- if (reportLoadError) {
208
- if (!res.ok || !data || typeof data.content !== 'string') {
209
- throw new Error((data as { error?: string } | null)?.error || `HTTP ${res.status}`);
210
- }
211
- return data.content as string;
198
+ const data = (await res.json().catch(() => null)) as { content?: unknown; error?: string } | null;
199
+ if (!res.ok || !data || typeof data.content !== 'string') {
200
+ throw new Error(data?.error || `HTTP ${res.status}`);
212
201
  }
213
- return typeof data?.content === 'string' ? (data.content as string) : null;
202
+ return data.content;
214
203
  })
215
204
  .then((text) => {
216
205
  if (cancelled) return;
@@ -222,18 +211,16 @@ export function useFileEditor(
222
211
  if (typed !== undefined && typed !== baselinesRef.current[fileKey]) return;
223
212
  const queued = pendingRef.current;
224
213
  if (queued && targetKey(queued.file) === fileKey) return;
225
- remember(fileKey, text ?? (fallback ? fallback(target.name) : ''));
214
+ remember(fileKey, text);
226
215
  })
227
216
  .catch((err: unknown) => {
228
217
  if (cancelled) return;
229
218
  // A failed re-read keeps what is on screen; only a first load reports.
230
219
  if (!firstLoad) return;
231
- if (reportLoadError) {
232
- remember(fileKey, '');
233
- setLoadError(err instanceof Error ? err.message : 'Failed to read file');
234
- } else {
235
- remember(fileKey, fallback ? fallback(target.name) : '');
236
- }
220
+ // An empty buffer, never a substitute: the next save would write the
221
+ // substitute over the real file.
222
+ remember(fileKey, '');
223
+ setLoadError(err instanceof Error ? err.message : 'Failed to read file');
237
224
  })
238
225
  .finally(() => {
239
226
  if (!cancelled) setIsLoading(false);
@@ -242,7 +229,7 @@ export function useFileEditor(
242
229
  return () => {
243
230
  cancelled = true;
244
231
  };
245
- }, [key, target, fallback, reportLoadError, remember, revision, isImage]);
232
+ }, [key, target, remember, revision, isImage]);
246
233
 
247
234
  useEffect(() => {
248
235
  if (saveStatus !== 'saved') return;
@@ -250,6 +237,13 @@ export function useFileEditor(
250
237
  return () => window.clearTimeout(timer);
251
238
  }, [saveStatus]);
252
239
 
240
+ // The save status describes the file that was written, so it must not outlive
241
+ // that tab: opening another file used to keep showing the previous file's
242
+ // "Save failed" (and its error icon) against a file that was never saved.
243
+ useEffect(() => {
244
+ setSaveStatus('idle');
245
+ }, [key]);
246
+
253
247
  const onChange = useCallback(
254
248
  (next: string) => {
255
249
  if (!target || isImage) return;
@@ -268,6 +262,9 @@ export function useFileEditor(
268
262
 
269
263
  const saveNow = useCallback(() => {
270
264
  if (!target || isImage) return;
265
+ // A file whose read failed has no trustworthy buffer: saving it would write
266
+ // the empty placeholder over the real file. The user must reopen it first.
267
+ if (loadError) return;
271
268
  const text = contents[targetKey(target)];
272
269
  if (text === undefined) return;
273
270
  if (pendingRef.current) {
@@ -275,7 +272,7 @@ export function useFileEditor(
275
272
  pendingRef.current = null;
276
273
  }
277
274
  void persistRef.current(target, text);
278
- }, [target, contents, isImage]);
275
+ }, [target, contents, isImage, loadError]);
279
276
 
280
277
  const { copied, copy, download } = useFileTransfer({
281
278
  target,
@@ -1,5 +1,5 @@
1
1
  import type { ModelsResponse } from '@/shared/lib/models/client';
2
- import { providerNamesFromConnected } from '@/shared/lib/models/provider-label';
2
+ import { providerNamesFromConnected } from '@/shared/lib/models/provider/label';
3
3
  import { useModelsCatalog } from '@/client/hooks/models/use-models-catalog';
4
4
 
5
5
  /** slug → display-name map from `connectedProviders` ({} when none connected). */
@@ -0,0 +1,337 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The provider mutation actions behind Settings → Providers.
8
+ *
9
+ * Split from the hook (`providers.ts`) so that file holds state wiring only:
10
+ * every action here is a write whose failure has to be reported, and keeping
11
+ * them together means the "what does each mutation actually change" contract
12
+ * has one home. `deps` is a single record rather than a positional list, so a
13
+ * caller cannot silently pass the wrong setter.
14
+ */
15
+
16
+ import type { ProviderItem, ProviderModel } from '@/shared/types';
17
+ import { notifyModelsUpdated } from '@/shared/lib/models/client';
18
+ import {
19
+ deleteProvider,
20
+ saveProviderOverlay,
21
+ setProviderEnabled,
22
+ } from '@/shared/lib/models/provider/connection';
23
+ import {
24
+ fetchProviderModelsRemote,
25
+ mergeProviderModels,
26
+ syncProviderModelsToCatalog,
27
+ } from '@/shared/lib/models/provider/models';
28
+ import { removeLegacyKenariModels } from '@/shared/lib/models/provider/cleanup';
29
+ import { addProviderMessage, fetchModelsNote } from '@/client/hooks/settings/providers/messages';
30
+ import { saveModelOverrideRemote } from '@/client/hooks/settings/providers/api';
31
+
32
+ export interface ProviderActionsDeps {
33
+ providers: ProviderItem[];
34
+ selectedProvider: ProviderItem | undefined;
35
+ connectedProviders: ProviderItem[];
36
+ isFetchingModels: boolean;
37
+ isDeletingProvider: boolean;
38
+ pushToast: (message: string, tone: 'success' | 'error') => void;
39
+ setProviders: (providers: ProviderItem[]) => void;
40
+ setSelectedProviderId: (id: string) => void;
41
+ setIsAddModalOpen: (open: boolean) => void;
42
+ setIsDeleteModalOpen: (open: boolean) => void;
43
+ setIsDeletingProvider: (deleting: boolean) => void;
44
+ setIsFetchingModels: (fetching: boolean) => void;
45
+ onAddModalClose?: () => void;
46
+ }
47
+
48
+ export function createProviderActions(deps: ProviderActionsDeps) {
49
+ const {
50
+ providers,
51
+ selectedProvider,
52
+ connectedProviders,
53
+ isFetchingModels,
54
+ isDeletingProvider,
55
+ pushToast,
56
+ setProviders,
57
+ setSelectedProviderId,
58
+ setIsAddModalOpen,
59
+ setIsDeleteModalOpen,
60
+ setIsDeletingProvider,
61
+ setIsFetchingModels,
62
+ onAddModalClose,
63
+ } = deps;
64
+
65
+ /** Publish an overlay edit and adopt whatever the server merged back. */
66
+ const persistProviders = async (updated: ProviderItem[]) => {
67
+ setProviders(updated);
68
+ try {
69
+ const merged = await saveProviderOverlay(updated);
70
+ if (merged) setProviders(merged);
71
+ } catch (error) {
72
+ console.error('Failed to save providers via API:', error);
73
+ pushToast('Failed to save provider settings.', 'error');
74
+ }
75
+ };
76
+
77
+ const toggleProviderEnabled = async (
78
+ slug: string,
79
+ enabled: boolean,
80
+ ): Promise<ProviderItem[] | null> => {
81
+ try {
82
+ const merged = await setProviderEnabled(slug, enabled);
83
+ if (merged) setProviders(merged);
84
+ return merged;
85
+ } catch (error) {
86
+ console.error('Failed to update provider connection:', error);
87
+ pushToast('Failed to update the provider connection.', 'error');
88
+ return null;
89
+ }
90
+ };
91
+
92
+ const handleAddProvider = async (
93
+ newProvider: ProviderItem,
94
+ options: { fetchedCount: number; ompNote?: string },
95
+ ) => {
96
+ // Replace by slug rather than append: an endpoint override reuses the slug
97
+ // of a provider omp already ships, and two overlay rows for one slug would
98
+ // let the stale one win the next merge.
99
+ const slug = newProvider.slug.trim().toLowerCase();
100
+ const withoutExisting = providers.filter((p) => p.slug.trim().toLowerCase() !== slug);
101
+ await persistProviders([...withoutExisting, newProvider]);
102
+ const merged = await toggleProviderEnabled(newProvider.slug, true);
103
+ const registered = merged?.find((provider) => provider.slug.trim().toLowerCase() === slug);
104
+ setSelectedProviderId(registered?.id || newProvider.id);
105
+ setIsAddModalOpen(false);
106
+ onAddModalClose?.();
107
+
108
+ const message = addProviderMessage(newProvider, options);
109
+ pushToast(message.text, message.tone);
110
+ };
111
+
112
+ const handleReconnect = async (updates: Partial<ProviderItem>) => {
113
+ if (!selectedProvider) return;
114
+ const target = selectedProvider;
115
+ // A keyless provider stores no credential, so a masked placeholder echoed
116
+ // back from the dialog must never be persisted as one — it would show a key
117
+ // that authenticates nothing.
118
+ const patch: Partial<ProviderItem> = target.auth === 'none'
119
+ ? Object.fromEntries(Object.entries(updates).filter(([key]) => key !== 'apiKey'))
120
+ : updates;
121
+ await persistProviders(providers.map((p) => (
122
+ p.id === target.id ? { ...p, ...patch, status: 'connected' as const } : p
123
+ )));
124
+ if (target.status !== 'connected') {
125
+ await toggleProviderEnabled(target.slug, true);
126
+ }
127
+ };
128
+
129
+ const handleFetchModels = async (credentials: { apiKey?: string; baseUrl?: string }) => {
130
+ if (!selectedProvider || isFetchingModels) return;
131
+ const baseUrl = credentials.baseUrl || selectedProvider.baseUrl;
132
+ if (!baseUrl) {
133
+ pushToast('Base URL is required to fetch models.', 'error');
134
+ return;
135
+ }
136
+ setIsFetchingModels(true);
137
+ try {
138
+ const result = await fetchModelsFor(selectedProvider, baseUrl, credentials.apiKey);
139
+ if (!result) return;
140
+ if (result.omp?.written) notifyModelsUpdated();
141
+ } finally {
142
+ setIsFetchingModels(false);
143
+ }
144
+ };
145
+
146
+ /**
147
+ * The listing probe plus its reporting. Kept as one unit because the three
148
+ * outcomes — models added, legacy models pruned, nothing new — each have their
149
+ * own message, and a partial report reads as a failed fetch.
150
+ */
151
+ const fetchModelsFor = async (
152
+ provider: ProviderItem,
153
+ baseUrl: string,
154
+ credential: string | undefined,
155
+ ): Promise<{ omp?: { written: boolean; reason?: string } } | null> => {
156
+ const result = await fetchProviderModelsRemote({
157
+ baseUrl,
158
+ apiKey: credential || provider.apiKey,
159
+ providerSlug: provider.slug,
160
+ persistToOmp: true,
161
+ // The provider's own dialect decides the auth header the probe sends;
162
+ // falling back to the URL classifier would send a Bearer header to an
163
+ // Anthropic-shaped proxy that only accepts x-api-key.
164
+ ...(provider.api ? { api: provider.api } : {}),
165
+ ...(provider.auth ? { auth: provider.auth } : {}),
166
+ });
167
+ if (!result.ok || !result.models) {
168
+ pushToast(result.error || 'Failed to fetch models from the provider.', 'error');
169
+ return null;
170
+ }
171
+ const cleaned = removeLegacyKenariModels(provider, provider.models);
172
+ const removedCount = provider.models.length - cleaned.length;
173
+ const { merged, addedCount } = mergeProviderModels(cleaned, result.models);
174
+ if (addedCount > 0 || removedCount > 0) {
175
+ await handleReconnect({ models: merged });
176
+ }
177
+ if (addedCount > 0) {
178
+ pushToast(`Fetched ${addedCount} new model${addedCount === 1 ? '' : 's'} from the provider.`, 'success');
179
+ } else if (removedCount > 0) {
180
+ pushToast('Removed legacy fallback models from the provider.', 'success');
181
+ } else {
182
+ pushToast('No new models found — all fetched models already exist.', 'success');
183
+ }
184
+ const note = fetchModelsNote(result);
185
+ if (note) pushToast(note.text, note.tone);
186
+ if (addedCount > 0) void syncProviderModelsToCatalog(provider.name, result.models);
187
+ return { omp: result.omp };
188
+ };
189
+
190
+ const handleFetchModelsFromList = async () => {
191
+ if (!selectedProvider) return;
192
+ await handleFetchModels({
193
+ apiKey: selectedProvider.apiKey,
194
+ baseUrl: selectedProvider.baseUrl,
195
+ });
196
+ };
197
+
198
+ const handleOmpAuthSuccess = () => {
199
+ if (!selectedProvider) return;
200
+ const target = selectedProvider;
201
+ void persistProviders(providers.map((p) => (
202
+ p.id === target.id ? { ...p, status: 'connected' as const } : p
203
+ )));
204
+ void toggleProviderEnabled(target.slug, true);
205
+ };
206
+
207
+ const handleToggleDisconnect = async () => {
208
+ if (!selectedProvider) return;
209
+ const target = selectedProvider;
210
+ const disconnecting = target.status === 'connected';
211
+ if (disconnecting) {
212
+ setSelectedProviderId(connectedProviders.find((provider) => provider.id !== target.id)?.id || '');
213
+ }
214
+ const merged = await toggleProviderEnabled(target.slug, !disconnecting);
215
+ if (!merged) return;
216
+ pushToast(
217
+ disconnecting
218
+ ? `${target.name} disconnected — its models are no longer offered in the chat picker.`
219
+ : `${target.name} reconnected.`,
220
+ 'success',
221
+ );
222
+ };
223
+
224
+ const updateSelectedModels = (transform: (models: ProviderModel[]) => ProviderModel[]) => {
225
+ if (!selectedProvider) return;
226
+ void persistProviders(providers.map((p) => (
227
+ p.id === selectedProvider.id ? { ...p, models: transform(p.models) } : p
228
+ )));
229
+ };
230
+
231
+ /**
232
+ * Disable/enable from omp's own config.yml `disabledProviders`. This is the
233
+ * coarse switch that removes the provider — and every model it serves — from
234
+ * the chat picker; the per-model eye toggle is the fine-grained one.
235
+ */
236
+ const handleToggleProviderDisabled = async () => {
237
+ if (!selectedProvider) return;
238
+ const target = selectedProvider;
239
+ const disabling = target.disabled !== true;
240
+ const merged = await toggleProviderEnabled(target.slug, !disabling);
241
+ if (!merged) return;
242
+ pushToast(
243
+ disabling
244
+ ? `${target.name} disabled — it no longer appears in the model list.`
245
+ : `${target.name} enabled — its models are offered again.`,
246
+ 'success',
247
+ );
248
+ };
249
+
250
+ /**
251
+ * Delete a provider's `models.yml` entry (and the chamber's overlay row).
252
+ *
253
+ * Only offered for a provider the server reports as `inModelsYml`, because
254
+ * the action is a file edit: a login provider's credential is untouched, and
255
+ * a provider with no file entry has nothing to delete. The selection moves off
256
+ * the deleted provider BEFORE the list is republished — leaving it selected
257
+ * would render a detail pane for a provider that no longer exists.
258
+ */
259
+ const handleDeleteProvider = async () => {
260
+ if (!selectedProvider || isDeletingProvider) return;
261
+ const target = selectedProvider;
262
+ setIsDeletingProvider(true);
263
+ try {
264
+ const result = await deleteProvider(target.id);
265
+ if (!result.ok) {
266
+ pushToast(result.error || 'Failed to delete the provider.', 'error');
267
+ return;
268
+ }
269
+ setIsDeleteModalOpen(false);
270
+ const remaining = result.providers ?? [];
271
+ setSelectedProviderId(remaining.find((provider) => provider.status === 'connected')?.id || '');
272
+ setProviders(remaining);
273
+ // The overlay row is gone even when the file cleanup failed, so the
274
+ // warning is reported rather than presented as a clean delete.
275
+ if (result.warning) {
276
+ pushToast(`${target.name} removed from the chamber, but models.yml cleanup failed: ${result.warning}`, 'error');
277
+ } else if (result.removedFromOmp) {
278
+ const count = result.modelsRemoved ?? 0;
279
+ pushToast(
280
+ `${target.name} deleted from models.yml${count > 0 ? ` with its ${count} model${count === 1 ? '' : 's'}` : ''}.`,
281
+ 'success',
282
+ );
283
+ } else {
284
+ pushToast(`${target.name} removed from the chamber list.`, 'success');
285
+ }
286
+ } finally {
287
+ setIsDeletingProvider(false);
288
+ }
289
+ };
290
+
291
+ const handleHideAll = () => updateSelectedModels(
292
+ (models) => models.map((m) => ({ ...m, isVisible: false })),
293
+ );
294
+
295
+ const handleShowAll = () => updateSelectedModels(
296
+ (models) => models.map((m) => ({ ...m, isVisible: true })),
297
+ );
298
+
299
+ const handleToggleModelVisibility = (modelId: string) => updateSelectedModels(
300
+ (models) => models.map((m) => (m.id === modelId ? { ...m, isVisible: !m.isVisible } : m)),
301
+ );
302
+
303
+ /**
304
+ * Save the per-model knobs omp honours (`models.yml` modelOverrides). The
305
+ * overlay still stores the display value so the dialog reopens where the user
306
+ * left it, but omp only reads the override — writing just the overlay was the
307
+ * bug that made these controls inert.
308
+ */
309
+ const handleSaveModelConfig = async (modelId: string, updates: Partial<ProviderModel>) => {
310
+ if (!selectedProvider) return;
311
+ const provider = selectedProvider;
312
+ updateSelectedModels((models) => models.map((m) => (m.id === modelId ? { ...m, ...updates } : m)));
313
+
314
+ const result = await saveModelOverrideRemote(provider.slug, modelId, updates);
315
+ if (!result.ok) {
316
+ pushToast(result.error || 'Failed to save the model configuration.', 'error');
317
+ return;
318
+ }
319
+ if (result.written) notifyModelsUpdated();
320
+ else if (result.reason) pushToast(result.reason, 'error');
321
+ };
322
+
323
+ return {
324
+ handleAddProvider,
325
+ handleReconnect,
326
+ handleFetchModels,
327
+ handleFetchModelsFromList,
328
+ handleOmpAuthSuccess,
329
+ handleToggleDisconnect,
330
+ handleToggleProviderDisabled,
331
+ handleDeleteProvider,
332
+ handleHideAll,
333
+ handleShowAll,
334
+ handleToggleModelVisibility,
335
+ handleSaveModelConfig,
336
+ };
337
+ }