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
|
@@ -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
|
</>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ChatMessageData } from '@/shared/types';
|
|
2
|
+
import { AiMessageFooter } from '@/client/components/workspace/chat-timeline/AiMessageFooter';
|
|
3
|
+
import { messageAnswerText } from '@/shared/lib/chat/notice-row';
|
|
4
|
+
import { formatMessageStamp } from '@/shared/lib/format/time';
|
|
5
|
+
|
|
6
|
+
interface RunFooterProps {
|
|
7
|
+
/** The run's last real AI message (`RunFooterSlot.msg`). */
|
|
8
|
+
msg: ChatMessageData;
|
|
9
|
+
/** Session-level provider, used when the turn recorded none. */
|
|
10
|
+
provider?: string;
|
|
11
|
+
providerNames?: Record<string, string>;
|
|
12
|
+
modelName?: string;
|
|
13
|
+
/** id → display-name map from the model catalog. */
|
|
14
|
+
modelNames?: Record<string, string>;
|
|
15
|
+
/** Session thinking level, used when the turn recorded none. */
|
|
16
|
+
thinkingLevel?: string;
|
|
17
|
+
durationMs?: number | null;
|
|
18
|
+
onRetry?: (msgId: string) => void;
|
|
19
|
+
onNewChat?: (content: string) => void;
|
|
20
|
+
/** Mobile uses a compact provider/model footer with a bottom-sheet menu. */
|
|
21
|
+
isMobile?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Boundary footer of an AI response run: the timeline renders it as the run's
|
|
26
|
+
* own row — after every row the run owns, immediately before the next user
|
|
27
|
+
* message — never inside the answer bubble. The slot's AI message supplies the
|
|
28
|
+
* turn's model/usage/content.
|
|
29
|
+
*/
|
|
30
|
+
export function RunFooter({
|
|
31
|
+
msg,
|
|
32
|
+
provider,
|
|
33
|
+
providerNames,
|
|
34
|
+
modelName,
|
|
35
|
+
modelNames,
|
|
36
|
+
thinkingLevel,
|
|
37
|
+
durationMs,
|
|
38
|
+
onRetry,
|
|
39
|
+
onNewChat,
|
|
40
|
+
isMobile = false,
|
|
41
|
+
}: RunFooterProps) {
|
|
42
|
+
return (
|
|
43
|
+
<div className="mt-2">
|
|
44
|
+
<AiMessageFooter
|
|
45
|
+
provider={msg.provider || provider}
|
|
46
|
+
providerNames={providerNames}
|
|
47
|
+
currentModel={(msg.model ? (modelNames?.[msg.model] ?? msg.model) : '') || modelName || ''}
|
|
48
|
+
thinkingLevel={msg.thinkingLevel ?? thinkingLevel}
|
|
49
|
+
dateStr={formatMessageStamp(msg)}
|
|
50
|
+
durationMs={durationMs ?? msg.durationMs}
|
|
51
|
+
usage={msg.usage}
|
|
52
|
+
content={messageAnswerText(msg)}
|
|
53
|
+
msgId={msg.id}
|
|
54
|
+
onRetry={onRetry}
|
|
55
|
+
onNewChat={onNewChat}
|
|
56
|
+
isMobile={isMobile}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fragment } from 'preact';
|
|
2
|
+
import { useEffect, useMemo, useRef } from 'preact/hooks';
|
|
2
3
|
import { ArrowLeft, Bot, Loader2, Lock } from 'lucide-preact';
|
|
3
4
|
import type { SubagentInfo } from '@/shared/types';
|
|
4
5
|
import { SubagentStatusIcon } from '@/client/components/common/SubagentStatusIcon';
|
|
5
6
|
import { ChatMessageItem } from '@/client/components/workspace/chat-timeline/MessageItem';
|
|
7
|
+
import { RunFooter } from '@/client/components/workspace/chat-timeline/RunFooter';
|
|
6
8
|
import { useSubagentTranscript } from '@/client/hooks/chat/subagent';
|
|
7
9
|
import { useModelNames } from '@/client/hooks/models/use-model-names';
|
|
10
|
+
import { isNoticeRow } from '@/shared/lib/chat/notice-row';
|
|
11
|
+
import { previousNonNoticeIndex, resolveRunFooters, streamingRowIndex } from '@/shared/lib/chat/timeline/run-footer';
|
|
8
12
|
import { formatCompactTokens } from '@/shared/lib/format/number';
|
|
9
13
|
|
|
10
14
|
interface SubagentViewProps {
|
|
@@ -43,6 +47,17 @@ export function SubagentView({ sessionId, subagent, onBack, provider, providerNa
|
|
|
43
47
|
return () => cancelAnimationFrame(frame);
|
|
44
48
|
}, [messages.length, isRunning]);
|
|
45
49
|
|
|
50
|
+
// Boundary footers: a run's footer renders after its last row, so trailing
|
|
51
|
+
// notice rows never separate it from the next user message.
|
|
52
|
+
const { streamingIdx, footers, prevNonNoticeIdx } = useMemo(
|
|
53
|
+
() => ({
|
|
54
|
+
streamingIdx: streamingRowIndex(messages),
|
|
55
|
+
footers: resolveRunFooters(messages, isRunning),
|
|
56
|
+
prevNonNoticeIdx: previousNonNoticeIndex(messages),
|
|
57
|
+
}),
|
|
58
|
+
[messages, isRunning],
|
|
59
|
+
);
|
|
60
|
+
|
|
46
61
|
if (!subagent || !isActive) return null;
|
|
47
62
|
|
|
48
63
|
const progress = status?.progress ?? subagent.progress;
|
|
@@ -104,30 +119,31 @@ export function SubagentView({ sessionId, subagent, onBack, provider, providerNa
|
|
|
104
119
|
</div>
|
|
105
120
|
) : (
|
|
106
121
|
messages.map((msg, idx) => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const isLoading = isRunning && idx === lastAiIdx && msg.role !== 'user' && !msg.notice;
|
|
110
|
-
const nextReal = messages.slice(idx + 1).find((m) => !m.notice);
|
|
111
|
-
const isLastAi = msg.role !== 'user' && !msg.notice && (!nextReal || nextReal.role === 'user');
|
|
112
|
-
let prevRealIdx = idx - 1;
|
|
113
|
-
while (prevRealIdx >= 0 && messages[prevRealIdx].notice) prevRealIdx--;
|
|
122
|
+
const isLoading = isRunning && idx === streamingIdx && msg.role === 'ai';
|
|
123
|
+
const prevRealIdx = prevNonNoticeIdx[idx];
|
|
114
124
|
const prevReal = prevRealIdx >= 0 ? messages[prevRealIdx] : null;
|
|
115
125
|
const isPrevAssistant = Boolean(msg.role !== 'user' && prevReal && prevReal.role !== 'user');
|
|
126
|
+
const footer = footers[idx];
|
|
116
127
|
|
|
117
128
|
return (
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
<Fragment key={msg.id}>
|
|
130
|
+
<ChatMessageItem
|
|
131
|
+
msg={msg}
|
|
132
|
+
isStreaming={isLoading}
|
|
133
|
+
isPrevAssistant={isPrevAssistant}
|
|
134
|
+
className={isNoticeRow(msg) ? 'mt-3 mb-1' : isPrevAssistant ? 'mt-1' : 'mt-3'}
|
|
135
|
+
/>
|
|
136
|
+
{footer && (
|
|
137
|
+
<RunFooter
|
|
138
|
+
msg={footer.msg}
|
|
139
|
+
provider={provider}
|
|
140
|
+
providerNames={providerNames}
|
|
141
|
+
modelName={modelName}
|
|
142
|
+
modelNames={modelNames}
|
|
143
|
+
durationMs={footer.durationMs}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
146
|
+
</Fragment>
|
|
131
147
|
);
|
|
132
148
|
})
|
|
133
149
|
)}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { useCallback, useMemo } from 'preact/hooks';
|
|
2
2
|
import { memo } from 'preact/compat';
|
|
3
|
-
import { Boxes, Brain, BrainCircuit, Camera, Check, Code2, Cpu, FileCode, FileText, GitPullRequest, Globe, HelpCircle, ListTodo, Search, Server, Shield, Terminal, Wrench } from 'lucide-preact';
|
|
3
|
+
import { Bell, Boxes, Brain, BrainCircuit, Camera, Check, Code2, Cpu, FileCode, FileText, GitPullRequest, Globe, HelpCircle, ListTodo, Search, Server, Shield, Terminal, Wrench } from 'lucide-preact';
|
|
4
4
|
import type { ToolCallData } from '@/shared/types';
|
|
5
|
+
import { stripAnsiCodes } from '@/shared/lib/code/ansi';
|
|
6
|
+
import { isReminderTag, unwrapXmlEnvelope } from '@/shared/lib/chat/xml-envelope';
|
|
5
7
|
import { CopyButton } from '@/client/components/common/CopyButton';
|
|
6
8
|
import { ToolCardShell } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/ToolCardShell';
|
|
7
9
|
import { DiffView } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView';
|
|
@@ -154,6 +156,13 @@ export const ToolCallCard = memo(function ToolCallCard({ tool, isOpen, onToggle,
|
|
|
154
156
|
[hasPanel, commandOrInput]
|
|
155
157
|
);
|
|
156
158
|
|
|
159
|
+
// A reminder envelope in the result is the runtime interrupting the call, not
|
|
160
|
+
// its outcome — the header flags it next to the status badge.
|
|
161
|
+
const isReminder = useMemo(() => {
|
|
162
|
+
const envelope = unwrapXmlEnvelope(stripAnsiCodes(outputText));
|
|
163
|
+
return Boolean(envelope && isReminderTag(envelope.tag));
|
|
164
|
+
}, [outputText]);
|
|
165
|
+
|
|
157
166
|
// Clean title & subtitle extraction
|
|
158
167
|
let displayTitle = '';
|
|
159
168
|
let displaySubtitle: string | undefined;
|
|
@@ -226,9 +235,18 @@ export const ToolCallCard = memo(function ToolCallCard({ tool, isOpen, onToggle,
|
|
|
226
235
|
? targetFilePath
|
|
227
236
|
: displaySubtitle || (tool.detail && !targetFilePath ? tool.detail : undefined);
|
|
228
237
|
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
238
|
+
const duration = tool.duration || tool.time;
|
|
239
|
+
const meta =
|
|
240
|
+
duration || isReminder ? (
|
|
241
|
+
<span className="flex items-center gap-2">
|
|
242
|
+
{duration ? <span className="font-mono text-[10px] text-ink/40">{duration}</span> : null}
|
|
243
|
+
{isReminder && (
|
|
244
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-error/10 px-2 py-0.5 text-[10px] font-semibold text-error">
|
|
245
|
+
<Bell size={10} /> Reminder
|
|
246
|
+
</span>
|
|
247
|
+
)}
|
|
248
|
+
</span>
|
|
249
|
+
) : null;
|
|
232
250
|
|
|
233
251
|
return (
|
|
234
252
|
<ToolCardShell
|
|
@@ -16,7 +16,6 @@ import { useModelNames } from '@/client/hooks/models/use-model-names';
|
|
|
16
16
|
import { useProviderNames } from '@/client/hooks/models/use-provider-names';
|
|
17
17
|
import { useToasts } from '@/client/hooks/ui/toasts';
|
|
18
18
|
import { ToastStack } from '@/client/components/common/ToastStack';
|
|
19
|
-
import { normalizeNoticePositions } from '@/shared/lib/chat/order';
|
|
20
19
|
import { composerRootFor } from '@/shared/lib/workspace/active-project';
|
|
21
20
|
import { useSidebarData } from '@/client/hooks/chat/omp/session-list';
|
|
22
21
|
|
|
@@ -125,8 +124,6 @@ export function ChatTimeline({ className = '', appSettings = {}, onSessionTitle,
|
|
|
125
124
|
? (modelNames[sessionData.model.modelId] ?? sessionData.model.modelId)
|
|
126
125
|
: sessionData?.model;
|
|
127
126
|
|
|
128
|
-
const orderedMessages = useMemo(() => normalizeNoticePositions(localMessages), [localMessages]);
|
|
129
|
-
|
|
130
127
|
// A pending "new-…" session has no messages yet, so the workspace picker
|
|
131
128
|
// must stay available until the first chat is sent (which spawns the real
|
|
132
129
|
// omp session). Never lock the user into a folder before sending.
|
|
@@ -137,7 +134,7 @@ export function ChatTimeline({ className = '', appSettings = {}, onSessionTitle,
|
|
|
137
134
|
// covers the whole chat timeline (body + composer) so a session switch shows
|
|
138
135
|
// one coherent placeholder instead of a half-drawn view. Skipped while an
|
|
139
136
|
// optimistic send owns the tail (fresh spawn adoption must keep its bubbles).
|
|
140
|
-
const showFullSkeleton = !isPendingSession && sessionLoading &&
|
|
137
|
+
const showFullSkeleton = !isPendingSession && sessionLoading && localMessages.length === 0;
|
|
141
138
|
|
|
142
139
|
if (!sessionId || isPendingSession) {
|
|
143
140
|
return (
|
|
@@ -154,7 +151,7 @@ export function ChatTimeline({ className = '', appSettings = {}, onSessionTitle,
|
|
|
154
151
|
onSend={handleSend}
|
|
155
152
|
isGenerating={isGenerating}
|
|
156
153
|
appSettings={appSettings}
|
|
157
|
-
|
|
154
|
+
messages={localMessages}
|
|
158
155
|
provider={sessionProvider}
|
|
159
156
|
providerNames={providerNames}
|
|
160
157
|
modelName={sessionModelName}
|
|
@@ -201,7 +198,7 @@ export function ChatTimeline({ className = '', appSettings = {}, onSessionTitle,
|
|
|
201
198
|
hasMore={hasMore}
|
|
202
199
|
loadOlderError={loadOlderError}
|
|
203
200
|
loadOlder={loadOlder}
|
|
204
|
-
messages={
|
|
201
|
+
messages={localMessages}
|
|
205
202
|
isGenerating={isGenerating}
|
|
206
203
|
provider={sessionProvider}
|
|
207
204
|
providerNames={providerNames}
|
|
@@ -4,9 +4,12 @@ import type { ToolCallData } from '@/shared/types';
|
|
|
4
4
|
import { CopyButton } from '@/client/components/common/CopyButton';
|
|
5
5
|
import { DiffView } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView';
|
|
6
6
|
import { HashlinePatch } from '@/client/components/workspace/chat-timeline/tool-renderers/hashline-patch';
|
|
7
|
-
import {
|
|
7
|
+
import { MarkdownRenderer } from '@/client/components/common/MarkdownRenderer';
|
|
8
|
+
import { EnvelopeHeader } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/EnvelopeHeader';
|
|
9
|
+
import { getLanguageFromPath, highlightCode } from '@/shared/lib/code/syntax-highlight';
|
|
8
10
|
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
9
11
|
import { MAX_OUTPUT_LINES, truncateTailLines } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/truncate';
|
|
12
|
+
import { outputMarkdown, readToolOutput } from '@/shared/lib/chat/tool-output';
|
|
10
13
|
import { hashlinePatchFromArgs } from '@/shared/lib/omp/session/hashline-patch';
|
|
11
14
|
import { formatBytes } from '@/shared/lib/format/number';
|
|
12
15
|
|
|
@@ -148,10 +151,11 @@ export function Edit({ tool }: { tool: ToolCallData }) {
|
|
|
148
151
|
() => (newContent ? highlightCode(newContent.split('\n').slice(0, 80).join('\n'), lang) : ''),
|
|
149
152
|
[newContent, lang, syntaxReady]
|
|
150
153
|
);
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
const outputText = useMemo(() => readToolOutput(output), [output]);
|
|
155
|
+
const truncatedOutput = useMemo(() => truncateTailLines(outputText.content, MAX_OUTPUT_LINES), [outputText]);
|
|
156
|
+
const outputBody = useMemo(
|
|
157
|
+
() => outputMarkdown(truncatedOutput.text, outputText.format),
|
|
158
|
+
[truncatedOutput, outputText.format]
|
|
155
159
|
);
|
|
156
160
|
|
|
157
161
|
return (
|
|
@@ -261,26 +265,18 @@ export function Edit({ tool }: { tool: ToolCallData }) {
|
|
|
261
265
|
</div>
|
|
262
266
|
)}
|
|
263
267
|
|
|
264
|
-
{/* Execution Result Notification */}
|
|
268
|
+
{/* Execution Result Notification — envelope XML dilepas, isinya markdown */}
|
|
265
269
|
{output && (
|
|
266
|
-
<div className="rounded-lg border border-ink/8 bg-paper p-2.5
|
|
270
|
+
<div className="rounded-lg border border-ink/8 bg-paper p-2.5 select-text">
|
|
267
271
|
<div className="flex items-center gap-1.5 mb-1.5 text-[9.5px] font-semibold uppercase tracking-wider text-ink/40">
|
|
268
272
|
<ArrowRight size={11} className="text-success" />
|
|
269
273
|
<span>Execution Output</span>
|
|
270
274
|
</div>
|
|
271
|
-
{
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
<div className="mb-1 font-mono text-[9.5px] text-ink/45">… {truncatedOutput.skipped} earlier lines hidden</div>
|
|
275
|
-
)}
|
|
276
|
-
<pre
|
|
277
|
-
className="max-h-56 overflow-auto rounded bg-canvas/40 p-2 text-ink/85 whitespace-pre leading-relaxed scrollbar-overlay-container scrollbar-overlay-static"
|
|
278
|
-
dangerouslySetInnerHTML={{ __html: highlightedOutput }}
|
|
279
|
-
/>
|
|
280
|
-
</>
|
|
281
|
-
) : (
|
|
282
|
-
<div className="text-ink/75 leading-relaxed whitespace-pre-wrap">{truncatedOutput.text}</div>
|
|
275
|
+
{outputText.envelope && <EnvelopeHeader envelope={outputText.envelope} />}
|
|
276
|
+
{truncatedOutput.skipped > 0 && (
|
|
277
|
+
<div className="mb-1 font-mono text-[9.5px] text-ink/45">… {truncatedOutput.skipped} earlier lines hidden</div>
|
|
283
278
|
)}
|
|
279
|
+
<MarkdownRenderer content={outputBody} className="text-[11.5px] text-ink/85" />
|
|
284
280
|
</div>
|
|
285
281
|
)}
|
|
286
282
|
</div>
|
package/src/client/components/workspace/chat-timeline/tool-renderers/shared/EnvelopeHeader.tsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AlertCircle, Bell, Boxes } from 'lucide-preact';
|
|
2
|
+
import { isReminderTag, type XmlEnvelope } from '@/shared/lib/chat/xml-envelope';
|
|
3
|
+
import { toTitleCase } from '@/shared/lib/chat/title-case';
|
|
4
|
+
|
|
5
|
+
/** Badge tone per wrapper family: a system reminder is advisory (warning), an
|
|
6
|
+
* error envelope is a failure, anything else is informational. */
|
|
7
|
+
function envelopeTone(tag: string) {
|
|
8
|
+
if (isReminderTag(tag)) {
|
|
9
|
+
return { icon: <Bell size={12} />, badge: 'bg-warning/10 text-warning' };
|
|
10
|
+
}
|
|
11
|
+
if (tag === 'error' || tag === 'failure') {
|
|
12
|
+
return { icon: <AlertCircle size={12} />, badge: 'bg-error/10 text-error' };
|
|
13
|
+
}
|
|
14
|
+
return { icon: <Boxes size={12} />, badge: 'bg-info/10 text-info' };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface EnvelopeHeaderProps {
|
|
18
|
+
envelope: XmlEnvelope;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Identity row for a peeled XML envelope: which wrapper carried the output and
|
|
22
|
+
* the attributes it declared — for a system reminder those are the rule that
|
|
23
|
+
* fired and the file behind it, which is the reason the wrapper existed. */
|
|
24
|
+
export function EnvelopeHeader({ envelope }: EnvelopeHeaderProps) {
|
|
25
|
+
const tone = envelopeTone(envelope.tag);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
29
|
+
<span className={`flex h-5 w-5 shrink-0 items-center justify-center rounded ${tone.badge}`}>
|
|
30
|
+
{tone.icon}
|
|
31
|
+
</span>
|
|
32
|
+
<span className="text-[9.5px] font-semibold uppercase tracking-[0.14em] text-ink/60">
|
|
33
|
+
{toTitleCase(envelope.tag)}
|
|
34
|
+
</span>
|
|
35
|
+
{Object.entries(envelope.attributes).map(([key, value]) => (
|
|
36
|
+
<span
|
|
37
|
+
key={key}
|
|
38
|
+
title={`${key}=${value}`}
|
|
39
|
+
className="max-w-[240px] truncate rounded bg-ink/5 px-1.5 py-0.5 font-mono text-[9.5px] text-ink/60"
|
|
40
|
+
>
|
|
41
|
+
{value ? `${key}=${value}` : key}
|
|
42
|
+
</span>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
package/src/client/components/workspace/chat-timeline/tool-renderers/shared/FallbackOutput.tsx
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import type { ReactNode } from 'preact/compat';
|
|
2
1
|
import { useMemo } from 'preact/hooks';
|
|
3
2
|
import { Code } from 'lucide-preact';
|
|
4
3
|
import { MarkdownRenderer } from '@/client/components/common/MarkdownRenderer';
|
|
5
|
-
import { sanitizeHtml } from '@/shared/lib/markdown/sanitize';
|
|
6
4
|
import { CopyButton } from '@/client/components/common/CopyButton';
|
|
5
|
+
import { EnvelopeHeader } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/EnvelopeHeader';
|
|
7
6
|
import { stripAnsiCodes } from '@/shared/lib/code/ansi';
|
|
8
|
-
import {
|
|
9
|
-
import { highlightCode, tryParseJson } from '@/shared/lib/code/syntax-highlight';
|
|
10
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
11
|
-
import { JsonCodeBlock } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock';
|
|
7
|
+
import { outputMarkdown, readToolOutput } from '@/shared/lib/chat/tool-output';
|
|
12
8
|
import { MAX_OUTPUT_LINES, truncateTailLines } from '@/client/components/workspace/chat-timeline/tool-renderers/shared/truncate';
|
|
13
9
|
|
|
14
10
|
interface FallbackOutputProps {
|
|
@@ -16,64 +12,16 @@ interface FallbackOutputProps {
|
|
|
16
12
|
text: string;
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
/**
|
|
20
|
-
*
|
|
15
|
+
/** Blok "Output" generik untuk panel tanpa renderer khusus. Output selalu
|
|
16
|
+
* dirender sebagai markdown: envelope XML yang membungkus seluruh output
|
|
17
|
+
* dilepas dulu, dan konten verbatim (log/JSON/HTML) dipagari fence supaya
|
|
18
|
+
* parser tidak meremasnya jadi satu paragraf. */
|
|
21
19
|
export function FallbackOutput({ text }: FallbackOutputProps) {
|
|
22
20
|
const stripped = useMemo(() => stripAnsiCodes(text), [text]);
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
return stripped.split(/\r?\n/).length;
|
|
28
|
-
}, [jsonResult, stripped]);
|
|
29
|
-
|
|
30
|
-
const display = useMemo(() => {
|
|
31
|
-
const source = format === 'json' && jsonResult?.isValid && jsonResult.pretty ? jsonResult.pretty : stripped;
|
|
32
|
-
return truncateTailLines(source, MAX_OUTPUT_LINES);
|
|
33
|
-
}, [format, jsonResult, stripped]);
|
|
34
|
-
|
|
35
|
-
const isPlainText = format !== 'markdown' && format !== 'json' && format !== 'html';
|
|
36
|
-
const syntaxReady = useSyntaxReady();
|
|
37
|
-
const highlightedText = useMemo(
|
|
38
|
-
() => (isPlainText ? highlightCode(display.text, 'javascript') : ''),
|
|
39
|
-
[isPlainText, display, syntaxReady]
|
|
40
|
-
);
|
|
41
|
-
const copyText = useMemo(
|
|
42
|
-
() => (jsonResult?.isValid && jsonResult.pretty ? jsonResult.pretty : stripped),
|
|
43
|
-
[jsonResult, stripped]
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
let body: ReactNode;
|
|
47
|
-
if (format === 'markdown') {
|
|
48
|
-
body = (
|
|
49
|
-
<div className="prose-content max-h-72 overflow-auto rounded-lg border border-ink/8 bg-paper px-3 py-2.5 text-[12px] leading-relaxed text-ink/85 select-text">
|
|
50
|
-
<MarkdownRenderer content={display.text} />
|
|
51
|
-
</div>
|
|
52
|
-
);
|
|
53
|
-
} else if (format === 'json') {
|
|
54
|
-
body = (
|
|
55
|
-
<JsonCodeBlock
|
|
56
|
-
jsonString={display.text}
|
|
57
|
-
maxHeightClass="max-h-72"
|
|
58
|
-
showHeader={false}
|
|
59
|
-
/>
|
|
60
|
-
);
|
|
61
|
-
} else if (format === 'html') {
|
|
62
|
-
const sanitized = sanitizeHtml(display.text);
|
|
63
|
-
body = (
|
|
64
|
-
<div
|
|
65
|
-
className="max-h-72 overflow-auto rounded-lg border border-ink/8 bg-paper px-3 py-2.5 text-[12px] leading-relaxed text-ink/85 select-text"
|
|
66
|
-
dangerouslySetInnerHTML={{ __html: sanitized }}
|
|
67
|
-
/>
|
|
68
|
-
);
|
|
69
|
-
} else {
|
|
70
|
-
body = (
|
|
71
|
-
<pre
|
|
72
|
-
className="max-h-72 overflow-auto rounded-lg border border-ink/8 bg-paper px-3 py-2.5 font-mono text-[11px] leading-relaxed whitespace-pre-wrap break-words text-ink/80 select-text"
|
|
73
|
-
dangerouslySetInnerHTML={{ __html: highlightedText }}
|
|
74
|
-
/>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
21
|
+
const output = useMemo(() => readToolOutput(stripped), [stripped]);
|
|
22
|
+
const display = useMemo(() => truncateTailLines(output.content, MAX_OUTPUT_LINES), [output]);
|
|
23
|
+
const markdown = useMemo(() => outputMarkdown(display.text, output.format), [display, output.format]);
|
|
24
|
+
const lines = useMemo(() => output.content.split(/\r?\n/).length, [output.content]);
|
|
77
25
|
|
|
78
26
|
return (
|
|
79
27
|
<div className="space-y-1.5">
|
|
@@ -82,26 +30,28 @@ export function FallbackOutput({ text }: FallbackOutputProps) {
|
|
|
82
30
|
<Code size={11} className="text-ink/40" />
|
|
83
31
|
<span>Output</span>
|
|
84
32
|
<span className="rounded bg-ink/5 px-1.5 py-0.2 font-mono text-[9px] text-ink/45">
|
|
85
|
-
{format}
|
|
33
|
+
{output.format}
|
|
86
34
|
</span>
|
|
87
|
-
{
|
|
35
|
+
{lines > 1 && (
|
|
88
36
|
<span className="font-mono text-[9px] text-ink/35">
|
|
89
|
-
{
|
|
37
|
+
{lines} lines
|
|
90
38
|
</span>
|
|
91
39
|
)}
|
|
92
40
|
</div>
|
|
93
41
|
<CopyButton
|
|
94
|
-
text={
|
|
42
|
+
text={output.content}
|
|
95
43
|
className="flex cursor-pointer items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-ink/45 transition-colors hover:bg-ink/5 hover:text-ink"
|
|
96
44
|
onClick={(e) => e.stopPropagation()}
|
|
97
45
|
label="Copy"
|
|
98
46
|
/>
|
|
99
47
|
</div>
|
|
48
|
+
{output.envelope && <EnvelopeHeader envelope={output.envelope} />}
|
|
100
49
|
{display.skipped > 0 && (
|
|
101
50
|
<div className="font-mono text-[10px] text-ink/45">… {display.skipped} earlier lines hidden</div>
|
|
102
51
|
)}
|
|
103
|
-
|
|
52
|
+
<div className="max-h-72 overflow-auto rounded-lg border border-ink/8 bg-paper px-3 py-2.5 select-text">
|
|
53
|
+
<MarkdownRenderer content={markdown} className="text-[12px] text-ink/85" />
|
|
54
|
+
</div>
|
|
104
55
|
</div>
|
|
105
56
|
);
|
|
106
57
|
}
|
|
107
|
-
|