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
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What to TELL the user after a provider was added or re-fetched.
|
|
8
|
+
*
|
|
9
|
+
* The message depends on the shape that was written, not on the model count,
|
|
10
|
+
* because two of the three shapes have no local model list BY DESIGN: a
|
|
11
|
+
* discovery provider is listed by omp at runtime, and an endpoint override
|
|
12
|
+
* leaves the models to omp's own catalog. Reporting "0 models" for either made
|
|
13
|
+
* a registration that succeeded read as a failure.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ProviderItem } from '@/shared/types';
|
|
17
|
+
|
|
18
|
+
/** The success/failure line for an Add Provider submission. */
|
|
19
|
+
export function addProviderMessage(
|
|
20
|
+
provider: ProviderItem,
|
|
21
|
+
options: { fetchedCount: number; ompNote?: string },
|
|
22
|
+
): { text: string; tone: 'success' | 'error' } {
|
|
23
|
+
if (provider.modelSource === 'discovery') {
|
|
24
|
+
return { text: `${provider.name} registered — omp discovers its models on each run.`, tone: 'success' };
|
|
25
|
+
}
|
|
26
|
+
if (provider.modelSource === 'override') {
|
|
27
|
+
return {
|
|
28
|
+
text: `${provider.name} endpoint registered — omp keeps its own model list for this provider.`,
|
|
29
|
+
tone: 'success',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (options.fetchedCount > 0) {
|
|
33
|
+
return {
|
|
34
|
+
text: `Fetched ${options.fetchedCount} new model${options.fetchedCount === 1 ? '' : 's'} from the provider.`,
|
|
35
|
+
tone: 'success',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (options.fetchedCount === -1) {
|
|
39
|
+
// The listing failed. When omp reported a reason it is the actionable half,
|
|
40
|
+
// so it is quoted rather than replaced by a generic line.
|
|
41
|
+
return {
|
|
42
|
+
text: options.ompNote
|
|
43
|
+
? `Provider registered, but models could not be listed: ${options.ompNote}`
|
|
44
|
+
: 'Auto-fetch models failed — provider added with default models.',
|
|
45
|
+
tone: 'error',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return { text: `${provider.name} registered — no new models to add.`, tone: 'success' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The extra line reported after a manual "fetch models": what the chamber wrote
|
|
53
|
+
* into `models.yml` versus what omp already had. A file write and a model-count
|
|
54
|
+
* change are different outcomes, and conflating them hid a failed write.
|
|
55
|
+
*/
|
|
56
|
+
export function fetchModelsNote(result: {
|
|
57
|
+
omp?: { written: boolean; addedCount: number; backfilledCount: number; reason?: string };
|
|
58
|
+
}): { text: string; tone: 'success' | 'error' } | null {
|
|
59
|
+
if (!result.omp) return null;
|
|
60
|
+
if (result.omp.written) {
|
|
61
|
+
const parts: string[] = [];
|
|
62
|
+
if (result.omp.addedCount > 0) {
|
|
63
|
+
parts.push(`${result.omp.addedCount} new model${result.omp.addedCount === 1 ? '' : 's'} registered`);
|
|
64
|
+
}
|
|
65
|
+
if (result.omp.backfilledCount > 0) {
|
|
66
|
+
parts.push(`${result.omp.backfilledCount} model${result.omp.backfilledCount === 1 ? '' : 's'} enriched`);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
text: `omp models.yml updated${parts.length > 0 ? `: ${parts.join(', ')}` : ''}.`,
|
|
70
|
+
tone: 'success',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (result.omp.reason) {
|
|
74
|
+
return { text: `omp models.yml not updated: ${result.omp.reason}`, tone: 'error' };
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
@@ -5,24 +5,29 @@ export interface UsageReportState {
|
|
|
5
5
|
report: UsageReport | null;
|
|
6
6
|
isLoading: boolean;
|
|
7
7
|
error: string | null;
|
|
8
|
-
reload: (opts?: { silent?: boolean }) => void;
|
|
8
|
+
reload: (opts?: { silent?: boolean; force?: boolean }) => void;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Shared `GET /api/settings/usage` loader for the Settings → Usage panel and
|
|
13
13
|
* the Usage right panel. Both surfaces mount independently, so each keeps its
|
|
14
14
|
* own copy of the last successful report until reload completes.
|
|
15
|
+
*
|
|
16
|
+
* `force` asks the server to re-probe the providers instead of answering from
|
|
17
|
+
* its one-minute quota cache — used by the user's own Refresh button, never by
|
|
18
|
+
* the background poll.
|
|
15
19
|
*/
|
|
16
20
|
export function useUsageReport(): UsageReportState {
|
|
17
21
|
const [report, setReport] = useState<UsageReport | null>(null);
|
|
18
22
|
const [isLoading, setIsLoading] = useState(true);
|
|
19
23
|
const [error, setError] = useState<string | null>(null);
|
|
20
24
|
|
|
21
|
-
const reload = useCallback(async (opts?: { silent?: boolean }) => {
|
|
25
|
+
const reload = useCallback(async (opts?: { silent?: boolean; force?: boolean }) => {
|
|
22
26
|
if (!opts?.silent) setIsLoading(true);
|
|
23
27
|
setError(null);
|
|
24
28
|
try {
|
|
25
|
-
const
|
|
29
|
+
const url = opts?.force ? '/api/settings/usage?refresh=1' : '/api/settings/usage';
|
|
30
|
+
const response = await fetch(url);
|
|
26
31
|
if (!response.ok) throw new Error(`Usage request failed (${response.status})`);
|
|
27
32
|
const data = (await response.json()) as UsageReport;
|
|
28
33
|
setReport(data);
|
|
@@ -35,9 +35,9 @@ export function currentThemeId(): string {
|
|
|
35
35
|
* Put the catalog on the document before the first render.
|
|
36
36
|
*
|
|
37
37
|
* The SSR shell already inlines the stylesheet and both attributes; this is the
|
|
38
|
-
* guard for a document that predates them — a cached page, or
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* guard for a document that predates them — a cached page, or markup rendered
|
|
39
|
+
* before the server restarted. Without it the page keeps `:root`'s default
|
|
40
|
+
* palette and no variant attribute,
|
|
41
41
|
* which is a wrong palette, not merely a stale one.
|
|
42
42
|
*
|
|
43
43
|
* Called once from `main.tsx` rather than from a hook: the only components that
|
|
@@ -2,8 +2,18 @@ import { useCallback, useEffect, useRef } from 'preact/hooks';
|
|
|
2
2
|
import { useSessionState } from '@/client/hooks/workspace/session-state';
|
|
3
3
|
import { useSessionStateContext } from '@/client/hooks/workspace/session-state/context';
|
|
4
4
|
import { useChamberEvent } from '@/client/hooks/ui/window-event';
|
|
5
|
+
import { diffTabId, diffTabName, fileTabId } from '@/shared/lib/workspace/file-tab-id';
|
|
5
6
|
import type { OpenedFile } from '@/shared/types/fs';
|
|
6
7
|
|
|
8
|
+
export interface OpenFileInput {
|
|
9
|
+
id?: number | string;
|
|
10
|
+
name: string;
|
|
11
|
+
path: string;
|
|
12
|
+
content?: string;
|
|
13
|
+
root?: string;
|
|
14
|
+
repo?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
7
17
|
export function useFileTabs(
|
|
8
18
|
activeProjectPath: string | null,
|
|
9
19
|
sessionId: string,
|
|
@@ -25,9 +35,12 @@ export function useFileTabs(
|
|
|
25
35
|
setActiveFileId(null);
|
|
26
36
|
}, [activeProjectPath, sessionId, layoutReady, setOpenedFiles, setActiveFileId]);
|
|
27
37
|
|
|
28
|
-
const handleOpenFile = useCallback((file:
|
|
38
|
+
const handleOpenFile = useCallback((file: OpenFileInput) => {
|
|
29
39
|
const fileEntry: OpenedFile = {
|
|
30
40
|
...file,
|
|
41
|
+
// One id scheme for a path, so a file opened from the explorer, a chat
|
|
42
|
+
// link and a converted diff tab all land on the same tab.
|
|
43
|
+
id: file.id ?? fileTabId(file.path),
|
|
31
44
|
root: file.root ?? activeProjectPath ?? undefined,
|
|
32
45
|
};
|
|
33
46
|
setOpenedFiles(prev => {
|
|
@@ -52,13 +65,12 @@ export function useFileTabs(
|
|
|
52
65
|
root?: string;
|
|
53
66
|
}) => {
|
|
54
67
|
const rawPath = diffInfo.file.replace(/^\/+/, '');
|
|
55
|
-
const fileName = rawPath.split('/').pop() || rawPath;
|
|
56
68
|
const isStaged = Boolean(diffInfo.staged);
|
|
57
|
-
const diffId =
|
|
69
|
+
const diffId = diffTabId(rawPath, isStaged);
|
|
58
70
|
|
|
59
71
|
const diffEntry: OpenedFile = {
|
|
60
72
|
id: diffId,
|
|
61
|
-
name:
|
|
73
|
+
name: diffTabName(rawPath),
|
|
62
74
|
path: rawPath,
|
|
63
75
|
isDiff: true,
|
|
64
76
|
diffStatus: diffInfo.status || 'M',
|
|
@@ -91,6 +103,50 @@ export function useFileTabs(
|
|
|
91
103
|
});
|
|
92
104
|
}, [activeFileId, setOpenedFiles, setActiveFileId]);
|
|
93
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Turn a diff tab into the plain editor tab for the same file.
|
|
108
|
+
*
|
|
109
|
+
* The panel used to do this by assigning `activeFile.isDiff = false` on the
|
|
110
|
+
* object it was handed. That object is the one inside `layout.openedFiles`,
|
|
111
|
+
* so the flag was mutated in place: React never saw a change (the array
|
|
112
|
+
* identity was identical), the tab kept rendering the diff until the next
|
|
113
|
+
* unrelated render, and — because the array is what gets persisted — the
|
|
114
|
+
* mutation reached the database. After a reload the "diff" tab came back as
|
|
115
|
+
* a file tab, and re-opening that file's diff found an entry that already
|
|
116
|
+
* matched, so the diff never came back at all.
|
|
117
|
+
*
|
|
118
|
+
* The target id is `fileTabId`, so if that file already has a tab the two
|
|
119
|
+
* entries would share an id and React would render duplicate keys. The diff
|
|
120
|
+
* entry is dropped in that case and the existing tab activated instead — the
|
|
121
|
+
* user asked to see the file, and it is already open.
|
|
122
|
+
*/
|
|
123
|
+
const convertDiffToEditor = useCallback((diffId: number | string) => {
|
|
124
|
+
setOpenedFiles(prev => {
|
|
125
|
+
const index = prev.findIndex(f => f.id === diffId);
|
|
126
|
+
if (index === -1) return prev;
|
|
127
|
+
const diff = prev[index];
|
|
128
|
+
const fileId = fileTabId(diff.path);
|
|
129
|
+
const alreadyOpen = prev.find(f => f.id === fileId);
|
|
130
|
+
|
|
131
|
+
if (alreadyOpen) {
|
|
132
|
+
setActiveFileId(alreadyOpen.id);
|
|
133
|
+
return prev.filter(f => f.id !== diffId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const next = [...prev];
|
|
137
|
+
next[index] = {
|
|
138
|
+
id: fileId,
|
|
139
|
+
name: diff.path.split('/').pop() || diff.name,
|
|
140
|
+
path: diff.path,
|
|
141
|
+
root: diff.root,
|
|
142
|
+
repo: diff.repo,
|
|
143
|
+
};
|
|
144
|
+
setActiveFileId(fileId);
|
|
145
|
+
return next;
|
|
146
|
+
});
|
|
147
|
+
onOpenTab?.();
|
|
148
|
+
}, [onOpenTab, setOpenedFiles, setActiveFileId]);
|
|
149
|
+
|
|
94
150
|
// Global listeners for omp:open-file and omp:open-diff
|
|
95
151
|
useChamberEvent('omp:open-file', (e) => {
|
|
96
152
|
const customEvent = e as CustomEvent<{ path: string; name?: string; id?: number; content?: string; root?: string; repo?: string }>;
|
|
@@ -98,15 +154,9 @@ export function useFileTabs(
|
|
|
98
154
|
|
|
99
155
|
const rawPath = customEvent.detail.path.replace(/^\/+/, '');
|
|
100
156
|
const name = customEvent.detail.name || rawPath.split('/').pop() || 'file';
|
|
101
|
-
let hash = 0;
|
|
102
|
-
for (let i = 0; i < rawPath.length; i++) {
|
|
103
|
-
hash = (hash << 5) - hash + rawPath.charCodeAt(i);
|
|
104
|
-
hash |= 0;
|
|
105
|
-
}
|
|
106
|
-
const id = customEvent.detail.id || Math.abs(hash) || Date.now();
|
|
107
157
|
|
|
108
158
|
handleOpenFile({
|
|
109
|
-
id,
|
|
159
|
+
id: customEvent.detail.id || fileTabId(rawPath),
|
|
110
160
|
name,
|
|
111
161
|
path: rawPath,
|
|
112
162
|
content: customEvent.detail.content,
|
|
@@ -136,5 +186,6 @@ export function useFileTabs(
|
|
|
136
186
|
handleOpenFile,
|
|
137
187
|
handleOpenDiff,
|
|
138
188
|
handleCloseFile,
|
|
189
|
+
convertDiffToEditor,
|
|
139
190
|
};
|
|
140
191
|
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `useFileActions` — the Files panel's server contract.
|
|
8
|
+
*
|
|
9
|
+
* Three rules are asserted here because each was a bug: a create that SUCCEEDS
|
|
10
|
+
* opens the new file (the dialog used to close and leave the user hunting for
|
|
11
|
+
* an empty row), a create that is REFUSED keeps the dialog open with the reason
|
|
12
|
+
* beside the field (rather than closing as if it had worked), and a settled
|
|
13
|
+
* response is acted on exactly ONCE — an unguarded effect re-ran on every render
|
|
14
|
+
* and pinned the tab the moment a file was created.
|
|
15
|
+
*
|
|
16
|
+
* Runs against a real DOM (happy-dom) because it is a Preact hook driving
|
|
17
|
+
* fetches. The modules below are therefore imported DYNAMICALLY, inside
|
|
18
|
+
* `beforeAll`: Preact binds its environment at evaluation time, so a static
|
|
19
|
+
* import would capture the DOM-less one and the render would fail — the same
|
|
20
|
+
* boundary `use-file-editor.test.ts` documents.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
24
|
+
import { Window } from 'happy-dom';
|
|
25
|
+
import type { h as PreactH, render as PreactRender } from 'preact';
|
|
26
|
+
import type { act as PreactAct } from 'preact/test-utils';
|
|
27
|
+
import type { FileActions, useFileActions as UseFileActionsHook } from '@/client/hooks/workspace/file-tree-actions';
|
|
28
|
+
|
|
29
|
+
let useFileActions: typeof UseFileActionsHook;
|
|
30
|
+
let h: typeof PreactH;
|
|
31
|
+
let render: typeof PreactRender;
|
|
32
|
+
let act: typeof PreactAct;
|
|
33
|
+
|
|
34
|
+
let mounted: FileActions | null = null;
|
|
35
|
+
let container: HTMLElement;
|
|
36
|
+
|
|
37
|
+
/** What the stubbed endpoint was asked, and what it answers. */
|
|
38
|
+
interface Server {
|
|
39
|
+
requests: Array<Record<string, string>>;
|
|
40
|
+
/** Response for the next create: `success` with a path, or a refusal. */
|
|
41
|
+
createResult: { success?: boolean; path?: string; error?: string };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const opened: Array<{ path: string; name: string }> = [];
|
|
45
|
+
let refreshes = 0;
|
|
46
|
+
|
|
47
|
+
function stubFetch(server: Server) {
|
|
48
|
+
globalThis.fetch = (async (_input: string | URL | Request, init?: RequestInit) => {
|
|
49
|
+
const form = init?.body as FormData;
|
|
50
|
+
const fields: Record<string, string> = {};
|
|
51
|
+
for (const [key, value] of form.entries()) fields[key] = String(value);
|
|
52
|
+
server.requests.push(fields);
|
|
53
|
+
// The fetcher reads `text()` and parses it itself, so a stub that only
|
|
54
|
+
// offers `json()` would leave every response looking empty — the request
|
|
55
|
+
// would be recorded and the effect would never see an answer.
|
|
56
|
+
const body = JSON.stringify(server.createResult);
|
|
57
|
+
return { ok: true, text: async () => body };
|
|
58
|
+
}) as unknown as typeof fetch;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Let the request chain and the render it queues land.
|
|
63
|
+
*
|
|
64
|
+
* Several `act` boundaries, each draining microtasks: the fetcher's chain
|
|
65
|
+
* (`fetch` → `text()` → parse → `setData` → `setState`) resolves over several
|
|
66
|
+
* turns, and a state update queued outside an `act` boundary is only flushed
|
|
67
|
+
* when the next one arrives. No timers — the hook resolves on promises, so
|
|
68
|
+
* waiting on the clock would only be guessing at how many turns it needs.
|
|
69
|
+
*/
|
|
70
|
+
async function settle() {
|
|
71
|
+
for (let i = 0; i < 8; i++) {
|
|
72
|
+
await act(async () => {
|
|
73
|
+
for (let turn = 0; turn < 8; turn++) await Promise.resolve();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mount(file: { path: string; name: string }, diff?: { staged?: boolean; status?: string }) {
|
|
79
|
+
const Probe = () => {
|
|
80
|
+
mounted = useFileActions({
|
|
81
|
+
// `basePath` is the ABSOLUTE listing root the server reported, and it is
|
|
82
|
+
// what Copy Path anchors on — `rootPath` is the client's own (possibly
|
|
83
|
+
// `~`-relative) workspace path, which is not resolvable outside the panel.
|
|
84
|
+
file: { ...file, basePath: '/root', rootPath: '/root', repo: '.' },
|
|
85
|
+
diff,
|
|
86
|
+
onOpenFile: (target) => opened.push({ path: target.path, name: target.name }),
|
|
87
|
+
onActionComplete: () => refreshes++,
|
|
88
|
+
});
|
|
89
|
+
return null;
|
|
90
|
+
};
|
|
91
|
+
return act(async () => {
|
|
92
|
+
render(h(Probe, {}), container);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Open the create dialog the way the context menu does. */
|
|
97
|
+
async function openCreateDialog() {
|
|
98
|
+
await act(async () => {
|
|
99
|
+
mounted?.handleAction('new_file');
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Submit the create dialog with `name` and let the response land — the whole
|
|
105
|
+
* user sequence (open the dialog, type, press Create), because the state under
|
|
106
|
+
* test is what survives it.
|
|
107
|
+
*/
|
|
108
|
+
async function createNamed(name: string) {
|
|
109
|
+
await openCreateDialog();
|
|
110
|
+
await act(async () => {
|
|
111
|
+
mounted?.setCreateName(name);
|
|
112
|
+
});
|
|
113
|
+
await act(async () => {
|
|
114
|
+
mounted?.submitCreate({ preventDefault() {} } as never);
|
|
115
|
+
});
|
|
116
|
+
await settle();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function domGlobals(win: Window): Record<string, unknown> {
|
|
120
|
+
return {
|
|
121
|
+
window: win,
|
|
122
|
+
document: win.document,
|
|
123
|
+
navigator: win.navigator,
|
|
124
|
+
Node: win.Node,
|
|
125
|
+
Element: win.Element,
|
|
126
|
+
HTMLElement: win.HTMLElement,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const installed: Record<string, unknown> = {};
|
|
131
|
+
const displaced: Record<string, unknown> = {};
|
|
132
|
+
|
|
133
|
+
beforeAll(async () => {
|
|
134
|
+
const win = new Window({ url: 'http://localhost' });
|
|
135
|
+
for (const [key, value] of Object.entries(domGlobals(win))) {
|
|
136
|
+
displaced[key] = (globalThis as Record<string, unknown>)[key];
|
|
137
|
+
installed[key] = value;
|
|
138
|
+
(globalThis as Record<string, unknown>)[key] = value;
|
|
139
|
+
}
|
|
140
|
+
({ useFileActions } = await import('@/client/hooks/workspace/file-tree-actions'));
|
|
141
|
+
({ h, render } = await import('preact'));
|
|
142
|
+
({ act } = await import('preact/test-utils'));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
afterAll(() => {
|
|
146
|
+
for (const key of Object.keys(installed)) {
|
|
147
|
+
if (displaced[key] === undefined) delete (globalThis as Record<string, unknown>)[key];
|
|
148
|
+
else (globalThis as Record<string, unknown>)[key] = displaced[key];
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
beforeEach(() => {
|
|
153
|
+
mounted = null;
|
|
154
|
+
opened.length = 0;
|
|
155
|
+
refreshes = 0;
|
|
156
|
+
container = document.createElement('div');
|
|
157
|
+
document.body.appendChild(container);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
afterEach(() => {
|
|
161
|
+
render(null, container);
|
|
162
|
+
container.remove();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
function server(createResult: Server['createResult']): Server {
|
|
166
|
+
return { requests: [], createResult };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
describe('useFileActions create', () => {
|
|
170
|
+
test('a created file opens in the editor and refreshes the tree', async () => {
|
|
171
|
+
const srv = server({ success: true, path: 'src/new-file.ts' });
|
|
172
|
+
stubFetch(srv);
|
|
173
|
+
await mount({ path: 'src', name: 'src' });
|
|
174
|
+
await createNamed('new-file.ts');
|
|
175
|
+
|
|
176
|
+
expect(srv.requests[0]).toEqual({ actionType: 'create', path: 'src', name: 'new-file.ts', root: '/root' });
|
|
177
|
+
expect(opened).toEqual([{ path: 'src/new-file.ts', name: 'new-file.ts' }]);
|
|
178
|
+
expect(refreshes).toBe(1);
|
|
179
|
+
expect(mounted?.showCreateModal).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('a refusal keeps the dialog open and reports the server’s reason', async () => {
|
|
183
|
+
stubFetch(server({ error: 'new-file.ts already exists' }));
|
|
184
|
+
await mount({ path: 'src', name: 'src' });
|
|
185
|
+
await createNamed('new-file.ts');
|
|
186
|
+
|
|
187
|
+
expect(mounted?.showCreateModal).toBe(true);
|
|
188
|
+
expect(mounted?.createError).toBe('new-file.ts already exists');
|
|
189
|
+
expect(opened).toEqual([]);
|
|
190
|
+
expect(refreshes).toBe(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('a refusal with no message still says something', async () => {
|
|
194
|
+
stubFetch(server({}));
|
|
195
|
+
await mount({ path: 'src', name: 'src' });
|
|
196
|
+
await createNamed('new-file.ts');
|
|
197
|
+
|
|
198
|
+
expect(mounted?.createError).toBe('Could not create the file');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('a settled response is acted on once, however many times the row re-renders', async () => {
|
|
202
|
+
// The unguarded version re-ran this effect on every render and re-opened
|
|
203
|
+
// the file forever, which pinned the tab.
|
|
204
|
+
const srv = server({ success: true, path: 'src/once.ts' });
|
|
205
|
+
stubFetch(srv);
|
|
206
|
+
await mount({ path: 'src', name: 'src' });
|
|
207
|
+
await createNamed('once.ts');
|
|
208
|
+
|
|
209
|
+
for (let i = 0; i < 3; i++) {
|
|
210
|
+
await act(async () => {
|
|
211
|
+
mounted?.setCreateName('');
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
await settle();
|
|
215
|
+
|
|
216
|
+
expect(refreshes).toBe(1);
|
|
217
|
+
expect(opened.length).toBe(1);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('an empty name never reaches the server', async () => {
|
|
221
|
+
const srv = server({ success: true, path: 'src/x.ts' });
|
|
222
|
+
stubFetch(srv);
|
|
223
|
+
await mount({ path: 'src', name: 'src' });
|
|
224
|
+
|
|
225
|
+
await act(async () => {
|
|
226
|
+
mounted?.setCreateName(' ');
|
|
227
|
+
});
|
|
228
|
+
await act(async () => {
|
|
229
|
+
mounted?.submitCreate({ preventDefault() {} } as never);
|
|
230
|
+
});
|
|
231
|
+
await settle();
|
|
232
|
+
|
|
233
|
+
expect(srv.requests).toEqual([]);
|
|
234
|
+
expect(mounted?.showCreateModal).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('useFileActions context menu', () => {
|
|
239
|
+
test('opening the create dialog resets the previous attempt', async () => {
|
|
240
|
+
stubFetch(server({ error: 'taken' }));
|
|
241
|
+
await mount({ path: 'src', name: 'src' });
|
|
242
|
+
await createNamed('taken.ts');
|
|
243
|
+
expect(mounted?.createError).toBe('taken');
|
|
244
|
+
|
|
245
|
+
await act(async () => {
|
|
246
|
+
mounted?.handleAction('new_file');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
expect(mounted?.showCreateModal).toBe(true);
|
|
250
|
+
expect(mounted?.createError).toBeNull();
|
|
251
|
+
expect(mounted?.createName).toBe('');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('copying the path reports the ABSOLUTE path, and does not call the server', async () => {
|
|
255
|
+
const srv = server({});
|
|
256
|
+
stubFetch(srv);
|
|
257
|
+
const writes: string[] = [];
|
|
258
|
+
// `copyToClipboard` prefers the async clipboard API, and only when the page
|
|
259
|
+
// is a secure context — the legacy textarea path needs `execCommand`, which
|
|
260
|
+
// happy-dom does not implement.
|
|
261
|
+
Object.defineProperty(globalThis.window, 'isSecureContext', { value: true, configurable: true });
|
|
262
|
+
Object.defineProperty(globalThis.navigator, 'clipboard', {
|
|
263
|
+
value: {
|
|
264
|
+
writeText: async (text: string) => {
|
|
265
|
+
writes.push(text);
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
configurable: true,
|
|
269
|
+
});
|
|
270
|
+
await mount({ path: 'src/deep/file.ts', name: 'file.ts' });
|
|
271
|
+
|
|
272
|
+
await act(async () => {
|
|
273
|
+
mounted?.handleAction('copy_path');
|
|
274
|
+
});
|
|
275
|
+
await settle();
|
|
276
|
+
|
|
277
|
+
expect(writes).toEqual(['/root/src/deep/file.ts']);
|
|
278
|
+
expect(srv.requests).toEqual([]);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('a folder gets the create action; a file does not offer it', async () => {
|
|
282
|
+
stubFetch(server({}));
|
|
283
|
+
await mount({ path: 'src', name: 'src' });
|
|
284
|
+
|
|
285
|
+
await act(async () => {
|
|
286
|
+
mounted?.handleAction('new_file');
|
|
287
|
+
});
|
|
288
|
+
expect(mounted?.showCreateModal).toBe(true);
|
|
289
|
+
|
|
290
|
+
await act(async () => {
|
|
291
|
+
mounted?.closeCreate();
|
|
292
|
+
});
|
|
293
|
+
expect(mounted?.showCreateModal).toBe(false);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('useFileActions rename and delete', () => {
|
|
298
|
+
test('rename sends the new path and closes on success', async () => {
|
|
299
|
+
const srv = server({ success: true });
|
|
300
|
+
stubFetch(srv);
|
|
301
|
+
await mount({ path: 'src/old.ts', name: 'old.ts' });
|
|
302
|
+
|
|
303
|
+
await act(async () => {
|
|
304
|
+
mounted?.handleAction('rename');
|
|
305
|
+
});
|
|
306
|
+
await act(async () => {
|
|
307
|
+
mounted?.setRenameValue('src/new.ts');
|
|
308
|
+
});
|
|
309
|
+
await act(async () => {
|
|
310
|
+
mounted?.submitRename({ preventDefault() {} } as never);
|
|
311
|
+
});
|
|
312
|
+
await settle();
|
|
313
|
+
|
|
314
|
+
expect(srv.requests[0]).toEqual({ actionType: 'rename', path: 'src/old.ts', newPath: 'src/new.ts', root: '/root' });
|
|
315
|
+
expect(mounted?.showRenameModal).toBe(false);
|
|
316
|
+
expect(refreshes).toBe(1);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('delete posts the delete action for the row’s own path', async () => {
|
|
320
|
+
const srv = server({ success: true });
|
|
321
|
+
stubFetch(srv);
|
|
322
|
+
await mount({ path: 'src/gone.ts', name: 'gone.ts' });
|
|
323
|
+
|
|
324
|
+
await act(async () => {
|
|
325
|
+
mounted?.submitDelete();
|
|
326
|
+
});
|
|
327
|
+
await settle();
|
|
328
|
+
|
|
329
|
+
expect(srv.requests[0]).toEqual({ actionType: 'delete', path: 'src/gone.ts', root: '/root' });
|
|
330
|
+
expect(refreshes).toBe(1);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('a git-history response does not close the dialogs or refresh the tree', async () => {
|
|
334
|
+
// `type` marks the history payload; treating it as a mutation would refresh
|
|
335
|
+
// the tree under an open modal.
|
|
336
|
+
const srv = server({ success: true, type: 'history', data: [] } as never);
|
|
337
|
+
stubFetch(srv);
|
|
338
|
+
await mount({ path: 'src/a.ts', name: 'a.ts' });
|
|
339
|
+
|
|
340
|
+
await act(async () => {
|
|
341
|
+
mounted?.handleAction('history');
|
|
342
|
+
});
|
|
343
|
+
await settle();
|
|
344
|
+
|
|
345
|
+
expect(mounted?.showHistoryModal).toBe(true);
|
|
346
|
+
expect(refreshes).toBe(0);
|
|
347
|
+
});
|
|
348
|
+
});
|