ompchamber 2.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/README.md +31 -11
  2. package/bunfig.toml +11 -0
  3. package/dist/client/index.html +16 -5
  4. package/dist/client/index.js +1393 -0
  5. package/dist/client/static/js/index-8t70jhyj.js +5149 -0
  6. package/dist/client/static/js/index-n4wkqj7t.css +1 -0
  7. package/index.html +54 -0
  8. package/package.json +9 -10
  9. package/public/apple-touch-icon.png +0 -0
  10. package/public/icon-192x192.png +0 -0
  11. package/public/icon.svg +60 -0
  12. package/public/manifest.webmanifest +38 -0
  13. package/src/cli/lib/commands/serve.js +9 -5
  14. package/src/cli/lib/runtime.js +18 -7
  15. package/src/client/components/common/code-editor/handler.test.ts +274 -0
  16. package/src/client/components/common/code-editor/handler.ts +175 -0
  17. package/src/client/components/common/code-editor/index.tsx +185 -220
  18. package/src/client/components/common/code-surface/index.tsx +109 -12
  19. package/src/client/components/common/lazy-panels.tsx +5 -1
  20. package/src/client/components/common/provider-icon/glyphs.ts +118 -0
  21. package/src/client/components/common/provider-icon/index.tsx +85 -0
  22. package/src/client/components/common/viewport.ts +5 -3
  23. package/src/client/components/layout/RightActivityBar.tsx +2 -1
  24. package/src/client/components/layout/desktop-layout/TopNavbar.tsx +21 -4
  25. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
  26. package/src/client/components/layout/desktop-layout/index.tsx +26 -4
  27. package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
  28. package/src/client/components/mobile/RightSidebar.tsx +11 -4
  29. package/src/client/components/mobile/SessionSidebar.tsx +10 -0
  30. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +0 -1
  31. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -1
  32. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +3 -0
  33. package/src/client/components/settings/categories/ProviderSettings.tsx +45 -4
  34. package/src/client/components/settings/categories/UsageSettings.tsx +22 -7
  35. package/src/client/components/settings/categories/provider-settings/AddModelModal.tsx +322 -0
  36. package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +152 -56
  37. package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +52 -1
  38. package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +133 -0
  39. package/src/client/components/settings/categories/provider-settings/DialectFields.tsx +136 -0
  40. package/src/client/components/settings/categories/provider-settings/EffortLadder.tsx +89 -0
  41. package/src/client/components/settings/categories/provider-settings/Header.tsx +2 -2
  42. package/src/client/components/settings/categories/provider-settings/LoginModal.tsx +2 -2
  43. package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +22 -2
  44. package/src/client/components/settings/categories/provider-settings/PresetPicker.tsx +82 -0
  45. package/src/client/components/settings/categories/provider-settings/PriceFields.tsx +107 -0
  46. package/src/client/components/settings/categories/provider-settings/ReconnectModal.tsx +43 -22
  47. package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +2 -2
  48. package/src/client/components/settings/categories/usage-settings/LimitsSection.tsx +115 -0
  49. package/src/client/components/settings/categories/usage-settings/ProviderCard.tsx +19 -26
  50. package/src/client/components/settings/categories/usage-settings/ProviderDetail.tsx +29 -22
  51. package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +60 -48
  52. package/src/client/components/settings/categories/usage-settings/format.ts +36 -0
  53. package/src/client/components/settings/categories/usage-settings/providers.ts +52 -28
  54. package/src/client/components/workspace/browser-panel/AddressBar.tsx +1 -10
  55. package/src/client/components/workspace/browser-panel/Screencast.tsx +4 -5
  56. package/src/client/components/workspace/browser-panel/index.tsx +6 -6
  57. package/src/client/components/workspace/browser-toolbar/index.tsx +22 -12
  58. package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +1 -1
  59. package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +1 -1
  60. package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +1 -1
  61. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Todo.tsx +1 -1
  62. package/src/client/components/workspace/diff-panel/Toolbar.tsx +71 -38
  63. package/src/client/components/workspace/diff-panel/index.tsx +139 -53
  64. package/src/client/components/workspace/editor/CommandPalette.test.ts +242 -0
  65. package/src/client/components/workspace/editor/CommandPalette.tsx +138 -0
  66. package/src/client/components/workspace/editor/FindWidget.test.ts +261 -0
  67. package/src/client/components/workspace/editor/FindWidget.tsx +219 -0
  68. package/src/client/components/workspace/editor/Tabs.tsx +2 -2
  69. package/src/client/components/workspace/editor/Toolbar.tsx +129 -26
  70. package/src/client/components/workspace/editor/index.tsx +180 -62
  71. package/src/client/components/workspace/file-explorer/Modals.tsx +79 -2
  72. package/src/client/components/workspace/file-explorer/TreeItem.tsx +43 -113
  73. package/src/client/components/workspace/model-dropdown/Item.tsx +13 -23
  74. package/src/client/components/workspace/model-dropdown/Panel.tsx +1 -1
  75. package/src/client/components/workspace/model-dropdown/Section.tsx +13 -4
  76. package/src/client/components/workspace/model-dropdown/index.tsx +7 -3
  77. package/src/client/components/workspace/todo-panel/PhaseGroup.tsx +76 -0
  78. package/src/client/components/workspace/todo-panel/Row.tsx +69 -0
  79. package/src/client/components/workspace/todo-panel/index.tsx +161 -0
  80. package/src/client/components/workspace/usage-panel/ProviderSelect.tsx +10 -5
  81. package/src/client/components/workspace/usage-panel/index.tsx +36 -18
  82. package/src/client/data/samples/dialogue-session.ts +1 -1
  83. package/src/client/data/samples/sample-tools/other.ts +1 -1
  84. package/src/client/data/settings/provider.ts +141 -9
  85. package/src/client/hooks/editor/find-harness.test-util.ts +170 -0
  86. package/src/client/hooks/editor/scroll-container.test.ts +102 -0
  87. package/src/client/hooks/editor/scroll-container.ts +26 -0
  88. package/src/client/hooks/editor/use-code-window.ts +2 -20
  89. package/src/client/hooks/editor/use-editor-find-write.test.ts +170 -0
  90. package/src/client/hooks/editor/use-editor-find.test.ts +118 -0
  91. package/src/client/hooks/editor/use-editor-find.ts +321 -0
  92. package/src/client/hooks/editor/use-editor-history.test.ts +216 -0
  93. package/src/client/hooks/editor/use-editor-history.ts +134 -0
  94. package/src/client/hooks/editor/use-file-editor.ts +24 -27
  95. package/src/client/hooks/editor/use-occurrences.test.ts +236 -0
  96. package/src/client/hooks/editor/use-occurrences.ts +98 -0
  97. package/src/client/hooks/models/use-provider-names.ts +1 -1
  98. package/src/client/hooks/settings/providers/actions.ts +337 -0
  99. package/src/client/hooks/settings/providers/api.ts +69 -0
  100. package/src/client/hooks/settings/providers/index.ts +138 -0
  101. package/src/client/hooks/settings/providers/messages.ts +77 -0
  102. package/src/client/hooks/settings/useUsageReport.ts +8 -3
  103. package/src/client/hooks/ui/theme.ts +3 -3
  104. package/src/client/hooks/workspace/file-tabs.ts +62 -11
  105. package/src/client/hooks/workspace/file-tree-actions.test.ts +348 -0
  106. package/src/client/hooks/workspace/file-tree-actions.ts +250 -0
  107. package/src/client/hooks/workspace/session-todos.ts +102 -0
  108. package/src/client/stubs/elkjs.ts +6 -1
  109. package/src/client/tailwind.css +30 -0
  110. package/src/server/index.ts +20 -2
  111. package/src/server/lib/assets/font-css.server.ts +86 -0
  112. package/src/server/lib/assets/fonts.server.ts +117 -0
  113. package/src/server/lib/bundler/css.ts +116 -0
  114. package/src/server/lib/lifecycle/shell-route.ts +17 -0
  115. package/src/server/lib/models/provider-listing.server.ts +257 -0
  116. package/src/server/lib/models/provider-registry.server.test.ts +122 -0
  117. package/src/server/lib/models/provider-registry.server.ts +76 -25
  118. package/src/server/lib/omp/config/models-config.ts +11 -0
  119. package/src/server/lib/omp/config/models-validation.test.ts +137 -0
  120. package/src/server/lib/omp/config/models-validation.ts +111 -2
  121. package/src/server/lib/omp/config/provider-seeds.test.ts +167 -0
  122. package/src/server/lib/omp/config/provider-seeds.ts +176 -0
  123. package/src/server/lib/omp/config/providers.ts +94 -100
  124. package/src/server/lib/omp/core/auth-credentials.ts +45 -0
  125. package/src/server/lib/omp/session/todos.ts +100 -0
  126. package/src/server/lib/omp/session/{usage.ts → usage/aggregate.ts} +1 -118
  127. package/src/server/lib/omp/session/usage/shaping.ts +130 -0
  128. package/src/server/lib/usage/deepseek.ts +4 -5
  129. package/src/server/lib/usage/kenari.ts +7 -6
  130. package/src/server/lib/usage/omp-usage.server.test.ts +126 -0
  131. package/src/server/lib/usage/omp-usage.server.ts +194 -0
  132. package/src/server/lib/usage/provider-key.ts +81 -3
  133. package/src/server/lib/usage/providers.server.test.ts +47 -0
  134. package/src/server/lib/usage/providers.server.ts +208 -0
  135. package/src/server/lib/usage/vendor-balance.server.test.ts +104 -0
  136. package/src/server/lib/usage/vendor-balance.server.ts +284 -0
  137. package/src/server/plugins/shell.server.ts +56 -0
  138. package/src/server/plugins/ssr.ts +24 -36
  139. package/src/server/plugins/static.test.ts +26 -29
  140. package/src/server/plugins/static.ts +40 -19
  141. package/src/server/routes/fs/action-create.test.ts +90 -0
  142. package/src/server/routes/fs/action.ts +23 -0
  143. package/src/server/routes/omp/index.ts +2 -0
  144. package/src/server/routes/omp/session-todos.ts +80 -0
  145. package/src/server/routes/settings/index.ts +2 -0
  146. package/src/server/routes/settings/provider-model.ts +107 -0
  147. package/src/server/routes/settings/provider-models.ts +65 -202
  148. package/src/server/routes/settings/providers.ts +22 -7
  149. package/src/server/routes/settings/usage.ts +125 -45
  150. package/src/server/routes/telemetry/tokens.ts +2 -1
  151. package/src/shared/lib/chat/{todo-parser.ts → todo/parser.ts} +17 -0
  152. package/src/shared/lib/chat/todo/progress.ts +97 -0
  153. package/src/shared/lib/chat/todo/snapshot.test.ts +156 -0
  154. package/src/shared/lib/chat/todo/snapshot.ts +215 -0
  155. package/src/shared/lib/code/editor/commands.test.ts +155 -0
  156. package/src/shared/lib/code/editor/commands.ts +260 -0
  157. package/src/shared/lib/code/editor/edits.test.ts +100 -0
  158. package/src/shared/lib/code/editor/edits.ts +122 -0
  159. package/src/shared/lib/code/editor/find-status.test.ts +50 -0
  160. package/src/shared/lib/code/editor/find-status.ts +36 -0
  161. package/src/shared/lib/code/editor/find.test.ts +203 -0
  162. package/src/shared/lib/code/editor/find.ts +224 -0
  163. package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.test.ts +1 -1
  164. package/src/shared/lib/code/editor/keymap.test.ts +109 -0
  165. package/src/shared/lib/code/editor/keymap.ts +151 -0
  166. package/src/shared/lib/code/editor/layers.test.ts +77 -0
  167. package/src/shared/lib/code/editor/layers.ts +67 -0
  168. package/src/shared/lib/code/editor/line-edits.test.ts +131 -0
  169. package/src/shared/lib/code/editor/line-edits.ts +159 -0
  170. package/src/shared/lib/code/editor/lines.test.ts +81 -0
  171. package/src/shared/lib/code/editor/lines.ts +83 -0
  172. package/src/shared/lib/code/editor/marks.test.ts +86 -0
  173. package/src/shared/lib/code/editor/marks.ts +106 -0
  174. package/src/shared/lib/code/editor/occurrences.test.ts +117 -0
  175. package/src/shared/lib/code/editor/occurrences.ts +111 -0
  176. package/src/shared/lib/code/editor/palette.test.ts +91 -0
  177. package/src/shared/lib/code/editor/palette.ts +92 -0
  178. package/src/shared/lib/code/editor/reset-css.test.ts +35 -0
  179. package/src/shared/lib/code/editor/reset-css.ts +32 -0
  180. package/src/shared/lib/code/editor/text-commands.test.ts +178 -0
  181. package/src/shared/lib/code/editor/text-commands.ts +154 -0
  182. package/src/shared/lib/code/syntax-highlight.test.ts +75 -0
  183. package/src/shared/lib/code/syntax-highlight.ts +29 -17
  184. package/src/shared/lib/code/windowed-highlight.ts +52 -1
  185. package/src/shared/lib/fs/diff-parser.test.ts +116 -0
  186. package/src/shared/lib/fs/diff-parser.ts +86 -33
  187. package/src/shared/lib/models/{provider-connection.ts → provider/connection.ts} +37 -0
  188. package/src/shared/lib/models/provider/dialect.ts +179 -0
  189. package/src/shared/lib/models/provider/glyph.test.ts +116 -0
  190. package/src/shared/lib/models/provider/glyph.ts +200 -0
  191. package/src/shared/lib/models/{provider-models.ts → provider/models.ts} +91 -6
  192. package/src/shared/lib/models/provider/presets.test.ts +172 -0
  193. package/src/shared/lib/models/provider/presets.ts +83 -0
  194. package/src/shared/lib/ui/key-binding.test.ts +115 -0
  195. package/src/shared/lib/ui/key-binding.ts +143 -0
  196. package/src/shared/lib/util/platform.test.ts +87 -0
  197. package/src/shared/lib/util/platform.ts +30 -0
  198. package/src/shared/lib/workspace/file-tab-id.ts +29 -0
  199. package/src/shared/lib/workspace/keymap.test.ts +82 -0
  200. package/src/shared/lib/workspace/keymap.ts +30 -0
  201. package/src/shared/lib/workspace/refresh-cadence.ts +9 -0
  202. package/src/shared/lib/workspace/right-panels.ts +8 -0
  203. package/src/shared/lib/workspace/session-state/store.ts +0 -1
  204. package/src/shared/types/index.ts +1 -0
  205. package/src/shared/types/settings/provider.ts +81 -0
  206. package/src/shared/types/settings/usage.ts +88 -14
  207. package/src/shared/types/todo.ts +108 -0
  208. package/tsconfig.json +3 -1
  209. package/dist/client/static/css/index.5f630421b7.css +0 -2
  210. package/dist/client/static/font/KaTeX_AMS-Regular.a79f1c3119.woff2 +0 -0
  211. package/dist/client/static/font/KaTeX_Caligraphic-Bold.ec17d13264.woff2 +0 -0
  212. package/dist/client/static/font/KaTeX_Caligraphic-Regular.55fac25845.woff2 +0 -0
  213. package/dist/client/static/font/KaTeX_Fraktur-Bold.d42a5579b0.woff2 +0 -0
  214. package/dist/client/static/font/KaTeX_Fraktur-Regular.d3c882a649.woff2 +0 -0
  215. package/dist/client/static/font/KaTeX_Main-Bold.c3fb5ac22f.woff2 +0 -0
  216. package/dist/client/static/font/KaTeX_Main-BoldItalic.6f2bb1dff2.woff2 +0 -0
  217. package/dist/client/static/font/KaTeX_Main-Italic.8916142bec.woff2 +0 -0
  218. package/dist/client/static/font/KaTeX_Main-Regular.0462f03bdf.woff2 +0 -0
  219. package/dist/client/static/font/KaTeX_Math-BoldItalic.572d331f69.woff2 +0 -0
  220. package/dist/client/static/font/KaTeX_Math-Italic.f28c23acad.woff2 +0 -0
  221. package/dist/client/static/font/KaTeX_SansSerif-Bold.8c5b5494b6.woff2 +0 -0
  222. package/dist/client/static/font/KaTeX_SansSerif-Italic.3b1e59b3ba.woff2 +0 -0
  223. package/dist/client/static/font/KaTeX_SansSerif-Regular.ba21ed5f84.woff2 +0 -0
  224. package/dist/client/static/font/KaTeX_Script-Regular.03e9641d6f.woff2 +0 -0
  225. package/dist/client/static/font/KaTeX_Size1-Regular.eae34984b3.woff2 +0 -0
  226. package/dist/client/static/font/KaTeX_Size2-Regular.5916a24fa3.woff2 +0 -0
  227. package/dist/client/static/font/KaTeX_Size4-Regular.10d95fd3a2.woff2 +0 -0
  228. package/dist/client/static/font/KaTeX_Typewriter-Regular.a8709e3622.woff2 +0 -0
  229. package/dist/client/static/font/fira-code-latin-400-normal.2655b10c55.woff2 +0 -0
  230. package/dist/client/static/font/fira-code-latin-500-normal.92a991f204.woff2 +0 -0
  231. package/dist/client/static/font/fira-code-latin-600-normal.43788eb7f1.woff2 +0 -0
  232. package/dist/client/static/font/fira-code-latin-700-normal.2667215d33.woff2 +0 -0
  233. package/dist/client/static/js/77.0d81c74cf0.js +0 -139
  234. package/dist/client/static/js/77.0d81c74cf0.js.LICENSE.txt +0 -8
  235. package/dist/client/static/js/async/0.8e59f2eb5b.js +0 -1
  236. package/dist/client/static/js/async/0k.5852e6b4fa.js +0 -1
  237. package/dist/client/static/js/async/0w.9ed36d5860.js +0 -1
  238. package/dist/client/static/js/async/1.f387c12b63.js +0 -1
  239. package/dist/client/static/js/async/12.d091f91c99.js +0 -1
  240. package/dist/client/static/js/async/13.8f810a8a6c.js +0 -1
  241. package/dist/client/static/js/async/19.6fe909d55a.js +0 -160
  242. package/dist/client/static/js/async/1b.23f1141a38.js +0 -1
  243. package/dist/client/static/js/async/1k.973e8e21fc.js +0 -2
  244. package/dist/client/static/js/async/1k.973e8e21fc.js.LICENSE.txt +0 -11
  245. package/dist/client/static/js/async/1p.0d5cddda96.js +0 -1
  246. package/dist/client/static/js/async/2.b589f1733b.js +0 -1
  247. package/dist/client/static/js/async/2v.96165101de.js +0 -1
  248. package/dist/client/static/js/async/3.0cacc08423.js +0 -1
  249. package/dist/client/static/js/async/37.b28b2f61cd.js +0 -1
  250. package/dist/client/static/js/async/3d.5085fb913f.js +0 -1
  251. package/dist/client/static/js/async/3j.357c80ff38.js +0 -2
  252. package/dist/client/static/js/async/3j.357c80ff38.js.LICENSE.txt +0 -11
  253. package/dist/client/static/js/async/3p.bcf32a0983.js +0 -1
  254. package/dist/client/static/js/async/3r.3a69e98ee0.js +0 -64
  255. package/dist/client/static/js/async/3u.1228460e2a.js +0 -1
  256. package/dist/client/static/js/async/3y.3530b824ad.js +0 -198
  257. package/dist/client/static/js/async/3y.3530b824ad.js.LICENSE.txt +0 -5
  258. package/dist/client/static/js/async/4.9442eeb472.js +0 -1
  259. package/dist/client/static/js/async/4e.678285ca55.js +0 -1
  260. package/dist/client/static/js/async/4k.33a0c7eeb4.js +0 -1
  261. package/dist/client/static/js/async/4m.928a1dde85.js +0 -31
  262. package/dist/client/static/js/async/4m.928a1dde85.js.LICENSE.txt +0 -11
  263. package/dist/client/static/js/async/4q.aad20dab25.js +0 -1
  264. package/dist/client/static/js/async/5.ebaf8b89cc.js +0 -36
  265. package/dist/client/static/js/async/51.95bcae98af.js +0 -2
  266. package/dist/client/static/js/async/51.95bcae98af.js.LICENSE.txt +0 -6
  267. package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
  268. package/dist/client/static/js/async/5j.d6407c4248.js +0 -287
  269. package/dist/client/static/js/async/5o.88316d4460.js +0 -62
  270. package/dist/client/static/js/async/6.f034c9b528.js +0 -1
  271. package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
  272. package/dist/client/static/js/async/6o.5b1a06a382.js +0 -1
  273. package/dist/client/static/js/async/7.e4a9288dd0.js +0 -1
  274. package/dist/client/static/js/async/73.308dff4da8.js +0 -1
  275. package/dist/client/static/js/async/7e.a1cc9bfd88.js +0 -1
  276. package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
  277. package/dist/client/static/js/async/7l.fadbbc0583.js +0 -1
  278. package/dist/client/static/js/async/8.5067439d8c.js +0 -1
  279. package/dist/client/static/js/async/8j.ea110663fb.js +0 -1
  280. package/dist/client/static/js/async/a.bce4542392.js +0 -1
  281. package/dist/client/static/js/async/aa.65d3852576.js +0 -1
  282. package/dist/client/static/js/async/ar.9303e5f2e8.js +0 -2
  283. package/dist/client/static/js/async/ar.9303e5f2e8.js.LICENSE.txt +0 -11
  284. package/dist/client/static/js/async/az.d656062b0f.js +0 -1
  285. package/dist/client/static/js/async/b.b4f6f06515.js +0 -1
  286. package/dist/client/static/js/async/bi.09688ba75c.js +0 -1
  287. package/dist/client/static/js/async/bq.586bc738d1.js +0 -1
  288. package/dist/client/static/js/async/c.797d933882.js +0 -1
  289. package/dist/client/static/js/async/c0.0af1e640d3.js +0 -24
  290. package/dist/client/static/js/async/ca.8a52cfd447.js +0 -1
  291. package/dist/client/static/js/async/cf.d34fff5d67.js +0 -1
  292. package/dist/client/static/js/async/ct.b74928f7a3.js +0 -1
  293. package/dist/client/static/js/async/cx.3cb0242a3a.js +0 -226
  294. package/dist/client/static/js/async/d.a5fc8fe1b3.js +0 -1
  295. package/dist/client/static/js/async/da.f62c70f6c8.js +0 -1
  296. package/dist/client/static/js/async/dp.1725d6432c.js +0 -1
  297. package/dist/client/static/js/async/e.93214d622b.js +0 -79
  298. package/dist/client/static/js/async/f.ab50467719.js +0 -19
  299. package/dist/client/static/js/async/f.ab50467719.js.LICENSE.txt +0 -12
  300. package/dist/client/static/js/async/f7.46971e9d6d.js +0 -2
  301. package/dist/client/static/js/async/f7.46971e9d6d.js.LICENSE.txt +0 -6
  302. package/dist/client/static/js/async/f8.bbf0dbae70.js +0 -1
  303. package/dist/client/static/js/async/g.cb81980498.js +0 -2
  304. package/dist/client/static/js/async/g.cb81980498.js.LICENSE.txt +0 -11
  305. package/dist/client/static/js/async/h.4e790471e4.js +0 -27
  306. package/dist/client/static/js/async/h1.393f066d09.js +0 -1
  307. package/dist/client/static/js/async/h3.216e9798ab.js +0 -148
  308. package/dist/client/static/js/async/hd.0da931b622.js +0 -1
  309. package/dist/client/static/js/async/hk.ba7fb04d41.js +0 -1
  310. package/dist/client/static/js/async/hn.b645041e00.js +0 -1
  311. package/dist/client/static/js/async/ho.475a5e1ae4.js +0 -1
  312. package/dist/client/static/js/async/hp.e9fe2e395a.js +0 -2
  313. package/dist/client/static/js/async/hp.e9fe2e395a.js.LICENSE.txt +0 -17
  314. package/dist/client/static/js/async/hs.e7c59d8090.js +0 -41
  315. package/dist/client/static/js/async/i.7c148e8440.js +0 -1
  316. package/dist/client/static/js/async/id.573df20f29.js +0 -1
  317. package/dist/client/static/js/async/j.ab925882d8.js +0 -1
  318. package/dist/client/static/js/async/jd.63886a1f09.js +0 -1
  319. package/dist/client/static/js/async/jg.d86ebd4013.js +0 -86
  320. package/dist/client/static/js/async/jl.874d352278.js +0 -1
  321. package/dist/client/static/js/async/jll.11313b455a.js +0 -1
  322. package/dist/client/static/js/async/js.67182759ef.js +0 -1
  323. package/dist/client/static/js/async/jsc.23b70d6f40.js +0 -1
  324. package/dist/client/static/js/async/jse.334588ff5d.js +0 -1
  325. package/dist/client/static/js/async/jz.72eb929106.js +0 -77
  326. package/dist/client/static/js/async/k.3210286401.js +0 -217
  327. package/dist/client/static/js/async/ka.c1990ecaa3.js +0 -115
  328. package/dist/client/static/js/async/katex-vendor.de6a87c308.js +0 -1
  329. package/dist/client/static/js/async/ku.29c8a59d0c.js +0 -1
  330. package/dist/client/static/js/async/ky.8dab1dade0.js +0 -1
  331. package/dist/client/static/js/async/l.12dd34e1be.js +0 -1
  332. package/dist/client/static/js/async/l9.949710adaf.js +0 -83
  333. package/dist/client/static/js/async/l9.949710adaf.js.LICENSE.txt +0 -13
  334. package/dist/client/static/js/async/ls.9d9a01026c.js +0 -104
  335. package/dist/client/static/js/async/m.eb2e6752e2.js +0 -28
  336. package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +0 -11
  337. package/dist/client/static/js/async/m3.d2b05f9604.js +0 -2
  338. package/dist/client/static/js/async/m3.d2b05f9604.js.LICENSE.txt +0 -6
  339. package/dist/client/static/js/async/ma.2c261b04dd.js +0 -2
  340. package/dist/client/static/js/async/ma.2c261b04dd.js.LICENSE.txt +0 -6
  341. package/dist/client/static/js/async/me.8ace414f80.js +0 -23
  342. package/dist/client/static/js/async/ms.98960af4eb.js +0 -67
  343. package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +0 -12
  344. package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
  345. package/dist/client/static/js/async/n.a9f1c51701.js +0 -1
  346. package/dist/client/static/js/async/na.a36cd532d6.js +0 -1
  347. package/dist/client/static/js/async/nk.fc7e58325e.js +0 -1
  348. package/dist/client/static/js/async/o.36353dfb88.js +0 -1
  349. package/dist/client/static/js/async/oc.b73042e8bd.js +0 -8
  350. package/dist/client/static/js/async/og.8eb577893a.js +0 -193
  351. package/dist/client/static/js/async/p.48ecfcd14e.js +0 -1
  352. package/dist/client/static/js/async/px.fc6877924b.js +0 -1
  353. package/dist/client/static/js/async/q.22588845a9.js +0 -21
  354. package/dist/client/static/js/async/q.22588845a9.js.LICENSE.txt +0 -6
  355. package/dist/client/static/js/async/q5.97dc1481b6.js +0 -286
  356. package/dist/client/static/js/async/qs.2a9ddb3c72.js +0 -1
  357. package/dist/client/static/js/async/r.51010ad87c.js +0 -1
  358. package/dist/client/static/js/async/rd.cb6bf495bb.js +0 -2
  359. package/dist/client/static/js/async/rd.cb6bf495bb.js.LICENSE.txt +0 -4
  360. package/dist/client/static/js/async/s.0f395cdda5.js +0 -94
  361. package/dist/client/static/js/async/t.4dca0e2ba8.js +0 -1
  362. package/dist/client/static/js/async/t9.d44f57d518.js +0 -1
  363. package/dist/client/static/js/async/ta.973534bbec.js +0 -1
  364. package/dist/client/static/js/async/tr.e878658140.js +0 -309
  365. package/dist/client/static/js/async/u.0871ac5b22.js +0 -1
  366. package/dist/client/static/js/async/ud.3065bfee31.js +0 -2
  367. package/dist/client/static/js/async/ud.3065bfee31.js.LICENSE.txt +0 -4
  368. package/dist/client/static/js/async/up.ea990da0ff.js +0 -32
  369. package/dist/client/static/js/async/uw.8890abbabc.js +0 -1
  370. package/dist/client/static/js/async/v.7d28d3c241.js +0 -34
  371. package/dist/client/static/js/async/vp.9b13bf8610.js +0 -1
  372. package/dist/client/static/js/async/vt.42416b707d.js +0 -1
  373. package/dist/client/static/js/async/w.08ac210f60.js +0 -1
  374. package/dist/client/static/js/async/wt.ad92393a90.js +0 -1
  375. package/dist/client/static/js/async/x.563f72ca6b.js +0 -1
  376. package/dist/client/static/js/async/x0.475516418f.js +0 -1
  377. package/dist/client/static/js/async/x1.da8687159b.js +0 -90
  378. package/dist/client/static/js/async/xe.da8dcc2015.js +0 -2
  379. package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +0 -11
  380. package/dist/client/static/js/async/xl.a3c562a58c.js +0 -1
  381. package/dist/client/static/js/async/xn.6510c0abe2.js +0 -2
  382. package/dist/client/static/js/async/xn.6510c0abe2.js.LICENSE.txt +0 -6
  383. package/dist/client/static/js/async/xn1.83a7784fe1.js +0 -10
  384. package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +0 -11
  385. package/dist/client/static/js/async/xnl.feb0021e50.js +0 -2
  386. package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +0 -12
  387. package/dist/client/static/js/async/y.1a9ed7a5d8.js +0 -1
  388. package/dist/client/static/js/async/y6.43c57defe4.js +0 -1
  389. package/dist/client/static/js/async/yb.8c1342401d.js +0 -1
  390. package/dist/client/static/js/async/ybq.4491f2f2dc.js +0 -1
  391. package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
  392. package/dist/client/static/js/async/yq.917ddd7ba2.js +0 -1
  393. package/dist/client/static/js/async/ys.00687cf82d.js +0 -38
  394. package/dist/client/static/js/async/yv.d99d829136.js +0 -1
  395. package/dist/client/static/js/async/z.56f61a4374.js +0 -1
  396. package/dist/client/static/js/async/zi.5fdb07b86a.js +0 -24
  397. package/dist/client/static/js/async/zj.654f37e8ff.js +0 -1
  398. package/dist/client/static/js/async/zn.966d6f1cc6.js +0 -1
  399. package/dist/client/static/js/async/zo.26213b40c0.js +0 -1
  400. package/dist/client/static/js/async/zx.e9f4105e00.js +0 -1
  401. package/dist/client/static/js/index.e6a257402a.js +0 -38
  402. package/dist/client/static/js/index.e6a257402a.js.LICENSE.txt +0 -4
  403. package/dist/client/static/js/markdown-vendor.71e43278b3.js +0 -144
  404. package/dist/client/static/js/shiki-vendor.cb8452ddcd.js +0 -1
  405. package/src/client/components/settings/categories/provider-settings/Icons.tsx +0 -35
  406. package/src/client/hooks/settings/providers.ts +0 -326
  407. package/src/server/plugins/dev-assets.ts +0 -49
  408. package/src/shared/lib/code/editor-utils.ts +0 -16
  409. package/src/shared/lib/models/provider-presets.ts +0 -29
  410. /package/dist/client/{apple-touch-icon.png → static/apple-touch-icon-dskx5sr3.png} +0 -0
  411. /package/dist/client/{icon-192x192.png → static/icon-192x192-6gn5cr2f.png} +0 -0
  412. /package/dist/client/{icon.svg → static/icon-d07z9dtz.svg} +0 -0
  413. /package/dist/client/{manifest.webmanifest → static/manifest-bnyy75kt.webmanifest} +0 -0
  414. /package/{dist/client → public}/icon-512x512.png +0 -0
  415. /package/{dist/client → public}/icon-maskable-512x512.png +0 -0
  416. /package/{dist/client → public}/sw.js +0 -0
  417. /package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.ts +0 -0
  418. /package/src/shared/lib/models/{provider-cleanup.ts → provider/cleanup.ts} +0 -0
  419. /package/src/shared/lib/models/{provider-label.ts → provider/label.ts} +0 -0
@@ -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 Document, type YAMLMap } from 'yaml';
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, sanitizeModelEntry } from '@/server/lib/omp/config/models-validation';
20
- import { isRecord } from '@/shared/lib/util/guards';
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
- /** One model entry destined for models.yml (cost numbers are USD per 1M tokens). */
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?: 'openai-completions' | 'anthropic-messages' | 'google-generative-ai';
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. Merges preserve unrelated keys, comments and formatting (document
111
- * model), and the write is atomic and mode-preserving.
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
- * Provider creation requires an apiKey — omp rejects models-cfg providers
114
- * without one unless auth is "none" or "oauth"; existing providers keep
115
- * whatever credential they already have. The finished document is validated
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
- const backfillIds = (Array.isArray(existing?.models) ? existing.models : [])
183
- .filter((model): model is Record<string, unknown> => {
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
- if (additions.length === 0 && backfillIds.length === 0) {
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
- apiKey: input.apiKey,
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 if (!existing.api) {
223
- // Existing provider without an api: adding models without one would make
224
- // the whole models.yml fail omp validation, so fill it.
225
- doc.setIn(['providers', slug, 'api'], input.api ?? 'openai-completions');
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 { BreakdownRow, CadenceType, ChartSeriesPoint, TimeRangeType, TokenUsageMetricSet } from '@/shared/types';
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
+ }