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
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { afterAll, describe, expect, test } from 'bun:test';
7
+ import fs from 'fs';
8
+ import { join } from 'path';
9
+
10
+ import { loadSessionMessages } from '@/server/lib/omp/session/messages';
11
+ import { isNoticeRow } from '@/shared/lib/chat/notice-row';
12
+
13
+ const tempDirs: string[] = [];
14
+
15
+ /** Write a session JSONL whose lines are exactly the given records, in order. */
16
+ async function writeSession(lines: unknown[]): Promise<string> {
17
+ const dir = await fs.promises.mkdtemp('omp-messages-');
18
+ tempDirs.push(dir);
19
+ const file = join(dir, 'session.jsonl');
20
+ await Bun.write(file, `${lines.map((line) => JSON.stringify(line)).join('\n')}\n`);
21
+ return file;
22
+ }
23
+
24
+ afterAll(() => {
25
+ for (const dir of tempDirs) fs.rmSync(dir, { recursive: true, force: true });
26
+ });
27
+
28
+ const at = (second: number) => `2026-09-21T03:00:${String(second).padStart(2, '0')}.000Z`;
29
+
30
+ /** omp records harness notices as their own `custom_message` entry. */
31
+ const notice = (id: string, text: string, second: number) => ({
32
+ type: 'custom_message',
33
+ id,
34
+ customType: 'ultrathink-notice',
35
+ timestamp: at(second),
36
+ content: [{ type: 'text', text: `<system-notice>${text}</system-notice>` }],
37
+ });
38
+
39
+ const turn = (id: string, role: 'user' | 'assistant', text: string, second: number) => ({
40
+ type: 'message',
41
+ id,
42
+ timestamp: at(second),
43
+ message: { role, content: [{ type: 'text', text }] },
44
+ });
45
+
46
+ describe('loadSessionMessages ordering', () => {
47
+ test('returns entries in file order, leaving a notice before its following user turn', async () => {
48
+ const file = await writeSession([
49
+ notice('n1', 'Background job finished.', 0),
50
+ turn('u1', 'user', 'halo', 1),
51
+ turn('a1', 'assistant', 'hai', 2),
52
+ ]);
53
+
54
+ const messages = await loadSessionMessages(file);
55
+
56
+ expect(messages.map((m) => m.id)).toEqual(['n1', 'u1', 'a1']);
57
+ expect(messages[0].notice).toBe('Background job finished.');
58
+ expect(messages[1]).toMatchObject({ role: 'user', content: 'halo' });
59
+ expect(messages[2]).toMatchObject({ role: 'ai', content: 'hai' });
60
+ });
61
+
62
+ test('preserves a notice that sits between two turns', async () => {
63
+ const file = await writeSession([
64
+ turn('u1', 'user', 'satu', 0),
65
+ turn('a1', 'assistant', 'dua', 1),
66
+ notice('n1', 'Job done.', 2),
67
+ turn('u2', 'user', 'tiga', 3),
68
+ turn('a2', 'assistant', 'empat', 4),
69
+ ]);
70
+
71
+ expect((await loadSessionMessages(file)).map((m) => m.id)).toEqual(['u1', 'a1', 'n1', 'u2', 'a2']);
72
+ });
73
+ });
74
+
75
+ describe('loadSessionMessages reminder classification', () => {
76
+ const blocks = (id: string, texts: string[], second: number) => ({
77
+ type: 'message',
78
+ id,
79
+ timestamp: at(second),
80
+ message: { role: 'assistant', content: texts.map((text) => ({ type: 'text', text })), usage: { input: 1, output: 2 } },
81
+ });
82
+
83
+ test('prose that quotes the tag stays the answer', async () => {
84
+ const prose = 'Chip shows the peeled tag (`system-reminder`).\n\nExample: <system-reminder>…</system-reminder> then free text.';
85
+ const file = await writeSession([blocks('a1', [prose], 0)]);
86
+
87
+ const [message] = await loadSessionMessages(file);
88
+
89
+ expect(message.notice).toBeUndefined();
90
+ expect(message.content).toBe(prose);
91
+ expect(isNoticeRow(message)).toBe(false);
92
+ });
93
+
94
+ test('a text block that IS the reminder envelope becomes a notice', async () => {
95
+ const file = await writeSession([blocks('a1', ['<system-reminder>\n10 todo items still open.\n</system-reminder>'], 0)]);
96
+
97
+ const [message] = await loadSessionMessages(file);
98
+
99
+ expect(message.content).toBe('');
100
+ expect(message.notice).toBe('<system-reminder>\n10 todo items still open.\n</system-reminder>');
101
+ expect(isNoticeRow(message)).toBe(true);
102
+ });
103
+
104
+ test('an envelope and prose in one turn split between notice and content', async () => {
105
+ const file = await writeSession([blocks('a1', ['Jawaban akhir.', '<system-reminder>2 items open</system-reminder>'], 0)]);
106
+
107
+ const [message] = await loadSessionMessages(file);
108
+
109
+ expect(message.content).toBe('Jawaban akhir.');
110
+ expect(message.notice).toBe('<system-reminder>2 items open</system-reminder>');
111
+ });
112
+ });
@@ -21,7 +21,6 @@
21
21
  */
22
22
 
23
23
  import { parseJsonlLenient } from '@/shared/lib/omp/session/jsonl';
24
- import { normalizeNoticePositions } from '@/shared/lib/chat/order';
25
24
  import { normalizeThinkingLevel } from '@/shared/lib/models/thinking-levels';
26
25
  import type { ChatMessageData } from '@/shared/types/chat';
27
26
  import { collectToolOutputs, noticeFromCustomMessage, toChatMessage, type SequenceState } from '@/shared/lib/omp/session/messages-map';
@@ -53,9 +52,11 @@ function skillUserMessageFromRecord(record: Record<string, unknown>): ChatMessag
53
52
  }
54
53
 
55
54
  /**
56
- * Load a session file and return the chat timeline in chronological order.
57
- * Assistant messages carry thinking accordion + tool calls (with outputs
58
- * paired from their toolResult entries); tool plumbing rows are folded in.
55
+ * Load a session file and return the chat timeline in **file order** — the
56
+ * order omp wrote the entries, which is the session's chronology. The API
57
+ * response is a faithful projection of the JSONL: nothing here reorders rows,
58
+ * and no display-side reordering is applied anywhere either (a notice row
59
+ * renders exactly where omp wrote it).
59
60
  */
60
61
  export async function loadSessionMessages(filePath: string): Promise<ChatMessageData[]> {
61
62
  try {
@@ -120,7 +121,7 @@ export async function loadSessionMessages(filePath: string): Promise<ChatMessage
120
121
  }
121
122
  state.messages.push(mapped);
122
123
  }
123
- return normalizeNoticePositions(state.messages);
124
+ return state.messages;
124
125
  }
125
126
 
126
127
  /** Derive the display title from the JSONL header/title slot (cheap read). */
@@ -5,29 +5,23 @@
5
5
 
6
6
  /**
7
7
  * Utility to determine whether the application is running in MOCK mode.
8
- *
8
+ *
9
9
  * - When MOCK=true (or MOCK=1): all data uses mock / simulated presets (demo chat traces, mock token analytics, mock settings presets).
10
10
  * - When MOCK=false (or MOCK=0): all data uses real data (real SQLite persistence, real token calculations, real workspace state).
11
+ * - Unset (or any unrecognized value) defaults to REAL data — the chamber is a
12
+ * diagnostic console for a live omp install, so demo presets must be opted
13
+ * into explicitly rather than inherited by omission.
11
14
  */
12
15
  export function isMockMode(): boolean {
13
16
  const envVal = (Bun.env.MOCK || '').trim().toLowerCase();
14
-
15
- if (envVal === 'false' || envVal === '0' || envVal === 'off' || envVal === 'no') {
16
- return false;
17
- }
18
-
19
- if (envVal === 'true' || envVal === '1' || envVal === 'on' || envVal === 'yes') {
20
- return true;
21
- }
22
17
 
23
- // Default to true for sandbox / preview demo mode if not explicitly set to false
24
- return true;
18
+ return envVal === 'true' || envVal === '1' || envVal === 'on' || envVal === 'yes';
25
19
  }
26
20
 
27
21
  export function getMockModeInfo(): { isMock: boolean; rawEnv: string } {
28
22
  const isMock = isMockMode();
29
23
  return {
30
24
  isMock,
31
- rawEnv: Bun.env.MOCK || (isMock ? 'true (default)' : 'false'),
25
+ rawEnv: Bun.env.MOCK || (isMock ? 'true' : 'false (default)'),
32
26
  };
33
27
  }
@@ -1,7 +1,7 @@
1
1
  import { json } from '@/server/lib/remix-compat';
2
2
  import type { ActionFunctionArgs, LoaderFunctionArgs } from '@/server/lib/remix-compat';
3
3
  import { resolveSessionPathOr404 } from '@/server/lib/omp/session/locator';
4
- import { WebRpcError, getRpcSession, resolveSpawnCwd, startRpcSession } from '@/server/lib/omp/rpc/manager';
4
+ import { WebRpcError, getRpcSession, resolveSpawnCwd, startRpcSession, type AgentSessionWrapper } from '@/server/lib/omp/rpc/manager';
5
5
  import { getSpawnApprovalMode, reconcileSpawnApprovalMode } from '@/server/lib/omp/rpc/session-registry';
6
6
  import { isApprovalMode } from '@/shared/lib/omp/config/access-mode';
7
7
  import { loadPersistedAccessMode } from '@/shared/lib/omp/config/access-mode.server';
@@ -53,29 +53,47 @@ export async function sendCommand({ params, request }: ActionFunctionArgs) {
53
53
  }
54
54
  }
55
55
 
56
+ /** RPC-free view of a busy session. `state` carries only the two flags the
57
+ * attach probe reads; the full snapshot resumes once the turn settles. */
58
+ function busySessionPayload(session: AgentSessionWrapper) {
59
+ return {
60
+ running: true,
61
+ busy: true,
62
+ state: { isStreaming: session.streaming, isPromptRunning: session.promptRunning },
63
+ pendingUiRequests: session.getPendingUiDialogs(),
64
+ };
65
+ }
66
+
56
67
  // GET /api/agent/:sessionId — current agent state (running set + live state).
57
68
  export async function getAgentState({ params }: LoaderFunctionArgs) {
58
69
  const { sessionId } = params;
59
70
  if (!sessionId) return json({ error: 'session id is required' }, { status: 400 });
60
71
 
72
+ const session = getRpcSession(sessionId);
73
+ if (!session || !session.isAlive()) {
74
+ return json({ running: false });
75
+ }
76
+
77
+ // A busy session answers from local flags: its `get_state` would queue behind
78
+ // the running turn (omp runs RPC handlers one at a time), and a timeout there
79
+ // is no reason to reset a session that is demonstrably working — subagents
80
+ // included. These flags are all the client needs to reattach its stream.
81
+ if (session.isBusy()) return json(busySessionPayload(session));
82
+
61
83
  try {
62
- const session = getRpcSession(sessionId);
63
- if (!session || !session.isAlive()) {
64
- return json({ running: false });
84
+ const state = await session.send({ type: 'get_state' });
85
+ // Dialogs omp is still blocked on: a client that reloaded mid-ask has no
86
+ // other way to learn the request id it must answer, and omp never
87
+ // re-emits the frame.
88
+ return json({ running: true, state, pendingUiRequests: session.getPendingUiDialogs() });
89
+ } catch (error) {
90
+ if (error instanceof WebRpcError && error.code === 'session_unresponsive') {
91
+ return json({ running: false, recovered: true });
65
92
  }
66
- try {
67
- const state = await session.send({ type: 'get_state' });
68
- // Dialogs omp is still blocked on: a client that reloaded mid-ask has no
69
- // other way to learn the request id it must answer, and omp never
70
- // re-emits the frame.
71
- return json({ running: true, state, pendingUiRequests: session.getPendingUiDialogs() });
72
- } catch (error) {
73
- if (error instanceof WebRpcError && error.code === 'session_unresponsive') {
74
- return json({ running: false, recovered: true });
75
- }
76
- throw error;
93
+ // A turn started between the check above and the RPC: report busy, not dead.
94
+ if (error instanceof WebRpcError && error.code === 'session_busy') {
95
+ return json(busySessionPayload(session));
77
96
  }
78
- } catch (error) {
79
97
  return rpcErrorResponse(error);
80
98
  }
81
99
  }
@@ -1,6 +1,6 @@
1
1
  import type { ActionFunctionArgs, LoaderFunctionArgs } from '@/server/lib/remix-compat';
2
2
  import { isMockMode } from '@/server/mock.server';
3
- import { handleGeminiStreaming, handleSimulatedStreaming } from '@/shared/lib/chat/stream-service';
3
+ import { handleSimulatedStreaming } from '@/shared/lib/chat/stream-service';
4
4
  import { createSseStream } from '@/server/lib/sse';
5
5
 
6
6
  export async function loader({ request }: LoaderFunctionArgs) {
@@ -31,7 +31,7 @@ async function handleStreamingRequest(request: Request) {
31
31
  }
32
32
 
33
33
  // Real mode routes through the omp agent RPC bridge (POST /api/agent/:id +
34
- // SSE /api/agent/:id/events). This Gemini/simulated stream is the MOCK-only
34
+ // SSE /api/agent/:id/events). This simulated stream is the MOCK-only
35
35
  // path — refuse it in real mode so the two never conflict.
36
36
  if (!isMockMode()) {
37
37
  return new Response(JSON.stringify({ error: 'Streaming is handled by the omp agent bridge in real mode' }), {
@@ -68,34 +68,17 @@ async function handleStreamingRequest(request: Request) {
68
68
  model: modelName,
69
69
  });
70
70
 
71
- const apiKey = Bun.env.GEMINI_API_KEY;
72
- const mock = isMockMode();
73
-
74
- // If real Gemini API is available and not forced mock mode
75
- if (apiKey && !mock) {
76
- await handleGeminiStreaming({
77
- apiKey,
78
- prompt,
79
- modelName,
80
- sessionId,
81
- messageId,
82
- timeStr,
83
- sendEvent,
84
- isAborted: () => isAborted,
85
- });
86
- } else {
87
- // Fallback / simulated intelligent streaming for OMPChamber
88
- await handleSimulatedStreaming({
89
- prompt,
90
- modelName,
91
- workspaceName,
92
- sessionId,
93
- messageId,
94
- timeStr,
95
- sendEvent,
96
- isAborted: () => isAborted,
97
- });
98
- }
71
+ // Simulated intelligent streaming — the MOCK-only path for OMPChamber.
72
+ await handleSimulatedStreaming({
73
+ prompt,
74
+ modelName,
75
+ workspaceName,
76
+ sessionId,
77
+ messageId,
78
+ timeStr,
79
+ sendEvent,
80
+ isAborted: () => isAborted,
81
+ });
99
82
  } catch (err: any) {
100
83
  if (!isAborted) {
101
84
  sendEvent('error', { error: err?.message || 'Streaming execution error' });
@@ -11,6 +11,7 @@ import fs from 'fs';
11
11
  import path from 'path';
12
12
  import { isMockMode } from '@/server/mock.server';
13
13
  import { getDefaultFsRoot, resolveRoot, resolveWithinRoot } from '@/server/lib/fs/root';
14
+ import { collectIgnoredPaths } from '@/server/lib/fs/git-ignore';
14
15
 
15
16
  /**
16
17
  * GET /api/fs/browse?path=<abs> — list subdirectories of a folder for the
@@ -92,19 +93,26 @@ const NOISE_DIRS: Record<string, true> = { node_modules: true, '.git': true, dis
92
93
  // fetch them on demand — directories are NOT recursed here (keeps the initial
93
94
  // payload tiny for deep workspaces). Hidden (dot-prefixed) entries are part of
94
95
  // the listing: lazy children keep `.github`, `.config`, `.env`, … cheap.
96
+ // Entries git would refuse to track carry `ignored: true` so the panel can dim
97
+ // them; that is one `git check-ignore` per listing, never one per entry.
95
98
  async function listEntries(dirPath: string, rootPath: string): Promise<any[]> {
96
- const entries = (await fs.promises.readdir(dirPath))
97
- .filter(child => !NOISE_DIRS[child]);
99
+ const listed = (await fs.promises.readdir(dirPath))
100
+ .filter(child => !NOISE_DIRS[child])
101
+ .map(child => {
102
+ const full = path.join(dirPath, child);
103
+ return { child, full, rel: path.relative(rootPath, full) };
104
+ });
98
105
 
99
- const mapped = await Promise.all(entries.map(async child => {
100
- const full = path.join(dirPath, child);
106
+ const ignored = await collectIgnoredPaths(rootPath, listed.map(entry => entry.rel));
107
+
108
+ const mapped = await Promise.all(listed.map(async ({ child, full, rel }) => {
101
109
  try {
102
110
  const st = await Bun.file(full).stat();
103
- const rel = path.relative(rootPath, full);
111
+ const isIgnored = ignored.has(rel);
104
112
  if (st.isDirectory()) {
105
- return { id: rel, name: child, type: 'folder', path: rel, children: null, is_expanded: 0 };
113
+ return { id: rel, name: child, type: 'folder', path: rel, children: null, is_expanded: 0, ignored: isIgnored };
106
114
  }
107
- return { id: rel, name: child, type: 'file', path: rel };
115
+ return { id: rel, name: child, type: 'file', path: rel, ignored: isIgnored };
108
116
  } catch {
109
117
  return null; // unreadable entry / broken symlink — skip
110
118
  }
@@ -274,7 +274,11 @@ export async function action({ request }: ActionFunctionArgs) {
274
274
  const body = await request.json();
275
275
 
276
276
  const catalog = await readSettingsJson<unknown>(db, MODELS_CATALOG_KEY, null);
277
- let models: AIModelOption[] = Array.isArray(catalog) ? catalog : INITIAL_MODELS_CATALOG;
277
+ // The shipped demo catalog is MOCK-only: a real install starts from an empty
278
+ // list, so a demo model can never be persisted into its stored catalog.
279
+ let models: AIModelOption[] = Array.isArray(catalog)
280
+ ? catalog
281
+ : (isMockMode() ? INITIAL_MODELS_CATALOG : []);
278
282
 
279
283
  const { actionType, modelId, provider, thinkingLevel, model, models: newModels } = body;
280
284
 
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { describe, expect, test } from 'bun:test';
7
+
8
+ import {
9
+ isNoticeRow,
10
+ messageAnswerText,
11
+ noticeIsAssistantText,
12
+ reminderPartIndex,
13
+ } from '@/shared/lib/chat/notice-row';
14
+ import type { ChatMessageData } from '@/shared/types';
15
+
16
+ const row = (over: Partial<ChatMessageData>): ChatMessageData => ({
17
+ id: 'n1',
18
+ role: 'ai',
19
+ content: '',
20
+ ...over,
21
+ });
22
+
23
+ const answer = { model: 'deepseek-v4', usage: { totalTokens: 150_640 }, durationMs: 5_574 };
24
+
25
+ describe('reminderPartIndex', () => {
26
+ test('finds a text block that is a reminder envelope', () => {
27
+ expect(reminderPartIndex(['<system-reminder>\n10 todos open\n</system-reminder>'])).toBe(0);
28
+ });
29
+
30
+ test('ignores a tag quoted inside prose', () => {
31
+ expect(reminderPartIndex(['Chip shows the peeled tag (`system-reminder`).'])).toBeUndefined();
32
+ expect(reminderPartIndex(['Example: <system-reminder>…</system-reminder> then free text'])).toBeUndefined();
33
+ expect(
34
+ reminderPartIndex(['A wrapper is `<a>1</a>`; see <system-reminder> handling below']),
35
+ ).toBeUndefined();
36
+ });
37
+
38
+ test('ignores an unclosed or mismatched tag pair', () => {
39
+ expect(reminderPartIndex(['<system-reminder>\nno closing tag'])).toBeUndefined();
40
+ expect(reminderPartIndex(['<system-reminder><b>10 todos</system-reminder>'])).toBeUndefined();
41
+ });
42
+
43
+ test('picks the envelope out of a multi-block turn', () => {
44
+ expect(reminderPartIndex(['prose first', '<system-reminder>note</system-reminder>'])).toBe(1);
45
+ });
46
+ });
47
+
48
+ describe('notice row classification', () => {
49
+ test('a bare notice row is a notice card', () => {
50
+ const notice = row({ notice: 'Late LSP diagnostics arrived after the edit returned.' });
51
+ expect(noticeIsAssistantText(notice)).toBe(false);
52
+ expect(isNoticeRow(notice)).toBe(true);
53
+ expect(messageAnswerText(notice)).toBe('');
54
+ });
55
+
56
+ test('a notice row that kept the turn metadata is the answer', () => {
57
+ const text = 'Implementasi selesai.\n\n## Aturan';
58
+ const diverted = row({ notice: text, ...answer });
59
+ expect(noticeIsAssistantText(diverted)).toBe(true);
60
+ expect(isNoticeRow(diverted)).toBe(false);
61
+ expect(messageAnswerText(diverted)).toBe(text);
62
+ });
63
+
64
+ test('a wrapped reminder stays a notice card even with turn metadata', () => {
65
+ const reminder = row({ notice: '<system-reminder>10 todo items still open.</system-reminder>', ...answer });
66
+ expect(noticeIsAssistantText(reminder)).toBe(false);
67
+ expect(isNoticeRow(reminder)).toBe(true);
68
+ });
69
+
70
+ test('a task result stays a notice card', () => {
71
+ const task = row({ notice: '<task-result id="Job" status="completed">done</task-result>', ...answer });
72
+ expect(isNoticeRow(task)).toBe(true);
73
+ });
74
+
75
+ test('the live thinking-level stamp alone does not make a notice an answer', () => {
76
+ expect(isNoticeRow(row({ notice: 'Job done.', thinkingLevel: 'high' }))).toBe(true);
77
+ });
78
+
79
+ test('an ordinary answer row is unaffected', () => {
80
+ const plain = row({ content: 'Halo', ...answer });
81
+ expect(isNoticeRow(plain)).toBe(false);
82
+ expect(messageAnswerText(plain)).toBe('Halo');
83
+ });
84
+
85
+ test('an empty notice is not a notice row', () => {
86
+ expect(isNoticeRow(row({ notice: ' ' }))).toBe(false);
87
+ expect(isNoticeRow(row({}))).toBe(false);
88
+ });
89
+ });
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * `notice` rows — and the one case where the field carries the ANSWER.
8
+ *
9
+ * A real notice row holds nothing but the notice: omp writes harness reminders,
10
+ * task results and late diagnostics as their own entry, so those rows have no
11
+ * model, no usage and no thinking. When the assistant's answer itself ends up
12
+ * in `notice` (omp diverted the turn's text block), the row keeps the turn's
13
+ * own metadata — model/provider/usage/duration/thinking/toolCalls — and is
14
+ * therefore an answer that must render as text, never as a System Notice card.
15
+ *
16
+ * The decision lives here once so every consumer agrees: the timeline row
17
+ * (`MessageItem`), the boundary rules of the run footer, and the live-stream
18
+ * callbacks.
19
+ */
20
+
21
+ import type { ChatMessageData } from '@/shared/types';
22
+ import { isReminderTag, unwrapXmlEnvelope } from '@/shared/lib/chat/xml-envelope';
23
+
24
+ /** Tag that identifies a notice payload rather than prose. A notice keeps its
25
+ * wrapper — that tag is the field's identity, and `SystemNotice` reads it to
26
+ * pick "System Reminder" / "Task Result" over the generic card. */
27
+ const NOTICE_TAG_RE = /<\/?(?:system-reminder|reminder|system-notice|task-result)\b/i;
28
+
29
+ /** Fields only a real assistant turn carries. `thinkingLevel` is deliberately
30
+ * absent: the live stream stamps it on every non-user frame, notice rows
31
+ * included, so it proves nothing about the row being an answer. */
32
+ const TURN_FIELDS = [
33
+ 'model',
34
+ 'provider',
35
+ 'usage',
36
+ 'durationMs',
37
+ 'startedAt',
38
+ 'completedAt',
39
+ 'thinking',
40
+ 'toolCalls',
41
+ ] as const;
42
+
43
+ /**
44
+ * Index of the text part that IS a reminder envelope — `undefined` when the
45
+ * turn's prose merely quotes the tag (`see <system-reminder> handling` is
46
+ * content, not a wrapper). The envelope must open the part and enclose a whole
47
+ * element, so both statuses of a reminder turn are distinguishable before
48
+ * anything is moved into `notice`.
49
+ */
50
+ export function reminderPartIndex(parts: string[]): number | undefined {
51
+ const index = parts.findIndex((part) => {
52
+ const envelope = unwrapXmlEnvelope(part);
53
+ return envelope !== undefined && isReminderTag(envelope.tag);
54
+ });
55
+ return index === -1 ? undefined : index;
56
+ }
57
+
58
+ /** True when the row's `notice` is the assistant answer omp diverted there. */
59
+ export function noticeIsAssistantText(
60
+ msg: ChatMessageData | null | undefined,
61
+ ): msg is ChatMessageData & { notice: string } {
62
+ if (typeof msg?.notice !== 'string' || !msg.notice.trim()) return false;
63
+ if (NOTICE_TAG_RE.test(msg.notice)) return false;
64
+ return TURN_FIELDS.some((field) => msg[field] !== undefined && msg[field] !== null);
65
+ }
66
+
67
+ /** True when the row renders as a notice card (a real notice, not an answer). */
68
+ export function isNoticeRow(
69
+ msg: ChatMessageData | null | undefined,
70
+ ): msg is ChatMessageData & { notice: string } {
71
+ return typeof msg?.notice === 'string' && Boolean(msg.notice.trim()) && !noticeIsAssistantText(msg);
72
+ }
73
+
74
+ /** The row's answer text: `content`, or the notice when the answer landed there. */
75
+ export function messageAnswerText(msg: ChatMessageData | null | undefined): string {
76
+ if (noticeIsAssistantText(msg)) return msg.notice;
77
+ return typeof msg?.content === 'string' ? msg.content : '';
78
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Stop-button escalation for a live session.
8
+ *
9
+ * `abort` is awaited server-side until the turn actually stops, so a session
10
+ * wedged on a subagent — or on a child that stopped answering — never replies
11
+ * to it and the Stop button would hang with it. If the abort request is still
12
+ * pending after the grace period it is cancelled here and the stop escalates to
13
+ * `force_reset`, which the server answers by destroying the session's omp child
14
+ * (and every subagent under it) instead of waiting for a clean stop.
15
+ */
16
+
17
+ /** How long a stop may take before the process is reset instead. */
18
+ const ABORT_GRACE_MS = 10_000;
19
+
20
+ function postCommand(sessionId: string, body: Record<string, unknown>, signal?: AbortSignal): Promise<Response> {
21
+ return fetch(`/api/agent/${encodeURIComponent(sessionId)}`, {
22
+ method: 'POST',
23
+ headers: { 'Content-Type': 'application/json' },
24
+ body: JSON.stringify(body),
25
+ ...(signal ? { signal } : {}),
26
+ });
27
+ }
28
+
29
+ /** Ask the session to stop, then reset its process if it does not stop in time.
30
+ * Failures are swallowed: the event stream is the source of truth for state. */
31
+ export async function stopAgentSession(sessionId: string): Promise<void> {
32
+ const grace = new AbortController();
33
+ const timer = setTimeout(() => grace.abort(), ABORT_GRACE_MS);
34
+ try {
35
+ const stopped = await postCommand(sessionId, { type: 'abort' }, grace.signal).then(() => true, () => false);
36
+ if (stopped) return;
37
+ await postCommand(sessionId, { type: 'force_reset' }).catch(() => null);
38
+ } finally {
39
+ clearTimeout(timer);
40
+ }
41
+ }
@@ -1,5 +1,4 @@
1
1
  import { getDb } from '@/server/db.server';
2
- import { GoogleGenAI } from '@google/genai';
3
2
  import type { ChatMessageData, ToolCallData } from '@/shared/types';
4
3
 
5
4
  // Helper to delay execution for realistic stream simulation
@@ -35,81 +34,6 @@ async function persistAiMessage(sessionId: string, aiMessage: ChatMessageData) {
35
34
  }
36
35
  }
37
36
 
38
- // Handler for real Gemini API streaming
39
- export async function handleGeminiStreaming(options: {
40
- apiKey: string;
41
- prompt: string;
42
- modelName: string;
43
- sessionId: string;
44
- messageId: string;
45
- timeStr: string;
46
- sendEvent: StreamSendEvent;
47
- isAborted: () => boolean;
48
- }) {
49
- const { apiKey, prompt, sessionId, messageId, timeStr, sendEvent, isAborted } = options;
50
- const ai = new GoogleGenAI({
51
- apiKey,
52
- httpOptions: {
53
- headers: {
54
- 'User-Agent': 'aistudio-build',
55
- },
56
- },
57
- });
58
-
59
- sendEvent('thinking_start', { title: 'Deep Reasoning' });
60
- const thoughtText = `Deconstructing prompt: "${prompt}".\nAnalyzing project context, evaluating edge dependencies, and generating validated response.`;
61
-
62
- // Stream thinking chunk
63
- sendEvent('thinking_chunk', { delta: thoughtText });
64
- sendEvent('thinking_end', {
65
- thought: thoughtText,
66
- summary: 'Analyze context and stream response',
67
- duration: '0.8s',
68
- });
69
-
70
- sendEvent('content_start', {});
71
-
72
- const responseStream = await ai.models.generateContentStream({
73
- model: 'gemini-3.8-flash',
74
- contents: prompt || 'Hello',
75
- config: {
76
- systemInstruction: 'You are OMPChamber AI Assistant, integrated with Oh-My-Pi, remisJS, and Bun runtime environments. Provide concise, clean, markdown-formatted responses.',
77
- },
78
- });
79
-
80
- let fullContent = '';
81
- for await (const chunk of responseStream) {
82
- if (isAborted()) break;
83
- const textChunk = chunk.text || '';
84
- if (textChunk) {
85
- fullContent += textChunk;
86
- sendEvent('content_chunk', { delta: textChunk });
87
- }
88
- }
89
-
90
- sendEvent('content_end', {});
91
-
92
- const summary = 'Generation completed successfully via Gemini API.';
93
- sendEvent('summary', { summary });
94
-
95
- const finalMessage: ChatMessageData = {
96
- id: messageId,
97
- role: 'ai',
98
- date: `Today, ${timeStr}`,
99
- timestamp: timeStr,
100
- thinking: {
101
- duration: '0.8s',
102
- thought: thoughtText,
103
- summary: 'Analyze context and stream response',
104
- },
105
- content: fullContent,
106
- summary,
107
- };
108
-
109
- sendEvent('done', { message: finalMessage });
110
- await persistAiMessage(sessionId, finalMessage);
111
- }
112
-
113
37
  // Handler for domain-aware simulated streaming with tool executions & chunked tokens
114
38
  export async function handleSimulatedStreaming(options: {
115
39
  prompt: string;