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,201 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Nested-repo discovery store, keyed by workspace root.
8
+ *
9
+ * The leak this module exists to prevent: a repo is a path RELATIVE to a root
10
+ * (`projects/jns6_5/token` only names a directory under `~/JatisMobile/Workspace`),
11
+ * so a list discovered for one workspace is meaningless under another. Keying
12
+ * every entry by the root it was discovered for makes that structural — a new
13
+ * root has no entry and starts from {@link UNKNOWN_LIST} instead of inheriting
14
+ * the previous list.
15
+ *
16
+ * A store rather than a fetch per hook: the right-panel views ask the same
17
+ * question about the same root, and independent copies meant one request per
18
+ * panel per poll interval plus lists that could disagree.
19
+ *
20
+ * Both platform dependencies are injected — `fetch` and the retry scheduler —
21
+ * so the invalidation rules are testable without a DOM and without wall-clock
22
+ * timers.
23
+ */
24
+
25
+ import { REPO_DISCOVERY_POLL_MS } from '@/shared/lib/workspace/refresh-cadence';
26
+ import { isRecord } from '@/shared/lib/util/guards';
27
+
28
+ export interface RepoListSnapshot {
29
+ repos: string[];
30
+ /** Discovery is still running server-side, or a forced rescan is. */
31
+ scanning: boolean;
32
+ }
33
+
34
+ /** The list before discovery has answered: the workspace root alone. */
35
+ export const UNKNOWN_LIST: RepoListSnapshot = { repos: ['.'], scanning: false };
36
+
37
+ /** `fetch` seam: resolves the parsed `?reposOnly=1` payload, rejects on error. */
38
+ export type RepoFetch = (url: string) => Promise<unknown>;
39
+
40
+ /**
41
+ * Retry seam. Runs `run` once after `delayMs` and returns a cancel; the default
42
+ * is a one-shot timer, and the caller re-arms it per attempt.
43
+ */
44
+ export type RepoScheduler = (run: () => void, delayMs: number) => () => void;
45
+
46
+ interface RootEntry {
47
+ snapshot: RepoListSnapshot;
48
+ /** True once a discovery response was accepted for this root. Drives the
49
+ * "start discovery on first subscriber" decision — object identity of
50
+ * `snapshot` cannot, because clearing the spinner mints a new one. */
51
+ loaded: boolean;
52
+ listeners: Set<() => void>;
53
+ controller: AbortController | null;
54
+ cancelRetry: (() => void) | null;
55
+ /** The discovery currently in flight, so a caller can await its settlement. */
56
+ pending: Promise<void> | null;
57
+ }
58
+
59
+ export interface RepoStore {
60
+ /** Repos discovered for `root`; {@link UNKNOWN_LIST} until one is. */
61
+ snapshot: (root: string) => RepoListSnapshot;
62
+ /** Await the discovery in flight for `root` (a no-op when there is none). */
63
+ settled: (root: string) => Promise<void>;
64
+ /** Observe `root`; the first subscriber starts discovery. Returns a cancel. */
65
+ subscribe: (root: string, listener: () => void) => () => void;
66
+ /** Re-run discovery for `root` from scratch, ignoring any cached result. */
67
+ rescan: (root: string) => void;
68
+ }
69
+
70
+ export interface RepoStoreOptions {
71
+ fetch: RepoFetch;
72
+ scheduler?: RepoScheduler;
73
+ }
74
+
75
+ const defaultScheduler: RepoScheduler = (run, delayMs) => {
76
+ const id = setTimeout(run, delayMs);
77
+ return () => clearTimeout(id);
78
+ };
79
+
80
+ export function createRepoStore({ fetch: fetchImpl, scheduler = defaultScheduler }: RepoStoreOptions): RepoStore {
81
+ // Per-store, not module-level: two stores must not share state, and a test
82
+ // store must not see the app store's roots.
83
+ const entries = new Map<string, RootEntry>();
84
+
85
+ function entryFor(root: string): RootEntry {
86
+ let entry = entries.get(root);
87
+ if (!entry) {
88
+ entry = {
89
+ snapshot: UNKNOWN_LIST,
90
+ loaded: false,
91
+ listeners: new Set(),
92
+ controller: null,
93
+ cancelRetry: null,
94
+ pending: null,
95
+ };
96
+ entries.set(root, entry);
97
+ }
98
+ return entry;
99
+ }
100
+
101
+ function publish(root: string, patch: Partial<RepoListSnapshot>): void {
102
+ const entry = entries.get(root);
103
+ if (!entry) return;
104
+ const next = { ...entry.snapshot, ...patch };
105
+ if (next.repos === entry.snapshot.repos && next.scanning === entry.snapshot.scanning) return;
106
+ entry.snapshot = next;
107
+ for (const listener of entry.listeners) listener();
108
+ }
109
+
110
+ function cancelRetry(entry: RootEntry): void {
111
+ entry.cancelRetry?.();
112
+ entry.cancelRetry = null;
113
+ }
114
+
115
+ function load(root: string, rescan: boolean): Promise<void> {
116
+ const entry = entryFor(root);
117
+ // A newer load supersedes this one: its response must not overwrite what
118
+ // the newer request is already about to replace.
119
+ entry.controller?.abort();
120
+ const controller = new AbortController();
121
+ entry.controller = controller;
122
+ publish(root, { scanning: true });
123
+
124
+ const params = new URLSearchParams({ reposOnly: '1' });
125
+ if (root) params.set('root', root);
126
+ if (rescan) params.set('rescan', '1');
127
+ params.set('t', String(Date.now()));
128
+
129
+ const pending = (async () => {
130
+ const data = await fetchImpl(`/api/fs/git?${params.toString()}`).catch(() => null);
131
+ if (entry.controller !== controller) return;
132
+ entry.controller = null;
133
+
134
+ const repos = isRecord(data) && Array.isArray(data.repos) ? data.repos : null;
135
+ if (!repos) {
136
+ // A failed read leaves the snapshot as it was — for a root nobody has
137
+ // successfully listed yet that is still `UNKNOWN_LIST`, so the next
138
+ // subscriber retries instead of sitting on an empty list.
139
+ publish(root, { scanning: false });
140
+ return;
141
+ }
142
+ const scanning = isRecord(data) && data.reposPending === true;
143
+ entry.loaded = true;
144
+ publish(root, { repos: repos.filter((r): r is string => typeof r === 'string'), scanning });
145
+
146
+ // Discovery runs in the background server-side — the loader returns at
147
+ // once with `reposPending` — so retry until it settles.
148
+ if (scanning) {
149
+ cancelRetry(entry);
150
+ entry.cancelRetry = scheduler(() => {
151
+ entry.cancelRetry = null;
152
+ if (entry.listeners.size === 0) return;
153
+ void load(root, false);
154
+ }, REPO_DISCOVERY_POLL_MS);
155
+ } else {
156
+ cancelRetry(entry);
157
+ }
158
+ })();
159
+
160
+ entry.pending = pending;
161
+ return pending;
162
+ }
163
+
164
+ function subscribe(root: string, listener: () => void): () => void {
165
+ const entry = entryFor(root);
166
+ entry.listeners.add(listener);
167
+ // First subscriber for this root — or the first since everyone left —
168
+ // starts (or resumes) discovery, unless a settled list is already known for
169
+ // this same root: a right-panel view switch must not re-run the server's
170
+ // `find`.
171
+ if (entry.controller === null && entry.cancelRetry === null && (!entry.loaded || entry.snapshot.scanning)) {
172
+ void load(root, false);
173
+ }
174
+ return () => {
175
+ entry.listeners.delete(listener);
176
+ if (entry.listeners.size > 0) return;
177
+ // Nobody is showing this root's list any more: abandon the read in flight
178
+ // and drop the retry. The snapshot keeps its `scanning` flag, so the next
179
+ // subscriber resumes discovery rather than inheriting a spinner that
180
+ // nothing is driving.
181
+ entry.controller?.abort();
182
+ entry.controller = null;
183
+ cancelRetry(entry);
184
+ };
185
+ }
186
+
187
+ return {
188
+ snapshot: (root) => entries.get(root)?.snapshot ?? UNKNOWN_LIST,
189
+ settled: (root) => entries.get(root)?.pending ?? Promise.resolve(),
190
+ subscribe,
191
+ rescan: (root) => void load(root, true),
192
+ };
193
+ }
194
+
195
+ /** The app's single store, over the browser's `fetch` and real timers. */
196
+ export const repoStore = createRepoStore({
197
+ fetch: async (url) => {
198
+ const response = await fetch(url);
199
+ return response.json();
200
+ },
201
+ });
@@ -16,14 +16,21 @@ interface SearchStreamState {
16
16
  * encoding the old `useFetcher.submit` used) and the response is consumed
17
17
  * incrementally via the shared SSE reader. A new `start` call aborts the
18
18
  * previous run — matching the fetcher semantics the panel relied on.
19
+ *
20
+ * Results carry the `scope` they were searched under (the workspace root plus
21
+ * the selected repo). A workspace switch therefore shows no hits at all rather
22
+ * than the previous workspace's — a match's path is relative to the root it was
23
+ * found under, so the same string names a different file, or none, here.
19
24
  */
20
- export function useSearchStream(): SearchStreamState & {
25
+ export function useSearchStream(scope: string): SearchStreamState & {
21
26
  start: (body: Record<string, string>) => Promise<void>;
22
27
  } {
23
- const [results, setResults] = useState<SearchResultItem[]>([]);
28
+ const [state, setState] = useState<{ scope: string; results: SearchResultItem[] }>({ scope, results: [] });
24
29
  const [isSearching, setIsSearching] = useState(false);
25
30
  const abortRef = useRef<AbortController | null>(null);
26
31
  const aliveRef = useRef(true);
32
+ const scopeRef = useRef(scope);
33
+ scopeRef.current = scope;
27
34
 
28
35
  useEffect(() => {
29
36
  aliveRef.current = true;
@@ -38,8 +45,9 @@ export function useSearchStream(): SearchStreamState & {
38
45
  abortRef.current?.abort();
39
46
  const controller = new AbortController();
40
47
  abortRef.current = controller;
48
+ const requested = scopeRef.current;
41
49
 
42
- setResults([]);
50
+ setState({ scope: requested, results: [] });
43
51
  setIsSearching(true);
44
52
  try {
45
53
  const response = await fetch('/api/fs/search', {
@@ -53,9 +61,15 @@ export function useSearchStream(): SearchStreamState & {
53
61
  response,
54
62
  (event, data) => {
55
63
  if (event !== 'matches') return;
64
+ // A run superseded by a workspace switch is no longer this panel's.
65
+ if (scopeRef.current !== requested) return;
56
66
  try {
57
67
  const parsed = JSON.parse(data);
58
- if (Array.isArray(parsed)) setResults(prev => [...prev, ...parsed]);
68
+ if (Array.isArray(parsed)) {
69
+ setState(prev => prev.scope === requested
70
+ ? { scope: requested, results: [...prev.results, ...parsed] }
71
+ : prev);
72
+ }
59
73
  } catch {
60
74
  // Malformed frame — skip; the next frame carries its own payload.
61
75
  }
@@ -72,5 +86,10 @@ export function useSearchStream(): SearchStreamState & {
72
86
  }
73
87
  }, []);
74
88
 
75
- return { results, isSearching, start };
89
+ return {
90
+ results: state.scope === scope ? state.results : [],
91
+ // A run in flight for another scope is not searching THIS one.
92
+ isSearching: isSearching && state.scope === scope,
93
+ start,
94
+ };
76
95
  }
@@ -2,6 +2,7 @@ import { hydrate } from 'preact';
2
2
  import { App } from '@/client/App';
3
3
  import { bootSyntax } from '@/shared/lib/code/highlighter';
4
4
  import { primeChamberSettings } from '@/shared/lib/settings/client';
5
+ import { initDocumentTheme } from '@/client/hooks/ui/theme';
5
6
 
6
7
  import '@/client/tailwind.css';
7
8
  import '@/shared/lib/markdown/katex-fonts.css';
@@ -12,6 +13,11 @@ const bootstrap = window.__OMP_BOOTSTRAP__;
12
13
 
13
14
  primeChamberSettings(bootstrap?.appSettings);
14
15
 
16
+ // The catalog stylesheet and both theme attributes, before the first render:
17
+ // the server writes them into the shell, so this only repairs a document served
18
+ // by an older build.
19
+ initDocumentTheme();
20
+
15
21
  // Fire-and-forget: warms the Shiki highlighter in parallel with hydration.
16
22
  bootSyntax();
17
23
 
@@ -1,6 +1,5 @@
1
1
  @import "tailwindcss";
2
2
  @import "@xterm/xterm/css/xterm.css";
3
- @plugin "@tailwindcss/typography";
4
3
 
5
4
  @theme {
6
5
  --color-ink: var(--theme-ink);
@@ -8,8 +7,6 @@
8
7
  --color-canvas: var(--theme-canvas);
9
8
  --color-error: var(--theme-error);
10
9
  --color-success: var(--theme-success);
11
- --color-success-bg: var(--theme-success-bg);
12
- --color-success-border: var(--theme-success-border);
13
10
  --color-meta: var(--theme-meta);
14
11
  --color-info: var(--theme-info);
15
12
  --color-info-hover: var(--theme-info-hover);
@@ -18,19 +15,23 @@
18
15
  }
19
16
 
20
17
  @layer base {
18
+ /* The default palette. Every other theme's block is generated from the
19
+ * catalog in `src/shared/lib/theme/palettes/` and injected as
20
+ * `html[data-theme=…]` by the SSR shell (see `shared/lib/theme/css.ts`) —
21
+ * hand-writing them here would duplicate the same eight values the appearance
22
+ * cards render as chips. */
21
23
  :root {
22
24
  --theme-paper: #faf8f3;
23
25
  --theme-canvas: #f4f1ea;
24
26
  --theme-ink: #141310;
25
27
  --theme-error: #c8321e;
26
28
  --theme-success: #047857;
27
- --theme-success-bg: #ecfdf5;
28
- --theme-success-border: #a7f3d0;
29
29
  --theme-meta: #4f46e5;
30
30
  --theme-info: #0284c7;
31
- --theme-info-hover: #0369a1;
31
+ --theme-info-hover: color-mix(in srgb, var(--theme-info) 85%, var(--theme-ink));
32
32
  --theme-warning: #c2410c;
33
- --theme-warning-hover: #9a3412;
33
+ --theme-warning-hover: color-mix(in srgb, var(--theme-warning) 85%, var(--theme-ink));
34
+ color-scheme: light;
34
35
 
35
36
  --paper-bg: var(--theme-canvas);
36
37
  --paper-raised: var(--theme-paper);
@@ -42,51 +43,6 @@
42
43
  --signal-red: var(--theme-error);
43
44
  }
44
45
 
45
- [data-theme="contrast"] {
46
- --theme-paper: #ffffff;
47
- --theme-canvas: #f4f4f4;
48
- --theme-ink: #111111;
49
- --theme-error: #b42318;
50
- --theme-success: #067647;
51
- --theme-success-bg: #ecfdf3;
52
- --theme-success-border: #abefc6;
53
- --theme-meta: #4338ca;
54
- --theme-info: #0369a1;
55
- --theme-info-hover: #075985;
56
- --theme-warning: #b54708;
57
- --theme-warning-hover: #93370d;
58
- }
59
-
60
- [data-theme="noir"] {
61
- --theme-paper: #282c34;
62
- --theme-canvas: #21252b;
63
- --theme-ink: #abb2bf;
64
- --theme-error: #e06c75;
65
- --theme-success: #34d399;
66
- --theme-success-bg: rgba(5, 150, 105, 0.15);
67
- --theme-success-border: rgba(16, 185, 129, 0.2);
68
- --theme-meta: #818cf8;
69
- --theme-info: #61afef;
70
- --theme-info-hover: #8ec7ff;
71
- --theme-warning: #fb923c;
72
- --theme-warning-hover: #fdba74;
73
- }
74
-
75
- [data-theme="one-dark-pro-soft"] {
76
- --theme-paper: #282c34;
77
- --theme-canvas: #21252b;
78
- --theme-ink: #abb2bf;
79
- --theme-error: #e06c75;
80
- --theme-success: #34d399;
81
- --theme-success-bg: rgba(5, 150, 105, 0.15);
82
- --theme-success-border: rgba(16, 185, 129, 0.2);
83
- --theme-meta: #818cf8;
84
- --theme-info: #61afef;
85
- --theme-info-hover: #8ec7ff;
86
- --theme-warning: #fb923c;
87
- --theme-warning-hover: #fdba74;
88
- }
89
-
90
46
  body {
91
47
  font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
92
48
  background-color: var(--theme-canvas);
@@ -235,6 +191,13 @@
235
191
  .prose-content li { line-height: inherit; position: relative; }
236
192
  .prose-content li + li { margin-top: 0.375rem; }
237
193
  .prose-content li p { margin: 0.25rem 0; }
194
+ /* Tailwind's preflight makes every `img` block-level. Markdown images are
195
+ * inline content: a README badge row is one `<p align="center">` of
196
+ * `<a><img></a>` links, and a block image stacks it one badge per line (and
197
+ * pushes an image off the text line it was written on). `inline-block` restores
198
+ * the line box GitHub renders; the preflight's `max-width: 100%` still applies,
199
+ * and `vertical-align: middle` keeps badges of different heights aligned. */
200
+ .prose-content img { display: inline-block; vertical-align: middle; }
238
201
  .prose-content ul > li::before { content: ''; display: block; position: absolute; left: -1.25em; top: 0; }
239
202
  .prose-content blockquote {
240
203
  border-left: 2px solid color-mix(in srgb, var(--theme-ink) 30%, transparent);
@@ -276,13 +239,13 @@
276
239
  .prose-content .code-block pre { margin: 0; padding: 0.75rem; overflow-x: auto; line-height: var(--markdown-code-block-line-height); }
277
240
  .prose-content .code-block code { font-family: 'Fira Code', monospace; font-size: 0.75rem; line-height: var(--markdown-code-block-line-height); }
278
241
  .prose-content .code-block pre code { background: transparent; text-shadow: none; }
279
- /* Shiki dual-theme tokens: the highlighter emits BOTH palettes (min-light for
242
+ /* Shiki dual-theme tokens: the highlighter emits BOTH palettes (one-light for
280
243
  * light themes, one-dark-pro for dark themes) as CSS variables on every token
281
244
  * span and no inline `color`, so the active [data-theme] selects the palette.
282
- * Switching themes recolors code blocks with zero JS and no re-highlight. */
245
+ * Switching themes recolors code blocks with zero JS and no re-highlight.
246
+ * The selector keys off the variant attribute, not a list of theme ids. */
283
247
  .shiki span { color: var(--shiki-light); }
284
- [data-theme="noir"] .shiki span,
285
- [data-theme="one-dark-pro-soft"] .shiki span { color: var(--shiki-dark); }
248
+ html[data-theme-variant="dark"] .shiki span { color: var(--shiki-dark); }
286
249
  .prose-content .katex-display { overflow-x: auto; overflow-y: hidden; }
287
250
 
288
251
  /* Math placeholder: `marked.ts` emits a `.math-pending` span and KaTeX is
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Spawn arguments for a topic's side child.
8
+ *
9
+ * The side child is an ordinary `omp --mode rpc-ui` that resumes the topic's
10
+ * transcript. It runs WITH tools (unlike the TUI's `/btw`, which passes
11
+ * `--no-tools`): the panel exposes a real access-control dropdown, and an
12
+ * approval mode that governs nothing is a lie in the UI. What the child may
13
+ * actually do is therefore the `--approval-mode` flag's business, exactly as it
14
+ * is for a chat session.
15
+ *
16
+ * `--no-title` stays: auto-titling would spend a model call on a transcript
17
+ * nobody lists in the sidebar.
18
+ */
19
+
20
+ import type { ApprovalMode } from '@/shared/lib/omp/config/access-mode';
21
+ import type { BtwModel } from '@/shared/types';
22
+
23
+ export interface BtwSpawnSettings {
24
+ model?: BtwModel;
25
+ /** omp's own default when absent (`auto`). */
26
+ thinkingLevel?: string;
27
+ /** Spawn-time only: omp exposes no RPC to change the approval mode. */
28
+ approvalMode?: ApprovalMode;
29
+ }
30
+
31
+ export function buildBtwSpawnArgs(sessionFile: string, settings: BtwSpawnSettings): string[] {
32
+ const args = ['--resume', sessionFile, '--no-title'];
33
+ if (settings.model) args.push('--model', `${settings.model.provider}/${settings.model.id}`);
34
+ // 'auto' means "leave omp's level alone" — the same rule the composer applies,
35
+ // so it is omitted rather than translated into a concrete level.
36
+ if (settings.thinkingLevel && settings.thinkingLevel !== 'auto') {
37
+ args.push('--thinking', settings.thinkingLevel);
38
+ }
39
+ if (settings.approvalMode) args.push('--approval-mode', settings.approvalMode);
40
+ return args;
41
+ }