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.
- package/README.md +7 -6
- package/bunfig.toml +11 -0
- package/dist/client/index.html +16 -5
- package/dist/client/index.js +1393 -0
- package/dist/client/static/js/index-prbmx3yj.js +5123 -0
- package/dist/client/static/js/index-ranztes9.css +1 -0
- package/index.html +54 -0
- package/package.json +9 -10
- package/public/apple-touch-icon.png +0 -0
- package/public/icon-192x192.png +0 -0
- package/public/icon.svg +60 -0
- package/public/manifest.webmanifest +38 -0
- package/src/cli/lib/commands/serve.js +9 -5
- package/src/cli/lib/runtime.js +18 -7
- package/src/client/components/common/provider-icon/glyphs.ts +118 -0
- package/src/client/components/common/provider-icon/index.tsx +85 -0
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
- package/src/client/components/layout/desktop-layout/index.tsx +2 -0
- package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
- package/src/client/components/mobile/SessionSidebar.tsx +10 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +0 -1
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -1
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +3 -0
- package/src/client/components/settings/categories/ProviderSettings.tsx +45 -4
- package/src/client/components/settings/categories/UsageSettings.tsx +22 -7
- package/src/client/components/settings/categories/provider-settings/AddModelModal.tsx +322 -0
- package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +152 -56
- package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +52 -1
- package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +133 -0
- package/src/client/components/settings/categories/provider-settings/DialectFields.tsx +136 -0
- package/src/client/components/settings/categories/provider-settings/EffortLadder.tsx +89 -0
- package/src/client/components/settings/categories/provider-settings/Header.tsx +2 -2
- package/src/client/components/settings/categories/provider-settings/LoginModal.tsx +2 -2
- package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +22 -2
- package/src/client/components/settings/categories/provider-settings/PresetPicker.tsx +82 -0
- package/src/client/components/settings/categories/provider-settings/PriceFields.tsx +107 -0
- package/src/client/components/settings/categories/provider-settings/ReconnectModal.tsx +43 -22
- package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +2 -2
- package/src/client/components/settings/categories/usage-settings/LimitsSection.tsx +115 -0
- package/src/client/components/settings/categories/usage-settings/ProviderCard.tsx +19 -26
- package/src/client/components/settings/categories/usage-settings/ProviderDetail.tsx +29 -22
- package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +60 -48
- package/src/client/components/settings/categories/usage-settings/format.ts +36 -0
- package/src/client/components/settings/categories/usage-settings/providers.ts +52 -28
- package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +1 -1
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +71 -38
- package/src/client/components/workspace/diff-panel/index.tsx +139 -53
- package/src/client/components/workspace/editor/Tabs.tsx +2 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +111 -26
- package/src/client/components/workspace/editor/index.tsx +27 -9
- package/src/client/components/workspace/model-dropdown/Item.tsx +13 -23
- package/src/client/components/workspace/model-dropdown/Panel.tsx +1 -1
- package/src/client/components/workspace/model-dropdown/Section.tsx +13 -4
- package/src/client/components/workspace/model-dropdown/index.tsx +7 -3
- package/src/client/components/workspace/usage-panel/ProviderSelect.tsx +10 -5
- package/src/client/components/workspace/usage-panel/index.tsx +36 -18
- package/src/client/data/samples/dialogue-session.ts +1 -1
- package/src/client/data/samples/sample-tools/other.ts +1 -1
- package/src/client/data/settings/provider.ts +141 -9
- package/src/client/hooks/editor/use-file-editor.ts +24 -27
- package/src/client/hooks/models/use-provider-names.ts +1 -1
- package/src/client/hooks/settings/providers/actions.ts +337 -0
- package/src/client/hooks/settings/providers/api.ts +69 -0
- package/src/client/hooks/settings/providers/index.ts +138 -0
- package/src/client/hooks/settings/providers/messages.ts +77 -0
- package/src/client/hooks/settings/useUsageReport.ts +8 -3
- package/src/client/hooks/ui/theme.ts +3 -3
- package/src/client/hooks/workspace/file-tabs.ts +62 -11
- package/src/client/stubs/elkjs.ts +6 -1
- package/src/server/index.ts +20 -2
- package/src/server/lib/assets/font-css.server.ts +86 -0
- package/src/server/lib/assets/fonts.server.ts +117 -0
- package/src/server/lib/bundler/css.ts +116 -0
- package/src/server/lib/lifecycle/shell-route.ts +17 -0
- package/src/server/lib/models/provider-listing.server.ts +257 -0
- package/src/server/lib/models/provider-registry.server.test.ts +122 -0
- package/src/server/lib/models/provider-registry.server.ts +76 -25
- package/src/server/lib/omp/config/models-config.ts +11 -0
- package/src/server/lib/omp/config/models-validation.test.ts +137 -0
- package/src/server/lib/omp/config/models-validation.ts +111 -2
- package/src/server/lib/omp/config/provider-seeds.test.ts +167 -0
- package/src/server/lib/omp/config/provider-seeds.ts +176 -0
- package/src/server/lib/omp/config/providers.ts +94 -100
- package/src/server/lib/omp/core/auth-credentials.ts +45 -0
- package/src/server/lib/omp/session/{usage.ts → usage/aggregate.ts} +1 -118
- package/src/server/lib/omp/session/usage/shaping.ts +130 -0
- package/src/server/lib/usage/deepseek.ts +4 -5
- package/src/server/lib/usage/kenari.ts +7 -6
- package/src/server/lib/usage/omp-usage.server.test.ts +126 -0
- package/src/server/lib/usage/omp-usage.server.ts +194 -0
- package/src/server/lib/usage/provider-key.ts +81 -3
- package/src/server/lib/usage/providers.server.test.ts +47 -0
- package/src/server/lib/usage/providers.server.ts +208 -0
- package/src/server/lib/usage/vendor-balance.server.test.ts +104 -0
- package/src/server/lib/usage/vendor-balance.server.ts +284 -0
- package/src/server/plugins/shell.server.ts +56 -0
- package/src/server/plugins/ssr.ts +24 -36
- package/src/server/plugins/static.test.ts +26 -29
- package/src/server/plugins/static.ts +40 -19
- package/src/server/routes/settings/index.ts +2 -0
- package/src/server/routes/settings/provider-model.ts +107 -0
- package/src/server/routes/settings/provider-models.ts +65 -202
- package/src/server/routes/settings/providers.ts +22 -7
- package/src/server/routes/settings/usage.ts +125 -45
- package/src/server/routes/telemetry/tokens.ts +2 -1
- package/src/shared/lib/fs/diff-parser.test.ts +116 -0
- package/src/shared/lib/fs/diff-parser.ts +86 -33
- package/src/shared/lib/models/{provider-connection.ts → provider/connection.ts} +37 -0
- package/src/shared/lib/models/provider/dialect.ts +179 -0
- package/src/shared/lib/models/provider/glyph.test.ts +116 -0
- package/src/shared/lib/models/provider/glyph.ts +200 -0
- package/src/shared/lib/models/{provider-models.ts → provider/models.ts} +91 -6
- package/src/shared/lib/models/provider/presets.test.ts +172 -0
- package/src/shared/lib/models/provider/presets.ts +83 -0
- package/src/shared/lib/workspace/file-tab-id.ts +29 -0
- package/src/shared/types/settings/provider.ts +81 -0
- package/src/shared/types/settings/usage.ts +88 -14
- package/tsconfig.json +3 -1
- package/dist/client/static/css/index.5f630421b7.css +0 -2
- package/dist/client/static/font/KaTeX_AMS-Regular.a79f1c3119.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Caligraphic-Bold.ec17d13264.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Caligraphic-Regular.55fac25845.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Fraktur-Bold.d42a5579b0.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Fraktur-Regular.d3c882a649.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Main-Bold.c3fb5ac22f.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Main-BoldItalic.6f2bb1dff2.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Main-Italic.8916142bec.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Main-Regular.0462f03bdf.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Math-BoldItalic.572d331f69.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Math-Italic.f28c23acad.woff2 +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Bold.8c5b5494b6.woff2 +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Italic.3b1e59b3ba.woff2 +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Regular.ba21ed5f84.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Script-Regular.03e9641d6f.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Size1-Regular.eae34984b3.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Size2-Regular.5916a24fa3.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Size4-Regular.10d95fd3a2.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Typewriter-Regular.a8709e3622.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-400-normal.2655b10c55.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-500-normal.92a991f204.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-600-normal.43788eb7f1.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-700-normal.2667215d33.woff2 +0 -0
- package/dist/client/static/js/77.0d81c74cf0.js +0 -139
- package/dist/client/static/js/77.0d81c74cf0.js.LICENSE.txt +0 -8
- package/dist/client/static/js/async/0.8e59f2eb5b.js +0 -1
- package/dist/client/static/js/async/0k.5852e6b4fa.js +0 -1
- package/dist/client/static/js/async/0w.9ed36d5860.js +0 -1
- package/dist/client/static/js/async/1.f387c12b63.js +0 -1
- package/dist/client/static/js/async/12.d091f91c99.js +0 -1
- package/dist/client/static/js/async/13.8f810a8a6c.js +0 -1
- package/dist/client/static/js/async/19.6fe909d55a.js +0 -160
- package/dist/client/static/js/async/1b.23f1141a38.js +0 -1
- package/dist/client/static/js/async/1k.973e8e21fc.js +0 -2
- package/dist/client/static/js/async/1k.973e8e21fc.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/1p.0d5cddda96.js +0 -1
- package/dist/client/static/js/async/2.b589f1733b.js +0 -1
- package/dist/client/static/js/async/2v.96165101de.js +0 -1
- package/dist/client/static/js/async/3.0cacc08423.js +0 -1
- package/dist/client/static/js/async/37.b28b2f61cd.js +0 -1
- package/dist/client/static/js/async/3d.5085fb913f.js +0 -1
- package/dist/client/static/js/async/3j.357c80ff38.js +0 -2
- package/dist/client/static/js/async/3j.357c80ff38.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/3p.bcf32a0983.js +0 -1
- package/dist/client/static/js/async/3r.3a69e98ee0.js +0 -64
- package/dist/client/static/js/async/3u.1228460e2a.js +0 -1
- package/dist/client/static/js/async/3y.3530b824ad.js +0 -198
- package/dist/client/static/js/async/3y.3530b824ad.js.LICENSE.txt +0 -5
- package/dist/client/static/js/async/4.9442eeb472.js +0 -1
- package/dist/client/static/js/async/4e.678285ca55.js +0 -1
- package/dist/client/static/js/async/4k.33a0c7eeb4.js +0 -1
- package/dist/client/static/js/async/4m.928a1dde85.js +0 -31
- package/dist/client/static/js/async/4m.928a1dde85.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/4q.aad20dab25.js +0 -1
- package/dist/client/static/js/async/5.ebaf8b89cc.js +0 -36
- package/dist/client/static/js/async/51.95bcae98af.js +0 -2
- package/dist/client/static/js/async/51.95bcae98af.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
- package/dist/client/static/js/async/5j.d6407c4248.js +0 -287
- package/dist/client/static/js/async/5o.88316d4460.js +0 -62
- package/dist/client/static/js/async/6.f034c9b528.js +0 -1
- package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
- package/dist/client/static/js/async/6o.5b1a06a382.js +0 -1
- package/dist/client/static/js/async/7.e4a9288dd0.js +0 -1
- package/dist/client/static/js/async/73.308dff4da8.js +0 -1
- package/dist/client/static/js/async/7e.a1cc9bfd88.js +0 -1
- package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
- package/dist/client/static/js/async/7l.fadbbc0583.js +0 -1
- package/dist/client/static/js/async/8.5067439d8c.js +0 -1
- package/dist/client/static/js/async/8j.ea110663fb.js +0 -1
- package/dist/client/static/js/async/a.bce4542392.js +0 -1
- package/dist/client/static/js/async/aa.65d3852576.js +0 -1
- package/dist/client/static/js/async/ar.9303e5f2e8.js +0 -2
- package/dist/client/static/js/async/ar.9303e5f2e8.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/az.d656062b0f.js +0 -1
- package/dist/client/static/js/async/b.b4f6f06515.js +0 -1
- package/dist/client/static/js/async/bi.09688ba75c.js +0 -1
- package/dist/client/static/js/async/bq.586bc738d1.js +0 -1
- package/dist/client/static/js/async/c.797d933882.js +0 -1
- package/dist/client/static/js/async/c0.0af1e640d3.js +0 -24
- package/dist/client/static/js/async/ca.8a52cfd447.js +0 -1
- package/dist/client/static/js/async/cf.d34fff5d67.js +0 -1
- package/dist/client/static/js/async/ct.b74928f7a3.js +0 -1
- package/dist/client/static/js/async/cx.3cb0242a3a.js +0 -226
- package/dist/client/static/js/async/d.a5fc8fe1b3.js +0 -1
- package/dist/client/static/js/async/da.f62c70f6c8.js +0 -1
- package/dist/client/static/js/async/dp.1725d6432c.js +0 -1
- package/dist/client/static/js/async/e.93214d622b.js +0 -79
- package/dist/client/static/js/async/f.ab50467719.js +0 -19
- package/dist/client/static/js/async/f.ab50467719.js.LICENSE.txt +0 -12
- package/dist/client/static/js/async/f7.46971e9d6d.js +0 -2
- package/dist/client/static/js/async/f7.46971e9d6d.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/f8.bbf0dbae70.js +0 -1
- package/dist/client/static/js/async/g.cb81980498.js +0 -2
- package/dist/client/static/js/async/g.cb81980498.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/h.4e790471e4.js +0 -27
- package/dist/client/static/js/async/h1.393f066d09.js +0 -1
- package/dist/client/static/js/async/h3.216e9798ab.js +0 -148
- package/dist/client/static/js/async/hd.0da931b622.js +0 -1
- package/dist/client/static/js/async/hk.ba7fb04d41.js +0 -1
- package/dist/client/static/js/async/hn.b645041e00.js +0 -1
- package/dist/client/static/js/async/ho.475a5e1ae4.js +0 -1
- package/dist/client/static/js/async/hp.e9fe2e395a.js +0 -2
- package/dist/client/static/js/async/hp.e9fe2e395a.js.LICENSE.txt +0 -17
- package/dist/client/static/js/async/hs.e7c59d8090.js +0 -41
- package/dist/client/static/js/async/i.7c148e8440.js +0 -1
- package/dist/client/static/js/async/id.573df20f29.js +0 -1
- package/dist/client/static/js/async/j.ab925882d8.js +0 -1
- package/dist/client/static/js/async/jd.63886a1f09.js +0 -1
- package/dist/client/static/js/async/jg.d86ebd4013.js +0 -86
- package/dist/client/static/js/async/jl.874d352278.js +0 -1
- package/dist/client/static/js/async/jll.11313b455a.js +0 -1
- package/dist/client/static/js/async/js.67182759ef.js +0 -1
- package/dist/client/static/js/async/jsc.23b70d6f40.js +0 -1
- package/dist/client/static/js/async/jse.334588ff5d.js +0 -1
- package/dist/client/static/js/async/jz.72eb929106.js +0 -77
- package/dist/client/static/js/async/k.3210286401.js +0 -217
- package/dist/client/static/js/async/ka.c1990ecaa3.js +0 -115
- package/dist/client/static/js/async/katex-vendor.de6a87c308.js +0 -1
- package/dist/client/static/js/async/ku.29c8a59d0c.js +0 -1
- package/dist/client/static/js/async/ky.8dab1dade0.js +0 -1
- package/dist/client/static/js/async/l.12dd34e1be.js +0 -1
- package/dist/client/static/js/async/l9.949710adaf.js +0 -83
- package/dist/client/static/js/async/l9.949710adaf.js.LICENSE.txt +0 -13
- package/dist/client/static/js/async/ls.9d9a01026c.js +0 -104
- package/dist/client/static/js/async/m.eb2e6752e2.js +0 -28
- package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/m3.d2b05f9604.js +0 -2
- package/dist/client/static/js/async/m3.d2b05f9604.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/ma.2c261b04dd.js +0 -2
- package/dist/client/static/js/async/ma.2c261b04dd.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/me.8ace414f80.js +0 -23
- package/dist/client/static/js/async/ms.98960af4eb.js +0 -67
- package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +0 -12
- package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
- package/dist/client/static/js/async/n.a9f1c51701.js +0 -1
- package/dist/client/static/js/async/na.a36cd532d6.js +0 -1
- package/dist/client/static/js/async/nk.fc7e58325e.js +0 -1
- package/dist/client/static/js/async/o.36353dfb88.js +0 -1
- package/dist/client/static/js/async/oc.b73042e8bd.js +0 -8
- package/dist/client/static/js/async/og.8eb577893a.js +0 -193
- package/dist/client/static/js/async/p.48ecfcd14e.js +0 -1
- package/dist/client/static/js/async/px.fc6877924b.js +0 -1
- package/dist/client/static/js/async/q.22588845a9.js +0 -21
- package/dist/client/static/js/async/q.22588845a9.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/q5.97dc1481b6.js +0 -286
- package/dist/client/static/js/async/qs.2a9ddb3c72.js +0 -1
- package/dist/client/static/js/async/r.51010ad87c.js +0 -1
- package/dist/client/static/js/async/rd.cb6bf495bb.js +0 -2
- package/dist/client/static/js/async/rd.cb6bf495bb.js.LICENSE.txt +0 -4
- package/dist/client/static/js/async/s.0f395cdda5.js +0 -94
- package/dist/client/static/js/async/t.4dca0e2ba8.js +0 -1
- package/dist/client/static/js/async/t9.d44f57d518.js +0 -1
- package/dist/client/static/js/async/ta.973534bbec.js +0 -1
- package/dist/client/static/js/async/tr.e878658140.js +0 -309
- package/dist/client/static/js/async/u.0871ac5b22.js +0 -1
- package/dist/client/static/js/async/ud.3065bfee31.js +0 -2
- package/dist/client/static/js/async/ud.3065bfee31.js.LICENSE.txt +0 -4
- package/dist/client/static/js/async/up.ea990da0ff.js +0 -32
- package/dist/client/static/js/async/uw.8890abbabc.js +0 -1
- package/dist/client/static/js/async/v.7d28d3c241.js +0 -34
- package/dist/client/static/js/async/vp.9b13bf8610.js +0 -1
- package/dist/client/static/js/async/vt.42416b707d.js +0 -1
- package/dist/client/static/js/async/w.08ac210f60.js +0 -1
- package/dist/client/static/js/async/wt.ad92393a90.js +0 -1
- package/dist/client/static/js/async/x.563f72ca6b.js +0 -1
- package/dist/client/static/js/async/x0.475516418f.js +0 -1
- package/dist/client/static/js/async/x1.da8687159b.js +0 -90
- package/dist/client/static/js/async/xe.da8dcc2015.js +0 -2
- package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/xl.a3c562a58c.js +0 -1
- package/dist/client/static/js/async/xn.6510c0abe2.js +0 -2
- package/dist/client/static/js/async/xn.6510c0abe2.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/xn1.83a7784fe1.js +0 -10
- package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/xnl.feb0021e50.js +0 -2
- package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +0 -12
- package/dist/client/static/js/async/y.1a9ed7a5d8.js +0 -1
- package/dist/client/static/js/async/y6.43c57defe4.js +0 -1
- package/dist/client/static/js/async/yb.8c1342401d.js +0 -1
- package/dist/client/static/js/async/ybq.4491f2f2dc.js +0 -1
- package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
- package/dist/client/static/js/async/yq.917ddd7ba2.js +0 -1
- package/dist/client/static/js/async/ys.00687cf82d.js +0 -38
- package/dist/client/static/js/async/yv.d99d829136.js +0 -1
- package/dist/client/static/js/async/z.56f61a4374.js +0 -1
- package/dist/client/static/js/async/zi.5fdb07b86a.js +0 -24
- package/dist/client/static/js/async/zj.654f37e8ff.js +0 -1
- package/dist/client/static/js/async/zn.966d6f1cc6.js +0 -1
- package/dist/client/static/js/async/zo.26213b40c0.js +0 -1
- package/dist/client/static/js/async/zx.e9f4105e00.js +0 -1
- package/dist/client/static/js/index.e6a257402a.js +0 -38
- package/dist/client/static/js/index.e6a257402a.js.LICENSE.txt +0 -4
- package/dist/client/static/js/markdown-vendor.71e43278b3.js +0 -144
- package/dist/client/static/js/shiki-vendor.cb8452ddcd.js +0 -1
- package/src/client/components/settings/categories/provider-settings/Icons.tsx +0 -35
- package/src/client/hooks/settings/providers.ts +0 -326
- package/src/server/plugins/dev-assets.ts +0 -49
- package/src/shared/lib/code/editor-utils.ts +0 -16
- package/src/shared/lib/models/provider-presets.ts +0 -29
- /package/dist/client/{apple-touch-icon.png → static/apple-touch-icon-dskx5sr3.png} +0 -0
- /package/dist/client/{icon-192x192.png → static/icon-192x192-6gn5cr2f.png} +0 -0
- /package/dist/client/{icon.svg → static/icon-d07z9dtz.svg} +0 -0
- /package/dist/client/{manifest.webmanifest → static/manifest-bnyy75kt.webmanifest} +0 -0
- /package/{dist/client → public}/icon-512x512.png +0 -0
- /package/{dist/client → public}/icon-maskable-512x512.png +0 -0
- /package/{dist/client → public}/sw.js +0 -0
- /package/src/shared/lib/models/{provider-cleanup.ts → provider/cleanup.ts} +0 -0
- /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
|
|
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
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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:
|
|
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 =
|
|
69
|
+
const diffId = diffTabId(rawPath, isStaged);
|
|
58
70
|
|
|
59
71
|
const diffEntry: OpenedFile = {
|
|
60
72
|
id: diffId,
|
|
61
|
-
name:
|
|
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
|
|
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
|
package/src/server/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|