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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ompchamber",
3
- "version": "1.2.1",
3
+ "version": "2.0.1",
4
4
  "description": "Developer web console and diagnostic chamber for AI Oh-My-Pi — Elysia + Preact on Bun.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Wijanarko Putra Rajeb <wijanarko.rajeb@gmail.com>",
@@ -49,7 +49,6 @@
49
49
  "@fontsource/fira-code": "^5.3.0",
50
50
  "@shikijs/langs": "^4.4.3",
51
51
  "@shikijs/themes": "^4.4.3",
52
- "@tailwindcss/typography": "^0.5.20",
53
52
  "@xterm/addon-fit": "^0.11.0",
54
53
  "@xterm/addon-webgl": "^0.19.0",
55
54
  "@xterm/xterm": "^6.0.0",
@@ -63,7 +62,8 @@
63
62
  "preact": "^10.29.8",
64
63
  "remend": "^1.3.1",
65
64
  "ripgrep": "^0.3.1",
66
- "shiki": "^4.4.3"
65
+ "shiki": "^4.4.3",
66
+ "yaml": "^2.9.1"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@rsbuild/core": "^2.2.7",
@@ -19,6 +19,13 @@ export interface ComposerTextareaProps {
19
19
  className?: string;
20
20
  onPaste?: (e: ClipboardEvent<HTMLTextAreaElement>) => void;
21
21
  rootPath?: string | null;
22
+ /**
23
+ * Whether the `@` / `/` / `!` / `#` autocomplete runs. Off for a composer with
24
+ * nothing to complete against — the side-question form has no file tree, no
25
+ * commands and no skills, and an `@` mention left in its text would reach the
26
+ * model as a literal no prompt builder translates.
27
+ */
28
+ enablePicker?: boolean;
22
29
  /**
23
30
  * `mobile` makes a bare Enter insert a newline instead of sending: phone
24
31
  * keyboards have no Shift key, so the configured Enter-to-send binding would
@@ -39,12 +46,13 @@ export function ComposerTextarea({
39
46
  className,
40
47
  onPaste,
41
48
  rootPath,
49
+ enablePicker = true,
42
50
  variant = 'desktop',
43
51
  }: ComposerTextareaProps): ReactElement {
44
52
  const isMobile = variant === 'mobile';
45
53
  const textareaRef = useRef<HTMLTextAreaElement>(null);
46
54
  const wrapperRef = useRef<HTMLDivElement>(null);
47
- const composer = useComposerTrigger({ value, setValue: onChange, textareaRef, disabled, rootPath });
55
+ const composer = useComposerTrigger({ value, setValue: onChange, textareaRef, disabled, rootPath, enabled: enablePicker });
48
56
 
49
57
  useOnClickOutside(wrapperRef, composer.close);
50
58
 
@@ -16,10 +16,12 @@
16
16
  import type { TargetedMouseEvent } from 'preact';
17
17
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks';
18
18
  import { renderMarkdown } from '@/shared/lib/markdown/marked';
19
+ import { rewriteDocumentReferences, type DocumentScope } from '@/shared/lib/markdown/document-urls';
19
20
  import { hydrateMathBlocks, MATH_PENDING_CLASS, preloadKatex } from '@/shared/lib/markdown/katex';
20
21
  import { sanitizeHtml } from '@/shared/lib/markdown/sanitize';
21
22
  import { hydrateMermaidBlocks } from '@/shared/lib/markdown/mermaid';
22
23
  import { copyToClipboard } from '@/client/hooks/ui/clipboard';
24
+ import { openFileInEditor } from '@/client/hooks/ui/open-file';
23
25
  import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
24
26
  import { DiagramViewer } from '@/client/components/common/diagram-viewer';
25
27
  import { readDiagramBlock, type DiagramSnapshot } from '@/client/components/common/diagram-viewer/read-block';
@@ -30,9 +32,17 @@ const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : us
30
32
  interface MarkdownRendererProps {
31
33
  content: string;
32
34
  className?: string;
35
+ /**
36
+ * Render the content as the document it IS (an opened file's preview) rather
37
+ * than as agent output: the source's own HTML renders as markup instead of
38
+ * showing its tags, and relative references resolve against `scope`.
39
+ */
40
+ document?: boolean;
41
+ /** Where the document lives, so its relative links and images resolve. */
42
+ scope?: DocumentScope;
33
43
  }
34
44
 
35
- export function MarkdownRenderer({ content, className = '' }: MarkdownRendererProps) {
45
+ export function MarkdownRenderer({ content, className = '', document: isDocument = false, scope }: MarkdownRendererProps) {
36
46
  const containerRef = useRef<HTMLDivElement>(null);
37
47
  /** Block the open viewer was read from, so a theme re-render can refresh it. */
38
48
  const diagramBlockRef = useRef<HTMLElement | null>(null);
@@ -40,15 +50,24 @@ export function MarkdownRenderer({ content, className = '' }: MarkdownRendererPr
40
50
 
41
51
  const syntaxReady = useSyntaxReady();
42
52
 
53
+ // Primitive deps: call sites build `scope` inline, and a new object identity
54
+ // per render would re-run marked + DOMPurify over the whole document.
55
+ const scopePath = scope?.path;
56
+ const scopeRoot = scope?.root;
57
+ const scopeRepo = scope?.repo;
58
+
43
59
  const html = useMemo(() => {
44
60
  if (!content) return '';
45
- const rendered = renderMarkdown(content);
61
+ const rendered = renderMarkdown(content, { allowHtml: isDocument });
62
+ const scoped = isDocument && scopePath
63
+ ? rewriteDocumentReferences(rendered, { path: scopePath, root: scopeRoot, repo: scopeRepo })
64
+ : rendered;
46
65
  // If the message contains math, start fetching KaTeX during parse rather
47
66
  // than waiting for the post-mount hydration effect — the request overlaps
48
67
  // sanitization and the rest of the render instead of following it.
49
- if (rendered.includes(MATH_PENDING_CLASS)) preloadKatex();
50
- return sanitizeHtml(rendered);
51
- }, [content, syntaxReady]);
68
+ if (scoped.includes(MATH_PENDING_CLASS)) preloadKatex();
69
+ return sanitizeHtml(scoped);
70
+ }, [content, syntaxReady, isDocument, scopePath, scopeRoot, scopeRepo]);
52
71
 
53
72
  const hasMermaid = html.includes('mermaid-block');
54
73
  const hasMath = html.includes(MATH_PENDING_CLASS);
@@ -154,15 +173,34 @@ export function MarkdownRenderer({ content, className = '' }: MarkdownRendererPr
154
173
  }
155
174
 
156
175
  // A rendered diagram is a door into the zoom/pan viewer; pending, failed,
157
- // and still-streaming blocks stay inert.
176
+ // and still-streaming blocks stay inert. It is checked before the document
177
+ // links below so a diagram's own anchors never steal its click.
158
178
  const block = (e.target as HTMLElement).closest<HTMLElement>('.mermaid-block[data-mermaid-state="done"]');
159
- if (!block) return;
160
- const snapshot = readDiagramBlock(block);
161
- if (!snapshot) return;
179
+ if (block) {
180
+ const snapshot = readDiagramBlock(block);
181
+ if (!snapshot) return;
182
+ e.preventDefault();
183
+ diagramBlockRef.current = block;
184
+ setDiagram(snapshot);
185
+ return;
186
+ }
187
+
188
+ if (!isDocument) return;
189
+
190
+ // A document's own links: a relative one is rewritten to the workspace
191
+ // file it names, so it belongs in an editor tab rather than in a top-level
192
+ // navigation off the console; an external one leaves the console.
193
+ const link = (e.target as HTMLElement).closest<HTMLAnchorElement>('a[href]');
194
+ if (!link) return;
162
195
  e.preventDefault();
163
- diagramBlockRef.current = block;
164
- setDiagram(snapshot);
165
- }, []);
196
+ e.stopPropagation();
197
+ const filePath = link.dataset.filePath;
198
+ if (filePath) {
199
+ openFileInEditor({ path: filePath, root: scopeRoot, repo: scopeRepo });
200
+ } else if (/^https?:/i.test(link.href)) {
201
+ window.open(link.href, '_blank', 'noopener,noreferrer');
202
+ }
203
+ }, [isDocument, scopeRoot, scopeRepo]);
166
204
 
167
205
  const closeDiagram = useCallback(() => {
168
206
  diagramBlockRef.current = null;
@@ -78,9 +78,15 @@ export function MobileFullEditor({ file, onClose, onFileSaved }: MobileFullEdito
78
78
  <span className="text-[11px] text-ink/50 font-mono break-all">{editor.loadError}</span>
79
79
  </div>
80
80
  ) : isMd && isPreview ? (
81
- /* Markdown Preview Mode */
82
- <div className="p-4 bg-paper text-ink min-h-full font-sans prose prose-sm max-w-none">
83
- <MarkdownRenderer content={content} />
81
+ /* Markdown Preview Mode — styled by `.prose-content` (the chat
82
+ timeline's system), see the desktop editor for why there is no
83
+ `prose` wrapper. */
84
+ <div className="p-4 bg-paper text-ink min-h-full font-sans">
85
+ <MarkdownRenderer
86
+ content={content}
87
+ document
88
+ scope={{ path: file.path ?? file.name, root: file.root, repo: file.repo }}
89
+ />
84
90
  </div>
85
91
  ) : (
86
92
  /* Code Editor with Line Numbers - Desktop styled: gutter bg-canvas border-r border-ink/10 */
@@ -2,8 +2,10 @@ import { useEffect, useState } from 'preact/hooks';
2
2
  import { Check, ChevronLeft, X } from 'lucide-preact';
3
3
  import type { SettingsCategoryId, SettingsState } from '@/shared/types';
4
4
  import { mergeChamberSettings } from '@/shared/lib/settings/client';
5
+ import { diffSettings } from '@/shared/lib/settings/diff';
5
6
  import { useChamberSettingsWriter } from '@/client/hooks/settings/use-chamber-setting';
6
7
  import { applyDocumentTheme } from '@/client/hooks/ui/theme';
8
+ import { DEFAULT_THEME_ID } from '@/shared/lib/theme/catalog';
7
9
  import { useChamberEvent } from '@/client/hooks/ui/window-event';
8
10
  import { SETTINGS_CATEGORIES, SettingsSidebar } from '@/client/components/settings/Sidebar';
9
11
  import { AppearanceSettings } from '@/client/components/settings/categories/AppearanceSettings';
@@ -35,7 +37,7 @@ const DEFAULT_SETTINGS: SettingsState = {
35
37
  showUpdateNotifications: true,
36
38
  agentControlTool: true,
37
39
  ompChamberWebTool: true,
38
- theme: 'paper',
40
+ theme: DEFAULT_THEME_ID,
39
41
  fontSize: 'standard',
40
42
  editorFont: 'JetBrains Mono',
41
43
  streamResponses: true,
@@ -104,7 +106,14 @@ export function SettingsModal({
104
106
  if (typeof document !== 'undefined' && next.theme) {
105
107
  applyDocumentTheme(next.theme);
106
108
  }
107
- writeChamberSettings(next);
109
+ // Only the keys this update actually touched are persisted. The modal
110
+ // holds a full `SettingsState` snapshot taken when it opened, so writing
111
+ // the whole object back would also republish every value it read at that
112
+ // moment — a tab left open across an external change (another tab, a
113
+ // `curl` to /api/settings) would silently revert it on the next unrelated
114
+ // toggle. `theme` in particular is written by the appearance picker and
115
+ // read by the SSR shell, so the stale copy is not just cosmetic.
116
+ writeChamberSettings(diffSettings(prev, next, updater));
108
117
  return next;
109
118
  });
110
119
  setToastMessage('Setting was saved');
@@ -1,5 +1,7 @@
1
- import { Check, Moon, Palette, Sun } from 'lucide-preact';
1
+ import { Palette } from 'lucide-preact';
2
2
  import type { SettingsState } from '@/shared/types';
3
+ import { resolveThemeId } from '@/shared/lib/theme/catalog';
4
+ import { ThemeGrid } from '@/client/components/settings/categories/appearance-settings/ThemeGrid';
3
5
 
4
6
  interface AppearanceSettingsProps {
5
7
  settings: SettingsState;
@@ -7,11 +9,6 @@ interface AppearanceSettingsProps {
7
9
  }
8
10
 
9
11
  export function AppearanceSettings({ settings, onUpdate }: AppearanceSettingsProps) {
10
- const themes = [
11
- { id: 'paper', name: 'E-Ink Paper', desc: 'Tema terang bawaan E-Ink Monochrome.', icon: Sun },
12
- { id: 'one-dark-pro-soft', name: 'One Dark Pro Soft', desc: 'Tema gelap ala One Dark Pro.', icon: Moon }
13
- ] as const;
14
-
15
12
  return (
16
13
  <div className="w-full space-y-8 text-xs text-ink">
17
14
  <section className="space-y-4">
@@ -20,30 +17,10 @@ export function AppearanceSettings({ settings, onUpdate }: AppearanceSettingsPro
20
17
  <h3 className="font-semibold text-sm uppercase tracking-wider text-ink/80">Theme Preferences</h3>
21
18
  </div>
22
19
 
23
- <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
24
- {themes.map(theme => (
25
- <button
26
- key={theme.id}
27
- onClick={() => onUpdate({ theme: theme.id })}
28
- className={`flex items-start space-x-3 p-3 rounded-lg border text-left transition-all ${
29
- settings.theme === theme.id
30
- ? 'border-ink bg-ink/5 ring-1 ring-ink/20'
31
- : 'border-ink/20 bg-paper hover:border-ink/40 hover:bg-ink/5'
32
- }`}
33
- >
34
- <div className={`p-1.5 rounded-full ${settings.theme === theme.id ? 'bg-ink text-paper' : 'bg-canvas text-ink/60'}`}>
35
- <theme.icon size={16} />
36
- </div>
37
- <div className="flex-1">
38
- <div className="font-semibold text-ink">{theme.name}</div>
39
- <div className="text-ink/60 mt-0.5 leading-relaxed">{theme.desc}</div>
40
- </div>
41
- {settings.theme === theme.id && (
42
- <Check size={16} className="text-ink mt-1" />
43
- )}
44
- </button>
45
- ))}
46
- </div>
20
+ <ThemeGrid
21
+ selectedId={settings.theme}
22
+ onSelect={(theme) => onUpdate({ theme: resolveThemeId(theme) })}
23
+ />
47
24
  </section>
48
25
  </div>
49
26
  );
@@ -20,6 +20,7 @@ export function ProviderSettings({
20
20
  onAddModalClose,
21
21
  }: ProviderSettingsProps) {
22
22
  const {
23
+ providers,
23
24
  connectedProviders,
24
25
  availablePresetProviders,
25
26
  selectedProvider,
@@ -107,6 +108,7 @@ export function ProviderSettings({
107
108
  }}
108
109
  onAddProvider={handleAddProvider}
109
110
  presets={availablePresetProviders}
111
+ existingSlugs={providers.map((provider) => provider.slug)}
110
112
  />
111
113
 
112
114
  {selectedProvider && (
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * One theme as a palette card: the name, a light/dark badge, and the eight
8
+ * colors the theme actually paints with — canvas, surface, ink, error, success,
9
+ * info, warning and accent.
10
+ *
11
+ * The chips are the palette's own hex values, not the live CSS variables, so
12
+ * the card shows what selecting it will do rather than what is currently on
13
+ * screen. Hovering a chip names its role; the two surface colors carry the
14
+ * card's frame, which is why they lead the strip.
15
+ */
16
+
17
+ import { Check } from 'lucide-preact';
18
+ import type { ThemePalette } from '@/shared/types/theme';
19
+ import { THEME_SWATCHES } from '@/shared/lib/theme/catalog';
20
+
21
+ interface ThemeCardProps {
22
+ theme: ThemePalette;
23
+ selected: boolean;
24
+ onSelect: (id: string) => void;
25
+ }
26
+
27
+ export function ThemeCard({ theme, selected, onSelect }: ThemeCardProps) {
28
+ return (
29
+ <button
30
+ type="button"
31
+ onClick={() => onSelect(theme.id)}
32
+ aria-pressed={selected}
33
+ className={`group flex w-full flex-col gap-2.5 rounded-lg border p-2.5 text-left transition-all cursor-pointer ${
34
+ selected
35
+ ? 'border-ink bg-ink/5 ring-1 ring-ink/20'
36
+ : 'border-ink/20 bg-paper hover:border-ink/40 hover:bg-ink/5'
37
+ }`}
38
+ >
39
+ <div className="flex items-center gap-2">
40
+ <span className="truncate font-semibold text-ink" title={theme.name}>
41
+ {theme.name}
42
+ </span>
43
+ <span className="shrink-0 rounded-full border border-ink/15 px-1.5 py-px text-[9px] font-semibold uppercase tracking-wider text-ink/50">
44
+ {theme.variant}
45
+ </span>
46
+ {selected && <Check size={14} className="ml-auto shrink-0 text-ink" />}
47
+ </div>
48
+
49
+ <div className="flex items-center gap-1">
50
+ {THEME_SWATCHES.map((swatch) => (
51
+ <span
52
+ key={swatch.key}
53
+ title={`${swatch.label} · ${theme[swatch.key]}`}
54
+ style={{ backgroundColor: theme[swatch.key] }}
55
+ className="h-5 flex-1 rounded-[3px] border border-ink/10"
56
+ />
57
+ ))}
58
+ </div>
59
+ </button>
60
+ );
61
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The theme catalog as a filterable grid of palette cards.
8
+ *
9
+ * Two axes of grouping, both from the catalog: the light/dark filter narrows
10
+ * what is shown, and the family grouping keeps a theme's light and dark entries
11
+ * adjacent (`Nord` and `Nord` rather than `Nord` and `Monokai`). The filter is
12
+ * local state — it changes nothing about the app, only what the grid lists.
13
+ */
14
+
15
+ import { useMemo, useState } from 'preact/hooks';
16
+ import { Moon, Sun } from 'lucide-preact';
17
+ import type { ThemePalette, ThemeVariant } from '@/shared/types/theme';
18
+ import { THEMES } from '@/shared/lib/theme/catalog';
19
+ import { ThemeCard } from '@/client/components/settings/categories/appearance-settings/ThemeCard';
20
+
21
+ interface ThemeGridProps {
22
+ selectedId: string;
23
+ onSelect: (id: string) => void;
24
+ }
25
+
26
+ type VariantFilter = 'all' | ThemeVariant;
27
+
28
+ const FILTERS: ReadonlyArray<{ id: VariantFilter; label: string }> = [
29
+ { id: 'all', label: 'All' },
30
+ { id: 'light', label: 'Light' },
31
+ { id: 'dark', label: 'Dark' },
32
+ ];
33
+
34
+ /**
35
+ * Catalog order is already family-grouped (see `catalog.ts`), so filtering is a
36
+ * plain pass — a second grouping step here would only risk disagreeing with it.
37
+ */
38
+ function filterThemes(filter: VariantFilter): readonly ThemePalette[] {
39
+ return filter === 'all' ? THEMES : THEMES.filter((theme) => theme.variant === filter);
40
+ }
41
+
42
+ export function ThemeGrid({ selectedId, onSelect }: ThemeGridProps) {
43
+ const [filter, setFilter] = useState<VariantFilter>('all');
44
+ const themes = useMemo(() => filterThemes(filter), [filter]);
45
+
46
+ return (
47
+ <div className="space-y-3 pb-6">
48
+ <div className="flex items-center gap-1.5">
49
+ {FILTERS.map((option) => (
50
+ <button
51
+ key={option.id}
52
+ type="button"
53
+ onClick={() => setFilter(option.id)}
54
+ className={`rounded-full border px-2.5 py-1 text-[11px] font-medium transition-colors cursor-pointer ${
55
+ filter === option.id
56
+ ? 'border-ink bg-ink text-paper'
57
+ : 'border-ink/20 bg-paper text-ink/70 hover:border-ink/40 hover:text-ink'
58
+ }`}
59
+ >
60
+ {option.id === 'light' && <Sun size={11} className="mr-1 inline align-[-1px]" />}
61
+ {option.id === 'dark' && <Moon size={11} className="mr-1 inline align-[-1px]" />}
62
+ {option.label}
63
+ </button>
64
+ ))}
65
+ <span className="ml-auto font-mono text-[11px] text-ink/50">{themes.length} themes</span>
66
+ </div>
67
+
68
+ <div className="grid grid-cols-1 gap-2.5 sm:grid-cols-2 lg:grid-cols-3">
69
+ {themes.map((theme) => (
70
+ <ThemeCard
71
+ key={theme.id}
72
+ theme={theme}
73
+ selected={theme.id === selectedId}
74
+ onSelect={onSelect}
75
+ />
76
+ ))}
77
+ </div>
78
+ </div>
79
+ );
80
+ }
@@ -20,7 +20,7 @@ const OPTIONS: Array<{
20
20
  badge: 'Default',
21
21
  icon: Sparkles,
22
22
  description:
23
- 'After the first run settles, the session is named from the conversation using omp\'s own tiny title model. Sessions stop showing "New Session - <timestamp>" in the sidebar.',
23
+ 'As soon as your first message lands, the session is named from it using omp\'s own tiny title model — retried once when the run ends if that first attempt came back empty. Sessions stop showing "New Session - <timestamp>" in the sidebar.',
24
24
  note: 'One small model call per session',
25
25
  },
26
26
  {
@@ -11,13 +11,24 @@ interface AddProviderModalProps {
11
11
  onClose: () => void;
12
12
  onAddProvider: (newProvider: ProviderItem, options: { fetchedCount: number }) => void;
13
13
  presets: PresetProviderOption[];
14
+ /**
15
+ * Slugs already present in the omp registry. A new provider's slug becomes a
16
+ * `models.yml` key, so reusing one silently overwrites that provider's
17
+ * `baseUrl`, `apiKey`, `api` and per-model metadata — the entry is written
18
+ * under the same key, not merged into it.
19
+ */
20
+ existingSlugs: string[];
14
21
  }
15
22
 
23
+ /** Slug shape omp accepts as a `models.yml` provider key. */
24
+ const SLUG_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
25
+
16
26
  export function AddProviderModal({
17
27
  isOpen,
18
28
  onClose,
19
29
  onAddProvider,
20
30
  presets,
31
+ existingSlugs,
21
32
  }: AddProviderModalProps) {
22
33
  const presetList = presets;
23
34
  const initialPreset = presetList[0];
@@ -42,19 +53,35 @@ export function AddProviderModal({
42
53
 
43
54
  if (!isOpen) return null;
44
55
 
56
+ const selectedPreset = presetList.find((p) => p.id === selectedPresetId);
57
+ const trimmedName = name.trim();
58
+ // Prefer the preset's own slug so a provider lands under the id omp knows it
59
+ // by; only a renamed provider gets one derived from its display name.
60
+ const slug = (trimmedName && selectedPreset && trimmedName === selectedPreset.name
61
+ ? selectedPreset.slug
62
+ : trimmedName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
63
+ ).toLowerCase();
64
+ const takenSlugs = new Set(existingSlugs.map((value) => value.trim().toLowerCase()));
65
+ const slugError = !slug
66
+ ? 'Enter a provider name to derive an id.'
67
+ : !SLUG_PATTERN.test(slug)
68
+ ? 'The derived id must start with a letter or digit.'
69
+ : takenSlugs.has(slug)
70
+ ? `"${slug}" is already registered in models.yml — connecting it would overwrite that provider. Pick another name.`
71
+ : '';
72
+
45
73
  const handleSubmit = async (e: { preventDefault: () => void }) => {
46
74
  e.preventDefault();
47
- if (!name.trim() || presetList.length === 0 || isSubmitting) return;
75
+ if (!trimmedName || presetList.length === 0 || isSubmitting || slugError) return;
48
76
  setIsSubmitting(true);
49
77
 
50
78
  try {
51
- const preset = presetList.find((p) => p.id === selectedPresetId);
52
79
  const timestamp = Date.now();
53
80
  const newProvider: ProviderItem = {
54
81
  id: `provider-${timestamp}`,
55
- name: name.trim(),
56
- slug: name.toLowerCase().replace(/[^a-z0-9]/g, '-') || `provider-${timestamp}`,
57
- icon: preset?.icon || 'custom',
82
+ name: trimmedName,
83
+ slug,
84
+ icon: selectedPreset?.icon || 'custom',
58
85
  status: 'connected',
59
86
  configuredIn: 'auth credentials',
60
87
  baseUrl: baseUrl.trim(),
@@ -97,7 +124,7 @@ export function AddProviderModal({
97
124
  </button>
98
125
  <button
99
126
  type="submit"
100
- disabled={isSubmitting || presetList.length === 0}
127
+ disabled={isSubmitting || presetList.length === 0 || Boolean(slugError)}
101
128
  className="px-3.5 py-1.5 rounded-md bg-ink text-canvas text-xs font-medium hover:opacity-90 transition-opacity cursor-pointer disabled:opacity-50 flex items-center gap-1.5"
102
129
  >
103
130
  {isSubmitting ? (
@@ -160,6 +187,16 @@ export function AddProviderModal({
160
187
  placeholder="e.g. My Provider"
161
188
  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"
162
189
  />
190
+ {slug && (
191
+ <p className="text-[10px] text-ink/40 mt-1 font-mono">
192
+ models.yml id: {slug}
193
+ </p>
194
+ )}
195
+ {slugError && (
196
+ <p className="text-[11px] text-error mt-1">
197
+ {slugError}
198
+ </p>
199
+ )}
163
200
  </div>
164
201
 
165
202
  {/* Base URL */}