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
@@ -11,28 +11,41 @@ interface ModelConfigModalProps {
11
11
  onSaveModelConfig: (modelId: string, updates: Partial<ProviderModel>) => void;
12
12
  }
13
13
 
14
+ /** The reasoning levels omp's `thinking.efforts` accepts, in its own order. */
15
+ const EFFORT_LEVELS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'] as const;
16
+ type EffortLevel = (typeof EFFORT_LEVELS)[number];
17
+
18
+ /**
19
+ * Per-model configuration for one entry of `models.yml`.
20
+ *
21
+ * Only the knobs omp resolves per MODEL live here: `maxTokens` and the
22
+ * reasoning `thinking` block. omp has no per-model temperature or top-P — those
23
+ * are global `config.yml` settings, editable in Settings → OMP Engine — so
24
+ * offering sliders for them here would be a control that changes nothing. The
25
+ * dialog points at that panel instead.
26
+ */
14
27
  export function ModelConfigModal({
15
28
  isOpen,
16
29
  model,
17
30
  onClose,
18
31
  onSaveModelConfig,
19
32
  }: ModelConfigModalProps) {
20
- if (!isOpen || !model) return null;
21
-
22
- const [temperature, setTemperature] = useState<number>(model.temperature ?? 0.7);
23
- const [maxTokens, setMaxTokens] = useState<number>(model.maxTokens ?? 384000);
24
- const [topP, setTopP] = useState<number>(model.topP ?? 0.95);
25
- const [reasoningEffort, setReasoningEffort] = useState<'low' | 'medium' | 'high'>(
26
- model.reasoningEffort ?? 'medium'
33
+ const [maxTokens, setMaxTokens] = useState<string>(
34
+ model?.maxTokens ? String(model.maxTokens) : '',
27
35
  );
36
+ const [reasoningEffort, setReasoningEffort] = useState<EffortLevel | ''>(
37
+ (model?.reasoningEffort as EffortLevel | undefined) ?? '',
38
+ );
39
+
40
+ if (!isOpen || !model) return null;
28
41
 
29
42
  const handleSubmit = (e: FormEvent) => {
30
43
  e.preventDefault();
44
+ const parsed = Number.parseInt(maxTokens, 10);
31
45
  onSaveModelConfig(model.id, {
32
- temperature,
33
- maxTokens,
34
- topP,
35
- reasoningEffort,
46
+ // Empty clears the override, so the model returns to the catalog value.
47
+ maxTokens: Number.isFinite(parsed) && parsed > 0 ? parsed : undefined,
48
+ reasoningEffort: reasoningEffort || undefined,
36
49
  });
37
50
  onClose();
38
51
  };
@@ -73,43 +86,6 @@ export function ModelConfigModal({
73
86
  </div>
74
87
  }
75
88
  >
76
- {/* Temperature Slider */}
77
- <div>
78
- <div className="flex items-center justify-between mb-1 text-xs">
79
- <span className="font-semibold text-ink">Sampling Temperature</span>
80
- <span className="font-mono text-ink/70 font-semibold">{temperature.toFixed(2)}</span>
81
- </div>
82
- <input
83
- type="range"
84
- min="0.0"
85
- max="2.0"
86
- step="0.05"
87
- value={temperature}
88
- onChange={(e) => setTemperature(parseFloat(e.currentTarget.value))}
89
- className="w-full accent-ink cursor-pointer"
90
- />
91
- <p className="text-[10px] text-ink/40 mt-0.5">
92
- Controls randomness: lower is more deterministic, higher is more creative.
93
- </p>
94
- </div>
95
-
96
- {/* Top P Slider */}
97
- <div>
98
- <div className="flex items-center justify-between mb-1 text-xs">
99
- <span className="font-semibold text-ink">Top P (Nucleus Sampling)</span>
100
- <span className="font-mono text-ink/70 font-semibold">{topP.toFixed(2)}</span>
101
- </div>
102
- <input
103
- type="range"
104
- min="0.1"
105
- max="1.0"
106
- step="0.05"
107
- value={topP}
108
- onChange={(e) => setTopP(parseFloat(e.currentTarget.value))}
109
- className="w-full accent-ink cursor-pointer"
110
- />
111
- </div>
112
-
113
89
  {/* Max Output Tokens */}
114
90
  <div>
115
91
  <label className="block text-xs font-semibold text-ink mb-1">
@@ -117,23 +93,37 @@ export function ModelConfigModal({
117
93
  </label>
118
94
  <input
119
95
  type="number"
120
- min={1024}
121
- max={1000000}
96
+ min={1}
122
97
  step={1024}
123
98
  value={maxTokens}
124
- onChange={(e) => setMaxTokens(parseInt(e.currentTarget.value, 10) || 4096)}
99
+ placeholder="Catalog default"
100
+ onChange={(e) => setMaxTokens(e.currentTarget.value)}
125
101
  className="w-full bg-paper border border-ink/20 rounded-md px-3 py-1.5 text-xs text-ink outline-none focus:border-ink/60 font-mono"
126
102
  />
103
+ <p className="text-[10px] text-ink/40 mt-0.5">
104
+ Written to models.yml as this model's output cap. Empty uses the catalog value.
105
+ </p>
127
106
  </div>
128
107
 
129
- {/* Reasoning Effort (if supported) */}
108
+ {/* Reasoning Effort (only for models that support reasoning) */}
130
109
  {model.hasReasoning && (
131
110
  <div>
132
111
  <label className="block text-xs font-semibold text-ink mb-1.5">
133
- Reasoning Effort
112
+ Default Reasoning Effort
134
113
  </label>
135
114
  <div className="grid grid-cols-3 gap-2">
136
- {(['low', 'medium', 'high'] as const).map((lvl) => (
115
+ <button
116
+ type="button"
117
+ onClick={() => setReasoningEffort('')}
118
+ className={`py-1.5 rounded-md text-xs font-medium border text-center transition-colors cursor-pointer ${
119
+ reasoningEffort === ''
120
+ ? 'border-ink/50 bg-ink/10 text-ink'
121
+ : 'border-ink/15 text-ink/60 hover:border-ink/30'
122
+ }`}
123
+ >
124
+ Auto
125
+ </button>
126
+ {EFFORT_LEVELS.map((lvl) => (
137
127
  <button
138
128
  key={lvl}
139
129
  type="button"
@@ -148,8 +138,18 @@ export function ModelConfigModal({
148
138
  </button>
149
139
  ))}
150
140
  </div>
141
+ <p className="text-[10px] text-ink/40 mt-0.5">
142
+ The level omp starts this model at. Auto leaves the choice to the chat composer.
143
+ </p>
151
144
  </div>
152
145
  )}
146
+
147
+ <div className="flex items-start gap-2 px-3 py-2 rounded-md bg-ink/5 border border-ink/10 text-[11px] text-ink/60">
148
+ <span>
149
+ Sampling temperature and Top P are global omp settings, not per-model —
150
+ change them in <span className="font-medium text-ink">Settings → OMP Engine</span>.
151
+ </span>
152
+ </div>
153
153
  </Modal>
154
154
  );
155
155
  }
@@ -6,6 +6,7 @@ import { useFetcher } from '@/client/lib/router/fetcher';
6
6
  import { GitRepoDropdown } from '@/client/components/workspace/file-explorer/GitRepoDropdown';
7
7
  import { useScrollbarFade, scrollbarFadeClass } from '@/client/hooks/ui/scrollbar-fade';
8
8
  import { useSessionState } from '@/client/hooks/workspace/session-state';
9
+ import { useRepoList, useRepoScope } from '@/client/hooks/workspace/repo-scope';
9
10
  import { useSearchStream } from '@/client/hooks/workspace/search-stream';
10
11
  import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
11
12
  import { getLanguageFromPath, highlightCode } from '@/shared/lib/code/syntax-highlight';
@@ -23,13 +24,18 @@ export function SearchPanel({ className = '', enabled = true, rootPath }: { clas
23
24
  const [includeFiles, setIncludeFiles] = useSessionState<string>('search.includeFiles', '');
24
25
  const [showMenu, setShowMenu] = useState(false);
25
26
  const [showIncludeField, setShowIncludeField] = useSessionState<boolean>('search.showIncludeField', false);
26
- const [activeRepo, setActiveRepo] = useSessionState<string>('search.activeRepo', '.');
27
+ const { activeRepo, setActiveRepo } = useRepoScope(rootPath, 'search.activeRepo');
28
+ const { repos, scanning: reposScanning, rescan: rescanRepos } = useRepoList(rootPath, enabled);
27
29
 
28
30
  const menuRef = useRef<HTMLDivElement>(null);
29
31
  const { isScrolling, handleScroll } = useScrollbarFade();
30
32
 
31
33
  const replaceFetcher = useFetcher<{ success: boolean, results: any[] }>();
32
- const { results, isSearching, start: startSearch } = useSearchStream();
34
+
35
+ // The tree being searched: the workspace root plus the selected repo. Every
36
+ // result is relative to it, so a switch must not keep the previous tree's
37
+ // hits on screen — `useSearchStream` tags them with this scope.
38
+ const { results, isSearching, start: startSearch } = useSearchStream(`${rootPath ?? ''}\u0000${activeRepo}`);
33
39
 
34
40
  useOnClickOutside(menuRef, () => setShowMenu(false));
35
41
 
@@ -109,7 +115,14 @@ export function SearchPanel({ className = '', enabled = true, rootPath }: { clas
109
115
  <div className="p-3 border-b border-ink/10 flex items-center justify-between">
110
116
  <div className="flex items-center space-x-2 min-w-0">
111
117
  <h2 className="text-xs font-semibold text-ink uppercase tracking-wider">Search</h2>
112
- <GitRepoDropdown rootPath={rootPath} activeRepo={activeRepo} onSelectRepo={setActiveRepo} />
118
+ <GitRepoDropdown
119
+ rootPath={rootPath}
120
+ activeRepo={activeRepo}
121
+ onSelectRepo={setActiveRepo}
122
+ repos={repos}
123
+ scanning={reposScanning}
124
+ onRefreshRepos={rescanRepos}
125
+ />
113
126
  </div>
114
127
  <div className="relative" ref={menuRef}>
115
128
  <button
@@ -4,12 +4,17 @@
4
4
  */
5
5
 
6
6
  /**
7
- * The ask card — the top card of the side-question form.
7
+ * The ask card — the HEADER of the side-question panel.
8
8
  *
9
- * It is the form's own entry, not the chat's: the question typed here is always
10
- * a question of its own (a new topic), which is why it carries its own `⟳`
11
- * (start over) and `✕` (leave the form). The topic history and promotion ride
12
- * along here because the form replaced the composer that used to hold them.
9
+ * It is the panel's own entry, not the chat's: the question typed here is
10
+ * always a question of its own (a new topic), which is why it carries the
11
+ * panel's actions — `⟳` start over, `⌄` topic history, `⧉` promote, `✕` leave.
12
+ *
13
+ * It is a header rather than another card in the stack on purpose. The panel
14
+ * also holds a composer, and a second free-standing input beside it read as
15
+ * "the same thing again": users typed a follow-up into the new-question field.
16
+ * As a header it belongs to the answers card it sits on, and the composer below
17
+ * is the only input that continues the topic on screen.
13
18
  */
14
19
 
15
20
  import { useEffect, useRef, useState } from 'preact/hooks';
@@ -77,7 +82,7 @@ export function BtwAskCard({
77
82
  };
78
83
 
79
84
  return (
80
- <div className="bg-paper border border-ink/20 rounded-2xl shadow-sm flex items-center gap-1 pl-1.5 pr-2 py-1 focus-within:border-ink transition-colors">
85
+ <div className="flex items-center gap-1 pl-1.5 pr-2 py-1.5 border-b border-ink/10 bg-canvas/40">
81
86
  <button
82
87
  type="button"
83
88
  onClick={() => {
@@ -1,34 +1,48 @@
1
1
  /**
2
- * BTW panel message body — the scrollable turn list of the active topic.
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * BTW panel message body — the scrollable conversation of the active topic.
3
8
  *
4
- * Pure presentation: the parent streams `liveAnswer` in and this file only
5
- * decides which text is current. No fetch, no session state.
9
+ * It renders through the CHAT's own `MessageList`, not a btw-specific renderer:
10
+ * a turn's `messages` are `ChatMessageData` (thinking, tool calls, usage,
11
+ * errors), so a tool the side child runs draws the same card it would in the
12
+ * chat, and the two can never drift. This file only assembles the row list —
13
+ * each turn's question as a user row, then that turn's conversation — and adds
14
+ * the btw-only status line for a turn that did not complete cleanly.
15
+ *
16
+ * Pure presentation: no fetch, no session state.
6
17
  */
7
18
 
8
19
  import { useEffect, useMemo, useRef } from 'preact/hooks';
9
- import type { BtwTopic, BtwTurnStatus } from '@/shared/types/btw';
10
- import { MarkdownRenderer } from '@/client/components/common/MarkdownRenderer';
20
+ import type { BtwTopic, BtwTurn, BtwTurnStatus, ChatMessageData } from '@/shared/types';
21
+ import { MessageList } from '@/client/components/workspace/chat-timeline/MessageList';
11
22
 
12
23
  export interface BtwMessagesProps {
13
24
  topic: BtwTopic | null;
14
- /** Partial text of the running turn ('' when none). */
15
- liveAnswer: string;
25
+ /** The running turn's conversation, chat-shaped (empty when none). */
26
+ liveMessages: ChatMessageData[];
16
27
  /** Panel-level error line, surfaced above the turns. */
17
28
  error: string | null;
29
+ provider?: string;
30
+ providerNames?: Record<string, string>;
31
+ modelName?: string;
32
+ /** Thinking level the side child runs at, for the run footer. */
33
+ thinkingLevel?: string;
34
+ isMobile?: boolean;
18
35
  }
19
36
 
20
- /** Status line under an answer that is not `complete`. */
37
+ /**
38
+ * Status line for a turn that did not complete cleanly.
39
+ *
40
+ * `running` has no line here: the panel reports a turn in flight with the
41
+ * chat's own `GeneratingIndicator` at the bottom of the card, and a second
42
+ * "Thinking…" under the answer said the same thing twice.
43
+ */
21
44
  function AnswerStatus({ status }: { status: BtwTurnStatus }) {
22
- if (status === 'complete') return null;
23
-
24
- if (status === 'running') {
25
- return (
26
- <div className="flex items-center gap-1.5 text-[11px] text-ink/50">
27
- <span className="w-1.5 h-1.5 rounded-full bg-ink/40 animate-pulse" />
28
- <span>Thinking…</span>
29
- </div>
30
- );
31
- }
45
+ if (status === 'complete' || status === 'running') return null;
32
46
 
33
47
  if (status === 'cancelled') {
34
48
  return <div className="text-[11px] text-ink/50">Cancelled</div>;
@@ -37,15 +51,63 @@ function AnswerStatus({ status }: { status: BtwTurnStatus }) {
37
51
  return <div className="text-[11px] text-error">{status === 'failed' ? 'Failed' : 'Interrupted'}</div>;
38
52
  }
39
53
 
40
- export function BtwMessages({ topic, liveAnswer, error }: BtwMessagesProps) {
54
+ /** The question row for a turn, in the chat's own user-bubble shape. */
55
+ function questionRow(topic: BtwTopic, turn: BtwTurn): ChatMessageData {
56
+ return {
57
+ id: `btw-${topic.id}-${turn.index}-user`,
58
+ role: 'user',
59
+ content: turn.question,
60
+ timestamp: new Date(turn.createdAt).toISOString(),
61
+ };
62
+ }
63
+
64
+ /**
65
+ * A turn's rows: its stored conversation, or — for a turn written before the
66
+ * conversation was stored — its plain `answer` as a single assistant row.
67
+ * Without this fallback every pre-existing turn would render its question and
68
+ * no answer at all.
69
+ *
70
+ * The child ECHOES the prompt it received, and that echo is the `<btw>` wrapper
71
+ * this panel itself built — the caller already renders the user's own question
72
+ * as a row, so the echo is dropped rather than shown as a second, uglier copy
73
+ * of the same turn.
74
+ */
75
+ function turnRows(topic: BtwTopic, turn: BtwTurn): ChatMessageData[] {
76
+ const conversation = turn.messages.filter((message) => message.role !== 'user');
77
+ if (conversation.length > 0) return conversation;
78
+ if (!turn.answer) return [];
79
+ return [
80
+ {
81
+ id: `btw-${topic.id}-${turn.index}-answer`,
82
+ role: 'ai',
83
+ content: turn.answer,
84
+ timestamp: new Date(turn.updatedAt).toISOString(),
85
+ },
86
+ ];
87
+ }
88
+
89
+ export function BtwMessages({ topic, liveMessages, error, provider, providerNames, modelName, thinkingLevel, isMobile = false }: BtwMessagesProps) {
41
90
  const scrollRef = useRef<HTMLDivElement>(null);
42
91
  const turns = topic?.turns ?? [];
43
92
 
93
+ // One row list for the whole topic: each question, then its conversation. The
94
+ // running turn's live rows replace its (still empty) stored ones.
95
+ const rows = useMemo<ChatMessageData[]>(() => {
96
+ if (!topic) return [];
97
+ return turns.flatMap((turn) => {
98
+ const running = turn.status === 'running' && liveMessages.length > 0;
99
+ // The live buffer carries the same echoed prompt the stored conversation
100
+ // does, so it is filtered the same way (see `turnRows`).
101
+ const conversation = running ? liveMessages.filter((message) => message.role !== 'user') : turnRows(topic, turn);
102
+ return [questionRow(topic, turn), ...conversation];
103
+ });
104
+ }, [topic, turns, liveMessages]);
105
+
44
106
  // Cheap scroll signal: any visible text growth (stream delta, new question,
45
107
  // topic switch) changes this number.
46
108
  const renderedLength = useMemo(
47
- () => turns.reduce((total, turn) => total + turn.question.length + turn.answer.length, 0) + liveAnswer.length,
48
- [turns, liveAnswer],
109
+ () => rows.reduce((total, row) => total + row.content.length + (row.toolCalls?.length ?? 0) * 64, 0),
110
+ [rows],
49
111
  );
50
112
 
51
113
  useEffect(() => {
@@ -53,47 +115,39 @@ export function BtwMessages({ topic, liveAnswer, error }: BtwMessagesProps) {
53
115
  if (body) body.scrollTop = body.scrollHeight;
54
116
  }, [renderedLength]);
55
117
 
56
- const lastIndex = turns.length - 1;
57
-
58
118
  return (
59
119
  <div
60
120
  ref={scrollRef}
61
- className="flex-1 min-h-0 overflow-y-auto scrollbar-overlay-container scrollbar-overlay-static px-4 py-3 space-y-4"
121
+ className="flex-1 min-h-0 overflow-y-auto scrollbar-overlay-container scrollbar-overlay-static px-4 py-3"
62
122
  >
63
123
  {error && (
64
124
  <div className="bg-error/10 border border-error/30 text-error rounded-lg px-3 py-2 text-[12px]">{error}</div>
65
125
  )}
66
126
 
67
127
  {topic === null ? (
68
- <div className="flex h-full items-center justify-center px-4 text-center text-[12px] text-ink/50">
69
- Start a side question — the answer stays out of this chat's transcript.
128
+ <div className="flex min-h-[72px] items-center justify-center px-4 text-center text-[12px] text-ink/50">
129
+ Ask a question above — the answer stays out of this chat's transcript.
70
130
  </div>
71
131
  ) : (
72
- turns.map((turn, index) => {
73
- // The streamed text wins while the newest turn is live: either it is
74
- // still running, or it already outgrew the last flushed answer.
75
- const streaming =
76
- index === lastIndex &&
77
- liveAnswer !== '' &&
78
- (turn.status === 'running' || liveAnswer.length > turn.answer.length);
79
- const answer = streaming ? liveAnswer : turn.answer;
80
-
81
- return (
82
- <div key={turn.index} className="space-y-2">
83
- <div className="ml-auto max-w-[85%] w-fit bg-ink/[0.06] border border-ink/10 rounded-xl px-3.5 py-2.5 text-[13px] text-ink break-words">
84
- <MarkdownRenderer content={turn.question} />
85
- </div>
86
-
87
- {answer !== '' && (
88
- <div className="px-1 text-[13px] text-ink leading-relaxed">
89
- <MarkdownRenderer content={answer} />
90
- </div>
91
- )}
92
-
93
- <AnswerStatus status={turn.status} />
94
- </div>
95
- );
96
- })
132
+ <>
133
+ <MessageList
134
+ messages={rows}
135
+ isGenerating={topic.turns.some((turn) => turn.status === 'running')}
136
+ provider={provider}
137
+ providerNames={providerNames}
138
+ modelName={modelName}
139
+ thinkingLevel={thinkingLevel}
140
+ isMobile={isMobile}
141
+ userActions={false}
142
+ />
143
+ {/* A turn that stopped without completing is the only thing the chat's
144
+ own rows cannot express, so it is the only btw-specific line. */}
145
+ {turns
146
+ .filter((turn) => turn.status !== 'complete' && turn.status !== 'running')
147
+ .map((turn) => (
148
+ <AnswerStatus key={turn.index} status={turn.status} />
149
+ ))}
150
+ </>
97
151
  )}
98
152
  </div>
99
153
  );
@@ -4,33 +4,67 @@
4
4
  */
5
5
 
6
6
  /**
7
- * The side-question form — what the chat composer becomes while BTW mode is on.
7
+ * The side-question panel — what the chat composer becomes while BTW mode is on.
8
8
  *
9
- * Stacked cards, top to bottom: the topic's turns (only once there is something
10
- * to show, or an error to explain), the ask card (a question of its own), and
11
- * the side session's composer (follow-ups, attachments, model chip). Nothing it
12
- * produces enters the chat transcript — that is the point of a side question —
13
- * and promotion is the one explicit action that moves an answer into the chat,
14
- * as a session branched from it.
9
+ * TWO cards, and the split is what keeps it unambiguous:
10
+ *
11
+ * 1. the panel card — the ask card as its HEADER (the field that starts a new
12
+ * topic, plus `⟳` start over / `⌄` topic history / `⧉` promote / `✕` leave),
13
+ * the topic's turns as its body, and the running turn's indicator pinned at
14
+ * the BOTTOM, where it reports the answer being streamed above it;
15
+ * 2. the composer — the chat's own `ChatInput`, for follow-ups to the topic on
16
+ * screen.
17
+ *
18
+ * The ask field used to be a free-standing card directly above that composer,
19
+ * and users typed a follow-up into it. As a header it belongs to the answers
20
+ * card it sits on, and only one input continues the topic in view.
21
+ *
22
+ * Nothing here enters the chat transcript — that is the point of a side
23
+ * question — and promotion is the one explicit action that moves an answer into
24
+ * the chat, as a session branched from it.
25
+ *
26
+ * The composer card is the CHAT's own `ChatInput`, not a look-alike: a side
27
+ * question runs a child with tools and an access mode, so it offers the same
28
+ * model / thinking / access controls, the same attachments, drop targets and
29
+ * keybindings. Only the autocomplete is off (`enablePicker`) — a side question
30
+ * has no file tree, commands or skills to complete against.
15
31
  */
16
32
 
17
33
  import { useState } from 'preact/hooks';
34
+ import type { Attachment } from '@/shared/types';
18
35
  import type { BtwMode } from '@/client/hooks/chat/btw/mode';
19
36
  import { BtwAskCard } from '@/client/components/workspace/btw-panel/AskCard';
20
37
  import { BtwMessages } from '@/client/components/workspace/btw-panel/Messages';
21
- import { BtwComposer } from '@/client/components/workspace/btw-panel/Composer';
22
- import { btwImagesFrom, readBtwAttachments, type BtwAttachment } from '@/client/components/workspace/btw-panel/attachments';
38
+ import { ChatInput } from '@/client/components/workspace/chat-timeline/chat-input/index';
39
+ import { GeneratingIndicator } from '@/client/components/workspace/chat-timeline/GeneratingIndicator';
40
+ import { ExtensionDialog } from '@/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/Lazy';
41
+
42
+ /** Tallest the answer card grows before its own scrollbar takes over. */
43
+ const ANSWERS_MAX_HEIGHT_PX = 520;
23
44
 
24
45
  export interface BtwFormProps {
25
46
  mode: BtwMode;
26
- /** The chat's own model, shown until the topic reports the one it ran on. */
47
+ appSettings: Record<string, unknown>;
48
+ composerModelRef: { current: { provider: string; modelId: string; thinkingLevel: string } | null };
49
+ rootPath: string | null;
50
+ variant: 'desktop' | 'mobile';
51
+ /** Chat model/provider, shown until the topic reports the one it ran on. */
27
52
  modelName?: string;
28
53
  provider?: string;
29
54
  providerNames?: Record<string, string>;
30
55
  }
31
56
 
32
- export function BtwForm({ mode, modelName, provider, providerNames }: BtwFormProps) {
33
- const [attachments, setAttachments] = useState<BtwAttachment[]>([]);
57
+ export function BtwForm({
58
+ mode,
59
+ appSettings,
60
+ composerModelRef,
61
+ rootPath,
62
+ variant,
63
+ modelName,
64
+ provider,
65
+ providerNames,
66
+ }: BtwFormProps) {
67
+ const [attachments, setAttachments] = useState<Attachment[]>([]);
34
68
  const lastTurn = mode.activeTopic?.turns[mode.activeTopic.turns.length - 1];
35
69
  // `busy` is the command in flight (a promote POST, say): promoting again from
36
70
  // the same click burst would be a second request for work already running.
@@ -38,51 +72,96 @@ export function BtwForm({ mode, modelName, provider, providerNames }: BtwFormPro
38
72
  // session exists, and the server hands that id back on a repeat promote.
39
73
  const canPromote =
40
74
  Boolean(mode.activeTopic) && !mode.runningTopicId && !mode.busy && !mode.activeTopic?.promotedSessionId && lastTurn?.status === 'complete';
41
- const showTurns = (mode.activeTopic?.turns.length ?? 0) > 0 || Boolean(mode.error);
75
+ const dialog = mode.dialogs[0];
76
+ const running = Boolean(mode.runningTopicId);
42
77
 
43
78
  return (
44
79
  <div className="flex flex-col gap-2">
45
- {showTurns && (
46
- <div className="bg-paper border border-ink/15 rounded-2xl shadow-xl flex flex-col max-h-[45vh] overflow-hidden">
47
- <BtwMessages topic={mode.activeTopic} liveAnswer={mode.liveAnswer} error={mode.error} />
48
- </div>
49
- )}
80
+ {/* ONE card, so the header's field and the body's answers read as one
81
+ panel — and the composer below it as the other. The ask card is that
82
+ header: a second free-standing input beside the composer is what made
83
+ users type a follow-up into the new-question field. */}
84
+ <div
85
+ className="bg-paper border border-ink/15 rounded-2xl shadow-xl flex flex-col overflow-hidden"
86
+ style={{ maxHeight: ANSWERS_MAX_HEIGHT_PX }}
87
+ >
88
+ <BtwAskCard
89
+ value={mode.askDraft}
90
+ onChange={mode.setAskDraft}
91
+ onSubmit={mode.submitAsk}
92
+ onNewQuestion={mode.resetQuestion}
93
+ onClose={mode.exit}
94
+ running={running}
95
+ topics={mode.topics}
96
+ activeTopicId={mode.activeTopic?.id ?? null}
97
+ onSelectTopic={mode.selectTopic}
98
+ onDeleteTopic={mode.removeTopic}
99
+ canPromote={canPromote}
100
+ onPromote={() => void mode.promote()}
101
+ />
50
102
 
51
- <BtwAskCard
52
- value={mode.askDraft}
53
- onChange={mode.setAskDraft}
54
- onSubmit={mode.submitAsk}
55
- onNewQuestion={mode.resetQuestion}
56
- onClose={mode.exit}
57
- running={Boolean(mode.runningTopicId)}
58
- topics={mode.topics}
59
- activeTopicId={mode.activeTopic?.id ?? null}
60
- onSelectTopic={mode.selectTopic}
61
- onDeleteTopic={mode.removeTopic}
62
- canPromote={canPromote}
63
- onPromote={() => void mode.promote()}
64
- />
103
+ <BtwMessages
104
+ topic={mode.activeTopic}
105
+ liveMessages={mode.liveMessages}
106
+ error={mode.error}
107
+ provider={mode.activeTopic?.model?.provider ?? provider}
108
+ providerNames={providerNames}
109
+ modelName={mode.activeTopic?.model?.name ?? modelName}
110
+ thinkingLevel={mode.thinkingLevel}
111
+ isMobile={variant === 'mobile'}
112
+ />
113
+
114
+ {/* The turn in flight reports itself HERE, at the bottom of the card and
115
+ below the answer it is streaming — not above it, and not as the
116
+ "Thinking…" line the transcript used to draw under the answer. */}
117
+ {running && (
118
+ <GeneratingIndicator
119
+ id="btw-generating-indicator"
120
+ generatingVerb={mode.liveVerb}
121
+ modelName={mode.activeTopic?.model?.name ?? modelName}
122
+ provider={mode.activeTopic?.model?.provider ?? provider}
123
+ providerNames={providerNames}
124
+ />
125
+ )}
126
+ </div>
65
127
 
66
- <BtwComposer
128
+ <ChatInput
67
129
  value={mode.followUpDraft}
68
130
  onChange={mode.setFollowUpDraft}
69
- onSubmit={() => {
70
- // Hand the images over and clear them: leaving them staged would
71
- // re-send the same picture with every later follow-up.
72
- mode.submitFollowUp(mode.followUpDraft, btwImagesFrom(attachments));
131
+ onSend={(outgoing) => {
132
+ // Hand the attachments over and clear them: leaving them staged would
133
+ // re-send the same file with every later follow-up.
134
+ mode.submitFollowUp(mode.followUpDraft, outgoing);
73
135
  setAttachments([]);
74
136
  }}
137
+ isGenerating={running}
75
138
  onStop={mode.abort}
76
- running={Boolean(mode.runningTopicId)}
77
- modelLabel={mode.activeTopic?.model?.name ?? modelName}
78
- provider={mode.activeTopic?.model?.provider ?? provider}
79
- providerNames={providerNames}
139
+ appSettings={appSettings}
80
140
  attachments={attachments}
81
- onFilesSelected={(files) => {
82
- void readBtwAttachments(files).then((next) => setAttachments((prev) => [...prev, ...next]));
83
- }}
84
- onRemoveAttachment={(id) => setAttachments((prev) => prev.filter((attachment) => attachment.id !== id))}
141
+ onAttachmentsChange={setAttachments}
142
+ rootPath={rootPath}
143
+ variant={variant}
144
+ enablePicker={false}
145
+ placeholder="Ask in this btw session..."
146
+ accessMode={mode.accessMode}
147
+ onAccessModeChange={mode.setAccessMode}
148
+ onThinkingLevelChange={mode.setThinkingLevel}
149
+ onModelChange={mode.setModel}
150
+ sessionModel={mode.activeTopic?.model ? { provider: mode.activeTopic.model.provider, modelId: mode.activeTopic.model.id } : null}
151
+ sessionThinkingLevel={mode.thinkingLevel}
152
+ composerModelRef={composerModelRef}
85
153
  />
154
+
155
+ {/* A side child runs with tools, so a gated call parks its turn on an
156
+ approval dialog. There is no tool card here to host one inline (unlike
157
+ the chat's `ask`), so every answerable request is a modal — and the
158
+ dialog brings its own backdrop. */}
159
+ {dialog && (
160
+ <ExtensionDialog
161
+ request={dialog.request}
162
+ onRespond={(_, response) => mode.respondToDialog(dialog.request.id, response)}
163
+ />
164
+ )}
86
165
  </div>
87
166
  );
88
167
  }