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
@@ -2,17 +2,21 @@ import { AlignJustify, Check, Code2, Columns, Copy, Minus, Plus, RefreshCw, Spac
2
2
  import { FileIcon } from '@/client/components/common/file-icon';
3
3
  import { getGitStatusInfo } from '@/shared/lib/fs/git-status';
4
4
 
5
+ export type DiffViewMode = 'unified' | 'split';
6
+
5
7
  interface DiffToolbarProps {
6
8
  filePath: string;
7
9
  status: string;
8
10
  isStaged: boolean;
9
- viewMode: 'unified' | 'split';
11
+ viewMode: DiffViewMode;
10
12
  ignoreWhitespace: boolean;
11
13
  additions: number;
12
14
  deletions: number;
13
15
  isLoading: boolean;
14
16
  isCopied: boolean;
15
- onToggleViewMode: () => void;
17
+ /** A stage/unstage/discard write is in flight; the toolbar locks its actions. */
18
+ isBusy?: boolean;
19
+ onSetViewMode: (mode: DiffViewMode) => void;
16
20
  onToggleWhitespace: () => void;
17
21
  onStageUnstage: () => void;
18
22
  onDiscard: () => void;
@@ -21,6 +25,9 @@ interface DiffToolbarProps {
21
25
  onCopyDiff: () => void;
22
26
  }
23
27
 
28
+ const ACTION_CLASS =
29
+ 'flex items-center justify-center p-1.5 rounded transition-colors cursor-pointer disabled:cursor-default disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-ink/50';
30
+
24
31
  export function DiffToolbar({
25
32
  filePath,
26
33
  status,
@@ -31,7 +38,8 @@ export function DiffToolbar({
31
38
  deletions,
32
39
  isLoading,
33
40
  isCopied,
34
- onToggleViewMode,
41
+ isBusy = false,
42
+ onSetViewMode,
35
43
  onToggleWhitespace,
36
44
  onStageUnstage,
37
45
  onDiscard,
@@ -41,56 +49,72 @@ export function DiffToolbar({
41
49
  }: DiffToolbarProps) {
42
50
  const fileName = filePath.split('/').pop() || filePath;
43
51
  const statusInfo = getGitStatusInfo(status, isStaged);
52
+ // Only the actions that write to git lock while one is in flight. Copying the
53
+ // diff is a local read of what is already on screen, so it stays available.
54
+ const locked = isLoading || isBusy;
44
55
 
45
56
  return (
46
- <div className="flex items-center justify-between px-3 py-1.5 bg-paper border-b border-ink/10 text-xs font-sans select-none flex-shrink-0 overflow-x-auto no-scrollbar">
47
- {/* Left: File name, status badge, additions/deletions. Hidden on phone
48
- widths: the mobile diff header already carries the file identity, and
49
- dropping it keeps the actions reachable without a sideways scroll. */}
50
- <div className="hidden md:flex items-center space-x-2 min-w-0 pr-2">
57
+ // `@container` so the labels below can hide on a NARROW PANEL rather than a
58
+ // narrow viewport: the editor panel is 320px wide on a 1440px screen, and
59
+ // `sm:` (viewport-keyed) kept every label on, overflowing the toolbar.
60
+ <div className="@container flex items-center justify-between px-3 py-1.5 bg-paper border-b border-ink/10 text-xs font-sans select-none flex-shrink-0 gap-2 min-w-0">
61
+ {/* Left: File name and status badge. Hidden when the panel is narrow —
62
+ the editor tab already carries the name and the status character, and
63
+ the mobile diff header carries both. */}
64
+ <div className="hidden @[560px]:flex items-center space-x-2 min-w-0 pr-2">
51
65
  <FileIcon name={fileName} size={14} className="flex-shrink-0" />
52
66
  <span className="font-mono text-ink font-medium truncate" title={filePath}>
53
67
  {fileName}
54
68
  </span>
55
-
69
+
56
70
  <span className={`px-1.5 py-0.5 text-[10px] font-mono font-semibold rounded border ${statusInfo.badgeBgClass} flex-shrink-0`}>
57
71
  {statusInfo.charStatus} {statusInfo.label}
58
72
  </span>
73
+ </div>
59
74
 
75
+ {/* Right: Controls & actions. `ml-auto` keeps them right-aligned where the
76
+ identity block is hidden. This strip scrolls on its own when the panel
77
+ is too narrow, so no action is ever clipped out of reach. Each segment
78
+ sets its OWN mode — toggling meant clicking the already-active segment
79
+ flipped the view to the other one. */}
80
+ <div className="flex items-center space-x-0.5 @[440px]:space-x-1 text-ink/70 ml-auto min-w-0 overflow-x-auto no-scrollbar">
81
+ {/* Change counts live here, not in the identity block: the editor tab
82
+ carries the file name but never the counts, so this is the one piece
83
+ of the header that must survive a narrow panel. */}
60
84
  {(additions > 0 || deletions > 0) && (
61
- <div className="flex items-center space-x-1.5 text-[11px] font-mono flex-shrink-0 ml-1">
85
+ <div className="flex items-center space-x-1.5 text-[11px] font-mono flex-shrink-0 pr-1.5 mr-0.5 @[440px]:mr-1 border-r border-ink/10">
62
86
  {additions > 0 && <span className="text-success font-semibold">+{additions}</span>}
63
87
  {deletions > 0 && <span className="text-error font-semibold">-{deletions}</span>}
64
88
  </div>
65
89
  )}
66
- </div>
67
90
 
68
- {/* Right: Controls & actions. `ml-auto` keeps them right-aligned on phone
69
- widths, where the identity block above is hidden. */}
70
- <div className="flex items-center space-x-1 text-ink/70 flex-shrink-0 ml-auto">
71
91
  {/* View mode toggle (Unified vs Split) */}
72
- <div className="flex items-center bg-canvas rounded border border-ink/15 p-0.5 mr-1">
92
+ <div className="flex items-center bg-canvas rounded border border-ink/15 p-0.5 mr-0.5 @[440px]:mr-1 flex-shrink-0">
73
93
  <button
74
94
  type="button"
75
- onClick={onToggleViewMode}
76
- className={`px-2 py-0.5 rounded text-[11px] font-medium flex items-center space-x-1 transition-colors ${
95
+ onClick={() => onSetViewMode('split')}
96
+ className={`px-2 py-0.5 rounded text-[11px] font-medium flex items-center space-x-1 transition-colors cursor-pointer flex-shrink-0 ${
77
97
  viewMode === 'split' ? 'bg-paper text-ink shadow-xs' : 'text-ink/60 hover:text-ink'
78
98
  }`}
79
- title="Split (side-by-side) Diff"
99
+ title="Split (side-by-side) diff"
100
+ aria-label="Split (side-by-side) diff"
101
+ aria-pressed={viewMode === 'split'}
80
102
  >
81
103
  <Columns size={12} />
82
- <span className="hidden sm:inline">Split</span>
104
+ <span className="hidden @[420px]:inline">Split</span>
83
105
  </button>
84
106
  <button
85
107
  type="button"
86
- onClick={onToggleViewMode}
87
- className={`px-2 py-0.5 rounded text-[11px] font-medium flex items-center space-x-1 transition-colors ${
108
+ onClick={() => onSetViewMode('unified')}
109
+ className={`px-2 py-0.5 rounded text-[11px] font-medium flex items-center space-x-1 transition-colors cursor-pointer flex-shrink-0 ${
88
110
  viewMode === 'unified' ? 'bg-paper text-ink shadow-xs' : 'text-ink/60 hover:text-ink'
89
111
  }`}
90
- title="Unified Diff"
112
+ title="Unified diff"
113
+ aria-label="Unified diff"
114
+ aria-pressed={viewMode === 'unified'}
91
115
  >
92
116
  <AlignJustify size={12} />
93
- <span className="hidden sm:inline">Unified</span>
117
+ <span className="hidden @[420px]:inline">Unified</span>
94
118
  </button>
95
119
  </div>
96
120
 
@@ -98,10 +122,12 @@ export function DiffToolbar({
98
122
  <button
99
123
  type="button"
100
124
  onClick={onToggleWhitespace}
101
- className={`p-1.5 rounded transition-colors ${
125
+ className={`p-1.5 rounded transition-colors cursor-pointer flex-shrink-0 ${
102
126
  ignoreWhitespace ? 'bg-ink/10 text-ink' : 'text-ink/50 hover:text-ink hover:bg-ink/5'
103
127
  }`}
104
- title={ignoreWhitespace ? 'Whitespace ignored' : 'Ignore whitespace'}
128
+ title={ignoreWhitespace ? 'Whitespace changes hidden' : 'Hide whitespace changes'}
129
+ aria-label={ignoreWhitespace ? 'Whitespace changes hidden' : 'Hide whitespace changes'}
130
+ aria-pressed={ignoreWhitespace}
105
131
  >
106
132
  <Space size={13} />
107
133
  </button>
@@ -110,19 +136,21 @@ export function DiffToolbar({
110
136
  <button
111
137
  type="button"
112
138
  onClick={onRefresh}
113
- className="p-1.5 text-ink/50 hover:text-ink hover:bg-ink/5 rounded transition-colors"
114
- title="Refresh Diff"
115
- disabled={isLoading}
139
+ className={`${ACTION_CLASS} text-ink/50 hover:text-ink hover:bg-ink/5 flex-shrink-0`}
140
+ title="Reload this diff"
141
+ aria-label="Reload this diff"
142
+ disabled={locked}
116
143
  >
117
144
  <RefreshCw size={13} className={isLoading ? 'animate-spin' : ''} />
118
145
  </button>
119
146
 
120
- {/* Copy diff */}
147
+ {/* Copy diff — never disabled: it reads the buffer already on screen. */}
121
148
  <button
122
149
  type="button"
123
150
  onClick={onCopyDiff}
124
- className="p-1.5 text-ink/50 hover:text-ink hover:bg-ink/5 rounded transition-colors"
125
- title="Copy Diff Text"
151
+ className={`${ACTION_CLASS} text-ink/50 hover:text-ink hover:bg-ink/5 flex-shrink-0`}
152
+ title={isCopied ? 'Copied' : 'Copy diff text'}
153
+ aria-label={isCopied ? 'Copied' : 'Copy diff text'}
126
154
  >
127
155
  {isCopied ? <Check size={13} className="text-success" /> : <Copy size={13} />}
128
156
  </button>
@@ -132,8 +160,10 @@ export function DiffToolbar({
132
160
  <button
133
161
  type="button"
134
162
  onClick={onDiscard}
135
- className="p-1.5 text-ink/50 hover:text-error hover:bg-error/10 rounded transition-colors"
163
+ className={`${ACTION_CLASS} text-ink/50 hover:text-error hover:bg-error/10 flex-shrink-0`}
136
164
  title="Discard changes in this file"
165
+ aria-label="Discard changes in this file"
166
+ disabled={locked}
137
167
  >
138
168
  <Undo2 size={13} />
139
169
  </button>
@@ -143,18 +173,20 @@ export function DiffToolbar({
143
173
  <button
144
174
  type="button"
145
175
  onClick={onStageUnstage}
146
- className="px-2 py-1 text-[11px] font-medium rounded border border-ink/15 hover:bg-ink/5 flex items-center space-x-1 transition-colors text-ink"
176
+ className="px-2 py-1 text-[11px] font-medium rounded border border-ink/15 hover:bg-ink/5 flex items-center space-x-1 transition-colors text-ink cursor-pointer disabled:cursor-default disabled:opacity-40 disabled:hover:bg-transparent flex-shrink-0"
147
177
  title={isStaged ? 'Unstage file' : 'Stage file'}
178
+ aria-label={isStaged ? 'Unstage file' : 'Stage file'}
179
+ disabled={locked}
148
180
  >
149
181
  {isStaged ? (
150
182
  <>
151
183
  <Minus size={12} />
152
- <span>Unstage</span>
184
+ <span className="hidden @[360px]:inline">Unstage</span>
153
185
  </>
154
186
  ) : (
155
187
  <>
156
188
  <Plus size={12} />
157
- <span>Stage</span>
189
+ <span className="hidden @[360px]:inline">Stage</span>
158
190
  </>
159
191
  )}
160
192
  </button>
@@ -164,11 +196,12 @@ export function DiffToolbar({
164
196
  <button
165
197
  type="button"
166
198
  onClick={onOpenInEditor}
167
- className="px-2 py-1 text-[11px] font-medium rounded bg-ink text-canvas hover:bg-ink/90 flex items-center space-x-1 transition-colors shadow-xs ml-1"
168
- title="Open in Code Editor"
199
+ className="px-2 py-1 text-[11px] font-medium rounded bg-ink text-canvas hover:bg-ink/90 flex items-center space-x-1 transition-colors shadow-xs ml-1 cursor-pointer flex-shrink-0"
200
+ title="Open in code editor"
201
+ aria-label="Open in code editor"
169
202
  >
170
203
  <Code2 size={12} />
171
- <span className="hidden sm:inline">Edit File</span>
204
+ <span className="hidden @[360px]:inline">Edit File</span>
172
205
  </button>
173
206
  )}
174
207
  </div>
@@ -1,9 +1,8 @@
1
- import { useCallback, useEffect, useState } from 'preact/hooks';
2
- import { useFetcher } from '@/client/lib/router/fetcher';
1
+ import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
3
2
  import { useSessionUiState } from '@/client/hooks/workspace/session-state';
4
3
  import { parseUnifiedDiff } from '@/shared/lib/fs/diff-parser';
5
4
  import { getLanguageFromPath } from '@/shared/lib/code/syntax-highlight';
6
- import { DiffToolbar } from '@/client/components/workspace/diff-panel/Toolbar';
5
+ import { DiffToolbar, type DiffViewMode } from '@/client/components/workspace/diff-panel/Toolbar';
7
6
  import { UnifiedView } from '@/client/components/workspace/diff-panel/UnifiedView';
8
7
  import { SplitView } from '@/client/components/workspace/diff-panel/SplitView';
9
8
  import { AlertCircle, RefreshCw } from 'lucide-preact';
@@ -30,62 +29,94 @@ export function DiffPanel({
30
29
  className = '',
31
30
  }: DiffPanelProps) {
32
31
  // Read and persist view mode & whitespace preference in session_ui_state
33
- const [viewMode, setViewMode] = useSessionUiState<'unified' | 'split'>('diff.viewMode', 'unified');
32
+ const [viewMode, setViewMode] = useSessionUiState<DiffViewMode>('diff.viewMode', 'unified');
34
33
  const [ignoreWhitespace, setIgnoreWhitespace] = useSessionUiState<boolean>('diff.ignoreWhitespace', false);
35
34
 
36
35
  const [rawDiff, setRawDiff] = useState<string>('');
37
36
  const [currentStatus, setCurrentStatus] = useState<string>(status);
38
37
  const [currentStaged, setCurrentStaged] = useState<boolean>(isStaged);
39
38
  const [isLoading, setIsLoading] = useState<boolean>(true);
39
+ const [isBusy, setIsBusy] = useState<boolean>(false);
40
40
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
41
+ const [actionError, setActionError] = useState<string | null>(null);
41
42
  const [isCopied, setIsCopied] = useState<boolean>(false);
42
43
  const [showDiscardConfirm, setShowDiscardConfirm] = useState<boolean>(false);
43
44
 
44
- const fetcher = useFetcher();
45
+ /**
46
+ * The staged flag / status the next fetch must ask for. The state copies lag
47
+ * a click by a render, and keeping them in `fetchDiff`'s dependency list made
48
+ * every response re-key the callback and fire another fetch. Refs keep the
49
+ * callback stable and always read the post-action value.
50
+ */
51
+ const stagedRef = useRef<boolean>(isStaged);
52
+ const statusRef = useRef<string>(status);
53
+ const fetchSeqRef = useRef(0);
45
54
 
46
- const fetchDiff = useCallback(async () => {
55
+ const fetchDiff = useCallback(async (overrides: { staged?: boolean; status?: string } = {}) => {
47
56
  if (!filePath) return;
57
+ const seq = ++fetchSeqRef.current;
48
58
  setIsLoading(true);
49
59
  setErrorMessage(null);
50
60
  try {
61
+ const staged = overrides.staged ?? stagedRef.current;
62
+ const statusForFetch = overrides.status ?? statusRef.current;
51
63
  const params = new URLSearchParams({
52
64
  fileDiff: '1',
53
65
  file: filePath,
54
- staged: currentStaged ? '1' : '0',
66
+ staged: staged ? '1' : '0',
55
67
  repo: repo || '.',
56
68
  });
57
- if (status || currentStatus) params.set('status', status || currentStatus);
69
+ if (statusForFetch) params.set('status', statusForFetch);
58
70
  if (root) params.set('root', root);
59
71
 
60
72
  const res = await fetch(`/api/fs/git?${params.toString()}`);
61
- const data = await res.json();
62
- if (data.success) {
73
+ const data = (await res.json().catch(() => null)) as {
74
+ success?: boolean;
75
+ diff?: string;
76
+ status?: string;
77
+ staged?: boolean;
78
+ error?: string;
79
+ } | null;
80
+ // A slower earlier read must not overwrite a newer one (the action path
81
+ // fires a fetch of its own while the initial one is still in flight).
82
+ if (seq !== fetchSeqRef.current) return;
83
+ if (data?.success) {
63
84
  setRawDiff(data.diff || '');
64
- if (data.status) setCurrentStatus(data.status);
85
+ if (data.status) {
86
+ statusRef.current = data.status;
87
+ setCurrentStatus(data.status);
88
+ }
89
+ if (typeof data.staged === 'boolean') {
90
+ stagedRef.current = data.staged;
91
+ setCurrentStaged(data.staged);
92
+ }
65
93
  } else {
66
- setErrorMessage(data.error || 'Failed to load file diff');
94
+ setErrorMessage(data?.error || `Failed to load file diff (HTTP ${res.status})`);
67
95
  }
68
- } catch (err: any) {
69
- setErrorMessage(err.message || 'Error fetching diff');
96
+ } catch (err: unknown) {
97
+ if (seq !== fetchSeqRef.current) return;
98
+ setErrorMessage(err instanceof Error ? err.message : 'Error fetching diff');
70
99
  } finally {
71
- setIsLoading(false);
100
+ if (seq === fetchSeqRef.current) setIsLoading(false);
72
101
  }
73
- }, [filePath, currentStaged, repo, root]);
102
+ }, [filePath, repo, root]);
74
103
 
75
104
  useEffect(() => {
76
105
  setCurrentStatus(status);
77
106
  setCurrentStaged(isStaged);
107
+ stagedRef.current = isStaged;
108
+ statusRef.current = status;
78
109
  }, [status, isStaged, filePath]);
79
110
 
80
111
  useEffect(() => {
81
- fetchDiff();
112
+ void fetchDiff();
82
113
  }, [fetchDiff]);
83
114
 
84
115
  const parsed = parseUnifiedDiff(rawDiff, ignoreWhitespace);
85
116
  const language = getLanguageFromPath(filePath);
86
117
 
87
- const handleToggleViewMode = () => {
88
- setViewMode(viewMode === 'unified' ? 'split' : 'unified');
118
+ const handleSetViewMode = (mode: DiffViewMode) => {
119
+ setViewMode(mode);
89
120
  };
90
121
 
91
122
  const handleToggleWhitespace = () => {
@@ -94,44 +125,82 @@ export function DiffPanel({
94
125
 
95
126
  const handleCopyDiff = () => {
96
127
  if (!rawDiff) return;
97
- navigator.clipboard.writeText(rawDiff);
98
- setIsCopied(true);
99
- setTimeout(() => setIsCopied(false), 2000);
128
+ void navigator.clipboard.writeText(rawDiff).then(
129
+ () => {
130
+ setIsCopied(true);
131
+ setTimeout(() => setIsCopied(false), 2000);
132
+ },
133
+ () => setActionError('Clipboard write was refused by the browser'),
134
+ );
100
135
  };
101
136
 
102
- const handleStageUnstage = () => {
103
- const actionType = currentStaged ? 'unstage' : 'stage';
104
- const fd = new FormData();
105
- fd.set('actionType', actionType);
106
- fd.set('file', filePath);
107
- fd.set('repo', repo || '.');
108
- if (root) fd.set('root', root);
109
-
110
- fetcher.submit(fd, { method: 'POST', action: '/api/fs/git' });
111
- setCurrentStaged(!currentStaged);
112
- setTimeout(() => {
113
- fetchDiff();
137
+ /**
138
+ * POST one git action and report whether it landed. The panel used to fire
139
+ * and forget (`fetcher.submit` + an optimistic flip + a 300ms timer), which
140
+ * made a rejected action indistinguishable from a successful one: the badge
141
+ * flipped and the diff silently stayed put.
142
+ */
143
+ const runGitAction = useCallback(
144
+ async (fields: Record<string, string>): Promise<boolean> => {
145
+ const fd = new FormData();
146
+ for (const [key, value] of Object.entries(fields)) fd.set(key, value);
147
+ fd.set('repo', repo || '.');
148
+ if (root) fd.set('root', root);
149
+ try {
150
+ const res = await fetch('/api/fs/git', { method: 'POST', body: fd });
151
+ const data = (await res.json().catch(() => null)) as { success?: boolean; error?: string } | null;
152
+ if (!res.ok || !data?.success) {
153
+ setActionError(data?.error || `Request failed (HTTP ${res.status})`);
154
+ return false;
155
+ }
156
+ setActionError(null);
157
+ return true;
158
+ } catch (err: unknown) {
159
+ setActionError(err instanceof Error ? err.message : 'Request failed');
160
+ return false;
161
+ }
162
+ },
163
+ [repo, root],
164
+ );
165
+
166
+ const handleStageUnstage = async () => {
167
+ if (isBusy) return;
168
+ const nextStaged = !currentStaged;
169
+ setIsBusy(true);
170
+ try {
171
+ const ok = await runGitAction({
172
+ actionType: currentStaged ? 'unstage' : 'stage',
173
+ file: filePath,
174
+ });
175
+ if (!ok) return;
176
+ stagedRef.current = nextStaged;
177
+ setCurrentStaged(nextStaged);
114
178
  onFileSaved?.();
115
- }, 300);
179
+ await fetchDiff({ staged: nextStaged });
180
+ } finally {
181
+ setIsBusy(false);
182
+ }
116
183
  };
117
184
 
118
185
  const handleDiscard = () => {
119
186
  setShowDiscardConfirm(true);
120
187
  };
121
188
 
122
- const confirmDiscard = () => {
189
+ const confirmDiscard = async () => {
123
190
  setShowDiscardConfirm(false);
124
- const fd = new FormData();
125
- fd.set('actionType', 'discard');
126
- fd.set('file', filePath);
127
- fd.set('repo', repo || '.');
128
- if (root) fd.set('root', root);
129
-
130
- fetcher.submit(fd, { method: 'POST', action: '/api/fs/git' });
131
- setTimeout(() => {
132
- fetchDiff();
191
+ setIsBusy(true);
192
+ try {
193
+ // `revert` is the server's own name for "restore this file from the index",
194
+ // and it is the one that handles the untracked case (there the file is
195
+ // removed). Sending `discard` — which no handler matched — returned
196
+ // `{success:true}` for doing nothing at all.
197
+ const ok = await runGitAction({ actionType: 'revert', file: filePath });
198
+ if (!ok) return;
133
199
  onFileSaved?.();
134
- }, 300);
200
+ await fetchDiff();
201
+ } finally {
202
+ setIsBusy(false);
203
+ }
135
204
  };
136
205
 
137
206
  return (
@@ -154,15 +223,32 @@ export function DiffPanel({
154
223
  deletions={parsed.deletions}
155
224
  isLoading={isLoading}
156
225
  isCopied={isCopied}
157
- onToggleViewMode={handleToggleViewMode}
226
+ isBusy={isBusy}
227
+ onSetViewMode={handleSetViewMode}
158
228
  onToggleWhitespace={handleToggleWhitespace}
159
229
  onStageUnstage={handleStageUnstage}
160
230
  onDiscard={handleDiscard}
161
231
  onOpenInEditor={onOpenInEditor}
162
- onRefresh={fetchDiff}
232
+ onRefresh={() => void fetchDiff()}
163
233
  onCopyDiff={handleCopyDiff}
164
234
  />
165
235
 
236
+ {actionError && (
237
+ <div className="flex items-center gap-2 border-b border-error/25 bg-error/10 px-3 py-1.5 text-[11px] text-error flex-shrink-0">
238
+ <AlertCircle size={13} className="flex-shrink-0" />
239
+ <span className="min-w-0 flex-1 truncate" title={actionError}>{actionError}</span>
240
+ <button
241
+ type="button"
242
+ onClick={() => setActionError(null)}
243
+ className="px-1.5 rounded hover:bg-error/15 cursor-pointer"
244
+ title="Dismiss"
245
+ aria-label="Dismiss"
246
+ >
247
+ ✕
248
+ </button>
249
+ </div>
250
+ )}
251
+
166
252
  {/* Main Diff Content Area */}
167
253
  <div className="flex-1 overflow-hidden relative flex flex-col bg-paper">
168
254
  {isLoading ? (
@@ -175,8 +261,8 @@ export function DiffPanel({
175
261
  <AlertCircle size={20} className="text-error" />
176
262
  <p className="text-xs text-error font-medium">{errorMessage}</p>
177
263
  <button
178
- onClick={fetchDiff}
179
- className="px-3 py-1 text-xs bg-ink/10 hover:bg-ink/15 text-ink rounded font-sans transition-colors"
264
+ onClick={() => void fetchDiff()}
265
+ className="px-3 py-1 text-xs bg-ink/10 hover:bg-ink/15 text-ink rounded font-sans transition-colors cursor-pointer"
180
266
  >
181
267
  Retry
182
268
  </button>
@@ -200,14 +286,14 @@ export function DiffPanel({
200
286
  <button
201
287
  type="button"
202
288
  onClick={() => setShowDiscardConfirm(false)}
203
- className="px-3 py-1.5 text-xs text-ink hover:bg-ink/5 rounded transition-colors"
289
+ className="px-3 py-1.5 text-xs text-ink hover:bg-ink/5 rounded transition-colors cursor-pointer"
204
290
  >
205
291
  Cancel
206
292
  </button>
207
293
  <button
208
294
  type="button"
209
- onClick={confirmDiscard}
210
- className="px-3 py-1.5 text-xs bg-error text-white rounded hover:bg-error/90 font-medium transition-colors"
295
+ onClick={() => void confirmDiscard()}
296
+ className="px-3 py-1.5 text-xs bg-error text-white rounded hover:bg-error/90 font-medium transition-colors cursor-pointer"
211
297
  >
212
298
  Discard Changes
213
299
  </button>
@@ -74,7 +74,7 @@ export function EditorTabs({ openedFiles, activeFileId, onSelectFile, onCloseFil
74
74
  title={file.path}
75
75
  >
76
76
  {isDiff ? (
77
- <GitCompare size={14} className={isActive ? 'text-blue-600 dark:text-blue-400 flex-shrink-0' : 'opacity-60 flex-shrink-0'} />
77
+ <GitCompare size={14} className={isActive ? 'text-info flex-shrink-0' : 'text-info/70 flex-shrink-0'} />
78
78
  ) : (
79
79
  <FileIcon name={file.name} size={14} className={isActive ? 'flex-shrink-0' : 'opacity-60 flex-shrink-0'} />
80
80
  )}
@@ -123,7 +123,7 @@ export function EditorTabs({ openedFiles, activeFileId, onSelectFile, onCloseFil
123
123
  className="flex items-center space-x-2 px-3 py-1.5 hover:bg-ink/5 cursor-pointer group"
124
124
  >
125
125
  {file.isDiff ? (
126
- <GitCompare size={14} className="text-blue-600 opacity-70" />
126
+ <GitCompare size={14} className="text-info/80" />
127
127
  ) : (
128
128
  <FileIcon name={file.name} size={14} className="opacity-60" />
129
129
  )}