@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.
Files changed (606) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/CLAUDE.md +29 -4
  3. package/THIRD-PARTY-NOTICES.md +1 -1
  4. package/assets/skills/ppm/SKILL.md +1 -1
  5. package/assets/skills/ppm/references/http-api.md +1 -1
  6. package/bun.lock +16 -1
  7. package/dist/web/assets/accounts-settings-section-BVr9-FZJ.js +1 -0
  8. package/dist/web/assets/{ai-resource-editor-F09pf8Hd.js → ai-resource-editor-CdDpOwjh.js} +1 -1
  9. package/dist/web/assets/ai-settings-section-B1hQyjAd.js +1 -0
  10. package/dist/web/assets/ai-settings-section-X2UOfSaS.js +1 -0
  11. package/dist/web/assets/{angular-html-DkR4iCth.js → angular-html-G66FNlyn.js} +1 -1
  12. package/dist/web/assets/{angular-ts-DcyL4Chx.js → angular-ts-B-PnUMyt.js} +1 -1
  13. package/dist/web/assets/{api-client-bfB7cXRM.js → api-client-C1ZwRL4Y.js} +1 -1
  14. package/dist/web/assets/api-client-pQL53cDQ.js +1 -0
  15. package/dist/web/assets/{api-settings-BcNjZIvG.js → api-settings-_7Q-ULxp.js} +1 -1
  16. package/dist/web/assets/{api-themes-DBfyVUtX.js → api-themes-DRAERo4Q.js} +1 -1
  17. package/dist/web/assets/{apl-BVlrVtjt.js → apl-BmDqqwE8.js} +1 -1
  18. package/dist/web/assets/{appearance-settings-section-whgnq3ss.js → appearance-settings-section-DLoPK0LD.js} +1 -1
  19. package/dist/web/assets/architecture-PBZL5I3N-Dx6nBEKT.js +1 -0
  20. package/dist/web/assets/{astro-MjJlozLj.js → astro-BfwMNfxr.js} +1 -1
  21. package/dist/web/assets/{audio-preview-Bc54GCAV.js → audio-preview-DsYvgKqj.js} +1 -1
  22. package/dist/web/assets/{blade-DnJWi1fz.js → blade-BsFV4pnN.js} +1 -1
  23. package/dist/web/assets/branch-review-tab-G84Ojzae.js +1 -0
  24. package/dist/web/assets/{bsl-DCmJxSIz.js → bsl-Cw9Outng.js} +1 -1
  25. package/dist/web/assets/{button-VW4QGKCn.js → button-B9CTrsEf.js} +1 -1
  26. package/dist/web/assets/{cairo-C8dDenub.js → cairo-DOK5Wqnd.js} +1 -1
  27. package/dist/web/assets/chat-tab-DQJfjm8E.js +15 -0
  28. package/dist/web/assets/{cobol-DPx_nifl.js → cobol-DRaRy4FK.js} +1 -1
  29. package/dist/web/assets/code-editor-D04iUPsZ.js +7 -0
  30. package/dist/web/assets/{codex-daily-guard-CfybHv_c.js → codex-daily-guard-BSccLRGT.js} +1 -1
  31. package/dist/web/assets/{coffee-DvFzqhgN.js → coffee-DvXuBmEJ.js} +1 -1
  32. package/dist/web/assets/{conflict-editor-DenQUOT2.js → conflict-editor-C4ay676e.js} +2 -2
  33. package/dist/web/assets/context-menu-C6pIsf83.js +1 -0
  34. package/dist/web/assets/{cpp-BLJN0umi.js → cpp-zCmm8rKA.js} +1 -1
  35. package/dist/web/assets/{crystal-BXR-9fGw.js → crystal-CdQx6lB6.js} +1 -1
  36. package/dist/web/assets/{csv-preview-CClrJOuR.js → csv-preview-DbwEmG7o.js} +1 -1
  37. package/dist/web/assets/{data-grid-overlay-editor-D4fhcMfT.js → data-grid-overlay-editor-6xUCeLHh.js} +1 -1
  38. package/dist/web/assets/{database-viewer-DItjm8wB.js → database-viewer-S6LsALjX.js} +1 -1
  39. package/dist/web/assets/{dialog-DQTn55Mt.js → dialog-B84_DJQ8.js} +1 -1
  40. package/dist/web/assets/diff-viewer-3oBA7vgQ.js +4 -0
  41. package/dist/web/assets/{docx-preview-DYBkiUaE.js → docx-preview-CXuk-emm.js} +1 -1
  42. package/dist/web/assets/{edge-rWOlCmXT.js → edge-E8QLt7oi.js} +1 -1
  43. package/dist/web/assets/{edit-diff-preview-COtmo54G.js → edit-diff-preview-BGGGecVw.js} +1 -1
  44. package/dist/web/assets/{editor-language-service-DxkZ51E5.js → editor-language-service-DJmaKVY0.js} +1 -1
  45. package/dist/web/assets/{elixir-E7Y82okW.js → elixir-DPb_-72P.js} +1 -1
  46. package/dist/web/assets/{elm-DTPGbc-w.js → elm-BMzJY5b0.js} +1 -1
  47. package/dist/web/assets/{erb-DHqLHrE6.js → erb-AABQNtMK.js} +1 -1
  48. package/dist/web/assets/{erlang-Byg0nNiV.js → erlang-CEIKOkfP.js} +1 -1
  49. package/dist/web/assets/{esm-D_DmOM7n.js → esm-B9RVMpwV.js} +1 -1
  50. package/dist/web/assets/explorer-body-BQ0jyAGv.js +2 -0
  51. package/dist/web/assets/explorer-window-content-jdkWt34J.js +1 -0
  52. package/dist/web/assets/{extension-manager-section-BhwB6nqr.js → extension-manager-section-DG1guILi.js} +1 -1
  53. package/dist/web/assets/{extension-webview-DI4VL2tJ.js → extension-webview-BGSDUbdK.js} +1 -1
  54. package/dist/web/assets/file-icons-BXdvggK2.js +1 -0
  55. package/dist/web/assets/file-icons.generated-BtBErEwb.css +1 -0
  56. package/dist/web/assets/file-store-ojgMXgAw.js +1 -0
  57. package/dist/web/assets/files-settings-section-Gnyd7han.js +1 -0
  58. package/dist/web/assets/{format-file-meta-DOpzJJhS.js → format-file-meta-C77h04cZ.js} +1 -1
  59. package/dist/web/assets/{fortran-fixed-form-CY1n0Xf6.js → fortran-fixed-form-CrXvLHyi.js} +1 -1
  60. package/dist/web/assets/{fsharp-_vEDSCJz.js → fsharp-DCI4qAz_.js} +1 -1
  61. package/dist/web/assets/{gdresource-B6XT0wAC.js → gdresource-Vb2kO1MG.js} +1 -1
  62. package/dist/web/assets/{general-settings-section-BcxwiZ0X.js → general-settings-section-V6Khh15V.js} +1 -1
  63. package/dist/web/assets/{git-commit-Wk9ovq59.js → git-commit-BMI_OWn-.js} +1 -1
  64. package/dist/web/assets/git-log-panel-Bi513qjT.js +1 -0
  65. package/dist/web/assets/{git-rebase-DSarGPvK.js → git-rebase-D-d_2iqZ.js} +1 -1
  66. package/dist/web/assets/{git-repo-store-Dy69YCHN.js → git-repo-store-88Z8v-pF.js} +1 -1
  67. package/dist/web/assets/gitGraph-HDMCJU4V-C6qRruHI.js +1 -0
  68. package/dist/web/assets/{glide-data-grid-BvgAqSUr.js → glide-data-grid-DnQwYSmQ.js} +7 -7
  69. package/dist/web/assets/{glimmer-js-CkYI35_P.js → glimmer-js-BmNNiR0Z.js} +1 -1
  70. package/dist/web/assets/{glimmer-ts-BqmIZcds.js → glimmer-ts-DQE2Ufha.js} +1 -1
  71. package/dist/web/assets/{glsl-Be-yBCF5.js → glsl-BcIDsxRJ.js} +1 -1
  72. package/dist/web/assets/{graphql-a5DrENHY.js → graphql-DGKSLmGH.js} +1 -1
  73. package/dist/web/assets/group-chat-tab-DpczdwyV.js +1 -0
  74. package/dist/web/assets/{hack-BLAg20aD.js → hack-YiC8bEIb.js} +1 -1
  75. package/dist/web/assets/{haml-Dj6bGOQH.js → haml-DjblxAsz.js} +1 -1
  76. package/dist/web/assets/{handlebars-dmEa36Oh.js → handlebars-BQepOXaU.js} +1 -1
  77. package/dist/web/assets/{html-yacZYzSE.js → html-Cayj_2GP.js} +1 -1
  78. package/dist/web/assets/{html-derivative-lLi3-RNT.js → html-derivative-Weq79bL6.js} +1 -1
  79. package/dist/web/assets/{http-1KaxcE4e.js → http-BeLfdGdz.js} +1 -1
  80. package/dist/web/assets/{hurl-ST-dEEZR.js → hurl-D9rypCLK.js} +1 -1
  81. package/dist/web/assets/{hxml-CpDtuq__.js → hxml-clG8mmSH.js} +1 -1
  82. package/dist/web/assets/{image-preview-CJVxL0uH.js → image-preview-CzCc4_Do.js} +1 -1
  83. package/dist/web/assets/image-preview-geometry-9lgeLYB3.js +1 -0
  84. package/dist/web/assets/index-BH99TIE0.css +1 -0
  85. package/dist/web/assets/index-DU68xk06.js +52 -0
  86. package/dist/web/assets/info-3K5VOQVL-BOSpdwlz.js +1 -0
  87. package/dist/web/assets/{jinja-Cjw-evAn.js → jinja-BShBWfs5.js} +1 -1
  88. package/dist/web/assets/{jira-watcher-section-DIC1yIV3.js → jira-watcher-section-f2HUbGDY.js} +1 -1
  89. package/dist/web/assets/{jison-DfATMYoY.js → jison-dN2Nf_Dr.js} +1 -1
  90. package/dist/web/assets/{julia-DDCaxiqT.js → julia-5U0vR-uJ.js} +1 -1
  91. package/dist/web/assets/{just-CJL529u6.js → just-BMppVRy6.js} +1 -1
  92. package/dist/web/assets/keybindings-store-DCRsnKg5.js +1 -0
  93. package/dist/web/assets/{keyboard-shortcuts-section-BW03P3AF.js → keyboard-shortcuts-section-BpvVEw_Y.js} +1 -1
  94. package/dist/web/assets/{latex-Zc4EpLpW.js → latex-DSSOKjis.js} +1 -1
  95. package/dist/web/assets/{liquid-B_6KMNYS.js → liquid-C24dKoyI.js} +1 -1
  96. package/dist/web/assets/{lua-BtS2idK7.js → lua-DNOsnyhn.js} +1 -1
  97. package/dist/web/assets/markdown-renderer-Bcaf9_bA.js +1 -0
  98. package/dist/web/assets/markdown-renderer-DGJp8HC3.js +3 -0
  99. package/dist/web/assets/{marko-Wk6LBnck.js → marko-26MHO_Q9.js} +1 -1
  100. package/dist/web/assets/{mdc-cRaXBrzL.js → mdc-Dhw9mGrC.js} +1 -1
  101. package/dist/web/assets/{media-url-DL8OCGGg.js → media-url-yfqHpPz0.js} +1 -1
  102. package/dist/web/assets/{mobile-bottom-sheet-CoUauvLF.js → mobile-bottom-sheet-C3TniEP-.js} +1 -1
  103. package/dist/web/assets/mobile-explorer-sheet-Bol3XAO1.js +1 -0
  104. package/dist/web/assets/{nextflow-Bos2wvDV.js → nextflow-xH4oJTex.js} +1 -1
  105. package/dist/web/assets/{nginx-DMlOwzdr.js → nginx-CMz4w0M8.js} +1 -1
  106. package/dist/web/assets/{nim-D2dZQA88.js → nim-D8FnIYsx.js} +1 -1
  107. package/dist/web/assets/notification-store-BXt54VIn.js +1 -0
  108. package/dist/web/assets/{notifications-settings-section-DPUROMw-.js → notifications-settings-section-DqlPDfNX.js} +1 -1
  109. package/dist/web/assets/{number-overlay-editor-DZ2i_Bnb.js → number-overlay-editor-CRGwULhR.js} +1 -1
  110. package/dist/web/assets/{overview-panel-R146OIom.js → overview-panel-Di-3aV9p.js} +1 -1
  111. package/dist/web/assets/packet-RMMSAZCW-BGIE1QdU.js +1 -0
  112. package/dist/web/assets/panel-store-CboGJAwh.js +1 -0
  113. package/dist/web/assets/{pdf-preview-BWUPGLp6.js → pdf-preview-CmBI6E7d.js} +1 -1
  114. package/dist/web/assets/{perl-C5-Z6fPn.js → perl-D9iNWYwb.js} +1 -1
  115. package/dist/web/assets/{php-BXNNhkMH.js → php-Bw3Qo0Zv.js} +1 -1
  116. package/dist/web/assets/pie-UPGHQEXC-Dg-QTbXX.js +1 -0
  117. package/dist/web/assets/{postgres-viewer-CjOvaruz.js → postgres-viewer-8ZAqEALv.js} +3 -3
  118. package/dist/web/assets/{ppmbot-settings-section-DrvWh8l8.js → ppmbot-settings-section-DKJUA3Kq.js} +1 -1
  119. package/dist/web/assets/problems-panel-CFcoUx8h.js +1 -0
  120. package/dist/web/assets/{process-table-BRMS1MrX.js → process-table-EKr2dqG6.js} +1 -1
  121. package/dist/web/assets/project-framework-store-B70Q3dNM.js +1 -0
  122. package/dist/web/assets/project-store-B87hLRSk.js +1 -0
  123. package/dist/web/assets/{proxy-settings-section-Bv0Ny-Yy.js → proxy-settings-section-B7qRVl8w.js} +1 -1
  124. package/dist/web/assets/{pug-C1zf1yWy.js → pug-fT5JC6Vm.js} +1 -1
  125. package/dist/web/assets/{qml-OW58jGpD.js → qml-P_wEDB1w.js} +1 -1
  126. package/dist/web/assets/{query-audit-section-BcHJZcVC.js → query-audit-section-By6qyqdB.js} +1 -1
  127. package/dist/web/assets/radar-KQ55EAFF-DZ6IVu2s.js +1 -0
  128. package/dist/web/assets/{razor-DzjSaobM.js → razor-Drrt2_Ph.js} +1 -1
  129. package/dist/web/assets/remote-desktop-clipboard-notice-0WDYsyCA.js +1 -0
  130. package/dist/web/assets/remote-desktop-mobile-sheet-COYDg16H.js +2 -0
  131. package/dist/web/assets/remote-desktop-mobile-view-2DqSuS2U.js +1 -0
  132. package/dist/web/assets/{remote-desktop-readiness-gate-BTzss2wX.js → remote-desktop-readiness-gate-Dm_qMJMH.js} +1 -1
  133. package/dist/web/assets/remote-desktop-window-content-UodhsF7W.js +1 -0
  134. package/dist/web/assets/{rst-RfuihOUW.js → rst-Dr2mW4-h.js} +1 -1
  135. package/dist/web/assets/{ruby-CNO3pli1.js → ruby-C6EpUsTY.js} +1 -1
  136. package/dist/web/assets/{run-in-terminal-Cb_hVU8x.js → run-in-terminal-CdefEe_R.js} +1 -1
  137. package/dist/web/assets/{sas-DZY5BtXx.js → sas-D8ZEeP6v.js} +1 -1
  138. package/dist/web/assets/{schedules-settings-section-DO8xAOVj.js → schedules-settings-section-bG2lYfGv.js} +1 -1
  139. package/dist/web/assets/{scroll-area-LtNV8g5o.js → scroll-area-BzeiOd-B.js} +1 -1
  140. package/dist/web/assets/{scss--4g-REcN.js → scss-ITL7jQ1f.js} +1 -1
  141. package/dist/web/assets/{send-to-chat-Cnpru1M0.js → send-to-chat-F207yH3u.js} +1 -1
  142. package/dist/web/assets/{separator-Cds1yxy8.js → separator-TRMsJUYe.js} +1 -1
  143. package/dist/web/assets/settings-body-C06bFarv.js +2 -0
  144. package/dist/web/assets/settings-store-krC6bc79.js +2 -0
  145. package/dist/web/assets/settings-tab-34RSaASd.js +1 -0
  146. package/dist/web/assets/settings-window-content-Dr_2lmHH.js +1 -0
  147. package/dist/web/assets/{shaderlab-Cu4sBiVn.js → shaderlab-mW-IIlhk.js} +1 -1
  148. package/dist/web/assets/{shellsession-DWzJfEv7.js → shellsession-DhNl5STt.js} +1 -1
  149. package/dist/web/assets/{soy-BnqE-5Ff.js → soy-SdY0Qjtr.js} +1 -1
  150. package/dist/web/assets/{sparql-Ctg__NhZ.js → sparql-ftqZo2i-.js} +1 -1
  151. package/dist/web/assets/{sql-query-editor-D9k19T_M.js → sql-query-editor-BgRH0cHD.js} +1 -1
  152. package/dist/web/assets/{sqlite-viewer-DoV9hnWJ.js → sqlite-viewer-DUxGslpa.js} +1 -1
  153. package/dist/web/assets/{stata-Qk2h66XW.js → stata-Bj7Yjb4b.js} +1 -1
  154. package/dist/web/assets/{surrealql-8QEX7EfH.js → surrealql-CLhYGAfB.js} +1 -1
  155. package/dist/web/assets/{svelte-D_mubC5-.js → svelte-DTAlRHhK.js} +1 -1
  156. package/dist/web/assets/system-monitor-body-IZuLn7Qh.js +2 -0
  157. package/dist/web/assets/system-monitor-tab-DiSA490Z.js +1 -0
  158. package/dist/web/assets/{system-monitor-window-content-qXLcicFM.js → system-monitor-window-content-CEaFISgj.js} +1 -1
  159. package/dist/web/assets/{tab-host-window-content-7fPZNEni.js → tab-host-window-content-D0TN8Wrq.js} +1 -1
  160. package/dist/web/assets/{tab-store-DGJGnbwv.js → tab-store-OJa1GPCq.js} +1 -1
  161. package/dist/web/assets/team-member-sheet-DDPNcCjd.js +2 -0
  162. package/dist/web/assets/{team-member-window-content-BgvJFypM.js → team-member-window-content-DCiW_1-E.js} +1 -1
  163. package/dist/web/assets/{templ-C1jV0fsO.js → templ-BMfhACxb.js} +1 -1
  164. package/dist/web/assets/{terminal-tab-Cs0sGljx.js → terminal-tab-4ypP0NDY.js} +3 -3
  165. package/dist/web/assets/{tex-D_StFXy_.js → tex-BcssM-QA.js} +1 -1
  166. package/dist/web/assets/tool-cards-Dd6E7Q3k.js +9 -0
  167. package/dist/web/assets/treemap-KZPCXAKY-CE30wCXa.js +1 -0
  168. package/dist/web/assets/{ts-tags-B5m47rEm.js → ts-tags-CrR5IYI8.js} +1 -1
  169. package/dist/web/assets/{twig-C_br6GIl.js → twig-DchfUmyI.js} +1 -1
  170. package/dist/web/assets/{use-blob-url-C-SGi_Hb.js → use-blob-url-CPtCcY3i.js} +1 -1
  171. package/dist/web/assets/{use-inline-blame-B4trxGcR.js → use-inline-blame-vrfFHuaR.js} +1 -1
  172. package/dist/web/assets/{use-lsp-CV0vzILO.js → use-lsp-CsuZu49X.js} +1 -1
  173. package/dist/web/assets/{use-monaco-theme-DBqbWjWX.js → use-monaco-theme-BIVYJBIn.js} +1 -1
  174. package/dist/web/assets/{use-open-team-member-DENsjp0s.js → use-open-team-member-C3mrUzIC.js} +1 -1
  175. package/dist/web/assets/{use-websocket-LgXiX1qN.js → use-websocket-BMbufhX7.js} +1 -1
  176. package/dist/web/assets/{vendor-mermaid-Cbig5zA6.js → vendor-mermaid-BitiXJlL.js} +1 -1
  177. package/dist/web/assets/{vendor-ui-BBdkypKW.js → vendor-ui-DmZ-Mb-H.js} +1 -1
  178. package/dist/web/assets/video-preview-Byj8P_jA.js +1 -0
  179. package/dist/web/assets/{vue-DQwCnLdr.js → vue-1EL6Pw1h.js} +1 -1
  180. package/dist/web/assets/{vue-html-T_EoGpPJ.js → vue-html-BPQNwPC6.js} +1 -1
  181. package/dist/web/assets/{vue-vine-DmxLqX50.js → vue-vine-Bb_dsWLt.js} +1 -1
  182. package/dist/web/assets/{window-pip-registry-3l4hoxuD.js → window-pip-registry-yD6esSju.js} +1 -1
  183. package/dist/web/assets/ws-auth-CEYVQ8NH.js +1 -0
  184. package/dist/web/assets/{xml-Bs6Bw8is.js → xml-dxkLNA0K.js} +1 -1
  185. package/dist/web/assets/{xsl-f5ZNhRCc.js → xsl-Bw2adwrl.js} +1 -1
  186. package/dist/web/index.html +27 -26
  187. package/dist/web/sw.js +1 -1
  188. package/docs/architecture/plugins-and-tracing.md +285 -0
  189. package/docs/lessons-learned.md +31 -0
  190. package/docs/system-architecture.md +1 -0
  191. package/package.json +2 -1
  192. package/packages/ext-git-graph/src/compare-diff-base.test.ts +173 -0
  193. package/packages/ext-git-graph/src/compare-view.ts +22 -3
  194. package/packages/ext-git-graph/src/extension.ts +6 -0
  195. package/packages/ext-git-graph/src/types.ts +19 -0
  196. package/packages/ext-git-graph/src/webview-html.test.ts +134 -1
  197. package/packages/ext-git-graph/src/webview-html.ts +277 -27
  198. package/scripts/gen-file-icons.ts +493 -242
  199. package/src/providers/claude-agent-sdk.ts +42 -1
  200. package/src/providers/codex-app-server/codex-history.ts +77 -23
  201. package/src/server/index.ts +42 -7
  202. package/src/server/routes/chat.ts +15 -11
  203. package/src/server/routes/git.ts +83 -3
  204. package/src/server/routes/remote-desktop.ts +7 -2
  205. package/src/server/ws/global.ts +1 -1
  206. package/src/server/ws/remote-desktop.ts +12 -5
  207. package/src/services/binary-content.ts +25 -0
  208. package/src/services/chat-search.service.ts +1 -1
  209. package/src/services/chat.service.ts +9 -0
  210. package/src/services/event-loop-lag.ts +208 -0
  211. package/src/services/file-lines.ts +114 -0
  212. package/src/services/file-watcher/recreated-dir-poller.ts +108 -6
  213. package/src/services/file-watcher/watch-tree.ts +131 -10
  214. package/src/services/file-watcher.service.ts +73 -23
  215. package/src/services/git-branch-diff/branch-diff-parse.ts +151 -0
  216. package/src/services/git-branch-diff/branch-diff.service.ts +79 -0
  217. package/src/services/git-hunks/git-hunks.service.ts +26 -11
  218. package/src/services/git-hunks/unified-diff.ts +15 -1
  219. package/src/services/git.service.ts +74 -24
  220. package/src/services/jsonl-transcript-parser.ts +138 -6
  221. package/src/services/log-rotate.ts +142 -0
  222. package/src/services/media-transcode/ffmpeg-capabilities.ts +72 -2
  223. package/src/services/remote-desktop/remote-desktop-audio.ts +193 -0
  224. package/src/services/remote-desktop/remote-desktop-capture-input.ts +87 -14
  225. package/src/services/remote-desktop/remote-desktop-capture.ts +40 -7
  226. package/src/services/remote-desktop/remote-desktop-clipboard.ts +166 -0
  227. package/src/services/remote-desktop/remote-desktop-displays.ts +77 -1
  228. package/src/services/remote-desktop/remote-desktop-encoder-args.ts +25 -6
  229. package/src/services/remote-desktop/remote-desktop-evdev-key-map.ts +69 -0
  230. package/src/services/remote-desktop/remote-desktop-input-linux.ts +253 -0
  231. package/src/services/remote-desktop/remote-desktop-input-uinput.ts +242 -0
  232. package/src/services/remote-desktop/remote-desktop-input.ts +40 -8
  233. package/src/services/remote-desktop/remote-desktop-linux-session.ts +146 -0
  234. package/src/services/remote-desktop/remote-desktop-ogg-opus.ts +144 -0
  235. package/src/services/remote-desktop/remote-desktop-privacy.ts +163 -0
  236. package/src/services/remote-desktop/remote-desktop-quality.ts +138 -0
  237. package/src/services/remote-desktop/remote-desktop-requirements.ts +158 -5
  238. package/src/services/remote-desktop/remote-desktop-resolution.ts +302 -0
  239. package/src/services/remote-desktop/remote-desktop-session.ts +523 -21
  240. package/src/services/remote-desktop/remote-desktop-x11.ts +250 -0
  241. package/src/services/supervisor.ts +47 -4
  242. package/src/shared/remote-desktop-custom-quality.ts +117 -0
  243. package/src/shared/remote-desktop-quality.ts +127 -0
  244. package/src/types/chat.ts +4 -0
  245. package/src/types/git.ts +52 -0
  246. package/src/web/components/branch-review/branch-review-tab.tsx +360 -0
  247. package/src/web/components/branch-review/branch-review-tree-row.tsx +138 -0
  248. package/src/web/components/chat/attachment-chips.tsx +30 -6
  249. package/src/web/components/chat/chat-tab.tsx +12 -1
  250. package/src/web/components/chat/message-events.tsx +273 -0
  251. package/src/web/components/chat/message-list.tsx +107 -861
  252. package/src/web/components/chat/message-markdown.tsx +34 -0
  253. package/src/web/components/chat/message-media.tsx +174 -0
  254. package/src/web/components/chat/message-user-bubble.tsx +399 -0
  255. package/src/web/components/chat/provider-selector.tsx +30 -22
  256. package/src/web/components/chat/session-history-list.tsx +2 -2
  257. package/src/web/components/chat/session-list-panel.tsx +3 -2
  258. package/src/web/components/editor/binary-diff-view.tsx +212 -0
  259. package/src/web/components/editor/diff-viewer.tsx +76 -3
  260. package/src/web/components/explorer/file-tree.tsx +235 -134
  261. package/src/web/components/explorer/flatten-visible-tree.ts +35 -40
  262. package/src/web/components/explorer/tree-node.tsx +69 -69
  263. package/src/web/components/explorer/use-tree-row-dnd.ts +3 -4
  264. package/src/web/components/git/git-status-panel.tsx +25 -0
  265. package/src/web/components/layout/draggable-tab.tsx +4 -2
  266. package/src/web/components/layout/nav-section-rail.tsx +12 -3
  267. package/src/web/components/layout/status-bar.tsx +10 -6
  268. package/src/web/components/layout/tab-content.tsx +5 -0
  269. package/src/web/components/layout/tab-pool.tsx +1 -0
  270. package/src/web/components/layout/upgrade-button.tsx +5 -1
  271. package/src/web/components/remote-desktop/remote-desktop-clipboard-client.ts +67 -0
  272. package/src/web/components/remote-desktop/remote-desktop-clipboard-notice.tsx +127 -0
  273. package/src/web/components/remote-desktop/remote-desktop-codec-labels.ts +28 -0
  274. package/src/web/components/remote-desktop/remote-desktop-custom-quality-dialog.tsx +144 -0
  275. package/src/web/components/remote-desktop/remote-desktop-mobile-key-bar.tsx +13 -2
  276. package/src/web/components/remote-desktop/remote-desktop-mobile-settings.tsx +232 -0
  277. package/src/web/components/remote-desktop/remote-desktop-mobile-toolbar.tsx +21 -8
  278. package/src/web/components/remote-desktop/remote-desktop-mobile-view.tsx +82 -3
  279. package/src/web/components/remote-desktop/remote-desktop-recording.ts +76 -0
  280. package/src/web/components/remote-desktop/remote-desktop-resolution-list.ts +100 -0
  281. package/src/web/components/remote-desktop/remote-desktop-toolbar.tsx +497 -0
  282. package/src/web/components/remote-desktop/remote-desktop-view-style.ts +145 -0
  283. package/src/web/components/remote-desktop/remote-desktop-window-content.tsx +126 -35
  284. package/src/web/components/remote-desktop/use-h264-canvas-decoder.ts +11 -0
  285. package/src/web/components/remote-desktop/use-opus-audio-player.ts +148 -0
  286. package/src/web/components/remote-desktop/use-remote-desktop-connection.ts +284 -1
  287. package/src/web/components/remote-desktop/use-remote-desktop-readiness.ts +75 -1
  288. package/src/web/components/remote-desktop/use-remote-desktop-recorder.ts +103 -0
  289. package/src/web/components/remote-desktop/use-remote-desktop-virtual-keyboard.ts +19 -2
  290. package/src/web/components/remote-desktop/use-remote-input-capture.ts +39 -3
  291. package/src/web/components/settings/theme-picker.tsx +2 -1
  292. package/src/web/components/ui/tooltip.tsx +15 -2
  293. package/src/web/hooks/use-delayed-flag.ts +35 -0
  294. package/src/web/lib/branch-review-state.ts +175 -0
  295. package/src/web/lib/file-icon-name.ts +62 -0
  296. package/src/web/lib/file-icons.generated.ts +3287 -25
  297. package/src/web/lib/file-icons.tsx +71 -73
  298. package/src/web/lib/flatten-expansions.ts +18 -5
  299. package/src/web/lib/message-ordinals.ts +23 -0
  300. package/src/web/lib/provider-logos.tsx +143 -0
  301. package/src/web/lib/tab-type-icons.ts +11 -0
  302. package/src/web/stores/file-store.ts +73 -35
  303. package/src/web/stores/panel-utils.ts +5 -0
  304. package/src/web/stores/project-framework-store.ts +82 -0
  305. package/src/web/stores/project-store.ts +7 -1
  306. package/src/web/stores/settings-store.ts +128 -0
  307. package/src/web/stores/tab-store.ts +1 -0
  308. package/src/web/stores/window-panel-persistence.ts +1 -0
  309. package/src/web/styles/file-icons.generated.css +3282 -234
  310. package/src/web/styles/globals.css +26 -0
  311. package/dist/web/assets/accounts-settings-section-BuxD9zH3.js +0 -1
  312. package/dist/web/assets/ai-settings-section-Dd9tOUp0.js +0 -1
  313. package/dist/web/assets/ai-settings-section-FrU2X8Fu.js +0 -1
  314. package/dist/web/assets/api-client-BUCo1lms.js +0 -1
  315. package/dist/web/assets/architecture-PBZL5I3N-5K0wEkAn.js +0 -1
  316. package/dist/web/assets/chat-tab-Cr6_KSGO.js +0 -15
  317. package/dist/web/assets/code-editor-DfmlGamv.js +0 -7
  318. package/dist/web/assets/context-menu-DOODKZdF.js +0 -1
  319. package/dist/web/assets/diff-viewer-BpNj7pN9.js +0 -4
  320. package/dist/web/assets/explorer-body-D-5z464e.js +0 -2
  321. package/dist/web/assets/explorer-window-content-7-1wuRul.js +0 -1
  322. package/dist/web/assets/file-icons-EzPL0q-k.js +0 -1
  323. package/dist/web/assets/file-icons.generated-CfaJuvZu.css +0 -1
  324. package/dist/web/assets/file-store-DMABxOkV.js +0 -1
  325. package/dist/web/assets/files-settings-section-CZzABuKz.js +0 -1
  326. package/dist/web/assets/git-log-panel-CRN4JVOx.js +0 -1
  327. package/dist/web/assets/gitGraph-HDMCJU4V-DkVT2MAM.js +0 -1
  328. package/dist/web/assets/group-chat-tab-DFa5mAx5.js +0 -1
  329. package/dist/web/assets/index-D7DbXD3G.css +0 -1
  330. package/dist/web/assets/index-ZtfxZOyb.js +0 -52
  331. package/dist/web/assets/info-3K5VOQVL-BBsmKGzA.js +0 -1
  332. package/dist/web/assets/keybindings-store-tVfhPANS.js +0 -1
  333. package/dist/web/assets/markdown-renderer-CvL1pWeK.js +0 -3
  334. package/dist/web/assets/markdown-renderer-sJNNVhlS.js +0 -1
  335. package/dist/web/assets/mobile-explorer-sheet-CZ0HZ2E_.js +0 -1
  336. package/dist/web/assets/notification-store-BZbsPnW0.js +0 -1
  337. package/dist/web/assets/packet-RMMSAZCW-DwmYPOlx.js +0 -1
  338. package/dist/web/assets/panel-store-BVLBRn6M.js +0 -1
  339. package/dist/web/assets/pie-UPGHQEXC-D0oogIFi.js +0 -1
  340. package/dist/web/assets/problems-panel-B7BWVhdy.js +0 -1
  341. package/dist/web/assets/project-store-BhMbVln3.js +0 -1
  342. package/dist/web/assets/radar-KQ55EAFF-CQIJOQGa.js +0 -1
  343. package/dist/web/assets/remote-desktop-mobile-sheet-ByjaeZKX.js +0 -2
  344. package/dist/web/assets/remote-desktop-mobile-view-D13mtr9F.js +0 -1
  345. package/dist/web/assets/remote-desktop-window-content-CbqTCaC6.js +0 -1
  346. package/dist/web/assets/settings-body-Rnu-FcX-.js +0 -2
  347. package/dist/web/assets/settings-store-DZPgPtEA.js +0 -2
  348. package/dist/web/assets/settings-tab-C6PHxs0Z.js +0 -1
  349. package/dist/web/assets/settings-window-content-CQ4gT2qj.js +0 -1
  350. package/dist/web/assets/system-monitor-body-2vQ1kMxE.js +0 -2
  351. package/dist/web/assets/system-monitor-tab-krAcvwF7.js +0 -1
  352. package/dist/web/assets/team-member-sheet-2FnRmk1a.js +0 -2
  353. package/dist/web/assets/tool-cards-DU_S2o51.js +0 -9
  354. package/dist/web/assets/treemap-KZPCXAKY-BhEtjljr.js +0 -1
  355. package/dist/web/assets/use-remote-desktop-display-choice-BK_f787P.js +0 -1
  356. package/dist/web/assets/video-preview-BDNwumbW.js +0 -1
  357. package/dist/web/assets/ws-auth-DEAJiGCR.js +0 -1
  358. /package/dist/web/assets/{abap-tkptwpsK.js → abap-IN2yINdN.js} +0 -0
  359. /package/dist/web/assets/{actionscript-3-Di9TMGwg.js → actionscript-3-C_t772eR.js} +0 -0
  360. /package/dist/web/assets/{ada-CB9F079D.js → ada-DPme3emm.js} +0 -0
  361. /package/dist/web/assets/{andromeeda-Dcl8jpb7.js → andromeeda-DSV4i4mZ.js} +0 -0
  362. /package/dist/web/assets/{apache-D0Nk0qsH.js → apache-CWr8s8I8.js} +0 -0
  363. /package/dist/web/assets/{apex-mQIoWncD.js → apex-Z20JnDF5.js} +0 -0
  364. /package/dist/web/assets/{applescript-C6ljbcsI.js → applescript-BKNPu3ga.js} +0 -0
  365. /package/dist/web/assets/{apply-theme-BwIMNXyX.js → apply-theme-CbR31K6I.js} +0 -0
  366. /package/dist/web/assets/{ara-WFqRPGPe.js → ara-hWdY3O96.js} +0 -0
  367. /package/dist/web/assets/{asciidoc-bNd8w4nf.js → asciidoc-eynhq496.js} +0 -0
  368. /package/dist/web/assets/{asm-BQ1_F580.js → asm-Bu02Kq4P.js} +0 -0
  369. /package/dist/web/assets/{aurora-x-Ciqs_Dbu.js → aurora-x-BHBGXbIZ.js} +0 -0
  370. /package/dist/web/assets/{awk-GCR-6KZV.js → awk-B4ojezyv.js} +0 -0
  371. /package/dist/web/assets/{ayu-dark-DxJXuj99.js → ayu-dark-D0GQzFi9.js} +0 -0
  372. /package/dist/web/assets/{ayu-light-jvSyJWDV.js → ayu-light-BVYb5Yn8.js} +0 -0
  373. /package/dist/web/assets/{ayu-mirage-Cg3BPO6H.js → ayu-mirage-BGFW6oNb.js} +0 -0
  374. /package/dist/web/assets/{ballerina-BPz4CrX6.js → ballerina-B6Og3IKT.js} +0 -0
  375. /package/dist/web/assets/{bat-CSTs2hIG.js → bat-D8YFILfx.js} +0 -0
  376. /package/dist/web/assets/{beancount-DMateAZX.js → beancount-BjlbBBuX.js} +0 -0
  377. /package/dist/web/assets/{berry-9lXg0GRM.js → berry-Cz1ygeoj.js} +0 -0
  378. /package/dist/web/assets/{bibtex-DIbqmvFg.js → bibtex-CVkJceJh.js} +0 -0
  379. /package/dist/web/assets/{bicep-02WGsoJ8.js → bicep-DrhYI5GX.js} +0 -0
  380. /package/dist/web/assets/{bird2-BY0tbu3k.js → bird2-BV-KJtmc.js} +0 -0
  381. /package/dist/web/assets/{c-jMBX_9Je.js → c-CuLlvxbG.js} +0 -0
  382. /package/dist/web/assets/{c3-B8JVbNjW.js → c3-CI7-1CJ0.js} +0 -0
  383. /package/dist/web/assets/{cadence-DUIf1tvL.js → cadence-DjxhNP3Q.js} +0 -0
  384. /package/dist/web/assets/{can-open-in-ppm-DGEc0s4P.js → can-open-in-ppm-cv5--yHX.js} +0 -0
  385. /package/dist/web/assets/{catppuccin-frappe-FhUCVxex.js → catppuccin-frappe-CEozpJRh.js} +0 -0
  386. /package/dist/web/assets/{catppuccin-latte-CzfrtmgL.js → catppuccin-latte-C4sXQFMc.js} +0 -0
  387. /package/dist/web/assets/{catppuccin-macchiato-6xL0lOtj.js → catppuccin-macchiato-Ci-fDhXy.js} +0 -0
  388. /package/dist/web/assets/{catppuccin-mocha-D39-xS3P.js → catppuccin-mocha-CH7YXw8d.js} +0 -0
  389. /package/dist/web/assets/{clarity-D8TeGvoT.js → clarity-BWrJ7UmT.js} +0 -0
  390. /package/dist/web/assets/{clojure-Dx_acSX7.js → clojure-C94fh2hb.js} +0 -0
  391. /package/dist/web/assets/{cmake-CyeYPq8o.js → cmake-CtvCkMsG.js} +0 -0
  392. /package/dist/web/assets/{codeowners-DrTI0lmI.js → codeowners-CJnubqzH.js} +0 -0
  393. /package/dist/web/assets/{codeql-CPkFxVW-.js → codeql-BOoP5HY2.js} +0 -0
  394. /package/dist/web/assets/{common-lisp-B0k-3LTj.js → common-lisp-B9nlMFJd.js} +0 -0
  395. /package/dist/web/assets/{coq-uygpo7to.js → coq-CgzIQiSH.js} +0 -0
  396. /package/dist/web/assets/{csharp-DKUYGfSp.js → csharp-4xAIx3jw.js} +0 -0
  397. /package/dist/web/assets/{css-DoOUp7v4.js → css-D4YoLYGn.js} +0 -0
  398. /package/dist/web/assets/{csv-B3YE13gi.js → csv-C2v-ZakW.js} +0 -0
  399. /package/dist/web/assets/{csv-parser-DXePqmXe.js → csv-parser-_oDBUkwQ.js} +0 -0
  400. /package/dist/web/assets/{cue-DViO2wvn.js → cue-Bbznh_an.js} +0 -0
  401. /package/dist/web/assets/{cypher-4BMCIW5N.js → cypher-BKhVEARF.js} +0 -0
  402. /package/dist/web/assets/{d-BT_sTHas.js → d-D6IhWopl.js} +0 -0
  403. /package/dist/web/assets/{dark-plus-DVVXu-1N.js → dark-plus-xAWu790O.js} +0 -0
  404. /package/dist/web/assets/{dart-DHz6f6_6.js → dart-Bj6S9O1j.js} +0 -0
  405. /package/dist/web/assets/{data-grid-types-OgUxMLdQ.js → data-grid-types-C5bthITf.js} +0 -0
  406. /package/dist/web/assets/{dax-ChYCjweK.js → dax-CF2zvaqq.js} +0 -0
  407. /package/dist/web/assets/{desktop-D6Lt4p04.js → desktop-CnmyxMKE.js} +0 -0
  408. /package/dist/web/assets/{diff-D8TDe4hP.js → diff-Qi8WuJsI.js} +0 -0
  409. /package/dist/web/assets/{dist-DOTlFQJA.js → dist-Bjseolz4.js} +0 -0
  410. /package/dist/web/assets/{dist-CrBOCtCW.js → dist-Czq7BUMy.js} +0 -0
  411. /package/dist/web/assets/{docker-Bsp0YbPx.js → docker-vLbbNP5d.js} +0 -0
  412. /package/dist/web/assets/{dotenv-B_7giSY_.js → dotenv-DJMvvNzn.js} +0 -0
  413. /package/dist/web/assets/{dracula-TrQL2iB5.js → dracula-CtAh2rhB.js} +0 -0
  414. /package/dist/web/assets/{dracula-soft-n2XOlgh-.js → dracula-soft-CXtuAAf6.js} +0 -0
  415. /package/dist/web/assets/{dream-maker-CdIEdhpT.js → dream-maker-D5YQyK49.js} +0 -0
  416. /package/dist/web/assets/{editor-font-BDrdiGf1.js → editor-font-ClTC9gaD.js} +0 -0
  417. /package/dist/web/assets/{emacs-lisp-CSbixs6u.js → emacs-lisp-CGTjbiqF.js} +0 -0
  418. /package/dist/web/assets/{everforest-dark-BuZahZYp.js → everforest-dark-D59p7vAQ.js} +0 -0
  419. /package/dist/web/assets/{everforest-light-DPH53z5H.js → everforest-light-B3Qtud3V.js} +0 -0
  420. /package/dist/web/assets/{fennel-B1-K_hlJ.js → fennel-BCyHNUwf.js} +0 -0
  421. /package/dist/web/assets/{fish-Cy12Dbua.js → fish-DDDlLV2G.js} +0 -0
  422. /package/dist/web/assets/{fluent-B_82ys8E.js → fluent-By54R9tl.js} +0 -0
  423. /package/dist/web/assets/{format-bytes-D5sBgIqU.js → format-bytes-D1AQyUCt.js} +0 -0
  424. /package/dist/web/assets/{fortran-free-form-BRQdNzZF.js → fortran-free-form-tP8mRym_.js} +0 -0
  425. /package/dist/web/assets/{gdscript-XBDmLUm9.js → gdscript-nHC2z6Te.js} +0 -0
  426. /package/dist/web/assets/{gdshader-DhVfB3u4.js → gdshader-K-cSmPlW.js} +0 -0
  427. /package/dist/web/assets/{genie-X3K29Y49.js → genie-BAcRhvEe.js} +0 -0
  428. /package/dist/web/assets/{gherkin-DKIx-6cX.js → gherkin-CcifxMLb.js} +0 -0
  429. /package/dist/web/assets/{github-dark-Bf7zdp5s.js → github-dark-67KKlMdB.js} +0 -0
  430. /package/dist/web/assets/{github-dark-default-DqsBBV11.js → github-dark-default-tja8pidR.js} +0 -0
  431. /package/dist/web/assets/{github-dark-dimmed-X2gqRz6w.js → github-dark-dimmed-CtjhYQtx.js} +0 -0
  432. /package/dist/web/assets/{github-dark-high-contrast-Z7tvbGX4.js → github-dark-high-contrast-Bf-LPFgn.js} +0 -0
  433. /package/dist/web/assets/{github-light-BKxt-7zZ.js → github-light-CbJxE7n3.js} +0 -0
  434. /package/dist/web/assets/{github-light-default-B0liF4d4.js → github-light-default-DRQmdHxt.js} +0 -0
  435. /package/dist/web/assets/{github-light-high-contrast-F4encJwr.js → github-light-high-contrast-CJkDgTeM.js} +0 -0
  436. /package/dist/web/assets/{gleam-Pz2bHgBB.js → gleam-D4LCl2ri.js} +0 -0
  437. /package/dist/web/assets/{gn-CgmdNsPU.js → gn-COY8zZmC.js} +0 -0
  438. /package/dist/web/assets/{gnuplot-qLNF5yZ9.js → gnuplot-ROk1VitR.js} +0 -0
  439. /package/dist/web/assets/{go-CSDUNhE7.js → go-H30c3p0C.js} +0 -0
  440. /package/dist/web/assets/{groovy-B3NEnOHF.js → groovy-DBLCaelV.js} +0 -0
  441. /package/dist/web/assets/{gruvbox-dark-hard-DcoftiUX.js → gruvbox-dark-hard-DJy4irps.js} +0 -0
  442. /package/dist/web/assets/{gruvbox-dark-medium-D1wVSuD8.js → gruvbox-dark-medium-Dhfw1QTO.js} +0 -0
  443. /package/dist/web/assets/{gruvbox-dark-soft-DWpunymH.js → gruvbox-dark-soft-BncJU-8w.js} +0 -0
  444. /package/dist/web/assets/{gruvbox-light-hard-rHMvyUdS.js → gruvbox-light-hard-Cq12_YvM.js} +0 -0
  445. /package/dist/web/assets/{gruvbox-light-medium-3O2KiXO7.js → gruvbox-light-medium-DqVja5_v.js} +0 -0
  446. /package/dist/web/assets/{gruvbox-light-soft-lNlXJPwH.js → gruvbox-light-soft-IvwhveGF.js} +0 -0
  447. /package/dist/web/assets/{haskell-aaG3aC0h.js → haskell-DA1JV-MC.js} +0 -0
  448. /package/dist/web/assets/{haxe-CPcHhUDd.js → haxe-V9RTRc5E.js} +0 -0
  449. /package/dist/web/assets/{hcl-CMQNlR_4.js → hcl-hr1lyZ10.js} +0 -0
  450. /package/dist/web/assets/{hjson-DeuklPFj.js → hjson-D3vW3i7X.js} +0 -0
  451. /package/dist/web/assets/{hlsl-BI3hlsaM.js → hlsl-BB85gfJT.js} +0 -0
  452. /package/dist/web/assets/{horizon-C7-gOcjs.js → horizon-DU8OOnj8.js} +0 -0
  453. /package/dist/web/assets/{horizon-bright-Cxt9bRW7.js → horizon-bright-DNMUclW6.js} +0 -0
  454. /package/dist/web/assets/{houston-DbF2G7iD.js → houston-DAiZKf6T.js} +0 -0
  455. /package/dist/web/assets/{hy-DRG6Hs2I.js → hy-C6Zv0LMe.js} +0 -0
  456. /package/dist/web/assets/{image-gallery-CObzTisL.js → image-gallery-CGvlv8zR.js} +0 -0
  457. /package/dist/web/assets/{imba-CxKv_2TW.js → imba-Didnp6Y-.js} +0 -0
  458. /package/dist/web/assets/{ini-BiHsyCdO.js → ini-m0T7q4gQ.js} +0 -0
  459. /package/dist/web/assets/{input-D-dR8RI6.js → input-C7Q8FxeL.js} +0 -0
  460. /package/dist/web/assets/{java-D3RZzgDB.js → java-Buy07jGn.js} +0 -0
  461. /package/dist/web/assets/{javascript-4FmSqG-T.js → javascript-Cjq3CbRL.js} +0 -0
  462. /package/dist/web/assets/{json-CjB236_E.js → json-BSVu8IqD.js} +0 -0
  463. /package/dist/web/assets/{json5-bkfO-Rco.js → json5-NN_Vb7e6.js} +0 -0
  464. /package/dist/web/assets/{jsonc-BwaTeLyv.js → jsonc-BdyZhBUw.js} +0 -0
  465. /package/dist/web/assets/{jsonl-CfxNSwGd.js → jsonl-BsDSNvwi.js} +0 -0
  466. /package/dist/web/assets/{jsonnet-CQETCYrp.js → jsonnet-Gm963F-U.js} +0 -0
  467. /package/dist/web/assets/{jssm-D5o3Zzdz.js → jssm-Ds3bKWgJ.js} +0 -0
  468. /package/dist/web/assets/{jsx-BAMILPLo.js → jsx-BT5mW58X.js} +0 -0
  469. /package/dist/web/assets/{kanagawa-dragon-BhGVxu3R.js → kanagawa-dragon-BvNvf7xd.js} +0 -0
  470. /package/dist/web/assets/{kanagawa-lotus-FH4Y59ro.js → kanagawa-lotus-CuSiliSn.js} +0 -0
  471. /package/dist/web/assets/{kanagawa-wave-eilvP9MB.js → kanagawa-wave-C6g-d6jT.js} +0 -0
  472. /package/dist/web/assets/{kdl-BQ-yvXUK.js → kdl-Bx4kZef-.js} +0 -0
  473. /package/dist/web/assets/{kotlin-BuCx0-VT.js → kotlin-qvaVvCcm.js} +0 -0
  474. /package/dist/web/assets/{kusto-DAN_n_Cy.js → kusto-BPLyNCN6.js} +0 -0
  475. /package/dist/web/assets/{laserwave-Cq2BlxrT.js → laserwave-Sf8ZEhWP.js} +0 -0
  476. /package/dist/web/assets/{lean-Dhq7uXKw.js → lean-DGe6uqgk.js} +0 -0
  477. /package/dist/web/assets/{less-CxaMQb8j.js → less-CqM85x-9.js} +0 -0
  478. /package/dist/web/assets/{lib-BVwmtv2i.js → lib-CD6pOS4u.js} +0 -0
  479. /package/dist/web/assets/{libesm-DdtEjhIi.js → libesm-CPh1Av0r.js} +0 -0
  480. /package/dist/web/assets/{light-plus-B4sMAend.js → light-plus-DBtKwhFA.js} +0 -0
  481. /package/dist/web/assets/{llvm-DQ6-B_6Y.js → llvm-Dpbq3gky.js} +0 -0
  482. /package/dist/web/assets/{log-Bb_vqvu7.js → log-qKMcPm9S.js} +0 -0
  483. /package/dist/web/assets/{logo-BzfYv3A_.js → logo-DFo7i5KH.js} +0 -0
  484. /package/dist/web/assets/{luau-BnAeNR77.js → luau-DGlqWFyL.js} +0 -0
  485. /package/dist/web/assets/{make-B6IFuxpC.js → make-CixdvUQe.js} +0 -0
  486. /package/dist/web/assets/{markdown-Z0xi77Fn.js → markdown-CmRnbngE.js} +0 -0
  487. /package/dist/web/assets/{material-theme-1scy1SYn.js → material-theme-WDUbafB-.js} +0 -0
  488. /package/dist/web/assets/{material-theme-darker-G-JykXQP.js → material-theme-darker-u8y2xpOk.js} +0 -0
  489. /package/dist/web/assets/{material-theme-lighter-BtKkCMVq.js → material-theme-lighter-cyQZK3Tg.js} +0 -0
  490. /package/dist/web/assets/{material-theme-ocean-irrDC89L.js → material-theme-ocean-LBAVvw7k.js} +0 -0
  491. /package/dist/web/assets/{material-theme-palenight-Eq5_K4LM.js → material-theme-palenight-BJqhBxL0.js} +0 -0
  492. /package/dist/web/assets/{matlab-BE3oCXZC.js → matlab-B38sOJee.js} +0 -0
  493. /package/dist/web/assets/{mdx-DYiuFTGw.js → mdx-C8QrE_IW.js} +0 -0
  494. /package/dist/web/assets/{mermaid-DVlaEvtw.js → mermaid-BQ-eeU8A.js} +0 -0
  495. /package/dist/web/assets/{min-dark-DSkeTpQa.js → min-dark-D4gNa5tv.js} +0 -0
  496. /package/dist/web/assets/{min-light-CFv2Q0uF.js → min-light-Dfbhn_PL.js} +0 -0
  497. /package/dist/web/assets/{mipsasm-CiAFuZQ2.js → mipsasm-bLOhJkdm.js} +0 -0
  498. /package/dist/web/assets/{mojo-ARshcaSQ.js → mojo-B0BCOMQW.js} +0 -0
  499. /package/dist/web/assets/{monaco-dotenv-language-BDYnKcnr.js → monaco-dotenv-language-uB-wok6C.js} +0 -0
  500. /package/dist/web/assets/{monokai-CPTV-SOg.js → monokai-Bdw_07OM.js} +0 -0
  501. /package/dist/web/assets/{moonbit-D57B0dZr.js → moonbit-BFdGaeuk.js} +0 -0
  502. /package/dist/web/assets/{move-qgy3G_o9.js → move-CyXQFGPM.js} +0 -0
  503. /package/dist/web/assets/{narrat-CZ3rC-oc.js → narrat-9DvTD3_3.js} +0 -0
  504. /package/dist/web/assets/{nextflow-groovy-BxxIOcI4.js → nextflow-groovy-C4VY3kNY.js} +0 -0
  505. /package/dist/web/assets/{night-owl-B1ryGCda.js → night-owl-feYm66V0.js} +0 -0
  506. /package/dist/web/assets/{night-owl-light-BZvNnvMF.js → night-owl-light-DALqFjvx.js} +0 -0
  507. /package/dist/web/assets/{nix-BVBQi8yv.js → nix-C1kPA9Sw.js} +0 -0
  508. /package/dist/web/assets/{nord-3NXw613h.js → nord-1dvrj1Al.js} +0 -0
  509. /package/dist/web/assets/{nushell-BMOLRalb.js → nushell-DSQ_9Uqj.js} +0 -0
  510. /package/dist/web/assets/{objective-c-AxruEUgh.js → objective-c-Dhcn5ZK8.js} +0 -0
  511. /package/dist/web/assets/{objective-cpp-cFT0qRsD.js → objective-cpp-CGVYCSrp.js} +0 -0
  512. /package/dist/web/assets/{ocaml-_8JH6O8J.js → ocaml-rxXW21An.js} +0 -0
  513. /package/dist/web/assets/{odin-nwYvR0O6.js → odin-ClSJX2DE.js} +0 -0
  514. /package/dist/web/assets/{one-dark-pro-C1LaYJaH.js → one-dark-pro-3gbWDfFW.js} +0 -0
  515. /package/dist/web/assets/{one-light-B7rMc__c.js → one-light-C2q5ZZAw.js} +0 -0
  516. /package/dist/web/assets/{openscad-Cwes8bKO.js → openscad-eHXeafBl.js} +0 -0
  517. /package/dist/web/assets/{pascal-DMt-6tdZ.js → pascal-kKha3Txs.js} +0 -0
  518. /package/dist/web/assets/{pkl-QBTyYQmB.js → pkl-CTiPytg-.js} +0 -0
  519. /package/dist/web/assets/{plastic-BTSUp8Fk.js → plastic-D1Ygezi4.js} +0 -0
  520. /package/dist/web/assets/{plsql-DQv3IFXP.js → plsql-D_gbgZIr.js} +0 -0
  521. /package/dist/web/assets/{po-CNwZx4tW.js → po-BkhbDUnq.js} +0 -0
  522. /package/dist/web/assets/{poimandres-8Os7Qequ.js → poimandres-Do6NywrA.js} +0 -0
  523. /package/dist/web/assets/{polar-b4yK8oD8.js → polar-B3B23wca.js} +0 -0
  524. /package/dist/web/assets/{postcss-sFctwpxx.js → postcss-Dv_YBvWd.js} +0 -0
  525. /package/dist/web/assets/{powerquery-Cg_WrtuN.js → powerquery-fKJWI-Sh.js} +0 -0
  526. /package/dist/web/assets/{powershell-C7Ifz95h.js → powershell-B4Y7KIgx.js} +0 -0
  527. /package/dist/web/assets/{prisma-Z52HUJzQ.js → prisma-J--XdTXX.js} +0 -0
  528. /package/dist/web/assets/{prolog-DLqW-zR1.js → prolog-BBTg4Vb3.js} +0 -0
  529. /package/dist/web/assets/{proto-BOIzxCCJ.js → proto-DWlum3T8.js} +0 -0
  530. /package/dist/web/assets/{puppet-De7TjZIf.js → puppet-ZiUfc9JV.js} +0 -0
  531. /package/dist/web/assets/{purescript-R76ou1WT.js → purescript-CmT8SCHB.js} +0 -0
  532. /package/dist/web/assets/{python-Dprm0tNP.js → python-BURpj-fq.js} +0 -0
  533. /package/dist/web/assets/{qmldir-Clp_jpNw.js → qmldir-CpaiJywQ.js} +0 -0
  534. /package/dist/web/assets/{qss-eQPYYzSo.js → qss-C-zdz0i5.js} +0 -0
  535. /package/dist/web/assets/{r-CCcRHIf5.js → r-K56BeS-s.js} +0 -0
  536. /package/dist/web/assets/{racket-CzE472yp.js → racket-gkn6Z_Jw.js} +0 -0
  537. /package/dist/web/assets/{raku-DQwXrTSi.js → raku-C0ZBuKlx.js} +0 -0
  538. /package/dist/web/assets/{red-KCS6wyop.js → red-BWZ6x-3p.js} +0 -0
  539. /package/dist/web/assets/{reg-CcrAddUh.js → reg-Dt_EsyGR.js} +0 -0
  540. /package/dist/web/assets/{regexp-DDbud-Ri.js → regexp-CT6BLG2C.js} +0 -0
  541. /package/dist/web/assets/{rel-99GDgaHp.js → rel-MJ2LcyzJ.js} +0 -0
  542. /package/dist/web/assets/{resolve-theme-CW2niGV2.js → resolve-theme-B_ukrg_8.js} +0 -0
  543. /package/dist/web/assets/{riscv-Dm2eNv0R.js → riscv-BZYWzMAA.js} +0 -0
  544. /package/dist/web/assets/{ron-CzFt0ygH.js → ron-CLbwHWMn.js} +0 -0
  545. /package/dist/web/assets/{rose-pine-Ck6lCrCP.js → rose-pine-BPiI_daR.js} +0 -0
  546. /package/dist/web/assets/{rose-pine-dawn-vtALRJNj.js → rose-pine-dawn-BjkJc7dW.js} +0 -0
  547. /package/dist/web/assets/{rose-pine-moon-DPto8qXw.js → rose-pine-moon-BQSxkt7g.js} +0 -0
  548. /package/dist/web/assets/{rosmsg-NtS6429H.js → rosmsg-yt4151pJ.js} +0 -0
  549. /package/dist/web/assets/{rust-BjIXvF9h.js → rust-1dAcengL.js} +0 -0
  550. /package/dist/web/assets/{sass-CQ23wf5E.js → sass-DUgtQJsg.js} +0 -0
  551. /package/dist/web/assets/{scala-LfKLM143.js → scala-CxoGYpS0.js} +0 -0
  552. /package/dist/web/assets/{scheme-D3SLBXlF.js → scheme-B2K3u9e3.js} +0 -0
  553. /package/dist/web/assets/{sdbl-ODRd2rGN.js → sdbl-C0Ygk0n6.js} +0 -0
  554. /package/dist/web/assets/{shellscript-3KB1G_oA.js → shellscript-C2CvhqMk.js} +0 -0
  555. /package/dist/web/assets/{slack-dark-DTtyWmIy.js → slack-dark-DYdgzRGj.js} +0 -0
  556. /package/dist/web/assets/{slack-ochin-Bu0PApk3.js → slack-ochin-X-WD0WCg.js} +0 -0
  557. /package/dist/web/assets/{smalltalk-haMT-Sbe.js → smalltalk-Dp7g_zb9.js} +0 -0
  558. /package/dist/web/assets/{snazzy-light-DXJzefE9.js → snazzy-light-DWHWr-a6.js} +0 -0
  559. /package/dist/web/assets/{solarized-dark-B6LyYa18.js → solarized-dark-CxdLsjvb.js} +0 -0
  560. /package/dist/web/assets/{solarized-light-QLADebkv.js → solarized-light-Cz9rAvG5.js} +0 -0
  561. /package/dist/web/assets/{solidity-mzfq1aDc.js → solidity-D33gHHdn.js} +0 -0
  562. /package/dist/web/assets/{splunk-CyH4RWBp.js → splunk-BFbOroYw.js} +0 -0
  563. /package/dist/web/assets/{sql-CWVlT5D3.js → sql-ChpxvmdY.js} +0 -0
  564. /package/dist/web/assets/{ssh-config-DNvQJ1Qx.js → ssh-config-CUuSQ6JG.js} +0 -0
  565. /package/dist/web/assets/{streaming-store-uDu3PoN1.js → streaming-store-BYukdB56.js} +0 -0
  566. /package/dist/web/assets/{stylus-ByQQdtmf.js → stylus-DDihP0_W.js} +0 -0
  567. /package/dist/web/assets/{swift-D1cpY4ow.js → swift-XoalUn4x.js} +0 -0
  568. /package/dist/web/assets/{synthwave-84-BDF_-QiW.js → synthwave-84-DjkG0Bff.js} +0 -0
  569. /package/dist/web/assets/{system-verilog-BYrkMydC.js → system-verilog-f_pLKMYd.js} +0 -0
  570. /package/dist/web/assets/{systemd-CQm3zyK0.js → systemd-DjDcLfxp.js} +0 -0
  571. /package/dist/web/assets/{talonscript-DMW5CzE1.js → talonscript-BpUzYcDl.js} +0 -0
  572. /package/dist/web/assets/{tasl-BhFzMCr2.js → tasl-DP-cUbg8.js} +0 -0
  573. /package/dist/web/assets/{tcl-D7VEAF-9.js → tcl-BHoy-dfE.js} +0 -0
  574. /package/dist/web/assets/{terraform-DdT5A0Vy.js → terraform-vCVxN25B.js} +0 -0
  575. /package/dist/web/assets/{tokyo-night-WGLMM6Q9.js → tokyo-night-D0u4xZba.js} +0 -0
  576. /package/dist/web/assets/{toml-CZ7ipXHZ.js → toml-BS8D0__A.js} +0 -0
  577. /package/dist/web/assets/{tsv-B1zVvoTr.js → tsv-DCb-NQh3.js} +0 -0
  578. /package/dist/web/assets/{tsx-D4HwRUBq.js → tsx-DF8YeRgG.js} +0 -0
  579. /package/dist/web/assets/{turtle-B4r8KEJH.js → turtle-Bhx1GlIy.js} +0 -0
  580. /package/dist/web/assets/{typescript-BnHhxMtp.js → typescript-CAVD2Wb6.js} +0 -0
  581. /package/dist/web/assets/{typespec-DP2q46GT.js → typespec-D7mchdY_.js} +0 -0
  582. /package/dist/web/assets/{typst-DFsD_yaD.js → typst-CeXzq6Yw.js} +0 -0
  583. /package/dist/web/assets/{use-is-mobile-BQ9Bv5Ga.js → use-is-mobile-TkcwP1Gx.js} +0 -0
  584. /package/dist/web/assets/{use-visual-viewport-CbwnktSG.js → use-visual-viewport-Lp878uzs.js} +0 -0
  585. /package/dist/web/assets/{v-Bji8hSVT.js → v-DhzO54je.js} +0 -0
  586. /package/dist/web/assets/{vala-D3TIgGmU.js → vala-Dw_RAk3E.js} +0 -0
  587. /package/dist/web/assets/{vb-DvcxFS2q.js → vb-CiXdNh2Y.js} +0 -0
  588. /package/dist/web/assets/{vendor-xterm-BqPDsE9b.js → vendor-xterm-BHEY6Ctu.js} +0 -0
  589. /package/dist/web/assets/{verilog-Dy7cF35d.js → verilog-fITdxqwV.js} +0 -0
  590. /package/dist/web/assets/{vesper-BzjwUv9J.js → vesper-B1wm_At7.js} +0 -0
  591. /package/dist/web/assets/{vhdl-DuFnFNmW.js → vhdl-ebHjqKp9.js} +0 -0
  592. /package/dist/web/assets/{viml-yAPgKlbz.js → viml-Dgmnoxoo.js} +0 -0
  593. /package/dist/web/assets/{vitesse-black-oSSQdyIZ.js → vitesse-black-BZ5gMb7l.js} +0 -0
  594. /package/dist/web/assets/{vitesse-dark-BZxwPihB.js → vitesse-dark-Lij7kkOt.js} +0 -0
  595. /package/dist/web/assets/{vitesse-light-uIigycvt.js → vitesse-light-DW6Wq7VC.js} +0 -0
  596. /package/dist/web/assets/{vyper-CX3NAxm5.js → vyper-6FxDnVNg.js} +0 -0
  597. /package/dist/web/assets/{wasm-6mEOOvVa.js → wasm-BDxaNPCj.js} +0 -0
  598. /package/dist/web/assets/{wasm-D9td37qO.js → wasm-DKhAtzfe.js} +0 -0
  599. /package/dist/web/assets/{wenyan-lwNIHgk6.js → wenyan-C0k0DYG_.js} +0 -0
  600. /package/dist/web/assets/{wgsl-DQAzCfog.js → wgsl-Y2zagbdn.js} +0 -0
  601. /package/dist/web/assets/{wikitext-DTXmt5NE.js → wikitext-C4QGWzkY.js} +0 -0
  602. /package/dist/web/assets/{wit-Bdw_7WDH.js → wit-CQESzjHy.js} +0 -0
  603. /package/dist/web/assets/{wolfram-DqyHDZ07.js → wolfram-B-ia69Ia.js} +0 -0
  604. /package/dist/web/assets/{yaml-CPXhdByd.js → yaml-CEYYTQPm.js} +0 -0
  605. /package/dist/web/assets/{zenscript-6s_i5zek.js → zenscript-BggWXIeJ.js} +0 -0
  606. /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
- /** Above this many buffered bytes, drop delta AUs until the next keyframe rather than
29
- * queueing forever — a slow WAN/tunnel link must degrade to "waits ~2s for a keyframe",
30
- * never to unbounded memory growth or an ever-growing latency queue. */
31
- const BACKPRESSURE_THRESHOLD_BYTES = 512 * 1024;
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
- private exitResolve: (() => void) | null = null;
44
- /** Resolves once the underlying ffmpeg process has actually exited (not merely asked to). */
45
- readonly exited: Promise<void>;
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(private readonly ws: RemoteDesktopSocket, private readonly display: RemoteDisplay | null) {
48
- this.exited = new Promise((resolve) => { this.exitResolve = resolve; });
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
- onAccessUnit: (au) => this.handleAccessUnit(au),
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
- this.exitResolve?.();
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
- this.heartbeatTimer = setInterval(() => this.checkHeartbeat(), HEARTBEAT_INTERVAL_MS);
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
- private checkHeartbeat(): void {
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
- private handleAccessUnit(au: AccessUnit): void {
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({ type: "config", codec }));
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
- const buffered = this.ws.getBufferedAmount?.() ?? 0;
160
- if (buffered > BACKPRESSURE_THRESHOLD_BYTES && !au.isKey) {
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 ? 1 : 0;
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(ws: RemoteDesktopSocket, displayId?: string): Promise<RemoteDesktopSession> {
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 session = new RemoteDesktopSession(ws, await resolveDisplay(displayId));
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;