@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
@@ -0,0 +1,144 @@
1
+ /**
2
+ * RustDesk's "Custom image quality" dialog: a bitrate-percentage slider, an fps slider, a
3
+ * "More" checkbox that raises the bitrate ceiling from 100% to 2000%, and Close.
4
+ *
5
+ * Both sliders commit on a debounce, not per tick: each commit respawns the host's ffmpeg
6
+ * (`-b:v`/`-r` are fixed at spawn), so a dragged slider would otherwise restart the capture
7
+ * dozens of times and the picture would strobe. RustDesk gets away with per-tick updates
8
+ * because its encoder is retunable in place; ours is not, and that difference is the reason
9
+ * for the delay rather than an arbitrary choice.
10
+ *
11
+ * Shown as a bottom sheet below `md` per `docs/design-guidelines.md`.
12
+ */
13
+ import { useEffect, useRef, useState } from "react";
14
+ import {
15
+ Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
16
+ } from "@/components/ui/dialog";
17
+ import { Button } from "@/components/ui/button";
18
+ import {
19
+ CUSTOM_FPS_MAX, CUSTOM_FPS_MIN, CUSTOM_QUALITY_MAX_MORE_PERCENT, CUSTOM_QUALITY_MAX_PERCENT,
20
+ CUSTOM_QUALITY_MIN_PERCENT, clampCustomFps, clampCustomQualityPercent, customBitrateKbps,
21
+ } from "../../../shared/remote-desktop-custom-quality";
22
+
23
+ /** Long enough that a drag is one respawn, short enough to feel like a live control. */
24
+ const COMMIT_DEBOUNCE_MS = 400;
25
+
26
+ export interface RemoteDesktopCustomQualityDialogProps {
27
+ open: boolean;
28
+ onOpenChange: (open: boolean) => void;
29
+ percent: number;
30
+ fps: number;
31
+ more: boolean;
32
+ onSetMore: (more: boolean) => void;
33
+ /** Commit both values; also selects the custom rung. */
34
+ onCommit: (percent: number, fps: number) => void;
35
+ /** The capture's own size, so the dialog can say what the percentage really costs. */
36
+ frameSize: { width: number; height: number };
37
+ }
38
+
39
+ export function RemoteDesktopCustomQualityDialog({
40
+ open, onOpenChange, percent, fps, more, onSetMore, onCommit, frameSize,
41
+ }: RemoteDesktopCustomQualityDialogProps) {
42
+ const [localPercent, setLocalPercent] = useState(percent);
43
+ const [localFps, setLocalFps] = useState(fps);
44
+ const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
45
+
46
+ // Adopt the stored values whenever the dialog is opened, so it never shows a stale drag.
47
+ useEffect(() => { if (open) { setLocalPercent(percent); setLocalFps(fps); } }, [open, percent, fps]);
48
+ useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);
49
+
50
+ const max = more ? CUSTOM_QUALITY_MAX_MORE_PERCENT : CUSTOM_QUALITY_MAX_PERCENT;
51
+
52
+ const commitLater = (nextPercent: number, nextFps: number) => {
53
+ if (timer.current) clearTimeout(timer.current);
54
+ timer.current = setTimeout(() => onCommit(nextPercent, nextFps), COMMIT_DEBOUNCE_MS);
55
+ };
56
+ const changePercent = (raw: number) => {
57
+ const next = clampCustomQualityPercent(raw, more);
58
+ setLocalPercent(next);
59
+ commitLater(next, localFps);
60
+ };
61
+ const changeFps = (raw: number) => {
62
+ const next = clampCustomFps(raw);
63
+ setLocalFps(next);
64
+ commitLater(localPercent, next);
65
+ };
66
+ const toggleMore = (next: boolean) => {
67
+ onSetMore(next);
68
+ // Turning it off strands a percent above the new ceiling; commit the clamped value so the
69
+ // host is never left encoding at a rate the dialog no longer shows.
70
+ const clamped = clampCustomQualityPercent(localPercent, next);
71
+ if (clamped !== localPercent) { setLocalPercent(clamped); commitLater(clamped, localFps); }
72
+ };
73
+
74
+ const kbps = customBitrateKbps(localPercent, frameSize.width, frameSize.height);
75
+
76
+ return (
77
+ <Dialog open={open} onOpenChange={onOpenChange}>
78
+ <DialogContent className="sm:max-w-md" data-testid="remote-desktop-custom-quality-dialog">
79
+ <DialogHeader>
80
+ <DialogTitle>Custom image quality</DialogTitle>
81
+ <DialogDescription>
82
+ {/* RustDesk shows the bare percentage. The real figure is worth showing because the
83
+ percentage is not a ratio — 50% is the resolution's *base* bitrate, not half of
84
+ anything — so the number alone reads as "half quality" and is not. */}
85
+ {frameSize.width > 0
86
+ ? `${localPercent}% of the base rate for ${frameSize.width}×${frameSize.height} — about ${(kbps / 1000).toFixed(1)} Mbit/s`
87
+ : "Waiting for the first frame"}
88
+ </DialogDescription>
89
+ </DialogHeader>
90
+
91
+ <div className="space-y-4 py-2">
92
+ <div className="flex items-center gap-3">
93
+ <input
94
+ type="range"
95
+ min={CUSTOM_QUALITY_MIN_PERCENT}
96
+ max={max}
97
+ step={1}
98
+ value={Math.min(localPercent, max)}
99
+ onChange={(e) => changePercent(Number(e.target.value))}
100
+ aria-label="Bitrate"
101
+ data-testid="remote-desktop-custom-bitrate"
102
+ className="h-1 min-w-0 flex-1 cursor-pointer appearance-none rounded bg-accent accent-primary"
103
+ />
104
+ <span className="w-14 text-right text-xs tabular-nums">{localPercent}%</span>
105
+ <span className="w-14 text-xs text-muted-foreground">Bitrate</span>
106
+ <label className="flex shrink-0 items-center gap-1.5 text-xs">
107
+ <input
108
+ type="checkbox"
109
+ checked={more}
110
+ onChange={(e) => toggleMore(e.target.checked)}
111
+ data-testid="remote-desktop-custom-more"
112
+ className="size-3.5 accent-primary"
113
+ />
114
+ More
115
+ </label>
116
+ </div>
117
+
118
+ <div className="flex items-center gap-3">
119
+ <input
120
+ type="range"
121
+ min={CUSTOM_FPS_MIN}
122
+ max={CUSTOM_FPS_MAX}
123
+ step={1}
124
+ value={localFps}
125
+ onChange={(e) => changeFps(Number(e.target.value))}
126
+ aria-label="FPS"
127
+ data-testid="remote-desktop-custom-fps"
128
+ className="h-1 min-w-0 flex-1 cursor-pointer appearance-none rounded bg-accent accent-primary"
129
+ />
130
+ <span className="w-14 text-right text-xs tabular-nums">{localFps}</span>
131
+ <span className="w-14 text-xs text-muted-foreground">FPS</span>
132
+ {/* Keeps the two rows' sliders the same length as the row above, which carries an
133
+ extra checkbox. */}
134
+ <span className="w-[4.25rem] shrink-0" aria-hidden />
135
+ </div>
136
+ </div>
137
+
138
+ <DialogFooter>
139
+ <Button onClick={() => onOpenChange(false)}>Close</Button>
140
+ </DialogFooter>
141
+ </DialogContent>
142
+ </Dialog>
143
+ );
144
+ }
@@ -14,8 +14,18 @@
14
14
  import { useCallback, useEffect, useRef, useState } from "react";
15
15
  import { cn } from "@/lib/utils";
16
16
 
17
+ /** Ctrl/Cmd + C or X, by `code` rather than by button label so renaming a button cannot
18
+ * silently stop the host clipboard being fetched. */
19
+ function isCopyChord(codes: readonly string[]): boolean {
20
+ const mod = codes.includes("ControlLeft") || codes.includes("MetaLeft");
21
+ return mod && (codes.includes("KeyC") || codes.includes("KeyX"));
22
+ }
23
+
17
24
  export interface RemoteDesktopMobileKeyBarProps {
18
25
  sendMessage: (msg: Record<string, unknown>) => void;
26
+ /** Fired after a copy/cut combo — the touch equivalent of the desktop's Ctrl+C cue, since
27
+ * none of these buttons goes through `use-remote-input-capture`'s key handler. */
28
+ onCopyCombo?: () => void;
19
29
  }
20
30
 
21
31
  const MODIFIERS = [
@@ -73,7 +83,7 @@ function KeyBarButton({
73
83
  );
74
84
  }
75
85
 
76
- export function RemoteDesktopMobileKeyBar({ sendMessage }: RemoteDesktopMobileKeyBarProps) {
86
+ export function RemoteDesktopMobileKeyBar({ sendMessage, onCopyCombo }: RemoteDesktopMobileKeyBarProps) {
77
87
  const [held, setHeld] = useState<ReadonlySet<string>>(new Set());
78
88
  const heldRef = useRef(held);
79
89
  heldRef.current = held;
@@ -106,7 +116,8 @@ export function RemoteDesktopMobileKeyBar({ sendMessage }: RemoteDesktopMobileKe
106
116
  // chord (last key down first key up would read as a different shortcut on some apps).
107
117
  codes.forEach((code) => sendMessage({ type: "key", code, down: true }));
108
118
  [...codes].reverse().forEach((code) => sendMessage({ type: "key", code, down: false }));
109
- }, [sendMessage]);
119
+ if (isCopyChord(codes)) onCopyCombo?.();
120
+ }, [sendMessage, onCopyCombo]);
110
121
 
111
122
  return (
112
123
  <div
@@ -0,0 +1,232 @@
1
+ /**
2
+ * The mobile viewer's settings panel — the toggles the desktop keeps in its `Settings2`
3
+ * dropdown, which has no room in a thumb-zone row of 44px buttons.
4
+ *
5
+ * Rendered *in flow* inside the bottom bar rather than through `mobile-bottom-sheet`, exactly
6
+ * like `remote-desktop-mobile-key-bar.tsx`: the viewer already lives in its own full-viewport
7
+ * portal, so a second portal would have to win a z-index argument with it, and a panel in the
8
+ * bottom bar rides that bar's `keyboardInset` lift for free.
9
+ *
10
+ * Scale modes are deliberately absent: pinch-zoom and the toolbar's Reset zoom already own the
11
+ * transform here, and they do it continuously rather than in three fixed steps.
12
+ */
13
+ import { useMemo } from "react";
14
+ import { Circle, Clipboard, EyeOff, Gauge, Image, MousePointer2, Square, Volume2 } from "@/lib/icons";
15
+ import { cn } from "@/lib/utils";
16
+ import { fitLocalMode, localScreenSize, resolutionChoices } from "./remote-desktop-resolution-list";
17
+ import type { HostMode } from "./use-remote-desktop-readiness";
18
+
19
+ export interface RemoteDesktopMobileSettingsProps {
20
+ statsVisible: boolean;
21
+ onToggleStats: () => void;
22
+ showCursor: boolean;
23
+ onSetShowCursor: (show: boolean) => void;
24
+ clipboardEnabled: boolean;
25
+ onSetClipboardEnabled: (enabled: boolean) => void;
26
+ audioOn: boolean;
27
+ /** Why audio cannot be turned on, or null when it can. */
28
+ audioReason: string | null;
29
+ onSetAudioOn: (on: boolean) => void;
30
+ privacyOn: boolean;
31
+ privacyReason: string | null;
32
+ onSetPrivacyOn: (on: boolean) => void;
33
+ /** The host's own display modes, collapsed to one row per size by the panel. Empty on a host
34
+ * whose resolution cannot be changed, which renders no section at all. */
35
+ hostModes: HostMode[];
36
+ hostModeId: string | null;
37
+ /** The mode the host was on before this session changed it, for the "Original" chip. */
38
+ hostOriginalModeId: string | null;
39
+ onSetHostMode: (modeId: string) => void;
40
+ /** Input injection available. Resizing the host is host control, so a view-only session must
41
+ * not offer it — RustDesk gates the same submenu on `ffiModel.keyboard`. */
42
+ canResizeHost: boolean;
43
+ resolutionError: string | null;
44
+ recording: boolean;
45
+ recordingSupported: boolean;
46
+ onToggleRecording: () => void;
47
+ onScreenshot: () => void;
48
+ }
49
+
50
+ function SettingRow({
51
+ icon, label, hint, checked, onToggle, testId, disabled,
52
+ }: {
53
+ icon: React.ReactNode;
54
+ label: string;
55
+ hint?: string;
56
+ checked: boolean;
57
+ onToggle: () => void;
58
+ testId: string;
59
+ disabled?: boolean;
60
+ }) {
61
+ return (
62
+ <button
63
+ type="button"
64
+ role="switch"
65
+ aria-checked={checked}
66
+ disabled={disabled}
67
+ onClick={onToggle}
68
+ data-testid={testId}
69
+ className="flex min-h-11 w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-white/80 active:bg-white/10 disabled:opacity-50"
70
+ >
71
+ <span className="shrink-0 opacity-80">{icon}</span>
72
+ <span className="min-w-0 flex-1">
73
+ <span className="block truncate text-xs">{label}</span>
74
+ {hint && <span className="block truncate text-[10px] text-white/45">{hint}</span>}
75
+ </span>
76
+ {/* A drawn switch rather than the `ui/switch` primitive: this panel sits on black inside
77
+ the viewer's own portal, where the themed component's surface colours disappear. */}
78
+ <span
79
+ className={cn(
80
+ "relative h-5 w-9 shrink-0 rounded-full transition-colors",
81
+ checked ? "bg-primary" : "bg-white/20",
82
+ )}
83
+ >
84
+ <span
85
+ className={cn(
86
+ "absolute top-0.5 size-4 rounded-full bg-white transition-all",
87
+ checked ? "left-[1.125rem]" : "left-0.5",
88
+ )}
89
+ />
90
+ </span>
91
+ </button>
92
+ );
93
+ }
94
+
95
+ export function RemoteDesktopMobileSettings({
96
+ statsVisible, onToggleStats, showCursor, onSetShowCursor,
97
+ clipboardEnabled, onSetClipboardEnabled,
98
+ audioOn, audioReason, onSetAudioOn,
99
+ privacyOn, privacyReason, onSetPrivacyOn,
100
+ hostModes, hostModeId, hostOriginalModeId, onSetHostMode, canResizeHost, resolutionError,
101
+ recording, recordingSupported, onToggleRecording, onScreenshot,
102
+ }: RemoteDesktopMobileSettingsProps) {
103
+ const resolutions = useMemo(() => resolutionChoices(hostModes), [hostModes]);
104
+ const localScreen = useMemo(() => localScreenSize(), []);
105
+ const fitLocal = useMemo(
106
+ () => fitLocalMode(hostModes, localScreen, hostModeId),
107
+ [hostModes, localScreen, hostModeId],
108
+ );
109
+ return (
110
+ <div
111
+ className="mx-2 mb-1 space-y-0.5 rounded-xl border border-white/10 bg-black/90 p-1"
112
+ data-testid="remote-desktop-mobile-settings"
113
+ >
114
+ <SettingRow
115
+ icon={<Gauge className="size-4" />}
116
+ label="Quality monitor"
117
+ checked={statsVisible}
118
+ onToggle={onToggleStats}
119
+ testId="remote-desktop-mobile-stats-toggle"
120
+ />
121
+ <SettingRow
122
+ icon={<MousePointer2 className="size-4" />}
123
+ label="Show remote cursor"
124
+ hint="Restarts the stream"
125
+ checked={showCursor}
126
+ onToggle={() => onSetShowCursor(!showCursor)}
127
+ testId="remote-desktop-mobile-cursor-toggle"
128
+ />
129
+ <SettingRow
130
+ icon={<Clipboard className="size-4" />}
131
+ label="Sync clipboard"
132
+ hint="Off: Ctrl+V pastes the host's own clipboard"
133
+ checked={clipboardEnabled}
134
+ onToggle={() => onSetClipboardEnabled(!clipboardEnabled)}
135
+ testId="remote-desktop-mobile-clipboard-toggle"
136
+ />
137
+ {/* The tap on this row is the user gesture the autoplay policy needs to resume the
138
+ `AudioContext`, which is why audio can only ever be started from a control. */}
139
+ <SettingRow
140
+ icon={<Volume2 className="size-4" />}
141
+ label="Play host audio"
142
+ hint={audioReason ?? undefined}
143
+ checked={audioOn}
144
+ disabled={audioReason !== null}
145
+ onToggle={() => onSetAudioOn(!audioOn)}
146
+ testId="remote-desktop-mobile-audio-toggle"
147
+ />
148
+ <SettingRow
149
+ icon={<EyeOff className="size-4" />}
150
+ label="Privacy mode"
151
+ hint={privacyReason ?? "Blocks the host's own keyboard and mouse"}
152
+ checked={privacyOn}
153
+ disabled={privacyReason !== null && !privacyOn}
154
+ onToggle={() => onSetPrivacyOn(!privacyOn)}
155
+ testId="remote-desktop-mobile-privacy-toggle"
156
+ />
157
+ {/* A scrolling list of chips rather than a select: a native `<select>` on a phone opens
158
+ the OS picker *over* the viewer's own portal, and this panel is inside it. */}
159
+ {canResizeHost && resolutions.length > 1 && (
160
+ <div className="px-3 pt-1">
161
+ <div className="pb-1.5 text-[10px] uppercase tracking-wide text-white/40">Resolution</div>
162
+ <div className="flex max-h-28 flex-wrap gap-1.5 overflow-y-auto">
163
+ {hostOriginalModeId && hostOriginalModeId !== hostModeId && (
164
+ <button
165
+ type="button"
166
+ onClick={() => onSetHostMode(hostOriginalModeId)}
167
+ data-testid="remote-desktop-mobile-resolution-original"
168
+ className="min-h-9 rounded-md bg-white/10 px-2.5 py-1 text-[11px] text-white/70 active:bg-white/20"
169
+ >
170
+ Original
171
+ </button>
172
+ )}
173
+ {/* A phone's screen is almost never one of the host's advertised sizes, so this chip
174
+ is normally absent — the same as RustDesk on the same pair. */}
175
+ {fitLocal && (
176
+ <button
177
+ type="button"
178
+ onClick={() => onSetHostMode(fitLocal.id)}
179
+ data-testid="remote-desktop-mobile-resolution-fit-local"
180
+ className="min-h-9 rounded-md bg-white/10 px-2.5 py-1 text-[11px] text-white/70 active:bg-white/20"
181
+ >
182
+ Fit local
183
+ </button>
184
+ )}
185
+ {resolutions.map((r) => {
186
+ const active = (hostModeId ?? resolutions.find((c) => c.current)?.id) === r.id;
187
+ return (
188
+ <button
189
+ key={r.id}
190
+ type="button"
191
+ onClick={() => onSetHostMode(r.id)}
192
+ data-testid={`remote-desktop-mobile-resolution-${r.width}x${r.height}`}
193
+ className={cn(
194
+ "min-h-9 rounded-md px-2.5 py-1 text-[11px] tabular-nums",
195
+ active ? "bg-white/25 text-white" : "bg-white/10 text-white/70 active:bg-white/20",
196
+ )}
197
+ >
198
+ {r.width}×{r.height}
199
+ </button>
200
+ );
201
+ })}
202
+ </div>
203
+ {resolutionError && (
204
+ <div className="pt-1.5 text-[10px] leading-tight text-red-400">{resolutionError}</div>
205
+ )}
206
+ </div>
207
+ )}
208
+ {/* Actions, not switches — a plain row each, so the drawn switch is never shown for
209
+ something that does not stay on. */}
210
+ <button
211
+ type="button"
212
+ onClick={onScreenshot}
213
+ data-testid="remote-desktop-mobile-screenshot"
214
+ className="flex min-h-11 w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-xs text-white/80 active:bg-white/10"
215
+ >
216
+ <Image className="size-4 opacity-80" /> Save screenshot
217
+ </button>
218
+ {recordingSupported && (
219
+ <button
220
+ type="button"
221
+ onClick={onToggleRecording}
222
+ data-testid="remote-desktop-mobile-record"
223
+ className="flex min-h-11 w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-xs text-white/80 active:bg-white/10"
224
+ >
225
+ {recording
226
+ ? <><Square className="size-4 text-red-500" /> Stop recording</>
227
+ : <><Circle className="size-4 opacity-80" /> Start recording</>}
228
+ </button>
229
+ )}
230
+ </div>
231
+ );
232
+ }
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Bottom (thumb-zone) toolbar for the mobile remote-desktop viewer: Touch/Mouse mode toggle,
3
3
  * virtual keyboard, zoom reset, close. Every button is a 44px+ touch target per
4
- * `docs/design-guidelines.md`'s Mobile-First UI Rules.
4
+ * `docs/design-guidelines.md`'s Mobile-First UI Rules — which is also why the three on/off
5
+ * settings live behind the More button in `remote-desktop-mobile-settings.tsx` instead of as
6
+ * three more buttons here: the row is `flex-1` per button, and at eight of them each target
7
+ * falls under 44px on a small phone.
5
8
  */
6
- import { Hand, MousePointer2, Keyboard, ZoomOut, Gauge, Monitor, X } from "@/lib/icons";
9
+ import { Hand, MousePointer2, Keyboard, ZoomOut, Settings2, Monitor, Wifi, X } from "@/lib/icons";
7
10
  import { cn } from "@/lib/utils";
8
- import { useSettingsStore } from "@/stores/settings-store";
9
11
  import type { RemoteDesktopInputMode } from "./use-remote-desktop-touch";
10
12
 
11
13
  export interface RemoteDesktopMobileToolbarProps {
@@ -13,10 +15,17 @@ export interface RemoteDesktopMobileToolbarProps {
13
15
  displayLabel: string | null;
14
16
  /** Cycle to the next display — one tap per hop beats a dropdown in the thumb zone. */
15
17
  onNextDisplay: () => void;
18
+ /** Short name of the rung being streamed ("Auto", "480p"…) for the quality button's label. */
19
+ qualityLabel: string;
20
+ /** Cycle auto → tiny → low → balanced → high → auto, same one-tap-per-hop reasoning. */
21
+ onNextQuality: () => void;
16
22
  mode: RemoteDesktopInputMode;
17
23
  onToggleMode: () => void;
18
24
  onOpenKeyboard: () => void;
19
25
  onResetZoom: () => void;
26
+ /** Show/hide the settings panel above this row (stats, remote cursor, clipboard sync). */
27
+ settingsOpen: boolean;
28
+ onToggleSettings: () => void;
20
29
  onClose: () => void;
21
30
  }
22
31
 
@@ -51,15 +60,16 @@ function ToolbarButton({
51
60
  export function RemoteDesktopMobileToolbar({
52
61
  displayLabel,
53
62
  onNextDisplay,
63
+ qualityLabel,
64
+ onNextQuality,
54
65
  mode,
55
66
  onToggleMode,
56
67
  onOpenKeyboard,
57
68
  onResetZoom,
69
+ settingsOpen,
70
+ onToggleSettings,
58
71
  onClose,
59
72
  }: RemoteDesktopMobileToolbarProps) {
60
- const statsVisible = useSettingsStore((s) => s.remoteDesktopStatsVisible);
61
- const toggleStats = useSettingsStore((s) => s.toggleRemoteDesktopStatsVisible);
62
-
63
73
  return (
64
74
  <div
65
75
  className="flex shrink-0 items-stretch gap-1 border-t border-white/10 bg-black/90 px-2 pb-[max(0.375rem,env(safe-area-inset-bottom))] pt-1.5"
@@ -79,8 +89,11 @@ export function RemoteDesktopMobileToolbar({
79
89
  <Monitor className="size-5" />
80
90
  </ToolbarButton>
81
91
  )}
82
- <ToolbarButton onClick={toggleStats} label="Stats" active={statsVisible}>
83
- <Gauge className="size-5" />
92
+ <ToolbarButton onClick={onNextQuality} label={qualityLabel}>
93
+ <Wifi className="size-5" />
94
+ </ToolbarButton>
95
+ <ToolbarButton onClick={onToggleSettings} label="More" active={settingsOpen}>
96
+ <Settings2 className="size-5" />
84
97
  </ToolbarButton>
85
98
  <ToolbarButton onClick={onClose} label="Close">
86
99
  <X className="size-5" />
@@ -9,14 +9,23 @@
9
9
  import { useRef, useState, useCallback } from "react";
10
10
  import { RotateCw, MonitorX } from "@/lib/icons";
11
11
  import { useVisualViewport } from "@/hooks/use-visual-viewport";
12
+ import { useSettingsStore } from "@/stores/settings-store";
12
13
  import { useRemoteDesktopConnection } from "./use-remote-desktop-connection";
13
14
  import { useRemoteDesktopTouch, type RemoteDesktopInputMode } from "./use-remote-desktop-touch";
14
15
  import { useRemoteDesktopVirtualKeyboard } from "./use-remote-desktop-virtual-keyboard";
15
16
  import { RemoteDesktopMobileToolbar } from "./remote-desktop-mobile-toolbar";
17
+ import {
18
+ nextQualityChoice, shortQualityLabel, type QualityChoice,
19
+ } from "../../../shared/remote-desktop-quality";
16
20
  import { RemoteDesktopMobileKeyBar } from "./remote-desktop-mobile-key-bar";
21
+ import { RemoteDesktopMobileSettings } from "./remote-desktop-mobile-settings";
22
+ import { useRemoteDesktopRecorder } from "./use-remote-desktop-recorder";
23
+ import { saveCanvasScreenshot } from "./remote-desktop-recording";
17
24
  import { RemoteDesktopStatsOverlay } from "./remote-desktop-stats-overlay";
18
25
  import { letterboxedContentRect } from "./remote-desktop-coords";
19
26
  import { useRemoteDesktopDisplayChoice } from "./use-remote-desktop-display-choice";
27
+ import { RemoteDesktopClipboardNotice } from "./remote-desktop-clipboard-notice";
28
+ import { useRemoteDesktopReadiness } from "./use-remote-desktop-readiness";
20
29
 
21
30
  export interface RemoteDesktopMobileViewProps {
22
31
  onClose: () => void;
@@ -27,12 +36,21 @@ export default function RemoteDesktopMobileView({ onClose }: RemoteDesktopMobile
27
36
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
28
37
  const [mode, setMode] = useState<RemoteDesktopInputMode>("mouse");
29
38
  const [keyBarOpen, setKeyBarOpen] = useState(false);
39
+ const [settingsOpen, setSettingsOpen] = useState(false);
30
40
  const display = useRemoteDesktopDisplayChoice();
31
41
 
32
42
  const {
33
43
  connState, errorMessage, decoderStatus, decoderErrorMessage, sendMessage, reconnect,
34
- getTotalBytes, getFrameCount,
44
+ getTotalBytes, getFrameCount, quality, setQuality, showCursor, setShowCursor,
45
+ audioOn, setAudioOn, getAudioTracks, privacyOn, privacyError, setPrivacyOn,
46
+ hostModeId, hostOriginalModeId, resolutionError, setHostMode,
47
+ pendingHostClipboard, clearHostClipboard, sendClipboard, requestHostClipboard,
35
48
  } = useRemoteDesktopConnection(canvasRef, { displayId: display.displayId });
49
+ const statsVisible = useSettingsStore((s) => s.remoteDesktopStatsVisible);
50
+ const toggleStats = useSettingsStore((s) => s.toggleRemoteDesktopStatsVisible);
51
+ const clipboardSync = useSettingsStore((s) => s.remoteDesktopClipboardSync);
52
+ const setClipboardSync = useSettingsStore((s) => s.setRemoteDesktopClipboardSync);
53
+ const qualityChoice = useSettingsStore((st) => st.remoteDesktopQuality);
36
54
  const streaming = connState === "streaming";
37
55
 
38
56
  const { transform, virtualCursor, resetZoom } = useRemoteDesktopTouch({
@@ -42,7 +60,24 @@ export default function RemoteDesktopMobileView({ onClose }: RemoteDesktopMobile
42
60
  sendMessage,
43
61
  enabled: streaming,
44
62
  });
45
- const { inputRef: keyboardInputRef, show: showKeyboard } = useRemoteDesktopVirtualKeyboard(sendMessage, streaming);
63
+ // A phone paste goes through the *host clipboard* rather than being typed character by
64
+ // character: typing is subject to the host's IME (a Telex host rewrites "World" as "ửold"),
65
+ // and a pasted URL or token is exactly the text that must survive verbatim. The server falls
66
+ // back to typing by itself when the host has no clipboard tool.
67
+ const [pasteAttempted, setPasteAttempted] = useState(false);
68
+ // Undefined with sync off, which is how the toggle is implemented on this surface: the
69
+ // virtual keyboard then lets the `paste` event's own default action stand (i.e. nothing
70
+ // leaves the device), and the key bar's Ctrl+V button still pastes the *host's* clipboard.
71
+ const onPasteText = useCallback((text: string) => {
72
+ setPasteAttempted(true);
73
+ sendClipboard(text, true);
74
+ }, [sendClipboard]);
75
+ // One fetch, no polling — read only to explain a paste that had to fall back to typing, and
76
+ // an audio toggle the host cannot honour.
77
+ const { caps } = useRemoteDesktopReadiness(false);
78
+ const recorder = useRemoteDesktopRecorder(canvasRef, getAudioTracks);
79
+ const { inputRef: keyboardInputRef, show: showKeyboard } =
80
+ useRemoteDesktopVirtualKeyboard(sendMessage, streaming, clipboardSync ? onPasteText : undefined);
46
81
 
47
82
  // Lifts the toolbar/key-bar row above the on-screen keyboard instead of the whole sheet
48
83
  // shrinking to make room for it (that was the previous, since-reverted behavior — see
@@ -169,14 +204,58 @@ export default function RemoteDesktopMobileView({ onClose }: RemoteDesktopMobile
169
204
  style={keyboardInset > 0 ? { transform: `translateY(-${keyboardInset}px)` } : undefined}
170
205
  data-testid="remote-desktop-mobile-bottom-bar"
171
206
  >
172
- {keyBarOpen && <RemoteDesktopMobileKeyBar sendMessage={sendMessage} />}
207
+ {/* In flow above the key bar so it rides the same `keyboardInset` lift and is never
208
+ hidden behind the toolbar. */}
209
+ <RemoteDesktopClipboardNotice
210
+ pendingText={pendingHostClipboard}
211
+ onDismiss={clearHostClipboard}
212
+ missingToolAction={pasteAttempted && caps && !caps.clipboard.available ? caps.clipboard.action : null}
213
+ onDismissMissingTool={() => setPasteAttempted(false)}
214
+ positionClassName="mx-2 mb-1 max-w-[calc(100%-1rem)]"
215
+ />
216
+ {settingsOpen && (
217
+ <RemoteDesktopMobileSettings
218
+ statsVisible={statsVisible}
219
+ onToggleStats={toggleStats}
220
+ showCursor={showCursor}
221
+ onSetShowCursor={setShowCursor}
222
+ clipboardEnabled={clipboardSync}
223
+ onSetClipboardEnabled={setClipboardSync}
224
+ audioOn={audioOn}
225
+ audioReason={caps && !caps.audio.available ? caps.audio.reason : null}
226
+ onSetAudioOn={setAudioOn}
227
+ privacyOn={privacyOn}
228
+ privacyReason={privacyError ?? (caps && !caps.privacy.available ? caps.privacy.reason : null)}
229
+ hostModes={caps?.resolutions?.modes ?? []}
230
+ hostModeId={hostModeId}
231
+ hostOriginalModeId={hostOriginalModeId}
232
+ onSetHostMode={setHostMode}
233
+ canResizeHost={caps?.inputReady ?? false}
234
+ resolutionError={resolutionError}
235
+ onSetPrivacyOn={setPrivacyOn}
236
+ recording={recorder.recording}
237
+ recordingSupported={recorder.supported}
238
+ onToggleRecording={recorder.toggle}
239
+ onScreenshot={() => void saveCanvasScreenshot(canvasRef.current)}
240
+ />
241
+ )}
242
+ {keyBarOpen && (
243
+ <RemoteDesktopMobileKeyBar
244
+ sendMessage={sendMessage}
245
+ onCopyCombo={clipboardSync ? requestHostClipboard : undefined}
246
+ />
247
+ )}
173
248
  <RemoteDesktopMobileToolbar
174
249
  displayLabel={display.displays.length > 1 ? display.current?.label ?? null : null}
175
250
  onNextDisplay={display.next}
251
+ qualityLabel={shortQualityLabel(qualityChoice)}
252
+ onNextQuality={() => setQuality(nextQualityChoice(qualityChoice))}
176
253
  mode={mode}
177
254
  onToggleMode={toggleMode}
178
255
  onOpenKeyboard={openKeyboard}
179
256
  onResetZoom={resetZoom}
257
+ settingsOpen={settingsOpen}
258
+ onToggleSettings={() => setSettingsOpen((open) => !open)}
180
259
  onClose={onClose}
181
260
  />
182
261
  </div>