@hienlh/ppm 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -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/http-api.md +1 -1
- package/bun.lock +16 -1
- package/dist/web/assets/accounts-settings-section-BVr9-FZJ.js +1 -0
- package/dist/web/assets/{ai-resource-editor-F09pf8Hd.js → ai-resource-editor-CdDpOwjh.js} +1 -1
- package/dist/web/assets/ai-settings-section-B1hQyjAd.js +1 -0
- package/dist/web/assets/ai-settings-section-X2UOfSaS.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-DLoPK0LD.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-G84Ojzae.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-DQJfjm8E.js +15 -0
- package/dist/web/assets/{cobol-DPx_nifl.js → cobol-DRaRy4FK.js} +1 -1
- package/dist/web/assets/code-editor-D04iUPsZ.js +7 -0
- package/dist/web/assets/{codex-daily-guard-CfybHv_c.js → codex-daily-guard-BSccLRGT.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-C4ay676e.js} +2 -2
- 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-DbwEmG7o.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-S6LsALjX.js} +1 -1
- package/dist/web/assets/{dialog-DQTn55Mt.js → dialog-B84_DJQ8.js} +1 -1
- package/dist/web/assets/diff-viewer-3oBA7vgQ.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-DJmaKVY0.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-BQ0jyAGv.js +2 -0
- package/dist/web/assets/explorer-window-content-jdkWt34J.js +1 -0
- package/dist/web/assets/{extension-manager-section-BhwB6nqr.js → extension-manager-section-DG1guILi.js} +1 -1
- package/dist/web/assets/{extension-webview-DI4VL2tJ.js → extension-webview-BGSDUbdK.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-Gnyd7han.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-V6Khh15V.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-Bi513qjT.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-DnQwYSmQ.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-DpczdwyV.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-DU68xk06.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-f2HUbGDY.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-DCRsnKg5.js +1 -0
- package/dist/web/assets/{keyboard-shortcuts-section-BW03P3AF.js → keyboard-shortcuts-section-BpvVEw_Y.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-Bcaf9_bA.js +1 -0
- package/dist/web/assets/markdown-renderer-DGJp8HC3.js +3 -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-Bol3XAO1.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-BXt54VIn.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-DKJUA3Kq.js} +1 -1
- package/dist/web/assets/problems-panel-CFcoUx8h.js +1 -0
- package/dist/web/assets/{process-table-BRMS1MrX.js → process-table-EKr2dqG6.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-B7qRVl8w.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-0WDYsyCA.js +1 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-COYDg16H.js +2 -0
- package/dist/web/assets/remote-desktop-mobile-view-2DqSuS2U.js +1 -0
- package/dist/web/assets/{remote-desktop-readiness-gate-BTzss2wX.js → remote-desktop-readiness-gate-Dm_qMJMH.js} +1 -1
- package/dist/web/assets/remote-desktop-window-content-UodhsF7W.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-bG2lYfGv.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-C06bFarv.js +2 -0
- package/dist/web/assets/settings-store-krC6bc79.js +2 -0
- package/dist/web/assets/settings-tab-34RSaASd.js +1 -0
- package/dist/web/assets/settings-window-content-Dr_2lmHH.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-BgRH0cHD.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-DoV9hnWJ.js → sqlite-viewer-DUxGslpa.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-IZuLn7Qh.js +2 -0
- package/dist/web/assets/system-monitor-tab-DiSA490Z.js +1 -0
- package/dist/web/assets/{system-monitor-window-content-qXLcicFM.js → system-monitor-window-content-CEaFISgj.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-7fPZNEni.js → tab-host-window-content-D0TN8Wrq.js} +1 -1
- package/dist/web/assets/{tab-store-DGJGnbwv.js → tab-store-OJa1GPCq.js} +1 -1
- package/dist/web/assets/team-member-sheet-DDPNcCjd.js +2 -0
- package/dist/web/assets/{team-member-window-content-BgvJFypM.js → team-member-window-content-DCiW_1-E.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-4ypP0NDY.js} +3 -3
- package/dist/web/assets/{tex-D_StFXy_.js → tex-BcssM-QA.js} +1 -1
- package/dist/web/assets/tool-cards-Dd6E7Q3k.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-vrfFHuaR.js} +1 -1
- package/dist/web/assets/{use-lsp-CV0vzILO.js → use-lsp-CsuZu49X.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-DBqbWjWX.js → use-monaco-theme-BIVYJBIn.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-BMbufhX7.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-Byj8P_jA.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/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 +134 -1
- package/packages/ext-git-graph/src/webview-html.ts +277 -27
- 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/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/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/git/git-status-panel.tsx +25 -0
- 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/theme-picker.tsx +2 -1
- package/src/web/components/ui/tooltip.tsx +15 -2
- package/src/web/hooks/use-delayed-flag.ts +35 -0
- package/src/web/lib/branch-review-state.ts +175 -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 +11 -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/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-CvL1pWeK.js +0 -3
- 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/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,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
|
+
}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* `RemoteDesktopWarningGate`, so the connection hook (and its session nonce) only runs once the
|
|
6
6
|
* user has read the warning.
|
|
7
7
|
*/
|
|
8
|
-
import { useCallback, useRef } from "react";
|
|
9
|
-
import { RotateCw, MonitorX
|
|
8
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
9
|
+
import { RotateCw, MonitorX } from "@/lib/icons";
|
|
10
10
|
import type { WindowContentProps } from "@/components/floating-window/window-content-registry";
|
|
11
11
|
import { useWindowStore } from "@/components/floating-window/window-store";
|
|
12
12
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
@@ -17,6 +17,13 @@ import { RemoteDesktopStatsOverlay } from "./remote-desktop-stats-overlay";
|
|
|
17
17
|
import { RemoteDesktopWarningGate } from "./remote-desktop-warning-gate";
|
|
18
18
|
import { RemoteDesktopReadinessGate } from "./remote-desktop-readiness-gate";
|
|
19
19
|
import { useRemoteDesktopDisplayChoice } from "./use-remote-desktop-display-choice";
|
|
20
|
+
import { RemoteDesktopClipboardNotice } from "./remote-desktop-clipboard-notice";
|
|
21
|
+
import { useRemoteDesktopReadiness } from "./use-remote-desktop-readiness";
|
|
22
|
+
import { RemoteDesktopToolbar } from "./remote-desktop-toolbar";
|
|
23
|
+
import { canvasCssSize } from "./remote-desktop-view-style";
|
|
24
|
+
import { useRemoteDesktopRecorder } from "./use-remote-desktop-recorder";
|
|
25
|
+
import { saveCanvasScreenshot } from "./remote-desktop-recording";
|
|
26
|
+
import { RemoteDesktopCustomQualityDialog } from "./remote-desktop-custom-quality-dialog";
|
|
20
27
|
|
|
21
28
|
export default function RemoteDesktopWindowContent({ id }: WindowContentProps) {
|
|
22
29
|
const closeWindow = useWindowStore((s) => s.close);
|
|
@@ -33,12 +40,53 @@ export default function RemoteDesktopWindowContent({ id }: WindowContentProps) {
|
|
|
33
40
|
function RemoteDesktopViewer() {
|
|
34
41
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
35
42
|
const display = useRemoteDesktopDisplayChoice();
|
|
36
|
-
const {
|
|
37
|
-
|
|
43
|
+
const {
|
|
44
|
+
connState, errorMessage, decoderStatus, decoderErrorMessage, sendMessage, reconnect,
|
|
45
|
+
getFrameCount, getTotalBytes, frameSize, quality, setQuality, setCustomQuality,
|
|
46
|
+
showCursor, setShowCursor,
|
|
47
|
+
codec, setCodec, audioOn, setAudioOn, getAudioTracks,
|
|
48
|
+
privacyOn, privacyError, setPrivacyOn,
|
|
49
|
+
hostModeId, hostOriginalModeId, resolutionError, setHostMode,
|
|
50
|
+
pendingHostClipboard, clearHostClipboard, sendClipboard, requestHostClipboard,
|
|
51
|
+
} = useRemoteDesktopConnection(canvasRef, { displayId: display.displayId });
|
|
38
52
|
const statsVisible = useSettingsStore((s) => s.remoteDesktopStatsVisible);
|
|
39
53
|
const toggleStats = useSettingsStore((s) => s.toggleRemoteDesktopStatsVisible);
|
|
54
|
+
const viewStyle = useSettingsStore((s) => s.remoteDesktopViewStyle);
|
|
55
|
+
const customScale = useSettingsStore((s) => s.remoteDesktopCustomScale);
|
|
56
|
+
const setViewStyle = useSettingsStore((s) => s.setRemoteDesktopViewStyle);
|
|
57
|
+
const setCustomScale = useSettingsStore((s) => s.setRemoteDesktopCustomScale);
|
|
58
|
+
const clipboardSync = useSettingsStore((s) => s.remoteDesktopClipboardSync);
|
|
59
|
+
const qualityChoice = useSettingsStore((s) => s.remoteDesktopQuality);
|
|
60
|
+
const customPercent = useSettingsStore((s) => s.remoteDesktopCustomQualityPercent);
|
|
61
|
+
const customFps = useSettingsStore((s) => s.remoteDesktopCustomFps);
|
|
62
|
+
const customMore = useSettingsStore((s) => s.remoteDesktopCustomQualityMore);
|
|
63
|
+
const setCustomMore = useSettingsStore((s) => s.setRemoteDesktopCustomQualityMore);
|
|
64
|
+
const [customQualityOpen, setCustomQualityOpen] = useState(false);
|
|
65
|
+
const setClipboardSync = useSettingsStore((s) => s.setRemoteDesktopClipboardSync);
|
|
66
|
+
// One fetch, no polling: this is read only to explain a clipboard that cannot work. The
|
|
67
|
+
// readiness *gate* above already cleared, so nothing here is allowed to block the viewer.
|
|
68
|
+
const { caps } = useRemoteDesktopReadiness(false);
|
|
69
|
+
const [pasteAttempted, setPasteAttempted] = useState(false);
|
|
40
70
|
|
|
41
|
-
useRemoteInputCapture
|
|
71
|
+
// Stable object: `useRemoteInputCapture` lists it as an effect dependency, so a fresh literal
|
|
72
|
+
// per render would re-bind every canvas listener on every render.
|
|
73
|
+
//
|
|
74
|
+
// Undefined with sync off, which is the whole implementation of the toggle: the hook then
|
|
75
|
+
// stops exempting Ctrl+V from `preventDefault` and forwards it as an ordinary keystroke, so
|
|
76
|
+
// the host pastes its *own* clipboard and no text crosses the connection either way.
|
|
77
|
+
const clipboardHooks = useMemo(() => (clipboardSync ? {
|
|
78
|
+
onPasteText: (text: string, shift: boolean) => { setPasteAttempted(true); sendClipboard(text, true, shift); },
|
|
79
|
+
onCopyCombo: requestHostClipboard,
|
|
80
|
+
} : undefined), [clipboardSync, sendClipboard, requestHostClipboard]);
|
|
81
|
+
|
|
82
|
+
useRemoteInputCapture(canvasRef, sendMessage, connState === "streaming", clipboardHooks);
|
|
83
|
+
|
|
84
|
+
// The recorder muxes the session audio in when it is playing, so one file carries both.
|
|
85
|
+
const recorder = useRemoteDesktopRecorder(canvasRef, getAudioTracks);
|
|
86
|
+
|
|
87
|
+
// null in `adaptive` (and before the first frame): the canvas keeps its `max-*-full` classes
|
|
88
|
+
// and the browser fits it, which is the path this viewer has always been on.
|
|
89
|
+
const cssSize = canvasCssSize(viewStyle, customScale, frameSize);
|
|
42
90
|
|
|
43
91
|
const overlayMessage = decoderStatus === "unsupported"
|
|
44
92
|
? decoderErrorMessage
|
|
@@ -56,37 +104,80 @@ function RemoteDesktopViewer() {
|
|
|
56
104
|
data-testid="remote-desktop-window"
|
|
57
105
|
data-conn-state={connState}
|
|
58
106
|
>
|
|
59
|
-
|
|
107
|
+
{/* The scroll container is a child rather than this root, because the overlays below are
|
|
108
|
+
absolutely positioned against the root — inside a scroller they would scroll away with
|
|
109
|
+
the picture. `m-auto` on the canvas rather than `justify-center` on the box: centring
|
|
110
|
+
an oversized child with `justify-content` puts half the overflow *before* the scroll
|
|
111
|
+
origin, where no scrollbar can reach it (the top-left of a 1:1 3440×1440 capture in a
|
|
112
|
+
small window). Auto margins collapse to 0 when the child overflows, so both work. */}
|
|
113
|
+
<div className={cn("flex h-full w-full", cssSize ? "overflow-auto" : "overflow-hidden")}>
|
|
114
|
+
<canvas
|
|
115
|
+
ref={canvasRef}
|
|
116
|
+
data-testid="remote-desktop-canvas"
|
|
117
|
+
data-view-style={viewStyle}
|
|
118
|
+
className={cn("m-auto outline-none", !cssSize && "max-h-full max-w-full")}
|
|
119
|
+
style={cssSize ? { width: cssSize.width, height: cssSize.height } : undefined}
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
60
122
|
<RemoteDesktopStatsOverlay canvasRef={canvasRef} getFrameCount={getFrameCount} getTotalBytes={getTotalBytes} />
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
123
|
+
<RemoteDesktopToolbar
|
|
124
|
+
displays={display.displays}
|
|
125
|
+
currentDisplayId={display.current?.id}
|
|
126
|
+
onSelectDisplay={display.select}
|
|
127
|
+
quality={quality}
|
|
128
|
+
qualityChoice={qualityChoice}
|
|
129
|
+
onSetQuality={setQuality}
|
|
130
|
+
onOpenCustomQuality={() => setCustomQualityOpen(true)}
|
|
131
|
+
codecs={caps?.encoders ?? []}
|
|
132
|
+
codec={codec}
|
|
133
|
+
onSetCodec={setCodec}
|
|
134
|
+
hostModes={caps?.resolutions?.modes ?? []}
|
|
135
|
+
hostModeId={hostModeId}
|
|
136
|
+
hostOriginalModeId={hostOriginalModeId}
|
|
137
|
+
onSetHostMode={setHostMode}
|
|
138
|
+
canResizeHost={caps?.inputReady ?? false}
|
|
139
|
+
resolutionError={resolutionError}
|
|
140
|
+
viewStyle={viewStyle}
|
|
141
|
+
customScale={customScale}
|
|
142
|
+
onSetViewStyle={setViewStyle}
|
|
143
|
+
onSetCustomScale={setCustomScale}
|
|
144
|
+
showCursor={showCursor}
|
|
145
|
+
onSetShowCursor={setShowCursor}
|
|
146
|
+
audioOn={audioOn}
|
|
147
|
+
audioReason={caps && !caps.audio.available ? caps.audio.reason : null}
|
|
148
|
+
onSetAudioOn={setAudioOn}
|
|
149
|
+
privacyOn={privacyOn}
|
|
150
|
+
privacyReason={privacyError ?? (caps && !caps.privacy.available ? caps.privacy.reason : null)}
|
|
151
|
+
privacyCanBlank={caps?.privacy?.canBlank ?? false}
|
|
152
|
+
onSetPrivacyOn={setPrivacyOn}
|
|
153
|
+
recording={recorder.recording}
|
|
154
|
+
recordingElapsedSec={recorder.elapsedSec}
|
|
155
|
+
recordingSupported={recorder.supported}
|
|
156
|
+
onToggleRecording={recorder.toggle}
|
|
157
|
+
onScreenshot={() => void saveCanvasScreenshot(canvasRef.current)}
|
|
158
|
+
clipboardEnabled={clipboardSync}
|
|
159
|
+
onSetClipboardEnabled={setClipboardSync}
|
|
160
|
+
statsVisible={statsVisible}
|
|
161
|
+
onToggleStats={toggleStats}
|
|
162
|
+
/>
|
|
163
|
+
{/* Only after a real paste attempt: a host with no clipboard tool is not worth a warning
|
|
164
|
+
until the user actually reaches for it. */}
|
|
165
|
+
<RemoteDesktopClipboardNotice
|
|
166
|
+
pendingText={pendingHostClipboard}
|
|
167
|
+
onDismiss={clearHostClipboard}
|
|
168
|
+
missingToolAction={pasteAttempted && caps && !caps.clipboard.available ? caps.clipboard.action : null}
|
|
169
|
+
onDismissMissingTool={() => setPasteAttempted(false)}
|
|
170
|
+
/>
|
|
171
|
+
<RemoteDesktopCustomQualityDialog
|
|
172
|
+
open={customQualityOpen}
|
|
173
|
+
onOpenChange={setCustomQualityOpen}
|
|
174
|
+
percent={customPercent}
|
|
175
|
+
fps={customFps}
|
|
176
|
+
more={customMore}
|
|
177
|
+
onSetMore={setCustomMore}
|
|
178
|
+
onCommit={setCustomQuality}
|
|
179
|
+
frameSize={frameSize}
|
|
180
|
+
/>
|
|
90
181
|
{overlayMessage && (
|
|
91
182
|
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/70 text-sm text-white">
|
|
92
183
|
<MonitorX className="size-6 opacity-70" />
|
|
@@ -46,6 +46,13 @@ export interface UseH264CanvasDecoderResult {
|
|
|
46
46
|
* last `reset()`. Ref-backed, not React state — read it from a poll (e.g. the stats overlay),
|
|
47
47
|
* not a render dependency, so every decoded frame doesn't force a re-render. */
|
|
48
48
|
getFrameCount: () => number;
|
|
49
|
+
/** Pixel size of the decoded picture — what `canvas.width`/`height` were last set to, 0×0
|
|
50
|
+
* before the first frame. Real state rather than a ref (unlike the counter above) because
|
|
51
|
+
* the `original`/`custom` scale modes size the *element* from it, and it changes once per
|
|
52
|
+
* rung rather than once per frame. Deliberately kept across a `reset()`: a rung switch
|
|
53
|
+
* respawns the host encoder, and blanking this mid-switch would collapse the canvas to
|
|
54
|
+
* nothing for ~400ms before the new resolution arrives. */
|
|
55
|
+
frameSize: { width: number; height: number };
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
export function useH264CanvasDecoder(
|
|
@@ -53,6 +60,7 @@ export function useH264CanvasDecoder(
|
|
|
53
60
|
): UseH264CanvasDecoderResult {
|
|
54
61
|
const [status, setStatus] = useState<DecoderStatus>("idle");
|
|
55
62
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
63
|
+
const [frameSize, setFrameSize] = useState({ width: 0, height: 0 });
|
|
56
64
|
const decoderRef = useRef<VideoDecoder | null>(null);
|
|
57
65
|
const ctxRef = useRef<CanvasRenderingContext2D | null>(null);
|
|
58
66
|
const decodedAnyKeyRef = useRef(false);
|
|
@@ -97,6 +105,8 @@ export function useH264CanvasDecoder(
|
|
|
97
105
|
if (canvas.width !== frame.displayWidth || canvas.height !== frame.displayHeight) {
|
|
98
106
|
canvas.width = frame.displayWidth;
|
|
99
107
|
canvas.height = frame.displayHeight;
|
|
108
|
+
// Inside the size guard, so this is one setState per resolution change, not per frame.
|
|
109
|
+
setFrameSize({ width: frame.displayWidth, height: frame.displayHeight });
|
|
100
110
|
}
|
|
101
111
|
if (!ctxRef.current) ctxRef.current = canvas.getContext("2d");
|
|
102
112
|
ctxRef.current?.drawImage(frame, 0, 0, canvas.width, canvas.height);
|
|
@@ -171,5 +181,6 @@ export function useH264CanvasDecoder(
|
|
|
171
181
|
decodeAccessUnit,
|
|
172
182
|
reset,
|
|
173
183
|
getFrameCount,
|
|
184
|
+
frameSize,
|
|
174
185
|
};
|
|
175
186
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plays the host's audio: Opus packets → `AudioDecoder` → scheduled into one `AudioContext`.
|
|
3
|
+
*
|
|
4
|
+
* The sibling of `use-h264-canvas-decoder.ts`, and it needs no new platform support: every
|
|
5
|
+
* browser that has `VideoDecoder` (which the viewer already hard-requires) has `AudioDecoder`.
|
|
6
|
+
*
|
|
7
|
+
* Scheduling is one `AudioBufferSourceNode` per decoded 20 ms frame against a running cursor,
|
|
8
|
+
* rather than an `AudioWorklet` ring buffer. That is 50 nodes a second, which the audio thread
|
|
9
|
+
* does not notice, and it avoids a separate worklet module — the thing a worklet would buy is
|
|
10
|
+
* sample-accurate underrun handling, and this player wants the opposite: on an underrun it
|
|
11
|
+
* should jump back to "now" and carry on, because a remote desktop's audio has to stay in sync
|
|
12
|
+
* with the picture rather than play every sample late.
|
|
13
|
+
*
|
|
14
|
+
* `TARGET_LEAD_MS` is that resync threshold in both directions. Too small and every network
|
|
15
|
+
* hiccup is a gap; too large and the audio lags the video visibly. 120 ms sits above the ~40 ms
|
|
16
|
+
* the host's Ogg pages arrive in with room for one missed page.
|
|
17
|
+
*/
|
|
18
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
19
|
+
|
|
20
|
+
/** How far ahead of the clock the next frame is scheduled. Also the resync window: a cursor
|
|
21
|
+
* further behind than this has fallen off and is snapped forward. */
|
|
22
|
+
const TARGET_LEAD_MS = 120;
|
|
23
|
+
/** Beyond this the client is scheduling audio it will not hear for a noticeable time, which
|
|
24
|
+
* means the host is producing faster than realtime (a resumed suspended sink) — drop to the
|
|
25
|
+
* target rather than building a queue that can only be heard late. */
|
|
26
|
+
const MAX_LEAD_MS = 400;
|
|
27
|
+
|
|
28
|
+
export type AudioPlayerStatus = "idle" | "playing" | "unsupported";
|
|
29
|
+
|
|
30
|
+
export interface UseOpusAudioPlayerResult {
|
|
31
|
+
status: AudioPlayerStatus;
|
|
32
|
+
/** Configure for a stream; safe to call again when the host restarts its encoder. */
|
|
33
|
+
configure: (channels: number, sampleRate: number) => Promise<void>;
|
|
34
|
+
/** Feed one Opus packet. Ignored until `configure` has run. */
|
|
35
|
+
decodePacket: (packet: Uint8Array) => void;
|
|
36
|
+
/** Tear the decoder and the context down. */
|
|
37
|
+
reset: () => void;
|
|
38
|
+
/** Tracks for the recorder to mux in, empty while idle. */
|
|
39
|
+
getTracks: () => MediaStreamTrack[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function useOpusAudioPlayer(): UseOpusAudioPlayerResult {
|
|
43
|
+
const ctxRef = useRef<AudioContext | null>(null);
|
|
44
|
+
const decoderRef = useRef<AudioDecoder | null>(null);
|
|
45
|
+
const destRef = useRef<MediaStreamAudioDestinationNode | null>(null);
|
|
46
|
+
/** When the next frame should start, in the context's own clock. */
|
|
47
|
+
const cursorRef = useRef(0);
|
|
48
|
+
const timestampRef = useRef(0);
|
|
49
|
+
const [status, setStatus] = useState<AudioPlayerStatus>("idle");
|
|
50
|
+
|
|
51
|
+
const reset = useCallback(() => {
|
|
52
|
+
try { decoderRef.current?.close(); } catch { /* already closed */ }
|
|
53
|
+
decoderRef.current = null;
|
|
54
|
+
destRef.current = null;
|
|
55
|
+
// The context is closed too, not just disconnected: a suspended AudioContext still holds
|
|
56
|
+
// an audio device open, which on some hosts keeps the sink awake for the whole session.
|
|
57
|
+
const ctx = ctxRef.current;
|
|
58
|
+
ctxRef.current = null;
|
|
59
|
+
if (ctx) void ctx.close().catch(() => {});
|
|
60
|
+
cursorRef.current = 0;
|
|
61
|
+
timestampRef.current = 0;
|
|
62
|
+
setStatus((s) => (s === "unsupported" ? s : "idle"));
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const play = useCallback((data: AudioData) => {
|
|
66
|
+
const ctx = ctxRef.current;
|
|
67
|
+
if (!ctx) { data.close(); return; }
|
|
68
|
+
const frames = data.numberOfFrames;
|
|
69
|
+
const channels = data.numberOfChannels;
|
|
70
|
+
const buffer = ctx.createBuffer(channels, frames, data.sampleRate);
|
|
71
|
+
// `copyTo` per plane: the decoder hands back planar f32, which is exactly what an
|
|
72
|
+
// AudioBuffer channel wants, so there is no interleaving to undo.
|
|
73
|
+
for (let ch = 0; ch < channels; ch++) {
|
|
74
|
+
const plane = new Float32Array(frames);
|
|
75
|
+
data.copyTo(plane, { planeIndex: ch, format: "f32-planar" });
|
|
76
|
+
buffer.copyToChannel(plane, ch);
|
|
77
|
+
}
|
|
78
|
+
data.close();
|
|
79
|
+
|
|
80
|
+
const now = ctx.currentTime;
|
|
81
|
+
const lead = (cursorRef.current - now) * 1000;
|
|
82
|
+
// Underrun (the cursor is in the past) or a runaway queue: snap back to the target lead.
|
|
83
|
+
// Without this the first case plays everything late forever and the second drifts further
|
|
84
|
+
// behind the picture with every packet.
|
|
85
|
+
if (lead < 0 || lead > MAX_LEAD_MS) cursorRef.current = now + TARGET_LEAD_MS / 1000;
|
|
86
|
+
|
|
87
|
+
const source = ctx.createBufferSource();
|
|
88
|
+
source.buffer = buffer;
|
|
89
|
+
source.connect(ctx.destination);
|
|
90
|
+
if (destRef.current) source.connect(destRef.current);
|
|
91
|
+
source.start(cursorRef.current);
|
|
92
|
+
cursorRef.current += buffer.duration;
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
const configure = useCallback(async (channels: number, sampleRate: number) => {
|
|
96
|
+
if (typeof AudioDecoder === "undefined" || typeof AudioContext === "undefined") {
|
|
97
|
+
setStatus("unsupported");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
reset();
|
|
101
|
+
const ctx = new AudioContext({ sampleRate, latencyHint: "interactive" });
|
|
102
|
+
ctxRef.current = ctx;
|
|
103
|
+
// Autoplay policy: a context created without a user gesture starts suspended, and
|
|
104
|
+
// `resume()` only succeeds once the page has one. The toggle that turns audio on *is* a
|
|
105
|
+
// click, so by the time this runs there has been one.
|
|
106
|
+
if (ctx.state === "suspended") await ctx.resume().catch(() => {});
|
|
107
|
+
destRef.current = ctx.createMediaStreamDestination();
|
|
108
|
+
|
|
109
|
+
const decoder = new AudioDecoder({
|
|
110
|
+
output: play,
|
|
111
|
+
// A decode error is one bad packet, not a dead stream: Opus frames are independent, so
|
|
112
|
+
// the next one decodes fine. Resetting instead of closing keeps the stream alive.
|
|
113
|
+
error: () => { try { decoderRef.current?.reset(); } catch { /* gone */ } },
|
|
114
|
+
});
|
|
115
|
+
try {
|
|
116
|
+
decoder.configure({ codec: "opus", sampleRate, numberOfChannels: channels });
|
|
117
|
+
} catch {
|
|
118
|
+
setStatus("unsupported");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
decoderRef.current = decoder;
|
|
122
|
+
cursorRef.current = 0;
|
|
123
|
+
timestampRef.current = 0;
|
|
124
|
+
setStatus("playing");
|
|
125
|
+
}, [play, reset]);
|
|
126
|
+
|
|
127
|
+
const decodePacket = useCallback((packet: Uint8Array) => {
|
|
128
|
+
const decoder = decoderRef.current;
|
|
129
|
+
if (!decoder || decoder.state !== "configured") return;
|
|
130
|
+
// Timestamps have to advance monotonically or the decoder rejects the chunk, and the host
|
|
131
|
+
// sends no per-packet pts — so they are counted here at the one frame duration the encoder
|
|
132
|
+
// is pinned to (`-frame_duration 20`).
|
|
133
|
+
const timestamp = timestampRef.current;
|
|
134
|
+
timestampRef.current += 20_000;
|
|
135
|
+
try {
|
|
136
|
+
decoder.decode(new EncodedAudioChunk({ type: "key", timestamp, data: packet }));
|
|
137
|
+
} catch { /* a chunk the decoder refused; the next one is independent */ }
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
const getTracks = useCallback(
|
|
141
|
+
() => (destRef.current ? destRef.current.stream.getAudioTracks() : []),
|
|
142
|
+
[],
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
useEffect(() => () => reset(), [reset]);
|
|
146
|
+
|
|
147
|
+
return { status, configure, decodePacket, reset, getTracks };
|
|
148
|
+
}
|