@hienlh/ppm 0.21.0 → 0.22.1
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/CHANGELOG.md +50 -0
- package/CLAUDE.md +29 -4
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +4 -4
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/bun.lock +16 -1
- package/dist/web/assets/accounts-settings-section-D3BylchP.js +1 -0
- package/dist/web/assets/{ai-resource-editor-F09pf8Hd.js → ai-resource-editor-BLPcnhTM.js} +1 -1
- package/dist/web/assets/ai-settings-section-BtR7rF-T.js +1 -0
- package/dist/web/assets/ai-settings-section-r9JctHNM.js +1 -0
- package/dist/web/assets/{angular-html-DkR4iCth.js → angular-html-G66FNlyn.js} +1 -1
- package/dist/web/assets/{angular-ts-DcyL4Chx.js → angular-ts-B-PnUMyt.js} +1 -1
- package/dist/web/assets/{api-client-bfB7cXRM.js → api-client-C1ZwRL4Y.js} +1 -1
- package/dist/web/assets/api-client-pQL53cDQ.js +1 -0
- package/dist/web/assets/{api-settings-BcNjZIvG.js → api-settings-_7Q-ULxp.js} +1 -1
- package/dist/web/assets/{api-themes-DBfyVUtX.js → api-themes-DRAERo4Q.js} +1 -1
- package/dist/web/assets/{apl-BVlrVtjt.js → apl-BmDqqwE8.js} +1 -1
- package/dist/web/assets/{appearance-settings-section-whgnq3ss.js → appearance-settings-section-CiMdk3sb.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-Dx6nBEKT.js +1 -0
- package/dist/web/assets/{astro-MjJlozLj.js → astro-BfwMNfxr.js} +1 -1
- package/dist/web/assets/{audio-preview-Bc54GCAV.js → audio-preview-DsYvgKqj.js} +1 -1
- package/dist/web/assets/{blade-DnJWi1fz.js → blade-BsFV4pnN.js} +1 -1
- package/dist/web/assets/branch-review-tab-B1pFjlKM.js +1 -0
- package/dist/web/assets/{bsl-DCmJxSIz.js → bsl-Cw9Outng.js} +1 -1
- package/dist/web/assets/{button-VW4QGKCn.js → button-B9CTrsEf.js} +1 -1
- package/dist/web/assets/{cairo-C8dDenub.js → cairo-DOK5Wqnd.js} +1 -1
- package/dist/web/assets/chat-tab-DZagX06m.js +15 -0
- package/dist/web/assets/{cobol-DPx_nifl.js → cobol-DRaRy4FK.js} +1 -1
- package/dist/web/assets/code-editor-BJhOEsnZ.js +7 -0
- package/dist/web/assets/{codex-daily-guard-CfybHv_c.js → codex-daily-guard-CaCQJ4r_.js} +1 -1
- package/dist/web/assets/{coffee-DvFzqhgN.js → coffee-DvXuBmEJ.js} +1 -1
- package/dist/web/assets/{conflict-editor-DenQUOT2.js → conflict-editor-B7u_p49h.js} +3 -3
- package/dist/web/assets/context-menu-C6pIsf83.js +1 -0
- package/dist/web/assets/{cpp-BLJN0umi.js → cpp-zCmm8rKA.js} +1 -1
- package/dist/web/assets/{crystal-BXR-9fGw.js → crystal-CdQx6lB6.js} +1 -1
- package/dist/web/assets/{csv-preview-CClrJOuR.js → csv-preview-CttjJU1e.js} +1 -1
- package/dist/web/assets/{data-grid-overlay-editor-D4fhcMfT.js → data-grid-overlay-editor-6xUCeLHh.js} +1 -1
- package/dist/web/assets/{database-viewer-DItjm8wB.js → database-viewer-Ca4HxIfx.js} +1 -1
- package/dist/web/assets/{dialog-DQTn55Mt.js → dialog-B84_DJQ8.js} +1 -1
- package/dist/web/assets/diff-viewer-9-XjLIwa.js +4 -0
- package/dist/web/assets/{docx-preview-DYBkiUaE.js → docx-preview-CXuk-emm.js} +1 -1
- package/dist/web/assets/{edge-rWOlCmXT.js → edge-E8QLt7oi.js} +1 -1
- package/dist/web/assets/{edit-diff-preview-COtmo54G.js → edit-diff-preview-BGGGecVw.js} +1 -1
- package/dist/web/assets/{editor-language-service-DxkZ51E5.js → editor-language-service-D0DuEOs_.js} +1 -1
- package/dist/web/assets/{elixir-E7Y82okW.js → elixir-DPb_-72P.js} +1 -1
- package/dist/web/assets/{elm-DTPGbc-w.js → elm-BMzJY5b0.js} +1 -1
- package/dist/web/assets/{erb-DHqLHrE6.js → erb-AABQNtMK.js} +1 -1
- package/dist/web/assets/{erlang-Byg0nNiV.js → erlang-CEIKOkfP.js} +1 -1
- package/dist/web/assets/{esm-D_DmOM7n.js → esm-B9RVMpwV.js} +1 -1
- package/dist/web/assets/explorer-body-VdCBqcZu.js +2 -0
- package/dist/web/assets/explorer-window-content-BIsbl7Th.js +1 -0
- package/dist/web/assets/extension-manager-section-HkY7geS5.js +1 -0
- package/dist/web/assets/{extension-webview-DI4VL2tJ.js → extension-webview-DZG8YHX9.js} +1 -1
- package/dist/web/assets/file-icons-BXdvggK2.js +1 -0
- package/dist/web/assets/file-icons.generated-BtBErEwb.css +1 -0
- package/dist/web/assets/file-store-ojgMXgAw.js +1 -0
- package/dist/web/assets/files-settings-section-qnRtkX05.js +1 -0
- package/dist/web/assets/{format-file-meta-DOpzJJhS.js → format-file-meta-C77h04cZ.js} +1 -1
- package/dist/web/assets/{fortran-fixed-form-CY1n0Xf6.js → fortran-fixed-form-CrXvLHyi.js} +1 -1
- package/dist/web/assets/{fsharp-_vEDSCJz.js → fsharp-DCI4qAz_.js} +1 -1
- package/dist/web/assets/{gdresource-B6XT0wAC.js → gdresource-Vb2kO1MG.js} +1 -1
- package/dist/web/assets/{general-settings-section-BcxwiZ0X.js → general-settings-section-BAFSZmCE.js} +1 -1
- package/dist/web/assets/{git-commit-Wk9ovq59.js → git-commit-BMI_OWn-.js} +1 -1
- package/dist/web/assets/git-log-panel-2sGpfEla.js +1 -0
- package/dist/web/assets/{git-rebase-DSarGPvK.js → git-rebase-D-d_2iqZ.js} +1 -1
- package/dist/web/assets/{git-repo-store-Dy69YCHN.js → git-repo-store-88Z8v-pF.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-C6qRruHI.js +1 -0
- package/dist/web/assets/{glide-data-grid-BvgAqSUr.js → glide-data-grid-f0hyITiF.js} +7 -7
- package/dist/web/assets/{glimmer-js-CkYI35_P.js → glimmer-js-BmNNiR0Z.js} +1 -1
- package/dist/web/assets/{glimmer-ts-BqmIZcds.js → glimmer-ts-DQE2Ufha.js} +1 -1
- package/dist/web/assets/{glsl-Be-yBCF5.js → glsl-BcIDsxRJ.js} +1 -1
- package/dist/web/assets/{graphql-a5DrENHY.js → graphql-DGKSLmGH.js} +1 -1
- package/dist/web/assets/group-chat-tab-Ck6sk7yY.js +1 -0
- package/dist/web/assets/{hack-BLAg20aD.js → hack-YiC8bEIb.js} +1 -1
- package/dist/web/assets/{haml-Dj6bGOQH.js → haml-DjblxAsz.js} +1 -1
- package/dist/web/assets/{handlebars-dmEa36Oh.js → handlebars-BQepOXaU.js} +1 -1
- package/dist/web/assets/{html-yacZYzSE.js → html-Cayj_2GP.js} +1 -1
- package/dist/web/assets/{html-derivative-lLi3-RNT.js → html-derivative-Weq79bL6.js} +1 -1
- package/dist/web/assets/{http-1KaxcE4e.js → http-BeLfdGdz.js} +1 -1
- package/dist/web/assets/{hurl-ST-dEEZR.js → hurl-D9rypCLK.js} +1 -1
- package/dist/web/assets/{hxml-CpDtuq__.js → hxml-clG8mmSH.js} +1 -1
- package/dist/web/assets/{image-preview-CJVxL0uH.js → image-preview-CzCc4_Do.js} +1 -1
- package/dist/web/assets/image-preview-geometry-9lgeLYB3.js +1 -0
- package/dist/web/assets/index-BH99TIE0.css +1 -0
- package/dist/web/assets/index-CEZVxM4G.js +52 -0
- package/dist/web/assets/info-3K5VOQVL-BOSpdwlz.js +1 -0
- package/dist/web/assets/{jinja-Cjw-evAn.js → jinja-BShBWfs5.js} +1 -1
- package/dist/web/assets/{jira-watcher-section-DIC1yIV3.js → jira-watcher-section-CeLmhvu1.js} +1 -1
- package/dist/web/assets/{jison-DfATMYoY.js → jison-dN2Nf_Dr.js} +1 -1
- package/dist/web/assets/{julia-DDCaxiqT.js → julia-5U0vR-uJ.js} +1 -1
- package/dist/web/assets/{just-CJL529u6.js → just-BMppVRy6.js} +1 -1
- package/dist/web/assets/keybindings-store-C_3ChOEf.js +1 -0
- package/dist/web/assets/{keyboard-shortcuts-section-BW03P3AF.js → keyboard-shortcuts-section-PVz6kuh2.js} +1 -1
- package/dist/web/assets/{latex-Zc4EpLpW.js → latex-DSSOKjis.js} +1 -1
- package/dist/web/assets/{liquid-B_6KMNYS.js → liquid-C24dKoyI.js} +1 -1
- package/dist/web/assets/{lua-BtS2idK7.js → lua-DNOsnyhn.js} +1 -1
- package/dist/web/assets/{markdown-renderer-CvL1pWeK.js → markdown-renderer-C140Ijk0.js} +3 -3
- package/dist/web/assets/markdown-renderer-Dz7-DYUg.js +1 -0
- package/dist/web/assets/{marko-Wk6LBnck.js → marko-26MHO_Q9.js} +1 -1
- package/dist/web/assets/{mdc-cRaXBrzL.js → mdc-Dhw9mGrC.js} +1 -1
- package/dist/web/assets/{media-url-DL8OCGGg.js → media-url-yfqHpPz0.js} +1 -1
- package/dist/web/assets/{mobile-bottom-sheet-CoUauvLF.js → mobile-bottom-sheet-C3TniEP-.js} +1 -1
- package/dist/web/assets/mobile-explorer-sheet-CUG_owdf.js +1 -0
- package/dist/web/assets/{nextflow-Bos2wvDV.js → nextflow-xH4oJTex.js} +1 -1
- package/dist/web/assets/{nginx-DMlOwzdr.js → nginx-CMz4w0M8.js} +1 -1
- package/dist/web/assets/{nim-D2dZQA88.js → nim-D8FnIYsx.js} +1 -1
- package/dist/web/assets/notification-store-6vib1ISy.js +1 -0
- package/dist/web/assets/{notifications-settings-section-DPUROMw-.js → notifications-settings-section-DqlPDfNX.js} +1 -1
- package/dist/web/assets/{number-overlay-editor-DZ2i_Bnb.js → number-overlay-editor-CRGwULhR.js} +1 -1
- package/dist/web/assets/{overview-panel-R146OIom.js → overview-panel-Di-3aV9p.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-BGIE1QdU.js +1 -0
- package/dist/web/assets/panel-store-CboGJAwh.js +1 -0
- package/dist/web/assets/{pdf-preview-BWUPGLp6.js → pdf-preview-CmBI6E7d.js} +1 -1
- package/dist/web/assets/{perl-C5-Z6fPn.js → perl-D9iNWYwb.js} +1 -1
- package/dist/web/assets/{php-BXNNhkMH.js → php-Bw3Qo0Zv.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-Dg-QTbXX.js +1 -0
- package/dist/web/assets/{postgres-viewer-CjOvaruz.js → postgres-viewer-8ZAqEALv.js} +3 -3
- package/dist/web/assets/{ppmbot-settings-section-DrvWh8l8.js → ppmbot-settings-section-Cji-Mil-.js} +1 -1
- package/dist/web/assets/problems-panel-a6_hrWvl.js +1 -0
- package/dist/web/assets/{process-table-BRMS1MrX.js → process-table-CiIqlOIK.js} +1 -1
- package/dist/web/assets/project-framework-store-B70Q3dNM.js +1 -0
- package/dist/web/assets/project-store-B87hLRSk.js +1 -0
- package/dist/web/assets/{proxy-settings-section-Bv0Ny-Yy.js → proxy-settings-section-BjI4IFgp.js} +1 -1
- package/dist/web/assets/{pug-C1zf1yWy.js → pug-fT5JC6Vm.js} +1 -1
- package/dist/web/assets/{qml-OW58jGpD.js → qml-P_wEDB1w.js} +1 -1
- package/dist/web/assets/{query-audit-section-BcHJZcVC.js → query-audit-section-By6qyqdB.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DZ6IVu2s.js +1 -0
- package/dist/web/assets/{razor-DzjSaobM.js → razor-Drrt2_Ph.js} +1 -1
- package/dist/web/assets/remote-desktop-clipboard-notice-eXYvwgx7.js +1 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-Doh-fci-.js +2 -0
- package/dist/web/assets/remote-desktop-mobile-view-Dt8Akc3p.js +1 -0
- package/dist/web/assets/{remote-desktop-readiness-gate-BTzss2wX.js → remote-desktop-readiness-gate-t83fqbID.js} +1 -1
- package/dist/web/assets/remote-desktop-window-content-CguhZMmx.js +1 -0
- package/dist/web/assets/{rst-RfuihOUW.js → rst-Dr2mW4-h.js} +1 -1
- package/dist/web/assets/{ruby-CNO3pli1.js → ruby-C6EpUsTY.js} +1 -1
- package/dist/web/assets/{run-in-terminal-Cb_hVU8x.js → run-in-terminal-CdefEe_R.js} +1 -1
- package/dist/web/assets/{sas-DZY5BtXx.js → sas-D8ZEeP6v.js} +1 -1
- package/dist/web/assets/{schedules-settings-section-DO8xAOVj.js → schedules-settings-section-BVkpW5ms.js} +1 -1
- package/dist/web/assets/{scroll-area-LtNV8g5o.js → scroll-area-BzeiOd-B.js} +1 -1
- package/dist/web/assets/{scss--4g-REcN.js → scss-ITL7jQ1f.js} +1 -1
- package/dist/web/assets/{send-to-chat-Cnpru1M0.js → send-to-chat-F207yH3u.js} +1 -1
- package/dist/web/assets/{separator-Cds1yxy8.js → separator-TRMsJUYe.js} +1 -1
- package/dist/web/assets/settings-body-CoIxYAh6.js +2 -0
- package/dist/web/assets/settings-store-krC6bc79.js +2 -0
- package/dist/web/assets/settings-tab-7DKVUTbM.js +1 -0
- package/dist/web/assets/settings-window-content-3_39TeX-.js +1 -0
- package/dist/web/assets/{shaderlab-Cu4sBiVn.js → shaderlab-mW-IIlhk.js} +1 -1
- package/dist/web/assets/{shellsession-DWzJfEv7.js → shellsession-DhNl5STt.js} +1 -1
- package/dist/web/assets/{soy-BnqE-5Ff.js → soy-SdY0Qjtr.js} +1 -1
- package/dist/web/assets/{sparql-Ctg__NhZ.js → sparql-ftqZo2i-.js} +1 -1
- package/dist/web/assets/{sql-query-editor-D9k19T_M.js → sql-query-editor-CQB7Yp7U.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-DoV9hnWJ.js → sqlite-viewer-Bd7rjr1D.js} +1 -1
- package/dist/web/assets/{stata-Qk2h66XW.js → stata-Bj7Yjb4b.js} +1 -1
- package/dist/web/assets/{surrealql-8QEX7EfH.js → surrealql-CLhYGAfB.js} +1 -1
- package/dist/web/assets/{svelte-D_mubC5-.js → svelte-DTAlRHhK.js} +1 -1
- package/dist/web/assets/system-monitor-body-DsRXeMyU.js +2 -0
- package/dist/web/assets/system-monitor-tab-DdSZgux3.js +1 -0
- package/dist/web/assets/{system-monitor-window-content-qXLcicFM.js → system-monitor-window-content-ClPo4-El.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-7fPZNEni.js → tab-host-window-content-d2TAD4PU.js} +1 -1
- package/dist/web/assets/{tab-store-DGJGnbwv.js → tab-store-OJa1GPCq.js} +1 -1
- package/dist/web/assets/team-member-sheet-7d2Qp-ie.js +2 -0
- package/dist/web/assets/{team-member-window-content-BgvJFypM.js → team-member-window-content-Bctg2gmg.js} +1 -1
- package/dist/web/assets/{templ-C1jV0fsO.js → templ-BMfhACxb.js} +1 -1
- package/dist/web/assets/{terminal-tab-Cs0sGljx.js → terminal-tab-0HQic25V.js} +3 -3
- package/dist/web/assets/{tex-D_StFXy_.js → tex-BcssM-QA.js} +1 -1
- package/dist/web/assets/tool-cards-CmLLRACd.js +9 -0
- package/dist/web/assets/treemap-KZPCXAKY-CE30wCXa.js +1 -0
- package/dist/web/assets/{ts-tags-B5m47rEm.js → ts-tags-CrR5IYI8.js} +1 -1
- package/dist/web/assets/{twig-C_br6GIl.js → twig-DchfUmyI.js} +1 -1
- package/dist/web/assets/{use-blob-url-C-SGi_Hb.js → use-blob-url-CPtCcY3i.js} +1 -1
- package/dist/web/assets/{use-inline-blame-B4trxGcR.js → use-inline-blame-sDMjcz1-.js} +3 -3
- package/dist/web/assets/{use-lsp-CV0vzILO.js → use-lsp-CBgID-EU.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-DBqbWjWX.js → use-monaco-theme-gvhJ_jZh.js} +1 -1
- package/dist/web/assets/{use-open-team-member-DENsjp0s.js → use-open-team-member-C3mrUzIC.js} +1 -1
- package/dist/web/assets/{use-websocket-LgXiX1qN.js → use-websocket-C9wlRliB.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-Cbig5zA6.js → vendor-mermaid-BitiXJlL.js} +1 -1
- package/dist/web/assets/{vendor-ui-BBdkypKW.js → vendor-ui-DmZ-Mb-H.js} +1 -1
- package/dist/web/assets/video-preview-CSDK5j6H.js +1 -0
- package/dist/web/assets/{vue-DQwCnLdr.js → vue-1EL6Pw1h.js} +1 -1
- package/dist/web/assets/{vue-html-T_EoGpPJ.js → vue-html-BPQNwPC6.js} +1 -1
- package/dist/web/assets/{vue-vine-DmxLqX50.js → vue-vine-Bb_dsWLt.js} +1 -1
- package/dist/web/assets/{window-pip-registry-3l4hoxuD.js → window-pip-registry-yD6esSju.js} +1 -1
- package/dist/web/assets/ws-auth-CEYVQ8NH.js +1 -0
- package/dist/web/assets/{xml-Bs6Bw8is.js → xml-dxkLNA0K.js} +1 -1
- package/dist/web/assets/{xsl-f5ZNhRCc.js → xsl-Bw2adwrl.js} +1 -1
- package/dist/web/index.html +27 -26
- package/dist/web/sw.js +1 -1
- package/docs/architecture/ai-chat-and-providers.md +9 -0
- package/docs/architecture/plugins-and-tracing.md +285 -0
- package/docs/lessons-learned.md +31 -0
- package/docs/system-architecture.md +1 -0
- package/package.json +2 -1
- package/packages/ext-git-graph/src/compare-diff-base.test.ts +173 -0
- package/packages/ext-git-graph/src/compare-view.ts +22 -3
- package/packages/ext-git-graph/src/extension.ts +6 -0
- package/packages/ext-git-graph/src/types.ts +19 -0
- package/packages/ext-git-graph/src/webview-html.test.ts +159 -1
- package/packages/ext-git-graph/src/webview-html.ts +344 -26
- package/scripts/gen-file-icons.ts +493 -242
- package/src/providers/claude-agent-sdk.ts +42 -1
- package/src/providers/codex-app-server/codex-history.ts +77 -23
- package/src/server/index.ts +42 -7
- package/src/server/routes/chat.ts +15 -11
- package/src/server/routes/git.ts +83 -3
- package/src/server/routes/remote-desktop.ts +7 -2
- package/src/server/ws/global.ts +1 -1
- package/src/server/ws/remote-desktop.ts +12 -5
- package/src/services/binary-content.ts +25 -0
- package/src/services/chat-search.service.ts +1 -1
- package/src/services/chat.service.ts +9 -0
- package/src/services/edge-forwarder.ts +191 -191
- package/src/services/event-loop-lag.ts +208 -0
- package/src/services/file-lines.ts +114 -0
- package/src/services/file-watcher/recreated-dir-poller.ts +108 -6
- package/src/services/file-watcher/watch-tree.ts +131 -10
- package/src/services/file-watcher.service.ts +73 -23
- package/src/services/git-branch-diff/branch-diff-parse.ts +151 -0
- package/src/services/git-branch-diff/branch-diff.service.ts +79 -0
- package/src/services/git-hunks/git-hunks.service.ts +26 -11
- package/src/services/git-hunks/unified-diff.ts +15 -1
- package/src/services/git.service.ts +74 -24
- package/src/services/jsonl-transcript-parser.ts +138 -6
- package/src/services/log-rotate.ts +142 -0
- package/src/services/media-transcode/ffmpeg-capabilities.ts +72 -2
- package/src/services/remote-desktop/remote-desktop-audio.ts +193 -0
- package/src/services/remote-desktop/remote-desktop-capture-input.ts +87 -14
- package/src/services/remote-desktop/remote-desktop-capture.ts +40 -7
- package/src/services/remote-desktop/remote-desktop-clipboard.ts +166 -0
- package/src/services/remote-desktop/remote-desktop-displays.ts +77 -1
- package/src/services/remote-desktop/remote-desktop-encoder-args.ts +25 -6
- package/src/services/remote-desktop/remote-desktop-evdev-key-map.ts +69 -0
- package/src/services/remote-desktop/remote-desktop-input-linux.ts +253 -0
- package/src/services/remote-desktop/remote-desktop-input-uinput.ts +242 -0
- package/src/services/remote-desktop/remote-desktop-input.ts +40 -8
- package/src/services/remote-desktop/remote-desktop-linux-session.ts +146 -0
- package/src/services/remote-desktop/remote-desktop-ogg-opus.ts +144 -0
- package/src/services/remote-desktop/remote-desktop-privacy.ts +163 -0
- package/src/services/remote-desktop/remote-desktop-quality.ts +138 -0
- package/src/services/remote-desktop/remote-desktop-requirements.ts +158 -5
- package/src/services/remote-desktop/remote-desktop-resolution.ts +302 -0
- package/src/services/remote-desktop/remote-desktop-session.ts +523 -21
- package/src/services/remote-desktop/remote-desktop-x11.ts +250 -0
- package/src/services/supervisor.ts +47 -4
- package/src/services/windows-process-tree.ts +351 -351
- package/src/shared/codex-daily-guard.ts +12 -5
- package/src/shared/remote-desktop-custom-quality.ts +117 -0
- package/src/shared/remote-desktop-quality.ts +127 -0
- package/src/types/chat.ts +4 -0
- package/src/types/git.ts +52 -0
- package/src/web/components/branch-review/branch-review-tab.tsx +360 -0
- package/src/web/components/branch-review/branch-review-tree-row.tsx +138 -0
- package/src/web/components/chat/attachment-chips.tsx +30 -6
- package/src/web/components/chat/chat-tab.tsx +12 -1
- package/src/web/components/chat/message-events.tsx +273 -0
- package/src/web/components/chat/message-list.tsx +107 -861
- package/src/web/components/chat/message-markdown.tsx +34 -0
- package/src/web/components/chat/message-media.tsx +174 -0
- package/src/web/components/chat/message-user-bubble.tsx +399 -0
- package/src/web/components/chat/provider-selector.tsx +30 -22
- package/src/web/components/chat/session-history-list.tsx +2 -2
- package/src/web/components/chat/session-list-panel.tsx +3 -2
- package/src/web/components/editor/binary-diff-view.tsx +212 -0
- package/src/web/components/editor/diff-viewer.tsx +76 -3
- package/src/web/components/explorer/file-tree.tsx +235 -134
- package/src/web/components/explorer/flatten-visible-tree.ts +35 -40
- package/src/web/components/explorer/tree-node.tsx +69 -69
- package/src/web/components/explorer/use-tree-row-dnd.ts +3 -4
- package/src/web/components/extensions/extension-tree-view.tsx +4 -11
- package/src/web/components/git/git-status-panel.tsx +25 -0
- package/src/web/components/layout/command-palette.tsx +2 -18
- package/src/web/components/layout/draggable-tab.tsx +4 -2
- package/src/web/components/layout/nav-section-rail.tsx +12 -3
- package/src/web/components/layout/status-bar.tsx +10 -6
- package/src/web/components/layout/tab-content.tsx +5 -0
- package/src/web/components/layout/tab-pool.tsx +1 -0
- package/src/web/components/layout/upgrade-button.tsx +5 -1
- package/src/web/components/remote-desktop/remote-desktop-clipboard-client.ts +67 -0
- package/src/web/components/remote-desktop/remote-desktop-clipboard-notice.tsx +127 -0
- package/src/web/components/remote-desktop/remote-desktop-codec-labels.ts +28 -0
- package/src/web/components/remote-desktop/remote-desktop-custom-quality-dialog.tsx +144 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-key-bar.tsx +13 -2
- package/src/web/components/remote-desktop/remote-desktop-mobile-settings.tsx +232 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-toolbar.tsx +21 -8
- package/src/web/components/remote-desktop/remote-desktop-mobile-view.tsx +82 -3
- package/src/web/components/remote-desktop/remote-desktop-recording.ts +76 -0
- package/src/web/components/remote-desktop/remote-desktop-resolution-list.ts +100 -0
- package/src/web/components/remote-desktop/remote-desktop-toolbar.tsx +497 -0
- package/src/web/components/remote-desktop/remote-desktop-view-style.ts +145 -0
- package/src/web/components/remote-desktop/remote-desktop-window-content.tsx +126 -35
- package/src/web/components/remote-desktop/use-h264-canvas-decoder.ts +11 -0
- package/src/web/components/remote-desktop/use-opus-audio-player.ts +148 -0
- package/src/web/components/remote-desktop/use-remote-desktop-connection.ts +284 -1
- package/src/web/components/remote-desktop/use-remote-desktop-readiness.ts +75 -1
- package/src/web/components/remote-desktop/use-remote-desktop-recorder.ts +103 -0
- package/src/web/components/remote-desktop/use-remote-desktop-virtual-keyboard.ts +19 -2
- package/src/web/components/remote-desktop/use-remote-input-capture.ts +39 -3
- package/src/web/components/settings/accounts/account-card.tsx +2 -2
- package/src/web/components/settings/accounts/codex-accounts-section.tsx +4 -4
- package/src/web/components/settings/extension-manager-section.tsx +8 -1
- package/src/web/components/settings/theme-picker.tsx +2 -1
- package/src/web/components/ui/tooltip.tsx +15 -2
- package/src/web/lib/branch-review-state.ts +175 -0
- package/src/web/lib/extension-icons.ts +70 -0
- package/src/web/lib/file-icon-name.ts +62 -0
- package/src/web/lib/file-icons.generated.ts +3287 -25
- package/src/web/lib/file-icons.tsx +71 -73
- package/src/web/lib/flatten-expansions.ts +18 -5
- package/src/web/lib/message-ordinals.ts +23 -0
- package/src/web/lib/provider-logos.tsx +143 -0
- package/src/web/lib/tab-type-icons.ts +25 -0
- package/src/web/stores/file-store.ts +73 -35
- package/src/web/stores/panel-utils.ts +5 -0
- package/src/web/stores/project-framework-store.ts +82 -0
- package/src/web/stores/project-store.ts +7 -1
- package/src/web/stores/settings-store.ts +128 -0
- package/src/web/stores/tab-store.ts +1 -0
- package/src/web/stores/window-panel-persistence.ts +1 -0
- package/src/web/styles/file-icons.generated.css +3282 -234
- package/src/web/styles/globals.css +26 -0
- package/dist/web/assets/accounts-settings-section-BuxD9zH3.js +0 -1
- package/dist/web/assets/ai-settings-section-Dd9tOUp0.js +0 -1
- package/dist/web/assets/ai-settings-section-FrU2X8Fu.js +0 -1
- package/dist/web/assets/api-client-BUCo1lms.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-5K0wEkAn.js +0 -1
- package/dist/web/assets/chat-tab-Cr6_KSGO.js +0 -15
- package/dist/web/assets/code-editor-DfmlGamv.js +0 -7
- package/dist/web/assets/context-menu-DOODKZdF.js +0 -1
- package/dist/web/assets/diff-viewer-BpNj7pN9.js +0 -4
- package/dist/web/assets/explorer-body-D-5z464e.js +0 -2
- package/dist/web/assets/explorer-window-content-7-1wuRul.js +0 -1
- package/dist/web/assets/extension-manager-section-BhwB6nqr.js +0 -1
- package/dist/web/assets/file-icons-EzPL0q-k.js +0 -1
- package/dist/web/assets/file-icons.generated-CfaJuvZu.css +0 -1
- package/dist/web/assets/file-store-DMABxOkV.js +0 -1
- package/dist/web/assets/files-settings-section-CZzABuKz.js +0 -1
- package/dist/web/assets/git-log-panel-CRN4JVOx.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-DkVT2MAM.js +0 -1
- package/dist/web/assets/group-chat-tab-DFa5mAx5.js +0 -1
- package/dist/web/assets/index-D7DbXD3G.css +0 -1
- package/dist/web/assets/index-ZtfxZOyb.js +0 -52
- package/dist/web/assets/info-3K5VOQVL-BBsmKGzA.js +0 -1
- package/dist/web/assets/keybindings-store-tVfhPANS.js +0 -1
- package/dist/web/assets/markdown-renderer-sJNNVhlS.js +0 -1
- package/dist/web/assets/mobile-explorer-sheet-CZ0HZ2E_.js +0 -1
- package/dist/web/assets/notification-store-BZbsPnW0.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-DwmYPOlx.js +0 -1
- package/dist/web/assets/panel-store-BVLBRn6M.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-D0oogIFi.js +0 -1
- package/dist/web/assets/problems-panel-B7BWVhdy.js +0 -1
- package/dist/web/assets/project-store-BhMbVln3.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-CQIJOQGa.js +0 -1
- package/dist/web/assets/remote-desktop-mobile-sheet-ByjaeZKX.js +0 -2
- package/dist/web/assets/remote-desktop-mobile-view-D13mtr9F.js +0 -1
- package/dist/web/assets/remote-desktop-window-content-CbqTCaC6.js +0 -1
- package/dist/web/assets/settings-body-Rnu-FcX-.js +0 -2
- package/dist/web/assets/settings-store-DZPgPtEA.js +0 -2
- package/dist/web/assets/settings-tab-C6PHxs0Z.js +0 -1
- package/dist/web/assets/settings-window-content-CQ4gT2qj.js +0 -1
- package/dist/web/assets/system-monitor-body-2vQ1kMxE.js +0 -2
- package/dist/web/assets/system-monitor-tab-krAcvwF7.js +0 -1
- package/dist/web/assets/team-member-sheet-2FnRmk1a.js +0 -2
- package/dist/web/assets/tool-cards-DU_S2o51.js +0 -9
- package/dist/web/assets/treemap-KZPCXAKY-BhEtjljr.js +0 -1
- package/dist/web/assets/use-remote-desktop-display-choice-BK_f787P.js +0 -1
- package/dist/web/assets/video-preview-BDNwumbW.js +0 -1
- package/dist/web/assets/ws-auth-DEAJiGCR.js +0 -1
- package/packages/vscode-compat/node_modules/.bin/tsc.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsc.exe +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.exe +0 -0
- /package/dist/web/assets/{abap-tkptwpsK.js → abap-IN2yINdN.js} +0 -0
- /package/dist/web/assets/{actionscript-3-Di9TMGwg.js → actionscript-3-C_t772eR.js} +0 -0
- /package/dist/web/assets/{ada-CB9F079D.js → ada-DPme3emm.js} +0 -0
- /package/dist/web/assets/{andromeeda-Dcl8jpb7.js → andromeeda-DSV4i4mZ.js} +0 -0
- /package/dist/web/assets/{apache-D0Nk0qsH.js → apache-CWr8s8I8.js} +0 -0
- /package/dist/web/assets/{apex-mQIoWncD.js → apex-Z20JnDF5.js} +0 -0
- /package/dist/web/assets/{applescript-C6ljbcsI.js → applescript-BKNPu3ga.js} +0 -0
- /package/dist/web/assets/{apply-theme-BwIMNXyX.js → apply-theme-CbR31K6I.js} +0 -0
- /package/dist/web/assets/{ara-WFqRPGPe.js → ara-hWdY3O96.js} +0 -0
- /package/dist/web/assets/{asciidoc-bNd8w4nf.js → asciidoc-eynhq496.js} +0 -0
- /package/dist/web/assets/{asm-BQ1_F580.js → asm-Bu02Kq4P.js} +0 -0
- /package/dist/web/assets/{aurora-x-Ciqs_Dbu.js → aurora-x-BHBGXbIZ.js} +0 -0
- /package/dist/web/assets/{awk-GCR-6KZV.js → awk-B4ojezyv.js} +0 -0
- /package/dist/web/assets/{ayu-dark-DxJXuj99.js → ayu-dark-D0GQzFi9.js} +0 -0
- /package/dist/web/assets/{ayu-light-jvSyJWDV.js → ayu-light-BVYb5Yn8.js} +0 -0
- /package/dist/web/assets/{ayu-mirage-Cg3BPO6H.js → ayu-mirage-BGFW6oNb.js} +0 -0
- /package/dist/web/assets/{ballerina-BPz4CrX6.js → ballerina-B6Og3IKT.js} +0 -0
- /package/dist/web/assets/{bat-CSTs2hIG.js → bat-D8YFILfx.js} +0 -0
- /package/dist/web/assets/{beancount-DMateAZX.js → beancount-BjlbBBuX.js} +0 -0
- /package/dist/web/assets/{berry-9lXg0GRM.js → berry-Cz1ygeoj.js} +0 -0
- /package/dist/web/assets/{bibtex-DIbqmvFg.js → bibtex-CVkJceJh.js} +0 -0
- /package/dist/web/assets/{bicep-02WGsoJ8.js → bicep-DrhYI5GX.js} +0 -0
- /package/dist/web/assets/{bird2-BY0tbu3k.js → bird2-BV-KJtmc.js} +0 -0
- /package/dist/web/assets/{c-jMBX_9Je.js → c-CuLlvxbG.js} +0 -0
- /package/dist/web/assets/{c3-B8JVbNjW.js → c3-CI7-1CJ0.js} +0 -0
- /package/dist/web/assets/{cadence-DUIf1tvL.js → cadence-DjxhNP3Q.js} +0 -0
- /package/dist/web/assets/{can-open-in-ppm-DGEc0s4P.js → can-open-in-ppm-cv5--yHX.js} +0 -0
- /package/dist/web/assets/{catppuccin-frappe-FhUCVxex.js → catppuccin-frappe-CEozpJRh.js} +0 -0
- /package/dist/web/assets/{catppuccin-latte-CzfrtmgL.js → catppuccin-latte-C4sXQFMc.js} +0 -0
- /package/dist/web/assets/{catppuccin-macchiato-6xL0lOtj.js → catppuccin-macchiato-Ci-fDhXy.js} +0 -0
- /package/dist/web/assets/{catppuccin-mocha-D39-xS3P.js → catppuccin-mocha-CH7YXw8d.js} +0 -0
- /package/dist/web/assets/{clarity-D8TeGvoT.js → clarity-BWrJ7UmT.js} +0 -0
- /package/dist/web/assets/{clojure-Dx_acSX7.js → clojure-C94fh2hb.js} +0 -0
- /package/dist/web/assets/{cmake-CyeYPq8o.js → cmake-CtvCkMsG.js} +0 -0
- /package/dist/web/assets/{codeowners-DrTI0lmI.js → codeowners-CJnubqzH.js} +0 -0
- /package/dist/web/assets/{codeql-CPkFxVW-.js → codeql-BOoP5HY2.js} +0 -0
- /package/dist/web/assets/{common-lisp-B0k-3LTj.js → common-lisp-B9nlMFJd.js} +0 -0
- /package/dist/web/assets/{coq-uygpo7to.js → coq-CgzIQiSH.js} +0 -0
- /package/dist/web/assets/{csharp-DKUYGfSp.js → csharp-4xAIx3jw.js} +0 -0
- /package/dist/web/assets/{css-DoOUp7v4.js → css-D4YoLYGn.js} +0 -0
- /package/dist/web/assets/{csv-B3YE13gi.js → csv-C2v-ZakW.js} +0 -0
- /package/dist/web/assets/{csv-parser-DXePqmXe.js → csv-parser-_oDBUkwQ.js} +0 -0
- /package/dist/web/assets/{cue-DViO2wvn.js → cue-Bbznh_an.js} +0 -0
- /package/dist/web/assets/{cypher-4BMCIW5N.js → cypher-BKhVEARF.js} +0 -0
- /package/dist/web/assets/{d-BT_sTHas.js → d-D6IhWopl.js} +0 -0
- /package/dist/web/assets/{dark-plus-DVVXu-1N.js → dark-plus-xAWu790O.js} +0 -0
- /package/dist/web/assets/{dart-DHz6f6_6.js → dart-Bj6S9O1j.js} +0 -0
- /package/dist/web/assets/{data-grid-types-OgUxMLdQ.js → data-grid-types-C5bthITf.js} +0 -0
- /package/dist/web/assets/{dax-ChYCjweK.js → dax-CF2zvaqq.js} +0 -0
- /package/dist/web/assets/{desktop-D6Lt4p04.js → desktop-CnmyxMKE.js} +0 -0
- /package/dist/web/assets/{diff-D8TDe4hP.js → diff-Qi8WuJsI.js} +0 -0
- /package/dist/web/assets/{dist-DOTlFQJA.js → dist-Bjseolz4.js} +0 -0
- /package/dist/web/assets/{dist-CrBOCtCW.js → dist-Czq7BUMy.js} +0 -0
- /package/dist/web/assets/{docker-Bsp0YbPx.js → docker-vLbbNP5d.js} +0 -0
- /package/dist/web/assets/{dotenv-B_7giSY_.js → dotenv-DJMvvNzn.js} +0 -0
- /package/dist/web/assets/{dracula-TrQL2iB5.js → dracula-CtAh2rhB.js} +0 -0
- /package/dist/web/assets/{dracula-soft-n2XOlgh-.js → dracula-soft-CXtuAAf6.js} +0 -0
- /package/dist/web/assets/{dream-maker-CdIEdhpT.js → dream-maker-D5YQyK49.js} +0 -0
- /package/dist/web/assets/{editor-font-BDrdiGf1.js → editor-font-ClTC9gaD.js} +0 -0
- /package/dist/web/assets/{emacs-lisp-CSbixs6u.js → emacs-lisp-CGTjbiqF.js} +0 -0
- /package/dist/web/assets/{everforest-dark-BuZahZYp.js → everforest-dark-D59p7vAQ.js} +0 -0
- /package/dist/web/assets/{everforest-light-DPH53z5H.js → everforest-light-B3Qtud3V.js} +0 -0
- /package/dist/web/assets/{fennel-B1-K_hlJ.js → fennel-BCyHNUwf.js} +0 -0
- /package/dist/web/assets/{fish-Cy12Dbua.js → fish-DDDlLV2G.js} +0 -0
- /package/dist/web/assets/{fluent-B_82ys8E.js → fluent-By54R9tl.js} +0 -0
- /package/dist/web/assets/{format-bytes-D5sBgIqU.js → format-bytes-D1AQyUCt.js} +0 -0
- /package/dist/web/assets/{fortran-free-form-BRQdNzZF.js → fortran-free-form-tP8mRym_.js} +0 -0
- /package/dist/web/assets/{gdscript-XBDmLUm9.js → gdscript-nHC2z6Te.js} +0 -0
- /package/dist/web/assets/{gdshader-DhVfB3u4.js → gdshader-K-cSmPlW.js} +0 -0
- /package/dist/web/assets/{genie-X3K29Y49.js → genie-BAcRhvEe.js} +0 -0
- /package/dist/web/assets/{gherkin-DKIx-6cX.js → gherkin-CcifxMLb.js} +0 -0
- /package/dist/web/assets/{github-dark-Bf7zdp5s.js → github-dark-67KKlMdB.js} +0 -0
- /package/dist/web/assets/{github-dark-default-DqsBBV11.js → github-dark-default-tja8pidR.js} +0 -0
- /package/dist/web/assets/{github-dark-dimmed-X2gqRz6w.js → github-dark-dimmed-CtjhYQtx.js} +0 -0
- /package/dist/web/assets/{github-dark-high-contrast-Z7tvbGX4.js → github-dark-high-contrast-Bf-LPFgn.js} +0 -0
- /package/dist/web/assets/{github-light-BKxt-7zZ.js → github-light-CbJxE7n3.js} +0 -0
- /package/dist/web/assets/{github-light-default-B0liF4d4.js → github-light-default-DRQmdHxt.js} +0 -0
- /package/dist/web/assets/{github-light-high-contrast-F4encJwr.js → github-light-high-contrast-CJkDgTeM.js} +0 -0
- /package/dist/web/assets/{gleam-Pz2bHgBB.js → gleam-D4LCl2ri.js} +0 -0
- /package/dist/web/assets/{gn-CgmdNsPU.js → gn-COY8zZmC.js} +0 -0
- /package/dist/web/assets/{gnuplot-qLNF5yZ9.js → gnuplot-ROk1VitR.js} +0 -0
- /package/dist/web/assets/{go-CSDUNhE7.js → go-H30c3p0C.js} +0 -0
- /package/dist/web/assets/{groovy-B3NEnOHF.js → groovy-DBLCaelV.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-hard-DcoftiUX.js → gruvbox-dark-hard-DJy4irps.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-medium-D1wVSuD8.js → gruvbox-dark-medium-Dhfw1QTO.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-soft-DWpunymH.js → gruvbox-dark-soft-BncJU-8w.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-hard-rHMvyUdS.js → gruvbox-light-hard-Cq12_YvM.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-medium-3O2KiXO7.js → gruvbox-light-medium-DqVja5_v.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-soft-lNlXJPwH.js → gruvbox-light-soft-IvwhveGF.js} +0 -0
- /package/dist/web/assets/{haskell-aaG3aC0h.js → haskell-DA1JV-MC.js} +0 -0
- /package/dist/web/assets/{haxe-CPcHhUDd.js → haxe-V9RTRc5E.js} +0 -0
- /package/dist/web/assets/{hcl-CMQNlR_4.js → hcl-hr1lyZ10.js} +0 -0
- /package/dist/web/assets/{hjson-DeuklPFj.js → hjson-D3vW3i7X.js} +0 -0
- /package/dist/web/assets/{hlsl-BI3hlsaM.js → hlsl-BB85gfJT.js} +0 -0
- /package/dist/web/assets/{horizon-C7-gOcjs.js → horizon-DU8OOnj8.js} +0 -0
- /package/dist/web/assets/{horizon-bright-Cxt9bRW7.js → horizon-bright-DNMUclW6.js} +0 -0
- /package/dist/web/assets/{houston-DbF2G7iD.js → houston-DAiZKf6T.js} +0 -0
- /package/dist/web/assets/{hy-DRG6Hs2I.js → hy-C6Zv0LMe.js} +0 -0
- /package/dist/web/assets/{image-gallery-CObzTisL.js → image-gallery-CGvlv8zR.js} +0 -0
- /package/dist/web/assets/{imba-CxKv_2TW.js → imba-Didnp6Y-.js} +0 -0
- /package/dist/web/assets/{ini-BiHsyCdO.js → ini-m0T7q4gQ.js} +0 -0
- /package/dist/web/assets/{input-D-dR8RI6.js → input-C7Q8FxeL.js} +0 -0
- /package/dist/web/assets/{java-D3RZzgDB.js → java-Buy07jGn.js} +0 -0
- /package/dist/web/assets/{javascript-4FmSqG-T.js → javascript-Cjq3CbRL.js} +0 -0
- /package/dist/web/assets/{json-CjB236_E.js → json-BSVu8IqD.js} +0 -0
- /package/dist/web/assets/{json5-bkfO-Rco.js → json5-NN_Vb7e6.js} +0 -0
- /package/dist/web/assets/{jsonc-BwaTeLyv.js → jsonc-BdyZhBUw.js} +0 -0
- /package/dist/web/assets/{jsonl-CfxNSwGd.js → jsonl-BsDSNvwi.js} +0 -0
- /package/dist/web/assets/{jsonnet-CQETCYrp.js → jsonnet-Gm963F-U.js} +0 -0
- /package/dist/web/assets/{jssm-D5o3Zzdz.js → jssm-Ds3bKWgJ.js} +0 -0
- /package/dist/web/assets/{jsx-BAMILPLo.js → jsx-BT5mW58X.js} +0 -0
- /package/dist/web/assets/{kanagawa-dragon-BhGVxu3R.js → kanagawa-dragon-BvNvf7xd.js} +0 -0
- /package/dist/web/assets/{kanagawa-lotus-FH4Y59ro.js → kanagawa-lotus-CuSiliSn.js} +0 -0
- /package/dist/web/assets/{kanagawa-wave-eilvP9MB.js → kanagawa-wave-C6g-d6jT.js} +0 -0
- /package/dist/web/assets/{kdl-BQ-yvXUK.js → kdl-Bx4kZef-.js} +0 -0
- /package/dist/web/assets/{kotlin-BuCx0-VT.js → kotlin-qvaVvCcm.js} +0 -0
- /package/dist/web/assets/{kusto-DAN_n_Cy.js → kusto-BPLyNCN6.js} +0 -0
- /package/dist/web/assets/{laserwave-Cq2BlxrT.js → laserwave-Sf8ZEhWP.js} +0 -0
- /package/dist/web/assets/{lean-Dhq7uXKw.js → lean-DGe6uqgk.js} +0 -0
- /package/dist/web/assets/{less-CxaMQb8j.js → less-CqM85x-9.js} +0 -0
- /package/dist/web/assets/{lib-BVwmtv2i.js → lib-CD6pOS4u.js} +0 -0
- /package/dist/web/assets/{libesm-DdtEjhIi.js → libesm-CPh1Av0r.js} +0 -0
- /package/dist/web/assets/{light-plus-B4sMAend.js → light-plus-DBtKwhFA.js} +0 -0
- /package/dist/web/assets/{llvm-DQ6-B_6Y.js → llvm-Dpbq3gky.js} +0 -0
- /package/dist/web/assets/{log-Bb_vqvu7.js → log-qKMcPm9S.js} +0 -0
- /package/dist/web/assets/{logo-BzfYv3A_.js → logo-DFo7i5KH.js} +0 -0
- /package/dist/web/assets/{luau-BnAeNR77.js → luau-DGlqWFyL.js} +0 -0
- /package/dist/web/assets/{make-B6IFuxpC.js → make-CixdvUQe.js} +0 -0
- /package/dist/web/assets/{markdown-Z0xi77Fn.js → markdown-CmRnbngE.js} +0 -0
- /package/dist/web/assets/{material-theme-1scy1SYn.js → material-theme-WDUbafB-.js} +0 -0
- /package/dist/web/assets/{material-theme-darker-G-JykXQP.js → material-theme-darker-u8y2xpOk.js} +0 -0
- /package/dist/web/assets/{material-theme-lighter-BtKkCMVq.js → material-theme-lighter-cyQZK3Tg.js} +0 -0
- /package/dist/web/assets/{material-theme-ocean-irrDC89L.js → material-theme-ocean-LBAVvw7k.js} +0 -0
- /package/dist/web/assets/{material-theme-palenight-Eq5_K4LM.js → material-theme-palenight-BJqhBxL0.js} +0 -0
- /package/dist/web/assets/{matlab-BE3oCXZC.js → matlab-B38sOJee.js} +0 -0
- /package/dist/web/assets/{mdx-DYiuFTGw.js → mdx-C8QrE_IW.js} +0 -0
- /package/dist/web/assets/{mermaid-DVlaEvtw.js → mermaid-BQ-eeU8A.js} +0 -0
- /package/dist/web/assets/{min-dark-DSkeTpQa.js → min-dark-D4gNa5tv.js} +0 -0
- /package/dist/web/assets/{min-light-CFv2Q0uF.js → min-light-Dfbhn_PL.js} +0 -0
- /package/dist/web/assets/{mipsasm-CiAFuZQ2.js → mipsasm-bLOhJkdm.js} +0 -0
- /package/dist/web/assets/{mojo-ARshcaSQ.js → mojo-B0BCOMQW.js} +0 -0
- /package/dist/web/assets/{monaco-dotenv-language-BDYnKcnr.js → monaco-dotenv-language-uB-wok6C.js} +0 -0
- /package/dist/web/assets/{monokai-CPTV-SOg.js → monokai-Bdw_07OM.js} +0 -0
- /package/dist/web/assets/{moonbit-D57B0dZr.js → moonbit-BFdGaeuk.js} +0 -0
- /package/dist/web/assets/{move-qgy3G_o9.js → move-CyXQFGPM.js} +0 -0
- /package/dist/web/assets/{narrat-CZ3rC-oc.js → narrat-9DvTD3_3.js} +0 -0
- /package/dist/web/assets/{nextflow-groovy-BxxIOcI4.js → nextflow-groovy-C4VY3kNY.js} +0 -0
- /package/dist/web/assets/{night-owl-B1ryGCda.js → night-owl-feYm66V0.js} +0 -0
- /package/dist/web/assets/{night-owl-light-BZvNnvMF.js → night-owl-light-DALqFjvx.js} +0 -0
- /package/dist/web/assets/{nix-BVBQi8yv.js → nix-C1kPA9Sw.js} +0 -0
- /package/dist/web/assets/{nord-3NXw613h.js → nord-1dvrj1Al.js} +0 -0
- /package/dist/web/assets/{nushell-BMOLRalb.js → nushell-DSQ_9Uqj.js} +0 -0
- /package/dist/web/assets/{objective-c-AxruEUgh.js → objective-c-Dhcn5ZK8.js} +0 -0
- /package/dist/web/assets/{objective-cpp-cFT0qRsD.js → objective-cpp-CGVYCSrp.js} +0 -0
- /package/dist/web/assets/{ocaml-_8JH6O8J.js → ocaml-rxXW21An.js} +0 -0
- /package/dist/web/assets/{odin-nwYvR0O6.js → odin-ClSJX2DE.js} +0 -0
- /package/dist/web/assets/{one-dark-pro-C1LaYJaH.js → one-dark-pro-3gbWDfFW.js} +0 -0
- /package/dist/web/assets/{one-light-B7rMc__c.js → one-light-C2q5ZZAw.js} +0 -0
- /package/dist/web/assets/{openscad-Cwes8bKO.js → openscad-eHXeafBl.js} +0 -0
- /package/dist/web/assets/{pascal-DMt-6tdZ.js → pascal-kKha3Txs.js} +0 -0
- /package/dist/web/assets/{pkl-QBTyYQmB.js → pkl-CTiPytg-.js} +0 -0
- /package/dist/web/assets/{plastic-BTSUp8Fk.js → plastic-D1Ygezi4.js} +0 -0
- /package/dist/web/assets/{plsql-DQv3IFXP.js → plsql-D_gbgZIr.js} +0 -0
- /package/dist/web/assets/{po-CNwZx4tW.js → po-BkhbDUnq.js} +0 -0
- /package/dist/web/assets/{poimandres-8Os7Qequ.js → poimandres-Do6NywrA.js} +0 -0
- /package/dist/web/assets/{polar-b4yK8oD8.js → polar-B3B23wca.js} +0 -0
- /package/dist/web/assets/{postcss-sFctwpxx.js → postcss-Dv_YBvWd.js} +0 -0
- /package/dist/web/assets/{powerquery-Cg_WrtuN.js → powerquery-fKJWI-Sh.js} +0 -0
- /package/dist/web/assets/{powershell-C7Ifz95h.js → powershell-B4Y7KIgx.js} +0 -0
- /package/dist/web/assets/{prisma-Z52HUJzQ.js → prisma-J--XdTXX.js} +0 -0
- /package/dist/web/assets/{prolog-DLqW-zR1.js → prolog-BBTg4Vb3.js} +0 -0
- /package/dist/web/assets/{proto-BOIzxCCJ.js → proto-DWlum3T8.js} +0 -0
- /package/dist/web/assets/{puppet-De7TjZIf.js → puppet-ZiUfc9JV.js} +0 -0
- /package/dist/web/assets/{purescript-R76ou1WT.js → purescript-CmT8SCHB.js} +0 -0
- /package/dist/web/assets/{python-Dprm0tNP.js → python-BURpj-fq.js} +0 -0
- /package/dist/web/assets/{qmldir-Clp_jpNw.js → qmldir-CpaiJywQ.js} +0 -0
- /package/dist/web/assets/{qss-eQPYYzSo.js → qss-C-zdz0i5.js} +0 -0
- /package/dist/web/assets/{r-CCcRHIf5.js → r-K56BeS-s.js} +0 -0
- /package/dist/web/assets/{racket-CzE472yp.js → racket-gkn6Z_Jw.js} +0 -0
- /package/dist/web/assets/{raku-DQwXrTSi.js → raku-C0ZBuKlx.js} +0 -0
- /package/dist/web/assets/{red-KCS6wyop.js → red-BWZ6x-3p.js} +0 -0
- /package/dist/web/assets/{reg-CcrAddUh.js → reg-Dt_EsyGR.js} +0 -0
- /package/dist/web/assets/{regexp-DDbud-Ri.js → regexp-CT6BLG2C.js} +0 -0
- /package/dist/web/assets/{rel-99GDgaHp.js → rel-MJ2LcyzJ.js} +0 -0
- /package/dist/web/assets/{resolve-theme-CW2niGV2.js → resolve-theme-B_ukrg_8.js} +0 -0
- /package/dist/web/assets/{riscv-Dm2eNv0R.js → riscv-BZYWzMAA.js} +0 -0
- /package/dist/web/assets/{ron-CzFt0ygH.js → ron-CLbwHWMn.js} +0 -0
- /package/dist/web/assets/{rose-pine-Ck6lCrCP.js → rose-pine-BPiI_daR.js} +0 -0
- /package/dist/web/assets/{rose-pine-dawn-vtALRJNj.js → rose-pine-dawn-BjkJc7dW.js} +0 -0
- /package/dist/web/assets/{rose-pine-moon-DPto8qXw.js → rose-pine-moon-BQSxkt7g.js} +0 -0
- /package/dist/web/assets/{rosmsg-NtS6429H.js → rosmsg-yt4151pJ.js} +0 -0
- /package/dist/web/assets/{rust-BjIXvF9h.js → rust-1dAcengL.js} +0 -0
- /package/dist/web/assets/{sass-CQ23wf5E.js → sass-DUgtQJsg.js} +0 -0
- /package/dist/web/assets/{scala-LfKLM143.js → scala-CxoGYpS0.js} +0 -0
- /package/dist/web/assets/{scheme-D3SLBXlF.js → scheme-B2K3u9e3.js} +0 -0
- /package/dist/web/assets/{sdbl-ODRd2rGN.js → sdbl-C0Ygk0n6.js} +0 -0
- /package/dist/web/assets/{shellscript-3KB1G_oA.js → shellscript-C2CvhqMk.js} +0 -0
- /package/dist/web/assets/{slack-dark-DTtyWmIy.js → slack-dark-DYdgzRGj.js} +0 -0
- /package/dist/web/assets/{slack-ochin-Bu0PApk3.js → slack-ochin-X-WD0WCg.js} +0 -0
- /package/dist/web/assets/{smalltalk-haMT-Sbe.js → smalltalk-Dp7g_zb9.js} +0 -0
- /package/dist/web/assets/{snazzy-light-DXJzefE9.js → snazzy-light-DWHWr-a6.js} +0 -0
- /package/dist/web/assets/{solarized-dark-B6LyYa18.js → solarized-dark-CxdLsjvb.js} +0 -0
- /package/dist/web/assets/{solarized-light-QLADebkv.js → solarized-light-Cz9rAvG5.js} +0 -0
- /package/dist/web/assets/{solidity-mzfq1aDc.js → solidity-D33gHHdn.js} +0 -0
- /package/dist/web/assets/{splunk-CyH4RWBp.js → splunk-BFbOroYw.js} +0 -0
- /package/dist/web/assets/{sql-CWVlT5D3.js → sql-ChpxvmdY.js} +0 -0
- /package/dist/web/assets/{ssh-config-DNvQJ1Qx.js → ssh-config-CUuSQ6JG.js} +0 -0
- /package/dist/web/assets/{streaming-store-uDu3PoN1.js → streaming-store-BYukdB56.js} +0 -0
- /package/dist/web/assets/{stylus-ByQQdtmf.js → stylus-DDihP0_W.js} +0 -0
- /package/dist/web/assets/{swift-D1cpY4ow.js → swift-XoalUn4x.js} +0 -0
- /package/dist/web/assets/{synthwave-84-BDF_-QiW.js → synthwave-84-DjkG0Bff.js} +0 -0
- /package/dist/web/assets/{system-verilog-BYrkMydC.js → system-verilog-f_pLKMYd.js} +0 -0
- /package/dist/web/assets/{systemd-CQm3zyK0.js → systemd-DjDcLfxp.js} +0 -0
- /package/dist/web/assets/{talonscript-DMW5CzE1.js → talonscript-BpUzYcDl.js} +0 -0
- /package/dist/web/assets/{tasl-BhFzMCr2.js → tasl-DP-cUbg8.js} +0 -0
- /package/dist/web/assets/{tcl-D7VEAF-9.js → tcl-BHoy-dfE.js} +0 -0
- /package/dist/web/assets/{terraform-DdT5A0Vy.js → terraform-vCVxN25B.js} +0 -0
- /package/dist/web/assets/{tokyo-night-WGLMM6Q9.js → tokyo-night-D0u4xZba.js} +0 -0
- /package/dist/web/assets/{toml-CZ7ipXHZ.js → toml-BS8D0__A.js} +0 -0
- /package/dist/web/assets/{tsv-B1zVvoTr.js → tsv-DCb-NQh3.js} +0 -0
- /package/dist/web/assets/{tsx-D4HwRUBq.js → tsx-DF8YeRgG.js} +0 -0
- /package/dist/web/assets/{turtle-B4r8KEJH.js → turtle-Bhx1GlIy.js} +0 -0
- /package/dist/web/assets/{typescript-BnHhxMtp.js → typescript-CAVD2Wb6.js} +0 -0
- /package/dist/web/assets/{typespec-DP2q46GT.js → typespec-D7mchdY_.js} +0 -0
- /package/dist/web/assets/{typst-DFsD_yaD.js → typst-CeXzq6Yw.js} +0 -0
- /package/dist/web/assets/{use-is-mobile-BQ9Bv5Ga.js → use-is-mobile-TkcwP1Gx.js} +0 -0
- /package/dist/web/assets/{use-visual-viewport-CbwnktSG.js → use-visual-viewport-Lp878uzs.js} +0 -0
- /package/dist/web/assets/{v-Bji8hSVT.js → v-DhzO54je.js} +0 -0
- /package/dist/web/assets/{vala-D3TIgGmU.js → vala-Dw_RAk3E.js} +0 -0
- /package/dist/web/assets/{vb-DvcxFS2q.js → vb-CiXdNh2Y.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-BqPDsE9b.js → vendor-xterm-BHEY6Ctu.js} +0 -0
- /package/dist/web/assets/{verilog-Dy7cF35d.js → verilog-fITdxqwV.js} +0 -0
- /package/dist/web/assets/{vesper-BzjwUv9J.js → vesper-B1wm_At7.js} +0 -0
- /package/dist/web/assets/{vhdl-DuFnFNmW.js → vhdl-ebHjqKp9.js} +0 -0
- /package/dist/web/assets/{viml-yAPgKlbz.js → viml-Dgmnoxoo.js} +0 -0
- /package/dist/web/assets/{vitesse-black-oSSQdyIZ.js → vitesse-black-BZ5gMb7l.js} +0 -0
- /package/dist/web/assets/{vitesse-dark-BZxwPihB.js → vitesse-dark-Lij7kkOt.js} +0 -0
- /package/dist/web/assets/{vitesse-light-uIigycvt.js → vitesse-light-DW6Wq7VC.js} +0 -0
- /package/dist/web/assets/{vyper-CX3NAxm5.js → vyper-6FxDnVNg.js} +0 -0
- /package/dist/web/assets/{wasm-6mEOOvVa.js → wasm-BDxaNPCj.js} +0 -0
- /package/dist/web/assets/{wasm-D9td37qO.js → wasm-DKhAtzfe.js} +0 -0
- /package/dist/web/assets/{wenyan-lwNIHgk6.js → wenyan-C0k0DYG_.js} +0 -0
- /package/dist/web/assets/{wgsl-DQAzCfog.js → wgsl-Y2zagbdn.js} +0 -0
- /package/dist/web/assets/{wikitext-DTXmt5NE.js → wikitext-C4QGWzkY.js} +0 -0
- /package/dist/web/assets/{wit-Bdw_7WDH.js → wit-CQESzjHy.js} +0 -0
- /package/dist/web/assets/{wolfram-DqyHDZ07.js → wolfram-B-ia69Ia.js} +0 -0
- /package/dist/web/assets/{yaml-CPXhdByd.js → yaml-CEYYTQPm.js} +0 -0
- /package/dist/web/assets/{zenscript-6s_i5zek.js → zenscript-BggWXIeJ.js} +0 -0
- /package/dist/web/assets/{zig-B2htezJ_.js → zig-CbaUfBQe.js} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, useMemo, useCallback, useLayoutEffect, memo
|
|
1
|
+
import { useEffect, useRef, useState, useMemo, useCallback, useLayoutEffect, memo } from "react";
|
|
2
|
+
import { userMessageOrdinals } from "@/lib/message-ordinals";
|
|
2
3
|
import { useStickToBottom } from "use-stick-to-bottom";
|
|
3
|
-
import {
|
|
4
|
-
import type { ChatMessage, ChatEvent } from "../../../types/chat";
|
|
4
|
+
import type { ChatMessage } from "../../../types/chat";
|
|
5
5
|
import type { SessionPhase } from "../../../types/api";
|
|
6
6
|
import type { BashPartialEntry } from "../../hooks/use-chat";
|
|
7
7
|
import { ToolCard } from "./tool-cards";
|
|
@@ -15,50 +15,26 @@ import { TurnChangeRollup } from "./turn-change-rollup";
|
|
|
15
15
|
import { TurnCostWarning } from "./turn-cost-warning";
|
|
16
16
|
import { TaskTracker } from "./task-tracker";
|
|
17
17
|
import { extractJsonlPath } from "./pre-compact-button";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const markdownRendererImport = import("@/components/shared/markdown-renderer");
|
|
22
|
-
const MarkdownRenderer = lazy(() =>
|
|
23
|
-
markdownRendererImport.then((m) => ({ default: m.MarkdownRenderer }))
|
|
24
|
-
);
|
|
25
|
-
import { cn, basename } from "@/lib/utils";
|
|
18
|
+
import { MarkdownContent } from "./message-markdown";
|
|
19
|
+
import { UserBubble } from "./message-user-bubble";
|
|
20
|
+
import { InterleavedEvents, ThinkingIndicator } from "./message-events";
|
|
26
21
|
import { RenderErrorBoundary } from "@/components/shared/markdown-error-boundary";
|
|
27
22
|
|
|
28
23
|
import {
|
|
29
24
|
AlertCircle,
|
|
30
25
|
ShieldAlert,
|
|
31
26
|
Bot,
|
|
32
|
-
ChevronDown,
|
|
33
|
-
ChevronRight,
|
|
34
|
-
FileText,
|
|
35
|
-
Image as ImageIcon,
|
|
36
27
|
Copy,
|
|
37
28
|
Check,
|
|
38
|
-
CheckCircle2,
|
|
39
29
|
Loader2,
|
|
40
|
-
RotateCcw,
|
|
41
|
-
TerminalSquare,
|
|
42
|
-
ChevronUp,
|
|
43
|
-
Tag,
|
|
44
30
|
XCircle,
|
|
45
|
-
ExternalLink,
|
|
46
|
-
Slash,
|
|
47
|
-
Pencil,
|
|
48
31
|
} from "@/lib/icons";
|
|
49
32
|
import { ChatWelcome } from "./chat-welcome";
|
|
50
33
|
import { ChatScrollNav } from "./chat-scroll-nav";
|
|
51
|
-
import { MessageActionBar, ActionButton } from "./message-action-bar";
|
|
52
|
-
import { VersionSwitcher } from "./version-switcher";
|
|
53
34
|
import type { VersionGroup } from "../../../types/api";
|
|
54
35
|
import { QuestionCard } from "./question-card";
|
|
55
|
-
import { parseUserMessage, type SystemTag } from "./user-message-parse";
|
|
56
36
|
import type { Question } from "./question-card";
|
|
57
|
-
import {
|
|
58
|
-
import { api } from "@/lib/api-client";
|
|
59
|
-
import { useProjectStore } from "@/stores/project-store";
|
|
60
|
-
import { useImageOverlay } from "@/stores/image-overlay-store";
|
|
61
|
-
import { collectGallery, GALLERY_ITEM_ATTR, GALLERY_ROOT_ATTR } from "@/lib/image-gallery";
|
|
37
|
+
import { GALLERY_ROOT_ATTR } from "@/lib/image-gallery";
|
|
62
38
|
|
|
63
39
|
interface MessageListProps {
|
|
64
40
|
messages: ChatMessage[];
|
|
@@ -105,6 +81,40 @@ interface MessageListProps {
|
|
|
105
81
|
isCompactExpanded?: (compactMessageId: string) => boolean;
|
|
106
82
|
}
|
|
107
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Placeholder for a compaction segment being fetched.
|
|
86
|
+
*
|
|
87
|
+
* It sits in the flow rather than floating, so scrolling up lands on something
|
|
88
|
+
* the size of the turns that are coming instead of a blank gap that then jerks
|
|
89
|
+
* downward. The layout effect that preserves distance-from-bottom is what keeps
|
|
90
|
+
* inserting it from moving the messages already on screen.
|
|
91
|
+
*/
|
|
92
|
+
function PreCompactSkeleton() {
|
|
93
|
+
return (
|
|
94
|
+
<div
|
|
95
|
+
className="px-4 pt-4 space-y-4"
|
|
96
|
+
aria-busy="true"
|
|
97
|
+
aria-label="Loading previous conversation"
|
|
98
|
+
>
|
|
99
|
+
<div className="flex items-center justify-center gap-1.5 text-xs text-text-secondary">
|
|
100
|
+
<Loader2 className="size-3 animate-spin" />
|
|
101
|
+
Loading previous conversation…
|
|
102
|
+
</div>
|
|
103
|
+
{[0, 1, 2].map((i) => (
|
|
104
|
+
<div key={i} className={i % 2 === 0 ? "flex justify-end" : "flex justify-start"}>
|
|
105
|
+
<div
|
|
106
|
+
className={`animate-pulse space-y-2 rounded-lg bg-surface p-3 ${i % 2 === 0 ? "w-1/2" : "w-3/4"}`}
|
|
107
|
+
>
|
|
108
|
+
<div className="h-3 rounded bg-border" />
|
|
109
|
+
<div className="h-3 w-5/6 rounded bg-border" />
|
|
110
|
+
{i % 2 === 1 && <div className="h-3 w-2/3 rounded bg-border" />}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
108
118
|
export function MessageList({
|
|
109
119
|
messages,
|
|
110
120
|
messagesLoading,
|
|
@@ -150,6 +160,10 @@ export function MessageList({
|
|
|
150
160
|
return hasContent || hasEvents;
|
|
151
161
|
}), [messages]);
|
|
152
162
|
|
|
163
|
+
// Counted once for the whole list — see `userMessageOrdinals` for why the
|
|
164
|
+
// obvious per-row form is the thing that makes a long transcript unusable.
|
|
165
|
+
const userOrdinals = useMemo(() => userMessageOrdinals(filtered), [filtered]);
|
|
166
|
+
|
|
153
167
|
// The approval card + "thinking…" indicator ride at the end, inside the scrolled
|
|
154
168
|
// content so stick-to-bottom keeps them in view.
|
|
155
169
|
const hasTrailing = !!pendingApproval || isStreaming;
|
|
@@ -200,14 +214,26 @@ export function MessageList({
|
|
|
200
214
|
// distance-from-bottom before the prepend, restore scrollTop after so the content
|
|
201
215
|
// being read doesn't jump. Only fires for prepends — streaming appends leave the
|
|
202
216
|
// ref null, so this is a no-op during normal streaming.
|
|
217
|
+
// Declared above the scroll-preserving layout effect below, which reads
|
|
218
|
+
// `autoLoadingCompact` to know whether the skeleton is still occupying space.
|
|
219
|
+
const [autoLoadingCompact, setAutoLoadingCompact] = useState(false);
|
|
220
|
+
// A failed expand used to be invisible: the fetch rejected, nothing caught it,
|
|
221
|
+
// and scrolling to the top of a long chat simply did nothing forever. Holding
|
|
222
|
+
// the reason both shows it and stops the loader retrying on every intersection.
|
|
223
|
+
const [compactLoadError, setCompactLoadError] = useState<string | null>(null);
|
|
224
|
+
|
|
203
225
|
const preserveFromBottomRef = useRef<number | null>(null);
|
|
204
226
|
useLayoutEffect(() => {
|
|
205
227
|
const el = scrollEl;
|
|
206
228
|
if (el && preserveFromBottomRef.current != null) {
|
|
207
229
|
el.scrollTop = el.scrollHeight - preserveFromBottomRef.current;
|
|
208
|
-
|
|
230
|
+
// Held, not cleared, while the skeleton is up: it occupies real space at
|
|
231
|
+
// the top, so it moves `scrollHeight` without `filtered.length` changing.
|
|
232
|
+
// Clearing here would leave the view jumped by the skeleton's height for
|
|
233
|
+
// as long as the fetch takes, then jumped back when it resolved.
|
|
234
|
+
if (!autoLoadingCompact) preserveFromBottomRef.current = null;
|
|
209
235
|
}
|
|
210
|
-
}, [filtered.length, scrollEl]);
|
|
236
|
+
}, [filtered.length, scrollEl, autoLoadingCompact]);
|
|
211
237
|
|
|
212
238
|
// Jump to the newest message when the conversation/session swaps (initial mount is
|
|
213
239
|
// handled by `initial: "instant"`). Keyed on sessionId — NOT on filtered[0].id,
|
|
@@ -295,30 +321,53 @@ export function MessageList({
|
|
|
295
321
|
const topUnexpandedCompact = findTopUnexpandedCompact();
|
|
296
322
|
const hasMore = !!topUnexpandedCompact;
|
|
297
323
|
|
|
324
|
+
// Held as two strings rather than as the object. `findTopUnexpandedCompact`
|
|
325
|
+
// runs on every render and answers with a fresh literal, so depending on that
|
|
326
|
+
// object made `loadMore` new every render — and the observer effect below
|
|
327
|
+
// tore the IntersectionObserver down and built another one with it, which
|
|
328
|
+
// during streaming is every token batch. Measured before: 1 observer at
|
|
329
|
+
// mount, +1 per re-render against an unchanged message list.
|
|
330
|
+
const topCompactId = topUnexpandedCompact?.id ?? null;
|
|
331
|
+
const topCompactPath = topUnexpandedCompact?.jsonlPath ?? null;
|
|
332
|
+
|
|
298
333
|
// Fetch pre-compact history from the server (prepends older messages).
|
|
299
|
-
const [autoLoadingCompact, setAutoLoadingCompact] = useState(false);
|
|
300
334
|
const loadMore = useCallback(async () => {
|
|
301
|
-
if (!
|
|
335
|
+
if (!topCompactId || !topCompactPath || !onExpandCompact || autoLoadingCompact) return;
|
|
302
336
|
// Capture distance-from-bottom so the post-prepend layout effect can hold the
|
|
303
337
|
// reading position steady while older messages are inserted above.
|
|
304
338
|
const el = scrollEl;
|
|
305
339
|
preserveFromBottomRef.current = el ? el.scrollHeight - el.scrollTop : null;
|
|
306
340
|
setAutoLoadingCompact(true);
|
|
341
|
+
setCompactLoadError(null);
|
|
307
342
|
try {
|
|
308
|
-
await onExpandCompact(
|
|
343
|
+
await onExpandCompact(topCompactId, topCompactPath);
|
|
344
|
+
} catch (e) {
|
|
345
|
+
setCompactLoadError(e instanceof Error ? e.message : "Could not load previous conversation");
|
|
309
346
|
} finally {
|
|
310
347
|
setAutoLoadingCompact(false);
|
|
311
348
|
}
|
|
312
|
-
}, [
|
|
313
|
-
|
|
314
|
-
//
|
|
349
|
+
}, [topCompactId, topCompactPath, onExpandCompact, autoLoadingCompact, scrollEl]);
|
|
350
|
+
|
|
351
|
+
// Lazy-load older history when a sentinel at the top of the transcript comes
|
|
352
|
+
// into range. This replaces a scroll listener that re-ran its check on every
|
|
353
|
+
// scroll event and — the part that actually failed — could not fire at all
|
|
354
|
+
// when the loaded history was shorter than the viewport, because there was
|
|
355
|
+
// nothing to scroll. `loadMore` guards on `autoLoadingCompact`, so repeat
|
|
356
|
+
// intersections while a fetch is in flight are free.
|
|
357
|
+
const topSentinelRef = useRef<HTMLDivElement | null>(null);
|
|
315
358
|
useEffect(() => {
|
|
316
359
|
const el = scrollEl;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
360
|
+
const sentinel = topSentinelRef.current;
|
|
361
|
+
if (!el || !sentinel || !hasMore || compactLoadError) return;
|
|
362
|
+
const io = new IntersectionObserver(
|
|
363
|
+
(entries) => { if (entries.some((e) => e.isIntersecting)) loadMore(); },
|
|
364
|
+
// Start the fetch while the top is still a screenful away, so the next
|
|
365
|
+
// segment is usually there before the user reaches the end of this one.
|
|
366
|
+
{ root: el, rootMargin: "400px 0px 0px 0px" },
|
|
367
|
+
);
|
|
368
|
+
io.observe(sentinel);
|
|
369
|
+
return () => io.disconnect();
|
|
370
|
+
}, [scrollEl, hasMore, compactLoadError, loadMore]);
|
|
322
371
|
|
|
323
372
|
if (messagesLoading && (!keepStaleWhileLoading || messages.length === 0)) {
|
|
324
373
|
return (
|
|
@@ -353,10 +402,17 @@ export function MessageList({
|
|
|
353
402
|
</button>
|
|
354
403
|
</div>
|
|
355
404
|
)}
|
|
356
|
-
{autoLoadingCompact && (
|
|
357
|
-
<div className="absolute top-2 left-0 right-0 z-10 flex
|
|
358
|
-
<
|
|
359
|
-
|
|
405
|
+
{!autoLoadingCompact && compactLoadError && (
|
|
406
|
+
<div className="absolute top-2 left-0 right-0 z-10 flex justify-center">
|
|
407
|
+
<button
|
|
408
|
+
type="button"
|
|
409
|
+
onClick={() => { setCompactLoadError(null); loadMore(); }}
|
|
410
|
+
className="inline-flex items-center gap-1.5 rounded-md border border-border bg-surface px-3 py-2 text-xs text-text-primary hover:bg-surface-hover min-h-[44px] md:min-h-0"
|
|
411
|
+
>
|
|
412
|
+
<AlertCircle className="size-3.5 text-destructive" />
|
|
413
|
+
<span>Could not load previous conversation: {compactLoadError}</span>
|
|
414
|
+
<span className="underline">Retry</span>
|
|
415
|
+
</button>
|
|
360
416
|
</div>
|
|
361
417
|
)}
|
|
362
418
|
<div
|
|
@@ -366,12 +422,12 @@ export function MessageList({
|
|
|
366
422
|
style={{ WebkitOverflowScrolling: "touch", overscrollBehavior: "contain" }}
|
|
367
423
|
>
|
|
368
424
|
<div ref={contentRef as unknown as React.Ref<HTMLDivElement>} className="w-full">
|
|
425
|
+
{hasMore && <div ref={topSentinelRef} aria-hidden className="h-px" />}
|
|
426
|
+
{autoLoadingCompact && <PreCompactSkeleton />}
|
|
369
427
|
{filtered.map((msg, globalIdx) => {
|
|
370
428
|
const prevMsg = globalIdx > 0 ? filtered[globalIdx - 1] : undefined;
|
|
371
429
|
// User-message ordinal (1-based) — stable version-group anchor across forks.
|
|
372
|
-
const versionOrdinal =
|
|
373
|
-
? filtered.slice(0, globalIdx + 1).reduce((n, m) => n + (m.role === "user" ? 1 : 0), 0)
|
|
374
|
-
: 0;
|
|
430
|
+
const versionOrdinal = userOrdinals[globalIdx] ?? 0;
|
|
375
431
|
// Resolved here rather than deeper down so the ordinal→group lookup
|
|
376
432
|
// happens once per message instead of being threaded through bubbles.
|
|
377
433
|
const versionGroup = versionOrdinal ? versionMap?.[versionOrdinal] : undefined;
|
|
@@ -533,820 +589,10 @@ const MessageBubble = memo(function MessageBubble({ message, isStreaming, isLast
|
|
|
533
589
|
);
|
|
534
590
|
});
|
|
535
591
|
|
|
536
|
-
/** Image extensions that can be previewed inline */
|
|
537
|
-
const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
538
|
-
|
|
539
|
-
/** Build a preview URL for an uploaded file (served from /chat/uploads/:filename) */
|
|
540
|
-
function uploadPreviewUrl(filePath: string, projectName?: string): string {
|
|
541
|
-
const filename = basename(filePath);
|
|
542
|
-
// Use a generic project name — the upload route is project-scoped but files are global
|
|
543
|
-
return `/api/project/${encodeURIComponent(projectName ?? "_")}/chat/uploads/${encodeURIComponent(filename)}`;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
/** Check if a file path is an image based on extension */
|
|
547
|
-
function isImagePath(path: string): boolean {
|
|
548
|
-
const dot = path.lastIndexOf(".");
|
|
549
|
-
if (dot === -1) return false;
|
|
550
|
-
return IMAGE_EXTS.has(path.slice(dot).toLowerCase());
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
function isPdfPath(path: string): boolean {
|
|
554
|
-
return path.toLowerCase().endsWith(".pdf");
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/** Detect if tags contain system-injected content (not real user input) */
|
|
558
|
-
const SYSTEM_TAG_NAMES = new Set(["task-notification", "environment_details", "local-command-caveat"]);
|
|
559
|
-
|
|
560
|
-
/** User message bubble — full width, collapsible, with system tag badges */
|
|
561
|
-
function UserBubble({ content, messageId, timestamp, projectName, onFork, onEdit, isEditing, sessionId, providerId, versionGroup, onNavigateVersion, versionNavDisabled }: {
|
|
562
|
-
content: string;
|
|
563
|
-
messageId?: string;
|
|
564
|
-
timestamp: string;
|
|
565
|
-
projectName?: string;
|
|
566
|
-
onFork?: () => void;
|
|
567
|
-
onEdit?: () => void;
|
|
568
|
-
isEditing?: boolean;
|
|
569
|
-
sessionId?: string;
|
|
570
|
-
providerId?: string;
|
|
571
|
-
versionGroup?: VersionGroup;
|
|
572
|
-
onNavigateVersion?: (sessionId: string) => void;
|
|
573
|
-
versionNavDisabled?: boolean;
|
|
574
|
-
}) {
|
|
575
|
-
const { files, text, tags, command, terminalBlocks, idePath, agent } = useMemo(
|
|
576
|
-
() => parseUserMessage(content),
|
|
577
|
-
[content],
|
|
578
|
-
);
|
|
579
|
-
|
|
580
|
-
const isSystemContext = tags.some((t) => SYSTEM_TAG_NAMES.has(t.name));
|
|
581
|
-
|
|
582
|
-
const [expanded, setExpanded] = useState(false);
|
|
583
|
-
const [isOverflowing, setIsOverflowing] = useState(false);
|
|
584
|
-
const contentRef = useRef<HTMLDivElement>(null);
|
|
585
|
-
|
|
586
|
-
useEffect(() => {
|
|
587
|
-
const el = contentRef.current;
|
|
588
|
-
if (!el) return;
|
|
589
|
-
const check = () => setIsOverflowing(el.scrollHeight > el.clientHeight + 2);
|
|
590
|
-
check();
|
|
591
|
-
const ro = new ResizeObserver(check);
|
|
592
|
-
ro.observe(el);
|
|
593
|
-
return () => ro.disconnect();
|
|
594
|
-
}, [text]);
|
|
595
|
-
|
|
596
|
-
return (
|
|
597
|
-
<div className={cn("flex flex-col gap-1", !isSystemContext && "items-end")}>
|
|
598
|
-
{/* Own-content bubble: accent-wash fill + custom radius (sharp bottom-right
|
|
599
|
-
corner anchors it to the sender on the right). */}
|
|
600
|
-
<div
|
|
601
|
-
data-user-message={!isSystemContext ? "true" : undefined}
|
|
602
|
-
style={!isSystemContext ? { borderRadius: "var(--rad) var(--rad) 4px var(--rad)" } : undefined}
|
|
603
|
-
className={cn(
|
|
604
|
-
"group/user relative px-3 py-2 text-sm border shadow-sm transition-all",
|
|
605
|
-
isSystemContext
|
|
606
|
-
? "rounded-lg bg-surface/40 border-border/40 text-text-secondary"
|
|
607
|
-
: "max-w-[80%] bg-accent-wash border-accent-wash-border text-text",
|
|
608
|
-
isEditing && "ring-2 ring-primary/60 border-primary/40",
|
|
609
|
-
)}>
|
|
610
|
-
{/* System tags as badges */}
|
|
611
|
-
{tags.length > 0 && <SystemTagBadges tags={tags} />}
|
|
612
|
-
|
|
613
|
-
{/* Agent delegation chip — parsed back from the "Use the X agent to" prefix */}
|
|
614
|
-
{agent && (
|
|
615
|
-
<div className="flex items-center gap-1.5 mb-0.5">
|
|
616
|
-
<span className="inline-flex items-center gap-1 rounded-md bg-sky-500/15 border border-sky-500/25 px-2 py-0.5 text-xs font-medium text-sky-600 dark:text-sky-400">
|
|
617
|
-
<Bot className="size-3 shrink-0" />
|
|
618
|
-
{agent}
|
|
619
|
-
</span>
|
|
620
|
-
</div>
|
|
621
|
-
)}
|
|
622
|
-
|
|
623
|
-
{/* Slash command chip — args rendered in body for expand/collapse support */}
|
|
624
|
-
{command && (
|
|
625
|
-
<div className="flex items-center gap-1.5 mb-0.5">
|
|
626
|
-
<span className="inline-flex items-center gap-1 rounded-md bg-primary/15 border border-primary/20 px-2 py-0.5 text-xs font-medium text-primary">
|
|
627
|
-
<Slash className="size-3 shrink-0" />
|
|
628
|
-
{command.name}
|
|
629
|
-
</span>
|
|
630
|
-
</div>
|
|
631
|
-
)}
|
|
632
|
-
|
|
633
|
-
{/* IDE context — the file the user had open, as a clickable chip */}
|
|
634
|
-
{idePath && (
|
|
635
|
-
<div className="flex items-center gap-1.5 mb-1 text-[11px] text-text-subtle">
|
|
636
|
-
<span className="shrink-0">Opened in IDE:</span>
|
|
637
|
-
<FilePathChip path={idePath} projectName={projectName} />
|
|
638
|
-
</div>
|
|
639
|
-
)}
|
|
640
|
-
|
|
641
|
-
{/* Attached files — image thumbnails + file chips */}
|
|
642
|
-
{files.length > 0 && (
|
|
643
|
-
<div className="flex flex-wrap gap-1.5">
|
|
644
|
-
{files.map((filePath, i) =>
|
|
645
|
-
isImagePath(filePath) ? (
|
|
646
|
-
<AuthImageThumbnail key={i} filePath={filePath} projectName={projectName} />
|
|
647
|
-
) : (
|
|
648
|
-
<div
|
|
649
|
-
key={i}
|
|
650
|
-
className="flex items-center gap-1 rounded-md border border-border/60 bg-background/40 px-1.5 py-0.5 text-[11px] text-text-secondary"
|
|
651
|
-
>
|
|
652
|
-
<FileText className="size-3 shrink-0" />
|
|
653
|
-
<span className="truncate max-w-32">{basename(filePath)}</span>
|
|
654
|
-
</div>
|
|
655
|
-
),
|
|
656
|
-
)}
|
|
657
|
-
</div>
|
|
658
|
-
)}
|
|
659
|
-
|
|
660
|
-
{/* Terminal output previews */}
|
|
661
|
-
{terminalBlocks.length > 0 && (
|
|
662
|
-
<div className="space-y-1.5">
|
|
663
|
-
{terminalBlocks.map((block, i) => (
|
|
664
|
-
<TerminalBlockPreview key={i} content={block} />
|
|
665
|
-
))}
|
|
666
|
-
</div>
|
|
667
|
-
)}
|
|
668
|
-
|
|
669
|
-
{/* Text content — 2-line clamp by default, expandable */}
|
|
670
|
-
{text && (
|
|
671
|
-
<div
|
|
672
|
-
ref={contentRef}
|
|
673
|
-
className={cn(
|
|
674
|
-
"whitespace-pre-wrap break-words transition-all duration-200 select-text",
|
|
675
|
-
!expanded && "line-clamp-2",
|
|
676
|
-
expanded && "max-h-[50vh] overflow-y-auto",
|
|
677
|
-
)}
|
|
678
|
-
>
|
|
679
|
-
{isSystemContext ? <TextWithFilePaths text={text} projectName={projectName} /> : <TextWithLinks text={text} />}
|
|
680
|
-
</div>
|
|
681
|
-
)}
|
|
682
|
-
{(isOverflowing || expanded) && (
|
|
683
|
-
<button
|
|
684
|
-
onClick={() => setExpanded(!expanded)}
|
|
685
|
-
className={cn(
|
|
686
|
-
"flex items-center gap-1 text-xs mt-1 transition-colors",
|
|
687
|
-
isSystemContext ? "text-text-subtle hover:text-text-secondary" : "text-primary/70 hover:text-primary",
|
|
688
|
-
)}
|
|
689
|
-
>
|
|
690
|
-
{expanded ? <><ChevronUp className="size-3" />Show less</> : <><ChevronDown className="size-3" />Show more</>}
|
|
691
|
-
</button>
|
|
692
|
-
)}
|
|
693
|
-
{/* Version switcher — only when this message has edited siblings */}
|
|
694
|
-
{!isSystemContext && onNavigateVersion && (
|
|
695
|
-
<VersionSwitcher
|
|
696
|
-
group={versionGroup}
|
|
697
|
-
onNavigate={onNavigateVersion}
|
|
698
|
-
disabled={versionNavDisabled}
|
|
699
|
-
/>
|
|
700
|
-
)}
|
|
701
|
-
</div>
|
|
702
|
-
{/* Action bar below the bubble — timestamp, copy, edit/fork (real user messages only) */}
|
|
703
|
-
{!isSystemContext && (
|
|
704
|
-
<MessageActionBar timestamp={timestamp} content={content}>
|
|
705
|
-
{onEdit && (
|
|
706
|
-
<ActionButton
|
|
707
|
-
icon={<Pencil className="size-3.5" />}
|
|
708
|
-
label="Edit"
|
|
709
|
-
title="Edit this message (continue in the same tab)"
|
|
710
|
-
onClick={onEdit}
|
|
711
|
-
/>
|
|
712
|
-
)}
|
|
713
|
-
{onFork && (
|
|
714
|
-
<ActionButton
|
|
715
|
-
icon={<RotateCcw className="size-3.5" />}
|
|
716
|
-
label="Fork"
|
|
717
|
-
title="Retry from this message (fork into a new tab)"
|
|
718
|
-
onClick={onFork}
|
|
719
|
-
/>
|
|
720
|
-
)}
|
|
721
|
-
</MessageActionBar>
|
|
722
|
-
)}
|
|
723
|
-
</div>
|
|
724
|
-
);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
/** Collapsible terminal output preview in user messages */
|
|
728
|
-
function TerminalBlockPreview({ content }: { content: string }) {
|
|
729
|
-
const [expanded, setExpanded] = useState(false);
|
|
730
|
-
return (
|
|
731
|
-
<div>
|
|
732
|
-
<button
|
|
733
|
-
type="button"
|
|
734
|
-
onClick={() => setExpanded(!expanded)}
|
|
735
|
-
className={cn(
|
|
736
|
-
"flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs transition-colors",
|
|
737
|
-
expanded
|
|
738
|
-
? "border-primary/50 bg-surface-elevated text-text-primary"
|
|
739
|
-
: "border-border/60 bg-background/40 text-text-secondary hover:bg-surface",
|
|
740
|
-
)}
|
|
741
|
-
>
|
|
742
|
-
<TerminalSquare className="size-3.5 shrink-0" />
|
|
743
|
-
<span>Terminal output</span>
|
|
744
|
-
<ChevronDown className={cn("size-3 shrink-0 transition-transform", expanded && "rotate-180")} />
|
|
745
|
-
</button>
|
|
746
|
-
{expanded && (
|
|
747
|
-
<pre className="mt-1 max-h-40 overflow-auto rounded-md border border-border bg-background p-2 text-xs text-text-primary font-mono whitespace-pre-wrap break-words">
|
|
748
|
-
{content}
|
|
749
|
-
</pre>
|
|
750
|
-
)}
|
|
751
|
-
</div>
|
|
752
|
-
);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
/** Render system tags as collapsible badges */
|
|
756
|
-
function SystemTagBadges({ tags }: { tags: SystemTag[] }) {
|
|
757
|
-
return (
|
|
758
|
-
<div className="flex flex-wrap gap-1.5">
|
|
759
|
-
{tags.map((tag, i) => (
|
|
760
|
-
<SystemTagBadge key={i} tag={tag} />
|
|
761
|
-
))}
|
|
762
|
-
</div>
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function SystemTagBadge({ tag }: { tag: SystemTag }) {
|
|
767
|
-
const [open, setOpen] = useState(false);
|
|
768
|
-
|
|
769
|
-
// Task notification: render formatted instead of raw XML
|
|
770
|
-
if (tag.name === "task-notification") {
|
|
771
|
-
return <TaskNotificationBadge content={tag.content} />;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
return (
|
|
775
|
-
<div className="text-xs">
|
|
776
|
-
<button
|
|
777
|
-
onClick={() => setOpen(!open)}
|
|
778
|
-
className="flex items-center gap-1 rounded-full border border-border/60 bg-surface/50 px-2 py-0.5 text-text-subtle hover:text-text-secondary hover:bg-surface transition-colors"
|
|
779
|
-
>
|
|
780
|
-
<Tag className="size-2.5" />
|
|
781
|
-
<span>{tag.label}</span>
|
|
782
|
-
<ChevronRight className={cn("size-2.5 transition-transform", open && "rotate-90")} />
|
|
783
|
-
</button>
|
|
784
|
-
{open && (
|
|
785
|
-
<div className="mt-1 rounded border border-border/40 bg-surface/30 px-2 py-1.5 text-[11px] text-text-subtle/80 whitespace-pre-wrap max-h-40 overflow-y-auto leading-relaxed">
|
|
786
|
-
{tag.content}
|
|
787
|
-
</div>
|
|
788
|
-
)}
|
|
789
|
-
</div>
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
/** Extract a sub-tag value from XML-like content */
|
|
794
|
-
function xmlTag(content: string, tag: string): string | undefined {
|
|
795
|
-
const m = content.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`));
|
|
796
|
-
return m?.[1]?.trim() || undefined;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
/** Formatted badge for <task-notification> — shows status, summary, output file, result */
|
|
800
|
-
function TaskNotificationBadge({ content }: { content: string }) {
|
|
801
|
-
const [open, setOpen] = useState(false);
|
|
802
|
-
const status = xmlTag(content, "status");
|
|
803
|
-
const summary = xmlTag(content, "summary");
|
|
804
|
-
const outputFile = xmlTag(content, "output-file");
|
|
805
|
-
const result = xmlTag(content, "result");
|
|
806
|
-
const isOk = status === "completed";
|
|
807
|
-
|
|
808
|
-
return (
|
|
809
|
-
<div className="text-xs">
|
|
810
|
-
<button
|
|
811
|
-
onClick={() => setOpen(!open)}
|
|
812
|
-
className="flex items-center gap-1.5 rounded-full border border-border/60 bg-surface/50 px-2 py-0.5 text-text-subtle hover:text-text-secondary hover:bg-surface transition-colors"
|
|
813
|
-
>
|
|
814
|
-
{isOk ? <CheckCircle2 className="size-2.5 text-success" /> : <XCircle className="size-2.5 text-warning" />}
|
|
815
|
-
<span className="truncate max-w-80">{summary ?? "Task notification"}</span>
|
|
816
|
-
<ChevronRight className={cn("size-2.5 transition-transform shrink-0", open && "rotate-90")} />
|
|
817
|
-
</button>
|
|
818
|
-
{open && (
|
|
819
|
-
<div className="mt-1 rounded border border-border/40 bg-surface/30 px-2 py-1.5 space-y-1.5">
|
|
820
|
-
{/* Full summary (button truncates it) */}
|
|
821
|
-
{summary && <p className="text-[11px] text-text-secondary">{summary}</p>}
|
|
822
|
-
{outputFile && <FilePathChip path={outputFile} />}
|
|
823
|
-
{result && (
|
|
824
|
-
<div className="text-[11px] text-text-subtle/80 max-h-60 overflow-y-auto leading-relaxed">
|
|
825
|
-
<MarkdownContent content={result} />
|
|
826
|
-
</div>
|
|
827
|
-
)}
|
|
828
|
-
</div>
|
|
829
|
-
)}
|
|
830
|
-
</div>
|
|
831
|
-
);
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
/** Clickable file path chip — opens file in editor tab */
|
|
835
|
-
function FilePathChip({ path, projectName }: { path: string; projectName?: string }) {
|
|
836
|
-
const handleClick = useCallback(() => {
|
|
837
|
-
const openTab = useTabStore.getState().openTab;
|
|
838
|
-
const pName = projectName ?? useProjectStore.getState().activeProject?.name;
|
|
839
|
-
const fileName = basename(path);
|
|
840
|
-
const meta: Record<string, unknown> = { filePath: path };
|
|
841
|
-
if (pName) meta.projectName = pName;
|
|
842
|
-
// Try to verify file exists, then open; fallback: open directly
|
|
843
|
-
api.get(`/api/fs/read?path=${encodeURIComponent(path)}`).then(() => {
|
|
844
|
-
openTab({ type: "editor", title: fileName, metadata: meta, projectId: null, closable: true });
|
|
845
|
-
}).catch(() => {
|
|
846
|
-
openTab({ type: "editor", title: fileName, metadata: meta, projectId: null, closable: true });
|
|
847
|
-
});
|
|
848
|
-
}, [path, projectName]);
|
|
849
|
-
|
|
850
|
-
return (
|
|
851
|
-
<button
|
|
852
|
-
type="button"
|
|
853
|
-
onClick={handleClick}
|
|
854
|
-
className="inline-flex items-center gap-1 rounded border border-border/50 bg-surface/50 px-1.5 py-0.5 font-mono text-[10px] text-text-secondary hover:text-text-primary hover:bg-surface transition-colors cursor-pointer"
|
|
855
|
-
>
|
|
856
|
-
<FileText className="size-2.5 shrink-0" />
|
|
857
|
-
<span className="truncate max-w-60">{basename(path)}</span>
|
|
858
|
-
<ExternalLink className="size-2 shrink-0 opacity-50" />
|
|
859
|
-
</button>
|
|
860
|
-
);
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
/** Render plain text with http(s) URLs turned into clickable links. Preserves
|
|
864
|
-
* whitespace/newlines via the surrounding `whitespace-pre-wrap` container. */
|
|
865
|
-
function TextWithLinks({ text }: { text: string }) {
|
|
866
|
-
const parts = useMemo(() => {
|
|
867
|
-
const re = /(https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/g;
|
|
868
|
-
const result: { kind: "text" | "url"; value: string }[] = [];
|
|
869
|
-
let last = 0;
|
|
870
|
-
let m;
|
|
871
|
-
while ((m = re.exec(text)) !== null) {
|
|
872
|
-
if (m.index > last) result.push({ kind: "text", value: text.slice(last, m.index) });
|
|
873
|
-
result.push({ kind: "url", value: m[1]! });
|
|
874
|
-
last = m.index + m[0].length;
|
|
875
|
-
}
|
|
876
|
-
if (last < text.length) result.push({ kind: "text", value: text.slice(last) });
|
|
877
|
-
return result;
|
|
878
|
-
}, [text]);
|
|
879
|
-
|
|
880
|
-
// No URLs — return the raw string so nothing about the layout changes.
|
|
881
|
-
if (parts.every((p) => p.kind === "text")) return <>{text}</>;
|
|
882
|
-
|
|
883
|
-
return (
|
|
884
|
-
<>
|
|
885
|
-
{parts.map((p, i) =>
|
|
886
|
-
p.kind === "url" ? (
|
|
887
|
-
<a
|
|
888
|
-
key={i}
|
|
889
|
-
href={p.value}
|
|
890
|
-
target="_blank"
|
|
891
|
-
rel="noopener noreferrer"
|
|
892
|
-
className="text-primary underline underline-offset-2 hover:text-primary/80 break-all"
|
|
893
|
-
onClick={(e) => e.stopPropagation()}
|
|
894
|
-
>
|
|
895
|
-
{p.value}
|
|
896
|
-
</a>
|
|
897
|
-
) : (
|
|
898
|
-
<span key={i}>{p.value}</span>
|
|
899
|
-
),
|
|
900
|
-
)}
|
|
901
|
-
</>
|
|
902
|
-
);
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
/** Render text with absolute file paths detected and turned into clickable chips */
|
|
906
|
-
function TextWithFilePaths({ text, projectName }: { text: string; projectName?: string }) {
|
|
907
|
-
const parts = useMemo(() => {
|
|
908
|
-
// Match absolute file paths (at least 2 segments)
|
|
909
|
-
const re = /(\/(?:[\w.\-]+\/)+[\w.\-]+)/g;
|
|
910
|
-
const result: { kind: "text" | "path"; value: string }[] = [];
|
|
911
|
-
let last = 0;
|
|
912
|
-
let m;
|
|
913
|
-
while ((m = re.exec(text)) !== null) {
|
|
914
|
-
if (m.index > last) result.push({ kind: "text", value: text.slice(last, m.index) });
|
|
915
|
-
result.push({ kind: "path", value: m[1]! });
|
|
916
|
-
last = m.index + m[0].length;
|
|
917
|
-
}
|
|
918
|
-
if (last < text.length) result.push({ kind: "text", value: text.slice(last) });
|
|
919
|
-
return result;
|
|
920
|
-
}, [text]);
|
|
921
|
-
|
|
922
|
-
return (
|
|
923
|
-
<>
|
|
924
|
-
{parts.map((p, i) =>
|
|
925
|
-
p.kind === "path"
|
|
926
|
-
? <FilePathChip key={i} path={p.value} projectName={projectName} />
|
|
927
|
-
: <span key={i}>{p.value}</span>,
|
|
928
|
-
)}
|
|
929
|
-
</>
|
|
930
|
-
);
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* Session-scoped image cache: blob URL + rendered box per src. Virtualized rows
|
|
935
|
-
* unmount/remount constantly while scrolling; refetching the image and re-growing
|
|
936
|
-
* from the placeholder on EVERY remount changed the row height after paint — the
|
|
937
|
-
* repeatable downward jerk when scrolling up through image-bearing messages.
|
|
938
|
-
* URLs are intentionally never revoked (bounded by unique images per session).
|
|
939
|
-
*/
|
|
940
|
-
const imageBlobCache = new Map<string, { url: string; w?: number; h?: number }>();
|
|
941
|
-
|
|
942
|
-
/** Hook: fetch an image via auth header, return blob URL (cached across mounts) */
|
|
943
|
-
function useAuthBlob(src: string): { blobUrl: string | null; error: boolean } {
|
|
944
|
-
const [blobUrl, setBlobUrl] = useState<string | null>(() => imageBlobCache.get(src)?.url ?? null);
|
|
945
|
-
const [error, setError] = useState(false);
|
|
946
|
-
|
|
947
|
-
useEffect(() => {
|
|
948
|
-
const cached = imageBlobCache.get(src);
|
|
949
|
-
if (cached) { setBlobUrl(cached.url); return; }
|
|
950
|
-
let stale = false;
|
|
951
|
-
const token = getAuthToken();
|
|
952
|
-
fetch(src, { headers: token ? { Authorization: `Bearer ${token}` } : {} })
|
|
953
|
-
.then((r) => { if (!r.ok) throw new Error("Failed"); return r.blob(); })
|
|
954
|
-
.then((blob) => {
|
|
955
|
-
if (stale) return;
|
|
956
|
-
const url = URL.createObjectURL(blob);
|
|
957
|
-
imageBlobCache.set(src, { url });
|
|
958
|
-
setBlobUrl(url);
|
|
959
|
-
})
|
|
960
|
-
.catch(() => { if (!stale) setError(true); });
|
|
961
|
-
return () => { stale = true; }; // cache owns the URL — no revoke
|
|
962
|
-
}, [src]);
|
|
963
|
-
|
|
964
|
-
return { blobUrl, error };
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
/** Fetches image with auth header, renders as blob URL — click opens lightbox */
|
|
968
|
-
function AuthImage({ src, alt }: { src: string; alt: string }) {
|
|
969
|
-
const { blobUrl, error } = useAuthBlob(src);
|
|
970
|
-
const openOverlay = useImageOverlay((s) => s.open);
|
|
971
|
-
// Rendered box captured after the first load — pins the layout on remounts so
|
|
972
|
-
// the row never grows after paint (see imageBlobCache).
|
|
973
|
-
const cached = imageBlobCache.get(src);
|
|
974
|
-
const box = cached?.w && cached?.h ? { width: cached.w, height: cached.h } : undefined;
|
|
975
|
-
|
|
976
|
-
if (error) {
|
|
977
|
-
return (
|
|
978
|
-
<div className="flex items-center gap-1.5 rounded-md border border-border bg-background/50 px-2 py-1 text-xs text-text-secondary">
|
|
979
|
-
<ImageIcon className="size-3.5 shrink-0" />
|
|
980
|
-
<span className="truncate max-w-40">{alt}</span>
|
|
981
|
-
</div>
|
|
982
|
-
);
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
if (!blobUrl) {
|
|
986
|
-
return <div className="rounded-md bg-surface border border-border h-24 w-32 animate-pulse" style={box} />;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
return (
|
|
990
|
-
<button
|
|
991
|
-
type="button"
|
|
992
|
-
onClick={(e) => openOverlay(blobUrl, alt, collectGallery(e.currentTarget))}
|
|
993
|
-
className="block text-left"
|
|
994
|
-
>
|
|
995
|
-
<img
|
|
996
|
-
src={blobUrl}
|
|
997
|
-
alt={alt}
|
|
998
|
-
style={box}
|
|
999
|
-
{...{ [GALLERY_ITEM_ATTR]: "" }}
|
|
1000
|
-
onLoad={(e) => {
|
|
1001
|
-
const el = e.currentTarget;
|
|
1002
|
-
const c = imageBlobCache.get(src);
|
|
1003
|
-
if (c && (!c.w || !c.h)) { c.w = el.offsetWidth; c.h = el.offsetHeight; }
|
|
1004
|
-
}}
|
|
1005
|
-
className="rounded-md max-h-48 max-w-full object-contain border border-border cursor-pointer hover:opacity-90 transition-opacity"
|
|
1006
|
-
/>
|
|
1007
|
-
</button>
|
|
1008
|
-
);
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
/** Chip for attached images in user bubble — tiny preview replaces icon, click opens lightbox */
|
|
1012
|
-
function AuthImageThumbnail({ filePath, projectName }: { filePath: string; projectName?: string }) {
|
|
1013
|
-
const src = uploadPreviewUrl(filePath, projectName);
|
|
1014
|
-
const { blobUrl, error } = useAuthBlob(src);
|
|
1015
|
-
const openOverlay = useImageOverlay((s) => s.open);
|
|
1016
|
-
const name = basename(filePath);
|
|
1017
|
-
|
|
1018
|
-
return (
|
|
1019
|
-
<button
|
|
1020
|
-
type="button"
|
|
1021
|
-
onClick={(e) => blobUrl && openOverlay(blobUrl, name, collectGallery(e.currentTarget))}
|
|
1022
|
-
className="flex items-center gap-1 rounded-md border border-border/60 bg-background/40 px-1.5 py-0.5 text-[11px] text-text-secondary hover:bg-surface transition-colors cursor-pointer"
|
|
1023
|
-
>
|
|
1024
|
-
{blobUrl ? (
|
|
1025
|
-
<img src={blobUrl} alt={name} {...{ [GALLERY_ITEM_ATTR]: "" }} className="size-4 rounded-sm object-cover shrink-0" />
|
|
1026
|
-
) : error ? (
|
|
1027
|
-
<ImageIcon className="size-3 shrink-0" />
|
|
1028
|
-
) : (
|
|
1029
|
-
<div className="size-4 rounded-sm bg-surface animate-pulse shrink-0" />
|
|
1030
|
-
)}
|
|
1031
|
-
<span className="truncate max-w-32">{name}</span>
|
|
1032
|
-
</button>
|
|
1033
|
-
);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
/** Fetches file with auth, opens in new browser tab (for PDFs, etc.) */
|
|
1037
|
-
function AuthFileLink({ src, filename, mimeType }: { src: string; filename: string; mimeType: string }) {
|
|
1038
|
-
const [loading, setLoading] = useState(false);
|
|
1039
|
-
|
|
1040
|
-
const handleClick = useCallback(async () => {
|
|
1041
|
-
setLoading(true);
|
|
1042
|
-
try {
|
|
1043
|
-
const token = getAuthToken();
|
|
1044
|
-
const res = await fetch(src, { headers: token ? { Authorization: `Bearer ${token}` } : {} });
|
|
1045
|
-
if (!res.ok) throw new Error("Failed to load");
|
|
1046
|
-
const blob = await res.blob();
|
|
1047
|
-
const url = URL.createObjectURL(new Blob([blob], { type: mimeType }));
|
|
1048
|
-
window.open(url, "_blank");
|
|
1049
|
-
// Revoke after a delay to let the new tab load
|
|
1050
|
-
setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
|
1051
|
-
} catch {
|
|
1052
|
-
// Fallback: try direct link
|
|
1053
|
-
window.open(src, "_blank");
|
|
1054
|
-
} finally {
|
|
1055
|
-
setLoading(false);
|
|
1056
|
-
}
|
|
1057
|
-
}, [src, mimeType]);
|
|
1058
592
|
|
|
1059
|
-
return (
|
|
1060
|
-
<button
|
|
1061
|
-
type="button"
|
|
1062
|
-
onClick={handleClick}
|
|
1063
|
-
disabled={loading}
|
|
1064
|
-
className="flex items-center gap-1.5 rounded-md border border-border bg-background/50 px-2 py-1 text-xs text-text-secondary hover:bg-surface hover:text-text-primary transition-colors cursor-pointer disabled:opacity-50"
|
|
1065
|
-
>
|
|
1066
|
-
<FileText className="size-3.5 shrink-0 text-error" />
|
|
1067
|
-
<span className="truncate max-w-40">{filename}</span>
|
|
1068
|
-
{loading && <span className="animate-spin text-[10px]">...</span>}
|
|
1069
|
-
</button>
|
|
1070
|
-
);
|
|
1071
|
-
}
|
|
1072
593
|
|
|
1073
|
-
/**
|
|
1074
|
-
* Renders events in order — consecutive text events merged into one bubble,
|
|
1075
|
-
* tool_use/tool_result render as cards between text sections.
|
|
1076
|
-
* Last text group shows streaming cursor when actively streaming.
|
|
1077
|
-
*/
|
|
1078
|
-
type EventGroup =
|
|
1079
|
-
| { kind: "text"; content: string }
|
|
1080
|
-
| { kind: "thinking"; content: string }
|
|
1081
|
-
| { kind: "tool"; tool: ChatEvent; result?: ChatEvent; completed?: boolean };
|
|
1082
594
|
|
|
1083
|
-
function InterleavedEvents({ events, isStreaming, projectName, bashPartialOutput }: {
|
|
1084
|
-
events: ChatEvent[];
|
|
1085
|
-
isStreaming: boolean;
|
|
1086
|
-
projectName?: string;
|
|
1087
|
-
bashPartialOutput?: React.RefObject<Map<string, BashPartialEntry>>;
|
|
1088
|
-
}) {
|
|
1089
|
-
// Group: consecutive text → merged text block; tool_use + tool_result paired by toolUseId
|
|
1090
|
-
const groups: EventGroup[] = [];
|
|
1091
|
-
let textBuffer = "";
|
|
1092
595
|
|
|
1093
|
-
// First pass: create groups for text, thinking, and tool_use events
|
|
1094
|
-
let thinkingBuffer = "";
|
|
1095
|
-
for (let i = 0; i < events.length; i++) {
|
|
1096
|
-
const event = events[i]!;
|
|
1097
|
-
if (event.type === "thinking") {
|
|
1098
|
-
// Flush text buffer first if any
|
|
1099
|
-
if (textBuffer) { groups.push({ kind: "text", content: textBuffer }); textBuffer = ""; }
|
|
1100
|
-
thinkingBuffer += event.content;
|
|
1101
|
-
continue;
|
|
1102
|
-
}
|
|
1103
|
-
// Flush thinking buffer when non-thinking event arrives
|
|
1104
|
-
if (thinkingBuffer) {
|
|
1105
|
-
groups.push({ kind: "thinking", content: thinkingBuffer });
|
|
1106
|
-
thinkingBuffer = "";
|
|
1107
|
-
}
|
|
1108
|
-
if (event.type === "account_retry") {
|
|
1109
|
-
if (textBuffer) { groups.push({ kind: "text", content: textBuffer }); textBuffer = ""; }
|
|
1110
|
-
const label = (event as any).accountLabel ?? "another account";
|
|
1111
|
-
const reason = (event as any).reason ?? "Auth failed";
|
|
1112
|
-
groups.push({ kind: "text", content: `\n\n> ↻ ${reason} — retrying with **${label}**...\n\n` });
|
|
1113
|
-
continue;
|
|
1114
|
-
}
|
|
1115
|
-
if (event.type === "text") {
|
|
1116
|
-
textBuffer += event.content;
|
|
1117
|
-
} else if (event.type === "tool_use") {
|
|
1118
|
-
// A call may be announced before it has anything to show and re-announced
|
|
1119
|
-
// once it does — image generation starts with no file and no prompt, and
|
|
1120
|
-
// only learns both when it finishes. The later event describes the same
|
|
1121
|
-
// call, so it replaces the card rather than adding a second one.
|
|
1122
|
-
const useId = (event as any).toolUseId;
|
|
1123
|
-
const existing = useId
|
|
1124
|
-
? groups.find(
|
|
1125
|
-
(g) => g.kind === "tool" && g.tool.type === "tool_use" && (g.tool as any).toolUseId === useId,
|
|
1126
|
-
) as (EventGroup & { kind: "tool" }) | undefined
|
|
1127
|
-
: undefined;
|
|
1128
|
-
if (existing) {
|
|
1129
|
-
existing.tool = event;
|
|
1130
|
-
continue;
|
|
1131
|
-
}
|
|
1132
|
-
if (textBuffer) {
|
|
1133
|
-
groups.push({ kind: "text", content: textBuffer });
|
|
1134
|
-
textBuffer = "";
|
|
1135
|
-
}
|
|
1136
|
-
groups.push({ kind: "tool", tool: event });
|
|
1137
|
-
} else if (event.type === "tool_result") {
|
|
1138
|
-
// Skip tool_results in first pass — matched below
|
|
1139
|
-
} else {
|
|
1140
|
-
if (textBuffer) {
|
|
1141
|
-
groups.push({ kind: "text", content: textBuffer });
|
|
1142
|
-
textBuffer = "";
|
|
1143
|
-
}
|
|
1144
|
-
groups.push({ kind: "tool", tool: event });
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
if (thinkingBuffer) {
|
|
1148
|
-
groups.push({ kind: "thinking", content: thinkingBuffer });
|
|
1149
|
-
}
|
|
1150
|
-
if (textBuffer) {
|
|
1151
|
-
groups.push({ kind: "text", content: textBuffer });
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
// Second pass: match tool_result events to their tool_use by toolUseId
|
|
1155
|
-
const toolResults = events.filter((e) => e.type === "tool_result");
|
|
1156
|
-
for (const tr of toolResults) {
|
|
1157
|
-
const trId = (tr as any).toolUseId;
|
|
1158
|
-
// Match by ID if available
|
|
1159
|
-
if (trId) {
|
|
1160
|
-
const match = groups.find(
|
|
1161
|
-
(g) => g.kind === "tool" && g.tool.type === "tool_use" && (g.tool as any).toolUseId === trId,
|
|
1162
|
-
) as (EventGroup & { kind: "tool" }) | undefined;
|
|
1163
|
-
if (match) {
|
|
1164
|
-
match.result = tr;
|
|
1165
|
-
continue;
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
// Fallback: attach to first tool group without a result
|
|
1169
|
-
const unmatched = groups.find(
|
|
1170
|
-
(g) => g.kind === "tool" && !g.result,
|
|
1171
|
-
) as (EventGroup & { kind: "tool" }) | undefined;
|
|
1172
|
-
if (unmatched) {
|
|
1173
|
-
unmatched.result = tr;
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
// Third pass: fallback to embedded result from buffer enrichment (reconnect).
|
|
1178
|
-
// When BE buffers tool_result, it also attaches result onto the matching tool_use event.
|
|
1179
|
-
for (const g of groups) {
|
|
1180
|
-
if (g.kind === "tool" && !g.result && g.tool.type === "tool_use") {
|
|
1181
|
-
const embedded = (g.tool as any).result;
|
|
1182
|
-
if (embedded) {
|
|
1183
|
-
g.result = { type: "tool_result", output: embedded.output, isError: embedded.isError, exitCode: embedded.exitCode } as ChatEvent;
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// Mark tool groups without explicit tool_result as completed when:
|
|
1189
|
-
// 1. It's a Read and a later Edit on the same file has a result (Edit implies Read finished)
|
|
1190
|
-
// 2. Streaming is fully finished
|
|
1191
|
-
for (let gi = 0; gi < groups.length; gi++) {
|
|
1192
|
-
const g = groups[gi]!;
|
|
1193
|
-
if (g.kind === "tool" && !g.result) {
|
|
1194
|
-
let impliedDone = false;
|
|
1195
|
-
if (g.tool.type === "tool_use" && g.tool.tool === "Read") {
|
|
1196
|
-
const readPath = (g.tool.input as any)?.file_path;
|
|
1197
|
-
if (readPath) {
|
|
1198
|
-
impliedDone = groups.slice(gi + 1).some(
|
|
1199
|
-
(later) => later.kind === "tool" && later.result
|
|
1200
|
-
&& later.tool.type === "tool_use" && later.tool.tool === "Edit"
|
|
1201
|
-
&& (later.tool.input as any)?.file_path === readPath,
|
|
1202
|
-
);
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
g.completed = impliedDone || !isStreaming;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
return (
|
|
1210
|
-
<>
|
|
1211
|
-
{groups.map((group, i) => {
|
|
1212
|
-
if (group.kind === "thinking") {
|
|
1213
|
-
return <ThinkingBlock key={`think-${i}`} content={group.content} isStreaming={isStreaming && i === groups.length - 1} />;
|
|
1214
|
-
}
|
|
1215
|
-
if (group.kind === "text") {
|
|
1216
|
-
const isLast = isStreaming && i === groups.length - 1;
|
|
1217
|
-
return (
|
|
1218
|
-
<div key={`text-${i}`} className="text-sm text-text-primary select-text">
|
|
1219
|
-
<StreamingText content={group.content} animate={isLast} projectName={projectName} />
|
|
1220
|
-
</div>
|
|
1221
|
-
);
|
|
1222
|
-
}
|
|
1223
|
-
return <ToolCard key={`tool-${i}`} tool={group.tool} result={group.result} completed={group.completed} projectName={projectName} bashPartialOutput={bashPartialOutput} />;
|
|
1224
|
-
})}
|
|
1225
|
-
</>
|
|
1226
|
-
);
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
/** Collapsible thinking block — shows Claude's reasoning, collapsed by default when done */
|
|
1230
|
-
function ThinkingBlock({ content, isStreaming }: { content: string; isStreaming: boolean }) {
|
|
1231
|
-
const [expanded, setExpanded] = useState(isStreaming);
|
|
1232
|
-
const scrollRef = useRef<HTMLDivElement>(null);
|
|
1233
|
-
|
|
1234
|
-
// Auto-collapse when streaming finishes
|
|
1235
|
-
useEffect(() => {
|
|
1236
|
-
if (!isStreaming && content.length > 0) setExpanded(false);
|
|
1237
|
-
}, [isStreaming, content.length]);
|
|
1238
|
-
|
|
1239
|
-
// Auto-scroll to bottom during streaming
|
|
1240
|
-
useEffect(() => {
|
|
1241
|
-
if (isStreaming && expanded && scrollRef.current) {
|
|
1242
|
-
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
1243
|
-
}
|
|
1244
|
-
}, [content, isStreaming, expanded]);
|
|
1245
|
-
|
|
1246
|
-
return (
|
|
1247
|
-
<div className="rounded border border-border/50 bg-surface/30 text-xs">
|
|
1248
|
-
<button
|
|
1249
|
-
onClick={() => setExpanded(!expanded)}
|
|
1250
|
-
className="flex items-center gap-2 px-2 py-1.5 w-full text-left hover:bg-surface transition-colors text-text-subtle"
|
|
1251
|
-
>
|
|
1252
|
-
{isStreaming ? <Loader2 className="size-3 animate-spin" /> : <ChevronRight className={`size-3 transition-transform ${expanded ? "rotate-90" : ""}`} />}
|
|
1253
|
-
<span>Thinking{isStreaming ? "..." : ""}</span>
|
|
1254
|
-
{!isStreaming && <span className="text-text-subtle/50 ml-auto">{content.length > 100 ? `${Math.round(content.length / 4)} tokens` : ""}</span>}
|
|
1255
|
-
</button>
|
|
1256
|
-
{expanded && (
|
|
1257
|
-
<div ref={scrollRef} className="max-h-60 overflow-y-auto">
|
|
1258
|
-
<div className="px-2 pb-2 text-text-subtle/80 whitespace-pre-wrap text-[11px] leading-relaxed">
|
|
1259
|
-
{content}
|
|
1260
|
-
</div>
|
|
1261
|
-
</div>
|
|
1262
|
-
)}
|
|
1263
|
-
</div>
|
|
1264
|
-
);
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
/**
|
|
1268
|
-
* Text component that renders streamed content directly.
|
|
1269
|
-
* WebSocket already delivers tokens incrementally — no fake animation needed.
|
|
1270
|
-
* When `isStreaming=true`, shows a blinking cursor at the end.
|
|
1271
|
-
*/
|
|
1272
|
-
function StreamingText({ content, animate: isStreaming, projectName }: { content: string; animate: boolean; projectName?: string }) {
|
|
1273
|
-
return (
|
|
1274
|
-
<>
|
|
1275
|
-
<MarkdownContent content={content} projectName={projectName} isStreaming={isStreaming} />
|
|
1276
|
-
{isStreaming && (
|
|
1277
|
-
<span className="text-text-subtle text-sm animate-pulse">Thinking...</span>
|
|
1278
|
-
)}
|
|
1279
|
-
</>
|
|
1280
|
-
);
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
/**
|
|
1284
|
-
* Shows streaming status with elapsed time and warnings:
|
|
1285
|
-
* - No assistant message: "Connecting to Claude..." with elapsed timer
|
|
1286
|
-
* - After tool: "Processing..."
|
|
1287
|
-
* - Text streaming: hidden
|
|
1288
|
-
*/
|
|
1289
|
-
function ThinkingIndicator({ lastMessage, phase, elapsed, statusMessage }: { lastMessage?: ChatMessage; phase?: SessionPhase; elapsed?: number; statusMessage?: string | null }) {
|
|
1290
|
-
// Show indicator when:
|
|
1291
|
-
// 1. No assistant message yet (waiting for first response)
|
|
1292
|
-
// 2. Last event is tool_result (Claude thinking after tool execution)
|
|
1293
|
-
// 3. statusMessage is active (account routing/refreshing)
|
|
1294
|
-
// Hide when text is actively streaming (text itself is the indicator)
|
|
1295
|
-
|
|
1296
|
-
const isWaiting = !lastMessage || lastMessage.role !== "assistant";
|
|
1297
|
-
const isAfterTool = (() => {
|
|
1298
|
-
if (!lastMessage?.events?.length) return false;
|
|
1299
|
-
const last = lastMessage.events[lastMessage.events.length - 1]!;
|
|
1300
|
-
return last.type === "tool_result";
|
|
1301
|
-
})();
|
|
1302
|
-
|
|
1303
|
-
if (!statusMessage && !isWaiting && !isAfterTool) return null;
|
|
1304
|
-
|
|
1305
|
-
const label = statusMessage
|
|
1306
|
-
? statusMessage
|
|
1307
|
-
: phase === "initializing" ? "Initializing"
|
|
1308
|
-
: phase === "connecting" ? "Connecting"
|
|
1309
|
-
: phase === "thinking" ? "Thinking"
|
|
1310
|
-
: "Processing";
|
|
1311
|
-
|
|
1312
|
-
const isLong = phase === "connecting" && (elapsed ?? 0) >= 30;
|
|
1313
|
-
|
|
1314
|
-
return (
|
|
1315
|
-
<div className="flex flex-col gap-1 text-sm">
|
|
1316
|
-
<div className="flex items-center gap-2 text-text-subtle">
|
|
1317
|
-
<Loader2 className="size-3 animate-spin" />
|
|
1318
|
-
<span>
|
|
1319
|
-
{label}
|
|
1320
|
-
{isWaiting && (elapsed ?? 0) > 0 && <span className="text-text-subtle/60">... ({elapsed}s)</span>}
|
|
1321
|
-
</span>
|
|
1322
|
-
</div>
|
|
1323
|
-
{isLong && (
|
|
1324
|
-
<p className="text-xs text-warning/80 ml-5">
|
|
1325
|
-
Taking longer than usual — may be rate-limited or API slow. Try sending a new message to retry.
|
|
1326
|
-
</p>
|
|
1327
|
-
)}
|
|
1328
|
-
</div>
|
|
1329
|
-
);
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
/** Strip SDK teammate-message XML tags from text — team popover shows these */
|
|
1333
|
-
const TEAMMATE_MSG_RE = /<teammate-message[^>]*>[\s\S]*?<\/teammate-message>/g;
|
|
1334
|
-
function stripTeammateMessages(text: string): string {
|
|
1335
|
-
return text.replace(TEAMMATE_MSG_RE, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
/** Wrapper: delegates to shared MarkdownRenderer with code actions enabled */
|
|
1339
|
-
function MarkdownContent({ content, projectName, isStreaming }: { content: string; projectName?: string; isStreaming?: boolean }) {
|
|
1340
|
-
const cleaned = stripTeammateMessages(content);
|
|
1341
|
-
if (!cleaned) return null;
|
|
1342
|
-
return (
|
|
1343
|
-
<RenderErrorBoundary fallbackContent={cleaned}>
|
|
1344
|
-
<Suspense fallback={<div className="animate-pulse h-4 bg-muted rounded" />}>
|
|
1345
|
-
<MarkdownRenderer content={cleaned} projectName={projectName} codeActions isStreaming={isStreaming} />
|
|
1346
|
-
</Suspense>
|
|
1347
|
-
</RenderErrorBoundary>
|
|
1348
|
-
);
|
|
1349
|
-
}
|
|
1350
596
|
|
|
1351
597
|
/* ToolCard, ToolSummary, ToolDetails extracted to ./tool-cards.tsx */
|
|
1352
598
|
|