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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ompchamber",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Developer web console and diagnostic chamber for AI Oh-My-Pi — Elysia + Preact on Bun.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -45,7 +45,6 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@fontsource/fira-code": "^5.3.0",
48
- "@google/genai": "^2.21.0",
49
48
  "@shikijs/langs": "^4.4.3",
50
49
  "@shikijs/themes": "^4.4.3",
51
50
  "@tailwindcss/typography": "^0.5.20",
@@ -69,6 +68,7 @@
69
68
  "@tailwindcss/postcss": "^4.1.14",
70
69
  "@types/bun": "^1.4.2",
71
70
  "@types/node": "^22.14.0",
71
+ "happy-dom": "^20.14.5",
72
72
  "tailwindcss": "^4.1.14",
73
73
  "typescript": "~5.8.2"
74
74
  }
@@ -216,6 +216,7 @@ ENVIRONMENT:
216
216
  OMPCHAMBER_DATA_DIR Override the data directory (default: ~/.ompchamber)
217
217
  OMPCHAMBER_PORT Default web server port
218
218
  OMPCHAMBER_HOST Default bind address
219
+ OMPCHAMBER_OMP_BIN Path to the omp binary (required; falls back to PATH)
219
220
  GITHUB_TOKEN Raise the GitHub API rate limit for update checks
220
221
 
221
222
  EXAMPLES:
@@ -10,6 +10,7 @@ import {
10
10
  } from '@/cli/lib/runtime.js';
11
11
  import { log, ok, warn, fail, printJson, isJson, isQuiet } from '@/cli/lib/output.js';
12
12
  import { wireChildProcessLifecycle } from '@/cli/lib/process-lifecycle.js';
13
+ import { ompStartupError } from '@/server/lib/omp/core/startup';
13
14
 
14
15
  const DEFAULT_PORT = 3000;
15
16
  const DEFAULT_HOST = '127.0.0.1';
@@ -45,6 +46,12 @@ export async function run(options, ctx) {
45
46
  const quiet = isQuiet();
46
47
  const pkgRoot = ctx?.pkgRoot ?? process.cwd();
47
48
 
49
+ // The same gate the server entry applies, run here first so a missing omp
50
+ // binary fails immediately — with no detached child spawned, no log file to
51
+ // read and no registry entry left pointing at a process that already exited.
52
+ const ompError = ompStartupError();
53
+ if (ompError) fail(ompError);
54
+
48
55
  if (mode === 'prod') {
49
56
  const serverEntry = joinPath(pkgRoot, 'dist', 'client', 'index.html');
50
57
  // Bun.file().exists() is file-only (false for directories) — correct here,
@@ -4,12 +4,27 @@
4
4
  */
5
5
 
6
6
  import { useEffect, useRef, useState } from 'preact/hooks';
7
+ import { Suspense, lazy } from 'preact/compat';
7
8
  import { useSearchParams } from '@/client/lib/router/search-params';
8
9
  import { DesktopLayout } from '@/client/components/layout/desktop-layout/index';
9
- import { MobileLayoutWrapper } from '@/client/components/mobile/LayoutWrapper';
10
10
  import { SessionStateProvider } from '@/client/components/common/session-state-provider';
11
11
  import { SidebarDataProvider } from '@/client/hooks/chat/omp/session-list';
12
12
 
13
+ /**
14
+ * The mobile and desktop layouts are mutually exclusive screen trees — exactly
15
+ * one is mounted per load, and `initialIsMobile` is known before the first
16
+ * render because the server injects it. Keeping the mobile tree out of the
17
+ * initial bundle is therefore pure win for the desktop (the common case for a
18
+ * developer console): its subtree moves to this async chunk and is fetched only
19
+ * when a phone boots, or when the user switches layout by hand at runtime.
20
+ *
21
+ * `fallback={null}` is safe rather than a blank flash: `body` already paints
22
+ * `--theme-canvas`, so the pre-hydration frame and the fallback look identical.
23
+ */
24
+ const MobileLayoutWrapper = lazy(() =>
25
+ import('@/client/components/mobile/LayoutWrapper').then((m) => ({ default: m.MobileLayoutWrapper }))
26
+ );
27
+
13
28
  export interface AppProps {
14
29
  /** Server-detected mobile verdict, so the first paint needs no UA check. */
15
30
  initialIsMobile?: boolean;
@@ -72,10 +87,12 @@ export function App({ initialIsMobile = false, appSettings = {} }: AppProps) {
72
87
  return (
73
88
  <SessionStateProvider sessionId={sessionId}>
74
89
  <SidebarDataProvider>
75
- <MobileLayoutWrapper
76
- onDesktopToggle={handleSwitchToDesktop}
77
- appSettings={appSettings}
78
- />
90
+ <Suspense fallback={null}>
91
+ <MobileLayoutWrapper
92
+ onDesktopToggle={handleSwitchToDesktop}
93
+ appSettings={appSettings}
94
+ />
95
+ </Suspense>
79
96
  </SidebarDataProvider>
80
97
  </SessionStateProvider>
81
98
  );
@@ -16,6 +16,7 @@
16
16
  import type { TargetedMouseEvent } from 'preact';
17
17
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'preact/hooks';
18
18
  import { renderMarkdown } from '@/shared/lib/markdown/marked';
19
+ import { hydrateMathBlocks, MATH_PENDING_CLASS, preloadKatex } from '@/shared/lib/markdown/katex';
19
20
  import { sanitizeHtml } from '@/shared/lib/markdown/sanitize';
20
21
  import { hydrateMermaidBlocks } from '@/shared/lib/markdown/mermaid';
21
22
  import { copyToClipboard } from '@/client/hooks/ui/clipboard';
@@ -36,10 +37,45 @@ export function MarkdownRenderer({ content, className = '' }: MarkdownRendererPr
36
37
  const html = useMemo(() => {
37
38
  if (!content) return '';
38
39
  const rendered = renderMarkdown(content);
40
+ // If the message contains math, start fetching KaTeX during parse rather
41
+ // than waiting for the post-mount hydration effect — the request overlaps
42
+ // sanitization and the rest of the render instead of following it.
43
+ if (rendered.includes(MATH_PENDING_CLASS)) preloadKatex();
39
44
  return sanitizeHtml(rendered);
40
45
  }, [content, syntaxReady]);
41
46
 
42
47
  const hasMermaid = html.includes('mermaid-block');
48
+ const hasMath = html.includes(MATH_PENDING_CLASS);
49
+
50
+ // KaTeX is ~522 kB of JS fetched only when a message contains math; the
51
+ // placeholders emitted by `marked.ts` are swapped for real markup here.
52
+ useEffect(() => {
53
+ if (!hasMath || typeof window === 'undefined') return;
54
+ const container = containerRef.current;
55
+ if (!container) return;
56
+
57
+ let cancelled = false;
58
+ const hydrate = async () => {
59
+ if (!container.isConnected) return;
60
+ await hydrateMathBlocks(container);
61
+ if (cancelled) return;
62
+ };
63
+ void hydrate();
64
+ // Same reason as the mermaid observer: a timeline re-render can replace the
65
+ // container's innerHTML with an identical string without this effect
66
+ // re-running, stranding new placeholders. hydrate() only writes inside the
67
+ // placeholder elements, so it never re-triggers this observer.
68
+ const observer = new MutationObserver(() => {
69
+ const current = containerRef.current;
70
+ if (current?.isConnected) void hydrateMathBlocks(current);
71
+ });
72
+ observer.observe(container, { childList: true });
73
+
74
+ return () => {
75
+ cancelled = true;
76
+ observer.disconnect();
77
+ };
78
+ }, [hasMath, html]);
43
79
 
44
80
  useIsomorphicLayoutEffect(() => {
45
81
  if (!hasMermaid || typeof window === 'undefined') return;
@@ -4,7 +4,7 @@ import { useSearchParams } from '@/client/lib/router/search-params';
4
4
  import { Group, Panel, type PanelImperativeHandle } from '@/client/components/layout/desktop-layout/resizer';
5
5
  import { SessionSidebar } from '@/client/components/layout/session-sidebar/index';
6
6
  import { type RightPanelType } from '@/shared/lib/workspace/right-panels';
7
- import { SettingsModal } from '@/client/components/settings/Modal';
7
+ import { SettingsModal } from '@/client/components/settings/LazyModal';
8
8
  import { PanelLeft } from 'lucide-preact';
9
9
  import type { SettingsCategoryId } from '@/shared/types';
10
10
  import { activeProjectForSession } from '@/shared/lib/workspace/active-project';
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useState } from 'preact/hooks';
2
2
  import { Clock, Columns, FolderOpen, Home, Maximize2, Plus, Send, Settings2, Terminal, X } from 'lucide-preact';
3
- import { SettingsModal } from '@/client/components/settings/Modal';
3
+ import { SettingsModal } from '@/client/components/settings/LazyModal';
4
4
  import { FolderPicker } from '@/client/components/common/FolderPicker';
5
5
 
6
6
  export { SettingsModal };
@@ -7,7 +7,7 @@ import { MobileSessionSidebar } from '@/client/components/mobile/SessionSidebar'
7
7
  import { MobileRightSidebar } from '@/client/components/mobile/RightSidebar';
8
8
  import { MobileFullEditor } from '@/client/components/mobile/mobile-right-sidebar/FullEditor';
9
9
  import { MobileFullDiff } from '@/client/components/mobile/mobile-right-sidebar/FullDiff';
10
- import { SettingsModal } from '@/client/components/settings/Modal';
10
+ import { SettingsModal } from '@/client/components/settings/LazyModal';
11
11
  import { activeProjectForSession } from '@/shared/lib/workspace/active-project';
12
12
  import { spawnCwdForNewSession, triggerSessionPrewarm } from '@/shared/lib/omp/session/prewarm';
13
13
  import { useSidebarData } from '@/client/hooks/chat/omp/session-list';
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Lazy boundary for the settings modal.
8
+ *
9
+ * The settings tree is 68 modules plus the ~160 kB `omp-schema.json`, and it is
10
+ * reached only by a click — yet `Modal.tsx` was imported statically by the
11
+ * desktop layout, the mobile layout, and the sidebar, so every page load paid
12
+ * for all of it. Importing through here moves the subtree to its own async
13
+ * chunk and takes ~84 kB gzip off the initial payload.
14
+ *
15
+ * The `isOpen` gate is load-bearing, not cosmetic: `lazy()` starts fetching as
16
+ * soon as the component renders, even when the wrapped component would render
17
+ * `null` internally. Without the gate the chunk would be requested at boot and
18
+ * nothing would be saved.
19
+ *
20
+ * All three call sites already own the open state (each layout runs its own
21
+ * `omp:open-settings` listener), so gating preserves the existing contract. The
22
+ * unmount is safe for settings values too: `mergeChamberSettings` re-reads the
23
+ * in-memory snapshot in `lib/settings/client`, which every write updates, not
24
+ * the stale bootstrap payload.
25
+ *
26
+ * `fallback={null}` matches the other lazy dialog boundary
27
+ * (`tool-renderers/ask-dialog/Lazy.tsx`): the modal is a full-screen overlay
28
+ * that appears on user intent, so a brief absence reads as the click landing
29
+ * rather than as a broken surface.
30
+ */
31
+
32
+ import { Suspense, lazy } from 'preact/compat';
33
+ import type { SettingsModalProps } from '@/client/components/settings/Modal';
34
+
35
+ const SettingsModalImpl = lazy(() =>
36
+ import('@/client/components/settings/Modal').then((m) => ({ default: m.SettingsModal }))
37
+ );
38
+
39
+ export function SettingsModal({ isOpen, ...rest }: SettingsModalProps) {
40
+ if (!isOpen) return null;
41
+ return (
42
+ <Suspense fallback={null}>
43
+ <SettingsModalImpl isOpen={isOpen} {...rest} />
44
+ </Suspense>
45
+ );
46
+ }
@@ -21,7 +21,7 @@ import { TokenUsageSettings } from '@/client/components/settings/categories/Toke
21
21
  import { UsageSettings } from '@/client/components/settings/categories/UsageSettings';
22
22
  import { NotificationSettings } from '@/client/components/settings/categories/NotificationSettings';
23
23
 
24
- interface SettingsModalProps {
24
+ export interface SettingsModalProps {
25
25
  isOpen: boolean;
26
26
  onClose: () => void;
27
27
  initialCategory?: SettingsCategoryId;
@@ -1,11 +1,14 @@
1
1
  import type { SetStateAction } from 'preact/compat';
2
+ import { Fragment } from 'preact';
2
3
  import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
3
4
  import { Check, ChevronDown, Folder } from 'lucide-preact';
4
5
  import type { Attachment, ChatMessageData } from '@/shared/types';
5
6
  import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
6
7
  import { ChatInput } from '@/client/components/workspace/chat-timeline/chat-input/index';
7
8
  import { ChatMessageItem } from '@/client/components/workspace/chat-timeline/MessageItem';
9
+ import { RunFooter } from '@/client/components/workspace/chat-timeline/RunFooter';
8
10
  import { GeneratingIndicator } from '@/client/components/workspace/chat-timeline/GeneratingIndicator';
11
+ import { resolveRunFooters, streamingRowIndex } from '@/shared/lib/chat/timeline/run-footer';
9
12
  import type { ApprovalMode } from '@/shared/lib/omp/config/access-mode';
10
13
 
11
14
  interface EmptyWorkspacePromptProps {
@@ -22,8 +25,9 @@ interface EmptyWorkspacePromptProps {
22
25
  appSettings?: Record<string, any>;
23
26
  /** Messages of the pending session: rendered above the input so the
24
27
  * optimistic user bubble shows immediately on send (before the omp spawn
25
- * completes and the real session timeline takes over). */
26
- localMessages?: ChatMessageData[];
28
+ * completes and the real session timeline takes over). Rendered in array
29
+ * order — no display-side reordering. */
30
+ messages?: ChatMessageData[];
27
31
  provider?: string;
28
32
  providerNames?: Record<string, string>;
29
33
  modelName?: string;
@@ -52,7 +56,7 @@ export function EmptyWorkspacePrompt({
52
56
  onSend,
53
57
  isGenerating,
54
58
  appSettings = {},
55
- localMessages = [],
59
+ messages = [],
56
60
  provider,
57
61
  providerNames,
58
62
  modelName,
@@ -77,11 +81,21 @@ export function EmptyWorkspacePrompt({
77
81
  return folders?.find(f => f.id === selectedFolderId);
78
82
  }, [folders, selectedFolderId]);
79
83
 
84
+ // Run footers are boundary rows: emitted after the run's last row, so a
85
+ // trailing notice never separates the footer from the next user message.
86
+ const { streamingIdx, footers } = useMemo(
87
+ () => ({
88
+ streamingIdx: streamingRowIndex(messages),
89
+ footers: resolveRunFooters(messages, isGenerating),
90
+ }),
91
+ [messages, isGenerating],
92
+ );
93
+
80
94
  useEffect(() => {
81
95
  if (timelineRef.current) {
82
96
  timelineRef.current.scrollTop = timelineRef.current.scrollHeight;
83
97
  }
84
- }, [localMessages.length]);
98
+ }, [messages.length]);
85
99
 
86
100
  return (
87
101
  <div className={`flex flex-col h-full bg-canvas ${variant === 'mobile' ? 'px-3 py-4' : 'p-8'} ${className}`}
@@ -89,13 +103,13 @@ export function EmptyWorkspacePrompt({
89
103
  >
90
104
  <div
91
105
  className={`mx-auto w-full max-w-[970px] flex flex-col min-h-0 space-y-3 ${
92
- localMessages.length > 0 ? 'flex-1' : 'my-auto justify-center'
106
+ messages.length > 0 ? 'flex-1' : 'my-auto justify-center'
93
107
  }`}
94
108
  >
95
109
  {/* Workspace Selection Seamless Dropdown (No border, transparent
96
110
  background). Hidden once a chat has been sent — the session now has
97
111
  a target workspace, so the composer is the only focus. */}
98
- {localMessages.length === 0 && (
112
+ {messages.length === 0 && (
99
113
  <div className="relative shrink-0" ref={workspaceRef}>
100
114
  <button
101
115
  type="button"
@@ -147,36 +161,36 @@ export function EmptyWorkspacePrompt({
147
161
  on send, then the real session (UUID) takes over the ChatTimeline.
148
162
  Only shown when there are messages so the empty composer stays
149
163
  centered next to the workspace picker. */}
150
- {localMessages.length > 0 && (
164
+ {messages.length > 0 && (
151
165
  <div
152
166
  ref={timelineRef}
153
167
  className="flex-1 min-h-0 overflow-y-auto scroll-smooth overscroll-contain"
154
168
  >
155
169
  <div className="mx-auto w-full max-w-[970px]">
156
- {localMessages.map((msg, idx) => {
157
- const prev = localMessages[idx - 1];
158
- let lastAiIdx = localMessages.length - 1;
159
- while (lastAiIdx >= 0 && localMessages[lastAiIdx].notice) lastAiIdx--;
160
- const isLoading = isGenerating && idx === lastAiIdx && msg.role === 'ai';
170
+ {messages.map((msg, idx) => {
171
+ const prev = messages[idx - 1];
172
+ const isLoading = isGenerating && idx === streamingIdx && msg.role === 'ai';
161
173
  const isAiFragment = msg.role !== 'user' && prev && prev.role !== 'user';
162
- // Notice rows are transparent for footer purposes — the last
163
- // real AI message of a run still owns the footer (mirror
164
- // ChatTimeline) and the notice itself never gets one.
165
- const nextReal = localMessages.slice(idx + 1).find(m => !m.notice);
166
- const isLastAi = msg.role !== 'user' && !msg.notice && (!nextReal || nextReal.role === 'user');
174
+ const footer = footers[idx];
167
175
  return (
168
- <ChatMessageItem
169
- key={msg.id}
170
- msg={msg}
171
- provider={provider}
172
- providerNames={providerNames}
173
- modelName={modelName}
174
- modelNames={modelNames}
175
- thinkingLevel={sessionThinkingLevel ?? undefined}
176
- isStreaming={isLoading}
177
- footerVisible={isLastAi}
178
- className={isAiFragment ? 'mt-1' : 'mt-8'}
179
- />
176
+ <Fragment key={msg.id}>
177
+ <ChatMessageItem
178
+ msg={msg}
179
+ isStreaming={isLoading}
180
+ className={isAiFragment ? 'mt-1' : 'mt-8'}
181
+ />
182
+ {footer && (
183
+ <RunFooter
184
+ msg={footer.msg}
185
+ provider={provider}
186
+ providerNames={providerNames}
187
+ modelName={modelName}
188
+ modelNames={modelNames}
189
+ thinkingLevel={sessionThinkingLevel ?? undefined}
190
+ durationMs={footer.durationMs}
191
+ />
192
+ )}
193
+ </Fragment>
180
194
  );
181
195
  })}
182
196
  </div>
@@ -8,85 +8,49 @@ import { ToolCallingSection } from '@/client/components/workspace/chat-timeline/
8
8
  import { SystemNotice } from '@/client/components/workspace/chat-timeline/SystemNotice';
9
9
  import { MarkdownRenderer } from '@/client/components/common/MarkdownRenderer';
10
10
  import { AttachmentChips } from '@/client/components/workspace/chat-timeline/AttachmentChips';
11
- import { AiMessageFooter } from '@/client/components/workspace/chat-timeline/AiMessageFooter';
12
11
  import { CopyButton } from '@/client/components/common/CopyButton';
13
12
  import { capitalizeFirstLetter } from '@/shared/lib/chat/capitalize';
14
- import { formatClock, parseTodayLabel } from '@/shared/lib/format/time';
13
+ import { isNoticeRow, messageAnswerText } from '@/shared/lib/chat/notice-row';
14
+ import { formatMessageStamp } from '@/shared/lib/format/time';
15
15
 
16
16
  interface ChatMessageItemProps {
17
17
  msg: ChatMessageData | any;
18
- provider?: string;
19
- providerNames?: Record<string, string>;
20
- modelName?: string;
21
- /** id → display-name map from the model catalog; resolves per-message ids. */
22
- modelNames?: Record<string, string>;
23
- /** Session thinking level shown in the AI footer. */
24
- thinkingLevel?: string;
25
18
  isStreaming?: boolean;
26
- onRetry?: (msgId: string) => void;
27
19
  onUndo?: (msgId: string, content?: string) => void;
28
20
  onNewChat?: (content: string) => void;
29
- /** Render the AI metadata/toolbar footer. Only the last AI message of a
30
- * response run should show it so multi-part JSONL responses do not repeat
31
- * the footer per message. */
32
- footerVisible?: boolean;
33
- /** Mobile uses a compact provider/model footer with a bottom-sheet menu. */
34
- isMobile?: boolean;
35
21
 
36
22
  /** Extra classes on the root wrapper (e.g. spacing between messages). */
37
23
  className?: string;
38
- /** Elapsed ms of the whole AI response run — shown as ⏳ duration. */
39
- durationMs?: number | null;
40
24
  /** Whether the message immediately preceding this one was also an assistant message. */
41
25
  isPrevAssistant?: boolean;
42
26
  }
43
27
 
44
28
  export const ChatMessageItem = memo(function ChatMessageItem({
45
29
  msg,
46
- provider,
47
- providerNames,
48
- modelName,
49
- modelNames,
50
- thinkingLevel,
51
30
  isStreaming = false,
52
- onRetry,
53
- onUndo,
31
+ onUndo,
54
32
  onNewChat,
55
- footerVisible = true,
56
- isMobile = false,
57
33
  className = '',
58
- durationMs = null,
59
34
  isPrevAssistant = false,
60
35
  }: ChatMessageItemProps) {
61
36
  const isUser = msg.role === 'user';
62
37
 
38
+ /** A notice row renders as a card. When omp diverted the turn's answer into
39
+ * `notice` instead, the row is an answer (it kept the turn's own metadata)
40
+ * and the notice text is the response — see chat/notice-row.ts. */
41
+ const isNotice = isNoticeRow(msg);
42
+ const effectiveContent = isNotice ? msg.content : messageAnswerText(msg);
43
+
63
44
  /** Stable string references for MarkdownRenderer so its internal
64
45
  * useMemo([content]) holds across parent re-renders (streaming frames
65
46
  * replace the timeline array identity every frame). */
66
- const userContent = useMemo(() => (typeof msg.content === 'string' ? msg.content.trim() : msg.content), [msg.content]);
67
- const assistantContent = useMemo(() => (typeof msg.content === 'string' ? capitalizeFirstLetter(msg.content) : msg.content), [msg.content]);
47
+ const userContent = useMemo(() => (typeof effectiveContent === 'string' ? effectiveContent.trim() : effectiveContent), [effectiveContent]);
48
+ const assistantContent = useMemo(() => (typeof effectiveContent === 'string' ? capitalizeFirstLetter(effectiveContent) : effectiveContent), [effectiveContent]);
68
49
 
69
50
  /** Content is not worth rendering when it is empty or only punctuation
70
51
  * placeholders ("." / "..." etc.) — chunked assistant turns often carry a
71
52
  * lone dot while the real payload lives in tool calls / thinking. */
72
- const hasRenderableContent = typeof msg.content === 'string' && /[A-Za-z0-9]/.test(msg.content);
73
-
74
- const formatFooterDate = (value?: string) => {
75
- const raw = value || msg.timestamp || msg.date;
76
- if (!raw) return '';
77
- if (raw.startsWith('Today,')) {
78
- return `Today, ${raw.slice('Today,'.length).trim()}`;
79
- }
80
- // Bare "10:30 AM" timestamp (live path) — resolve against today.
81
- if (/^\d{1,2}:\d{2}\s*(AM|PM)$/i.test(raw)) {
82
- const parsedMs = parseTodayLabel(`Today, ${raw}`);
83
- if (parsedMs !== null) return `Today, ${formatClock(parsedMs)}`;
84
- }
85
- const date = new Date(raw);
86
- if (Number.isNaN(date.getTime())) return '';
87
- const day = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
88
- return `${day}, ${formatClock(date)}`;
89
- };
53
+ const hasRenderableContent = typeof effectiveContent === 'string' && /[A-Za-z0-9]/.test(effectiveContent);
90
54
 
91
55
  const handleNewChat = (e?: TargetedMouseEvent<HTMLElement>) => {
92
56
  if (e) e.preventDefault();
@@ -138,7 +102,7 @@ export const ChatMessageItem = memo(function ChatMessageItem({
138
102
  ) : (
139
103
  <User size={11} className="text-ink/70 shrink-0" />
140
104
  )}
141
- {formatFooterDate() && <span>{formatFooterDate()}</span>}
105
+ {formatMessageStamp(msg) && <span>{formatMessageStamp(msg)}</span>}
142
106
  </div>
143
107
 
144
108
  <div className="flex items-center space-x-1">
@@ -197,8 +161,8 @@ export const ChatMessageItem = memo(function ChatMessageItem({
197
161
  {/* Main AI Response Container */}
198
162
  <div className="w-full space-y-2.5 font-sans leading-relaxed">
199
163
 
200
- {/* System Notice Alert */}
201
- {msg.notice && <SystemNotice notice={msg.notice} />}
164
+ {/* System Notice Alert (a notice carrying the answer renders as content below) */}
165
+ {isNotice && <SystemNotice notice={msg.notice} />}
202
166
 
203
167
  {/* Thinking / Reasoning Accordion */}
204
168
  {thinkingData && (
@@ -298,24 +262,6 @@ export const ChatMessageItem = memo(function ChatMessageItem({
298
262
  </div>
299
263
  )}
300
264
  </div>
301
-
302
- {/* Bottom AI Metadata & Actions Toolbar (Only shown once completed) */}
303
- {!isStreaming && footerVisible && (
304
- <AiMessageFooter
305
- provider={msg.provider || provider}
306
- providerNames={providerNames}
307
- currentModel={(msg.model ? (modelNames?.[msg.model] ?? msg.model) : '') || modelName || ''}
308
- thinkingLevel={msg.thinkingLevel ?? thinkingLevel}
309
- dateStr={formatFooterDate()}
310
- durationMs={durationMs ?? msg.durationMs}
311
- usage={msg.usage}
312
- content={msg.content}
313
- msgId={msg.id}
314
- onRetry={onRetry}
315
- onNewChat={onNewChat}
316
- isMobile={isMobile}
317
- />
318
- )}
319
265
  </div>
320
266
  );
321
267
  });