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
|
@@ -9,27 +9,44 @@
|
|
|
9
9
|
* The side question runs in a second `omp --mode rpc-ui` child that resumes the
|
|
10
10
|
* topic's own transcript — a snapshot of the parent session taken when the
|
|
11
11
|
* topic first asks. That gives the parent's context as real provider messages
|
|
12
|
-
* (no re-prompting it as text, and the parent's prompt-cache prefix survives)
|
|
13
|
-
* never writes the parent file
|
|
14
|
-
*
|
|
12
|
+
* (no re-prompting it as text, and the parent's prompt-cache prefix survives)
|
|
13
|
+
* and never writes the parent file.
|
|
14
|
+
*
|
|
15
|
+
* The child runs WITH tools, unlike omp's TUI `/btw` (`--no-tools`). The panel
|
|
16
|
+
* offers a real access-control dropdown, and an approval mode that governs
|
|
17
|
+
* nothing would be a lie in the UI — so the tool surface is whatever
|
|
18
|
+
* `--approval-mode` allows, and a gated call parks the turn on an
|
|
19
|
+
* `extension_ui_request` the panel answers (`PendingUiDialogs`).
|
|
15
20
|
*
|
|
16
21
|
* The child is disposable by design: history lives in SQLite plus the topic
|
|
17
22
|
* transcript, so an idle reclaim costs a respawn, not the conversation — a
|
|
18
23
|
* follow-up resumes the transcript and still sees every earlier turn.
|
|
24
|
+
*
|
|
25
|
+
* This class owns the process and the frame routing; the rules about what a turn
|
|
26
|
+
* is worth live in `lifecycle.server.ts`.
|
|
19
27
|
*/
|
|
20
28
|
|
|
21
29
|
import { RpcProcess, type RpcFrame } from '@/server/lib/omp/rpc/process';
|
|
22
|
-
import { GET_STATE_TIMEOUT_MS,
|
|
30
|
+
import { GET_STATE_TIMEOUT_MS, READY_TIMEOUT_MS, type RpcSessionState } from '@/server/lib/omp/rpc/constants';
|
|
31
|
+
import { PendingUiDialogs } from '@/server/lib/omp/rpc/pending-ui-dialogs';
|
|
23
32
|
import { resolveOmpBin } from '@/server/lib/omp/core/cli';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
33
|
+
import { buildBtwSpawnArgs, type BtwSpawnSettings } from '@/server/lib/btw/child.server';
|
|
34
|
+
import {
|
|
35
|
+
abortSideTurn,
|
|
36
|
+
askSideQuestion,
|
|
37
|
+
handleSideChildExit,
|
|
38
|
+
resetSideIdleTimer,
|
|
39
|
+
settleSideTurn,
|
|
40
|
+
teardownSideChild,
|
|
41
|
+
trackTurnActivity,
|
|
42
|
+
trackTurnMessage,
|
|
43
|
+
type BtwLifecycleHost,
|
|
44
|
+
} from '@/server/lib/btw/lifecycle.server';
|
|
45
|
+
import { setBtwTopicModel, setBtwTopicThinkingLevel } from '@/server/lib/btw/store.server';
|
|
26
46
|
import { createBtwWorkspace, resolveBtwWorkspacePaths, type BtwWorkspace } from '@/server/lib/btw/session-copy.server';
|
|
27
|
-
import { isRecord } from '@/shared/lib/util/guards';
|
|
28
47
|
import { finalAnswer, finalStatus, isAnswerableUiMethod } from '@/server/lib/btw/frames';
|
|
29
|
-
import type {
|
|
30
|
-
|
|
31
|
-
/** Longest an aborted turn may stay unsettled before its child is reclaimed. */
|
|
32
|
-
const ABORT_GRACE_MS = 10_000;
|
|
48
|
+
import type { ApprovalMode } from '@/shared/lib/omp/config/access-mode';
|
|
49
|
+
import type { AgentImage, BtwFrame, BtwLiveTurn, BtwModel, BtwTurnStatus, ChatMessageData } from '@/shared/types';
|
|
33
50
|
|
|
34
51
|
/** Prompt images, validated by the route before they reach the child. */
|
|
35
52
|
export type BtwImages = AgentImage[];
|
|
@@ -41,6 +58,8 @@ export interface BtwRuntimeContext {
|
|
|
41
58
|
parentSessionFile: string;
|
|
42
59
|
cwd: string;
|
|
43
60
|
model?: BtwModel;
|
|
61
|
+
thinkingLevel?: string;
|
|
62
|
+
approvalMode?: ApprovalMode;
|
|
44
63
|
}
|
|
45
64
|
|
|
46
65
|
/** Where the runtime reports what happened. */
|
|
@@ -61,21 +80,28 @@ export class BtwError extends Error {
|
|
|
61
80
|
}
|
|
62
81
|
}
|
|
63
82
|
|
|
64
|
-
export class BtwRuntime {
|
|
65
|
-
|
|
83
|
+
export class BtwRuntime implements BtwLifecycleHost {
|
|
84
|
+
proc: RpcProcess | null = null;
|
|
66
85
|
private starting: Promise<RpcProcess> | null = null;
|
|
67
86
|
private workspace: BtwWorkspace | null = null;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
idleTimer: NodeJS.Timeout | null = null;
|
|
88
|
+
abortTimer: NodeJS.Timeout | null = null;
|
|
89
|
+
lastIdleReset = 0;
|
|
90
|
+
turnIndex: number | null = null;
|
|
91
|
+
settling = false;
|
|
92
|
+
/** The turn's conversation so far (chat-shaped; see `trackTurnMessage`). */
|
|
93
|
+
messages: ChatMessageData[] = [];
|
|
94
|
+
/** Last activity phrase published, so repeats are not re-published. */
|
|
95
|
+
activity = '';
|
|
96
|
+
disposing: Promise<void> | null = null;
|
|
97
|
+
/** Ask/approval dialogs this child is blocked on (see the module note). */
|
|
98
|
+
readonly dialogs = new PendingUiDialogs();
|
|
99
|
+
/** The approval mode the live child was actually spawned with. */
|
|
100
|
+
spawnedApprovalMode: ApprovalMode | undefined;
|
|
75
101
|
|
|
76
102
|
constructor(
|
|
77
103
|
private readonly context: BtwRuntimeContext,
|
|
78
|
-
|
|
104
|
+
readonly sink: BtwRuntimeSink,
|
|
79
105
|
) {}
|
|
80
106
|
|
|
81
107
|
get topicId(): string {
|
|
@@ -98,49 +124,61 @@ export class BtwRuntime {
|
|
|
98
124
|
return this.settling;
|
|
99
125
|
}
|
|
100
126
|
|
|
101
|
-
/**
|
|
102
|
-
|
|
103
|
-
if (this.
|
|
104
|
-
|
|
105
|
-
|
|
127
|
+
/** The in-flight turn as the server has accumulated it, for `btw_state`. */
|
|
128
|
+
liveTurn(): BtwLiveTurn | null {
|
|
129
|
+
if (this.turnIndex === null) return null;
|
|
130
|
+
return {
|
|
131
|
+
topicId: this.topicId,
|
|
132
|
+
turnIndex: this.turnIndex,
|
|
133
|
+
messages: this.messages,
|
|
134
|
+
activity: this.activity,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
ask(question: string, images?: BtwImages): Promise<void> {
|
|
139
|
+
return askSideQuestion(this, question, images);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
abort(): Promise<void> {
|
|
143
|
+
return abortSideTurn(this);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Re-target the live child's model. `set_model` applies to the RUNNING turn
|
|
148
|
+
* (omp resolves it immediately), which is exactly what the panel's dropdown
|
|
149
|
+
* promises: the answer in flight continues on the model the user just chose.
|
|
150
|
+
*/
|
|
151
|
+
async setModel(provider: string, modelId: string): Promise<void> {
|
|
152
|
+
await setBtwTopicModel(this.topicId, { provider, id: modelId });
|
|
106
153
|
this.sink.stateChanged();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
{ type: 'prompt', message: buildBtwPrompt(question), ...(images?.length ? { images } : {}) },
|
|
111
|
-
PROMPT_ACK_TIMEOUT_MS,
|
|
112
|
-
);
|
|
113
|
-
} catch (error) {
|
|
114
|
-
await this.settle('failed');
|
|
115
|
-
this.sink.publish({
|
|
116
|
-
type: 'btw_error',
|
|
117
|
-
topicId: this.topicId,
|
|
118
|
-
message: error instanceof Error ? error.message : String(error),
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
this.resetIdleTimer(true);
|
|
154
|
+
const proc = this.proc;
|
|
155
|
+
if (!proc?.isAlive) return;
|
|
156
|
+
await proc.sendCommand({ type: 'set_model', provider, modelId }, GET_STATE_TIMEOUT_MS);
|
|
122
157
|
}
|
|
123
158
|
|
|
124
|
-
/**
|
|
125
|
-
async
|
|
126
|
-
if (!this.running) return;
|
|
159
|
+
/** Re-target the live child's thinking level (`auto` leaves omp alone). */
|
|
160
|
+
async setThinkingLevel(level: string): Promise<void> {
|
|
127
161
|
const proc = this.proc;
|
|
128
|
-
if (!proc?.isAlive)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
162
|
+
if (!proc?.isAlive || level === 'auto') return;
|
|
163
|
+
await proc.sendCommand({ type: 'set_thinking_level', level }, GET_STATE_TIMEOUT_MS);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Adopt a new approval mode. omp has no RPC for it — the flag is spawn-time
|
|
168
|
+
* only — so an idle child is destroyed and the next question respawns it.
|
|
169
|
+
* A running turn is never killed for this (same rule as the chat session's
|
|
170
|
+
* `reconcileSpawnApprovalMode`): the mode takes effect from the next child.
|
|
171
|
+
*/
|
|
172
|
+
async setApprovalMode(mode: ApprovalMode): Promise<void> {
|
|
173
|
+
this.context.approvalMode = mode;
|
|
174
|
+
if (this.spawnedApprovalMode === mode || this.running) return;
|
|
175
|
+
await this.teardownChild();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Answer a dialog this child is blocked on. */
|
|
179
|
+
respondToDialog(id: string, response: Record<string, unknown>): void {
|
|
180
|
+
this.proc?.sendFrame({ type: 'extension_ui_response', id, ...response });
|
|
181
|
+
if (this.dialogs.resolve(id)) this.sink.stateChanged();
|
|
144
182
|
}
|
|
145
183
|
|
|
146
184
|
/** Reclaim the child; the topic's history survives in SQLite and on disk. */
|
|
@@ -148,26 +186,14 @@ export class BtwRuntime {
|
|
|
148
186
|
if (this.disposing) return this.disposing;
|
|
149
187
|
if (this.running) {
|
|
150
188
|
// A turn would die with the child — settle it rather than lose the answer.
|
|
151
|
-
await this
|
|
189
|
+
await settleSideTurn(this, 'cancelled');
|
|
152
190
|
}
|
|
153
|
-
this.disposing = this.
|
|
191
|
+
this.disposing = this.teardownChild();
|
|
154
192
|
return this.disposing;
|
|
155
193
|
}
|
|
156
194
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (this.abortTimer) clearTimeout(this.abortTimer);
|
|
160
|
-
this.idleTimer = null;
|
|
161
|
-
this.abortTimer = null;
|
|
162
|
-
const proc = this.proc;
|
|
163
|
-
this.proc = null;
|
|
164
|
-
if (proc) await proc.dispose().catch(() => {});
|
|
165
|
-
this.disposing = null;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private async disposeIfUnsettled(): Promise<void> {
|
|
169
|
-
if (this.running) await this.settle('cancelled');
|
|
170
|
-
await this.dispose();
|
|
195
|
+
async teardownChild(): Promise<void> {
|
|
196
|
+
await teardownSideChild(this);
|
|
171
197
|
}
|
|
172
198
|
|
|
173
199
|
private ensureWorkspace(): Promise<BtwWorkspace> {
|
|
@@ -190,11 +216,10 @@ export class BtwRuntime {
|
|
|
190
216
|
this.workspace = workspace;
|
|
191
217
|
// The snapshot's leaf is what a later promotion must still find in the
|
|
192
218
|
// parent; a topic resumed from disk keeps the leaf it was created with.
|
|
193
|
-
if (workspace.leafId) await setBtwTopicLeaf(this.context.topicId, workspace.leafId);
|
|
194
219
|
return workspace;
|
|
195
220
|
}
|
|
196
221
|
|
|
197
|
-
|
|
222
|
+
start(): Promise<RpcProcess> {
|
|
198
223
|
if (this.starting) return this.starting;
|
|
199
224
|
const started = this.spawnChild();
|
|
200
225
|
this.starting = started;
|
|
@@ -210,35 +235,41 @@ export class BtwRuntime {
|
|
|
210
235
|
const bin = resolveOmpBin();
|
|
211
236
|
if (!bin) throw new BtwError('The omp binary could not be found.', 'btw_unavailable');
|
|
212
237
|
const workspace = await this.ensureWorkspace();
|
|
213
|
-
const
|
|
238
|
+
const settings: BtwSpawnSettings = {
|
|
239
|
+
model: this.context.model,
|
|
240
|
+
thinkingLevel: this.context.thinkingLevel,
|
|
241
|
+
approvalMode: this.context.approvalMode,
|
|
242
|
+
};
|
|
214
243
|
const proc = new RpcProcess({
|
|
215
244
|
cwd: this.context.cwd,
|
|
216
|
-
extraArgs:
|
|
217
|
-
'--resume',
|
|
218
|
-
workspace.sessionFile,
|
|
219
|
-
// Side questions never touch tools, so no approval mode is needed.
|
|
220
|
-
'--no-tools',
|
|
221
|
-
// Auto-titling would spend a model call on a transcript nobody lists.
|
|
222
|
-
'--no-title',
|
|
223
|
-
...(model ? ['--model', `${model.provider}/${model.id}`] : []),
|
|
224
|
-
],
|
|
245
|
+
extraArgs: buildBtwSpawnArgs(workspace.sessionFile, settings),
|
|
225
246
|
onFrame: (frame) => this.handleFrame(frame),
|
|
226
|
-
onExit: (info) => this
|
|
247
|
+
onExit: (info) => handleSideChildExit(this, info.stderrTail),
|
|
227
248
|
});
|
|
228
249
|
this.proc = proc;
|
|
250
|
+
this.spawnedApprovalMode = this.context.approvalMode;
|
|
229
251
|
const ready = await proc.waitReady(READY_TIMEOUT_MS);
|
|
230
252
|
await proc.negotiateProtocol(ready);
|
|
231
253
|
await this.captureModel(proc);
|
|
232
254
|
return proc;
|
|
233
255
|
}
|
|
234
256
|
|
|
235
|
-
/**
|
|
257
|
+
/**
|
|
258
|
+
* Record what the child reports about itself, for the panel's model chip and
|
|
259
|
+
* the run footer: its display name and the thinking level it actually runs at.
|
|
260
|
+
* `get_state` is the only source — the topic row holds what was REQUESTED, and
|
|
261
|
+
* omp may resolve it differently.
|
|
262
|
+
*/
|
|
236
263
|
private async captureModel(proc: RpcProcess): Promise<void> {
|
|
237
264
|
try {
|
|
238
265
|
const state = await proc.sendCommand<RpcSessionState>({ type: 'get_state' }, GET_STATE_TIMEOUT_MS);
|
|
239
266
|
const reported = state.model;
|
|
240
267
|
if (reported?.provider && reported.id && reported.name) {
|
|
241
|
-
await setBtwTopicModel(this.
|
|
268
|
+
await setBtwTopicModel(this.topicId, { provider: reported.provider, id: reported.id, name: reported.name });
|
|
269
|
+
this.sink.stateChanged();
|
|
270
|
+
}
|
|
271
|
+
if (typeof state.thinkingLevel === 'string' && state.thinkingLevel) {
|
|
272
|
+
await setBtwTopicThinkingLevel(this.topicId, state.thinkingLevel);
|
|
242
273
|
this.sink.stateChanged();
|
|
243
274
|
}
|
|
244
275
|
} catch {
|
|
@@ -247,78 +278,40 @@ export class BtwRuntime {
|
|
|
247
278
|
}
|
|
248
279
|
|
|
249
280
|
private handleFrame(frame: RpcFrame): void {
|
|
250
|
-
if (frame.type === 'message_update') {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
281
|
+
if (frame.type === 'message_update' || frame.type === 'message_start') {
|
|
282
|
+
// The turn's conversation is accumulated in the chat's own shape (see
|
|
283
|
+
// `trackTurnMessage`): thinking, prose, tool calls and their output all
|
|
284
|
+
// reach the panel through the same mapper the chat timeline uses.
|
|
285
|
+
trackTurnMessage(this, frame);
|
|
286
|
+
trackTurnActivity(this, frame);
|
|
287
|
+
} else if (frame.type === 'message_end') {
|
|
288
|
+
// The completed message is the authoritative copy: it carries the turn's
|
|
289
|
+
// real usage, where the streaming frames report zeros. Upserting it
|
|
290
|
+
// replaces the streaming row in place (same message id), which is what
|
|
291
|
+
// puts token counts on the panel's run footer.
|
|
292
|
+
trackTurnMessage(this, frame);
|
|
293
|
+
} else if (frame.type === 'tool_execution_start') {
|
|
294
|
+
trackTurnActivity(this, frame);
|
|
256
295
|
} else if (frame.type === 'agent_end') {
|
|
257
296
|
// `isTerminal: false` means maintenance scheduled more work; the turn is
|
|
258
297
|
// not done until a terminal end arrives (mirrors the session wrapper).
|
|
259
298
|
if (frame.isTerminal === false) return;
|
|
260
|
-
void this
|
|
299
|
+
void settleSideTurn(this, finalStatus(frame.messages), finalAnswer(frame.messages, ''));
|
|
261
300
|
} else if (frame.type === 'extension_ui_request') {
|
|
262
|
-
this.
|
|
301
|
+
this.trackDialog(frame);
|
|
263
302
|
}
|
|
264
|
-
this
|
|
303
|
+
resetSideIdleTimer(this);
|
|
265
304
|
}
|
|
266
305
|
|
|
267
|
-
/**
|
|
268
|
-
*
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
private async settle(status: BtwTurnStatus, answer?: string): Promise<void> {
|
|
276
|
-
const turnIndex = this.turnIndex;
|
|
277
|
-
if (turnIndex === null) return;
|
|
278
|
-
this.turnIndex = null;
|
|
279
|
-
const text = answer ?? this.answer;
|
|
280
|
-
this.answer = '';
|
|
281
|
-
// `busy` holds the topic live across the write: a state read that lands in
|
|
282
|
-
// this window must not settle the row as `interrupted` (see `btwStateFor`).
|
|
283
|
-
this.settling = true;
|
|
284
|
-
try {
|
|
285
|
-
await updateBtwTurn(this.topicId, turnIndex, { answer: text, status });
|
|
286
|
-
} finally {
|
|
287
|
-
this.settling = false;
|
|
288
|
-
this.sink.stateChanged();
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
private handleExit(stderrTail: string): void {
|
|
293
|
-
const wasRunning = this.running;
|
|
294
|
-
this.proc = null;
|
|
295
|
-
if (!wasRunning) return;
|
|
296
|
-
void settleRunningBtwTurns(this.topicId, 'interrupted').then(() => {
|
|
297
|
-
this.turnIndex = null;
|
|
298
|
-
this.answer = '';
|
|
299
|
-
this.sink.stateChanged();
|
|
300
|
-
const detail = stderrTail.trim().split('\n').pop();
|
|
301
|
-
this.sink.publish({
|
|
302
|
-
type: 'btw_error',
|
|
303
|
-
topicId: this.topicId,
|
|
304
|
-
message: `The side session exited before answering${detail ? `: ${detail}` : '.'}`,
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
private resetIdleTimer(force = false): void {
|
|
310
|
-
const now = Date.now();
|
|
311
|
-
if (!force && this.idleTimer && now - this.lastIdleReset < 5_000) return;
|
|
312
|
-
this.lastIdleReset = now;
|
|
313
|
-
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
314
|
-
const timer = setTimeout(() => {
|
|
315
|
-
if (this.running) {
|
|
316
|
-
this.resetIdleTimer(true);
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
void this.dispose();
|
|
320
|
-
}, IDLE_DESTROY_MS);
|
|
321
|
-
timer.unref?.();
|
|
322
|
-
this.idleTimer = timer;
|
|
306
|
+
/**
|
|
307
|
+
* Record an ask/approval dialog so the panel can render it. Unlike the TUI
|
|
308
|
+
* there is no tool card to host an `ask` inline, so every answerable request
|
|
309
|
+
* becomes a modal — and the state republish is what puts it on screen.
|
|
310
|
+
*/
|
|
311
|
+
private trackDialog(frame: RpcFrame): void {
|
|
312
|
+
if (typeof frame.id !== 'string' || !isAnswerableUiMethod(frame.method)) return;
|
|
313
|
+
if (this.dialogs.track(frame)) this.sink.stateChanged();
|
|
323
314
|
}
|
|
324
315
|
}
|
|
316
|
+
|
|
317
|
+
export type { BtwTurnStatus };
|
|
@@ -19,6 +19,8 @@ const BTW_SCHEMA_SQL = `
|
|
|
19
19
|
provider TEXT,
|
|
20
20
|
model_id TEXT,
|
|
21
21
|
model_name TEXT,
|
|
22
|
+
thinking_level TEXT,
|
|
23
|
+
approval_mode TEXT,
|
|
22
24
|
leaf_id TEXT,
|
|
23
25
|
promoted_session_id TEXT,
|
|
24
26
|
created_at INTEGER NOT NULL,
|
|
@@ -31,6 +33,7 @@ const BTW_SCHEMA_SQL = `
|
|
|
31
33
|
turn_index INTEGER NOT NULL,
|
|
32
34
|
question TEXT NOT NULL DEFAULT '',
|
|
33
35
|
answer TEXT NOT NULL DEFAULT '',
|
|
36
|
+
messages TEXT NOT NULL DEFAULT '[]',
|
|
34
37
|
status TEXT NOT NULL,
|
|
35
38
|
created_at INTEGER NOT NULL,
|
|
36
39
|
updated_at INTEGER NOT NULL,
|
|
@@ -38,7 +41,31 @@ const BTW_SCHEMA_SQL = `
|
|
|
38
41
|
);
|
|
39
42
|
`;
|
|
40
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Columns added after the tables shipped. `CREATE TABLE IF NOT EXISTS` leaves an
|
|
46
|
+
* existing table alone, so an install created before these existed would miss
|
|
47
|
+
* them and every read would fail on the absent column. Additive only: a column
|
|
48
|
+
* is added once, and a fresh database already has it from the DDL above.
|
|
49
|
+
*/
|
|
50
|
+
const BTW_ADDED_COLUMNS: { table: string; column: string; ddl: string }[] = [
|
|
51
|
+
{ table: 'btw_topics', column: 'thinking_level', ddl: 'ALTER TABLE btw_topics ADD COLUMN thinking_level TEXT' },
|
|
52
|
+
{ table: 'btw_topics', column: 'approval_mode', ddl: 'ALTER TABLE btw_topics ADD COLUMN approval_mode TEXT' },
|
|
53
|
+
// A turn's conversation used to be the plain `answer` string alone; the panel
|
|
54
|
+
// now renders the chat's own ChatMessageData, so the column arrives with the
|
|
55
|
+
// default an empty conversation has.
|
|
56
|
+
{ table: 'btw_turns', column: 'messages', ddl: "ALTER TABLE btw_turns ADD COLUMN messages TEXT NOT NULL DEFAULT '[]'" },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
async function hasColumn(db: DbClient, table: string, column: string): Promise<boolean> {
|
|
60
|
+
const rows = (await db.all(`PRAGMA table_info(${table})`)) as { name?: string }[];
|
|
61
|
+
return rows.some((row) => row.name === column);
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
/** Idempotent: side questions live beside the session they belong to. */
|
|
42
65
|
export async function ensureBtwSchema(db: DbClient): Promise<void> {
|
|
43
66
|
await db.exec(BTW_SCHEMA_SQL);
|
|
67
|
+
for (const added of BTW_ADDED_COLUMNS) {
|
|
68
|
+
if (await hasColumn(db, added.table, added.column)) continue;
|
|
69
|
+
await db.exec(added.ddl);
|
|
70
|
+
}
|
|
44
71
|
}
|
|
@@ -21,15 +21,42 @@ import { findSessionFileById } from '@/server/lib/omp/session/locator';
|
|
|
21
21
|
import { readRawHeaderLine } from '@/server/lib/omp/session/files';
|
|
22
22
|
import { loadSessionModel } from '@/server/lib/omp/session/messages';
|
|
23
23
|
import { resolveSpawnCwd } from '@/server/lib/omp/rpc/manager';
|
|
24
|
-
import {
|
|
24
|
+
import { loadPersistedAccessMode } from '@/shared/lib/omp/config/access-mode.server';
|
|
25
|
+
import type { ApprovalMode } from '@/shared/lib/omp/config/access-mode';
|
|
26
|
+
import { composeMessageWithTextAttachments, type AttachedTextFileData } from '@/shared/lib/chat/attachments';
|
|
27
|
+
import {
|
|
28
|
+
createBtwTopic,
|
|
29
|
+
deleteBtwTopic,
|
|
30
|
+
getBtwTopic,
|
|
31
|
+
readBtwTopicLeaf,
|
|
32
|
+
setBtwTopicApprovalMode,
|
|
33
|
+
setBtwTopicModel,
|
|
34
|
+
setBtwTopicPromoted,
|
|
35
|
+
setBtwTopicThinkingLevel,
|
|
36
|
+
} from '@/server/lib/btw/store.server';
|
|
25
37
|
import { promoteBtwTopic as materializePromotedTopic, readTranscriptLeaf, removeBtwWorkspace, resolveBtwWorkspacePaths } from '@/server/lib/btw/session-copy.server';
|
|
26
38
|
import { btwStateFor, ensureBtwRuntime, findRunningBtwRuntime, forgetBtwRuntime, getBtwRuntime, publishBtwState } from '@/server/lib/btw/registry.server';
|
|
27
39
|
import { BtwError, type BtwImages } from '@/server/lib/btw/runtime.server';
|
|
28
|
-
import type { BtwState } from '@/shared/types';
|
|
40
|
+
import type { BtwModel, BtwState } from '@/shared/types';
|
|
29
41
|
|
|
30
42
|
/** Longest a topic label may be, matching the sidebar's session-title length. */
|
|
31
43
|
const TOPIC_TITLE_CHARS = 60;
|
|
32
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Marks a session that was promoted out of a side question, in the sidebar list
|
|
47
|
+
* the user reads. Applied to the PROMOTED SESSION only — the topic's own label
|
|
48
|
+
* in the panel keeps the plain question.
|
|
49
|
+
*
|
|
50
|
+
* It lives in the session title because that is what the sidebar renders, and
|
|
51
|
+
* the title slot (line 1) is authoritative over the header. omp's auto-title
|
|
52
|
+
* cannot overwrite it: its latch is `autoTitlePending = messageCount === 0`, and
|
|
53
|
+
* a promoted transcript always carries the parent's history.
|
|
54
|
+
*
|
|
55
|
+
* Note the sidebar capitalizes the first character (`SessionItem`), so this
|
|
56
|
+
* renders as `Btw: …`.
|
|
57
|
+
*/
|
|
58
|
+
const PROMOTED_TITLE_PREFIX = 'btw: ';
|
|
59
|
+
|
|
33
60
|
function topicTitle(question: string): string {
|
|
34
61
|
const firstLine = question.trim().split('\n')[0].trim();
|
|
35
62
|
return firstLine.slice(0, TOPIC_TITLE_CHARS);
|
|
@@ -59,6 +86,22 @@ export interface AskBtwInput {
|
|
|
59
86
|
topicId?: string;
|
|
60
87
|
question: string;
|
|
61
88
|
images?: BtwImages;
|
|
89
|
+
/**
|
|
90
|
+
* Inline-able text files, read by the client at attach time and sent with the
|
|
91
|
+
* question. They are appended to the prompt body exactly as a chat prompt
|
|
92
|
+
* carries them (`composeMessageWithTextAttachments`), so a side question can
|
|
93
|
+
* be asked about a file the same way a chat turn can.
|
|
94
|
+
*/
|
|
95
|
+
textFiles?: AttachedTextFileData[];
|
|
96
|
+
/**
|
|
97
|
+
* Composer picks for the FIRST question of a topic, which has no row to write
|
|
98
|
+
* them to yet. Later questions address an existing topic and are re-targeted
|
|
99
|
+
* through the dedicated `set_model` / `set_thinking_level` / `set_access_mode`
|
|
100
|
+
* actions instead.
|
|
101
|
+
*/
|
|
102
|
+
model?: BtwModel;
|
|
103
|
+
thinkingLevel?: string;
|
|
104
|
+
approvalMode?: ApprovalMode;
|
|
62
105
|
}
|
|
63
106
|
|
|
64
107
|
export async function askBtw(sessionId: string, input: AskBtwInput): Promise<BtwState> {
|
|
@@ -76,12 +119,15 @@ export async function askBtw(sessionId: string, input: AskBtwInput): Promise<Btw
|
|
|
76
119
|
throw new BtwError('Unknown side question.', 'btw_topic_not_found');
|
|
77
120
|
}
|
|
78
121
|
|
|
122
|
+
const approvalMode = input.approvalMode ?? (await loadPersistedAccessMode());
|
|
79
123
|
const topic =
|
|
80
124
|
existing ??
|
|
81
125
|
(await createBtwTopic({
|
|
82
126
|
sessionId,
|
|
83
127
|
title: topicTitle(question),
|
|
84
|
-
model: await topicModel(parent.sessionFile),
|
|
128
|
+
model: input.model ?? (await topicModel(parent.sessionFile)),
|
|
129
|
+
thinkingLevel: input.thinkingLevel,
|
|
130
|
+
approvalMode,
|
|
85
131
|
}));
|
|
86
132
|
|
|
87
133
|
const runtime = ensureBtwRuntime({
|
|
@@ -90,9 +136,64 @@ export async function askBtw(sessionId: string, input: AskBtwInput): Promise<Btw
|
|
|
90
136
|
parentSessionFile: parent.sessionFile,
|
|
91
137
|
cwd: parent.cwd,
|
|
92
138
|
model: topic.model,
|
|
139
|
+
thinkingLevel: topic.thinkingLevel,
|
|
140
|
+
// The topic's own mode wins once set; a fresh topic adopts the composer's.
|
|
141
|
+
approvalMode: topic.approvalMode ?? approvalMode,
|
|
93
142
|
});
|
|
94
143
|
|
|
95
|
-
await runtime.ask(question, input.images);
|
|
144
|
+
await runtime.ask(composeMessageWithTextAttachments(question, input.textFiles ?? []), input.images);
|
|
145
|
+
return btwStateFor(sessionId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** One topic of this session, or a `btw_topic_not_found` refusal. */
|
|
149
|
+
async function requireTopic(sessionId: string, topicId: string) {
|
|
150
|
+
const topic = await getBtwTopic(topicId);
|
|
151
|
+
if (!topic || topic.sessionId !== sessionId) throw new BtwError('Unknown side question.', 'btw_topic_not_found');
|
|
152
|
+
return topic;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Re-target a topic's model. With a live child the change reaches it through
|
|
157
|
+
* `set_model` (omp applies it to the running turn, which is what the dropdown
|
|
158
|
+
* promises); an idle topic simply records it for the next spawn.
|
|
159
|
+
*/
|
|
160
|
+
export async function setBtwModel(sessionId: string, topicId: string, provider: string, modelId: string): Promise<BtwState> {
|
|
161
|
+
await requireTopic(sessionId, topicId);
|
|
162
|
+
const runtime = getBtwRuntime(topicId);
|
|
163
|
+
if (runtime) {
|
|
164
|
+
await runtime.setModel(provider, modelId);
|
|
165
|
+
} else {
|
|
166
|
+
await setBtwTopicModel(topicId, { provider, id: modelId });
|
|
167
|
+
await publishBtwState(sessionId);
|
|
168
|
+
}
|
|
169
|
+
return btwStateFor(sessionId);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function setBtwThinkingLevel(sessionId: string, topicId: string, level: string): Promise<BtwState> {
|
|
173
|
+
await requireTopic(sessionId, topicId);
|
|
174
|
+
await setBtwTopicThinkingLevel(topicId, level === 'auto' ? null : level);
|
|
175
|
+
await getBtwRuntime(topicId)?.setThinkingLevel(level);
|
|
176
|
+
await publishBtwState(sessionId);
|
|
177
|
+
return btwStateFor(sessionId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Adopt a new tool-approval mode for a topic. omp has no RPC for it, so an idle
|
|
182
|
+
* child is dropped and the next question respawns with the new flag; a running
|
|
183
|
+
* turn keeps its child (and therefore its mode) until it settles.
|
|
184
|
+
*/
|
|
185
|
+
export async function setBtwApprovalMode(sessionId: string, topicId: string, mode: ApprovalMode): Promise<BtwState> {
|
|
186
|
+
await requireTopic(sessionId, topicId);
|
|
187
|
+
await setBtwTopicApprovalMode(topicId, mode);
|
|
188
|
+
await getBtwRuntime(topicId)?.setApprovalMode(mode);
|
|
189
|
+
await publishBtwState(sessionId);
|
|
190
|
+
return btwStateFor(sessionId);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Answer an ask/approval dialog a side child is blocked on. */
|
|
194
|
+
export async function respondBtwDialog(sessionId: string, topicId: string, id: string, response: Record<string, unknown>): Promise<BtwState> {
|
|
195
|
+
await requireTopic(sessionId, topicId);
|
|
196
|
+
getBtwRuntime(topicId)?.respondToDialog(id, response);
|
|
96
197
|
return btwStateFor(sessionId);
|
|
97
198
|
}
|
|
98
199
|
|
|
@@ -145,7 +246,7 @@ export async function promoteBtw(sessionId: string, topicId: string): Promise<{
|
|
|
145
246
|
}
|
|
146
247
|
|
|
147
248
|
const paths = await resolveBtwWorkspacePaths(sessionId, topicId);
|
|
148
|
-
const title = topicTitle(topic.turns[0].question)
|
|
249
|
+
const title = `${PROMOTED_TITLE_PREFIX}${topicTitle(topic.turns[0].question)}`;
|
|
149
250
|
const created = await materializePromotedTopic({
|
|
150
251
|
parentSessionFile: parent.sessionFile,
|
|
151
252
|
topicSessionFile: paths.sessionFile,
|