ompchamber 1.2.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -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.74ff36419f.js +2 -0
- package/dist/client/static/js/async/{m.2e1ea3db9e.js.LICENSE.txt → g.74ff36419f.js.LICENSE.txt} +5 -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.33d6779f98.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/yg.35bebd6d0c.js +149 -0
- package/dist/client/static/js/async/yv.d99d829136.js +1 -0
- package/dist/client/static/js/index.c05535dbde.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/editor/index.tsx +11 -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/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/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/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/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/{1k.a411fe5b43.js.LICENSE.txt → q.33d6779f98.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.c05535dbde.js.LICENSE.txt} +0 -0
|
@@ -1,28 +1,71 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import { RefreshCw, Search } from 'lucide-preact';
|
|
3
|
+
import type { FsNode } from '@/shared/types';
|
|
3
4
|
import { rehydrateTree, setChildrenAt } from '@/shared/lib/fs/file-tree';
|
|
5
|
+
import { isRecord } from '@/shared/lib/util/guards';
|
|
4
6
|
import { GitRepoDropdown } from '@/client/components/workspace/file-explorer/GitRepoDropdown';
|
|
5
7
|
import { FileTreeItem } from '@/client/components/workspace/file-explorer/TreeItem';
|
|
6
8
|
import { useScrollbarFade, scrollbarFadeClass } from '@/client/hooks/ui/scrollbar-fade';
|
|
7
9
|
import { useSessionState } from '@/client/hooks/workspace/session-state';
|
|
8
10
|
import { useGitStatus } from '@/client/hooks/workspace/git-status';
|
|
11
|
+
import { useRepoList, useRepoScope } from '@/client/hooks/workspace/repo-scope';
|
|
9
12
|
import { usePanelRefresh, useFileMutationRefresh } from '@/client/hooks/workspace/panel-refresh';
|
|
10
13
|
|
|
14
|
+
/**
|
|
15
|
+
* The listing on screen, tagged with the `root\0repo` scope it was read for.
|
|
16
|
+
*
|
|
17
|
+
* Tagging it is what makes a workspace switch instant and complete: the panel
|
|
18
|
+
* renders an empty tree until the new root's listing lands, instead of showing
|
|
19
|
+
* the previous workspace's files under the new workspace's header. It also
|
|
20
|
+
* covers what a plain "reset on switch" effect cannot — the children loaded on
|
|
21
|
+
* demand and the expansion set are keyed by paths RELATIVE to the listed root,
|
|
22
|
+
* so `src/` in one workspace would rehydrate into `src/` in the next.
|
|
23
|
+
*/
|
|
24
|
+
interface Listing {
|
|
25
|
+
scope: string;
|
|
26
|
+
files: FsNode[];
|
|
27
|
+
/** Absolute base dir reported by `/api/fs/dir`; Copy Path anchors on it. */
|
|
28
|
+
root: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const EMPTY_LISTING: Listing = { scope: '', files: [], root: '' };
|
|
32
|
+
|
|
33
|
+
/** Directory listing payload from `/api/fs/dir`, validated field by field. */
|
|
34
|
+
function readDirPayload(data: unknown): { files: FsNode[]; root: string } {
|
|
35
|
+
if (!isRecord(data)) return { files: [], root: '' };
|
|
36
|
+
return {
|
|
37
|
+
files: Array.isArray(data.files) ? data.files.filter((f): f is FsNode => isRecord(f)) : [],
|
|
38
|
+
root: typeof data.root === 'string' ? data.root : '',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
11
42
|
export function FileExplorer({ className = '', enabled = true, rootPath, onOpenFile, refreshKey = 0, onRefresh }: { className?: string, enabled?: boolean, rootPath?: string, onOpenFile?: (file: any) => void, refreshKey?: number, onRefresh?: () => void }) {
|
|
12
|
-
const [
|
|
13
|
-
// Absolute base dir reported by `/api/fs/dir` for the current listing — the
|
|
14
|
-
// workspace root, or the selected nested repo. Copy Path anchors on it
|
|
15
|
-
// because the client's `rootPath` may be rejected/unset.
|
|
16
|
-
const [listingRoot, setListingRoot] = useState('');
|
|
43
|
+
const [listing, setListing] = useState<Listing>(EMPTY_LISTING);
|
|
17
44
|
const [searchQuery, setSearchQuery] = useSessionState<string>('files.searchQuery', '');
|
|
18
45
|
const [isLoading, setIsLoading] = useState(false);
|
|
19
46
|
const [storedExpandedPaths, setStoredExpandedPaths, expandedPathsReady] = useSessionState<string[]>('files.expandedPaths', []);
|
|
20
47
|
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => new Set(storedExpandedPaths));
|
|
21
|
-
const childrenCacheRef = useRef<Record<string,
|
|
22
|
-
const
|
|
48
|
+
const childrenCacheRef = useRef<Record<string, FsNode[]>>({});
|
|
49
|
+
const { activeRepo, setActiveRepo } = useRepoScope(rootPath, 'files.activeRepo');
|
|
50
|
+
const { repos, scanning: reposScanning, rescan: rescanRepos } = useRepoList(rootPath, enabled);
|
|
23
51
|
const { isScrolling, handleScroll } = useScrollbarFade();
|
|
24
52
|
const { fileMap: gitFileMap, folderMap: gitFolderMap, refreshGitStatus } = useGitStatus(rootPath, activeRepo, refreshKey, enabled);
|
|
25
53
|
|
|
54
|
+
// The scope every request and every cached child path below belongs to. A
|
|
55
|
+
// change to it means the previous workspace's or repo's entries are not this
|
|
56
|
+
// one's, so nothing read under the old value may be rendered.
|
|
57
|
+
const scope = `${rootPath ?? ''}\u0000${activeRepo}`;
|
|
58
|
+
const scopeRef = useRef(scope);
|
|
59
|
+
scopeRef.current = scope;
|
|
60
|
+
// The scope of the listing on screen, read outside the render closure so the
|
|
61
|
+
// async loaders can tell a superseded tree from the current one.
|
|
62
|
+
const listedScopeRef = useRef(EMPTY_LISTING.scope);
|
|
63
|
+
|
|
64
|
+
const commitListing = (next: Listing) => {
|
|
65
|
+
listedScopeRef.current = next.scope;
|
|
66
|
+
setListing(next);
|
|
67
|
+
};
|
|
68
|
+
|
|
26
69
|
useEffect(() => {
|
|
27
70
|
if (!expandedPathsReady) return;
|
|
28
71
|
setExpandedPaths(prev => {
|
|
@@ -43,14 +86,23 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
43
86
|
|
|
44
87
|
const loadFiles = (opts?: { silent?: boolean }) => {
|
|
45
88
|
if (!enabled) return;
|
|
89
|
+
const requested = scopeRef.current;
|
|
46
90
|
if (!opts?.silent) setIsLoading(true);
|
|
47
91
|
fetch(listUrl())
|
|
48
92
|
.then(r => r.json())
|
|
49
93
|
.then(data => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
94
|
+
// A read that was superseded by a workspace switch describes a tree
|
|
95
|
+
// this panel is no longer showing.
|
|
96
|
+
if (scopeRef.current !== requested) return;
|
|
97
|
+
// Children cached under the previous tree are keyed by paths relative
|
|
98
|
+
// to a root that is not this one: drop them before rehydrating.
|
|
99
|
+
if (listedScopeRef.current !== requested) childrenCacheRef.current = {};
|
|
100
|
+
const payload = readDirPayload(data);
|
|
101
|
+
commitListing({
|
|
102
|
+
scope: requested,
|
|
103
|
+
files: rehydrateTree(payload.files, childrenCacheRef.current),
|
|
104
|
+
root: payload.root,
|
|
105
|
+
});
|
|
54
106
|
})
|
|
55
107
|
.catch(() => {})
|
|
56
108
|
.finally(() => {
|
|
@@ -85,13 +137,17 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
85
137
|
// so a freshly listed row never renders bare next to a status map that
|
|
86
138
|
// still predates its change (the panel poll alone can lag a fresh edit).
|
|
87
139
|
refreshGitStatus();
|
|
140
|
+
const requested = scopeRef.current;
|
|
88
141
|
return fetch(listUrl(path))
|
|
89
142
|
.then(r => r.json())
|
|
90
143
|
.then(data => {
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
144
|
+
if (scopeRef.current !== requested) return;
|
|
145
|
+
const payload = readDirPayload(data);
|
|
146
|
+
if (payload.files.length === 0 && payload.root === '') return;
|
|
147
|
+
childrenCacheRef.current[path] = payload.files;
|
|
148
|
+
setListing(prev => prev.scope === requested
|
|
149
|
+
? { ...prev, files: setChildrenAt(prev.files, path, payload.files) }
|
|
150
|
+
: prev);
|
|
95
151
|
})
|
|
96
152
|
.catch(() => {});
|
|
97
153
|
};
|
|
@@ -106,6 +162,10 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
106
162
|
|
|
107
163
|
const handleSelectRepo = (repo: string) => {
|
|
108
164
|
if (repo === activeRepo) return;
|
|
165
|
+
// The expansion set holds paths relative to the repo that was listed, so a
|
|
166
|
+
// repo switch starts from a collapsed tree. (A workspace switch needs no
|
|
167
|
+
// equivalent: the listing is re-read under the new root, and the session's
|
|
168
|
+
// own expansion set is restored with the session.)
|
|
109
169
|
childrenCacheRef.current = {};
|
|
110
170
|
setExpandedPaths(new Set());
|
|
111
171
|
setStoredExpandedPaths([]);
|
|
@@ -120,6 +180,9 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
120
180
|
);
|
|
121
181
|
}
|
|
122
182
|
|
|
183
|
+
const tree = listing.scope === scope ? listing.files : [];
|
|
184
|
+
const listingRoot = listing.scope === scope ? listing.root : '';
|
|
185
|
+
|
|
123
186
|
const getFilteredFiles = () => {
|
|
124
187
|
if (!searchQuery.trim()) return tree;
|
|
125
188
|
|
|
@@ -158,7 +221,14 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
158
221
|
<div className={`flex flex-col h-full bg-paper ${className}`}>
|
|
159
222
|
<div className="p-3 border-b border-ink/10 flex flex-col space-y-2">
|
|
160
223
|
<div className="flex items-center justify-between">
|
|
161
|
-
<GitRepoDropdown
|
|
224
|
+
<GitRepoDropdown
|
|
225
|
+
rootPath={rootPath}
|
|
226
|
+
activeRepo={activeRepo}
|
|
227
|
+
onSelectRepo={handleSelectRepo}
|
|
228
|
+
repos={repos}
|
|
229
|
+
scanning={reposScanning}
|
|
230
|
+
onRefreshRepos={rescanRepos}
|
|
231
|
+
/>
|
|
162
232
|
<button
|
|
163
233
|
onClick={refresh}
|
|
164
234
|
className="p-1.5 text-ink/40 hover:text-ink hover:bg-ink/5 rounded transition-colors"
|
|
@@ -10,8 +10,8 @@ import { GitChangesList } from '@/client/components/workspace/git-panel/ChangesL
|
|
|
10
10
|
import { ToastStack } from '@/client/components/common/ToastStack';
|
|
11
11
|
import { useToasts } from '@/client/hooks/ui/toasts';
|
|
12
12
|
import { useSessionState } from '@/client/hooks/workspace/session-state';
|
|
13
|
+
import { resolveRepoForPanel, useRepoList, useRepoScope } from '@/client/hooks/workspace/repo-scope';
|
|
13
14
|
import { usePanelRefresh, useFileMutationRefresh } from '@/client/hooks/workspace/panel-refresh';
|
|
14
|
-
import { REPO_DISCOVERY_POLL_MS } from '@/shared/lib/workspace/refresh-cadence';
|
|
15
15
|
import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
|
|
16
16
|
|
|
17
17
|
interface GitPanelProps {
|
|
@@ -22,15 +22,46 @@ interface GitPanelProps {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export function GitPanel({ className = '', enabled = true, rootPath, refreshKey = 0 }: GitPanelProps) {
|
|
25
|
-
const fetcher = useFetcher<{ changes: GitChange[], branch: string, branches: string[], remoteBranches?: string[],
|
|
25
|
+
const fetcher = useFetcher<{ changes: GitChange[], branch: string, branches: string[], remoteBranches?: string[], syncCount?: { ahead: number, behind: number } }>();
|
|
26
26
|
const actionFetcher = useFetcher<{ success: boolean, type?: string, data?: any, error?: string }>();
|
|
27
27
|
|
|
28
|
-
const
|
|
29
|
-
const
|
|
28
|
+
const { activeRepo: pickedRepo, setActiveRepo, ready: activeRepoReady } = useRepoScope(rootPath, 'git.activeRepo');
|
|
29
|
+
const { repos, scanning: reposScanning, rescan: refreshRepos } = useRepoList(rootPath, enabled);
|
|
30
|
+
// When the user has picked nothing and the workspace root is not itself a
|
|
31
|
+
// repo, git falls back to the first repo discovery found — the same choice
|
|
32
|
+
// the loader makes for a request that names no repo. Resolving it here rather
|
|
33
|
+
// than reading it back from the response is what keeps the header label, the
|
|
34
|
+
// picker's checkmark and the requests describing one repo: the response
|
|
35
|
+
// arrives a render later, and by then it may belong to a workspace the user
|
|
36
|
+
// has already left.
|
|
37
|
+
const activeRepo = resolveRepoForPanel(pickedRepo, repos);
|
|
38
|
+
const activeRepoRef = useRef(activeRepo);
|
|
39
|
+
activeRepoRef.current = activeRepo;
|
|
40
|
+
|
|
41
|
+
// `root\0repo` — the working tree the panel is showing right now.
|
|
42
|
+
const scopeRef = useRef('');
|
|
43
|
+
scopeRef.current = `${rootPath ?? ''}\u0000${activeRepo}`;
|
|
44
|
+
// The scope the in-flight read was asked for, and the scope the payload
|
|
45
|
+
// currently in `fetcher.data` was asked for. Only one read is ever in flight
|
|
46
|
+
// (the fetcher aborts its predecessor), so the former is what the latter
|
|
47
|
+
// becomes when a new payload arrives.
|
|
48
|
+
const requestedScopeRef = useRef('');
|
|
49
|
+
const payloadScopeRef = useRef('');
|
|
50
|
+
const lastPayloadRef = useRef<unknown>(undefined);
|
|
51
|
+
if (fetcher.data !== lastPayloadRef.current) {
|
|
52
|
+
lastPayloadRef.current = fetcher.data;
|
|
53
|
+
payloadScopeRef.current = requestedScopeRef.current;
|
|
54
|
+
}
|
|
55
|
+
// A switch leaves the previous repo's branch, changes and branch list in
|
|
56
|
+
// `fetcher.data` until the new read lands. Rendering those under the new
|
|
57
|
+
// repo's header would claim the new working tree is on the old branch with
|
|
58
|
+
// the old changes, so the payload is used only while it describes the scope
|
|
59
|
+
// on screen.
|
|
60
|
+
const data = payloadScopeRef.current === scopeRef.current ? fetcher.data : undefined;
|
|
30
61
|
|
|
31
62
|
const loadRepo = (repo?: string) => {
|
|
32
63
|
if (!enabled) return;
|
|
33
|
-
const targetRepo = repo
|
|
64
|
+
const targetRepo = repo ?? activeRepoRef.current;
|
|
34
65
|
const params = new URLSearchParams();
|
|
35
66
|
if (rootPath) params.set('root', rootPath);
|
|
36
67
|
if (targetRepo && targetRepo !== '.') params.set('repo', targetRepo);
|
|
@@ -39,6 +70,11 @@ export function GitPanel({ className = '', enabled = true, rootPath, refreshKey
|
|
|
39
70
|
// numbers describe origin rather than the last fetch.
|
|
40
71
|
params.set('sync', '1');
|
|
41
72
|
params.set('t', String(Date.now()));
|
|
73
|
+
// Tag the response with the scope it was asked for: a switch leaves the
|
|
74
|
+
// previous repo's branch, changes and branch list in `fetcher.data` until
|
|
75
|
+
// the new read lands, and rendering those under the new repo's header
|
|
76
|
+
// claims the new working tree is on the old branch with the old changes.
|
|
77
|
+
requestedScopeRef.current = scopeRef.current;
|
|
42
78
|
fetcher.load(`/api/fs/git?${params.toString()}`);
|
|
43
79
|
};
|
|
44
80
|
|
|
@@ -66,70 +102,26 @@ export function GitPanel({ className = '', enabled = true, rootPath, refreshKey
|
|
|
66
102
|
|
|
67
103
|
const { toasts, pushToast, dismissToast } = useToasts();
|
|
68
104
|
|
|
69
|
-
// Background nested-repo discovery polling: the loader returns immediately
|
|
70
|
-
// with the root status and `reposPending`; we poll the lightweight
|
|
71
|
-
// `?reposOnly=1` endpoint until the discoverer finishes, then refresh.
|
|
72
|
-
const [extraRepos, setExtraRepos] = useState<string[] | null>(null);
|
|
73
|
-
const [pollingRepos, setPollingRepos] = useState(false);
|
|
74
|
-
const [rescanningRepos, setRescanningRepos] = useState(false);
|
|
75
|
-
|
|
76
105
|
useEffect(() => {
|
|
77
106
|
setMounted(true);
|
|
78
107
|
}, []);
|
|
79
108
|
|
|
109
|
+
// `activeRepo` is a dependency, not just the root: the repo list that
|
|
110
|
+
// resolves the root-level fallback arrives after the first render, and a
|
|
111
|
+
// workspace switch re-resolves it to '.'.
|
|
80
112
|
useEffect(() => {
|
|
81
113
|
if (!activeRepoReady) return;
|
|
82
|
-
loadRepo(
|
|
83
|
-
}, [refreshKey, rootPath, enabled, activeRepoReady]);
|
|
114
|
+
loadRepo();
|
|
115
|
+
}, [refreshKey, rootPath, enabled, activeRepoReady, activeRepo]);
|
|
84
116
|
|
|
85
117
|
// Auto refresh: keep the change list in step with external mutations
|
|
86
118
|
// (terminal commits, agent edits) that never bump `refreshKey`. `loadRepo`
|
|
87
119
|
// re-reads via `fetcher.load`, whose `t` param busts the cache; a load
|
|
88
120
|
// already in flight is ignored by the loader so polls cannot pile up.
|
|
89
|
-
usePanelRefresh(() => loadRepo(
|
|
121
|
+
usePanelRefresh(() => loadRepo(), enabled && activeRepoReady);
|
|
90
122
|
// Agent edits/commits land between poll ticks: re-read right after a
|
|
91
123
|
// file-mutating tool finishes so the change list tracks the AI's work.
|
|
92
|
-
useFileMutationRefresh(() => loadRepo(
|
|
93
|
-
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
if (fetcher.data?.activeRepo && fetcher.data.activeRepo !== storedActiveRepo) {
|
|
96
|
-
setStoredActiveRepo(fetcher.data.activeRepo);
|
|
97
|
-
}
|
|
98
|
-
}, [fetcher.data?.activeRepo, storedActiveRepo, setStoredActiveRepo]);
|
|
99
|
-
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
if (fetcher.data?.reposPending) setPollingRepos(true);
|
|
102
|
-
}, [fetcher.data]);
|
|
103
|
-
|
|
104
|
-
// A forced rescan may finish before the polling interval kicks in; when that
|
|
105
|
-
// happens the loader returns the fresh repo list synchronously.
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
if (
|
|
108
|
-
rescanningRepos &&
|
|
109
|
-
fetcher.data &&
|
|
110
|
-
!fetcher.data.reposPending &&
|
|
111
|
-
Array.isArray(fetcher.data.repos)
|
|
112
|
-
) {
|
|
113
|
-
setExtraRepos(fetcher.data.repos);
|
|
114
|
-
setRescanningRepos(false);
|
|
115
|
-
}
|
|
116
|
-
}, [fetcher.data, rescanningRepos]);
|
|
117
|
-
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
if (!pollingRepos) return;
|
|
120
|
-
const params = new URLSearchParams({ reposOnly: '1' });
|
|
121
|
-
if (rootPath) params.set('root', rootPath);
|
|
122
|
-
const id = setInterval(async () => {
|
|
123
|
-
const data = await fetch(`/api/fs/git?${params.toString()}`).then(r => r.json()).catch(() => null);
|
|
124
|
-
if (data && !data.reposPending && Array.isArray(data.repos)) {
|
|
125
|
-
setExtraRepos(data.repos);
|
|
126
|
-
setPollingRepos(false);
|
|
127
|
-
setRescanningRepos(false);
|
|
128
|
-
loadRepo(storedActiveRepo);
|
|
129
|
-
}
|
|
130
|
-
}, REPO_DISCOVERY_POLL_MS);
|
|
131
|
-
return () => clearInterval(id);
|
|
132
|
-
}, [pollingRepos, rootPath, storedActiveRepo]);
|
|
124
|
+
useFileMutationRefresh(() => loadRepo(), enabled && activeRepoReady);
|
|
133
125
|
|
|
134
126
|
// Focus input when branch prompt opens
|
|
135
127
|
useEffect(() => {
|
|
@@ -146,7 +138,7 @@ export function GitPanel({ className = '', enabled = true, rootPath, refreshKey
|
|
|
146
138
|
} else if (actionFetcher.data.type === 'graph') {
|
|
147
139
|
setViewingOutput({ title: 'Git Graph', data: actionFetcher.data.data || [] });
|
|
148
140
|
} else if (actionFetcher.data.success) {
|
|
149
|
-
loadRepo(
|
|
141
|
+
loadRepo();
|
|
150
142
|
} else {
|
|
151
143
|
pushToast(actionFetcher.data.error || 'Git operation failed');
|
|
152
144
|
}
|
|
@@ -188,11 +180,10 @@ export function GitPanel({ className = '', enabled = true, rootPath, refreshKey
|
|
|
188
180
|
}
|
|
189
181
|
};
|
|
190
182
|
|
|
191
|
-
const branch =
|
|
192
|
-
const branches =
|
|
193
|
-
const remoteBranches =
|
|
194
|
-
const
|
|
195
|
-
const changes = fetcher.data?.changes || [];
|
|
183
|
+
const branch = data?.branch || 'main';
|
|
184
|
+
const branches = data?.branches || ['main'];
|
|
185
|
+
const remoteBranches = data?.remoteBranches || [];
|
|
186
|
+
const changes = data?.changes || [];
|
|
196
187
|
const isLoading = fetcher.state === 'loading' || actionFetcher.state !== 'idle';
|
|
197
188
|
|
|
198
189
|
const stagedChanges = changes.filter(c => {
|
|
@@ -200,17 +191,6 @@ export function GitPanel({ className = '', enabled = true, rootPath, refreshKey
|
|
|
200
191
|
return status && status[0] !== ' ' && status[0] !== '?';
|
|
201
192
|
});
|
|
202
193
|
|
|
203
|
-
const refreshRepos = () => {
|
|
204
|
-
if (!enabled || pollingRepos || rescanningRepos) return;
|
|
205
|
-
setExtraRepos(repos);
|
|
206
|
-
setRescanningRepos(true);
|
|
207
|
-
const params = new URLSearchParams({ rescan: '1', sync: '1' });
|
|
208
|
-
if (rootPath) params.set('root', rootPath);
|
|
209
|
-
if (activeRepo !== '.') params.set('repo', activeRepo);
|
|
210
|
-
params.set('t', String(Date.now()));
|
|
211
|
-
fetcher.load(`/api/fs/git?${params.toString()}`);
|
|
212
|
-
};
|
|
213
|
-
|
|
214
194
|
if (!enabled) {
|
|
215
195
|
return (
|
|
216
196
|
<div className={`flex flex-col h-full bg-paper items-center justify-center text-ink/40 ${className}`}>
|
|
@@ -276,12 +256,14 @@ export function GitPanel({ className = '', enabled = true, rootPath, refreshKey
|
|
|
276
256
|
repos={repos}
|
|
277
257
|
isLoading={isLoading}
|
|
278
258
|
rootPath={rootPath}
|
|
279
|
-
reposScanning={
|
|
259
|
+
reposScanning={reposScanning}
|
|
280
260
|
onSelectRepo={(r) => {
|
|
281
|
-
|
|
282
|
-
|
|
261
|
+
// The reload is the scope effect's job: it fires on the resolved
|
|
262
|
+
// `activeRepo` change, so selecting here would fetch the same repo
|
|
263
|
+
// twice.
|
|
264
|
+
if (r !== activeRepo) setActiveRepo(r);
|
|
283
265
|
}}
|
|
284
|
-
onRefresh={() => loadRepo(
|
|
266
|
+
onRefresh={() => loadRepo()}
|
|
285
267
|
onRefreshRepos={refreshRepos}
|
|
286
268
|
/>
|
|
287
269
|
|
|
@@ -303,7 +285,7 @@ export function GitPanel({ className = '', enabled = true, rootPath, refreshKey
|
|
|
303
285
|
viewMode={viewMode}
|
|
304
286
|
setViewMode={setViewMode}
|
|
305
287
|
isBusy={isLoading}
|
|
306
|
-
syncCount={
|
|
288
|
+
syncCount={data?.syncCount}
|
|
307
289
|
onSync={() => executeAction('sync')}
|
|
308
290
|
/>
|
|
309
291
|
|
|
@@ -8,6 +8,11 @@ interface TerminalHeaderProps {
|
|
|
8
8
|
rootPath?: string;
|
|
9
9
|
activeRepo: string;
|
|
10
10
|
onSelectRepo: (repo: string) => void;
|
|
11
|
+
/** Discovered repos of the active root; the panel owns discovery. */
|
|
12
|
+
repos: string[];
|
|
13
|
+
/** Discovery or a forced rescan is in flight. */
|
|
14
|
+
reposScanning: boolean;
|
|
15
|
+
onRefreshRepos: () => void;
|
|
11
16
|
/** Working directory the shell actually runs in. */
|
|
12
17
|
cwd?: string;
|
|
13
18
|
shell?: string;
|
|
@@ -35,6 +40,9 @@ export function TerminalHeader({
|
|
|
35
40
|
rootPath,
|
|
36
41
|
activeRepo,
|
|
37
42
|
onSelectRepo,
|
|
43
|
+
repos,
|
|
44
|
+
reposScanning,
|
|
45
|
+
onRefreshRepos,
|
|
38
46
|
cwd,
|
|
39
47
|
shell,
|
|
40
48
|
gitBranch,
|
|
@@ -60,7 +68,14 @@ export function TerminalHeader({
|
|
|
60
68
|
<span>{status === 'exited' && exitCode !== null ? `EXIT ${exitCode}` : STATUS_LABEL[status]}</span>
|
|
61
69
|
</span>
|
|
62
70
|
|
|
63
|
-
<GitRepoDropdown
|
|
71
|
+
<GitRepoDropdown
|
|
72
|
+
rootPath={rootPath}
|
|
73
|
+
activeRepo={activeRepo}
|
|
74
|
+
onSelectRepo={onSelectRepo}
|
|
75
|
+
repos={repos}
|
|
76
|
+
scanning={reposScanning}
|
|
77
|
+
onRefreshRepos={onRefreshRepos}
|
|
78
|
+
/>
|
|
64
79
|
|
|
65
80
|
<button
|
|
66
81
|
type="button"
|
|
@@ -40,7 +40,7 @@ interface RealtimeXtermViewProps {
|
|
|
40
40
|
*/
|
|
41
41
|
export const RealtimeXtermView = forwardRef<RealtimeXtermHandle, RealtimeXtermViewProps>(
|
|
42
42
|
({ onInput, onBinaryInput, onGridChange, onReady }, ref) => {
|
|
43
|
-
const {
|
|
43
|
+
const { theme } = useTheme();
|
|
44
44
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
45
45
|
const mountRef = useRef<XtermMount | null>(null);
|
|
46
46
|
const [isScrolledUp, setIsScrolledUp] = useState(false);
|
|
@@ -53,15 +53,15 @@ export const RealtimeXtermView = forwardRef<RealtimeXtermHandle, RealtimeXtermVi
|
|
|
53
53
|
gridChangeRef.current = onGridChange;
|
|
54
54
|
const readyRef = useRef(onReady);
|
|
55
55
|
readyRef.current = onReady;
|
|
56
|
-
const themeRef = useRef(
|
|
57
|
-
themeRef.current =
|
|
56
|
+
const themeRef = useRef(theme);
|
|
57
|
+
themeRef.current = theme;
|
|
58
58
|
|
|
59
59
|
useEffect(() => {
|
|
60
60
|
const term = mountRef.current?.term;
|
|
61
61
|
if (!term) return;
|
|
62
|
-
term.options.theme = getXtermTheme(
|
|
62
|
+
term.options.theme = getXtermTheme(theme);
|
|
63
63
|
term.refresh(0, term.rows - 1);
|
|
64
|
-
}, [
|
|
64
|
+
}, [theme]);
|
|
65
65
|
|
|
66
66
|
useImperativeHandle(ref, () => ({
|
|
67
67
|
write(bytes: Uint8Array) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useRef } from 'preact/hooks';
|
|
2
2
|
import { useTerminal } from '@/client/hooks/workspace/terminal';
|
|
3
|
-
import {
|
|
3
|
+
import { useRepoList, useRepoScope } from '@/client/hooks/workspace/repo-scope';
|
|
4
4
|
import { useTheme } from '@/client/hooks/ui/theme';
|
|
5
5
|
import { TerminalHeader } from '@/client/components/workspace/terminal-panel/Header';
|
|
6
6
|
import { TerminalQuickActions } from '@/client/components/workspace/terminal-panel/QuickActions';
|
|
@@ -18,7 +18,8 @@ const encoder = new TextEncoder();
|
|
|
18
18
|
export function TerminalPanel({ className = '', enabled = true, rootPath, showHeader = true }: TerminalPanelProps) {
|
|
19
19
|
const xtermRef = useRef<RealtimeXtermHandle>(null);
|
|
20
20
|
const { isDark } = useTheme();
|
|
21
|
-
const
|
|
21
|
+
const { activeRepo, setActiveRepo } = useRepoScope(rootPath, 'terminal.activeRepo');
|
|
22
|
+
const { repos, scanning: reposScanning, rescan: rescanRepos } = useRepoList(rootPath, enabled);
|
|
22
23
|
const gridRef = useRef<{ cols: number; rows: number } | null>(null);
|
|
23
24
|
|
|
24
25
|
const handleOutput = useCallback((bytes: Uint8Array) => {
|
|
@@ -105,6 +106,9 @@ export function TerminalPanel({ className = '', enabled = true, rootPath, showHe
|
|
|
105
106
|
rootPath={rootPath}
|
|
106
107
|
activeRepo={activeRepo}
|
|
107
108
|
onSelectRepo={setActiveRepo}
|
|
109
|
+
repos={repos}
|
|
110
|
+
reposScanning={reposScanning}
|
|
111
|
+
onRefreshRepos={rescanRepos}
|
|
108
112
|
cwd={cwd}
|
|
109
113
|
shell={shell}
|
|
110
114
|
gitBranch={systemInfo.gitBranch}
|
|
@@ -71,7 +71,7 @@ export interface XtermMount {
|
|
|
71
71
|
*/
|
|
72
72
|
export async function mountXterm(
|
|
73
73
|
container: HTMLElement,
|
|
74
|
-
|
|
74
|
+
themeId: string,
|
|
75
75
|
callbacks: XtermMountCallbacks,
|
|
76
76
|
isCancelled: () => boolean,
|
|
77
77
|
): Promise<XtermMount | null> {
|
|
@@ -93,7 +93,7 @@ export async function mountXterm(
|
|
|
93
93
|
fontSize: 12,
|
|
94
94
|
lineHeight: 1.25,
|
|
95
95
|
fontFamily: XTERM_FONT_FAMILY,
|
|
96
|
-
theme: getXtermTheme(
|
|
96
|
+
theme: getXtermTheme(themeId),
|
|
97
97
|
// No `convertEol`: the PTY already emits CRLF, and converting a lone LF
|
|
98
98
|
// would re-introduce the line-feed doubling a real terminal avoids.
|
|
99
99
|
scrollback: 10000,
|
|
@@ -1,58 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
foreground: '#141310',
|
|
4
|
-
cursor: '#141310',
|
|
5
|
-
cursorAccent: '#f4f1ea',
|
|
6
|
-
selectionBackground: '#ded8ce',
|
|
7
|
-
selectionForeground: '#141310',
|
|
8
|
-
black: '#141310',
|
|
9
|
-
red: '#c8321e',
|
|
10
|
-
green: '#047857',
|
|
11
|
-
yellow: '#b45309',
|
|
12
|
-
blue: '#1d4ed8',
|
|
13
|
-
magenta: '#9333ea',
|
|
14
|
-
cyan: '#0284c7',
|
|
15
|
-
white: '#faf8f3',
|
|
16
|
-
brightBlack: '#78716c',
|
|
17
|
-
brightRed: '#dc2626',
|
|
18
|
-
brightGreen: '#059669',
|
|
19
|
-
brightYellow: '#d97706',
|
|
20
|
-
brightBlue: '#2563eb',
|
|
21
|
-
brightMagenta: '#a855f7',
|
|
22
|
-
brightCyan: '#0ea5e9',
|
|
23
|
-
brightWhite: '#292524',
|
|
24
|
-
};
|
|
1
|
+
import { mixHex } from '@/shared/lib/theme/color';
|
|
2
|
+
import { resolveTheme } from '@/shared/lib/theme/catalog';
|
|
25
3
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
brightCyan: '#56b6c2',
|
|
48
|
-
brightWhite: '#ffffff',
|
|
49
|
-
};
|
|
4
|
+
/**
|
|
5
|
+
* The terminal palette for a theme, derived from the same eight colors the rest
|
|
6
|
+
* of the app paints with.
|
|
7
|
+
*
|
|
8
|
+
* xterm parses its own colors and understands only literal notations (hex,
|
|
9
|
+
* `rgb()`, `hsl()`, named) — `color-mix()` is not among them in either the DOM
|
|
10
|
+
* or the WebGL renderer — so every blend is computed here in hex instead of
|
|
11
|
+
* being left to CSS. The result follows the active theme rather than a fixed
|
|
12
|
+
* light/dark pair: a Nord terminal is Nord, and switching themes repaints the
|
|
13
|
+
* scrollback instead of leaving the previous palette on screen.
|
|
14
|
+
*
|
|
15
|
+
* ANSI slots map onto the semantic colors the palette does carry (red=error,
|
|
16
|
+
* green=success, yellow=warning, blue=info, magenta=accent); `cyan` has no
|
|
17
|
+
* counterpart, so it is a deterministic mix of info and success, and the
|
|
18
|
+
* grayscale slots are mixes of ink over canvas. "Bright" variants mix 20% toward
|
|
19
|
+
* the foreground, which is lighter on a dark theme and darker on a light one —
|
|
20
|
+
* the direction that increases contrast on both.
|
|
21
|
+
*/
|
|
22
|
+
export function getXtermTheme(themeId: string) {
|
|
23
|
+
const palette = resolveTheme(themeId);
|
|
24
|
+
const { canvas, ink } = palette;
|
|
50
25
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
26
|
+
const towardInk = (color: string) => mixHex(color, ink, 0.2);
|
|
27
|
+
const grayscale = (ratio: number) => mixHex(canvas, ink, ratio);
|
|
54
28
|
|
|
55
|
-
|
|
29
|
+
return {
|
|
30
|
+
background: canvas,
|
|
31
|
+
foreground: ink,
|
|
32
|
+
cursor: palette.meta,
|
|
33
|
+
cursorAccent: canvas,
|
|
34
|
+
selectionBackground: grayscale(0.28),
|
|
35
|
+
selectionForeground: ink,
|
|
36
|
+
black: grayscale(0.2),
|
|
37
|
+
red: palette.error,
|
|
38
|
+
green: palette.success,
|
|
39
|
+
yellow: palette.warning,
|
|
40
|
+
blue: palette.info,
|
|
41
|
+
magenta: palette.meta,
|
|
42
|
+
cyan: mixHex(palette.info, palette.success, 0.5),
|
|
43
|
+
white: grayscale(0.75),
|
|
44
|
+
brightBlack: grayscale(0.5),
|
|
45
|
+
brightRed: towardInk(palette.error),
|
|
46
|
+
brightGreen: towardInk(palette.success),
|
|
47
|
+
brightYellow: towardInk(palette.warning),
|
|
48
|
+
brightBlue: towardInk(palette.info),
|
|
49
|
+
brightMagenta: towardInk(palette.meta),
|
|
50
|
+
brightCyan: towardInk(mixHex(palette.info, palette.success, 0.5)),
|
|
51
|
+
brightWhite: grayscale(0.92),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
56
54
|
|
|
57
55
|
/**
|
|
58
56
|
* Font stack for the terminal canvas.
|