ompchamber 2.0.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/README.md +7 -6
  2. package/bunfig.toml +11 -0
  3. package/dist/client/index.html +16 -5
  4. package/dist/client/index.js +1393 -0
  5. package/dist/client/static/js/index-prbmx3yj.js +5123 -0
  6. package/dist/client/static/js/index-ranztes9.css +1 -0
  7. package/index.html +54 -0
  8. package/package.json +9 -10
  9. package/public/apple-touch-icon.png +0 -0
  10. package/public/icon-192x192.png +0 -0
  11. package/public/icon.svg +60 -0
  12. package/public/manifest.webmanifest +38 -0
  13. package/src/cli/lib/commands/serve.js +9 -5
  14. package/src/cli/lib/runtime.js +18 -7
  15. package/src/client/components/common/provider-icon/glyphs.ts +118 -0
  16. package/src/client/components/common/provider-icon/index.tsx +85 -0
  17. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
  18. package/src/client/components/layout/desktop-layout/index.tsx +2 -0
  19. package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
  20. package/src/client/components/mobile/SessionSidebar.tsx +10 -0
  21. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +0 -1
  22. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -1
  23. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +3 -0
  24. package/src/client/components/settings/categories/ProviderSettings.tsx +45 -4
  25. package/src/client/components/settings/categories/UsageSettings.tsx +22 -7
  26. package/src/client/components/settings/categories/provider-settings/AddModelModal.tsx +322 -0
  27. package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +152 -56
  28. package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +52 -1
  29. package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +133 -0
  30. package/src/client/components/settings/categories/provider-settings/DialectFields.tsx +136 -0
  31. package/src/client/components/settings/categories/provider-settings/EffortLadder.tsx +89 -0
  32. package/src/client/components/settings/categories/provider-settings/Header.tsx +2 -2
  33. package/src/client/components/settings/categories/provider-settings/LoginModal.tsx +2 -2
  34. package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +22 -2
  35. package/src/client/components/settings/categories/provider-settings/PresetPicker.tsx +82 -0
  36. package/src/client/components/settings/categories/provider-settings/PriceFields.tsx +107 -0
  37. package/src/client/components/settings/categories/provider-settings/ReconnectModal.tsx +43 -22
  38. package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +2 -2
  39. package/src/client/components/settings/categories/usage-settings/LimitsSection.tsx +115 -0
  40. package/src/client/components/settings/categories/usage-settings/ProviderCard.tsx +19 -26
  41. package/src/client/components/settings/categories/usage-settings/ProviderDetail.tsx +29 -22
  42. package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +60 -48
  43. package/src/client/components/settings/categories/usage-settings/format.ts +36 -0
  44. package/src/client/components/settings/categories/usage-settings/providers.ts +52 -28
  45. package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +1 -1
  46. package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +1 -1
  47. package/src/client/components/workspace/diff-panel/Toolbar.tsx +71 -38
  48. package/src/client/components/workspace/diff-panel/index.tsx +139 -53
  49. package/src/client/components/workspace/editor/Tabs.tsx +2 -2
  50. package/src/client/components/workspace/editor/Toolbar.tsx +111 -26
  51. package/src/client/components/workspace/editor/index.tsx +27 -9
  52. package/src/client/components/workspace/model-dropdown/Item.tsx +13 -23
  53. package/src/client/components/workspace/model-dropdown/Panel.tsx +1 -1
  54. package/src/client/components/workspace/model-dropdown/Section.tsx +13 -4
  55. package/src/client/components/workspace/model-dropdown/index.tsx +7 -3
  56. package/src/client/components/workspace/usage-panel/ProviderSelect.tsx +10 -5
  57. package/src/client/components/workspace/usage-panel/index.tsx +36 -18
  58. package/src/client/data/samples/dialogue-session.ts +1 -1
  59. package/src/client/data/samples/sample-tools/other.ts +1 -1
  60. package/src/client/data/settings/provider.ts +141 -9
  61. package/src/client/hooks/editor/use-file-editor.ts +24 -27
  62. package/src/client/hooks/models/use-provider-names.ts +1 -1
  63. package/src/client/hooks/settings/providers/actions.ts +337 -0
  64. package/src/client/hooks/settings/providers/api.ts +69 -0
  65. package/src/client/hooks/settings/providers/index.ts +138 -0
  66. package/src/client/hooks/settings/providers/messages.ts +77 -0
  67. package/src/client/hooks/settings/useUsageReport.ts +8 -3
  68. package/src/client/hooks/ui/theme.ts +3 -3
  69. package/src/client/hooks/workspace/file-tabs.ts +62 -11
  70. package/src/client/stubs/elkjs.ts +6 -1
  71. package/src/server/index.ts +20 -2
  72. package/src/server/lib/assets/font-css.server.ts +86 -0
  73. package/src/server/lib/assets/fonts.server.ts +117 -0
  74. package/src/server/lib/bundler/css.ts +116 -0
  75. package/src/server/lib/lifecycle/shell-route.ts +17 -0
  76. package/src/server/lib/models/provider-listing.server.ts +257 -0
  77. package/src/server/lib/models/provider-registry.server.test.ts +122 -0
  78. package/src/server/lib/models/provider-registry.server.ts +76 -25
  79. package/src/server/lib/omp/config/models-config.ts +11 -0
  80. package/src/server/lib/omp/config/models-validation.test.ts +137 -0
  81. package/src/server/lib/omp/config/models-validation.ts +111 -2
  82. package/src/server/lib/omp/config/provider-seeds.test.ts +167 -0
  83. package/src/server/lib/omp/config/provider-seeds.ts +176 -0
  84. package/src/server/lib/omp/config/providers.ts +94 -100
  85. package/src/server/lib/omp/core/auth-credentials.ts +45 -0
  86. package/src/server/lib/omp/session/{usage.ts → usage/aggregate.ts} +1 -118
  87. package/src/server/lib/omp/session/usage/shaping.ts +130 -0
  88. package/src/server/lib/usage/deepseek.ts +4 -5
  89. package/src/server/lib/usage/kenari.ts +7 -6
  90. package/src/server/lib/usage/omp-usage.server.test.ts +126 -0
  91. package/src/server/lib/usage/omp-usage.server.ts +194 -0
  92. package/src/server/lib/usage/provider-key.ts +81 -3
  93. package/src/server/lib/usage/providers.server.test.ts +47 -0
  94. package/src/server/lib/usage/providers.server.ts +208 -0
  95. package/src/server/lib/usage/vendor-balance.server.test.ts +104 -0
  96. package/src/server/lib/usage/vendor-balance.server.ts +284 -0
  97. package/src/server/plugins/shell.server.ts +56 -0
  98. package/src/server/plugins/ssr.ts +24 -36
  99. package/src/server/plugins/static.test.ts +26 -29
  100. package/src/server/plugins/static.ts +40 -19
  101. package/src/server/routes/settings/index.ts +2 -0
  102. package/src/server/routes/settings/provider-model.ts +107 -0
  103. package/src/server/routes/settings/provider-models.ts +65 -202
  104. package/src/server/routes/settings/providers.ts +22 -7
  105. package/src/server/routes/settings/usage.ts +125 -45
  106. package/src/server/routes/telemetry/tokens.ts +2 -1
  107. package/src/shared/lib/fs/diff-parser.test.ts +116 -0
  108. package/src/shared/lib/fs/diff-parser.ts +86 -33
  109. package/src/shared/lib/models/{provider-connection.ts → provider/connection.ts} +37 -0
  110. package/src/shared/lib/models/provider/dialect.ts +179 -0
  111. package/src/shared/lib/models/provider/glyph.test.ts +116 -0
  112. package/src/shared/lib/models/provider/glyph.ts +200 -0
  113. package/src/shared/lib/models/{provider-models.ts → provider/models.ts} +91 -6
  114. package/src/shared/lib/models/provider/presets.test.ts +172 -0
  115. package/src/shared/lib/models/provider/presets.ts +83 -0
  116. package/src/shared/lib/workspace/file-tab-id.ts +29 -0
  117. package/src/shared/types/settings/provider.ts +81 -0
  118. package/src/shared/types/settings/usage.ts +88 -14
  119. package/tsconfig.json +3 -1
  120. package/dist/client/static/css/index.5f630421b7.css +0 -2
  121. package/dist/client/static/font/KaTeX_AMS-Regular.a79f1c3119.woff2 +0 -0
  122. package/dist/client/static/font/KaTeX_Caligraphic-Bold.ec17d13264.woff2 +0 -0
  123. package/dist/client/static/font/KaTeX_Caligraphic-Regular.55fac25845.woff2 +0 -0
  124. package/dist/client/static/font/KaTeX_Fraktur-Bold.d42a5579b0.woff2 +0 -0
  125. package/dist/client/static/font/KaTeX_Fraktur-Regular.d3c882a649.woff2 +0 -0
  126. package/dist/client/static/font/KaTeX_Main-Bold.c3fb5ac22f.woff2 +0 -0
  127. package/dist/client/static/font/KaTeX_Main-BoldItalic.6f2bb1dff2.woff2 +0 -0
  128. package/dist/client/static/font/KaTeX_Main-Italic.8916142bec.woff2 +0 -0
  129. package/dist/client/static/font/KaTeX_Main-Regular.0462f03bdf.woff2 +0 -0
  130. package/dist/client/static/font/KaTeX_Math-BoldItalic.572d331f69.woff2 +0 -0
  131. package/dist/client/static/font/KaTeX_Math-Italic.f28c23acad.woff2 +0 -0
  132. package/dist/client/static/font/KaTeX_SansSerif-Bold.8c5b5494b6.woff2 +0 -0
  133. package/dist/client/static/font/KaTeX_SansSerif-Italic.3b1e59b3ba.woff2 +0 -0
  134. package/dist/client/static/font/KaTeX_SansSerif-Regular.ba21ed5f84.woff2 +0 -0
  135. package/dist/client/static/font/KaTeX_Script-Regular.03e9641d6f.woff2 +0 -0
  136. package/dist/client/static/font/KaTeX_Size1-Regular.eae34984b3.woff2 +0 -0
  137. package/dist/client/static/font/KaTeX_Size2-Regular.5916a24fa3.woff2 +0 -0
  138. package/dist/client/static/font/KaTeX_Size4-Regular.10d95fd3a2.woff2 +0 -0
  139. package/dist/client/static/font/KaTeX_Typewriter-Regular.a8709e3622.woff2 +0 -0
  140. package/dist/client/static/font/fira-code-latin-400-normal.2655b10c55.woff2 +0 -0
  141. package/dist/client/static/font/fira-code-latin-500-normal.92a991f204.woff2 +0 -0
  142. package/dist/client/static/font/fira-code-latin-600-normal.43788eb7f1.woff2 +0 -0
  143. package/dist/client/static/font/fira-code-latin-700-normal.2667215d33.woff2 +0 -0
  144. package/dist/client/static/js/77.0d81c74cf0.js +0 -139
  145. package/dist/client/static/js/77.0d81c74cf0.js.LICENSE.txt +0 -8
  146. package/dist/client/static/js/async/0.8e59f2eb5b.js +0 -1
  147. package/dist/client/static/js/async/0k.5852e6b4fa.js +0 -1
  148. package/dist/client/static/js/async/0w.9ed36d5860.js +0 -1
  149. package/dist/client/static/js/async/1.f387c12b63.js +0 -1
  150. package/dist/client/static/js/async/12.d091f91c99.js +0 -1
  151. package/dist/client/static/js/async/13.8f810a8a6c.js +0 -1
  152. package/dist/client/static/js/async/19.6fe909d55a.js +0 -160
  153. package/dist/client/static/js/async/1b.23f1141a38.js +0 -1
  154. package/dist/client/static/js/async/1k.973e8e21fc.js +0 -2
  155. package/dist/client/static/js/async/1k.973e8e21fc.js.LICENSE.txt +0 -11
  156. package/dist/client/static/js/async/1p.0d5cddda96.js +0 -1
  157. package/dist/client/static/js/async/2.b589f1733b.js +0 -1
  158. package/dist/client/static/js/async/2v.96165101de.js +0 -1
  159. package/dist/client/static/js/async/3.0cacc08423.js +0 -1
  160. package/dist/client/static/js/async/37.b28b2f61cd.js +0 -1
  161. package/dist/client/static/js/async/3d.5085fb913f.js +0 -1
  162. package/dist/client/static/js/async/3j.357c80ff38.js +0 -2
  163. package/dist/client/static/js/async/3j.357c80ff38.js.LICENSE.txt +0 -11
  164. package/dist/client/static/js/async/3p.bcf32a0983.js +0 -1
  165. package/dist/client/static/js/async/3r.3a69e98ee0.js +0 -64
  166. package/dist/client/static/js/async/3u.1228460e2a.js +0 -1
  167. package/dist/client/static/js/async/3y.3530b824ad.js +0 -198
  168. package/dist/client/static/js/async/3y.3530b824ad.js.LICENSE.txt +0 -5
  169. package/dist/client/static/js/async/4.9442eeb472.js +0 -1
  170. package/dist/client/static/js/async/4e.678285ca55.js +0 -1
  171. package/dist/client/static/js/async/4k.33a0c7eeb4.js +0 -1
  172. package/dist/client/static/js/async/4m.928a1dde85.js +0 -31
  173. package/dist/client/static/js/async/4m.928a1dde85.js.LICENSE.txt +0 -11
  174. package/dist/client/static/js/async/4q.aad20dab25.js +0 -1
  175. package/dist/client/static/js/async/5.ebaf8b89cc.js +0 -36
  176. package/dist/client/static/js/async/51.95bcae98af.js +0 -2
  177. package/dist/client/static/js/async/51.95bcae98af.js.LICENSE.txt +0 -6
  178. package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
  179. package/dist/client/static/js/async/5j.d6407c4248.js +0 -287
  180. package/dist/client/static/js/async/5o.88316d4460.js +0 -62
  181. package/dist/client/static/js/async/6.f034c9b528.js +0 -1
  182. package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
  183. package/dist/client/static/js/async/6o.5b1a06a382.js +0 -1
  184. package/dist/client/static/js/async/7.e4a9288dd0.js +0 -1
  185. package/dist/client/static/js/async/73.308dff4da8.js +0 -1
  186. package/dist/client/static/js/async/7e.a1cc9bfd88.js +0 -1
  187. package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
  188. package/dist/client/static/js/async/7l.fadbbc0583.js +0 -1
  189. package/dist/client/static/js/async/8.5067439d8c.js +0 -1
  190. package/dist/client/static/js/async/8j.ea110663fb.js +0 -1
  191. package/dist/client/static/js/async/a.bce4542392.js +0 -1
  192. package/dist/client/static/js/async/aa.65d3852576.js +0 -1
  193. package/dist/client/static/js/async/ar.9303e5f2e8.js +0 -2
  194. package/dist/client/static/js/async/ar.9303e5f2e8.js.LICENSE.txt +0 -11
  195. package/dist/client/static/js/async/az.d656062b0f.js +0 -1
  196. package/dist/client/static/js/async/b.b4f6f06515.js +0 -1
  197. package/dist/client/static/js/async/bi.09688ba75c.js +0 -1
  198. package/dist/client/static/js/async/bq.586bc738d1.js +0 -1
  199. package/dist/client/static/js/async/c.797d933882.js +0 -1
  200. package/dist/client/static/js/async/c0.0af1e640d3.js +0 -24
  201. package/dist/client/static/js/async/ca.8a52cfd447.js +0 -1
  202. package/dist/client/static/js/async/cf.d34fff5d67.js +0 -1
  203. package/dist/client/static/js/async/ct.b74928f7a3.js +0 -1
  204. package/dist/client/static/js/async/cx.3cb0242a3a.js +0 -226
  205. package/dist/client/static/js/async/d.a5fc8fe1b3.js +0 -1
  206. package/dist/client/static/js/async/da.f62c70f6c8.js +0 -1
  207. package/dist/client/static/js/async/dp.1725d6432c.js +0 -1
  208. package/dist/client/static/js/async/e.93214d622b.js +0 -79
  209. package/dist/client/static/js/async/f.ab50467719.js +0 -19
  210. package/dist/client/static/js/async/f.ab50467719.js.LICENSE.txt +0 -12
  211. package/dist/client/static/js/async/f7.46971e9d6d.js +0 -2
  212. package/dist/client/static/js/async/f7.46971e9d6d.js.LICENSE.txt +0 -6
  213. package/dist/client/static/js/async/f8.bbf0dbae70.js +0 -1
  214. package/dist/client/static/js/async/g.cb81980498.js +0 -2
  215. package/dist/client/static/js/async/g.cb81980498.js.LICENSE.txt +0 -11
  216. package/dist/client/static/js/async/h.4e790471e4.js +0 -27
  217. package/dist/client/static/js/async/h1.393f066d09.js +0 -1
  218. package/dist/client/static/js/async/h3.216e9798ab.js +0 -148
  219. package/dist/client/static/js/async/hd.0da931b622.js +0 -1
  220. package/dist/client/static/js/async/hk.ba7fb04d41.js +0 -1
  221. package/dist/client/static/js/async/hn.b645041e00.js +0 -1
  222. package/dist/client/static/js/async/ho.475a5e1ae4.js +0 -1
  223. package/dist/client/static/js/async/hp.e9fe2e395a.js +0 -2
  224. package/dist/client/static/js/async/hp.e9fe2e395a.js.LICENSE.txt +0 -17
  225. package/dist/client/static/js/async/hs.e7c59d8090.js +0 -41
  226. package/dist/client/static/js/async/i.7c148e8440.js +0 -1
  227. package/dist/client/static/js/async/id.573df20f29.js +0 -1
  228. package/dist/client/static/js/async/j.ab925882d8.js +0 -1
  229. package/dist/client/static/js/async/jd.63886a1f09.js +0 -1
  230. package/dist/client/static/js/async/jg.d86ebd4013.js +0 -86
  231. package/dist/client/static/js/async/jl.874d352278.js +0 -1
  232. package/dist/client/static/js/async/jll.11313b455a.js +0 -1
  233. package/dist/client/static/js/async/js.67182759ef.js +0 -1
  234. package/dist/client/static/js/async/jsc.23b70d6f40.js +0 -1
  235. package/dist/client/static/js/async/jse.334588ff5d.js +0 -1
  236. package/dist/client/static/js/async/jz.72eb929106.js +0 -77
  237. package/dist/client/static/js/async/k.3210286401.js +0 -217
  238. package/dist/client/static/js/async/ka.c1990ecaa3.js +0 -115
  239. package/dist/client/static/js/async/katex-vendor.de6a87c308.js +0 -1
  240. package/dist/client/static/js/async/ku.29c8a59d0c.js +0 -1
  241. package/dist/client/static/js/async/ky.8dab1dade0.js +0 -1
  242. package/dist/client/static/js/async/l.12dd34e1be.js +0 -1
  243. package/dist/client/static/js/async/l9.949710adaf.js +0 -83
  244. package/dist/client/static/js/async/l9.949710adaf.js.LICENSE.txt +0 -13
  245. package/dist/client/static/js/async/ls.9d9a01026c.js +0 -104
  246. package/dist/client/static/js/async/m.eb2e6752e2.js +0 -28
  247. package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +0 -11
  248. package/dist/client/static/js/async/m3.d2b05f9604.js +0 -2
  249. package/dist/client/static/js/async/m3.d2b05f9604.js.LICENSE.txt +0 -6
  250. package/dist/client/static/js/async/ma.2c261b04dd.js +0 -2
  251. package/dist/client/static/js/async/ma.2c261b04dd.js.LICENSE.txt +0 -6
  252. package/dist/client/static/js/async/me.8ace414f80.js +0 -23
  253. package/dist/client/static/js/async/ms.98960af4eb.js +0 -67
  254. package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +0 -12
  255. package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
  256. package/dist/client/static/js/async/n.a9f1c51701.js +0 -1
  257. package/dist/client/static/js/async/na.a36cd532d6.js +0 -1
  258. package/dist/client/static/js/async/nk.fc7e58325e.js +0 -1
  259. package/dist/client/static/js/async/o.36353dfb88.js +0 -1
  260. package/dist/client/static/js/async/oc.b73042e8bd.js +0 -8
  261. package/dist/client/static/js/async/og.8eb577893a.js +0 -193
  262. package/dist/client/static/js/async/p.48ecfcd14e.js +0 -1
  263. package/dist/client/static/js/async/px.fc6877924b.js +0 -1
  264. package/dist/client/static/js/async/q.22588845a9.js +0 -21
  265. package/dist/client/static/js/async/q.22588845a9.js.LICENSE.txt +0 -6
  266. package/dist/client/static/js/async/q5.97dc1481b6.js +0 -286
  267. package/dist/client/static/js/async/qs.2a9ddb3c72.js +0 -1
  268. package/dist/client/static/js/async/r.51010ad87c.js +0 -1
  269. package/dist/client/static/js/async/rd.cb6bf495bb.js +0 -2
  270. package/dist/client/static/js/async/rd.cb6bf495bb.js.LICENSE.txt +0 -4
  271. package/dist/client/static/js/async/s.0f395cdda5.js +0 -94
  272. package/dist/client/static/js/async/t.4dca0e2ba8.js +0 -1
  273. package/dist/client/static/js/async/t9.d44f57d518.js +0 -1
  274. package/dist/client/static/js/async/ta.973534bbec.js +0 -1
  275. package/dist/client/static/js/async/tr.e878658140.js +0 -309
  276. package/dist/client/static/js/async/u.0871ac5b22.js +0 -1
  277. package/dist/client/static/js/async/ud.3065bfee31.js +0 -2
  278. package/dist/client/static/js/async/ud.3065bfee31.js.LICENSE.txt +0 -4
  279. package/dist/client/static/js/async/up.ea990da0ff.js +0 -32
  280. package/dist/client/static/js/async/uw.8890abbabc.js +0 -1
  281. package/dist/client/static/js/async/v.7d28d3c241.js +0 -34
  282. package/dist/client/static/js/async/vp.9b13bf8610.js +0 -1
  283. package/dist/client/static/js/async/vt.42416b707d.js +0 -1
  284. package/dist/client/static/js/async/w.08ac210f60.js +0 -1
  285. package/dist/client/static/js/async/wt.ad92393a90.js +0 -1
  286. package/dist/client/static/js/async/x.563f72ca6b.js +0 -1
  287. package/dist/client/static/js/async/x0.475516418f.js +0 -1
  288. package/dist/client/static/js/async/x1.da8687159b.js +0 -90
  289. package/dist/client/static/js/async/xe.da8dcc2015.js +0 -2
  290. package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +0 -11
  291. package/dist/client/static/js/async/xl.a3c562a58c.js +0 -1
  292. package/dist/client/static/js/async/xn.6510c0abe2.js +0 -2
  293. package/dist/client/static/js/async/xn.6510c0abe2.js.LICENSE.txt +0 -6
  294. package/dist/client/static/js/async/xn1.83a7784fe1.js +0 -10
  295. package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +0 -11
  296. package/dist/client/static/js/async/xnl.feb0021e50.js +0 -2
  297. package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +0 -12
  298. package/dist/client/static/js/async/y.1a9ed7a5d8.js +0 -1
  299. package/dist/client/static/js/async/y6.43c57defe4.js +0 -1
  300. package/dist/client/static/js/async/yb.8c1342401d.js +0 -1
  301. package/dist/client/static/js/async/ybq.4491f2f2dc.js +0 -1
  302. package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
  303. package/dist/client/static/js/async/yq.917ddd7ba2.js +0 -1
  304. package/dist/client/static/js/async/ys.00687cf82d.js +0 -38
  305. package/dist/client/static/js/async/yv.d99d829136.js +0 -1
  306. package/dist/client/static/js/async/z.56f61a4374.js +0 -1
  307. package/dist/client/static/js/async/zi.5fdb07b86a.js +0 -24
  308. package/dist/client/static/js/async/zj.654f37e8ff.js +0 -1
  309. package/dist/client/static/js/async/zn.966d6f1cc6.js +0 -1
  310. package/dist/client/static/js/async/zo.26213b40c0.js +0 -1
  311. package/dist/client/static/js/async/zx.e9f4105e00.js +0 -1
  312. package/dist/client/static/js/index.e6a257402a.js +0 -38
  313. package/dist/client/static/js/index.e6a257402a.js.LICENSE.txt +0 -4
  314. package/dist/client/static/js/markdown-vendor.71e43278b3.js +0 -144
  315. package/dist/client/static/js/shiki-vendor.cb8452ddcd.js +0 -1
  316. package/src/client/components/settings/categories/provider-settings/Icons.tsx +0 -35
  317. package/src/client/hooks/settings/providers.ts +0 -326
  318. package/src/server/plugins/dev-assets.ts +0 -49
  319. package/src/shared/lib/code/editor-utils.ts +0 -16
  320. package/src/shared/lib/models/provider-presets.ts +0 -29
  321. /package/dist/client/{apple-touch-icon.png → static/apple-touch-icon-dskx5sr3.png} +0 -0
  322. /package/dist/client/{icon-192x192.png → static/icon-192x192-6gn5cr2f.png} +0 -0
  323. /package/dist/client/{icon.svg → static/icon-d07z9dtz.svg} +0 -0
  324. /package/dist/client/{manifest.webmanifest → static/manifest-bnyy75kt.webmanifest} +0 -0
  325. /package/{dist/client → public}/icon-512x512.png +0 -0
  326. /package/{dist/client → public}/icon-maskable-512x512.png +0 -0
  327. /package/{dist/client → public}/sw.js +0 -0
  328. /package/src/shared/lib/models/{provider-cleanup.ts → provider/cleanup.ts} +0 -0
  329. /package/src/shared/lib/models/{provider-label.ts → provider/label.ts} +0 -0
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The settings API calls behind Settings → Providers.
8
+ *
9
+ * Split from the hook so the hook holds state transitions only — each of these
10
+ * is a request whose failure has to be reported to the user, and keeping them
11
+ * here means the "what does the server answer" contract has one home.
12
+ */
13
+
14
+ import type { PresetProviderOption, ProviderItem, ProviderModel } from '@/shared/types';
15
+
16
+ export interface ProviderSettingsPayload {
17
+ providers: ProviderItem[];
18
+ presetProviders?: PresetProviderOption[];
19
+ }
20
+
21
+ /** Load the merged provider registry (omp's sources + the chamber overlay). */
22
+ export async function loadProvidersFromApi(): Promise<ProviderSettingsPayload | null> {
23
+ try {
24
+ const response = await fetch('/api/settings/providers');
25
+ const data = await response.json() as Partial<ProviderSettingsPayload>;
26
+ if (!Array.isArray(data?.providers)) return null;
27
+ return { providers: data.providers, ...(data.presetProviders ? { presetProviders: data.presetProviders } : {}) };
28
+ } catch (error) {
29
+ console.error('Failed to load providers from API:', error);
30
+ return null;
31
+ }
32
+ }
33
+
34
+ export interface ModelOverrideResult {
35
+ ok: boolean;
36
+ written?: boolean;
37
+ reason?: string;
38
+ error?: string;
39
+ }
40
+
41
+ /**
42
+ * Write the per-model knobs omp honours (`maxTokens`, reasoning effort) into
43
+ * models.yml `modelOverrides`. Writing only the chamber overlay left the
44
+ * controls inert — omp never read them.
45
+ */
46
+ export async function saveModelOverrideRemote(
47
+ providerSlug: string,
48
+ modelId: string,
49
+ updates: Partial<ProviderModel>,
50
+ ): Promise<ModelOverrideResult> {
51
+ const body: Record<string, unknown> = { provider: providerSlug, modelId };
52
+ if ('maxTokens' in updates) body.maxTokens = updates.maxTokens ?? null;
53
+ if ('reasoningEffort' in updates) body.reasoningEffort = updates.reasoningEffort ?? null;
54
+ // Nothing omp would read — the overlay update alone is the whole change.
55
+ if (Object.keys(body).length === 2) return { ok: true, written: false };
56
+ try {
57
+ const response = await fetch('/api/settings/model-override', {
58
+ method: 'PUT',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify(body),
61
+ });
62
+ const data = await response.json() as { success?: boolean; written?: boolean; reason?: string; error?: string };
63
+ if (!response.ok) return { ok: false, error: data.error || 'Failed to save the model configuration.' };
64
+ return { ok: true, written: data.written, reason: data.reason };
65
+ } catch (error) {
66
+ console.error('Failed to save model override:', error);
67
+ return { ok: false, error: 'Failed to save the model configuration.' };
68
+ }
69
+ }
@@ -0,0 +1,138 @@
1
+ import { useEffect, useMemo, useState } from 'preact/hooks';
2
+ import type { PresetProviderOption, ProviderItem, ProviderModel } from '@/shared/types';
3
+ import { PRESET_NEW_PROVIDERS } from '@/client/data/settings/provider';
4
+ import { buildAvailableProviderPresets } from '@/shared/lib/models/provider/presets';
5
+ import { useToasts } from '@/client/hooks/ui/toasts';
6
+ import { loadProvidersFromApi } from '@/client/hooks/settings/providers/api';
7
+ import { createProviderActions } from '@/client/hooks/settings/providers/actions';
8
+
9
+ interface UseProviderSettingsOptions {
10
+ autoOpenAdd?: boolean;
11
+ onAddModalClose?: () => void;
12
+ }
13
+
14
+ /**
15
+ * State wiring behind Settings → Providers.
16
+ *
17
+ * The mutations themselves live in `providers/actions.ts`, which receives this
18
+ * hook's setters as one `deps` record — the split keeps both files under the
19
+ * repo's size ceiling without changing closure semantics, since every action
20
+ * still closes over the same state.
21
+ *
22
+ * Every mutation lands in one of two stores: the chamber-local overlay (per-model
23
+ * visibility and sampling config) or omp's own config.yml `disabledProviders`
24
+ * (connect / disconnect). Disconnect has to use the latter — the overlay is
25
+ * re-merged away on load, which is why a disconnected provider used to come back
26
+ * as connected.
27
+ */
28
+ export function useProviderSettings({
29
+ autoOpenAdd = false,
30
+ onAddModalClose,
31
+ }: UseProviderSettingsOptions = {}) {
32
+ const [providers, setProviders] = useState<ProviderItem[]>([]);
33
+ const [presetProviders, setPresetProviders] = useState<PresetProviderOption[]>(PRESET_NEW_PROVIDERS);
34
+ const [selectedProviderId, setSelectedProviderId] = useState<string>('provider-deepseek');
35
+ const [currentProject, setCurrentProject] = useState('ompchamber');
36
+ const [isFetchingModels, setIsFetchingModels] = useState(false);
37
+ const { toasts, pushToast, dismissToast } = useToasts();
38
+
39
+ const [isAddModalOpen, setIsAddModalOpen] = useState(autoOpenAdd);
40
+ const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false);
41
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
42
+ const [isDeletingProvider, setIsDeletingProvider] = useState(false);
43
+ const [isReconnectModalOpen, setIsReconnectModalOpen] = useState(false);
44
+ const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
45
+ const [configModel, setConfigModel] = useState<ProviderModel | null>(null);
46
+ const [capabilitiesModel, setCapabilitiesModel] = useState<ProviderModel | null>(null);
47
+
48
+ useEffect(() => {
49
+ if (autoOpenAdd) setIsAddModalOpen(true);
50
+ }, [autoOpenAdd]);
51
+
52
+ useEffect(() => {
53
+ let active = true;
54
+ void loadProvidersFromApi().then((data) => {
55
+ if (!active || !data) return;
56
+ setProviders(data.providers);
57
+ const connected = data.providers.filter((provider) => provider.status === 'connected');
58
+ setSelectedProviderId(connected.length > 0 ? connected[0].id : '');
59
+ if (data.presetProviders) setPresetProviders(data.presetProviders);
60
+ });
61
+ return () => { active = false; };
62
+ }, []);
63
+
64
+ // The sidebar lists connected providers plus every provider the user disabled:
65
+ // a disabled provider reports `disconnected`, so filtering on status alone
66
+ // would make the switch that hid it unreachable.
67
+ const connectedProviders = useMemo(
68
+ () => providers.filter(
69
+ (provider) => provider.status === 'connected' || provider.disabled === true,
70
+ ),
71
+ [providers],
72
+ );
73
+ const availablePresetProviders = useMemo(
74
+ () => buildAvailableProviderPresets(providers, presetProviders),
75
+ [providers, presetProviders],
76
+ );
77
+ const selectedProvider = connectedProviders.find((p) => p.id === selectedProviderId) || connectedProviders[0];
78
+
79
+ const actions = createProviderActions({
80
+ providers,
81
+ selectedProvider,
82
+ connectedProviders,
83
+ isFetchingModels,
84
+ isDeletingProvider,
85
+ pushToast,
86
+ setProviders,
87
+ setSelectedProviderId,
88
+ setIsAddModalOpen,
89
+ setIsDeleteModalOpen,
90
+ setIsDeletingProvider,
91
+ setIsFetchingModels,
92
+ ...(onAddModalClose ? { onAddModalClose } : {}),
93
+ });
94
+
95
+ /**
96
+ * After a hand-registered model lands in models.yml, the registry has to be
97
+ * re-read: the new row exists only in omp's file, so nothing in the chamber's
98
+ * own overlay knows about it yet. Re-fetching the MERGED list (rather than
99
+ * re-saving the overlay, whose response is the overlay alone) is what puts
100
+ * the model in the sidebar count and the chat picker.
101
+ */
102
+ const handleModelAdded = async (modelId: string) => {
103
+ pushToast(`${modelId} registered in models.yml.`, 'success');
104
+ const data = await loadProvidersFromApi();
105
+ if (data) setProviders(data.providers);
106
+ };
107
+
108
+ return {
109
+ providers,
110
+ connectedProviders,
111
+ availablePresetProviders,
112
+ selectedProvider,
113
+ currentProject,
114
+ setCurrentProject,
115
+ isFetchingModels,
116
+ toasts,
117
+ pushToast,
118
+ dismissToast,
119
+ isAddModelModalOpen,
120
+ setIsAddModelModalOpen,
121
+ handleModelAdded,
122
+ isDeleteModalOpen,
123
+ setIsDeleteModalOpen,
124
+ isDeletingProvider,
125
+ isAddModalOpen,
126
+ setIsAddModalOpen,
127
+ isReconnectModalOpen,
128
+ setIsReconnectModalOpen,
129
+ isLoginModalOpen,
130
+ setIsLoginModalOpen,
131
+ configModel,
132
+ setConfigModel,
133
+ capabilitiesModel,
134
+ setCapabilitiesModel,
135
+ setSelectedProviderId,
136
+ ...actions,
137
+ };
138
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * What to TELL the user after a provider was added or re-fetched.
8
+ *
9
+ * The message depends on the shape that was written, not on the model count,
10
+ * because two of the three shapes have no local model list BY DESIGN: a
11
+ * discovery provider is listed by omp at runtime, and an endpoint override
12
+ * leaves the models to omp's own catalog. Reporting "0 models" for either made
13
+ * a registration that succeeded read as a failure.
14
+ */
15
+
16
+ import type { ProviderItem } from '@/shared/types';
17
+
18
+ /** The success/failure line for an Add Provider submission. */
19
+ export function addProviderMessage(
20
+ provider: ProviderItem,
21
+ options: { fetchedCount: number; ompNote?: string },
22
+ ): { text: string; tone: 'success' | 'error' } {
23
+ if (provider.modelSource === 'discovery') {
24
+ return { text: `${provider.name} registered — omp discovers its models on each run.`, tone: 'success' };
25
+ }
26
+ if (provider.modelSource === 'override') {
27
+ return {
28
+ text: `${provider.name} endpoint registered — omp keeps its own model list for this provider.`,
29
+ tone: 'success',
30
+ };
31
+ }
32
+ if (options.fetchedCount > 0) {
33
+ return {
34
+ text: `Fetched ${options.fetchedCount} new model${options.fetchedCount === 1 ? '' : 's'} from the provider.`,
35
+ tone: 'success',
36
+ };
37
+ }
38
+ if (options.fetchedCount === -1) {
39
+ // The listing failed. When omp reported a reason it is the actionable half,
40
+ // so it is quoted rather than replaced by a generic line.
41
+ return {
42
+ text: options.ompNote
43
+ ? `Provider registered, but models could not be listed: ${options.ompNote}`
44
+ : 'Auto-fetch models failed — provider added with default models.',
45
+ tone: 'error',
46
+ };
47
+ }
48
+ return { text: `${provider.name} registered — no new models to add.`, tone: 'success' };
49
+ }
50
+
51
+ /**
52
+ * The extra line reported after a manual "fetch models": what the chamber wrote
53
+ * into `models.yml` versus what omp already had. A file write and a model-count
54
+ * change are different outcomes, and conflating them hid a failed write.
55
+ */
56
+ export function fetchModelsNote(result: {
57
+ omp?: { written: boolean; addedCount: number; backfilledCount: number; reason?: string };
58
+ }): { text: string; tone: 'success' | 'error' } | null {
59
+ if (!result.omp) return null;
60
+ if (result.omp.written) {
61
+ const parts: string[] = [];
62
+ if (result.omp.addedCount > 0) {
63
+ parts.push(`${result.omp.addedCount} new model${result.omp.addedCount === 1 ? '' : 's'} registered`);
64
+ }
65
+ if (result.omp.backfilledCount > 0) {
66
+ parts.push(`${result.omp.backfilledCount} model${result.omp.backfilledCount === 1 ? '' : 's'} enriched`);
67
+ }
68
+ return {
69
+ text: `omp models.yml updated${parts.length > 0 ? `: ${parts.join(', ')}` : ''}.`,
70
+ tone: 'success',
71
+ };
72
+ }
73
+ if (result.omp.reason) {
74
+ return { text: `omp models.yml not updated: ${result.omp.reason}`, tone: 'error' };
75
+ }
76
+ return null;
77
+ }
@@ -5,24 +5,29 @@ export interface UsageReportState {
5
5
  report: UsageReport | null;
6
6
  isLoading: boolean;
7
7
  error: string | null;
8
- reload: (opts?: { silent?: boolean }) => void;
8
+ reload: (opts?: { silent?: boolean; force?: boolean }) => void;
9
9
  }
10
10
 
11
11
  /**
12
12
  * Shared `GET /api/settings/usage` loader for the Settings → Usage panel and
13
13
  * the Usage right panel. Both surfaces mount independently, so each keeps its
14
14
  * own copy of the last successful report until reload completes.
15
+ *
16
+ * `force` asks the server to re-probe the providers instead of answering from
17
+ * its one-minute quota cache — used by the user's own Refresh button, never by
18
+ * the background poll.
15
19
  */
16
20
  export function useUsageReport(): UsageReportState {
17
21
  const [report, setReport] = useState<UsageReport | null>(null);
18
22
  const [isLoading, setIsLoading] = useState(true);
19
23
  const [error, setError] = useState<string | null>(null);
20
24
 
21
- const reload = useCallback(async (opts?: { silent?: boolean }) => {
25
+ const reload = useCallback(async (opts?: { silent?: boolean; force?: boolean }) => {
22
26
  if (!opts?.silent) setIsLoading(true);
23
27
  setError(null);
24
28
  try {
25
- const response = await fetch('/api/settings/usage');
29
+ const url = opts?.force ? '/api/settings/usage?refresh=1' : '/api/settings/usage';
30
+ const response = await fetch(url);
26
31
  if (!response.ok) throw new Error(`Usage request failed (${response.status})`);
27
32
  const data = (await response.json()) as UsageReport;
28
33
  setReport(data);
@@ -35,9 +35,9 @@ export function currentThemeId(): string {
35
35
  * Put the catalog on the document before the first render.
36
36
  *
37
37
  * The SSR shell already inlines the stylesheet and both attributes; this is the
38
- * guard for a document that predates them — a cached page, or `dist/client/
39
- * index.html` left over from an older build while only the server was restarted.
40
- * Without it the page keeps `:root`'s default palette and no variant attribute,
38
+ * guard for a document that predates them — a cached page, or markup rendered
39
+ * before the server restarted. Without it the page keeps `:root`'s default
40
+ * palette and no variant attribute,
41
41
  * which is a wrong palette, not merely a stale one.
42
42
  *
43
43
  * Called once from `main.tsx` rather than from a hook: the only components that
@@ -2,8 +2,18 @@ import { useCallback, useEffect, useRef } from 'preact/hooks';
2
2
  import { useSessionState } from '@/client/hooks/workspace/session-state';
3
3
  import { useSessionStateContext } from '@/client/hooks/workspace/session-state/context';
4
4
  import { useChamberEvent } from '@/client/hooks/ui/window-event';
5
+ import { diffTabId, diffTabName, fileTabId } from '@/shared/lib/workspace/file-tab-id';
5
6
  import type { OpenedFile } from '@/shared/types/fs';
6
7
 
8
+ export interface OpenFileInput {
9
+ id?: number | string;
10
+ name: string;
11
+ path: string;
12
+ content?: string;
13
+ root?: string;
14
+ repo?: string;
15
+ }
16
+
7
17
  export function useFileTabs(
8
18
  activeProjectPath: string | null,
9
19
  sessionId: string,
@@ -25,9 +35,12 @@ export function useFileTabs(
25
35
  setActiveFileId(null);
26
36
  }, [activeProjectPath, sessionId, layoutReady, setOpenedFiles, setActiveFileId]);
27
37
 
28
- const handleOpenFile = useCallback((file: any) => {
38
+ const handleOpenFile = useCallback((file: OpenFileInput) => {
29
39
  const fileEntry: OpenedFile = {
30
40
  ...file,
41
+ // One id scheme for a path, so a file opened from the explorer, a chat
42
+ // link and a converted diff tab all land on the same tab.
43
+ id: file.id ?? fileTabId(file.path),
31
44
  root: file.root ?? activeProjectPath ?? undefined,
32
45
  };
33
46
  setOpenedFiles(prev => {
@@ -52,13 +65,12 @@ export function useFileTabs(
52
65
  root?: string;
53
66
  }) => {
54
67
  const rawPath = diffInfo.file.replace(/^\/+/, '');
55
- const fileName = rawPath.split('/').pop() || rawPath;
56
68
  const isStaged = Boolean(diffInfo.staged);
57
- const diffId = `diff-${isStaged ? 'staged' : 'working'}-${rawPath}`;
69
+ const diffId = diffTabId(rawPath, isStaged);
58
70
 
59
71
  const diffEntry: OpenedFile = {
60
72
  id: diffId,
61
- name: `${fileName} (Diff)`,
73
+ name: diffTabName(rawPath),
62
74
  path: rawPath,
63
75
  isDiff: true,
64
76
  diffStatus: diffInfo.status || 'M',
@@ -91,6 +103,50 @@ export function useFileTabs(
91
103
  });
92
104
  }, [activeFileId, setOpenedFiles, setActiveFileId]);
93
105
 
106
+ /**
107
+ * Turn a diff tab into the plain editor tab for the same file.
108
+ *
109
+ * The panel used to do this by assigning `activeFile.isDiff = false` on the
110
+ * object it was handed. That object is the one inside `layout.openedFiles`,
111
+ * so the flag was mutated in place: React never saw a change (the array
112
+ * identity was identical), the tab kept rendering the diff until the next
113
+ * unrelated render, and — because the array is what gets persisted — the
114
+ * mutation reached the database. After a reload the "diff" tab came back as
115
+ * a file tab, and re-opening that file's diff found an entry that already
116
+ * matched, so the diff never came back at all.
117
+ *
118
+ * The target id is `fileTabId`, so if that file already has a tab the two
119
+ * entries would share an id and React would render duplicate keys. The diff
120
+ * entry is dropped in that case and the existing tab activated instead — the
121
+ * user asked to see the file, and it is already open.
122
+ */
123
+ const convertDiffToEditor = useCallback((diffId: number | string) => {
124
+ setOpenedFiles(prev => {
125
+ const index = prev.findIndex(f => f.id === diffId);
126
+ if (index === -1) return prev;
127
+ const diff = prev[index];
128
+ const fileId = fileTabId(diff.path);
129
+ const alreadyOpen = prev.find(f => f.id === fileId);
130
+
131
+ if (alreadyOpen) {
132
+ setActiveFileId(alreadyOpen.id);
133
+ return prev.filter(f => f.id !== diffId);
134
+ }
135
+
136
+ const next = [...prev];
137
+ next[index] = {
138
+ id: fileId,
139
+ name: diff.path.split('/').pop() || diff.name,
140
+ path: diff.path,
141
+ root: diff.root,
142
+ repo: diff.repo,
143
+ };
144
+ setActiveFileId(fileId);
145
+ return next;
146
+ });
147
+ onOpenTab?.();
148
+ }, [onOpenTab, setOpenedFiles, setActiveFileId]);
149
+
94
150
  // Global listeners for omp:open-file and omp:open-diff
95
151
  useChamberEvent('omp:open-file', (e) => {
96
152
  const customEvent = e as CustomEvent<{ path: string; name?: string; id?: number; content?: string; root?: string; repo?: string }>;
@@ -98,15 +154,9 @@ export function useFileTabs(
98
154
 
99
155
  const rawPath = customEvent.detail.path.replace(/^\/+/, '');
100
156
  const name = customEvent.detail.name || rawPath.split('/').pop() || 'file';
101
- let hash = 0;
102
- for (let i = 0; i < rawPath.length; i++) {
103
- hash = (hash << 5) - hash + rawPath.charCodeAt(i);
104
- hash |= 0;
105
- }
106
- const id = customEvent.detail.id || Math.abs(hash) || Date.now();
107
157
 
108
158
  handleOpenFile({
109
- id,
159
+ id: customEvent.detail.id || fileTabId(rawPath),
110
160
  name,
111
161
  path: rawPath,
112
162
  content: customEvent.detail.content,
@@ -136,5 +186,6 @@ export function useFileTabs(
136
186
  handleOpenFile,
137
187
  handleOpenDiff,
138
188
  handleCloseFile,
189
+ convertDiffToEditor,
139
190
  };
140
191
  }
@@ -4,7 +4,12 @@
4
4
  */
5
5
 
6
6
  /**
7
- * Browser replacement for `elkjs`, aliased in `rsbuild.config.ts`.
7
+ * Browser replacement for `elkjs`, aliased through `tsconfig.json` `paths`.
8
+ *
9
+ * The alias moved there when the client build moved to Bun: Bun's bundler does
10
+ * not read an `alias` option (it is absent from `Bun.build`'s config and from
11
+ * `bun-types`; passing one is silently ignored — verified), while `paths` it
12
+ * honours for both internal aliases and package specifiers.
8
13
  *
9
14
  * Mermaid 12 changed the global `layout` default from `dagre` to `elk`, so every
10
15
  * flowchart pulled in `elkjs/lib/elk.bundled.js` whether the diagram asked for
@@ -1,13 +1,16 @@
1
1
  import { Elysia } from 'elysia';
2
2
  import pkg from '@/../package.json';
3
+ import shell from '@/../index.html';
3
4
  import { apiRoutes } from '@/server/routes';
4
5
  import { ssrRoutes } from '@/server/plugins/ssr';
6
+ import { setShellSource } from '@/server/plugins/shell.server';
5
7
  import { getDatabasePath } from '@/server/db.server';
6
8
  import { fetchOmpRegistrySnapshot } from '@/server/lib/models/provider-registry.server';
7
9
  import { ompStartupError, ompStartupLogLines } from '@/server/lib/omp/core/startup';
8
10
  import { readInstanceRecord, removeInstanceRecord, writeInstanceRecord } from '@/server/lib/lifecycle/instance';
9
11
  import { resolveLaunchMode } from '@/server/lib/lifecycle/launch-mode';
10
12
  import { acquirePortLock, claimPort, PortInUseError } from '@/server/lib/lifecycle/port-guard';
13
+ import { SHELL_ROUTE } from '@/server/lib/lifecycle/shell-route';
11
14
  import { isMockMode } from '@/server/mock.server';
12
15
 
13
16
  // Refuse to run without a resolvable omp binary — before the listener opens and
@@ -39,7 +42,15 @@ if (!lock) {
39
42
  process.exit(1);
40
43
  }
41
44
 
42
- const app = new Elysia().use(apiRoutes).use(ssrRoutes);
45
+ // `serve.routes` is where the HTML shell lives. Elysia merges its own static
46
+ // routes into that same table (its Bun adapter passes `serve` straight to
47
+ // `Bun.serve`), and Bun's table wins for the paths it declares — so `/_shell`
48
+ // renders the bundle while every other path falls through to `fetch`. The
49
+ // WebSocket routes keep working: Elysia composes its `websocket` handler
50
+ // alongside this object.
51
+ const app = new Elysia({ serve: { routes: { [SHELL_ROUTE]: shell } } })
52
+ .use(apiRoutes)
53
+ .use(ssrRoutes);
43
54
 
44
55
  try {
45
56
  await claimPort({
@@ -50,7 +61,14 @@ try {
50
61
  // `reusePort: true`, which lets a second server bind a port already in
51
62
  // use and then sit invisible while the first-bound socket takes every
52
63
  // connection.
53
- await app.listen({ port, hostname: host, reusePort: false });
64
+ app.listen({ port, hostname: host, reusePort: false });
65
+ if (!app.server) throw new Error('Bun.serve returned no listener');
66
+ // The shell is rendered by asking this same listener for it: Bun renders
67
+ // an HTML route only while serving, and there is no in-process API for an
68
+ // `HTMLBundle` (`new Response(bundle)` is "[object HTMLBundle]",
69
+ // `app.handle` answers 404). Handing the listener over is what makes
70
+ // `renderShell()` possible.
71
+ setShellSource(app.server);
54
72
  },
55
73
  });
56
74
  } catch (error) {
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The font stylesheet, assembled at request time.
8
+ *
9
+ * `@font-face` blocks cannot live in the bundle: Bun's CSS loader resolves every
10
+ * local `url()`, and a font is always a local file — so a bundled face is either
11
+ * a base64 blob or an absolute filesystem path. `lib/bundler/css.ts` lifts each
12
+ * block out of its stylesheet and rewrites the urls to `<FONT_ROUTE_PREFIX><basename>`;
13
+ * this module serves the result.
14
+ *
15
+ * Two jobs, one route each:
16
+ *
17
+ * - `FONT_STYLESHEET_ROUTE` (`/fonts.css`) concatenates the extracted faces.
18
+ * Read per request rather than cached: it is a handful of kilobytes, and a
19
+ * cache is what would keep a stale face alive across an edit to a stylesheet.
20
+ * A missing directory is an empty stylesheet, not a 404 — the shell links this
21
+ * unconditionally.
22
+ *
23
+ * - `FONT_ROUTE_PREFIX` (`/fonts/<basename>`) serves the file itself from the
24
+ * installed packages. The basename is the whole key, so the lookup is an index
25
+ * rather than a path built from the request; see `fonts.server.ts`.
26
+ */
27
+
28
+ import { join } from 'path';
29
+ import { Glob } from 'bun';
30
+
31
+ import { packageRoot, resolveFontFile } from '@/server/lib/assets/fonts.server';
32
+
33
+ /** Directory holding one extracted face file per source stylesheet. */
34
+ export const FONT_FACE_DIR = join('.ompchamber-build', 'font-faces');
35
+
36
+ /** The route the assembled stylesheet is served on. */
37
+ export const FONT_STYLESHEET_ROUTE = '/fonts.css';
38
+
39
+ const FONT_MIME: Record<string, string> = {
40
+ '.woff2': 'font/woff2',
41
+ '.woff': 'font/woff',
42
+ '.ttf': 'font/ttf',
43
+ '.otf': 'font/otf',
44
+ };
45
+
46
+ const STYLESHEET_HEADERS = {
47
+ 'content-type': 'text/css; charset=utf-8',
48
+ 'cache-control': 'no-store',
49
+ } as const;
50
+
51
+ /** Every extracted face file, concatenated in a stable order. */
52
+ export async function fontStylesheet(): Promise<string> {
53
+ const dir = join(packageRoot(), FONT_FACE_DIR);
54
+ const glob = new Glob('*.css');
55
+ const files: string[] = [];
56
+ try {
57
+ for await (const file of glob.scan({ cwd: dir, onlyFiles: true })) files.push(file);
58
+ } catch {
59
+ return '';
60
+ }
61
+ // Sorted so the stylesheet is byte-stable between builds: the browser caches
62
+ // on the ETag, and an unstable order would invalidate it for no change.
63
+ files.sort();
64
+ const parts = await Promise.all(files.map((file) => Bun.file(join(dir, file)).text()));
65
+ return parts.join('\n');
66
+ }
67
+
68
+ /** The `/fonts.css` response. */
69
+ export async function serveFontStylesheet(): Promise<Response> {
70
+ return new Response(await fontStylesheet(), { headers: STYLESHEET_HEADERS });
71
+ }
72
+
73
+ /** The `/fonts/<basename>` response, or null when no package ships that file. */
74
+ export async function serveFontFile(name: string): Promise<Response | null> {
75
+ const file = await resolveFontFile(name);
76
+ if (!file) return null;
77
+ const ext = name.slice(name.lastIndexOf('.')).toLowerCase();
78
+ return new Response(Bun.file(file), {
79
+ headers: {
80
+ 'content-type': FONT_MIME[ext] ?? 'application/octet-stream',
81
+ // A font's bytes never change under a fixed basename inside a pinned
82
+ // dependency, so this is the one asset that can be cached hard.
83
+ 'cache-control': 'public, max-age=31536000, immutable',
84
+ },
85
+ });
86
+ }