ompchamber 2.0.2 → 3.1.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 (419) hide show
  1. package/README.md +31 -11
  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-8t70jhyj.js +5149 -0
  6. package/dist/client/static/js/index-n4wkqj7t.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/code-editor/handler.test.ts +274 -0
  16. package/src/client/components/common/code-editor/handler.ts +175 -0
  17. package/src/client/components/common/code-editor/index.tsx +185 -220
  18. package/src/client/components/common/code-surface/index.tsx +109 -12
  19. package/src/client/components/common/lazy-panels.tsx +5 -1
  20. package/src/client/components/common/provider-icon/glyphs.ts +118 -0
  21. package/src/client/components/common/provider-icon/index.tsx +85 -0
  22. package/src/client/components/common/viewport.ts +5 -3
  23. package/src/client/components/layout/RightActivityBar.tsx +2 -1
  24. package/src/client/components/layout/desktop-layout/TopNavbar.tsx +21 -4
  25. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
  26. package/src/client/components/layout/desktop-layout/index.tsx +26 -4
  27. package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
  28. package/src/client/components/mobile/RightSidebar.tsx +11 -4
  29. package/src/client/components/mobile/SessionSidebar.tsx +10 -0
  30. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +0 -1
  31. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -1
  32. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +3 -0
  33. package/src/client/components/settings/categories/ProviderSettings.tsx +45 -4
  34. package/src/client/components/settings/categories/UsageSettings.tsx +22 -7
  35. package/src/client/components/settings/categories/provider-settings/AddModelModal.tsx +322 -0
  36. package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +152 -56
  37. package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +52 -1
  38. package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +133 -0
  39. package/src/client/components/settings/categories/provider-settings/DialectFields.tsx +136 -0
  40. package/src/client/components/settings/categories/provider-settings/EffortLadder.tsx +89 -0
  41. package/src/client/components/settings/categories/provider-settings/Header.tsx +2 -2
  42. package/src/client/components/settings/categories/provider-settings/LoginModal.tsx +2 -2
  43. package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +22 -2
  44. package/src/client/components/settings/categories/provider-settings/PresetPicker.tsx +82 -0
  45. package/src/client/components/settings/categories/provider-settings/PriceFields.tsx +107 -0
  46. package/src/client/components/settings/categories/provider-settings/ReconnectModal.tsx +43 -22
  47. package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +2 -2
  48. package/src/client/components/settings/categories/usage-settings/LimitsSection.tsx +115 -0
  49. package/src/client/components/settings/categories/usage-settings/ProviderCard.tsx +19 -26
  50. package/src/client/components/settings/categories/usage-settings/ProviderDetail.tsx +29 -22
  51. package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +60 -48
  52. package/src/client/components/settings/categories/usage-settings/format.ts +36 -0
  53. package/src/client/components/settings/categories/usage-settings/providers.ts +52 -28
  54. package/src/client/components/workspace/browser-panel/AddressBar.tsx +1 -10
  55. package/src/client/components/workspace/browser-panel/Screencast.tsx +4 -5
  56. package/src/client/components/workspace/browser-panel/index.tsx +6 -6
  57. package/src/client/components/workspace/browser-toolbar/index.tsx +22 -12
  58. package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +1 -1
  59. package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +1 -1
  60. package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +1 -1
  61. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Todo.tsx +1 -1
  62. package/src/client/components/workspace/diff-panel/Toolbar.tsx +71 -38
  63. package/src/client/components/workspace/diff-panel/index.tsx +139 -53
  64. package/src/client/components/workspace/editor/CommandPalette.test.ts +242 -0
  65. package/src/client/components/workspace/editor/CommandPalette.tsx +138 -0
  66. package/src/client/components/workspace/editor/FindWidget.test.ts +261 -0
  67. package/src/client/components/workspace/editor/FindWidget.tsx +219 -0
  68. package/src/client/components/workspace/editor/Tabs.tsx +2 -2
  69. package/src/client/components/workspace/editor/Toolbar.tsx +129 -26
  70. package/src/client/components/workspace/editor/index.tsx +180 -62
  71. package/src/client/components/workspace/file-explorer/Modals.tsx +79 -2
  72. package/src/client/components/workspace/file-explorer/TreeItem.tsx +43 -113
  73. package/src/client/components/workspace/model-dropdown/Item.tsx +13 -23
  74. package/src/client/components/workspace/model-dropdown/Panel.tsx +1 -1
  75. package/src/client/components/workspace/model-dropdown/Section.tsx +13 -4
  76. package/src/client/components/workspace/model-dropdown/index.tsx +7 -3
  77. package/src/client/components/workspace/todo-panel/PhaseGroup.tsx +76 -0
  78. package/src/client/components/workspace/todo-panel/Row.tsx +69 -0
  79. package/src/client/components/workspace/todo-panel/index.tsx +161 -0
  80. package/src/client/components/workspace/usage-panel/ProviderSelect.tsx +10 -5
  81. package/src/client/components/workspace/usage-panel/index.tsx +36 -18
  82. package/src/client/data/samples/dialogue-session.ts +1 -1
  83. package/src/client/data/samples/sample-tools/other.ts +1 -1
  84. package/src/client/data/settings/provider.ts +141 -9
  85. package/src/client/hooks/editor/find-harness.test-util.ts +170 -0
  86. package/src/client/hooks/editor/scroll-container.test.ts +102 -0
  87. package/src/client/hooks/editor/scroll-container.ts +26 -0
  88. package/src/client/hooks/editor/use-code-window.ts +2 -20
  89. package/src/client/hooks/editor/use-editor-find-write.test.ts +170 -0
  90. package/src/client/hooks/editor/use-editor-find.test.ts +118 -0
  91. package/src/client/hooks/editor/use-editor-find.ts +321 -0
  92. package/src/client/hooks/editor/use-editor-history.test.ts +216 -0
  93. package/src/client/hooks/editor/use-editor-history.ts +134 -0
  94. package/src/client/hooks/editor/use-file-editor.ts +24 -27
  95. package/src/client/hooks/editor/use-occurrences.test.ts +236 -0
  96. package/src/client/hooks/editor/use-occurrences.ts +98 -0
  97. package/src/client/hooks/models/use-provider-names.ts +1 -1
  98. package/src/client/hooks/settings/providers/actions.ts +337 -0
  99. package/src/client/hooks/settings/providers/api.ts +69 -0
  100. package/src/client/hooks/settings/providers/index.ts +138 -0
  101. package/src/client/hooks/settings/providers/messages.ts +77 -0
  102. package/src/client/hooks/settings/useUsageReport.ts +8 -3
  103. package/src/client/hooks/ui/theme.ts +3 -3
  104. package/src/client/hooks/workspace/file-tabs.ts +62 -11
  105. package/src/client/hooks/workspace/file-tree-actions.test.ts +348 -0
  106. package/src/client/hooks/workspace/file-tree-actions.ts +250 -0
  107. package/src/client/hooks/workspace/session-todos.ts +102 -0
  108. package/src/client/stubs/elkjs.ts +6 -1
  109. package/src/client/tailwind.css +30 -0
  110. package/src/server/index.ts +20 -2
  111. package/src/server/lib/assets/font-css.server.ts +86 -0
  112. package/src/server/lib/assets/fonts.server.ts +117 -0
  113. package/src/server/lib/bundler/css.ts +116 -0
  114. package/src/server/lib/lifecycle/shell-route.ts +17 -0
  115. package/src/server/lib/models/provider-listing.server.ts +257 -0
  116. package/src/server/lib/models/provider-registry.server.test.ts +122 -0
  117. package/src/server/lib/models/provider-registry.server.ts +76 -25
  118. package/src/server/lib/omp/config/models-config.ts +11 -0
  119. package/src/server/lib/omp/config/models-validation.test.ts +137 -0
  120. package/src/server/lib/omp/config/models-validation.ts +111 -2
  121. package/src/server/lib/omp/config/provider-seeds.test.ts +167 -0
  122. package/src/server/lib/omp/config/provider-seeds.ts +176 -0
  123. package/src/server/lib/omp/config/providers.ts +94 -100
  124. package/src/server/lib/omp/core/auth-credentials.ts +45 -0
  125. package/src/server/lib/omp/session/todos.ts +100 -0
  126. package/src/server/lib/omp/session/{usage.ts → usage/aggregate.ts} +1 -118
  127. package/src/server/lib/omp/session/usage/shaping.ts +130 -0
  128. package/src/server/lib/usage/deepseek.ts +4 -5
  129. package/src/server/lib/usage/kenari.ts +7 -6
  130. package/src/server/lib/usage/omp-usage.server.test.ts +126 -0
  131. package/src/server/lib/usage/omp-usage.server.ts +194 -0
  132. package/src/server/lib/usage/provider-key.ts +81 -3
  133. package/src/server/lib/usage/providers.server.test.ts +47 -0
  134. package/src/server/lib/usage/providers.server.ts +208 -0
  135. package/src/server/lib/usage/vendor-balance.server.test.ts +104 -0
  136. package/src/server/lib/usage/vendor-balance.server.ts +284 -0
  137. package/src/server/plugins/shell.server.ts +56 -0
  138. package/src/server/plugins/ssr.ts +24 -36
  139. package/src/server/plugins/static.test.ts +26 -29
  140. package/src/server/plugins/static.ts +40 -19
  141. package/src/server/routes/fs/action-create.test.ts +90 -0
  142. package/src/server/routes/fs/action.ts +23 -0
  143. package/src/server/routes/omp/index.ts +2 -0
  144. package/src/server/routes/omp/session-todos.ts +80 -0
  145. package/src/server/routes/settings/index.ts +2 -0
  146. package/src/server/routes/settings/provider-model.ts +107 -0
  147. package/src/server/routes/settings/provider-models.ts +65 -202
  148. package/src/server/routes/settings/providers.ts +22 -7
  149. package/src/server/routes/settings/usage.ts +125 -45
  150. package/src/server/routes/telemetry/tokens.ts +2 -1
  151. package/src/shared/lib/chat/{todo-parser.ts → todo/parser.ts} +17 -0
  152. package/src/shared/lib/chat/todo/progress.ts +97 -0
  153. package/src/shared/lib/chat/todo/snapshot.test.ts +156 -0
  154. package/src/shared/lib/chat/todo/snapshot.ts +215 -0
  155. package/src/shared/lib/code/editor/commands.test.ts +155 -0
  156. package/src/shared/lib/code/editor/commands.ts +260 -0
  157. package/src/shared/lib/code/editor/edits.test.ts +100 -0
  158. package/src/shared/lib/code/editor/edits.ts +122 -0
  159. package/src/shared/lib/code/editor/find-status.test.ts +50 -0
  160. package/src/shared/lib/code/editor/find-status.ts +36 -0
  161. package/src/shared/lib/code/editor/find.test.ts +203 -0
  162. package/src/shared/lib/code/editor/find.ts +224 -0
  163. package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.test.ts +1 -1
  164. package/src/shared/lib/code/editor/keymap.test.ts +109 -0
  165. package/src/shared/lib/code/editor/keymap.ts +151 -0
  166. package/src/shared/lib/code/editor/layers.test.ts +77 -0
  167. package/src/shared/lib/code/editor/layers.ts +67 -0
  168. package/src/shared/lib/code/editor/line-edits.test.ts +131 -0
  169. package/src/shared/lib/code/editor/line-edits.ts +159 -0
  170. package/src/shared/lib/code/editor/lines.test.ts +81 -0
  171. package/src/shared/lib/code/editor/lines.ts +83 -0
  172. package/src/shared/lib/code/editor/marks.test.ts +86 -0
  173. package/src/shared/lib/code/editor/marks.ts +106 -0
  174. package/src/shared/lib/code/editor/occurrences.test.ts +117 -0
  175. package/src/shared/lib/code/editor/occurrences.ts +111 -0
  176. package/src/shared/lib/code/editor/palette.test.ts +91 -0
  177. package/src/shared/lib/code/editor/palette.ts +92 -0
  178. package/src/shared/lib/code/editor/reset-css.test.ts +35 -0
  179. package/src/shared/lib/code/editor/reset-css.ts +32 -0
  180. package/src/shared/lib/code/editor/text-commands.test.ts +178 -0
  181. package/src/shared/lib/code/editor/text-commands.ts +154 -0
  182. package/src/shared/lib/code/syntax-highlight.test.ts +75 -0
  183. package/src/shared/lib/code/syntax-highlight.ts +29 -17
  184. package/src/shared/lib/code/windowed-highlight.ts +52 -1
  185. package/src/shared/lib/fs/diff-parser.test.ts +116 -0
  186. package/src/shared/lib/fs/diff-parser.ts +86 -33
  187. package/src/shared/lib/models/{provider-connection.ts → provider/connection.ts} +37 -0
  188. package/src/shared/lib/models/provider/dialect.ts +179 -0
  189. package/src/shared/lib/models/provider/glyph.test.ts +116 -0
  190. package/src/shared/lib/models/provider/glyph.ts +200 -0
  191. package/src/shared/lib/models/{provider-models.ts → provider/models.ts} +91 -6
  192. package/src/shared/lib/models/provider/presets.test.ts +172 -0
  193. package/src/shared/lib/models/provider/presets.ts +83 -0
  194. package/src/shared/lib/ui/key-binding.test.ts +115 -0
  195. package/src/shared/lib/ui/key-binding.ts +143 -0
  196. package/src/shared/lib/util/platform.test.ts +87 -0
  197. package/src/shared/lib/util/platform.ts +30 -0
  198. package/src/shared/lib/workspace/file-tab-id.ts +29 -0
  199. package/src/shared/lib/workspace/keymap.test.ts +82 -0
  200. package/src/shared/lib/workspace/keymap.ts +30 -0
  201. package/src/shared/lib/workspace/refresh-cadence.ts +9 -0
  202. package/src/shared/lib/workspace/right-panels.ts +8 -0
  203. package/src/shared/lib/workspace/session-state/store.ts +0 -1
  204. package/src/shared/types/index.ts +1 -0
  205. package/src/shared/types/settings/provider.ts +81 -0
  206. package/src/shared/types/settings/usage.ts +88 -14
  207. package/src/shared/types/todo.ts +108 -0
  208. package/tsconfig.json +3 -1
  209. package/dist/client/static/css/index.5f630421b7.css +0 -2
  210. package/dist/client/static/font/KaTeX_AMS-Regular.a79f1c3119.woff2 +0 -0
  211. package/dist/client/static/font/KaTeX_Caligraphic-Bold.ec17d13264.woff2 +0 -0
  212. package/dist/client/static/font/KaTeX_Caligraphic-Regular.55fac25845.woff2 +0 -0
  213. package/dist/client/static/font/KaTeX_Fraktur-Bold.d42a5579b0.woff2 +0 -0
  214. package/dist/client/static/font/KaTeX_Fraktur-Regular.d3c882a649.woff2 +0 -0
  215. package/dist/client/static/font/KaTeX_Main-Bold.c3fb5ac22f.woff2 +0 -0
  216. package/dist/client/static/font/KaTeX_Main-BoldItalic.6f2bb1dff2.woff2 +0 -0
  217. package/dist/client/static/font/KaTeX_Main-Italic.8916142bec.woff2 +0 -0
  218. package/dist/client/static/font/KaTeX_Main-Regular.0462f03bdf.woff2 +0 -0
  219. package/dist/client/static/font/KaTeX_Math-BoldItalic.572d331f69.woff2 +0 -0
  220. package/dist/client/static/font/KaTeX_Math-Italic.f28c23acad.woff2 +0 -0
  221. package/dist/client/static/font/KaTeX_SansSerif-Bold.8c5b5494b6.woff2 +0 -0
  222. package/dist/client/static/font/KaTeX_SansSerif-Italic.3b1e59b3ba.woff2 +0 -0
  223. package/dist/client/static/font/KaTeX_SansSerif-Regular.ba21ed5f84.woff2 +0 -0
  224. package/dist/client/static/font/KaTeX_Script-Regular.03e9641d6f.woff2 +0 -0
  225. package/dist/client/static/font/KaTeX_Size1-Regular.eae34984b3.woff2 +0 -0
  226. package/dist/client/static/font/KaTeX_Size2-Regular.5916a24fa3.woff2 +0 -0
  227. package/dist/client/static/font/KaTeX_Size4-Regular.10d95fd3a2.woff2 +0 -0
  228. package/dist/client/static/font/KaTeX_Typewriter-Regular.a8709e3622.woff2 +0 -0
  229. package/dist/client/static/font/fira-code-latin-400-normal.2655b10c55.woff2 +0 -0
  230. package/dist/client/static/font/fira-code-latin-500-normal.92a991f204.woff2 +0 -0
  231. package/dist/client/static/font/fira-code-latin-600-normal.43788eb7f1.woff2 +0 -0
  232. package/dist/client/static/font/fira-code-latin-700-normal.2667215d33.woff2 +0 -0
  233. package/dist/client/static/js/77.0d81c74cf0.js +0 -139
  234. package/dist/client/static/js/77.0d81c74cf0.js.LICENSE.txt +0 -8
  235. package/dist/client/static/js/async/0.8e59f2eb5b.js +0 -1
  236. package/dist/client/static/js/async/0k.5852e6b4fa.js +0 -1
  237. package/dist/client/static/js/async/0w.9ed36d5860.js +0 -1
  238. package/dist/client/static/js/async/1.f387c12b63.js +0 -1
  239. package/dist/client/static/js/async/12.d091f91c99.js +0 -1
  240. package/dist/client/static/js/async/13.8f810a8a6c.js +0 -1
  241. package/dist/client/static/js/async/19.6fe909d55a.js +0 -160
  242. package/dist/client/static/js/async/1b.23f1141a38.js +0 -1
  243. package/dist/client/static/js/async/1k.973e8e21fc.js +0 -2
  244. package/dist/client/static/js/async/1k.973e8e21fc.js.LICENSE.txt +0 -11
  245. package/dist/client/static/js/async/1p.0d5cddda96.js +0 -1
  246. package/dist/client/static/js/async/2.b589f1733b.js +0 -1
  247. package/dist/client/static/js/async/2v.96165101de.js +0 -1
  248. package/dist/client/static/js/async/3.0cacc08423.js +0 -1
  249. package/dist/client/static/js/async/37.b28b2f61cd.js +0 -1
  250. package/dist/client/static/js/async/3d.5085fb913f.js +0 -1
  251. package/dist/client/static/js/async/3j.357c80ff38.js +0 -2
  252. package/dist/client/static/js/async/3j.357c80ff38.js.LICENSE.txt +0 -11
  253. package/dist/client/static/js/async/3p.bcf32a0983.js +0 -1
  254. package/dist/client/static/js/async/3r.3a69e98ee0.js +0 -64
  255. package/dist/client/static/js/async/3u.1228460e2a.js +0 -1
  256. package/dist/client/static/js/async/3y.3530b824ad.js +0 -198
  257. package/dist/client/static/js/async/3y.3530b824ad.js.LICENSE.txt +0 -5
  258. package/dist/client/static/js/async/4.9442eeb472.js +0 -1
  259. package/dist/client/static/js/async/4e.678285ca55.js +0 -1
  260. package/dist/client/static/js/async/4k.33a0c7eeb4.js +0 -1
  261. package/dist/client/static/js/async/4m.928a1dde85.js +0 -31
  262. package/dist/client/static/js/async/4m.928a1dde85.js.LICENSE.txt +0 -11
  263. package/dist/client/static/js/async/4q.aad20dab25.js +0 -1
  264. package/dist/client/static/js/async/5.ebaf8b89cc.js +0 -36
  265. package/dist/client/static/js/async/51.95bcae98af.js +0 -2
  266. package/dist/client/static/js/async/51.95bcae98af.js.LICENSE.txt +0 -6
  267. package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
  268. package/dist/client/static/js/async/5j.d6407c4248.js +0 -287
  269. package/dist/client/static/js/async/5o.88316d4460.js +0 -62
  270. package/dist/client/static/js/async/6.f034c9b528.js +0 -1
  271. package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
  272. package/dist/client/static/js/async/6o.5b1a06a382.js +0 -1
  273. package/dist/client/static/js/async/7.e4a9288dd0.js +0 -1
  274. package/dist/client/static/js/async/73.308dff4da8.js +0 -1
  275. package/dist/client/static/js/async/7e.a1cc9bfd88.js +0 -1
  276. package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
  277. package/dist/client/static/js/async/7l.fadbbc0583.js +0 -1
  278. package/dist/client/static/js/async/8.5067439d8c.js +0 -1
  279. package/dist/client/static/js/async/8j.ea110663fb.js +0 -1
  280. package/dist/client/static/js/async/a.bce4542392.js +0 -1
  281. package/dist/client/static/js/async/aa.65d3852576.js +0 -1
  282. package/dist/client/static/js/async/ar.9303e5f2e8.js +0 -2
  283. package/dist/client/static/js/async/ar.9303e5f2e8.js.LICENSE.txt +0 -11
  284. package/dist/client/static/js/async/az.d656062b0f.js +0 -1
  285. package/dist/client/static/js/async/b.b4f6f06515.js +0 -1
  286. package/dist/client/static/js/async/bi.09688ba75c.js +0 -1
  287. package/dist/client/static/js/async/bq.586bc738d1.js +0 -1
  288. package/dist/client/static/js/async/c.797d933882.js +0 -1
  289. package/dist/client/static/js/async/c0.0af1e640d3.js +0 -24
  290. package/dist/client/static/js/async/ca.8a52cfd447.js +0 -1
  291. package/dist/client/static/js/async/cf.d34fff5d67.js +0 -1
  292. package/dist/client/static/js/async/ct.b74928f7a3.js +0 -1
  293. package/dist/client/static/js/async/cx.3cb0242a3a.js +0 -226
  294. package/dist/client/static/js/async/d.a5fc8fe1b3.js +0 -1
  295. package/dist/client/static/js/async/da.f62c70f6c8.js +0 -1
  296. package/dist/client/static/js/async/dp.1725d6432c.js +0 -1
  297. package/dist/client/static/js/async/e.93214d622b.js +0 -79
  298. package/dist/client/static/js/async/f.ab50467719.js +0 -19
  299. package/dist/client/static/js/async/f.ab50467719.js.LICENSE.txt +0 -12
  300. package/dist/client/static/js/async/f7.46971e9d6d.js +0 -2
  301. package/dist/client/static/js/async/f7.46971e9d6d.js.LICENSE.txt +0 -6
  302. package/dist/client/static/js/async/f8.bbf0dbae70.js +0 -1
  303. package/dist/client/static/js/async/g.cb81980498.js +0 -2
  304. package/dist/client/static/js/async/g.cb81980498.js.LICENSE.txt +0 -11
  305. package/dist/client/static/js/async/h.4e790471e4.js +0 -27
  306. package/dist/client/static/js/async/h1.393f066d09.js +0 -1
  307. package/dist/client/static/js/async/h3.216e9798ab.js +0 -148
  308. package/dist/client/static/js/async/hd.0da931b622.js +0 -1
  309. package/dist/client/static/js/async/hk.ba7fb04d41.js +0 -1
  310. package/dist/client/static/js/async/hn.b645041e00.js +0 -1
  311. package/dist/client/static/js/async/ho.475a5e1ae4.js +0 -1
  312. package/dist/client/static/js/async/hp.e9fe2e395a.js +0 -2
  313. package/dist/client/static/js/async/hp.e9fe2e395a.js.LICENSE.txt +0 -17
  314. package/dist/client/static/js/async/hs.e7c59d8090.js +0 -41
  315. package/dist/client/static/js/async/i.7c148e8440.js +0 -1
  316. package/dist/client/static/js/async/id.573df20f29.js +0 -1
  317. package/dist/client/static/js/async/j.ab925882d8.js +0 -1
  318. package/dist/client/static/js/async/jd.63886a1f09.js +0 -1
  319. package/dist/client/static/js/async/jg.d86ebd4013.js +0 -86
  320. package/dist/client/static/js/async/jl.874d352278.js +0 -1
  321. package/dist/client/static/js/async/jll.11313b455a.js +0 -1
  322. package/dist/client/static/js/async/js.67182759ef.js +0 -1
  323. package/dist/client/static/js/async/jsc.23b70d6f40.js +0 -1
  324. package/dist/client/static/js/async/jse.334588ff5d.js +0 -1
  325. package/dist/client/static/js/async/jz.72eb929106.js +0 -77
  326. package/dist/client/static/js/async/k.3210286401.js +0 -217
  327. package/dist/client/static/js/async/ka.c1990ecaa3.js +0 -115
  328. package/dist/client/static/js/async/katex-vendor.de6a87c308.js +0 -1
  329. package/dist/client/static/js/async/ku.29c8a59d0c.js +0 -1
  330. package/dist/client/static/js/async/ky.8dab1dade0.js +0 -1
  331. package/dist/client/static/js/async/l.12dd34e1be.js +0 -1
  332. package/dist/client/static/js/async/l9.949710adaf.js +0 -83
  333. package/dist/client/static/js/async/l9.949710adaf.js.LICENSE.txt +0 -13
  334. package/dist/client/static/js/async/ls.9d9a01026c.js +0 -104
  335. package/dist/client/static/js/async/m.eb2e6752e2.js +0 -28
  336. package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +0 -11
  337. package/dist/client/static/js/async/m3.d2b05f9604.js +0 -2
  338. package/dist/client/static/js/async/m3.d2b05f9604.js.LICENSE.txt +0 -6
  339. package/dist/client/static/js/async/ma.2c261b04dd.js +0 -2
  340. package/dist/client/static/js/async/ma.2c261b04dd.js.LICENSE.txt +0 -6
  341. package/dist/client/static/js/async/me.8ace414f80.js +0 -23
  342. package/dist/client/static/js/async/ms.98960af4eb.js +0 -67
  343. package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +0 -12
  344. package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
  345. package/dist/client/static/js/async/n.a9f1c51701.js +0 -1
  346. package/dist/client/static/js/async/na.a36cd532d6.js +0 -1
  347. package/dist/client/static/js/async/nk.fc7e58325e.js +0 -1
  348. package/dist/client/static/js/async/o.36353dfb88.js +0 -1
  349. package/dist/client/static/js/async/oc.b73042e8bd.js +0 -8
  350. package/dist/client/static/js/async/og.8eb577893a.js +0 -193
  351. package/dist/client/static/js/async/p.48ecfcd14e.js +0 -1
  352. package/dist/client/static/js/async/px.fc6877924b.js +0 -1
  353. package/dist/client/static/js/async/q.22588845a9.js +0 -21
  354. package/dist/client/static/js/async/q.22588845a9.js.LICENSE.txt +0 -6
  355. package/dist/client/static/js/async/q5.97dc1481b6.js +0 -286
  356. package/dist/client/static/js/async/qs.2a9ddb3c72.js +0 -1
  357. package/dist/client/static/js/async/r.51010ad87c.js +0 -1
  358. package/dist/client/static/js/async/rd.cb6bf495bb.js +0 -2
  359. package/dist/client/static/js/async/rd.cb6bf495bb.js.LICENSE.txt +0 -4
  360. package/dist/client/static/js/async/s.0f395cdda5.js +0 -94
  361. package/dist/client/static/js/async/t.4dca0e2ba8.js +0 -1
  362. package/dist/client/static/js/async/t9.d44f57d518.js +0 -1
  363. package/dist/client/static/js/async/ta.973534bbec.js +0 -1
  364. package/dist/client/static/js/async/tr.e878658140.js +0 -309
  365. package/dist/client/static/js/async/u.0871ac5b22.js +0 -1
  366. package/dist/client/static/js/async/ud.3065bfee31.js +0 -2
  367. package/dist/client/static/js/async/ud.3065bfee31.js.LICENSE.txt +0 -4
  368. package/dist/client/static/js/async/up.ea990da0ff.js +0 -32
  369. package/dist/client/static/js/async/uw.8890abbabc.js +0 -1
  370. package/dist/client/static/js/async/v.7d28d3c241.js +0 -34
  371. package/dist/client/static/js/async/vp.9b13bf8610.js +0 -1
  372. package/dist/client/static/js/async/vt.42416b707d.js +0 -1
  373. package/dist/client/static/js/async/w.08ac210f60.js +0 -1
  374. package/dist/client/static/js/async/wt.ad92393a90.js +0 -1
  375. package/dist/client/static/js/async/x.563f72ca6b.js +0 -1
  376. package/dist/client/static/js/async/x0.475516418f.js +0 -1
  377. package/dist/client/static/js/async/x1.da8687159b.js +0 -90
  378. package/dist/client/static/js/async/xe.da8dcc2015.js +0 -2
  379. package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +0 -11
  380. package/dist/client/static/js/async/xl.a3c562a58c.js +0 -1
  381. package/dist/client/static/js/async/xn.6510c0abe2.js +0 -2
  382. package/dist/client/static/js/async/xn.6510c0abe2.js.LICENSE.txt +0 -6
  383. package/dist/client/static/js/async/xn1.83a7784fe1.js +0 -10
  384. package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +0 -11
  385. package/dist/client/static/js/async/xnl.feb0021e50.js +0 -2
  386. package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +0 -12
  387. package/dist/client/static/js/async/y.1a9ed7a5d8.js +0 -1
  388. package/dist/client/static/js/async/y6.43c57defe4.js +0 -1
  389. package/dist/client/static/js/async/yb.8c1342401d.js +0 -1
  390. package/dist/client/static/js/async/ybq.4491f2f2dc.js +0 -1
  391. package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
  392. package/dist/client/static/js/async/yq.917ddd7ba2.js +0 -1
  393. package/dist/client/static/js/async/ys.00687cf82d.js +0 -38
  394. package/dist/client/static/js/async/yv.d99d829136.js +0 -1
  395. package/dist/client/static/js/async/z.56f61a4374.js +0 -1
  396. package/dist/client/static/js/async/zi.5fdb07b86a.js +0 -24
  397. package/dist/client/static/js/async/zj.654f37e8ff.js +0 -1
  398. package/dist/client/static/js/async/zn.966d6f1cc6.js +0 -1
  399. package/dist/client/static/js/async/zo.26213b40c0.js +0 -1
  400. package/dist/client/static/js/async/zx.e9f4105e00.js +0 -1
  401. package/dist/client/static/js/index.e6a257402a.js +0 -38
  402. package/dist/client/static/js/index.e6a257402a.js.LICENSE.txt +0 -4
  403. package/dist/client/static/js/markdown-vendor.71e43278b3.js +0 -144
  404. package/dist/client/static/js/shiki-vendor.cb8452ddcd.js +0 -1
  405. package/src/client/components/settings/categories/provider-settings/Icons.tsx +0 -35
  406. package/src/client/hooks/settings/providers.ts +0 -326
  407. package/src/server/plugins/dev-assets.ts +0 -49
  408. package/src/shared/lib/code/editor-utils.ts +0 -16
  409. package/src/shared/lib/models/provider-presets.ts +0 -29
  410. /package/dist/client/{apple-touch-icon.png → static/apple-touch-icon-dskx5sr3.png} +0 -0
  411. /package/dist/client/{icon-192x192.png → static/icon-192x192-6gn5cr2f.png} +0 -0
  412. /package/dist/client/{icon.svg → static/icon-d07z9dtz.svg} +0 -0
  413. /package/dist/client/{manifest.webmanifest → static/manifest-bnyy75kt.webmanifest} +0 -0
  414. /package/{dist/client → public}/icon-512x512.png +0 -0
  415. /package/{dist/client → public}/icon-maskable-512x512.png +0 -0
  416. /package/{dist/client → public}/sw.js +0 -0
  417. /package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.ts +0 -0
  418. /package/src/shared/lib/models/{provider-cleanup.ts → provider/cleanup.ts} +0 -0
  419. /package/src/shared/lib/models/{provider-label.ts → provider/label.ts} +0 -0
@@ -0,0 +1,322 @@
1
+ import { useState } from 'preact/hooks';
2
+ import type { FormEvent } from 'preact/compat';
3
+ import { Check, Globe, Hash, Layers, Plus, Sparkles } from 'lucide-preact';
4
+ import type { ProviderItem } from '@/shared/types';
5
+ import { addProviderModel, type ManualModelRequest } from '@/shared/lib/models/provider/models';
6
+ import {
7
+ PROVIDER_THINKING_EFFORTS,
8
+ type ProviderThinkingEffort,
9
+ } from '@/shared/lib/models/provider/dialect';
10
+ import { Modal } from '@/client/components/common/Modal';
11
+ import { EffortLadder } from '@/client/components/settings/categories/provider-settings/EffortLadder';
12
+ import { PriceFields } from '@/client/components/settings/categories/provider-settings/PriceFields';
13
+
14
+ interface AddModelModalProps {
15
+ isOpen: boolean;
16
+ provider: ProviderItem;
17
+ onClose: () => void;
18
+ /** Called after a successful write so the list and the chat picker refresh. */
19
+ onAdded: (modelId: string) => void;
20
+ /** Reports a refusal the dialog cannot fix itself (bad endpoint, bad key). */
21
+ onError: (message: string) => void;
22
+ }
23
+
24
+ /**
25
+ * Register ONE model by hand.
26
+ *
27
+ * The auto-fetch needs a listing endpoint, and plenty of real providers have
28
+ * none worth using: an Azure deployment is a name the user chose, a gateway may
29
+ * serve a model its `/models` route omits, and Bedrock/Vertex expose no
30
+ * `/models` at all. This dialog writes the same models.yml entry the fetch
31
+ * would, with the metadata the user actually knows.
32
+ *
33
+ * Only `id` is required. Every other field is metadata omp can work without —
34
+ * a missing context window is estimated, a missing price is simply not
35
+ * accounted — so requiring them would block the common case of "just add this
36
+ * id I know exists".
37
+ */
38
+ export function AddModelModal({
39
+ isOpen,
40
+ provider,
41
+ onClose,
42
+ onAdded,
43
+ onError,
44
+ }: AddModelModalProps) {
45
+ const [id, setId] = useState('');
46
+ const [name, setName] = useState('');
47
+ const [contextWindow, setContextWindow] = useState('');
48
+ const [maxTokens, setMaxTokens] = useState('');
49
+ const [costInput, setCostInput] = useState('');
50
+ const [costOutput, setCostOutput] = useState('');
51
+ const [costCacheRead, setCostCacheRead] = useState('');
52
+ const [costCacheWrite, setCostCacheWrite] = useState('');
53
+ const [reasoning, setReasoning] = useState(false);
54
+ const [imageInput, setImageInput] = useState(false);
55
+ const [efforts, setEfforts] = useState<ProviderThinkingEffort[]>([]);
56
+ const [isSubmitting, setIsSubmitting] = useState(false);
57
+
58
+ if (!isOpen) return null;
59
+
60
+ const trimmedId = id.trim();
61
+ const duplicate = provider.models.some((model) => model.id === trimmedId);
62
+ const idError = !trimmedId
63
+ ? 'Enter the model id exactly as the provider expects it.'
64
+ : duplicate
65
+ ? `"${trimmedId}" is already registered under this provider.`
66
+ : '';
67
+ const numberError = (value: string, label: string): string =>
68
+ value.trim() && !(Number(value) > 0) ? `${label} must be a positive number of tokens.` : '';
69
+ const limitsError = numberError(contextWindow, 'Context window') || numberError(maxTokens, 'Max output');
70
+ const costError = (value: string): string =>
71
+ value.trim() && !(Number(value) >= 0) ? 'Prices must be zero or more USD per 1M tokens.' : '';
72
+ const priceError = costError(costInput) || costError(costOutput)
73
+ || costError(costCacheRead) || costError(costCacheWrite);
74
+ // A price is written only when BOTH headline sides are known: omp's schema
75
+ // requires all four cost fields together, so half a price is not a price. The
76
+ // two cache rates may be left empty — they are written as 0, which is what
77
+ // "no cached-token discount" means.
78
+ const halfPrice = Boolean(costInput.trim()) !== Boolean(costOutput.trim())
79
+ ? 'Enter both input and output price, or neither.'
80
+ : (costCacheRead.trim() || costCacheWrite.trim()) && !costInput.trim()
81
+ ? 'A cache rate needs the input and output price too.'
82
+ : '';
83
+ // The ladder is a capability of a reasoning model; ticking levels while
84
+ // Reasoning is off would describe a model omp is not told can reason.
85
+ const ladderError = !reasoning && efforts.length > 0
86
+ ? 'Turn on Reasoning to declare its levels.'
87
+ : '';
88
+ const canSubmit = Boolean(trimmedId) && !idError && !limitsError && !priceError
89
+ && !halfPrice && !ladderError && !isSubmitting;
90
+
91
+ const numberOrUndefined = (value: string): number | undefined => {
92
+ const parsed = Number(value);
93
+ return value.trim() && Number.isFinite(parsed) ? parsed : undefined;
94
+ };
95
+
96
+ const toggleEffort = (effort: ProviderThinkingEffort) => {
97
+ setEfforts((prev) => (
98
+ prev.includes(effort)
99
+ ? prev.filter((entry) => entry !== effort)
100
+ // Kept in omp's own order so the written ladder reads lowest-first
101
+ // regardless of the order the user ticked the boxes.
102
+ : PROVIDER_THINKING_EFFORTS.filter((level) => level === effort || prev.includes(level))
103
+ ));
104
+ };
105
+
106
+ const handleSubmit = async (e: FormEvent) => {
107
+ e.preventDefault();
108
+ if (!canSubmit) return;
109
+ setIsSubmitting(true);
110
+ try {
111
+ const request: ManualModelRequest = {
112
+ provider: provider.slug,
113
+ id: trimmedId,
114
+ ...(name.trim() ? { name: name.trim() } : {}),
115
+ ...(numberOrUndefined(contextWindow) !== undefined
116
+ ? { contextWindow: numberOrUndefined(contextWindow) }
117
+ : {}),
118
+ ...(numberOrUndefined(maxTokens) !== undefined ? { maxTokens: numberOrUndefined(maxTokens) } : {}),
119
+ ...(reasoning ? { reasoning: true } : {}),
120
+ ...(reasoning && efforts.length > 0 ? { efforts } : {}),
121
+ ...(imageInput ? { imageInput: true } : {}),
122
+ ...(costInput.trim() && costOutput.trim()
123
+ ? {
124
+ costInput: numberOrUndefined(costInput),
125
+ costOutput: numberOrUndefined(costOutput),
126
+ // Empty cache rates become 0 in the seed builder; sending them only
127
+ // when typed keeps the request honest about what the user entered.
128
+ ...(numberOrUndefined(costCacheRead) !== undefined
129
+ ? { costCacheRead: numberOrUndefined(costCacheRead) }
130
+ : {}),
131
+ ...(numberOrUndefined(costCacheWrite) !== undefined
132
+ ? { costCacheWrite: numberOrUndefined(costCacheWrite) }
133
+ : {}),
134
+ }
135
+ : {}),
136
+ // The endpoint and credential are only consulted for a provider with no
137
+ // models.yml entry yet; the writer is add-only and keeps an existing
138
+ // one. A keyless provider passes `auth: none` so omp accepts the entry
139
+ // without a credential.
140
+ ...(provider.baseUrl ? { baseUrl: provider.baseUrl } : {}),
141
+ ...(provider.apiKey ? { apiKey: provider.apiKey } : {}),
142
+ ...(provider.auth ? { auth: provider.auth } : {}),
143
+ };
144
+ const result = await addProviderModel(request);
145
+ if (!result.success) {
146
+ // The refusal is the actionable half (an endpoint override with no
147
+ // credential, a provider not in models.yml) — surface it verbatim.
148
+ onError(result.error || result.reason || `Could not register ${trimmedId}.`);
149
+ return;
150
+ }
151
+ onAdded(trimmedId);
152
+ onClose();
153
+ } finally {
154
+ setIsSubmitting(false);
155
+ }
156
+ };
157
+
158
+ return (
159
+ <Modal
160
+ onClose={onClose}
161
+ header={
162
+ <div className="flex items-center gap-2">
163
+ <Plus size={16} className="text-ink/70" />
164
+ <div>
165
+ <h3 className="text-sm font-semibold text-ink">Add Model</h3>
166
+ <p className="text-[11px] text-ink/50 font-mono truncate max-w-[280px]">{provider.slug}</p>
167
+ </div>
168
+ </div>
169
+ }
170
+ form={{ onSubmit: handleSubmit, className: 'p-5 space-y-4' }}
171
+ footer={
172
+ <div className="pt-2 flex items-center justify-end gap-2 border-t border-ink/10">
173
+ <button
174
+ type="button"
175
+ onClick={onClose}
176
+ className="px-3.5 py-1.5 rounded-md border border-ink/20 text-xs font-medium text-ink hover:bg-ink/5 transition-colors cursor-pointer"
177
+ >
178
+ Cancel
179
+ </button>
180
+ <button
181
+ type="submit"
182
+ disabled={!canSubmit}
183
+ className="px-3.5 py-1.5 rounded-md bg-ink text-canvas text-xs font-medium hover:opacity-90 transition-opacity cursor-pointer disabled:opacity-50 flex items-center gap-1.5"
184
+ >
185
+ {isSubmitting ? (
186
+ <>
187
+ <Layers size={14} className="animate-pulse" />
188
+ <span>Registering...</span>
189
+ </>
190
+ ) : (
191
+ <>
192
+ <Check size={14} />
193
+ <span>Add Model</span>
194
+ </>
195
+ )}
196
+ </button>
197
+ </div>
198
+ }
199
+ >
200
+ {/* Model id */}
201
+ <div>
202
+ <label className="block text-xs font-semibold text-ink mb-1">Model ID</label>
203
+ <div className="relative flex items-center">
204
+ <Hash size={13} className="absolute left-2.5 text-ink/40 pointer-events-none" />
205
+ <input
206
+ type="text"
207
+ required
208
+ autoFocus
209
+ value={id}
210
+ onChange={(e) => setId(e.currentTarget.value)}
211
+ placeholder="e.g. claude-sonnet-5 or my-deployment"
212
+ className="w-full bg-paper border border-ink/20 rounded-md pl-8 pr-3 py-1.5 text-xs text-ink outline-none focus:border-ink/60 font-mono"
213
+ />
214
+ </div>
215
+ {idError && <p className="text-[11px] text-error mt-1">{idError}</p>}
216
+ <p className="text-[10px] text-ink/40 mt-1">
217
+ Sent to the provider verbatim — this is the id omp puts on the wire.
218
+ </p>
219
+ </div>
220
+
221
+ {/* Display name */}
222
+ <div>
223
+ <label className="block text-xs font-semibold text-ink mb-1">
224
+ Display Name <span className="font-normal text-ink/40">— optional</span>
225
+ </label>
226
+ <input
227
+ type="text"
228
+ value={name}
229
+ onChange={(e) => setName(e.currentTarget.value)}
230
+ placeholder="Shown in the chat model picker"
231
+ className="w-full bg-paper border border-ink/20 rounded-md px-3 py-1.5 text-xs text-ink outline-none focus:border-ink/60"
232
+ />
233
+ </div>
234
+
235
+ {/* Limits */}
236
+ <div className="grid grid-cols-2 gap-3">
237
+ <div>
238
+ <label className="block text-xs font-semibold text-ink mb-1">
239
+ Context window <span className="font-normal text-ink/40">— tokens</span>
240
+ </label>
241
+ <input
242
+ type="text"
243
+ inputMode="numeric"
244
+ value={contextWindow}
245
+ onChange={(e) => setContextWindow(e.currentTarget.value)}
246
+ placeholder="200000"
247
+ className="w-full bg-paper border border-ink/20 rounded-md px-3 py-1.5 text-xs text-ink outline-none focus:border-ink/60 font-mono"
248
+ />
249
+ </div>
250
+ <div>
251
+ <label className="block text-xs font-semibold text-ink mb-1">
252
+ Max output <span className="font-normal text-ink/40">— tokens</span>
253
+ </label>
254
+ <input
255
+ type="text"
256
+ inputMode="numeric"
257
+ value={maxTokens}
258
+ onChange={(e) => setMaxTokens(e.currentTarget.value)}
259
+ placeholder="8192"
260
+ className="w-full bg-paper border border-ink/20 rounded-md px-3 py-1.5 text-xs text-ink outline-none focus:border-ink/60 font-mono"
261
+ />
262
+ </div>
263
+ </div>
264
+ {limitsError && <p className="text-[11px] text-error -mt-2">{limitsError}</p>}
265
+
266
+ {/* Capabilities */}
267
+ <div>
268
+ <label className="block text-xs font-semibold text-ink mb-1.5">Capabilities</label>
269
+ <div className="grid grid-cols-2 gap-2">
270
+ <button
271
+ type="button"
272
+ onClick={() => setReasoning((prev) => !prev)}
273
+ aria-pressed={reasoning}
274
+ className={`flex items-center gap-2 px-2.5 py-2 rounded-lg border text-left text-[11px] transition-colors cursor-pointer ${
275
+ reasoning ? 'border-ink/50 bg-ink/5 font-semibold text-ink' : 'border-ink/15 hover:border-ink/30 text-ink/70'
276
+ }`}
277
+ >
278
+ <Sparkles size={13} className="shrink-0" />
279
+ <span className="truncate">Reasoning</span>
280
+ </button>
281
+ <button
282
+ type="button"
283
+ onClick={() => setImageInput((prev) => !prev)}
284
+ aria-pressed={imageInput}
285
+ className={`flex items-center gap-2 px-2.5 py-2 rounded-lg border text-left text-[11px] transition-colors cursor-pointer ${
286
+ imageInput ? 'border-ink/50 bg-ink/5 font-semibold text-ink' : 'border-ink/15 hover:border-ink/30 text-ink/70'
287
+ }`}
288
+ >
289
+ <Globe size={13} className="shrink-0" />
290
+ <span className="truncate">Vision</span>
291
+ </button>
292
+ </div>
293
+ <p className="text-[10px] text-ink/40 mt-1">
294
+ Leave both off when unsure — omp still serves the model; only its declared abilities change.
295
+ </p>
296
+ </div>
297
+
298
+ {/* Reasoning levels — only meaningful for a reasoning model */}
299
+ {reasoning && (
300
+ <EffortLadder
301
+ selected={efforts}
302
+ onToggle={toggleEffort}
303
+ onSelectAll={() => setEfforts([...PROVIDER_THINKING_EFFORTS])}
304
+ onClear={() => setEfforts([])}
305
+ />
306
+ )}
307
+ {ladderError && <p className="text-[11px] text-error -mt-2">{ladderError}</p>}
308
+
309
+ <PriceFields
310
+ costInput={costInput}
311
+ costOutput={costOutput}
312
+ costCacheRead={costCacheRead}
313
+ costCacheWrite={costCacheWrite}
314
+ onCostInputChange={setCostInput}
315
+ onCostOutputChange={setCostOutput}
316
+ onCostCacheReadChange={setCostCacheRead}
317
+ onCostCacheWriteChange={setCostCacheWrite}
318
+ error={priceError || halfPrice}
319
+ />
320
+ </Modal>
321
+ );
322
+ }
@@ -1,21 +1,27 @@
1
1
  import { useEffect, useState } from 'preact/hooks';
2
2
  import { Check, DownloadCloud, Globe, Key, Layers } from 'lucide-preact';
3
3
  import type { ProviderItem } from '@/shared/types';
4
- import type { PresetProviderOption } from '@/shared/types/settings/provider';
5
- import { fetchProviderModelsRemote } from '@/shared/lib/models/provider-models';
6
- import { ProviderIcon } from '@/client/components/settings/categories/provider-settings/Icons';
4
+ import type {
5
+ OmpProviderApi,
6
+ PresetProviderOption,
7
+ ProviderAuthMode,
8
+ ProviderDiscoveryType,
9
+ ProviderModelSource,
10
+ } from '@/shared/types/settings/provider';
11
+ import { fetchProviderModelsRemote } from '@/shared/lib/models/provider/models';
12
+ import { inferProviderApi } from '@/shared/lib/models/provider/dialect';
7
13
  import { Modal } from '@/client/components/common/Modal';
14
+ import { DialectFields } from '@/client/components/settings/categories/provider-settings/DialectFields';
15
+ import { PresetPicker } from '@/client/components/settings/categories/provider-settings/PresetPicker';
8
16
 
9
17
  interface AddProviderModalProps {
10
18
  isOpen: boolean;
11
19
  onClose: () => void;
12
- onAddProvider: (newProvider: ProviderItem, options: { fetchedCount: number }) => void;
20
+ onAddProvider: (newProvider: ProviderItem, options: { fetchedCount: number; ompNote?: string }) => void;
13
21
  presets: PresetProviderOption[];
14
22
  /**
15
23
  * Slugs already present in the omp registry. A new provider's slug becomes a
16
- * `models.yml` key, so reusing one silently overwrites that provider's
17
- * `baseUrl`, `apiKey`, `api` and per-model metadata — the entry is written
18
- * under the same key, not merged into it.
24
+ * models.yml key, so reusing one would overwrite that provider's entry.
19
25
  */
20
26
  existingSlugs: string[];
21
27
  }
@@ -23,6 +29,40 @@ interface AddProviderModalProps {
23
29
  /** Slug shape omp accepts as a `models.yml` provider key. */
24
30
  const SLUG_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
25
31
 
32
+ /** The dialect a preset seeds, with the model-source its shape implies. */
33
+ function presetDefaults(preset: PresetProviderOption | undefined) {
34
+ const api: OmpProviderApi = preset?.api ?? inferProviderApi(preset?.defaultUrl ?? '');
35
+ return {
36
+ api,
37
+ auth: preset?.auth ?? ('apiKey' as ProviderAuthMode),
38
+ discovery: preset?.discovery ?? ('openai-models-list' as ProviderDiscoveryType),
39
+ source: (preset?.discovery
40
+ ? 'discovery'
41
+ : preset?.bundled
42
+ ? 'override'
43
+ : 'fetch') as ProviderModelSource,
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Add Provider.
49
+ *
50
+ * The form exists to write ONE `models.yml` provider entry, so it asks for
51
+ * exactly the fields omp reads: the endpoint, the wire dialect, the auth mode,
52
+ * and where the model list comes from. Everything else is derived.
53
+ *
54
+ * Two shapes the previous version could not express are the point of this
55
+ * dialog:
56
+ *
57
+ * - A provider omp already bundles (Azure, Bedrock, Vertex, Codex) is written
58
+ * as an OVERRIDE entry: omp keeps its own model list and only the endpoint
59
+ * changes. Writing ids for it would shadow the bundled catalog with a
60
+ * snapshot.
61
+ * - A provider omp can discover (Ollama, LM Studio, llama.cpp, LiteLLM, any
62
+ * OpenAI-compatible endpoint) is registered with an EMPTY model list and a
63
+ * `discovery` block, so omp lists the models live instead of serving a frozen
64
+ * copy that goes stale the moment the endpoint adds one.
65
+ */
26
66
  export function AddProviderModal({
27
67
  isOpen,
28
68
  onClose,
@@ -32,16 +72,26 @@ export function AddProviderModal({
32
72
  }: AddProviderModalProps) {
33
73
  const presetList = presets;
34
74
  const initialPreset = presetList[0];
75
+ const initial = presetDefaults(initialPreset);
35
76
  const [selectedPresetId, setSelectedPresetId] = useState(initialPreset?.id || 'openai');
36
77
  const [name, setName] = useState(initialPreset?.name || 'OpenAI');
37
78
  const [baseUrl, setBaseUrl] = useState(initialPreset?.defaultUrl || 'https://api.openai.com/v1');
38
79
  const [apiKey, setApiKey] = useState('');
80
+ const [api, setApi] = useState<OmpProviderApi>(initial.api);
81
+ const [auth, setAuth] = useState<ProviderAuthMode>(initial.auth);
82
+ const [source, setSource] = useState<ProviderModelSource>(initial.source);
83
+ const [discovery, setDiscovery] = useState<ProviderDiscoveryType>(initial.discovery);
39
84
  const [isSubmitting, setIsSubmitting] = useState(false);
40
85
 
41
86
  const handleSelectPreset = (preset: PresetProviderOption) => {
87
+ const defaults = presetDefaults(preset);
42
88
  setSelectedPresetId(preset.id);
43
89
  setName(preset.name);
44
90
  setBaseUrl(preset.defaultUrl);
91
+ setApi(defaults.api);
92
+ setAuth(defaults.auth);
93
+ setDiscovery(defaults.discovery);
94
+ setSource(defaults.source);
45
95
  };
46
96
 
47
97
  useEffect(() => {
@@ -62,21 +112,37 @@ export function AddProviderModal({
62
112
  : trimmedName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
63
113
  ).toLowerCase();
64
114
  const takenSlugs = new Set(existingSlugs.map((value) => value.trim().toLowerCase()));
115
+ // A bundled preset names a provider omp already ships, and its entry in
116
+ // models.yml is an add-only OVERRIDE — reusing that slug is the point.
117
+ const isBundledOverride = selectedPreset?.bundled === true;
65
118
  const slugError = !slug
66
119
  ? 'Enter a provider name to derive an id.'
67
120
  : !SLUG_PATTERN.test(slug)
68
121
  ? 'The derived id must start with a letter or digit.'
69
- : takenSlugs.has(slug)
122
+ : takenSlugs.has(slug) && !isBundledOverride
70
123
  ? `"${slug}" is already registered in models.yml — connecting it would overwrite that provider. Pick another name.`
71
124
  : '';
125
+ // omp requires a key only for a provider that carries MODELS. An endpoint
126
+ // override (omp keeps its own model list) and a discovery shell are both
127
+ // valid without one — that is how a keyless local server and a proxy that
128
+ // authenticates through `/login` are declared — so the key is optional there
129
+ // rather than hidden, since a proxy usually still wants its own bearer.
130
+ const showsApiKey = auth === 'apiKey';
131
+ const requiresApiKey = showsApiKey && source === 'fetch';
132
+ const keyError = requiresApiKey && !apiKey.trim()
133
+ ? 'Enter the API key, or set Auth to "No auth" for a keyless local server.'
134
+ : '';
135
+ const canSubmit = Boolean(trimmedName) && !slugError && !keyError && !isSubmitting;
72
136
 
73
137
  const handleSubmit = async (e: { preventDefault: () => void }) => {
74
138
  e.preventDefault();
75
- if (!trimmedName || presetList.length === 0 || isSubmitting || slugError) return;
139
+ if (!canSubmit) return;
76
140
  setIsSubmitting(true);
77
141
 
78
142
  try {
79
143
  const timestamp = Date.now();
144
+ const trimmedUrl = baseUrl.trim();
145
+ const trimmedKey = apiKey.trim();
80
146
  const newProvider: ProviderItem = {
81
147
  id: `provider-${timestamp}`,
82
148
  name: trimmedName,
@@ -84,17 +150,41 @@ export function AddProviderModal({
84
150
  icon: selectedPreset?.icon || 'custom',
85
151
  status: 'connected',
86
152
  configuredIn: 'auth credentials',
87
- baseUrl: baseUrl.trim(),
88
- apiKey: apiKey.trim() || 'sk-custom-••••••••••••••••••••••••',
153
+ credentialSource: 'models.yml',
154
+ baseUrl: trimmedUrl,
155
+ ...(trimmedKey ? { apiKey: trimmedKey } : {}),
156
+ api,
157
+ ...(auth !== 'apiKey' ? { auth } : {}),
158
+ ...(source === 'discovery' ? { discovery } : {}),
159
+ modelSource: source,
89
160
  models: [],
90
161
  };
91
162
 
92
- const fetchResult = await fetchProviderModelsRemote(baseUrl.trim(), apiKey.trim() || undefined, newProvider.slug, true);
163
+ const fetchResult = await fetchProviderModelsRemote({
164
+ baseUrl: trimmedUrl,
165
+ apiKey: trimmedKey || undefined,
166
+ providerSlug: newProvider.slug,
167
+ persistToOmp: true,
168
+ api,
169
+ auth,
170
+ ...(source === 'discovery' ? { discovery } : {}),
171
+ ...(source === 'override' ? { registerOnly: true } : {}),
172
+ });
173
+
93
174
  if (fetchResult.ok && fetchResult.models) {
94
175
  newProvider.models = fetchResult.models;
95
- onAddProvider(newProvider, { fetchedCount: fetchResult.models.length });
176
+ onAddProvider(newProvider, {
177
+ fetchedCount: fetchResult.models.length,
178
+ ompNote: fetchResult.omp?.reason,
179
+ });
96
180
  } else {
97
- onAddProvider(newProvider, { fetchedCount: -1 });
181
+ // The listing failed but the provider may still have been registered
182
+ // (discovery/override shapes never list). Report what omp said rather
183
+ // than a bare "failed" that hides a written file.
184
+ onAddProvider(newProvider, {
185
+ fetchedCount: -1,
186
+ ompNote: fetchResult.omp?.written ? undefined : fetchResult.error,
187
+ });
98
188
  }
99
189
 
100
190
  onClose();
@@ -113,6 +203,7 @@ export function AddProviderModal({
113
203
  </div>
114
204
  }
115
205
  form={{ onSubmit: handleSubmit, className: 'p-5 space-y-4' }}
206
+ maxWidthClass="max-w-lg"
116
207
  footer={
117
208
  <div className="pt-2 flex items-center justify-end gap-2 border-t border-ink/10">
118
209
  <button
@@ -124,13 +215,13 @@ export function AddProviderModal({
124
215
  </button>
125
216
  <button
126
217
  type="submit"
127
- disabled={isSubmitting || presetList.length === 0 || Boolean(slugError)}
218
+ disabled={!canSubmit}
128
219
  className="px-3.5 py-1.5 rounded-md bg-ink text-canvas text-xs font-medium hover:opacity-90 transition-opacity cursor-pointer disabled:opacity-50 flex items-center gap-1.5"
129
220
  >
130
221
  {isSubmitting ? (
131
222
  <>
132
223
  <DownloadCloud size={14} className="animate-pulse" />
133
- <span>Fetching models...</span>
224
+ <span>{source === 'fetch' ? 'Fetching models...' : 'Registering...'}</span>
134
225
  </>
135
226
  ) : (
136
227
  <>
@@ -147,31 +238,11 @@ export function AddProviderModal({
147
238
  <label className="block text-xs font-semibold text-ink mb-1.5">
148
239
  Provider Preset
149
240
  </label>
150
- <div className="grid grid-cols-2 gap-2 max-h-36 scrollbar-overlay-container scrollbar-overlay-static pr-1">
151
- {presetList.length === 0 && (
152
- <p className="col-span-2 px-2 py-3 text-xs text-ink/50">
153
- All known provider types are already connected.
154
- </p>
155
- )}
156
- {presetList.map((preset) => {
157
- const isSelected = preset.id === selectedPresetId;
158
- return (
159
- <button
160
- key={preset.id}
161
- type="button"
162
- onClick={() => handleSelectPreset(preset)}
163
- className={`flex items-center gap-2 p-2 rounded-lg border text-left text-xs transition-colors cursor-pointer ${
164
- isSelected
165
- ? 'border-ink/50 bg-ink/5 font-semibold text-ink shadow-2xs'
166
- : 'border-ink/15 hover:border-ink/30 text-ink/70'
167
- }`}
168
- >
169
- <ProviderIcon icon={preset.icon} size={14} />
170
- <span className="truncate">{preset.name}</span>
171
- </button>
172
- );
173
- })}
174
- </div>
241
+ <PresetPicker
242
+ presets={presetList}
243
+ selectedPresetId={selectedPresetId}
244
+ onSelectPreset={handleSelectPreset}
245
+ />
175
246
  </div>
176
247
 
177
248
  {/* Provider Name */}
@@ -217,22 +288,47 @@ export function AddProviderModal({
217
288
  </div>
218
289
  </div>
219
290
 
220
- {/* API Key */}
221
- <div>
222
- <label className="block text-xs font-semibold text-ink mb-1">
223
- API Key (stored securely)
224
- </label>
225
- <div className="relative flex items-center">
226
- <Key size={13} className="absolute left-2.5 text-ink/40 pointer-events-none" />
227
- <input
228
- type="password"
229
- value={apiKey}
230
- onChange={(e) => setApiKey(e.currentTarget.value)}
231
- placeholder="sk-••••••••••••••••"
232
- className="w-full bg-paper border border-ink/20 rounded-md pl-8 pr-3 py-1.5 text-xs text-ink outline-none focus:border-ink/60 font-mono"
233
- />
291
+ <DialectFields
292
+ api={api}
293
+ onApiChange={setApi}
294
+ auth={auth}
295
+ onAuthChange={setAuth}
296
+ source={source}
297
+ onSourceChange={setSource}
298
+ discovery={discovery}
299
+ onDiscoveryChange={setDiscovery}
300
+ />
301
+
302
+ {/* API Key — hidden only for a keyless provider, which omp accepts
303
+ without a credential at all. */}
304
+ {showsApiKey && (
305
+ <div>
306
+ <label className="block text-xs font-semibold text-ink mb-1">
307
+ API Key (stored securely)
308
+ {!requiresApiKey && <span className="font-normal text-ink/40"> — optional</span>}
309
+ </label>
310
+ <div className="relative flex items-center">
311
+ <Key size={13} className="absolute left-2.5 text-ink/40 pointer-events-none" />
312
+ <input
313
+ type="password"
314
+ value={apiKey}
315
+ onChange={(e) => setApiKey(e.currentTarget.value)}
316
+ placeholder="sk-••••••••••••••••"
317
+ className="w-full bg-paper border border-ink/20 rounded-md pl-8 pr-3 py-1.5 text-xs text-ink outline-none focus:border-ink/60 font-mono"
318
+ />
319
+ </div>
320
+ {keyError && (
321
+ <p className="text-[11px] text-error mt-1">{keyError}</p>
322
+ )}
323
+ <p className="text-[10px] text-ink/40 mt-1">
324
+ {requiresApiKey
325
+ ? 'May name an environment variable (e.g. '
326
+ : 'Leave empty to keep using the provider’s existing credential. May name an environment variable (e.g. '}
327
+ <span className="font-mono">MY_KEY</span>) or run a command with a leading{' '}
328
+ <span className="font-mono">!</span>.
329
+ </p>
234
330
  </div>
235
- </div>
331
+ )}
236
332
  </Modal>
237
333
  );
238
334
  }