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
|
@@ -5,12 +5,16 @@ import { useFetcher } from '@/client/lib/router/fetcher';
|
|
|
5
5
|
import { FileIcon } from '@/client/components/common/file-icon';
|
|
6
6
|
import { FileContextMenu, FileDeleteModal, FileHistoryModal, FileRenameModal } from '@/client/components/workspace/file-explorer/Modals';
|
|
7
7
|
import { getGitStatusInfo, type FolderGitStatusInfo } from '@/shared/lib/fs/git-status';
|
|
8
|
+
import { toAbsolutePath } from '@/shared/lib/fs/paths';
|
|
9
|
+
import { copyToClipboard } from '@/client/hooks/ui/clipboard';
|
|
8
10
|
import type { GitChange } from '@/shared/types/git';
|
|
9
11
|
import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
|
|
10
12
|
|
|
11
13
|
interface FileTreeItemProps {
|
|
12
14
|
file: any;
|
|
13
15
|
rootPath?: string;
|
|
16
|
+
/** Absolute base dir of the listing (server-reported `root`), anchors Copy Path. */
|
|
17
|
+
basePath?: string;
|
|
14
18
|
repo?: string;
|
|
15
19
|
onLoadChildren?: (path: string) => Promise<void>;
|
|
16
20
|
onOpenFile?: (file: any) => void;
|
|
@@ -24,6 +28,7 @@ interface FileTreeItemProps {
|
|
|
24
28
|
export function FileTreeItem({
|
|
25
29
|
file,
|
|
26
30
|
rootPath,
|
|
31
|
+
basePath,
|
|
27
32
|
repo,
|
|
28
33
|
onLoadChildren,
|
|
29
34
|
onOpenFile,
|
|
@@ -53,6 +58,9 @@ export function FileTreeItem({
|
|
|
53
58
|
const gitStatusInfo = gitChange ? getGitStatusInfo(gitChange.status, gitChange.staged) : null;
|
|
54
59
|
const folderStatus = isFolder ? (gitFolderMap?.get(normalizedPath) || gitFolderMap?.get(file.path)) : null;
|
|
55
60
|
const hasGitStatus = Boolean(gitStatusInfo);
|
|
61
|
+
// Git-ignored entries (local .gitignore or the global excludes file) are still
|
|
62
|
+
// listed but rendered faded, the way an editor dims untracked noise.
|
|
63
|
+
const isIgnored = file.ignored === true;
|
|
56
64
|
|
|
57
65
|
const isExpanded = expandedPaths ? expandedPaths.has(file.path) : isOpen;
|
|
58
66
|
const actualIsOpen = file.forceExpanded !== undefined ? file.forceExpanded : isExpanded;
|
|
@@ -148,9 +156,9 @@ export function FileTreeItem({
|
|
|
148
156
|
fd.append('path', file.path);
|
|
149
157
|
submitAction(fd);
|
|
150
158
|
} else if (actionType === 'copy_path') {
|
|
151
|
-
|
|
159
|
+
void copyToClipboard(toAbsolutePath(basePath, file.path));
|
|
152
160
|
} else if (actionType === 'copy_relative') {
|
|
153
|
-
|
|
161
|
+
void copyToClipboard(file.path);
|
|
154
162
|
} else if (actionType === 'history') {
|
|
155
163
|
setShowHistoryModal(true);
|
|
156
164
|
const fd = new FormData();
|
|
@@ -194,8 +202,11 @@ export function FileTreeItem({
|
|
|
194
202
|
<span className="w-3 flex-shrink-0"></span>
|
|
195
203
|
)}
|
|
196
204
|
|
|
197
|
-
<FileIcon name={file.name} isFolder={isFolder} isOpen={actualIsOpen} size={12} className=
|
|
198
|
-
<span
|
|
205
|
+
<FileIcon name={file.name} isFolder={isFolder} isOpen={actualIsOpen} size={12} className={`flex-shrink-0${isIgnored ? ' opacity-40' : ''}`} />
|
|
206
|
+
<span
|
|
207
|
+
className={`truncate min-w-0 flex-1 ${gitStatusInfo ? gitStatusInfo.colorClass : folderStatus ? folderStatus.colorClass : isIgnored ? 'text-ink/40' : ''}`}
|
|
208
|
+
title={isIgnored ? `${file.path} — git-ignored` : undefined}
|
|
209
|
+
>
|
|
199
210
|
{file.name}
|
|
200
211
|
</span>
|
|
201
212
|
|
|
@@ -230,6 +241,7 @@ export function FileTreeItem({
|
|
|
230
241
|
key={child.id}
|
|
231
242
|
file={child}
|
|
232
243
|
rootPath={rootPath}
|
|
244
|
+
basePath={basePath}
|
|
233
245
|
repo={repo}
|
|
234
246
|
onLoadChildren={onLoadChildren}
|
|
235
247
|
onOpenFile={onOpenFile}
|
|
@@ -10,6 +10,10 @@ import { usePanelRefresh, useFileMutationRefresh } from '@/client/hooks/workspac
|
|
|
10
10
|
|
|
11
11
|
export function FileExplorer({ className = '', enabled = true, rootPath, onOpenFile, refreshKey = 0, onRefresh }: { className?: string, enabled?: boolean, rootPath?: string, onOpenFile?: (file: any) => void, refreshKey?: number, onRefresh?: () => void }) {
|
|
12
12
|
const [tree, setTree] = useState<any[]>([]);
|
|
13
|
+
// Absolute base dir reported by `/api/fs/dir` for the current listing — the
|
|
14
|
+
// workspace root, or the selected nested repo. Copy Path anchors on it
|
|
15
|
+
// because the client's `rootPath` may be rejected/unset.
|
|
16
|
+
const [listingRoot, setListingRoot] = useState('');
|
|
13
17
|
const [searchQuery, setSearchQuery] = useSessionState<string>('files.searchQuery', '');
|
|
14
18
|
const [isLoading, setIsLoading] = useState(false);
|
|
15
19
|
const [storedExpandedPaths, setStoredExpandedPaths, expandedPathsReady] = useSessionState<string[]>('files.expandedPaths', []);
|
|
@@ -43,6 +47,7 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
43
47
|
fetch(listUrl())
|
|
44
48
|
.then(r => r.json())
|
|
45
49
|
.then(data => {
|
|
50
|
+
if (typeof data.root === 'string') setListingRoot(data.root);
|
|
46
51
|
if (Array.isArray(data.files)) {
|
|
47
52
|
setTree(rehydrateTree(data.files, childrenCacheRef.current, expandedPaths));
|
|
48
53
|
}
|
|
@@ -176,6 +181,7 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
176
181
|
key={file.id}
|
|
177
182
|
file={file}
|
|
178
183
|
rootPath={rootPath}
|
|
184
|
+
basePath={listingRoot}
|
|
179
185
|
repo={activeRepo}
|
|
180
186
|
onLoadChildren={loadChildren}
|
|
181
187
|
onOpenFile={onOpenFile}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { AIModelOption } from '@/shared/types';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* MOCK-only demo catalog: the model rows a `MOCK=true` chamber shows before the
|
|
5
|
+
* operator adds any of their own.
|
|
6
|
+
*
|
|
7
|
+
* Real mode never reads this — `/api/models` there is fed by the live omp model
|
|
8
|
+
* registry, and its mutations start from an empty list — so a demo model can
|
|
9
|
+
* never leak into a real installation (see `routes/models/root.ts`).
|
|
10
|
+
*/
|
|
3
11
|
export const INITIAL_MODELS_CATALOG: AIModelOption[] = [
|
|
4
12
|
// FAVORITES (As seen in image.png)
|
|
5
13
|
{
|
|
@@ -277,7 +277,7 @@ export function getSampleDialogueSession(): {
|
|
|
277
277
|
id: '65e71033',
|
|
278
278
|
role: 'ai',
|
|
279
279
|
content:
|
|
280
|
-
'Repo summary from `RepoSummarizer`:\n\n**OMPChamber** — developer web-view console and diagnostic chamber for AI **Oh-My-Pi** autonomous agent.\n\n**Stack:** Remix v2 + React 19 + Vite, Tailwind CSS v4, TypeScript (strict, ES2022). Runs under npm or Bun.\n\n**Structure:**\n- `app/routes/` — Remix flat-routes, large REST-style `api/` domain tree\n- `app/components/` — common, layout, workspace, mobile, settings\n- `app/hooks/` — chat, terminal, git-tree, agent streaming\n- `app/lib/omp/` — bridges to oh-my-pi runtime (RPC managers, session readers)\n- `app/types/`, `app/data/` (mock presets)\n\n**Config:** `MOCK` mode toggles seeded demo data vs real data. SQLite persistence (`app/db.server.ts`).\n\n**Deployment-relevant:** PWA support,
|
|
280
|
+
'Repo summary from `RepoSummarizer`:\n\n**OMPChamber** — developer web-view console and diagnostic chamber for AI **Oh-My-Pi** autonomous agent.\n\n**Stack:** Remix v2 + React 19 + Vite, Tailwind CSS v4, TypeScript (strict, ES2022). Runs under npm or Bun.\n\n**Structure:**\n- `app/routes/` — Remix flat-routes, large REST-style `api/` domain tree\n- `app/components/` — common, layout, workspace, mobile, settings\n- `app/hooks/` — chat, terminal, git-tree, agent streaming\n- `app/lib/omp/` — bridges to oh-my-pi runtime (RPC managers, session readers)\n- `app/types/`, `app/data/` (mock presets)\n\n**Config:** `MOCK` mode toggles seeded demo data vs real data. SQLite persistence (`app/db.server.ts`).\n\n**Deployment-relevant:** PWA support, xterm/KaTeX/Shiki rendering.',
|
|
281
281
|
date: 'Sep 9, 04:44 AM',
|
|
282
282
|
},
|
|
283
283
|
],
|
|
@@ -88,7 +88,7 @@ export const otherTools: ToolCallData[] = [
|
|
|
88
88
|
target: 'app/mock.server.ts',
|
|
89
89
|
output: `1: export function isMockMode(): boolean {
|
|
90
90
|
2: const envVal = (Bun.env.MOCK || "").trim().toLowerCase();
|
|
91
|
-
3: return envVal
|
|
91
|
+
3: return envVal === "true" || envVal === "1" || envVal === "on" || envVal === "yes";
|
|
92
92
|
4: }`,
|
|
93
93
|
status: 'success',
|
|
94
94
|
duration: '14ms',
|
|
@@ -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
|
});
|
|
@@ -312,7 +311,7 @@ export function useChatTimelineSend(deps: ChatTimelineSendDeps): ChatTimelineSen
|
|
|
312
311
|
}
|
|
313
312
|
}
|
|
314
313
|
|
|
315
|
-
// Mock / chamber-created session: existing
|
|
314
|
+
// Mock / chamber-created session: existing simulated SSE path.
|
|
316
315
|
// Cancel any previous stream
|
|
317
316
|
optimisticUserIdRef.current = null;
|
|
318
317
|
if (abortControllerRef.current) {
|
|
@@ -21,7 +21,6 @@ import type { Dispatch, RefObject, SetStateAction } from 'preact/compat';
|
|
|
21
21
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
|
22
22
|
|
|
23
23
|
import type { ChatMessageData } from '@/shared/types';
|
|
24
|
-
import { normalizeNoticePositions } from '@/shared/lib/chat/order';
|
|
25
24
|
import { SESSION_META_RETRY_SCHEDULE_MS } from '@/shared/lib/workspace/refresh-cadence';
|
|
26
25
|
|
|
27
26
|
export interface SessionDataShape {
|
|
@@ -210,9 +209,9 @@ export function useSessionLoad(deps: UseSessionLoadDeps) {
|
|
|
210
209
|
if (isGeneratingRef.current) {
|
|
211
210
|
const fetchedIds = fetched.map((m: ChatMessageData) => m.id);
|
|
212
211
|
const liveTail = localMessagesRef.current.filter(m => !fetchedIds.includes(m.id));
|
|
213
|
-
setLocalMessages(
|
|
212
|
+
setLocalMessages([...fetched, ...liveTail]);
|
|
214
213
|
} else {
|
|
215
|
-
setLocalMessages(
|
|
214
|
+
setLocalMessages(fetched);
|
|
216
215
|
}
|
|
217
216
|
} else if (!sessionId.startsWith('new-') && !optimisticOwnsTail) {
|
|
218
217
|
setLocalMessages([]);
|
|
@@ -284,7 +283,7 @@ export function useSessionLoad(deps: UseSessionLoadDeps) {
|
|
|
284
283
|
setLocalMessages(prev => {
|
|
285
284
|
// Drop overlap guard: the server window is index-based, so the
|
|
286
285
|
// fetched slice is strictly older than everything already mounted.
|
|
287
|
-
return [...
|
|
286
|
+
return [...older, ...prev];
|
|
288
287
|
});
|
|
289
288
|
// Bump the prepend tick so the layout effect below re-runs for THIS
|
|
290
289
|
// commit — it previously depended only on stable identities and ran
|
|
@@ -5,13 +5,26 @@
|
|
|
5
5
|
|
|
6
6
|
import { useEffect, useState } from 'preact/hooks';
|
|
7
7
|
|
|
8
|
-
import { isSyntaxReady, onSyntaxReady } from '@/shared/lib/code/highlighter';
|
|
8
|
+
import { isSyntaxReady, onLanguageReady, onSyntaxReady } from '@/shared/lib/code/highlighter';
|
|
9
9
|
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Re-renders when the Shiki highlighter finishes booting **and** whenever a
|
|
12
|
+
* grammar lands. Grammars are fetched on demand — a language nobody renders is
|
|
13
|
+
* never downloaded — so a component may see a `true` value here while the
|
|
14
|
+
* grammar for its own code is still in flight; the second subscription is what
|
|
15
|
+
* swaps the plain-text fallback for tokenized markup once the chunk arrives.
|
|
16
|
+
*/
|
|
11
17
|
export function useSyntaxReady(): boolean {
|
|
12
18
|
const [ready, setReady] = useState<boolean>(() => isSyntaxReady());
|
|
13
19
|
|
|
14
|
-
useEffect(() =>
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const offBoot = onSyntaxReady(() => setReady(true));
|
|
22
|
+
const offLang = onLanguageReady(() => setReady(true));
|
|
23
|
+
return () => {
|
|
24
|
+
offBoot();
|
|
25
|
+
offLang();
|
|
26
|
+
};
|
|
27
|
+
}, []);
|
|
15
28
|
|
|
16
29
|
return ready;
|
|
17
30
|
}
|
package/src/client/main.tsx
CHANGED
|
@@ -4,11 +4,8 @@ import { bootSyntax } from '@/shared/lib/code/highlighter';
|
|
|
4
4
|
import { primeChamberSettings } from '@/shared/lib/settings/client';
|
|
5
5
|
|
|
6
6
|
import '@/client/tailwind.css';
|
|
7
|
-
import '
|
|
8
|
-
import '
|
|
9
|
-
import '@fontsource/fira-code/500.css';
|
|
10
|
-
import '@fontsource/fira-code/600.css';
|
|
11
|
-
import '@fontsource/fira-code/700.css';
|
|
7
|
+
import '@/shared/lib/markdown/katex-fonts.css';
|
|
8
|
+
import '@/shared/lib/markdown/fira-code-fonts.css';
|
|
12
9
|
|
|
13
10
|
const root = document.getElementById('app');
|
|
14
11
|
const bootstrap = window.__OMP_BOOTSTRAP__;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Browser replacement for `elkjs`, aliased in `rsbuild.config.ts`.
|
|
8
|
+
*
|
|
9
|
+
* Mermaid 12 changed the global `layout` default from `dagre` to `elk`, so every
|
|
10
|
+
* flowchart pulled in `elkjs/lib/elk.bundled.js` whether the diagram asked for
|
|
11
|
+
* ELK or not — the single largest artifact in the client build (~1.4 MB raw,
|
|
12
|
+
* 434 kB gzip). `src/shared/lib/markdown/mermaid.ts` pins `layout: 'dagre'`,
|
|
13
|
+
* which is what makes this stub safe: with dagre selected explicitly, mermaid
|
|
14
|
+
* never calls into `runElkLayoutCore`, the only code path that constructs an ELK
|
|
15
|
+
* instance (verified across 20 diagram types, byte-identical results to a build
|
|
16
|
+
* with the real elkjs).
|
|
17
|
+
*
|
|
18
|
+
* `layout: 'elk'` is still a valid *request* — a diagram using the
|
|
19
|
+
* `flowchart-elk` directive, or a syntax flagged `@config{layout: "elk"}`. Those
|
|
20
|
+
* now fail loudly here instead of silently rendering with a different layout
|
|
21
|
+
* than asked for. Throwing is deliberate: mermaid's `render()` propagates the
|
|
22
|
+
* error to the caller, and `mermaid.ts` already renders a fallback block on
|
|
23
|
+
* failure, so a mis-specified diagram degrades to a visible message rather than
|
|
24
|
+
* a wrong diagram.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
function unavailable(): never {
|
|
28
|
+
throw new Error('elkjs is not bundled in this build; use the default dagre layout');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class ELK {}
|
|
32
|
+
|
|
33
|
+
export default new Proxy(ELK as unknown as object, {
|
|
34
|
+
construct: unavailable,
|
|
35
|
+
apply: unavailable,
|
|
36
|
+
get: unavailable,
|
|
37
|
+
}) as unknown as typeof ELK;
|
package/src/client/tailwind.css
CHANGED
|
@@ -285,6 +285,16 @@
|
|
|
285
285
|
[data-theme="one-dark-pro-soft"] .shiki span { color: var(--shiki-dark); }
|
|
286
286
|
.prose-content .katex-display { overflow-x: auto; overflow-y: hidden; }
|
|
287
287
|
|
|
288
|
+
/* Math placeholder: `marked.ts` emits a `.math-pending` span and KaTeX is
|
|
289
|
+
* fetched on demand (~522 kB async chunk), so the span is empty for one frame.
|
|
290
|
+
* Reserve display-math height so the message does not jump when the formula
|
|
291
|
+
* lands, and keep invalid/queued source faintly visible rather than blank. */
|
|
292
|
+
.prose-content .math-pending::before {
|
|
293
|
+
content: '…';
|
|
294
|
+
opacity: 0.35;
|
|
295
|
+
}
|
|
296
|
+
.prose-content p:has(> .math-pending[data-math-display='1']) { min-height: 2.5rem; }
|
|
297
|
+
|
|
288
298
|
/* Streaming placeholder: remend turns a half-received [text](url into this
|
|
289
299
|
* span; it becomes a real link once the URL arrives in a later frame. */
|
|
290
300
|
.prose-content .md-incomplete-link {
|
package/src/server/index.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { Elysia } from 'elysia';
|
|
2
2
|
import { apiRoutes } from '@/server/routes';
|
|
3
3
|
import { ssrRoutes } from '@/server/plugins/ssr';
|
|
4
|
+
import { getDatabasePath } from '@/server/db.server';
|
|
5
|
+
import { fetchOmpRegistrySnapshot } from '@/server/lib/models/provider-registry.server';
|
|
6
|
+
import { ompStartupError, ompStartupLogLines } from '@/server/lib/omp/core/startup';
|
|
7
|
+
import { isMockMode } from '@/server/mock.server';
|
|
8
|
+
|
|
9
|
+
// Refuse to run without a resolvable omp binary — before the listener opens and
|
|
10
|
+
// before the first request can reach a route that shells out to it. The database
|
|
11
|
+
// is opened lazily by getDb(), so this exits without touching it.
|
|
12
|
+
const startupError = ompStartupError();
|
|
13
|
+
if (startupError) {
|
|
14
|
+
console.error(startupError);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Which omp the chamber drives, which ~/.omp tree it reads and which SQLite file
|
|
19
|
+
// it writes — the paths every session/agent diagnostic traces back to. The data
|
|
20
|
+
// mode comes first because it changes what every later line means.
|
|
21
|
+
console.log(`[ompchamber] mock mode: ${isMockMode() ? 'true (demo presets)' : 'false (real omp data)'}`);
|
|
22
|
+
for (const line of ompStartupLogLines()) console.log(`[ompchamber] ${line}`);
|
|
23
|
+
console.log(`[ompchamber] db: ${await getDatabasePath()}`);
|
|
4
24
|
|
|
5
25
|
const port = Number(Bun.env.PORT) || 3000;
|
|
6
26
|
const host = Bun.env.HOST || 'localhost';
|
|
@@ -9,8 +29,41 @@ const app = new Elysia().use(apiRoutes).use(ssrRoutes);
|
|
|
9
29
|
|
|
10
30
|
await app.listen({ port, hostname: host });
|
|
11
31
|
|
|
32
|
+
// The listener is already accepting requests here, so the probe only postpones
|
|
33
|
+
// this last banner line, never startup. Printing it after the probe keeps the
|
|
34
|
+
// one line that matters — the URL — at the bottom of the log.
|
|
35
|
+
await logDetectedRegistry();
|
|
36
|
+
|
|
12
37
|
console.log(`[ompchamber] listening on http://${host}:${port}`);
|
|
13
38
|
|
|
39
|
+
/**
|
|
40
|
+
* What omp reports it can actually run. A live RPC round-trip: a cold utility
|
|
41
|
+
* process takes seconds, which is why it runs after the listener is up. Side
|
|
42
|
+
* benefit — it warms the shared utility process the model picker drives, so the
|
|
43
|
+
* first /api/models call does not pay the spawn.
|
|
44
|
+
*/
|
|
45
|
+
async function logDetectedRegistry(): Promise<void> {
|
|
46
|
+
if (isMockMode()) {
|
|
47
|
+
console.log('[ompchamber] providers: n/a (MOCK mode)');
|
|
48
|
+
console.log('[ompchamber] models: n/a (MOCK mode)');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const { providers, models } = await fetchOmpRegistrySnapshot();
|
|
53
|
+
// Count providers that actually serve a model — the distinct `provider`
|
|
54
|
+
// values in the model list, i.e. the same set GET /api/models groups by.
|
|
55
|
+
// omp's login catalog lists every sign-in offer it supports (75 here, 73 of
|
|
56
|
+
// them without credentials); counting those would report a provider set the
|
|
57
|
+
// chamber cannot use. The catalog size is kept as context.
|
|
58
|
+
const servingProviders = new Set(models.map((model) => model.provider)).size;
|
|
59
|
+
console.log(`[ompchamber] providers: ${servingProviders} detected (${providers.length} known to omp)`);
|
|
60
|
+
console.log(`[ompchamber] models: ${models.length} detected`);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
+
console.log(`[ompchamber] providers/models: detection failed — ${message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
14
67
|
for (const signal of ['SIGTERM', 'SIGINT'] as const) {
|
|
15
68
|
process.once(signal, () => {
|
|
16
69
|
void app.stop();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Git-ignore resolution for the Files panel.
|
|
8
|
+
*
|
|
9
|
+
* `git check-ignore` is the only authority that evaluates every rule source in
|
|
10
|
+
* one pass — per-directory `.gitignore`, `.git/info/exclude`, and the global
|
|
11
|
+
* `core.excludesFile` (`~/.gitignore`) — so the panel dims exactly what git
|
|
12
|
+
* would refuse to track. Paths are fed NUL-separated over stdin (`--stdin -z`)
|
|
13
|
+
* so a directory listing of any size cannot overflow argv, and the verbatim
|
|
14
|
+
* NUL-separated reply maps straight back onto those same paths.
|
|
15
|
+
*
|
|
16
|
+
* Paths must be relative to `dir` and must exist on disk: git resolves
|
|
17
|
+
* directory-only patterns (`build/`) by stat-ing the path.
|
|
18
|
+
*/
|
|
19
|
+
export async function collectIgnoredPaths(dir: string, relPaths: string[]): Promise<Set<string>> {
|
|
20
|
+
if (relPaths.length === 0) return new Set();
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const proc = Bun.spawn({
|
|
24
|
+
cmd: ['git', 'check-ignore', '--stdin', '-z'],
|
|
25
|
+
cwd: dir,
|
|
26
|
+
stdin: 'pipe',
|
|
27
|
+
stdout: 'pipe',
|
|
28
|
+
stderr: 'ignore',
|
|
29
|
+
timeout: 10_000,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Drain stdout while the listing is written: a directory big enough to
|
|
33
|
+
// fill both pipe buffers would otherwise deadlock child against parent.
|
|
34
|
+
const stdout = new Response(proc.stdout).text();
|
|
35
|
+
proc.stdin.write(relPaths.map(rel => `${rel}\0`).join(''));
|
|
36
|
+
// FileSink.end() types as `number | Promise<number>` — EPIPE (child gone)
|
|
37
|
+
// must not surface as an unhandled rejection.
|
|
38
|
+
void Promise.resolve(proc.stdin.end()).catch(() => {});
|
|
39
|
+
|
|
40
|
+
const [text, exitCode] = await Promise.all([stdout, proc.exited]);
|
|
41
|
+
// 0 = at least one path matched, 1 = none, 128 = no repository. Only 0 can
|
|
42
|
+
// carry a match list.
|
|
43
|
+
if (exitCode !== 0) return new Set();
|
|
44
|
+
|
|
45
|
+
return new Set(text.split('\0').filter(Boolean));
|
|
46
|
+
} catch {
|
|
47
|
+
return new Set(); // git missing / spawn failed — nothing is dimmed
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -23,12 +23,54 @@ export const PROVIDERS_SETTINGS_KEY = 'omp_providers_config';
|
|
|
23
23
|
|
|
24
24
|
const RPC_CACHE_TTL_MS = 60_000;
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
/** Minimal shape of a get_login_providers entry, as the chamber reads it. */
|
|
27
|
+
export interface DetectedLoginProvider {
|
|
27
28
|
id: string;
|
|
28
29
|
name: string;
|
|
29
30
|
authenticated: boolean;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function isDetectedLoginProvider(provider: unknown): provider is DetectedLoginProvider {
|
|
34
|
+
return (
|
|
35
|
+
typeof provider === 'object' && provider !== null
|
|
36
|
+
&& typeof (provider as { id?: unknown }).id === 'string'
|
|
37
|
+
&& typeof (provider as { name?: unknown }).name === 'string'
|
|
38
|
+
&& typeof (provider as { authenticated?: unknown }).authenticated === 'boolean'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isOmpModel(model: unknown): model is OmpModel {
|
|
43
|
+
return (
|
|
44
|
+
typeof model === 'object' && model !== null
|
|
45
|
+
&& typeof (model as OmpModel).id === 'string'
|
|
46
|
+
&& typeof (model as OmpModel).provider === 'string'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface OmpRegistrySnapshot {
|
|
51
|
+
/** Login providers omp reports (authenticated or not). */
|
|
52
|
+
providers: DetectedLoginProvider[];
|
|
53
|
+
/** Every model omp currently serves. */
|
|
54
|
+
models: OmpModel[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Raw omp registry probe — the two commands that define "detected" for the
|
|
59
|
+
* chamber: the login providers omp knows and the models they serve. Parsed here
|
|
60
|
+
* once so the provider settings page and the startup banner count the same set.
|
|
61
|
+
* Throws when the utility RPC is unreachable; each caller degrades its own way.
|
|
62
|
+
*/
|
|
63
|
+
export async function fetchOmpRegistrySnapshot(): Promise<OmpRegistrySnapshot> {
|
|
64
|
+
const [loginResponse, modelsResponse] = await Promise.all([
|
|
65
|
+
runUtilityCommand<{ providers?: unknown }>({ type: 'get_login_providers' }, 30_000),
|
|
66
|
+
runUtilityCommand<{ models?: unknown }>({ type: 'get_available_models' }, 60_000),
|
|
67
|
+
]);
|
|
68
|
+
return {
|
|
69
|
+
providers: Array.isArray(loginResponse.providers) ? loginResponse.providers.filter(isDetectedLoginProvider) : [],
|
|
70
|
+
models: Array.isArray(modelsResponse.models) ? modelsResponse.models.filter(isOmpModel) : [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
32
74
|
/**
|
|
33
75
|
* Provider registry straight from the omp agent: authenticated login providers
|
|
34
76
|
* (omp auth) plus their registered models (get_available_models). This is the
|
|
@@ -38,25 +80,7 @@ async function loadRpcProviderItems(): Promise<ProviderItem[]> {
|
|
|
38
80
|
const cached = globalThis.__ompChamberProvidersRpcCache;
|
|
39
81
|
if (cached && cached.expiresAt > Date.now()) return cached.data;
|
|
40
82
|
try {
|
|
41
|
-
const
|
|
42
|
-
runUtilityCommand<{ providers?: unknown }>({ type: 'get_login_providers' }, 30_000),
|
|
43
|
-
runUtilityCommand<{ models?: unknown }>({ type: 'get_available_models' }, 60_000),
|
|
44
|
-
]);
|
|
45
|
-
const loginProviders = Array.isArray(loginResponse.providers)
|
|
46
|
-
? loginResponse.providers.filter((provider): provider is LoginProvider => (
|
|
47
|
-
typeof provider === 'object' && provider !== null
|
|
48
|
-
&& typeof (provider as { id?: unknown }).id === 'string'
|
|
49
|
-
&& typeof (provider as { name?: unknown }).name === 'string'
|
|
50
|
-
&& typeof (provider as { authenticated?: unknown }).authenticated === 'boolean'
|
|
51
|
-
))
|
|
52
|
-
: [];
|
|
53
|
-
const available = Array.isArray(modelsResponse.models)
|
|
54
|
-
? modelsResponse.models.filter((model): model is OmpModel => (
|
|
55
|
-
typeof model === 'object' && model !== null
|
|
56
|
-
&& typeof (model as OmpModel).id === 'string'
|
|
57
|
-
&& typeof (model as OmpModel).provider === 'string'
|
|
58
|
-
))
|
|
59
|
-
: [];
|
|
83
|
+
const { providers: loginProviders, models: available } = await fetchOmpRegistrySnapshot();
|
|
60
84
|
const disabled = await readDisabledProviders().catch(() => new Set<string>());
|
|
61
85
|
|
|
62
86
|
const items = loginProviders.map((provider) => {
|
|
@@ -20,6 +20,7 @@ let cachedBin: string | null = null;
|
|
|
20
20
|
let binMissAt = 0;
|
|
21
21
|
|
|
22
22
|
const BIN_NAME = process.platform === 'win32' ? 'omp.exe' : 'omp';
|
|
23
|
+
|
|
23
24
|
// Only successes are cached for the process lifetime. omp may be installed (or
|
|
24
25
|
// PATH repaired) while the server runs; a permanently cached "not found" would
|
|
25
26
|
// keep the UI reporting a missing binary until restart.
|
|
@@ -32,7 +33,7 @@ export function invalidateOmpCliCache(): void {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function probeOmpBin(): string | null {
|
|
35
|
-
const override = Bun.env.
|
|
36
|
+
const override = Bun.env.OMPCHAMBER_OMP_BIN;
|
|
36
37
|
if (override) return existsSync(override) ? override : null;
|
|
37
38
|
|
|
38
39
|
const onPath = Bun.which(BIN_NAME);
|
|
@@ -49,7 +50,7 @@ function probeOmpBin(): string | null {
|
|
|
49
50
|
return Bun.which(BIN_NAME, { PATH: fallbackPath });
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
/** Resolve the omp binary:
|
|
53
|
+
/** Resolve the omp binary: OMPCHAMBER_OMP_BIN override, then PATH lookup. Returns
|
|
53
54
|
* null when omp is not installed. A hit is cached for the process lifetime; a
|
|
54
55
|
* miss is re-probed after MISS_TTL_MS. */
|
|
55
56
|
export function resolveOmpBin(): string | null {
|