@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
|
@@ -5,10 +5,26 @@
|
|
|
5
5
|
* capture so two `gdigrab` processes never contend for the same display.
|
|
6
6
|
*/
|
|
7
7
|
import { startCapture, type CaptureHandle } from "./remote-desktop-capture.ts";
|
|
8
|
+
import { workingEncoders } from "../media-transcode/ffmpeg-capabilities.ts";
|
|
9
|
+
import { startAudioCapture, type AudioHandle } from "./remote-desktop-audio.ts";
|
|
10
|
+
import { engagePrivacy, type PrivacyHandle } from "./remote-desktop-privacy.ts";
|
|
11
|
+
import { listHostResolutions, setHostResolution } from "./remote-desktop-resolution.ts";
|
|
12
|
+
import {
|
|
13
|
+
clampCustomFps, clampCustomQualityPercent, customBitrateArg, ratioBitrateArg,
|
|
14
|
+
} from "../../shared/remote-desktop-custom-quality.ts";
|
|
8
15
|
import { avc1CodecString } from "./avc1-codec-string.ts";
|
|
9
16
|
import type { AccessUnit } from "./access-unit-assembler.ts";
|
|
10
|
-
import { injectPointer, injectKey, injectWheel, injectText, releaseAllModifiers, isInputAvailable } from "./remote-desktop-input.ts";
|
|
17
|
+
import { injectPointer, injectKey, injectWheel, injectText, releaseAllModifiers, isInputAvailable, releaseRemoteInput } from "./remote-desktop-input.ts";
|
|
11
18
|
import { resolveDisplay, type RemoteDisplay } from "./remote-desktop-displays.ts";
|
|
19
|
+
import {
|
|
20
|
+
MAX_CLIPBOARD_CHARS, pasteComboCodes, readHostClipboard, writeHostClipboard,
|
|
21
|
+
} from "./remote-desktop-clipboard.ts";
|
|
22
|
+
import {
|
|
23
|
+
BACKPRESSURE_THRESHOLD_BYTES, DEFAULT_FPS, DEFAULT_PRESET_ID, QUALITY_PRESETS,
|
|
24
|
+
congestionState, initialAdaptiveState, nextRatioScale, noteBackpressure, parsePresetId,
|
|
25
|
+
type AdaptiveState, type QualityPresetId,
|
|
26
|
+
type QualityPreset,
|
|
27
|
+
} from "./remote-desktop-quality.ts";
|
|
12
28
|
|
|
13
29
|
/** Minimal socket surface this module needs — matches Bun's `ServerWebSocket` shape closely
|
|
14
30
|
* enough to be faked in a unit test without a real connection. */
|
|
@@ -25,12 +41,17 @@ const HEARTBEAT_INTERVAL_MS = 5_000;
|
|
|
25
41
|
// is throttled while its tab is briefly backgrounded, without leaving an unattended session
|
|
26
42
|
// (ffmpeg + input access) live for long. The client also re-pings the moment its tab is visible.
|
|
27
43
|
const HEARTBEAT_TIMEOUT_MS = 30_000;
|
|
28
|
-
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
|
|
44
|
+
/** Sustained backpressure drops delta AUs until the next keyframe rather than queueing
|
|
45
|
+
* forever — a slow WAN/tunnel link must degrade to "waits for a keyframe", never to unbounded
|
|
46
|
+
* memory growth or an ever-growing latency queue. Both thresholds and the "has it lasted long
|
|
47
|
+
* enough to be real" rule live in `remote-desktop-quality.ts` (`congestionState`). */
|
|
32
48
|
/** Longest `text` message injected in one go — a paste, not a file; anything bigger is dropped. */
|
|
33
49
|
const MAX_TEXT_CHARS = 1024;
|
|
50
|
+
/** Binary frame kinds. Video keeps 0/1 (delta/key) so the existing client framing is unchanged;
|
|
51
|
+
* audio is a third value rather than a second socket, which would need its own auth. */
|
|
52
|
+
const FRAME_VIDEO_DELTA = 0;
|
|
53
|
+
const FRAME_VIDEO_KEY = 1;
|
|
54
|
+
const FRAME_AUDIO = 2;
|
|
34
55
|
|
|
35
56
|
export class RemoteDesktopSession {
|
|
36
57
|
private capture: CaptureHandle | null = null;
|
|
@@ -40,24 +61,121 @@ export class RemoteDesktopSession {
|
|
|
40
61
|
private sentConfig = false;
|
|
41
62
|
private closed = false;
|
|
42
63
|
private readonly heldKeyCodes = new Set<string>();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
64
|
+
/** Bumped on every (re)start of capture. An ffmpeg that has been superseded by a quality
|
|
65
|
+
* change still emits buffered access units and *always* fires `onExit`; without this both
|
|
66
|
+
* would be attributed to the live capture — the exit would tear the whole session down and
|
|
67
|
+
* the stale frames would be fed to a decoder already reconfigured for another resolution. */
|
|
68
|
+
private captureGeneration = 0;
|
|
69
|
+
/** Resolves when the *current* ffmpeg has exited. Replaced per start, so the eviction wait in
|
|
70
|
+
* `createRemoteDesktopSession` always tracks the process that is actually running rather
|
|
71
|
+
* than one that exited during some earlier quality change. */
|
|
72
|
+
private captureExited: Promise<void> = Promise.resolve();
|
|
73
|
+
private presetId: QualityPresetId = DEFAULT_PRESET_ID;
|
|
74
|
+
/** How much of the chosen rung's ratio is actually in force: 1 is all of it, less means a
|
|
75
|
+
* congested link has pulled the bitrate down *underneath* the user's choice. RustDesk treats
|
|
76
|
+
* a chosen image quality as the maximum ratio and keeps adapting below it; the old code made
|
|
77
|
+
* the choice a freeze instead, which left a link that could not carry the rung discarding
|
|
78
|
+
* delta frames forever — the reported continuous flicker. Never above 1. */
|
|
79
|
+
private ratioScale = 1;
|
|
80
|
+
private adaptive: AdaptiveState = initialAdaptiveState(Date.now());
|
|
81
|
+
/** Guards against a client spamming quality changes into overlapping respawns. */
|
|
82
|
+
private switchingPreset = false;
|
|
83
|
+
/** When the socket's buffer first went over threshold and stayed there, or null while it is
|
|
84
|
+
* draining normally. See `BACKPRESSURE_SUSTAIN_MS`. */
|
|
85
|
+
private backpressureSince: number | null = null;
|
|
86
|
+
/** Last text known to be on *both* clipboards. Without it the two directions feed each other:
|
|
87
|
+
* the client pastes T, the host clipboard becomes T, the next `clipboardRead` reads T back and
|
|
88
|
+
* pushes it to the client, whose own clipboard write re-arms the loop. */
|
|
89
|
+
private lastClipboardText: string | null = null;
|
|
90
|
+
/** Whether the host pointer is drawn into the frames. Capture-time on every grabber, so a
|
|
91
|
+
* change is a respawn — see `restartCapture`. */
|
|
92
|
+
private drawMouse: boolean;
|
|
93
|
+
/** H.264 encoder this session asked for, or null to use the capability probe's first choice.
|
|
94
|
+
* Like the cursor, it is fixed for the life of an ffmpeg, so a change is a respawn. */
|
|
95
|
+
private encoder: string | null;
|
|
96
|
+
/** Host audio, off until the client asks — capture costs a second ffmpeg and most sessions
|
|
97
|
+
* are a screen share, not a video. */
|
|
98
|
+
private audio: AudioHandle | null = null;
|
|
99
|
+
/** Guards a client toggling audio faster than ffmpeg starts. */
|
|
100
|
+
private switchingAudio = false;
|
|
101
|
+
/** Local input blocked + host monitor blanked, while engaged. Held on the shared X
|
|
102
|
+
* connection, so it also dies with the process — see `remote-desktop-privacy.ts`. */
|
|
103
|
+
private privacy: PrivacyHandle | null = null;
|
|
104
|
+
/** Guards a client clicking through the resolution list faster than X can switch. */
|
|
105
|
+
private switchingResolution = false;
|
|
106
|
+
/** RustDesk's custom rung, or null while a named preset is in force. Its bitrate is derived
|
|
107
|
+
* from the *capture* size rather than stored, because the host can change resolution
|
|
108
|
+
* mid-session and a bitrate computed for the old size would then be wrong. */
|
|
109
|
+
private custom: { percent: number; fps: number } | null = null;
|
|
110
|
+
/** The mode that was live when this session first changed it, so teardown can put it back.
|
|
111
|
+
* Set once and never overwritten: after two switches the *first* value is the user's own. */
|
|
112
|
+
private originalModeId: string | null = null;
|
|
46
113
|
|
|
47
|
-
constructor(
|
|
48
|
-
|
|
114
|
+
constructor(
|
|
115
|
+
private readonly ws: RemoteDesktopSocket,
|
|
116
|
+
private readonly display: RemoteDisplay | null,
|
|
117
|
+
drawMouse = true,
|
|
118
|
+
encoder: string | null = null,
|
|
119
|
+
) {
|
|
120
|
+
this.drawMouse = drawMouse;
|
|
121
|
+
this.encoder = encoder;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Resolves once the underlying ffmpeg process has actually exited (not merely asked to). */
|
|
125
|
+
get exited(): Promise<void> {
|
|
126
|
+
return this.captureExited;
|
|
49
127
|
}
|
|
50
128
|
|
|
51
129
|
async start(): Promise<void> {
|
|
130
|
+
await this.beginCapture();
|
|
131
|
+
this.heartbeatTimer = setInterval(() => this.onHeartbeatTick(), HEARTBEAT_INTERVAL_MS);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Spawn ffmpeg for the current preset. Also used by `applyPreset`, which is why the config
|
|
135
|
+
* and backpressure flags are reset here: the new process emits its own SPS, and the client
|
|
136
|
+
* has to be told to reconfigure its decoder before the first frame of it arrives. */
|
|
137
|
+
/** The rung ffmpeg is actually given. For `custom` this keeps the *display's own* height —
|
|
138
|
+
* RustDesk's image quality never downscales, and `height` is a ceiling clamped to the
|
|
139
|
+
* source, so passing the display height means "no scaling" without a special case in the
|
|
140
|
+
* filter builder. */
|
|
141
|
+
/** The fps and bitrate ffmpeg is actually given. A rung carries only a *ratio*, so the real
|
|
142
|
+
* bitrate cannot exist until the capture size does — it is `base_bitrate(w, h)` for this
|
|
143
|
+
* display times the ratio, exactly as RustDesk computes it. The resolution is never part of
|
|
144
|
+
* this: every rung streams the host's own size. */
|
|
145
|
+
private effectivePreset(): QualityPreset {
|
|
146
|
+
const width = this.display?.width ?? 1920;
|
|
147
|
+
const height = this.display?.height ?? 1080;
|
|
148
|
+
if (this.custom) {
|
|
149
|
+
return {
|
|
150
|
+
fps: this.custom.fps,
|
|
151
|
+
bitrate: customBitrateArg(this.custom.percent, width, height),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// `ratioScale` is what congestion has held back; the rung's own ratio is the ceiling.
|
|
155
|
+
const ratio = QUALITY_PRESETS[this.presetId].ratio * this.ratioScale;
|
|
156
|
+
return { fps: DEFAULT_FPS, bitrate: ratioBitrateArg(ratio, width, height) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async beginCapture(): Promise<void> {
|
|
160
|
+
const generation = ++this.captureGeneration;
|
|
161
|
+
let resolveExit!: () => void;
|
|
162
|
+
this.captureExited = new Promise<void>((resolve) => { resolveExit = resolve; });
|
|
163
|
+
this.sentConfig = false;
|
|
164
|
+
this.droppingUntilKey = false;
|
|
52
165
|
this.capture = await startCapture({
|
|
53
166
|
display: this.display,
|
|
54
|
-
|
|
167
|
+
preset: this.effectivePreset(),
|
|
168
|
+
drawMouse: this.drawMouse,
|
|
169
|
+
...(this.encoder ? { encoder: this.encoder } : {}),
|
|
170
|
+
onAccessUnit: (au) => this.handleAccessUnit(au, generation),
|
|
55
171
|
// `reason` is only set when ffmpeg died on its own (crash, access denied, etc) — tell
|
|
56
172
|
// the client *why* before closing instead of leaving it to guess from a bare
|
|
57
173
|
// disconnect (this is exactly what happens today for e.g. gdigrab failing against a
|
|
58
174
|
// disconnected Windows session: "Failed to capture image (error 5)").
|
|
59
175
|
onExit: (_code, reason) => {
|
|
60
|
-
|
|
176
|
+
resolveExit();
|
|
177
|
+
// A process we deliberately replaced: its exit says nothing about the session.
|
|
178
|
+
if (generation !== this.captureGeneration) return;
|
|
61
179
|
if (!this.closed) {
|
|
62
180
|
if (reason) {
|
|
63
181
|
try { this.ws.send(JSON.stringify({ type: "error", message: `Capture failed: ${reason}` })); } catch { /* closing anyway */ }
|
|
@@ -66,7 +184,198 @@ export class RemoteDesktopSession {
|
|
|
66
184
|
}
|
|
67
185
|
},
|
|
68
186
|
});
|
|
69
|
-
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Apply a change that ffmpeg can only take at startup: stop the old process, wait for it to
|
|
190
|
+
* actually go, then start a new one. `mutate` is what the *next* spawn should differ by —
|
|
191
|
+
* it runs after the guards so a rejected restart never leaves the session claiming a setting
|
|
192
|
+
* its ffmpeg is not using. Returns false when there was nothing to restart.
|
|
193
|
+
*
|
|
194
|
+
* Sequential on purpose — two grabbers on one display contend (see the note on
|
|
195
|
+
* `createRemoteDesktopSession`), so the ~400ms gap is accepted rather than overlapped. */
|
|
196
|
+
private async restartCapture(mutate: () => void): Promise<boolean> {
|
|
197
|
+
if (this.closed || this.switchingPreset) return false;
|
|
198
|
+
this.switchingPreset = true;
|
|
199
|
+
try {
|
|
200
|
+
mutate();
|
|
201
|
+
// Invalidate the outgoing process BEFORE killing it. `beginCapture` bumps the generation
|
|
202
|
+
// too, but that happens after the old ffmpeg has already exited — so without this bump
|
|
203
|
+
// its `onExit` still matches the current generation, reads as "capture died on its own"
|
|
204
|
+
// and tears down the whole session on every quality change.
|
|
205
|
+
this.captureGeneration++;
|
|
206
|
+
const previous = this.captureExited;
|
|
207
|
+
this.capture?.stop();
|
|
208
|
+
await Promise.race([previous, Bun.sleep(2000)]);
|
|
209
|
+
if (this.closed) return false;
|
|
210
|
+
await this.beginCapture();
|
|
211
|
+
return true;
|
|
212
|
+
} finally {
|
|
213
|
+
this.switchingPreset = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Switch rung. The client is told the new rung only once the new ffmpeg is up, because that
|
|
218
|
+
* message is also what makes its decoder reconfigure. */
|
|
219
|
+
private async applyPreset(next: QualityPresetId): Promise<void> {
|
|
220
|
+
if (next === this.presetId && this.ratioScale === 1) return;
|
|
221
|
+
const restarted = await this.restartCapture(() => {
|
|
222
|
+
this.presetId = next;
|
|
223
|
+
// Picking a rung is an explicit statement about bandwidth, so it also clears whatever
|
|
224
|
+
// congestion had held back — otherwise choosing "Good image quality" on a link that has
|
|
225
|
+
// been struggling would apply a third of it and look like the menu had not worked.
|
|
226
|
+
this.ratioScale = 1;
|
|
227
|
+
this.adaptive = initialAdaptiveState(Date.now());
|
|
228
|
+
});
|
|
229
|
+
if (restarted) this.sendQuality();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Apply what congestion has decided, keeping the rung the user chose. This is the whole
|
|
233
|
+
* difference from the old behaviour: the stream gets cheaper, the choice stays put, and no
|
|
234
|
+
* encoded frame is thrown away. */
|
|
235
|
+
private async applyRatioScale(scale: number): Promise<void> {
|
|
236
|
+
if (scale === this.ratioScale) return;
|
|
237
|
+
const restarted = await this.restartCapture(() => {
|
|
238
|
+
this.ratioScale = scale;
|
|
239
|
+
this.adaptive = initialAdaptiveState(Date.now());
|
|
240
|
+
});
|
|
241
|
+
if (restarted) this.sendQuality();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Switch the host's own display mode, then respawn the grabber onto the new geometry.
|
|
245
|
+
*
|
|
246
|
+
* The respawn is not optional: `-video_size`/`-i :0.0+X,Y` are baked at spawn, so an ffmpeg
|
|
247
|
+
* left running across a mode change keeps grabbing a rectangle that no longer matches the
|
|
248
|
+
* framebuffer — which reads as a frozen or torn picture rather than an error. The display
|
|
249
|
+
* list is re-read for the same reason, since the crop this session uses comes from it. */
|
|
250
|
+
private async applyResolution(modeId: string): Promise<void> {
|
|
251
|
+
if (this.closed || this.switchingResolution) return;
|
|
252
|
+
this.switchingResolution = true;
|
|
253
|
+
try {
|
|
254
|
+
// Remember where the host started *before* the first change, not after.
|
|
255
|
+
if (this.originalModeId === null) {
|
|
256
|
+
const current = (await listHostResolutions()).modes.find((m) => m.current);
|
|
257
|
+
if (current) this.originalModeId = current.id;
|
|
258
|
+
}
|
|
259
|
+
const result = await setHostResolution(modeId);
|
|
260
|
+
if (!result.ok) {
|
|
261
|
+
this.send({ type: "resolutionError", message: result.error ?? "The host refused the mode." });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
await this.restartCapture(() => { /* geometry is read fresh by `beginCapture` */ });
|
|
265
|
+
// Read back rather than echoing `result`: the picker must tick what the host is really
|
|
266
|
+
// doing, and X is free to have landed on something else.
|
|
267
|
+
await this.sendResolution();
|
|
268
|
+
} finally {
|
|
269
|
+
this.switchingResolution = false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Engage or release privacy mode. Always answers with the state that actually holds: a
|
|
274
|
+
* failed engage (another client already holds a grab — a screen locker, an open menu) must
|
|
275
|
+
* report "off" rather than leave a switch claiming the host is locked out when it is not. */
|
|
276
|
+
private async setPrivacy(enabled: boolean): Promise<void> {
|
|
277
|
+
if (this.closed) return;
|
|
278
|
+
if (!enabled) {
|
|
279
|
+
this.privacy?.release();
|
|
280
|
+
this.privacy = null;
|
|
281
|
+
this.send({ type: "privacy", enabled: false, blanked: false });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (this.privacy) return;
|
|
285
|
+
const handle = await engagePrivacy();
|
|
286
|
+
// Engaging and then immediately closing would leave the grab held with nobody to release
|
|
287
|
+
// it — the one sequence that could lock the local user out for real.
|
|
288
|
+
if (this.closed) { handle?.release(); return; }
|
|
289
|
+
this.privacy = handle;
|
|
290
|
+
this.send({
|
|
291
|
+
type: "privacy",
|
|
292
|
+
enabled: handle !== null,
|
|
293
|
+
blanked: handle?.blanked === true,
|
|
294
|
+
...(handle ? {} : { reason: "Another program already holds the keyboard (a screen lock, or an open menu)." }),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Start or stop the audio ffmpeg. Independent of the video capture on purpose: audio must
|
|
299
|
+
* survive a quality change (which respawns the video encoder), and a host with no loopback
|
|
300
|
+
* device must not fail to share its screen. */
|
|
301
|
+
private async setAudio(enabled: boolean): Promise<void> {
|
|
302
|
+
if (this.closed || this.switchingAudio || enabled === (this.audio !== null)) return;
|
|
303
|
+
this.switchingAudio = true;
|
|
304
|
+
try {
|
|
305
|
+
if (!enabled) {
|
|
306
|
+
this.audio?.stop();
|
|
307
|
+
this.audio = null;
|
|
308
|
+
this.send({ type: "audio", enabled: false });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const handle = await startAudioCapture({
|
|
312
|
+
onPacket: (packet) => this.handleAudioPacket(packet),
|
|
313
|
+
onStreamInfo: (info) => this.send({ type: "audio", enabled: true, ...info }),
|
|
314
|
+
// Audio dying is not a session failure — the picture keeps going and the client is
|
|
315
|
+
// told the switch is back off, unlike the video capture whose exit closes everything.
|
|
316
|
+
onExit: () => {
|
|
317
|
+
if (this.audio === null || this.closed) return;
|
|
318
|
+
this.audio = null;
|
|
319
|
+
this.send({ type: "audio", enabled: false });
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
// A host with no loopback device: say so rather than leaving the toggle looking on.
|
|
323
|
+
if (!handle || this.closed) { handle?.stop(); this.send({ type: "audio", enabled: false }); return; }
|
|
324
|
+
this.audio = handle;
|
|
325
|
+
} finally {
|
|
326
|
+
this.switchingAudio = false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Audio is dropped under congestion rather than queued, and never triggers the video's
|
|
331
|
+
* drop-until-keyframe path: Opus packets are independent, so a gap is a short click that
|
|
332
|
+
* heals itself, where a dropped H.264 delta freezes the picture until the next keyframe. */
|
|
333
|
+
private handleAudioPacket(packet: Uint8Array): void {
|
|
334
|
+
if (this.closed || this.audio === null) return;
|
|
335
|
+
if ((this.ws.getBufferedAmount?.() ?? 0) > BACKPRESSURE_THRESHOLD_BYTES) return;
|
|
336
|
+
const framed = new Uint8Array(1 + packet.length);
|
|
337
|
+
framed[0] = FRAME_AUDIO;
|
|
338
|
+
framed.set(packet, 1);
|
|
339
|
+
try { this.ws.send(framed); } catch { /* socket going away */ }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private send(msg: Record<string, unknown>): void {
|
|
343
|
+
try { this.ws.send(JSON.stringify(msg)); } catch { /* socket going away */ }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private sendCodec(): void {
|
|
347
|
+
try {
|
|
348
|
+
this.ws.send(JSON.stringify({ type: "codec", encoder: this.encoder }));
|
|
349
|
+
} catch { /* socket going away */ }
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** The mode that is live now, so the picker ticks what the host is really doing rather than
|
|
353
|
+
* what was asked for — a refused switch must not leave the menu lying. */
|
|
354
|
+
private async sendResolution(): Promise<void> {
|
|
355
|
+
const { output, modes } = await listHostResolutions();
|
|
356
|
+
const current = modes.find((m) => m.current);
|
|
357
|
+
try {
|
|
358
|
+
this.ws.send(JSON.stringify({
|
|
359
|
+
type: "resolution", output, modeId: current?.id ?? null,
|
|
360
|
+
width: current?.width ?? 0, height: current?.height ?? 0,
|
|
361
|
+
// RustDesk's `isOriginalResolutionSet`: what to offer as "Original". Null until this
|
|
362
|
+
// session has actually changed something, so a session that never did offers nothing.
|
|
363
|
+
original: this.originalModeId,
|
|
364
|
+
}));
|
|
365
|
+
} catch { /* socket going away */ }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private sendQuality(): void {
|
|
369
|
+
try {
|
|
370
|
+
this.ws.send(JSON.stringify({
|
|
371
|
+
type: "quality", preset: this.presetId,
|
|
372
|
+
// Not "which rung am I on" — that is `preset`, and it is whatever the user chose. This
|
|
373
|
+
// is how much of it the link is currently allowing, so the picker can show that the
|
|
374
|
+
// choice stands while the stream is temporarily cheaper.
|
|
375
|
+
held: this.ratioScale < 1 ? Number(this.ratioScale.toFixed(2)) : undefined,
|
|
376
|
+
...(this.custom ? { custom: this.custom } : {}),
|
|
377
|
+
}));
|
|
378
|
+
} catch { /* socket going away; the next message will fail the same way */ }
|
|
70
379
|
}
|
|
71
380
|
|
|
72
381
|
noteClientAlive(): void {
|
|
@@ -78,6 +387,110 @@ export class RemoteDesktopSession {
|
|
|
78
387
|
try { msg = JSON.parse(raw); } catch { return; }
|
|
79
388
|
if (msg.type === "ping") { this.noteClientAlive(); return; }
|
|
80
389
|
if (msg.type === "stop") { this.close(); return; }
|
|
390
|
+
if (msg.type === "quality") {
|
|
391
|
+
// RustDesk's custom rung: a bitrate percentage and an fps, both re-clamped here because
|
|
392
|
+
// they arrive off the wire from a device whose localStorage the user can edit. `More` is
|
|
393
|
+
// not sent — the ceiling it unlocks is the wire maximum, and the *client* is what decides
|
|
394
|
+
// whether to offer it, so the host allows the full range rather than second-guessing.
|
|
395
|
+
if (msg.preset === "custom") {
|
|
396
|
+
const { percent, fps } = msg as { percent?: unknown; fps?: unknown };
|
|
397
|
+
const next = {
|
|
398
|
+
percent: clampCustomQualityPercent(percent, true),
|
|
399
|
+
fps: clampCustomFps(fps),
|
|
400
|
+
};
|
|
401
|
+
if (this.custom && this.custom.percent === next.percent && this.custom.fps === next.fps) return;
|
|
402
|
+
const restarted = await this.restartCapture(() => {
|
|
403
|
+
this.custom = next;
|
|
404
|
+
// Typed numbers replace whatever congestion had held back, the same as a named rung.
|
|
405
|
+
this.ratioScale = 1;
|
|
406
|
+
this.adaptive = initialAdaptiveState(Date.now());
|
|
407
|
+
});
|
|
408
|
+
if (restarted) this.sendQuality();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
// Anything that is not a rung id is garbage off the wire, not a request for `auto`: there
|
|
412
|
+
// is no `auto` any more, because the adaptation always runs (see `considerRatioScale`).
|
|
413
|
+
const chosen = parsePresetId(msg.preset);
|
|
414
|
+
if (!chosen) return;
|
|
415
|
+
// Leaving custom has to clear it, or the named rung it switches to would keep the custom
|
|
416
|
+
// bitrate and fps and only look like it changed.
|
|
417
|
+
if (this.custom) {
|
|
418
|
+
const leaving = this.custom;
|
|
419
|
+
this.custom = null;
|
|
420
|
+
if (chosen === this.presetId) {
|
|
421
|
+
// Same rung id as before custom: `applyPreset` would early-return and the capture
|
|
422
|
+
// would keep running with `leaving`'s numbers.
|
|
423
|
+
const restarted = await this.restartCapture(() => {
|
|
424
|
+
this.ratioScale = 1;
|
|
425
|
+
this.adaptive = initialAdaptiveState(Date.now());
|
|
426
|
+
});
|
|
427
|
+
if (restarted) this.sendQuality();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
await this.applyPreset(chosen);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (msg.type === "privacy") {
|
|
435
|
+
const { enabled } = msg as { enabled?: unknown };
|
|
436
|
+
if (typeof enabled !== "boolean") return;
|
|
437
|
+
await this.setPrivacy(enabled);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (msg.type === "audio") {
|
|
441
|
+
const { enabled } = msg as { enabled?: unknown };
|
|
442
|
+
if (typeof enabled !== "boolean") return;
|
|
443
|
+
await this.setAudio(enabled);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (msg.type === "codec") {
|
|
447
|
+
// Checked against the probe rather than a hardcoded list: naming an encoder this build
|
|
448
|
+
// or GPU cannot run makes ffmpeg exit immediately, which would surface to the user as
|
|
449
|
+
// "Capture failed" on a menu item the UI itself offered.
|
|
450
|
+
const { encoder } = msg as { encoder?: unknown };
|
|
451
|
+
if (typeof encoder !== "string" || encoder === this.encoder) return;
|
|
452
|
+
if (!(await workingEncoders()).includes(encoder)) return;
|
|
453
|
+
const restarted = await this.restartCapture(() => { this.encoder = encoder; });
|
|
454
|
+
if (restarted) this.sendCodec();
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (msg.type === "resolution") {
|
|
458
|
+
const { modeId } = msg as { modeId?: unknown };
|
|
459
|
+
if (typeof modeId !== "string" || modeId.length === 0) return;
|
|
460
|
+
await this.applyResolution(modeId);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (msg.type === "cursor") {
|
|
464
|
+
// The pointer is burned into the frames by the grabber, so this is a respawn rather than
|
|
465
|
+
// an overlay the client could draw or hide by itself. Not echoed back: unlike `quality`
|
|
466
|
+
// (which `auto` lets the *session* change), nothing here moves it but the client.
|
|
467
|
+
const { show } = msg as { show?: unknown };
|
|
468
|
+
if (typeof show !== "boolean" || show === this.drawMouse) return;
|
|
469
|
+
await this.restartCapture(() => { this.drawMouse = show; });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
// Both clipboard directions sit *above* the input gate: moving text is not input
|
|
473
|
+
// injection, so it keeps working on a view-only host (only the paste keystroke needs it).
|
|
474
|
+
if (msg.type === "clipboard") {
|
|
475
|
+
const { text, paste, shift } = msg as { text?: unknown; paste?: unknown; shift?: unknown };
|
|
476
|
+
if (typeof text !== "string" || text.length === 0 || text.length > MAX_CLIPBOARD_CHARS) return;
|
|
477
|
+
const wrote = await writeHostClipboard(text);
|
|
478
|
+
if (wrote) this.lastClipboardText = text;
|
|
479
|
+
if (paste !== true || !isInputAvailable()) return;
|
|
480
|
+
// The client swallows its own Ctrl+V (it has to: `preventDefault` on that keydown is what
|
|
481
|
+
// cancels the browser's `paste` event, so the text would never arrive) — so the paste
|
|
482
|
+
// keystroke is ours to deliver, and only after the write has actually landed.
|
|
483
|
+
if (wrote) await this.injectPasteCombo(shift === true);
|
|
484
|
+
// No clipboard tool on the host: type the text instead of dropping the paste on the
|
|
485
|
+
// floor. Slower and subject to the host's IME, but a paste that arrives beats silence —
|
|
486
|
+
// and `/capabilities` already told the client to offer the install command.
|
|
487
|
+
else if (text.length <= MAX_TEXT_CHARS) await injectText(text);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (msg.type === "clipboardRead") {
|
|
491
|
+
await this.sendHostClipboard();
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
81
494
|
if (!isInputAvailable()) return; // part 1b unsupported on this OS — silently ignore
|
|
82
495
|
|
|
83
496
|
if (msg.type === "pointer") {
|
|
@@ -113,6 +526,28 @@ export class RemoteDesktopSession {
|
|
|
113
526
|
}
|
|
114
527
|
}
|
|
115
528
|
|
|
529
|
+
/** Read the host clipboard and push it to the client, skipping text the client already has.
|
|
530
|
+
* Silent when the host has no clipboard tool — `clipboardAvailable` is reported up front in
|
|
531
|
+
* `/capabilities`, so the UI explains it once instead of per keystroke. */
|
|
532
|
+
private async sendHostClipboard(): Promise<void> {
|
|
533
|
+
const text = await readHostClipboard();
|
|
534
|
+
if (this.closed || text === null || text === this.lastClipboardText) return;
|
|
535
|
+
this.lastClipboardText = text;
|
|
536
|
+
try { this.ws.send(JSON.stringify({ type: "clipboard", text })); } catch { /* socket going away */ }
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Press the host's paste shortcut. `releaseHeldKeys` first because the client *did* forward
|
|
540
|
+
* the modifier keydown before swallowing the V (keydown order is Control, then Shift, then V,
|
|
541
|
+
* then `paste`), and a stale Ctrl/Shift held from that would turn this into another shortcut.
|
|
542
|
+
*
|
|
543
|
+
* `shift` mirrors what the user actually pressed — see `pasteComboCodes`. */
|
|
544
|
+
private async injectPasteCombo(shift: boolean): Promise<void> {
|
|
545
|
+
const combo = pasteComboCodes(process.platform, shift);
|
|
546
|
+
await this.releaseHeldKeys();
|
|
547
|
+
for (const code of combo) await injectKey(code, true);
|
|
548
|
+
for (const code of [...combo].reverse()) await injectKey(code, false);
|
|
549
|
+
}
|
|
550
|
+
|
|
116
551
|
/** Force a keyup for every key this session has tracked as held, then run the modifier
|
|
117
552
|
* backstop too — `heldKeyCodes` covers whatever the client actually pressed (letters,
|
|
118
553
|
* digits, etc, not just modifiers); `releaseAllModifiers()` is a second backstop for a
|
|
@@ -131,41 +566,88 @@ export class RemoteDesktopSession {
|
|
|
131
566
|
this.closed = true;
|
|
132
567
|
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
133
568
|
this.capture?.stop();
|
|
569
|
+
this.audio?.stop();
|
|
570
|
+
this.audio = null;
|
|
571
|
+
// Before anything that could throw: a held grab is the only piece of state here that
|
|
572
|
+
// degrades the *host* rather than the session.
|
|
573
|
+
this.privacy?.release();
|
|
574
|
+
this.privacy = null;
|
|
575
|
+
// Fire-and-forget, like `releaseHeldKeys` below: `close()` is synchronous (it runs from the
|
|
576
|
+
// socket's close handler) and this is an async X round trip. Restoring is not optional —
|
|
577
|
+
// leaving the host at 1280x720 after the remote user disconnects means its owner comes back
|
|
578
|
+
// to a desktop with every window crammed into a corner, and no way to know why.
|
|
579
|
+
if (this.originalModeId !== null) {
|
|
580
|
+
const restore = this.originalModeId;
|
|
581
|
+
this.originalModeId = null;
|
|
582
|
+
setHostResolution(restore).catch(() => {});
|
|
583
|
+
}
|
|
134
584
|
activeSessions.delete(this);
|
|
585
|
+
// The uinput virtual devices are per *process*, not per session, so a Wayland host was left
|
|
586
|
+
// with a "PPM Remote Keyboard" and a "PPM Remote Pointer" in its device list — visible in
|
|
587
|
+
// system settings, and to anything that enumerates input devices — for the life of the
|
|
588
|
+
// server after one connection. Released with the last session; the next one recreates them.
|
|
589
|
+
if (activeSessions.size === 0) releaseRemoteInput();
|
|
135
590
|
if (this.heldKeyCodes.size > 0) {
|
|
136
591
|
this.releaseHeldKeys().catch(() => {});
|
|
137
592
|
}
|
|
138
593
|
try { this.ws.close(); } catch { /* already closing */ }
|
|
139
594
|
}
|
|
140
595
|
|
|
141
|
-
|
|
596
|
+
/** The heartbeat tick is also where an *upgrade* is considered: a quiet link produces no
|
|
597
|
+
* events at all, so there is nothing else to hang "things have been fine for a while" on. */
|
|
598
|
+
private onHeartbeatTick(): void {
|
|
142
599
|
if (Date.now() - this.lastPingAt > HEARTBEAT_TIMEOUT_MS) {
|
|
143
600
|
console.warn("[remote-desktop] heartbeat timeout — tearing down session");
|
|
144
601
|
this.close();
|
|
602
|
+
return;
|
|
145
603
|
}
|
|
604
|
+
this.considerRatioScale();
|
|
146
605
|
}
|
|
147
606
|
|
|
148
|
-
|
|
607
|
+
/** Runs whatever the user picked — a named rung is a ceiling, not an off switch. */
|
|
608
|
+
private considerRatioScale(): void {
|
|
609
|
+
if (this.closed || this.switchingPreset) return;
|
|
610
|
+
// The custom dialog's numbers are exempt: the user typed a bitrate, and quietly encoding at
|
|
611
|
+
// a third of it would make the dialog lie about what the host is doing.
|
|
612
|
+
if (this.custom) return;
|
|
613
|
+
const next = nextRatioScale(this.ratioScale, this.adaptive, Date.now());
|
|
614
|
+
if (next !== null) void this.applyRatioScale(next);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private handleAccessUnit(au: AccessUnit, generation: number): void {
|
|
149
618
|
if (this.closed) return;
|
|
619
|
+
// Frames still draining out of an ffmpeg we replaced: the client's decoder has already been
|
|
620
|
+
// reconfigured for the new rung, so these would decode as corruption at best.
|
|
621
|
+
if (generation !== this.captureGeneration) return;
|
|
150
622
|
if (!this.sentConfig) {
|
|
151
623
|
const sps = this.capture?.cachedSps();
|
|
152
624
|
if (!sps) return; // nothing decodable yet — wait for the encoder's first SPS
|
|
153
625
|
const codec = avc1CodecString(sps);
|
|
154
626
|
if (!codec) return;
|
|
155
|
-
this.ws.send(JSON.stringify({
|
|
627
|
+
this.ws.send(JSON.stringify({
|
|
628
|
+
type: "config", codec, preset: this.presetId,
|
|
629
|
+
encoder: this.encoder,
|
|
630
|
+
}));
|
|
156
631
|
this.sentConfig = true;
|
|
157
632
|
}
|
|
158
633
|
|
|
159
|
-
|
|
160
|
-
|
|
634
|
+
// A backlog has to persist before it costs the client a resync — see `congestionState`.
|
|
635
|
+
const now = Date.now();
|
|
636
|
+
const congestion = congestionState(this.backpressureSince, this.ws.getBufferedAmount?.() ?? 0, now);
|
|
637
|
+
this.backpressureSince = congestion.since;
|
|
638
|
+
if (congestion.congested && !au.isKey) {
|
|
161
639
|
this.droppingUntilKey = true;
|
|
640
|
+
// Only sustained congestion feeds the ladder too: degrading a rung respawns ffmpeg for
|
|
641
|
+
// ~400ms, which is a worse hiccup than the one a spike would have caused.
|
|
642
|
+
this.adaptive = noteBackpressure(this.adaptive, now);
|
|
643
|
+
this.considerRatioScale();
|
|
162
644
|
return;
|
|
163
645
|
}
|
|
164
646
|
if (this.droppingUntilKey && !au.isKey) return;
|
|
165
647
|
if (au.isKey) this.droppingUntilKey = false;
|
|
166
648
|
|
|
167
649
|
const framed = new Uint8Array(1 + au.bytes.length);
|
|
168
|
-
framed[0] = au.isKey ?
|
|
650
|
+
framed[0] = au.isKey ? FRAME_VIDEO_KEY : FRAME_VIDEO_DELTA;
|
|
169
651
|
framed.set(au.bytes, 1);
|
|
170
652
|
this.ws.send(framed);
|
|
171
653
|
}
|
|
@@ -173,14 +655,34 @@ export class RemoteDesktopSession {
|
|
|
173
655
|
|
|
174
656
|
const activeSessions = new Set<RemoteDesktopSession>();
|
|
175
657
|
|
|
658
|
+
export interface CreateRemoteDesktopSessionOptions {
|
|
659
|
+
/** One of `/capabilities`' `displays[].id`; absent/unknown = the host's primary. */
|
|
660
|
+
displayId?: string;
|
|
661
|
+
/** Draw the host pointer from the very first frame. Taken at create time rather than left to
|
|
662
|
+
* a `cursor` message so a client that keeps the pointer off does not pay for a respawn
|
|
663
|
+
* immediately after connecting. */
|
|
664
|
+
showCursor?: boolean;
|
|
665
|
+
/** H.264 encoder to start on, for the same reason. Dropped if this host cannot run it: the
|
|
666
|
+
* pref is device-local, so the same browser reaching a second host arrives asking for that
|
|
667
|
+
* first host's GPU encoder — and an encoder ffmpeg cannot open makes it exit at once, i.e. a
|
|
668
|
+
* viewer that never shows a frame and blames "Capture failed". */
|
|
669
|
+
encoder?: string;
|
|
670
|
+
}
|
|
671
|
+
|
|
176
672
|
/** Evict any previous session (awaiting its ffmpeg exit, capped so a wedged process can't
|
|
177
673
|
* hang a reconnect) before starting the new one. */
|
|
178
|
-
export async function createRemoteDesktopSession(
|
|
674
|
+
export async function createRemoteDesktopSession(
|
|
675
|
+
ws: RemoteDesktopSocket,
|
|
676
|
+
{ displayId, showCursor = true, encoder }: CreateRemoteDesktopSessionOptions = {},
|
|
677
|
+
): Promise<RemoteDesktopSession> {
|
|
179
678
|
for (const existing of [...activeSessions]) {
|
|
180
679
|
existing.close();
|
|
181
680
|
await Promise.race([existing.exited, Bun.sleep(2000)]);
|
|
182
681
|
}
|
|
183
|
-
const
|
|
682
|
+
const usable = encoder && (await workingEncoders()).includes(encoder) ? encoder : null;
|
|
683
|
+
const session = new RemoteDesktopSession(
|
|
684
|
+
ws, await resolveDisplay(displayId), showCursor, usable,
|
|
685
|
+
);
|
|
184
686
|
await session.start();
|
|
185
687
|
activeSessions.add(session);
|
|
186
688
|
return session;
|