ompchamber 1.2.1 → 2.0.1
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 +143 -147
- package/dist/client/index.html +4 -2
- package/dist/client/static/css/index.5f630421b7.css +2 -0
- package/dist/client/static/js/77.0d81c74cf0.js +139 -0
- package/dist/client/static/js/async/1k.973e8e21fc.js +2 -0
- package/dist/client/static/js/async/{3y.3186ecdd63.js → 3y.3530b824ad.js} +6 -6
- package/dist/client/static/js/async/{q.b453193538.js → 4m.928a1dde85.js} +10 -10
- package/dist/client/static/js/async/66.5dfa29f957.js +93 -0
- package/dist/client/static/js/async/7g.a7814350c3.js +1 -0
- package/dist/client/static/js/async/ar.9303e5f2e8.js +2 -0
- package/dist/client/static/js/async/{cx.9c8268d542.js → cx.3cb0242a3a.js} +1 -1
- package/dist/client/static/js/async/e.93214d622b.js +79 -0
- package/dist/client/static/js/async/g.cb81980498.js +2 -0
- package/dist/client/static/js/async/h1.393f066d09.js +1 -0
- package/dist/client/static/js/async/jl.874d352278.js +1 -0
- package/dist/client/static/js/async/{k.d16cd09ae7.js → k.3210286401.js} +1 -1
- package/dist/client/static/js/async/{m.2e1ea3db9e.js → m.eb2e6752e2.js} +3 -3
- package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/mt.0352e976d8.js +1 -0
- package/dist/client/static/js/async/{oc.eb22093f05.js → oc.b73042e8bd.js} +1 -1
- package/dist/client/static/js/async/{og.53f068804c.js → og.8eb577893a.js} +1 -1
- package/dist/client/static/js/async/px.fc6877924b.js +1 -0
- package/dist/client/static/js/async/q.22588845a9.js +21 -0
- package/dist/client/static/js/async/{q5.88b50ffa95.js → q5.97dc1481b6.js} +1 -1
- package/dist/client/static/js/async/s.0f395cdda5.js +94 -0
- package/dist/client/static/js/async/{tr.2a0d2b7600.js → tr.e878658140.js} +1 -1
- package/dist/client/static/js/async/vt.42416b707d.js +1 -0
- package/dist/client/static/js/async/x1.da8687159b.js +90 -0
- package/dist/client/static/js/async/xe.da8dcc2015.js +2 -0
- package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/{xn.6360379f71.js → xn.6510c0abe2.js} +2 -2
- package/dist/client/static/js/async/{yx.af2936c368.js → xn1.83a7784fe1.js} +3 -3
- package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/yg.35bebd6d0c.js +149 -0
- package/dist/client/static/js/async/yv.d99d829136.js +1 -0
- package/dist/client/static/js/index.1fd9e1c543.js +38 -0
- package/package.json +3 -3
- package/src/client/components/common/ComposerTextarea.tsx +9 -1
- package/src/client/components/common/MarkdownRenderer.tsx +50 -12
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +9 -3
- package/src/client/components/settings/Modal.tsx +11 -2
- package/src/client/components/settings/categories/AppearanceSettings.tsx +7 -30
- package/src/client/components/settings/categories/ProviderSettings.tsx +2 -0
- package/src/client/components/settings/categories/appearance-settings/ThemeCard.tsx +61 -0
- package/src/client/components/settings/categories/appearance-settings/ThemeGrid.tsx +80 -0
- package/src/client/components/settings/categories/chat-settings/TitleSection.tsx +1 -1
- package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +43 -6
- package/src/client/components/settings/categories/provider-settings/ModelConfigModal.tsx +54 -54
- package/src/client/components/workspace/SearchPanel.tsx +16 -3
- package/src/client/components/workspace/btw-panel/AskCard.tsx +11 -6
- package/src/client/components/workspace/btw-panel/Messages.tsx +105 -51
- package/src/client/components/workspace/btw-panel/index.tsx +124 -45
- package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +30 -14
- package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +10 -1
- package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +8 -2
- package/src/client/components/workspace/chat-timeline/MessageItem.tsx +27 -16
- package/src/client/components/workspace/chat-timeline/MessageList.tsx +11 -0
- package/src/client/components/workspace/chat-timeline/RunFooter.tsx +8 -0
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +12 -1
- package/src/client/components/workspace/chat-timeline/index.tsx +7 -2
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/QuestionBlock.tsx +42 -15
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.tsx +13 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/index.tsx +13 -4
- package/src/client/components/workspace/diff-panel/SplitView.tsx +4 -1
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +70 -65
- package/src/client/components/workspace/diff-panel/index.tsx +8 -1
- package/src/client/components/workspace/editor/index.tsx +18 -2
- package/src/client/components/workspace/file-explorer/GitRepoDropdown.tsx +15 -41
- package/src/client/components/workspace/file-explorer/index.tsx +86 -16
- package/src/client/components/workspace/git-panel/index.tsx +60 -78
- package/src/client/components/workspace/terminal-panel/Header.tsx +16 -1
- package/src/client/components/workspace/terminal-panel/RealtimeXtermView.tsx +5 -5
- package/src/client/components/workspace/terminal-panel/index.tsx +6 -2
- package/src/client/components/workspace/terminal-panel/mount.ts +2 -2
- package/src/client/data/theme/terminal.ts +50 -52
- package/src/client/hooks/chat/btw/index.ts +139 -18
- package/src/client/hooks/chat/btw/intercept.ts +8 -8
- package/src/client/hooks/chat/btw/mode.ts +111 -16
- package/src/client/hooks/chat/composer/index.ts +10 -4
- package/src/client/hooks/chat/timeline/access-mode.ts +41 -0
- package/src/client/hooks/chat/timeline/ask-frames.test.ts +87 -0
- package/src/client/hooks/chat/timeline/ask-frames.ts +20 -1
- package/src/client/hooks/chat/timeline/auto-scroll.ts +13 -4
- package/src/client/hooks/chat/timeline/index.ts +8 -20
- package/src/client/hooks/chat/timeline/queue.ts +66 -9
- package/src/client/hooks/chat/timeline/scroll.test.ts +260 -0
- package/src/client/hooks/chat/timeline/scroll.ts +93 -20
- package/src/client/hooks/chat/timeline/session-load.ts +7 -1
- package/src/client/hooks/chat/timeline/session-pagination.test.ts +167 -0
- package/src/client/hooks/chat/timeline/session-pagination.ts +37 -11
- package/src/client/hooks/editor/save-file.ts +62 -0
- package/src/client/hooks/editor/use-file-editor.test.ts +61 -5
- package/src/client/hooks/editor/use-file-editor.ts +38 -29
- package/src/client/hooks/editor/use-file-transfer.ts +5 -2
- package/src/client/hooks/settings/providers.ts +34 -3
- package/src/client/hooks/ui/open-file.ts +7 -1
- package/src/client/hooks/ui/theme.ts +82 -57
- package/src/client/hooks/workspace/file-tabs.ts +5 -1
- package/src/client/hooks/workspace/repo-scope/index.ts +119 -0
- package/src/client/hooks/workspace/repo-scope/store.test.ts +161 -0
- package/src/client/hooks/workspace/repo-scope/store.ts +201 -0
- package/src/client/hooks/workspace/search-stream.ts +24 -5
- package/src/client/main.tsx +6 -0
- package/src/client/tailwind.css +19 -56
- package/src/server/lib/btw/child.server.ts +41 -0
- package/src/server/lib/btw/lifecycle.server.ts +254 -0
- package/src/server/lib/btw/prompt.ts +13 -5
- package/src/server/lib/btw/registry.server.ts +13 -4
- package/src/server/lib/btw/runtime.server.ts +149 -156
- package/src/server/lib/btw/schema.server.ts +27 -0
- package/src/server/lib/btw/service.server.ts +106 -5
- package/src/server/lib/btw/store.server.ts +57 -6
- package/src/server/lib/db/schema.ts +4 -0
- package/src/server/lib/db/settings-store.test.ts +65 -0
- package/src/server/lib/db/settings-store.ts +25 -0
- package/src/server/lib/fs/atomic-write.ts +42 -4
- package/src/server/lib/fs/config-lock.ts +74 -0
- package/src/server/lib/fs/git-ignore.ts +55 -5
- package/src/server/lib/fs/ripgrep.ts +119 -0
- package/src/server/lib/lifecycle/identity.ts +10 -64
- package/src/server/lib/lifecycle/proc/darwin.ts +238 -0
- package/src/server/lib/lifecycle/proc/index.ts +38 -0
- package/src/server/lib/lifecycle/proc/linux.ts +80 -0
- package/src/server/lib/lifecycle/proc/ps.ts +86 -0
- package/src/server/lib/lifecycle/proc/types.ts +33 -0
- package/src/server/lib/lifecycle/proc/win32.ts +53 -0
- package/src/server/lib/models/provider-registry.server.ts +2 -2
- package/src/server/lib/models/registry.server.ts +1 -1
- package/src/server/lib/omp/config/behavior.ts +17 -17
- package/src/server/lib/omp/config/disabled-providers.ts +84 -35
- package/src/server/lib/omp/config/document.ts +128 -0
- package/src/server/lib/omp/config/extensions.ts +11 -10
- package/src/server/lib/omp/config/mcp.ts +4 -57
- package/src/server/lib/omp/config/model-overrides.ts +123 -0
- package/src/server/lib/omp/config/models-config.ts +92 -0
- package/src/server/lib/omp/config/models-validation.ts +159 -0
- package/src/server/lib/omp/config/path-scoped.ts +74 -0
- package/src/server/lib/omp/config/providers.ts +241 -211
- package/src/server/lib/omp/config/roles.ts +6 -14
- package/src/server/lib/omp/core/auth-credentials.ts +1 -1
- package/src/server/lib/omp/rpc/frame-fold.test.ts +124 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +43 -24
- package/src/server/lib/omp/rpc/lifecycle.test.ts +41 -0
- package/src/server/lib/omp/rpc/manager.ts +21 -21
- package/src/server/lib/omp/rpc/pending-ui-dialogs.ts +11 -7
- package/src/server/lib/omp/rpc/session-commands.ts +17 -29
- package/src/server/lib/omp/rpc/session-registry.ts +5 -66
- package/src/server/lib/omp/rpc/web-state.ts +0 -6
- package/src/server/lib/omp/session/auto-title.server.test.ts +109 -11
- package/src/server/lib/omp/session/auto-title.server.ts +95 -20
- package/src/server/lib/omp/session/sidebar-data.server.ts +11 -6
- package/src/server/lib/queue/delivery-dispatch.server.test.ts +162 -0
- package/src/server/lib/queue/delivery-harness.ts +88 -0
- package/src/server/lib/queue/delivery.server.test.ts +78 -149
- package/src/server/lib/queue/delivery.server.ts +105 -13
- package/src/server/lib/queue/store.server.ts +30 -5
- package/src/server/lib/terminal/foreground.test.ts +164 -0
- package/src/server/lib/terminal/foreground.ts +34 -6
- package/src/server/lib/terminal/history.ts +8 -7
- package/src/server/plugins/compress.ts +33 -5
- package/src/server/plugins/ssr.ts +14 -7
- package/src/server/routes/btw/topics.ts +89 -2
- package/src/server/routes/fs/action.test.ts +64 -0
- package/src/server/routes/fs/action.ts +10 -1
- package/src/server/routes/fs/git.ts +5 -6
- package/src/server/routes/fs/search.ts +17 -26
- package/src/server/routes/sessions/queue.ts +17 -4
- package/src/server/routes/settings/index.ts +2 -0
- package/src/server/routes/settings/model-override.ts +69 -0
- package/src/server/routes/settings/provider-models.ts +35 -12
- package/src/server/routes/settings/providers.ts +31 -1
- package/src/server/routes/settings/root.ts +8 -3
- package/src/shared/lib/chat/btw/client.ts +34 -0
- package/src/shared/lib/chat/timeline/tool-phrases.ts +7 -0
- package/src/shared/lib/code/line-endings.test.ts +38 -0
- package/src/shared/lib/code/line-endings.ts +56 -0
- package/src/shared/lib/code/shiki-themes.ts +9 -5
- package/src/shared/lib/fs/file-tree.ts +17 -12
- package/src/shared/lib/markdown/autolink.test.ts +32 -0
- package/src/shared/lib/markdown/autolink.ts +50 -0
- package/src/shared/lib/markdown/document-urls.test.ts +68 -0
- package/src/shared/lib/markdown/document-urls.ts +118 -0
- package/src/shared/lib/markdown/marked.ts +27 -15
- package/src/shared/lib/markdown/mermaid.ts +2 -2
- package/src/shared/lib/models/catalog.ts +1 -1
- package/src/shared/lib/omp/session/schema-migrations.ts +30 -0
- package/src/shared/lib/omp/session/stream-state.server.test.ts +54 -0
- package/src/shared/lib/omp/session/stream-state.server.ts +70 -21
- package/src/shared/lib/settings/client.ts +10 -4
- package/src/shared/lib/settings/diff.test.ts +31 -0
- package/src/shared/lib/settings/diff.ts +37 -0
- package/src/shared/lib/theme/catalog.test.ts +168 -0
- package/src/shared/lib/theme/catalog.ts +98 -0
- package/src/shared/lib/theme/color.ts +88 -0
- package/src/shared/lib/theme/css.ts +70 -0
- package/src/shared/lib/theme/palettes/chamber.ts +61 -0
- package/src/shared/lib/theme/palettes/dark.ts +313 -0
- package/src/shared/lib/theme/palettes/light.ts +313 -0
- package/src/shared/types/btw.ts +83 -4
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/settings/provider.ts +8 -1
- package/src/shared/types/settings/state.ts +10 -4
- package/src/shared/types/theme.ts +48 -0
- package/dist/client/static/css/index.b938f4a693.css +0 -2
- package/dist/client/static/js/77.8022a800b5.js +0 -139
- package/dist/client/static/js/async/1k.a411fe5b43.js +0 -2
- package/dist/client/static/js/async/66.86544ad741.js +0 -93
- package/dist/client/static/js/async/7g.db82aca7ba.js +0 -1
- package/dist/client/static/js/async/ar.ad5c8822a8.js +0 -2
- package/dist/client/static/js/async/e.6443e739a2.js +0 -79
- package/dist/client/static/js/async/g.ee41bfed7f.js +0 -2
- package/dist/client/static/js/async/h1.d7091cef78.js +0 -1
- package/dist/client/static/js/async/jl.4911fa3383.js +0 -1
- package/dist/client/static/js/async/m.2e1ea3db9e.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/mt.853be2b5aa.js +0 -1
- package/dist/client/static/js/async/ot.84c71b308f.js +0 -2
- package/dist/client/static/js/async/px.783b6f91f2.js +0 -1
- package/dist/client/static/js/async/q8.7f1b48e9cd.js +0 -21
- package/dist/client/static/js/async/q8.7f1b48e9cd.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/s.aad2ecebc6.js +0 -94
- package/dist/client/static/js/async/vt.20406a5604.js +0 -1
- package/dist/client/static/js/async/x1.fe8af17016.js +0 -90
- package/dist/client/static/js/async/xn.6360379f71.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/yg.5a94e1fc8a.js +0 -149
- package/dist/client/static/js/async/yv.2fa22f009a.js +0 -1
- package/dist/client/static/js/index.5fe7a2cb64.js +0 -34
- package/src/client/components/workspace/btw-panel/Composer.tsx +0 -183
- package/src/client/components/workspace/btw-panel/attachments.ts +0 -80
- /package/dist/client/static/js/{77.8022a800b5.js.LICENSE.txt → 77.0d81c74cf0.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{g.ee41bfed7f.js.LICENSE.txt → 1k.973e8e21fc.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{3y.3186ecdd63.js.LICENSE.txt → 3y.3530b824ad.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{ot.84c71b308f.js.LICENSE.txt → 4m.928a1dde85.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{q.b453193538.js.LICENSE.txt → ar.9303e5f2e8.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{yx.af2936c368.js.LICENSE.txt → g.cb81980498.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{1k.a411fe5b43.js.LICENSE.txt → q.22588845a9.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{ar.ad5c8822a8.js.LICENSE.txt → xn.6510c0abe2.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{index.5fe7a2cb64.js.LICENSE.txt → index.1fd9e1c543.js.LICENSE.txt} +0 -0
|
@@ -5,6 +5,10 @@ export interface OpenFilePayload {
|
|
|
5
5
|
name?: string;
|
|
6
6
|
id?: number;
|
|
7
7
|
content?: string;
|
|
8
|
+
/** Absolute workspace root the path belongs to; the panel defaults it to the active project. */
|
|
9
|
+
root?: string;
|
|
10
|
+
/** Repo picked in the panel, relative to `root` — the path's own scope when set. */
|
|
11
|
+
repo?: string;
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
/**
|
|
@@ -26,7 +30,9 @@ export function openFileInEditor(payloadOrPath: string | OpenFilePayload) {
|
|
|
26
30
|
path: cleanPath,
|
|
27
31
|
name: fileName,
|
|
28
32
|
id: payload.id,
|
|
29
|
-
content: payload.content
|
|
33
|
+
content: payload.content,
|
|
34
|
+
root: payload.root,
|
|
35
|
+
repo: payload.repo,
|
|
30
36
|
}
|
|
31
37
|
})
|
|
32
38
|
);
|
|
@@ -1,92 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The single writer for the document's theme.
|
|
8
|
+
*
|
|
9
|
+
* `data-theme` (the palette id) and `data-theme-variant` (light/dark) are
|
|
10
|
+
* written together, and both are read from the catalog: the variant used to be
|
|
11
|
+
* a hardcoded id list here, in the Shiki selector, in the mermaid hydrator and
|
|
12
|
+
* in the SSR shell — four copies that a new theme would have silently missed.
|
|
13
|
+
*
|
|
14
|
+
* The `data-theme` attribute is the durable copy of the palette; the
|
|
15
|
+
* `omp:theme-changed` event is the notification. CSS reads the attribute, and
|
|
16
|
+
* the mermaid hydrator observes it, so a write that skips the event still
|
|
17
|
+
* recolors — but a write that skips the attribute does not.
|
|
18
|
+
*/
|
|
19
|
+
|
|
1
20
|
import { useCallback, useEffect, useState } from 'preact/hooks';
|
|
2
21
|
import { useChamberSettingsWriter } from '@/client/hooks/settings/use-chamber-setting';
|
|
22
|
+
import { DEFAULT_THEME_ID, resolveTheme } from '@/shared/lib/theme/catalog';
|
|
23
|
+
import { THEME_STYLE_ELEMENT_ID, themeStyleSheet } from '@/shared/lib/theme/css';
|
|
3
24
|
|
|
4
|
-
/** Broadcast on every theme write. Detail is the new theme id
|
|
5
|
-
* `data-theme` attribute on `<html>` is the durable copy of the same fact. */
|
|
25
|
+
/** Broadcast on every theme write. Detail is the new theme id. */
|
|
6
26
|
export const THEME_CHANGED_EVENT = 'omp:theme-changed';
|
|
7
27
|
|
|
28
|
+
/** The palette id currently on `<html>`, falling back to the shipped default. */
|
|
29
|
+
export function currentThemeId(): string {
|
|
30
|
+
if (typeof document === 'undefined') return DEFAULT_THEME_ID;
|
|
31
|
+
return document.documentElement.dataset.theme || document.documentElement.getAttribute('data-theme') || DEFAULT_THEME_ID;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Put the catalog on the document before the first render.
|
|
36
|
+
*
|
|
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 `dist/client/
|
|
39
|
+
* index.html` left over from an older build while only the server was restarted.
|
|
40
|
+
* Without it the page keeps `:root`'s default palette and no variant attribute,
|
|
41
|
+
* which is a wrong palette, not merely a stale one.
|
|
42
|
+
*
|
|
43
|
+
* Called once from `main.tsx` rather than from a hook: the only components that
|
|
44
|
+
* read the theme are lazily mounted panels, so a hook would not run until the
|
|
45
|
+
* user opened one.
|
|
46
|
+
*/
|
|
47
|
+
export function initDocumentTheme(): void {
|
|
48
|
+
if (typeof document === 'undefined') return;
|
|
49
|
+
if (!document.getElementById(THEME_STYLE_ELEMENT_ID)) {
|
|
50
|
+
const style = document.createElement('style');
|
|
51
|
+
style.id = THEME_STYLE_ELEMENT_ID;
|
|
52
|
+
style.textContent = themeStyleSheet();
|
|
53
|
+
document.head.appendChild(style);
|
|
54
|
+
}
|
|
55
|
+
applyDocumentTheme(currentThemeId());
|
|
56
|
+
}
|
|
57
|
+
|
|
8
58
|
/**
|
|
9
59
|
* Point the document at `theme` and announce it.
|
|
10
60
|
*
|
|
11
|
-
* The single writer path for the theme: the
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* previous
|
|
15
|
-
* watch the attribute too, so this stays correct even if the event is missed.
|
|
61
|
+
* The single writer path for the theme: the settings modal and every future
|
|
62
|
+
* picker call this, because a writer that only sets `data-theme` leaves runtime
|
|
63
|
+
* consumers (the mermaid hydrator re-renders diagrams per palette) on the
|
|
64
|
+
* previous colors until the next reload.
|
|
16
65
|
*/
|
|
17
66
|
export function applyDocumentTheme(theme: string): void {
|
|
18
67
|
if (typeof document === 'undefined') return;
|
|
19
|
-
|
|
20
|
-
|
|
68
|
+
const palette = resolveTheme(theme);
|
|
69
|
+
const root = document.documentElement;
|
|
70
|
+
root.dataset.theme = palette.id;
|
|
71
|
+
root.dataset.themeVariant = palette.variant;
|
|
72
|
+
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', palette.canvas);
|
|
73
|
+
window.dispatchEvent(new CustomEvent(THEME_CHANGED_EVENT, { detail: palette.id }));
|
|
21
74
|
}
|
|
22
75
|
|
|
23
76
|
export function useTheme() {
|
|
24
77
|
const writeChamberSettings = useChamberSettingsWriter();
|
|
25
|
-
const [theme, setThemeState] = useState<string>(
|
|
26
|
-
if (typeof document !== 'undefined') {
|
|
27
|
-
return document.documentElement.dataset.theme || document.documentElement.getAttribute('data-theme') || 'paper';
|
|
28
|
-
}
|
|
29
|
-
return 'paper';
|
|
30
|
-
});
|
|
78
|
+
const [theme, setThemeState] = useState<string>(currentThemeId);
|
|
31
79
|
|
|
32
|
-
const isDark = theme === '
|
|
80
|
+
const isDark = resolveTheme(theme).variant === 'dark';
|
|
33
81
|
|
|
34
82
|
useEffect(() => {
|
|
35
83
|
if (typeof document === 'undefined') return;
|
|
36
84
|
|
|
37
|
-
const updateTheme = () => {
|
|
38
|
-
const current = document.documentElement.dataset.theme || document.documentElement.getAttribute('data-theme') || 'paper';
|
|
39
|
-
setThemeState(current);
|
|
40
|
-
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
|
|
41
|
-
if (metaThemeColor) {
|
|
42
|
-
metaThemeColor.setAttribute('content', current === 'one-dark-pro-soft' ? '#282c34' : '#faf8f3');
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
updateTheme();
|
|
47
|
-
|
|
48
85
|
const observer = new MutationObserver((mutations) => {
|
|
49
86
|
for (const mutation of mutations) {
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
(mutation.attributeName === 'data-theme' || mutation.attributeName === 'class')
|
|
53
|
-
) {
|
|
54
|
-
updateTheme();
|
|
87
|
+
if (mutation.type === 'attributes' && mutation.attributeName === 'data-theme') {
|
|
88
|
+
setThemeState(currentThemeId());
|
|
55
89
|
}
|
|
56
90
|
}
|
|
57
91
|
});
|
|
92
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
58
93
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
const handleCustomChange = (e: Event) => {
|
|
65
|
-
const customEvent = e as CustomEvent<string>;
|
|
66
|
-
if (customEvent.detail) {
|
|
67
|
-
setThemeState(customEvent.detail);
|
|
68
|
-
} else {
|
|
69
|
-
updateTheme();
|
|
70
|
-
}
|
|
94
|
+
const handleCustomChange = (event: Event) => {
|
|
95
|
+
const detail = (event as CustomEvent<string>).detail;
|
|
96
|
+
setThemeState(detail ? resolveTheme(detail).id : currentThemeId());
|
|
71
97
|
};
|
|
72
|
-
|
|
73
|
-
window.addEventListener('omp:theme-changed', handleCustomChange);
|
|
98
|
+
window.addEventListener(THEME_CHANGED_EVENT, handleCustomChange);
|
|
74
99
|
|
|
75
100
|
return () => {
|
|
76
101
|
observer.disconnect();
|
|
77
|
-
window.removeEventListener(
|
|
102
|
+
window.removeEventListener(THEME_CHANGED_EVENT, handleCustomChange);
|
|
78
103
|
};
|
|
79
104
|
}, []);
|
|
80
105
|
|
|
81
|
-
const setTheme = useCallback(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
const setTheme = useCallback(
|
|
107
|
+
(newTheme: string) => {
|
|
108
|
+
const id = resolveTheme(newTheme).id;
|
|
109
|
+
applyDocumentTheme(id);
|
|
110
|
+
setThemeState(id);
|
|
111
|
+
writeChamberSettings({ theme: id });
|
|
112
|
+
},
|
|
113
|
+
[writeChamberSettings],
|
|
114
|
+
);
|
|
90
115
|
|
|
91
|
-
return { theme, isDark, setTheme
|
|
116
|
+
return { theme, isDark, setTheme };
|
|
92
117
|
}
|
|
@@ -93,7 +93,7 @@ export function useFileTabs(
|
|
|
93
93
|
|
|
94
94
|
// Global listeners for omp:open-file and omp:open-diff
|
|
95
95
|
useChamberEvent('omp:open-file', (e) => {
|
|
96
|
-
const customEvent = e as CustomEvent<{ path: string; name?: string; id?: number; content?: string }>;
|
|
96
|
+
const customEvent = e as CustomEvent<{ path: string; name?: string; id?: number; content?: string; root?: string; repo?: string }>;
|
|
97
97
|
if (!customEvent.detail || !customEvent.detail.path) return;
|
|
98
98
|
|
|
99
99
|
const rawPath = customEvent.detail.path.replace(/^\/+/, '');
|
|
@@ -110,6 +110,10 @@ export function useFileTabs(
|
|
|
110
110
|
name,
|
|
111
111
|
path: rawPath,
|
|
112
112
|
content: customEvent.detail.content,
|
|
113
|
+
// A caller that knows its own scope (a preview's relative link) keeps it;
|
|
114
|
+
// otherwise the file belongs to the active project.
|
|
115
|
+
root: customEvent.detail.root,
|
|
116
|
+
repo: customEvent.detail.repo,
|
|
113
117
|
});
|
|
114
118
|
});
|
|
115
119
|
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Nested-repo scope for the right-panel views (files, search, git, terminal).
|
|
8
|
+
*
|
|
9
|
+
* Every one of them needs the same two things, and both are meaningless without
|
|
10
|
+
* the workspace root they describe:
|
|
11
|
+
*
|
|
12
|
+
* - WHICH repo the view operates on (`useRepoScope`). A repo is a path
|
|
13
|
+
* relative to the active root — `projects/jns6_5/token` only names a
|
|
14
|
+
* directory under `~/JatisMobile/Workspace` — so a choice is only valid
|
|
15
|
+
* next to the root it was made under.
|
|
16
|
+
* - WHAT repos exist under that root (`useRepoList`), discovered server-side
|
|
17
|
+
* and stored with the root it was discovered for (see `./store`).
|
|
18
|
+
*
|
|
19
|
+
* Kept as bare per-panel values, both outlived the workspace they belonged to:
|
|
20
|
+
* after a workspace switch the picker still listed the previous workspace's
|
|
21
|
+
* repos, still showed one of them as the selection, and every request the panel
|
|
22
|
+
* made named a path that did not exist under the new root. Pairing each value
|
|
23
|
+
* with its root invalidates it by construction — no reset effect to order
|
|
24
|
+
* against the per-session state restore — while a session returned to inside
|
|
25
|
+
* the same workspace keeps the repo it was left on.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { useCallback, useRef, useSyncExternalStore } from 'preact/compat';
|
|
29
|
+
import { useSessionState } from '@/client/hooks/workspace/session-state';
|
|
30
|
+
import { repoStore, UNKNOWN_LIST } from '@/client/hooks/workspace/repo-scope/store';
|
|
31
|
+
|
|
32
|
+
/** A repo choice, kept next to the workspace root it was made under. */
|
|
33
|
+
interface RepoPick {
|
|
34
|
+
root: string;
|
|
35
|
+
repo: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The repo a git panel works on: the user's pick, or — when they have none and
|
|
40
|
+
* the workspace root is not itself a repo — the first repo discovery found.
|
|
41
|
+
*
|
|
42
|
+
* The git loader applies the same fallback when it is sent no repo at all
|
|
43
|
+
* (`repo = repos.includes('.') ? '.' : repos[0]`), so resolving it here is what
|
|
44
|
+
* keeps the header label, the picker's checkmark and the requests all naming
|
|
45
|
+
* the repo git is actually operating on. Only git resolves it: browsing the
|
|
46
|
+
* workspace root is legitimate even when that root is not a repo — a folder of
|
|
47
|
+
* nested repos is a normal workspace — so the files, search and terminal panels
|
|
48
|
+
* keep `'.'` as the root itself.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveRepoForPanel(pick: string, repos: readonly string[]): string {
|
|
51
|
+
if (pick !== '.' || repos.includes('.')) return pick;
|
|
52
|
+
return repos[0] ?? '.';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The repo this panel operates on, restored per session but dropped the moment
|
|
57
|
+
* the active workspace root changes.
|
|
58
|
+
*/
|
|
59
|
+
export function useRepoScope(
|
|
60
|
+
rootPath: string | undefined,
|
|
61
|
+
stateKey: string,
|
|
62
|
+
): { activeRepo: string; setActiveRepo: (repo: string) => void; ready: boolean } {
|
|
63
|
+
const [pick, setPick, ready] = useSessionState<RepoPick | null>(stateKey, null);
|
|
64
|
+
const scope = rootPath ?? '';
|
|
65
|
+
|
|
66
|
+
// Read through refs so the setter keeps ONE identity: it is handed to child
|
|
67
|
+
// components and used in effect dependencies, and `useSessionState` mints a
|
|
68
|
+
// fresh setter per render.
|
|
69
|
+
const scopeRef = useRef(scope);
|
|
70
|
+
scopeRef.current = scope;
|
|
71
|
+
const setPickRef = useRef(setPick);
|
|
72
|
+
setPickRef.current = setPick;
|
|
73
|
+
const setActiveRepo = useCallback((repo: string) => {
|
|
74
|
+
setPickRef.current({ root: scopeRef.current, repo });
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
// A pick made under another root (or stored before the root was recorded
|
|
78
|
+
// alongside it) names a directory that need not exist here: fall back to the
|
|
79
|
+
// workspace root.
|
|
80
|
+
const activeRepo =
|
|
81
|
+
pick && typeof pick === 'object' && pick.root === scope && typeof pick.repo === 'string' && pick.repo
|
|
82
|
+
? pick.repo
|
|
83
|
+
: '.';
|
|
84
|
+
|
|
85
|
+
return { activeRepo, setActiveRepo, ready };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface RepoListHandle {
|
|
89
|
+
/** Repos of the active root; the workspace root alone until discovery answers. */
|
|
90
|
+
repos: string[];
|
|
91
|
+
/** Discovery is still running — the server said so, or a rescan is in flight. */
|
|
92
|
+
scanning: boolean;
|
|
93
|
+
/** Re-run discovery from scratch (the picker's refresh button). */
|
|
94
|
+
rescan: () => void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Discovered repos of the active root.
|
|
99
|
+
*
|
|
100
|
+
* @param active - when false nothing is subscribed; a hidden panel has no use
|
|
101
|
+
* for a list that costs the server a `find` over the workspace.
|
|
102
|
+
*/
|
|
103
|
+
export function useRepoList(rootPath: string | undefined, active: boolean): RepoListHandle {
|
|
104
|
+
const scope = rootPath ?? '';
|
|
105
|
+
const subscribe = useCallback(
|
|
106
|
+
(listener: () => void) => (active ? repoStore.subscribe(scope, listener) : () => {}),
|
|
107
|
+
[scope, active],
|
|
108
|
+
);
|
|
109
|
+
const getSnapshot = useCallback(
|
|
110
|
+
() => (active ? repoStore.snapshot(scope) : UNKNOWN_LIST),
|
|
111
|
+
[scope, active],
|
|
112
|
+
);
|
|
113
|
+
const snapshot = useSyncExternalStore(subscribe, getSnapshot);
|
|
114
|
+
const rescan = useCallback(() => {
|
|
115
|
+
if (active) repoStore.rescan(scope);
|
|
116
|
+
}, [scope, active]);
|
|
117
|
+
|
|
118
|
+
return { repos: snapshot.repos, scanning: snapshot.scanning, rescan };
|
|
119
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from 'bun:test';
|
|
7
|
+
|
|
8
|
+
import { createRepoStore, type RepoFetch, type RepoScheduler } from '@/client/hooks/workspace/repo-scope/store';
|
|
9
|
+
|
|
10
|
+
/** Resolves each request from a per-root table, recording the roots asked for. */
|
|
11
|
+
function fakeFetch(byRoot: Record<string, { repos: string[]; reposPending?: boolean }>) {
|
|
12
|
+
const requested: string[] = [];
|
|
13
|
+
const fetchImpl: RepoFetch = async (url) => {
|
|
14
|
+
const root = new URL(url, 'http://localhost').searchParams.get('root') ?? '';
|
|
15
|
+
requested.push(root);
|
|
16
|
+
return byRoot[root] ?? { repos: ['.'] };
|
|
17
|
+
};
|
|
18
|
+
return { fetchImpl, requested };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Retries the caller arms, so a test can fire them instead of waiting them out. */
|
|
22
|
+
function fakeScheduler() {
|
|
23
|
+
const armed: (() => void)[] = [];
|
|
24
|
+
const scheduler: RepoScheduler = (run) => {
|
|
25
|
+
armed.push(run);
|
|
26
|
+
return () => {
|
|
27
|
+
const index = armed.indexOf(run);
|
|
28
|
+
if (index >= 0) armed.splice(index, 1);
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
return { scheduler, armed };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('repo list invalidation on workspace switch', () => {
|
|
35
|
+
test('a new root never reports the previous root\'s repos', async () => {
|
|
36
|
+
const { fetchImpl } = fakeFetch({
|
|
37
|
+
'/ws/a': { repos: ['.', 'projects/a'] },
|
|
38
|
+
'/ws/b': { repos: ['.', 'projects/b'] },
|
|
39
|
+
});
|
|
40
|
+
const store = createRepoStore({ fetch: fetchImpl });
|
|
41
|
+
|
|
42
|
+
const seenA: string[][] = [];
|
|
43
|
+
const unsubscribeA = store.subscribe('/ws/a', () => seenA.push(store.snapshot('/ws/a').repos));
|
|
44
|
+
await store.settled('/ws/a');
|
|
45
|
+
expect(store.snapshot('/ws/a').repos).toEqual(['.', 'projects/a']);
|
|
46
|
+
|
|
47
|
+
// Switching workspaces: root B has no entry, so it starts from the root
|
|
48
|
+
// alone — not from A's list.
|
|
49
|
+
expect(store.snapshot('/ws/b').repos).toEqual(['.']);
|
|
50
|
+
unsubscribeA();
|
|
51
|
+
|
|
52
|
+
const seenB: string[][] = [];
|
|
53
|
+
const unsubscribeB = store.subscribe('/ws/b', () => seenB.push(store.snapshot('/ws/b').repos));
|
|
54
|
+
await store.settled('/ws/b');
|
|
55
|
+
expect(store.snapshot('/ws/b').repos).toEqual(['.', 'projects/b']);
|
|
56
|
+
// Every notification for B reported B's list — A's list was never published
|
|
57
|
+
// under B, not even for the render before the read landed.
|
|
58
|
+
expect(seenB).toEqual([['.'], ['.', 'projects/b']]);
|
|
59
|
+
expect(seenA).toEqual([['.'], ['.', 'projects/a']]);
|
|
60
|
+
unsubscribeB();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('a list already discovered for a root is reused without re-asking', async () => {
|
|
64
|
+
const { fetchImpl, requested } = fakeFetch({ '/ws/a': { repos: ['.', 'projects/a'] } });
|
|
65
|
+
const store = createRepoStore({ fetch: fetchImpl });
|
|
66
|
+
|
|
67
|
+
const unsubscribeFirst = store.subscribe('/ws/a', () => {});
|
|
68
|
+
await store.settled('/ws/a');
|
|
69
|
+
unsubscribeFirst();
|
|
70
|
+
expect(requested).toEqual(['/ws/a']);
|
|
71
|
+
|
|
72
|
+
// A right-panel view switch re-subscribes to the same root.
|
|
73
|
+
const unsubscribeSecond = store.subscribe('/ws/a', () => {});
|
|
74
|
+
await store.settled('/ws/a');
|
|
75
|
+
expect(requested).toEqual(['/ws/a']);
|
|
76
|
+
expect(store.snapshot('/ws/a').repos).toEqual(['.', 'projects/a']);
|
|
77
|
+
unsubscribeSecond();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('an unsettled discovery is resumed, not abandoned, when a subscriber returns', async () => {
|
|
81
|
+
const { fetchImpl, requested } = fakeFetch({ '/ws/a': { repos: ['.'], reposPending: true } });
|
|
82
|
+
const { scheduler, armed } = fakeScheduler();
|
|
83
|
+
const store = createRepoStore({ fetch: fetchImpl, scheduler });
|
|
84
|
+
|
|
85
|
+
const unsubscribe = store.subscribe('/ws/a', () => {});
|
|
86
|
+
await store.settled('/ws/a');
|
|
87
|
+
expect(store.snapshot('/ws/a').scanning).toBe(true);
|
|
88
|
+
expect(armed).toHaveLength(1);
|
|
89
|
+
unsubscribe();
|
|
90
|
+
// The retry is dropped with the last subscriber rather than left armed.
|
|
91
|
+
expect(armed).toHaveLength(0);
|
|
92
|
+
|
|
93
|
+
// Re-subscribing resumes the scan instead of inheriting a spinner that
|
|
94
|
+
// nothing drives.
|
|
95
|
+
const unsubscribeAgain = store.subscribe('/ws/a', () => {});
|
|
96
|
+
await store.settled('/ws/a');
|
|
97
|
+
expect(requested).toEqual(['/ws/a', '/ws/a']);
|
|
98
|
+
expect(armed).toHaveLength(1);
|
|
99
|
+
unsubscribeAgain();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('a failed discovery leaves the root unlisted so the next subscriber retries', async () => {
|
|
103
|
+
let calls = 0;
|
|
104
|
+
const store = createRepoStore({
|
|
105
|
+
fetch: async () => {
|
|
106
|
+
calls += 1;
|
|
107
|
+
if (calls === 1) throw new Error('offline');
|
|
108
|
+
return { repos: ['.', 'projects/a'] };
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const unsubscribe = store.subscribe('/ws/a', () => {});
|
|
113
|
+
await store.settled('/ws/a');
|
|
114
|
+
expect(store.snapshot('/ws/a').repos).toEqual(['.']);
|
|
115
|
+
unsubscribe();
|
|
116
|
+
|
|
117
|
+
const unsubscribeAgain = store.subscribe('/ws/a', () => {});
|
|
118
|
+
await store.settled('/ws/a');
|
|
119
|
+
expect(calls).toBe(2);
|
|
120
|
+
expect(store.snapshot('/ws/a').repos).toEqual(['.', 'projects/a']);
|
|
121
|
+
unsubscribeAgain();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('a response that lands after its root lost every subscriber is dropped', async () => {
|
|
125
|
+
const gate = Promise.withResolvers<unknown>();
|
|
126
|
+
const store = createRepoStore({ fetch: () => gate.promise });
|
|
127
|
+
|
|
128
|
+
const unsubscribe = store.subscribe('/ws/a', () => {});
|
|
129
|
+
unsubscribe();
|
|
130
|
+
gate.resolve({ repos: ['.', 'projects/a'] });
|
|
131
|
+
await store.settled('/ws/a');
|
|
132
|
+
|
|
133
|
+
// The read was abandoned with the subscription: nothing is reported for a
|
|
134
|
+
// root nobody is showing.
|
|
135
|
+
expect(store.snapshot('/ws/a').repos).toEqual(['.']);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('a rescan reports the fresh list and clears the pending flag', async () => {
|
|
139
|
+
let repos = ['.'];
|
|
140
|
+
const requested: string[] = [];
|
|
141
|
+
const store = createRepoStore({
|
|
142
|
+
fetch: async (url) => {
|
|
143
|
+
requested.push(new URL(url, 'http://localhost').searchParams.get('rescan') ?? '');
|
|
144
|
+
return { repos };
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const unsubscribe = store.subscribe('/ws/a', () => {});
|
|
149
|
+
await store.settled('/ws/a');
|
|
150
|
+
expect(store.snapshot('/ws/a')).toEqual({ repos: ['.'], scanning: false });
|
|
151
|
+
|
|
152
|
+
// A nested repo that appeared after the first discovery.
|
|
153
|
+
repos = ['.', 'projects/late'];
|
|
154
|
+
store.rescan('/ws/a');
|
|
155
|
+
await store.settled('/ws/a');
|
|
156
|
+
|
|
157
|
+
expect(requested).toEqual(['', '1']);
|
|
158
|
+
expect(store.snapshot('/ws/a')).toEqual({ repos: ['.', 'projects/late'], scanning: false });
|
|
159
|
+
unsubscribe();
|
|
160
|
+
});
|
|
161
|
+
});
|