@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
@@ -16,12 +16,22 @@
16
16
  */
17
17
  import { useCallback, useEffect, useRef, useState } from "react";
18
18
  import { api } from "@/lib/api-client";
19
+ import { useSettingsStore } from "@/stores/settings-store";
19
20
  import { resolveRemoteDesktopWsUrl } from "./remote-desktop-ws-url";
20
21
  import { useH264CanvasDecoder, type DecoderStatus } from "./use-h264-canvas-decoder";
22
+ import {
23
+ DEFAULT_PRESET_ID, parsePresetId, type QualityChoice, type QualityPresetId,
24
+ } from "../../../shared/remote-desktop-quality";
25
+ import { CLIPBOARD_READ_DELAY_MS, writeClientClipboard } from "./remote-desktop-clipboard-client";
26
+ import { useOpusAudioPlayer } from "./use-opus-audio-player";
21
27
 
22
28
  export type RemoteDesktopConnState = "connecting" | "streaming" | "error" | "closed";
23
29
 
24
30
  const PING_INTERVAL_MS = 5_000;
31
+ /** First byte of every binary message. 0/1 are the video's delta/key flags, unchanged; audio is
32
+ * a third kind on the same socket rather than a second connection, which would need its own
33
+ * nonce and its own eviction rules. */
34
+ const FRAME_AUDIO = 2;
25
35
 
26
36
  export interface UseRemoteDesktopConnectionResult {
27
37
  connState: RemoteDesktopConnState;
@@ -37,6 +47,69 @@ export interface UseRemoteDesktopConnectionResult {
37
47
  getTotalBytes: () => number;
38
48
  /** Forwards the decoder's own frame counter (see `use-h264-canvas-decoder.ts`). */
39
49
  getFrameCount: () => number;
50
+ /** Pixel size of the decoded picture, 0×0 until the first frame. What the `original`/`custom`
51
+ * scale modes multiply — see `remote-desktop-view-style.ts`. */
52
+ frameSize: { width: number; height: number };
53
+ /** The rung the *server* is actually streaming at, and whether it is free to change it
54
+ * itself. Both come from the server (`config`, then `quality` on every change) rather than
55
+ * from what was last requested: the host echoes the rung it settled on, and `held` says how
56
+ * much of that rung's bitrate the link is currently allowing (1 = all of it). */
57
+ quality: { preset: QualityPresetId; held: number };
58
+ /** Choose a rung. There is no `auto`: the adaptation always runs beneath whichever is
59
+ * chosen, so the choice is a ceiling rather than an off switch. */
60
+ setQuality: (preset: QualityChoice) => void;
61
+ /** Commit RustDesk's custom rung: a bitrate percentage and an fps. Also selects `custom`. */
62
+ setCustomQuality: (percent: number, fps: number) => void;
63
+ /** Whether the host pointer is drawn into the frames. Device pref, not a server report — the
64
+ * session never changes this by itself, so there is nothing to echo back. */
65
+ showCursor: boolean;
66
+ /** Toggle the host pointer. Respawns the host's ffmpeg (~400ms of held picture): every
67
+ * grabber takes `-draw_mouse`/`-capture_cursor` at startup and none can be retuned live. */
68
+ setShowCursor: (show: boolean) => void;
69
+ /** The encoder the host is *actually* running, or null while it is on its own first choice.
70
+ * Read back from the server rather than from the pref, because the server drops an encoder
71
+ * this host cannot run — so the menu shows what is live, not what was asked for. */
72
+ codec: string | null;
73
+ /** Ask the host for another H.264 encoder. Also a respawn. */
74
+ setCodec: (encoder: string | null) => void;
75
+ /** Whether the host is streaming its audio. Reported by the server, because it also turns
76
+ * this back off by itself when the host has no loopback device or its ffmpeg dies. */
77
+ audioOn: boolean;
78
+ /** Ask the host to start/stop streaming audio. Starting also resumes the `AudioContext`, so
79
+ * it has to be called from a real user gesture (a click), not from an effect. */
80
+ setAudioOn: (on: boolean) => void;
81
+ /** Audio tracks for the recorder to mux in; empty while audio is off. */
82
+ getAudioTracks: () => MediaStreamTrack[];
83
+ /** Local input blocked + host monitor blanked. Server-reported, because an engage can fail
84
+ * (a screen lock already holds the keyboard) and the switch must show what actually holds. */
85
+ privacyOn: boolean;
86
+ /** Why the last engage failed, or null. */
87
+ privacyError: string | null;
88
+ setPrivacyOn: (on: boolean) => void;
89
+ /** The host mode id that is live now, read back from the host after every switch — so the
90
+ * picker ticks what the host is really doing rather than what was asked for. Null until the
91
+ * host has reported once (a non-X11 host never does). */
92
+ hostModeId: string | null;
93
+ /** The mode that was live before this session changed it, or null when it has changed none.
94
+ * Drives the "Original" item, exactly as RustDesk's `isOriginalResolutionSet` does. */
95
+ hostOriginalModeId: string | null;
96
+ /** Why the last switch failed, or null. */
97
+ resolutionError: string | null;
98
+ /** Ask the host to change its own display mode. Respawns the capture: `-video_size` is fixed
99
+ * at spawn, so the old grabber would keep reading a rectangle the framebuffer no longer has. */
100
+ setHostMode: (modeId: string) => void;
101
+ /** Text the host copied that could NOT be written to this device's clipboard automatically —
102
+ * `navigator.clipboard` is secure-context only and PPM is usually plain HTTP on a LAN. Non-null
103
+ * means the UI must offer a real click to copy it; null means nothing is pending. */
104
+ pendingHostClipboard: string | null;
105
+ /** Discard the pending text (copied it, or dismissed the notice). */
106
+ clearHostClipboard: () => void;
107
+ /** Put client text on the host clipboard; `paste` also presses the host's paste shortcut,
108
+ * with Shift when the user pressed Ctrl+Shift+V (a terminal's paste). */
109
+ sendClipboard: (text: string, paste: boolean, shift?: boolean) => void;
110
+ /** Ask the host for its clipboard, after `CLIPBOARD_READ_DELAY_MS` so a just-forwarded copy
111
+ * combo has actually landed on the host clipboard first. */
112
+ requestHostClipboard: () => void;
40
113
  }
41
114
 
42
115
  export interface UseRemoteDesktopConnectionOptions {
@@ -54,19 +127,158 @@ export function useRemoteDesktopConnection(
54
127
  const [connState, setConnState] = useState<RemoteDesktopConnState>("connecting");
55
128
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
56
129
  const [generation, setGeneration] = useState(0); // bump to force a manual reconnect
130
+ const [quality, setQualityState] = useState<{ preset: QualityPresetId; held: number }>(() => {
131
+ // Reflect the restored choice immediately: the server confirms it a moment later with its
132
+ // own `quality` message, and until then the picker must not tick the wrong rung.
133
+ const stored = parsePresetId(useSettingsStore.getState().remoteDesktopQuality);
134
+ return { preset: stored ?? DEFAULT_PRESET_ID, held: 1 };
135
+ });
136
+ /** The user's pinned choice, re-applied after a reconnect (the server always starts a fresh
137
+ * session on `auto`). A ref, not state: this must never be a dependency of the effect that
138
+ * owns the socket, or changing quality would tear down the connection it is tuning.
139
+ *
140
+ * Seeded from the device-local pref via `getState()` rather than the hook, for that same
141
+ * reason — subscribing would make the socket effect re-run on a quality change. `useRef`
142
+ * reads its argument on the first render only, which is exactly the "restore on open"
143
+ * semantics wanted here. */
144
+ const pinnedRef = useRef<QualityChoice | null>(
145
+ parsePresetId(useSettingsStore.getState().remoteDesktopQuality),
146
+ );
147
+ const [pendingHostClipboard, setPendingHostClipboard] = useState<string | null>(null);
148
+ const clipboardReadTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
149
+ const showCursor = useSettingsStore((s) => s.remoteDesktopShowCursor);
150
+ const clipboardSync = useSettingsStore((s) => s.remoteDesktopClipboardSync);
151
+ /** Mirrors of the two prefs above for the socket's own closures. The socket effect runs only
152
+ * on `generation`/`displayId`, so `onopen`/`onmessage` capture whatever these were when it
153
+ * last ran — reading the pref through a ref is what keeps a toggle mid-session honest
154
+ * without making a settings change tear the connection down. */
155
+ const showCursorRef = useRef(showCursor);
156
+ const clipboardSyncRef = useRef(clipboardSync);
157
+ useEffect(() => { showCursorRef.current = showCursor; }, [showCursor]);
158
+ useEffect(() => { clipboardSyncRef.current = clipboardSync; }, [clipboardSync]);
159
+ /** Same ref-for-the-socket-closure reasoning; seeded from the pref so the first ffmpeg is
160
+ * already the right one. `codec` state is what the server confirmed. */
161
+ const codecRef = useRef(useSettingsStore.getState().remoteDesktopCodec);
162
+ const [codec, setCodecState] = useState<string | null>(codecRef.current);
57
163
 
58
164
  const decoder = useH264CanvasDecoder(canvasRef);
165
+ const audio = useOpusAudioPlayer();
166
+ const [audioOn, setAudioOn] = useState(false);
167
+ const [privacyOn, setPrivacyOn] = useState(false);
168
+ const [privacyError, setPrivacyError] = useState<string | null>(null);
169
+ const [hostModeId, setHostModeId] = useState<string | null>(null);
170
+ const [hostOriginalModeId, setHostOriginalModeId] = useState<string | null>(null);
171
+ const [resolutionError, setResolutionError] = useState<string | null>(null);
59
172
 
60
173
  const sendMessage = useCallback((msg: Record<string, unknown>) => {
61
174
  const ws = wsRef.current;
62
175
  if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg));
63
176
  }, []);
64
177
 
178
+ /** Adopt whatever rung the server reports, from either `config` or `quality`. */
179
+ const applyQualityMessage = useCallback((msg: Record<string, unknown>) => {
180
+ const preset = parsePresetId(msg.preset);
181
+ if (!preset) return;
182
+ // `held` is absent whenever the link is allowing the whole rung, which is the normal case.
183
+ const held = typeof msg.held === "number" && msg.held > 0 && msg.held < 1 ? msg.held : 1;
184
+ setQualityState({ preset, held });
185
+ }, []);
186
+
187
+ /** The wire form of a rung. `custom` carries its two numbers, because the server cannot look
188
+ * them up — they live in this device's prefs, and the same session may be reconnected to. */
189
+ const qualityMessage = useCallback((choice: QualityChoice): Record<string, unknown> => {
190
+ if (choice !== "custom") return { type: "quality", preset: choice };
191
+ const st = useSettingsStore.getState();
192
+ return {
193
+ type: "quality", preset: "custom",
194
+ percent: st.remoteDesktopCustomQualityPercent,
195
+ fps: st.remoteDesktopCustomFps,
196
+ };
197
+ }, []);
198
+
199
+ const setQuality = useCallback((preset: QualityChoice) => {
200
+ pinnedRef.current = preset;
201
+ // Remembered per device so a LAN desktop reopens on its 60fps rung without being asked
202
+ // again, while a phone keeps whatever that phone chose.
203
+ useSettingsStore.getState().setRemoteDesktopQuality(preset);
204
+ sendMessage(qualityMessage(preset));
205
+ }, [qualityMessage, sendMessage]);
206
+
207
+ /** Commit the custom dialog's numbers. Writes them first so `qualityMessage` reads the new
208
+ * pair, then re-sends — selecting "Custom" and then dragging a slider are the same request
209
+ * as far as the host is concerned. */
210
+ const setCustomQuality = useCallback((percent: number, fps: number) => {
211
+ const st = useSettingsStore.getState();
212
+ st.setRemoteDesktopCustomQuality(percent, fps);
213
+ st.setRemoteDesktopQuality("custom");
214
+ pinnedRef.current = "custom";
215
+ sendMessage(qualityMessage("custom"));
216
+ }, [qualityMessage, sendMessage]);
217
+
218
+ const requestPrivacy = useCallback((on: boolean) => {
219
+ // Not optimistic, unlike audio: claiming the host is locked out when the grab was refused
220
+ // is the one wrong answer that matters here, so the switch waits for the server.
221
+ setPrivacyError(null);
222
+ sendMessage({ type: "privacy", enabled: on });
223
+ }, [sendMessage]);
224
+
225
+ const requestAudio = useCallback((on: boolean) => {
226
+ // Optimistic: the server confirms with its own `audio` message (and contradicts this when
227
+ // the host cannot capture), but the AudioContext must be created inside this click.
228
+ setAudioOn(on);
229
+ if (!on) audio.reset();
230
+ sendMessage({ type: "audio", enabled: on });
231
+ }, [audio, sendMessage]);
232
+
233
+ const setCodec = useCallback((encoder: string | null) => {
234
+ codecRef.current = encoder;
235
+ useSettingsStore.getState().setRemoteDesktopCodec(encoder);
236
+ // null means "your choice": there is no message for that (ffmpeg has to be told *some*
237
+ // encoder), so the session keeps the current one until the next connect. Reflected locally
238
+ // so the menu does not claim the switch already happened.
239
+ setCodecState(encoder);
240
+ if (encoder) sendMessage({ type: "codec", encoder });
241
+ }, [sendMessage]);
242
+
243
+ const setHostMode = useCallback((modeId: string) => {
244
+ // Not optimistic, for the same reason as privacy: the X server can refuse a mode, and a
245
+ // picker that ticked it anyway would be describing a screen nobody has.
246
+ setResolutionError(null);
247
+ sendMessage({ type: "resolution", modeId });
248
+ }, [sendMessage]);
249
+
250
+ const setShowCursor = useCallback((show: boolean) => {
251
+ showCursorRef.current = show;
252
+ useSettingsStore.getState().setRemoteDesktopShowCursor(show);
253
+ sendMessage({ type: "cursor", show });
254
+ }, [sendMessage]);
255
+
256
+ const sendClipboard = useCallback((text: string, paste: boolean, shift = false) => {
257
+ sendMessage({ type: "clipboard", text, paste, shift });
258
+ }, [sendMessage]);
259
+
260
+ /** Coalesced: holding Ctrl+C repeats the keydown, and one read per repeat would spawn a
261
+ * clipboard helper per frame on the host. */
262
+ const requestHostClipboard = useCallback(() => {
263
+ if (clipboardReadTimer.current) clearTimeout(clipboardReadTimer.current);
264
+ clipboardReadTimer.current = setTimeout(() => {
265
+ clipboardReadTimer.current = null;
266
+ sendMessage({ type: "clipboardRead" });
267
+ }, CLIPBOARD_READ_DELAY_MS);
268
+ }, [sendMessage]);
269
+
270
+ const clearHostClipboard = useCallback(() => setPendingHostClipboard(null), []);
271
+
272
+ useEffect(() => () => { if (clipboardReadTimer.current) clearTimeout(clipboardReadTimer.current); }, []);
273
+
65
274
  useEffect(() => {
66
275
  let cancelled = false;
67
276
  let pingTimer: ReturnType<typeof setInterval> | null = null;
68
277
  setConnState("connecting");
69
278
  setErrorMessage(null);
279
+ // A fresh session engages nothing, and the old session's grab died with its socket.
280
+ setPrivacyOn(false);
281
+ setPrivacyError(null);
70
282
  totalBytesRef.current = 0;
71
283
  decoder.reset();
72
284
 
@@ -89,7 +301,14 @@ export function useRemoteDesktopConnection(
89
301
  wsRef.current = ws;
90
302
 
91
303
  ws.onopen = () => {
92
- ws.send(JSON.stringify({ type: "auth", nonce, ...(displayId ? { displayId } : {}) }));
304
+ // The cursor pref rides along with `auth`: the grabber takes it at startup, so sending
305
+ // it as its own message would respawn ffmpeg immediately after every connect.
306
+ ws.send(JSON.stringify({
307
+ type: "auth", nonce, ...(displayId ? { displayId } : {}), cursor: showCursorRef.current,
308
+ ...(codecRef.current ? { codec: codecRef.current } : {}),
309
+ }));
310
+ // Re-pin across a reconnect so a deliberate choice is not silently reset to balanced.
311
+ if (pinnedRef.current) ws.send(JSON.stringify(qualityMessage(pinnedRef.current)));
93
312
  pingTimer = setInterval(() => sendMessage({ type: "ping" }), PING_INTERVAL_MS);
94
313
  };
95
314
  ws.onmessage = (event) => {
@@ -97,7 +316,44 @@ export function useRemoteDesktopConnection(
97
316
  let msg: Record<string, unknown>;
98
317
  try { msg = JSON.parse(event.data); } catch { return; }
99
318
  if (msg.type === "config" && typeof msg.codec === "string") {
319
+ // A second `config` arrives on every quality change: the new ffmpeg has its own SPS
320
+ // and resolution, and `configure()` closes the old decoder and waits for a keyframe.
321
+ applyQualityMessage(msg);
322
+ setCodecState(typeof msg.encoder === "string" ? msg.encoder : null);
100
323
  decoder.configure(msg.codec).then(() => { if (!cancelled) setConnState("streaming"); });
324
+ } else if (msg.type === "quality") {
325
+ applyQualityMessage(msg);
326
+ } else if (msg.type === "codec") {
327
+ setCodecState(typeof msg.encoder === "string" ? msg.encoder : null);
328
+ } else if (msg.type === "resolution") {
329
+ setHostModeId(typeof msg.modeId === "string" ? msg.modeId : null);
330
+ setHostOriginalModeId(typeof msg.original === "string" ? msg.original : null);
331
+ setResolutionError(null);
332
+ } else if (msg.type === "resolutionError") {
333
+ setResolutionError(typeof msg.message === "string" ? msg.message : "The host refused the mode.");
334
+ } else if (msg.type === "privacy") {
335
+ setPrivacyOn(msg.enabled === true);
336
+ setPrivacyError(typeof msg.reason === "string" ? msg.reason : null);
337
+ } else if (msg.type === "audio") {
338
+ const on = msg.enabled === true;
339
+ setAudioOn(on);
340
+ if (!on) { audio.reset(); return; }
341
+ // The host announces its layout once the encoder's header has been parsed, which
342
+ // is what the decoder needs before the first packet arrives.
343
+ const channels = typeof msg.channels === "number" ? msg.channels : 2;
344
+ const rate = typeof msg.sampleRate === "number" ? msg.sampleRate : 48_000;
345
+ void audio.configure(channels, rate);
346
+ } else if (msg.type === "clipboard" && typeof msg.text === "string") {
347
+ // Sync can be turned off while a read is already in flight (the client's own
348
+ // `clipboardRead` is delayed by 350ms), and "off" has to mean the host's text never
349
+ // reaches this device's clipboard — so the check is here, at the one place it lands.
350
+ if (!clipboardSyncRef.current) return;
351
+ const text = msg.text;
352
+ // Only surface the manual affordance when the automatic write actually failed, so a
353
+ // secure-context client syncs silently and an HTTP one gets one button.
354
+ void writeClientClipboard(text).then((wrote) => {
355
+ if (!wrote && !cancelled) setPendingHostClipboard(text);
356
+ });
101
357
  } else if (msg.type === "error") {
102
358
  setConnState("error");
103
359
  setErrorMessage(typeof msg.message === "string" ? msg.message : "Server error");
@@ -107,6 +363,10 @@ export function useRemoteDesktopConnection(
107
363
  const bytes = new Uint8Array(event.data as ArrayBuffer);
108
364
  if (bytes.length < 1) return;
109
365
  totalBytesRef.current += bytes.length;
366
+ // Audio shares the socket, so the kind byte has to be read before anything is treated
367
+ // as a video access unit — feeding an Opus packet to the H.264 decoder is a decode
368
+ // error per 20ms, i.e. a stream that dies the moment sound is turned on.
369
+ if (bytes[0] === FRAME_AUDIO) { audio.decodePacket(bytes.subarray(1)); return; }
110
370
  decoder.decodeAccessUnit(bytes.subarray(1), bytes[0] === 1);
111
371
  };
112
372
  ws.onerror = () => { if (!cancelled) { setConnState("error"); setErrorMessage("WebSocket error"); } };
@@ -134,6 +394,7 @@ export function useRemoteDesktopConnection(
134
394
  try { wsRef.current?.close(); } catch { /* tearing down regardless */ }
135
395
  wsRef.current = null;
136
396
  try { decoder.reset(); } catch { /* tearing down regardless */ }
397
+ try { audio.reset(); } catch { /* tearing down regardless */ }
137
398
  };
138
399
  // eslint-disable-next-line react-hooks/exhaustive-deps -- reconnect is driven by `generation` + the display pick
139
400
  }, [generation, displayId]);
@@ -150,5 +411,27 @@ export function useRemoteDesktopConnection(
150
411
  reconnect,
151
412
  getTotalBytes,
152
413
  getFrameCount: decoder.getFrameCount,
414
+ frameSize: decoder.frameSize,
415
+ quality,
416
+ setQuality,
417
+ setCustomQuality,
418
+ showCursor,
419
+ setShowCursor,
420
+ codec,
421
+ setCodec,
422
+ audioOn,
423
+ setAudioOn: requestAudio,
424
+ getAudioTracks: audio.getTracks,
425
+ privacyOn,
426
+ privacyError,
427
+ setPrivacyOn: requestPrivacy,
428
+ hostModeId,
429
+ hostOriginalModeId,
430
+ resolutionError,
431
+ setHostMode,
432
+ pendingHostClipboard,
433
+ clearHostClipboard,
434
+ sendClipboard,
435
+ requestHostClipboard,
153
436
  };
154
437
  }
@@ -32,8 +32,52 @@ export interface RemoteDisplay {
32
32
  height: number;
33
33
  }
34
34
 
35
+ /** Whether the host can stream its own audio, and why not when it cannot. Same shape and same
36
+ * reasoning as `ClipboardSupport` below — informational, never a gate. */
37
+ export interface AudioSupport {
38
+ available: boolean;
39
+ reason: string | null;
40
+ }
41
+
42
+ /** Whether the host can block its local input and blank its monitor. `canBlank` false means
43
+ * input blocking works on its own — which is still worth offering. */
44
+ export interface PrivacySupport {
45
+ available: boolean;
46
+ reason: string | null;
47
+ canBlank: boolean;
48
+ }
49
+
50
+ /** One of the host's own display modes. `id` is an XRandR XID as a string — a 64-bit value that
51
+ * has to survive JSON, so it is never a number. */
52
+ export interface HostMode {
53
+ id: string;
54
+ width: number;
55
+ height: number;
56
+ refresh: number;
57
+ current: boolean;
58
+ preferred: boolean;
59
+ }
60
+
61
+ /** The host's display modes. Empty with a `reason` on a host whose resolution cannot be changed
62
+ * (anything but X11 today) — informational, never a gate, like the two above. */
63
+ export interface HostResolutions {
64
+ output: string | null;
65
+ modes: HostMode[];
66
+ reason: string | null;
67
+ }
68
+
69
+ /** Clipboard sync is not a gate (see the server-side note): it never blocks the viewer, it only
70
+ * needs explaining when the host has no tool for it. */
71
+ export interface ClipboardSupport {
72
+ available: boolean;
73
+ action: RequirementAction | null;
74
+ }
75
+
35
76
  export interface RemoteDesktopCapabilities {
36
77
  displays: RemoteDisplay[];
78
+ /** Every H.264 encoder that really encodes on the host, preference order; the first is the
79
+ * one a session uses unless asked otherwise. Empty when ffmpeg is missing. */
80
+ encoders: string[];
37
81
  ffmpegAvailable: boolean;
38
82
  videoAvailable: boolean;
39
83
  inputAvailable: boolean;
@@ -43,6 +87,36 @@ export interface RemoteDesktopCapabilities {
43
87
  requirements: RemoteDesktopRequirement[];
44
88
  videoReady: boolean;
45
89
  inputReady: boolean;
90
+ clipboard: ClipboardSupport;
91
+ audio: AudioSupport;
92
+ privacy: PrivacySupport;
93
+ resolutions: HostResolutions;
94
+ }
95
+
96
+ /**
97
+ * Fills in whatever the host did not send.
98
+ *
99
+ * The client and the host are not one deployable: a browser holds a bundle until it reloads, a
100
+ * phone holds one until its service worker updates, and `dist/ppm` is a separately-built binary
101
+ * — so a *new* viewer routinely talks to an *older* `/capabilities`. Every field below arrived
102
+ * in a different version, and reading one that predates the host crashes the whole viewer
103
+ * rather than degrading: `caps.resolutions.modes` on a host that has never heard of
104
+ * `resolutions` is a `TypeError` past the last error boundary, i.e. the blank "could not finish
105
+ * rendering" page. The call sites cannot be trusted to each remember an optional chain, and a
106
+ * missing one is invisible until that exact version pairing happens — so the shape is repaired
107
+ * once, here, and the interface stays non-optional because it is now actually true.
108
+ */
109
+ export function normalizeCapabilities(raw: RemoteDesktopCapabilities): RemoteDesktopCapabilities {
110
+ return {
111
+ ...raw,
112
+ displays: raw.displays ?? [],
113
+ encoders: raw.encoders ?? [],
114
+ requirements: raw.requirements ?? [],
115
+ clipboard: raw.clipboard ?? { available: false, action: null },
116
+ audio: raw.audio ?? { available: false, reason: null },
117
+ privacy: raw.privacy ?? { available: false, reason: null, canBlank: false },
118
+ resolutions: raw.resolutions ?? { output: null, modes: [], reason: null },
119
+ };
46
120
  }
47
121
 
48
122
  export const READINESS_POLL_MS = 2000;
@@ -62,7 +136,7 @@ export function useRemoteDesktopReadiness(poll: boolean): {
62
136
  useEffect(() => {
63
137
  let cancelled = false;
64
138
  api.get<RemoteDesktopCapabilities>("/api/remote-desktop/capabilities")
65
- .then((c) => { if (!cancelled) { setCaps(c); setFailed(false); } })
139
+ .then((c) => { if (!cancelled) { setCaps(normalizeCapabilities(c)); setFailed(false); } })
66
140
  .catch(() => { if (!cancelled) setFailed(true); });
67
141
  return () => { cancelled = true; };
68
142
  }, [tick]);
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Records the viewer's canvas (and the session audio, when it is on) to a file the browser
3
+ * downloads on stop. See `remote-desktop-recording.ts` for why the canvas is the source.
4
+ *
5
+ * Chunks are kept in memory until stop, which is the honest limit of this approach: a long
6
+ * recording is a long array of blobs. `MAX_RECORDING_MS` stops it by itself rather than letting
7
+ * a session left recording overnight grow until the tab is killed — a file that stops after two
8
+ * hours is recoverable, a tab that dies takes everything with it.
9
+ */
10
+ import { useCallback, useEffect, useRef, useState } from "react";
11
+ import {
12
+ downloadBlob, extensionForMime, pickRecordingMime, recordingFileName,
13
+ } from "./remote-desktop-recording";
14
+
15
+ /** Two hours. Long enough for any real session, short of the point where the blob array is the
16
+ * problem (~2 GB at a 1440p60 rung). */
17
+ export const MAX_RECORDING_MS = 2 * 60 * 60 * 1000;
18
+
19
+ export interface UseRemoteDesktopRecorderResult {
20
+ /** True while recording. */
21
+ recording: boolean;
22
+ /** Whole seconds since the recording started; 0 when idle. Drives the toolbar's timer. */
23
+ elapsedSec: number;
24
+ /** null when this browser has no `MediaRecorder` support at all — the UI hides the control
25
+ * rather than offering a button that cannot work. */
26
+ supported: boolean;
27
+ /** Start, or stop-and-download. Safe to call in either state. */
28
+ toggle: () => void;
29
+ }
30
+
31
+ export function useRemoteDesktopRecorder(
32
+ canvasRef: React.RefObject<HTMLCanvasElement | null>,
33
+ /** Extra tracks to mux in — the session's audio, when it is playing. Read at start time. */
34
+ getAudioTracks?: () => MediaStreamTrack[],
35
+ ): UseRemoteDesktopRecorderResult {
36
+ const recorderRef = useRef<MediaRecorder | null>(null);
37
+ const chunksRef = useRef<Blob[]>([]);
38
+ const [recording, setRecording] = useState(false);
39
+ const [elapsedSec, setElapsedSec] = useState(0);
40
+ const mime = useRef<string | null>(null);
41
+ if (mime.current === null) mime.current = pickRecordingMime();
42
+
43
+ const stop = useCallback(() => {
44
+ // `stop()` fires `dataavailable` then `onstop`, which is where the download happens — so
45
+ // this only asks; it does not assemble the file itself.
46
+ recorderRef.current?.stop();
47
+ }, []);
48
+
49
+ const start = useCallback(() => {
50
+ const canvas = canvasRef.current;
51
+ const mimeType = mime.current;
52
+ if (!canvas || !mimeType || recorderRef.current) return;
53
+ // No fps argument: follow the canvas's own draw cadence (see the module header).
54
+ const stream = canvas.captureStream();
55
+ for (const track of getAudioTracks?.() ?? []) stream.addTrack(track);
56
+ let recorder: MediaRecorder;
57
+ try {
58
+ recorder = new MediaRecorder(stream, { mimeType });
59
+ } catch {
60
+ return; // the MIME was supported but this stream's tracks are not; nothing to report
61
+ }
62
+ chunksRef.current = [];
63
+ recorder.ondataavailable = (e) => { if (e.data.size > 0) chunksRef.current.push(e.data); };
64
+ recorder.onstop = () => {
65
+ const blob = new Blob(chunksRef.current, { type: mimeType });
66
+ chunksRef.current = [];
67
+ recorderRef.current = null;
68
+ setRecording(false);
69
+ setElapsedSec(0);
70
+ // A recorder stopped before any frame was drawn has nothing to save, and an empty file
71
+ // is worse than no file: it looks like the feature ran and lost the recording.
72
+ if (blob.size > 0) downloadBlob(blob, recordingFileName(extensionForMime(mimeType)));
73
+ };
74
+ // One second per chunk rather than one blob at the end: a tab that crashes mid-recording
75
+ // still has everything up to the last second in memory for `onstop` to assemble, and the
76
+ // allocation pattern stays flat instead of one giant buffer at the finish.
77
+ recorder.start(1_000);
78
+ recorderRef.current = recorder;
79
+ setRecording(true);
80
+ setElapsedSec(0);
81
+ }, [canvasRef, getAudioTracks]);
82
+
83
+ const toggle = useCallback(() => {
84
+ if (recorderRef.current) stop(); else start();
85
+ }, [start, stop]);
86
+
87
+ useEffect(() => {
88
+ if (!recording) return;
89
+ const startedAt = Date.now();
90
+ const id = setInterval(() => {
91
+ const ms = Date.now() - startedAt;
92
+ setElapsedSec(Math.floor(ms / 1000));
93
+ if (ms >= MAX_RECORDING_MS) stop();
94
+ }, 1_000);
95
+ return () => clearInterval(id);
96
+ }, [recording, stop]);
97
+
98
+ // A viewer closed mid-recording must still save what it has: without this the recorder is
99
+ // garbage-collected with the chunks and the whole recording is silently lost.
100
+ useEffect(() => () => { recorderRef.current?.stop(); }, []);
101
+
102
+ return { recording, elapsedSec, supported: mime.current !== null, toggle };
103
+ }
@@ -6,6 +6,10 @@
6
6
  * - A keydown with a real `code` (hardware keyboard, iOS for many keys) is forwarded as a
7
7
  * layout-independent `key` message and `preventDefault`-ed, exactly like
8
8
  * `use-remote-input-capture.ts` — that also suppresses the `beforeinput` that would follow.
9
+ * - A paste is taken from the `paste` event rather than `beforeinput`: Chromium reports it as
10
+ * `insertFromPaste` with the text in `data` and a *null* `dataTransfer`, which is the opposite
11
+ * of what the spec's contenteditable case describes — `clipboardData` is the one place every
12
+ * browser agrees on. `preventDefault()` there also stops the `beforeinput` that would follow.
9
13
  * - Android soft keyboards send keydown with `code === ""` / keyCode 229 and put the actual
10
14
  * characters in `beforeinput` (`insertText`) or, for predictive/IME entry, in a composition
11
15
  * that only settles at `compositionend`. Those go up as `{ type: "text" }`, which the host
@@ -46,7 +50,12 @@ export function forwardBeforeInput(inputType: string, data: string | null, send:
46
50
  }
47
51
  }
48
52
 
49
- export function useRemoteDesktopVirtualKeyboard(sendMessage: Send, enabled: boolean): UseRemoteDesktopVirtualKeyboardResult {
53
+ export function useRemoteDesktopVirtualKeyboard(
54
+ sendMessage: Send,
55
+ enabled: boolean,
56
+ /** Text pasted into the hidden input, for the caller to route through the host clipboard. */
57
+ onPasteText?: (text: string) => void,
58
+ ): UseRemoteDesktopVirtualKeyboardResult {
50
59
  const inputRef = useRef<HTMLInputElement | null>(null);
51
60
 
52
61
  useEffect(() => {
@@ -74,6 +83,12 @@ export function useRemoteDesktopVirtualKeyboard(sendMessage: Send, enabled: bool
74
83
  if (e.data) sendMessage({ type: "text", text: e.data });
75
84
  input.value = ""; // the composed text landed in the input; the host already has it
76
85
  };
86
+ const onPaste = (e: ClipboardEvent) => {
87
+ const text = e.clipboardData?.getData("text/plain");
88
+ if (!text || !onPasteText) return;
89
+ e.preventDefault(); // suppresses the `insertFromPaste` beforeinput that would double it
90
+ onPasteText(text);
91
+ };
77
92
  const onBlur = () => releaseAll();
78
93
  const onVisibilityChange = () => { if (document.hidden) releaseAll(); };
79
94
 
@@ -81,6 +96,7 @@ export function useRemoteDesktopVirtualKeyboard(sendMessage: Send, enabled: bool
81
96
  input.addEventListener("keyup", onKeyUp);
82
97
  input.addEventListener("beforeinput", onBeforeInput);
83
98
  input.addEventListener("compositionend", onCompositionEnd);
99
+ input.addEventListener("paste", onPaste);
84
100
  input.addEventListener("blur", onBlur);
85
101
  document.addEventListener("visibilitychange", onVisibilityChange);
86
102
 
@@ -89,11 +105,12 @@ export function useRemoteDesktopVirtualKeyboard(sendMessage: Send, enabled: bool
89
105
  input.removeEventListener("keyup", onKeyUp);
90
106
  input.removeEventListener("beforeinput", onBeforeInput);
91
107
  input.removeEventListener("compositionend", onCompositionEnd);
108
+ input.removeEventListener("paste", onPaste);
92
109
  input.removeEventListener("blur", onBlur);
93
110
  document.removeEventListener("visibilitychange", onVisibilityChange);
94
111
  releaseAll();
95
112
  };
96
- }, [enabled, sendMessage]);
113
+ }, [enabled, sendMessage, onPasteText]);
97
114
 
98
115
  const show = useCallback(() => inputRef.current?.focus(), []);
99
116