ompchamber 3.3.2 → 3.4.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/dist/client/index.html +1 -1
- package/dist/client/index.js +275 -268
- package/dist/client/static/js/{index-e3axkbk1.js → index-h05z90mw.js} +581 -581
- package/package.json +1 -1
- package/src/client/components/settings/categories/AppearanceSettings.tsx +1 -1
- package/src/client/components/settings/categories/ChatSettings.tsx +1 -1
- package/src/client/components/workspace/btw-panel/AskCard.tsx +6 -1
- package/src/client/components/workspace/btw-panel/Messages.tsx +1 -1
- package/src/client/components/workspace/btw-panel/TopicMenu.tsx +1 -1
- package/src/client/components/workspace/btw-panel/index.tsx +20 -27
- package/src/client/components/workspace/chat-timeline/chat-input/Toolbar.tsx +29 -18
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +9 -39
- package/src/client/components/workspace/chat-timeline/chat-input/props.ts +62 -0
- package/src/client/hooks/chat/btw/index.ts +7 -73
- package/src/client/hooks/chat/btw/intercept.test.ts +80 -0
- package/src/client/hooks/chat/btw/intercept.ts +5 -2
- package/src/client/hooks/chat/btw/mode.ts +16 -76
- package/src/client/hooks/chat/timeline/actions.ts +22 -30
- package/src/client/hooks/chat/timeline/queue-actions.ts +88 -0
- package/src/client/hooks/chat/timeline/send.ts +8 -1
- package/src/client/hooks/chat/timeline/tui-only-guard.ts +58 -0
- package/src/server/lib/assets/font-css.server.test.ts +87 -0
- package/src/server/lib/assets/font-css.server.ts +46 -20
- package/src/server/lib/assets/font-faces.ts +69 -0
- package/src/server/lib/assets/fonts.server.ts +27 -4
- package/src/server/lib/btw/child.server.test.ts +68 -0
- package/src/server/lib/btw/child.server.ts +17 -16
- package/src/server/lib/btw/frames.test.ts +3 -3
- package/src/server/lib/btw/frames.ts +1 -1
- package/src/server/lib/btw/lifecycle.server.test.ts +139 -0
- package/src/server/lib/btw/lifecycle.server.ts +41 -31
- package/src/server/lib/btw/prompt.ts +7 -11
- package/src/server/lib/btw/registry.server.ts +1 -9
- package/src/server/lib/btw/reply.ts +54 -0
- package/src/server/lib/btw/runtime.server.test.ts +128 -0
- package/src/server/lib/btw/runtime.server.ts +77 -86
- package/src/server/lib/btw/schema.server.test.ts +84 -0
- package/src/server/lib/btw/schema.server.ts +8 -0
- package/src/server/lib/btw/service.server.ts +47 -78
- package/src/server/lib/btw/store.server.ts +9 -20
- package/src/server/lib/bundler/build-errors.server.ts +8 -3
- package/src/server/lib/bundler/css.ts +12 -61
- package/src/server/lib/omp/rpc/constants.ts +10 -0
- package/src/server/lib/omp/rpc/session-commands.ts +45 -0
- package/src/server/lib/omp/session/merge-stored-notices.test.ts +81 -0
- package/src/server/lib/omp/session/merge-stored.ts +70 -1
- package/src/server/lib/omp/session/messages.ts +27 -2
- package/src/server/plugins/ssr.ts +4 -4
- package/src/server/plugins/static.ts +14 -10
- package/src/server/routes/agent/command.ts +9 -1
- package/src/server/routes/btw/topics.ts +2 -59
- package/src/shared/lib/chat/btw/client.ts +0 -30
- package/src/shared/lib/chat/composer/tui-only.test.ts +90 -0
- package/src/shared/lib/chat/composer/tui-only.ts +138 -0
- package/src/shared/lib/chat/notice-text.ts +33 -0
- package/src/shared/lib/chat/omp/agent-events.ts +18 -37
- package/src/shared/lib/chat/omp/available-commands-cache.test.ts +66 -0
- package/src/shared/lib/chat/omp/command-output-ansi.test.ts +84 -0
- package/src/shared/lib/chat/omp/fold-deps.ts +48 -0
- package/src/shared/lib/chat/timeline/command-output.ts +65 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +4 -17
- package/src/shared/lib/omp/session/messages-map.ts +6 -1
- package/src/shared/types/btw.ts +13 -34
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*
|
|
5
|
+
* `available_commands_update` must drop the composer's cached command pool.
|
|
6
|
+
* The cache lives five minutes, and this frame is the only signal that omp's
|
|
7
|
+
* discovered command set changed (`/reload-plugins`, `/move`, a skill install),
|
|
8
|
+
* so ignoring it leaves the popup offering a list omp no longer matches.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, test } from 'bun:test';
|
|
12
|
+
|
|
13
|
+
import { foldAgentEvent, type OmpAgentFoldDeps } from '@/shared/lib/chat/omp/agent-events';
|
|
14
|
+
import { invalidateComposerCache, loadComposerItems } from '@/shared/lib/chat/composer/client';
|
|
15
|
+
|
|
16
|
+
function foldDeps(): OmpAgentFoldDeps {
|
|
17
|
+
const noop = () => {};
|
|
18
|
+
const ref = <T,>(value: T) => ({ current: value });
|
|
19
|
+
return {
|
|
20
|
+
setState: noop,
|
|
21
|
+
sessionId: 'test-session',
|
|
22
|
+
callbacksRef: { current: {} },
|
|
23
|
+
toolResultsRef: ref(new Map()),
|
|
24
|
+
lastToolMessageRef: ref(null),
|
|
25
|
+
interruptPendingRef: ref(false),
|
|
26
|
+
activityRef: ref(''),
|
|
27
|
+
currentThinkingLevelRef: ref(undefined),
|
|
28
|
+
fileMutatingCallsRef: ref(new Set()),
|
|
29
|
+
} as unknown as OmpAgentFoldDeps;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('available_commands_update', () => {
|
|
33
|
+
test('refetches the command pool on the next composer open', async () => {
|
|
34
|
+
let fetches = 0;
|
|
35
|
+
const originalFetch = globalThis.fetch;
|
|
36
|
+
globalThis.fetch = (async (url: string | URL | Request) => {
|
|
37
|
+
const href = String(url);
|
|
38
|
+
if (href.includes('/api/settings/commands')) {
|
|
39
|
+
fetches += 1;
|
|
40
|
+
return new Response(JSON.stringify({ commands: [] }), { status: 200 });
|
|
41
|
+
}
|
|
42
|
+
if (href.includes('/api/settings/skills')) {
|
|
43
|
+
return new Response(JSON.stringify({ skills: [] }), { status: 200 });
|
|
44
|
+
}
|
|
45
|
+
return new Response('{}', { status: 200 });
|
|
46
|
+
}) as typeof fetch;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
invalidateComposerCache();
|
|
50
|
+
await loadComposerItems('command');
|
|
51
|
+
expect(fetches).toBe(1);
|
|
52
|
+
// Cached: a second open inside the TTL does not refetch.
|
|
53
|
+
await loadComposerItems('command');
|
|
54
|
+
expect(fetches).toBe(1);
|
|
55
|
+
|
|
56
|
+
// omp reports the command set changed.
|
|
57
|
+
foldAgentEvent({ type: 'available_commands_update', commands: [] }, foldDeps());
|
|
58
|
+
|
|
59
|
+
await loadComposerItems('command');
|
|
60
|
+
expect(fetches).toBe(2);
|
|
61
|
+
} finally {
|
|
62
|
+
globalThis.fetch = originalFetch;
|
|
63
|
+
invalidateComposerCache();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*
|
|
5
|
+
* Builtin-command output must be stripped of ANSI on BOTH paths that can render
|
|
6
|
+
* it: the live `command_output` frame and the custom entry a reload replays.
|
|
7
|
+
* The two are separate code paths, and a fix applied to only one makes a
|
|
8
|
+
* command's output change appearance when the page is refreshed — measured on
|
|
9
|
+
* `/context`, whose progress bars carry 48 escape bytes that the notice card
|
|
10
|
+
* drew literally as `38;2;107;114;128m████…`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, test } from 'bun:test';
|
|
14
|
+
|
|
15
|
+
import { foldAgentEvent, type OmpAgentFoldDeps } from '@/shared/lib/chat/omp/agent-events';
|
|
16
|
+
import { noticeFromCustomMessage } from '@/shared/lib/omp/session/messages-map';
|
|
17
|
+
|
|
18
|
+
/** The real shape omp emits for `/context`: SGR runs around half-block bars. */
|
|
19
|
+
const CONTEXT_OUTPUT = 'Context window: 1000000 tokens (1% used)\n'
|
|
20
|
+
+ ' System prompt [\u001b[38;2;107;114;128m░░░░░░\u001b[39m\u001b[1m\u001b[38;2;0;180;255m░░░░\u001b[22m\u001b[39m] 0% 2541 tokens';
|
|
21
|
+
|
|
22
|
+
function foldDeps(): { deps: OmpAgentFoldDeps; outputs: string[] } {
|
|
23
|
+
const outputs: string[] = [];
|
|
24
|
+
const noop = () => {};
|
|
25
|
+
const ref = <T,>(value: T) => ({ current: value });
|
|
26
|
+
const deps = {
|
|
27
|
+
setState: noop,
|
|
28
|
+
sessionId: 'test-session',
|
|
29
|
+
callbacksRef: {
|
|
30
|
+
current: {
|
|
31
|
+
onCommandOutput: (text: string) => outputs.push(text),
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
toolResultsRef: ref(new Map()),
|
|
35
|
+
lastToolMessageRef: ref(null),
|
|
36
|
+
interruptPendingRef: ref(false),
|
|
37
|
+
activityRef: ref(''),
|
|
38
|
+
currentThinkingLevelRef: ref(undefined),
|
|
39
|
+
fileMutatingCallsRef: ref(new Set()),
|
|
40
|
+
} as unknown as OmpAgentFoldDeps;
|
|
41
|
+
return { deps, outputs };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('command_output ANSI stripping (live path)', () => {
|
|
45
|
+
test('strips escapes before the notice row sees them', () => {
|
|
46
|
+
const { deps, outputs } = foldDeps();
|
|
47
|
+
foldAgentEvent({ type: 'command_output', text: CONTEXT_OUTPUT }, deps);
|
|
48
|
+
|
|
49
|
+
expect(outputs).toHaveLength(1);
|
|
50
|
+
expect(outputs[0]).not.toContain('\u001b');
|
|
51
|
+
// The content itself survives — only the escapes go.
|
|
52
|
+
expect(outputs[0]).toContain('Context window: 1000000 tokens');
|
|
53
|
+
expect(outputs[0]).toContain('░░░░░░');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('keeps the empty-frame guard', () => {
|
|
57
|
+
const { deps, outputs } = foldDeps();
|
|
58
|
+
foldAgentEvent({ type: 'command_output', text: ' \u001b[39m ' }, deps);
|
|
59
|
+
expect(outputs).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('noticeFromCustomMessage ANSI stripping (reload path)', () => {
|
|
64
|
+
test('strips escapes from a replayed custom entry', () => {
|
|
65
|
+
const notice = noticeFromCustomMessage({
|
|
66
|
+
type: 'custom_message',
|
|
67
|
+
customType: 'command_output',
|
|
68
|
+
content: CONTEXT_OUTPUT,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(notice).not.toBeNull();
|
|
72
|
+
expect(notice?.notice).not.toContain('\u001b');
|
|
73
|
+
expect(notice?.notice).toContain('Context window: 1000000 tokens');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('still unwraps a system-notice envelope', () => {
|
|
77
|
+
const notice = noticeFromCustomMessage({
|
|
78
|
+
type: 'custom_message',
|
|
79
|
+
customType: 'notice',
|
|
80
|
+
content: '<system-notice>\u001b[32mAll good\u001b[39m</system-notice>',
|
|
81
|
+
});
|
|
82
|
+
expect(notice?.notice).toBe('All good');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The deps record `foldAgentEvent` reads, plus the two small helpers that
|
|
8
|
+
* narrow it. Split out of `agent-events.ts` so that file — which is already the
|
|
9
|
+
* switch over every frame omp can send — stays under the repo's per-file size
|
|
10
|
+
* ceiling.
|
|
11
|
+
*
|
|
12
|
+
* These are pure accessors over caller-owned state: no closure of their own, no
|
|
13
|
+
* module state, so importing them from either side of the fold is free.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Dispatch, RefObject, SetStateAction } from 'preact/compat';
|
|
17
|
+
import type { ChatMessageData, OmpAgentCallbacks, OmpAgentState } from '@/shared/types';
|
|
18
|
+
import type { ToolResultHost, ToolResultRecord } from '@/shared/lib/chat/omp/tool-results';
|
|
19
|
+
|
|
20
|
+
export interface OmpAgentFoldDeps extends ToolResultHost {
|
|
21
|
+
sessionId: string;
|
|
22
|
+
setState: Dispatch<SetStateAction<OmpAgentState>>;
|
|
23
|
+
callbacksRef: RefObject<OmpAgentCallbacks>;
|
|
24
|
+
toolResultsRef: RefObject<Map<string, ToolResultRecord>>;
|
|
25
|
+
lastToolMessageRef: RefObject<ChatMessageData>;
|
|
26
|
+
interruptPendingRef: RefObject<boolean>;
|
|
27
|
+
/** Last activity phrase published to the indicator; guards per-token frames
|
|
28
|
+
* from re-setting identical state. */
|
|
29
|
+
activityRef: RefObject<string>;
|
|
30
|
+
/** Thinking level in effect for the live run (last `thinking_level_changed`
|
|
31
|
+
* frame); stamped onto assistant turns as they stream. */
|
|
32
|
+
currentThinkingLevelRef: RefObject<string | undefined>;
|
|
33
|
+
/** toolCallIds of in-flight file-mutating calls, cleared on `agent_start`. */
|
|
34
|
+
fileMutatingCallsRef: RefObject<Set<string>>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Publish a new indicator phrase, skipping repeats (thinking/text deltas
|
|
38
|
+
* arrive per token and would otherwise re-set state on every frame). */
|
|
39
|
+
export function setActivity(verb: string | undefined, deps: OmpAgentFoldDeps): void {
|
|
40
|
+
if (!verb || verb === deps.activityRef.current) return;
|
|
41
|
+
deps.activityRef.current = verb;
|
|
42
|
+
deps.callbacksRef.current?.onActivity?.(verb);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Narrow a deps record to the tool-output host, adding the re-emit sink. */
|
|
46
|
+
export function toolHost(deps: OmpAgentFoldDeps): ToolResultHost {
|
|
47
|
+
return { ...deps, onMessageUpdate: deps.callbacksRef.current?.onMessageUpdate };
|
|
48
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The `command_output` fold step: a builtin command's result becomes a notice
|
|
8
|
+
* row, and that row is PERSISTED.
|
|
9
|
+
*
|
|
10
|
+
* Persistence is the point. omp answers a builtin on the command path and
|
|
11
|
+
* writes nothing to the session JSONL — the output exists only in this frame —
|
|
12
|
+
* so a timeline rebuilt from the file had forgotten a `/context` the user just
|
|
13
|
+
* ran. The chamber's own copy keeps it, and `mergeOmpAttachments` splices it
|
|
14
|
+
* back under the turn that produced it on the next load.
|
|
15
|
+
*
|
|
16
|
+
* Split out of `omp-callbacks.ts` (which keeps only the wiring) so both files
|
|
17
|
+
* stay under the repo's per-file size ceiling.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Dispatch, SetStateAction } from 'preact/compat';
|
|
21
|
+
import type { ChatMessageData } from '@/shared/types';
|
|
22
|
+
|
|
23
|
+
export interface CommandOutputDeps {
|
|
24
|
+
setLocalMessages: Dispatch<SetStateAction<ChatMessageData[]>>;
|
|
25
|
+
/** The optimistic AI bubble this run is streaming into, if any. */
|
|
26
|
+
aiPlaceholderIdRef: { current: string | null };
|
|
27
|
+
persistMessages: (messages: ChatMessageData[]) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Append a command's output as a notice row, before the active placeholder.
|
|
32
|
+
*
|
|
33
|
+
* Placing it before the placeholder is what keeps it under the turn that
|
|
34
|
+
* produced it: the placeholder becomes the assistant's answer, and the notice
|
|
35
|
+
* must read as the command's result rather than as part of that answer. With no
|
|
36
|
+
* placeholder the row goes at the tail.
|
|
37
|
+
*/
|
|
38
|
+
export function appendCommandOutputNotice(text: string, deps: CommandOutputDeps): void {
|
|
39
|
+
const { setLocalMessages, aiPlaceholderIdRef, persistMessages } = deps;
|
|
40
|
+
const row: ChatMessageData = {
|
|
41
|
+
id: `cmdout-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
42
|
+
role: 'ai',
|
|
43
|
+
content: '',
|
|
44
|
+
notice: text,
|
|
45
|
+
};
|
|
46
|
+
setLocalMessages(prev => {
|
|
47
|
+
const placeholderId = aiPlaceholderIdRef.current;
|
|
48
|
+
const pIdx = placeholderId ? prev.findIndex(m => m.id === placeholderId) : -1;
|
|
49
|
+
if (pIdx !== -1) {
|
|
50
|
+
const next = [...prev.slice(0, pIdx), row, ...prev.slice(pIdx)];
|
|
51
|
+
// The streaming placeholder is deliberately NOT persisted — it is a
|
|
52
|
+
// client-side bubble that `onPromptSettled` removes, and executeSend
|
|
53
|
+
// filters it for the same reason. Writing it here left an empty assistant
|
|
54
|
+
// row in the chamber's copy of a command-only turn.
|
|
55
|
+
persistMessages(next.filter(m => m.id !== placeholderId));
|
|
56
|
+
return next;
|
|
57
|
+
}
|
|
58
|
+
// Already recorded (the same command re-run while nothing streams): keep
|
|
59
|
+
// the list as-is rather than stacking an identical card.
|
|
60
|
+
if (prev.some(m => m.notice === text)) return prev;
|
|
61
|
+
const next = [...prev, row];
|
|
62
|
+
persistMessages(next);
|
|
63
|
+
return next;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
queueStreamingUpdate,
|
|
24
24
|
} from '@/shared/lib/chat/timeline/stream-coalescer';
|
|
25
25
|
import { PHASE_VERBS } from '@/shared/lib/chat/timeline/tool-phrases';
|
|
26
|
+
import { appendCommandOutputNotice } from '@/shared/lib/chat/timeline/command-output';
|
|
26
27
|
|
|
27
28
|
export interface OmpAgentCallbacksDeps {
|
|
28
29
|
setGenerating: (v: boolean) => void;
|
|
@@ -303,24 +304,10 @@ export function createOmpAgentCallbacks(deps: OmpAgentCallbacksDeps): OmpAgentCa
|
|
|
303
304
|
});
|
|
304
305
|
}
|
|
305
306
|
},
|
|
306
|
-
// Built-in slash command output
|
|
307
|
-
//
|
|
307
|
+
// Built-in slash command output: a notice row, persisted — see the module
|
|
308
|
+
// doc for why the chamber's own copy is the only place it can survive.
|
|
308
309
|
onCommandOutput: (text) => {
|
|
309
|
-
|
|
310
|
-
id: `cmdout-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
311
|
-
role: 'ai',
|
|
312
|
-
content: '',
|
|
313
|
-
notice: text,
|
|
314
|
-
};
|
|
315
|
-
setLocalMessages(prev => {
|
|
316
|
-
const placeholderId = aiPlaceholderIdRef.current;
|
|
317
|
-
if (placeholderId) {
|
|
318
|
-
const pIdx = prev.findIndex(m => m.id === placeholderId);
|
|
319
|
-
if (pIdx !== -1) return [...prev.slice(0, pIdx), row, ...prev.slice(pIdx)];
|
|
320
|
-
}
|
|
321
|
-
if (prev.some(m => m.notice === text)) return prev;
|
|
322
|
-
return [...prev, row];
|
|
323
|
-
});
|
|
310
|
+
appendCommandOutputNotice(text, { setLocalMessages, aiPlaceholderIdRef, persistMessages });
|
|
324
311
|
},
|
|
325
312
|
// omp renamed the session — the auto-title generation the chamber asks for
|
|
326
313
|
// after a settled run, a `/rename`, or an RPC set_session_name. The slot
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { ChatMessageData } from '@/shared/types/chat';
|
|
7
7
|
import { extractInlinedTextAttachments, extractText, extractUserImageAttachments, parseAssistantContent, resultOutput, roleFor, stripInlinedTextAttachments, type OmpMessageEntry } from '@/shared/lib/omp/session/messages-parse';
|
|
8
8
|
import { isRecord } from '@/shared/lib/util/guards';
|
|
9
|
+
import { normalizeNoticeText } from '@/shared/lib/chat/notice-text';
|
|
9
10
|
import { reminderPartIndex } from '@/shared/lib/chat/notice-row';
|
|
10
11
|
import { deriveTurnError } from '@/shared/lib/omp/session/turn-error';
|
|
11
12
|
import { toEpochMs } from '@/shared/lib/omp/session/timestamps';
|
|
@@ -180,7 +181,11 @@ export function noticeFromCustomMessage(record: Record<string, unknown>): ChatMe
|
|
|
180
181
|
.map((b) => (isRecord(b) && b.type === 'text' && typeof b.text === 'string' ? b.text : ''))
|
|
181
182
|
.join('')
|
|
182
183
|
: '';
|
|
183
|
-
|
|
184
|
+
// Same terminal-formatted text the live `command_output` frame carries: a
|
|
185
|
+
// custom entry replayed from the JSONL keeps its ANSI runs, and the notice
|
|
186
|
+
// card renders them literally. One normalizer serves both paths so a command's
|
|
187
|
+
// output looks identical whether it was just run or reloaded.
|
|
188
|
+
const notice = normalizeNoticeText(text);
|
|
184
189
|
if (!notice) return null;
|
|
185
190
|
return {
|
|
186
191
|
id,
|
package/src/shared/types/btw.ts
CHANGED
|
@@ -13,18 +13,21 @@
|
|
|
13
13
|
* `btw_message` frames in the chat's own `ChatMessageData` shape, and the
|
|
14
14
|
* running turn is also carried in `BtwState.live` so a client attaching
|
|
15
15
|
* mid-turn renders the answer already produced.
|
|
16
|
+
*
|
|
17
|
+
* A side question runs WITHOUT tools and on the CHAT's model and thinking
|
|
18
|
+
* selector, so there is no per-topic model/access picker and no approval dialog
|
|
19
|
+
* to answer — the panel's composer is an input and a send button, exactly like
|
|
20
|
+
* the TUI's.
|
|
16
21
|
*/
|
|
17
22
|
|
|
18
|
-
import type { ApprovalMode } from '@/shared/lib/omp/config/access-mode';
|
|
19
23
|
import type { ChatMessageData } from '@/shared/types/chat';
|
|
20
|
-
import type { ExtensionUiDialogRequest } from '@/shared/types/omp/agent';
|
|
21
24
|
|
|
22
25
|
/** Lifecycle of one question/answer pair inside a topic. */
|
|
23
|
-
export type BtwTurnStatus = 'running' | 'complete' | 'cancelled' | '
|
|
26
|
+
export type BtwTurnStatus = 'running' | 'complete' | 'cancelled' | 'error' | 'interrupted';
|
|
24
27
|
|
|
25
28
|
/** Model a topic's side conversation runs on — the parent session's model,
|
|
26
|
-
*
|
|
27
|
-
*
|
|
29
|
+
* re-read from the chat's transcript at every ask (omp's `/btw` runs the side
|
|
30
|
+
* turn on `request.session.model`), so the panel offers no model picker. */
|
|
28
31
|
export interface BtwModel {
|
|
29
32
|
provider: string;
|
|
30
33
|
id: string;
|
|
@@ -63,17 +66,11 @@ export interface BtwTopic {
|
|
|
63
66
|
updatedAt: number;
|
|
64
67
|
model?: BtwModel;
|
|
65
68
|
/**
|
|
66
|
-
* Thinking
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
+
* Thinking SELECTOR the side child runs at (`auto` included), re-read from the
|
|
70
|
+
* chat's transcript at every ask so a level changed in the chat is mirrored.
|
|
71
|
+
* Undefined when the chat's transcript records none.
|
|
69
72
|
*/
|
|
70
73
|
thinkingLevel?: string;
|
|
71
|
-
/**
|
|
72
|
-
* Tool-approval mode the side child was SPAWNED with. omp exposes no RPC for
|
|
73
|
-
* this, so a change destroys and respawns an idle child — the same reconcile
|
|
74
|
-
* the chat session does (`reconcileSpawnApprovalMode`).
|
|
75
|
-
*/
|
|
76
|
-
approvalMode?: ApprovalMode;
|
|
77
74
|
/** Set once the topic was promoted into the chat as a session of its own. */
|
|
78
75
|
promotedSessionId?: string;
|
|
79
76
|
turns: BtwTurn[];
|
|
@@ -85,13 +82,12 @@ export interface BtwTopic {
|
|
|
85
82
|
* Part of the state rather than only a stream of frames: a client that attaches
|
|
86
83
|
* mid-turn (a reload, a second tab) would otherwise receive a `btw_state` whose
|
|
87
84
|
* running turn still has no persisted messages and would show an empty answer
|
|
88
|
-
* while the child streams on.
|
|
89
|
-
* comes again, so the state has to carry it.
|
|
85
|
+
* while the child streams on.
|
|
90
86
|
*/
|
|
91
87
|
export interface BtwLiveTurn {
|
|
92
88
|
topicId: string;
|
|
93
89
|
turnIndex: number;
|
|
94
|
-
/** Chat-shaped conversation so far (thinking
|
|
90
|
+
/** Chat-shaped conversation so far (thinking and prose). */
|
|
95
91
|
messages: ChatMessageData[];
|
|
96
92
|
/** Activity phrase for the panel's indicator; '' before the child reports one. */
|
|
97
93
|
activity: string;
|
|
@@ -105,23 +101,6 @@ export interface BtwState {
|
|
|
105
101
|
runningTopicId: string | null;
|
|
106
102
|
/** The running turn's live conversation, or null when nothing is in flight. */
|
|
107
103
|
live: BtwLiveTurn | null;
|
|
108
|
-
/**
|
|
109
|
-
* Dialogs a side child is blocked on right now, oldest first. Part of the
|
|
110
|
-
* state rather than its own frame so a reload (or a fresh panel) replays the
|
|
111
|
-
* modal a child is waiting on — omp never re-delivers the request, so a lost
|
|
112
|
-
* one leaves the turn blocked with nothing to answer.
|
|
113
|
-
*/
|
|
114
|
-
dialogs: BtwDialog[];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* A dialog the side child is blocked on. The panel renders it with the chat's
|
|
119
|
-
* own `ExtensionDialog`; unlike the chat there is no tool card to host an `ask`
|
|
120
|
-
* inline, so every answerable request is a modal.
|
|
121
|
-
*/
|
|
122
|
-
export interface BtwDialog {
|
|
123
|
-
topicId: string;
|
|
124
|
-
request: ExtensionUiDialogRequest;
|
|
125
104
|
}
|
|
126
105
|
|
|
127
106
|
/** Server → client frames on `/api/btw/:sessionId/*` (WS and SSE alike). */
|