ompchamber 2.0.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/README.md +7 -6
  2. package/bunfig.toml +11 -0
  3. package/dist/client/index.html +16 -5
  4. package/dist/client/index.js +1393 -0
  5. package/dist/client/static/js/index-prbmx3yj.js +5123 -0
  6. package/dist/client/static/js/index-ranztes9.css +1 -0
  7. package/index.html +54 -0
  8. package/package.json +9 -10
  9. package/public/apple-touch-icon.png +0 -0
  10. package/public/icon-192x192.png +0 -0
  11. package/public/icon.svg +60 -0
  12. package/public/manifest.webmanifest +38 -0
  13. package/src/cli/lib/commands/serve.js +9 -5
  14. package/src/cli/lib/runtime.js +18 -7
  15. package/src/client/components/common/provider-icon/glyphs.ts +118 -0
  16. package/src/client/components/common/provider-icon/index.tsx +85 -0
  17. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
  18. package/src/client/components/layout/desktop-layout/index.tsx +2 -0
  19. package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
  20. package/src/client/components/mobile/SessionSidebar.tsx +10 -0
  21. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +0 -1
  22. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -1
  23. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +3 -0
  24. package/src/client/components/settings/categories/ProviderSettings.tsx +45 -4
  25. package/src/client/components/settings/categories/UsageSettings.tsx +22 -7
  26. package/src/client/components/settings/categories/provider-settings/AddModelModal.tsx +322 -0
  27. package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +152 -56
  28. package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +52 -1
  29. package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +133 -0
  30. package/src/client/components/settings/categories/provider-settings/DialectFields.tsx +136 -0
  31. package/src/client/components/settings/categories/provider-settings/EffortLadder.tsx +89 -0
  32. package/src/client/components/settings/categories/provider-settings/Header.tsx +2 -2
  33. package/src/client/components/settings/categories/provider-settings/LoginModal.tsx +2 -2
  34. package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +22 -2
  35. package/src/client/components/settings/categories/provider-settings/PresetPicker.tsx +82 -0
  36. package/src/client/components/settings/categories/provider-settings/PriceFields.tsx +107 -0
  37. package/src/client/components/settings/categories/provider-settings/ReconnectModal.tsx +43 -22
  38. package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +2 -2
  39. package/src/client/components/settings/categories/usage-settings/LimitsSection.tsx +115 -0
  40. package/src/client/components/settings/categories/usage-settings/ProviderCard.tsx +19 -26
  41. package/src/client/components/settings/categories/usage-settings/ProviderDetail.tsx +29 -22
  42. package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +60 -48
  43. package/src/client/components/settings/categories/usage-settings/format.ts +36 -0
  44. package/src/client/components/settings/categories/usage-settings/providers.ts +52 -28
  45. package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +1 -1
  46. package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +1 -1
  47. package/src/client/components/workspace/diff-panel/Toolbar.tsx +71 -38
  48. package/src/client/components/workspace/diff-panel/index.tsx +139 -53
  49. package/src/client/components/workspace/editor/Tabs.tsx +2 -2
  50. package/src/client/components/workspace/editor/Toolbar.tsx +111 -26
  51. package/src/client/components/workspace/editor/index.tsx +27 -9
  52. package/src/client/components/workspace/model-dropdown/Item.tsx +13 -23
  53. package/src/client/components/workspace/model-dropdown/Panel.tsx +1 -1
  54. package/src/client/components/workspace/model-dropdown/Section.tsx +13 -4
  55. package/src/client/components/workspace/model-dropdown/index.tsx +7 -3
  56. package/src/client/components/workspace/usage-panel/ProviderSelect.tsx +10 -5
  57. package/src/client/components/workspace/usage-panel/index.tsx +36 -18
  58. package/src/client/data/samples/dialogue-session.ts +1 -1
  59. package/src/client/data/samples/sample-tools/other.ts +1 -1
  60. package/src/client/data/settings/provider.ts +141 -9
  61. package/src/client/hooks/editor/use-file-editor.ts +24 -27
  62. package/src/client/hooks/models/use-provider-names.ts +1 -1
  63. package/src/client/hooks/settings/providers/actions.ts +337 -0
  64. package/src/client/hooks/settings/providers/api.ts +69 -0
  65. package/src/client/hooks/settings/providers/index.ts +138 -0
  66. package/src/client/hooks/settings/providers/messages.ts +77 -0
  67. package/src/client/hooks/settings/useUsageReport.ts +8 -3
  68. package/src/client/hooks/ui/theme.ts +3 -3
  69. package/src/client/hooks/workspace/file-tabs.ts +62 -11
  70. package/src/client/stubs/elkjs.ts +6 -1
  71. package/src/server/index.ts +20 -2
  72. package/src/server/lib/assets/font-css.server.ts +86 -0
  73. package/src/server/lib/assets/fonts.server.ts +117 -0
  74. package/src/server/lib/bundler/css.ts +116 -0
  75. package/src/server/lib/lifecycle/shell-route.ts +17 -0
  76. package/src/server/lib/models/provider-listing.server.ts +257 -0
  77. package/src/server/lib/models/provider-registry.server.test.ts +122 -0
  78. package/src/server/lib/models/provider-registry.server.ts +76 -25
  79. package/src/server/lib/omp/config/models-config.ts +11 -0
  80. package/src/server/lib/omp/config/models-validation.test.ts +137 -0
  81. package/src/server/lib/omp/config/models-validation.ts +111 -2
  82. package/src/server/lib/omp/config/provider-seeds.test.ts +167 -0
  83. package/src/server/lib/omp/config/provider-seeds.ts +176 -0
  84. package/src/server/lib/omp/config/providers.ts +94 -100
  85. package/src/server/lib/omp/core/auth-credentials.ts +45 -0
  86. package/src/server/lib/omp/session/{usage.ts → usage/aggregate.ts} +1 -118
  87. package/src/server/lib/omp/session/usage/shaping.ts +130 -0
  88. package/src/server/lib/usage/deepseek.ts +4 -5
  89. package/src/server/lib/usage/kenari.ts +7 -6
  90. package/src/server/lib/usage/omp-usage.server.test.ts +126 -0
  91. package/src/server/lib/usage/omp-usage.server.ts +194 -0
  92. package/src/server/lib/usage/provider-key.ts +81 -3
  93. package/src/server/lib/usage/providers.server.test.ts +47 -0
  94. package/src/server/lib/usage/providers.server.ts +208 -0
  95. package/src/server/lib/usage/vendor-balance.server.test.ts +104 -0
  96. package/src/server/lib/usage/vendor-balance.server.ts +284 -0
  97. package/src/server/plugins/shell.server.ts +56 -0
  98. package/src/server/plugins/ssr.ts +24 -36
  99. package/src/server/plugins/static.test.ts +26 -29
  100. package/src/server/plugins/static.ts +40 -19
  101. package/src/server/routes/settings/index.ts +2 -0
  102. package/src/server/routes/settings/provider-model.ts +107 -0
  103. package/src/server/routes/settings/provider-models.ts +65 -202
  104. package/src/server/routes/settings/providers.ts +22 -7
  105. package/src/server/routes/settings/usage.ts +125 -45
  106. package/src/server/routes/telemetry/tokens.ts +2 -1
  107. package/src/shared/lib/fs/diff-parser.test.ts +116 -0
  108. package/src/shared/lib/fs/diff-parser.ts +86 -33
  109. package/src/shared/lib/models/{provider-connection.ts → provider/connection.ts} +37 -0
  110. package/src/shared/lib/models/provider/dialect.ts +179 -0
  111. package/src/shared/lib/models/provider/glyph.test.ts +116 -0
  112. package/src/shared/lib/models/provider/glyph.ts +200 -0
  113. package/src/shared/lib/models/{provider-models.ts → provider/models.ts} +91 -6
  114. package/src/shared/lib/models/provider/presets.test.ts +172 -0
  115. package/src/shared/lib/models/provider/presets.ts +83 -0
  116. package/src/shared/lib/workspace/file-tab-id.ts +29 -0
  117. package/src/shared/types/settings/provider.ts +81 -0
  118. package/src/shared/types/settings/usage.ts +88 -14
  119. package/tsconfig.json +3 -1
  120. package/dist/client/static/css/index.5f630421b7.css +0 -2
  121. package/dist/client/static/font/KaTeX_AMS-Regular.a79f1c3119.woff2 +0 -0
  122. package/dist/client/static/font/KaTeX_Caligraphic-Bold.ec17d13264.woff2 +0 -0
  123. package/dist/client/static/font/KaTeX_Caligraphic-Regular.55fac25845.woff2 +0 -0
  124. package/dist/client/static/font/KaTeX_Fraktur-Bold.d42a5579b0.woff2 +0 -0
  125. package/dist/client/static/font/KaTeX_Fraktur-Regular.d3c882a649.woff2 +0 -0
  126. package/dist/client/static/font/KaTeX_Main-Bold.c3fb5ac22f.woff2 +0 -0
  127. package/dist/client/static/font/KaTeX_Main-BoldItalic.6f2bb1dff2.woff2 +0 -0
  128. package/dist/client/static/font/KaTeX_Main-Italic.8916142bec.woff2 +0 -0
  129. package/dist/client/static/font/KaTeX_Main-Regular.0462f03bdf.woff2 +0 -0
  130. package/dist/client/static/font/KaTeX_Math-BoldItalic.572d331f69.woff2 +0 -0
  131. package/dist/client/static/font/KaTeX_Math-Italic.f28c23acad.woff2 +0 -0
  132. package/dist/client/static/font/KaTeX_SansSerif-Bold.8c5b5494b6.woff2 +0 -0
  133. package/dist/client/static/font/KaTeX_SansSerif-Italic.3b1e59b3ba.woff2 +0 -0
  134. package/dist/client/static/font/KaTeX_SansSerif-Regular.ba21ed5f84.woff2 +0 -0
  135. package/dist/client/static/font/KaTeX_Script-Regular.03e9641d6f.woff2 +0 -0
  136. package/dist/client/static/font/KaTeX_Size1-Regular.eae34984b3.woff2 +0 -0
  137. package/dist/client/static/font/KaTeX_Size2-Regular.5916a24fa3.woff2 +0 -0
  138. package/dist/client/static/font/KaTeX_Size4-Regular.10d95fd3a2.woff2 +0 -0
  139. package/dist/client/static/font/KaTeX_Typewriter-Regular.a8709e3622.woff2 +0 -0
  140. package/dist/client/static/font/fira-code-latin-400-normal.2655b10c55.woff2 +0 -0
  141. package/dist/client/static/font/fira-code-latin-500-normal.92a991f204.woff2 +0 -0
  142. package/dist/client/static/font/fira-code-latin-600-normal.43788eb7f1.woff2 +0 -0
  143. package/dist/client/static/font/fira-code-latin-700-normal.2667215d33.woff2 +0 -0
  144. package/dist/client/static/js/77.0d81c74cf0.js +0 -139
  145. package/dist/client/static/js/77.0d81c74cf0.js.LICENSE.txt +0 -8
  146. package/dist/client/static/js/async/0.8e59f2eb5b.js +0 -1
  147. package/dist/client/static/js/async/0k.5852e6b4fa.js +0 -1
  148. package/dist/client/static/js/async/0w.9ed36d5860.js +0 -1
  149. package/dist/client/static/js/async/1.f387c12b63.js +0 -1
  150. package/dist/client/static/js/async/12.d091f91c99.js +0 -1
  151. package/dist/client/static/js/async/13.8f810a8a6c.js +0 -1
  152. package/dist/client/static/js/async/19.6fe909d55a.js +0 -160
  153. package/dist/client/static/js/async/1b.23f1141a38.js +0 -1
  154. package/dist/client/static/js/async/1k.973e8e21fc.js +0 -2
  155. package/dist/client/static/js/async/1k.973e8e21fc.js.LICENSE.txt +0 -11
  156. package/dist/client/static/js/async/1p.0d5cddda96.js +0 -1
  157. package/dist/client/static/js/async/2.b589f1733b.js +0 -1
  158. package/dist/client/static/js/async/2v.96165101de.js +0 -1
  159. package/dist/client/static/js/async/3.0cacc08423.js +0 -1
  160. package/dist/client/static/js/async/37.b28b2f61cd.js +0 -1
  161. package/dist/client/static/js/async/3d.5085fb913f.js +0 -1
  162. package/dist/client/static/js/async/3j.357c80ff38.js +0 -2
  163. package/dist/client/static/js/async/3j.357c80ff38.js.LICENSE.txt +0 -11
  164. package/dist/client/static/js/async/3p.bcf32a0983.js +0 -1
  165. package/dist/client/static/js/async/3r.3a69e98ee0.js +0 -64
  166. package/dist/client/static/js/async/3u.1228460e2a.js +0 -1
  167. package/dist/client/static/js/async/3y.3530b824ad.js +0 -198
  168. package/dist/client/static/js/async/3y.3530b824ad.js.LICENSE.txt +0 -5
  169. package/dist/client/static/js/async/4.9442eeb472.js +0 -1
  170. package/dist/client/static/js/async/4e.678285ca55.js +0 -1
  171. package/dist/client/static/js/async/4k.33a0c7eeb4.js +0 -1
  172. package/dist/client/static/js/async/4m.928a1dde85.js +0 -31
  173. package/dist/client/static/js/async/4m.928a1dde85.js.LICENSE.txt +0 -11
  174. package/dist/client/static/js/async/4q.aad20dab25.js +0 -1
  175. package/dist/client/static/js/async/5.ebaf8b89cc.js +0 -36
  176. package/dist/client/static/js/async/51.95bcae98af.js +0 -2
  177. package/dist/client/static/js/async/51.95bcae98af.js.LICENSE.txt +0 -6
  178. package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
  179. package/dist/client/static/js/async/5j.d6407c4248.js +0 -287
  180. package/dist/client/static/js/async/5o.88316d4460.js +0 -62
  181. package/dist/client/static/js/async/6.f034c9b528.js +0 -1
  182. package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
  183. package/dist/client/static/js/async/6o.5b1a06a382.js +0 -1
  184. package/dist/client/static/js/async/7.e4a9288dd0.js +0 -1
  185. package/dist/client/static/js/async/73.308dff4da8.js +0 -1
  186. package/dist/client/static/js/async/7e.a1cc9bfd88.js +0 -1
  187. package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
  188. package/dist/client/static/js/async/7l.fadbbc0583.js +0 -1
  189. package/dist/client/static/js/async/8.5067439d8c.js +0 -1
  190. package/dist/client/static/js/async/8j.ea110663fb.js +0 -1
  191. package/dist/client/static/js/async/a.bce4542392.js +0 -1
  192. package/dist/client/static/js/async/aa.65d3852576.js +0 -1
  193. package/dist/client/static/js/async/ar.9303e5f2e8.js +0 -2
  194. package/dist/client/static/js/async/ar.9303e5f2e8.js.LICENSE.txt +0 -11
  195. package/dist/client/static/js/async/az.d656062b0f.js +0 -1
  196. package/dist/client/static/js/async/b.b4f6f06515.js +0 -1
  197. package/dist/client/static/js/async/bi.09688ba75c.js +0 -1
  198. package/dist/client/static/js/async/bq.586bc738d1.js +0 -1
  199. package/dist/client/static/js/async/c.797d933882.js +0 -1
  200. package/dist/client/static/js/async/c0.0af1e640d3.js +0 -24
  201. package/dist/client/static/js/async/ca.8a52cfd447.js +0 -1
  202. package/dist/client/static/js/async/cf.d34fff5d67.js +0 -1
  203. package/dist/client/static/js/async/ct.b74928f7a3.js +0 -1
  204. package/dist/client/static/js/async/cx.3cb0242a3a.js +0 -226
  205. package/dist/client/static/js/async/d.a5fc8fe1b3.js +0 -1
  206. package/dist/client/static/js/async/da.f62c70f6c8.js +0 -1
  207. package/dist/client/static/js/async/dp.1725d6432c.js +0 -1
  208. package/dist/client/static/js/async/e.93214d622b.js +0 -79
  209. package/dist/client/static/js/async/f.ab50467719.js +0 -19
  210. package/dist/client/static/js/async/f.ab50467719.js.LICENSE.txt +0 -12
  211. package/dist/client/static/js/async/f7.46971e9d6d.js +0 -2
  212. package/dist/client/static/js/async/f7.46971e9d6d.js.LICENSE.txt +0 -6
  213. package/dist/client/static/js/async/f8.bbf0dbae70.js +0 -1
  214. package/dist/client/static/js/async/g.cb81980498.js +0 -2
  215. package/dist/client/static/js/async/g.cb81980498.js.LICENSE.txt +0 -11
  216. package/dist/client/static/js/async/h.4e790471e4.js +0 -27
  217. package/dist/client/static/js/async/h1.393f066d09.js +0 -1
  218. package/dist/client/static/js/async/h3.216e9798ab.js +0 -148
  219. package/dist/client/static/js/async/hd.0da931b622.js +0 -1
  220. package/dist/client/static/js/async/hk.ba7fb04d41.js +0 -1
  221. package/dist/client/static/js/async/hn.b645041e00.js +0 -1
  222. package/dist/client/static/js/async/ho.475a5e1ae4.js +0 -1
  223. package/dist/client/static/js/async/hp.e9fe2e395a.js +0 -2
  224. package/dist/client/static/js/async/hp.e9fe2e395a.js.LICENSE.txt +0 -17
  225. package/dist/client/static/js/async/hs.e7c59d8090.js +0 -41
  226. package/dist/client/static/js/async/i.7c148e8440.js +0 -1
  227. package/dist/client/static/js/async/id.573df20f29.js +0 -1
  228. package/dist/client/static/js/async/j.ab925882d8.js +0 -1
  229. package/dist/client/static/js/async/jd.63886a1f09.js +0 -1
  230. package/dist/client/static/js/async/jg.d86ebd4013.js +0 -86
  231. package/dist/client/static/js/async/jl.874d352278.js +0 -1
  232. package/dist/client/static/js/async/jll.11313b455a.js +0 -1
  233. package/dist/client/static/js/async/js.67182759ef.js +0 -1
  234. package/dist/client/static/js/async/jsc.23b70d6f40.js +0 -1
  235. package/dist/client/static/js/async/jse.334588ff5d.js +0 -1
  236. package/dist/client/static/js/async/jz.72eb929106.js +0 -77
  237. package/dist/client/static/js/async/k.3210286401.js +0 -217
  238. package/dist/client/static/js/async/ka.c1990ecaa3.js +0 -115
  239. package/dist/client/static/js/async/katex-vendor.de6a87c308.js +0 -1
  240. package/dist/client/static/js/async/ku.29c8a59d0c.js +0 -1
  241. package/dist/client/static/js/async/ky.8dab1dade0.js +0 -1
  242. package/dist/client/static/js/async/l.12dd34e1be.js +0 -1
  243. package/dist/client/static/js/async/l9.949710adaf.js +0 -83
  244. package/dist/client/static/js/async/l9.949710adaf.js.LICENSE.txt +0 -13
  245. package/dist/client/static/js/async/ls.9d9a01026c.js +0 -104
  246. package/dist/client/static/js/async/m.eb2e6752e2.js +0 -28
  247. package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +0 -11
  248. package/dist/client/static/js/async/m3.d2b05f9604.js +0 -2
  249. package/dist/client/static/js/async/m3.d2b05f9604.js.LICENSE.txt +0 -6
  250. package/dist/client/static/js/async/ma.2c261b04dd.js +0 -2
  251. package/dist/client/static/js/async/ma.2c261b04dd.js.LICENSE.txt +0 -6
  252. package/dist/client/static/js/async/me.8ace414f80.js +0 -23
  253. package/dist/client/static/js/async/ms.98960af4eb.js +0 -67
  254. package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +0 -12
  255. package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
  256. package/dist/client/static/js/async/n.a9f1c51701.js +0 -1
  257. package/dist/client/static/js/async/na.a36cd532d6.js +0 -1
  258. package/dist/client/static/js/async/nk.fc7e58325e.js +0 -1
  259. package/dist/client/static/js/async/o.36353dfb88.js +0 -1
  260. package/dist/client/static/js/async/oc.b73042e8bd.js +0 -8
  261. package/dist/client/static/js/async/og.8eb577893a.js +0 -193
  262. package/dist/client/static/js/async/p.48ecfcd14e.js +0 -1
  263. package/dist/client/static/js/async/px.fc6877924b.js +0 -1
  264. package/dist/client/static/js/async/q.22588845a9.js +0 -21
  265. package/dist/client/static/js/async/q.22588845a9.js.LICENSE.txt +0 -6
  266. package/dist/client/static/js/async/q5.97dc1481b6.js +0 -286
  267. package/dist/client/static/js/async/qs.2a9ddb3c72.js +0 -1
  268. package/dist/client/static/js/async/r.51010ad87c.js +0 -1
  269. package/dist/client/static/js/async/rd.cb6bf495bb.js +0 -2
  270. package/dist/client/static/js/async/rd.cb6bf495bb.js.LICENSE.txt +0 -4
  271. package/dist/client/static/js/async/s.0f395cdda5.js +0 -94
  272. package/dist/client/static/js/async/t.4dca0e2ba8.js +0 -1
  273. package/dist/client/static/js/async/t9.d44f57d518.js +0 -1
  274. package/dist/client/static/js/async/ta.973534bbec.js +0 -1
  275. package/dist/client/static/js/async/tr.e878658140.js +0 -309
  276. package/dist/client/static/js/async/u.0871ac5b22.js +0 -1
  277. package/dist/client/static/js/async/ud.3065bfee31.js +0 -2
  278. package/dist/client/static/js/async/ud.3065bfee31.js.LICENSE.txt +0 -4
  279. package/dist/client/static/js/async/up.ea990da0ff.js +0 -32
  280. package/dist/client/static/js/async/uw.8890abbabc.js +0 -1
  281. package/dist/client/static/js/async/v.7d28d3c241.js +0 -34
  282. package/dist/client/static/js/async/vp.9b13bf8610.js +0 -1
  283. package/dist/client/static/js/async/vt.42416b707d.js +0 -1
  284. package/dist/client/static/js/async/w.08ac210f60.js +0 -1
  285. package/dist/client/static/js/async/wt.ad92393a90.js +0 -1
  286. package/dist/client/static/js/async/x.563f72ca6b.js +0 -1
  287. package/dist/client/static/js/async/x0.475516418f.js +0 -1
  288. package/dist/client/static/js/async/x1.da8687159b.js +0 -90
  289. package/dist/client/static/js/async/xe.da8dcc2015.js +0 -2
  290. package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +0 -11
  291. package/dist/client/static/js/async/xl.a3c562a58c.js +0 -1
  292. package/dist/client/static/js/async/xn.6510c0abe2.js +0 -2
  293. package/dist/client/static/js/async/xn.6510c0abe2.js.LICENSE.txt +0 -6
  294. package/dist/client/static/js/async/xn1.83a7784fe1.js +0 -10
  295. package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +0 -11
  296. package/dist/client/static/js/async/xnl.feb0021e50.js +0 -2
  297. package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +0 -12
  298. package/dist/client/static/js/async/y.1a9ed7a5d8.js +0 -1
  299. package/dist/client/static/js/async/y6.43c57defe4.js +0 -1
  300. package/dist/client/static/js/async/yb.8c1342401d.js +0 -1
  301. package/dist/client/static/js/async/ybq.4491f2f2dc.js +0 -1
  302. package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
  303. package/dist/client/static/js/async/yq.917ddd7ba2.js +0 -1
  304. package/dist/client/static/js/async/ys.00687cf82d.js +0 -38
  305. package/dist/client/static/js/async/yv.d99d829136.js +0 -1
  306. package/dist/client/static/js/async/z.56f61a4374.js +0 -1
  307. package/dist/client/static/js/async/zi.5fdb07b86a.js +0 -24
  308. package/dist/client/static/js/async/zj.654f37e8ff.js +0 -1
  309. package/dist/client/static/js/async/zn.966d6f1cc6.js +0 -1
  310. package/dist/client/static/js/async/zo.26213b40c0.js +0 -1
  311. package/dist/client/static/js/async/zx.e9f4105e00.js +0 -1
  312. package/dist/client/static/js/index.e6a257402a.js +0 -38
  313. package/dist/client/static/js/index.e6a257402a.js.LICENSE.txt +0 -4
  314. package/dist/client/static/js/markdown-vendor.71e43278b3.js +0 -144
  315. package/dist/client/static/js/shiki-vendor.cb8452ddcd.js +0 -1
  316. package/src/client/components/settings/categories/provider-settings/Icons.tsx +0 -35
  317. package/src/client/hooks/settings/providers.ts +0 -326
  318. package/src/server/plugins/dev-assets.ts +0 -49
  319. package/src/shared/lib/code/editor-utils.ts +0 -16
  320. package/src/shared/lib/models/provider-presets.ts +0 -29
  321. /package/dist/client/{apple-touch-icon.png → static/apple-touch-icon-dskx5sr3.png} +0 -0
  322. /package/dist/client/{icon-192x192.png → static/icon-192x192-6gn5cr2f.png} +0 -0
  323. /package/dist/client/{icon.svg → static/icon-d07z9dtz.svg} +0 -0
  324. /package/dist/client/{manifest.webmanifest → static/manifest-bnyy75kt.webmanifest} +0 -0
  325. /package/{dist/client → public}/icon-512x512.png +0 -0
  326. /package/{dist/client → public}/icon-maskable-512x512.png +0 -0
  327. /package/{dist/client → public}/sw.js +0 -0
  328. /package/src/shared/lib/models/{provider-cleanup.ts → provider/cleanup.ts} +0 -0
  329. /package/src/shared/lib/models/{provider-label.ts → provider/label.ts} +0 -0
@@ -1,13 +1,20 @@
1
- import { Check, Copy, Download, Eye, EyeOff, Maximize, Save, WrapText, ZoomIn, ZoomOut } from 'lucide-preact';
1
+ import { AlertCircle, Check, Copy, Download, Eye, EyeOff, Maximize, Minimize, Save, WrapText, ZoomIn, ZoomOut } from 'lucide-preact';
2
+
3
+ export type EditorSaveStatus = 'idle' | 'saving' | 'saved' | 'error';
2
4
 
3
5
  interface EditorToolbarProps {
4
6
  path: string;
5
- saveStatus: 'idle' | 'saving' | 'saved';
7
+ saveStatus: EditorSaveStatus;
6
8
  wordWrap: boolean;
7
9
  isMd: boolean;
8
10
  isPreview: boolean;
11
+ /** True for the 1.5s after a copy landed, so the button can confirm it. */
12
+ copied: boolean;
9
13
  /** Raster image: no text to save or wrap, and zoom belongs to the picture. */
10
14
  isImage?: boolean;
15
+ /** A read failure left no trustworthy buffer, so there is nothing to save. */
16
+ saveDisabled?: boolean;
17
+ isMaximized?: boolean;
11
18
  onSave: () => void;
12
19
  onTogglePreview: () => void;
13
20
  onToggleWordWrap: () => void;
@@ -18,13 +25,25 @@ interface EditorToolbarProps {
18
25
  onToggleMaximize: () => void;
19
26
  }
20
27
 
28
+ /**
29
+ * Shared icon-button shell. Every action here is icon-only, so each one carries
30
+ * a `title` (the tooltip) and an `aria-label` (the accessible name) — a bare
31
+ * `<svg onClick>` is unreachable by keyboard and unlabeled to a screen reader.
32
+ * `p-1.5` also gives the 14px glyph a ~26px hit target.
33
+ */
34
+ const ACTION_CLASS =
35
+ 'flex items-center justify-center p-1.5 rounded transition-colors cursor-pointer disabled:cursor-default disabled:opacity-50';
36
+
21
37
  export function EditorToolbar({
22
38
  path,
23
39
  saveStatus,
24
40
  wordWrap,
25
41
  isMd,
26
42
  isPreview,
43
+ copied,
27
44
  isImage = false,
45
+ saveDisabled = false,
46
+ isMaximized = false,
28
47
  onSave,
29
48
  onTogglePreview,
30
49
  onToggleWordWrap,
@@ -34,65 +53,131 @@ export function EditorToolbar({
34
53
  onDownload,
35
54
  onToggleMaximize,
36
55
  }: EditorToolbarProps) {
56
+ const saveTitle =
57
+ saveDisabled
58
+ ? 'Nothing to save — this file could not be read'
59
+ : saveStatus === 'saving'
60
+ ? 'Saving…'
61
+ : saveStatus === 'error'
62
+ ? 'Save failed — the file on disk was not changed'
63
+ : 'Save file (Ctrl+S)';
64
+
37
65
  return (
38
- <div className="flex items-center justify-between px-3 py-2 border-b border-ink/10 bg-paper">
66
+ <div className="flex items-center justify-between px-3 py-2 border-b border-ink/10 bg-paper flex-shrink-0">
39
67
  {/* Left status */}
40
- <div className="flex items-center space-x-2 text-xs font-mono text-ink/40">
68
+ <div className="flex items-center space-x-2 text-xs font-mono text-ink/40 min-w-0">
41
69
  <span className="truncate max-w-[300px]" title={path}>{path}</span>
42
70
  </div>
43
71
 
44
72
  {/* Right actions */}
45
- <div className="flex items-center space-x-3 text-ink/40">
73
+ <div className="flex items-center space-x-1 text-ink/40 flex-shrink-0">
46
74
  {!isImage && (
47
- <div className="flex items-center pr-3 border-r border-ink/10">
75
+ <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
48
76
  <button
49
77
  type="button"
50
78
  onClick={onSave}
51
- className="flex items-center justify-center rounded hover:bg-ink/5 transition-colors"
52
- title="Save file (Ctrl+S)"
79
+ disabled={saveDisabled || saveStatus === 'saving'}
80
+ className={`${ACTION_CLASS} hover:bg-ink/5`}
81
+ title={saveTitle}
82
+ aria-label={saveTitle}
53
83
  >
54
84
  {saveStatus === 'saving' ? (
55
85
  <Save size={14} className="text-amber-500 animate-pulse" />
56
86
  ) : saveStatus === 'saved' ? (
57
87
  <Check size={14} className="text-success" />
88
+ ) : saveStatus === 'error' ? (
89
+ <AlertCircle size={14} className="text-error" />
58
90
  ) : (
59
- <Save size={14} className="hover:text-ink cursor-pointer" />
91
+ <Save size={14} className="hover:text-ink" />
60
92
  )}
61
93
  </button>
62
94
  </div>
63
95
  )}
64
96
 
65
97
  {isMd && (
66
- <div onClick={onTogglePreview} className="flex items-center pr-3 border-r border-ink/10">
67
- {isPreview ? (
68
- <EyeOff size={14} className="hover:text-ink cursor-pointer" />
69
- ) : (
70
- <Eye size={14} className="hover:text-ink cursor-pointer" />
71
- )}
98
+ <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
99
+ <button
100
+ type="button"
101
+ onClick={onTogglePreview}
102
+ className={`${ACTION_CLASS} hover:bg-ink/5 ${isPreview ? 'text-ink' : 'hover:text-ink'}`}
103
+ title={isPreview ? 'Show markdown source' : 'Preview markdown'}
104
+ aria-label={isPreview ? 'Show markdown source' : 'Preview markdown'}
105
+ aria-pressed={isPreview}
106
+ >
107
+ {isPreview ? <EyeOff size={14} /> : <Eye size={14} />}
108
+ </button>
72
109
  </div>
73
110
  )}
74
111
 
75
112
  {!isImage && (
76
113
  <>
77
- <div className="flex items-center space-x-2 pr-3 border-r border-ink/10">
78
- <WrapText size={14} className={`cursor-pointer ${wordWrap ? 'text-ink' : 'hover:text-ink'}`} onClick={onToggleWordWrap} />
114
+ <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
115
+ <button
116
+ type="button"
117
+ onClick={onToggleWordWrap}
118
+ className={`${ACTION_CLASS} hover:bg-ink/5 ${wordWrap ? 'text-ink' : 'hover:text-ink'}`}
119
+ title={wordWrap ? 'Word wrap on' : 'Word wrap off'}
120
+ aria-label={wordWrap ? 'Word wrap on' : 'Word wrap off'}
121
+ aria-pressed={wordWrap}
122
+ >
123
+ <WrapText size={14} />
124
+ </button>
79
125
  </div>
80
126
 
81
- <div className="flex items-center space-x-2 pr-3 border-r border-ink/10">
82
- <ZoomOut size={14} className="hover:text-ink cursor-pointer" onClick={onZoomOut} />
83
- <ZoomIn size={14} className="hover:text-ink cursor-pointer" onClick={onZoomIn} />
127
+ <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
128
+ <button
129
+ type="button"
130
+ onClick={onZoomOut}
131
+ className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
132
+ title="Decrease font size"
133
+ aria-label="Decrease font size"
134
+ >
135
+ <ZoomOut size={14} />
136
+ </button>
137
+ <button
138
+ type="button"
139
+ onClick={onZoomIn}
140
+ className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
141
+ title="Increase font size"
142
+ aria-label="Increase font size"
143
+ >
144
+ <ZoomIn size={14} />
145
+ </button>
84
146
  </div>
85
147
  </>
86
148
  )}
87
149
 
88
- <div className="flex items-center space-x-2 pr-3 border-r border-ink/10">
89
- <Copy size={14} className="hover:text-ink cursor-pointer" onClick={onCopy} />
90
- <Download size={14} className="hover:text-ink cursor-pointer" onClick={onDownload} />
150
+ <div className="flex items-center pr-2 mr-1 border-r border-ink/10">
151
+ <button
152
+ type="button"
153
+ onClick={onCopy}
154
+ className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
155
+ title={copied ? 'Copied' : isImage ? 'Copy image' : 'Copy content'}
156
+ aria-label={copied ? 'Copied' : isImage ? 'Copy image' : 'Copy content'}
157
+ >
158
+ {copied ? <Check size={14} className="text-success" /> : <Copy size={14} />}
159
+ </button>
160
+ <button
161
+ type="button"
162
+ onClick={onDownload}
163
+ className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink`}
164
+ title="Download file"
165
+ aria-label="Download file"
166
+ >
167
+ <Download size={14} />
168
+ </button>
91
169
  </div>
92
170
 
93
- <div className="flex items-center pl-1">
94
- <Maximize size={14} className="hover:text-ink cursor-pointer" onClick={onToggleMaximize} />
95
- </div>
171
+ <button
172
+ type="button"
173
+ onClick={onToggleMaximize}
174
+ className={`${ACTION_CLASS} hover:bg-ink/5 hover:text-ink ${isMaximized ? 'text-ink' : ''}`}
175
+ title={isMaximized ? 'Exit full screen' : 'Full screen'}
176
+ aria-label={isMaximized ? 'Exit full screen' : 'Full screen'}
177
+ aria-pressed={isMaximized}
178
+ >
179
+ {isMaximized ? <Minimize size={14} /> : <Maximize size={14} />}
180
+ </button>
96
181
  </div>
97
182
  </div>
98
183
  );
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useState } from 'preact/hooks';
2
+ import { AlertTriangle, Loader2 } from 'lucide-preact';
2
3
  import { FileIcon } from '@/client/components/common/file-icon';
3
4
  import { CodeSurface } from '@/client/components/common/code-surface';
4
5
  import { MarkdownRenderer } from '@/client/components/common/MarkdownRenderer';
@@ -6,7 +7,6 @@ import { useScrollbarFade, scrollbarFadeClass } from '@/client/hooks/ui/scrollba
6
7
  import { EditorTabs } from '@/client/components/workspace/editor/Tabs';
7
8
  import { EditorToolbar } from '@/client/components/workspace/editor/Toolbar';
8
9
  import { ImageViewer } from '@/client/components/common/image-viewer';
9
- import { getDefaultContent } from '@/shared/lib/code/editor-utils';
10
10
  import { DiffPanel } from '@/client/components/workspace/diff-panel';
11
11
  import { useSessionState } from '@/client/hooks/workspace/session-state';
12
12
  import { useSessionStateContext } from '@/client/hooks/workspace/session-state/context';
@@ -19,6 +19,8 @@ interface EditorProps {
19
19
  activeFileId: number | string | null;
20
20
  onSelectFile: (id: number | string) => void;
21
21
  onCloseFile: (id: number | string) => void;
22
+ /** Replace a diff tab with the plain editor tab for the same file. */
23
+ onConvertDiffToEditor: (id: number | string) => void;
22
24
  refreshKey?: number;
23
25
  onFileSaved?: () => void;
24
26
  }
@@ -29,6 +31,7 @@ export function Editor({
29
31
  activeFileId,
30
32
  onSelectFile,
31
33
  onCloseFile,
34
+ onConvertDiffToEditor,
32
35
  refreshKey = 0,
33
36
  onFileSaved
34
37
  }: EditorProps) {
@@ -42,7 +45,10 @@ export function Editor({
42
45
  const { isScrolling, handleScroll } = useScrollbarFade();
43
46
 
44
47
  const editor = useFileEditor(activeFile ?? null, {
45
- fallback: getDefaultContent,
48
+ // A file that could not be read must report the failure, not substitute
49
+ // placeholder text — the mock sample used to be written straight over the
50
+ // real file by the next save.
51
+ reportSaveError: true,
46
52
  onFileSaved,
47
53
  });
48
54
 
@@ -96,7 +102,6 @@ export function Editor({
96
102
  const currentContent = editor.content;
97
103
  const isMd = activeFile?.name.endsWith('.md');
98
104
  const isPreview = activeFile ? previewMode[activeFile.id] : false;
99
- const toolbarSaveStatus = editor.saveStatus === 'error' ? 'idle' : editor.saveStatus;
100
105
 
101
106
  const editorContainerClass = isMaximized
102
107
  ? 'fixed inset-0 z-50 flex flex-col bg-canvas'
@@ -119,11 +124,7 @@ export function Editor({
119
124
  isStaged={Boolean(activeFile.diffStaged)}
120
125
  root={activeFile.root}
121
126
  repo={activeFile.repo || '.'}
122
- onOpenInEditor={() => {
123
- // Convert diff tab to regular editor tab
124
- activeFile.isDiff = false;
125
- onSelectFile(activeFile.id);
126
- }}
127
+ onOpenInEditor={() => onConvertDiffToEditor(activeFile.id)}
127
128
  onFileSaved={onFileSaved}
128
129
  className="flex-1"
129
130
  />
@@ -131,11 +132,14 @@ export function Editor({
131
132
  <>
132
133
  <EditorToolbar
133
134
  path={activeFile.path}
134
- saveStatus={toolbarSaveStatus}
135
+ saveStatus={editor.saveStatus}
135
136
  wordWrap={wordWrap}
136
137
  isMd={isMd}
137
138
  isPreview={isPreview}
139
+ copied={editor.copied}
138
140
  isImage={editor.isImage}
141
+ saveDisabled={Boolean(editor.loadError)}
142
+ isMaximized={isMaximized}
139
143
  onSave={editor.saveNow}
140
144
  onTogglePreview={togglePreview}
141
145
  onToggleWordWrap={() => setWordWrap(!wordWrap)}
@@ -151,6 +155,20 @@ export function Editor({
151
155
  // the scrolling text container — a wheel there would scroll the
152
156
  // pane instead of zooming the image.
153
157
  <ImageViewer src={editor.imageUrl} name={activeFile.name} className="flex-1 min-h-0" />
158
+ ) : editor.isLoading ? (
159
+ <div className="flex-1 flex items-center justify-center gap-2 text-ink/40 text-xs font-mono">
160
+ <Loader2 size={14} className="animate-spin" />
161
+ <span>Loading file content…</span>
162
+ </div>
163
+ ) : editor.loadError ? (
164
+ // A read that failed must say so. Without this the pane rendered
165
+ // an empty (or substituted) buffer, and the next save wrote it
166
+ // over the real file.
167
+ <div className="flex-1 flex flex-col items-center justify-center gap-2 px-6 text-center">
168
+ <AlertTriangle size={20} className="text-error" />
169
+ <span className="text-xs text-error font-sans">Could not read {activeFile.name}</span>
170
+ <span className="text-[11px] text-ink/50 font-mono break-all">{editor.loadError}</span>
171
+ </div>
154
172
  ) : (
155
173
  <div onScroll={handleScroll} className={`flex-1 overflow-auto bg-paper flex ${scrollbarFadeClass(isScrolling)}`}>
156
174
  {(isMd && isPreview) ? (
@@ -1,7 +1,8 @@
1
1
  import type { TargetedMouseEvent } from 'preact';
2
- import { Bot, Boxes, Brain, Check, Command, Globe, GripVertical, Sparkles, Star, Zap } from 'lucide-preact';
2
+ import { Brain, Check, Command, GripVertical, Star } from 'lucide-preact';
3
3
  import type { AIModelOption } from '@/shared/types';
4
4
  import { formatContextWindow } from '@/shared/lib/code/format';
5
+ import { ProviderIcon } from '@/client/components/common/provider-icon';
5
6
 
6
7
  interface ModelDropdownItemProps {
7
8
  model: AIModelOption;
@@ -35,28 +36,17 @@ export function ModelDropdownItem({
35
36
  );
36
37
  }
37
38
 
38
- switch (model.providerIcon || model.provider.toLowerCase()) {
39
- case 'deepseek':
40
- return (
41
- <span className="text-[12px] leading-none flex-shrink-0" title="DeepSeek">
42
- 🐋
43
- </span>
44
- );
45
- case 'anthropic':
46
- case 'claude':
47
- return <Bot size={13} className="text-orange-400 flex-shrink-0" />;
48
- case 'openai':
49
- return <Boxes size={13} className="text-emerald-500 flex-shrink-0" />;
50
- case 'google':
51
- case 'gemini':
52
- return <Sparkles size={13} className="text-blue-400 flex-shrink-0" />;
53
- case 'opencode':
54
- return <Zap size={13} className="text-violet-400 flex-shrink-0" />;
55
- case 'mistral':
56
- return <Globe size={13} className="text-rose-400 flex-shrink-0" />;
57
- default:
58
- return <Sparkles size={13} className="text-ink/60 flex-shrink-0" />;
59
- }
39
+ // The provider's own mark, keyed on the catalog's `providerIcon` first so a
40
+ // legacy key still resolves; unknown providers fall back to their initials.
41
+ return (
42
+ <ProviderIcon
43
+ icon={model.providerIcon}
44
+ slug={model.provider}
45
+ name={model.provider}
46
+ size={13}
47
+ className="text-ink/70"
48
+ />
49
+ );
60
50
  };
61
51
 
62
52
  return (
@@ -115,7 +115,7 @@ export function ModelDropdownPanel({
115
115
  key={provider}
116
116
  id={provider}
117
117
  title={provider}
118
- iconType={provider === 'DEEPSEEK' ? 'whale' : 'provider'}
118
+ iconType="provider"
119
119
  models={pModels}
120
120
  isCollapsed={!!collapsedSections[provider]}
121
121
  onToggleCollapse={onToggleCollapse}
@@ -1,13 +1,14 @@
1
1
  import type { TargetedMouseEvent } from 'preact';
2
- import { ChevronDown, ChevronUp, GripVertical, History, Star } from 'lucide-preact';
2
+ import { ChevronDown, ChevronUp, History, Star } from 'lucide-preact';
3
3
  import type { AIModelOption } from '@/shared/types';
4
4
  import { ModelDropdownItem } from '@/client/components/workspace/model-dropdown/Item';
5
+ import { ProviderIcon } from '@/client/components/common/provider-icon';
5
6
  import { modelKey } from '@/shared/lib/models/identity';
6
7
 
7
8
  interface ModelDropdownSectionProps {
8
9
  id: string;
9
10
  title: string;
10
- iconType?: 'star' | 'recent' | 'provider' | 'whale';
11
+ iconType?: 'star' | 'recent' | 'provider';
11
12
  models: AIModelOption[];
12
13
  isCollapsed: boolean;
13
14
  onToggleCollapse: (id: string) => void;
@@ -49,8 +50,16 @@ export function ModelDropdownSection({
49
50
  <div className="flex items-center space-x-1.5">
50
51
  {iconType === 'star' && <Star size={12} className="fill-sky-400 text-sky-400" />}
51
52
  {iconType === 'recent' && <History size={12} className="text-ink/60" />}
52
- {iconType === 'whale' && <span>🐋</span>}
53
- {iconType === 'provider' && <GripVertical size={11} className="text-ink/25" />}
53
+ {iconType === 'provider' && (
54
+ // The section's own provider, taken from a row rather than the
55
+ // uppercased group key, so the mark resolves off the real slug.
56
+ <ProviderIcon
57
+ icon={models[0]?.providerIcon}
58
+ slug={models[0]?.provider ?? id}
59
+ name={models[0]?.provider ?? id}
60
+ size={12}
61
+ />
62
+ )}
54
63
  <span>{title}</span>
55
64
  </div>
56
65
  {isCollapsed ? (
@@ -5,6 +5,7 @@ import type { AIModelOption, ModelPreferences } from '@/shared/types';
5
5
  import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
6
6
  import { invalidateModelsCache } from '@/shared/lib/models/client';
7
7
  import { ModelDropdownPanel } from '@/client/components/workspace/model-dropdown/Panel';
8
+ import { ProviderIcon } from '@/client/components/common/provider-icon';
8
9
  import { useModelCatalog } from '@/client/components/workspace/model-dropdown/use-catalog';
9
10
  import { buildPickerGroups } from '@/client/components/workspace/model-dropdown/groups';
10
11
  import { modelKey } from '@/shared/lib/models/identity';
@@ -229,10 +230,13 @@ export function ModelDropdown({
229
230
  <span className="text-ink/60 flex-shrink-0">
230
231
  {selectedModel.isCmdAgent ? (
231
232
  <span className="font-mono text-[10px] font-bold text-ink/70">⌘</span>
232
- ) : selectedModel.provider.toLowerCase() === 'deepseek' ? (
233
- <span>🐋</span>
234
233
  ) : (
235
- <Sparkles size={12} className="text-ink/60" />
234
+ <ProviderIcon
235
+ icon={selectedModel.providerIcon}
236
+ slug={selectedModel.provider}
237
+ name={selectedModel.provider}
238
+ size={12}
239
+ />
236
240
  )}
237
241
  </span>
238
242
  <span className="flex items-center space-x-1 min-w-0 max-w-[280px]">
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
2
2
  import type { KeyboardEvent as ReactKeyboardEvent } from 'preact/compat';
3
3
  import { Check, ChevronDown, Search } from 'lucide-preact';
4
4
  import { statusLabel, type ProviderEntry, type UsageProviderId } from '@/client/components/settings/categories/usage-settings/providers';
5
+ import { ProviderIcon } from '@/client/components/common/provider-icon';
5
6
  import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
6
7
 
7
8
  interface ProviderSelectProps {
@@ -120,7 +121,8 @@ export function ProviderSelect({ providers, value, onChange }: ProviderSelectPro
120
121
  title="Select usage provider"
121
122
  className="flex w-full min-w-0 items-center justify-between gap-2 bg-paper border border-ink/15 rounded-lg px-3 py-1.5 text-xs text-ink hover:bg-ink/5 transition-colors"
122
123
  >
123
- <span className="flex min-w-0 items-baseline gap-2">
124
+ <span className="flex min-w-0 items-center gap-2">
125
+ {selected && <ProviderIcon slug={selected.id} name={selected.name} size={13} className="text-ink/60" />}
124
126
  <span className="truncate font-medium">{selected?.name ?? 'Select provider'}</span>
125
127
  {selected && (
126
128
  <span className="truncate text-[10px] text-ink/50">{statusLabel(selected)}</span>
@@ -190,10 +192,13 @@ export function ProviderSelect({ providers, value, onChange }: ProviderSelectPro
190
192
  index === activeIndex ? 'bg-ink/5' : 'hover:bg-ink/5'
191
193
  }`}
192
194
  >
193
- <span className="min-w-0">
194
- <span className="block truncate font-medium text-ink">{provider.name}</span>
195
- <span className="block truncate text-[10px] text-ink/50">
196
- {statusLabel(provider)} · {provider.hint}
195
+ <span className="flex min-w-0 items-center gap-2">
196
+ <ProviderIcon slug={provider.id} name={provider.name} size={14} className="text-ink/60" />
197
+ <span className="min-w-0">
198
+ <span className="block truncate font-medium text-ink">{provider.name}</span>
199
+ <span className="block truncate text-[10px] text-ink/50">
200
+ {statusLabel(provider)} · {provider.hint}
201
+ </span>
197
202
  </span>
198
203
  </span>
199
204
  {provider.id === value && <Check size={12} className="flex-shrink-0 text-ink" />}
@@ -1,3 +1,4 @@
1
+ import { useEffect } from 'preact/hooks';
1
2
  import { AlertCircle, RefreshCw } from 'lucide-preact';
2
3
  import { ProviderDetail } from '@/client/components/settings/categories/usage-settings/ProviderDetail';
3
4
  import { buildProviders, type UsageProviderId } from '@/client/components/settings/categories/usage-settings/providers';
@@ -14,19 +15,30 @@ interface UsagePanelProps {
14
15
  active?: boolean;
15
16
  }
16
17
 
17
- /** Right-panel Usage: the selected provider's balance/quota via a dropdown. */
18
+ /** Right-panel Usage: the selected provider's quota and balance via a dropdown. */
18
19
  export function UsagePanel({ className = '', active = true }: UsagePanelProps) {
19
20
  const { report, isLoading, error, reload } = useUsageReport();
20
21
  const [selectedProviderId, setSelectedProviderId] = useSessionState<UsageProviderId>(
21
22
  'usage.selectedProviderId',
22
- 'kenari',
23
+ '',
23
24
  );
24
25
 
25
- // Auto refresh: keep balance/quota current without a manual reload. Silent —
26
- // the spinner stays reserved for the user's own refresh button. Polls only
27
- // while the panel is actually visible.
26
+ // Derived before every early return: a hook must not sit behind a conditional
27
+ // return, or the hook count changes between the loading and loaded renders.
28
+ const providers = report ? buildProviders(report) : [];
29
+ const selected = providers.find((provider) => provider.id === selectedProviderId) ?? providers[0];
30
+
31
+ // Auto refresh: keep quota current without a manual reload. Silent — the
32
+ // spinner stays reserved for the user's own refresh button. Polls only while
33
+ // the panel is actually visible.
28
34
  usePanelRefresh(() => reload({ silent: true }), active);
29
35
 
36
+ // A provider can disappear from the report (its key was removed) or a new one
37
+ // can take the first slot; keep the persisted selection valid.
38
+ useEffect(() => {
39
+ if (selected && selected.id !== selectedProviderId) setSelectedProviderId(selected.id);
40
+ }, [selected, selectedProviderId, setSelectedProviderId]);
41
+
30
42
  if (error && !report) {
31
43
  return (
32
44
  <div className={`flex h-full w-full flex-col items-center justify-center gap-2 p-8 text-center text-xs ${className}`}>
@@ -51,8 +63,6 @@ export function UsagePanel({ className = '', active = true }: UsagePanelProps) {
51
63
  );
52
64
  }
53
65
 
54
- const providers = buildProviders(report);
55
-
56
66
  return (
57
67
  <div className={`flex h-full w-full min-w-0 min-h-0 flex-col overflow-hidden bg-paper text-xs text-ink ${className}`}>
58
68
  <div className="px-3.5 py-2.5 border-b border-ink/10 flex items-center justify-between gap-3 flex-shrink-0 min-w-0">
@@ -69,7 +79,7 @@ export function UsagePanel({ className = '', active = true }: UsagePanelProps) {
69
79
  </div>
70
80
  <button
71
81
  type="button"
72
- onClick={() => reload()}
82
+ onClick={() => reload({ force: true })}
73
83
  disabled={isLoading}
74
84
  className="p-1.5 bg-paper border border-ink/15 rounded-md hover:bg-ink/5 text-ink/80 transition-colors disabled:opacity-50 flex-shrink-0"
75
85
  title="Refresh usage"
@@ -78,20 +88,28 @@ export function UsagePanel({ className = '', active = true }: UsagePanelProps) {
78
88
  </button>
79
89
  </div>
80
90
 
81
- <div className="px-3.5 py-2.5 border-b border-ink/10 flex-shrink-0 min-w-0">
82
- <div className="flex items-center gap-2 min-w-0">
83
- <span className="text-[11px] font-semibold text-ink/60 flex-shrink-0">Provider</span>
84
- <ProviderSelect
85
- providers={providers}
86
- value={selectedProviderId}
87
- onChange={setSelectedProviderId}
88
- />
91
+ {providers.length > 0 && (
92
+ <div className="px-3.5 py-2.5 border-b border-ink/10 flex-shrink-0 min-w-0">
93
+ <div className="flex items-center gap-2 min-w-0">
94
+ <span className="text-[11px] font-semibold text-ink/60 flex-shrink-0">Provider</span>
95
+ <ProviderSelect
96
+ providers={providers}
97
+ value={selected?.id ?? ''}
98
+ onChange={setSelectedProviderId}
99
+ />
100
+ </div>
89
101
  </div>
90
- </div>
102
+ )}
91
103
 
92
104
  <div className="flex-1 min-w-0 min-h-0 w-full overflow-y-auto scrollbar-overlay-container scrollbar-overlay-static p-3.5 space-y-4">
93
105
  {error && <p className="text-error text-xs">{error}</p>}
94
- <ProviderDetail providerId={selectedProviderId} report={report} />
106
+ {selected ? (
107
+ <ProviderDetail summary={selected.summary} />
108
+ ) : (
109
+ <p className="text-[11px] text-ink/50 leading-relaxed">
110
+ No provider credentials detected. Add an API key in Settings → Providers and it will appear here.
111
+ </p>
112
+ )}
95
113
  </div>
96
114
  </div>
97
115
  );
@@ -42,7 +42,7 @@ export function getSampleDialogueSession(): {
42
42
  id: '1a6cb095',
43
43
  role: 'ai',
44
44
  content:
45
- 'Elysia + Bun app (OMPChamber). Top-level contents:\n\n- **App source** — `src/` (client, server, shared)\n- **Config** — `package.json`, `tsconfig.json`, `rsbuild.config.ts`, `postcss.config.mjs`, `bun.lock`, `AGENTS.md`, `DESIGN.md`\n- **Static/PWA** — `public/` (icons, manifest, service worker)\n- **DB** — `workspace.db` (112 KB)\n- **Misc** — `notes/` (2 planning docs)\n\nRoot has no stray source files; project is cleanly under `src/`.',
45
+ 'Elysia + Bun app (OMPChamber). Top-level contents:\n\n- **App source** — `src/` (client, server, shared)\n- **Config** — `package.json`, `tsconfig.json`, `bunfig.toml`, `bun.lock`, `AGENTS.md`, `DESIGN.md`\n- **Static/PWA** — `public/` (icons, manifest, service worker)\n- **DB** — `workspace.db` (112 KB)\n- **Misc** — `notes/` (2 planning docs)\n\nRoot has no stray source files; project is cleanly under `src/`.',
46
46
  date: 'Sep 9, 04:33 AM',
47
47
  thinking: {
48
48
  thought: 'Brief listing.',
@@ -74,7 +74,7 @@ export const otherTools: ToolCallData[] = [
74
74
  "type": "module",
75
75
  "scripts": {
76
76
  "dev": "bun run --watch src/server/index.ts",
77
- "build": "rsbuild build"
77
+ "build": "bun run scripts/build-client.ts"
78
78
  }
79
79
  }`,
80
80
  status: 'success',