@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
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which graphical session this Linux host is running, and how to reach it.
|
|
3
|
+
*
|
|
4
|
+
* The PPM process very often has NO `DISPLAY` / `WAYLAND_DISPLAY` of its own: started by a
|
|
5
|
+
* systemd user unit (`autostart-register.ts`), by an ssh login, or from a tmux pane detached
|
|
6
|
+
* long ago, it inherits a bare environment while a full desktop session is running two feet
|
|
7
|
+
* away. Reading only the env would report "no graphical session" on a machine that plainly has
|
|
8
|
+
* one, so every lookup falls back to probing the sockets the session itself created.
|
|
9
|
+
*
|
|
10
|
+
* Wayland is checked BEFORE X11 and that order is load-bearing: a Wayland session almost
|
|
11
|
+
* always also runs XWayland, which sets `DISPLAY` and answers `x11grab` — with a view of
|
|
12
|
+
* XWayland clients only, i.e. a black or half-empty screen rather than an error. Preferring
|
|
13
|
+
* X11 whenever it answers would therefore silently capture the wrong thing on every modern
|
|
14
|
+
* GNOME/KDE-Wayland host.
|
|
15
|
+
*
|
|
16
|
+
* `homedir()` here is a real home path on purpose (`~/.Xauthority`), like
|
|
17
|
+
* `autostart-generator.ts` — it has nothing to do with the PPM directory and must not go
|
|
18
|
+
* through `getPpmDir()`.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
|
|
24
|
+
export type LinuxSession =
|
|
25
|
+
/** `display` is an X display string (`":0"`); `xauthority` is best-effort — capture and XTEST
|
|
26
|
+
* both work without it when PPM runs as the session's own user, which is the normal case. */
|
|
27
|
+
| { kind: "x11"; display: string; xauthority: string | null }
|
|
28
|
+
| { kind: "wayland"; display: string; runtimeDir: string };
|
|
29
|
+
|
|
30
|
+
/** `$XDG_RUNTIME_DIR`, or the path systemd would have used for this uid. */
|
|
31
|
+
function runtimeDir(env: NodeJS.ProcessEnv): string {
|
|
32
|
+
const fromEnv = env.XDG_RUNTIME_DIR?.trim();
|
|
33
|
+
if (fromEnv) return fromEnv;
|
|
34
|
+
return `/run/user/${typeof process.getuid === "function" ? process.getuid() : 1000}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Entries of `dir` matching `re`, or [] when the directory cannot be read (absent, not ours). */
|
|
38
|
+
function socketsIn(dir: string, re: RegExp): string[] {
|
|
39
|
+
try {
|
|
40
|
+
return readdirSync(dir).filter((name) => re.test(name)).sort();
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The Wayland display name (`"wayland-0"`) for this host, or null. */
|
|
47
|
+
function findWaylandDisplay(env: NodeJS.ProcessEnv): string | null {
|
|
48
|
+
const fromEnv = env.WAYLAND_DISPLAY?.trim();
|
|
49
|
+
if (fromEnv) return fromEnv;
|
|
50
|
+
// `wayland-0.lock` sits next to the socket — match the socket only.
|
|
51
|
+
return socketsIn(runtimeDir(env), /^wayland-\d+$/)[0] ?? null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The X display string (`":0"`) for this host, or null. */
|
|
55
|
+
function findX11Display(env: NodeJS.ProcessEnv): string | null {
|
|
56
|
+
const fromEnv = env.DISPLAY?.trim();
|
|
57
|
+
if (fromEnv) return fromEnv;
|
|
58
|
+
const socket = socketsIn("/tmp/.X11-unix", /^X\d+$/)[0];
|
|
59
|
+
return socket ? `:${socket.slice(1)}` : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The X authority file, or null when none is found (X still accepts the owning user locally). */
|
|
63
|
+
function findXauthority(env: NodeJS.ProcessEnv): string | null {
|
|
64
|
+
const fromEnv = env.XAUTHORITY?.trim();
|
|
65
|
+
if (fromEnv && existsSync(fromEnv)) return fromEnv;
|
|
66
|
+
// What Xorg is actually started with on a modern seat: `-auth /run/user/1000/xauth_XXXXXX`.
|
|
67
|
+
const dir = runtimeDir(env);
|
|
68
|
+
const runtimeAuth = socketsIn(dir, /^xauth_/)[0];
|
|
69
|
+
if (runtimeAuth) return join(dir, runtimeAuth);
|
|
70
|
+
const legacy = join(homedir(), ".Xauthority");
|
|
71
|
+
return existsSync(legacy) ? legacy : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* How long a probe result is reused before the filesystem is asked again.
|
|
76
|
+
*
|
|
77
|
+
* Short enough that the answer follows the host, long enough that the saving is the whole
|
|
78
|
+
* point: pointer input arrives at roughly 120 probes a second, so five seconds removes about
|
|
79
|
+
* 99.6% of them and still lets a session that appears — or changes kind — be seen within one.
|
|
80
|
+
*/
|
|
81
|
+
const SESSION_TTL_MS = 5_000;
|
|
82
|
+
|
|
83
|
+
/** Memoized answer for `process.env`. See `detectLinuxSession`. */
|
|
84
|
+
let cached: { session: LinuxSession | null; at: number } | null = null;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The graphical session on this host, or null when there is none to capture (a headless
|
|
88
|
+
* server, a CI container). Pure w.r.t. `env` so callers can pin it in tests.
|
|
89
|
+
*
|
|
90
|
+
* The answer for the *ambient* environment is memoized, because this sits on the injected-input
|
|
91
|
+
* path: `remote-desktop-input.ts` picks a backend and `remote-desktop-input-linux.ts` resolves
|
|
92
|
+
* the display, so every pointer event detected twice. The client coalesces pointer moves to one
|
|
93
|
+
* per animation frame, so on the host this code exists for — PPM under a systemd user unit, no
|
|
94
|
+
* `DISPLAY` in its environment, every probe hitting the filesystem — that was ~240 blocking
|
|
95
|
+
* `readdirSync`/`existsSync` calls a second on the event loop the rest of this release spends
|
|
96
|
+
* effort keeping free.
|
|
97
|
+
*
|
|
98
|
+
* The memo expires, and the negative answer is the reason. PPM can easily start before the
|
|
99
|
+
* graphical session — a boot-time systemd unit, an ssh start, a login screen — and a null
|
|
100
|
+
* cached for the life of the process would report Remote Desktop unsupported on a host that
|
|
101
|
+
* has a desktop by the time anyone asks. The same expiry is what lets an X11 → Wayland
|
|
102
|
+
* re-login stop routing input at a display that is gone. `resetX11()` still drops it eagerly
|
|
103
|
+
* alongside the connection it belongs to, but nothing depends on that being called.
|
|
104
|
+
*
|
|
105
|
+
* A caller that passes its own `env` is pinning a scenario and neither reads nor fills the cache.
|
|
106
|
+
*/
|
|
107
|
+
export function detectLinuxSession(env: NodeJS.ProcessEnv = process.env): LinuxSession | null {
|
|
108
|
+
if (env !== process.env) return probeLinuxSession(env);
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
if (cached && now - cached.at < SESSION_TTL_MS) return cached.session;
|
|
111
|
+
cached = { session: probeLinuxSession(env), at: now };
|
|
112
|
+
return cached.session;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Forget the memoized session. Called by `resetX11()`, which is the other half of the same
|
|
116
|
+
* state: a session that changed invalidates the connection opened against it. */
|
|
117
|
+
export function resetLinuxSession(): void {
|
|
118
|
+
cached = null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function probeLinuxSession(env: NodeJS.ProcessEnv): LinuxSession | null {
|
|
122
|
+
// `XDG_SESSION_TYPE` is authoritative when present and stops an XWayland `DISPLAY` from
|
|
123
|
+
// outvoting a Wayland session; when absent, socket order below encodes the same preference.
|
|
124
|
+
const declared = env.XDG_SESSION_TYPE?.trim().toLowerCase();
|
|
125
|
+
|
|
126
|
+
if (declared !== "x11") {
|
|
127
|
+
const wayland = findWaylandDisplay(env);
|
|
128
|
+
if (wayland) return { kind: "wayland", display: wayland, runtimeDir: runtimeDir(env) };
|
|
129
|
+
}
|
|
130
|
+
if (declared !== "wayland") {
|
|
131
|
+
const display = findX11Display(env);
|
|
132
|
+
if (display) return { kind: "x11", display, xauthority: findXauthority(env) };
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Env additions a child process (ffmpeg) needs to reach the session. Spread over
|
|
138
|
+
* `process.env`, never used alone — ffmpeg still needs `PATH`. */
|
|
139
|
+
export function linuxSessionEnv(session: LinuxSession): Record<string, string> {
|
|
140
|
+
if (session.kind === "wayland") {
|
|
141
|
+
return { WAYLAND_DISPLAY: session.display, XDG_RUNTIME_DIR: session.runtimeDir };
|
|
142
|
+
}
|
|
143
|
+
return session.xauthority
|
|
144
|
+
? { DISPLAY: session.display, XAUTHORITY: session.xauthority }
|
|
145
|
+
: { DISPLAY: session.display };
|
|
146
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulls Opus packets out of the Ogg stream ffmpeg writes, so the browser gets framed packets it
|
|
3
|
+
* can hand straight to `AudioDecoder` instead of a container it would have to demux itself.
|
|
4
|
+
*
|
|
5
|
+
* Server-side for the same reason `access-unit-assembler.ts` is: the bytes arrive as arbitrary
|
|
6
|
+
* stdout chunks with no relation to packet boundaries, and the code that finds those boundaries
|
|
7
|
+
* is a pure function worth testing once rather than a second implementation in the client.
|
|
8
|
+
*
|
|
9
|
+
* Two things about the Ogg muxer are worth knowing before touching this. It buffers a whole
|
|
10
|
+
* **`page_duration`** before writing a page, and that option defaults to **one second** — the
|
|
11
|
+
* capture spawns with `-page_duration 20000` because the default measured 2000 ms between
|
|
12
|
+
* writes, i.e. two seconds of audio latency on a stream whose video arrives in 20 ms. And a
|
|
13
|
+
* packet may be *split across pages* (the lacing table's 255 means "continues"), which for
|
|
14
|
+
* 96 kbit/s Opus is rare enough to never show up in a short test and certain to show up
|
|
15
|
+
* eventually — a demuxer that ignores continuation emits a truncated packet, which
|
|
16
|
+
* `AudioDecoder` reports as a decode error rather than as a click.
|
|
17
|
+
*
|
|
18
|
+
* Ogg page layout (little-endian): `OggS`, version, header flags, 8-byte granule, 4-byte
|
|
19
|
+
* serial, 4-byte sequence, 4-byte CRC, segment count, then that many lacing bytes, then the
|
|
20
|
+
* payload. The CRC is deliberately **not** verified: this is a pipe from a child process on the
|
|
21
|
+
* same machine, not a network, and the one failure it could catch (a partial write) is already
|
|
22
|
+
* handled by waiting for the full page.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const OGG_MAGIC = [0x4f, 0x67, 0x67, 0x53]; // "OggS"
|
|
26
|
+
const HEADER_BYTES = 27;
|
|
27
|
+
/** `OpusHead` is 19 bytes; anything shorter is not one. */
|
|
28
|
+
const OPUS_HEAD_MIN = 19;
|
|
29
|
+
|
|
30
|
+
export interface OpusStreamInfo {
|
|
31
|
+
channels: number;
|
|
32
|
+
/** Samples the decoder should discard at the start (Opus encoder warm-up). */
|
|
33
|
+
preSkip: number;
|
|
34
|
+
/** The rate the *source* was at. Opus always decodes at 48 kHz regardless. */
|
|
35
|
+
inputSampleRate: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Parse an `OpusHead` identification header, or null when these bytes are not one. */
|
|
39
|
+
export function parseOpusHead(bytes: Uint8Array): OpusStreamInfo | null {
|
|
40
|
+
if (bytes.length < OPUS_HEAD_MIN) return null;
|
|
41
|
+
const magic = String.fromCharCode(...bytes.subarray(0, 8));
|
|
42
|
+
if (magic !== "OpusHead") return null;
|
|
43
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
44
|
+
return {
|
|
45
|
+
channels: bytes[9]!,
|
|
46
|
+
preSkip: view.getUint16(10, true),
|
|
47
|
+
inputSampleRate: view.getUint32(12, true),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Feed stdout chunks, get whole Opus audio packets. The two header packets (`OpusHead`,
|
|
52
|
+
* `OpusTags`) are consumed rather than emitted — the first becomes `info`. */
|
|
53
|
+
export class OggOpusDemuxer {
|
|
54
|
+
private buffer = new Uint8Array(0);
|
|
55
|
+
/** Bytes of a packet that ran past the end of its page, awaiting the continuation. */
|
|
56
|
+
private partial: Uint8Array[] = [];
|
|
57
|
+
private streamInfo: OpusStreamInfo | null = null;
|
|
58
|
+
private sawTags = false;
|
|
59
|
+
|
|
60
|
+
get info(): OpusStreamInfo | null {
|
|
61
|
+
return this.streamInfo;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
push(chunk: Uint8Array): Uint8Array[] {
|
|
65
|
+
this.buffer = concat([this.buffer, chunk]);
|
|
66
|
+
const out: Uint8Array[] = [];
|
|
67
|
+
let offset = 0;
|
|
68
|
+
|
|
69
|
+
for (;;) {
|
|
70
|
+
const start = findMagic(this.buffer, offset);
|
|
71
|
+
if (start < 0) break;
|
|
72
|
+
if (this.buffer.length < start + HEADER_BYTES) break;
|
|
73
|
+
const segmentCount = this.buffer[start + HEADER_BYTES - 1]!;
|
|
74
|
+
const tableEnd = start + HEADER_BYTES + segmentCount;
|
|
75
|
+
if (this.buffer.length < tableEnd) break;
|
|
76
|
+
const table = this.buffer.subarray(start + HEADER_BYTES, tableEnd);
|
|
77
|
+
let payloadLength = 0;
|
|
78
|
+
for (const n of table) payloadLength += n;
|
|
79
|
+
const pageEnd = tableEnd + payloadLength;
|
|
80
|
+
if (this.buffer.length < pageEnd) break; // page still arriving
|
|
81
|
+
|
|
82
|
+
// A page whose first packet continues one from the previous page: `partial` already holds
|
|
83
|
+
// its head. Dropping `partial` on a page that is *not* a continuation is deliberate — it
|
|
84
|
+
// means bytes were lost, and half a packet decodes to noise.
|
|
85
|
+
const continued = (this.buffer[start + 5]! & 0x01) !== 0;
|
|
86
|
+
if (!continued) this.partial = [];
|
|
87
|
+
|
|
88
|
+
let cursor = tableEnd;
|
|
89
|
+
let pending: Uint8Array[] = this.partial;
|
|
90
|
+
for (const size of table) {
|
|
91
|
+
pending.push(this.buffer.subarray(cursor, cursor + size));
|
|
92
|
+
cursor += size;
|
|
93
|
+
if (size === 255) continue; // packet continues in the next lacing value
|
|
94
|
+
const packet = concat(pending);
|
|
95
|
+
pending = [];
|
|
96
|
+
this.classify(packet, out);
|
|
97
|
+
}
|
|
98
|
+
// Anything still pending ran off the end of this page; 255 as the last lacing value is
|
|
99
|
+
// exactly how Ogg says "continues on the next page".
|
|
100
|
+
this.partial = pending;
|
|
101
|
+
offset = pageEnd;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.buffer = offset > 0 ? this.buffer.slice(offset) : this.buffer;
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Header packets set up the stream; everything else is audio. Recognised by content rather
|
|
109
|
+
* than by position, because a stream can be re-announced mid-pipe (ffmpeg restarted). */
|
|
110
|
+
private classify(packet: Uint8Array, out: Uint8Array[]): void {
|
|
111
|
+
if (packet.length === 0) return;
|
|
112
|
+
const head = parseOpusHead(packet);
|
|
113
|
+
if (head) { this.streamInfo = head; this.sawTags = false; return; }
|
|
114
|
+
if (!this.sawTags && startsWith(packet, "OpusTags")) { this.sawTags = true; return; }
|
|
115
|
+
out.push(packet);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function startsWith(bytes: Uint8Array, ascii: string): boolean {
|
|
120
|
+
if (bytes.length < ascii.length) return false;
|
|
121
|
+
for (let i = 0; i < ascii.length; i++) if (bytes[i] !== ascii.charCodeAt(i)) return false;
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function findMagic(bytes: Uint8Array, from: number): number {
|
|
126
|
+
for (let i = from; i + 4 <= bytes.length; i++) {
|
|
127
|
+
if (bytes[i] === OGG_MAGIC[0] && bytes[i + 1] === OGG_MAGIC[1]
|
|
128
|
+
&& bytes[i + 2] === OGG_MAGIC[2] && bytes[i + 3] === OGG_MAGIC[3]) return i;
|
|
129
|
+
}
|
|
130
|
+
// Keep the last 3 bytes: they may be the start of a magic split across chunks.
|
|
131
|
+
return -1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Always copies, even for a single part. The parts are `subarray` views into the receive
|
|
135
|
+
* buffer, so returning one would hand the session a window onto a buffer this demuxer goes on
|
|
136
|
+
* to replace — and would pin the whole old chunk alive for the life of one 240-byte packet. */
|
|
137
|
+
function concat(parts: readonly Uint8Array[]): Uint8Array<ArrayBuffer> {
|
|
138
|
+
let total = 0;
|
|
139
|
+
for (const p of parts) total += p.length;
|
|
140
|
+
const out = new Uint8Array(total);
|
|
141
|
+
let at = 0;
|
|
142
|
+
for (const p of parts) { out.set(p, at); at += p.length; }
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Privacy mode: stop the person sitting at the host from using it, and turn their monitor off,
|
|
3
|
+
* while the remote session keeps working. RustDesk's `toolbarPrivacyMode` plus its separate
|
|
4
|
+
* "block user input", which on Linux are the same two mechanisms and so are one switch here.
|
|
5
|
+
*
|
|
6
|
+
* **Blocking local input is an X grab, not `EVIOCGRAB`.** The evdev route reads like the right
|
|
7
|
+
* layer — grab the device and the kernel stops delivering its events to X at all — and it is
|
|
8
|
+
* the one that does not work on a real desktop: an evdev grab is *exclusive*, and keyboard
|
|
9
|
+
* remappers hold one on every keyboard for exactly this reason. Measured on this host, with
|
|
10
|
+
* `toshy` running: `EVIOCGRAB` returned **EBUSY on 3 of 4 devices** (both keyboards and the
|
|
11
|
+
* wireless receiver) and succeeded only on the mouse — which is the worst possible outcome,
|
|
12
|
+
* because "privacy mode on" would have been reported while the local keyboard still typed.
|
|
13
|
+
* kmonad, keyd and xremap all take the same grab.
|
|
14
|
+
*
|
|
15
|
+
* `XGrabKeyboard` + `XGrabPointer` with `owner_events = False` sit above all of that: whatever
|
|
16
|
+
* a remapper re-emits still enters X, and X routes it to the grabbing client, which drops it.
|
|
17
|
+
* The catch that makes this look impossible at first is that a grab would swallow the remote
|
|
18
|
+
* session's *own* injected events too — and the fix is one call, `XTestGrabControl(dpy, True)`,
|
|
19
|
+
* whose entire purpose is to make a client's XTEST events impervious to grabs. Verified rather
|
|
20
|
+
* than assumed: an `XTestFakeMotionEvent` issued while both grabs were held moved the real
|
|
21
|
+
* pointer to the requested coordinate.
|
|
22
|
+
*
|
|
23
|
+
* That also gives the safety property this feature needs: the grabs belong to the X
|
|
24
|
+
* *connection*, so they are released by `XUngrab*`, by the connection closing, and by the PPM
|
|
25
|
+
* process exiting. The worst case for a bug here is `pkill ppm`, never a reboot.
|
|
26
|
+
*
|
|
27
|
+
* **Blanking is DPMS, not a black window.** x11grab captures the root window, so a fullscreen
|
|
28
|
+
* black override-redirect window would be captured too — the remote would see the black screen
|
|
29
|
+
* and nothing else. `DPMSForceLevel(DPMSModeOff)` turns the *output* off and leaves the
|
|
30
|
+
* framebuffer alone, so the capture is unaffected. It needs re-asserting on a timer: injected
|
|
31
|
+
* XTEST events reset the server's idle timer exactly as real ones do, so the monitor would
|
|
32
|
+
* otherwise come back on the first remote keystroke.
|
|
33
|
+
*/
|
|
34
|
+
import { detectLinuxSession } from "./remote-desktop-linux-session.ts";
|
|
35
|
+
import { getX11, type X11Connection } from "./remote-desktop-x11.ts";
|
|
36
|
+
|
|
37
|
+
/** `XGrabKeyboard`/`XGrabPointer` return codes we care about. */
|
|
38
|
+
const GRAB_SUCCESS = 0;
|
|
39
|
+
const GRAB_MODE_ASYNC = 1;
|
|
40
|
+
const CURRENT_TIME = 0n;
|
|
41
|
+
const NONE = 0n;
|
|
42
|
+
/** Pointer events the grab claims. Every button and motion bit — the point is that nothing
|
|
43
|
+
* reaches another client, so the mask is deliberately everything rather than a subset. */
|
|
44
|
+
const POINTER_EVENT_MASK = 0xffff;
|
|
45
|
+
|
|
46
|
+
/** DPMS power levels from `dpms.h`. */
|
|
47
|
+
const DPMS_MODE_ON = 0;
|
|
48
|
+
const DPMS_MODE_OFF = 3;
|
|
49
|
+
/** The idle timer is reset by our own injected input, so the level has to be re-forced. Two
|
|
50
|
+
* seconds is short enough that a monitor never visibly comes back and long enough to be free. */
|
|
51
|
+
const BLANK_REASSERT_MS = 2_000;
|
|
52
|
+
|
|
53
|
+
export interface PrivacySupport {
|
|
54
|
+
available: boolean;
|
|
55
|
+
/** Why not, and what to do about it. Null when privacy mode works. */
|
|
56
|
+
reason: string | null;
|
|
57
|
+
/** Whether the monitor can also be blanked. False means input blocking works on its own —
|
|
58
|
+
* useful, so it is offered rather than withheld. */
|
|
59
|
+
canBlank: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function privacySupport(platform: NodeJS.Platform = process.platform): Promise<PrivacySupport> {
|
|
63
|
+
if (platform !== "linux") {
|
|
64
|
+
return { available: false, reason: "Only implemented on Linux (X11) so far.", canBlank: false };
|
|
65
|
+
}
|
|
66
|
+
const session = detectLinuxSession();
|
|
67
|
+
if (session?.kind !== "x11") {
|
|
68
|
+
return { available: false, reason: "Needs an X11 session.", canBlank: false };
|
|
69
|
+
}
|
|
70
|
+
const x11 = await getX11(session);
|
|
71
|
+
if (!x11) return { available: false, reason: "Cannot reach the host's X server.", canBlank: false };
|
|
72
|
+
// XTEST is what makes the grab survivable: without it the grab would block the remote
|
|
73
|
+
// session's input along with the local user's.
|
|
74
|
+
if (!x11.hasXTest) {
|
|
75
|
+
return {
|
|
76
|
+
available: false,
|
|
77
|
+
reason: "The host's X server has no XTEST extension, so a grab would block remote input too.",
|
|
78
|
+
canBlank: false,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { available: true, reason: null, canBlank: x11.dpms !== null };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** One engaged privacy mode. `release()` is idempotent and runs on every teardown path. */
|
|
85
|
+
export interface PrivacyHandle {
|
|
86
|
+
release(): void;
|
|
87
|
+
/** True when local keyboard *and* pointer are both held. A partial grab is never returned —
|
|
88
|
+
* see `engagePrivacy`. */
|
|
89
|
+
inputBlocked: boolean;
|
|
90
|
+
/** False when the monitor could not be blanked but input *is* blocked. */
|
|
91
|
+
blanked: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Take both grabs, or neither. A keyboard grab without a pointer grab (or the reverse) reports
|
|
95
|
+
* privacy mode as on while half the host is still usable, which is worse than refusing. */
|
|
96
|
+
function grabInput(x11: X11Connection): boolean {
|
|
97
|
+
const kb = x11.x11.XGrabKeyboard(x11.dpy, x11.root, 0, GRAB_MODE_ASYNC, GRAB_MODE_ASYNC, CURRENT_TIME);
|
|
98
|
+
if (kb !== GRAB_SUCCESS) return false;
|
|
99
|
+
const pt = x11.x11.XGrabPointer(
|
|
100
|
+
x11.dpy, x11.root, 0, POINTER_EVENT_MASK, GRAB_MODE_ASYNC, GRAB_MODE_ASYNC, NONE, NONE, CURRENT_TIME,
|
|
101
|
+
);
|
|
102
|
+
if (pt !== GRAB_SUCCESS) {
|
|
103
|
+
x11.x11.XUngrabKeyboard(x11.dpy, CURRENT_TIME);
|
|
104
|
+
x11.x11.XFlush(x11.dpy);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
// Without this the injected events below would be delivered to *us* (the grabbing client)
|
|
108
|
+
// rather than to the focused window, i.e. the remote session would go dead on engage.
|
|
109
|
+
x11.xtst?.XTestGrabControl(x11.dpy, 1);
|
|
110
|
+
x11.x11.XSync(x11.dpy, 0);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Engage privacy mode, or return null when this host cannot (see `privacySupport`) or when
|
|
116
|
+
* another client already holds a grab — a screen locker or an open menu both do, and
|
|
117
|
+
* `AlreadyGrabbed` is a real, temporary condition rather than a broken host.
|
|
118
|
+
*
|
|
119
|
+
* Never throws: a session must not die because privacy mode failed.
|
|
120
|
+
*/
|
|
121
|
+
export async function engagePrivacy(): Promise<PrivacyHandle | null> {
|
|
122
|
+
if (process.platform !== "linux") return null;
|
|
123
|
+
const session = detectLinuxSession();
|
|
124
|
+
if (session?.kind !== "x11") return null;
|
|
125
|
+
const x11 = await getX11(session);
|
|
126
|
+
if (!x11?.hasXTest) return null;
|
|
127
|
+
|
|
128
|
+
if (!grabInput(x11)) return null;
|
|
129
|
+
|
|
130
|
+
let blankTimer: ReturnType<typeof setInterval> | null = null;
|
|
131
|
+
let blanked = false;
|
|
132
|
+
const force = (level: number): boolean => {
|
|
133
|
+
if (!x11.dpms) return false;
|
|
134
|
+
try {
|
|
135
|
+
x11.dpms.DPMSEnable(x11.dpy);
|
|
136
|
+
x11.dpms.DPMSForceLevel(x11.dpy, level);
|
|
137
|
+
x11.x11.XFlush(x11.dpy);
|
|
138
|
+
return true;
|
|
139
|
+
} catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
blanked = force(DPMS_MODE_OFF);
|
|
144
|
+
if (blanked) blankTimer = setInterval(() => force(DPMS_MODE_OFF), BLANK_REASSERT_MS);
|
|
145
|
+
|
|
146
|
+
let released = false;
|
|
147
|
+
return {
|
|
148
|
+
inputBlocked: true,
|
|
149
|
+
blanked,
|
|
150
|
+
release: () => {
|
|
151
|
+
if (released) return;
|
|
152
|
+
released = true;
|
|
153
|
+
if (blankTimer) clearInterval(blankTimer);
|
|
154
|
+
if (blanked) force(DPMS_MODE_ON);
|
|
155
|
+
try {
|
|
156
|
+
x11.xtst?.XTestGrabControl(x11.dpy, 0);
|
|
157
|
+
x11.x11.XUngrabKeyboard(x11.dpy, CURRENT_TIME);
|
|
158
|
+
x11.x11.XUngrabPointer(x11.dpy, CURRENT_TIME);
|
|
159
|
+
x11.x11.XFlush(x11.dpy);
|
|
160
|
+
} catch { /* the server may already be gone; the grabs died with the connection */ }
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The *policy* over the quality ladder: how far a congested link may pull the bitrate below the
|
|
3
|
+
* rung the user chose, and when it moves. The ladder itself (labels and ratios) is in
|
|
4
|
+
* `src/shared/remote-desktop-quality.ts`, because the client renders it too.
|
|
5
|
+
*
|
|
6
|
+
* The shape of this is RustDesk's, and replacing what was here fixed a reported flicker — but
|
|
7
|
+
* not by the mechanism it first looked like, which is worth writing down because the wrong
|
|
8
|
+
* diagnosis is the plausible one.
|
|
9
|
+
*
|
|
10
|
+
* What the old ladder did was cap *resolution* per rung and hand the top rung a flat 25 Mbit/s
|
|
11
|
+
* at 1440p60. Over a wifi link that cannot carry that, the capture pipe backs up and **ffmpeg
|
|
12
|
+
* throttles itself**: measured at 4 Mbit/s, 22.9 of 60 fps arrived with inter-frame gaps to
|
|
13
|
+
* **181 ms** — bursts and stalls rather than a steady cadence, which at 3440x1440 is exactly
|
|
14
|
+
* what reads as continuous flicker. The same rung on an unthrottled link measured 60.0 fps at
|
|
15
|
+
* `gap p50=16.5 p95=22.8 max=35.7 ms`, so the rung was never the problem; asking for a rate the
|
|
16
|
+
* link could not carry was.
|
|
17
|
+
*
|
|
18
|
+
* The tempting explanation was the frame-dropping path — discarding one delta makes every later
|
|
19
|
+
* delta in its GOP undecodable, so dropping under congestion really would break the picture up
|
|
20
|
+
* continuously. It was **not** what happened: `congestionState` never fired in any of those
|
|
21
|
+
* runs, because `ws.getBufferedAmount()` is Bun's *userspace* queue and it drains into the
|
|
22
|
+
* kernel socket buffer, so it never reached `BACKPRESSURE_THRESHOLD_BYTES`. The keyframe rate
|
|
23
|
+
* gives it away — 0.75/s against a 30-frame GOP is ffmpeg encoding at 22 fps, not a server
|
|
24
|
+
* discarding two thirds of what it encoded.
|
|
25
|
+
*
|
|
26
|
+
* So the measured fix is the ladder: a rung now sets only a bitrate *ratio* against
|
|
27
|
+
* `base_bitrate(w, h)`, which for a 3440x1440 display makes the top rung 6 Mbit/s rather than
|
|
28
|
+
* 25, and the stream holds **30.0 fps with a 42 ms maximum gap over both a 5 Mbit/s and a
|
|
29
|
+
* 2 Mbit/s link**.
|
|
30
|
+
*
|
|
31
|
+
* What is here regardless is RustDesk's own rule, because the old code had a real defect even if
|
|
32
|
+
* it was not the one being reported: choosing a rung used to set `autoQuality = false`, which
|
|
33
|
+
* turned the adaptation off entirely and left frame-dropping as the only remaining response.
|
|
34
|
+
* `video_qos.rs` is explicit that the choice is a *maximum* — "user set image quality => update
|
|
35
|
+
* to the **maximum** ratio of the latest quality" — with the adaptation always running
|
|
36
|
+
* underneath it, and encoded frames never thrown away. It reduces the bitrate ratio first and
|
|
37
|
+
* only then the frame rate, because "bitrate-targeted encoders do not send fewer bytes at fewer
|
|
38
|
+
* frames".
|
|
39
|
+
*
|
|
40
|
+
* Two caveats on `nextRatioScale`, stated rather than implied. It nudges where RustDesk nudges
|
|
41
|
+
* every three seconds: ours cannot, because every bitrate change respawns ffmpeg for ~400 ms, so
|
|
42
|
+
* the same idea is applied in coarse steps behind `CHANGE_COOLDOWN_MS`. And it is **unit-tested
|
|
43
|
+
* only** — no end-to-end run has exercised it, because the detector it hangs off did not fire
|
|
44
|
+
* even at 0.4 Mbit/s with a relay applying real TCP backpressure. If congestion needs to be
|
|
45
|
+
* acted on in practice, the thing to fix first is the *signal*, not this policy.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
DEFAULT_FPS, DEFAULT_PRESET_ID, QUALITY_PRESETS, QUALITY_PRESET_ORDER, parsePresetId,
|
|
50
|
+
type QualityPreset, type QualityPresetId, type QualityRung,
|
|
51
|
+
} from "../../shared/remote-desktop-quality.ts";
|
|
52
|
+
|
|
53
|
+
/** Backpressure events within `DEGRADE_WINDOW_MS` that mean "this link is not keeping up".
|
|
54
|
+
* One event is a hiccup — a scroll, a video frame, a tunnel reconnect. */
|
|
55
|
+
export const DEGRADE_EVENT_THRESHOLD = 3;
|
|
56
|
+
export const DEGRADE_WINDOW_MS = 5_000;
|
|
57
|
+
/** Quiet stretch before climbing back toward the chosen rung. Long, because the cost of
|
|
58
|
+
* guessing wrong is a stall and then a second respawn to undo it. */
|
|
59
|
+
export const UPGRADE_QUIET_MS = 20_000;
|
|
60
|
+
/** Floor between two changes, whichever direction — bounds the respawn hitch to at worst one
|
|
61
|
+
* per this interval no matter how the link behaves. */
|
|
62
|
+
export const CHANGE_COOLDOWN_MS = 10_000;
|
|
63
|
+
|
|
64
|
+
/** Each reduction multiplies the ratio by this; each recovery divides by it. 0.6 is a third of
|
|
65
|
+
* the way down per step, so three steps reach the floor — few enough that a genuinely bad link
|
|
66
|
+
* settles inside a minute, coarse enough that each step is worth a respawn. */
|
|
67
|
+
export const CONGESTION_STEP = 0.6;
|
|
68
|
+
/** How far below the chosen rung congestion may go. At `balanced` (ratio 0.67) this bottoms out
|
|
69
|
+
* at 0.13, which `clampRatio` then lifts to RustDesk's own `BR_MIN` of 0.2. */
|
|
70
|
+
export const CONGESTION_MIN_SCALE = 0.2;
|
|
71
|
+
|
|
72
|
+
/** Bytes buffered on the socket above which the link is considered behind. */
|
|
73
|
+
export const BACKPRESSURE_THRESHOLD_BYTES = 512 * 1024;
|
|
74
|
+
/** How long it must *stay* above that before frames are discarded — see `congestionState`. */
|
|
75
|
+
export const BACKPRESSURE_SUSTAIN_MS = 250;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Whether the socket is genuinely congested, given how long it has been backed up.
|
|
79
|
+
*
|
|
80
|
+
* Reacting to the first over-threshold frame is strictly counterproductive, and measurably so:
|
|
81
|
+
* discarding one delta makes every later delta in that GOP undecodable, so the client freezes
|
|
82
|
+
* until the next keyframe. A **120 ms** spike bought a **502 ms** frozen picture that way. A
|
|
83
|
+
* spike that short is a client GC pause or a single wifi retransmit — it drains on its own, and
|
|
84
|
+
* riding it out costs only the few hundred KB that keep flowing meanwhile.
|
|
85
|
+
*
|
|
86
|
+
* `since` is the caller's previous value, threaded back in so this stays pure.
|
|
87
|
+
*/
|
|
88
|
+
export function congestionState(
|
|
89
|
+
since: number | null, buffered: number, now: number,
|
|
90
|
+
): { since: number | null; congested: boolean } {
|
|
91
|
+
if (buffered <= BACKPRESSURE_THRESHOLD_BYTES) return { since: null, congested: false };
|
|
92
|
+
const startedAt = since ?? now;
|
|
93
|
+
return { since: startedAt, congested: now - startedAt >= BACKPRESSURE_SUSTAIN_MS };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface AdaptiveState {
|
|
97
|
+
/** Timestamps of recent backpressure events, pruned to `DEGRADE_WINDOW_MS`. */
|
|
98
|
+
events: number[];
|
|
99
|
+
/** When the bitrate last changed, so a flapping link cannot respawn ffmpeg repeatedly. */
|
|
100
|
+
lastChangeAt: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function initialAdaptiveState(now: number): AdaptiveState {
|
|
104
|
+
return { events: [], lastChangeAt: now };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Record a backpressure event, dropping any older than the window. */
|
|
108
|
+
export function noteBackpressure(state: AdaptiveState, now: number): AdaptiveState {
|
|
109
|
+
const events = [...state.events, now].filter((t) => now - t <= DEGRADE_WINDOW_MS);
|
|
110
|
+
return { ...state, events };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The scale the effective bitrate ratio should be at, or null to stay put. Pure so the whole
|
|
115
|
+
* policy is testable without a socket: `current` is the scale in force (1 = the chosen rung's
|
|
116
|
+
* own ratio, nothing held back), `state` is the session's backpressure history.
|
|
117
|
+
*
|
|
118
|
+
* Never returns above 1: the rung the user picked is the ceiling, and a quiet link is not a
|
|
119
|
+
* reason to spend more than they asked for.
|
|
120
|
+
*/
|
|
121
|
+
export function nextRatioScale(
|
|
122
|
+
current: number, state: AdaptiveState, now: number,
|
|
123
|
+
): number | null {
|
|
124
|
+
if (now - state.lastChangeAt < CHANGE_COOLDOWN_MS) return null;
|
|
125
|
+
|
|
126
|
+
const recent = state.events.filter((t) => now - t <= DEGRADE_WINDOW_MS);
|
|
127
|
+
if (recent.length >= DEGRADE_EVENT_THRESHOLD) {
|
|
128
|
+
if (current <= CONGESTION_MIN_SCALE) return null; // already as low as this goes
|
|
129
|
+
return Math.max(CONGESTION_MIN_SCALE, current * CONGESTION_STEP);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const lastEvent = state.events.length > 0 ? Math.max(...state.events) : null;
|
|
133
|
+
const quietFor = lastEvent === null ? now - state.lastChangeAt : now - lastEvent;
|
|
134
|
+
if (quietFor >= UPGRADE_QUIET_MS && current < 1) {
|
|
135
|
+
return Math.min(1, current / CONGESTION_STEP);
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|