@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
@@ -17,46 +17,119 @@
17
17
  * `-pix_fmt` request leak into the device negotiation logs a warning per open.
18
18
  * - A missing Screen Recording grant does NOT fail the spawn — macOS hands the process black
19
19
  * frames instead; that is surfaced client-side by the "real frame" check, not here.
20
+ *
21
+ * X11 (`x11grab`) notes:
22
+ * - The grabber honours `-framerate` (unlike avfoundation), so it keeps gdigrab's plain filter.
23
+ * - X11 gives every monitor one root-window coordinate space, so capturing a single monitor is
24
+ * a crop: `-video_size WxH -i :0.0+X,Y`. Without the offsets you get the union of all
25
+ * monitors, which on a two-screen host is a letterboxed image of both.
26
+ * - `-draw_mouse` defaults to 1, but it is passed explicitly: it is the one thing that makes a
27
+ * remote session usable and it should not silently change with an ffmpeg default. It is also
28
+ * the only cursor control there is — the pointer is burned into the frames at *capture* time,
29
+ * so turning it off is a respawn, never a client-side decision.
30
+ * - The spawn needs `DISPLAY`/`XAUTHORITY` in its env — see `remote-desktop-linux-session.ts`
31
+ * for why the PPM process usually does not have them itself.
20
32
  */
21
- import { CAPTURE_FRAMERATE } from "./remote-desktop-encoder-args.ts";
33
+ import { DEFAULT_FPS, type QualityPreset } from "./remote-desktop-quality.ts";
34
+ import { VAAPI_UPLOAD_FILTER } from "../media-transcode/ffmpeg-capabilities.ts";
35
+ import { detectLinuxSession, type LinuxSession } from "./remote-desktop-linux-session.ts";
36
+
37
+ /** The region to grab, in the OS's global coordinate space. null = the backend's whole surface. */
38
+ export interface CaptureRect { x: number; y: number; width: number; height: number }
22
39
 
23
40
  export type CaptureInput =
24
41
  | { kind: "gdigrab" }
25
- | { kind: "avfoundation"; screen: string };
42
+ | { kind: "avfoundation"; screen: string }
43
+ | { kind: "x11grab"; display: string; rect: CaptureRect | null };
26
44
 
27
45
  /** avfoundation's name for the N-th display in `CGGetActiveDisplayList` order (0 = main). */
28
46
  export function avfoundationScreenName(captureIndex: number): string {
29
47
  return `Capture screen ${captureIndex}`;
30
48
  }
31
49
 
32
- /** ffmpeg args from `-f <grabber>` through `-i <source>` for the given input. */
33
- export function captureInputArgs(input: CaptureInput): string[] {
50
+ /** x11grab's source string: `:<display>.<screen>+<x>,<y>`. The screen number is only appended
51
+ * when the display string does not already carry one — `DISPLAY` is normally `:0`, but a
52
+ * `:0.0` in the environment would otherwise become `:0.0.0` and fail to parse. */
53
+ export function x11GrabSource(display: string, rect: CaptureRect | null): string {
54
+ const withScreen = display.includes(".") ? display : `${display}.0`;
55
+ return rect ? `${withScreen}+${rect.x},${rect.y}` : withScreen;
56
+ }
57
+
58
+ /** ffmpeg args from `-f <grabber>` through `-i <source>` for the given input.
59
+ *
60
+ * `drawMouse` is the host pointer. All three grabbers draw it by default and each spells the
61
+ * flag differently — gdigrab and x11grab take `-draw_mouse`, avfoundation `-capture_cursor` —
62
+ * so the one place that knows which grabber is in use is the one that has to translate it. */
63
+ export function captureInputArgs(
64
+ input: CaptureInput,
65
+ preset: QualityPreset = { fps: DEFAULT_FPS, bitrate: "1M" },
66
+ drawMouse = true,
67
+ ): string[] {
68
+ const fps = String(preset.fps);
69
+ const mouse = drawMouse ? "1" : "0";
34
70
  switch (input.kind) {
35
71
  case "gdigrab":
36
- return ["-f", "gdigrab", "-framerate", String(CAPTURE_FRAMERATE), "-i", "desktop"];
72
+ return ["-f", "gdigrab", "-draw_mouse", mouse, "-framerate", fps, "-i", "desktop"];
37
73
  case "avfoundation":
38
- // `-capture_cursor 1` draws the pointer into the frame (gdigrab does so by default).
39
74
  return ["-use_wallclock_as_timestamps", "1",
40
- "-f", "avfoundation", "-capture_cursor", "1", "-pixel_format", "nv12",
41
- "-framerate", String(CAPTURE_FRAMERATE), "-i", input.screen];
75
+ "-f", "avfoundation", "-capture_cursor", mouse, "-pixel_format", "nv12",
76
+ "-framerate", fps, "-i", input.screen];
77
+ case "x11grab":
78
+ return ["-f", "x11grab", "-draw_mouse", mouse, "-framerate", fps,
79
+ ...(input.rect ? ["-video_size", `${input.rect.width}x${input.rect.height}`] : []),
80
+ "-i", x11GrabSource(input.display, input.rect)];
42
81
  }
43
82
  }
44
83
 
45
- /** `-vf` chain for the given input; the scale step is shared, avfoundation adds the rate cap. */
46
- export function captureVideoFilter(input: CaptureInput): string {
47
- const scale = "scale=-2:720";
48
- return input.kind === "avfoundation" ? `fps=${CAPTURE_FRAMERATE},${scale}` : scale;
84
+ /** `-vf` chain for the given input; avfoundation adds the rate cap and VAAPI appends the upload
85
+ * to GPU memory that its encoder requires. One `-vf` only: a second occurrence would silently
86
+ * replace the first rather than combining.
87
+ *
88
+ * There is **no scale step**. A quality rung sets a bitrate ratio and nothing else — RustDesk's
89
+ * image quality never touches the resolution, and measured on a 3440x1440 host the downscale
90
+ * this used to do bought almost nothing: 720p used 1.17 Mbit/s against 1.26 Mbit/s for native
91
+ * at the same cap, because a desktop is mostly static and H.264 spends bits on *change* rather
92
+ * than on area. It cost 4.8x the pixels for 7% of the bandwidth. Dropping the filter also
93
+ * removes the upscale hazard it existed to guard against: with no scaling at all, a rung cannot
94
+ * ask for more pixels than the host has.
95
+ *
96
+ * Returns an empty string when there is nothing to filter, which the caller must treat as
97
+ * "omit `-vf` entirely" — ffmpeg rejects an empty filtergraph argument. */
98
+ export function captureVideoFilter(
99
+ input: CaptureInput,
100
+ encoder = "libx264",
101
+ preset: QualityPreset = { fps: DEFAULT_FPS, bitrate: "1M" },
102
+ ): string {
103
+ const parts: string[] = [];
104
+ // avfoundation ignores `-framerate` and delivers at display refresh with a stuck pts, so the
105
+ // cadence has to be imposed here; the other grabbers take it at the input.
106
+ if (input.kind === "avfoundation") parts.push(`fps=${preset.fps}`);
107
+ if (encoder === "h264_vaapi") parts.push(VAAPI_UPLOAD_FILTER);
108
+ return parts.join(",");
49
109
  }
50
110
 
51
111
  /** Pick the capture input for this host; null on platforms without a grabber. Whether the
52
112
  * grabber actually works (ffmpeg built without it, no display) surfaces through ffmpeg's
53
113
  * own exit + stderr tail in `startCapture`, the same way gdigrab failures do.
54
114
  * `captureIndex` selects the display on backends that capture one at a time (avfoundation);
55
- * gdigrab always grabs the whole virtual desktop. */
56
- export function captureInputForPlatform(platform: NodeJS.Platform = process.platform, captureIndex = 0): CaptureInput | null {
115
+ * gdigrab always grabs the whole virtual desktop, x11grab crops by `rect`.
116
+ *
117
+ * `opts.session` is passed explicitly by tests so this stays a pure function of its
118
+ * arguments: it otherwise probes the host, and "is there an X server" is exactly the thing a
119
+ * headless CI runner answers differently from a developer's desktop. */
120
+ export function captureInputForPlatform(
121
+ platform: NodeJS.Platform = process.platform,
122
+ captureIndex = 0,
123
+ opts: { session?: LinuxSession | null; rect?: CaptureRect | null } = {},
124
+ ): CaptureInput | null {
57
125
  switch (platform) {
58
126
  case "win32": return { kind: "gdigrab" };
59
127
  case "darwin": return { kind: "avfoundation", screen: avfoundationScreenName(captureIndex) };
128
+ case "linux": {
129
+ const session = "session" in opts ? opts.session : detectLinuxSession();
130
+ if (session?.kind !== "x11") return null;
131
+ return { kind: "x11grab", display: session.display, rect: opts.rect ?? null };
132
+ }
60
133
  default: return null;
61
134
  }
62
135
  }
@@ -10,11 +10,14 @@
10
10
  * the in-flight `reader.read()` then resolves with `done: true` and the pump loop exits on
11
11
  * its own — it is never cancelled from outside.
12
12
  */
13
- import { getFfmpegCapabilities } from "../media-transcode/ffmpeg-capabilities.ts";
13
+ import { encoderDeviceArgs, getFfmpegCapabilities } from "../media-transcode/ffmpeg-capabilities.ts";
14
14
  import { captureEncoderArgs } from "./remote-desktop-encoder-args.ts";
15
15
  import {
16
- captureInputArgs, captureVideoFilter, captureInputForPlatform, type CaptureInput,
16
+ captureInputArgs, captureVideoFilter, captureInputForPlatform,
17
+ type CaptureInput, type CaptureRect,
17
18
  } from "./remote-desktop-capture-input.ts";
19
+ import { detectLinuxSession, linuxSessionEnv } from "./remote-desktop-linux-session.ts";
20
+ import { DEFAULT_FPS, type QualityPreset } from "./remote-desktop-quality.ts";
18
21
  import { AccessUnitAssembler, type AccessUnit } from "./access-unit-assembler.ts";
19
22
  import type { RemoteDisplay } from "./remote-desktop-displays.ts";
20
23
 
@@ -34,18 +37,28 @@ export function buildCaptureArgs(
34
37
  ffmpeg: string,
35
38
  encoder: string = "libx264",
36
39
  input: CaptureInput = { kind: "gdigrab" },
40
+ preset: QualityPreset = { fps: DEFAULT_FPS, bitrate: "1M" },
41
+ drawMouse = true,
37
42
  ): string[] {
38
43
  return [
39
44
  ffmpeg, "-hide_banner", "-loglevel", "error", "-nostdin",
40
45
  "-fflags", "nobuffer", "-flags", "low_delay",
41
- ...captureInputArgs(input),
42
- "-vf", captureVideoFilter(input),
43
- ...captureEncoderArgs(encoder),
46
+ // VAAPI opens its DRM device during input setup, so this has to precede `-i`.
47
+ ...encoderDeviceArgs(encoder),
48
+ ...captureInputArgs(input, preset, drawMouse),
49
+ // Spread rather than a fixed pair: with no scale step a software encoder on x11grab/gdigrab
50
+ // has nothing left to filter, and ffmpeg rejects `-vf ""` outright rather than ignoring it.
51
+ ...withVideoFilter(captureVideoFilter(input, encoder, preset)),
52
+ ...captureEncoderArgs(encoder, preset),
44
53
  "-flush_packets", "1",
45
54
  "-f", "h264", "pipe:1",
46
55
  ];
47
56
  }
48
57
 
58
+ function withVideoFilter(filter: string): string[] {
59
+ return filter ? ["-vf", filter] : [];
60
+ }
61
+
49
62
  export interface CaptureHandle {
50
63
  /** SPS bytes cached from the stream (for the avc1 codec string), null before the encoder's
51
64
  * first keyframe has been parsed. */
@@ -59,6 +72,15 @@ export interface CaptureHandle {
59
72
  export interface StartCaptureOptions {
60
73
  /** Which display to grab; null/undefined = the platform default (primary / whole desktop). */
61
74
  display?: RemoteDisplay | null;
75
+ /** Resolution/frame-rate/bitrate rung; defaults to the balanced preset. */
76
+ preset?: QualityPreset;
77
+ /** Draw the host pointer into the frames. Defaults to true, and can only be changed by
78
+ * respawning: every grabber takes it as a startup flag. */
79
+ drawMouse?: boolean;
80
+ /** H.264 encoder to use, overriding the capability probe's first choice. Must be one that
81
+ * `workingEncoders()` reported: an encoder this build/GPU cannot run makes ffmpeg exit at
82
+ * once, which the session then reports as "Capture failed". */
83
+ encoder?: string;
62
84
  onAccessUnit: (au: AccessUnit) => void;
63
85
  /** Called once the process exits, whether via `stop()` or on its own (crash/killed
64
86
  * externally) — lets the session registry clean up without polling. `reason` is a short
@@ -73,13 +95,24 @@ export interface StartCaptureOptions {
73
95
  export async function startCapture(opts: StartCaptureOptions): Promise<CaptureHandle> {
74
96
  const caps = await getFfmpegCapabilities();
75
97
  if (!caps.ffmpeg) throw new CaptureUnavailableError();
76
- const input = captureInputForPlatform(process.platform, opts.display?.captureIndex ?? 0);
98
+ // Linux only: the grabber is chosen by session type, and ffmpeg is handed the session's
99
+ // DISPLAY/XAUTHORITY because the PPM process very often has neither of its own.
100
+ const session = process.platform === "linux" ? detectLinuxSession() : null;
101
+ const d = opts.display;
102
+ const rect: CaptureRect | null =
103
+ d && d.width > 0 && d.height > 0 ? { x: d.x, y: d.y, width: d.width, height: d.height } : null;
104
+ const input = captureInputForPlatform(process.platform, d?.captureIndex ?? 0, { session, rect });
77
105
  if (!input) throw new CaptureUnavailableError(`no screen capture input on ${process.platform}`);
78
106
 
79
- const proc = Bun.spawn(buildCaptureArgs(caps.ffmpeg, caps.encoder ?? "libx264", input), {
107
+ const argv = buildCaptureArgs(
108
+ caps.ffmpeg, opts.encoder ?? caps.encoder ?? "libx264", input, opts.preset,
109
+ opts.drawMouse ?? true,
110
+ );
111
+ const proc = Bun.spawn(argv, {
80
112
  stdout: "pipe",
81
113
  stderr: "pipe",
82
114
  stdin: "ignore",
115
+ ...(session ? { env: { ...process.env, ...linuxSessionEnv(session) } } : {}),
83
116
  });
84
117
 
85
118
  let stopped = false;
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Read and write the *host's* clipboard for the remote-desktop session.
3
+ *
4
+ * Shelling out rather than talking X11/Win32 directly, because owning a selection is not a
5
+ * one-shot call: an X11 clipboard owner has to stay alive answering `SelectionRequest` events
6
+ * for as long as it holds the selection, which means an event pump for the lifetime of the
7
+ * copy. `xclip`/`wl-copy` already fork a helper that does exactly that, so the cost of this
8
+ * choice is one package the host may not have — surfaced as `clipboardTool() === null` and
9
+ * an install command, never as a silent no-op (this dev host has *none* of xclip/xsel/wl-copy
10
+ * while running a perfectly good X11 desktop, which is the same trap as `xrandr` the binary).
11
+ *
12
+ * Everything is text/plain. Images are deliberately out of scope: they would need a second
13
+ * transport and the browser half only accepts them in a secure context anyway.
14
+ */
15
+ import { existsSync } from "node:fs";
16
+ import { detectLinuxSession, linuxSessionEnv, type LinuxSession } from "./remote-desktop-linux-session.ts";
17
+
18
+ /** A paste is a paste, not a file transfer. 64 KiB is far more than any realistic snippet and
19
+ * bounds both the WS message and the argv/stdin handed to a helper process. */
20
+ export const MAX_CLIPBOARD_CHARS = 64 * 1024;
21
+ /** A clipboard helper that hangs (no selection owner, a wedged compositor) must not hang the
22
+ * session's message loop, which is awaited per WS message. */
23
+ const CLIPBOARD_TIMEOUT_MS = 2_000;
24
+
25
+ export interface ClipboardTool {
26
+ /** Reads text/plain from the clipboard on stdout. */
27
+ read: string[];
28
+ /** Writes stdin to the clipboard. */
29
+ write: string[];
30
+ /** Both sides are base64 so no console code page can mangle non-ASCII (Windows). */
31
+ base64: boolean;
32
+ /** The package to install when this host has no tool at all. */
33
+ install?: string;
34
+ }
35
+
36
+ /** PowerShell's `Get-Clipboard`/`Set-Clipboard` go through stdout/stdin, whose encoding follows
37
+ * the console code page — so the text is base64'd on both sides rather than trusting it.
38
+ * `-Raw` keeps a multi-line clipboard as one string, and the `$null` guard is needed because
39
+ * an empty clipboard makes `GetBytes` throw rather than return nothing. */
40
+ const WIN_READ = "$t = Get-Clipboard -Raw; if ($null -eq $t) { '' } else "
41
+ + "{ [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($t)) }";
42
+ const WIN_WRITE = "Set-Clipboard -Value "
43
+ + "([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String([Console]::In.ReadToEnd())))";
44
+
45
+ /** Which helper this host has, or null. Linux is by session type: `wl-copy` talks to the
46
+ * compositor and `xclip`/`xsel` to the X server, and on a Wayland session the X pair would
47
+ * address XWayland's own clipboard — a different clipboard from the one the user sees. */
48
+ export function clipboardTool(
49
+ platform: NodeJS.Platform = process.platform,
50
+ session: LinuxSession | null = platform === "linux" ? detectLinuxSession() : null,
51
+ ): ClipboardTool | null {
52
+ if (platform === "darwin") {
53
+ return { read: ["pbpaste"], write: ["pbcopy"], base64: false };
54
+ }
55
+ if (platform === "win32") {
56
+ const pwsh = ["powershell", "-NoProfile", "-NonInteractive", "-Command"];
57
+ return { read: [...pwsh, WIN_READ], write: [...pwsh, WIN_WRITE], base64: true };
58
+ }
59
+ if (platform !== "linux" || !session) return null;
60
+ if (session.kind === "wayland") {
61
+ return has("wl-copy") && has("wl-paste")
62
+ ? { read: ["wl-paste", "--no-newline", "--type", "text/plain"], write: ["wl-copy"], base64: false, install: "wl-clipboard" }
63
+ : { read: [], write: [], base64: false, install: "wl-clipboard" };
64
+ }
65
+ if (has("xclip")) {
66
+ return {
67
+ read: ["xclip", "-selection", "clipboard", "-o"],
68
+ write: ["xclip", "-selection", "clipboard", "-i"],
69
+ base64: false,
70
+ install: "xclip",
71
+ };
72
+ }
73
+ if (has("xsel")) {
74
+ return {
75
+ read: ["xsel", "--clipboard", "--output"],
76
+ write: ["xsel", "--clipboard", "--input"],
77
+ base64: false,
78
+ install: "xsel",
79
+ };
80
+ }
81
+ return { read: [], write: [], base64: false, install: "xclip" };
82
+ }
83
+
84
+ /** `Bun.which` also finds a shell builtin/alias shape; an explicit PATH walk is enough here and
85
+ * keeps the tool list assertable in a test without a real binary. */
86
+ function has(bin: string): boolean {
87
+ return (process.env.PATH ?? "").split(":").some((dir) => dir && existsSync(`${dir}/${bin}`));
88
+ }
89
+
90
+ /** True when this host can actually move the clipboard. A tool entry with empty argv means
91
+ * "this is the tool you want, it is not installed" — that is what carries `install` to the UI. */
92
+ export function clipboardAvailable(tool: ClipboardTool | null = clipboardTool()): boolean {
93
+ return !!tool && tool.read.length > 0 && tool.write.length > 0;
94
+ }
95
+
96
+ /** Env a Linux helper needs to reach the session (a PPM started by its systemd user unit has
97
+ * no `DISPLAY`/`WAYLAND_DISPLAY` at all — same reason ffmpeg is spawned with this). */
98
+ function spawnEnv(session: LinuxSession | null): Record<string, string | undefined> {
99
+ return session ? { ...process.env, ...linuxSessionEnv(session) } : process.env;
100
+ }
101
+
102
+ /** The host clipboard as text, or null when there is no tool, it failed, or it holds no text
103
+ * (an image-only clipboard reads as empty rather than as an error). */
104
+ export async function readHostClipboard(): Promise<string | null> {
105
+ const session = process.platform === "linux" ? detectLinuxSession() : null;
106
+ const tool = clipboardTool(process.platform, session);
107
+ if (!clipboardAvailable(tool)) return null;
108
+ try {
109
+ const proc = Bun.spawn(tool!.read, { stdout: "pipe", stderr: "ignore", env: spawnEnv(session) });
110
+ const out = await withTimeout(new Response(proc.stdout).text(), () => proc.kill());
111
+ if (out === null) return null;
112
+ // A non-zero exit is normal for an empty clipboard (`xclip` reports no owner), so the exit
113
+ // code is not treated as failure — an empty read is simply an empty clipboard.
114
+ const text = tool!.base64 ? decodeBase64(out.trim()) : out;
115
+ return text === null || text.length === 0 ? null : text.slice(0, MAX_CLIPBOARD_CHARS);
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+
121
+ /** Put `text` on the host clipboard. Returns whether it landed. */
122
+ export async function writeHostClipboard(text: string): Promise<boolean> {
123
+ if (text.length === 0 || text.length > MAX_CLIPBOARD_CHARS) return false;
124
+ const session = process.platform === "linux" ? detectLinuxSession() : null;
125
+ const tool = clipboardTool(process.platform, session);
126
+ if (!clipboardAvailable(tool)) return false;
127
+ try {
128
+ const payload = tool!.base64 ? Buffer.from(text, "utf8").toString("base64") : text;
129
+ const proc = Bun.spawn(tool!.write, {
130
+ stdin: new TextEncoder().encode(payload),
131
+ stdout: "ignore",
132
+ stderr: "ignore",
133
+ env: spawnEnv(session),
134
+ });
135
+ // `xclip -i`/`wl-copy` fork a helper that keeps owning the selection and the parent exits
136
+ // straight away, so this waits on the parent only — by design, not by accident.
137
+ const code = await withTimeout(proc.exited, () => proc.kill());
138
+ return code === 0;
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
143
+
144
+ /** Resolve `p`, or run `onTimeout` and give null, so one wedged helper cannot stall the session. */
145
+ async function withTimeout<T>(p: Promise<T>, onTimeout: () => void): Promise<T | null> {
146
+ const timeout = Bun.sleep(CLIPBOARD_TIMEOUT_MS).then(() => null);
147
+ const result = await Promise.race([p, timeout]);
148
+ if (result === null) { try { onTimeout(); } catch { /* already gone */ } }
149
+ return result as T | null;
150
+ }
151
+
152
+ /** The keys to press so the host pastes, as `KeyboardEvent.code`s to press in order (and
153
+ * release in reverse).
154
+ *
155
+ * `shift` is not cosmetic: a terminal pastes on Ctrl+**Shift**+V and reads a plain Ctrl+V as
156
+ * the literal quoted-insert control code, so answering every paste with Ctrl+V would insert
157
+ * `^V` into every remote shell. The client sends what the user actually pressed. */
158
+ export function pasteComboCodes(platform: NodeJS.Platform, shift: boolean): string[] {
159
+ const mod = platform === "darwin" ? "MetaLeft" : "ControlLeft";
160
+ return shift ? [mod, "ShiftLeft", "KeyV"] : [mod, "KeyV"];
161
+ }
162
+
163
+ function decodeBase64(b64: string): string | null {
164
+ if (b64.length === 0) return "";
165
+ try { return Buffer.from(b64, "base64").toString("utf8"); } catch { return null; }
166
+ }
@@ -11,8 +11,21 @@
11
11
  *
12
12
  * Windows: gdigrab `desktop` grabs the whole virtual screen and SendInput maps 0..65535 onto
13
13
  * that same rectangle, so there is exactly one "display" and no offsets to apply.
14
+ *
15
+ * X11: monitors come from RandR 1.5 `XRRGetMonitors` through FFI, NOT from parsing `xrandr`.
16
+ * The binary is a separate package from the library and is genuinely absent on hosts that have
17
+ * the library (verified: this dev host has `libXrandr.so.2` and no `xrandr`), so a shell-out
18
+ * reports "no displays" on a working desktop. X11 puts every monitor in one root-window
19
+ * coordinate space — the same model as the Windows virtual desktop — so `x`/`y` are real
20
+ * offsets and a per-monitor capture is a crop of the root window, not a separate device.
21
+ *
22
+ * Wayland: deliberately empty. There is no client-side monitor list to offer — the desktop
23
+ * portal's own dialog is what picks the screen, and its answer arrives as a PipeWire node.
14
24
  */
15
25
 
26
+ import { detectLinuxSession, type LinuxSession } from "./remote-desktop-linux-session.ts";
27
+ import { asPointer, getX11 } from "./remote-desktop-x11.ts";
28
+
16
29
  export interface RemoteDisplay {
17
30
  /** Stable per host session (`CGDirectDisplayID` on macOS, `"desktop"` on Windows). */
18
31
  id: string;
@@ -30,6 +43,56 @@ export interface RemoteDisplay {
30
43
  const CACHE_MS = 5000;
31
44
  let cache: { at: number; displays: RemoteDisplay[] } | null = null;
32
45
 
46
+ /** `XRRMonitorInfo` on x86-64: `Atom name` (8), `Bool primary` (4), `Bool automatic` (4),
47
+ * `int noutput` (4), `int x, y, width, height` (16), `int mwidth, mheight` (8), then a
48
+ * pointer that forces the trailing 4 bytes of padding. Verified against a real 3440x1440
49
+ * monitor: reading `primary`/`x`/`y`/`width`/`height` at these offsets returns 1/0/0/3440/1440. */
50
+ const MONITOR_INFO_SIZE = 56;
51
+ const MONITOR_OFF = { name: 0, primary: 8, x: 20, y: 24, width: 28, height: 32 } as const;
52
+
53
+ async function listX11Displays(session: LinuxSession): Promise<RemoteDisplay[]> {
54
+ const conn = await getX11(session);
55
+ if (!conn) return [];
56
+ const { ffi, x11, xrandr, dpy } = conn;
57
+
58
+ /** No libXrandr: one display covering the whole root window, which is what x11grab
59
+ * captures by default anyway. */
60
+ const wholeScreen = (): RemoteDisplay[] => [{
61
+ id: "screen", label: "Whole screen", primary: true, x: 0, y: 0,
62
+ width: x11.XDisplayWidth(dpy, conn.screen), height: x11.XDisplayHeight(dpy, conn.screen),
63
+ captureIndex: 0,
64
+ }];
65
+ if (!xrandr) return wholeScreen();
66
+
67
+ const countOut = new Int32Array(1);
68
+ const monitors = xrandr.XRRGetMonitors(dpy, conn.root, 1, ffi.ptr(countOut));
69
+ const count = countOut[0] ?? 0;
70
+ if (!monitors || count <= 0) return wholeScreen();
71
+
72
+ const displays: RemoteDisplay[] = [];
73
+ for (let i = 0; i < count; i++) {
74
+ const base = asPointer(Number(monitors) + i * MONITOR_INFO_SIZE);
75
+ const namePtr = x11.XGetAtomName(dpy, ffi.read.u64(base, MONITOR_OFF.name));
76
+ let label = `Display ${i + 1}`;
77
+ if (namePtr) {
78
+ label = new ffi.CString(namePtr).toString();
79
+ x11.XFree(namePtr);
80
+ }
81
+ displays.push({
82
+ id: label,
83
+ label,
84
+ primary: ffi.read.i32(base, MONITOR_OFF.primary) !== 0,
85
+ x: ffi.read.i32(base, MONITOR_OFF.x),
86
+ y: ffi.read.i32(base, MONITOR_OFF.y),
87
+ width: ffi.read.i32(base, MONITOR_OFF.width),
88
+ height: ffi.read.i32(base, MONITOR_OFF.height),
89
+ captureIndex: i,
90
+ });
91
+ }
92
+ xrandr.XRRFreeMonitors(monitors);
93
+ return displays.length > 0 ? displays : wholeScreen();
94
+ }
95
+
33
96
  async function listDarwinDisplays(): Promise<RemoteDisplay[]> {
34
97
  const { dlopen, FFIType: T, ptr } = await import("bun:ffi");
35
98
  const cg = dlopen("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics", {
@@ -71,10 +134,23 @@ async function listDarwinDisplays(): Promise<RemoteDisplay[]> {
71
134
  return displays;
72
135
  }
73
136
 
74
- export async function listDisplays(platform: NodeJS.Platform = process.platform): Promise<RemoteDisplay[]> {
137
+ /** `linuxSession` is passed explicitly by tests: it otherwise probes the host, and "is there
138
+ * an X server" is exactly what a headless runner answers differently from a desktop. */
139
+ export async function listDisplays(
140
+ platform: NodeJS.Platform = process.platform,
141
+ linuxSession = platform === "linux" ? detectLinuxSession() : null,
142
+ ): Promise<RemoteDisplay[]> {
75
143
  if (platform === "win32") {
76
144
  return [{ id: "desktop", label: "All displays", primary: true, x: 0, y: 0, width: 0, height: 0, captureIndex: 0 }];
77
145
  }
146
+ if (platform === "linux") {
147
+ // Wayland: the portal dialog picks the screen, so there is nothing to list here.
148
+ if (linuxSession?.kind !== "x11") return [];
149
+ if (cache && Date.now() - cache.at < CACHE_MS) return cache.displays;
150
+ const displays = await listX11Displays(linuxSession);
151
+ cache = { at: Date.now(), displays };
152
+ return displays;
153
+ }
78
154
  if (platform !== "darwin") return [];
79
155
  if (cache && Date.now() - cache.at < CACHE_MS) return cache.displays;
80
156
  const displays = await listDarwinDisplays();
@@ -10,10 +10,19 @@
10
10
  * predictable during development.
11
11
  */
12
12
 
13
- /** 30fps + a 1s keyframe interval (30 frames): smoother than 15fps and bounds how long a
14
- * dropped/late-joining client waits to resync (see WS backpressure handling). */
15
- export const CAPTURE_FRAMERATE = 30;
16
- export const CAPTURE_GOP_FRAMES = 30;
13
+ import { DEFAULT_FPS, type QualityPreset } from "./remote-desktop-quality.ts";
14
+
15
+ /** Half a second of frames. The GOP is the single lever on "how bad is a hiccup": discarding
16
+ * one delta makes every later delta in the GOP undecodable, so the client stares at a frozen
17
+ * picture until the next keyframe — measured at 502 ms from a 120 ms backpressure spike while
18
+ * this was one full second. Halving it halves that worst case, and costs **no bitrate**:
19
+ * measured against a full-motion 3440x1440 source, -0.0% at `balanced`, -0.1% at `high`,
20
+ * +0.3% at `tiny`. It is free because the encode is rate-capped (`-b:v` = `-maxrate`), so the
21
+ * extra keyframes redistribute the same budget instead of adding to it — the real price is a
22
+ * little per-frame quality, which is the right trade for halving a frozen picture. */
23
+ export function gopFrames(preset: QualityPreset): number {
24
+ return Math.max(1, Math.round(preset.fps / 2));
25
+ }
17
26
 
18
27
  /** ffmpeg output args for the H.264 encode step (goes after `-i desktop -vf ...`).
19
28
  * Prefers hardware NVENC (verified to work even under an RDP session on this class of GPU) —
@@ -21,8 +30,13 @@ export const CAPTURE_GOP_FRAMES = 30;
21
30
  * a fast/LAN transport. Falls back to a low-latency libx264 profile when no hardware H.264
22
31
  * encoder is available. The avc1 codec string is derived from the real SPS at runtime, so the
23
32
  * encoder's actual profile/level is handled regardless of which branch runs. */
24
- export function captureEncoderArgs(encoder: string = "libx264"): string[] {
25
- const common = ["-b:v", "6M", "-maxrate", "6M", "-g", String(CAPTURE_GOP_FRAMES), "-bf", "0", "-pix_fmt", "yuv420p"];
33
+ export function captureEncoderArgs(
34
+ encoder: string = "libx264",
35
+ preset: QualityPreset = { fps: DEFAULT_FPS, bitrate: "1M" },
36
+ ): string[] {
37
+ const rate = ["-b:v", preset.bitrate, "-maxrate", preset.bitrate,
38
+ "-g", String(gopFrames(preset)), "-bf", "0"];
39
+ const common = [...rate, "-pix_fmt", "yuv420p"];
26
40
  switch (encoder) {
27
41
  case "h264_nvenc":
28
42
  // p2 + `-tune ll` (low latency), CBR, no frame reordering, minimal output delay/VBV.
@@ -30,6 +44,11 @@ export function captureEncoderArgs(encoder: string = "libx264"): string[] {
30
44
  "-zerolatency", "1", "-delay", "0", "-bufsize", "512k", ...common];
31
45
  case "h264_qsv":
32
46
  return ["-c:v", "h264_qsv", "-preset", "veryfast", "-low_power", "1", "-bufsize", "512k", ...common];
47
+ case "h264_vaapi":
48
+ // Linux hardware encode on Intel and AMD. No `-pix_fmt`: the frames arriving here are
49
+ // already GPU-resident nv12 (see `VAAPI_UPLOAD_FILTER`), and a software pixel format on
50
+ // the output makes ffmpeg refuse the hardware frame context outright.
51
+ return ["-c:v", "h264_vaapi", "-rc_mode", "CBR", ...rate];
33
52
  case "h264_amf":
34
53
  return ["-c:v", "h264_amf", "-quality", "speed", "-rc", "cbr", "-bufsize", "512k", ...common];
35
54
  case "h264_videotoolbox":
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `KeyboardEvent.code` (physical key, layout-independent) → Linux evdev key code
3
+ * (`KEY_*` from `linux/input-event-codes.h`). Same contract as `remote-desktop-vk-map.ts`
4
+ * and `remote-desktop-cg-key-map.ts`: only `code` is accepted, never `key`.
5
+ *
6
+ * Evdev codes rather than X11 keysyms, and that choice is what makes this table correct on a
7
+ * non-US host. `XKeysymToKeycode(XK_a)` answers "which key currently *produces* an a", which
8
+ * on an AZERTY host is the physical Q key — so a keysym table would silently transpose the
9
+ * whole keyboard for anyone not on a US layout. An evdev code names the physical switch, which
10
+ * is exactly what `KeyboardEvent.code` also names, so the mapping is a rename rather than a
11
+ * lookup through the active layout.
12
+ *
13
+ * The same table serves both Linux backends: XTEST wants `evdev + 8` (see `X11_KEYCODE_OFFSET`)
14
+ * and uinput wants the raw code.
15
+ */
16
+
17
+ const CODE_TO_EVDEV: Record<string, number> = {
18
+ Escape: 1,
19
+ Digit1: 2, Digit2: 3, Digit3: 4, Digit4: 5, Digit5: 6,
20
+ Digit6: 7, Digit7: 8, Digit8: 9, Digit9: 10, Digit0: 11,
21
+ Minus: 12, Equal: 13, Backspace: 14, Tab: 15,
22
+ KeyQ: 16, KeyW: 17, KeyE: 18, KeyR: 19, KeyT: 20, KeyY: 21,
23
+ KeyU: 22, KeyI: 23, KeyO: 24, KeyP: 25,
24
+ BracketLeft: 26, BracketRight: 27, Enter: 28, ControlLeft: 29,
25
+ KeyA: 30, KeyS: 31, KeyD: 32, KeyF: 33, KeyG: 34,
26
+ KeyH: 35, KeyJ: 36, KeyK: 37, KeyL: 38,
27
+ Semicolon: 39, Quote: 40, Backquote: 41, ShiftLeft: 42, Backslash: 43,
28
+ KeyZ: 44, KeyX: 45, KeyC: 46, KeyV: 47, KeyB: 48, KeyN: 49, KeyM: 50,
29
+ Comma: 51, Period: 52, Slash: 53, ShiftRight: 54,
30
+ NumpadMultiply: 55, AltLeft: 56, Space: 57, CapsLock: 58,
31
+ F1: 59, F2: 60, F3: 61, F4: 62, F5: 63, F6: 64, F7: 65, F8: 66, F9: 67, F10: 68,
32
+ NumLock: 69, ScrollLock: 70,
33
+ Numpad7: 71, Numpad8: 72, Numpad9: 73, NumpadSubtract: 74,
34
+ Numpad4: 75, Numpad5: 76, Numpad6: 77, NumpadAdd: 78,
35
+ Numpad1: 79, Numpad2: 80, Numpad3: 81, Numpad0: 82, NumpadDecimal: 83,
36
+ /** The extra key ISO layouts have next to the left Shift (`KEY_102ND`). */
37
+ IntlBackslash: 86,
38
+ F11: 87, F12: 88,
39
+ /** Japanese layouts (`KEY_RO`, `KEY_YEN`). */
40
+ IntlRo: 89,
41
+ NumpadEnter: 96, ControlRight: 97, NumpadDivide: 98,
42
+ PrintScreen: 99, AltRight: 100,
43
+ Home: 102, ArrowUp: 103, PageUp: 104, ArrowLeft: 105, ArrowRight: 106,
44
+ End: 107, ArrowDown: 108, PageDown: 109, Insert: 110, Delete: 111,
45
+ Pause: 119,
46
+ IntlYen: 124,
47
+ MetaLeft: 125, MetaRight: 126, ContextMenu: 127,
48
+ F13: 183, F14: 184, F15: 185, F16: 186, F17: 187, F18: 188,
49
+ F19: 189, F20: 190, F21: 191, F22: 192, F23: 193, F24: 194,
50
+ };
51
+
52
+ /** X11 keycodes are evdev codes shifted by 8 — an XFree86 legacy that every Linux X server
53
+ * still honours (verified against a live server: `KEY_A` 30 → X keycode 38, `KEY_ESC` 1 → 9,
54
+ * `KEY_LEFTMETA` 125 → 133). */
55
+ export const X11_KEYCODE_OFFSET = 8;
56
+
57
+ /** Every modifier's evdev code — used to force-release stuck modifiers on teardown (a lost
58
+ * keyup otherwise leaves e.g. Shift logically held on the host after the WS drops). */
59
+ export const MODIFIER_EVDEV_CODES = [42, 54, 29, 97, 56, 100, 125, 126] as const;
60
+
61
+ /** Every code this table can emit. A uinput device must declare each key it will ever send
62
+ * (`UI_SET_KEYBIT`) before creation — an undeclared key is dropped by the kernel silently. */
63
+ export const ALL_EVDEV_CODES: readonly number[] = [...new Set(Object.values(CODE_TO_EVDEV))].sort((a, b) => a - b);
64
+
65
+ /** Resolve a `KeyboardEvent.code` to its evdev code; null for anything unmapped (IME
66
+ * composition, media keys) rather than guessing and injecting the wrong key. */
67
+ export function codeToEvdev(code: string): number | null {
68
+ return CODE_TO_EVDEV[code] ?? null;
69
+ }