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