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
|
@@ -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
|
-
|
|
@@ -53,6 +53,9 @@ export function FileTreeItem({
|
|
|
53
53
|
const gitStatusInfo = gitChange ? getGitStatusInfo(gitChange.status, gitChange.staged) : null;
|
|
54
54
|
const folderStatus = isFolder ? (gitFolderMap?.get(normalizedPath) || gitFolderMap?.get(file.path)) : null;
|
|
55
55
|
const hasGitStatus = Boolean(gitStatusInfo);
|
|
56
|
+
// Git-ignored entries (local .gitignore or the global excludes file) are still
|
|
57
|
+
// listed but rendered faded, the way an editor dims untracked noise.
|
|
58
|
+
const isIgnored = file.ignored === true;
|
|
56
59
|
|
|
57
60
|
const isExpanded = expandedPaths ? expandedPaths.has(file.path) : isOpen;
|
|
58
61
|
const actualIsOpen = file.forceExpanded !== undefined ? file.forceExpanded : isExpanded;
|
|
@@ -194,8 +197,11 @@ export function FileTreeItem({
|
|
|
194
197
|
<span className="w-3 flex-shrink-0"></span>
|
|
195
198
|
)}
|
|
196
199
|
|
|
197
|
-
<FileIcon name={file.name} isFolder={isFolder} isOpen={actualIsOpen} size={12} className=
|
|
198
|
-
<span
|
|
200
|
+
<FileIcon name={file.name} isFolder={isFolder} isOpen={actualIsOpen} size={12} className={`flex-shrink-0${isIgnored ? ' opacity-40' : ''}`} />
|
|
201
|
+
<span
|
|
202
|
+
className={`truncate min-w-0 flex-1 ${gitStatusInfo ? gitStatusInfo.colorClass : folderStatus ? folderStatus.colorClass : isIgnored ? 'text-ink/40' : ''}`}
|
|
203
|
+
title={isIgnored ? `${file.path} — git-ignored` : undefined}
|
|
204
|
+
>
|
|
199
205
|
{file.name}
|
|
200
206
|
</span>
|
|
201
207
|
|
|
@@ -3,6 +3,7 @@ import type { ChatMessageData, OmpAgentCallbacks, OmpAgentHandle, OmpAgentState,
|
|
|
3
3
|
import type { ExtensionUiDialogRequest } from '@/shared/types/omp/agent';
|
|
4
4
|
import type { ApprovalMode } from '@/shared/lib/omp/config/access-mode';
|
|
5
5
|
import { type ToolResultRecord, useOmpAgentStream } from '@/client/hooks/chat/omp/stream';
|
|
6
|
+
import { stopAgentSession } from '@/shared/lib/chat/omp/abort';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Live omp agent bridge for the chamber chat (real mode, MOCK=false). Mirrors
|
|
@@ -72,12 +73,14 @@ export function useOmpAgent(sessionId: string | null, callbacks: OmpAgentCallbac
|
|
|
72
73
|
.then(res => (res.ok ? res.json() : null))
|
|
73
74
|
.then((data: {
|
|
74
75
|
running?: boolean;
|
|
76
|
+
/** Set when the server answered from local flags, not a get_state RPC. */
|
|
77
|
+
busy?: boolean;
|
|
75
78
|
state?: { isStreaming?: boolean; isPromptRunning?: boolean };
|
|
76
79
|
pendingUiRequests?: ExtensionUiDialogRequest[];
|
|
77
80
|
} | null) => {
|
|
78
81
|
if (cancelled || !data?.running) return;
|
|
79
82
|
const probe = data.state;
|
|
80
|
-
if (
|
|
83
|
+
if (data.busy || probe?.isStreaming || probe?.isPromptRunning) {
|
|
81
84
|
connect(sessionId);
|
|
82
85
|
callbacksRef.current.onResumeStream?.();
|
|
83
86
|
}
|
|
@@ -132,6 +135,8 @@ export function useOmpAgent(sessionId: string | null, callbacks: OmpAgentCallbac
|
|
|
132
135
|
});
|
|
133
136
|
const body = (await res.json().catch(() => ({}))) as { success?: boolean; error?: string };
|
|
134
137
|
if (!res.ok || body.error) {
|
|
138
|
+
// `session_busy`: the ack timed out behind a still-running turn, so the
|
|
139
|
+
// prompt may already be accepted — never resend it automatically.
|
|
135
140
|
setState((prev) => ({ ...prev, isGenerating: false, error: body.error ?? `HTTP ${res.status}` }));
|
|
136
141
|
return false;
|
|
137
142
|
}
|
|
@@ -209,15 +214,9 @@ export function useOmpAgent(sessionId: string | null, callbacks: OmpAgentCallbac
|
|
|
209
214
|
// A user stop supersedes an in-flight interrupt-and-reply: clearing the
|
|
210
215
|
// guard lets the aborted turn's agent_end reach onAgentEnd (no stuck spinner).
|
|
211
216
|
interruptPendingRef.current = false;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
headers: { 'Content-Type': 'application/json' },
|
|
216
|
-
body: JSON.stringify({ type: 'abort' }),
|
|
217
|
-
});
|
|
218
|
-
} catch {
|
|
219
|
-
// Abort is best-effort; the event stream surfaces the terminal state.
|
|
220
|
-
}
|
|
217
|
+
// Self-escalating: a child that does not stop within the helper's grace
|
|
218
|
+
// period is reset instead, so a wedged session cannot hang the button.
|
|
219
|
+
await stopAgentSession(sid);
|
|
221
220
|
setState((prev) => ({ ...prev, isGenerating: false }));
|
|
222
221
|
}, []);
|
|
223
222
|
|
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
* Throttled revalidation for the session sidebars.
|
|
8
8
|
*
|
|
9
9
|
* `omp:session-updated` arrives in bursts while a run is active (agent start,
|
|
10
|
-
* JSONL write, spawn adoption, agent end) — and
|
|
11
|
-
* retry loop can emit one every 250ms until the
|
|
12
|
-
* turn. Each dispatch would otherwise trigger a
|
|
13
|
-
* with two sidebars mounted (desktop + mobile
|
|
14
|
-
* run could revalidate several times per
|
|
10
|
+
* first completed assistant turn, JSONL write, spawn adoption, agent end) — and
|
|
11
|
+
* the chat timeline's metadata retry loop can emit one every 250ms until the
|
|
12
|
+
* session file carries the user turn. Each dispatch would otherwise trigger a
|
|
13
|
+
* full loader revalidation, and with two sidebars mounted (desktop + mobile
|
|
14
|
+
* layouts keep theirs alive) a busy run could revalidate several times per
|
|
15
|
+
* second.
|
|
15
16
|
*
|
|
16
17
|
* Implemented as a LEADING + TRAILING throttle, not a trailing debounce:
|
|
17
18
|
*
|
|
@@ -16,7 +16,6 @@ import type { Dispatch, SetStateAction } from 'preact/compat';
|
|
|
16
16
|
import type { Attachment, ChatMessageData, OmpAgentHandle, QueuedMessageModel } from '@/shared/types';
|
|
17
17
|
import type { QueuedMessage } from '@/client/components/workspace/chat-timeline/QueueList';
|
|
18
18
|
import type { ApprovalMode } from '@/shared/lib/omp/config/access-mode';
|
|
19
|
-
import { normalizeNoticePositions } from '@/shared/lib/chat/order';
|
|
20
19
|
|
|
21
20
|
export interface ChatTimelineActionsDeps {
|
|
22
21
|
inputValue: string;
|
|
@@ -211,7 +210,7 @@ export function useChatTimelineActions(deps: ChatTimelineActionsDeps): ChatTimel
|
|
|
211
210
|
if (!res.ok) return false;
|
|
212
211
|
const next = await fetch(`/api/chat/${encodeURIComponent(sessionId)}`).then(r => r.json()).catch(() => null);
|
|
213
212
|
const messages: ChatMessageData[] = next?.session?.messages ?? [];
|
|
214
|
-
if (messages.length > 0) setLocalMessages(
|
|
213
|
+
if (messages.length > 0) setLocalMessages(messages);
|
|
215
214
|
return true;
|
|
216
215
|
}
|
|
217
216
|
|
|
@@ -99,6 +99,10 @@ export function useChatTimeline({ folders = [], appSettings = {} }: UseChatTimel
|
|
|
99
99
|
// responding (agent_start = first chunk) — the omp JSONL now carries the
|
|
100
100
|
// user turn, so the sidebar item + real title appear immediately.
|
|
101
101
|
const metaRefreshedRef = useRef<string | null>(null);
|
|
102
|
+
// Per-run guard for the "first assistant answer landed" sidebar signal
|
|
103
|
+
// (omp-callbacks onMessageEnd): the sidebar refreshes once per run on the
|
|
104
|
+
// completed first assistant turn, not on every assistant segment.
|
|
105
|
+
const firstAssistantRef = useRef(false);
|
|
102
106
|
// Optimistic user bubble awaiting omp's echo (reconciled by the callbacks).
|
|
103
107
|
const optimisticUserIdRef = useRef<string | null>(null);
|
|
104
108
|
// Raw composer text for steer/follow-up echoes (those paths have no bubble).
|
|
@@ -193,6 +197,7 @@ export function useChatTimeline({ folders = [], appSettings = {} }: UseChatTimel
|
|
|
193
197
|
adoptedSessionIdRef,
|
|
194
198
|
sessionIdRef,
|
|
195
199
|
metaRefreshedRef,
|
|
200
|
+
firstAssistantRef,
|
|
196
201
|
refreshSessionMeta,
|
|
197
202
|
setLocalMessages,
|
|
198
203
|
aiPlaceholderIdRef,
|
|
@@ -18,7 +18,6 @@ import { streamChatResponse } from '@/client/hooks/chat/stream';
|
|
|
18
18
|
import { composeMessageWithTextAttachments, isTextAttachmentFile } from '@/shared/lib/chat/attachments';
|
|
19
19
|
import { loadAgentNames } from '@/shared/lib/chat/composer/client';
|
|
20
20
|
import { translateAgentMentions, translateFileMentions } from '@/shared/lib/chat/composer/translate';
|
|
21
|
-
import { normalizeNoticePositions } from '@/shared/lib/chat/order';
|
|
22
21
|
import { createMockStreamCallbacks } from '@/shared/lib/chat/timeline/stream-callbacks';
|
|
23
22
|
import { PHASE_VERBS } from '@/shared/lib/chat/timeline/tool-phrases';
|
|
24
23
|
import { formatClock } from '@/shared/lib/format/time';
|
|
@@ -213,7 +212,7 @@ export function useChatTimelineSend(deps: ChatTimelineSendDeps): ChatTimelineSen
|
|
|
213
212
|
};
|
|
214
213
|
|
|
215
214
|
setLocalMessages(prev => {
|
|
216
|
-
const next =
|
|
215
|
+
const next = [...prev, newUserMsg, initialAiMsg];
|
|
217
216
|
persistMessages(next.filter(m => m.id !== aiPlaceholderId));
|
|
218
217
|
return next;
|
|
219
218
|
});
|