ompchamber 1.2.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +143 -147
  2. package/dist/client/index.html +4 -2
  3. package/dist/client/static/css/index.5f630421b7.css +2 -0
  4. package/dist/client/static/js/77.0d81c74cf0.js +139 -0
  5. package/dist/client/static/js/async/1k.973e8e21fc.js +2 -0
  6. package/dist/client/static/js/async/{3y.3186ecdd63.js → 3y.3530b824ad.js} +6 -6
  7. package/dist/client/static/js/async/{q.b453193538.js → 4m.928a1dde85.js} +10 -10
  8. package/dist/client/static/js/async/66.5dfa29f957.js +93 -0
  9. package/dist/client/static/js/async/7g.a7814350c3.js +1 -0
  10. package/dist/client/static/js/async/ar.9303e5f2e8.js +2 -0
  11. package/dist/client/static/js/async/{cx.9c8268d542.js → cx.3cb0242a3a.js} +1 -1
  12. package/dist/client/static/js/async/e.93214d622b.js +79 -0
  13. package/dist/client/static/js/async/g.cb81980498.js +2 -0
  14. package/dist/client/static/js/async/h1.393f066d09.js +1 -0
  15. package/dist/client/static/js/async/jl.874d352278.js +1 -0
  16. package/dist/client/static/js/async/{k.d16cd09ae7.js → k.3210286401.js} +1 -1
  17. package/dist/client/static/js/async/{m.2e1ea3db9e.js → m.eb2e6752e2.js} +3 -3
  18. package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +11 -0
  19. package/dist/client/static/js/async/mt.0352e976d8.js +1 -0
  20. package/dist/client/static/js/async/{oc.eb22093f05.js → oc.b73042e8bd.js} +1 -1
  21. package/dist/client/static/js/async/{og.53f068804c.js → og.8eb577893a.js} +1 -1
  22. package/dist/client/static/js/async/px.fc6877924b.js +1 -0
  23. package/dist/client/static/js/async/q.22588845a9.js +21 -0
  24. package/dist/client/static/js/async/{q5.88b50ffa95.js → q5.97dc1481b6.js} +1 -1
  25. package/dist/client/static/js/async/s.0f395cdda5.js +94 -0
  26. package/dist/client/static/js/async/{tr.2a0d2b7600.js → tr.e878658140.js} +1 -1
  27. package/dist/client/static/js/async/vt.42416b707d.js +1 -0
  28. package/dist/client/static/js/async/x1.da8687159b.js +90 -0
  29. package/dist/client/static/js/async/xe.da8dcc2015.js +2 -0
  30. package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +11 -0
  31. package/dist/client/static/js/async/{xn.6360379f71.js → xn.6510c0abe2.js} +2 -2
  32. package/dist/client/static/js/async/{yx.af2936c368.js → xn1.83a7784fe1.js} +3 -3
  33. package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +11 -0
  34. package/dist/client/static/js/async/yg.35bebd6d0c.js +149 -0
  35. package/dist/client/static/js/async/yv.d99d829136.js +1 -0
  36. package/dist/client/static/js/index.1fd9e1c543.js +38 -0
  37. package/package.json +3 -3
  38. package/src/client/components/common/ComposerTextarea.tsx +9 -1
  39. package/src/client/components/common/MarkdownRenderer.tsx +50 -12
  40. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +9 -3
  41. package/src/client/components/settings/Modal.tsx +11 -2
  42. package/src/client/components/settings/categories/AppearanceSettings.tsx +7 -30
  43. package/src/client/components/settings/categories/ProviderSettings.tsx +2 -0
  44. package/src/client/components/settings/categories/appearance-settings/ThemeCard.tsx +61 -0
  45. package/src/client/components/settings/categories/appearance-settings/ThemeGrid.tsx +80 -0
  46. package/src/client/components/settings/categories/chat-settings/TitleSection.tsx +1 -1
  47. package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +43 -6
  48. package/src/client/components/settings/categories/provider-settings/ModelConfigModal.tsx +54 -54
  49. package/src/client/components/workspace/SearchPanel.tsx +16 -3
  50. package/src/client/components/workspace/btw-panel/AskCard.tsx +11 -6
  51. package/src/client/components/workspace/btw-panel/Messages.tsx +105 -51
  52. package/src/client/components/workspace/btw-panel/index.tsx +124 -45
  53. package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +30 -14
  54. package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +10 -1
  55. package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +8 -2
  56. package/src/client/components/workspace/chat-timeline/MessageItem.tsx +27 -16
  57. package/src/client/components/workspace/chat-timeline/MessageList.tsx +11 -0
  58. package/src/client/components/workspace/chat-timeline/RunFooter.tsx +8 -0
  59. package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +12 -1
  60. package/src/client/components/workspace/chat-timeline/index.tsx +7 -2
  61. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/QuestionBlock.tsx +42 -15
  62. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.tsx +13 -1
  63. package/src/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/index.tsx +13 -4
  64. package/src/client/components/workspace/diff-panel/SplitView.tsx +4 -1
  65. package/src/client/components/workspace/diff-panel/UnifiedView.tsx +70 -65
  66. package/src/client/components/workspace/diff-panel/index.tsx +8 -1
  67. package/src/client/components/workspace/editor/index.tsx +18 -2
  68. package/src/client/components/workspace/file-explorer/GitRepoDropdown.tsx +15 -41
  69. package/src/client/components/workspace/file-explorer/index.tsx +86 -16
  70. package/src/client/components/workspace/git-panel/index.tsx +60 -78
  71. package/src/client/components/workspace/terminal-panel/Header.tsx +16 -1
  72. package/src/client/components/workspace/terminal-panel/RealtimeXtermView.tsx +5 -5
  73. package/src/client/components/workspace/terminal-panel/index.tsx +6 -2
  74. package/src/client/components/workspace/terminal-panel/mount.ts +2 -2
  75. package/src/client/data/theme/terminal.ts +50 -52
  76. package/src/client/hooks/chat/btw/index.ts +139 -18
  77. package/src/client/hooks/chat/btw/intercept.ts +8 -8
  78. package/src/client/hooks/chat/btw/mode.ts +111 -16
  79. package/src/client/hooks/chat/composer/index.ts +10 -4
  80. package/src/client/hooks/chat/timeline/access-mode.ts +41 -0
  81. package/src/client/hooks/chat/timeline/ask-frames.test.ts +87 -0
  82. package/src/client/hooks/chat/timeline/ask-frames.ts +20 -1
  83. package/src/client/hooks/chat/timeline/auto-scroll.ts +13 -4
  84. package/src/client/hooks/chat/timeline/index.ts +8 -20
  85. package/src/client/hooks/chat/timeline/queue.ts +66 -9
  86. package/src/client/hooks/chat/timeline/scroll.test.ts +260 -0
  87. package/src/client/hooks/chat/timeline/scroll.ts +93 -20
  88. package/src/client/hooks/chat/timeline/session-load.ts +7 -1
  89. package/src/client/hooks/chat/timeline/session-pagination.test.ts +167 -0
  90. package/src/client/hooks/chat/timeline/session-pagination.ts +37 -11
  91. package/src/client/hooks/editor/save-file.ts +62 -0
  92. package/src/client/hooks/editor/use-file-editor.test.ts +61 -5
  93. package/src/client/hooks/editor/use-file-editor.ts +38 -29
  94. package/src/client/hooks/editor/use-file-transfer.ts +5 -2
  95. package/src/client/hooks/settings/providers.ts +34 -3
  96. package/src/client/hooks/ui/open-file.ts +7 -1
  97. package/src/client/hooks/ui/theme.ts +82 -57
  98. package/src/client/hooks/workspace/file-tabs.ts +5 -1
  99. package/src/client/hooks/workspace/repo-scope/index.ts +119 -0
  100. package/src/client/hooks/workspace/repo-scope/store.test.ts +161 -0
  101. package/src/client/hooks/workspace/repo-scope/store.ts +201 -0
  102. package/src/client/hooks/workspace/search-stream.ts +24 -5
  103. package/src/client/main.tsx +6 -0
  104. package/src/client/tailwind.css +19 -56
  105. package/src/server/lib/btw/child.server.ts +41 -0
  106. package/src/server/lib/btw/lifecycle.server.ts +254 -0
  107. package/src/server/lib/btw/prompt.ts +13 -5
  108. package/src/server/lib/btw/registry.server.ts +13 -4
  109. package/src/server/lib/btw/runtime.server.ts +149 -156
  110. package/src/server/lib/btw/schema.server.ts +27 -0
  111. package/src/server/lib/btw/service.server.ts +106 -5
  112. package/src/server/lib/btw/store.server.ts +57 -6
  113. package/src/server/lib/db/schema.ts +4 -0
  114. package/src/server/lib/db/settings-store.test.ts +65 -0
  115. package/src/server/lib/db/settings-store.ts +25 -0
  116. package/src/server/lib/fs/atomic-write.ts +42 -4
  117. package/src/server/lib/fs/config-lock.ts +74 -0
  118. package/src/server/lib/fs/git-ignore.ts +55 -5
  119. package/src/server/lib/fs/ripgrep.ts +119 -0
  120. package/src/server/lib/lifecycle/identity.ts +10 -64
  121. package/src/server/lib/lifecycle/proc/darwin.ts +238 -0
  122. package/src/server/lib/lifecycle/proc/index.ts +38 -0
  123. package/src/server/lib/lifecycle/proc/linux.ts +80 -0
  124. package/src/server/lib/lifecycle/proc/ps.ts +86 -0
  125. package/src/server/lib/lifecycle/proc/types.ts +33 -0
  126. package/src/server/lib/lifecycle/proc/win32.ts +53 -0
  127. package/src/server/lib/models/provider-registry.server.ts +2 -2
  128. package/src/server/lib/models/registry.server.ts +1 -1
  129. package/src/server/lib/omp/config/behavior.ts +17 -17
  130. package/src/server/lib/omp/config/disabled-providers.ts +84 -35
  131. package/src/server/lib/omp/config/document.ts +128 -0
  132. package/src/server/lib/omp/config/extensions.ts +11 -10
  133. package/src/server/lib/omp/config/mcp.ts +4 -57
  134. package/src/server/lib/omp/config/model-overrides.ts +123 -0
  135. package/src/server/lib/omp/config/models-config.ts +92 -0
  136. package/src/server/lib/omp/config/models-validation.ts +159 -0
  137. package/src/server/lib/omp/config/path-scoped.ts +74 -0
  138. package/src/server/lib/omp/config/providers.ts +241 -211
  139. package/src/server/lib/omp/config/roles.ts +6 -14
  140. package/src/server/lib/omp/core/auth-credentials.ts +1 -1
  141. package/src/server/lib/omp/rpc/frame-fold.test.ts +124 -0
  142. package/src/server/lib/omp/rpc/frame-fold.ts +43 -24
  143. package/src/server/lib/omp/rpc/lifecycle.test.ts +41 -0
  144. package/src/server/lib/omp/rpc/manager.ts +21 -21
  145. package/src/server/lib/omp/rpc/pending-ui-dialogs.ts +11 -7
  146. package/src/server/lib/omp/rpc/session-commands.ts +17 -29
  147. package/src/server/lib/omp/rpc/session-registry.ts +5 -66
  148. package/src/server/lib/omp/rpc/web-state.ts +0 -6
  149. package/src/server/lib/omp/session/auto-title.server.test.ts +109 -11
  150. package/src/server/lib/omp/session/auto-title.server.ts +95 -20
  151. package/src/server/lib/omp/session/sidebar-data.server.ts +11 -6
  152. package/src/server/lib/queue/delivery-dispatch.server.test.ts +162 -0
  153. package/src/server/lib/queue/delivery-harness.ts +88 -0
  154. package/src/server/lib/queue/delivery.server.test.ts +78 -149
  155. package/src/server/lib/queue/delivery.server.ts +105 -13
  156. package/src/server/lib/queue/store.server.ts +30 -5
  157. package/src/server/lib/terminal/foreground.test.ts +164 -0
  158. package/src/server/lib/terminal/foreground.ts +34 -6
  159. package/src/server/lib/terminal/history.ts +8 -7
  160. package/src/server/plugins/compress.ts +33 -5
  161. package/src/server/plugins/ssr.ts +14 -7
  162. package/src/server/routes/btw/topics.ts +89 -2
  163. package/src/server/routes/fs/action.test.ts +64 -0
  164. package/src/server/routes/fs/action.ts +10 -1
  165. package/src/server/routes/fs/git.ts +5 -6
  166. package/src/server/routes/fs/search.ts +17 -26
  167. package/src/server/routes/sessions/queue.ts +17 -4
  168. package/src/server/routes/settings/index.ts +2 -0
  169. package/src/server/routes/settings/model-override.ts +69 -0
  170. package/src/server/routes/settings/provider-models.ts +35 -12
  171. package/src/server/routes/settings/providers.ts +31 -1
  172. package/src/server/routes/settings/root.ts +8 -3
  173. package/src/shared/lib/chat/btw/client.ts +34 -0
  174. package/src/shared/lib/chat/timeline/tool-phrases.ts +7 -0
  175. package/src/shared/lib/code/line-endings.test.ts +38 -0
  176. package/src/shared/lib/code/line-endings.ts +56 -0
  177. package/src/shared/lib/code/shiki-themes.ts +9 -5
  178. package/src/shared/lib/fs/file-tree.ts +17 -12
  179. package/src/shared/lib/markdown/autolink.test.ts +32 -0
  180. package/src/shared/lib/markdown/autolink.ts +50 -0
  181. package/src/shared/lib/markdown/document-urls.test.ts +68 -0
  182. package/src/shared/lib/markdown/document-urls.ts +118 -0
  183. package/src/shared/lib/markdown/marked.ts +27 -15
  184. package/src/shared/lib/markdown/mermaid.ts +2 -2
  185. package/src/shared/lib/models/catalog.ts +1 -1
  186. package/src/shared/lib/omp/session/schema-migrations.ts +30 -0
  187. package/src/shared/lib/omp/session/stream-state.server.test.ts +54 -0
  188. package/src/shared/lib/omp/session/stream-state.server.ts +70 -21
  189. package/src/shared/lib/settings/client.ts +10 -4
  190. package/src/shared/lib/settings/diff.test.ts +31 -0
  191. package/src/shared/lib/settings/diff.ts +37 -0
  192. package/src/shared/lib/theme/catalog.test.ts +168 -0
  193. package/src/shared/lib/theme/catalog.ts +98 -0
  194. package/src/shared/lib/theme/color.ts +88 -0
  195. package/src/shared/lib/theme/css.ts +70 -0
  196. package/src/shared/lib/theme/palettes/chamber.ts +61 -0
  197. package/src/shared/lib/theme/palettes/dark.ts +313 -0
  198. package/src/shared/lib/theme/palettes/light.ts +313 -0
  199. package/src/shared/types/btw.ts +83 -4
  200. package/src/shared/types/index.ts +1 -0
  201. package/src/shared/types/settings/provider.ts +8 -1
  202. package/src/shared/types/settings/state.ts +10 -4
  203. package/src/shared/types/theme.ts +48 -0
  204. package/dist/client/static/css/index.b938f4a693.css +0 -2
  205. package/dist/client/static/js/77.8022a800b5.js +0 -139
  206. package/dist/client/static/js/async/1k.a411fe5b43.js +0 -2
  207. package/dist/client/static/js/async/66.86544ad741.js +0 -93
  208. package/dist/client/static/js/async/7g.db82aca7ba.js +0 -1
  209. package/dist/client/static/js/async/ar.ad5c8822a8.js +0 -2
  210. package/dist/client/static/js/async/e.6443e739a2.js +0 -79
  211. package/dist/client/static/js/async/g.ee41bfed7f.js +0 -2
  212. package/dist/client/static/js/async/h1.d7091cef78.js +0 -1
  213. package/dist/client/static/js/async/jl.4911fa3383.js +0 -1
  214. package/dist/client/static/js/async/m.2e1ea3db9e.js.LICENSE.txt +0 -6
  215. package/dist/client/static/js/async/mt.853be2b5aa.js +0 -1
  216. package/dist/client/static/js/async/ot.84c71b308f.js +0 -2
  217. package/dist/client/static/js/async/px.783b6f91f2.js +0 -1
  218. package/dist/client/static/js/async/q8.7f1b48e9cd.js +0 -21
  219. package/dist/client/static/js/async/q8.7f1b48e9cd.js.LICENSE.txt +0 -6
  220. package/dist/client/static/js/async/s.aad2ecebc6.js +0 -94
  221. package/dist/client/static/js/async/vt.20406a5604.js +0 -1
  222. package/dist/client/static/js/async/x1.fe8af17016.js +0 -90
  223. package/dist/client/static/js/async/xn.6360379f71.js.LICENSE.txt +0 -6
  224. package/dist/client/static/js/async/yg.5a94e1fc8a.js +0 -149
  225. package/dist/client/static/js/async/yv.2fa22f009a.js +0 -1
  226. package/dist/client/static/js/index.5fe7a2cb64.js +0 -34
  227. package/src/client/components/workspace/btw-panel/Composer.tsx +0 -183
  228. package/src/client/components/workspace/btw-panel/attachments.ts +0 -80
  229. /package/dist/client/static/js/{77.8022a800b5.js.LICENSE.txt → 77.0d81c74cf0.js.LICENSE.txt} +0 -0
  230. /package/dist/client/static/js/async/{g.ee41bfed7f.js.LICENSE.txt → 1k.973e8e21fc.js.LICENSE.txt} +0 -0
  231. /package/dist/client/static/js/async/{3y.3186ecdd63.js.LICENSE.txt → 3y.3530b824ad.js.LICENSE.txt} +0 -0
  232. /package/dist/client/static/js/async/{ot.84c71b308f.js.LICENSE.txt → 4m.928a1dde85.js.LICENSE.txt} +0 -0
  233. /package/dist/client/static/js/async/{q.b453193538.js.LICENSE.txt → ar.9303e5f2e8.js.LICENSE.txt} +0 -0
  234. /package/dist/client/static/js/async/{yx.af2936c368.js.LICENSE.txt → g.cb81980498.js.LICENSE.txt} +0 -0
  235. /package/dist/client/static/js/async/{1k.a411fe5b43.js.LICENSE.txt → q.22588845a9.js.LICENSE.txt} +0 -0
  236. /package/dist/client/static/js/async/{ar.ad5c8822a8.js.LICENSE.txt → xn.6510c0abe2.js.LICENSE.txt} +0 -0
  237. /package/dist/client/static/js/{index.5fe7a2cb64.js.LICENSE.txt → index.1fd9e1c543.js.LICENSE.txt} +0 -0
@@ -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. Byte-for-byte the template omp's own `/btw`
8
- * sends (`packages/coding-agent/src/prompts/system/btw-user.md`): the side
9
- * answer must stay short, must not reach for tools, and must not open a
10
- * follow-up dialogue of its own — follow-ups are explicit user turns.
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
- sessionBtwRuntimes(sessionId)
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