@hienlh/ppm 0.21.0 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/CLAUDE.md +29 -4
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +4 -4
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/bun.lock +16 -1
- package/dist/web/assets/accounts-settings-section-D3BylchP.js +1 -0
- package/dist/web/assets/{ai-resource-editor-F09pf8Hd.js → ai-resource-editor-BLPcnhTM.js} +1 -1
- package/dist/web/assets/ai-settings-section-BtR7rF-T.js +1 -0
- package/dist/web/assets/ai-settings-section-r9JctHNM.js +1 -0
- package/dist/web/assets/{angular-html-DkR4iCth.js → angular-html-G66FNlyn.js} +1 -1
- package/dist/web/assets/{angular-ts-DcyL4Chx.js → angular-ts-B-PnUMyt.js} +1 -1
- package/dist/web/assets/{api-client-bfB7cXRM.js → api-client-C1ZwRL4Y.js} +1 -1
- package/dist/web/assets/api-client-pQL53cDQ.js +1 -0
- package/dist/web/assets/{api-settings-BcNjZIvG.js → api-settings-_7Q-ULxp.js} +1 -1
- package/dist/web/assets/{api-themes-DBfyVUtX.js → api-themes-DRAERo4Q.js} +1 -1
- package/dist/web/assets/{apl-BVlrVtjt.js → apl-BmDqqwE8.js} +1 -1
- package/dist/web/assets/{appearance-settings-section-whgnq3ss.js → appearance-settings-section-CiMdk3sb.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-Dx6nBEKT.js +1 -0
- package/dist/web/assets/{astro-MjJlozLj.js → astro-BfwMNfxr.js} +1 -1
- package/dist/web/assets/{audio-preview-Bc54GCAV.js → audio-preview-DsYvgKqj.js} +1 -1
- package/dist/web/assets/{blade-DnJWi1fz.js → blade-BsFV4pnN.js} +1 -1
- package/dist/web/assets/branch-review-tab-B1pFjlKM.js +1 -0
- package/dist/web/assets/{bsl-DCmJxSIz.js → bsl-Cw9Outng.js} +1 -1
- package/dist/web/assets/{button-VW4QGKCn.js → button-B9CTrsEf.js} +1 -1
- package/dist/web/assets/{cairo-C8dDenub.js → cairo-DOK5Wqnd.js} +1 -1
- package/dist/web/assets/chat-tab-DZagX06m.js +15 -0
- package/dist/web/assets/{cobol-DPx_nifl.js → cobol-DRaRy4FK.js} +1 -1
- package/dist/web/assets/code-editor-BJhOEsnZ.js +7 -0
- package/dist/web/assets/{codex-daily-guard-CfybHv_c.js → codex-daily-guard-CaCQJ4r_.js} +1 -1
- package/dist/web/assets/{coffee-DvFzqhgN.js → coffee-DvXuBmEJ.js} +1 -1
- package/dist/web/assets/{conflict-editor-DenQUOT2.js → conflict-editor-B7u_p49h.js} +3 -3
- package/dist/web/assets/context-menu-C6pIsf83.js +1 -0
- package/dist/web/assets/{cpp-BLJN0umi.js → cpp-zCmm8rKA.js} +1 -1
- package/dist/web/assets/{crystal-BXR-9fGw.js → crystal-CdQx6lB6.js} +1 -1
- package/dist/web/assets/{csv-preview-CClrJOuR.js → csv-preview-CttjJU1e.js} +1 -1
- package/dist/web/assets/{data-grid-overlay-editor-D4fhcMfT.js → data-grid-overlay-editor-6xUCeLHh.js} +1 -1
- package/dist/web/assets/{database-viewer-DItjm8wB.js → database-viewer-Ca4HxIfx.js} +1 -1
- package/dist/web/assets/{dialog-DQTn55Mt.js → dialog-B84_DJQ8.js} +1 -1
- package/dist/web/assets/diff-viewer-9-XjLIwa.js +4 -0
- package/dist/web/assets/{docx-preview-DYBkiUaE.js → docx-preview-CXuk-emm.js} +1 -1
- package/dist/web/assets/{edge-rWOlCmXT.js → edge-E8QLt7oi.js} +1 -1
- package/dist/web/assets/{edit-diff-preview-COtmo54G.js → edit-diff-preview-BGGGecVw.js} +1 -1
- package/dist/web/assets/{editor-language-service-DxkZ51E5.js → editor-language-service-D0DuEOs_.js} +1 -1
- package/dist/web/assets/{elixir-E7Y82okW.js → elixir-DPb_-72P.js} +1 -1
- package/dist/web/assets/{elm-DTPGbc-w.js → elm-BMzJY5b0.js} +1 -1
- package/dist/web/assets/{erb-DHqLHrE6.js → erb-AABQNtMK.js} +1 -1
- package/dist/web/assets/{erlang-Byg0nNiV.js → erlang-CEIKOkfP.js} +1 -1
- package/dist/web/assets/{esm-D_DmOM7n.js → esm-B9RVMpwV.js} +1 -1
- package/dist/web/assets/explorer-body-VdCBqcZu.js +2 -0
- package/dist/web/assets/explorer-window-content-BIsbl7Th.js +1 -0
- package/dist/web/assets/extension-manager-section-HkY7geS5.js +1 -0
- package/dist/web/assets/{extension-webview-DI4VL2tJ.js → extension-webview-DZG8YHX9.js} +1 -1
- package/dist/web/assets/file-icons-BXdvggK2.js +1 -0
- package/dist/web/assets/file-icons.generated-BtBErEwb.css +1 -0
- package/dist/web/assets/file-store-ojgMXgAw.js +1 -0
- package/dist/web/assets/files-settings-section-qnRtkX05.js +1 -0
- package/dist/web/assets/{format-file-meta-DOpzJJhS.js → format-file-meta-C77h04cZ.js} +1 -1
- package/dist/web/assets/{fortran-fixed-form-CY1n0Xf6.js → fortran-fixed-form-CrXvLHyi.js} +1 -1
- package/dist/web/assets/{fsharp-_vEDSCJz.js → fsharp-DCI4qAz_.js} +1 -1
- package/dist/web/assets/{gdresource-B6XT0wAC.js → gdresource-Vb2kO1MG.js} +1 -1
- package/dist/web/assets/{general-settings-section-BcxwiZ0X.js → general-settings-section-BAFSZmCE.js} +1 -1
- package/dist/web/assets/{git-commit-Wk9ovq59.js → git-commit-BMI_OWn-.js} +1 -1
- package/dist/web/assets/git-log-panel-2sGpfEla.js +1 -0
- package/dist/web/assets/{git-rebase-DSarGPvK.js → git-rebase-D-d_2iqZ.js} +1 -1
- package/dist/web/assets/{git-repo-store-Dy69YCHN.js → git-repo-store-88Z8v-pF.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-C6qRruHI.js +1 -0
- package/dist/web/assets/{glide-data-grid-BvgAqSUr.js → glide-data-grid-f0hyITiF.js} +7 -7
- package/dist/web/assets/{glimmer-js-CkYI35_P.js → glimmer-js-BmNNiR0Z.js} +1 -1
- package/dist/web/assets/{glimmer-ts-BqmIZcds.js → glimmer-ts-DQE2Ufha.js} +1 -1
- package/dist/web/assets/{glsl-Be-yBCF5.js → glsl-BcIDsxRJ.js} +1 -1
- package/dist/web/assets/{graphql-a5DrENHY.js → graphql-DGKSLmGH.js} +1 -1
- package/dist/web/assets/group-chat-tab-Ck6sk7yY.js +1 -0
- package/dist/web/assets/{hack-BLAg20aD.js → hack-YiC8bEIb.js} +1 -1
- package/dist/web/assets/{haml-Dj6bGOQH.js → haml-DjblxAsz.js} +1 -1
- package/dist/web/assets/{handlebars-dmEa36Oh.js → handlebars-BQepOXaU.js} +1 -1
- package/dist/web/assets/{html-yacZYzSE.js → html-Cayj_2GP.js} +1 -1
- package/dist/web/assets/{html-derivative-lLi3-RNT.js → html-derivative-Weq79bL6.js} +1 -1
- package/dist/web/assets/{http-1KaxcE4e.js → http-BeLfdGdz.js} +1 -1
- package/dist/web/assets/{hurl-ST-dEEZR.js → hurl-D9rypCLK.js} +1 -1
- package/dist/web/assets/{hxml-CpDtuq__.js → hxml-clG8mmSH.js} +1 -1
- package/dist/web/assets/{image-preview-CJVxL0uH.js → image-preview-CzCc4_Do.js} +1 -1
- package/dist/web/assets/image-preview-geometry-9lgeLYB3.js +1 -0
- package/dist/web/assets/index-BH99TIE0.css +1 -0
- package/dist/web/assets/index-CEZVxM4G.js +52 -0
- package/dist/web/assets/info-3K5VOQVL-BOSpdwlz.js +1 -0
- package/dist/web/assets/{jinja-Cjw-evAn.js → jinja-BShBWfs5.js} +1 -1
- package/dist/web/assets/{jira-watcher-section-DIC1yIV3.js → jira-watcher-section-CeLmhvu1.js} +1 -1
- package/dist/web/assets/{jison-DfATMYoY.js → jison-dN2Nf_Dr.js} +1 -1
- package/dist/web/assets/{julia-DDCaxiqT.js → julia-5U0vR-uJ.js} +1 -1
- package/dist/web/assets/{just-CJL529u6.js → just-BMppVRy6.js} +1 -1
- package/dist/web/assets/keybindings-store-C_3ChOEf.js +1 -0
- package/dist/web/assets/{keyboard-shortcuts-section-BW03P3AF.js → keyboard-shortcuts-section-PVz6kuh2.js} +1 -1
- package/dist/web/assets/{latex-Zc4EpLpW.js → latex-DSSOKjis.js} +1 -1
- package/dist/web/assets/{liquid-B_6KMNYS.js → liquid-C24dKoyI.js} +1 -1
- package/dist/web/assets/{lua-BtS2idK7.js → lua-DNOsnyhn.js} +1 -1
- package/dist/web/assets/{markdown-renderer-CvL1pWeK.js → markdown-renderer-C140Ijk0.js} +3 -3
- package/dist/web/assets/markdown-renderer-Dz7-DYUg.js +1 -0
- package/dist/web/assets/{marko-Wk6LBnck.js → marko-26MHO_Q9.js} +1 -1
- package/dist/web/assets/{mdc-cRaXBrzL.js → mdc-Dhw9mGrC.js} +1 -1
- package/dist/web/assets/{media-url-DL8OCGGg.js → media-url-yfqHpPz0.js} +1 -1
- package/dist/web/assets/{mobile-bottom-sheet-CoUauvLF.js → mobile-bottom-sheet-C3TniEP-.js} +1 -1
- package/dist/web/assets/mobile-explorer-sheet-CUG_owdf.js +1 -0
- package/dist/web/assets/{nextflow-Bos2wvDV.js → nextflow-xH4oJTex.js} +1 -1
- package/dist/web/assets/{nginx-DMlOwzdr.js → nginx-CMz4w0M8.js} +1 -1
- package/dist/web/assets/{nim-D2dZQA88.js → nim-D8FnIYsx.js} +1 -1
- package/dist/web/assets/notification-store-6vib1ISy.js +1 -0
- package/dist/web/assets/{notifications-settings-section-DPUROMw-.js → notifications-settings-section-DqlPDfNX.js} +1 -1
- package/dist/web/assets/{number-overlay-editor-DZ2i_Bnb.js → number-overlay-editor-CRGwULhR.js} +1 -1
- package/dist/web/assets/{overview-panel-R146OIom.js → overview-panel-Di-3aV9p.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-BGIE1QdU.js +1 -0
- package/dist/web/assets/panel-store-CboGJAwh.js +1 -0
- package/dist/web/assets/{pdf-preview-BWUPGLp6.js → pdf-preview-CmBI6E7d.js} +1 -1
- package/dist/web/assets/{perl-C5-Z6fPn.js → perl-D9iNWYwb.js} +1 -1
- package/dist/web/assets/{php-BXNNhkMH.js → php-Bw3Qo0Zv.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-Dg-QTbXX.js +1 -0
- package/dist/web/assets/{postgres-viewer-CjOvaruz.js → postgres-viewer-8ZAqEALv.js} +3 -3
- package/dist/web/assets/{ppmbot-settings-section-DrvWh8l8.js → ppmbot-settings-section-Cji-Mil-.js} +1 -1
- package/dist/web/assets/problems-panel-a6_hrWvl.js +1 -0
- package/dist/web/assets/{process-table-BRMS1MrX.js → process-table-CiIqlOIK.js} +1 -1
- package/dist/web/assets/project-framework-store-B70Q3dNM.js +1 -0
- package/dist/web/assets/project-store-B87hLRSk.js +1 -0
- package/dist/web/assets/{proxy-settings-section-Bv0Ny-Yy.js → proxy-settings-section-BjI4IFgp.js} +1 -1
- package/dist/web/assets/{pug-C1zf1yWy.js → pug-fT5JC6Vm.js} +1 -1
- package/dist/web/assets/{qml-OW58jGpD.js → qml-P_wEDB1w.js} +1 -1
- package/dist/web/assets/{query-audit-section-BcHJZcVC.js → query-audit-section-By6qyqdB.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DZ6IVu2s.js +1 -0
- package/dist/web/assets/{razor-DzjSaobM.js → razor-Drrt2_Ph.js} +1 -1
- package/dist/web/assets/remote-desktop-clipboard-notice-eXYvwgx7.js +1 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-Doh-fci-.js +2 -0
- package/dist/web/assets/remote-desktop-mobile-view-Dt8Akc3p.js +1 -0
- package/dist/web/assets/{remote-desktop-readiness-gate-BTzss2wX.js → remote-desktop-readiness-gate-t83fqbID.js} +1 -1
- package/dist/web/assets/remote-desktop-window-content-CguhZMmx.js +1 -0
- package/dist/web/assets/{rst-RfuihOUW.js → rst-Dr2mW4-h.js} +1 -1
- package/dist/web/assets/{ruby-CNO3pli1.js → ruby-C6EpUsTY.js} +1 -1
- package/dist/web/assets/{run-in-terminal-Cb_hVU8x.js → run-in-terminal-CdefEe_R.js} +1 -1
- package/dist/web/assets/{sas-DZY5BtXx.js → sas-D8ZEeP6v.js} +1 -1
- package/dist/web/assets/{schedules-settings-section-DO8xAOVj.js → schedules-settings-section-BVkpW5ms.js} +1 -1
- package/dist/web/assets/{scroll-area-LtNV8g5o.js → scroll-area-BzeiOd-B.js} +1 -1
- package/dist/web/assets/{scss--4g-REcN.js → scss-ITL7jQ1f.js} +1 -1
- package/dist/web/assets/{send-to-chat-Cnpru1M0.js → send-to-chat-F207yH3u.js} +1 -1
- package/dist/web/assets/{separator-Cds1yxy8.js → separator-TRMsJUYe.js} +1 -1
- package/dist/web/assets/settings-body-CoIxYAh6.js +2 -0
- package/dist/web/assets/settings-store-krC6bc79.js +2 -0
- package/dist/web/assets/settings-tab-7DKVUTbM.js +1 -0
- package/dist/web/assets/settings-window-content-3_39TeX-.js +1 -0
- package/dist/web/assets/{shaderlab-Cu4sBiVn.js → shaderlab-mW-IIlhk.js} +1 -1
- package/dist/web/assets/{shellsession-DWzJfEv7.js → shellsession-DhNl5STt.js} +1 -1
- package/dist/web/assets/{soy-BnqE-5Ff.js → soy-SdY0Qjtr.js} +1 -1
- package/dist/web/assets/{sparql-Ctg__NhZ.js → sparql-ftqZo2i-.js} +1 -1
- package/dist/web/assets/{sql-query-editor-D9k19T_M.js → sql-query-editor-CQB7Yp7U.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-DoV9hnWJ.js → sqlite-viewer-Bd7rjr1D.js} +1 -1
- package/dist/web/assets/{stata-Qk2h66XW.js → stata-Bj7Yjb4b.js} +1 -1
- package/dist/web/assets/{surrealql-8QEX7EfH.js → surrealql-CLhYGAfB.js} +1 -1
- package/dist/web/assets/{svelte-D_mubC5-.js → svelte-DTAlRHhK.js} +1 -1
- package/dist/web/assets/system-monitor-body-DsRXeMyU.js +2 -0
- package/dist/web/assets/system-monitor-tab-DdSZgux3.js +1 -0
- package/dist/web/assets/{system-monitor-window-content-qXLcicFM.js → system-monitor-window-content-ClPo4-El.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-7fPZNEni.js → tab-host-window-content-d2TAD4PU.js} +1 -1
- package/dist/web/assets/{tab-store-DGJGnbwv.js → tab-store-OJa1GPCq.js} +1 -1
- package/dist/web/assets/team-member-sheet-7d2Qp-ie.js +2 -0
- package/dist/web/assets/{team-member-window-content-BgvJFypM.js → team-member-window-content-Bctg2gmg.js} +1 -1
- package/dist/web/assets/{templ-C1jV0fsO.js → templ-BMfhACxb.js} +1 -1
- package/dist/web/assets/{terminal-tab-Cs0sGljx.js → terminal-tab-0HQic25V.js} +3 -3
- package/dist/web/assets/{tex-D_StFXy_.js → tex-BcssM-QA.js} +1 -1
- package/dist/web/assets/tool-cards-CmLLRACd.js +9 -0
- package/dist/web/assets/treemap-KZPCXAKY-CE30wCXa.js +1 -0
- package/dist/web/assets/{ts-tags-B5m47rEm.js → ts-tags-CrR5IYI8.js} +1 -1
- package/dist/web/assets/{twig-C_br6GIl.js → twig-DchfUmyI.js} +1 -1
- package/dist/web/assets/{use-blob-url-C-SGi_Hb.js → use-blob-url-CPtCcY3i.js} +1 -1
- package/dist/web/assets/{use-inline-blame-B4trxGcR.js → use-inline-blame-sDMjcz1-.js} +3 -3
- package/dist/web/assets/{use-lsp-CV0vzILO.js → use-lsp-CBgID-EU.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-DBqbWjWX.js → use-monaco-theme-gvhJ_jZh.js} +1 -1
- package/dist/web/assets/{use-open-team-member-DENsjp0s.js → use-open-team-member-C3mrUzIC.js} +1 -1
- package/dist/web/assets/{use-websocket-LgXiX1qN.js → use-websocket-C9wlRliB.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-Cbig5zA6.js → vendor-mermaid-BitiXJlL.js} +1 -1
- package/dist/web/assets/{vendor-ui-BBdkypKW.js → vendor-ui-DmZ-Mb-H.js} +1 -1
- package/dist/web/assets/video-preview-CSDK5j6H.js +1 -0
- package/dist/web/assets/{vue-DQwCnLdr.js → vue-1EL6Pw1h.js} +1 -1
- package/dist/web/assets/{vue-html-T_EoGpPJ.js → vue-html-BPQNwPC6.js} +1 -1
- package/dist/web/assets/{vue-vine-DmxLqX50.js → vue-vine-Bb_dsWLt.js} +1 -1
- package/dist/web/assets/{window-pip-registry-3l4hoxuD.js → window-pip-registry-yD6esSju.js} +1 -1
- package/dist/web/assets/ws-auth-CEYVQ8NH.js +1 -0
- package/dist/web/assets/{xml-Bs6Bw8is.js → xml-dxkLNA0K.js} +1 -1
- package/dist/web/assets/{xsl-f5ZNhRCc.js → xsl-Bw2adwrl.js} +1 -1
- package/dist/web/index.html +27 -26
- package/dist/web/sw.js +1 -1
- package/docs/architecture/ai-chat-and-providers.md +9 -0
- package/docs/architecture/plugins-and-tracing.md +285 -0
- package/docs/lessons-learned.md +31 -0
- package/docs/system-architecture.md +1 -0
- package/package.json +2 -1
- package/packages/ext-git-graph/src/compare-diff-base.test.ts +173 -0
- package/packages/ext-git-graph/src/compare-view.ts +22 -3
- package/packages/ext-git-graph/src/extension.ts +6 -0
- package/packages/ext-git-graph/src/types.ts +19 -0
- package/packages/ext-git-graph/src/webview-html.test.ts +159 -1
- package/packages/ext-git-graph/src/webview-html.ts +344 -26
- package/scripts/gen-file-icons.ts +493 -242
- package/src/providers/claude-agent-sdk.ts +42 -1
- package/src/providers/codex-app-server/codex-history.ts +77 -23
- package/src/server/index.ts +42 -7
- package/src/server/routes/chat.ts +15 -11
- package/src/server/routes/git.ts +83 -3
- package/src/server/routes/remote-desktop.ts +7 -2
- package/src/server/ws/global.ts +1 -1
- package/src/server/ws/remote-desktop.ts +12 -5
- package/src/services/binary-content.ts +25 -0
- package/src/services/chat-search.service.ts +1 -1
- package/src/services/chat.service.ts +9 -0
- package/src/services/edge-forwarder.ts +191 -191
- package/src/services/event-loop-lag.ts +208 -0
- package/src/services/file-lines.ts +114 -0
- package/src/services/file-watcher/recreated-dir-poller.ts +108 -6
- package/src/services/file-watcher/watch-tree.ts +131 -10
- package/src/services/file-watcher.service.ts +73 -23
- package/src/services/git-branch-diff/branch-diff-parse.ts +151 -0
- package/src/services/git-branch-diff/branch-diff.service.ts +79 -0
- package/src/services/git-hunks/git-hunks.service.ts +26 -11
- package/src/services/git-hunks/unified-diff.ts +15 -1
- package/src/services/git.service.ts +74 -24
- package/src/services/jsonl-transcript-parser.ts +138 -6
- package/src/services/log-rotate.ts +142 -0
- package/src/services/media-transcode/ffmpeg-capabilities.ts +72 -2
- package/src/services/remote-desktop/remote-desktop-audio.ts +193 -0
- package/src/services/remote-desktop/remote-desktop-capture-input.ts +87 -14
- package/src/services/remote-desktop/remote-desktop-capture.ts +40 -7
- package/src/services/remote-desktop/remote-desktop-clipboard.ts +166 -0
- package/src/services/remote-desktop/remote-desktop-displays.ts +77 -1
- package/src/services/remote-desktop/remote-desktop-encoder-args.ts +25 -6
- package/src/services/remote-desktop/remote-desktop-evdev-key-map.ts +69 -0
- package/src/services/remote-desktop/remote-desktop-input-linux.ts +253 -0
- package/src/services/remote-desktop/remote-desktop-input-uinput.ts +242 -0
- package/src/services/remote-desktop/remote-desktop-input.ts +40 -8
- package/src/services/remote-desktop/remote-desktop-linux-session.ts +146 -0
- package/src/services/remote-desktop/remote-desktop-ogg-opus.ts +144 -0
- package/src/services/remote-desktop/remote-desktop-privacy.ts +163 -0
- package/src/services/remote-desktop/remote-desktop-quality.ts +138 -0
- package/src/services/remote-desktop/remote-desktop-requirements.ts +158 -5
- package/src/services/remote-desktop/remote-desktop-resolution.ts +302 -0
- package/src/services/remote-desktop/remote-desktop-session.ts +523 -21
- package/src/services/remote-desktop/remote-desktop-x11.ts +250 -0
- package/src/services/supervisor.ts +47 -4
- package/src/services/windows-process-tree.ts +351 -351
- package/src/shared/codex-daily-guard.ts +12 -5
- package/src/shared/remote-desktop-custom-quality.ts +117 -0
- package/src/shared/remote-desktop-quality.ts +127 -0
- package/src/types/chat.ts +4 -0
- package/src/types/git.ts +52 -0
- package/src/web/components/branch-review/branch-review-tab.tsx +360 -0
- package/src/web/components/branch-review/branch-review-tree-row.tsx +138 -0
- package/src/web/components/chat/attachment-chips.tsx +30 -6
- package/src/web/components/chat/chat-tab.tsx +12 -1
- package/src/web/components/chat/message-events.tsx +273 -0
- package/src/web/components/chat/message-list.tsx +107 -861
- package/src/web/components/chat/message-markdown.tsx +34 -0
- package/src/web/components/chat/message-media.tsx +174 -0
- package/src/web/components/chat/message-user-bubble.tsx +399 -0
- package/src/web/components/chat/provider-selector.tsx +30 -22
- package/src/web/components/chat/session-history-list.tsx +2 -2
- package/src/web/components/chat/session-list-panel.tsx +3 -2
- package/src/web/components/editor/binary-diff-view.tsx +212 -0
- package/src/web/components/editor/diff-viewer.tsx +76 -3
- package/src/web/components/explorer/file-tree.tsx +235 -134
- package/src/web/components/explorer/flatten-visible-tree.ts +35 -40
- package/src/web/components/explorer/tree-node.tsx +69 -69
- package/src/web/components/explorer/use-tree-row-dnd.ts +3 -4
- package/src/web/components/extensions/extension-tree-view.tsx +4 -11
- package/src/web/components/git/git-status-panel.tsx +25 -0
- package/src/web/components/layout/command-palette.tsx +2 -18
- package/src/web/components/layout/draggable-tab.tsx +4 -2
- package/src/web/components/layout/nav-section-rail.tsx +12 -3
- package/src/web/components/layout/status-bar.tsx +10 -6
- package/src/web/components/layout/tab-content.tsx +5 -0
- package/src/web/components/layout/tab-pool.tsx +1 -0
- package/src/web/components/layout/upgrade-button.tsx +5 -1
- package/src/web/components/remote-desktop/remote-desktop-clipboard-client.ts +67 -0
- package/src/web/components/remote-desktop/remote-desktop-clipboard-notice.tsx +127 -0
- package/src/web/components/remote-desktop/remote-desktop-codec-labels.ts +28 -0
- package/src/web/components/remote-desktop/remote-desktop-custom-quality-dialog.tsx +144 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-key-bar.tsx +13 -2
- package/src/web/components/remote-desktop/remote-desktop-mobile-settings.tsx +232 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-toolbar.tsx +21 -8
- package/src/web/components/remote-desktop/remote-desktop-mobile-view.tsx +82 -3
- package/src/web/components/remote-desktop/remote-desktop-recording.ts +76 -0
- package/src/web/components/remote-desktop/remote-desktop-resolution-list.ts +100 -0
- package/src/web/components/remote-desktop/remote-desktop-toolbar.tsx +497 -0
- package/src/web/components/remote-desktop/remote-desktop-view-style.ts +145 -0
- package/src/web/components/remote-desktop/remote-desktop-window-content.tsx +126 -35
- package/src/web/components/remote-desktop/use-h264-canvas-decoder.ts +11 -0
- package/src/web/components/remote-desktop/use-opus-audio-player.ts +148 -0
- package/src/web/components/remote-desktop/use-remote-desktop-connection.ts +284 -1
- package/src/web/components/remote-desktop/use-remote-desktop-readiness.ts +75 -1
- package/src/web/components/remote-desktop/use-remote-desktop-recorder.ts +103 -0
- package/src/web/components/remote-desktop/use-remote-desktop-virtual-keyboard.ts +19 -2
- package/src/web/components/remote-desktop/use-remote-input-capture.ts +39 -3
- package/src/web/components/settings/accounts/account-card.tsx +2 -2
- package/src/web/components/settings/accounts/codex-accounts-section.tsx +4 -4
- package/src/web/components/settings/extension-manager-section.tsx +8 -1
- package/src/web/components/settings/theme-picker.tsx +2 -1
- package/src/web/components/ui/tooltip.tsx +15 -2
- package/src/web/lib/branch-review-state.ts +175 -0
- package/src/web/lib/extension-icons.ts +70 -0
- package/src/web/lib/file-icon-name.ts +62 -0
- package/src/web/lib/file-icons.generated.ts +3287 -25
- package/src/web/lib/file-icons.tsx +71 -73
- package/src/web/lib/flatten-expansions.ts +18 -5
- package/src/web/lib/message-ordinals.ts +23 -0
- package/src/web/lib/provider-logos.tsx +143 -0
- package/src/web/lib/tab-type-icons.ts +25 -0
- package/src/web/stores/file-store.ts +73 -35
- package/src/web/stores/panel-utils.ts +5 -0
- package/src/web/stores/project-framework-store.ts +82 -0
- package/src/web/stores/project-store.ts +7 -1
- package/src/web/stores/settings-store.ts +128 -0
- package/src/web/stores/tab-store.ts +1 -0
- package/src/web/stores/window-panel-persistence.ts +1 -0
- package/src/web/styles/file-icons.generated.css +3282 -234
- package/src/web/styles/globals.css +26 -0
- package/dist/web/assets/accounts-settings-section-BuxD9zH3.js +0 -1
- package/dist/web/assets/ai-settings-section-Dd9tOUp0.js +0 -1
- package/dist/web/assets/ai-settings-section-FrU2X8Fu.js +0 -1
- package/dist/web/assets/api-client-BUCo1lms.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-5K0wEkAn.js +0 -1
- package/dist/web/assets/chat-tab-Cr6_KSGO.js +0 -15
- package/dist/web/assets/code-editor-DfmlGamv.js +0 -7
- package/dist/web/assets/context-menu-DOODKZdF.js +0 -1
- package/dist/web/assets/diff-viewer-BpNj7pN9.js +0 -4
- package/dist/web/assets/explorer-body-D-5z464e.js +0 -2
- package/dist/web/assets/explorer-window-content-7-1wuRul.js +0 -1
- package/dist/web/assets/extension-manager-section-BhwB6nqr.js +0 -1
- package/dist/web/assets/file-icons-EzPL0q-k.js +0 -1
- package/dist/web/assets/file-icons.generated-CfaJuvZu.css +0 -1
- package/dist/web/assets/file-store-DMABxOkV.js +0 -1
- package/dist/web/assets/files-settings-section-CZzABuKz.js +0 -1
- package/dist/web/assets/git-log-panel-CRN4JVOx.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-DkVT2MAM.js +0 -1
- package/dist/web/assets/group-chat-tab-DFa5mAx5.js +0 -1
- package/dist/web/assets/index-D7DbXD3G.css +0 -1
- package/dist/web/assets/index-ZtfxZOyb.js +0 -52
- package/dist/web/assets/info-3K5VOQVL-BBsmKGzA.js +0 -1
- package/dist/web/assets/keybindings-store-tVfhPANS.js +0 -1
- package/dist/web/assets/markdown-renderer-sJNNVhlS.js +0 -1
- package/dist/web/assets/mobile-explorer-sheet-CZ0HZ2E_.js +0 -1
- package/dist/web/assets/notification-store-BZbsPnW0.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-DwmYPOlx.js +0 -1
- package/dist/web/assets/panel-store-BVLBRn6M.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-D0oogIFi.js +0 -1
- package/dist/web/assets/problems-panel-B7BWVhdy.js +0 -1
- package/dist/web/assets/project-store-BhMbVln3.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-CQIJOQGa.js +0 -1
- package/dist/web/assets/remote-desktop-mobile-sheet-ByjaeZKX.js +0 -2
- package/dist/web/assets/remote-desktop-mobile-view-D13mtr9F.js +0 -1
- package/dist/web/assets/remote-desktop-window-content-CbqTCaC6.js +0 -1
- package/dist/web/assets/settings-body-Rnu-FcX-.js +0 -2
- package/dist/web/assets/settings-store-DZPgPtEA.js +0 -2
- package/dist/web/assets/settings-tab-C6PHxs0Z.js +0 -1
- package/dist/web/assets/settings-window-content-CQ4gT2qj.js +0 -1
- package/dist/web/assets/system-monitor-body-2vQ1kMxE.js +0 -2
- package/dist/web/assets/system-monitor-tab-krAcvwF7.js +0 -1
- package/dist/web/assets/team-member-sheet-2FnRmk1a.js +0 -2
- package/dist/web/assets/tool-cards-DU_S2o51.js +0 -9
- package/dist/web/assets/treemap-KZPCXAKY-BhEtjljr.js +0 -1
- package/dist/web/assets/use-remote-desktop-display-choice-BK_f787P.js +0 -1
- package/dist/web/assets/video-preview-BDNwumbW.js +0 -1
- package/dist/web/assets/ws-auth-DEAJiGCR.js +0 -1
- package/packages/vscode-compat/node_modules/.bin/tsc.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsc.exe +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.exe +0 -0
- /package/dist/web/assets/{abap-tkptwpsK.js → abap-IN2yINdN.js} +0 -0
- /package/dist/web/assets/{actionscript-3-Di9TMGwg.js → actionscript-3-C_t772eR.js} +0 -0
- /package/dist/web/assets/{ada-CB9F079D.js → ada-DPme3emm.js} +0 -0
- /package/dist/web/assets/{andromeeda-Dcl8jpb7.js → andromeeda-DSV4i4mZ.js} +0 -0
- /package/dist/web/assets/{apache-D0Nk0qsH.js → apache-CWr8s8I8.js} +0 -0
- /package/dist/web/assets/{apex-mQIoWncD.js → apex-Z20JnDF5.js} +0 -0
- /package/dist/web/assets/{applescript-C6ljbcsI.js → applescript-BKNPu3ga.js} +0 -0
- /package/dist/web/assets/{apply-theme-BwIMNXyX.js → apply-theme-CbR31K6I.js} +0 -0
- /package/dist/web/assets/{ara-WFqRPGPe.js → ara-hWdY3O96.js} +0 -0
- /package/dist/web/assets/{asciidoc-bNd8w4nf.js → asciidoc-eynhq496.js} +0 -0
- /package/dist/web/assets/{asm-BQ1_F580.js → asm-Bu02Kq4P.js} +0 -0
- /package/dist/web/assets/{aurora-x-Ciqs_Dbu.js → aurora-x-BHBGXbIZ.js} +0 -0
- /package/dist/web/assets/{awk-GCR-6KZV.js → awk-B4ojezyv.js} +0 -0
- /package/dist/web/assets/{ayu-dark-DxJXuj99.js → ayu-dark-D0GQzFi9.js} +0 -0
- /package/dist/web/assets/{ayu-light-jvSyJWDV.js → ayu-light-BVYb5Yn8.js} +0 -0
- /package/dist/web/assets/{ayu-mirage-Cg3BPO6H.js → ayu-mirage-BGFW6oNb.js} +0 -0
- /package/dist/web/assets/{ballerina-BPz4CrX6.js → ballerina-B6Og3IKT.js} +0 -0
- /package/dist/web/assets/{bat-CSTs2hIG.js → bat-D8YFILfx.js} +0 -0
- /package/dist/web/assets/{beancount-DMateAZX.js → beancount-BjlbBBuX.js} +0 -0
- /package/dist/web/assets/{berry-9lXg0GRM.js → berry-Cz1ygeoj.js} +0 -0
- /package/dist/web/assets/{bibtex-DIbqmvFg.js → bibtex-CVkJceJh.js} +0 -0
- /package/dist/web/assets/{bicep-02WGsoJ8.js → bicep-DrhYI5GX.js} +0 -0
- /package/dist/web/assets/{bird2-BY0tbu3k.js → bird2-BV-KJtmc.js} +0 -0
- /package/dist/web/assets/{c-jMBX_9Je.js → c-CuLlvxbG.js} +0 -0
- /package/dist/web/assets/{c3-B8JVbNjW.js → c3-CI7-1CJ0.js} +0 -0
- /package/dist/web/assets/{cadence-DUIf1tvL.js → cadence-DjxhNP3Q.js} +0 -0
- /package/dist/web/assets/{can-open-in-ppm-DGEc0s4P.js → can-open-in-ppm-cv5--yHX.js} +0 -0
- /package/dist/web/assets/{catppuccin-frappe-FhUCVxex.js → catppuccin-frappe-CEozpJRh.js} +0 -0
- /package/dist/web/assets/{catppuccin-latte-CzfrtmgL.js → catppuccin-latte-C4sXQFMc.js} +0 -0
- /package/dist/web/assets/{catppuccin-macchiato-6xL0lOtj.js → catppuccin-macchiato-Ci-fDhXy.js} +0 -0
- /package/dist/web/assets/{catppuccin-mocha-D39-xS3P.js → catppuccin-mocha-CH7YXw8d.js} +0 -0
- /package/dist/web/assets/{clarity-D8TeGvoT.js → clarity-BWrJ7UmT.js} +0 -0
- /package/dist/web/assets/{clojure-Dx_acSX7.js → clojure-C94fh2hb.js} +0 -0
- /package/dist/web/assets/{cmake-CyeYPq8o.js → cmake-CtvCkMsG.js} +0 -0
- /package/dist/web/assets/{codeowners-DrTI0lmI.js → codeowners-CJnubqzH.js} +0 -0
- /package/dist/web/assets/{codeql-CPkFxVW-.js → codeql-BOoP5HY2.js} +0 -0
- /package/dist/web/assets/{common-lisp-B0k-3LTj.js → common-lisp-B9nlMFJd.js} +0 -0
- /package/dist/web/assets/{coq-uygpo7to.js → coq-CgzIQiSH.js} +0 -0
- /package/dist/web/assets/{csharp-DKUYGfSp.js → csharp-4xAIx3jw.js} +0 -0
- /package/dist/web/assets/{css-DoOUp7v4.js → css-D4YoLYGn.js} +0 -0
- /package/dist/web/assets/{csv-B3YE13gi.js → csv-C2v-ZakW.js} +0 -0
- /package/dist/web/assets/{csv-parser-DXePqmXe.js → csv-parser-_oDBUkwQ.js} +0 -0
- /package/dist/web/assets/{cue-DViO2wvn.js → cue-Bbznh_an.js} +0 -0
- /package/dist/web/assets/{cypher-4BMCIW5N.js → cypher-BKhVEARF.js} +0 -0
- /package/dist/web/assets/{d-BT_sTHas.js → d-D6IhWopl.js} +0 -0
- /package/dist/web/assets/{dark-plus-DVVXu-1N.js → dark-plus-xAWu790O.js} +0 -0
- /package/dist/web/assets/{dart-DHz6f6_6.js → dart-Bj6S9O1j.js} +0 -0
- /package/dist/web/assets/{data-grid-types-OgUxMLdQ.js → data-grid-types-C5bthITf.js} +0 -0
- /package/dist/web/assets/{dax-ChYCjweK.js → dax-CF2zvaqq.js} +0 -0
- /package/dist/web/assets/{desktop-D6Lt4p04.js → desktop-CnmyxMKE.js} +0 -0
- /package/dist/web/assets/{diff-D8TDe4hP.js → diff-Qi8WuJsI.js} +0 -0
- /package/dist/web/assets/{dist-DOTlFQJA.js → dist-Bjseolz4.js} +0 -0
- /package/dist/web/assets/{dist-CrBOCtCW.js → dist-Czq7BUMy.js} +0 -0
- /package/dist/web/assets/{docker-Bsp0YbPx.js → docker-vLbbNP5d.js} +0 -0
- /package/dist/web/assets/{dotenv-B_7giSY_.js → dotenv-DJMvvNzn.js} +0 -0
- /package/dist/web/assets/{dracula-TrQL2iB5.js → dracula-CtAh2rhB.js} +0 -0
- /package/dist/web/assets/{dracula-soft-n2XOlgh-.js → dracula-soft-CXtuAAf6.js} +0 -0
- /package/dist/web/assets/{dream-maker-CdIEdhpT.js → dream-maker-D5YQyK49.js} +0 -0
- /package/dist/web/assets/{editor-font-BDrdiGf1.js → editor-font-ClTC9gaD.js} +0 -0
- /package/dist/web/assets/{emacs-lisp-CSbixs6u.js → emacs-lisp-CGTjbiqF.js} +0 -0
- /package/dist/web/assets/{everforest-dark-BuZahZYp.js → everforest-dark-D59p7vAQ.js} +0 -0
- /package/dist/web/assets/{everforest-light-DPH53z5H.js → everforest-light-B3Qtud3V.js} +0 -0
- /package/dist/web/assets/{fennel-B1-K_hlJ.js → fennel-BCyHNUwf.js} +0 -0
- /package/dist/web/assets/{fish-Cy12Dbua.js → fish-DDDlLV2G.js} +0 -0
- /package/dist/web/assets/{fluent-B_82ys8E.js → fluent-By54R9tl.js} +0 -0
- /package/dist/web/assets/{format-bytes-D5sBgIqU.js → format-bytes-D1AQyUCt.js} +0 -0
- /package/dist/web/assets/{fortran-free-form-BRQdNzZF.js → fortran-free-form-tP8mRym_.js} +0 -0
- /package/dist/web/assets/{gdscript-XBDmLUm9.js → gdscript-nHC2z6Te.js} +0 -0
- /package/dist/web/assets/{gdshader-DhVfB3u4.js → gdshader-K-cSmPlW.js} +0 -0
- /package/dist/web/assets/{genie-X3K29Y49.js → genie-BAcRhvEe.js} +0 -0
- /package/dist/web/assets/{gherkin-DKIx-6cX.js → gherkin-CcifxMLb.js} +0 -0
- /package/dist/web/assets/{github-dark-Bf7zdp5s.js → github-dark-67KKlMdB.js} +0 -0
- /package/dist/web/assets/{github-dark-default-DqsBBV11.js → github-dark-default-tja8pidR.js} +0 -0
- /package/dist/web/assets/{github-dark-dimmed-X2gqRz6w.js → github-dark-dimmed-CtjhYQtx.js} +0 -0
- /package/dist/web/assets/{github-dark-high-contrast-Z7tvbGX4.js → github-dark-high-contrast-Bf-LPFgn.js} +0 -0
- /package/dist/web/assets/{github-light-BKxt-7zZ.js → github-light-CbJxE7n3.js} +0 -0
- /package/dist/web/assets/{github-light-default-B0liF4d4.js → github-light-default-DRQmdHxt.js} +0 -0
- /package/dist/web/assets/{github-light-high-contrast-F4encJwr.js → github-light-high-contrast-CJkDgTeM.js} +0 -0
- /package/dist/web/assets/{gleam-Pz2bHgBB.js → gleam-D4LCl2ri.js} +0 -0
- /package/dist/web/assets/{gn-CgmdNsPU.js → gn-COY8zZmC.js} +0 -0
- /package/dist/web/assets/{gnuplot-qLNF5yZ9.js → gnuplot-ROk1VitR.js} +0 -0
- /package/dist/web/assets/{go-CSDUNhE7.js → go-H30c3p0C.js} +0 -0
- /package/dist/web/assets/{groovy-B3NEnOHF.js → groovy-DBLCaelV.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-hard-DcoftiUX.js → gruvbox-dark-hard-DJy4irps.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-medium-D1wVSuD8.js → gruvbox-dark-medium-Dhfw1QTO.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-soft-DWpunymH.js → gruvbox-dark-soft-BncJU-8w.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-hard-rHMvyUdS.js → gruvbox-light-hard-Cq12_YvM.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-medium-3O2KiXO7.js → gruvbox-light-medium-DqVja5_v.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-soft-lNlXJPwH.js → gruvbox-light-soft-IvwhveGF.js} +0 -0
- /package/dist/web/assets/{haskell-aaG3aC0h.js → haskell-DA1JV-MC.js} +0 -0
- /package/dist/web/assets/{haxe-CPcHhUDd.js → haxe-V9RTRc5E.js} +0 -0
- /package/dist/web/assets/{hcl-CMQNlR_4.js → hcl-hr1lyZ10.js} +0 -0
- /package/dist/web/assets/{hjson-DeuklPFj.js → hjson-D3vW3i7X.js} +0 -0
- /package/dist/web/assets/{hlsl-BI3hlsaM.js → hlsl-BB85gfJT.js} +0 -0
- /package/dist/web/assets/{horizon-C7-gOcjs.js → horizon-DU8OOnj8.js} +0 -0
- /package/dist/web/assets/{horizon-bright-Cxt9bRW7.js → horizon-bright-DNMUclW6.js} +0 -0
- /package/dist/web/assets/{houston-DbF2G7iD.js → houston-DAiZKf6T.js} +0 -0
- /package/dist/web/assets/{hy-DRG6Hs2I.js → hy-C6Zv0LMe.js} +0 -0
- /package/dist/web/assets/{image-gallery-CObzTisL.js → image-gallery-CGvlv8zR.js} +0 -0
- /package/dist/web/assets/{imba-CxKv_2TW.js → imba-Didnp6Y-.js} +0 -0
- /package/dist/web/assets/{ini-BiHsyCdO.js → ini-m0T7q4gQ.js} +0 -0
- /package/dist/web/assets/{input-D-dR8RI6.js → input-C7Q8FxeL.js} +0 -0
- /package/dist/web/assets/{java-D3RZzgDB.js → java-Buy07jGn.js} +0 -0
- /package/dist/web/assets/{javascript-4FmSqG-T.js → javascript-Cjq3CbRL.js} +0 -0
- /package/dist/web/assets/{json-CjB236_E.js → json-BSVu8IqD.js} +0 -0
- /package/dist/web/assets/{json5-bkfO-Rco.js → json5-NN_Vb7e6.js} +0 -0
- /package/dist/web/assets/{jsonc-BwaTeLyv.js → jsonc-BdyZhBUw.js} +0 -0
- /package/dist/web/assets/{jsonl-CfxNSwGd.js → jsonl-BsDSNvwi.js} +0 -0
- /package/dist/web/assets/{jsonnet-CQETCYrp.js → jsonnet-Gm963F-U.js} +0 -0
- /package/dist/web/assets/{jssm-D5o3Zzdz.js → jssm-Ds3bKWgJ.js} +0 -0
- /package/dist/web/assets/{jsx-BAMILPLo.js → jsx-BT5mW58X.js} +0 -0
- /package/dist/web/assets/{kanagawa-dragon-BhGVxu3R.js → kanagawa-dragon-BvNvf7xd.js} +0 -0
- /package/dist/web/assets/{kanagawa-lotus-FH4Y59ro.js → kanagawa-lotus-CuSiliSn.js} +0 -0
- /package/dist/web/assets/{kanagawa-wave-eilvP9MB.js → kanagawa-wave-C6g-d6jT.js} +0 -0
- /package/dist/web/assets/{kdl-BQ-yvXUK.js → kdl-Bx4kZef-.js} +0 -0
- /package/dist/web/assets/{kotlin-BuCx0-VT.js → kotlin-qvaVvCcm.js} +0 -0
- /package/dist/web/assets/{kusto-DAN_n_Cy.js → kusto-BPLyNCN6.js} +0 -0
- /package/dist/web/assets/{laserwave-Cq2BlxrT.js → laserwave-Sf8ZEhWP.js} +0 -0
- /package/dist/web/assets/{lean-Dhq7uXKw.js → lean-DGe6uqgk.js} +0 -0
- /package/dist/web/assets/{less-CxaMQb8j.js → less-CqM85x-9.js} +0 -0
- /package/dist/web/assets/{lib-BVwmtv2i.js → lib-CD6pOS4u.js} +0 -0
- /package/dist/web/assets/{libesm-DdtEjhIi.js → libesm-CPh1Av0r.js} +0 -0
- /package/dist/web/assets/{light-plus-B4sMAend.js → light-plus-DBtKwhFA.js} +0 -0
- /package/dist/web/assets/{llvm-DQ6-B_6Y.js → llvm-Dpbq3gky.js} +0 -0
- /package/dist/web/assets/{log-Bb_vqvu7.js → log-qKMcPm9S.js} +0 -0
- /package/dist/web/assets/{logo-BzfYv3A_.js → logo-DFo7i5KH.js} +0 -0
- /package/dist/web/assets/{luau-BnAeNR77.js → luau-DGlqWFyL.js} +0 -0
- /package/dist/web/assets/{make-B6IFuxpC.js → make-CixdvUQe.js} +0 -0
- /package/dist/web/assets/{markdown-Z0xi77Fn.js → markdown-CmRnbngE.js} +0 -0
- /package/dist/web/assets/{material-theme-1scy1SYn.js → material-theme-WDUbafB-.js} +0 -0
- /package/dist/web/assets/{material-theme-darker-G-JykXQP.js → material-theme-darker-u8y2xpOk.js} +0 -0
- /package/dist/web/assets/{material-theme-lighter-BtKkCMVq.js → material-theme-lighter-cyQZK3Tg.js} +0 -0
- /package/dist/web/assets/{material-theme-ocean-irrDC89L.js → material-theme-ocean-LBAVvw7k.js} +0 -0
- /package/dist/web/assets/{material-theme-palenight-Eq5_K4LM.js → material-theme-palenight-BJqhBxL0.js} +0 -0
- /package/dist/web/assets/{matlab-BE3oCXZC.js → matlab-B38sOJee.js} +0 -0
- /package/dist/web/assets/{mdx-DYiuFTGw.js → mdx-C8QrE_IW.js} +0 -0
- /package/dist/web/assets/{mermaid-DVlaEvtw.js → mermaid-BQ-eeU8A.js} +0 -0
- /package/dist/web/assets/{min-dark-DSkeTpQa.js → min-dark-D4gNa5tv.js} +0 -0
- /package/dist/web/assets/{min-light-CFv2Q0uF.js → min-light-Dfbhn_PL.js} +0 -0
- /package/dist/web/assets/{mipsasm-CiAFuZQ2.js → mipsasm-bLOhJkdm.js} +0 -0
- /package/dist/web/assets/{mojo-ARshcaSQ.js → mojo-B0BCOMQW.js} +0 -0
- /package/dist/web/assets/{monaco-dotenv-language-BDYnKcnr.js → monaco-dotenv-language-uB-wok6C.js} +0 -0
- /package/dist/web/assets/{monokai-CPTV-SOg.js → monokai-Bdw_07OM.js} +0 -0
- /package/dist/web/assets/{moonbit-D57B0dZr.js → moonbit-BFdGaeuk.js} +0 -0
- /package/dist/web/assets/{move-qgy3G_o9.js → move-CyXQFGPM.js} +0 -0
- /package/dist/web/assets/{narrat-CZ3rC-oc.js → narrat-9DvTD3_3.js} +0 -0
- /package/dist/web/assets/{nextflow-groovy-BxxIOcI4.js → nextflow-groovy-C4VY3kNY.js} +0 -0
- /package/dist/web/assets/{night-owl-B1ryGCda.js → night-owl-feYm66V0.js} +0 -0
- /package/dist/web/assets/{night-owl-light-BZvNnvMF.js → night-owl-light-DALqFjvx.js} +0 -0
- /package/dist/web/assets/{nix-BVBQi8yv.js → nix-C1kPA9Sw.js} +0 -0
- /package/dist/web/assets/{nord-3NXw613h.js → nord-1dvrj1Al.js} +0 -0
- /package/dist/web/assets/{nushell-BMOLRalb.js → nushell-DSQ_9Uqj.js} +0 -0
- /package/dist/web/assets/{objective-c-AxruEUgh.js → objective-c-Dhcn5ZK8.js} +0 -0
- /package/dist/web/assets/{objective-cpp-cFT0qRsD.js → objective-cpp-CGVYCSrp.js} +0 -0
- /package/dist/web/assets/{ocaml-_8JH6O8J.js → ocaml-rxXW21An.js} +0 -0
- /package/dist/web/assets/{odin-nwYvR0O6.js → odin-ClSJX2DE.js} +0 -0
- /package/dist/web/assets/{one-dark-pro-C1LaYJaH.js → one-dark-pro-3gbWDfFW.js} +0 -0
- /package/dist/web/assets/{one-light-B7rMc__c.js → one-light-C2q5ZZAw.js} +0 -0
- /package/dist/web/assets/{openscad-Cwes8bKO.js → openscad-eHXeafBl.js} +0 -0
- /package/dist/web/assets/{pascal-DMt-6tdZ.js → pascal-kKha3Txs.js} +0 -0
- /package/dist/web/assets/{pkl-QBTyYQmB.js → pkl-CTiPytg-.js} +0 -0
- /package/dist/web/assets/{plastic-BTSUp8Fk.js → plastic-D1Ygezi4.js} +0 -0
- /package/dist/web/assets/{plsql-DQv3IFXP.js → plsql-D_gbgZIr.js} +0 -0
- /package/dist/web/assets/{po-CNwZx4tW.js → po-BkhbDUnq.js} +0 -0
- /package/dist/web/assets/{poimandres-8Os7Qequ.js → poimandres-Do6NywrA.js} +0 -0
- /package/dist/web/assets/{polar-b4yK8oD8.js → polar-B3B23wca.js} +0 -0
- /package/dist/web/assets/{postcss-sFctwpxx.js → postcss-Dv_YBvWd.js} +0 -0
- /package/dist/web/assets/{powerquery-Cg_WrtuN.js → powerquery-fKJWI-Sh.js} +0 -0
- /package/dist/web/assets/{powershell-C7Ifz95h.js → powershell-B4Y7KIgx.js} +0 -0
- /package/dist/web/assets/{prisma-Z52HUJzQ.js → prisma-J--XdTXX.js} +0 -0
- /package/dist/web/assets/{prolog-DLqW-zR1.js → prolog-BBTg4Vb3.js} +0 -0
- /package/dist/web/assets/{proto-BOIzxCCJ.js → proto-DWlum3T8.js} +0 -0
- /package/dist/web/assets/{puppet-De7TjZIf.js → puppet-ZiUfc9JV.js} +0 -0
- /package/dist/web/assets/{purescript-R76ou1WT.js → purescript-CmT8SCHB.js} +0 -0
- /package/dist/web/assets/{python-Dprm0tNP.js → python-BURpj-fq.js} +0 -0
- /package/dist/web/assets/{qmldir-Clp_jpNw.js → qmldir-CpaiJywQ.js} +0 -0
- /package/dist/web/assets/{qss-eQPYYzSo.js → qss-C-zdz0i5.js} +0 -0
- /package/dist/web/assets/{r-CCcRHIf5.js → r-K56BeS-s.js} +0 -0
- /package/dist/web/assets/{racket-CzE472yp.js → racket-gkn6Z_Jw.js} +0 -0
- /package/dist/web/assets/{raku-DQwXrTSi.js → raku-C0ZBuKlx.js} +0 -0
- /package/dist/web/assets/{red-KCS6wyop.js → red-BWZ6x-3p.js} +0 -0
- /package/dist/web/assets/{reg-CcrAddUh.js → reg-Dt_EsyGR.js} +0 -0
- /package/dist/web/assets/{regexp-DDbud-Ri.js → regexp-CT6BLG2C.js} +0 -0
- /package/dist/web/assets/{rel-99GDgaHp.js → rel-MJ2LcyzJ.js} +0 -0
- /package/dist/web/assets/{resolve-theme-CW2niGV2.js → resolve-theme-B_ukrg_8.js} +0 -0
- /package/dist/web/assets/{riscv-Dm2eNv0R.js → riscv-BZYWzMAA.js} +0 -0
- /package/dist/web/assets/{ron-CzFt0ygH.js → ron-CLbwHWMn.js} +0 -0
- /package/dist/web/assets/{rose-pine-Ck6lCrCP.js → rose-pine-BPiI_daR.js} +0 -0
- /package/dist/web/assets/{rose-pine-dawn-vtALRJNj.js → rose-pine-dawn-BjkJc7dW.js} +0 -0
- /package/dist/web/assets/{rose-pine-moon-DPto8qXw.js → rose-pine-moon-BQSxkt7g.js} +0 -0
- /package/dist/web/assets/{rosmsg-NtS6429H.js → rosmsg-yt4151pJ.js} +0 -0
- /package/dist/web/assets/{rust-BjIXvF9h.js → rust-1dAcengL.js} +0 -0
- /package/dist/web/assets/{sass-CQ23wf5E.js → sass-DUgtQJsg.js} +0 -0
- /package/dist/web/assets/{scala-LfKLM143.js → scala-CxoGYpS0.js} +0 -0
- /package/dist/web/assets/{scheme-D3SLBXlF.js → scheme-B2K3u9e3.js} +0 -0
- /package/dist/web/assets/{sdbl-ODRd2rGN.js → sdbl-C0Ygk0n6.js} +0 -0
- /package/dist/web/assets/{shellscript-3KB1G_oA.js → shellscript-C2CvhqMk.js} +0 -0
- /package/dist/web/assets/{slack-dark-DTtyWmIy.js → slack-dark-DYdgzRGj.js} +0 -0
- /package/dist/web/assets/{slack-ochin-Bu0PApk3.js → slack-ochin-X-WD0WCg.js} +0 -0
- /package/dist/web/assets/{smalltalk-haMT-Sbe.js → smalltalk-Dp7g_zb9.js} +0 -0
- /package/dist/web/assets/{snazzy-light-DXJzefE9.js → snazzy-light-DWHWr-a6.js} +0 -0
- /package/dist/web/assets/{solarized-dark-B6LyYa18.js → solarized-dark-CxdLsjvb.js} +0 -0
- /package/dist/web/assets/{solarized-light-QLADebkv.js → solarized-light-Cz9rAvG5.js} +0 -0
- /package/dist/web/assets/{solidity-mzfq1aDc.js → solidity-D33gHHdn.js} +0 -0
- /package/dist/web/assets/{splunk-CyH4RWBp.js → splunk-BFbOroYw.js} +0 -0
- /package/dist/web/assets/{sql-CWVlT5D3.js → sql-ChpxvmdY.js} +0 -0
- /package/dist/web/assets/{ssh-config-DNvQJ1Qx.js → ssh-config-CUuSQ6JG.js} +0 -0
- /package/dist/web/assets/{streaming-store-uDu3PoN1.js → streaming-store-BYukdB56.js} +0 -0
- /package/dist/web/assets/{stylus-ByQQdtmf.js → stylus-DDihP0_W.js} +0 -0
- /package/dist/web/assets/{swift-D1cpY4ow.js → swift-XoalUn4x.js} +0 -0
- /package/dist/web/assets/{synthwave-84-BDF_-QiW.js → synthwave-84-DjkG0Bff.js} +0 -0
- /package/dist/web/assets/{system-verilog-BYrkMydC.js → system-verilog-f_pLKMYd.js} +0 -0
- /package/dist/web/assets/{systemd-CQm3zyK0.js → systemd-DjDcLfxp.js} +0 -0
- /package/dist/web/assets/{talonscript-DMW5CzE1.js → talonscript-BpUzYcDl.js} +0 -0
- /package/dist/web/assets/{tasl-BhFzMCr2.js → tasl-DP-cUbg8.js} +0 -0
- /package/dist/web/assets/{tcl-D7VEAF-9.js → tcl-BHoy-dfE.js} +0 -0
- /package/dist/web/assets/{terraform-DdT5A0Vy.js → terraform-vCVxN25B.js} +0 -0
- /package/dist/web/assets/{tokyo-night-WGLMM6Q9.js → tokyo-night-D0u4xZba.js} +0 -0
- /package/dist/web/assets/{toml-CZ7ipXHZ.js → toml-BS8D0__A.js} +0 -0
- /package/dist/web/assets/{tsv-B1zVvoTr.js → tsv-DCb-NQh3.js} +0 -0
- /package/dist/web/assets/{tsx-D4HwRUBq.js → tsx-DF8YeRgG.js} +0 -0
- /package/dist/web/assets/{turtle-B4r8KEJH.js → turtle-Bhx1GlIy.js} +0 -0
- /package/dist/web/assets/{typescript-BnHhxMtp.js → typescript-CAVD2Wb6.js} +0 -0
- /package/dist/web/assets/{typespec-DP2q46GT.js → typespec-D7mchdY_.js} +0 -0
- /package/dist/web/assets/{typst-DFsD_yaD.js → typst-CeXzq6Yw.js} +0 -0
- /package/dist/web/assets/{use-is-mobile-BQ9Bv5Ga.js → use-is-mobile-TkcwP1Gx.js} +0 -0
- /package/dist/web/assets/{use-visual-viewport-CbwnktSG.js → use-visual-viewport-Lp878uzs.js} +0 -0
- /package/dist/web/assets/{v-Bji8hSVT.js → v-DhzO54je.js} +0 -0
- /package/dist/web/assets/{vala-D3TIgGmU.js → vala-Dw_RAk3E.js} +0 -0
- /package/dist/web/assets/{vb-DvcxFS2q.js → vb-CiXdNh2Y.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-BqPDsE9b.js → vendor-xterm-BHEY6Ctu.js} +0 -0
- /package/dist/web/assets/{verilog-Dy7cF35d.js → verilog-fITdxqwV.js} +0 -0
- /package/dist/web/assets/{vesper-BzjwUv9J.js → vesper-B1wm_At7.js} +0 -0
- /package/dist/web/assets/{vhdl-DuFnFNmW.js → vhdl-ebHjqKp9.js} +0 -0
- /package/dist/web/assets/{viml-yAPgKlbz.js → viml-Dgmnoxoo.js} +0 -0
- /package/dist/web/assets/{vitesse-black-oSSQdyIZ.js → vitesse-black-BZ5gMb7l.js} +0 -0
- /package/dist/web/assets/{vitesse-dark-BZxwPihB.js → vitesse-dark-Lij7kkOt.js} +0 -0
- /package/dist/web/assets/{vitesse-light-uIigycvt.js → vitesse-light-DW6Wq7VC.js} +0 -0
- /package/dist/web/assets/{vyper-CX3NAxm5.js → vyper-6FxDnVNg.js} +0 -0
- /package/dist/web/assets/{wasm-6mEOOvVa.js → wasm-BDxaNPCj.js} +0 -0
- /package/dist/web/assets/{wasm-D9td37qO.js → wasm-DKhAtzfe.js} +0 -0
- /package/dist/web/assets/{wenyan-lwNIHgk6.js → wenyan-C0k0DYG_.js} +0 -0
- /package/dist/web/assets/{wgsl-DQAzCfog.js → wgsl-Y2zagbdn.js} +0 -0
- /package/dist/web/assets/{wikitext-DTXmt5NE.js → wikitext-C4QGWzkY.js} +0 -0
- /package/dist/web/assets/{wit-Bdw_7WDH.js → wit-CQESzjHy.js} +0 -0
- /package/dist/web/assets/{wolfram-DqyHDZ07.js → wolfram-B-ia69Ia.js} +0 -0
- /package/dist/web/assets/{yaml-CPXhdByd.js → yaml-CEYYTQPm.js} +0 -0
- /package/dist/web/assets/{zenscript-6s_i5zek.js → zenscript-BggWXIeJ.js} +0 -0
- /package/dist/web/assets/{zig-B2htezJ_.js → zig-CbaUfBQe.js} +0 -0
|
@@ -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 {
|
|
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
|
-
/**
|
|
33
|
-
|
|
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",
|
|
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",
|
|
41
|
-
"-framerate",
|
|
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;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
...
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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(
|
|
25
|
-
|
|
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
|
+
}
|