ompchamber 2.0.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -11
- 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-8t70jhyj.js +5149 -0
- package/dist/client/static/js/index-n4wkqj7t.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/code-editor/handler.test.ts +274 -0
- package/src/client/components/common/code-editor/handler.ts +175 -0
- package/src/client/components/common/code-editor/index.tsx +185 -220
- package/src/client/components/common/code-surface/index.tsx +109 -12
- package/src/client/components/common/lazy-panels.tsx +5 -1
- 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/common/viewport.ts +5 -3
- package/src/client/components/layout/RightActivityBar.tsx +2 -1
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +21 -4
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
- package/src/client/components/layout/desktop-layout/index.tsx +26 -4
- package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
- package/src/client/components/mobile/RightSidebar.tsx +11 -4
- 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/browser-panel/AddressBar.tsx +1 -10
- package/src/client/components/workspace/browser-panel/Screencast.tsx +4 -5
- package/src/client/components/workspace/browser-panel/index.tsx +6 -6
- package/src/client/components/workspace/browser-toolbar/index.tsx +22 -12
- 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/chat-timeline/ToolCallCard.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Todo.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/CommandPalette.test.ts +242 -0
- package/src/client/components/workspace/editor/CommandPalette.tsx +138 -0
- package/src/client/components/workspace/editor/FindWidget.test.ts +261 -0
- package/src/client/components/workspace/editor/FindWidget.tsx +219 -0
- package/src/client/components/workspace/editor/Tabs.tsx +2 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +129 -26
- package/src/client/components/workspace/editor/index.tsx +180 -62
- package/src/client/components/workspace/file-explorer/Modals.tsx +79 -2
- package/src/client/components/workspace/file-explorer/TreeItem.tsx +43 -113
- 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/todo-panel/PhaseGroup.tsx +76 -0
- package/src/client/components/workspace/todo-panel/Row.tsx +69 -0
- package/src/client/components/workspace/todo-panel/index.tsx +161 -0
- 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/find-harness.test-util.ts +170 -0
- package/src/client/hooks/editor/scroll-container.test.ts +102 -0
- package/src/client/hooks/editor/scroll-container.ts +26 -0
- package/src/client/hooks/editor/use-code-window.ts +2 -20
- package/src/client/hooks/editor/use-editor-find-write.test.ts +170 -0
- package/src/client/hooks/editor/use-editor-find.test.ts +118 -0
- package/src/client/hooks/editor/use-editor-find.ts +321 -0
- package/src/client/hooks/editor/use-editor-history.test.ts +216 -0
- package/src/client/hooks/editor/use-editor-history.ts +134 -0
- package/src/client/hooks/editor/use-file-editor.ts +24 -27
- package/src/client/hooks/editor/use-occurrences.test.ts +236 -0
- package/src/client/hooks/editor/use-occurrences.ts +98 -0
- 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/hooks/workspace/file-tree-actions.test.ts +348 -0
- package/src/client/hooks/workspace/file-tree-actions.ts +250 -0
- package/src/client/hooks/workspace/session-todos.ts +102 -0
- package/src/client/stubs/elkjs.ts +6 -1
- package/src/client/tailwind.css +30 -0
- 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/todos.ts +100 -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/fs/action-create.test.ts +90 -0
- package/src/server/routes/fs/action.ts +23 -0
- package/src/server/routes/omp/index.ts +2 -0
- package/src/server/routes/omp/session-todos.ts +80 -0
- 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/chat/{todo-parser.ts → todo/parser.ts} +17 -0
- package/src/shared/lib/chat/todo/progress.ts +97 -0
- package/src/shared/lib/chat/todo/snapshot.test.ts +156 -0
- package/src/shared/lib/chat/todo/snapshot.ts +215 -0
- package/src/shared/lib/code/editor/commands.test.ts +155 -0
- package/src/shared/lib/code/editor/commands.ts +260 -0
- package/src/shared/lib/code/editor/edits.test.ts +100 -0
- package/src/shared/lib/code/editor/edits.ts +122 -0
- package/src/shared/lib/code/editor/find-status.test.ts +50 -0
- package/src/shared/lib/code/editor/find-status.ts +36 -0
- package/src/shared/lib/code/editor/find.test.ts +203 -0
- package/src/shared/lib/code/editor/find.ts +224 -0
- package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.test.ts +1 -1
- package/src/shared/lib/code/editor/keymap.test.ts +109 -0
- package/src/shared/lib/code/editor/keymap.ts +151 -0
- package/src/shared/lib/code/editor/layers.test.ts +77 -0
- package/src/shared/lib/code/editor/layers.ts +67 -0
- package/src/shared/lib/code/editor/line-edits.test.ts +131 -0
- package/src/shared/lib/code/editor/line-edits.ts +159 -0
- package/src/shared/lib/code/editor/lines.test.ts +81 -0
- package/src/shared/lib/code/editor/lines.ts +83 -0
- package/src/shared/lib/code/editor/marks.test.ts +86 -0
- package/src/shared/lib/code/editor/marks.ts +106 -0
- package/src/shared/lib/code/editor/occurrences.test.ts +117 -0
- package/src/shared/lib/code/editor/occurrences.ts +111 -0
- package/src/shared/lib/code/editor/palette.test.ts +91 -0
- package/src/shared/lib/code/editor/palette.ts +92 -0
- package/src/shared/lib/code/editor/reset-css.test.ts +35 -0
- package/src/shared/lib/code/editor/reset-css.ts +32 -0
- package/src/shared/lib/code/editor/text-commands.test.ts +178 -0
- package/src/shared/lib/code/editor/text-commands.ts +154 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +75 -0
- package/src/shared/lib/code/syntax-highlight.ts +29 -17
- package/src/shared/lib/code/windowed-highlight.ts +52 -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/ui/key-binding.test.ts +115 -0
- package/src/shared/lib/ui/key-binding.ts +143 -0
- package/src/shared/lib/util/platform.test.ts +87 -0
- package/src/shared/lib/util/platform.ts +30 -0
- package/src/shared/lib/workspace/file-tab-id.ts +29 -0
- package/src/shared/lib/workspace/keymap.test.ts +82 -0
- package/src/shared/lib/workspace/keymap.ts +30 -0
- package/src/shared/lib/workspace/refresh-cadence.ts +9 -0
- package/src/shared/lib/workspace/right-panels.ts +8 -0
- package/src/shared/lib/workspace/session-state/store.ts +0 -1
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/settings/provider.ts +81 -0
- package/src/shared/types/settings/usage.ts +88 -14
- package/src/shared/types/todo.ts +108 -0
- 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/{client/components/common/code-editor → shared/lib/code/editor}/history.ts +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
|
@@ -13,27 +13,48 @@
|
|
|
13
13
|
* schema before it reaches disk. Reading lives in `./models-config.ts`.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { isMap, isSeq, type
|
|
16
|
+
import { isMap, isSeq, type YAMLMap } from 'yaml';
|
|
17
17
|
import { getModelsConfigPath } from '@/server/lib/omp/config/models-config';
|
|
18
18
|
import { plainOf, withOmpYamlDocument, OmpConfigError } from '@/server/lib/omp/config/document';
|
|
19
|
-
import { validateModelsDocument
|
|
20
|
-
import {
|
|
19
|
+
import { validateModelsDocument } from '@/server/lib/omp/config/models-validation';
|
|
20
|
+
import {
|
|
21
|
+
backfillableIds,
|
|
22
|
+
backfillEntry,
|
|
23
|
+
knownModelIds,
|
|
24
|
+
toModelEntry,
|
|
25
|
+
type OmpProviderModelSeed,
|
|
26
|
+
} from '@/server/lib/omp/config/provider-seeds';
|
|
27
|
+
import { isMaskedApiKey } from '@/shared/lib/models/provider/dialect';
|
|
28
|
+
import type {
|
|
29
|
+
OmpProviderApi,
|
|
30
|
+
ProviderAuthMode,
|
|
31
|
+
ProviderDiscoveryType,
|
|
32
|
+
} from '@/shared/types/settings/provider';
|
|
21
33
|
|
|
22
|
-
|
|
23
|
-
export interface OmpProviderModelSeed {
|
|
24
|
-
id: string;
|
|
25
|
-
name?: string;
|
|
26
|
-
reasoning?: boolean;
|
|
27
|
-
imageInput?: boolean;
|
|
28
|
-
contextWindow?: number;
|
|
29
|
-
maxTokens?: number;
|
|
30
|
-
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
31
|
-
}
|
|
34
|
+
export type { OmpProviderModelSeed } from '@/server/lib/omp/config/provider-seeds';
|
|
32
35
|
|
|
33
36
|
export interface OmpProviderUpsertInput {
|
|
34
37
|
baseUrl: string;
|
|
35
38
|
apiKey?: string;
|
|
36
|
-
api?:
|
|
39
|
+
api?: OmpProviderApi;
|
|
40
|
+
/**
|
|
41
|
+
* omp's auth mode. `none` is what registers a keyless local server: without
|
|
42
|
+
* it omp demands an `apiKey` and rejects the provider as unconfigured.
|
|
43
|
+
*/
|
|
44
|
+
auth?: ProviderAuthMode;
|
|
45
|
+
/**
|
|
46
|
+
* Let OMP list this provider's models live instead of the chamber writing a
|
|
47
|
+
* snapshot of ids. A discovery provider is legitimately registered with an
|
|
48
|
+
* empty `models` array.
|
|
49
|
+
*/
|
|
50
|
+
discovery?: ProviderDiscoveryType;
|
|
51
|
+
/**
|
|
52
|
+
* Register/override the provider WITHOUT writing any model ids — the shape
|
|
53
|
+
* omp documents as an "override-only provider". Used when the provider is one
|
|
54
|
+
* omp already bundles (its own catalog supplies the models) or when its
|
|
55
|
+
* model list is owned by discovery.
|
|
56
|
+
*/
|
|
57
|
+
overrideOnly?: boolean;
|
|
37
58
|
models: OmpProviderModelSeed[];
|
|
38
59
|
}
|
|
39
60
|
|
|
@@ -46,75 +67,30 @@ export interface OmpProviderUpsertResult {
|
|
|
46
67
|
reason?: string;
|
|
47
68
|
}
|
|
48
69
|
|
|
49
|
-
/** Masks a stored credential echoed back by the UI; not a value to persist. */
|
|
50
|
-
const MASKED_KEY_PATTERN = /•{3,}/;
|
|
51
|
-
|
|
52
|
-
/** True when `value` is a credential the UI masked rather than a real key. */
|
|
53
|
-
export function isMaskedApiKey(value: unknown): boolean {
|
|
54
|
-
return typeof value === 'string' && MASKED_KEY_PATTERN.test(value);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** The model ids already registered under `provider`, array form only. */
|
|
58
|
-
function knownModelIds(provider: Record<string, unknown> | undefined): Set<string> {
|
|
59
|
-
if (!Array.isArray(provider?.models)) return new Set();
|
|
60
|
-
return new Set(
|
|
61
|
-
provider.models
|
|
62
|
-
.map((model) => (isRecord(model) && typeof model.id === 'string' ? model.id : ''))
|
|
63
|
-
.filter(Boolean),
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** The model entry as omp expects it, omitting anything omp would reject. */
|
|
68
|
-
function toModelEntry(model: OmpProviderModelSeed): Record<string, unknown> {
|
|
69
|
-
return sanitizeModelEntry({
|
|
70
|
-
id: model.id,
|
|
71
|
-
...(model.name ? { name: model.name } : {}),
|
|
72
|
-
...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {}),
|
|
73
|
-
...(model.imageInput ? { input: ['text', 'image'] } : { input: ['text'] }),
|
|
74
|
-
...(model.contextWindow && model.contextWindow > 0 ? { contextWindow: model.contextWindow } : {}),
|
|
75
|
-
...(model.maxTokens && model.maxTokens > 0 ? { maxTokens: model.maxTokens } : {}),
|
|
76
|
-
...(model.cost ? { cost: model.cost } : {}),
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Metadata a bare existing entry is missing, taken from the freshly fetched
|
|
82
|
-
* seed. Values already present are never touched. Writes through the YAML node
|
|
83
|
-
* so the entry's own comments and key order stay as the user wrote them.
|
|
84
|
-
*/
|
|
85
|
-
function backfillEntry(doc: Document, entry: YAMLMap, seed: OmpProviderModelSeed): void {
|
|
86
|
-
if (seed.name && !entry.has('name')) entry.set('name', seed.name);
|
|
87
|
-
if (seed.reasoning !== undefined && !entry.has('reasoning')) {
|
|
88
|
-
entry.set('reasoning', seed.reasoning);
|
|
89
|
-
}
|
|
90
|
-
const currentInput = plainOf<string[]>(doc, entry.get('input'));
|
|
91
|
-
if (seed.imageInput && (!Array.isArray(currentInput) || !currentInput.includes('image'))) {
|
|
92
|
-
entry.set('input', ['text', 'image']);
|
|
93
|
-
}
|
|
94
|
-
if (seed.contextWindow && seed.contextWindow > 0 && !plainOf(doc, entry.get('contextWindow'))) {
|
|
95
|
-
entry.set('contextWindow', seed.contextWindow);
|
|
96
|
-
}
|
|
97
|
-
if (seed.maxTokens && seed.maxTokens > 0 && !plainOf(doc, entry.get('maxTokens'))) {
|
|
98
|
-
entry.set('maxTokens', seed.maxTokens);
|
|
99
|
-
}
|
|
100
|
-
if (seed.cost && !entry.has('cost')) {
|
|
101
|
-
entry.set('cost', seed.cost);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
70
|
/**
|
|
106
71
|
* Add-only upsert of a provider (and its models) into the native omp
|
|
107
72
|
* models.yml — the agent's own registry, where per-model cost feeds usage
|
|
108
|
-
* tracking. Existing provider fields (apiKey, baseUrl) and existing model
|
|
73
|
+
* tracking. Existing provider fields (apiKey, baseUrl, api) and existing model
|
|
109
74
|
* entries are never modified; only models whose id is not yet registered are
|
|
110
|
-
* appended
|
|
111
|
-
* model), and the write is
|
|
75
|
+
* appended, and only fields the entry is MISSING are filled. Merges preserve
|
|
76
|
+
* unrelated keys, comments and formatting (document model), and the write is
|
|
77
|
+
* atomic and mode-preserving.
|
|
78
|
+
*
|
|
79
|
+
* Three provider shapes are written here, and omp's rules differ for each:
|
|
80
|
+
*
|
|
81
|
+
* - **full** (`models` non-empty): `baseUrl` plus an `apiKey` unless
|
|
82
|
+
* `auth: none`/`oauth`.
|
|
83
|
+
* - **discovery** (`discovery` set): a shell omp lists models from itself. The
|
|
84
|
+
* model list stays empty — writing ids as well would freeze a snapshot omp
|
|
85
|
+
* would then serve alongside its live discovery.
|
|
86
|
+
* - **override** (`overrideOnly`, or an empty model list for a provider omp
|
|
87
|
+
* already bundles): keep omp's own catalog models and only change the
|
|
88
|
+
* endpoint. A provider with neither models nor any config field is rejected
|
|
89
|
+
* by omp, so one of `baseUrl`/`apiKey`/`auth: none`/`discovery` must be set.
|
|
112
90
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* against omp's own rules before the write: a schema violation anywhere in the
|
|
117
|
-
* file makes omp disable every custom provider at once.
|
|
91
|
+
* The finished document is validated against omp's own rules before the write:
|
|
92
|
+
* a schema violation anywhere in the file makes omp disable every custom
|
|
93
|
+
* provider at once.
|
|
118
94
|
*/
|
|
119
95
|
export async function upsertOmpProviderModels(
|
|
120
96
|
slug: string,
|
|
@@ -144,11 +120,18 @@ export async function upsertOmpProviderModels(
|
|
|
144
120
|
+ 'Fix that entry before adding models; omp currently disables every custom provider because of it.',
|
|
145
121
|
);
|
|
146
122
|
}
|
|
123
|
+
// omp demands an `apiKey` only when a provider carries MODELS: an
|
|
124
|
+
// override-only entry (baseUrl/discovery/auth) is valid without one, which
|
|
125
|
+
// is how a proxy override for a bundled provider keeps using `/login`
|
|
126
|
+
// credentials, and how a keyless local server is declared.
|
|
127
|
+
const overrideOnly = input.overrideOnly === true || input.discovery !== undefined;
|
|
128
|
+
const writesModels = !overrideOnly && input.models.length > 0;
|
|
129
|
+
const keyless = input.auth === 'none' || input.auth === 'oauth';
|
|
147
130
|
const known = knownModelIds(existing);
|
|
148
131
|
const incomingById = new Map(input.models.map((model) => [model.id, model]));
|
|
149
132
|
const additions = input.models.filter((model) => model.id && !known.has(model.id));
|
|
150
133
|
|
|
151
|
-
if (!existing && !input.apiKey) {
|
|
134
|
+
if (!existing && writesModels && !input.apiKey && !keyless) {
|
|
152
135
|
return {
|
|
153
136
|
result: {
|
|
154
137
|
written: false,
|
|
@@ -164,7 +147,7 @@ export async function upsertOmpProviderModels(
|
|
|
164
147
|
// writer is the last gate before a file omp will authenticate with — a
|
|
165
148
|
// masked placeholder persisted here would look like a configured provider
|
|
166
149
|
// that cannot authenticate.
|
|
167
|
-
if (!existing && isMaskedApiKey(input.apiKey)) {
|
|
150
|
+
if (!existing && writesModels && isMaskedApiKey(input.apiKey)) {
|
|
168
151
|
return {
|
|
169
152
|
result: {
|
|
170
153
|
written: false,
|
|
@@ -179,22 +162,21 @@ export async function upsertOmpProviderModels(
|
|
|
179
162
|
|
|
180
163
|
// Existing entries with bare ids (no context, no capabilities) get the
|
|
181
164
|
// fetched metadata filled in — values already present are never touched.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (!isRecord(model) || typeof model.id !== 'string') return false;
|
|
185
|
-
const seed = incomingById.get(model.id);
|
|
186
|
-
if (!seed) return false;
|
|
187
|
-
const missingContext = typeof model.contextWindow !== 'number' || !model.contextWindow;
|
|
188
|
-
return Boolean(
|
|
189
|
-
(missingContext && (seed.contextWindow || seed.maxTokens))
|
|
190
|
-
|| (!model.cost && seed.cost)
|
|
191
|
-
|| (seed.reasoning !== undefined && model.reasoning === undefined)
|
|
192
|
-
|| (!Array.isArray(model.input) && seed.imageInput),
|
|
193
|
-
);
|
|
194
|
-
})
|
|
195
|
-
.map((model) => String(model.id));
|
|
165
|
+
// A discovery provider keeps its empty list: omp owns those models.
|
|
166
|
+
const backfillIds = overrideOnly ? [] : backfillableIds(existing?.models, incomingById);
|
|
196
167
|
|
|
197
|
-
|
|
168
|
+
// A provider the user re-saves without any new model and without a dialect
|
|
169
|
+
// change has nothing to write — reporting `written: true` there would claim
|
|
170
|
+
// a file edit that never happened. A provider that does NOT exist yet is
|
|
171
|
+
// always a change: the entry itself is the write, which is the whole point
|
|
172
|
+
// of a discovery or override registration that carries no model ids.
|
|
173
|
+
const providerChanged = !existing || Boolean(
|
|
174
|
+
(input.api && !existing.api)
|
|
175
|
+
|| (input.auth && input.auth !== 'apiKey' && !existing.auth)
|
|
176
|
+
|| (input.discovery && !existing.discovery),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (additions.length === 0 && backfillIds.length === 0 && !providerChanged) {
|
|
198
180
|
return {
|
|
199
181
|
result: {
|
|
200
182
|
written: false,
|
|
@@ -213,16 +195,28 @@ export async function upsertOmpProviderModels(
|
|
|
213
195
|
if (!existing) {
|
|
214
196
|
doc.setIn(['providers', slug], doc.createNode({
|
|
215
197
|
baseUrl: input.baseUrl,
|
|
216
|
-
|
|
198
|
+
// `auth: none` makes an apiKey optional, so a keyless shell omits it
|
|
199
|
+
// rather than writing an empty string omp rejects.
|
|
200
|
+
...(input.apiKey ? { apiKey: input.apiKey } : {}),
|
|
217
201
|
// omp disables every custom provider when a models-carrying provider
|
|
218
202
|
// lacks "api" (provider or model level) — always set one.
|
|
219
203
|
api: input.api ?? 'openai-completions',
|
|
204
|
+
...(input.auth && input.auth !== 'apiKey' ? { auth: input.auth } : {}),
|
|
205
|
+
...(input.discovery ? { discovery: { type: input.discovery } } : {}),
|
|
220
206
|
models: [],
|
|
221
207
|
}));
|
|
222
|
-
} else
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
208
|
+
} else {
|
|
209
|
+
// Add-only: a field the entry already carries belongs to the user, and
|
|
210
|
+
// rewriting it would discard a credential or endpoint they pinned.
|
|
211
|
+
if (!existing.api) {
|
|
212
|
+
doc.setIn(['providers', slug, 'api'], input.api ?? 'openai-completions');
|
|
213
|
+
}
|
|
214
|
+
if (input.auth && input.auth !== 'apiKey' && !existing.auth) {
|
|
215
|
+
doc.setIn(['providers', slug, 'auth'], input.auth);
|
|
216
|
+
}
|
|
217
|
+
if (input.discovery && !existing.discovery) {
|
|
218
|
+
doc.setIn(['providers', slug, 'discovery'], doc.createNode({ type: input.discovery }));
|
|
219
|
+
}
|
|
226
220
|
}
|
|
227
221
|
|
|
228
222
|
// `addIn` on a missing key creates a MAP, not a sequence — exactly the
|
|
@@ -85,3 +85,48 @@ export async function readOmpProviderApiKey(slug: string): Promise<string | null
|
|
|
85
85
|
if (fromYml) return fromYml;
|
|
86
86
|
return readAgentDbApiKey(slug);
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Slugs holding a live (non-disabled) credential in omp's agent.db. Used to
|
|
91
|
+
* decide which providers a usage surface may list — a provider with no
|
|
92
|
+
* credential must not be shown at all. Returns slugs only, never key material.
|
|
93
|
+
*/
|
|
94
|
+
export async function listAgentDbCredentialSlugs(): Promise<string[]> {
|
|
95
|
+
const dbPath = join(getAgentDir(), 'agent.db');
|
|
96
|
+
if (!(await Bun.file(dbPath).exists())) return [];
|
|
97
|
+
let db: Database | null = null;
|
|
98
|
+
try {
|
|
99
|
+
db = new Database(dbPath, { readonly: true });
|
|
100
|
+
const rows = db
|
|
101
|
+
.query<{ provider: string }, []>(
|
|
102
|
+
`SELECT DISTINCT provider FROM ${AUTH_CREDENTIALS_TABLE} WHERE disabled_cause IS NULL`,
|
|
103
|
+
)
|
|
104
|
+
.all();
|
|
105
|
+
return rows
|
|
106
|
+
.map((row) => (typeof row.provider === 'string' ? row.provider.trim().toLowerCase() : ''))
|
|
107
|
+
.filter(Boolean);
|
|
108
|
+
} catch {
|
|
109
|
+
return []; // no-excuse-ok: catch — best-effort read of an external store
|
|
110
|
+
} finally {
|
|
111
|
+
db?.close();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Slugs carrying an `apiKey` in models.yml. Returns slugs only. */
|
|
116
|
+
export async function listModelsYmlCredentialSlugs(): Promise<string[]> {
|
|
117
|
+
const path = await getModelsConfigPath();
|
|
118
|
+
if (!(await Bun.file(path).exists())) return [];
|
|
119
|
+
try {
|
|
120
|
+
const data = Bun.YAML.parse(await Bun.file(path).text());
|
|
121
|
+
if (!isRecord(data) || !isRecord(data.providers)) return [];
|
|
122
|
+
const slugs: string[] = [];
|
|
123
|
+
for (const [slug, value] of Object.entries(data.providers)) {
|
|
124
|
+
if (!isRecord(value)) continue;
|
|
125
|
+
const key = typeof value.apiKey === 'string' ? value.apiKey.trim() : '';
|
|
126
|
+
if (key.length > 0) slugs.push(slug.trim().toLowerCase());
|
|
127
|
+
}
|
|
128
|
+
return slugs;
|
|
129
|
+
} catch {
|
|
130
|
+
return []; // no-excuse-ok: catch — a malformed models.yml is a soft miss
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read one session's todo state from its transcript.
|
|
8
|
+
*
|
|
9
|
+
* The list is omp's, and its only durable home is the session JSONL — there is
|
|
10
|
+
* no separate todo file and no chamber-side copy to fall out of date. So the
|
|
11
|
+
* read is: locate the session file, parse it, and take the deepest committed
|
|
12
|
+
* snapshot on the active branch (`latestTodoSnapshot`).
|
|
13
|
+
*
|
|
14
|
+
* Two properties of this read are deliberate:
|
|
15
|
+
*
|
|
16
|
+
* - **It is a full-file parse, cached by size+mtime.** Measured on real
|
|
17
|
+
* sessions (7.2 MB / 1905 entries) a full parse is ~15 ms, and the todos
|
|
18
|
+
* may sit megabytes from EOF — a `todo` call near the start of a long
|
|
19
|
+
* session stays the live list for the rest of it, so a bounded tail window
|
|
20
|
+
* would report "no todos" for exactly the sessions that have the most. omp
|
|
21
|
+
* flushes each entry as it appends, so a poll sees a new list within the
|
|
22
|
+
* second it was written.
|
|
23
|
+
* - **It never creates an omp process.** The session is read-only here; a
|
|
24
|
+
* panel that spawned a child per poll would fight the chat's own session
|
|
25
|
+
* wrapper for the same session file.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { findSessionFileById } from '@/server/lib/omp/session/locator';
|
|
29
|
+
import { parseJsonlLenient } from '@/shared/lib/omp/session/jsonl';
|
|
30
|
+
import { latestTodoSnapshot, todoProgress } from '@/shared/lib/chat/todo/snapshot';
|
|
31
|
+
import type { SessionTodoState } from '@/shared/types/todo';
|
|
32
|
+
|
|
33
|
+
/** Sessions larger than this are refused rather than parsed; a transcript past
|
|
34
|
+
* it is pathological, and the read runs behind an HTTP request. */
|
|
35
|
+
const MAX_TODO_SESSION_BYTES = 512 * 1024 * 1024;
|
|
36
|
+
|
|
37
|
+
interface TodoCacheEntry {
|
|
38
|
+
size: number;
|
|
39
|
+
mtimeMs: number;
|
|
40
|
+
state: SessionTodoState;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Cache keyed by session id. Hangs off `globalThis` for the same reason the
|
|
45
|
+
* session-file caches do: a `bun --hot` reload re-evaluates modules but keeps
|
|
46
|
+
* `globalThis`, and a cache that vanished on every edit would re-parse a
|
|
47
|
+
* multi-megabyte file per poll during development.
|
|
48
|
+
*/
|
|
49
|
+
interface TodoCacheHost {
|
|
50
|
+
entries: Map<string, TodoCacheEntry>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
declare global {
|
|
54
|
+
// eslint-disable-next-line no-var
|
|
55
|
+
var __ompChamberTodoCache: TodoCacheHost | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cache(): TodoCacheHost {
|
|
59
|
+
globalThis.__ompChamberTodoCache ??= { entries: new Map() };
|
|
60
|
+
return globalThis.__ompChamberTodoCache;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const EMPTY_TODO_STATE: SessionTodoState = { snapshot: null, progress: todoProgress([]) };
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The todo state of one session. Returns an empty state (never a throw) when
|
|
67
|
+
* the transcript cannot be read: a session whose file is gone is a normal
|
|
68
|
+
* condition for a sidebar entry the user has not opened yet, and the panel
|
|
69
|
+
* reports "no todos" rather than an error for it.
|
|
70
|
+
*/
|
|
71
|
+
export async function readSessionTodos(sessionId: string): Promise<SessionTodoState> {
|
|
72
|
+
const filePath = await findSessionFileById(sessionId);
|
|
73
|
+
if (!filePath) return EMPTY_TODO_STATE;
|
|
74
|
+
|
|
75
|
+
let size: number;
|
|
76
|
+
let mtimeMs: number;
|
|
77
|
+
try {
|
|
78
|
+
const stats = await Bun.file(filePath).stat();
|
|
79
|
+
if (stats.size > MAX_TODO_SESSION_BYTES) return EMPTY_TODO_STATE;
|
|
80
|
+
size = stats.size;
|
|
81
|
+
mtimeMs = stats.mtimeMs;
|
|
82
|
+
} catch {
|
|
83
|
+
return EMPTY_TODO_STATE;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const slot = cache().entries.get(sessionId);
|
|
87
|
+
if (slot && slot.size === size && slot.mtimeMs === mtimeMs) return slot.state;
|
|
88
|
+
|
|
89
|
+
let body: string;
|
|
90
|
+
try {
|
|
91
|
+
body = await Bun.file(filePath).text();
|
|
92
|
+
} catch {
|
|
93
|
+
return EMPTY_TODO_STATE;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const snapshot = latestTodoSnapshot(parseJsonlLenient<Record<string, unknown>>(body));
|
|
97
|
+
const state: SessionTodoState = { snapshot, progress: todoProgress(snapshot?.phases ?? []) };
|
|
98
|
+
cache().entries.set(sessionId, { size, mtimeMs, state });
|
|
99
|
+
return state;
|
|
100
|
+
}
|
|
@@ -15,7 +15,7 @@ import fs from 'fs';
|
|
|
15
15
|
import { join } from 'path';
|
|
16
16
|
import { getSessionsDir } from '@/server/lib/omp/core/paths';
|
|
17
17
|
import { parseJsonlLenient } from '@/shared/lib/omp/session/jsonl';
|
|
18
|
-
import type {
|
|
18
|
+
import type { TimeRangeType } from '@/shared/types';
|
|
19
19
|
import type { OmpMessageEntry } from '@/shared/types/omp/session';
|
|
20
20
|
|
|
21
21
|
export type UsageWindow =
|
|
@@ -179,120 +179,3 @@ export async function aggregateUsage(window: UsageWindow): Promise<UsageAggregat
|
|
|
179
179
|
cache.set(key, { at: Date.now(), data });
|
|
180
180
|
return data;
|
|
181
181
|
}
|
|
182
|
-
|
|
183
|
-
const fmtInt = (value: number): string => value.toLocaleString('en-US');
|
|
184
|
-
const fmtUsd = (value: number): string => `$${value.toFixed(2)}`;
|
|
185
|
-
|
|
186
|
-
const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] as const;
|
|
187
|
-
|
|
188
|
-
function dayLabel(dayKey: string): string {
|
|
189
|
-
const date = new Date(`${dayKey}T00:00:00`);
|
|
190
|
-
return Number.isNaN(date.getTime()) ? dayKey : `${MONTH_LABELS[date.getMonth()]} ${date.getDate()}`;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function bucketKey(dayKey: string, cadence: CadenceType): string {
|
|
194
|
-
if (cadence === 'monthly') return dayKey.slice(0, 7);
|
|
195
|
-
if (cadence === 'weekly') {
|
|
196
|
-
const date = new Date(`${dayKey}T00:00:00`);
|
|
197
|
-
if (Number.isNaN(date.getTime())) return dayKey;
|
|
198
|
-
const monday = new Date(date);
|
|
199
|
-
const shift = (date.getDay() + 6) % 7;
|
|
200
|
-
monday.setDate(date.getDate() - shift);
|
|
201
|
-
return monday.toISOString().slice(0, 10);
|
|
202
|
-
}
|
|
203
|
-
return dayKey;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function labelForBucket(key: string, cadence: CadenceType): string {
|
|
207
|
-
if (cadence === 'monthly') {
|
|
208
|
-
const [year, month] = key.split('-');
|
|
209
|
-
const monthIndex = Number(month) - 1;
|
|
210
|
-
const label = MONTH_LABELS[monthIndex] ?? key;
|
|
211
|
-
return year && Number(year) !== new Date().getFullYear() ? `${label} ${year}` : label;
|
|
212
|
-
}
|
|
213
|
-
return dayLabel(key);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** Shape the per-day aggregate into an ordered chart series for the cadence:
|
|
217
|
-
* weekly buckets align to Monday, monthly to YYYY-MM, and the series is
|
|
218
|
-
* truncated to the last 60 buckets so the SVG path stays readable. */
|
|
219
|
-
export function buildChartSeries(a: UsageAggregate, cadence: CadenceType): ChartSeriesPoint[] {
|
|
220
|
-
const buckets = new Map<string, { cost: number; tokens: number }>();
|
|
221
|
-
for (const [dayKey, v] of a.byDay) {
|
|
222
|
-
const key = bucketKey(dayKey, cadence);
|
|
223
|
-
const current = buckets.get(key) ?? { cost: 0, tokens: 0 };
|
|
224
|
-
current.cost += v.cost;
|
|
225
|
-
current.tokens += v.tokens;
|
|
226
|
-
buckets.set(key, current);
|
|
227
|
-
}
|
|
228
|
-
return [...buckets.entries()]
|
|
229
|
-
.sort(([aKey], [bKey]) => (aKey < bKey ? -1 : aKey > bKey ? 1 : 0))
|
|
230
|
-
.slice(-60)
|
|
231
|
-
.map(([key, v]) => ({ label: labelForBucket(key, cadence), cost: v.cost, tokens: v.tokens }));
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export function toMetricSet(a: UsageAggregate, cadence: CadenceType = 'daily'): TokenUsageMetricSet {
|
|
235
|
-
const observedInput = a.inputTokens + a.cacheReadTokens;
|
|
236
|
-
const cachedPercent = observedInput > 0 ? ((a.cacheReadTokens / observedInput) * 100).toFixed(1) : '0.0';
|
|
237
|
-
const unit = cadence === 'weekly' ? 'week' : cadence === 'monthly' ? 'month' : 'day';
|
|
238
|
-
return {
|
|
239
|
-
rawCost: fmtUsd(a.costTotal),
|
|
240
|
-
processedTokens: fmtInt(a.inputTokens + a.outputTokens + a.cacheReadTokens + a.cacheWriteTokens),
|
|
241
|
-
activeRate: `${a.activeDays.size} active ${unit}${a.activeDays.size === 1 ? '' : 's'}`,
|
|
242
|
-
cachedInput: fmtInt(a.cacheReadTokens),
|
|
243
|
-
cachedPercent: `${cachedPercent}% of observed input`,
|
|
244
|
-
uncachedInput: fmtInt(a.inputTokens),
|
|
245
|
-
outputTokens: fmtInt(a.outputTokens),
|
|
246
|
-
reasoning: `includes ${fmtInt(a.reasoningTokens)} reasoning`,
|
|
247
|
-
cacheSavings: fmtUsd(a.costCacheRead),
|
|
248
|
-
scannedTranscripts: a.scannedTranscripts,
|
|
249
|
-
usageRecords: a.usageRecords,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const BREAKDOWN_DOT_COLORS = [
|
|
254
|
-
'bg-[var(--theme-ink)]',
|
|
255
|
-
'bg-[var(--theme-ink)]/70',
|
|
256
|
-
'bg-[var(--theme-ink)]/50',
|
|
257
|
-
'bg-[var(--theme-ink)]/35',
|
|
258
|
-
'bg-[var(--theme-ink)]/20',
|
|
259
|
-
] as const;
|
|
260
|
-
|
|
261
|
-
/** Build breakdown rows (model/day/project) from the aggregate; the `day` tab
|
|
262
|
-
* is bucketed by the cadence (day/week/month) so it stays consistent with the chart. */
|
|
263
|
-
export function toBreakdownRows(
|
|
264
|
-
a: UsageAggregate,
|
|
265
|
-
tab: 'model' | 'day' | 'project',
|
|
266
|
-
cadence: CadenceType = 'daily',
|
|
267
|
-
): BreakdownRow[] {
|
|
268
|
-
if (tab === 'day' && cadence !== 'daily') {
|
|
269
|
-
const buckets = new Map<string, { tokens: number; cost: number }>();
|
|
270
|
-
for (const [dayKey, v] of a.byDay) {
|
|
271
|
-
const key = bucketKey(dayKey, cadence);
|
|
272
|
-
const current = buckets.get(key) ?? { tokens: 0, cost: 0 };
|
|
273
|
-
current.tokens += v.tokens;
|
|
274
|
-
current.cost += v.cost;
|
|
275
|
-
buckets.set(key, current);
|
|
276
|
-
}
|
|
277
|
-
const merged: UsageAggregate = { ...a, byDay: buckets };
|
|
278
|
-
return toBreakdownRows(merged, 'day', 'daily');
|
|
279
|
-
}
|
|
280
|
-
const source = tab === 'model' ? a.byModel : tab === 'day' ? a.byDay : a.byProject;
|
|
281
|
-
return [...source.entries()]
|
|
282
|
-
.map(([name, v]) => ({
|
|
283
|
-
name: tab === 'day' ? labelForBucket(name, cadence) : name,
|
|
284
|
-
tokens: v.tokens,
|
|
285
|
-
cost: v.cost,
|
|
286
|
-
sharePercent: a.costTotal > 0 ? Math.round((v.cost / a.costTotal) * 100) : 0,
|
|
287
|
-
}))
|
|
288
|
-
.sort((x, y) => y.cost - x.cost)
|
|
289
|
-
.slice(0, 12)
|
|
290
|
-
.map((row, index) => ({
|
|
291
|
-
name: row.name,
|
|
292
|
-
tokens: fmtInt(row.tokens),
|
|
293
|
-
cost: fmtUsd(row.cost),
|
|
294
|
-
share: `${row.sharePercent}%`,
|
|
295
|
-
sharePercent: row.sharePercent,
|
|
296
|
-
dotColorClass: BREAKDOWN_DOT_COLORS[index % BREAKDOWN_DOT_COLORS.length],
|
|
297
|
-
}));
|
|
298
|
-
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Presentation shaping over the aggregate built by `./usage`: chart series,
|
|
8
|
+
* metric set, and breakdown rows. Split from the aggregator so the disk-scanning
|
|
9
|
+
* module stays focused (and under the repo's per-file size ceiling).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { BreakdownRow, CadenceType, ChartSeriesPoint, TokenUsageMetricSet } from '@/shared/types';
|
|
13
|
+
import type { UsageAggregate } from '@/server/lib/omp/session/usage/aggregate';
|
|
14
|
+
|
|
15
|
+
const fmtInt = (value: number): string => value.toLocaleString('en-US');
|
|
16
|
+
const fmtUsd = (value: number): string => `$${value.toFixed(2)}`;
|
|
17
|
+
|
|
18
|
+
const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] as const;
|
|
19
|
+
|
|
20
|
+
function dayLabel(dayKey: string): string {
|
|
21
|
+
const date = new Date(`${dayKey}T00:00:00`);
|
|
22
|
+
return Number.isNaN(date.getTime()) ? dayKey : `${MONTH_LABELS[date.getMonth()]} ${date.getDate()}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function bucketKey(dayKey: string, cadence: CadenceType): string {
|
|
26
|
+
if (cadence === 'monthly') return dayKey.slice(0, 7);
|
|
27
|
+
if (cadence === 'weekly') {
|
|
28
|
+
const date = new Date(`${dayKey}T00:00:00`);
|
|
29
|
+
if (Number.isNaN(date.getTime())) return dayKey;
|
|
30
|
+
const monday = new Date(date);
|
|
31
|
+
const shift = (date.getDay() + 6) % 7;
|
|
32
|
+
monday.setDate(date.getDate() - shift);
|
|
33
|
+
return monday.toISOString().slice(0, 10);
|
|
34
|
+
}
|
|
35
|
+
return dayKey;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function labelForBucket(key: string, cadence: CadenceType): string {
|
|
39
|
+
if (cadence === 'monthly') {
|
|
40
|
+
const [year, month] = key.split('-');
|
|
41
|
+
const monthIndex = Number(month) - 1;
|
|
42
|
+
const label = MONTH_LABELS[monthIndex] ?? key;
|
|
43
|
+
return year && Number(year) !== new Date().getFullYear() ? `${label} ${year}` : label;
|
|
44
|
+
}
|
|
45
|
+
return dayLabel(key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Shape the per-day aggregate into an ordered chart series for the cadence:
|
|
49
|
+
* weekly buckets align to Monday, monthly to YYYY-MM, and the series is
|
|
50
|
+
* truncated to the last 60 buckets so the SVG path stays readable. */
|
|
51
|
+
export function buildChartSeries(a: UsageAggregate, cadence: CadenceType): ChartSeriesPoint[] {
|
|
52
|
+
const buckets = new Map<string, { cost: number; tokens: number }>();
|
|
53
|
+
for (const [dayKey, v] of a.byDay) {
|
|
54
|
+
const key = bucketKey(dayKey, cadence);
|
|
55
|
+
const current = buckets.get(key) ?? { cost: 0, tokens: 0 };
|
|
56
|
+
current.cost += v.cost;
|
|
57
|
+
current.tokens += v.tokens;
|
|
58
|
+
buckets.set(key, current);
|
|
59
|
+
}
|
|
60
|
+
return [...buckets.entries()]
|
|
61
|
+
.sort(([aKey], [bKey]) => (aKey < bKey ? -1 : aKey > bKey ? 1 : 0))
|
|
62
|
+
.slice(-60)
|
|
63
|
+
.map(([key, v]) => ({ label: labelForBucket(key, cadence), cost: v.cost, tokens: v.tokens }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function toMetricSet(a: UsageAggregate, cadence: CadenceType = 'daily'): TokenUsageMetricSet {
|
|
67
|
+
const observedInput = a.inputTokens + a.cacheReadTokens;
|
|
68
|
+
const cachedPercent = observedInput > 0 ? ((a.cacheReadTokens / observedInput) * 100).toFixed(1) : '0.0';
|
|
69
|
+
const unit = cadence === 'weekly' ? 'week' : cadence === 'monthly' ? 'month' : 'day';
|
|
70
|
+
return {
|
|
71
|
+
rawCost: fmtUsd(a.costTotal),
|
|
72
|
+
processedTokens: fmtInt(a.inputTokens + a.outputTokens + a.cacheReadTokens + a.cacheWriteTokens),
|
|
73
|
+
activeRate: `${a.activeDays.size} active ${unit}${a.activeDays.size === 1 ? '' : 's'}`,
|
|
74
|
+
cachedInput: fmtInt(a.cacheReadTokens),
|
|
75
|
+
cachedPercent: `${cachedPercent}% of observed input`,
|
|
76
|
+
uncachedInput: fmtInt(a.inputTokens),
|
|
77
|
+
outputTokens: fmtInt(a.outputTokens),
|
|
78
|
+
reasoning: `includes ${fmtInt(a.reasoningTokens)} reasoning`,
|
|
79
|
+
cacheSavings: fmtUsd(a.costCacheRead),
|
|
80
|
+
scannedTranscripts: a.scannedTranscripts,
|
|
81
|
+
usageRecords: a.usageRecords,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const BREAKDOWN_DOT_COLORS = [
|
|
86
|
+
'bg-[var(--theme-ink)]',
|
|
87
|
+
'bg-[var(--theme-ink)]/70',
|
|
88
|
+
'bg-[var(--theme-ink)]/50',
|
|
89
|
+
'bg-[var(--theme-ink)]/35',
|
|
90
|
+
'bg-[var(--theme-ink)]/20',
|
|
91
|
+
] as const;
|
|
92
|
+
|
|
93
|
+
/** Build breakdown rows (model/day/project) from the aggregate; the `day` tab
|
|
94
|
+
* is bucketed by the cadence (day/week/month) so it stays consistent with the chart. */
|
|
95
|
+
export function toBreakdownRows(
|
|
96
|
+
a: UsageAggregate,
|
|
97
|
+
tab: 'model' | 'day' | 'project',
|
|
98
|
+
cadence: CadenceType = 'daily',
|
|
99
|
+
): BreakdownRow[] {
|
|
100
|
+
if (tab === 'day' && cadence !== 'daily') {
|
|
101
|
+
const buckets = new Map<string, { tokens: number; cost: number }>();
|
|
102
|
+
for (const [dayKey, v] of a.byDay) {
|
|
103
|
+
const key = bucketKey(dayKey, cadence);
|
|
104
|
+
const current = buckets.get(key) ?? { tokens: 0, cost: 0 };
|
|
105
|
+
current.tokens += v.tokens;
|
|
106
|
+
current.cost += v.cost;
|
|
107
|
+
buckets.set(key, current);
|
|
108
|
+
}
|
|
109
|
+
const merged: UsageAggregate = { ...a, byDay: buckets };
|
|
110
|
+
return toBreakdownRows(merged, 'day', 'daily');
|
|
111
|
+
}
|
|
112
|
+
const source = tab === 'model' ? a.byModel : tab === 'day' ? a.byDay : a.byProject;
|
|
113
|
+
return [...source.entries()]
|
|
114
|
+
.map(([name, v]) => ({
|
|
115
|
+
name: tab === 'day' ? labelForBucket(name, cadence) : name,
|
|
116
|
+
tokens: v.tokens,
|
|
117
|
+
cost: v.cost,
|
|
118
|
+
sharePercent: a.costTotal > 0 ? Math.round((v.cost / a.costTotal) * 100) : 0,
|
|
119
|
+
}))
|
|
120
|
+
.sort((x, y) => y.cost - x.cost)
|
|
121
|
+
.slice(0, 12)
|
|
122
|
+
.map((row, index) => ({
|
|
123
|
+
name: row.name,
|
|
124
|
+
tokens: fmtInt(row.tokens),
|
|
125
|
+
cost: fmtUsd(row.cost),
|
|
126
|
+
share: `${row.sharePercent}%`,
|
|
127
|
+
sharePercent: row.sharePercent,
|
|
128
|
+
dotColorClass: BREAKDOWN_DOT_COLORS[index % BREAKDOWN_DOT_COLORS.length],
|
|
129
|
+
}));
|
|
130
|
+
}
|