ompchamber 2.0.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -11
- package/bunfig.toml +11 -0
- package/dist/client/index.html +16 -5
- package/dist/client/index.js +1393 -0
- package/dist/client/static/js/index-8t70jhyj.js +5149 -0
- package/dist/client/static/js/index-n4wkqj7t.css +1 -0
- package/index.html +54 -0
- package/package.json +9 -10
- package/public/apple-touch-icon.png +0 -0
- package/public/icon-192x192.png +0 -0
- package/public/icon.svg +60 -0
- package/public/manifest.webmanifest +38 -0
- package/src/cli/lib/commands/serve.js +9 -5
- package/src/cli/lib/runtime.js +18 -7
- package/src/client/components/common/code-editor/handler.test.ts +274 -0
- package/src/client/components/common/code-editor/handler.ts +175 -0
- package/src/client/components/common/code-editor/index.tsx +185 -220
- package/src/client/components/common/code-surface/index.tsx +109 -12
- package/src/client/components/common/lazy-panels.tsx +5 -1
- package/src/client/components/common/provider-icon/glyphs.ts +118 -0
- package/src/client/components/common/provider-icon/index.tsx +85 -0
- package/src/client/components/common/viewport.ts +5 -3
- package/src/client/components/layout/RightActivityBar.tsx +2 -1
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +21 -4
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
- package/src/client/components/layout/desktop-layout/index.tsx +26 -4
- package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
- package/src/client/components/mobile/RightSidebar.tsx +11 -4
- package/src/client/components/mobile/SessionSidebar.tsx +10 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +0 -1
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -1
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +3 -0
- package/src/client/components/settings/categories/ProviderSettings.tsx +45 -4
- package/src/client/components/settings/categories/UsageSettings.tsx +22 -7
- package/src/client/components/settings/categories/provider-settings/AddModelModal.tsx +322 -0
- package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +152 -56
- package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +52 -1
- package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +133 -0
- package/src/client/components/settings/categories/provider-settings/DialectFields.tsx +136 -0
- package/src/client/components/settings/categories/provider-settings/EffortLadder.tsx +89 -0
- package/src/client/components/settings/categories/provider-settings/Header.tsx +2 -2
- package/src/client/components/settings/categories/provider-settings/LoginModal.tsx +2 -2
- package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +22 -2
- package/src/client/components/settings/categories/provider-settings/PresetPicker.tsx +82 -0
- package/src/client/components/settings/categories/provider-settings/PriceFields.tsx +107 -0
- package/src/client/components/settings/categories/provider-settings/ReconnectModal.tsx +43 -22
- package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +2 -2
- package/src/client/components/settings/categories/usage-settings/LimitsSection.tsx +115 -0
- package/src/client/components/settings/categories/usage-settings/ProviderCard.tsx +19 -26
- package/src/client/components/settings/categories/usage-settings/ProviderDetail.tsx +29 -22
- package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +60 -48
- package/src/client/components/settings/categories/usage-settings/format.ts +36 -0
- package/src/client/components/settings/categories/usage-settings/providers.ts +52 -28
- package/src/client/components/workspace/browser-panel/AddressBar.tsx +1 -10
- package/src/client/components/workspace/browser-panel/Screencast.tsx +4 -5
- package/src/client/components/workspace/browser-panel/index.tsx +6 -6
- package/src/client/components/workspace/browser-toolbar/index.tsx +22 -12
- package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Todo.tsx +1 -1
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +71 -38
- package/src/client/components/workspace/diff-panel/index.tsx +139 -53
- package/src/client/components/workspace/editor/CommandPalette.test.ts +242 -0
- package/src/client/components/workspace/editor/CommandPalette.tsx +138 -0
- package/src/client/components/workspace/editor/FindWidget.test.ts +261 -0
- package/src/client/components/workspace/editor/FindWidget.tsx +219 -0
- package/src/client/components/workspace/editor/Tabs.tsx +2 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +129 -26
- package/src/client/components/workspace/editor/index.tsx +180 -62
- package/src/client/components/workspace/file-explorer/Modals.tsx +79 -2
- package/src/client/components/workspace/file-explorer/TreeItem.tsx +43 -113
- package/src/client/components/workspace/model-dropdown/Item.tsx +13 -23
- package/src/client/components/workspace/model-dropdown/Panel.tsx +1 -1
- package/src/client/components/workspace/model-dropdown/Section.tsx +13 -4
- package/src/client/components/workspace/model-dropdown/index.tsx +7 -3
- package/src/client/components/workspace/todo-panel/PhaseGroup.tsx +76 -0
- package/src/client/components/workspace/todo-panel/Row.tsx +69 -0
- package/src/client/components/workspace/todo-panel/index.tsx +161 -0
- package/src/client/components/workspace/usage-panel/ProviderSelect.tsx +10 -5
- package/src/client/components/workspace/usage-panel/index.tsx +36 -18
- package/src/client/data/samples/dialogue-session.ts +1 -1
- package/src/client/data/samples/sample-tools/other.ts +1 -1
- package/src/client/data/settings/provider.ts +141 -9
- package/src/client/hooks/editor/find-harness.test-util.ts +170 -0
- package/src/client/hooks/editor/scroll-container.test.ts +102 -0
- package/src/client/hooks/editor/scroll-container.ts +26 -0
- package/src/client/hooks/editor/use-code-window.ts +2 -20
- package/src/client/hooks/editor/use-editor-find-write.test.ts +170 -0
- package/src/client/hooks/editor/use-editor-find.test.ts +118 -0
- package/src/client/hooks/editor/use-editor-find.ts +321 -0
- package/src/client/hooks/editor/use-editor-history.test.ts +216 -0
- package/src/client/hooks/editor/use-editor-history.ts +134 -0
- package/src/client/hooks/editor/use-file-editor.ts +24 -27
- package/src/client/hooks/editor/use-occurrences.test.ts +236 -0
- package/src/client/hooks/editor/use-occurrences.ts +98 -0
- package/src/client/hooks/models/use-provider-names.ts +1 -1
- package/src/client/hooks/settings/providers/actions.ts +337 -0
- package/src/client/hooks/settings/providers/api.ts +69 -0
- package/src/client/hooks/settings/providers/index.ts +138 -0
- package/src/client/hooks/settings/providers/messages.ts +77 -0
- package/src/client/hooks/settings/useUsageReport.ts +8 -3
- package/src/client/hooks/ui/theme.ts +3 -3
- package/src/client/hooks/workspace/file-tabs.ts +62 -11
- package/src/client/hooks/workspace/file-tree-actions.test.ts +348 -0
- package/src/client/hooks/workspace/file-tree-actions.ts +250 -0
- package/src/client/hooks/workspace/session-todos.ts +102 -0
- package/src/client/stubs/elkjs.ts +6 -1
- package/src/client/tailwind.css +30 -0
- package/src/server/index.ts +20 -2
- package/src/server/lib/assets/font-css.server.ts +86 -0
- package/src/server/lib/assets/fonts.server.ts +117 -0
- package/src/server/lib/bundler/css.ts +116 -0
- package/src/server/lib/lifecycle/shell-route.ts +17 -0
- package/src/server/lib/models/provider-listing.server.ts +257 -0
- package/src/server/lib/models/provider-registry.server.test.ts +122 -0
- package/src/server/lib/models/provider-registry.server.ts +76 -25
- package/src/server/lib/omp/config/models-config.ts +11 -0
- package/src/server/lib/omp/config/models-validation.test.ts +137 -0
- package/src/server/lib/omp/config/models-validation.ts +111 -2
- package/src/server/lib/omp/config/provider-seeds.test.ts +167 -0
- package/src/server/lib/omp/config/provider-seeds.ts +176 -0
- package/src/server/lib/omp/config/providers.ts +94 -100
- package/src/server/lib/omp/core/auth-credentials.ts +45 -0
- package/src/server/lib/omp/session/todos.ts +100 -0
- package/src/server/lib/omp/session/{usage.ts → usage/aggregate.ts} +1 -118
- package/src/server/lib/omp/session/usage/shaping.ts +130 -0
- package/src/server/lib/usage/deepseek.ts +4 -5
- package/src/server/lib/usage/kenari.ts +7 -6
- package/src/server/lib/usage/omp-usage.server.test.ts +126 -0
- package/src/server/lib/usage/omp-usage.server.ts +194 -0
- package/src/server/lib/usage/provider-key.ts +81 -3
- package/src/server/lib/usage/providers.server.test.ts +47 -0
- package/src/server/lib/usage/providers.server.ts +208 -0
- package/src/server/lib/usage/vendor-balance.server.test.ts +104 -0
- package/src/server/lib/usage/vendor-balance.server.ts +284 -0
- package/src/server/plugins/shell.server.ts +56 -0
- package/src/server/plugins/ssr.ts +24 -36
- package/src/server/plugins/static.test.ts +26 -29
- package/src/server/plugins/static.ts +40 -19
- package/src/server/routes/fs/action-create.test.ts +90 -0
- package/src/server/routes/fs/action.ts +23 -0
- package/src/server/routes/omp/index.ts +2 -0
- package/src/server/routes/omp/session-todos.ts +80 -0
- package/src/server/routes/settings/index.ts +2 -0
- package/src/server/routes/settings/provider-model.ts +107 -0
- package/src/server/routes/settings/provider-models.ts +65 -202
- package/src/server/routes/settings/providers.ts +22 -7
- package/src/server/routes/settings/usage.ts +125 -45
- package/src/server/routes/telemetry/tokens.ts +2 -1
- package/src/shared/lib/chat/{todo-parser.ts → todo/parser.ts} +17 -0
- package/src/shared/lib/chat/todo/progress.ts +97 -0
- package/src/shared/lib/chat/todo/snapshot.test.ts +156 -0
- package/src/shared/lib/chat/todo/snapshot.ts +215 -0
- package/src/shared/lib/code/editor/commands.test.ts +155 -0
- package/src/shared/lib/code/editor/commands.ts +260 -0
- package/src/shared/lib/code/editor/edits.test.ts +100 -0
- package/src/shared/lib/code/editor/edits.ts +122 -0
- package/src/shared/lib/code/editor/find-status.test.ts +50 -0
- package/src/shared/lib/code/editor/find-status.ts +36 -0
- package/src/shared/lib/code/editor/find.test.ts +203 -0
- package/src/shared/lib/code/editor/find.ts +224 -0
- package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.test.ts +1 -1
- package/src/shared/lib/code/editor/keymap.test.ts +109 -0
- package/src/shared/lib/code/editor/keymap.ts +151 -0
- package/src/shared/lib/code/editor/layers.test.ts +77 -0
- package/src/shared/lib/code/editor/layers.ts +67 -0
- package/src/shared/lib/code/editor/line-edits.test.ts +131 -0
- package/src/shared/lib/code/editor/line-edits.ts +159 -0
- package/src/shared/lib/code/editor/lines.test.ts +81 -0
- package/src/shared/lib/code/editor/lines.ts +83 -0
- package/src/shared/lib/code/editor/marks.test.ts +86 -0
- package/src/shared/lib/code/editor/marks.ts +106 -0
- package/src/shared/lib/code/editor/occurrences.test.ts +117 -0
- package/src/shared/lib/code/editor/occurrences.ts +111 -0
- package/src/shared/lib/code/editor/palette.test.ts +91 -0
- package/src/shared/lib/code/editor/palette.ts +92 -0
- package/src/shared/lib/code/editor/reset-css.test.ts +35 -0
- package/src/shared/lib/code/editor/reset-css.ts +32 -0
- package/src/shared/lib/code/editor/text-commands.test.ts +178 -0
- package/src/shared/lib/code/editor/text-commands.ts +154 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +75 -0
- package/src/shared/lib/code/syntax-highlight.ts +29 -17
- package/src/shared/lib/code/windowed-highlight.ts +52 -1
- package/src/shared/lib/fs/diff-parser.test.ts +116 -0
- package/src/shared/lib/fs/diff-parser.ts +86 -33
- package/src/shared/lib/models/{provider-connection.ts → provider/connection.ts} +37 -0
- package/src/shared/lib/models/provider/dialect.ts +179 -0
- package/src/shared/lib/models/provider/glyph.test.ts +116 -0
- package/src/shared/lib/models/provider/glyph.ts +200 -0
- package/src/shared/lib/models/{provider-models.ts → provider/models.ts} +91 -6
- package/src/shared/lib/models/provider/presets.test.ts +172 -0
- package/src/shared/lib/models/provider/presets.ts +83 -0
- package/src/shared/lib/ui/key-binding.test.ts +115 -0
- package/src/shared/lib/ui/key-binding.ts +143 -0
- package/src/shared/lib/util/platform.test.ts +87 -0
- package/src/shared/lib/util/platform.ts +30 -0
- package/src/shared/lib/workspace/file-tab-id.ts +29 -0
- package/src/shared/lib/workspace/keymap.test.ts +82 -0
- package/src/shared/lib/workspace/keymap.ts +30 -0
- package/src/shared/lib/workspace/refresh-cadence.ts +9 -0
- package/src/shared/lib/workspace/right-panels.ts +8 -0
- package/src/shared/lib/workspace/session-state/store.ts +0 -1
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/settings/provider.ts +81 -0
- package/src/shared/types/settings/usage.ts +88 -14
- package/src/shared/types/todo.ts +108 -0
- package/tsconfig.json +3 -1
- package/dist/client/static/css/index.5f630421b7.css +0 -2
- package/dist/client/static/font/KaTeX_AMS-Regular.a79f1c3119.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Caligraphic-Bold.ec17d13264.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Caligraphic-Regular.55fac25845.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Fraktur-Bold.d42a5579b0.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Fraktur-Regular.d3c882a649.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Main-Bold.c3fb5ac22f.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Main-BoldItalic.6f2bb1dff2.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Main-Italic.8916142bec.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Main-Regular.0462f03bdf.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Math-BoldItalic.572d331f69.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Math-Italic.f28c23acad.woff2 +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Bold.8c5b5494b6.woff2 +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Italic.3b1e59b3ba.woff2 +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Regular.ba21ed5f84.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Script-Regular.03e9641d6f.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Size1-Regular.eae34984b3.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Size2-Regular.5916a24fa3.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Size4-Regular.10d95fd3a2.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Typewriter-Regular.a8709e3622.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-400-normal.2655b10c55.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-500-normal.92a991f204.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-600-normal.43788eb7f1.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-700-normal.2667215d33.woff2 +0 -0
- package/dist/client/static/js/77.0d81c74cf0.js +0 -139
- package/dist/client/static/js/77.0d81c74cf0.js.LICENSE.txt +0 -8
- package/dist/client/static/js/async/0.8e59f2eb5b.js +0 -1
- package/dist/client/static/js/async/0k.5852e6b4fa.js +0 -1
- package/dist/client/static/js/async/0w.9ed36d5860.js +0 -1
- package/dist/client/static/js/async/1.f387c12b63.js +0 -1
- package/dist/client/static/js/async/12.d091f91c99.js +0 -1
- package/dist/client/static/js/async/13.8f810a8a6c.js +0 -1
- package/dist/client/static/js/async/19.6fe909d55a.js +0 -160
- package/dist/client/static/js/async/1b.23f1141a38.js +0 -1
- package/dist/client/static/js/async/1k.973e8e21fc.js +0 -2
- package/dist/client/static/js/async/1k.973e8e21fc.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/1p.0d5cddda96.js +0 -1
- package/dist/client/static/js/async/2.b589f1733b.js +0 -1
- package/dist/client/static/js/async/2v.96165101de.js +0 -1
- package/dist/client/static/js/async/3.0cacc08423.js +0 -1
- package/dist/client/static/js/async/37.b28b2f61cd.js +0 -1
- package/dist/client/static/js/async/3d.5085fb913f.js +0 -1
- package/dist/client/static/js/async/3j.357c80ff38.js +0 -2
- package/dist/client/static/js/async/3j.357c80ff38.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/3p.bcf32a0983.js +0 -1
- package/dist/client/static/js/async/3r.3a69e98ee0.js +0 -64
- package/dist/client/static/js/async/3u.1228460e2a.js +0 -1
- package/dist/client/static/js/async/3y.3530b824ad.js +0 -198
- package/dist/client/static/js/async/3y.3530b824ad.js.LICENSE.txt +0 -5
- package/dist/client/static/js/async/4.9442eeb472.js +0 -1
- package/dist/client/static/js/async/4e.678285ca55.js +0 -1
- package/dist/client/static/js/async/4k.33a0c7eeb4.js +0 -1
- package/dist/client/static/js/async/4m.928a1dde85.js +0 -31
- package/dist/client/static/js/async/4m.928a1dde85.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/4q.aad20dab25.js +0 -1
- package/dist/client/static/js/async/5.ebaf8b89cc.js +0 -36
- package/dist/client/static/js/async/51.95bcae98af.js +0 -2
- package/dist/client/static/js/async/51.95bcae98af.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
- package/dist/client/static/js/async/5j.d6407c4248.js +0 -287
- package/dist/client/static/js/async/5o.88316d4460.js +0 -62
- package/dist/client/static/js/async/6.f034c9b528.js +0 -1
- package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
- package/dist/client/static/js/async/6o.5b1a06a382.js +0 -1
- package/dist/client/static/js/async/7.e4a9288dd0.js +0 -1
- package/dist/client/static/js/async/73.308dff4da8.js +0 -1
- package/dist/client/static/js/async/7e.a1cc9bfd88.js +0 -1
- package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
- package/dist/client/static/js/async/7l.fadbbc0583.js +0 -1
- package/dist/client/static/js/async/8.5067439d8c.js +0 -1
- package/dist/client/static/js/async/8j.ea110663fb.js +0 -1
- package/dist/client/static/js/async/a.bce4542392.js +0 -1
- package/dist/client/static/js/async/aa.65d3852576.js +0 -1
- package/dist/client/static/js/async/ar.9303e5f2e8.js +0 -2
- package/dist/client/static/js/async/ar.9303e5f2e8.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/az.d656062b0f.js +0 -1
- package/dist/client/static/js/async/b.b4f6f06515.js +0 -1
- package/dist/client/static/js/async/bi.09688ba75c.js +0 -1
- package/dist/client/static/js/async/bq.586bc738d1.js +0 -1
- package/dist/client/static/js/async/c.797d933882.js +0 -1
- package/dist/client/static/js/async/c0.0af1e640d3.js +0 -24
- package/dist/client/static/js/async/ca.8a52cfd447.js +0 -1
- package/dist/client/static/js/async/cf.d34fff5d67.js +0 -1
- package/dist/client/static/js/async/ct.b74928f7a3.js +0 -1
- package/dist/client/static/js/async/cx.3cb0242a3a.js +0 -226
- package/dist/client/static/js/async/d.a5fc8fe1b3.js +0 -1
- package/dist/client/static/js/async/da.f62c70f6c8.js +0 -1
- package/dist/client/static/js/async/dp.1725d6432c.js +0 -1
- package/dist/client/static/js/async/e.93214d622b.js +0 -79
- package/dist/client/static/js/async/f.ab50467719.js +0 -19
- package/dist/client/static/js/async/f.ab50467719.js.LICENSE.txt +0 -12
- package/dist/client/static/js/async/f7.46971e9d6d.js +0 -2
- package/dist/client/static/js/async/f7.46971e9d6d.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/f8.bbf0dbae70.js +0 -1
- package/dist/client/static/js/async/g.cb81980498.js +0 -2
- package/dist/client/static/js/async/g.cb81980498.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/h.4e790471e4.js +0 -27
- package/dist/client/static/js/async/h1.393f066d09.js +0 -1
- package/dist/client/static/js/async/h3.216e9798ab.js +0 -148
- package/dist/client/static/js/async/hd.0da931b622.js +0 -1
- package/dist/client/static/js/async/hk.ba7fb04d41.js +0 -1
- package/dist/client/static/js/async/hn.b645041e00.js +0 -1
- package/dist/client/static/js/async/ho.475a5e1ae4.js +0 -1
- package/dist/client/static/js/async/hp.e9fe2e395a.js +0 -2
- package/dist/client/static/js/async/hp.e9fe2e395a.js.LICENSE.txt +0 -17
- package/dist/client/static/js/async/hs.e7c59d8090.js +0 -41
- package/dist/client/static/js/async/i.7c148e8440.js +0 -1
- package/dist/client/static/js/async/id.573df20f29.js +0 -1
- package/dist/client/static/js/async/j.ab925882d8.js +0 -1
- package/dist/client/static/js/async/jd.63886a1f09.js +0 -1
- package/dist/client/static/js/async/jg.d86ebd4013.js +0 -86
- package/dist/client/static/js/async/jl.874d352278.js +0 -1
- package/dist/client/static/js/async/jll.11313b455a.js +0 -1
- package/dist/client/static/js/async/js.67182759ef.js +0 -1
- package/dist/client/static/js/async/jsc.23b70d6f40.js +0 -1
- package/dist/client/static/js/async/jse.334588ff5d.js +0 -1
- package/dist/client/static/js/async/jz.72eb929106.js +0 -77
- package/dist/client/static/js/async/k.3210286401.js +0 -217
- package/dist/client/static/js/async/ka.c1990ecaa3.js +0 -115
- package/dist/client/static/js/async/katex-vendor.de6a87c308.js +0 -1
- package/dist/client/static/js/async/ku.29c8a59d0c.js +0 -1
- package/dist/client/static/js/async/ky.8dab1dade0.js +0 -1
- package/dist/client/static/js/async/l.12dd34e1be.js +0 -1
- package/dist/client/static/js/async/l9.949710adaf.js +0 -83
- package/dist/client/static/js/async/l9.949710adaf.js.LICENSE.txt +0 -13
- package/dist/client/static/js/async/ls.9d9a01026c.js +0 -104
- package/dist/client/static/js/async/m.eb2e6752e2.js +0 -28
- package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/m3.d2b05f9604.js +0 -2
- package/dist/client/static/js/async/m3.d2b05f9604.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/ma.2c261b04dd.js +0 -2
- package/dist/client/static/js/async/ma.2c261b04dd.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/me.8ace414f80.js +0 -23
- package/dist/client/static/js/async/ms.98960af4eb.js +0 -67
- package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +0 -12
- package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
- package/dist/client/static/js/async/n.a9f1c51701.js +0 -1
- package/dist/client/static/js/async/na.a36cd532d6.js +0 -1
- package/dist/client/static/js/async/nk.fc7e58325e.js +0 -1
- package/dist/client/static/js/async/o.36353dfb88.js +0 -1
- package/dist/client/static/js/async/oc.b73042e8bd.js +0 -8
- package/dist/client/static/js/async/og.8eb577893a.js +0 -193
- package/dist/client/static/js/async/p.48ecfcd14e.js +0 -1
- package/dist/client/static/js/async/px.fc6877924b.js +0 -1
- package/dist/client/static/js/async/q.22588845a9.js +0 -21
- package/dist/client/static/js/async/q.22588845a9.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/q5.97dc1481b6.js +0 -286
- package/dist/client/static/js/async/qs.2a9ddb3c72.js +0 -1
- package/dist/client/static/js/async/r.51010ad87c.js +0 -1
- package/dist/client/static/js/async/rd.cb6bf495bb.js +0 -2
- package/dist/client/static/js/async/rd.cb6bf495bb.js.LICENSE.txt +0 -4
- package/dist/client/static/js/async/s.0f395cdda5.js +0 -94
- package/dist/client/static/js/async/t.4dca0e2ba8.js +0 -1
- package/dist/client/static/js/async/t9.d44f57d518.js +0 -1
- package/dist/client/static/js/async/ta.973534bbec.js +0 -1
- package/dist/client/static/js/async/tr.e878658140.js +0 -309
- package/dist/client/static/js/async/u.0871ac5b22.js +0 -1
- package/dist/client/static/js/async/ud.3065bfee31.js +0 -2
- package/dist/client/static/js/async/ud.3065bfee31.js.LICENSE.txt +0 -4
- package/dist/client/static/js/async/up.ea990da0ff.js +0 -32
- package/dist/client/static/js/async/uw.8890abbabc.js +0 -1
- package/dist/client/static/js/async/v.7d28d3c241.js +0 -34
- package/dist/client/static/js/async/vp.9b13bf8610.js +0 -1
- package/dist/client/static/js/async/vt.42416b707d.js +0 -1
- package/dist/client/static/js/async/w.08ac210f60.js +0 -1
- package/dist/client/static/js/async/wt.ad92393a90.js +0 -1
- package/dist/client/static/js/async/x.563f72ca6b.js +0 -1
- package/dist/client/static/js/async/x0.475516418f.js +0 -1
- package/dist/client/static/js/async/x1.da8687159b.js +0 -90
- package/dist/client/static/js/async/xe.da8dcc2015.js +0 -2
- package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/xl.a3c562a58c.js +0 -1
- package/dist/client/static/js/async/xn.6510c0abe2.js +0 -2
- package/dist/client/static/js/async/xn.6510c0abe2.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/xn1.83a7784fe1.js +0 -10
- package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/xnl.feb0021e50.js +0 -2
- package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +0 -12
- package/dist/client/static/js/async/y.1a9ed7a5d8.js +0 -1
- package/dist/client/static/js/async/y6.43c57defe4.js +0 -1
- package/dist/client/static/js/async/yb.8c1342401d.js +0 -1
- package/dist/client/static/js/async/ybq.4491f2f2dc.js +0 -1
- package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
- package/dist/client/static/js/async/yq.917ddd7ba2.js +0 -1
- package/dist/client/static/js/async/ys.00687cf82d.js +0 -38
- package/dist/client/static/js/async/yv.d99d829136.js +0 -1
- package/dist/client/static/js/async/z.56f61a4374.js +0 -1
- package/dist/client/static/js/async/zi.5fdb07b86a.js +0 -24
- package/dist/client/static/js/async/zj.654f37e8ff.js +0 -1
- package/dist/client/static/js/async/zn.966d6f1cc6.js +0 -1
- package/dist/client/static/js/async/zo.26213b40c0.js +0 -1
- package/dist/client/static/js/async/zx.e9f4105e00.js +0 -1
- package/dist/client/static/js/index.e6a257402a.js +0 -38
- package/dist/client/static/js/index.e6a257402a.js.LICENSE.txt +0 -4
- package/dist/client/static/js/markdown-vendor.71e43278b3.js +0 -144
- package/dist/client/static/js/shiki-vendor.cb8452ddcd.js +0 -1
- package/src/client/components/settings/categories/provider-settings/Icons.tsx +0 -35
- package/src/client/hooks/settings/providers.ts +0 -326
- package/src/server/plugins/dev-assets.ts +0 -49
- package/src/shared/lib/code/editor-utils.ts +0 -16
- package/src/shared/lib/models/provider-presets.ts +0 -29
- /package/dist/client/{apple-touch-icon.png → static/apple-touch-icon-dskx5sr3.png} +0 -0
- /package/dist/client/{icon-192x192.png → static/icon-192x192-6gn5cr2f.png} +0 -0
- /package/dist/client/{icon.svg → static/icon-d07z9dtz.svg} +0 -0
- /package/dist/client/{manifest.webmanifest → static/manifest-bnyy75kt.webmanifest} +0 -0
- /package/{dist/client → public}/icon-512x512.png +0 -0
- /package/{dist/client → public}/icon-maskable-512x512.png +0 -0
- /package/{dist/client → public}/sw.js +0 -0
- /package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.ts +0 -0
- /package/src/shared/lib/models/{provider-cleanup.ts → provider/cleanup.ts} +0 -0
- /package/src/shared/lib/models/{provider-label.ts → provider/label.ts} +0 -0
|
@@ -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>
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The command palette's rendering.
|
|
8
|
+
*
|
|
9
|
+
* The list itself is built and filtered in `shared/lib/code/editor/palette`
|
|
10
|
+
* (tested there); what this covers is the interaction — that the field takes
|
|
11
|
+
* focus, that a filter narrows the list, that the arrow keys move the highlight
|
|
12
|
+
* without leaving the list, and that Enter runs the command that is actually
|
|
13
|
+
* highlighted rather than the first one.
|
|
14
|
+
*
|
|
15
|
+
* Rendered directly with `h()` (no JSX) against happy-dom; the component module
|
|
16
|
+
* is imported dynamically, after those globals exist.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
20
|
+
import { Window } from 'happy-dom';
|
|
21
|
+
import type { ComponentChildren, h as PreactH, render as PreactRender } from 'preact';
|
|
22
|
+
import type { act as PreactAct } from 'preact/test-utils';
|
|
23
|
+
import { EDITOR_KEY_BINDINGS, type EditorCommand } from '@/shared/lib/code/editor/keymap';
|
|
24
|
+
import { buildPaletteEntries } from '@/shared/lib/code/editor/palette';
|
|
25
|
+
import { isMacPlatform } from '@/shared/lib/util/platform';
|
|
26
|
+
|
|
27
|
+
let CommandPalette: (props: { onRun: (command: EditorCommand) => void; onClose: () => void }) => ComponentChildren;
|
|
28
|
+
let h: typeof PreactH;
|
|
29
|
+
let render: typeof PreactRender;
|
|
30
|
+
let act: typeof PreactAct;
|
|
31
|
+
|
|
32
|
+
let container: HTMLElement;
|
|
33
|
+
const ran: EditorCommand[] = [];
|
|
34
|
+
let closes = 0;
|
|
35
|
+
|
|
36
|
+
async function mount() {
|
|
37
|
+
await act(async () => {
|
|
38
|
+
render(h(CommandPalette, { onRun: (command) => ran.push(command), onClose: () => closes++ }), container);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function field(): HTMLInputElement | null {
|
|
43
|
+
return container.querySelector('input[aria-label="Command palette"]');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The visible rows, in order, as their label text. */
|
|
47
|
+
function rows(): string[] {
|
|
48
|
+
return Array.from(container.querySelectorAll('button')).map((button) => button.textContent ?? '');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The row the palette currently highlights (it renders a return-key glyph). */
|
|
52
|
+
function activeRow(): string {
|
|
53
|
+
return rows().find((text) => text.length > 0 && container.querySelector('button .lucide-corner-down-left') !== null)
|
|
54
|
+
? (Array.from(container.querySelectorAll('button')).find((button) => button.querySelector('.lucide-corner-down-left'))?.textContent ?? '')
|
|
55
|
+
: '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function typeQuery(value: string) {
|
|
59
|
+
const input = field();
|
|
60
|
+
if (!input) return;
|
|
61
|
+
input.value = value;
|
|
62
|
+
await act(async () => {
|
|
63
|
+
input.dispatchEvent(new globalThis.Event('input', { bubbles: true }));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function pressKey(key: string) {
|
|
68
|
+
await act(async () => {
|
|
69
|
+
field()?.dispatchEvent(new globalThis.KeyboardEvent('keydown', { key, bubbles: true }));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function domGlobals(win: Window): Record<string, unknown> {
|
|
74
|
+
return {
|
|
75
|
+
window: win,
|
|
76
|
+
document: win.document,
|
|
77
|
+
navigator: win.navigator,
|
|
78
|
+
Node: win.Node,
|
|
79
|
+
Element: win.Element,
|
|
80
|
+
HTMLElement: win.HTMLElement,
|
|
81
|
+
HTMLInputElement: win.HTMLInputElement,
|
|
82
|
+
Event: win.Event,
|
|
83
|
+
MouseEvent: win.MouseEvent,
|
|
84
|
+
KeyboardEvent: win.KeyboardEvent,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const installed: Record<string, unknown> = {};
|
|
89
|
+
const displaced: Record<string, unknown> = {};
|
|
90
|
+
|
|
91
|
+
beforeAll(async () => {
|
|
92
|
+
const win = new Window({ url: 'http://localhost' });
|
|
93
|
+
for (const [key, value] of Object.entries(domGlobals(win))) {
|
|
94
|
+
displaced[key] = (globalThis as Record<string, unknown>)[key];
|
|
95
|
+
installed[key] = value;
|
|
96
|
+
(globalThis as Record<string, unknown>)[key] = value;
|
|
97
|
+
}
|
|
98
|
+
({ CommandPalette } = await import('@/client/components/workspace/editor/CommandPalette'));
|
|
99
|
+
({ h, render } = await import('preact'));
|
|
100
|
+
({ act } = await import('preact/test-utils'));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterAll(() => {
|
|
104
|
+
for (const key of Object.keys(installed)) {
|
|
105
|
+
if (displaced[key] === undefined) delete (globalThis as Record<string, unknown>)[key];
|
|
106
|
+
else (globalThis as Record<string, unknown>)[key] = displaced[key];
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
ran.length = 0;
|
|
112
|
+
closes = 0;
|
|
113
|
+
container = document.createElement('div');
|
|
114
|
+
document.body.appendChild(container);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
afterEach(() => {
|
|
118
|
+
render(null, container);
|
|
119
|
+
container.remove();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('CommandPalette', () => {
|
|
123
|
+
test('takes focus on open, so the palette is usable without a click', async () => {
|
|
124
|
+
await mount();
|
|
125
|
+
|
|
126
|
+
expect(document.activeElement).toBe(field());
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('lists the keymap’s commands with their chords', async () => {
|
|
130
|
+
await mount();
|
|
131
|
+
const text = rows().join('\n');
|
|
132
|
+
|
|
133
|
+
expect(text).toContain('Add selection to next find match');
|
|
134
|
+
// The chord is the platform's own. Asserted through the palette's own
|
|
135
|
+
// builder so the expectation is the platform's real answer rather than a
|
|
136
|
+
// guess at which machine the suite is running on — the runner is Linux and a
|
|
137
|
+
// developer's laptop is a Mac, and a Mac-only literal passes on exactly one
|
|
138
|
+
// of them.
|
|
139
|
+
expect(text).toContain(buildPaletteEntries(isMacPlatform()).find((entry) => entry.command === 'selectNextOccurrence')!.chord);
|
|
140
|
+
expect(rows().length).toBeGreaterThan(20);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('every command it lists is one the keymap binds', async () => {
|
|
144
|
+
await mount();
|
|
145
|
+
|
|
146
|
+
// The palette is a VIEW of the keymap; an entry with no binding would be a
|
|
147
|
+
// command that does nothing when it is run.
|
|
148
|
+
for (const entry of buildPaletteEntries(true)) {
|
|
149
|
+
expect(EDITOR_KEY_BINDINGS.some((binding) => binding.command === entry.command)).toBe(true);
|
|
150
|
+
}
|
|
151
|
+
expect(rows().length).toBeGreaterThan(20);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('a query narrows the list', async () => {
|
|
155
|
+
await mount();
|
|
156
|
+
const before = rows().length;
|
|
157
|
+
await typeQuery('occurrence');
|
|
158
|
+
|
|
159
|
+
expect(rows().length).toBeLessThan(before);
|
|
160
|
+
expect(rows().join('\n')).toContain('Select all occurrences');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('a query nothing matches says so instead of listing everything', async () => {
|
|
164
|
+
await mount();
|
|
165
|
+
await typeQuery('zzzz');
|
|
166
|
+
|
|
167
|
+
expect(rows()).toEqual([]);
|
|
168
|
+
expect(container.textContent).toContain('No matching command');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('Enter runs the highlighted command and closes the palette', async () => {
|
|
172
|
+
await mount();
|
|
173
|
+
await typeQuery('toggle line comment');
|
|
174
|
+
await pressKey('Enter');
|
|
175
|
+
|
|
176
|
+
expect(ran).toEqual(['toggleComment']);
|
|
177
|
+
expect(closes).toBe(1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('the arrow keys move the highlight, and Enter runs the moved-to command', async () => {
|
|
181
|
+
await mount();
|
|
182
|
+
await typeQuery('occurrence');
|
|
183
|
+
const first = activeRow();
|
|
184
|
+
await pressKey('ArrowDown');
|
|
185
|
+
const second = activeRow();
|
|
186
|
+
|
|
187
|
+
expect(second).not.toBe(first);
|
|
188
|
+
|
|
189
|
+
await pressKey('Enter');
|
|
190
|
+
// Whichever row was highlighted is the one that ran — not the first match.
|
|
191
|
+
expect(ran.length).toBe(1);
|
|
192
|
+
expect(second).toContain(ran[0] === 'selectAllOccurrences' ? 'Select all' : 'Add selection');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('the highlight wraps at the end of the list', async () => {
|
|
196
|
+
await mount();
|
|
197
|
+
await typeQuery('undo');
|
|
198
|
+
const only = activeRow();
|
|
199
|
+
await pressKey('ArrowDown');
|
|
200
|
+
|
|
201
|
+
// One row: stepping wraps onto itself rather than falling off the list.
|
|
202
|
+
expect(activeRow()).toBe(only);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('ArrowUp from the top wraps to the last row', async () => {
|
|
206
|
+
await mount();
|
|
207
|
+
await typeQuery('line');
|
|
208
|
+
const list = rows();
|
|
209
|
+
await pressKey('ArrowUp');
|
|
210
|
+
const active = activeRow();
|
|
211
|
+
|
|
212
|
+
expect(active.length).toBeGreaterThan(0);
|
|
213
|
+
expect(list.length).toBeGreaterThan(1);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('Escape closes without running anything', async () => {
|
|
217
|
+
await mount();
|
|
218
|
+
await pressKey('Escape');
|
|
219
|
+
|
|
220
|
+
expect(closes).toBe(1);
|
|
221
|
+
expect(ran).toEqual([]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('clicking a row runs that row’s command', async () => {
|
|
225
|
+
await mount();
|
|
226
|
+
await typeQuery('copy line down');
|
|
227
|
+
const row = container.querySelector('button');
|
|
228
|
+
await act(async () => {
|
|
229
|
+
row?.dispatchEvent(new globalThis.MouseEvent('click', { bubbles: true }));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(ran).toEqual(['copyLineDown']);
|
|
233
|
+
expect(closes).toBe(1);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('find-bar commands are marked, so a reader knows where they land', async () => {
|
|
237
|
+
await mount();
|
|
238
|
+
await typeQuery('replace');
|
|
239
|
+
|
|
240
|
+
expect(container.textContent).toContain('find bar');
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The editor's command palette (⌘⇧P).
|
|
8
|
+
*
|
|
9
|
+
* Every editor command this app implements is listed here with the chord that
|
|
10
|
+
* reaches it, both derived from the same keymap table the textarea matches
|
|
11
|
+
* against. The palette is therefore a VIEW of the keymap rather than a second
|
|
12
|
+
* list to keep in sync — a command cannot be listed here and unbound there, and
|
|
13
|
+
* a chord cannot be shown that does not work.
|
|
14
|
+
*
|
|
15
|
+
* Commands this editor deliberately does not have (folding, go-to-definition,
|
|
16
|
+
* the `⌘K` prefix chords) are absent rather than listed and inert: a palette
|
|
17
|
+
* entry that does nothing is worse than no entry, because the user concludes
|
|
18
|
+
* the editor is broken rather than that the feature is missing.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
22
|
+
import type { TargetedKeyboardEvent } from 'preact';
|
|
23
|
+
import { CornerDownLeft, Search } from 'lucide-preact';
|
|
24
|
+
|
|
25
|
+
import { isFindBarCommand, type EditorCommand } from '@/shared/lib/code/editor/keymap';
|
|
26
|
+
import { buildPaletteEntries, filterPaletteEntries, type PaletteEntry } from '@/shared/lib/code/editor/palette';
|
|
27
|
+
import { isMacPlatform } from '@/shared/lib/util/platform';
|
|
28
|
+
|
|
29
|
+
interface CommandPaletteProps {
|
|
30
|
+
onRun: (command: EditorCommand) => void;
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function CommandPalette({ onRun, onClose }: CommandPaletteProps) {
|
|
35
|
+
const [query, setQuery] = useState('');
|
|
36
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
37
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
38
|
+
const listRef = useRef<HTMLDivElement | null>(null);
|
|
39
|
+
|
|
40
|
+
// Built once per mount from the keymap, so a filter can never offer something
|
|
41
|
+
// the keymap does not have.
|
|
42
|
+
const entries = useMemo(() => buildPaletteEntries(isMacPlatform()), []);
|
|
43
|
+
const matches = useMemo(() => filterPaletteEntries(entries, query), [entries, query]);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
inputRef.current?.focus();
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
// Keep the highlighted row in view while the filter shrinks the list under it.
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const list = listRef.current;
|
|
52
|
+
const active = list?.children[activeIndex] as HTMLElement | undefined;
|
|
53
|
+
active?.scrollIntoView({ block: 'nearest' });
|
|
54
|
+
}, [activeIndex, matches.length]);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setActiveIndex((previous) => Math.min(previous, Math.max(matches.length - 1, 0)));
|
|
58
|
+
}, [matches.length]);
|
|
59
|
+
|
|
60
|
+
const commit = (entry: PaletteEntry | undefined) => {
|
|
61
|
+
if (!entry) return;
|
|
62
|
+
onClose();
|
|
63
|
+
onRun(entry.command);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const handleKeyDown = (event: TargetedKeyboardEvent<HTMLInputElement>) => {
|
|
67
|
+
if (event.key === 'ArrowDown') {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
setActiveIndex((previous) => (matches.length === 0 ? 0 : (previous + 1) % matches.length));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (event.key === 'ArrowUp') {
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
setActiveIndex((previous) => (matches.length === 0 ? 0 : (previous - 1 + matches.length) % matches.length));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (event.key === 'Enter') {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
commit(matches[activeIndex]);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (event.key === 'Escape') {
|
|
83
|
+
// Handled on the panel's capture phase, but stopping here too keeps the
|
|
84
|
+
// key from reaching the document if the palette is ever mounted alone.
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
event.stopPropagation();
|
|
87
|
+
onClose();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className="absolute inset-0 z-40 flex items-start justify-center pt-12 bg-ink/10" onClick={onClose}>
|
|
93
|
+
<div
|
|
94
|
+
className="w-[520px] max-w-[92%] bg-paper border border-ink/15 rounded-lg shadow-2xl flex flex-col overflow-hidden"
|
|
95
|
+
onClick={(e) => e.stopPropagation()}
|
|
96
|
+
>
|
|
97
|
+
<div className="flex items-center gap-2 px-3 py-2 border-b border-ink/10">
|
|
98
|
+
<Search size={14} className="text-ink/40 flex-shrink-0" />
|
|
99
|
+
<input
|
|
100
|
+
ref={inputRef}
|
|
101
|
+
type="text"
|
|
102
|
+
value={query}
|
|
103
|
+
onInput={(e) => setQuery(e.currentTarget.value)}
|
|
104
|
+
onKeyDown={handleKeyDown}
|
|
105
|
+
placeholder="Type a command or a shortcut"
|
|
106
|
+
aria-label="Command palette"
|
|
107
|
+
className="flex-1 min-w-0 bg-transparent text-xs font-sans text-ink focus:outline-none placeholder-ink/40"
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div ref={listRef} className="max-h-[320px] overflow-y-auto py-1">
|
|
112
|
+
{matches.length === 0 ? (
|
|
113
|
+
<div className="px-3 py-4 text-center text-xs text-ink/40 font-sans">No matching command</div>
|
|
114
|
+
) : (
|
|
115
|
+
matches.map((entry, index) => (
|
|
116
|
+
<button
|
|
117
|
+
key={entry.command}
|
|
118
|
+
type="button"
|
|
119
|
+
onMouseEnter={() => setActiveIndex(index)}
|
|
120
|
+
onClick={() => commit(entry)}
|
|
121
|
+
className={`w-full flex items-center justify-between gap-3 px-3 py-1.5 text-left text-xs font-sans transition-colors ${
|
|
122
|
+
index === activeIndex ? 'bg-ink/5 text-ink' : 'text-ink/80'
|
|
123
|
+
}`}
|
|
124
|
+
>
|
|
125
|
+
<span className="flex items-center gap-2 min-w-0">
|
|
126
|
+
{index === activeIndex ? <CornerDownLeft size={12} className="text-ink/40 flex-shrink-0" /> : <span className="w-3" />}
|
|
127
|
+
<span className="truncate">{entry.label}</span>
|
|
128
|
+
{isFindBarCommand(entry.command) ? <span className="text-[10px] text-ink/40 flex-shrink-0">find bar</span> : null}
|
|
129
|
+
</span>
|
|
130
|
+
<span className="font-mono text-[10px] text-ink/50 flex-shrink-0">{entry.chord}</span>
|
|
131
|
+
</button>
|
|
132
|
+
))
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|