@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
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The desktop viewer's toolbar, modelled on RustDesk's: icon buttons that open grouped menus,
|
|
3
|
+
* floating over the video rather than taking layout from it.
|
|
4
|
+
*
|
|
5
|
+
* Structure follows RustDesk's `remote_toolbar.dart` — a **Monitor** menu (only meaningful on a
|
|
6
|
+
* multi-display host, so it renders nothing on a single-monitor one, exactly as RustDesk's
|
|
7
|
+
* does) and a **Display** menu holding the scale modes, image quality, and the toggles. What is
|
|
8
|
+
* absent is absent because the feature behind it does not exist yet, not because it was
|
|
9
|
+
* forgotten: codec choice (the host probes one H.264 encoder and uses it), host resolution,
|
|
10
|
+
* virtual displays, audio/mute, recording and privacy mode.
|
|
11
|
+
*
|
|
12
|
+
* The stats overlay keeps its own one-tap button instead of moving inside the menu where
|
|
13
|
+
* RustDesk keeps it: it is a thing people flick on and off while diagnosing a stream, and
|
|
14
|
+
* burying a frequently-toggled control two clicks deep to match another product's layout would
|
|
15
|
+
* be a regression dressed as parity.
|
|
16
|
+
*/
|
|
17
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
18
|
+
import { Circle, EyeOff, Gauge, Image, Monitor, Minus, Plus, Settings2, Square } from "@/lib/icons";
|
|
19
|
+
import { cn } from "@/lib/utils";
|
|
20
|
+
import {
|
|
21
|
+
DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem,
|
|
22
|
+
DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator,
|
|
23
|
+
DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger,
|
|
24
|
+
} from "@/components/ui/dropdown-menu";
|
|
25
|
+
import {
|
|
26
|
+
QUALITY_PRESETS, QUALITY_PRESET_ORDER, type QualityChoice, type QualityPresetId,
|
|
27
|
+
} from "../../../shared/remote-desktop-quality";
|
|
28
|
+
import {
|
|
29
|
+
CUSTOM_SCALE_DEBOUNCE_MS, CUSTOM_SCALE_MAX_PERCENT, CUSTOM_SCALE_MIN_PERCENT,
|
|
30
|
+
CUSTOM_SCALE_NUDGE_PERCENT, VIEW_STYLE_LABELS, VIEW_STYLE_ORDER,
|
|
31
|
+
clampCustomScale, clampScalePercent, scalePercentToPos, scalePosToPercent, snapScalePos,
|
|
32
|
+
type ViewStyle,
|
|
33
|
+
} from "./remote-desktop-view-style";
|
|
34
|
+
import { codecLabel } from "./remote-desktop-codec-labels";
|
|
35
|
+
import {
|
|
36
|
+
fitLocalMode, localScreenSize, resolutionChoices, resolutionLabel,
|
|
37
|
+
} from "./remote-desktop-resolution-list";
|
|
38
|
+
import type { HostMode } from "./use-remote-desktop-readiness";
|
|
39
|
+
import type { RemoteDisplay } from "./use-remote-desktop-readiness";
|
|
40
|
+
|
|
41
|
+
export interface RemoteDesktopToolbarProps {
|
|
42
|
+
displays: RemoteDisplay[];
|
|
43
|
+
currentDisplayId: string | undefined;
|
|
44
|
+
onSelectDisplay: (id: string) => void;
|
|
45
|
+
|
|
46
|
+
quality: { preset: QualityPresetId; held: number };
|
|
47
|
+
/** The rung the client has selected, which is what the menu ticks. Distinct from
|
|
48
|
+
* `quality.preset` (the rung the host confirmed) because `custom` is a choice no preset id
|
|
49
|
+
* can represent. */
|
|
50
|
+
qualityChoice: QualityChoice;
|
|
51
|
+
onSetQuality: (choice: QualityChoice) => void;
|
|
52
|
+
/** Opens the custom dialog. Selecting "Custom" must not commit anything by itself — the
|
|
53
|
+
* numbers live in the dialog. */
|
|
54
|
+
onOpenCustomQuality: () => void;
|
|
55
|
+
|
|
56
|
+
/** Every encoder the host can really run, preference order; the first is its own default. */
|
|
57
|
+
codecs: string[];
|
|
58
|
+
/** The encoder the host is running, or null while it is on its own first choice. */
|
|
59
|
+
codec: string | null;
|
|
60
|
+
onSetCodec: (encoder: string | null) => void;
|
|
61
|
+
|
|
62
|
+
/** The host's own display modes. Empty on a host whose resolution cannot be changed. */
|
|
63
|
+
hostModes: HostMode[];
|
|
64
|
+
/** The mode the host reports as live, or null before it has reported. */
|
|
65
|
+
hostModeId: string | null;
|
|
66
|
+
/** The mode the host was on before this session changed it, for the "Original" item. */
|
|
67
|
+
hostOriginalModeId: string | null;
|
|
68
|
+
onSetHostMode: (modeId: string) => void;
|
|
69
|
+
/** Whether input injection is available. RustDesk gates this whole submenu on
|
|
70
|
+
* `ffiModel.keyboard`: resizing someone else's screen is host control, so a view-only
|
|
71
|
+
* session must not be able to do it even though it can read the mode list. */
|
|
72
|
+
canResizeHost: boolean;
|
|
73
|
+
/** Why the last switch failed, or null. Shown inside the submenu rather than as a toast: the
|
|
74
|
+
* X server refuses a mode per-mode, so the message only means anything next to the list. */
|
|
75
|
+
resolutionError: string | null;
|
|
76
|
+
|
|
77
|
+
viewStyle: ViewStyle;
|
|
78
|
+
customScale: number;
|
|
79
|
+
onSetViewStyle: (style: ViewStyle) => void;
|
|
80
|
+
onSetCustomScale: (scale: number) => void;
|
|
81
|
+
|
|
82
|
+
showCursor: boolean;
|
|
83
|
+
onSetShowCursor: (show: boolean) => void;
|
|
84
|
+
|
|
85
|
+
/** Host audio. `audioReason` is why it cannot be turned on (no loopback device on the host);
|
|
86
|
+
* non-null disables the item and explains itself rather than hiding it, because "there is no
|
|
87
|
+
* sound" is otherwise indistinguishable from a bug. */
|
|
88
|
+
audioOn: boolean;
|
|
89
|
+
audioReason: string | null;
|
|
90
|
+
onSetAudioOn: (on: boolean) => void;
|
|
91
|
+
|
|
92
|
+
/** Privacy mode: the host's local keyboard and mouse stop working and its monitor goes dark.
|
|
93
|
+
* `privacyReason` is why it is unavailable *or* why the last engage failed. */
|
|
94
|
+
privacyOn: boolean;
|
|
95
|
+
privacyReason: string | null;
|
|
96
|
+
privacyCanBlank: boolean;
|
|
97
|
+
onSetPrivacyOn: (on: boolean) => void;
|
|
98
|
+
|
|
99
|
+
/** Session recording. `recordingSupported` is false only where `MediaRecorder` is absent. */
|
|
100
|
+
recording: boolean;
|
|
101
|
+
recordingElapsedSec: number;
|
|
102
|
+
recordingSupported: boolean;
|
|
103
|
+
onToggleRecording: () => void;
|
|
104
|
+
onScreenshot: () => void;
|
|
105
|
+
|
|
106
|
+
clipboardEnabled: boolean;
|
|
107
|
+
onSetClipboardEnabled: (enabled: boolean) => void;
|
|
108
|
+
|
|
109
|
+
statsVisible: boolean;
|
|
110
|
+
onToggleStats: () => void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const BUTTON = "flex size-7 shrink-0 items-center justify-center rounded bg-black/40 text-white/70 "
|
|
114
|
+
+ "outline-none hover:bg-black/60 hover:text-white data-[state=open]:bg-black/70 data-[state=open]:text-white";
|
|
115
|
+
|
|
116
|
+
export function RemoteDesktopToolbar({
|
|
117
|
+
displays, currentDisplayId, onSelectDisplay,
|
|
118
|
+
quality, qualityChoice, onSetQuality, onOpenCustomQuality,
|
|
119
|
+
codecs, codec, onSetCodec,
|
|
120
|
+
hostModes, hostModeId, hostOriginalModeId, onSetHostMode, canResizeHost, resolutionError,
|
|
121
|
+
viewStyle, customScale, onSetViewStyle, onSetCustomScale,
|
|
122
|
+
showCursor, onSetShowCursor,
|
|
123
|
+
audioOn, audioReason, onSetAudioOn,
|
|
124
|
+
privacyOn, privacyReason, privacyCanBlank, onSetPrivacyOn,
|
|
125
|
+
recording, recordingElapsedSec, recordingSupported, onToggleRecording, onScreenshot,
|
|
126
|
+
clipboardEnabled, onSetClipboardEnabled,
|
|
127
|
+
statsVisible, onToggleStats,
|
|
128
|
+
}: RemoteDesktopToolbarProps) {
|
|
129
|
+
// The server treats "no displayId" as the primary, so the radio group has to resolve the same
|
|
130
|
+
// way or the menu shows nothing selected on a session that never picked explicitly.
|
|
131
|
+
// Memoised: it is a fresh array per call and the menu re-renders on every stats tick.
|
|
132
|
+
const resolutions = useMemo(() => resolutionChoices(hostModes), [hostModes]);
|
|
133
|
+
// RustDesk reads the local screen once and offers the host mode that matches it exactly.
|
|
134
|
+
// `screen` does not change while a session is open, so this is a one-shot read.
|
|
135
|
+
const localScreen = useMemo(() => localScreenSize(), []);
|
|
136
|
+
const fitLocal = useMemo(
|
|
137
|
+
() => fitLocalMode(hostModes, localScreen, hostModeId),
|
|
138
|
+
[hostModes, localScreen, hostModeId],
|
|
139
|
+
);
|
|
140
|
+
// RustDesk's `showOriginalBtn`: only once the session has moved the host off its own mode.
|
|
141
|
+
const showOriginal = Boolean(hostOriginalModeId && hostOriginalModeId !== hostModeId);
|
|
142
|
+
|
|
143
|
+
const activeDisplay = currentDisplayId
|
|
144
|
+
?? displays.find((d) => d.primary)?.id
|
|
145
|
+
?? displays[0]?.id
|
|
146
|
+
?? "";
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div
|
|
150
|
+
className="absolute right-1 top-1 z-40 flex items-center gap-1"
|
|
151
|
+
data-testid="remote-desktop-toolbar"
|
|
152
|
+
>
|
|
153
|
+
{/* Nothing to choose between on a single-display host. */}
|
|
154
|
+
{displays.length > 1 && (
|
|
155
|
+
<DropdownMenu>
|
|
156
|
+
<DropdownMenuTrigger className={BUTTON} aria-label="Display" title="Display">
|
|
157
|
+
<Monitor className="size-3.5" />
|
|
158
|
+
</DropdownMenuTrigger>
|
|
159
|
+
<DropdownMenuContent align="end" className="w-64">
|
|
160
|
+
<DropdownMenuLabel>Display</DropdownMenuLabel>
|
|
161
|
+
<DropdownMenuRadioGroup value={activeDisplay} onValueChange={onSelectDisplay}>
|
|
162
|
+
{displays.map((d) => (
|
|
163
|
+
<DropdownMenuRadioItem key={d.id} value={d.id} data-testid={`remote-desktop-display-${d.id}`}>
|
|
164
|
+
{d.label}{d.primary ? " (main)" : ""} · {d.width}×{d.height}
|
|
165
|
+
</DropdownMenuRadioItem>
|
|
166
|
+
))}
|
|
167
|
+
</DropdownMenuRadioGroup>
|
|
168
|
+
</DropdownMenuContent>
|
|
169
|
+
</DropdownMenu>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
<DropdownMenu>
|
|
173
|
+
<DropdownMenuTrigger
|
|
174
|
+
className={BUTTON}
|
|
175
|
+
aria-label="Display settings"
|
|
176
|
+
title="Display settings"
|
|
177
|
+
data-testid="remote-desktop-settings-trigger"
|
|
178
|
+
>
|
|
179
|
+
<Settings2 className="size-3.5" />
|
|
180
|
+
</DropdownMenuTrigger>
|
|
181
|
+
<DropdownMenuContent align="end" className="w-64">
|
|
182
|
+
<DropdownMenuLabel>Scale</DropdownMenuLabel>
|
|
183
|
+
<DropdownMenuRadioGroup
|
|
184
|
+
value={viewStyle}
|
|
185
|
+
onValueChange={(v) => onSetViewStyle(v as ViewStyle)}
|
|
186
|
+
>
|
|
187
|
+
{VIEW_STYLE_ORDER.map((style) => (
|
|
188
|
+
<DropdownMenuRadioItem key={style} value={style} data-testid={`remote-desktop-view-style-${style}`}>
|
|
189
|
+
{VIEW_STYLE_LABELS[style]}
|
|
190
|
+
</DropdownMenuRadioItem>
|
|
191
|
+
))}
|
|
192
|
+
</DropdownMenuRadioGroup>
|
|
193
|
+
{viewStyle === "custom" && <ZoomRow scale={customScale} onSet={onSetCustomScale} />}
|
|
194
|
+
|
|
195
|
+
<DropdownMenuSeparator />
|
|
196
|
+
<DropdownMenuSub>
|
|
197
|
+
<DropdownMenuSubTrigger data-testid="remote-desktop-quality-submenu">
|
|
198
|
+
Image quality
|
|
199
|
+
{/* The choice is a ceiling, so a congested link streams *under* it. Saying so is
|
|
200
|
+
what stops that reading as the menu having been ignored — the alternative is a
|
|
201
|
+
ticked rung the picture does not match, which is how the old freeze behaved. */}
|
|
202
|
+
{quality.held < 1 && (
|
|
203
|
+
<span className="ml-1.5 text-[10px] opacity-60" data-testid="remote-desktop-quality-held">
|
|
204
|
+
{Math.round(quality.held * 100)}% — link limited
|
|
205
|
+
</span>
|
|
206
|
+
)}
|
|
207
|
+
</DropdownMenuSubTrigger>
|
|
208
|
+
<DropdownMenuSubContent className="w-56">
|
|
209
|
+
<DropdownMenuRadioGroup
|
|
210
|
+
value={qualityChoice}
|
|
211
|
+
onValueChange={(v) => (v === "custom" ? onOpenCustomQuality() : onSetQuality(v as QualityChoice))}
|
|
212
|
+
>
|
|
213
|
+
{/* RustDesk's four, in its order, with nothing else: no `auto` rung, because
|
|
214
|
+
the adaptation always runs beneath whichever of these is chosen. */}
|
|
215
|
+
{QUALITY_PRESET_ORDER.map((id) => (
|
|
216
|
+
<DropdownMenuRadioItem key={id} value={id} data-testid={`remote-desktop-quality-${id}`}>
|
|
217
|
+
{QUALITY_PRESETS[id].label}
|
|
218
|
+
</DropdownMenuRadioItem>
|
|
219
|
+
))}
|
|
220
|
+
{/* Last, as in RustDesk. It opens the dialog rather than committing, because
|
|
221
|
+
"custom" without its two numbers is not a setting. */}
|
|
222
|
+
<DropdownMenuRadioItem value="custom" data-testid="remote-desktop-quality-custom">
|
|
223
|
+
Custom…
|
|
224
|
+
</DropdownMenuRadioItem>
|
|
225
|
+
</DropdownMenuRadioGroup>
|
|
226
|
+
</DropdownMenuSubContent>
|
|
227
|
+
</DropdownMenuSub>
|
|
228
|
+
|
|
229
|
+
{/* One encoder means no choice to offer — which is also the ffmpeg-missing case, and
|
|
230
|
+
the readiness gate above has already explained that one. */}
|
|
231
|
+
{codecs.length > 1 && (
|
|
232
|
+
<DropdownMenuSub>
|
|
233
|
+
<DropdownMenuSubTrigger data-testid="remote-desktop-codec-submenu">
|
|
234
|
+
Codec
|
|
235
|
+
</DropdownMenuSubTrigger>
|
|
236
|
+
<DropdownMenuSubContent className="w-56">
|
|
237
|
+
<DropdownMenuRadioGroup
|
|
238
|
+
value={codec ?? "auto"}
|
|
239
|
+
onValueChange={(v) => onSetCodec(v === "auto" ? null : v)}
|
|
240
|
+
>
|
|
241
|
+
<DropdownMenuRadioItem value="auto">
|
|
242
|
+
Auto · {codecLabel(codecs[0]!)}
|
|
243
|
+
</DropdownMenuRadioItem>
|
|
244
|
+
{codecs.map((enc) => (
|
|
245
|
+
<DropdownMenuRadioItem key={enc} value={enc} data-testid={`remote-desktop-codec-${enc}`}>
|
|
246
|
+
{codecLabel(enc)}
|
|
247
|
+
</DropdownMenuRadioItem>
|
|
248
|
+
))}
|
|
249
|
+
</DropdownMenuRadioGroup>
|
|
250
|
+
</DropdownMenuSubContent>
|
|
251
|
+
</DropdownMenuSub>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
{/* RustDesk's own name for this is "Resolution" (`_ResolutionsMenu`, right here after
|
|
255
|
+
Image quality and Codec), so that is what it is called — calling it anything else
|
|
256
|
+
invites the reading that it changes *this* window rather than the host's screen,
|
|
257
|
+
which is what the scale modes above do.
|
|
258
|
+
One row per size rather than per timing — see `remote-desktop-resolution-list.ts`.
|
|
259
|
+
Absent entirely when the host cannot change its mode (anything but X11 today):
|
|
260
|
+
the reason is informational and belongs nowhere near a menu the user can click. */}
|
|
261
|
+
{canResizeHost && resolutions.length > 1 && (
|
|
262
|
+
<DropdownMenuSub>
|
|
263
|
+
<DropdownMenuSubTrigger data-testid="remote-desktop-resolution-submenu">
|
|
264
|
+
Resolution
|
|
265
|
+
</DropdownMenuSubTrigger>
|
|
266
|
+
<DropdownMenuSubContent className="max-h-80 w-56 overflow-y-auto">
|
|
267
|
+
{/* RustDesk shows "Original" only once the session has moved the host off it
|
|
268
|
+
(`isOriginalResolutionSet && !isOriginalResolution`) — otherwise it is a
|
|
269
|
+
button that does nothing. */}
|
|
270
|
+
{showOriginal && hostOriginalModeId && (
|
|
271
|
+
<DropdownMenuItem
|
|
272
|
+
onSelect={() => onSetHostMode(hostOriginalModeId)}
|
|
273
|
+
data-testid="remote-desktop-resolution-original"
|
|
274
|
+
>
|
|
275
|
+
Original
|
|
276
|
+
</DropdownMenuItem>
|
|
277
|
+
)}
|
|
278
|
+
{/* Absent unless the host advertises this client's screen size *exactly* and is
|
|
279
|
+
not already on it — see `fitLocalMode`. */}
|
|
280
|
+
{fitLocal && (
|
|
281
|
+
<DropdownMenuItem
|
|
282
|
+
onSelect={() => onSetHostMode(fitLocal.id)}
|
|
283
|
+
data-testid="remote-desktop-resolution-fit-local"
|
|
284
|
+
>
|
|
285
|
+
Fit local {fitLocal.width}×{fitLocal.height}
|
|
286
|
+
</DropdownMenuItem>
|
|
287
|
+
)}
|
|
288
|
+
{/* One divider for both, and only when there is something above it —
|
|
289
|
+
RustDesk's `_menuDivider` takes the same two flags. */}
|
|
290
|
+
{(showOriginal || fitLocal) && <DropdownMenuSeparator />}
|
|
291
|
+
<DropdownMenuRadioGroup
|
|
292
|
+
value={hostModeId ?? resolutions.find((r) => r.current)?.id ?? ""}
|
|
293
|
+
onValueChange={onSetHostMode}
|
|
294
|
+
>
|
|
295
|
+
{resolutions.map((r) => (
|
|
296
|
+
<DropdownMenuRadioItem key={r.id} value={r.id} data-testid={`remote-desktop-resolution-${r.width}x${r.height}`}>
|
|
297
|
+
{resolutionLabel(r)}
|
|
298
|
+
{r.preferred && <span className="ml-1.5 text-[10px] opacity-60">native</span>}
|
|
299
|
+
</DropdownMenuRadioItem>
|
|
300
|
+
))}
|
|
301
|
+
</DropdownMenuRadioGroup>
|
|
302
|
+
{resolutionError && (
|
|
303
|
+
<div className="px-2 py-1.5 text-[10px] leading-tight text-destructive">{resolutionError}</div>
|
|
304
|
+
)}
|
|
305
|
+
</DropdownMenuSubContent>
|
|
306
|
+
</DropdownMenuSub>
|
|
307
|
+
)}
|
|
308
|
+
|
|
309
|
+
<DropdownMenuSeparator />
|
|
310
|
+
{/* Toggling the cursor respawns the capture (~400ms of held picture), because ffmpeg
|
|
311
|
+
takes `-draw_mouse` at startup and there is no way to retune it on a live process. */}
|
|
312
|
+
<DropdownMenuCheckboxItem
|
|
313
|
+
checked={showCursor}
|
|
314
|
+
onCheckedChange={onSetShowCursor}
|
|
315
|
+
data-testid="remote-desktop-show-cursor"
|
|
316
|
+
>
|
|
317
|
+
Show remote cursor
|
|
318
|
+
</DropdownMenuCheckboxItem>
|
|
319
|
+
<DropdownMenuCheckboxItem
|
|
320
|
+
checked={clipboardEnabled}
|
|
321
|
+
onCheckedChange={onSetClipboardEnabled}
|
|
322
|
+
data-testid="remote-desktop-clipboard-toggle"
|
|
323
|
+
>
|
|
324
|
+
Sync clipboard
|
|
325
|
+
</DropdownMenuCheckboxItem>
|
|
326
|
+
{/* Starting audio creates an `AudioContext`, which the autoplay policy only lets
|
|
327
|
+
resume inside a user gesture — this click is that gesture, so it must stay a
|
|
328
|
+
menu item and never move into an effect. */}
|
|
329
|
+
<DropdownMenuCheckboxItem
|
|
330
|
+
checked={audioOn}
|
|
331
|
+
disabled={audioReason !== null}
|
|
332
|
+
onCheckedChange={onSetAudioOn}
|
|
333
|
+
data-testid="remote-desktop-audio-toggle"
|
|
334
|
+
>
|
|
335
|
+
<span className="flex min-w-0 flex-col">
|
|
336
|
+
<span>Play host audio</span>
|
|
337
|
+
{audioReason && <span className="text-[10px] leading-tight text-muted-foreground">{audioReason}</span>}
|
|
338
|
+
</span>
|
|
339
|
+
</DropdownMenuCheckboxItem>
|
|
340
|
+
|
|
341
|
+
<DropdownMenuSeparator />
|
|
342
|
+
{/* Last in the list and described in full, because this is the one item that takes
|
|
343
|
+
the host away from whoever is sitting at it. */}
|
|
344
|
+
<DropdownMenuCheckboxItem
|
|
345
|
+
checked={privacyOn}
|
|
346
|
+
disabled={privacyReason !== null && !privacyOn}
|
|
347
|
+
onCheckedChange={onSetPrivacyOn}
|
|
348
|
+
data-testid="remote-desktop-privacy-toggle"
|
|
349
|
+
>
|
|
350
|
+
<span className="flex min-w-0 flex-col">
|
|
351
|
+
<span>Privacy mode</span>
|
|
352
|
+
<span className="text-[10px] leading-tight text-muted-foreground">
|
|
353
|
+
{privacyReason
|
|
354
|
+
?? (privacyCanBlank
|
|
355
|
+
? "Blocks the host's keyboard and mouse, and blanks its screen"
|
|
356
|
+
: "Blocks the host's keyboard and mouse (its screen stays on)")}
|
|
357
|
+
</span>
|
|
358
|
+
</span>
|
|
359
|
+
</DropdownMenuCheckboxItem>
|
|
360
|
+
|
|
361
|
+
<DropdownMenuSeparator />
|
|
362
|
+
<DropdownMenuItem onSelect={onScreenshot} data-testid="remote-desktop-screenshot">
|
|
363
|
+
<Image className="size-3.5" /> Save screenshot
|
|
364
|
+
</DropdownMenuItem>
|
|
365
|
+
{recordingSupported && (
|
|
366
|
+
<DropdownMenuItem onSelect={onToggleRecording} data-testid="remote-desktop-record">
|
|
367
|
+
{recording
|
|
368
|
+
? <><Square className="size-3.5 text-red-500" /> Stop recording · {formatElapsed(recordingElapsedSec)}</>
|
|
369
|
+
: <><Circle className="size-3.5" /> Start recording</>}
|
|
370
|
+
</DropdownMenuItem>
|
|
371
|
+
)}
|
|
372
|
+
</DropdownMenuContent>
|
|
373
|
+
</DropdownMenu>
|
|
374
|
+
|
|
375
|
+
{/* Visible without opening the menu, and for a stronger reason than recording: the person
|
|
376
|
+
at the host cannot use it right now, and the only way out is from here. */}
|
|
377
|
+
{privacyOn && (
|
|
378
|
+
<button
|
|
379
|
+
type="button"
|
|
380
|
+
onClick={() => onSetPrivacyOn(false)}
|
|
381
|
+
title="Privacy mode is on — click to release the host"
|
|
382
|
+
aria-label="Turn privacy mode off"
|
|
383
|
+
className="flex h-7 shrink-0 items-center gap-1 rounded bg-amber-500/85 px-1.5 text-[10px] font-medium text-black hover:bg-amber-400"
|
|
384
|
+
data-testid="remote-desktop-privacy-indicator"
|
|
385
|
+
>
|
|
386
|
+
<EyeOff className="size-3" /> Host locked
|
|
387
|
+
</button>
|
|
388
|
+
)}
|
|
389
|
+
{/* Visible without opening the menu: a session that is quietly recording has to say so. */}
|
|
390
|
+
{recording && (
|
|
391
|
+
<button
|
|
392
|
+
type="button"
|
|
393
|
+
onClick={onToggleRecording}
|
|
394
|
+
title="Stop recording"
|
|
395
|
+
aria-label="Stop recording"
|
|
396
|
+
className="flex h-7 shrink-0 items-center gap-1 rounded bg-red-600/80 px-1.5 text-[10px] font-medium tabular-nums text-white hover:bg-red-600"
|
|
397
|
+
data-testid="remote-desktop-recording-indicator"
|
|
398
|
+
>
|
|
399
|
+
<span className="size-1.5 animate-pulse rounded-full bg-white" />
|
|
400
|
+
{formatElapsed(recordingElapsedSec)}
|
|
401
|
+
</button>
|
|
402
|
+
)}
|
|
403
|
+
<button
|
|
404
|
+
type="button"
|
|
405
|
+
onClick={onToggleStats}
|
|
406
|
+
aria-label="Toggle stats overlay"
|
|
407
|
+
aria-pressed={statsVisible}
|
|
408
|
+
title="Quality monitor"
|
|
409
|
+
className={cn(BUTTON, statsVisible && "bg-primary/70 text-primary-foreground hover:bg-primary/80")}
|
|
410
|
+
>
|
|
411
|
+
<Gauge className="size-3.5" />
|
|
412
|
+
</button>
|
|
413
|
+
</div>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** `m:ss`, or `h:mm:ss` past an hour. */
|
|
418
|
+
function formatElapsed(totalSec: number): string {
|
|
419
|
+
const h = Math.floor(totalSec / 3600);
|
|
420
|
+
const m = Math.floor((totalSec % 3600) / 60);
|
|
421
|
+
const sec = String(totalSec % 60).padStart(2, "0");
|
|
422
|
+
return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${sec}` : `${m}:${sec}`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Zoom stepper, shown only while the custom mode is selected — `e.preventDefault()` on the
|
|
426
|
+
* item keeps the menu open so the zoom can be nudged more than once per opening. */
|
|
427
|
+
/**
|
|
428
|
+
* RustDesk's scale-custom control: `Row([ −, Expanded(slider), + ])` with the percentage painted
|
|
429
|
+
* on the thumb (`_RectValueThumbShape`).
|
|
430
|
+
*
|
|
431
|
+
* Dragging updates the picture immediately but the *pref* is written on a 300ms debounce, the
|
|
432
|
+
* same as RustDesk's `kDebounceCustomScaleDuration` — a drag across the track is hundreds of
|
|
433
|
+
* ticks, and each one would otherwise be a `persistDevicePref` write.
|
|
434
|
+
*
|
|
435
|
+
* The slider's own range is the normalised track position, not the percent: the mapping is
|
|
436
|
+
* piecewise around the pivot, so a percent-valued slider would be linear and put 100% against
|
|
437
|
+
* the left stop. See `remote-desktop-view-style.ts`.
|
|
438
|
+
*/
|
|
439
|
+
function ZoomRow({ scale, onSet }: { scale: number; onSet: (n: number) => void }) {
|
|
440
|
+
// Local while dragging, so the thumb tracks the finger without waiting for the debounce.
|
|
441
|
+
const [dragPercent, setDragPercent] = useState<number | null>(null);
|
|
442
|
+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
443
|
+
const committed = clampScalePercent(Math.round(clampCustomScale(scale) * 100));
|
|
444
|
+
const pct = dragPercent ?? committed;
|
|
445
|
+
const pos = scalePercentToPos(pct);
|
|
446
|
+
|
|
447
|
+
useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);
|
|
448
|
+
|
|
449
|
+
const commit = (percent: number) => {
|
|
450
|
+
setDragPercent(percent);
|
|
451
|
+
if (timer.current) clearTimeout(timer.current);
|
|
452
|
+
timer.current = setTimeout(() => { setDragPercent(null); onSet(percent / 100); }, CUSTOM_SCALE_DEBOUNCE_MS);
|
|
453
|
+
};
|
|
454
|
+
const nudge = (direction: 1 | -1) => (e: React.MouseEvent) => {
|
|
455
|
+
e.preventDefault();
|
|
456
|
+
commit(clampScalePercent(pct + direction * CUSTOM_SCALE_NUDGE_PERCENT));
|
|
457
|
+
};
|
|
458
|
+
const btn = "flex size-6 shrink-0 items-center justify-center rounded hover:bg-accent disabled:opacity-40";
|
|
459
|
+
|
|
460
|
+
return (
|
|
461
|
+
<div className="px-2 py-1.5" data-testid="remote-desktop-zoom-row">
|
|
462
|
+
<div className="flex items-center gap-1.5">
|
|
463
|
+
<button type="button" className={btn} onClick={nudge(-1)} disabled={pct <= CUSTOM_SCALE_MIN_PERCENT} aria-label="Decrease">
|
|
464
|
+
<Minus className="size-3.5" />
|
|
465
|
+
</button>
|
|
466
|
+
<span className="relative flex-1 py-2.5">
|
|
467
|
+
<input
|
|
468
|
+
type="range"
|
|
469
|
+
min={0}
|
|
470
|
+
max={1}
|
|
471
|
+
// ~1% precision over the whole range, matching RustDesk's `divisions`.
|
|
472
|
+
step={1 / (CUSTOM_SCALE_MAX_PERCENT - CUSTOM_SCALE_MIN_PERCENT)}
|
|
473
|
+
value={pos}
|
|
474
|
+
aria-label="Scale"
|
|
475
|
+
aria-valuetext={`${pct}%`}
|
|
476
|
+
data-testid="remote-desktop-zoom-slider"
|
|
477
|
+
// `e.stopPropagation()`: the dropdown treats keys as typeahead and would close on
|
|
478
|
+
// the arrow keys the slider needs.
|
|
479
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
480
|
+
onChange={(e) => commit(scalePosToPercent(snapScalePos(Number(e.target.value))))}
|
|
481
|
+
className="h-1 w-full cursor-pointer appearance-none rounded bg-accent accent-primary"
|
|
482
|
+
/>
|
|
483
|
+
<span
|
|
484
|
+
className="pointer-events-none absolute top-0 -translate-x-1/2 rounded bg-primary px-1.5 text-[10px] leading-4 tabular-nums text-primary-foreground"
|
|
485
|
+
style={{ left: `${pos * 100}%` }}
|
|
486
|
+
data-testid="remote-desktop-zoom-value"
|
|
487
|
+
>
|
|
488
|
+
{pct}%
|
|
489
|
+
</span>
|
|
490
|
+
</span>
|
|
491
|
+
<button type="button" className={btn} onClick={nudge(1)} disabled={pct >= CUSTOM_SCALE_MAX_PERCENT} aria-label="Increase">
|
|
492
|
+
<Plus className="size-3.5" />
|
|
493
|
+
</button>
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the remote screen is fitted into the viewer — RustDesk's `ViewStyle`, ported.
|
|
3
|
+
*
|
|
4
|
+
* `adaptive` is the default there and here, and it is deliberately the one mode that computes
|
|
5
|
+
* *nothing*: the canvas keeps `max-w-full max-h-full` and the browser fits it, which is the
|
|
6
|
+
* behaviour the viewer has always had. Only `original` and `custom` need an explicit CSS size,
|
|
7
|
+
* and neither needs to know the viewport — they are a function of the capture's own pixel size,
|
|
8
|
+
* so there is no ResizeObserver anywhere in this feature.
|
|
9
|
+
*
|
|
10
|
+
* Input mapping needs no changes for any of this: `fractionFromPoint` reads the canvas's
|
|
11
|
+
* `getBoundingClientRect()`, so a canvas sized to 40% or 300% still maps a click to the right
|
|
12
|
+
* host pixel, and a scrolled container is already accounted for (the rect is viewport-relative).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type ViewStyle = "adaptive" | "original" | "custom";
|
|
16
|
+
|
|
17
|
+
/** RustDesk's default too — the only mode that always shows the whole remote screen. */
|
|
18
|
+
export const DEFAULT_VIEW_STYLE: ViewStyle = "adaptive";
|
|
19
|
+
|
|
20
|
+
/** Order the picker lists them in, matching RustDesk's menu. */
|
|
21
|
+
export const VIEW_STYLE_ORDER: readonly ViewStyle[] = ["original", "adaptive", "custom"];
|
|
22
|
+
|
|
23
|
+
export const VIEW_STYLE_LABELS: Record<ViewStyle, string> = {
|
|
24
|
+
original: "Scale original",
|
|
25
|
+
adaptive: "Scale adaptive",
|
|
26
|
+
custom: "Scale custom",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** 25%–300%. Below a quarter the picture is unusable and above 3x a 4K capture would ask the
|
|
30
|
+
* browser for a 12K-wide layer, which is where compositing starts failing outright. */
|
|
31
|
+
/* Bounds and slider mapping ported from RustDesk's `custom_scale_base.dart` +
|
|
32
|
+
* `consts.dart` (`kScaleCustomMinPercent` … `kDebounceCustomScaleDuration`), because the
|
|
33
|
+
* obvious implementation of this control is wrong in a way that is only obvious once built:
|
|
34
|
+
* the range is 5%–1000%, so a *linear* slider puts 100% at 9.5% of the track — the one value
|
|
35
|
+
* users actually want sits pinned against the left stop and cannot be hit. RustDesk's slider
|
|
36
|
+
* is piecewise-linear around a pivot: the first third of the track covers 5%→100% and the
|
|
37
|
+
* remaining two thirds cover 100%→1000%, with a detent that snaps back to exactly 100%. */
|
|
38
|
+
export const CUSTOM_SCALE_MIN_PERCENT = 5;
|
|
39
|
+
export const CUSTOM_SCALE_MAX_PERCENT = 1000;
|
|
40
|
+
export const CUSTOM_SCALE_PIVOT_PERCENT = 100;
|
|
41
|
+
/** 100% sits one third along the track. */
|
|
42
|
+
export const CUSTOM_SCALE_PIVOT_POS = 1 / 3;
|
|
43
|
+
/** Snap window around the pivot, in normalised track units (~0.6%). */
|
|
44
|
+
export const CUSTOM_SCALE_DETENT = 0.006;
|
|
45
|
+
/** The +/- buttons move one percentage point, matching RustDesk's `nudgeScale(±1)`. */
|
|
46
|
+
export const CUSTOM_SCALE_NUDGE_PERCENT = 1;
|
|
47
|
+
/** Committing on every slider tick would respawn nothing (this is pure CSS) but would write the
|
|
48
|
+
* device pref hundreds of times per drag. RustDesk debounces by the same 300ms. */
|
|
49
|
+
export const CUSTOM_SCALE_DEBOUNCE_MS = 300;
|
|
50
|
+
|
|
51
|
+
export const CUSTOM_SCALE_MIN = CUSTOM_SCALE_MIN_PERCENT / 100;
|
|
52
|
+
export const CUSTOM_SCALE_MAX = CUSTOM_SCALE_MAX_PERCENT / 100;
|
|
53
|
+
export const DEFAULT_CUSTOM_SCALE = 1;
|
|
54
|
+
|
|
55
|
+
/** Normalised track position [0,1] → percent. Piecewise around the pivot; see the note above. */
|
|
56
|
+
export function scalePosToPercent(pos: number): number {
|
|
57
|
+
if (!Number.isFinite(pos) || pos <= 0) return CUSTOM_SCALE_MIN_PERCENT;
|
|
58
|
+
if (pos >= 1) return CUSTOM_SCALE_MAX_PERCENT;
|
|
59
|
+
if (pos <= CUSTOM_SCALE_PIVOT_POS) {
|
|
60
|
+
const q = pos / CUSTOM_SCALE_PIVOT_POS;
|
|
61
|
+
return clampScalePercent(Math.round(
|
|
62
|
+
CUSTOM_SCALE_MIN_PERCENT + q * (CUSTOM_SCALE_PIVOT_PERCENT - CUSTOM_SCALE_MIN_PERCENT),
|
|
63
|
+
));
|
|
64
|
+
}
|
|
65
|
+
const q = (pos - CUSTOM_SCALE_PIVOT_POS) / (1 - CUSTOM_SCALE_PIVOT_POS);
|
|
66
|
+
return clampScalePercent(Math.round(
|
|
67
|
+
CUSTOM_SCALE_PIVOT_PERCENT + q * (CUSTOM_SCALE_MAX_PERCENT - CUSTOM_SCALE_PIVOT_PERCENT),
|
|
68
|
+
));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Percent → normalised track position. Inverse of `scalePosToPercent`. */
|
|
72
|
+
export function scalePercentToPos(percent: number): number {
|
|
73
|
+
const p = clampScalePercent(percent);
|
|
74
|
+
if (p <= CUSTOM_SCALE_PIVOT_PERCENT) {
|
|
75
|
+
const q = (p - CUSTOM_SCALE_MIN_PERCENT) / (CUSTOM_SCALE_PIVOT_PERCENT - CUSTOM_SCALE_MIN_PERCENT);
|
|
76
|
+
return q * CUSTOM_SCALE_PIVOT_POS;
|
|
77
|
+
}
|
|
78
|
+
const q = (p - CUSTOM_SCALE_PIVOT_PERCENT) / (CUSTOM_SCALE_MAX_PERCENT - CUSTOM_SCALE_PIVOT_PERCENT);
|
|
79
|
+
return CUSTOM_SCALE_PIVOT_POS + q * (1 - CUSTOM_SCALE_PIVOT_POS);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Snap a dragged position onto the pivot when it lands close, so 100% is reachable exactly. */
|
|
83
|
+
export function snapScalePos(pos: number): number {
|
|
84
|
+
if (!Number.isFinite(pos)) return CUSTOM_SCALE_PIVOT_POS;
|
|
85
|
+
if (Math.abs(pos - CUSTOM_SCALE_PIVOT_POS) <= CUSTOM_SCALE_DETENT) return CUSTOM_SCALE_PIVOT_POS;
|
|
86
|
+
return Math.min(Math.max(pos, 0), 1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function clampScalePercent(percent: number): number {
|
|
90
|
+
if (!Number.isFinite(percent)) return CUSTOM_SCALE_PIVOT_PERCENT;
|
|
91
|
+
return Math.min(Math.max(Math.round(percent), CUSTOM_SCALE_MIN_PERCENT), CUSTOM_SCALE_MAX_PERCENT);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Coerce a stored or wire value into a real mode. Same `Object.hasOwn` reasoning as
|
|
95
|
+
* `parsePresetId`: a prototype-walking check would let `"toString"` through. */
|
|
96
|
+
export function parseViewStyle(value: unknown): ViewStyle {
|
|
97
|
+
return typeof value === "string" && Object.hasOwn(VIEW_STYLE_LABELS, value)
|
|
98
|
+
? value as ViewStyle
|
|
99
|
+
: DEFAULT_VIEW_STYLE;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Clamp to the supported range, falling back to 1 for anything that is not a real number.
|
|
103
|
+
*
|
|
104
|
+
* Strict about the type rather than coercing: `Number(null)` is **0**, which is finite, so a
|
|
105
|
+
* coercing check would quietly turn a missing value into the 25% floor — a remote screen at
|
|
106
|
+
* quarter size looks like a rendering bug, not a default. And `width: NaNpx` is dropped
|
|
107
|
+
* silently, so a NaN that got this far would collapse the canvas to nothing with no error. */
|
|
108
|
+
export function clampCustomScale(value: unknown): number {
|
|
109
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_CUSTOM_SCALE;
|
|
110
|
+
return Math.min(Math.max(value, CUSTOM_SCALE_MIN), CUSTOM_SCALE_MAX);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** One step up or down for a +/- button: one percentage point, as RustDesk's `nudgeScale`. */
|
|
114
|
+
export function stepCustomScale(current: number, direction: 1 | -1): number {
|
|
115
|
+
const next = clampScalePercent(Math.round(clampCustomScale(current) * 100) + direction * CUSTOM_SCALE_NUDGE_PERCENT);
|
|
116
|
+
return next / 100;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface Size {
|
|
120
|
+
width: number;
|
|
121
|
+
height: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* CSS pixel size to give the canvas, or **null** to leave it to the stylesheet (adaptive).
|
|
126
|
+
*
|
|
127
|
+
* `natural` is the capture's own size (`canvas.width`/`canvas.height`, which the decoder sets
|
|
128
|
+
* from the decoded frame) — not the element's rendered box, which is what this decides.
|
|
129
|
+
* Returns null for a capture that has no size yet, so the first paint before any frame has
|
|
130
|
+
* arrived does not pin the canvas to 0x0.
|
|
131
|
+
*/
|
|
132
|
+
export function canvasCssSize(style: ViewStyle, customScale: number, natural: Size): Size | null {
|
|
133
|
+
if (style === "adaptive") return null;
|
|
134
|
+
if (natural.width <= 0 || natural.height <= 0) return null;
|
|
135
|
+
const scale = style === "original" ? 1 : clampCustomScale(customScale);
|
|
136
|
+
return { width: Math.round(natural.width * scale), height: Math.round(natural.height * scale) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** What to show on a compact control. Adaptive has no meaningful percentage (it is whatever
|
|
140
|
+
* fits), so it is named rather than measured. */
|
|
141
|
+
export function viewStyleShortLabel(style: ViewStyle, customScale: number): string {
|
|
142
|
+
if (style === "adaptive") return "Fit";
|
|
143
|
+
if (style === "original") return "1:1";
|
|
144
|
+
return `${Math.round(clampCustomScale(customScale) * 100)}%`;
|
|
145
|
+
}
|