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
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
2
2
  import type { KeyboardEvent as ReactKeyboardEvent } from 'preact/compat';
3
3
  import { Check, ChevronDown, Search } from 'lucide-preact';
4
4
  import { statusLabel, type ProviderEntry, type UsageProviderId } from '@/client/components/settings/categories/usage-settings/providers';
5
+ import { ProviderIcon } from '@/client/components/common/provider-icon';
5
6
  import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
6
7
 
7
8
  interface ProviderSelectProps {
@@ -120,7 +121,8 @@ export function ProviderSelect({ providers, value, onChange }: ProviderSelectPro
120
121
  title="Select usage provider"
121
122
  className="flex w-full min-w-0 items-center justify-between gap-2 bg-paper border border-ink/15 rounded-lg px-3 py-1.5 text-xs text-ink hover:bg-ink/5 transition-colors"
122
123
  >
123
- <span className="flex min-w-0 items-baseline gap-2">
124
+ <span className="flex min-w-0 items-center gap-2">
125
+ {selected && <ProviderIcon slug={selected.id} name={selected.name} size={13} className="text-ink/60" />}
124
126
  <span className="truncate font-medium">{selected?.name ?? 'Select provider'}</span>
125
127
  {selected && (
126
128
  <span className="truncate text-[10px] text-ink/50">{statusLabel(selected)}</span>
@@ -190,10 +192,13 @@ export function ProviderSelect({ providers, value, onChange }: ProviderSelectPro
190
192
  index === activeIndex ? 'bg-ink/5' : 'hover:bg-ink/5'
191
193
  }`}
192
194
  >
193
- <span className="min-w-0">
194
- <span className="block truncate font-medium text-ink">{provider.name}</span>
195
- <span className="block truncate text-[10px] text-ink/50">
196
- {statusLabel(provider)} · {provider.hint}
195
+ <span className="flex min-w-0 items-center gap-2">
196
+ <ProviderIcon slug={provider.id} name={provider.name} size={14} className="text-ink/60" />
197
+ <span className="min-w-0">
198
+ <span className="block truncate font-medium text-ink">{provider.name}</span>
199
+ <span className="block truncate text-[10px] text-ink/50">
200
+ {statusLabel(provider)} · {provider.hint}
201
+ </span>
197
202
  </span>
198
203
  </span>
199
204
  {provider.id === value && <Check size={12} className="flex-shrink-0 text-ink" />}
@@ -1,3 +1,4 @@
1
+ import { useEffect } from 'preact/hooks';
1
2
  import { AlertCircle, RefreshCw } from 'lucide-preact';
2
3
  import { ProviderDetail } from '@/client/components/settings/categories/usage-settings/ProviderDetail';
3
4
  import { buildProviders, type UsageProviderId } from '@/client/components/settings/categories/usage-settings/providers';
@@ -14,19 +15,30 @@ interface UsagePanelProps {
14
15
  active?: boolean;
15
16
  }
16
17
 
17
- /** Right-panel Usage: the selected provider's balance/quota via a dropdown. */
18
+ /** Right-panel Usage: the selected provider's quota and balance via a dropdown. */
18
19
  export function UsagePanel({ className = '', active = true }: UsagePanelProps) {
19
20
  const { report, isLoading, error, reload } = useUsageReport();
20
21
  const [selectedProviderId, setSelectedProviderId] = useSessionState<UsageProviderId>(
21
22
  'usage.selectedProviderId',
22
- 'kenari',
23
+ '',
23
24
  );
24
25
 
25
- // Auto refresh: keep balance/quota current without a manual reload. Silent —
26
- // the spinner stays reserved for the user's own refresh button. Polls only
27
- // while the panel is actually visible.
26
+ // Derived before every early return: a hook must not sit behind a conditional
27
+ // return, or the hook count changes between the loading and loaded renders.
28
+ const providers = report ? buildProviders(report) : [];
29
+ const selected = providers.find((provider) => provider.id === selectedProviderId) ?? providers[0];
30
+
31
+ // Auto refresh: keep quota current without a manual reload. Silent — the
32
+ // spinner stays reserved for the user's own refresh button. Polls only while
33
+ // the panel is actually visible.
28
34
  usePanelRefresh(() => reload({ silent: true }), active);
29
35
 
36
+ // A provider can disappear from the report (its key was removed) or a new one
37
+ // can take the first slot; keep the persisted selection valid.
38
+ useEffect(() => {
39
+ if (selected && selected.id !== selectedProviderId) setSelectedProviderId(selected.id);
40
+ }, [selected, selectedProviderId, setSelectedProviderId]);
41
+
30
42
  if (error && !report) {
31
43
  return (
32
44
  <div className={`flex h-full w-full flex-col items-center justify-center gap-2 p-8 text-center text-xs ${className}`}>
@@ -51,8 +63,6 @@ export function UsagePanel({ className = '', active = true }: UsagePanelProps) {
51
63
  );
52
64
  }
53
65
 
54
- const providers = buildProviders(report);
55
-
56
66
  return (
57
67
  <div className={`flex h-full w-full min-w-0 min-h-0 flex-col overflow-hidden bg-paper text-xs text-ink ${className}`}>
58
68
  <div className="px-3.5 py-2.5 border-b border-ink/10 flex items-center justify-between gap-3 flex-shrink-0 min-w-0">
@@ -69,7 +79,7 @@ export function UsagePanel({ className = '', active = true }: UsagePanelProps) {
69
79
  </div>
70
80
  <button
71
81
  type="button"
72
- onClick={() => reload()}
82
+ onClick={() => reload({ force: true })}
73
83
  disabled={isLoading}
74
84
  className="p-1.5 bg-paper border border-ink/15 rounded-md hover:bg-ink/5 text-ink/80 transition-colors disabled:opacity-50 flex-shrink-0"
75
85
  title="Refresh usage"
@@ -78,20 +88,28 @@ export function UsagePanel({ className = '', active = true }: UsagePanelProps) {
78
88
  </button>
79
89
  </div>
80
90
 
81
- <div className="px-3.5 py-2.5 border-b border-ink/10 flex-shrink-0 min-w-0">
82
- <div className="flex items-center gap-2 min-w-0">
83
- <span className="text-[11px] font-semibold text-ink/60 flex-shrink-0">Provider</span>
84
- <ProviderSelect
85
- providers={providers}
86
- value={selectedProviderId}
87
- onChange={setSelectedProviderId}
88
- />
91
+ {providers.length > 0 && (
92
+ <div className="px-3.5 py-2.5 border-b border-ink/10 flex-shrink-0 min-w-0">
93
+ <div className="flex items-center gap-2 min-w-0">
94
+ <span className="text-[11px] font-semibold text-ink/60 flex-shrink-0">Provider</span>
95
+ <ProviderSelect
96
+ providers={providers}
97
+ value={selected?.id ?? ''}
98
+ onChange={setSelectedProviderId}
99
+ />
100
+ </div>
89
101
  </div>
90
- </div>
102
+ )}
91
103
 
92
104
  <div className="flex-1 min-w-0 min-h-0 w-full overflow-y-auto scrollbar-overlay-container scrollbar-overlay-static p-3.5 space-y-4">
93
105
  {error && <p className="text-error text-xs">{error}</p>}
94
- <ProviderDetail providerId={selectedProviderId} report={report} />
106
+ {selected ? (
107
+ <ProviderDetail summary={selected.summary} />
108
+ ) : (
109
+ <p className="text-[11px] text-ink/50 leading-relaxed">
110
+ No provider credentials detected. Add an API key in Settings → Providers and it will appear here.
111
+ </p>
112
+ )}
95
113
  </div>
96
114
  </div>
97
115
  );
@@ -42,7 +42,7 @@ export function getSampleDialogueSession(): {
42
42
  id: '1a6cb095',
43
43
  role: 'ai',
44
44
  content:
45
- 'Elysia + Bun app (OMPChamber). Top-level contents:\n\n- **App source** — `src/` (client, server, shared)\n- **Config** — `package.json`, `tsconfig.json`, `rsbuild.config.ts`, `postcss.config.mjs`, `bun.lock`, `AGENTS.md`, `DESIGN.md`\n- **Static/PWA** — `public/` (icons, manifest, service worker)\n- **DB** — `workspace.db` (112 KB)\n- **Misc** — `notes/` (2 planning docs)\n\nRoot has no stray source files; project is cleanly under `src/`.',
45
+ 'Elysia + Bun app (OMPChamber). Top-level contents:\n\n- **App source** — `src/` (client, server, shared)\n- **Config** — `package.json`, `tsconfig.json`, `bunfig.toml`, `bun.lock`, `AGENTS.md`, `DESIGN.md`\n- **Static/PWA** — `public/` (icons, manifest, service worker)\n- **DB** — `workspace.db` (112 KB)\n- **Misc** — `notes/` (2 planning docs)\n\nRoot has no stray source files; project is cleanly under `src/`.',
46
46
  date: 'Sep 9, 04:33 AM',
47
47
  thinking: {
48
48
  thought: 'Brief listing.',
@@ -74,7 +74,7 @@ export const otherTools: ToolCallData[] = [
74
74
  "type": "module",
75
75
  "scripts": {
76
76
  "dev": "bun run --watch src/server/index.ts",
77
- "build": "rsbuild build"
77
+ "build": "bun run scripts/build-client.ts"
78
78
  }
79
79
  }`,
80
80
  status: 'success',
@@ -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
  ];
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The harness `use-editor-find` is exercised through.
8
+ *
9
+ * Split out because the find hook has two test files (opening and stepping /
10
+ * writing and options) and both must render it the same way — the session
11
+ * provider it needs, the surface calls it makes, and a fresh session id per test
12
+ * so the PERSISTED query does not leak between them.
13
+ *
14
+ * The hook's modules are imported dynamically, after the DOM globals exist:
15
+ * Preact binds its environment at evaluation time, and a static import would
16
+ * capture the DOM-less one.
17
+ */
18
+
19
+ import { afterAll, afterEach, beforeAll, beforeEach } from 'bun:test';
20
+ import { Window } from 'happy-dom';
21
+ import type { ComponentChildren, h as PreactH, render as PreactRender } from 'preact';
22
+ import type { act as PreactAct } from 'preact/test-utils';
23
+ import type { EditorFindState, useEditorFind as UseEditorFindHook } from '@/client/hooks/editor/use-editor-find';
24
+
25
+ export interface FindHarness {
26
+ /** The latest render's state; re-read after every settle, never held across one. */
27
+ readonly find: EditorFindState | null;
28
+ /** What the hook under test is handed — mutated per test, then re-rendered. */
29
+ readonly input: {
30
+ text: string;
31
+ documentKey: string;
32
+ selection: { start: number; end: number } | null;
33
+ };
34
+ /** Surface calls, so "did it select / reveal / write" is observable. */
35
+ readonly selections: Array<[number, number, boolean]>;
36
+ readonly reveals: number[];
37
+ readonly writes: Array<{ value: string; caretStart: number }>;
38
+ /** Re-render with the current `input` and settle. */
39
+ rerender: () => Promise<void>;
40
+ /** Flush pending effects and renders. */
41
+ flush: () => Promise<void>;
42
+ /**
43
+ * Preact's `act`, exposed because every interaction in these tests is a hook
44
+ * call the DOM never sees: it has to be wrapped to be flushed.
45
+ */
46
+ act: typeof PreactAct;
47
+ /** Register the before/after hooks this harness needs. */
48
+ install: () => void;
49
+ }
50
+
51
+ export async function createFindHarness(): Promise<FindHarness> {
52
+ let useEditorFind: typeof UseEditorFindHook;
53
+ let SessionStateContext: { Provider: (props: { value: { sessionId: string; ready: boolean }; children: ComponentChildren }) => ComponentChildren };
54
+ let h: typeof PreactH;
55
+ let render: typeof PreactRender;
56
+ let act: typeof PreactAct;
57
+
58
+ let find: EditorFindState | null = null;
59
+ let container: HTMLElement;
60
+ let sessionId = 'test-session';
61
+ let sessionCounter = 0;
62
+
63
+ const input: FindHarness['input'] = { text: 'alpha beta alpha', documentKey: 'file-1', selection: { start: 0, end: 5 } };
64
+ const selections: FindHarness['selections'] = [];
65
+ const reveals: number[] = [];
66
+ const writes: FindHarness['writes'] = [];
67
+
68
+ function Inner() {
69
+ find = useEditorFind({
70
+ text: input.text,
71
+ documentKey: input.documentKey,
72
+ readSelection: () => input.selection,
73
+ select: (start, end, focus = true) => selections.push([start, end, focus]),
74
+ reveal: (offset) => reveals.push(offset),
75
+ applyDocument: (value, caretStart) => writes.push({ value, caretStart }),
76
+ });
77
+ return null;
78
+ }
79
+
80
+ function Harness() {
81
+ // The hook persists the query and the toggles per session, so it needs the
82
+ // provider the panel supplies. A test session id is all it reads.
83
+ return h(SessionStateContext.Provider, { value: { sessionId, ready: true }, children: h(Inner, {}) });
84
+ }
85
+
86
+ async function flush() {
87
+ for (let i = 0; i < 3; i++) {
88
+ await act(async () => {
89
+ for (let turn = 0; turn < 4; turn++) await Promise.resolve();
90
+ });
91
+ }
92
+ }
93
+
94
+ async function rerender() {
95
+ await act(async () => {
96
+ render(h(Harness, {}), container);
97
+ });
98
+ await flush();
99
+ }
100
+
101
+ const domGlobals = (win: Window): Record<string, unknown> => ({
102
+ window: win,
103
+ document: win.document,
104
+ navigator: win.navigator,
105
+ Node: win.Node,
106
+ Element: win.Element,
107
+ HTMLElement: win.HTMLElement,
108
+ });
109
+
110
+ const installed: Record<string, unknown> = {};
111
+ const displaced: Record<string, unknown> = {};
112
+
113
+ return {
114
+ get find() {
115
+ return find;
116
+ },
117
+ input,
118
+ selections,
119
+ reveals,
120
+ writes,
121
+ rerender,
122
+ flush,
123
+ get act() {
124
+ return act;
125
+ },
126
+ install() {
127
+ beforeAll(async () => {
128
+ const win = new Window({ url: 'http://localhost' });
129
+ for (const [key, value] of Object.entries(domGlobals(win))) {
130
+ displaced[key] = (globalThis as Record<string, unknown>)[key];
131
+ installed[key] = value;
132
+ (globalThis as Record<string, unknown>)[key] = value;
133
+ }
134
+ ({ useEditorFind } = await import('@/client/hooks/editor/use-editor-find'));
135
+ ({ SessionStateContext } = await import('@/client/hooks/workspace/session-state/context'));
136
+ ({ h, render } = await import('preact'));
137
+ ({ act } = await import('preact/test-utils'));
138
+ });
139
+
140
+ afterAll(() => {
141
+ for (const key of Object.keys(installed)) {
142
+ if (displaced[key] === undefined) delete (globalThis as Record<string, unknown>)[key];
143
+ else (globalThis as Record<string, unknown>)[key] = displaced[key];
144
+ }
145
+ });
146
+
147
+ beforeEach(async () => {
148
+ find = null;
149
+ // The query and the toggles are persisted per session — that is the
150
+ // feature — so each test gets its OWN session id rather than sharing a
151
+ // blob with the test before it.
152
+ sessionId = `test-session-${++sessionCounter}`;
153
+ input.text = 'alpha beta alpha';
154
+ input.documentKey = 'file-1';
155
+ input.selection = { start: 0, end: 5 };
156
+ selections.length = 0;
157
+ reveals.length = 0;
158
+ writes.length = 0;
159
+ container = document.createElement('div');
160
+ document.body.appendChild(container);
161
+ await rerender();
162
+ });
163
+
164
+ afterEach(() => {
165
+ render(null, container);
166
+ container.remove();
167
+ });
168
+ },
169
+ };
170
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The scroll-container walk, shared by the editor's lazy window and the find
8
+ * widget's reveal.
9
+ *
10
+ * The two must agree: one resolves which lines to draw from the visible band,
11
+ * the other moves that band. Two copies of this walk would let the widget scroll
12
+ * a container the window never reads — the match would be marked somewhere
13
+ * nobody can see. Exercised against a real DOM (happy-dom) because the answer is
14
+ * a computed style.
15
+ */
16
+
17
+ import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
18
+ import { Window } from 'happy-dom';
19
+
20
+ import { findScrollContainer } from '@/client/hooks/editor/scroll-container';
21
+
22
+ const installed: Record<string, unknown> = {};
23
+ const displaced: Record<string, unknown> = {};
24
+
25
+ beforeAll(() => {
26
+ const win = new Window({ url: 'http://localhost' });
27
+ for (const key of ['window', 'document', 'Node', 'Element', 'HTMLElement', 'getComputedStyle']) {
28
+ displaced[key] = (globalThis as Record<string, unknown>)[key];
29
+ installed[key] = (win as unknown as Record<string, unknown>)[key];
30
+ (globalThis as Record<string, unknown>)[key] = installed[key];
31
+ }
32
+ });
33
+
34
+ afterAll(() => {
35
+ for (const key of Object.keys(installed)) {
36
+ if (displaced[key] === undefined) delete (globalThis as Record<string, unknown>)[key];
37
+ else (globalThis as Record<string, unknown>)[key] = displaced[key];
38
+ }
39
+ });
40
+
41
+ /** An element whose computed style reports `overflow-y`, with a scrollable height. */
42
+ function makeElement(overflowY: string, scrollable: boolean): HTMLElement {
43
+ const element = document.createElement('div');
44
+ element.style.overflowY = overflowY;
45
+ if (scrollable) {
46
+ // happy-dom reports clientHeight 0, so the walk's `scrollHeight > clientHeight`
47
+ // test needs a stubbed pair to describe a container that can actually scroll.
48
+ Object.defineProperty(element, 'scrollHeight', { value: 400, configurable: true });
49
+ Object.defineProperty(element, 'clientHeight', { value: 100, configurable: true });
50
+ }
51
+ return element;
52
+ }
53
+
54
+ describe('findScrollContainer', () => {
55
+ test('finds the nearest ancestor that scrolls', () => {
56
+ const outer = makeElement('auto', true);
57
+ const inner = makeElement('visible', false);
58
+ const surface = document.createElement('div');
59
+ inner.appendChild(surface);
60
+ outer.appendChild(inner);
61
+ document.body.appendChild(outer);
62
+
63
+ expect(findScrollContainer(surface)).toBe(outer);
64
+ outer.remove();
65
+ });
66
+
67
+ test('skips a container that cannot actually scroll', () => {
68
+ // `overflow: auto` on a box whose content fits is not a scroller: returning
69
+ // it would make `revealOffset` write scrollTop onto a box that ignores it.
70
+ const outer = makeElement('auto', true);
71
+ const fitted = makeElement('auto', false);
72
+ const surface = document.createElement('div');
73
+ fitted.appendChild(surface);
74
+ outer.appendChild(fitted);
75
+ document.body.appendChild(outer);
76
+
77
+ expect(findScrollContainer(surface)).toBe(outer);
78
+ outer.remove();
79
+ });
80
+
81
+ test('reports nothing when no ancestor scrolls', () => {
82
+ const plain = document.createElement('div');
83
+ const surface = document.createElement('div');
84
+ plain.appendChild(surface);
85
+ document.body.appendChild(plain);
86
+
87
+ expect(findScrollContainer(surface)).toBeNull();
88
+ plain.remove();
89
+ });
90
+
91
+ test('prefers the nearest scroller, so a nested panel scrolls itself', () => {
92
+ const outer = makeElement('auto', true);
93
+ const inner = makeElement('scroll', true);
94
+ const surface = document.createElement('div');
95
+ inner.appendChild(surface);
96
+ outer.appendChild(inner);
97
+ document.body.appendChild(outer);
98
+
99
+ expect(findScrollContainer(surface)).toBe(inner);
100
+ outer.remove();
101
+ });
102
+ });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The ancestor a code surface actually scrolls inside.
8
+ *
9
+ * The lazy editor window and the find widget's "scroll to this match" both need
10
+ * this answer, and they must agree: one resolves which lines to draw from the
11
+ * visible band, the other moves that band. Two copies of the walk would let the
12
+ * widget scroll a container the window never reads.
13
+ *
14
+ * Walks up rather than taking a ref: the surface is mounted under different
15
+ * scrollers (the desktop editor panel, the phone's full-screen editor) and is
16
+ * also mounted inside a page that scrolls itself, which is the `null` case.
17
+ */
18
+ export function findScrollContainer(element: HTMLElement): HTMLElement | null {
19
+ for (let node = element.parentElement; node; node = node.parentElement) {
20
+ const overflowY = getComputedStyle(node).overflowY;
21
+ if ((overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') && node.scrollHeight > node.clientHeight) {
22
+ return node;
23
+ }
24
+ }
25
+ return null;
26
+ }