ompchamber 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/README.md +17 -10
  2. package/dist/client/index.html +1 -1
  3. package/dist/client/static/css/index.5291f58223.css +2 -0
  4. package/dist/client/static/js/async/{1k.544adbf77d.js → 1k.5974df3b87.js} +2 -2
  5. package/dist/client/static/js/async/3j.357c80ff38.js +2 -0
  6. package/dist/client/static/js/async/{3y.22075aa079.js → 3y.ededeebde8.js} +5 -5
  7. package/dist/client/static/js/async/51.95bcae98af.js +2 -0
  8. package/dist/client/static/js/async/6.f034c9b528.js +1 -0
  9. package/dist/client/static/js/async/a.e05da304c8.js +10 -0
  10. package/dist/client/static/js/async/ar.1e14da5588.js +1 -0
  11. package/dist/client/static/js/async/ct.b74928f7a3.js +1 -0
  12. package/dist/client/static/js/async/dc.6dc4b0a04b.js +2 -0
  13. package/dist/client/static/js/async/fw.6a51998a8e.js +6 -0
  14. package/dist/client/static/js/async/g.7a4bbd4798.js +2 -0
  15. package/dist/client/static/js/async/g.7a4bbd4798.js.LICENSE.txt +11 -0
  16. package/dist/client/static/js/async/{k.e878dd350f.js → k.7466330d1a.js} +2 -2
  17. package/dist/client/static/js/async/katex-vendor.de6a87c308.js +1 -0
  18. package/dist/client/static/js/async/kc.bbdb35b771.js +1 -0
  19. package/dist/client/static/js/async/l6.c56640c205.js +1 -0
  20. package/dist/client/static/js/async/{m.3fc017a055.js → m.4b29d34f93.js} +3 -3
  21. package/dist/client/static/js/async/{og.ae7acf03ac.js → og.634fba99ac.js} +2 -2
  22. package/dist/client/static/js/async/q.7fb4d2f374.js +21 -0
  23. package/dist/client/static/js/async/rd.cb6bf495bb.js +2 -0
  24. package/dist/client/static/js/async/rf.d626afc367.js +2 -0
  25. package/dist/client/static/js/async/s.413c2a4683.js +2 -0
  26. package/dist/client/static/js/async/s.413c2a4683.js.LICENSE.txt +6 -0
  27. package/dist/client/static/js/async/sy.d16be843a7.js +94 -0
  28. package/dist/client/static/js/async/t.cb3fcf8615.js +1 -0
  29. package/dist/client/static/js/async/ud.3065bfee31.js +2 -0
  30. package/dist/client/static/js/async/uf.46ad6209fa.js +2 -0
  31. package/dist/client/static/js/async/uf.46ad6209fa.js.LICENSE.txt +6 -0
  32. package/dist/client/static/js/async/v.7d28d3c241.js +34 -0
  33. package/dist/client/static/js/async/v0.394ed37c6b.js +2 -0
  34. package/dist/client/static/js/async/v0.394ed37c6b.js.LICENSE.txt +6 -0
  35. package/dist/client/static/js/async/vt.42416b707d.js +1 -0
  36. package/dist/client/static/js/async/vy.bbe114c4e0.js +2 -0
  37. package/dist/client/static/js/async/vy.bbe114c4e0.js.LICENSE.txt +6 -0
  38. package/dist/client/static/js/async/xw.ae67ba06fd.js +31 -0
  39. package/dist/client/static/js/async/xw.ae67ba06fd.js.LICENSE.txt +4 -0
  40. package/dist/client/static/js/async/yg.35bebd6d0c.js +149 -0
  41. package/dist/client/static/js/async/yv.2fa22f009a.js +1 -0
  42. package/dist/client/static/js/index.0ac93ee878.js +33 -0
  43. package/dist/client/static/js/index.0ac93ee878.js.LICENSE.txt +4 -0
  44. package/dist/client/static/js/{markdown-vendor.9da142c67b.js → markdown-vendor.71e43278b3.js} +1 -1
  45. package/dist/client/static/js/uy.5f791144ce.js +139 -0
  46. package/package.json +2 -2
  47. package/src/cli/lib/args.js +1 -0
  48. package/src/cli/lib/commands/serve.js +7 -0
  49. package/src/client/App.tsx +22 -5
  50. package/src/client/components/common/MarkdownRenderer.tsx +36 -0
  51. package/src/client/components/layout/desktop-layout/index.tsx +1 -1
  52. package/src/client/components/layout/session-sidebar/Modals.tsx +1 -1
  53. package/src/client/components/mobile/LayoutWrapper.tsx +1 -1
  54. package/src/client/components/settings/LazyModal.tsx +46 -0
  55. package/src/client/components/settings/Modal.tsx +1 -1
  56. package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +43 -29
  57. package/src/client/components/workspace/chat-timeline/MessageItem.tsx +15 -69
  58. package/src/client/components/workspace/chat-timeline/MessageList.tsx +47 -66
  59. package/src/client/components/workspace/chat-timeline/RunFooter.tsx +60 -0
  60. package/src/client/components/workspace/chat-timeline/SubagentView.tsx +37 -21
  61. package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +22 -4
  62. package/src/client/components/workspace/chat-timeline/index.tsx +3 -6
  63. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +15 -19
  64. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/EnvelopeHeader.tsx +46 -0
  65. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/FallbackOutput.tsx +18 -68
  66. package/src/client/components/workspace/file-explorer/TreeItem.tsx +16 -4
  67. package/src/client/components/workspace/file-explorer/index.tsx +6 -0
  68. package/src/client/data/models/catalog.ts +8 -0
  69. package/src/client/data/samples/dialogue-session.ts +1 -1
  70. package/src/client/data/samples/sample-tools/other.ts +1 -1
  71. package/src/client/hooks/chat/omp/index.ts +9 -10
  72. package/src/client/hooks/chat/omp/revalidation-throttle.ts +6 -5
  73. package/src/client/hooks/chat/timeline/actions.ts +1 -2
  74. package/src/client/hooks/chat/timeline/index.ts +5 -0
  75. package/src/client/hooks/chat/timeline/send.ts +2 -3
  76. package/src/client/hooks/chat/timeline/session-load.ts +3 -4
  77. package/src/client/hooks/ui/syntax-ready.ts +16 -3
  78. package/src/client/main.tsx +2 -5
  79. package/src/client/stubs/elkjs.ts +37 -0
  80. package/src/client/tailwind.css +10 -0
  81. package/src/server/index.ts +53 -0
  82. package/src/server/lib/fs/git-ignore.ts +49 -0
  83. package/src/server/lib/models/provider-registry.server.ts +44 -20
  84. package/src/server/lib/omp/core/cli.ts +3 -2
  85. package/src/server/lib/omp/core/startup.ts +64 -0
  86. package/src/server/lib/omp/rpc/constants.ts +10 -0
  87. package/src/server/lib/omp/rpc/lifecycle.test.ts +138 -0
  88. package/src/server/lib/omp/rpc/manager.ts +32 -4
  89. package/src/server/lib/omp/rpc/process.ts +1 -1
  90. package/src/server/lib/omp/rpc/session-commands.ts +32 -9
  91. package/src/server/lib/omp/rpc/session-registry.ts +3 -2
  92. package/src/server/lib/omp/rpc/subagent-liveness.test.ts +62 -0
  93. package/src/server/lib/omp/rpc/subagent-liveness.ts +100 -0
  94. package/src/server/lib/omp/session/messages.test.ts +112 -0
  95. package/src/server/lib/omp/session/messages.ts +6 -5
  96. package/src/server/mock.server.ts +6 -12
  97. package/src/server/routes/agent/command.ts +34 -16
  98. package/src/server/routes/chat/stream.ts +13 -30
  99. package/src/server/routes/fs/read.ts +15 -7
  100. package/src/server/routes/models/root.ts +5 -1
  101. package/src/shared/lib/chat/notice-row.test.ts +89 -0
  102. package/src/shared/lib/chat/notice-row.ts +78 -0
  103. package/src/shared/lib/chat/omp/abort.ts +41 -0
  104. package/src/shared/lib/chat/stream-service.ts +0 -76
  105. package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +154 -0
  106. package/src/shared/lib/chat/timeline/omp-callbacks.ts +36 -7
  107. package/src/shared/lib/chat/timeline/run-footer.test.ts +84 -0
  108. package/src/shared/lib/chat/timeline/run-footer.ts +96 -0
  109. package/src/shared/lib/chat/tool-output.test.ts +91 -0
  110. package/src/shared/lib/chat/tool-output.ts +54 -0
  111. package/src/shared/lib/chat/xml-envelope.ts +158 -0
  112. package/src/shared/lib/code/highlighter-lazy.ts +106 -0
  113. package/src/shared/lib/code/highlighter.ts +31 -52
  114. package/src/shared/lib/code/syntax-highlight.test.ts +36 -1
  115. package/src/shared/lib/code/syntax-highlight.ts +17 -3
  116. package/src/shared/lib/format/time.ts +19 -0
  117. package/src/shared/lib/fs/paths.ts +19 -0
  118. package/src/shared/lib/markdown/fira-code-fonts.css +51 -0
  119. package/src/shared/lib/markdown/katex-fonts.css +20 -0
  120. package/src/shared/lib/markdown/katex.ts +101 -0
  121. package/src/shared/lib/markdown/marked.ts +23 -14
  122. package/src/shared/lib/markdown/math.test.ts +109 -0
  123. package/src/shared/lib/markdown/mermaid.ts +6 -0
  124. package/src/shared/lib/markdown/sanitize.ts +48 -2
  125. package/src/shared/lib/omp/session/mapper.ts +11 -3
  126. package/src/shared/lib/omp/session/messages-map.ts +12 -12
  127. package/src/shared/types/chat.ts +3 -1
  128. package/src/shared/types/fs.ts +3 -0
  129. package/dist/client/static/css/g.f36aa429fa.css +0 -1
  130. package/dist/client/static/css/index.a0f07455bd.css +0 -2
  131. package/dist/client/static/css/katex-vendor.ca3a0600e8.css +0 -1
  132. package/dist/client/static/font/KaTeX_AMS-Regular.1608a09b4a.woff +0 -0
  133. package/dist/client/static/font/KaTeX_AMS-Regular.4aafdb685c.ttf +0 -0
  134. package/dist/client/static/font/KaTeX_Caligraphic-Bold.b67709187b.woff +0 -0
  135. package/dist/client/static/font/KaTeX_Caligraphic-Bold.cce5b8ecea.ttf +0 -0
  136. package/dist/client/static/font/KaTeX_Caligraphic-Regular.07ef19e7b0.ttf +0 -0
  137. package/dist/client/static/font/KaTeX_Caligraphic-Regular.dad44a7fc6.woff +0 -0
  138. package/dist/client/static/font/KaTeX_Fraktur-Bold.9f256b8593.woff +0 -0
  139. package/dist/client/static/font/KaTeX_Fraktur-Bold.b18f59e1d1.ttf +0 -0
  140. package/dist/client/static/font/KaTeX_Fraktur-Regular.7c1871215e.woff +0 -0
  141. package/dist/client/static/font/KaTeX_Fraktur-Regular.ed38e79f57.ttf +0 -0
  142. package/dist/client/static/font/KaTeX_Main-Bold.b74a1a8b2c.ttf +0 -0
  143. package/dist/client/static/font/KaTeX_Main-Bold.d181c4650d.woff +0 -0
  144. package/dist/client/static/font/KaTeX_Main-BoldItalic.70d8b0a530.ttf +0 -0
  145. package/dist/client/static/font/KaTeX_Main-BoldItalic.e3f82f9d27.woff +0 -0
  146. package/dist/client/static/font/KaTeX_Main-Italic.47373d1e51.ttf +0 -0
  147. package/dist/client/static/font/KaTeX_Main-Italic.9024d815ba.woff +0 -0
  148. package/dist/client/static/font/KaTeX_Main-Regular.7f51fe0340.woff +0 -0
  149. package/dist/client/static/font/KaTeX_Main-Regular.b7f8fe9b5f.ttf +0 -0
  150. package/dist/client/static/font/KaTeX_Math-BoldItalic.a879cf8383.ttf +0 -0
  151. package/dist/client/static/font/KaTeX_Math-BoldItalic.f1035d8d5d.woff +0 -0
  152. package/dist/client/static/font/KaTeX_Math-Italic.5295ba483a.woff +0 -0
  153. package/dist/client/static/font/KaTeX_Math-Italic.939bc64440.ttf +0 -0
  154. package/dist/client/static/font/KaTeX_SansSerif-Bold.94e1e8dc5f.ttf +0 -0
  155. package/dist/client/static/font/KaTeX_SansSerif-Bold.bf59d231c3.woff +0 -0
  156. package/dist/client/static/font/KaTeX_SansSerif-Italic.7c9bc82b17.woff +0 -0
  157. package/dist/client/static/font/KaTeX_SansSerif-Italic.b4c20c84d8.ttf +0 -0
  158. package/dist/client/static/font/KaTeX_SansSerif-Regular.740484788f.woff +0 -0
  159. package/dist/client/static/font/KaTeX_SansSerif-Regular.d4d7ba4804.ttf +0 -0
  160. package/dist/client/static/font/KaTeX_Script-Regular.0750571032.woff +0 -0
  161. package/dist/client/static/font/KaTeX_Script-Regular.fe9cbbe1a0.ttf +0 -0
  162. package/dist/client/static/font/KaTeX_Size1-Regular.e1e279cbdd.woff +0 -0
  163. package/dist/client/static/font/KaTeX_Size1-Regular.fabc004aab.ttf +0 -0
  164. package/dist/client/static/font/KaTeX_Size2-Regular.577270225e.woff +0 -0
  165. package/dist/client/static/font/KaTeX_Size2-Regular.d6b476ecd3.ttf +0 -0
  166. package/dist/client/static/font/KaTeX_Size3-Regular.9acaf01c64.woff +0 -0
  167. package/dist/client/static/font/KaTeX_Size3-Regular.a144ef5840.ttf +0 -0
  168. package/dist/client/static/font/KaTeX_Size4-Regular.7a996c9da2.woff +0 -0
  169. package/dist/client/static/font/KaTeX_Size4-Regular.fbccdabe0a.ttf +0 -0
  170. package/dist/client/static/font/KaTeX_Typewriter-Regular.6258592bdc.woff +0 -0
  171. package/dist/client/static/font/KaTeX_Typewriter-Regular.d97aaf4a1e.ttf +0 -0
  172. package/dist/client/static/font/fira-code-cyrillic-400-normal.5971438a4a.woff2 +0 -0
  173. package/dist/client/static/font/fira-code-cyrillic-400-normal.9bb7bfea9c.woff +0 -0
  174. package/dist/client/static/font/fira-code-cyrillic-500-normal.5486265b18.woff2 +0 -0
  175. package/dist/client/static/font/fira-code-cyrillic-500-normal.89f92bfb31.woff +0 -0
  176. package/dist/client/static/font/fira-code-cyrillic-600-normal.2805b497e5.woff +0 -0
  177. package/dist/client/static/font/fira-code-cyrillic-600-normal.86e0df22a3.woff2 +0 -0
  178. package/dist/client/static/font/fira-code-cyrillic-700-normal.505a88f22a.woff +0 -0
  179. package/dist/client/static/font/fira-code-cyrillic-700-normal.76f5e8d2e2.woff2 +0 -0
  180. package/dist/client/static/font/fira-code-cyrillic-ext-400-normal.3c4b15e57d.woff2 +0 -0
  181. package/dist/client/static/font/fira-code-cyrillic-ext-400-normal.61323debaa.woff +0 -0
  182. package/dist/client/static/font/fira-code-cyrillic-ext-500-normal.0f34210f2a.woff2 +0 -0
  183. package/dist/client/static/font/fira-code-cyrillic-ext-500-normal.2497cfa1aa.woff +0 -0
  184. package/dist/client/static/font/fira-code-cyrillic-ext-600-normal.40fce94af9.woff2 +0 -0
  185. package/dist/client/static/font/fira-code-cyrillic-ext-600-normal.6c8774b829.woff +0 -0
  186. package/dist/client/static/font/fira-code-cyrillic-ext-700-normal.4b742eb865.woff +0 -0
  187. package/dist/client/static/font/fira-code-cyrillic-ext-700-normal.e75eb4fccc.woff2 +0 -0
  188. package/dist/client/static/font/fira-code-greek-400-normal.bb900468b6.woff2 +0 -0
  189. package/dist/client/static/font/fira-code-greek-400-normal.be76e699ba.woff +0 -0
  190. package/dist/client/static/font/fira-code-greek-500-normal.176c9fb001.woff2 +0 -0
  191. package/dist/client/static/font/fira-code-greek-500-normal.d2c596136b.woff +0 -0
  192. package/dist/client/static/font/fira-code-greek-600-normal.490c3deed7.woff +0 -0
  193. package/dist/client/static/font/fira-code-greek-600-normal.7df14376d9.woff2 +0 -0
  194. package/dist/client/static/font/fira-code-greek-700-normal.c82b6072fc.woff2 +0 -0
  195. package/dist/client/static/font/fira-code-greek-700-normal.d5f2b2917b.woff +0 -0
  196. package/dist/client/static/font/fira-code-greek-ext-400-normal.4849a79781.woff +0 -0
  197. package/dist/client/static/font/fira-code-greek-ext-400-normal.7f5bfc4538.woff2 +0 -0
  198. package/dist/client/static/font/fira-code-greek-ext-500-normal.aaccca4075.woff2 +0 -0
  199. package/dist/client/static/font/fira-code-greek-ext-500-normal.e383181256.woff +0 -0
  200. package/dist/client/static/font/fira-code-greek-ext-600-normal.78cb4bff8e.woff +0 -0
  201. package/dist/client/static/font/fira-code-greek-ext-600-normal.d6c93d54f9.woff2 +0 -0
  202. package/dist/client/static/font/fira-code-greek-ext-700-normal.57ec7c92d9.woff +0 -0
  203. package/dist/client/static/font/fira-code-greek-ext-700-normal.8fafdae5e8.woff2 +0 -0
  204. package/dist/client/static/font/fira-code-latin-400-normal.973295532b.woff +0 -0
  205. package/dist/client/static/font/fira-code-latin-500-normal.7ae2be8fb8.woff +0 -0
  206. package/dist/client/static/font/fira-code-latin-600-normal.1dbbe5bc27.woff +0 -0
  207. package/dist/client/static/font/fira-code-latin-700-normal.497a70148b.woff +0 -0
  208. package/dist/client/static/font/fira-code-latin-ext-400-normal.9dfae29aaf.woff +0 -0
  209. package/dist/client/static/font/fira-code-latin-ext-400-normal.e534ae46e2.woff2 +0 -0
  210. package/dist/client/static/font/fira-code-latin-ext-500-normal.24c7b781b0.woff +0 -0
  211. package/dist/client/static/font/fira-code-latin-ext-500-normal.aad44ab072.woff2 +0 -0
  212. package/dist/client/static/font/fira-code-latin-ext-600-normal.7bed0f2f7e.woff +0 -0
  213. package/dist/client/static/font/fira-code-latin-ext-600-normal.c509a7c9f1.woff2 +0 -0
  214. package/dist/client/static/font/fira-code-latin-ext-700-normal.6c66c8b0a9.woff2 +0 -0
  215. package/dist/client/static/font/fira-code-latin-ext-700-normal.88f64e2822.woff +0 -0
  216. package/dist/client/static/font/fira-code-symbols2-400-normal.97d3f4b17c.woff +0 -0
  217. package/dist/client/static/font/fira-code-symbols2-500-normal.b06f1a06e4.woff +0 -0
  218. package/dist/client/static/font/fira-code-symbols2-600-normal.304b653b93.woff +0 -0
  219. package/dist/client/static/font/fira-code-symbols2-700-normal.d8ce0ad586.woff +0 -0
  220. package/dist/client/static/js/async/3j.a2fcea99be.js +0 -2
  221. package/dist/client/static/js/async/51.1938cec320.js +0 -2
  222. package/dist/client/static/js/async/6.7929428505.js +0 -1
  223. package/dist/client/static/js/async/69.a18a10882f.js +0 -1
  224. package/dist/client/static/js/async/a.bce4542392.js +0 -1
  225. package/dist/client/static/js/async/ar.9dc8b1992a.js +0 -2
  226. package/dist/client/static/js/async/bi.cbfe8eaa8e.js +0 -1
  227. package/dist/client/static/js/async/ct.38d9da8a5a.js +0 -1
  228. package/dist/client/static/js/async/fw.fb1a08b92d.js +0 -6
  229. package/dist/client/static/js/async/l6.45f3b62e15.js +0 -1
  230. package/dist/client/static/js/async/q.0b6fb9bf8d.js +0 -21
  231. package/dist/client/static/js/async/rd.b77011ab7f.js +0 -2
  232. package/dist/client/static/js/async/rf.cfbde7f744.js +0 -2
  233. package/dist/client/static/js/async/s.c7d521a8ae.js +0 -2
  234. package/dist/client/static/js/async/sy.7345abcd67.js +0 -94
  235. package/dist/client/static/js/async/t.0dcb01dfde.js +0 -1
  236. package/dist/client/static/js/async/v.5c7a4b087b.js +0 -1
  237. package/dist/client/static/js/async/vu.311f1d202c.js +0 -34
  238. package/dist/client/static/js/async/yg.9bd4e09ecb.js +0 -149
  239. package/dist/client/static/js/async/yv.593a3f7d54.js +0 -1
  240. package/dist/client/static/js/g.124fba0826.js +0 -139
  241. package/dist/client/static/js/index.907fd4670e.js +0 -68
  242. package/dist/client/static/js/katex-vendor.95e48cf65a.js +0 -1
  243. package/src/shared/lib/chat/order.ts +0 -40
  244. /package/dist/client/static/js/async/{1k.544adbf77d.js.LICENSE.txt → 1k.5974df3b87.js.LICENSE.txt} +0 -0
  245. /package/dist/client/static/js/async/{3j.a2fcea99be.js.LICENSE.txt → 3j.357c80ff38.js.LICENSE.txt} +0 -0
  246. /package/dist/client/static/js/async/{3y.22075aa079.js.LICENSE.txt → 3y.ededeebde8.js.LICENSE.txt} +0 -0
  247. /package/dist/client/static/js/async/{51.1938cec320.js.LICENSE.txt → 51.95bcae98af.js.LICENSE.txt} +0 -0
  248. /package/dist/client/static/js/async/{ar.9dc8b1992a.js.LICENSE.txt → a.e05da304c8.js.LICENSE.txt} +0 -0
  249. /package/dist/client/static/js/async/{fw.fb1a08b92d.js.LICENSE.txt → dc.6dc4b0a04b.js.LICENSE.txt} +0 -0
  250. /package/dist/client/static/js/async/{m.3fc017a055.js.LICENSE.txt → fw.6a51998a8e.js.LICENSE.txt} +0 -0
  251. /package/dist/client/static/js/async/{q.0b6fb9bf8d.js.LICENSE.txt → m.4b29d34f93.js.LICENSE.txt} +0 -0
  252. /package/dist/client/static/js/async/{rf.cfbde7f744.js.LICENSE.txt → q.7fb4d2f374.js.LICENSE.txt} +0 -0
  253. /package/dist/client/static/js/async/{rd.b77011ab7f.js.LICENSE.txt → rd.cb6bf495bb.js.LICENSE.txt} +0 -0
  254. /package/dist/client/static/js/async/{s.c7d521a8ae.js.LICENSE.txt → rf.d626afc367.js.LICENSE.txt} +0 -0
  255. /package/dist/client/static/js/{index.907fd4670e.js.LICENSE.txt → async/ud.3065bfee31.js.LICENSE.txt} +0 -0
  256. /package/dist/client/static/js/{g.124fba0826.js.LICENSE.txt → uy.5f791144ce.js.LICENSE.txt} +0 -0
@@ -5,12 +5,16 @@ import { useFetcher } from '@/client/lib/router/fetcher';
5
5
  import { FileIcon } from '@/client/components/common/file-icon';
6
6
  import { FileContextMenu, FileDeleteModal, FileHistoryModal, FileRenameModal } from '@/client/components/workspace/file-explorer/Modals';
7
7
  import { getGitStatusInfo, type FolderGitStatusInfo } from '@/shared/lib/fs/git-status';
8
+ import { toAbsolutePath } from '@/shared/lib/fs/paths';
9
+ import { copyToClipboard } from '@/client/hooks/ui/clipboard';
8
10
  import type { GitChange } from '@/shared/types/git';
9
11
  import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
10
12
 
11
13
  interface FileTreeItemProps {
12
14
  file: any;
13
15
  rootPath?: string;
16
+ /** Absolute base dir of the listing (server-reported `root`), anchors Copy Path. */
17
+ basePath?: string;
14
18
  repo?: string;
15
19
  onLoadChildren?: (path: string) => Promise<void>;
16
20
  onOpenFile?: (file: any) => void;
@@ -24,6 +28,7 @@ interface FileTreeItemProps {
24
28
  export function FileTreeItem({
25
29
  file,
26
30
  rootPath,
31
+ basePath,
27
32
  repo,
28
33
  onLoadChildren,
29
34
  onOpenFile,
@@ -53,6 +58,9 @@ export function FileTreeItem({
53
58
  const gitStatusInfo = gitChange ? getGitStatusInfo(gitChange.status, gitChange.staged) : null;
54
59
  const folderStatus = isFolder ? (gitFolderMap?.get(normalizedPath) || gitFolderMap?.get(file.path)) : null;
55
60
  const hasGitStatus = Boolean(gitStatusInfo);
61
+ // Git-ignored entries (local .gitignore or the global excludes file) are still
62
+ // listed but rendered faded, the way an editor dims untracked noise.
63
+ const isIgnored = file.ignored === true;
56
64
 
57
65
  const isExpanded = expandedPaths ? expandedPaths.has(file.path) : isOpen;
58
66
  const actualIsOpen = file.forceExpanded !== undefined ? file.forceExpanded : isExpanded;
@@ -148,9 +156,9 @@ export function FileTreeItem({
148
156
  fd.append('path', file.path);
149
157
  submitAction(fd);
150
158
  } else if (actionType === 'copy_path') {
151
- navigator.clipboard.writeText('/app/applet/examples/' + file.path);
159
+ void copyToClipboard(toAbsolutePath(basePath, file.path));
152
160
  } else if (actionType === 'copy_relative') {
153
- navigator.clipboard.writeText(file.path);
161
+ void copyToClipboard(file.path);
154
162
  } else if (actionType === 'history') {
155
163
  setShowHistoryModal(true);
156
164
  const fd = new FormData();
@@ -194,8 +202,11 @@ export function FileTreeItem({
194
202
  <span className="w-3 flex-shrink-0"></span>
195
203
  )}
196
204
 
197
- <FileIcon name={file.name} isFolder={isFolder} isOpen={actualIsOpen} size={12} className="flex-shrink-0" />
198
- <span className={`truncate min-w-0 flex-1 ${gitStatusInfo ? gitStatusInfo.colorClass : folderStatus ? folderStatus.colorClass : ''}`}>
205
+ <FileIcon name={file.name} isFolder={isFolder} isOpen={actualIsOpen} size={12} className={`flex-shrink-0${isIgnored ? ' opacity-40' : ''}`} />
206
+ <span
207
+ className={`truncate min-w-0 flex-1 ${gitStatusInfo ? gitStatusInfo.colorClass : folderStatus ? folderStatus.colorClass : isIgnored ? 'text-ink/40' : ''}`}
208
+ title={isIgnored ? `${file.path} — git-ignored` : undefined}
209
+ >
199
210
  {file.name}
200
211
  </span>
201
212
 
@@ -230,6 +241,7 @@ export function FileTreeItem({
230
241
  key={child.id}
231
242
  file={child}
232
243
  rootPath={rootPath}
244
+ basePath={basePath}
233
245
  repo={repo}
234
246
  onLoadChildren={onLoadChildren}
235
247
  onOpenFile={onOpenFile}
@@ -10,6 +10,10 @@ import { usePanelRefresh, useFileMutationRefresh } from '@/client/hooks/workspac
10
10
 
11
11
  export function FileExplorer({ className = '', enabled = true, rootPath, onOpenFile, refreshKey = 0, onRefresh }: { className?: string, enabled?: boolean, rootPath?: string, onOpenFile?: (file: any) => void, refreshKey?: number, onRefresh?: () => void }) {
12
12
  const [tree, setTree] = useState<any[]>([]);
13
+ // Absolute base dir reported by `/api/fs/dir` for the current listing — the
14
+ // workspace root, or the selected nested repo. Copy Path anchors on it
15
+ // because the client's `rootPath` may be rejected/unset.
16
+ const [listingRoot, setListingRoot] = useState('');
13
17
  const [searchQuery, setSearchQuery] = useSessionState<string>('files.searchQuery', '');
14
18
  const [isLoading, setIsLoading] = useState(false);
15
19
  const [storedExpandedPaths, setStoredExpandedPaths, expandedPathsReady] = useSessionState<string[]>('files.expandedPaths', []);
@@ -43,6 +47,7 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
43
47
  fetch(listUrl())
44
48
  .then(r => r.json())
45
49
  .then(data => {
50
+ if (typeof data.root === 'string') setListingRoot(data.root);
46
51
  if (Array.isArray(data.files)) {
47
52
  setTree(rehydrateTree(data.files, childrenCacheRef.current, expandedPaths));
48
53
  }
@@ -176,6 +181,7 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
176
181
  key={file.id}
177
182
  file={file}
178
183
  rootPath={rootPath}
184
+ basePath={listingRoot}
179
185
  repo={activeRepo}
180
186
  onLoadChildren={loadChildren}
181
187
  onOpenFile={onOpenFile}
@@ -1,5 +1,13 @@
1
1
  import type { AIModelOption } from '@/shared/types';
2
2
 
3
+ /**
4
+ * MOCK-only demo catalog: the model rows a `MOCK=true` chamber shows before the
5
+ * operator adds any of their own.
6
+ *
7
+ * Real mode never reads this — `/api/models` there is fed by the live omp model
8
+ * registry, and its mutations start from an empty list — so a demo model can
9
+ * never leak into a real installation (see `routes/models/root.ts`).
10
+ */
3
11
  export const INITIAL_MODELS_CATALOG: AIModelOption[] = [
4
12
  // FAVORITES (As seen in image.png)
5
13
  {
@@ -277,7 +277,7 @@ export function getSampleDialogueSession(): {
277
277
  id: '65e71033',
278
278
  role: 'ai',
279
279
  content:
280
- 'Repo summary from `RepoSummarizer`:\n\n**OMPChamber** — developer web-view console and diagnostic chamber for AI **Oh-My-Pi** autonomous agent.\n\n**Stack:** Remix v2 + React 19 + Vite, Tailwind CSS v4, TypeScript (strict, ES2022). Runs under npm or Bun.\n\n**Structure:**\n- `app/routes/` — Remix flat-routes, large REST-style `api/` domain tree\n- `app/components/` — common, layout, workspace, mobile, settings\n- `app/hooks/` — chat, terminal, git-tree, agent streaming\n- `app/lib/omp/` — bridges to oh-my-pi runtime (RPC managers, session readers)\n- `app/types/`, `app/data/` (mock presets)\n\n**Config:** `MOCK` mode toggles seeded demo data vs real data. SQLite persistence (`app/db.server.ts`).\n\n**Deployment-relevant:** PWA support, `@google/genai` assistant, xterm/KaTeX/Shiki rendering.',
280
+ 'Repo summary from `RepoSummarizer`:\n\n**OMPChamber** — developer web-view console and diagnostic chamber for AI **Oh-My-Pi** autonomous agent.\n\n**Stack:** Remix v2 + React 19 + Vite, Tailwind CSS v4, TypeScript (strict, ES2022). Runs under npm or Bun.\n\n**Structure:**\n- `app/routes/` — Remix flat-routes, large REST-style `api/` domain tree\n- `app/components/` — common, layout, workspace, mobile, settings\n- `app/hooks/` — chat, terminal, git-tree, agent streaming\n- `app/lib/omp/` — bridges to oh-my-pi runtime (RPC managers, session readers)\n- `app/types/`, `app/data/` (mock presets)\n\n**Config:** `MOCK` mode toggles seeded demo data vs real data. SQLite persistence (`app/db.server.ts`).\n\n**Deployment-relevant:** PWA support, xterm/KaTeX/Shiki rendering.',
281
281
  date: 'Sep 9, 04:44 AM',
282
282
  },
283
283
  ],
@@ -88,7 +88,7 @@ export const otherTools: ToolCallData[] = [
88
88
  target: 'app/mock.server.ts',
89
89
  output: `1: export function isMockMode(): boolean {
90
90
  2: const envVal = (Bun.env.MOCK || "").trim().toLowerCase();
91
- 3: return envVal !== "false" && envVal !== "0";
91
+ 3: return envVal === "true" || envVal === "1" || envVal === "on" || envVal === "yes";
92
92
  4: }`,
93
93
  status: 'success',
94
94
  duration: '14ms',
@@ -3,6 +3,7 @@ import type { ChatMessageData, OmpAgentCallbacks, OmpAgentHandle, OmpAgentState,
3
3
  import type { ExtensionUiDialogRequest } from '@/shared/types/omp/agent';
4
4
  import type { ApprovalMode } from '@/shared/lib/omp/config/access-mode';
5
5
  import { type ToolResultRecord, useOmpAgentStream } from '@/client/hooks/chat/omp/stream';
6
+ import { stopAgentSession } from '@/shared/lib/chat/omp/abort';
6
7
 
7
8
  /**
8
9
  * Live omp agent bridge for the chamber chat (real mode, MOCK=false). Mirrors
@@ -72,12 +73,14 @@ export function useOmpAgent(sessionId: string | null, callbacks: OmpAgentCallbac
72
73
  .then(res => (res.ok ? res.json() : null))
73
74
  .then((data: {
74
75
  running?: boolean;
76
+ /** Set when the server answered from local flags, not a get_state RPC. */
77
+ busy?: boolean;
75
78
  state?: { isStreaming?: boolean; isPromptRunning?: boolean };
76
79
  pendingUiRequests?: ExtensionUiDialogRequest[];
77
80
  } | null) => {
78
81
  if (cancelled || !data?.running) return;
79
82
  const probe = data.state;
80
- if (probe && (probe.isStreaming || probe.isPromptRunning)) {
83
+ if (data.busy || probe?.isStreaming || probe?.isPromptRunning) {
81
84
  connect(sessionId);
82
85
  callbacksRef.current.onResumeStream?.();
83
86
  }
@@ -132,6 +135,8 @@ export function useOmpAgent(sessionId: string | null, callbacks: OmpAgentCallbac
132
135
  });
133
136
  const body = (await res.json().catch(() => ({}))) as { success?: boolean; error?: string };
134
137
  if (!res.ok || body.error) {
138
+ // `session_busy`: the ack timed out behind a still-running turn, so the
139
+ // prompt may already be accepted — never resend it automatically.
135
140
  setState((prev) => ({ ...prev, isGenerating: false, error: body.error ?? `HTTP ${res.status}` }));
136
141
  return false;
137
142
  }
@@ -209,15 +214,9 @@ export function useOmpAgent(sessionId: string | null, callbacks: OmpAgentCallbac
209
214
  // A user stop supersedes an in-flight interrupt-and-reply: clearing the
210
215
  // guard lets the aborted turn's agent_end reach onAgentEnd (no stuck spinner).
211
216
  interruptPendingRef.current = false;
212
- try {
213
- await fetch(`/api/agent/${encodeURIComponent(sid)}`, {
214
- method: 'POST',
215
- headers: { 'Content-Type': 'application/json' },
216
- body: JSON.stringify({ type: 'abort' }),
217
- });
218
- } catch {
219
- // Abort is best-effort; the event stream surfaces the terminal state.
220
- }
217
+ // Self-escalating: a child that does not stop within the helper's grace
218
+ // period is reset instead, so a wedged session cannot hang the button.
219
+ await stopAgentSession(sid);
221
220
  setState((prev) => ({ ...prev, isGenerating: false }));
222
221
  }, []);
223
222
 
@@ -7,11 +7,12 @@
7
7
  * Throttled revalidation for the session sidebars.
8
8
  *
9
9
  * `omp:session-updated` arrives in bursts while a run is active (agent start,
10
- * JSONL write, spawn adoption, agent end) — and the chat timeline's metadata
11
- * retry loop can emit one every 250ms until the session file carries the user
12
- * turn. Each dispatch would otherwise trigger a full loader revalidation, and
13
- * with two sidebars mounted (desktop + mobile layouts keep theirs alive) a busy
14
- * run could revalidate several times per second.
10
+ * first completed assistant turn, JSONL write, spawn adoption, agent end) — and
11
+ * the chat timeline's metadata retry loop can emit one every 250ms until the
12
+ * session file carries the user turn. Each dispatch would otherwise trigger a
13
+ * full loader revalidation, and with two sidebars mounted (desktop + mobile
14
+ * layouts keep theirs alive) a busy run could revalidate several times per
15
+ * second.
15
16
  *
16
17
  * Implemented as a LEADING + TRAILING throttle, not a trailing debounce:
17
18
  *
@@ -16,7 +16,6 @@ import type { Dispatch, SetStateAction } from 'preact/compat';
16
16
  import type { Attachment, ChatMessageData, OmpAgentHandle, QueuedMessageModel } from '@/shared/types';
17
17
  import type { QueuedMessage } from '@/client/components/workspace/chat-timeline/QueueList';
18
18
  import type { ApprovalMode } from '@/shared/lib/omp/config/access-mode';
19
- import { normalizeNoticePositions } from '@/shared/lib/chat/order';
20
19
 
21
20
  export interface ChatTimelineActionsDeps {
22
21
  inputValue: string;
@@ -211,7 +210,7 @@ export function useChatTimelineActions(deps: ChatTimelineActionsDeps): ChatTimel
211
210
  if (!res.ok) return false;
212
211
  const next = await fetch(`/api/chat/${encodeURIComponent(sessionId)}`).then(r => r.json()).catch(() => null);
213
212
  const messages: ChatMessageData[] = next?.session?.messages ?? [];
214
- if (messages.length > 0) setLocalMessages(normalizeNoticePositions(messages));
213
+ if (messages.length > 0) setLocalMessages(messages);
215
214
  return true;
216
215
  }
217
216
 
@@ -99,6 +99,10 @@ export function useChatTimeline({ folders = [], appSettings = {} }: UseChatTimel
99
99
  // responding (agent_start = first chunk) — the omp JSONL now carries the
100
100
  // user turn, so the sidebar item + real title appear immediately.
101
101
  const metaRefreshedRef = useRef<string | null>(null);
102
+ // Per-run guard for the "first assistant answer landed" sidebar signal
103
+ // (omp-callbacks onMessageEnd): the sidebar refreshes once per run on the
104
+ // completed first assistant turn, not on every assistant segment.
105
+ const firstAssistantRef = useRef(false);
102
106
  // Optimistic user bubble awaiting omp's echo (reconciled by the callbacks).
103
107
  const optimisticUserIdRef = useRef<string | null>(null);
104
108
  // Raw composer text for steer/follow-up echoes (those paths have no bubble).
@@ -193,6 +197,7 @@ export function useChatTimeline({ folders = [], appSettings = {} }: UseChatTimel
193
197
  adoptedSessionIdRef,
194
198
  sessionIdRef,
195
199
  metaRefreshedRef,
200
+ firstAssistantRef,
196
201
  refreshSessionMeta,
197
202
  setLocalMessages,
198
203
  aiPlaceholderIdRef,
@@ -18,7 +18,6 @@ import { streamChatResponse } from '@/client/hooks/chat/stream';
18
18
  import { composeMessageWithTextAttachments, isTextAttachmentFile } from '@/shared/lib/chat/attachments';
19
19
  import { loadAgentNames } from '@/shared/lib/chat/composer/client';
20
20
  import { translateAgentMentions, translateFileMentions } from '@/shared/lib/chat/composer/translate';
21
- import { normalizeNoticePositions } from '@/shared/lib/chat/order';
22
21
  import { createMockStreamCallbacks } from '@/shared/lib/chat/timeline/stream-callbacks';
23
22
  import { PHASE_VERBS } from '@/shared/lib/chat/timeline/tool-phrases';
24
23
  import { formatClock } from '@/shared/lib/format/time';
@@ -213,7 +212,7 @@ export function useChatTimelineSend(deps: ChatTimelineSendDeps): ChatTimelineSen
213
212
  };
214
213
 
215
214
  setLocalMessages(prev => {
216
- const next = normalizeNoticePositions([...prev, newUserMsg, initialAiMsg]);
215
+ const next = [...prev, newUserMsg, initialAiMsg];
217
216
  persistMessages(next.filter(m => m.id !== aiPlaceholderId));
218
217
  return next;
219
218
  });
@@ -312,7 +311,7 @@ export function useChatTimelineSend(deps: ChatTimelineSendDeps): ChatTimelineSen
312
311
  }
313
312
  }
314
313
 
315
- // Mock / chamber-created session: existing Gemini/simulated SSE path.
314
+ // Mock / chamber-created session: existing simulated SSE path.
316
315
  // Cancel any previous stream
317
316
  optimisticUserIdRef.current = null;
318
317
  if (abortControllerRef.current) {
@@ -21,7 +21,6 @@ import type { Dispatch, RefObject, SetStateAction } from 'preact/compat';
21
21
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
22
22
 
23
23
  import type { ChatMessageData } from '@/shared/types';
24
- import { normalizeNoticePositions } from '@/shared/lib/chat/order';
25
24
  import { SESSION_META_RETRY_SCHEDULE_MS } from '@/shared/lib/workspace/refresh-cadence';
26
25
 
27
26
  export interface SessionDataShape {
@@ -210,9 +209,9 @@ export function useSessionLoad(deps: UseSessionLoadDeps) {
210
209
  if (isGeneratingRef.current) {
211
210
  const fetchedIds = fetched.map((m: ChatMessageData) => m.id);
212
211
  const liveTail = localMessagesRef.current.filter(m => !fetchedIds.includes(m.id));
213
- setLocalMessages(normalizeNoticePositions([...fetched, ...liveTail]));
212
+ setLocalMessages([...fetched, ...liveTail]);
214
213
  } else {
215
- setLocalMessages(normalizeNoticePositions(fetched));
214
+ setLocalMessages(fetched);
216
215
  }
217
216
  } else if (!sessionId.startsWith('new-') && !optimisticOwnsTail) {
218
217
  setLocalMessages([]);
@@ -284,7 +283,7 @@ export function useSessionLoad(deps: UseSessionLoadDeps) {
284
283
  setLocalMessages(prev => {
285
284
  // Drop overlap guard: the server window is index-based, so the
286
285
  // fetched slice is strictly older than everything already mounted.
287
- return [...normalizeNoticePositions(older), ...prev];
286
+ return [...older, ...prev];
288
287
  });
289
288
  // Bump the prepend tick so the layout effect below re-runs for THIS
290
289
  // commit — it previously depended only on stable identities and ran
@@ -5,13 +5,26 @@
5
5
 
6
6
  import { useEffect, useState } from 'preact/hooks';
7
7
 
8
- import { isSyntaxReady, onSyntaxReady } from '@/shared/lib/code/highlighter';
8
+ import { isSyntaxReady, onLanguageReady, onSyntaxReady } from '@/shared/lib/code/highlighter';
9
9
 
10
- /** Re-renders once the Shiki highlighter has finished warming up. */
10
+ /**
11
+ * Re-renders when the Shiki highlighter finishes booting **and** whenever a
12
+ * grammar lands. Grammars are fetched on demand — a language nobody renders is
13
+ * never downloaded — so a component may see a `true` value here while the
14
+ * grammar for its own code is still in flight; the second subscription is what
15
+ * swaps the plain-text fallback for tokenized markup once the chunk arrives.
16
+ */
11
17
  export function useSyntaxReady(): boolean {
12
18
  const [ready, setReady] = useState<boolean>(() => isSyntaxReady());
13
19
 
14
- useEffect(() => onSyntaxReady(() => setReady(true)), []);
20
+ useEffect(() => {
21
+ const offBoot = onSyntaxReady(() => setReady(true));
22
+ const offLang = onLanguageReady(() => setReady(true));
23
+ return () => {
24
+ offBoot();
25
+ offLang();
26
+ };
27
+ }, []);
15
28
 
16
29
  return ready;
17
30
  }
@@ -4,11 +4,8 @@ import { bootSyntax } from '@/shared/lib/code/highlighter';
4
4
  import { primeChamberSettings } from '@/shared/lib/settings/client';
5
5
 
6
6
  import '@/client/tailwind.css';
7
- import 'katex/dist/katex.min.css';
8
- import '@fontsource/fira-code/400.css';
9
- import '@fontsource/fira-code/500.css';
10
- import '@fontsource/fira-code/600.css';
11
- import '@fontsource/fira-code/700.css';
7
+ import '@/shared/lib/markdown/katex-fonts.css';
8
+ import '@/shared/lib/markdown/fira-code-fonts.css';
12
9
 
13
10
  const root = document.getElementById('app');
14
11
  const bootstrap = window.__OMP_BOOTSTRAP__;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Browser replacement for `elkjs`, aliased in `rsbuild.config.ts`.
8
+ *
9
+ * Mermaid 12 changed the global `layout` default from `dagre` to `elk`, so every
10
+ * flowchart pulled in `elkjs/lib/elk.bundled.js` whether the diagram asked for
11
+ * ELK or not — the single largest artifact in the client build (~1.4 MB raw,
12
+ * 434 kB gzip). `src/shared/lib/markdown/mermaid.ts` pins `layout: 'dagre'`,
13
+ * which is what makes this stub safe: with dagre selected explicitly, mermaid
14
+ * never calls into `runElkLayoutCore`, the only code path that constructs an ELK
15
+ * instance (verified across 20 diagram types, byte-identical results to a build
16
+ * with the real elkjs).
17
+ *
18
+ * `layout: 'elk'` is still a valid *request* — a diagram using the
19
+ * `flowchart-elk` directive, or a syntax flagged `@config{layout: "elk"}`. Those
20
+ * now fail loudly here instead of silently rendering with a different layout
21
+ * than asked for. Throwing is deliberate: mermaid's `render()` propagates the
22
+ * error to the caller, and `mermaid.ts` already renders a fallback block on
23
+ * failure, so a mis-specified diagram degrades to a visible message rather than
24
+ * a wrong diagram.
25
+ */
26
+
27
+ function unavailable(): never {
28
+ throw new Error('elkjs is not bundled in this build; use the default dagre layout');
29
+ }
30
+
31
+ export class ELK {}
32
+
33
+ export default new Proxy(ELK as unknown as object, {
34
+ construct: unavailable,
35
+ apply: unavailable,
36
+ get: unavailable,
37
+ }) as unknown as typeof ELK;
@@ -285,6 +285,16 @@
285
285
  [data-theme="one-dark-pro-soft"] .shiki span { color: var(--shiki-dark); }
286
286
  .prose-content .katex-display { overflow-x: auto; overflow-y: hidden; }
287
287
 
288
+ /* Math placeholder: `marked.ts` emits a `.math-pending` span and KaTeX is
289
+ * fetched on demand (~522 kB async chunk), so the span is empty for one frame.
290
+ * Reserve display-math height so the message does not jump when the formula
291
+ * lands, and keep invalid/queued source faintly visible rather than blank. */
292
+ .prose-content .math-pending::before {
293
+ content: '…';
294
+ opacity: 0.35;
295
+ }
296
+ .prose-content p:has(> .math-pending[data-math-display='1']) { min-height: 2.5rem; }
297
+
288
298
  /* Streaming placeholder: remend turns a half-received [text](url into this
289
299
  * span; it becomes a real link once the URL arrives in a later frame. */
290
300
  .prose-content .md-incomplete-link {
@@ -1,6 +1,26 @@
1
1
  import { Elysia } from 'elysia';
2
2
  import { apiRoutes } from '@/server/routes';
3
3
  import { ssrRoutes } from '@/server/plugins/ssr';
4
+ import { getDatabasePath } from '@/server/db.server';
5
+ import { fetchOmpRegistrySnapshot } from '@/server/lib/models/provider-registry.server';
6
+ import { ompStartupError, ompStartupLogLines } from '@/server/lib/omp/core/startup';
7
+ import { isMockMode } from '@/server/mock.server';
8
+
9
+ // Refuse to run without a resolvable omp binary — before the listener opens and
10
+ // before the first request can reach a route that shells out to it. The database
11
+ // is opened lazily by getDb(), so this exits without touching it.
12
+ const startupError = ompStartupError();
13
+ if (startupError) {
14
+ console.error(startupError);
15
+ process.exit(1);
16
+ }
17
+
18
+ // Which omp the chamber drives, which ~/.omp tree it reads and which SQLite file
19
+ // it writes — the paths every session/agent diagnostic traces back to. The data
20
+ // mode comes first because it changes what every later line means.
21
+ console.log(`[ompchamber] mock mode: ${isMockMode() ? 'true (demo presets)' : 'false (real omp data)'}`);
22
+ for (const line of ompStartupLogLines()) console.log(`[ompchamber] ${line}`);
23
+ console.log(`[ompchamber] db: ${await getDatabasePath()}`);
4
24
 
5
25
  const port = Number(Bun.env.PORT) || 3000;
6
26
  const host = Bun.env.HOST || 'localhost';
@@ -9,8 +29,41 @@ const app = new Elysia().use(apiRoutes).use(ssrRoutes);
9
29
 
10
30
  await app.listen({ port, hostname: host });
11
31
 
32
+ // The listener is already accepting requests here, so the probe only postpones
33
+ // this last banner line, never startup. Printing it after the probe keeps the
34
+ // one line that matters — the URL — at the bottom of the log.
35
+ await logDetectedRegistry();
36
+
12
37
  console.log(`[ompchamber] listening on http://${host}:${port}`);
13
38
 
39
+ /**
40
+ * What omp reports it can actually run. A live RPC round-trip: a cold utility
41
+ * process takes seconds, which is why it runs after the listener is up. Side
42
+ * benefit — it warms the shared utility process the model picker drives, so the
43
+ * first /api/models call does not pay the spawn.
44
+ */
45
+ async function logDetectedRegistry(): Promise<void> {
46
+ if (isMockMode()) {
47
+ console.log('[ompchamber] providers: n/a (MOCK mode)');
48
+ console.log('[ompchamber] models: n/a (MOCK mode)');
49
+ return;
50
+ }
51
+ try {
52
+ const { providers, models } = await fetchOmpRegistrySnapshot();
53
+ // Count providers that actually serve a model — the distinct `provider`
54
+ // values in the model list, i.e. the same set GET /api/models groups by.
55
+ // omp's login catalog lists every sign-in offer it supports (75 here, 73 of
56
+ // them without credentials); counting those would report a provider set the
57
+ // chamber cannot use. The catalog size is kept as context.
58
+ const servingProviders = new Set(models.map((model) => model.provider)).size;
59
+ console.log(`[ompchamber] providers: ${servingProviders} detected (${providers.length} known to omp)`);
60
+ console.log(`[ompchamber] models: ${models.length} detected`);
61
+ } catch (error) {
62
+ const message = error instanceof Error ? error.message : String(error);
63
+ console.log(`[ompchamber] providers/models: detection failed — ${message}`);
64
+ }
65
+ }
66
+
14
67
  for (const signal of ['SIGTERM', 'SIGINT'] as const) {
15
68
  process.once(signal, () => {
16
69
  void app.stop();
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Git-ignore resolution for the Files panel.
8
+ *
9
+ * `git check-ignore` is the only authority that evaluates every rule source in
10
+ * one pass — per-directory `.gitignore`, `.git/info/exclude`, and the global
11
+ * `core.excludesFile` (`~/.gitignore`) — so the panel dims exactly what git
12
+ * would refuse to track. Paths are fed NUL-separated over stdin (`--stdin -z`)
13
+ * so a directory listing of any size cannot overflow argv, and the verbatim
14
+ * NUL-separated reply maps straight back onto those same paths.
15
+ *
16
+ * Paths must be relative to `dir` and must exist on disk: git resolves
17
+ * directory-only patterns (`build/`) by stat-ing the path.
18
+ */
19
+ export async function collectIgnoredPaths(dir: string, relPaths: string[]): Promise<Set<string>> {
20
+ if (relPaths.length === 0) return new Set();
21
+
22
+ try {
23
+ const proc = Bun.spawn({
24
+ cmd: ['git', 'check-ignore', '--stdin', '-z'],
25
+ cwd: dir,
26
+ stdin: 'pipe',
27
+ stdout: 'pipe',
28
+ stderr: 'ignore',
29
+ timeout: 10_000,
30
+ });
31
+
32
+ // Drain stdout while the listing is written: a directory big enough to
33
+ // fill both pipe buffers would otherwise deadlock child against parent.
34
+ const stdout = new Response(proc.stdout).text();
35
+ proc.stdin.write(relPaths.map(rel => `${rel}\0`).join(''));
36
+ // FileSink.end() types as `number | Promise<number>` — EPIPE (child gone)
37
+ // must not surface as an unhandled rejection.
38
+ void Promise.resolve(proc.stdin.end()).catch(() => {});
39
+
40
+ const [text, exitCode] = await Promise.all([stdout, proc.exited]);
41
+ // 0 = at least one path matched, 1 = none, 128 = no repository. Only 0 can
42
+ // carry a match list.
43
+ if (exitCode !== 0) return new Set();
44
+
45
+ return new Set(text.split('\0').filter(Boolean));
46
+ } catch {
47
+ return new Set(); // git missing / spawn failed — nothing is dimmed
48
+ }
49
+ }
@@ -23,12 +23,54 @@ export const PROVIDERS_SETTINGS_KEY = 'omp_providers_config';
23
23
 
24
24
  const RPC_CACHE_TTL_MS = 60_000;
25
25
 
26
- interface LoginProvider {
26
+ /** Minimal shape of a get_login_providers entry, as the chamber reads it. */
27
+ export interface DetectedLoginProvider {
27
28
  id: string;
28
29
  name: string;
29
30
  authenticated: boolean;
30
31
  }
31
32
 
33
+ function isDetectedLoginProvider(provider: unknown): provider is DetectedLoginProvider {
34
+ return (
35
+ typeof provider === 'object' && provider !== null
36
+ && typeof (provider as { id?: unknown }).id === 'string'
37
+ && typeof (provider as { name?: unknown }).name === 'string'
38
+ && typeof (provider as { authenticated?: unknown }).authenticated === 'boolean'
39
+ );
40
+ }
41
+
42
+ function isOmpModel(model: unknown): model is OmpModel {
43
+ return (
44
+ typeof model === 'object' && model !== null
45
+ && typeof (model as OmpModel).id === 'string'
46
+ && typeof (model as OmpModel).provider === 'string'
47
+ );
48
+ }
49
+
50
+ export interface OmpRegistrySnapshot {
51
+ /** Login providers omp reports (authenticated or not). */
52
+ providers: DetectedLoginProvider[];
53
+ /** Every model omp currently serves. */
54
+ models: OmpModel[];
55
+ }
56
+
57
+ /**
58
+ * Raw omp registry probe — the two commands that define "detected" for the
59
+ * chamber: the login providers omp knows and the models they serve. Parsed here
60
+ * once so the provider settings page and the startup banner count the same set.
61
+ * Throws when the utility RPC is unreachable; each caller degrades its own way.
62
+ */
63
+ export async function fetchOmpRegistrySnapshot(): Promise<OmpRegistrySnapshot> {
64
+ const [loginResponse, modelsResponse] = await Promise.all([
65
+ runUtilityCommand<{ providers?: unknown }>({ type: 'get_login_providers' }, 30_000),
66
+ runUtilityCommand<{ models?: unknown }>({ type: 'get_available_models' }, 60_000),
67
+ ]);
68
+ return {
69
+ providers: Array.isArray(loginResponse.providers) ? loginResponse.providers.filter(isDetectedLoginProvider) : [],
70
+ models: Array.isArray(modelsResponse.models) ? modelsResponse.models.filter(isOmpModel) : [],
71
+ };
72
+ }
73
+
32
74
  /**
33
75
  * Provider registry straight from the omp agent: authenticated login providers
34
76
  * (omp auth) plus their registered models (get_available_models). This is the
@@ -38,25 +80,7 @@ async function loadRpcProviderItems(): Promise<ProviderItem[]> {
38
80
  const cached = globalThis.__ompChamberProvidersRpcCache;
39
81
  if (cached && cached.expiresAt > Date.now()) return cached.data;
40
82
  try {
41
- const [loginResponse, modelsResponse] = await Promise.all([
42
- runUtilityCommand<{ providers?: unknown }>({ type: 'get_login_providers' }, 30_000),
43
- runUtilityCommand<{ models?: unknown }>({ type: 'get_available_models' }, 60_000),
44
- ]);
45
- const loginProviders = Array.isArray(loginResponse.providers)
46
- ? loginResponse.providers.filter((provider): provider is LoginProvider => (
47
- typeof provider === 'object' && provider !== null
48
- && typeof (provider as { id?: unknown }).id === 'string'
49
- && typeof (provider as { name?: unknown }).name === 'string'
50
- && typeof (provider as { authenticated?: unknown }).authenticated === 'boolean'
51
- ))
52
- : [];
53
- const available = Array.isArray(modelsResponse.models)
54
- ? modelsResponse.models.filter((model): model is OmpModel => (
55
- typeof model === 'object' && model !== null
56
- && typeof (model as OmpModel).id === 'string'
57
- && typeof (model as OmpModel).provider === 'string'
58
- ))
59
- : [];
83
+ const { providers: loginProviders, models: available } = await fetchOmpRegistrySnapshot();
60
84
  const disabled = await readDisabledProviders().catch(() => new Set<string>());
61
85
 
62
86
  const items = loginProviders.map((provider) => {
@@ -20,6 +20,7 @@ let cachedBin: string | null = null;
20
20
  let binMissAt = 0;
21
21
 
22
22
  const BIN_NAME = process.platform === 'win32' ? 'omp.exe' : 'omp';
23
+
23
24
  // Only successes are cached for the process lifetime. omp may be installed (or
24
25
  // PATH repaired) while the server runs; a permanently cached "not found" would
25
26
  // keep the UI reporting a missing binary until restart.
@@ -32,7 +33,7 @@ export function invalidateOmpCliCache(): void {
32
33
  }
33
34
 
34
35
  function probeOmpBin(): string | null {
35
- const override = Bun.env.OMP_WEB_OMP_BIN;
36
+ const override = Bun.env.OMPCHAMBER_OMP_BIN;
36
37
  if (override) return existsSync(override) ? override : null;
37
38
 
38
39
  const onPath = Bun.which(BIN_NAME);
@@ -49,7 +50,7 @@ function probeOmpBin(): string | null {
49
50
  return Bun.which(BIN_NAME, { PATH: fallbackPath });
50
51
  }
51
52
 
52
- /** Resolve the omp binary: OMP_WEB_OMP_BIN override, then PATH lookup. Returns
53
+ /** Resolve the omp binary: OMPCHAMBER_OMP_BIN override, then PATH lookup. Returns
53
54
  * null when omp is not installed. A hit is cached for the process lifetime; a
54
55
  * miss is re-probed after MISS_TTL_MS. */
55
56
  export function resolveOmpBin(): string | null {