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.
Files changed (63) hide show
  1. package/dist/client/index.html +1 -1
  2. package/dist/client/index.js +275 -268
  3. package/dist/client/static/js/{index-e3axkbk1.js → index-h05z90mw.js} +581 -581
  4. package/package.json +1 -1
  5. package/src/client/components/settings/categories/AppearanceSettings.tsx +1 -1
  6. package/src/client/components/settings/categories/ChatSettings.tsx +1 -1
  7. package/src/client/components/workspace/btw-panel/AskCard.tsx +6 -1
  8. package/src/client/components/workspace/btw-panel/Messages.tsx +1 -1
  9. package/src/client/components/workspace/btw-panel/TopicMenu.tsx +1 -1
  10. package/src/client/components/workspace/btw-panel/index.tsx +20 -27
  11. package/src/client/components/workspace/chat-timeline/chat-input/Toolbar.tsx +29 -18
  12. package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +9 -39
  13. package/src/client/components/workspace/chat-timeline/chat-input/props.ts +62 -0
  14. package/src/client/hooks/chat/btw/index.ts +7 -73
  15. package/src/client/hooks/chat/btw/intercept.test.ts +80 -0
  16. package/src/client/hooks/chat/btw/intercept.ts +5 -2
  17. package/src/client/hooks/chat/btw/mode.ts +16 -76
  18. package/src/client/hooks/chat/timeline/actions.ts +22 -30
  19. package/src/client/hooks/chat/timeline/queue-actions.ts +88 -0
  20. package/src/client/hooks/chat/timeline/send.ts +8 -1
  21. package/src/client/hooks/chat/timeline/tui-only-guard.ts +58 -0
  22. package/src/server/lib/assets/font-css.server.test.ts +87 -0
  23. package/src/server/lib/assets/font-css.server.ts +46 -20
  24. package/src/server/lib/assets/font-faces.ts +69 -0
  25. package/src/server/lib/assets/fonts.server.ts +27 -4
  26. package/src/server/lib/btw/child.server.test.ts +68 -0
  27. package/src/server/lib/btw/child.server.ts +17 -16
  28. package/src/server/lib/btw/frames.test.ts +3 -3
  29. package/src/server/lib/btw/frames.ts +1 -1
  30. package/src/server/lib/btw/lifecycle.server.test.ts +139 -0
  31. package/src/server/lib/btw/lifecycle.server.ts +41 -31
  32. package/src/server/lib/btw/prompt.ts +7 -11
  33. package/src/server/lib/btw/registry.server.ts +1 -9
  34. package/src/server/lib/btw/reply.ts +54 -0
  35. package/src/server/lib/btw/runtime.server.test.ts +128 -0
  36. package/src/server/lib/btw/runtime.server.ts +77 -86
  37. package/src/server/lib/btw/schema.server.test.ts +84 -0
  38. package/src/server/lib/btw/schema.server.ts +8 -0
  39. package/src/server/lib/btw/service.server.ts +47 -78
  40. package/src/server/lib/btw/store.server.ts +9 -20
  41. package/src/server/lib/bundler/build-errors.server.ts +8 -3
  42. package/src/server/lib/bundler/css.ts +12 -61
  43. package/src/server/lib/omp/rpc/constants.ts +10 -0
  44. package/src/server/lib/omp/rpc/session-commands.ts +45 -0
  45. package/src/server/lib/omp/session/merge-stored-notices.test.ts +81 -0
  46. package/src/server/lib/omp/session/merge-stored.ts +70 -1
  47. package/src/server/lib/omp/session/messages.ts +27 -2
  48. package/src/server/plugins/ssr.ts +4 -4
  49. package/src/server/plugins/static.ts +14 -10
  50. package/src/server/routes/agent/command.ts +9 -1
  51. package/src/server/routes/btw/topics.ts +2 -59
  52. package/src/shared/lib/chat/btw/client.ts +0 -30
  53. package/src/shared/lib/chat/composer/tui-only.test.ts +90 -0
  54. package/src/shared/lib/chat/composer/tui-only.ts +138 -0
  55. package/src/shared/lib/chat/notice-text.ts +33 -0
  56. package/src/shared/lib/chat/omp/agent-events.ts +18 -37
  57. package/src/shared/lib/chat/omp/available-commands-cache.test.ts +66 -0
  58. package/src/shared/lib/chat/omp/command-output-ansi.test.ts +84 -0
  59. package/src/shared/lib/chat/omp/fold-deps.ts +48 -0
  60. package/src/shared/lib/chat/timeline/command-output.ts +65 -0
  61. package/src/shared/lib/chat/timeline/omp-callbacks.ts +4 -17
  62. package/src/shared/lib/omp/session/messages-map.ts +6 -1
  63. 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 only rides the live stream — append it as
307
- // a notice row before any active placeholder, exactly like omp notices.
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
- const row: ChatMessageData = {
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
- const notice = text.replace(/<\/?system-notice[^>]*>/g, '').trim();
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,
@@ -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' | 'failed' | 'interrupted';
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
- * captured when the topic was created (omp's `/btw` uses the active model),
27
- * then re-targeted by the panel's own model dropdown. */
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 level the side child runs at, or undefined for omp's own default.
67
- * The panel's thinking dropdown writes it, and a change is applied with
68
- * `set_thinking_level` on the live child.
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. Same reasoning as `dialogs` — the request never
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, tool calls, prose). */
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). */