ompchamber 0.3.0 → 0.4.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 +9 -6
- 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/art.62139ed479.js +2 -0
- package/dist/client/static/js/async/ct.b74928f7a3.js +1 -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.bf1b1fe988.js +33 -0
- package/dist/client/static/js/index.bf1b1fe988.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 -1
- 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 +8 -2
- 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 +1 -2
- 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/lib/fs/git-ignore.ts +49 -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/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/routes/agent/command.ts +34 -16
- package/src/server/routes/fs/read.ts +15 -7
- 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/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/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 → art.62139ed479.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.4.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": {
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"@tailwindcss/postcss": "^4.1.14",
|
|
70
70
|
"@types/bun": "^1.4.2",
|
|
71
71
|
"@types/node": "^22.14.0",
|
|
72
|
+
"happy-dom": "^20.14.5",
|
|
72
73
|
"tailwindcss": "^4.1.14",
|
|
73
74
|
"typescript": "~5.8.2"
|
|
74
75
|
}
|
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
|
});
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { Fragment } from 'preact';
|
|
1
2
|
import { useMemo } from 'preact/hooks';
|
|
2
3
|
import { memo } from 'preact/compat';
|
|
3
4
|
|
|
4
5
|
import { ChatMessageItem } from '@/client/components/workspace/chat-timeline/MessageItem';
|
|
5
|
-
import {
|
|
6
|
+
import { RunFooter } from '@/client/components/workspace/chat-timeline/RunFooter';
|
|
7
|
+
import { isNoticeRow } from '@/shared/lib/chat/notice-row';
|
|
8
|
+
import { previousNonNoticeIndex, resolveRunFooters, streamingRowIndex } from '@/shared/lib/chat/timeline/run-footer';
|
|
6
9
|
import type { ChatMessageData } from '@/shared/types';
|
|
7
10
|
|
|
8
11
|
export interface MessageListProps {
|
|
@@ -22,14 +25,15 @@ export interface MessageListProps {
|
|
|
22
25
|
|
|
23
26
|
/**
|
|
24
27
|
* Shared timeline body: the ordered message rows plus the per-run footer
|
|
25
|
-
* (model + duration)
|
|
28
|
+
* (model + duration) placement rules. Desktop and mobile render the exact same
|
|
26
29
|
* rows — only the surrounding scroll container differs.
|
|
27
30
|
*
|
|
28
|
-
* Footer
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
+
* Footer placement: the footer is the run's own boundary row, so it renders
|
|
32
|
+
* after every row the run owns — including notice rows omp wrote at its tail —
|
|
33
|
+
* and therefore immediately before the next user message. A notice-only
|
|
34
|
+
* stretch never gets one.
|
|
31
35
|
*/
|
|
32
|
-
// Per-row lookups (
|
|
36
|
+
// Per-row lookups (previous non-notice neighbour, run footers) are built in
|
|
33
37
|
// one O(n) pass and memoized on `messages`, so a streaming frame renders in
|
|
34
38
|
// O(n) instead of re-scanning the array for every row. `memo` then skips the
|
|
35
39
|
// rows entirely when the props are referentially unchanged.
|
|
@@ -46,75 +50,52 @@ export const MessageList = memo(function MessageList({
|
|
|
46
50
|
onNewChat,
|
|
47
51
|
isMobile = false,
|
|
48
52
|
}: MessageListProps) {
|
|
49
|
-
const {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const nextNonNotice: (ChatMessageData | null)[] = new Array(count);
|
|
60
|
-
let next: ChatMessageData | null = null;
|
|
61
|
-
for (let i = count - 1; i >= 0; i--) {
|
|
62
|
-
nextNonNotice[i] = next;
|
|
63
|
-
if (!messages[i].notice) next = messages[i];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// prevNonNoticeIdx[idx]: nearest non-notice row index strictly BEFORE idx.
|
|
67
|
-
const prevNonNoticeIdx: number[] = new Array(count);
|
|
68
|
-
let prevIdx = -1;
|
|
69
|
-
for (let i = 0; i < count; i++) {
|
|
70
|
-
prevNonNoticeIdx[i] = prevIdx;
|
|
71
|
-
if (!messages[i].notice) prevIdx = i;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Footer-owning rows resolve their run duration once here (each is O(n) in
|
|
75
|
-
// the worst case, so doing it inline would be O(n²) overall).
|
|
76
|
-
const durations: (number | null)[] = new Array(count);
|
|
77
|
-
for (let i = 0; i < count; i++) {
|
|
78
|
-
const msg = messages[i];
|
|
79
|
-
const nextReal = nextNonNotice[i];
|
|
80
|
-
const ownsFooter = msg.role !== 'user' && !msg.notice && (!nextReal || nextReal.role === 'user');
|
|
81
|
-
durations[i] = ownsFooter ? responseRunDurationMs(messages, i) : null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return { lastAiIdx: lastAi, nextNonNotice, prevNonNoticeIdx, durations };
|
|
85
|
-
}, [messages]);
|
|
53
|
+
const { streamingIdx, footers, prevNonNoticeIdx } = useMemo(() => {
|
|
54
|
+
return {
|
|
55
|
+
// The streaming AI row is the last non-notice row (see streamingRowIndex).
|
|
56
|
+
streamingIdx: streamingRowIndex(messages),
|
|
57
|
+
// Runs resolve their footer + run duration once here, keyed by the row
|
|
58
|
+
// the footer renders after.
|
|
59
|
+
footers: resolveRunFooters(messages, isGenerating),
|
|
60
|
+
prevNonNoticeIdx: previousNonNoticeIndex(messages),
|
|
61
|
+
};
|
|
62
|
+
}, [messages, isGenerating]);
|
|
86
63
|
|
|
87
64
|
return (
|
|
88
65
|
<>
|
|
89
66
|
{messages.map((msg, idx) => {
|
|
90
67
|
const prev = messages[idx - 1];
|
|
91
|
-
const isLoading = isGenerating && idx ===
|
|
92
|
-
const
|
|
93
|
-
const isLastAi = msg.role !== 'user' && !msg.notice && (!nextReal || nextReal.role === 'user');
|
|
94
|
-
const isPrevNotice = Boolean(prev?.notice);
|
|
95
|
-
const isAiFragment = msg.role !== 'user' && prev && prev.role !== 'user' && !isPrevNotice;
|
|
68
|
+
const isLoading = isGenerating && idx === streamingIdx && msg.role === 'ai';
|
|
69
|
+
const isAiFragment = msg.role !== 'user' && prev && prev.role !== 'user' && !isNoticeRow(prev);
|
|
96
70
|
const prevRealIdx = prevNonNoticeIdx[idx];
|
|
97
71
|
const prevReal = prevRealIdx >= 0 ? messages[prevRealIdx] : null;
|
|
98
72
|
const isPrevAssistant = Boolean(msg.role !== 'user' && prevReal && prevReal.role !== 'user');
|
|
73
|
+
const footer = footers[idx];
|
|
99
74
|
return (
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
75
|
+
<Fragment key={msg.id}>
|
|
76
|
+
<ChatMessageItem
|
|
77
|
+
msg={msg}
|
|
78
|
+
isStreaming={isLoading}
|
|
79
|
+
onUndo={onUndo}
|
|
80
|
+
onNewChat={onNewChat}
|
|
81
|
+
isPrevAssistant={isPrevAssistant}
|
|
82
|
+
className={isNoticeRow(msg) ? 'mt-3 mb-1' : isAiFragment ? 'mt-1' : 'mt-3'}
|
|
83
|
+
/>
|
|
84
|
+
{footer && (
|
|
85
|
+
<RunFooter
|
|
86
|
+
msg={footer.msg}
|
|
87
|
+
provider={provider}
|
|
88
|
+
providerNames={providerNames}
|
|
89
|
+
modelName={modelName}
|
|
90
|
+
modelNames={modelNames}
|
|
91
|
+
thinkingLevel={thinkingLevel}
|
|
92
|
+
durationMs={footer.durationMs}
|
|
93
|
+
onRetry={onRetry}
|
|
94
|
+
onNewChat={onNewChat}
|
|
95
|
+
isMobile={isMobile}
|
|
96
|
+
/>
|
|
97
|
+
)}
|
|
98
|
+
</Fragment>
|
|
118
99
|
);
|
|
119
100
|
})}
|
|
120
101
|
</>
|