ompchamber 1.2.0 → 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 +129 -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.57b39ffa7f.js +0 -34
- package/src/client/components/workspace/btw-panel/Composer.tsx +0 -181
- 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.57b39ffa7f.js.LICENSE.txt → index.c05535dbde.js.LICENSE.txt} +0 -0
|
@@ -16,14 +16,21 @@ interface SearchStreamState {
|
|
|
16
16
|
* encoding the old `useFetcher.submit` used) and the response is consumed
|
|
17
17
|
* incrementally via the shared SSE reader. A new `start` call aborts the
|
|
18
18
|
* previous run — matching the fetcher semantics the panel relied on.
|
|
19
|
+
*
|
|
20
|
+
* Results carry the `scope` they were searched under (the workspace root plus
|
|
21
|
+
* the selected repo). A workspace switch therefore shows no hits at all rather
|
|
22
|
+
* than the previous workspace's — a match's path is relative to the root it was
|
|
23
|
+
* found under, so the same string names a different file, or none, here.
|
|
19
24
|
*/
|
|
20
|
-
export function useSearchStream(): SearchStreamState & {
|
|
25
|
+
export function useSearchStream(scope: string): SearchStreamState & {
|
|
21
26
|
start: (body: Record<string, string>) => Promise<void>;
|
|
22
27
|
} {
|
|
23
|
-
const [
|
|
28
|
+
const [state, setState] = useState<{ scope: string; results: SearchResultItem[] }>({ scope, results: [] });
|
|
24
29
|
const [isSearching, setIsSearching] = useState(false);
|
|
25
30
|
const abortRef = useRef<AbortController | null>(null);
|
|
26
31
|
const aliveRef = useRef(true);
|
|
32
|
+
const scopeRef = useRef(scope);
|
|
33
|
+
scopeRef.current = scope;
|
|
27
34
|
|
|
28
35
|
useEffect(() => {
|
|
29
36
|
aliveRef.current = true;
|
|
@@ -38,8 +45,9 @@ export function useSearchStream(): SearchStreamState & {
|
|
|
38
45
|
abortRef.current?.abort();
|
|
39
46
|
const controller = new AbortController();
|
|
40
47
|
abortRef.current = controller;
|
|
48
|
+
const requested = scopeRef.current;
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
setState({ scope: requested, results: [] });
|
|
43
51
|
setIsSearching(true);
|
|
44
52
|
try {
|
|
45
53
|
const response = await fetch('/api/fs/search', {
|
|
@@ -53,9 +61,15 @@ export function useSearchStream(): SearchStreamState & {
|
|
|
53
61
|
response,
|
|
54
62
|
(event, data) => {
|
|
55
63
|
if (event !== 'matches') return;
|
|
64
|
+
// A run superseded by a workspace switch is no longer this panel's.
|
|
65
|
+
if (scopeRef.current !== requested) return;
|
|
56
66
|
try {
|
|
57
67
|
const parsed = JSON.parse(data);
|
|
58
|
-
if (Array.isArray(parsed))
|
|
68
|
+
if (Array.isArray(parsed)) {
|
|
69
|
+
setState(prev => prev.scope === requested
|
|
70
|
+
? { scope: requested, results: [...prev.results, ...parsed] }
|
|
71
|
+
: prev);
|
|
72
|
+
}
|
|
59
73
|
} catch {
|
|
60
74
|
// Malformed frame — skip; the next frame carries its own payload.
|
|
61
75
|
}
|
|
@@ -72,5 +86,10 @@ export function useSearchStream(): SearchStreamState & {
|
|
|
72
86
|
}
|
|
73
87
|
}, []);
|
|
74
88
|
|
|
75
|
-
return {
|
|
89
|
+
return {
|
|
90
|
+
results: state.scope === scope ? state.results : [],
|
|
91
|
+
// A run in flight for another scope is not searching THIS one.
|
|
92
|
+
isSearching: isSearching && state.scope === scope,
|
|
93
|
+
start,
|
|
94
|
+
};
|
|
76
95
|
}
|
package/src/client/main.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { hydrate } from 'preact';
|
|
|
2
2
|
import { App } from '@/client/App';
|
|
3
3
|
import { bootSyntax } from '@/shared/lib/code/highlighter';
|
|
4
4
|
import { primeChamberSettings } from '@/shared/lib/settings/client';
|
|
5
|
+
import { initDocumentTheme } from '@/client/hooks/ui/theme';
|
|
5
6
|
|
|
6
7
|
import '@/client/tailwind.css';
|
|
7
8
|
import '@/shared/lib/markdown/katex-fonts.css';
|
|
@@ -12,6 +13,11 @@ const bootstrap = window.__OMP_BOOTSTRAP__;
|
|
|
12
13
|
|
|
13
14
|
primeChamberSettings(bootstrap?.appSettings);
|
|
14
15
|
|
|
16
|
+
// The catalog stylesheet and both theme attributes, before the first render:
|
|
17
|
+
// the server writes them into the shell, so this only repairs a document served
|
|
18
|
+
// by an older build.
|
|
19
|
+
initDocumentTheme();
|
|
20
|
+
|
|
15
21
|
// Fire-and-forget: warms the Shiki highlighter in parallel with hydration.
|
|
16
22
|
bootSyntax();
|
|
17
23
|
|
package/src/client/tailwind.css
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
2
|
@import "@xterm/xterm/css/xterm.css";
|
|
3
|
-
@plugin "@tailwindcss/typography";
|
|
4
3
|
|
|
5
4
|
@theme {
|
|
6
5
|
--color-ink: var(--theme-ink);
|
|
@@ -8,8 +7,6 @@
|
|
|
8
7
|
--color-canvas: var(--theme-canvas);
|
|
9
8
|
--color-error: var(--theme-error);
|
|
10
9
|
--color-success: var(--theme-success);
|
|
11
|
-
--color-success-bg: var(--theme-success-bg);
|
|
12
|
-
--color-success-border: var(--theme-success-border);
|
|
13
10
|
--color-meta: var(--theme-meta);
|
|
14
11
|
--color-info: var(--theme-info);
|
|
15
12
|
--color-info-hover: var(--theme-info-hover);
|
|
@@ -18,19 +15,23 @@
|
|
|
18
15
|
}
|
|
19
16
|
|
|
20
17
|
@layer base {
|
|
18
|
+
/* The default palette. Every other theme's block is generated from the
|
|
19
|
+
* catalog in `src/shared/lib/theme/palettes/` and injected as
|
|
20
|
+
* `html[data-theme=…]` by the SSR shell (see `shared/lib/theme/css.ts`) —
|
|
21
|
+
* hand-writing them here would duplicate the same eight values the appearance
|
|
22
|
+
* cards render as chips. */
|
|
21
23
|
:root {
|
|
22
24
|
--theme-paper: #faf8f3;
|
|
23
25
|
--theme-canvas: #f4f1ea;
|
|
24
26
|
--theme-ink: #141310;
|
|
25
27
|
--theme-error: #c8321e;
|
|
26
28
|
--theme-success: #047857;
|
|
27
|
-
--theme-success-bg: #ecfdf5;
|
|
28
|
-
--theme-success-border: #a7f3d0;
|
|
29
29
|
--theme-meta: #4f46e5;
|
|
30
30
|
--theme-info: #0284c7;
|
|
31
|
-
--theme-info-hover:
|
|
31
|
+
--theme-info-hover: color-mix(in srgb, var(--theme-info) 85%, var(--theme-ink));
|
|
32
32
|
--theme-warning: #c2410c;
|
|
33
|
-
--theme-warning-hover:
|
|
33
|
+
--theme-warning-hover: color-mix(in srgb, var(--theme-warning) 85%, var(--theme-ink));
|
|
34
|
+
color-scheme: light;
|
|
34
35
|
|
|
35
36
|
--paper-bg: var(--theme-canvas);
|
|
36
37
|
--paper-raised: var(--theme-paper);
|
|
@@ -42,51 +43,6 @@
|
|
|
42
43
|
--signal-red: var(--theme-error);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
[data-theme="contrast"] {
|
|
46
|
-
--theme-paper: #ffffff;
|
|
47
|
-
--theme-canvas: #f4f4f4;
|
|
48
|
-
--theme-ink: #111111;
|
|
49
|
-
--theme-error: #b42318;
|
|
50
|
-
--theme-success: #067647;
|
|
51
|
-
--theme-success-bg: #ecfdf3;
|
|
52
|
-
--theme-success-border: #abefc6;
|
|
53
|
-
--theme-meta: #4338ca;
|
|
54
|
-
--theme-info: #0369a1;
|
|
55
|
-
--theme-info-hover: #075985;
|
|
56
|
-
--theme-warning: #b54708;
|
|
57
|
-
--theme-warning-hover: #93370d;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
[data-theme="noir"] {
|
|
61
|
-
--theme-paper: #282c34;
|
|
62
|
-
--theme-canvas: #21252b;
|
|
63
|
-
--theme-ink: #abb2bf;
|
|
64
|
-
--theme-error: #e06c75;
|
|
65
|
-
--theme-success: #34d399;
|
|
66
|
-
--theme-success-bg: rgba(5, 150, 105, 0.15);
|
|
67
|
-
--theme-success-border: rgba(16, 185, 129, 0.2);
|
|
68
|
-
--theme-meta: #818cf8;
|
|
69
|
-
--theme-info: #61afef;
|
|
70
|
-
--theme-info-hover: #8ec7ff;
|
|
71
|
-
--theme-warning: #fb923c;
|
|
72
|
-
--theme-warning-hover: #fdba74;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
[data-theme="one-dark-pro-soft"] {
|
|
76
|
-
--theme-paper: #282c34;
|
|
77
|
-
--theme-canvas: #21252b;
|
|
78
|
-
--theme-ink: #abb2bf;
|
|
79
|
-
--theme-error: #e06c75;
|
|
80
|
-
--theme-success: #34d399;
|
|
81
|
-
--theme-success-bg: rgba(5, 150, 105, 0.15);
|
|
82
|
-
--theme-success-border: rgba(16, 185, 129, 0.2);
|
|
83
|
-
--theme-meta: #818cf8;
|
|
84
|
-
--theme-info: #61afef;
|
|
85
|
-
--theme-info-hover: #8ec7ff;
|
|
86
|
-
--theme-warning: #fb923c;
|
|
87
|
-
--theme-warning-hover: #fdba74;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
46
|
body {
|
|
91
47
|
font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
92
48
|
background-color: var(--theme-canvas);
|
|
@@ -235,6 +191,13 @@
|
|
|
235
191
|
.prose-content li { line-height: inherit; position: relative; }
|
|
236
192
|
.prose-content li + li { margin-top: 0.375rem; }
|
|
237
193
|
.prose-content li p { margin: 0.25rem 0; }
|
|
194
|
+
/* Tailwind's preflight makes every `img` block-level. Markdown images are
|
|
195
|
+
* inline content: a README badge row is one `<p align="center">` of
|
|
196
|
+
* `<a><img></a>` links, and a block image stacks it one badge per line (and
|
|
197
|
+
* pushes an image off the text line it was written on). `inline-block` restores
|
|
198
|
+
* the line box GitHub renders; the preflight's `max-width: 100%` still applies,
|
|
199
|
+
* and `vertical-align: middle` keeps badges of different heights aligned. */
|
|
200
|
+
.prose-content img { display: inline-block; vertical-align: middle; }
|
|
238
201
|
.prose-content ul > li::before { content: ''; display: block; position: absolute; left: -1.25em; top: 0; }
|
|
239
202
|
.prose-content blockquote {
|
|
240
203
|
border-left: 2px solid color-mix(in srgb, var(--theme-ink) 30%, transparent);
|
|
@@ -276,13 +239,13 @@
|
|
|
276
239
|
.prose-content .code-block pre { margin: 0; padding: 0.75rem; overflow-x: auto; line-height: var(--markdown-code-block-line-height); }
|
|
277
240
|
.prose-content .code-block code { font-family: 'Fira Code', monospace; font-size: 0.75rem; line-height: var(--markdown-code-block-line-height); }
|
|
278
241
|
.prose-content .code-block pre code { background: transparent; text-shadow: none; }
|
|
279
|
-
/* Shiki dual-theme tokens: the highlighter emits BOTH palettes (
|
|
242
|
+
/* Shiki dual-theme tokens: the highlighter emits BOTH palettes (one-light for
|
|
280
243
|
* light themes, one-dark-pro for dark themes) as CSS variables on every token
|
|
281
244
|
* span and no inline `color`, so the active [data-theme] selects the palette.
|
|
282
|
-
* Switching themes recolors code blocks with zero JS and no re-highlight.
|
|
245
|
+
* Switching themes recolors code blocks with zero JS and no re-highlight.
|
|
246
|
+
* The selector keys off the variant attribute, not a list of theme ids. */
|
|
283
247
|
.shiki span { color: var(--shiki-light); }
|
|
284
|
-
[data-theme="
|
|
285
|
-
[data-theme="one-dark-pro-soft"] .shiki span { color: var(--shiki-dark); }
|
|
248
|
+
html[data-theme-variant="dark"] .shiki span { color: var(--shiki-dark); }
|
|
286
249
|
.prose-content .katex-display { overflow-x: auto; overflow-y: hidden; }
|
|
287
250
|
|
|
288
251
|
/* Math placeholder: `marked.ts` emits a `.math-pending` span and KaTeX is
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Spawn arguments for a topic's side child.
|
|
8
|
+
*
|
|
9
|
+
* The side child is an ordinary `omp --mode rpc-ui` that resumes the topic's
|
|
10
|
+
* transcript. It runs WITH tools (unlike the TUI's `/btw`, which passes
|
|
11
|
+
* `--no-tools`): the panel exposes a real access-control dropdown, and an
|
|
12
|
+
* approval mode that governs nothing is a lie in the UI. What the child may
|
|
13
|
+
* actually do is therefore the `--approval-mode` flag's business, exactly as it
|
|
14
|
+
* is for a chat session.
|
|
15
|
+
*
|
|
16
|
+
* `--no-title` stays: auto-titling would spend a model call on a transcript
|
|
17
|
+
* nobody lists in the sidebar.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { ApprovalMode } from '@/shared/lib/omp/config/access-mode';
|
|
21
|
+
import type { BtwModel } from '@/shared/types';
|
|
22
|
+
|
|
23
|
+
export interface BtwSpawnSettings {
|
|
24
|
+
model?: BtwModel;
|
|
25
|
+
/** omp's own default when absent (`auto`). */
|
|
26
|
+
thinkingLevel?: string;
|
|
27
|
+
/** Spawn-time only: omp exposes no RPC to change the approval mode. */
|
|
28
|
+
approvalMode?: ApprovalMode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildBtwSpawnArgs(sessionFile: string, settings: BtwSpawnSettings): string[] {
|
|
32
|
+
const args = ['--resume', sessionFile, '--no-title'];
|
|
33
|
+
if (settings.model) args.push('--model', `${settings.model.provider}/${settings.model.id}`);
|
|
34
|
+
// 'auto' means "leave omp's level alone" — the same rule the composer applies,
|
|
35
|
+
// so it is omitted rather than translated into a concrete level.
|
|
36
|
+
if (settings.thinkingLevel && settings.thinkingLevel !== 'auto') {
|
|
37
|
+
args.push('--thinking', settings.thinkingLevel);
|
|
38
|
+
}
|
|
39
|
+
if (settings.approvalMode) args.push('--approval-mode', settings.approvalMode);
|
|
40
|
+
return args;
|
|
41
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One side question's lifecycle: asking, aborting, settling, and reclaiming the
|
|
8
|
+
* child that carried it.
|
|
9
|
+
*
|
|
10
|
+
* Extracted from `BtwRuntime` so that file stays under the repo's per-file size
|
|
11
|
+
* ceiling, in the same host-interface shape `session-commands.ts` uses for the
|
|
12
|
+
* chat wrapper. The runtime owns the process and the frame routing; the rules
|
|
13
|
+
* below decide when a turn starts, what a settled turn is worth, and when an
|
|
14
|
+
* idle child is worth keeping.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { RpcProcess, RpcFrame } from '@/server/lib/omp/rpc/process';
|
|
18
|
+
import { GET_STATE_TIMEOUT_MS, IDLE_DESTROY_MS, PROMPT_ACK_TIMEOUT_MS } from '@/server/lib/omp/rpc/constants';
|
|
19
|
+
import type { PendingUiDialogs } from '@/server/lib/omp/rpc/pending-ui-dialogs';
|
|
20
|
+
import { buildBtwPrompt } from '@/server/lib/btw/prompt';
|
|
21
|
+
import { appendBtwTurn, settleRunningBtwTurns, updateBtwTurn } from '@/server/lib/btw/store.server';
|
|
22
|
+
import type { BtwRuntimeSink } from '@/server/lib/btw/runtime.server';
|
|
23
|
+
import { toChatMessage } from '@/shared/lib/omp/session/mapper';
|
|
24
|
+
import { describeAssistantPhase, describeToolActivity } from '@/shared/lib/chat/timeline/tool-verbs';
|
|
25
|
+
import { isNoticeRow } from '@/shared/lib/chat/notice-row';
|
|
26
|
+
import type { ApprovalMode } from '@/shared/lib/omp/config/access-mode';
|
|
27
|
+
import type { AgentImage, BtwTurnStatus, ChatMessageData } from '@/shared/types';
|
|
28
|
+
|
|
29
|
+
/** Longest an aborted turn may stay unsettled before its child is reclaimed. */
|
|
30
|
+
const ABORT_GRACE_MS = 10_000;
|
|
31
|
+
|
|
32
|
+
/** The runtime state these rules read and write. */
|
|
33
|
+
export interface BtwLifecycleHost {
|
|
34
|
+
readonly topicId: string;
|
|
35
|
+
readonly sink: BtwRuntimeSink;
|
|
36
|
+
/** Ask/approval dialogs the child is blocked on. */
|
|
37
|
+
readonly dialogs: PendingUiDialogs;
|
|
38
|
+
/** A question is in flight in this topic. */
|
|
39
|
+
readonly running: boolean;
|
|
40
|
+
proc: RpcProcess | null;
|
|
41
|
+
idleTimer: NodeJS.Timeout | null;
|
|
42
|
+
abortTimer: NodeJS.Timeout | null;
|
|
43
|
+
lastIdleReset: number;
|
|
44
|
+
turnIndex: number | null;
|
|
45
|
+
/** True from the moment a turn starts settling until its row is written. */
|
|
46
|
+
settling: boolean;
|
|
47
|
+
/** The turn's conversation so far (chat-shaped; see `trackTurnMessage`). */
|
|
48
|
+
messages: ChatMessageData[];
|
|
49
|
+
/** Last activity phrase published, so repeats are not re-published. */
|
|
50
|
+
activity: string;
|
|
51
|
+
disposing: Promise<void> | null;
|
|
52
|
+
/** Approval mode the live child was spawned with. */
|
|
53
|
+
spawnedApprovalMode: ApprovalMode | undefined;
|
|
54
|
+
/** Spawn (or reuse) the child. */
|
|
55
|
+
start(): Promise<RpcProcess>;
|
|
56
|
+
/** Drop the child and everything scoped to it. */
|
|
57
|
+
teardownChild(): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Ask (or follow up) — the caller has already refused concurrent turns. */
|
|
61
|
+
export async function askSideQuestion(host: BtwLifecycleHost, question: string, images?: AgentImage[]): Promise<void> {
|
|
62
|
+
if (host.running) throw new Error('A side question is already running here.');
|
|
63
|
+
host.turnIndex = await appendBtwTurn(host.topicId, question);
|
|
64
|
+
host.messages = [];
|
|
65
|
+
host.activity = '';
|
|
66
|
+
host.sink.stateChanged();
|
|
67
|
+
try {
|
|
68
|
+
const proc = await host.start();
|
|
69
|
+
await proc.sendCommand(
|
|
70
|
+
{ type: 'prompt', message: buildBtwPrompt(question), ...(images?.length ? { images } : {}) },
|
|
71
|
+
PROMPT_ACK_TIMEOUT_MS,
|
|
72
|
+
);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
await settleSideTurn(host, 'failed');
|
|
75
|
+
host.sink.publish({
|
|
76
|
+
type: 'btw_error',
|
|
77
|
+
topicId: host.topicId,
|
|
78
|
+
message: error instanceof Error ? error.message : String(error),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
resetSideIdleTimer(host, true);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Cancel the running question, keeping whatever text already arrived. */
|
|
85
|
+
export async function abortSideTurn(host: BtwLifecycleHost): Promise<void> {
|
|
86
|
+
if (!host.running) return;
|
|
87
|
+
const proc = host.proc;
|
|
88
|
+
if (!proc?.isAlive) {
|
|
89
|
+
await settleSideTurn(host, 'cancelled');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await proc.sendCommand({ type: 'abort' }, GET_STATE_TIMEOUT_MS);
|
|
94
|
+
} catch {
|
|
95
|
+
// An unanswered abort is the escalate-to-cancel path below, not an error.
|
|
96
|
+
}
|
|
97
|
+
// `abort` parks the turn on a fresh grace window: the settle that follows is
|
|
98
|
+
// the one that decides. The guard is type-required — these fields are
|
|
99
|
+
// `Timeout | null` and `clearTimeout` takes `Timeout | undefined`.
|
|
100
|
+
if (host.abortTimer) clearTimeout(host.abortTimer);
|
|
101
|
+
const timer = setTimeout(() => {
|
|
102
|
+
host.abortTimer = null;
|
|
103
|
+
void disposeIfUnsettled(host);
|
|
104
|
+
}, ABORT_GRACE_MS);
|
|
105
|
+
timer.unref?.();
|
|
106
|
+
host.abortTimer = timer;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The turn's conversation as it accumulates, in the chat's own shape.
|
|
111
|
+
*
|
|
112
|
+
* omp emits `message_update` per SEGMENT, each carrying that segment's full
|
|
113
|
+
* accumulated content and its own message id — so a same-id frame replaces the
|
|
114
|
+
* entry in place and a new id appends. That is exactly the upsert the chat
|
|
115
|
+
* timeline's `onMessageUpdate` performs, and keeping it identical is what makes
|
|
116
|
+
* a side answer render through the chat's own `MessageList`: a tool the side
|
|
117
|
+
* child runs shows up here the same way it would in the chat.
|
|
118
|
+
*/
|
|
119
|
+
export function trackTurnMessage(host: BtwLifecycleHost, frame: RpcFrame): void {
|
|
120
|
+
const raw = frame.message;
|
|
121
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
|
|
122
|
+
const record = raw as Record<string, unknown>;
|
|
123
|
+
if (record.role === 'toolResult') return;
|
|
124
|
+
const turnIndex = host.turnIndex;
|
|
125
|
+
if (turnIndex === null) return;
|
|
126
|
+
const converted = toChatMessage(record);
|
|
127
|
+
if (!converted) return;
|
|
128
|
+
const existing = host.messages.findIndex((message) => message.id === converted.id);
|
|
129
|
+
host.messages =
|
|
130
|
+
existing === -1
|
|
131
|
+
? [...host.messages, converted]
|
|
132
|
+
: [...host.messages.slice(0, existing), converted, ...host.messages.slice(existing + 1)];
|
|
133
|
+
host.sink.publish({ type: 'btw_message', topicId: host.topicId, turnIndex, message: converted });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The activity phrase for the panel's indicator, from the side child's frames. */
|
|
137
|
+
export function trackTurnActivity(host: BtwLifecycleHost, frame: RpcFrame): void {
|
|
138
|
+
const verb = frame.type === 'tool_execution_start'
|
|
139
|
+
? describeToolActivity({
|
|
140
|
+
name: typeof frame.toolName === 'string' ? frame.toolName : undefined,
|
|
141
|
+
args: frame.args,
|
|
142
|
+
intent: typeof frame.intent === 'string' ? frame.intent : undefined,
|
|
143
|
+
})
|
|
144
|
+
: describeAssistantPhase(frame.assistantMessageEvent);
|
|
145
|
+
if (!verb || verb === host.activity) return;
|
|
146
|
+
host.activity = verb;
|
|
147
|
+
host.sink.publish({ type: 'btw_activity', topicId: host.topicId, verb });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The plain-text answer, for the turn's summary fields. */
|
|
151
|
+
function answerText(messages: ChatMessageData[], fallback: string): string {
|
|
152
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
153
|
+
const message = messages[index];
|
|
154
|
+
if (message.role === 'user' || isNoticeRow(message)) continue;
|
|
155
|
+
if (message.content) return message.content;
|
|
156
|
+
}
|
|
157
|
+
return fallback;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Write a turn's outcome. `busy` (`settling`) holds the topic live across the
|
|
162
|
+
* write: a state read landing in this window must not settle the row as
|
|
163
|
+
* `interrupted` (see `btwStateFor`).
|
|
164
|
+
*/
|
|
165
|
+
export async function settleSideTurn(host: BtwLifecycleHost, status: BtwTurnStatus, answer?: string): Promise<void> {
|
|
166
|
+
const turnIndex = host.turnIndex;
|
|
167
|
+
if (turnIndex === null) return;
|
|
168
|
+
host.turnIndex = null;
|
|
169
|
+
// `||`, not `??`: a turn that only ran tools has no prose, and the caller
|
|
170
|
+
// passes an empty string rather than undefined — the summary must then fall
|
|
171
|
+
// through to whatever the conversation does carry.
|
|
172
|
+
const text = answer || answerText(host.messages, '');
|
|
173
|
+
// A settled turn has no dialogs left to answer; a stale modal would sit over
|
|
174
|
+
// an idle panel with nothing behind it.
|
|
175
|
+
const hadDialogs = host.dialogs.list().length > 0;
|
|
176
|
+
host.dialogs.clear();
|
|
177
|
+
host.settling = true;
|
|
178
|
+
try {
|
|
179
|
+
await updateBtwTurn(host.topicId, turnIndex, { answer: text, status, messages: host.messages });
|
|
180
|
+
} finally {
|
|
181
|
+
host.settling = false;
|
|
182
|
+
host.messages = [];
|
|
183
|
+
host.sink.stateChanged();
|
|
184
|
+
if (hadDialogs) host.sink.stateChanged();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The child died. A turn that was in flight can never complete now. */
|
|
189
|
+
export function handleSideChildExit(host: BtwLifecycleHost, stderrTail: string): void {
|
|
190
|
+
const wasRunning = host.running;
|
|
191
|
+
host.proc = null;
|
|
192
|
+
host.spawnedApprovalMode = undefined;
|
|
193
|
+
const hadDialogs = host.dialogs.list().length > 0;
|
|
194
|
+
host.dialogs.clear();
|
|
195
|
+
if (!wasRunning) {
|
|
196
|
+
if (hadDialogs) host.sink.stateChanged();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
void settleRunningBtwTurns(host.topicId, 'interrupted').then(() => {
|
|
200
|
+
host.turnIndex = null;
|
|
201
|
+
host.messages = [];
|
|
202
|
+
host.sink.stateChanged();
|
|
203
|
+
const detail = stderrTail.trim().split('\n').pop();
|
|
204
|
+
host.sink.publish({
|
|
205
|
+
type: 'btw_error',
|
|
206
|
+
topicId: host.topicId,
|
|
207
|
+
message: `The side session exited before answering${detail ? `: ${detail}` : '.'}`,
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Clear the timers and the process, and drop the dialogs that belonged to it. */
|
|
213
|
+
export async function teardownSideChild(host: BtwLifecycleHost): Promise<void> {
|
|
214
|
+
// Type-required guards: the fields are `Timeout | null`, `clearTimeout` takes
|
|
215
|
+
// `Timeout | undefined`.
|
|
216
|
+
if (host.idleTimer) clearTimeout(host.idleTimer);
|
|
217
|
+
if (host.abortTimer) clearTimeout(host.abortTimer);
|
|
218
|
+
host.idleTimer = null;
|
|
219
|
+
host.abortTimer = null;
|
|
220
|
+
const hadDialogs = host.dialogs.list().length > 0;
|
|
221
|
+
host.dialogs.clear();
|
|
222
|
+
host.spawnedApprovalMode = undefined;
|
|
223
|
+
const proc = host.proc;
|
|
224
|
+
host.proc = null;
|
|
225
|
+
if (proc) await proc.dispose().catch(() => {});
|
|
226
|
+
host.disposing = null;
|
|
227
|
+
// A modal whose process is gone must not stay on screen.
|
|
228
|
+
if (hadDialogs) host.sink.stateChanged();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function disposeIfUnsettled(host: BtwLifecycleHost): Promise<void> {
|
|
232
|
+
if (host.running) await settleSideTurn(host, 'cancelled');
|
|
233
|
+
await host.teardownChild();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Reclaim an idle child after `IDLE_DESTROY_MS`. The reschedule is throttled:
|
|
238
|
+
* every streamed delta would otherwise clear and re-arm the timer.
|
|
239
|
+
*/
|
|
240
|
+
export function resetSideIdleTimer(host: BtwLifecycleHost, force = false): void {
|
|
241
|
+
const now = Date.now();
|
|
242
|
+
if (!force && host.idleTimer && now - host.lastIdleReset < 5_000) return;
|
|
243
|
+
host.lastIdleReset = now;
|
|
244
|
+
if (host.idleTimer) clearTimeout(host.idleTimer);
|
|
245
|
+
const timer = setTimeout(() => {
|
|
246
|
+
if (host.running) {
|
|
247
|
+
resetSideIdleTimer(host, true);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
void host.teardownChild();
|
|
251
|
+
}, IDLE_DESTROY_MS);
|
|
252
|
+
timer.unref?.();
|
|
253
|
+
host.idleTimer = timer;
|
|
254
|
+
}
|
|
@@ -4,10 +4,19 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* The side-question prompt.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* The side-question prompt.
|
|
8
|
+
*
|
|
9
|
+
* Derived from omp's own `/btw` template
|
|
10
|
+
* (`packages/coding-agent/src/prompts/system/btw-user.md`), with one
|
|
11
|
+
* deliberate divergence: the TUI's `/btw` runs the side child with
|
|
12
|
+
* `--no-tools` and its prompt says "NEVER use tools". The chamber's panel
|
|
13
|
+
* exposes a real access-control dropdown, which only means something when the
|
|
14
|
+
* child HAS a tool surface — so the tool prohibition is dropped and the
|
|
15
|
+
* approval mode governs what the side child may actually do.
|
|
16
|
+
*
|
|
17
|
+
* What is kept is the shape that makes a side question a side question: it
|
|
18
|
+
* answers briefly and directly, and it never opens a follow-up dialogue of its
|
|
19
|
+
* own — follow-ups are explicit user turns.
|
|
11
20
|
*
|
|
12
21
|
* The parent session's history is NOT part of this text: the side session
|
|
13
22
|
* resumes the parent's transcript, so the context arrives as real provider
|
|
@@ -19,7 +28,6 @@ export function buildBtwPrompt(question: string): string {
|
|
|
19
28
|
'<btw>',
|
|
20
29
|
'Ephemeral side question for current interactive session.',
|
|
21
30
|
'Answer briefly, directly; use conversation context already provided.',
|
|
22
|
-
'NEVER use tools.',
|
|
23
31
|
'NEVER ask follow-up questions.',
|
|
24
32
|
'Question:',
|
|
25
33
|
question,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { listBtwTopics, settleRunningBtwTurns } from '@/server/lib/btw/store.server';
|
|
22
22
|
import { BtwRuntime, type BtwRuntimeContext } from '@/server/lib/btw/runtime.server';
|
|
23
|
-
import type { BtwFrame, BtwState, BtwTopic } from '@/shared/types';
|
|
23
|
+
import type { BtwFrame, BtwState, BtwTopic, ExtensionUiDialogRequest } from '@/shared/types';
|
|
24
24
|
|
|
25
25
|
interface BtwRegistryHost {
|
|
26
26
|
runtimes: Map<string, BtwRuntime>;
|
|
@@ -111,10 +111,9 @@ export async function btwStateFor(sessionId: string): Promise<BtwState> {
|
|
|
111
111
|
// A runtime that is mid-`settle()` has already cleared its turn index but has
|
|
112
112
|
// not written the row yet; counting it as gone would race its own UPDATE and
|
|
113
113
|
// rewrite a completed turn as `interrupted`. `busy` covers that window.
|
|
114
|
+
const runtimes = sessionBtwRuntimes(sessionId);
|
|
114
115
|
const liveTopicIds = new Set(
|
|
115
|
-
|
|
116
|
-
.filter((runtime) => runtime.running || runtime.busy)
|
|
117
|
-
.map((runtime) => runtime.topicId),
|
|
116
|
+
runtimes.filter((runtime) => runtime.running || runtime.busy).map((runtime) => runtime.topicId),
|
|
118
117
|
);
|
|
119
118
|
|
|
120
119
|
const stale = topics.filter((topic) => topic.turns.some((turn) => turn.status === 'running') && !liveTopicIds.has(topic.id));
|
|
@@ -128,9 +127,19 @@ export async function btwStateFor(sessionId: string): Promise<BtwState> {
|
|
|
128
127
|
: { ...topic, turns: topic.turns.map((turn) => (turn.status === 'running' ? { ...turn, status: 'interrupted' as const } : turn)) },
|
|
129
128
|
);
|
|
130
129
|
|
|
130
|
+
const liveRuntime = runtimes.find((runtime) => runtime.running);
|
|
131
131
|
return {
|
|
132
132
|
topics: repaired,
|
|
133
133
|
runningTopicId: repaired.find((topic) => topic.turns.some((turn) => turn.status === 'running'))?.id ?? null,
|
|
134
|
+
live: liveRuntime?.liveTurn() ?? null,
|
|
135
|
+
// Dialogs live on the runtime, not in SQLite: they belong to the child
|
|
136
|
+
// process that raised them, and a child that is gone has none.
|
|
137
|
+
dialogs: runtimes.flatMap((runtime) =>
|
|
138
|
+
runtime.dialogs
|
|
139
|
+
.list()
|
|
140
|
+
.filter((request) => typeof request.id === 'string')
|
|
141
|
+
.map((request) => ({ topicId: runtime.topicId, request: request as unknown as ExtensionUiDialogRequest })),
|
|
142
|
+
),
|
|
134
143
|
};
|
|
135
144
|
}
|
|
136
145
|
|