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.
- package/README.md +17 -10
- package/dist/client/index.html +1 -1
- package/dist/client/static/css/index.5291f58223.css +2 -0
- package/dist/client/static/js/async/{1k.544adbf77d.js → 1k.5974df3b87.js} +2 -2
- package/dist/client/static/js/async/3j.357c80ff38.js +2 -0
- package/dist/client/static/js/async/{3y.22075aa079.js → 3y.ededeebde8.js} +5 -5
- package/dist/client/static/js/async/51.95bcae98af.js +2 -0
- package/dist/client/static/js/async/6.f034c9b528.js +1 -0
- package/dist/client/static/js/async/a.e05da304c8.js +10 -0
- package/dist/client/static/js/async/ar.1e14da5588.js +1 -0
- package/dist/client/static/js/async/ct.b74928f7a3.js +1 -0
- package/dist/client/static/js/async/dc.6dc4b0a04b.js +2 -0
- package/dist/client/static/js/async/fw.6a51998a8e.js +6 -0
- package/dist/client/static/js/async/g.7a4bbd4798.js +2 -0
- package/dist/client/static/js/async/g.7a4bbd4798.js.LICENSE.txt +11 -0
- package/dist/client/static/js/async/{k.e878dd350f.js → k.7466330d1a.js} +2 -2
- package/dist/client/static/js/async/katex-vendor.de6a87c308.js +1 -0
- package/dist/client/static/js/async/kc.bbdb35b771.js +1 -0
- package/dist/client/static/js/async/l6.c56640c205.js +1 -0
- package/dist/client/static/js/async/{m.3fc017a055.js → m.4b29d34f93.js} +3 -3
- package/dist/client/static/js/async/{og.ae7acf03ac.js → og.634fba99ac.js} +2 -2
- package/dist/client/static/js/async/q.7fb4d2f374.js +21 -0
- package/dist/client/static/js/async/rd.cb6bf495bb.js +2 -0
- package/dist/client/static/js/async/rf.d626afc367.js +2 -0
- package/dist/client/static/js/async/s.413c2a4683.js +2 -0
- package/dist/client/static/js/async/s.413c2a4683.js.LICENSE.txt +6 -0
- package/dist/client/static/js/async/sy.d16be843a7.js +94 -0
- package/dist/client/static/js/async/t.cb3fcf8615.js +1 -0
- package/dist/client/static/js/async/ud.3065bfee31.js +2 -0
- package/dist/client/static/js/async/uf.46ad6209fa.js +2 -0
- package/dist/client/static/js/async/uf.46ad6209fa.js.LICENSE.txt +6 -0
- package/dist/client/static/js/async/v.7d28d3c241.js +34 -0
- package/dist/client/static/js/async/v0.394ed37c6b.js +2 -0
- package/dist/client/static/js/async/v0.394ed37c6b.js.LICENSE.txt +6 -0
- package/dist/client/static/js/async/vt.42416b707d.js +1 -0
- package/dist/client/static/js/async/vy.bbe114c4e0.js +2 -0
- package/dist/client/static/js/async/vy.bbe114c4e0.js.LICENSE.txt +6 -0
- package/dist/client/static/js/async/xw.ae67ba06fd.js +31 -0
- package/dist/client/static/js/async/xw.ae67ba06fd.js.LICENSE.txt +4 -0
- package/dist/client/static/js/async/yg.35bebd6d0c.js +149 -0
- package/dist/client/static/js/async/yv.2fa22f009a.js +1 -0
- package/dist/client/static/js/index.0ac93ee878.js +33 -0
- package/dist/client/static/js/index.0ac93ee878.js.LICENSE.txt +4 -0
- package/dist/client/static/js/{markdown-vendor.9da142c67b.js → markdown-vendor.71e43278b3.js} +1 -1
- package/dist/client/static/js/uy.5f791144ce.js +139 -0
- package/package.json +2 -2
- package/src/cli/lib/args.js +1 -0
- package/src/cli/lib/commands/serve.js +7 -0
- package/src/client/App.tsx +22 -5
- package/src/client/components/common/MarkdownRenderer.tsx +36 -0
- package/src/client/components/layout/desktop-layout/index.tsx +1 -1
- package/src/client/components/layout/session-sidebar/Modals.tsx +1 -1
- package/src/client/components/mobile/LayoutWrapper.tsx +1 -1
- package/src/client/components/settings/LazyModal.tsx +46 -0
- package/src/client/components/settings/Modal.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +43 -29
- package/src/client/components/workspace/chat-timeline/MessageItem.tsx +15 -69
- package/src/client/components/workspace/chat-timeline/MessageList.tsx +47 -66
- package/src/client/components/workspace/chat-timeline/RunFooter.tsx +60 -0
- package/src/client/components/workspace/chat-timeline/SubagentView.tsx +37 -21
- package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +22 -4
- package/src/client/components/workspace/chat-timeline/index.tsx +3 -6
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +15 -19
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/EnvelopeHeader.tsx +46 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/FallbackOutput.tsx +18 -68
- package/src/client/components/workspace/file-explorer/TreeItem.tsx +16 -4
- package/src/client/components/workspace/file-explorer/index.tsx +6 -0
- package/src/client/data/models/catalog.ts +8 -0
- package/src/client/data/samples/dialogue-session.ts +1 -1
- package/src/client/data/samples/sample-tools/other.ts +1 -1
- package/src/client/hooks/chat/omp/index.ts +9 -10
- package/src/client/hooks/chat/omp/revalidation-throttle.ts +6 -5
- package/src/client/hooks/chat/timeline/actions.ts +1 -2
- package/src/client/hooks/chat/timeline/index.ts +5 -0
- package/src/client/hooks/chat/timeline/send.ts +2 -3
- package/src/client/hooks/chat/timeline/session-load.ts +3 -4
- package/src/client/hooks/ui/syntax-ready.ts +16 -3
- package/src/client/main.tsx +2 -5
- package/src/client/stubs/elkjs.ts +37 -0
- package/src/client/tailwind.css +10 -0
- package/src/server/index.ts +53 -0
- package/src/server/lib/fs/git-ignore.ts +49 -0
- package/src/server/lib/models/provider-registry.server.ts +44 -20
- package/src/server/lib/omp/core/cli.ts +3 -2
- package/src/server/lib/omp/core/startup.ts +64 -0
- package/src/server/lib/omp/rpc/constants.ts +10 -0
- package/src/server/lib/omp/rpc/lifecycle.test.ts +138 -0
- package/src/server/lib/omp/rpc/manager.ts +32 -4
- package/src/server/lib/omp/rpc/process.ts +1 -1
- package/src/server/lib/omp/rpc/session-commands.ts +32 -9
- package/src/server/lib/omp/rpc/session-registry.ts +3 -2
- package/src/server/lib/omp/rpc/subagent-liveness.test.ts +62 -0
- package/src/server/lib/omp/rpc/subagent-liveness.ts +100 -0
- package/src/server/lib/omp/session/messages.test.ts +112 -0
- package/src/server/lib/omp/session/messages.ts +6 -5
- package/src/server/mock.server.ts +6 -12
- package/src/server/routes/agent/command.ts +34 -16
- package/src/server/routes/chat/stream.ts +13 -30
- package/src/server/routes/fs/read.ts +15 -7
- package/src/server/routes/models/root.ts +5 -1
- package/src/shared/lib/chat/notice-row.test.ts +89 -0
- package/src/shared/lib/chat/notice-row.ts +78 -0
- package/src/shared/lib/chat/omp/abort.ts +41 -0
- package/src/shared/lib/chat/stream-service.ts +0 -76
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +154 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +36 -7
- package/src/shared/lib/chat/timeline/run-footer.test.ts +84 -0
- package/src/shared/lib/chat/timeline/run-footer.ts +96 -0
- package/src/shared/lib/chat/tool-output.test.ts +91 -0
- package/src/shared/lib/chat/tool-output.ts +54 -0
- package/src/shared/lib/chat/xml-envelope.ts +158 -0
- package/src/shared/lib/code/highlighter-lazy.ts +106 -0
- package/src/shared/lib/code/highlighter.ts +31 -52
- package/src/shared/lib/code/syntax-highlight.test.ts +36 -1
- package/src/shared/lib/code/syntax-highlight.ts +17 -3
- package/src/shared/lib/format/time.ts +19 -0
- package/src/shared/lib/fs/paths.ts +19 -0
- package/src/shared/lib/markdown/fira-code-fonts.css +51 -0
- package/src/shared/lib/markdown/katex-fonts.css +20 -0
- package/src/shared/lib/markdown/katex.ts +101 -0
- package/src/shared/lib/markdown/marked.ts +23 -14
- package/src/shared/lib/markdown/math.test.ts +109 -0
- package/src/shared/lib/markdown/mermaid.ts +6 -0
- package/src/shared/lib/markdown/sanitize.ts +48 -2
- package/src/shared/lib/omp/session/mapper.ts +11 -3
- package/src/shared/lib/omp/session/messages-map.ts +12 -12
- package/src/shared/types/chat.ts +3 -1
- package/src/shared/types/fs.ts +3 -0
- package/dist/client/static/css/g.f36aa429fa.css +0 -1
- package/dist/client/static/css/index.a0f07455bd.css +0 -2
- package/dist/client/static/css/katex-vendor.ca3a0600e8.css +0 -1
- package/dist/client/static/font/KaTeX_AMS-Regular.1608a09b4a.woff +0 -0
- package/dist/client/static/font/KaTeX_AMS-Regular.4aafdb685c.ttf +0 -0
- package/dist/client/static/font/KaTeX_Caligraphic-Bold.b67709187b.woff +0 -0
- package/dist/client/static/font/KaTeX_Caligraphic-Bold.cce5b8ecea.ttf +0 -0
- package/dist/client/static/font/KaTeX_Caligraphic-Regular.07ef19e7b0.ttf +0 -0
- package/dist/client/static/font/KaTeX_Caligraphic-Regular.dad44a7fc6.woff +0 -0
- package/dist/client/static/font/KaTeX_Fraktur-Bold.9f256b8593.woff +0 -0
- package/dist/client/static/font/KaTeX_Fraktur-Bold.b18f59e1d1.ttf +0 -0
- package/dist/client/static/font/KaTeX_Fraktur-Regular.7c1871215e.woff +0 -0
- package/dist/client/static/font/KaTeX_Fraktur-Regular.ed38e79f57.ttf +0 -0
- package/dist/client/static/font/KaTeX_Main-Bold.b74a1a8b2c.ttf +0 -0
- package/dist/client/static/font/KaTeX_Main-Bold.d181c4650d.woff +0 -0
- package/dist/client/static/font/KaTeX_Main-BoldItalic.70d8b0a530.ttf +0 -0
- package/dist/client/static/font/KaTeX_Main-BoldItalic.e3f82f9d27.woff +0 -0
- package/dist/client/static/font/KaTeX_Main-Italic.47373d1e51.ttf +0 -0
- package/dist/client/static/font/KaTeX_Main-Italic.9024d815ba.woff +0 -0
- package/dist/client/static/font/KaTeX_Main-Regular.7f51fe0340.woff +0 -0
- package/dist/client/static/font/KaTeX_Main-Regular.b7f8fe9b5f.ttf +0 -0
- package/dist/client/static/font/KaTeX_Math-BoldItalic.a879cf8383.ttf +0 -0
- package/dist/client/static/font/KaTeX_Math-BoldItalic.f1035d8d5d.woff +0 -0
- package/dist/client/static/font/KaTeX_Math-Italic.5295ba483a.woff +0 -0
- package/dist/client/static/font/KaTeX_Math-Italic.939bc64440.ttf +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Bold.94e1e8dc5f.ttf +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Bold.bf59d231c3.woff +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Italic.7c9bc82b17.woff +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Italic.b4c20c84d8.ttf +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Regular.740484788f.woff +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Regular.d4d7ba4804.ttf +0 -0
- package/dist/client/static/font/KaTeX_Script-Regular.0750571032.woff +0 -0
- package/dist/client/static/font/KaTeX_Script-Regular.fe9cbbe1a0.ttf +0 -0
- package/dist/client/static/font/KaTeX_Size1-Regular.e1e279cbdd.woff +0 -0
- package/dist/client/static/font/KaTeX_Size1-Regular.fabc004aab.ttf +0 -0
- package/dist/client/static/font/KaTeX_Size2-Regular.577270225e.woff +0 -0
- package/dist/client/static/font/KaTeX_Size2-Regular.d6b476ecd3.ttf +0 -0
- package/dist/client/static/font/KaTeX_Size3-Regular.9acaf01c64.woff +0 -0
- package/dist/client/static/font/KaTeX_Size3-Regular.a144ef5840.ttf +0 -0
- package/dist/client/static/font/KaTeX_Size4-Regular.7a996c9da2.woff +0 -0
- package/dist/client/static/font/KaTeX_Size4-Regular.fbccdabe0a.ttf +0 -0
- package/dist/client/static/font/KaTeX_Typewriter-Regular.6258592bdc.woff +0 -0
- package/dist/client/static/font/KaTeX_Typewriter-Regular.d97aaf4a1e.ttf +0 -0
- package/dist/client/static/font/fira-code-cyrillic-400-normal.5971438a4a.woff2 +0 -0
- package/dist/client/static/font/fira-code-cyrillic-400-normal.9bb7bfea9c.woff +0 -0
- package/dist/client/static/font/fira-code-cyrillic-500-normal.5486265b18.woff2 +0 -0
- package/dist/client/static/font/fira-code-cyrillic-500-normal.89f92bfb31.woff +0 -0
- package/dist/client/static/font/fira-code-cyrillic-600-normal.2805b497e5.woff +0 -0
- package/dist/client/static/font/fira-code-cyrillic-600-normal.86e0df22a3.woff2 +0 -0
- package/dist/client/static/font/fira-code-cyrillic-700-normal.505a88f22a.woff +0 -0
- package/dist/client/static/font/fira-code-cyrillic-700-normal.76f5e8d2e2.woff2 +0 -0
- package/dist/client/static/font/fira-code-cyrillic-ext-400-normal.3c4b15e57d.woff2 +0 -0
- package/dist/client/static/font/fira-code-cyrillic-ext-400-normal.61323debaa.woff +0 -0
- package/dist/client/static/font/fira-code-cyrillic-ext-500-normal.0f34210f2a.woff2 +0 -0
- package/dist/client/static/font/fira-code-cyrillic-ext-500-normal.2497cfa1aa.woff +0 -0
- package/dist/client/static/font/fira-code-cyrillic-ext-600-normal.40fce94af9.woff2 +0 -0
- package/dist/client/static/font/fira-code-cyrillic-ext-600-normal.6c8774b829.woff +0 -0
- package/dist/client/static/font/fira-code-cyrillic-ext-700-normal.4b742eb865.woff +0 -0
- package/dist/client/static/font/fira-code-cyrillic-ext-700-normal.e75eb4fccc.woff2 +0 -0
- package/dist/client/static/font/fira-code-greek-400-normal.bb900468b6.woff2 +0 -0
- package/dist/client/static/font/fira-code-greek-400-normal.be76e699ba.woff +0 -0
- package/dist/client/static/font/fira-code-greek-500-normal.176c9fb001.woff2 +0 -0
- package/dist/client/static/font/fira-code-greek-500-normal.d2c596136b.woff +0 -0
- package/dist/client/static/font/fira-code-greek-600-normal.490c3deed7.woff +0 -0
- package/dist/client/static/font/fira-code-greek-600-normal.7df14376d9.woff2 +0 -0
- package/dist/client/static/font/fira-code-greek-700-normal.c82b6072fc.woff2 +0 -0
- package/dist/client/static/font/fira-code-greek-700-normal.d5f2b2917b.woff +0 -0
- package/dist/client/static/font/fira-code-greek-ext-400-normal.4849a79781.woff +0 -0
- package/dist/client/static/font/fira-code-greek-ext-400-normal.7f5bfc4538.woff2 +0 -0
- package/dist/client/static/font/fira-code-greek-ext-500-normal.aaccca4075.woff2 +0 -0
- package/dist/client/static/font/fira-code-greek-ext-500-normal.e383181256.woff +0 -0
- package/dist/client/static/font/fira-code-greek-ext-600-normal.78cb4bff8e.woff +0 -0
- package/dist/client/static/font/fira-code-greek-ext-600-normal.d6c93d54f9.woff2 +0 -0
- package/dist/client/static/font/fira-code-greek-ext-700-normal.57ec7c92d9.woff +0 -0
- package/dist/client/static/font/fira-code-greek-ext-700-normal.8fafdae5e8.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-400-normal.973295532b.woff +0 -0
- package/dist/client/static/font/fira-code-latin-500-normal.7ae2be8fb8.woff +0 -0
- package/dist/client/static/font/fira-code-latin-600-normal.1dbbe5bc27.woff +0 -0
- package/dist/client/static/font/fira-code-latin-700-normal.497a70148b.woff +0 -0
- package/dist/client/static/font/fira-code-latin-ext-400-normal.9dfae29aaf.woff +0 -0
- package/dist/client/static/font/fira-code-latin-ext-400-normal.e534ae46e2.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-ext-500-normal.24c7b781b0.woff +0 -0
- package/dist/client/static/font/fira-code-latin-ext-500-normal.aad44ab072.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-ext-600-normal.7bed0f2f7e.woff +0 -0
- package/dist/client/static/font/fira-code-latin-ext-600-normal.c509a7c9f1.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-ext-700-normal.6c66c8b0a9.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-ext-700-normal.88f64e2822.woff +0 -0
- package/dist/client/static/font/fira-code-symbols2-400-normal.97d3f4b17c.woff +0 -0
- package/dist/client/static/font/fira-code-symbols2-500-normal.b06f1a06e4.woff +0 -0
- package/dist/client/static/font/fira-code-symbols2-600-normal.304b653b93.woff +0 -0
- package/dist/client/static/font/fira-code-symbols2-700-normal.d8ce0ad586.woff +0 -0
- package/dist/client/static/js/async/3j.a2fcea99be.js +0 -2
- package/dist/client/static/js/async/51.1938cec320.js +0 -2
- package/dist/client/static/js/async/6.7929428505.js +0 -1
- package/dist/client/static/js/async/69.a18a10882f.js +0 -1
- package/dist/client/static/js/async/a.bce4542392.js +0 -1
- package/dist/client/static/js/async/ar.9dc8b1992a.js +0 -2
- package/dist/client/static/js/async/bi.cbfe8eaa8e.js +0 -1
- package/dist/client/static/js/async/ct.38d9da8a5a.js +0 -1
- package/dist/client/static/js/async/fw.fb1a08b92d.js +0 -6
- package/dist/client/static/js/async/l6.45f3b62e15.js +0 -1
- package/dist/client/static/js/async/q.0b6fb9bf8d.js +0 -21
- package/dist/client/static/js/async/rd.b77011ab7f.js +0 -2
- package/dist/client/static/js/async/rf.cfbde7f744.js +0 -2
- package/dist/client/static/js/async/s.c7d521a8ae.js +0 -2
- package/dist/client/static/js/async/sy.7345abcd67.js +0 -94
- package/dist/client/static/js/async/t.0dcb01dfde.js +0 -1
- package/dist/client/static/js/async/v.5c7a4b087b.js +0 -1
- package/dist/client/static/js/async/vu.311f1d202c.js +0 -34
- package/dist/client/static/js/async/yg.9bd4e09ecb.js +0 -149
- package/dist/client/static/js/async/yv.593a3f7d54.js +0 -1
- package/dist/client/static/js/g.124fba0826.js +0 -139
- package/dist/client/static/js/index.907fd4670e.js +0 -68
- package/dist/client/static/js/katex-vendor.95e48cf65a.js +0 -1
- package/src/shared/lib/chat/order.ts +0 -40
- /package/dist/client/static/js/async/{1k.544adbf77d.js.LICENSE.txt → 1k.5974df3b87.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{3j.a2fcea99be.js.LICENSE.txt → 3j.357c80ff38.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{3y.22075aa079.js.LICENSE.txt → 3y.ededeebde8.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{51.1938cec320.js.LICENSE.txt → 51.95bcae98af.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{ar.9dc8b1992a.js.LICENSE.txt → a.e05da304c8.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{fw.fb1a08b92d.js.LICENSE.txt → dc.6dc4b0a04b.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{m.3fc017a055.js.LICENSE.txt → fw.6a51998a8e.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{q.0b6fb9bf8d.js.LICENSE.txt → m.4b29d34f93.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{rf.cfbde7f744.js.LICENSE.txt → q.7fb4d2f374.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{rd.b77011ab7f.js.LICENSE.txt → rd.cb6bf495bb.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{s.c7d521a8ae.js.LICENSE.txt → rf.d626afc367.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{index.907fd4670e.js.LICENSE.txt → async/ud.3065bfee31.js.LICENSE.txt} +0 -0
- /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
|
+
"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
|
}
|
package/src/cli/lib/args.js
CHANGED
|
@@ -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,
|
package/src/client/App.tsx
CHANGED
|
@@ -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
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
157
|
-
const prev =
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
67
|
-
const assistantContent = useMemo(() => (typeof
|
|
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
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
});
|