@hienlh/ppm 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -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/http-api.md +1 -1
- package/bun.lock +16 -1
- package/dist/web/assets/accounts-settings-section-BVr9-FZJ.js +1 -0
- package/dist/web/assets/{ai-resource-editor-F09pf8Hd.js → ai-resource-editor-CdDpOwjh.js} +1 -1
- package/dist/web/assets/ai-settings-section-B1hQyjAd.js +1 -0
- package/dist/web/assets/ai-settings-section-X2UOfSaS.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-DLoPK0LD.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-G84Ojzae.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-DQJfjm8E.js +15 -0
- package/dist/web/assets/{cobol-DPx_nifl.js → cobol-DRaRy4FK.js} +1 -1
- package/dist/web/assets/code-editor-D04iUPsZ.js +7 -0
- package/dist/web/assets/{codex-daily-guard-CfybHv_c.js → codex-daily-guard-BSccLRGT.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-C4ay676e.js} +2 -2
- 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-DbwEmG7o.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-S6LsALjX.js} +1 -1
- package/dist/web/assets/{dialog-DQTn55Mt.js → dialog-B84_DJQ8.js} +1 -1
- package/dist/web/assets/diff-viewer-3oBA7vgQ.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-DJmaKVY0.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-BQ0jyAGv.js +2 -0
- package/dist/web/assets/explorer-window-content-jdkWt34J.js +1 -0
- package/dist/web/assets/{extension-manager-section-BhwB6nqr.js → extension-manager-section-DG1guILi.js} +1 -1
- package/dist/web/assets/{extension-webview-DI4VL2tJ.js → extension-webview-BGSDUbdK.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-Gnyd7han.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-V6Khh15V.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-Bi513qjT.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-DnQwYSmQ.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-DpczdwyV.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-DU68xk06.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-f2HUbGDY.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-DCRsnKg5.js +1 -0
- package/dist/web/assets/{keyboard-shortcuts-section-BW03P3AF.js → keyboard-shortcuts-section-BpvVEw_Y.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-Bcaf9_bA.js +1 -0
- package/dist/web/assets/markdown-renderer-DGJp8HC3.js +3 -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-Bol3XAO1.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-BXt54VIn.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-DKJUA3Kq.js} +1 -1
- package/dist/web/assets/problems-panel-CFcoUx8h.js +1 -0
- package/dist/web/assets/{process-table-BRMS1MrX.js → process-table-EKr2dqG6.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-B7qRVl8w.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-0WDYsyCA.js +1 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-COYDg16H.js +2 -0
- package/dist/web/assets/remote-desktop-mobile-view-2DqSuS2U.js +1 -0
- package/dist/web/assets/{remote-desktop-readiness-gate-BTzss2wX.js → remote-desktop-readiness-gate-Dm_qMJMH.js} +1 -1
- package/dist/web/assets/remote-desktop-window-content-UodhsF7W.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-bG2lYfGv.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-C06bFarv.js +2 -0
- package/dist/web/assets/settings-store-krC6bc79.js +2 -0
- package/dist/web/assets/settings-tab-34RSaASd.js +1 -0
- package/dist/web/assets/settings-window-content-Dr_2lmHH.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-BgRH0cHD.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-DoV9hnWJ.js → sqlite-viewer-DUxGslpa.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-IZuLn7Qh.js +2 -0
- package/dist/web/assets/system-monitor-tab-DiSA490Z.js +1 -0
- package/dist/web/assets/{system-monitor-window-content-qXLcicFM.js → system-monitor-window-content-CEaFISgj.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-7fPZNEni.js → tab-host-window-content-D0TN8Wrq.js} +1 -1
- package/dist/web/assets/{tab-store-DGJGnbwv.js → tab-store-OJa1GPCq.js} +1 -1
- package/dist/web/assets/team-member-sheet-DDPNcCjd.js +2 -0
- package/dist/web/assets/{team-member-window-content-BgvJFypM.js → team-member-window-content-DCiW_1-E.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-4ypP0NDY.js} +3 -3
- package/dist/web/assets/{tex-D_StFXy_.js → tex-BcssM-QA.js} +1 -1
- package/dist/web/assets/tool-cards-Dd6E7Q3k.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-vrfFHuaR.js} +1 -1
- package/dist/web/assets/{use-lsp-CV0vzILO.js → use-lsp-CsuZu49X.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-DBqbWjWX.js → use-monaco-theme-BIVYJBIn.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-BMbufhX7.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-Byj8P_jA.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/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 +134 -1
- package/packages/ext-git-graph/src/webview-html.ts +277 -27
- 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/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/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/git/git-status-panel.tsx +25 -0
- 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/theme-picker.tsx +2 -1
- package/src/web/components/ui/tooltip.tsx +15 -2
- package/src/web/hooks/use-delayed-flag.ts +35 -0
- package/src/web/lib/branch-review-state.ts +175 -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 +11 -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/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-CvL1pWeK.js +0 -3
- 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/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
|
@@ -10,7 +10,45 @@ import type { ChatEvent, ChatMessage } from "../types/chat.ts";
|
|
|
10
10
|
import { stringifyToolResultContent } from "../shared/tool-result-content.ts";
|
|
11
11
|
import { stripSharedContext } from "../shared/provider-context.ts";
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* How large a transcript this module will look at, for the callers that go
|
|
15
|
+
* through `validateJsonlPath`.
|
|
16
|
+
*
|
|
17
|
+
* It was 50MB and it silently killed the whole expand-compact path for exactly
|
|
18
|
+
* the sessions that need it — a transcript with thirteen compactions in it had
|
|
19
|
+
* reached 77MB, so scrolling to the top of that chat answered 403 and loaded
|
|
20
|
+
* nothing, forever.
|
|
21
|
+
*
|
|
22
|
+
* It is a sanity bound and not a memory bound, and it is worth being exact
|
|
23
|
+
* about why, because the obvious reading is wrong. Measured on this repo's own
|
|
24
|
+
* 88.6MB transcript: the reader streams, the parsed array retains **11.1MB**
|
|
25
|
+
* (6974 messages, no single string over 200KB) — and RSS still rises by 355MB.
|
|
26
|
+
* That is `JSON.parse` garbage, not live objects: `Bun.gc(true)` does not give
|
|
27
|
+
* it back and neither does dropping the array, because the allocator keeps the
|
|
28
|
+
* pages. It also does not accumulate — four transcripts of 89/83/55/48MB in a
|
|
29
|
+
* row plateau at 390–466MB rather than summing — so the cost of reading one of
|
|
30
|
+
* these is a one-time high-water mark, reusable by the next.
|
|
31
|
+
*
|
|
32
|
+
* The number that actually bounds memory is therefore the *file* size, at about
|
|
33
|
+
* 4.3× transiently, which is what `FULL_PARSE_MAX_BYTES` below is for.
|
|
34
|
+
*/
|
|
35
|
+
const MAX_FILE_SIZE = 256 * 1024 * 1024; // 256MB
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* How many bytes a *whole-history* parse may read.
|
|
39
|
+
*
|
|
40
|
+
* `validateJsonlPath`'s bound never applied here: the search indexer reaches
|
|
41
|
+
* `parseJsonlTranscript` through `getFullMessages`, which does no validation at
|
|
42
|
+
* all, and it asks for every segment rather than one. At the measured 4.3×
|
|
43
|
+
* transient cost that is ~550MB of allocator growth at this bound, and nothing
|
|
44
|
+
* above it — a bigger transcript is read from `fullParseWindow` onwards
|
|
45
|
+
* instead of whole, so the cost stops rising with the file.
|
|
46
|
+
*
|
|
47
|
+
* Set above the largest transcript on this machine (88.8MB of 1666) with room
|
|
48
|
+
* to spare, so the windowing is for the pathological case and not for the
|
|
49
|
+
* sessions this file's history is about.
|
|
50
|
+
*/
|
|
51
|
+
export const FULL_PARSE_MAX_BYTES = 128 * 1024 * 1024; // 128MB
|
|
14
52
|
const TEAMMATE_MSG_RE = /<teammate-message[^>]*>[\s\S]*?<\/teammate-message>/g;
|
|
15
53
|
|
|
16
54
|
/** Strip SDK teammate-message XML tags from assistant text */
|
|
@@ -197,11 +235,41 @@ export function nestChildEventsAcrossMessages(messages: { content: string; event
|
|
|
197
235
|
}
|
|
198
236
|
}
|
|
199
237
|
|
|
238
|
+
/**
|
|
239
|
+
* How much of a transcript's tail a whole-history parse may read, as a byte
|
|
240
|
+
* offset to start at. `0` means the file is small enough to read entirely.
|
|
241
|
+
*
|
|
242
|
+
* A window rather than a refusal, because refusing is not the mitigation it
|
|
243
|
+
* looks like: `getFullMessages` falls back to the SDK's own walk, and that
|
|
244
|
+
* reads the same file — measured 3.1s on a 128MB fixture the guard had just
|
|
245
|
+
* declined to open. It also leaves the session unsearchable, which is the
|
|
246
|
+
* opposite of what the indexer was widened to do. Reading the tail bounds the
|
|
247
|
+
* work at `maxBytes` and indexes the newest part of the session, which is the
|
|
248
|
+
* part a search is most likely reaching for.
|
|
249
|
+
*
|
|
250
|
+
* Answers on `stat` alone, never by opening the file: the caller asks this once
|
|
251
|
+
* per stale session. A file that has vanished reads as `0` — let the parse
|
|
252
|
+
* report that in its own words rather than inventing a window into nothing.
|
|
253
|
+
*/
|
|
254
|
+
export function fullParseWindow(filePath: string, maxBytes: number = FULL_PARSE_MAX_BYTES): number {
|
|
255
|
+
try {
|
|
256
|
+
const { size } = statSync(filePath);
|
|
257
|
+
return size > maxBytes ? size - maxBytes : 0;
|
|
258
|
+
} catch {
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
200
263
|
/**
|
|
201
264
|
* Validate JSONL path — must be under ~/.claude/ (prevents arbitrary file reads).
|
|
202
265
|
* Throws Error with descriptive message. Returns resolved realpath on success.
|
|
266
|
+
*
|
|
267
|
+
* `maxBytes` is a parameter only so a test can assert the boundary against a
|
|
268
|
+
* one-byte-over fixture: a real 256MB transcript is not worth committing, and a
|
|
269
|
+
* bound asserted nowhere is how this one came to say 50MB while rejecting at a
|
|
270
|
+
* different number.
|
|
203
271
|
*/
|
|
204
|
-
export function validateJsonlPath(inputPath: string): string {
|
|
272
|
+
export function validateJsonlPath(inputPath: string, maxBytes = MAX_FILE_SIZE): string {
|
|
205
273
|
if (!inputPath) throw new Error("jsonlPath is required");
|
|
206
274
|
// Reject obvious traversal attempts before resolution
|
|
207
275
|
if (inputPath.includes("\0")) throw new Error("Invalid path: denied");
|
|
@@ -227,16 +295,70 @@ export function validateJsonlPath(inputPath: string): string {
|
|
|
227
295
|
|
|
228
296
|
const stat = statSync(real);
|
|
229
297
|
if (!stat.isFile()) throw new Error("Not a regular file");
|
|
230
|
-
|
|
231
|
-
|
|
298
|
+
// Interpolated, never spelled out: the literal "50MB" outlived the 50MB cap
|
|
299
|
+
// and sent the next reader looking in the wrong place for a file that was
|
|
300
|
+
// rejected at 256MB.
|
|
301
|
+
if (stat.size > maxBytes) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`File too large: ${Math.round(stat.size / 1024 / 1024)}MB exceeds ` +
|
|
304
|
+
`${Math.round(maxBytes / 1024 / 1024)}MB limit`,
|
|
305
|
+
);
|
|
232
306
|
}
|
|
233
307
|
return real;
|
|
234
308
|
}
|
|
235
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Yield a file's lines without holding the file in memory.
|
|
312
|
+
*
|
|
313
|
+
* `Bun.file().text()` plus `split("\n")` costs the whole transcript twice over —
|
|
314
|
+
* 277MB resident for a 77MB file, which is what forced a cap low enough to
|
|
315
|
+
* reject real sessions. Peak here is one chunk plus one line.
|
|
316
|
+
*/
|
|
317
|
+
async function* readLines(filePath: string, fromByte = 0): AsyncGenerator<string> {
|
|
318
|
+
const decoder = new TextDecoder();
|
|
319
|
+
// Explicit reader rather than `for await` over the stream: the DOM lib's
|
|
320
|
+
// ReadableStream is not typed as async-iterable, and the `finally` is what
|
|
321
|
+
// releases it when a caller breaks early on `beforeUuid`.
|
|
322
|
+
// One byte *before* the offset, so the first line the split produces is
|
|
323
|
+
// always the tail of the record the offset fell inside — the empty string
|
|
324
|
+
// when it fell exactly on a newline. That makes the drop below unconditional
|
|
325
|
+
// instead of a boundary case, and a boundary case here silently costs a whole
|
|
326
|
+
// record rather than a fragment.
|
|
327
|
+
const file = fromByte > 0 ? Bun.file(filePath).slice(fromByte - 1) : Bun.file(filePath);
|
|
328
|
+
const reader = file.stream().getReader();
|
|
329
|
+
// A fragment is not always unparseable — the tail of one record is sometimes
|
|
330
|
+
// a complete smaller document — so it has to go by position rather than be
|
|
331
|
+
// left for `JSON.parse` to reject, or it enters as an invented message.
|
|
332
|
+
let dropPartial = fromByte > 0;
|
|
333
|
+
let buffered = "";
|
|
334
|
+
try {
|
|
335
|
+
while (true) {
|
|
336
|
+
const { done, value } = await reader.read();
|
|
337
|
+
if (done) break;
|
|
338
|
+
buffered += decoder.decode(value, { stream: true });
|
|
339
|
+
const lines = buffered.split("\n");
|
|
340
|
+
buffered = lines.pop() ?? "";
|
|
341
|
+
for (const line of lines) {
|
|
342
|
+
if (dropPartial) { dropPartial = false; continue; }
|
|
343
|
+
yield line;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} finally {
|
|
347
|
+
reader.releaseLock();
|
|
348
|
+
}
|
|
349
|
+
buffered += decoder.decode();
|
|
350
|
+
if (buffered && !dropPartial) yield buffered;
|
|
351
|
+
}
|
|
352
|
+
|
|
236
353
|
/**
|
|
237
354
|
* Read a JSONL transcript file, parse entries, apply merge/nest pipeline, return ChatMessage[].
|
|
238
355
|
* Applies the same logic as ClaudeAgentSdkProvider.getMessages() but reads from file directly.
|
|
239
356
|
*
|
|
357
|
+
* @param opts.oneSegment Return only the stretch since the previous compaction,
|
|
358
|
+
* rather than everything before `beforeUuid`.
|
|
359
|
+
* @param opts.fromByte Start reading at this offset instead of at the start,
|
|
360
|
+
* dropping the partial record it lands in. `fullParseWindow`
|
|
361
|
+
* is what computes it; nothing else should.
|
|
240
362
|
* @param beforeUuid If provided, stop parsing at the line with this uuid (exclusive).
|
|
241
363
|
* Used for the expand-compact feature: Claude's compact summary references
|
|
242
364
|
* the CURRENT session file (pre+summary+post), so we truncate at the
|
|
@@ -245,10 +367,10 @@ export function validateJsonlPath(inputPath: string): string {
|
|
|
245
367
|
export async function parseJsonlTranscript(
|
|
246
368
|
filePath: string,
|
|
247
369
|
beforeUuid?: string,
|
|
370
|
+
opts?: { oneSegment?: boolean; fromByte?: number },
|
|
248
371
|
): Promise<ChatMessage[]> {
|
|
249
|
-
const text = await Bun.file(filePath).text();
|
|
250
372
|
const parsed: ChatMessage[] = [];
|
|
251
|
-
for (const line of
|
|
373
|
+
for await (const line of readLines(filePath, opts?.fromByte ?? 0)) {
|
|
252
374
|
const trimmed = line.trim();
|
|
253
375
|
if (!trimmed) continue;
|
|
254
376
|
let entry: any;
|
|
@@ -258,6 +380,16 @@ export async function parseJsonlTranscript(
|
|
|
258
380
|
continue; // skip malformed lines defensively
|
|
259
381
|
}
|
|
260
382
|
if (beforeUuid && entry.uuid === beforeUuid) break; // stop at compact boundary (exclusive)
|
|
383
|
+
// One segment only: drop everything collected so far each time another
|
|
384
|
+
// compaction is passed, so what survives is the stretch between the last
|
|
385
|
+
// compaction and `beforeUuid`. Without this a single expand answers with
|
|
386
|
+
// the whole history before the boundary — 5626 messages on a session with
|
|
387
|
+
// thirteen compactions, prepended into a view already carrying 3553 DOM
|
|
388
|
+
// nodes. The summary that resets it is itself a `user` record, so it is
|
|
389
|
+
// pushed straight back and becomes the segment's first message — which is
|
|
390
|
+
// what carries the transcript path, and therefore what lets the next
|
|
391
|
+
// scroll expand the segment before it.
|
|
392
|
+
if (opts?.oneSegment && entry.isCompactSummary) parsed.length = 0;
|
|
261
393
|
if (entry.type !== "user" && entry.type !== "assistant") continue;
|
|
262
394
|
if (!entry.uuid || !entry.message) continue;
|
|
263
395
|
parsed.push(parseSessionMessage(entry));
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeping `ppm.log` bounded, and keeping one line out of it twice.
|
|
3
|
+
*
|
|
4
|
+
* Two separate defects sat in the same file. On this machine it had reached
|
|
5
|
+
* **276 MB / 1,395,012 lines**, and of those only 532,792 carried the
|
|
6
|
+
* `[timestamp] [LEVEL]` prefix — the other 862,220 (62%) were the *same events*
|
|
7
|
+
* arriving by a second route.
|
|
8
|
+
*
|
|
9
|
+
* The second route is `supervisor.ts` spawning the server with
|
|
10
|
+
* `stdio: ["ignore", logFd, logFd]`, where `logFd` is `ppm.log` itself. So the
|
|
11
|
+
* server's own `console.log` already lands in the log through fd 1, and
|
|
12
|
+
* `setupLogFile()` then appends a formatted copy of the same line. That is the
|
|
13
|
+
* duplication — and it is not a harmless one, because only the appended copy
|
|
14
|
+
* goes through `redactSecrets()`. The raw stdout copy does not, which is how
|
|
15
|
+
* two lines matching `Token: <value>` are sitting in the log right now. The
|
|
16
|
+
* redaction was never wrong; it only ever covered one of the two doors.
|
|
17
|
+
*
|
|
18
|
+
* `fdWritesTo` is how a writer notices that its own stdout already reaches the
|
|
19
|
+
* file it was about to append to, so it can stop doing one of the two.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { fstatSync, statSync, copyFileSync, truncateSync, renameSync, rmSync, existsSync } from "node:fs";
|
|
23
|
+
|
|
24
|
+
/** Rotate once the log passes this. */
|
|
25
|
+
export const MAX_LOG_BYTES = 20 * 1024 * 1024;
|
|
26
|
+
|
|
27
|
+
/** How many previous logs to keep (`ppm.log.1` … `ppm.log.3`). */
|
|
28
|
+
export const LOG_GENERATIONS = 3;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Set by the supervisor on any child it hands `stdio: [\"ignore\", logFd, logFd]`.
|
|
32
|
+
*
|
|
33
|
+
* The inode comparison below cannot answer that question on Windows: `fstat` reports an inode
|
|
34
|
+
* of 0 for most handles there, so `fdWritesTo` says "not the same file" and both writers keep
|
|
35
|
+
* writing — which means the duplicate, **unredacted** stdout copy this module exists to remove
|
|
36
|
+
* was still being written on the one platform PPM is most often installed on. Detection cannot
|
|
37
|
+
* be made to work there, but the process doing the wiring knows for certain, so it says so.
|
|
38
|
+
*/
|
|
39
|
+
export const STDIO_IS_LOG_ENV = "PPM_STDIO_IS_LOG";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether console output on `fd` already reaches `filePath` — by what the spawner declared,
|
|
43
|
+
* or, for a process nobody declared anything to, by inode.
|
|
44
|
+
*/
|
|
45
|
+
export function stdioIsLogFile(fd: number, filePath: string, env: NodeJS.ProcessEnv = process.env): boolean {
|
|
46
|
+
return env[STDIO_IS_LOG_ENV] === "1" || fdWritesTo(fd, filePath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Drop the marker once it has been read.
|
|
51
|
+
*
|
|
52
|
+
* It is inherited like any other variable, and PPM spawns terminals, SDK children and
|
|
53
|
+
* `ppm` CLI invocations with its own environment — every one of which would otherwise start
|
|
54
|
+
* life believing its stdout is the log file and silently drop it.
|
|
55
|
+
*/
|
|
56
|
+
export function consumeStdioIsLogEnv(env: NodeJS.ProcessEnv = process.env): void {
|
|
57
|
+
delete env[STDIO_IS_LOG_ENV];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether writing to `fd` lands in `filePath` — same inode, same device.
|
|
62
|
+
*
|
|
63
|
+
* Answers "is my stdout already this log file?" for a process the supervisor did not label.
|
|
64
|
+
*
|
|
65
|
+
* Returns false whenever it cannot be sure, which on Windows is always — see
|
|
66
|
+
* `STDIO_IS_LOG_ENV`, which is how the supervisor's children get a real answer there.
|
|
67
|
+
*/
|
|
68
|
+
export function fdWritesTo(fd: number, filePath: string): boolean {
|
|
69
|
+
try {
|
|
70
|
+
const a = fstatSync(fd);
|
|
71
|
+
if (a.ino === 0) return false;
|
|
72
|
+
const b = statSync(filePath);
|
|
73
|
+
return a.ino === b.ino && a.dev === b.dev;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Truncate the log in place once it is oversized, keeping N previous copies.
|
|
81
|
+
*
|
|
82
|
+
* In place, and that is the whole design constraint: both the supervisor and
|
|
83
|
+
* the server child hold file descriptors opened on this inode with `O_APPEND`.
|
|
84
|
+
* Renaming the file would leave every one of those descriptors writing into the
|
|
85
|
+
* renamed file for the rest of the process's life — the log would appear to
|
|
86
|
+
* rotate and then never grow again, while `ppm.log.1` quietly became the real
|
|
87
|
+
* log. Copying the contents out and truncating the original keeps the inode,
|
|
88
|
+
* so an `O_APPEND` writer simply resumes at offset 0.
|
|
89
|
+
*
|
|
90
|
+
* The cost is the one `logrotate` calls `copytruncate` and accepts for the same
|
|
91
|
+
* reason: a line written between the copy and the truncate is lost. The window
|
|
92
|
+
* is a few milliseconds, once per `maxBytes` of log.
|
|
93
|
+
*
|
|
94
|
+
* Returns whether it rotated.
|
|
95
|
+
*/
|
|
96
|
+
export function rotateIfOversized(
|
|
97
|
+
filePath: string,
|
|
98
|
+
maxBytes: number = MAX_LOG_BYTES,
|
|
99
|
+
generations: number = LOG_GENERATIONS,
|
|
100
|
+
): boolean {
|
|
101
|
+
let size: number;
|
|
102
|
+
try {
|
|
103
|
+
size = statSync(filePath).size;
|
|
104
|
+
} catch {
|
|
105
|
+
return false; // no log yet
|
|
106
|
+
}
|
|
107
|
+
if (size <= maxBytes) return false;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Oldest first, or a shift would overwrite the generation it is about to move.
|
|
111
|
+
rmSync(`${filePath}.${generations}`, { force: true });
|
|
112
|
+
for (let i = generations - 1; i >= 1; i--) {
|
|
113
|
+
const from = `${filePath}.${i}`;
|
|
114
|
+
if (existsSync(from)) renameSync(from, `${filePath}.${i + 1}`);
|
|
115
|
+
}
|
|
116
|
+
copyFileSync(filePath, `${filePath}.1`);
|
|
117
|
+
truncateSync(filePath, 0);
|
|
118
|
+
return true;
|
|
119
|
+
} catch (e) {
|
|
120
|
+
// A log that cannot be rotated must not take the process with it — but it must not do so
|
|
121
|
+
// in silence either. `truncateSync` on a file the supervisor and the server child both
|
|
122
|
+
// hold open fails with EBUSY on Windows, and a blanket `catch` turned "this log is
|
|
123
|
+
// unbounded from here on" into no signal at all. Once per process, because the caller is
|
|
124
|
+
// a one-minute timer and the second failure says nothing the first did not.
|
|
125
|
+
if (!rotateFailureReported) {
|
|
126
|
+
rotateFailureReported = true;
|
|
127
|
+
console.warn(
|
|
128
|
+
`[log-rotate] Could not rotate ${filePath} (${(e as Error).message}) — it will keep `
|
|
129
|
+
+ "growing past its cap. On Windows this is usually another process holding the file open.",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** One warning per process; see the `catch` above. Exported for tests, which need to be able
|
|
137
|
+
* to observe the *first* failure rather than whichever one happens to run first. */
|
|
138
|
+
export function resetRotateFailureWarning(): void {
|
|
139
|
+
rotateFailureReported = false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let rotateFailureReported = false;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* grepping `ffmpeg -encoders`: the list only says the build was compiled with
|
|
17
17
|
* NVENC/QSV support, not that a compatible GPU/driver is present.
|
|
18
18
|
*/
|
|
19
|
+
import { readdirSync } from "node:fs";
|
|
19
20
|
|
|
20
21
|
export interface FfmpegCapabilities {
|
|
21
22
|
ffmpeg: string | null;
|
|
@@ -29,10 +30,38 @@ function encoderCandidates(): string[] {
|
|
|
29
30
|
switch (process.platform) {
|
|
30
31
|
case "win32": return ["h264_nvenc", "h264_qsv", "h264_amf", "libx264"];
|
|
31
32
|
case "darwin": return ["h264_videotoolbox", "libx264"];
|
|
32
|
-
|
|
33
|
+
// VAAPI sits after QSV (Intel does better through QSV) but before libx264, because it is
|
|
34
|
+
// the only hardware H.264 path an AMD Linux host has — `h264_amf` is Windows-only, so
|
|
35
|
+
// without VAAPI every Radeon machine fell all the way back to software encoding.
|
|
36
|
+
default: return ["h264_nvenc", "h264_qsv", "h264_vaapi", "libx264"];
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
/** The DRM render node VAAPI should use, or null when the host exposes none (no GPU driver,
|
|
41
|
+
* a container without `/dev/dri`). Picks the lowest `renderD*` rather than hardcoding
|
|
42
|
+
* `renderD128`: it is only the first node by convention and a host whose first GPU is not
|
|
43
|
+
* render-capable starts at `renderD129`. */
|
|
44
|
+
function vaapiRenderNode(): string | null {
|
|
45
|
+
try {
|
|
46
|
+
const nodes = readdirSync("/dev/dri").filter((n) => n.startsWith("renderD")).sort();
|
|
47
|
+
return nodes[0] ? `/dev/dri/${nodes[0]}` : null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Args VAAPI needs *before* `-i` (it opens the device during input setup), empty for every
|
|
54
|
+
* other encoder. Separate from `encoderArgs` because argv position matters here. */
|
|
55
|
+
export function encoderDeviceArgs(encoder: string): string[] {
|
|
56
|
+
if (encoder !== "h264_vaapi") return [];
|
|
57
|
+
const node = vaapiRenderNode();
|
|
58
|
+
return node ? ["-vaapi_device", node] : [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** VAAPI encodes from GPU-resident frames, so the chain has to convert and upload first.
|
|
62
|
+
* Callers that build their own `-vf` must append this rather than passing a second `-vf`. */
|
|
63
|
+
export const VAAPI_UPLOAD_FILTER = "format=nv12,hwupload";
|
|
64
|
+
|
|
36
65
|
/** Per-encoder quality/speed flags tuned for "watch a file inside the IDE" latency. */
|
|
37
66
|
export function encoderArgs(encoder: string): string[] {
|
|
38
67
|
switch (encoder) {
|
|
@@ -40,6 +69,8 @@ export function encoderArgs(encoder: string): string[] {
|
|
|
40
69
|
case "h264_qsv": return ["-c:v", "h264_qsv", "-preset", "veryfast", "-global_quality", "26"];
|
|
41
70
|
case "h264_amf": return ["-c:v", "h264_amf", "-quality", "speed", "-rc", "cqp", "-qp_i", "24", "-qp_p", "26"];
|
|
42
71
|
case "h264_videotoolbox": return ["-c:v", "h264_videotoolbox", "-b:v", "6M", "-realtime", "1"];
|
|
72
|
+
// No `-crf`/`-global_quality`: VAAPI's rate control is `-qp` on this encoder.
|
|
73
|
+
case "h264_vaapi": return ["-c:v", "h264_vaapi", "-qp", "26"];
|
|
43
74
|
default: return ["-c:v", "libx264", "-preset", "veryfast", "-crf", "23", "-maxrate", "8M", "-bufsize", "16M"];
|
|
44
75
|
}
|
|
45
76
|
}
|
|
@@ -50,8 +81,12 @@ async function encoderWorks(ffmpeg: string, encoder: string): Promise<boolean> {
|
|
|
50
81
|
[
|
|
51
82
|
ffmpeg, "-hide_banner", "-loglevel", "error", "-nostdin",
|
|
52
83
|
// 256px: hardware encoders reject frames below ~145×49 (NVENC "Frame Dimension less than minimum").
|
|
84
|
+
...encoderDeviceArgs(encoder),
|
|
53
85
|
"-f", "lavfi", "-i", "color=c=black:s=256x256:r=30:d=0.2",
|
|
54
|
-
"-frames:v", "3",
|
|
86
|
+
"-frames:v", "3",
|
|
87
|
+
// VAAPI wants hardware frames and rejects a software `-pix_fmt` on the output.
|
|
88
|
+
...(encoder === "h264_vaapi" ? ["-vf", VAAPI_UPLOAD_FILTER] : ["-pix_fmt", "yuv420p"]),
|
|
89
|
+
...encoderArgs(encoder),
|
|
55
90
|
"-f", "null", "-",
|
|
56
91
|
],
|
|
57
92
|
{ stdout: "ignore", stderr: "ignore", stdin: "ignore" },
|
|
@@ -124,7 +159,42 @@ export function getFfmpegCapabilities(): Promise<FfmpegCapabilities> {
|
|
|
124
159
|
return cached;
|
|
125
160
|
}
|
|
126
161
|
|
|
162
|
+
let cachedWorking: Promise<string[]> | null = null;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* **Every** candidate encoder that actually encodes on this host, in preference order — not
|
|
166
|
+
* just the first one, which is what `getFfmpegCapabilities().encoder` stops at.
|
|
167
|
+
*
|
|
168
|
+
* Separate call, and separately cached, on purpose: `detect()` short-circuits because playing a
|
|
169
|
+
* video inside the IDE has no use for the also-works list, and probing the rest costs one real
|
|
170
|
+
* encode each (plus a 10s timeout apiece against a hung driver). Only the remote-desktop codec
|
|
171
|
+
* picker wants the full list, so only it pays.
|
|
172
|
+
*/
|
|
173
|
+
export function workingEncoders(): Promise<string[]> {
|
|
174
|
+
if (!cachedWorking) {
|
|
175
|
+
cachedWorking = (async () => {
|
|
176
|
+
const { ffmpeg } = await getFfmpegCapabilities();
|
|
177
|
+
if (!ffmpeg) return [];
|
|
178
|
+
const found: string[] = [];
|
|
179
|
+
for (const candidate of encoderCandidates()) {
|
|
180
|
+
if (await encoderWorks(ffmpeg, candidate)) found.push(candidate);
|
|
181
|
+
}
|
|
182
|
+
return found;
|
|
183
|
+
})().then((list) => {
|
|
184
|
+
// An empty list means either no ffmpeg or no working encoder; both are conditions the
|
|
185
|
+
// user can fix from the checklist, so neither is remembered.
|
|
186
|
+
if (list.length === 0) cachedWorking = null;
|
|
187
|
+
return list;
|
|
188
|
+
}).catch((e) => {
|
|
189
|
+
cachedWorking = null;
|
|
190
|
+
throw e;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return cachedWorking;
|
|
194
|
+
}
|
|
195
|
+
|
|
127
196
|
/** Test hook: drop the cache so a different PATH can be probed. */
|
|
128
197
|
export function resetFfmpegCapabilitiesCache(): void {
|
|
129
198
|
cached = null;
|
|
199
|
+
cachedWorking = null;
|
|
130
200
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host audio → client: one ffmpeg per session capturing what the host is *playing* (not its
|
|
3
|
+
* microphone) and encoding it as Opus in Ogg, which `remote-desktop-ogg-opus.ts` unwraps into
|
|
4
|
+
* packets the browser's `AudioDecoder` takes directly.
|
|
5
|
+
*
|
|
6
|
+
* The hard part is not the encode, it is that only one of the three platforms can capture its
|
|
7
|
+
* own output without help:
|
|
8
|
+
*
|
|
9
|
+
* - **Linux**: PulseAudio and PipeWire both expose every sink's *monitor* as a capture source,
|
|
10
|
+
* and `@DEFAULT_MONITOR@` resolves to the current default sink's — so this follows the user
|
|
11
|
+
* switching from speakers to headphones with no re-detection. Needs ffmpeg built with
|
|
12
|
+
* `pulse` (checked, not assumed: a minimal build has ALSA only, and `-f pulse` on one of
|
|
13
|
+
* those fails at spawn with "Unknown input format").
|
|
14
|
+
* - **Windows**: ffmpeg has no WASAPI loopback input. The only route is DirectShow, and a
|
|
15
|
+
* loopback DirectShow device is something the host must already have — "Stereo Mix", which
|
|
16
|
+
* most drivers ship disabled, or a virtual cable. So this reports unavailable with the
|
|
17
|
+
* device name to enable rather than spawning an ffmpeg that exits instantly.
|
|
18
|
+
* - **macOS**: avfoundation can only capture real inputs; there is no system-audio device at
|
|
19
|
+
* all without a loopback driver (BlackHole). Same treatment.
|
|
20
|
+
*
|
|
21
|
+
* `-application lowdelay` + `-frame_duration 20` keeps the encoder's own delay at one frame,
|
|
22
|
+
* and `-page_duration 20000` is load-bearing: the Ogg muxer's default is **one second**, which
|
|
23
|
+
* measured 2000 ms between writes on this host — two seconds of audio latency against video
|
|
24
|
+
* arriving in twenty milliseconds. With it, pages land every ~40 ms.
|
|
25
|
+
*/
|
|
26
|
+
import { existsSync } from "node:fs";
|
|
27
|
+
import { getFfmpegCapabilities } from "../media-transcode/ffmpeg-capabilities.ts";
|
|
28
|
+
import { detectLinuxSession, linuxSessionEnv } from "./remote-desktop-linux-session.ts";
|
|
29
|
+
import { OggOpusDemuxer } from "./remote-desktop-ogg-opus.ts";
|
|
30
|
+
|
|
31
|
+
/** Opus always decodes at 48 kHz whatever the source rate was. */
|
|
32
|
+
export const AUDIO_SAMPLE_RATE = 48_000;
|
|
33
|
+
const AUDIO_BITRATE = "96k";
|
|
34
|
+
/** One Opus frame. Also the Ogg page duration, in microseconds, for the reason above. */
|
|
35
|
+
const FRAME_MS = 20;
|
|
36
|
+
|
|
37
|
+
export interface AudioSupport {
|
|
38
|
+
available: boolean;
|
|
39
|
+
/** Why not, and what the user can do about it. Null when audio works. */
|
|
40
|
+
reason: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The ffmpeg input for this host's own output, or null when it has none. */
|
|
44
|
+
export function audioInputArgs(
|
|
45
|
+
platform: NodeJS.Platform = process.platform,
|
|
46
|
+
hasPulse = pulseAvailable(),
|
|
47
|
+
): string[] | null {
|
|
48
|
+
if (platform !== "linux" || !hasPulse) return null;
|
|
49
|
+
return ["-f", "pulse", "-i", "@DEFAULT_MONITOR@"];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Is there a PulseAudio/PipeWire server to talk to? The socket, not `pactl`: the binary is a
|
|
53
|
+
* separate package (the same `xrandr` vs `libXrandr` trap), and a PPM started by its systemd
|
|
54
|
+
* unit has `XDG_RUNTIME_DIR` even when it has no `DISPLAY`. */
|
|
55
|
+
export function pulseAvailable(runtimeDir = process.env.XDG_RUNTIME_DIR): boolean {
|
|
56
|
+
if (!runtimeDir) return false;
|
|
57
|
+
return existsSync(`${runtimeDir}/pulse/native`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Whether this ffmpeg build has the `pulse` demuxer at all — a minimal build has ALSA only,
|
|
61
|
+
* and `-f pulse` there fails at spawn rather than degrading. */
|
|
62
|
+
export async function ffmpegHasPulse(ffmpeg: string): Promise<boolean> {
|
|
63
|
+
try {
|
|
64
|
+
const proc = Bun.spawn([ffmpeg, "-hide_banner", "-demuxers"], {
|
|
65
|
+
stdout: "pipe", stderr: "ignore", stdin: "ignore",
|
|
66
|
+
});
|
|
67
|
+
const text = await new Response(proc.stdout).text();
|
|
68
|
+
await proc.exited;
|
|
69
|
+
// `ffmpeg -demuxers` writes " D d pulse Pulse audio input" — flags, then a
|
|
70
|
+
// *space-separated* name. Anchoring on the flag column rather than on the whole line keeps
|
|
71
|
+
// this working for both the `-demuxers` and `-devices` spellings (" DE pulse").
|
|
72
|
+
return /^\s*D\S*\s+(?:\S+\s+)?pulse\s/m.test(text);
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Can this host stream its own audio, and if not, what should the user do. */
|
|
79
|
+
export async function audioSupport(platform: NodeJS.Platform = process.platform): Promise<AudioSupport> {
|
|
80
|
+
if (platform === "win32") {
|
|
81
|
+
return {
|
|
82
|
+
available: false,
|
|
83
|
+
reason: "Windows has no loopback capture ffmpeg can use — enable \"Stereo Mix\" in Sound "
|
|
84
|
+
+ "settings, or install a virtual audio cable.",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (platform === "darwin") {
|
|
88
|
+
return {
|
|
89
|
+
available: false,
|
|
90
|
+
reason: "macOS has no system-audio input — install a loopback driver such as BlackHole.",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (platform !== "linux") return { available: false, reason: "Not supported on this platform." };
|
|
94
|
+
if (!pulseAvailable()) {
|
|
95
|
+
return { available: false, reason: "No PulseAudio/PipeWire server is running on the host." };
|
|
96
|
+
}
|
|
97
|
+
const { ffmpeg } = await getFfmpegCapabilities();
|
|
98
|
+
if (!ffmpeg) return { available: false, reason: "ffmpeg is not installed." };
|
|
99
|
+
if (!await ffmpegHasPulse(ffmpeg)) {
|
|
100
|
+
return { available: false, reason: "This ffmpeg build has no PulseAudio support." };
|
|
101
|
+
}
|
|
102
|
+
return { available: true, reason: null };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Full argv, pure so it can be asserted without spawning. */
|
|
106
|
+
export function buildAudioArgs(ffmpeg: string, input: string[]): string[] {
|
|
107
|
+
return [
|
|
108
|
+
ffmpeg, "-hide_banner", "-loglevel", "error", "-nostdin",
|
|
109
|
+
"-fflags", "nobuffer", "-flags", "low_delay",
|
|
110
|
+
...input,
|
|
111
|
+
"-c:a", "libopus", "-b:a", AUDIO_BITRATE,
|
|
112
|
+
// `lowdelay` disables the encoder's look-ahead; without it Opus adds its own ~100ms.
|
|
113
|
+
"-application", "lowdelay", "-frame_duration", String(FRAME_MS),
|
|
114
|
+
"-ar", String(AUDIO_SAMPLE_RATE), "-ac", "2",
|
|
115
|
+
"-page_duration", String(FRAME_MS * 1000),
|
|
116
|
+
"-flush_packets", "1", "-f", "ogg", "pipe:1",
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface AudioHandle {
|
|
121
|
+
stop(): void;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface StartAudioOptions {
|
|
125
|
+
/** One decoded Opus packet, ready for `AudioDecoder`. */
|
|
126
|
+
onPacket: (packet: Uint8Array) => void;
|
|
127
|
+
/** Channel layout, once the stream header has been parsed. Fires before the first packet. */
|
|
128
|
+
onStreamInfo: (info: { channels: number; sampleRate: number }) => void;
|
|
129
|
+
onExit?: (code: number | null, reason?: string) => void;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Start capturing host audio, or return null when this host cannot (see `audioSupport`).
|
|
133
|
+
* Never throws for an unsupported host: audio is an extra, and a session must not fail to
|
|
134
|
+
* start because the machine has no loopback device. */
|
|
135
|
+
export async function startAudioCapture(opts: StartAudioOptions): Promise<AudioHandle | null> {
|
|
136
|
+
const { ffmpeg } = await getFfmpegCapabilities();
|
|
137
|
+
if (!ffmpeg) return null;
|
|
138
|
+
const support = await audioSupport();
|
|
139
|
+
if (!support.available) return null;
|
|
140
|
+
const input = audioInputArgs();
|
|
141
|
+
if (!input) return null;
|
|
142
|
+
|
|
143
|
+
const session = process.platform === "linux" ? detectLinuxSession() : null;
|
|
144
|
+
const proc = Bun.spawn(buildAudioArgs(ffmpeg, input), {
|
|
145
|
+
stdout: "pipe",
|
|
146
|
+
stderr: "pipe",
|
|
147
|
+
stdin: "ignore",
|
|
148
|
+
...(session ? { env: { ...process.env, ...linuxSessionEnv(session) } } : {}),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
let stopped = false;
|
|
152
|
+
const stop = () => {
|
|
153
|
+
if (stopped) return;
|
|
154
|
+
stopped = true;
|
|
155
|
+
// SIGKILL for the same reason the video capture uses it — see `remote-desktop-capture.ts`.
|
|
156
|
+
if (!proc.killed) proc.kill("SIGKILL");
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const stderrTail = new Response(proc.stderr).text().catch(() => "");
|
|
160
|
+
proc.exited.then(async (code) => {
|
|
161
|
+
const diedOnItsOwn = !stopped;
|
|
162
|
+
stopped = true;
|
|
163
|
+
let reason: string | undefined;
|
|
164
|
+
if (diedOnItsOwn && code !== 0 && code !== null) {
|
|
165
|
+
reason = (await stderrTail).trim().split("\n").slice(-2).join(" | ");
|
|
166
|
+
console.warn(`[remote-desktop] audio ffmpeg exited ${code}: ${reason}`);
|
|
167
|
+
}
|
|
168
|
+
opts.onExit?.(code, reason);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const demuxer = new OggOpusDemuxer();
|
|
172
|
+
let announced = false;
|
|
173
|
+
// Manual pull loop, never `for await`: cancelling this reader is what segfaults Bun on
|
|
174
|
+
// Windows (see `remote-desktop-capture.ts`'s header). `stop()` kills the process and the
|
|
175
|
+
// pending `read()` resolves done.
|
|
176
|
+
const reader = proc.stdout.getReader();
|
|
177
|
+
(async () => {
|
|
178
|
+
for (;;) {
|
|
179
|
+
const { done, value } = await reader.read();
|
|
180
|
+
if (done) return;
|
|
181
|
+
if (!value) continue;
|
|
182
|
+
const packets = demuxer.push(value);
|
|
183
|
+
const info = demuxer.info;
|
|
184
|
+
if (!announced && info) {
|
|
185
|
+
announced = true;
|
|
186
|
+
opts.onStreamInfo({ channels: info.channels, sampleRate: AUDIO_SAMPLE_RATE });
|
|
187
|
+
}
|
|
188
|
+
for (const p of packets) opts.onPacket(p);
|
|
189
|
+
}
|
|
190
|
+
})();
|
|
191
|
+
|
|
192
|
+
return { stop };
|
|
193
|
+
}
|