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
|
@@ -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.
|
|
@@ -15,18 +15,50 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
18
|
-
import type { AgentImage, BtwState, StreamTransport } from '@/shared/types';
|
|
19
|
-
import {
|
|
18
|
+
import type { AgentImage, Attachment, BtwState, ChatMessageData, StreamTransport } from '@/shared/types';
|
|
19
|
+
import type { ApprovalMode } from '@/shared/lib/omp/config/access-mode';
|
|
20
|
+
import { attachmentImage, readTextAttachments } from '@/shared/lib/chat/attachments';
|
|
21
|
+
import {
|
|
22
|
+
BtwRequestError,
|
|
23
|
+
abortBtwQuestion,
|
|
24
|
+
askBtwQuestion,
|
|
25
|
+
connectBtwStream,
|
|
26
|
+
deleteBtwTopic,
|
|
27
|
+
fetchBtwState,
|
|
28
|
+
promoteBtwTopic,
|
|
29
|
+
respondBtwDialog,
|
|
30
|
+
setBtwAccessMode,
|
|
31
|
+
setBtwModel,
|
|
32
|
+
setBtwThinkingLevel,
|
|
33
|
+
} from '@/shared/lib/chat/btw/client';
|
|
20
34
|
import type { StreamConnection } from '@/shared/lib/chat/omp/transport';
|
|
21
35
|
|
|
22
36
|
/** Images the side session accepts, already read as base64. */
|
|
23
37
|
export type BtwImage = AgentImage;
|
|
24
38
|
|
|
25
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* The turn streaming right now. `messages` is the turn's conversation in the
|
|
41
|
+
* chat's own shape, upserted by `message.id` exactly as the chat stream does —
|
|
42
|
+
* so the panel renders a live side answer through the same components the chat
|
|
43
|
+
* uses, tool calls included.
|
|
44
|
+
*/
|
|
26
45
|
export interface BtwLiveAnswer {
|
|
27
46
|
topicId: string;
|
|
28
47
|
turnIndex: number;
|
|
29
|
-
|
|
48
|
+
messages: ChatMessageData[];
|
|
49
|
+
/** Activity phrase for the panel's indicator, from the side child's frames. */
|
|
50
|
+
activity: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Composer picks for a topic's FIRST question. A topic that does not exist yet
|
|
55
|
+
* has no row to write them to, so they ride the ask and reach the spawn; later
|
|
56
|
+
* questions address an existing topic through the dedicated setters.
|
|
57
|
+
*/
|
|
58
|
+
export interface BtwFirstQuestionPicks {
|
|
59
|
+
model?: { provider: string; id: string };
|
|
60
|
+
thinkingLevel?: string;
|
|
61
|
+
approvalMode?: ApprovalMode;
|
|
30
62
|
}
|
|
31
63
|
|
|
32
64
|
export interface BtwSessionHandle {
|
|
@@ -37,8 +69,12 @@ export interface BtwSessionHandle {
|
|
|
37
69
|
busy: boolean;
|
|
38
70
|
/** Resolves with the canonical state the command produced, or null when it
|
|
39
71
|
* was refused (the reason is then in `error`). */
|
|
40
|
-
ask: (question: string,
|
|
72
|
+
ask: (question: string, attachments?: Attachment[], topicId?: string, picks?: BtwFirstQuestionPicks) => Promise<BtwState | null>;
|
|
41
73
|
abort: (topicId: string) => Promise<void>;
|
|
74
|
+
setModel: (topicId: string, provider: string, modelId: string) => Promise<void>;
|
|
75
|
+
setThinkingLevel: (topicId: string, level: string) => Promise<void>;
|
|
76
|
+
setAccessMode: (topicId: string, mode: ApprovalMode) => Promise<void>;
|
|
77
|
+
respondToDialog: (topicId: string, id: string, response: { value: string } | { confirmed: boolean } | { cancelled: true }) => Promise<void>;
|
|
42
78
|
/** Returns the new session id to open, or null when it was refused. */
|
|
43
79
|
promote: (topicId: string) => Promise<string | null>;
|
|
44
80
|
remove: (topicId: string) => Promise<void>;
|
|
@@ -74,20 +110,39 @@ export function useBtwSession(sessionId: string | null, options: BtwSessionOptio
|
|
|
74
110
|
switch (frame.type) {
|
|
75
111
|
case 'btw_state':
|
|
76
112
|
setState(frame.state);
|
|
77
|
-
//
|
|
78
|
-
//
|
|
113
|
+
// The snapshot carries the running turn's conversation, so a client
|
|
114
|
+
// attaching mid-turn (a reload, a second tab) shows the answer the
|
|
115
|
+
// child already produced instead of an empty bubble. A snapshot with
|
|
116
|
+
// no live turn ends the local buffer — the persisted rows own it now.
|
|
117
|
+
setLive(frame.state.live
|
|
118
|
+
? {
|
|
119
|
+
topicId: frame.state.live.topicId,
|
|
120
|
+
turnIndex: frame.state.live.turnIndex,
|
|
121
|
+
messages: frame.state.live.messages,
|
|
122
|
+
activity: frame.state.live.activity,
|
|
123
|
+
}
|
|
124
|
+
: null);
|
|
125
|
+
break;
|
|
126
|
+
case 'btw_message':
|
|
127
|
+
// Same upsert the chat stream performs: omp emits one frame per
|
|
128
|
+
// segment carrying that segment's full accumulated content, so a
|
|
129
|
+
// same-id frame replaces in place and a new id appends.
|
|
79
130
|
setLive((current) => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
131
|
+
const base =
|
|
132
|
+
current && current.topicId === frame.topicId && current.turnIndex === frame.turnIndex
|
|
133
|
+
? current
|
|
134
|
+
: { topicId: frame.topicId, turnIndex: frame.turnIndex, messages: [], activity: '' };
|
|
135
|
+
const existing = base.messages.findIndex((message) => message.id === frame.message.id);
|
|
136
|
+
const messages =
|
|
137
|
+
existing === -1
|
|
138
|
+
? [...base.messages, frame.message]
|
|
139
|
+
: [...base.messages.slice(0, existing), frame.message, ...base.messages.slice(existing + 1)];
|
|
140
|
+
return { ...base, messages };
|
|
84
141
|
});
|
|
85
142
|
break;
|
|
86
|
-
case '
|
|
143
|
+
case 'btw_activity':
|
|
87
144
|
setLive((current) =>
|
|
88
|
-
current && current.topicId === frame.topicId
|
|
89
|
-
? { ...current, text: current.text + frame.text }
|
|
90
|
-
: { topicId: frame.topicId, turnIndex: frame.turnIndex, text: frame.text },
|
|
145
|
+
current && current.topicId === frame.topicId ? { ...current, activity: frame.verb } : current,
|
|
91
146
|
);
|
|
92
147
|
break;
|
|
93
148
|
case 'btw_error':
|
|
@@ -127,9 +182,29 @@ export function useBtwSession(sessionId: string | null, options: BtwSessionOptio
|
|
|
127
182
|
);
|
|
128
183
|
|
|
129
184
|
const ask = useCallback(
|
|
130
|
-
async (
|
|
185
|
+
async (
|
|
186
|
+
question: string,
|
|
187
|
+
attachments: Attachment[] = [],
|
|
188
|
+
topicId?: string,
|
|
189
|
+
picks?: BtwFirstQuestionPicks,
|
|
190
|
+
): Promise<BtwState | null> => {
|
|
131
191
|
setError(null);
|
|
132
|
-
|
|
192
|
+
// Same split the chat send path makes: images travel as provider payloads,
|
|
193
|
+
// text files are inlined into the prompt. Both are read here, from the
|
|
194
|
+
// attachments the composer already filled in at attach time.
|
|
195
|
+
const images = attachments
|
|
196
|
+
.map((attachment) => attachmentImage(attachment))
|
|
197
|
+
.filter((image): image is NonNullable<typeof image> => image !== null);
|
|
198
|
+
const textFiles = (await readTextAttachments(attachments)).filter((file) => !file.missing);
|
|
199
|
+
const next = await run((id) => askBtwQuestion(id, {
|
|
200
|
+
question,
|
|
201
|
+
topicId,
|
|
202
|
+
...(images.length ? { images } : {}),
|
|
203
|
+
...(textFiles.length ? { textFiles } : {}),
|
|
204
|
+
// Only a first question carries picks; a follow-up addresses a topic
|
|
205
|
+
// that already owns them.
|
|
206
|
+
...(topicId ? {} : picks ?? {}),
|
|
207
|
+
}));
|
|
133
208
|
setLive(null);
|
|
134
209
|
if (next && !next.runningTopicId) {
|
|
135
210
|
setError('The side question was accepted but did not start running.');
|
|
@@ -147,6 +222,38 @@ export function useBtwSession(sessionId: string | null, options: BtwSessionOptio
|
|
|
147
222
|
[run],
|
|
148
223
|
);
|
|
149
224
|
|
|
225
|
+
const setModel = useCallback(
|
|
226
|
+
async (topicId: string, provider: string, modelId: string) => {
|
|
227
|
+
setError(null);
|
|
228
|
+
await run((id) => setBtwModel(id, topicId, provider, modelId));
|
|
229
|
+
},
|
|
230
|
+
[run],
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const setThinkingLevel = useCallback(
|
|
234
|
+
async (topicId: string, level: string) => {
|
|
235
|
+
setError(null);
|
|
236
|
+
await run((id) => setBtwThinkingLevel(id, topicId, level));
|
|
237
|
+
},
|
|
238
|
+
[run],
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const setAccessMode = useCallback(
|
|
242
|
+
async (topicId: string, mode: ApprovalMode) => {
|
|
243
|
+
setError(null);
|
|
244
|
+
await run((id) => setBtwAccessMode(id, topicId, mode));
|
|
245
|
+
},
|
|
246
|
+
[run],
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const respondToDialog = useCallback(
|
|
250
|
+
async (topicId: string, id: string, response: { value: string } | { confirmed: boolean } | { cancelled: true }) => {
|
|
251
|
+
setError(null);
|
|
252
|
+
await run((requestId) => respondBtwDialog(requestId, topicId, id, response));
|
|
253
|
+
},
|
|
254
|
+
[run],
|
|
255
|
+
);
|
|
256
|
+
|
|
150
257
|
const remove = useCallback(
|
|
151
258
|
async (topicId: string) => {
|
|
152
259
|
setError(null);
|
|
@@ -176,5 +283,19 @@ export function useBtwSession(sessionId: string | null, options: BtwSessionOptio
|
|
|
176
283
|
|
|
177
284
|
const clearError = useCallback(() => setError(null), []);
|
|
178
285
|
|
|
179
|
-
return {
|
|
286
|
+
return {
|
|
287
|
+
state,
|
|
288
|
+
live,
|
|
289
|
+
error,
|
|
290
|
+
busy,
|
|
291
|
+
ask,
|
|
292
|
+
abort,
|
|
293
|
+
setModel,
|
|
294
|
+
setThinkingLevel,
|
|
295
|
+
setAccessMode,
|
|
296
|
+
respondToDialog,
|
|
297
|
+
promote,
|
|
298
|
+
remove,
|
|
299
|
+
clearError,
|
|
300
|
+
};
|
|
180
301
|
}
|