@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,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one piece of clipboard UI, shared by the desktop window and the mobile viewer.
|
|
3
|
+
*
|
|
4
|
+
* It exists because both failure modes of clipboard sync are otherwise *silent*: text the host
|
|
5
|
+
* copied cannot be written to this device without a secure context (PPM is usually plain HTTP on
|
|
6
|
+
* a LAN), and a host with no `xclip`/`wl-copy` installed simply does nothing. One is fixed by a
|
|
7
|
+
* click, the other by a command — so this renders whichever applies and nothing at all when
|
|
8
|
+
* sync is working.
|
|
9
|
+
*/
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
import { Clipboard, ClipboardCheck, TerminalSquare, X } from "@/lib/icons";
|
|
12
|
+
import { runInTerminal } from "@/lib/run-in-terminal";
|
|
13
|
+
import { cn } from "@/lib/utils";
|
|
14
|
+
import { copyTextWithGesture } from "./remote-desktop-clipboard-client";
|
|
15
|
+
import type { RequirementAction } from "./use-remote-desktop-readiness";
|
|
16
|
+
|
|
17
|
+
export interface RemoteDesktopClipboardNoticeProps {
|
|
18
|
+
/** Host text that needs a real click to reach this device's clipboard. */
|
|
19
|
+
pendingText: string | null;
|
|
20
|
+
onDismiss: () => void;
|
|
21
|
+
/** Set when the user tried to paste and the *host* has no clipboard tool. */
|
|
22
|
+
missingToolAction?: RequirementAction | null;
|
|
23
|
+
onDismissMissingTool?: () => void;
|
|
24
|
+
/** Override the bar's placement. The desktop window floats it over the video; the mobile
|
|
25
|
+
* viewer puts it in normal flow above the toolbar, which the on-screen keyboard already
|
|
26
|
+
* lifts — floating it there would land it behind the toolbar instead. */
|
|
27
|
+
positionClassName?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function RemoteDesktopClipboardNotice({
|
|
31
|
+
pendingText,
|
|
32
|
+
onDismiss,
|
|
33
|
+
missingToolAction,
|
|
34
|
+
onDismissMissingTool,
|
|
35
|
+
positionClassName = "absolute bottom-2 left-1/2 max-w-[92%] -translate-x-1/2",
|
|
36
|
+
}: RemoteDesktopClipboardNoticeProps) {
|
|
37
|
+
const [copyFailed, setCopyFailed] = useState(false);
|
|
38
|
+
|
|
39
|
+
if (pendingText !== null) {
|
|
40
|
+
const onCopy = () => {
|
|
41
|
+
// Synchronous inside the click: `execCommand` is ignored outside a user gesture.
|
|
42
|
+
if (copyTextWithGesture(pendingText)) onDismiss();
|
|
43
|
+
else setCopyFailed(true);
|
|
44
|
+
};
|
|
45
|
+
return (
|
|
46
|
+
<NoticeBar onDismiss={onDismiss} testId="remote-desktop-clipboard-pending" positionClassName={positionClassName}>
|
|
47
|
+
<ClipboardCheck className="size-4 shrink-0 text-emerald-400" />
|
|
48
|
+
<span className="min-w-0 flex-1 truncate">
|
|
49
|
+
{copyFailed ? "Could not copy — select the text manually:" : "Copied on the host:"}
|
|
50
|
+
<span className="ml-1 text-white/60">{preview(pendingText)}</span>
|
|
51
|
+
</span>
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
onClick={onCopy}
|
|
55
|
+
className="min-h-8 shrink-0 rounded bg-white/15 px-2.5 text-xs hover:bg-white/25"
|
|
56
|
+
data-testid="remote-desktop-clipboard-copy"
|
|
57
|
+
>
|
|
58
|
+
Copy here
|
|
59
|
+
</button>
|
|
60
|
+
</NoticeBar>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (missingToolAction !== undefined && missingToolAction !== null) {
|
|
65
|
+
return (
|
|
66
|
+
<NoticeBar
|
|
67
|
+
onDismiss={onDismissMissingTool ?? (() => {})}
|
|
68
|
+
testId="remote-desktop-clipboard-missing-tool"
|
|
69
|
+
positionClassName={positionClassName}
|
|
70
|
+
>
|
|
71
|
+
<Clipboard className="size-4 shrink-0 text-amber-300" />
|
|
72
|
+
<span className="min-w-0 flex-1">The host has no clipboard tool installed.</span>
|
|
73
|
+
{missingToolAction.kind === "terminal" && (
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
onClick={() => runInTerminal(missingToolAction.command)}
|
|
77
|
+
title={missingToolAction.command}
|
|
78
|
+
className="inline-flex min-h-8 shrink-0 items-center gap-1.5 rounded bg-white/15 px-2.5 text-xs hover:bg-white/25"
|
|
79
|
+
>
|
|
80
|
+
<TerminalSquare className="size-3.5" /> {missingToolAction.label}
|
|
81
|
+
</button>
|
|
82
|
+
)}
|
|
83
|
+
</NoticeBar>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Bottom-centre by default, so it never covers the corner controls. */
|
|
91
|
+
function NoticeBar({
|
|
92
|
+
children,
|
|
93
|
+
onDismiss,
|
|
94
|
+
testId,
|
|
95
|
+
positionClassName,
|
|
96
|
+
}: {
|
|
97
|
+
children: React.ReactNode;
|
|
98
|
+
onDismiss: () => void;
|
|
99
|
+
testId: string;
|
|
100
|
+
positionClassName: string;
|
|
101
|
+
}) {
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
className={cn(
|
|
105
|
+
"z-50 flex items-center gap-2 rounded-lg border border-white/10 bg-black/85 px-3 py-2 text-xs text-white shadow-lg",
|
|
106
|
+
positionClassName,
|
|
107
|
+
)}
|
|
108
|
+
data-testid={testId}
|
|
109
|
+
>
|
|
110
|
+
{children}
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
onClick={onDismiss}
|
|
114
|
+
aria-label="Dismiss"
|
|
115
|
+
className="flex size-8 shrink-0 items-center justify-center rounded text-white/60 hover:bg-white/10 hover:text-white"
|
|
116
|
+
>
|
|
117
|
+
<X className="size-3.5" />
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Enough to recognise the text, on one line — a pasted stack trace must not take the screen. */
|
|
124
|
+
function preview(text: string): string {
|
|
125
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
126
|
+
return oneLine.length > 60 ? `${oneLine.slice(0, 60)}…` : oneLine;
|
|
127
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human names for ffmpeg's H.264 encoders, for the codec picker.
|
|
3
|
+
*
|
|
4
|
+
* The wire values are ffmpeg's own encoder names, because that is what the host probes and
|
|
5
|
+
* spawns — but `h264_qsv` says nothing to anyone who has not read the capture code, and the
|
|
6
|
+
* choice a user is actually making is "the GPU or the CPU". Unknown names are shown verbatim
|
|
7
|
+
* rather than hidden: a newer ffmpeg gaining an encoder should add a row here, and until it
|
|
8
|
+
* does the picker must still be able to name what the host is running.
|
|
9
|
+
*/
|
|
10
|
+
const CODEC_LABELS: Record<string, string> = {
|
|
11
|
+
h264_nvenc: "NVIDIA NVENC",
|
|
12
|
+
h264_qsv: "Intel QuickSync",
|
|
13
|
+
h264_vaapi: "VAAPI (GPU)",
|
|
14
|
+
h264_amf: "AMD AMF",
|
|
15
|
+
h264_videotoolbox: "Apple VideoToolbox",
|
|
16
|
+
libx264: "Software (x264)",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function codecLabel(encoder: string): string {
|
|
20
|
+
return Object.hasOwn(CODEC_LABELS, encoder) ? CODEC_LABELS[encoder]! : encoder;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Whether this encoder runs on the GPU — the part that matters for "why is my CPU at 100%".
|
|
24
|
+
* Everything except libx264 is hardware; the list is the encoders, not a name pattern, so a
|
|
25
|
+
* future software encoder cannot be mislabelled by matching `h264_*`. */
|
|
26
|
+
export function isHardwareCodec(encoder: string): boolean {
|
|
27
|
+
return encoder !== "libx264" && Object.hasOwn(CODEC_LABELS, encoder);
|
|
28
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RustDesk's "Custom image quality" dialog: a bitrate-percentage slider, an fps slider, a
|
|
3
|
+
* "More" checkbox that raises the bitrate ceiling from 100% to 2000%, and Close.
|
|
4
|
+
*
|
|
5
|
+
* Both sliders commit on a debounce, not per tick: each commit respawns the host's ffmpeg
|
|
6
|
+
* (`-b:v`/`-r` are fixed at spawn), so a dragged slider would otherwise restart the capture
|
|
7
|
+
* dozens of times and the picture would strobe. RustDesk gets away with per-tick updates
|
|
8
|
+
* because its encoder is retunable in place; ours is not, and that difference is the reason
|
|
9
|
+
* for the delay rather than an arbitrary choice.
|
|
10
|
+
*
|
|
11
|
+
* Shown as a bottom sheet below `md` per `docs/design-guidelines.md`.
|
|
12
|
+
*/
|
|
13
|
+
import { useEffect, useRef, useState } from "react";
|
|
14
|
+
import {
|
|
15
|
+
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
|
|
16
|
+
} from "@/components/ui/dialog";
|
|
17
|
+
import { Button } from "@/components/ui/button";
|
|
18
|
+
import {
|
|
19
|
+
CUSTOM_FPS_MAX, CUSTOM_FPS_MIN, CUSTOM_QUALITY_MAX_MORE_PERCENT, CUSTOM_QUALITY_MAX_PERCENT,
|
|
20
|
+
CUSTOM_QUALITY_MIN_PERCENT, clampCustomFps, clampCustomQualityPercent, customBitrateKbps,
|
|
21
|
+
} from "../../../shared/remote-desktop-custom-quality";
|
|
22
|
+
|
|
23
|
+
/** Long enough that a drag is one respawn, short enough to feel like a live control. */
|
|
24
|
+
const COMMIT_DEBOUNCE_MS = 400;
|
|
25
|
+
|
|
26
|
+
export interface RemoteDesktopCustomQualityDialogProps {
|
|
27
|
+
open: boolean;
|
|
28
|
+
onOpenChange: (open: boolean) => void;
|
|
29
|
+
percent: number;
|
|
30
|
+
fps: number;
|
|
31
|
+
more: boolean;
|
|
32
|
+
onSetMore: (more: boolean) => void;
|
|
33
|
+
/** Commit both values; also selects the custom rung. */
|
|
34
|
+
onCommit: (percent: number, fps: number) => void;
|
|
35
|
+
/** The capture's own size, so the dialog can say what the percentage really costs. */
|
|
36
|
+
frameSize: { width: number; height: number };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function RemoteDesktopCustomQualityDialog({
|
|
40
|
+
open, onOpenChange, percent, fps, more, onSetMore, onCommit, frameSize,
|
|
41
|
+
}: RemoteDesktopCustomQualityDialogProps) {
|
|
42
|
+
const [localPercent, setLocalPercent] = useState(percent);
|
|
43
|
+
const [localFps, setLocalFps] = useState(fps);
|
|
44
|
+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
45
|
+
|
|
46
|
+
// Adopt the stored values whenever the dialog is opened, so it never shows a stale drag.
|
|
47
|
+
useEffect(() => { if (open) { setLocalPercent(percent); setLocalFps(fps); } }, [open, percent, fps]);
|
|
48
|
+
useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);
|
|
49
|
+
|
|
50
|
+
const max = more ? CUSTOM_QUALITY_MAX_MORE_PERCENT : CUSTOM_QUALITY_MAX_PERCENT;
|
|
51
|
+
|
|
52
|
+
const commitLater = (nextPercent: number, nextFps: number) => {
|
|
53
|
+
if (timer.current) clearTimeout(timer.current);
|
|
54
|
+
timer.current = setTimeout(() => onCommit(nextPercent, nextFps), COMMIT_DEBOUNCE_MS);
|
|
55
|
+
};
|
|
56
|
+
const changePercent = (raw: number) => {
|
|
57
|
+
const next = clampCustomQualityPercent(raw, more);
|
|
58
|
+
setLocalPercent(next);
|
|
59
|
+
commitLater(next, localFps);
|
|
60
|
+
};
|
|
61
|
+
const changeFps = (raw: number) => {
|
|
62
|
+
const next = clampCustomFps(raw);
|
|
63
|
+
setLocalFps(next);
|
|
64
|
+
commitLater(localPercent, next);
|
|
65
|
+
};
|
|
66
|
+
const toggleMore = (next: boolean) => {
|
|
67
|
+
onSetMore(next);
|
|
68
|
+
// Turning it off strands a percent above the new ceiling; commit the clamped value so the
|
|
69
|
+
// host is never left encoding at a rate the dialog no longer shows.
|
|
70
|
+
const clamped = clampCustomQualityPercent(localPercent, next);
|
|
71
|
+
if (clamped !== localPercent) { setLocalPercent(clamped); commitLater(clamped, localFps); }
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const kbps = customBitrateKbps(localPercent, frameSize.width, frameSize.height);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
78
|
+
<DialogContent className="sm:max-w-md" data-testid="remote-desktop-custom-quality-dialog">
|
|
79
|
+
<DialogHeader>
|
|
80
|
+
<DialogTitle>Custom image quality</DialogTitle>
|
|
81
|
+
<DialogDescription>
|
|
82
|
+
{/* RustDesk shows the bare percentage. The real figure is worth showing because the
|
|
83
|
+
percentage is not a ratio — 50% is the resolution's *base* bitrate, not half of
|
|
84
|
+
anything — so the number alone reads as "half quality" and is not. */}
|
|
85
|
+
{frameSize.width > 0
|
|
86
|
+
? `${localPercent}% of the base rate for ${frameSize.width}×${frameSize.height} — about ${(kbps / 1000).toFixed(1)} Mbit/s`
|
|
87
|
+
: "Waiting for the first frame"}
|
|
88
|
+
</DialogDescription>
|
|
89
|
+
</DialogHeader>
|
|
90
|
+
|
|
91
|
+
<div className="space-y-4 py-2">
|
|
92
|
+
<div className="flex items-center gap-3">
|
|
93
|
+
<input
|
|
94
|
+
type="range"
|
|
95
|
+
min={CUSTOM_QUALITY_MIN_PERCENT}
|
|
96
|
+
max={max}
|
|
97
|
+
step={1}
|
|
98
|
+
value={Math.min(localPercent, max)}
|
|
99
|
+
onChange={(e) => changePercent(Number(e.target.value))}
|
|
100
|
+
aria-label="Bitrate"
|
|
101
|
+
data-testid="remote-desktop-custom-bitrate"
|
|
102
|
+
className="h-1 min-w-0 flex-1 cursor-pointer appearance-none rounded bg-accent accent-primary"
|
|
103
|
+
/>
|
|
104
|
+
<span className="w-14 text-right text-xs tabular-nums">{localPercent}%</span>
|
|
105
|
+
<span className="w-14 text-xs text-muted-foreground">Bitrate</span>
|
|
106
|
+
<label className="flex shrink-0 items-center gap-1.5 text-xs">
|
|
107
|
+
<input
|
|
108
|
+
type="checkbox"
|
|
109
|
+
checked={more}
|
|
110
|
+
onChange={(e) => toggleMore(e.target.checked)}
|
|
111
|
+
data-testid="remote-desktop-custom-more"
|
|
112
|
+
className="size-3.5 accent-primary"
|
|
113
|
+
/>
|
|
114
|
+
More
|
|
115
|
+
</label>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div className="flex items-center gap-3">
|
|
119
|
+
<input
|
|
120
|
+
type="range"
|
|
121
|
+
min={CUSTOM_FPS_MIN}
|
|
122
|
+
max={CUSTOM_FPS_MAX}
|
|
123
|
+
step={1}
|
|
124
|
+
value={localFps}
|
|
125
|
+
onChange={(e) => changeFps(Number(e.target.value))}
|
|
126
|
+
aria-label="FPS"
|
|
127
|
+
data-testid="remote-desktop-custom-fps"
|
|
128
|
+
className="h-1 min-w-0 flex-1 cursor-pointer appearance-none rounded bg-accent accent-primary"
|
|
129
|
+
/>
|
|
130
|
+
<span className="w-14 text-right text-xs tabular-nums">{localFps}</span>
|
|
131
|
+
<span className="w-14 text-xs text-muted-foreground">FPS</span>
|
|
132
|
+
{/* Keeps the two rows' sliders the same length as the row above, which carries an
|
|
133
|
+
extra checkbox. */}
|
|
134
|
+
<span className="w-[4.25rem] shrink-0" aria-hidden />
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<DialogFooter>
|
|
139
|
+
<Button onClick={() => onOpenChange(false)}>Close</Button>
|
|
140
|
+
</DialogFooter>
|
|
141
|
+
</DialogContent>
|
|
142
|
+
</Dialog>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -14,8 +14,18 @@
|
|
|
14
14
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
15
15
|
import { cn } from "@/lib/utils";
|
|
16
16
|
|
|
17
|
+
/** Ctrl/Cmd + C or X, by `code` rather than by button label so renaming a button cannot
|
|
18
|
+
* silently stop the host clipboard being fetched. */
|
|
19
|
+
function isCopyChord(codes: readonly string[]): boolean {
|
|
20
|
+
const mod = codes.includes("ControlLeft") || codes.includes("MetaLeft");
|
|
21
|
+
return mod && (codes.includes("KeyC") || codes.includes("KeyX"));
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
export interface RemoteDesktopMobileKeyBarProps {
|
|
18
25
|
sendMessage: (msg: Record<string, unknown>) => void;
|
|
26
|
+
/** Fired after a copy/cut combo — the touch equivalent of the desktop's Ctrl+C cue, since
|
|
27
|
+
* none of these buttons goes through `use-remote-input-capture`'s key handler. */
|
|
28
|
+
onCopyCombo?: () => void;
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
const MODIFIERS = [
|
|
@@ -73,7 +83,7 @@ function KeyBarButton({
|
|
|
73
83
|
);
|
|
74
84
|
}
|
|
75
85
|
|
|
76
|
-
export function RemoteDesktopMobileKeyBar({ sendMessage }: RemoteDesktopMobileKeyBarProps) {
|
|
86
|
+
export function RemoteDesktopMobileKeyBar({ sendMessage, onCopyCombo }: RemoteDesktopMobileKeyBarProps) {
|
|
77
87
|
const [held, setHeld] = useState<ReadonlySet<string>>(new Set());
|
|
78
88
|
const heldRef = useRef(held);
|
|
79
89
|
heldRef.current = held;
|
|
@@ -106,7 +116,8 @@ export function RemoteDesktopMobileKeyBar({ sendMessage }: RemoteDesktopMobileKe
|
|
|
106
116
|
// chord (last key down first key up would read as a different shortcut on some apps).
|
|
107
117
|
codes.forEach((code) => sendMessage({ type: "key", code, down: true }));
|
|
108
118
|
[...codes].reverse().forEach((code) => sendMessage({ type: "key", code, down: false }));
|
|
109
|
-
|
|
119
|
+
if (isCopyChord(codes)) onCopyCombo?.();
|
|
120
|
+
}, [sendMessage, onCopyCombo]);
|
|
110
121
|
|
|
111
122
|
return (
|
|
112
123
|
<div
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mobile viewer's settings panel — the toggles the desktop keeps in its `Settings2`
|
|
3
|
+
* dropdown, which has no room in a thumb-zone row of 44px buttons.
|
|
4
|
+
*
|
|
5
|
+
* Rendered *in flow* inside the bottom bar rather than through `mobile-bottom-sheet`, exactly
|
|
6
|
+
* like `remote-desktop-mobile-key-bar.tsx`: the viewer already lives in its own full-viewport
|
|
7
|
+
* portal, so a second portal would have to win a z-index argument with it, and a panel in the
|
|
8
|
+
* bottom bar rides that bar's `keyboardInset` lift for free.
|
|
9
|
+
*
|
|
10
|
+
* Scale modes are deliberately absent: pinch-zoom and the toolbar's Reset zoom already own the
|
|
11
|
+
* transform here, and they do it continuously rather than in three fixed steps.
|
|
12
|
+
*/
|
|
13
|
+
import { useMemo } from "react";
|
|
14
|
+
import { Circle, Clipboard, EyeOff, Gauge, Image, MousePointer2, Square, Volume2 } from "@/lib/icons";
|
|
15
|
+
import { cn } from "@/lib/utils";
|
|
16
|
+
import { fitLocalMode, localScreenSize, resolutionChoices } from "./remote-desktop-resolution-list";
|
|
17
|
+
import type { HostMode } from "./use-remote-desktop-readiness";
|
|
18
|
+
|
|
19
|
+
export interface RemoteDesktopMobileSettingsProps {
|
|
20
|
+
statsVisible: boolean;
|
|
21
|
+
onToggleStats: () => void;
|
|
22
|
+
showCursor: boolean;
|
|
23
|
+
onSetShowCursor: (show: boolean) => void;
|
|
24
|
+
clipboardEnabled: boolean;
|
|
25
|
+
onSetClipboardEnabled: (enabled: boolean) => void;
|
|
26
|
+
audioOn: boolean;
|
|
27
|
+
/** Why audio cannot be turned on, or null when it can. */
|
|
28
|
+
audioReason: string | null;
|
|
29
|
+
onSetAudioOn: (on: boolean) => void;
|
|
30
|
+
privacyOn: boolean;
|
|
31
|
+
privacyReason: string | null;
|
|
32
|
+
onSetPrivacyOn: (on: boolean) => void;
|
|
33
|
+
/** The host's own display modes, collapsed to one row per size by the panel. Empty on a host
|
|
34
|
+
* whose resolution cannot be changed, which renders no section at all. */
|
|
35
|
+
hostModes: HostMode[];
|
|
36
|
+
hostModeId: string | null;
|
|
37
|
+
/** The mode the host was on before this session changed it, for the "Original" chip. */
|
|
38
|
+
hostOriginalModeId: string | null;
|
|
39
|
+
onSetHostMode: (modeId: string) => void;
|
|
40
|
+
/** Input injection available. Resizing the host is host control, so a view-only session must
|
|
41
|
+
* not offer it — RustDesk gates the same submenu on `ffiModel.keyboard`. */
|
|
42
|
+
canResizeHost: boolean;
|
|
43
|
+
resolutionError: string | null;
|
|
44
|
+
recording: boolean;
|
|
45
|
+
recordingSupported: boolean;
|
|
46
|
+
onToggleRecording: () => void;
|
|
47
|
+
onScreenshot: () => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function SettingRow({
|
|
51
|
+
icon, label, hint, checked, onToggle, testId, disabled,
|
|
52
|
+
}: {
|
|
53
|
+
icon: React.ReactNode;
|
|
54
|
+
label: string;
|
|
55
|
+
hint?: string;
|
|
56
|
+
checked: boolean;
|
|
57
|
+
onToggle: () => void;
|
|
58
|
+
testId: string;
|
|
59
|
+
disabled?: boolean;
|
|
60
|
+
}) {
|
|
61
|
+
return (
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
role="switch"
|
|
65
|
+
aria-checked={checked}
|
|
66
|
+
disabled={disabled}
|
|
67
|
+
onClick={onToggle}
|
|
68
|
+
data-testid={testId}
|
|
69
|
+
className="flex min-h-11 w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-white/80 active:bg-white/10 disabled:opacity-50"
|
|
70
|
+
>
|
|
71
|
+
<span className="shrink-0 opacity-80">{icon}</span>
|
|
72
|
+
<span className="min-w-0 flex-1">
|
|
73
|
+
<span className="block truncate text-xs">{label}</span>
|
|
74
|
+
{hint && <span className="block truncate text-[10px] text-white/45">{hint}</span>}
|
|
75
|
+
</span>
|
|
76
|
+
{/* A drawn switch rather than the `ui/switch` primitive: this panel sits on black inside
|
|
77
|
+
the viewer's own portal, where the themed component's surface colours disappear. */}
|
|
78
|
+
<span
|
|
79
|
+
className={cn(
|
|
80
|
+
"relative h-5 w-9 shrink-0 rounded-full transition-colors",
|
|
81
|
+
checked ? "bg-primary" : "bg-white/20",
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
<span
|
|
85
|
+
className={cn(
|
|
86
|
+
"absolute top-0.5 size-4 rounded-full bg-white transition-all",
|
|
87
|
+
checked ? "left-[1.125rem]" : "left-0.5",
|
|
88
|
+
)}
|
|
89
|
+
/>
|
|
90
|
+
</span>
|
|
91
|
+
</button>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function RemoteDesktopMobileSettings({
|
|
96
|
+
statsVisible, onToggleStats, showCursor, onSetShowCursor,
|
|
97
|
+
clipboardEnabled, onSetClipboardEnabled,
|
|
98
|
+
audioOn, audioReason, onSetAudioOn,
|
|
99
|
+
privacyOn, privacyReason, onSetPrivacyOn,
|
|
100
|
+
hostModes, hostModeId, hostOriginalModeId, onSetHostMode, canResizeHost, resolutionError,
|
|
101
|
+
recording, recordingSupported, onToggleRecording, onScreenshot,
|
|
102
|
+
}: RemoteDesktopMobileSettingsProps) {
|
|
103
|
+
const resolutions = useMemo(() => resolutionChoices(hostModes), [hostModes]);
|
|
104
|
+
const localScreen = useMemo(() => localScreenSize(), []);
|
|
105
|
+
const fitLocal = useMemo(
|
|
106
|
+
() => fitLocalMode(hostModes, localScreen, hostModeId),
|
|
107
|
+
[hostModes, localScreen, hostModeId],
|
|
108
|
+
);
|
|
109
|
+
return (
|
|
110
|
+
<div
|
|
111
|
+
className="mx-2 mb-1 space-y-0.5 rounded-xl border border-white/10 bg-black/90 p-1"
|
|
112
|
+
data-testid="remote-desktop-mobile-settings"
|
|
113
|
+
>
|
|
114
|
+
<SettingRow
|
|
115
|
+
icon={<Gauge className="size-4" />}
|
|
116
|
+
label="Quality monitor"
|
|
117
|
+
checked={statsVisible}
|
|
118
|
+
onToggle={onToggleStats}
|
|
119
|
+
testId="remote-desktop-mobile-stats-toggle"
|
|
120
|
+
/>
|
|
121
|
+
<SettingRow
|
|
122
|
+
icon={<MousePointer2 className="size-4" />}
|
|
123
|
+
label="Show remote cursor"
|
|
124
|
+
hint="Restarts the stream"
|
|
125
|
+
checked={showCursor}
|
|
126
|
+
onToggle={() => onSetShowCursor(!showCursor)}
|
|
127
|
+
testId="remote-desktop-mobile-cursor-toggle"
|
|
128
|
+
/>
|
|
129
|
+
<SettingRow
|
|
130
|
+
icon={<Clipboard className="size-4" />}
|
|
131
|
+
label="Sync clipboard"
|
|
132
|
+
hint="Off: Ctrl+V pastes the host's own clipboard"
|
|
133
|
+
checked={clipboardEnabled}
|
|
134
|
+
onToggle={() => onSetClipboardEnabled(!clipboardEnabled)}
|
|
135
|
+
testId="remote-desktop-mobile-clipboard-toggle"
|
|
136
|
+
/>
|
|
137
|
+
{/* The tap on this row is the user gesture the autoplay policy needs to resume the
|
|
138
|
+
`AudioContext`, which is why audio can only ever be started from a control. */}
|
|
139
|
+
<SettingRow
|
|
140
|
+
icon={<Volume2 className="size-4" />}
|
|
141
|
+
label="Play host audio"
|
|
142
|
+
hint={audioReason ?? undefined}
|
|
143
|
+
checked={audioOn}
|
|
144
|
+
disabled={audioReason !== null}
|
|
145
|
+
onToggle={() => onSetAudioOn(!audioOn)}
|
|
146
|
+
testId="remote-desktop-mobile-audio-toggle"
|
|
147
|
+
/>
|
|
148
|
+
<SettingRow
|
|
149
|
+
icon={<EyeOff className="size-4" />}
|
|
150
|
+
label="Privacy mode"
|
|
151
|
+
hint={privacyReason ?? "Blocks the host's own keyboard and mouse"}
|
|
152
|
+
checked={privacyOn}
|
|
153
|
+
disabled={privacyReason !== null && !privacyOn}
|
|
154
|
+
onToggle={() => onSetPrivacyOn(!privacyOn)}
|
|
155
|
+
testId="remote-desktop-mobile-privacy-toggle"
|
|
156
|
+
/>
|
|
157
|
+
{/* A scrolling list of chips rather than a select: a native `<select>` on a phone opens
|
|
158
|
+
the OS picker *over* the viewer's own portal, and this panel is inside it. */}
|
|
159
|
+
{canResizeHost && resolutions.length > 1 && (
|
|
160
|
+
<div className="px-3 pt-1">
|
|
161
|
+
<div className="pb-1.5 text-[10px] uppercase tracking-wide text-white/40">Resolution</div>
|
|
162
|
+
<div className="flex max-h-28 flex-wrap gap-1.5 overflow-y-auto">
|
|
163
|
+
{hostOriginalModeId && hostOriginalModeId !== hostModeId && (
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
onClick={() => onSetHostMode(hostOriginalModeId)}
|
|
167
|
+
data-testid="remote-desktop-mobile-resolution-original"
|
|
168
|
+
className="min-h-9 rounded-md bg-white/10 px-2.5 py-1 text-[11px] text-white/70 active:bg-white/20"
|
|
169
|
+
>
|
|
170
|
+
Original
|
|
171
|
+
</button>
|
|
172
|
+
)}
|
|
173
|
+
{/* A phone's screen is almost never one of the host's advertised sizes, so this chip
|
|
174
|
+
is normally absent — the same as RustDesk on the same pair. */}
|
|
175
|
+
{fitLocal && (
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
onClick={() => onSetHostMode(fitLocal.id)}
|
|
179
|
+
data-testid="remote-desktop-mobile-resolution-fit-local"
|
|
180
|
+
className="min-h-9 rounded-md bg-white/10 px-2.5 py-1 text-[11px] text-white/70 active:bg-white/20"
|
|
181
|
+
>
|
|
182
|
+
Fit local
|
|
183
|
+
</button>
|
|
184
|
+
)}
|
|
185
|
+
{resolutions.map((r) => {
|
|
186
|
+
const active = (hostModeId ?? resolutions.find((c) => c.current)?.id) === r.id;
|
|
187
|
+
return (
|
|
188
|
+
<button
|
|
189
|
+
key={r.id}
|
|
190
|
+
type="button"
|
|
191
|
+
onClick={() => onSetHostMode(r.id)}
|
|
192
|
+
data-testid={`remote-desktop-mobile-resolution-${r.width}x${r.height}`}
|
|
193
|
+
className={cn(
|
|
194
|
+
"min-h-9 rounded-md px-2.5 py-1 text-[11px] tabular-nums",
|
|
195
|
+
active ? "bg-white/25 text-white" : "bg-white/10 text-white/70 active:bg-white/20",
|
|
196
|
+
)}
|
|
197
|
+
>
|
|
198
|
+
{r.width}×{r.height}
|
|
199
|
+
</button>
|
|
200
|
+
);
|
|
201
|
+
})}
|
|
202
|
+
</div>
|
|
203
|
+
{resolutionError && (
|
|
204
|
+
<div className="pt-1.5 text-[10px] leading-tight text-red-400">{resolutionError}</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
{/* Actions, not switches — a plain row each, so the drawn switch is never shown for
|
|
209
|
+
something that does not stay on. */}
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
onClick={onScreenshot}
|
|
213
|
+
data-testid="remote-desktop-mobile-screenshot"
|
|
214
|
+
className="flex min-h-11 w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-xs text-white/80 active:bg-white/10"
|
|
215
|
+
>
|
|
216
|
+
<Image className="size-4 opacity-80" /> Save screenshot
|
|
217
|
+
</button>
|
|
218
|
+
{recordingSupported && (
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
onClick={onToggleRecording}
|
|
222
|
+
data-testid="remote-desktop-mobile-record"
|
|
223
|
+
className="flex min-h-11 w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-xs text-white/80 active:bg-white/10"
|
|
224
|
+
>
|
|
225
|
+
{recording
|
|
226
|
+
? <><Square className="size-4 text-red-500" /> Stop recording</>
|
|
227
|
+
: <><Circle className="size-4 opacity-80" /> Start recording</>}
|
|
228
|
+
</button>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bottom (thumb-zone) toolbar for the mobile remote-desktop viewer: Touch/Mouse mode toggle,
|
|
3
3
|
* virtual keyboard, zoom reset, close. Every button is a 44px+ touch target per
|
|
4
|
-
* `docs/design-guidelines.md`'s Mobile-First UI Rules
|
|
4
|
+
* `docs/design-guidelines.md`'s Mobile-First UI Rules — which is also why the three on/off
|
|
5
|
+
* settings live behind the More button in `remote-desktop-mobile-settings.tsx` instead of as
|
|
6
|
+
* three more buttons here: the row is `flex-1` per button, and at eight of them each target
|
|
7
|
+
* falls under 44px on a small phone.
|
|
5
8
|
*/
|
|
6
|
-
import { Hand, MousePointer2, Keyboard, ZoomOut,
|
|
9
|
+
import { Hand, MousePointer2, Keyboard, ZoomOut, Settings2, Monitor, Wifi, X } from "@/lib/icons";
|
|
7
10
|
import { cn } from "@/lib/utils";
|
|
8
|
-
import { useSettingsStore } from "@/stores/settings-store";
|
|
9
11
|
import type { RemoteDesktopInputMode } from "./use-remote-desktop-touch";
|
|
10
12
|
|
|
11
13
|
export interface RemoteDesktopMobileToolbarProps {
|
|
@@ -13,10 +15,17 @@ export interface RemoteDesktopMobileToolbarProps {
|
|
|
13
15
|
displayLabel: string | null;
|
|
14
16
|
/** Cycle to the next display — one tap per hop beats a dropdown in the thumb zone. */
|
|
15
17
|
onNextDisplay: () => void;
|
|
18
|
+
/** Short name of the rung being streamed ("Auto", "480p"…) for the quality button's label. */
|
|
19
|
+
qualityLabel: string;
|
|
20
|
+
/** Cycle auto → tiny → low → balanced → high → auto, same one-tap-per-hop reasoning. */
|
|
21
|
+
onNextQuality: () => void;
|
|
16
22
|
mode: RemoteDesktopInputMode;
|
|
17
23
|
onToggleMode: () => void;
|
|
18
24
|
onOpenKeyboard: () => void;
|
|
19
25
|
onResetZoom: () => void;
|
|
26
|
+
/** Show/hide the settings panel above this row (stats, remote cursor, clipboard sync). */
|
|
27
|
+
settingsOpen: boolean;
|
|
28
|
+
onToggleSettings: () => void;
|
|
20
29
|
onClose: () => void;
|
|
21
30
|
}
|
|
22
31
|
|
|
@@ -51,15 +60,16 @@ function ToolbarButton({
|
|
|
51
60
|
export function RemoteDesktopMobileToolbar({
|
|
52
61
|
displayLabel,
|
|
53
62
|
onNextDisplay,
|
|
63
|
+
qualityLabel,
|
|
64
|
+
onNextQuality,
|
|
54
65
|
mode,
|
|
55
66
|
onToggleMode,
|
|
56
67
|
onOpenKeyboard,
|
|
57
68
|
onResetZoom,
|
|
69
|
+
settingsOpen,
|
|
70
|
+
onToggleSettings,
|
|
58
71
|
onClose,
|
|
59
72
|
}: RemoteDesktopMobileToolbarProps) {
|
|
60
|
-
const statsVisible = useSettingsStore((s) => s.remoteDesktopStatsVisible);
|
|
61
|
-
const toggleStats = useSettingsStore((s) => s.toggleRemoteDesktopStatsVisible);
|
|
62
|
-
|
|
63
73
|
return (
|
|
64
74
|
<div
|
|
65
75
|
className="flex shrink-0 items-stretch gap-1 border-t border-white/10 bg-black/90 px-2 pb-[max(0.375rem,env(safe-area-inset-bottom))] pt-1.5"
|
|
@@ -79,8 +89,11 @@ export function RemoteDesktopMobileToolbar({
|
|
|
79
89
|
<Monitor className="size-5" />
|
|
80
90
|
</ToolbarButton>
|
|
81
91
|
)}
|
|
82
|
-
<ToolbarButton onClick={
|
|
83
|
-
<
|
|
92
|
+
<ToolbarButton onClick={onNextQuality} label={qualityLabel}>
|
|
93
|
+
<Wifi className="size-5" />
|
|
94
|
+
</ToolbarButton>
|
|
95
|
+
<ToolbarButton onClick={onToggleSettings} label="More" active={settingsOpen}>
|
|
96
|
+
<Settings2 className="size-5" />
|
|
84
97
|
</ToolbarButton>
|
|
85
98
|
<ToolbarButton onClick={onClose} label="Close">
|
|
86
99
|
<X className="size-5" />
|