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