@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,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One shared Xlib connection for the X11 capture/input paths, over `bun:ffi` — no helper
|
|
3
|
+
* binary, matching `remote-desktop-input-win32.ts` (user32) and `-darwin.ts` (CoreGraphics).
|
|
4
|
+
*
|
|
5
|
+
* Two things here exist to keep a desktop problem from becoming a PPM problem:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Xlib's default I/O error handler calls `exit()`.** If the X server goes away — a
|
|
8
|
+
* logout, a GPU driver reset, `systemctl restart sddm` — libX11 prints `XIO: fatal IO
|
|
9
|
+
* error` and terminates the *calling process*, which here is the whole PPM server: every
|
|
10
|
+
* project, terminal and chat session dies because someone logged out of the desktop. Both
|
|
11
|
+
* handlers are therefore replaced (`XSetIOErrorHandler` returns instead of falling through,
|
|
12
|
+
* and libX11 ≥ 1.7's `XSetIOErrorExitHandler` cancels the exit that otherwise happens even
|
|
13
|
+
* when the first one returns). Protocol errors (`BadWindow` from a monitor unplugged
|
|
14
|
+
* mid-enumeration) go to a no-op `XSetErrorHandler` rather than the default's stderr spew.
|
|
15
|
+
* The callbacks are held in module scope: a GC'd `JSCallback` would leave Xlib calling a
|
|
16
|
+
* freed trampoline, which is a segfault rather than the crash it was installed to prevent.
|
|
17
|
+
*
|
|
18
|
+
* On libX11 **1.6** that protection is only half there, and the code says so rather than
|
|
19
|
+
* assuming otherwise: the third handler does not exist before 1.7 (2021), so an I/O error
|
|
20
|
+
* still reaches the `exit()` the second handler returns into. It is loaded from its own
|
|
21
|
+
* dlopen table for that reason — `bun:ffi`'s `dlopen` throws on the *first* missing symbol
|
|
22
|
+
* and hands back none of the others, so one optional function in the main table would take
|
|
23
|
+
* capture, input, privacy mode and monitor enumeration down together on every host older
|
|
24
|
+
* than that (Ubuntu 20.04 ships 1.6.9).
|
|
25
|
+
*
|
|
26
|
+
* 2. **A connection that took an I/O error is not reusable.** There is no safe call left on it,
|
|
27
|
+
* not even `XCloseDisplay`, so it is dropped un-closed (a few KB, once per X server
|
|
28
|
+
* lifetime) and the next caller opens a fresh one.
|
|
29
|
+
*
|
|
30
|
+
* libXtst and libXrandr are optional: a host can have Xlib but no XTEST extension (input goes
|
|
31
|
+
* view-only) or no libXrandr (one whole-screen display instead of per-monitor). Neither is a
|
|
32
|
+
* reason to fail capture, so both load into `null` rather than throwing.
|
|
33
|
+
*/
|
|
34
|
+
import { resetLinuxSession, type LinuxSession } from "./remote-desktop-linux-session.ts";
|
|
35
|
+
|
|
36
|
+
type Ffi = typeof import("bun:ffi");
|
|
37
|
+
/** Xlib pointer/handle as bun:ffi hands it back. */
|
|
38
|
+
type Ptr = number | bigint;
|
|
39
|
+
|
|
40
|
+
const X11_SYMBOLS = {
|
|
41
|
+
XOpenDisplay: { args: ["cstring"], returns: "ptr" },
|
|
42
|
+
XDefaultRootWindow: { args: ["ptr"], returns: "u64" },
|
|
43
|
+
XDefaultScreen: { args: ["ptr"], returns: "i32" },
|
|
44
|
+
XDisplayWidth: { args: ["ptr", "i32"], returns: "i32" },
|
|
45
|
+
XDisplayHeight: { args: ["ptr", "i32"], returns: "i32" },
|
|
46
|
+
XFlush: { args: ["ptr"], returns: "i32" },
|
|
47
|
+
XSync: { args: ["ptr", "i32"], returns: "i32" },
|
|
48
|
+
XGetAtomName: { args: ["ptr", "u64"], returns: "ptr" },
|
|
49
|
+
XFree: { args: ["ptr"], returns: "i32" },
|
|
50
|
+
XStringToKeysym: { args: ["cstring"], returns: "u64" },
|
|
51
|
+
XKeysymToKeycode: { args: ["ptr", "u64"], returns: "u8" },
|
|
52
|
+
XDisplayKeycodes: { args: ["ptr", "ptr", "ptr"], returns: "i32" },
|
|
53
|
+
XGetKeyboardMapping: { args: ["ptr", "u8", "i32", "ptr"], returns: "ptr" },
|
|
54
|
+
XChangeKeyboardMapping: { args: ["ptr", "i32", "i32", "ptr", "i32"], returns: "i32" },
|
|
55
|
+
XQueryPointer: { args: ["ptr", "u64", "ptr", "ptr", "ptr", "ptr", "ptr", "ptr", "ptr"], returns: "i32" },
|
|
56
|
+
// Privacy mode: an X grab with `owner_events = False` routes every local key and button to
|
|
57
|
+
// the grabbing client, which simply drops them. Paired with `XTestGrabControl` below, which
|
|
58
|
+
// is what keeps the *remote* session's injected events working through that grab.
|
|
59
|
+
XGrabKeyboard: { args: ["ptr", "u64", "i32", "i32", "i32", "u64"], returns: "i32" },
|
|
60
|
+
XGrabPointer: { args: ["ptr", "u64", "i32", "u32", "i32", "i32", "u64", "u64", "u64"], returns: "i32" },
|
|
61
|
+
XUngrabKeyboard: { args: ["ptr", "u64"], returns: "i32" },
|
|
62
|
+
XUngrabPointer: { args: ["ptr", "u64"], returns: "i32" },
|
|
63
|
+
XSetErrorHandler: { args: ["ptr"], returns: "ptr" },
|
|
64
|
+
XSetIOErrorHandler: { args: ["ptr"], returns: "ptr" },
|
|
65
|
+
} as const;
|
|
66
|
+
|
|
67
|
+
/** libX11 ≥ 1.7 only, which is why it is a table of its own rather than one more line above.
|
|
68
|
+
* Exported so a test can assert the split still holds: moving this key back into
|
|
69
|
+
* `X11_SYMBOLS` is a one-line change that costs every pre-1.7 host its input. */
|
|
70
|
+
export const X11_EXIT_SYMBOLS = {
|
|
71
|
+
XSetIOErrorExitHandler: { args: ["ptr", "ptr", "ptr"], returns: "void" },
|
|
72
|
+
} as const;
|
|
73
|
+
|
|
74
|
+
const XTST_SYMBOLS = {
|
|
75
|
+
XTestQueryExtension: { args: ["ptr", "ptr", "ptr", "ptr", "ptr"], returns: "i32" },
|
|
76
|
+
XTestFakeMotionEvent: { args: ["ptr", "i32", "i32", "i32", "u64"], returns: "i32" },
|
|
77
|
+
XTestFakeButtonEvent: { args: ["ptr", "u32", "i32", "u64"], returns: "i32" },
|
|
78
|
+
XTestFakeKeyEvent: { args: ["ptr", "u32", "i32", "u64"], returns: "i32" },
|
|
79
|
+
/** Makes this client's XTEST events *impervious to grabs* — the one call that lets privacy
|
|
80
|
+
* mode hold a keyboard/pointer grab without also blocking the remote session's own input. */
|
|
81
|
+
XTestGrabControl: { args: ["ptr", "i32"], returns: "i32" },
|
|
82
|
+
} as const;
|
|
83
|
+
|
|
84
|
+
const XRANDR_SYMBOLS = {
|
|
85
|
+
XRRGetMonitors: { args: ["ptr", "u64", "i32", "ptr"], returns: "ptr" },
|
|
86
|
+
XRRFreeMonitors: { args: ["ptr"], returns: "void" },
|
|
87
|
+
// Resolution switching. `…Current` rather than `XRRGetScreenResources`: the latter re-polls
|
|
88
|
+
// every output over DDC, which takes tens of milliseconds and can wake a sleeping monitor.
|
|
89
|
+
XRRGetScreenResourcesCurrent: { args: ["ptr", "u64"], returns: "ptr" },
|
|
90
|
+
XRRFreeScreenResources: { args: ["ptr"], returns: "void" },
|
|
91
|
+
XRRGetOutputInfo: { args: ["ptr", "ptr", "u64"], returns: "ptr" },
|
|
92
|
+
XRRFreeOutputInfo: { args: ["ptr"], returns: "void" },
|
|
93
|
+
XRRGetCrtcInfo: { args: ["ptr", "ptr", "u64"], returns: "ptr" },
|
|
94
|
+
XRRFreeCrtcInfo: { args: ["ptr"], returns: "void" },
|
|
95
|
+
XRRSetCrtcConfig: {
|
|
96
|
+
args: ["ptr", "ptr", "u64", "u64", "i32", "i32", "u64", "u16", "ptr", "i32"],
|
|
97
|
+
returns: "i32",
|
|
98
|
+
},
|
|
99
|
+
XRRSetScreenSize: { args: ["ptr", "u64", "i32", "i32", "i32", "i32"], returns: "void" },
|
|
100
|
+
} as const;
|
|
101
|
+
|
|
102
|
+
/** DPMS lives in libXext, not libX11. Used to turn the host's monitor off for privacy mode
|
|
103
|
+
* while leaving the framebuffer — and therefore the capture — alone. */
|
|
104
|
+
const DPMS_SYMBOLS = {
|
|
105
|
+
DPMSQueryExtension: { args: ["ptr", "ptr", "ptr"], returns: "i32" },
|
|
106
|
+
DPMSCapable: { args: ["ptr"], returns: "i32" },
|
|
107
|
+
DPMSEnable: { args: ["ptr"], returns: "i32" },
|
|
108
|
+
DPMSForceLevel: { args: ["ptr", "u16"], returns: "i32" },
|
|
109
|
+
DPMSInfo: { args: ["ptr", "ptr", "ptr"], returns: "i32" },
|
|
110
|
+
} as const;
|
|
111
|
+
|
|
112
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- dlopen's symbol types are per-call */
|
|
113
|
+
type X11Lib = { [K in keyof typeof X11_SYMBOLS]: any };
|
|
114
|
+
type X11ExitLib = { [K in keyof typeof X11_EXIT_SYMBOLS]: any };
|
|
115
|
+
type XtstLib = { [K in keyof typeof XTST_SYMBOLS]: any };
|
|
116
|
+
type XrandrLib = { [K in keyof typeof XRANDR_SYMBOLS]: any };
|
|
117
|
+
type DpmsLib = { [K in keyof typeof DPMS_SYMBOLS]: any };
|
|
118
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
119
|
+
|
|
120
|
+
export interface X11Connection {
|
|
121
|
+
ffi: Ffi;
|
|
122
|
+
x11: X11Lib;
|
|
123
|
+
/** null when the host has no libXtst — capture works, input does not. */
|
|
124
|
+
xtst: XtstLib | null;
|
|
125
|
+
/** null when the host has no libXrandr — one whole-screen display instead of per-monitor. */
|
|
126
|
+
xrandr: XrandrLib | null;
|
|
127
|
+
/** null when the host has no libXext, or its X server has no DPMS extension — privacy mode
|
|
128
|
+
* then blocks local input without blanking the monitor. */
|
|
129
|
+
dpms: DpmsLib | null;
|
|
130
|
+
dpy: Ptr;
|
|
131
|
+
root: bigint;
|
|
132
|
+
screen: number;
|
|
133
|
+
/** XTEST is present AND usable on this connection. */
|
|
134
|
+
hasXTest: boolean;
|
|
135
|
+
/** Which display this was opened against, so a later session change cannot reuse it. */
|
|
136
|
+
display: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let ffiModule: Ffi | null = null;
|
|
140
|
+
let connection: X11Connection | null = null;
|
|
141
|
+
/** Kept forever: Xlib holds raw pointers to these trampolines for the process lifetime. Named
|
|
142
|
+
* rather than an array, because the one read outside `installErrorHandlers` used to be
|
|
143
|
+
* `liveCallbacks[2]` — a fourth handler would have handed the wrong trampoline to Xlib, with
|
|
144
|
+
* a wrong signature, and nothing to say so until it was called. */
|
|
145
|
+
let errorCallbacks: { onError: Trampoline; onIoError: Trampoline; onIoExit: Trampoline } | null = null;
|
|
146
|
+
let connectionLost = false;
|
|
147
|
+
|
|
148
|
+
/** One `JSCallback`, whose `.ptr` is the address Xlib keeps for the process lifetime. */
|
|
149
|
+
type Trampoline = InstanceType<Ffi["JSCallback"]>;
|
|
150
|
+
|
|
151
|
+
function tryDlopen<T>(ffi: Ffi, names: string[], symbols: object): T | null {
|
|
152
|
+
for (const name of names) {
|
|
153
|
+
try { return ffi.dlopen(name, symbols as never).symbols as T; } catch { /* try the next soname */ }
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Replace the three handlers that would otherwise print to stderr or kill the process. */
|
|
159
|
+
function installErrorHandlers(ffi: Ffi, x11: X11Lib): void {
|
|
160
|
+
if (errorCallbacks) return;
|
|
161
|
+
const { JSCallback } = ffi;
|
|
162
|
+
|
|
163
|
+
// Protocol error (BadWindow/BadMatch): return 0, the same as the default handler minus stderr.
|
|
164
|
+
const onError = new JSCallback(() => 0, { args: ["ptr", "ptr"], returns: "i32" });
|
|
165
|
+
// I/O error: server gone. Mark dead and return — libX11 would otherwise fall through to exit.
|
|
166
|
+
const onIoError = new JSCallback(() => { connectionLost = true; return 0; }, { args: ["ptr"], returns: "i32" });
|
|
167
|
+
// The exit that happens anyway once the handler above returns. Cancelled by doing nothing.
|
|
168
|
+
const onIoExit = new JSCallback(() => { connectionLost = true; }, { args: ["ptr", "ptr"], returns: "void" });
|
|
169
|
+
|
|
170
|
+
x11.XSetErrorHandler(onError.ptr);
|
|
171
|
+
x11.XSetIOErrorHandler(onIoError.ptr);
|
|
172
|
+
// Written last, and this is the whole of the "installed" flag: the old boolean was set
|
|
173
|
+
// *before* the two calls above, so anything that read it in between was told the process was
|
|
174
|
+
// protected while the default handlers were still in place.
|
|
175
|
+
errorCallbacks = { onError, onIoError, onIoExit };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The shared connection for `session`, or null when Xlib is missing or the server refuses us
|
|
179
|
+
* (no `$DISPLAY` server running, an auth cookie we cannot read). */
|
|
180
|
+
export async function getX11(session: LinuxSession): Promise<X11Connection | null> {
|
|
181
|
+
if (session.kind !== "x11") return null;
|
|
182
|
+
if (connectionLost) { connection = null; connectionLost = false; }
|
|
183
|
+
// Matched on the display, not merely on there being a connection. The session is re-probed
|
|
184
|
+
// periodically now, so it can name a different display on a still-live server — a second
|
|
185
|
+
// seat, a nested Xephyr, `:0` becoming `:1`. Handing back the old `Display*` then sends
|
|
186
|
+
// every keystroke and click into the display nobody is looking at, silently and for good:
|
|
187
|
+
// nothing errors, because the old server is still there.
|
|
188
|
+
// Dropped rather than closed, the way `resetX11` and the module header already decide: Xlib's
|
|
189
|
+
// I/O error handler can `exit()` the server process, so `XCloseDisplay` is never called here.
|
|
190
|
+
if (connection && connection.display !== session.display) connection = null;
|
|
191
|
+
if (connection) return connection;
|
|
192
|
+
|
|
193
|
+
if (!ffiModule) ffiModule = await import("bun:ffi");
|
|
194
|
+
const ffi = ffiModule;
|
|
195
|
+
const x11 = tryDlopen<X11Lib>(ffi, ["libX11.so.6", "libX11.so"], X11_SYMBOLS);
|
|
196
|
+
if (!x11) return null;
|
|
197
|
+
// Same library, second `dlopen`: null here means libX11 1.6, not a missing libX11.
|
|
198
|
+
const x11Exit = tryDlopen<X11ExitLib>(ffi, ["libX11.so.6", "libX11.so"], X11_EXIT_SYMBOLS);
|
|
199
|
+
installErrorHandlers(ffi, x11);
|
|
200
|
+
|
|
201
|
+
const dpy = x11.XOpenDisplay(Buffer.from(`${session.display}\0`));
|
|
202
|
+
if (!dpy) return null;
|
|
203
|
+
|
|
204
|
+
const xtst = tryDlopen<XtstLib>(ffi, ["libXtst.so.6", "libXtst.so"], XTST_SYMBOLS);
|
|
205
|
+
const xrandr = tryDlopen<XrandrLib>(ffi, ["libXrandr.so.2", "libXrandr.so"], XRANDR_SYMBOLS);
|
|
206
|
+
const xext = tryDlopen<DpmsLib>(ffi, ["libXext.so.6", "libXext.so"], DPMS_SYMBOLS);
|
|
207
|
+
|
|
208
|
+
let hasXTest = false;
|
|
209
|
+
if (xtst) {
|
|
210
|
+
const out = new Int32Array(1);
|
|
211
|
+
hasXTest = xtst.XTestQueryExtension(dpy, ffi.ptr(out), ffi.ptr(out), ffi.ptr(out), ffi.ptr(out)) !== 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// The exit handler needs the Display, so it is installed after the connection exists — and
|
|
215
|
+
// only where the symbol resolved. The guard was on whether a *callback* existed, which is
|
|
216
|
+
// always true and says nothing about whether libX11 has the function to pass it to.
|
|
217
|
+
if (x11Exit && errorCallbacks) x11Exit.XSetIOErrorExitHandler(dpy, errorCallbacks.onIoExit.ptr, null);
|
|
218
|
+
|
|
219
|
+
// libXext being present is not enough: a server can be built without the DPMS extension, and
|
|
220
|
+
// `DPMSForceLevel` against one of those is a no-op that returns success.
|
|
221
|
+
let dpms: DpmsLib | null = null;
|
|
222
|
+
if (xext) {
|
|
223
|
+
const out = new Int32Array(2);
|
|
224
|
+
const present = xext.DPMSQueryExtension(dpy, ffi.ptr(out), ffi.ptr(out.subarray(1))) !== 0;
|
|
225
|
+
if (present && xext.DPMSCapable(dpy) !== 0) dpms = xext;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
connection = {
|
|
229
|
+
ffi, x11, xtst, xrandr, dpms, dpy,
|
|
230
|
+
root: BigInt(x11.XDefaultRootWindow(dpy)),
|
|
231
|
+
screen: x11.XDefaultScreen(dpy),
|
|
232
|
+
hasXTest,
|
|
233
|
+
display: session.display,
|
|
234
|
+
};
|
|
235
|
+
return connection;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** `bun:ffi` types every address as a branded `Pointer`, but walking a C array means doing
|
|
239
|
+
* arithmetic on a plain number. One cast, here, instead of at every struct field. */
|
|
240
|
+
export function asPointer(address: number): import("bun:ffi").Pointer {
|
|
241
|
+
return address as unknown as import("bun:ffi").Pointer;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Drop the cached connection (tests, and after a session change) and, with it, the memoized
|
|
245
|
+
* session it was opened against. */
|
|
246
|
+
export function resetX11(): void {
|
|
247
|
+
connection = null;
|
|
248
|
+
connectionLost = false;
|
|
249
|
+
resetLinuxSession();
|
|
250
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
unlinkSync, statSync,
|
|
12
12
|
} from "node:fs";
|
|
13
13
|
import { getPpmDir } from "./ppm-dir.ts";
|
|
14
|
+
import { stdioIsLogFile, consumeStdioIsLogEnv, rotateIfOversized, MAX_LOG_BYTES, STDIO_IS_LOG_ENV } from "./log-rotate.ts";
|
|
14
15
|
import { isCompiledBinary } from "./autostart-generator.ts";
|
|
15
16
|
import { cleanupStaleBinaryUpgradeArtifacts } from "./binary-upgrade-swap.ts";
|
|
16
17
|
import {
|
|
@@ -60,6 +61,7 @@ const DB_BACKUP_STALE_WARN_MS = 21_600_000; // 6h — snapshots stopped happenin
|
|
|
60
61
|
const SELF_REPLACE_TIMEOUT_MS = 30_000; // 30s to wait for new supervisor
|
|
61
62
|
const EDGE_PROBE_INTERVAL_MS = 10_000; // the public port is dark while the edge is down — check often
|
|
62
63
|
const SERVER_PORT_MIRROR_TIMEOUT_MS = 30_000; // how long to wait for the server to publish its port
|
|
64
|
+
const LOG_ROTATE_INTERVAL_MS = 60_000; // how often ppm.log is checked against its cap
|
|
63
65
|
|
|
64
66
|
const logFile = () => resolve(getPpmDir(), "ppm.log");
|
|
65
67
|
const restartingFlag = () => resolve(getPpmDir(), ".restarting");
|
|
@@ -154,12 +156,51 @@ let cloudConnected = false; // tracks whether we've initiated a cloud WS connect
|
|
|
154
156
|
let originalArgv: string[] = [];
|
|
155
157
|
|
|
156
158
|
// ─── Logging ───────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Whether this process's own stderr already lands in ppm.log.
|
|
162
|
+
*
|
|
163
|
+
* Normally it does not — under systemd stderr is the journal, and the
|
|
164
|
+
* supervisor's lines belong there as well as in the log. But the replacement
|
|
165
|
+
* supervisor of a self-upgrade is spawned with `stdio: ["ignore", newLogFd,
|
|
166
|
+
* newLogFd]`, and from then on every line would be written to the file twice.
|
|
167
|
+
* Resolved once, lazily: fd 2 does not change underneath a running process.
|
|
168
|
+
*/
|
|
169
|
+
let stderrIsLogFile: boolean | null = null;
|
|
170
|
+
|
|
157
171
|
function log(level: string, msg: string) {
|
|
158
172
|
const ts = new Date().toISOString();
|
|
159
173
|
const line = `[${ts}] [${level}] [supervisor] ${msg}\n`;
|
|
160
174
|
try { appendFileSync(logFile(), line); } catch {}
|
|
161
|
-
|
|
162
|
-
|
|
175
|
+
if (stderrIsLogFile === null) {
|
|
176
|
+
stderrIsLogFile = stdioIsLogFile(2, logFile());
|
|
177
|
+
consumeStdioIsLogEnv();
|
|
178
|
+
}
|
|
179
|
+
// Write supervisor logs to stderr so journalctl captures them — unless
|
|
180
|
+
// stderr is the log file itself, where that is the same line again.
|
|
181
|
+
if (!stderrIsLogFile) { try { process.stderr.write(line); } catch {} }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let logRotateTimer: ReturnType<typeof setInterval> | null = null;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Keep ppm.log under its cap. Only the supervisor does this.
|
|
188
|
+
*
|
|
189
|
+
* It is the long-lived owner of the descriptor every child was handed, and two
|
|
190
|
+
* processes truncating one file would race. Deliberately *not* run once at
|
|
191
|
+
* startup: the first rotation of a log that has been left to grow copies the
|
|
192
|
+
* whole of it, and doing that during boot adds to the one stall this work is
|
|
193
|
+
* trying to remove. The first tick is a minute away and nothing is worse for
|
|
194
|
+
* waiting.
|
|
195
|
+
*/
|
|
196
|
+
function startLogRotation() {
|
|
197
|
+
if (logRotateTimer) return;
|
|
198
|
+
logRotateTimer = setInterval(() => {
|
|
199
|
+
if (rotateIfOversized(logFile())) {
|
|
200
|
+
log("INFO", `Rotated ppm.log at cap ${Math.round(MAX_LOG_BYTES / 1048576)} MB`);
|
|
201
|
+
}
|
|
202
|
+
}, LOG_ROTATE_INTERVAL_MS);
|
|
203
|
+
logRotateTimer.unref?.();
|
|
163
204
|
}
|
|
164
205
|
|
|
165
206
|
// ─── Backoff calc ──────────────────────────────────────────────────────
|
|
@@ -622,7 +663,8 @@ export async function spawnServer(
|
|
|
622
663
|
serverChild = await withProbeSpawnGate(() => Bun.spawn({
|
|
623
664
|
cmd,
|
|
624
665
|
stdio: ["ignore", logFd, logFd],
|
|
625
|
-
|
|
666
|
+
// The child cannot work out on Windows that fd 1 is already the log, so it is told.
|
|
667
|
+
env: { ...process.env, [STDIO_IS_LOG_ENV]: "1" },
|
|
626
668
|
// No visible console window. Critical on Windows after an upgrade: the new
|
|
627
669
|
// supervisor is spawned consoleless (detached), so without this its console
|
|
628
670
|
// children — and the Claude SDK grandchildren they spawn — pop blank windows.
|
|
@@ -1509,7 +1551,7 @@ async function selfReplace(): Promise<{ success: boolean; error?: string }> {
|
|
|
1509
1551
|
const proc = await withProbeSpawnGate(() => nodeSpawn(cmd[0]!, cmd.slice(1), {
|
|
1510
1552
|
detached: true,
|
|
1511
1553
|
stdio: ["ignore", newLogFd, newLogFd] as any,
|
|
1512
|
-
env: process.env as NodeJS.ProcessEnv,
|
|
1554
|
+
env: { ...process.env, [STDIO_IS_LOG_ENV]: "1" } as NodeJS.ProcessEnv,
|
|
1513
1555
|
windowsHide: true,
|
|
1514
1556
|
}));
|
|
1515
1557
|
const killNewChild = () => { try { if (proc.pid) process.kill(proc.pid); } catch {} };
|
|
@@ -1873,6 +1915,7 @@ export async function runSupervisor(opts: {
|
|
|
1873
1915
|
originalArgv = [...process.argv];
|
|
1874
1916
|
|
|
1875
1917
|
const logFd = openSync(logFile(), "a");
|
|
1918
|
+
startLogRotation();
|
|
1876
1919
|
log("INFO", `Supervisor started (PID: ${process.pid}, port: ${opts.port}, share: ${opts.share})`);
|
|
1877
1920
|
|
|
1878
1921
|
// ── Systemd self-heal: if the unit file is stale (e.g. still has
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RustDesk's "Custom image quality": a bitrate *percentage* and an fps, with the real bitrate
|
|
3
|
+
* derived from the capture's own resolution. Ported rather than invented, because two parts of
|
|
4
|
+
* it are counter-intuitive enough that a reasonable guess is wrong:
|
|
5
|
+
*
|
|
6
|
+
* 1. **The percentage is not the ratio.** `video_qos.rs:444` reads it back as
|
|
7
|
+
* `((q >> 8 & 0xFFF) * 2) / 100.0`, so the ratio is `percent × 2 / 100` — the dialog's
|
|
8
|
+
* default **50% means ratio 1.0**, i.e. exactly `base_bitrate`. Reading 50% as ratio 0.5
|
|
9
|
+
* (the obvious reading) makes every custom setting half as sharp as the user asked for,
|
|
10
|
+
* and nothing reports it: the picture is simply dimmer than the number promises.
|
|
11
|
+
* 2. **`base_bitrate` is a nearest-preset lookup, not a formula.** `codec.rs:929` finds the
|
|
12
|
+
* table row whose *pixel count* is closest to the source's, then scales that row's bitrate
|
|
13
|
+
* by the pixel ratio. A plain bits-per-pixel constant does not reproduce it, because the
|
|
14
|
+
* table is deliberately non-linear (720p→1000, 1080p→2073, 4K→5000: 4× the pixels of
|
|
15
|
+
* 1080p for 2.4× the bits).
|
|
16
|
+
*
|
|
17
|
+
* The fixed rungs use the same machinery in RustDesk (`Quality::ratio()` → Best 1.5,
|
|
18
|
+
* Balanced 0.67, Low 0.5); PPM keeps its own absolute-bitrate ladder for those, so only the
|
|
19
|
+
* custom path needs this.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** `RESOLUTION_PRESETS` from `libs/scrap/src/common/codec.rs`, kbps. */
|
|
23
|
+
const RESOLUTION_BITRATE_PRESETS: readonly (readonly [number, number, number])[] = [
|
|
24
|
+
[640, 480, 400],
|
|
25
|
+
[800, 600, 500],
|
|
26
|
+
[1024, 768, 800],
|
|
27
|
+
[1280, 720, 1000],
|
|
28
|
+
[1366, 768, 1100],
|
|
29
|
+
[1440, 900, 1300],
|
|
30
|
+
[1600, 900, 1500],
|
|
31
|
+
[1920, 1080, 2073],
|
|
32
|
+
[2048, 1080, 2200],
|
|
33
|
+
[2560, 1440, 3000],
|
|
34
|
+
[3440, 1440, 4000],
|
|
35
|
+
[3840, 2160, 5000],
|
|
36
|
+
[7680, 4320, 12000],
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** `kMinQuality` / `kDefaultQuality` / `kMaxQuality` / `kMaxMoreQuality` from `consts.dart`. */
|
|
40
|
+
export const CUSTOM_QUALITY_MIN_PERCENT = 10;
|
|
41
|
+
export const CUSTOM_QUALITY_DEFAULT_PERCENT = 50;
|
|
42
|
+
export const CUSTOM_QUALITY_MAX_PERCENT = 100;
|
|
43
|
+
/** The "More" checkbox raises the ceiling this far — 2000% is ratio 40, which is `BR_MAX`. */
|
|
44
|
+
export const CUSTOM_QUALITY_MAX_MORE_PERCENT = 2000;
|
|
45
|
+
|
|
46
|
+
/** `kMinFps` / `kDefaultFps` / `kMaxFps`. */
|
|
47
|
+
export const CUSTOM_FPS_MIN = 5;
|
|
48
|
+
export const CUSTOM_FPS_DEFAULT = 30;
|
|
49
|
+
export const CUSTOM_FPS_MAX = 120;
|
|
50
|
+
|
|
51
|
+
/** `BR_MIN` / `BR_MAX` from `video_qos.rs`. */
|
|
52
|
+
const BR_MIN = 0.2;
|
|
53
|
+
const BR_MAX = 40.0;
|
|
54
|
+
|
|
55
|
+
/** Nearest-pixel-count preset, scaled by the pixel ratio. See note 2. */
|
|
56
|
+
export function baseBitrateKbps(width: number, height: number): number {
|
|
57
|
+
const pixels = Math.max(1, Math.round(width) * Math.round(height));
|
|
58
|
+
let best = RESOLUTION_BITRATE_PRESETS[7]!; // 1080p, the table's own fallback
|
|
59
|
+
let bestDistance = Infinity;
|
|
60
|
+
for (const preset of RESOLUTION_BITRATE_PRESETS) {
|
|
61
|
+
const distance = Math.abs(preset[0] * preset[1] - pixels);
|
|
62
|
+
if (distance < bestDistance) { bestDistance = distance; best = preset; }
|
|
63
|
+
}
|
|
64
|
+
const presetPixels = best[0] * best[1];
|
|
65
|
+
// Floored at 1: a 0x0 capture (before the first frame) otherwise scales the smallest preset
|
|
66
|
+
// down to `round(400 / 307200)` = **0**, and ffmpeg refuses a zero bitrate outright rather
|
|
67
|
+
// than picking something sensible.
|
|
68
|
+
return Math.max(1, Math.round(best[2] * (pixels / presetPixels)));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Percent → bitrate ratio. `percent × 2 / 100`, clamped. See note 1. */
|
|
72
|
+
export function customQualityRatio(percent: number): number {
|
|
73
|
+
if (!Number.isFinite(percent)) return (CUSTOM_QUALITY_DEFAULT_PERCENT * 2) / 100;
|
|
74
|
+
return Math.min(Math.max((percent * 2) / 100, BR_MIN), BR_MAX);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Clamp any bitrate ratio to RustDesk's own bounds. Named rungs already sit inside them; the
|
|
78
|
+
* congestion scaling in `remote-desktop-quality.ts` is what can push one out. */
|
|
79
|
+
export function clampRatio(ratio: number): number {
|
|
80
|
+
if (!Number.isFinite(ratio)) return BR_MIN;
|
|
81
|
+
return Math.min(Math.max(ratio, BR_MIN), BR_MAX);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The bitrate a ratio really encodes at, for this capture size. Every rung goes through here —
|
|
85
|
+
* a named one, a congestion-scaled one and the custom dialog's percentage alike, which is what
|
|
86
|
+
* makes "the chosen quality is a ceiling on the ratio" a statement about one number. */
|
|
87
|
+
export function ratioBitrateKbps(ratio: number, width: number, height: number): number {
|
|
88
|
+
// Floored at 1: the table's smallest row makes `baseBitrateKbps(0, 0)` round to 0, and ffmpeg
|
|
89
|
+
// refuses a zero bitrate outright.
|
|
90
|
+
return Math.max(1, Math.round(baseBitrateKbps(width, height) * clampRatio(ratio)));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** An ffmpeg bitrate string (`-b:v`/`-maxrate`) for a ratio. */
|
|
94
|
+
export function ratioBitrateArg(ratio: number, width: number, height: number): string {
|
|
95
|
+
return `${ratioBitrateKbps(ratio, width, height)}k`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The bitrate a custom setting really encodes at, for this capture size. */
|
|
99
|
+
export function customBitrateKbps(percent: number, width: number, height: number): number {
|
|
100
|
+
return ratioBitrateKbps(customQualityRatio(percent), width, height);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** An ffmpeg bitrate string (`-b:v`/`-maxrate`) for a custom setting. */
|
|
104
|
+
export function customBitrateArg(percent: number, width: number, height: number): string {
|
|
105
|
+
return `${customBitrateKbps(percent, width, height)}k`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function clampCustomQualityPercent(value: unknown, allowMore = false): number {
|
|
109
|
+
const max = allowMore ? CUSTOM_QUALITY_MAX_MORE_PERCENT : CUSTOM_QUALITY_MAX_PERCENT;
|
|
110
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return CUSTOM_QUALITY_DEFAULT_PERCENT;
|
|
111
|
+
return Math.min(Math.max(Math.round(value), CUSTOM_QUALITY_MIN_PERCENT), max);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function clampCustomFps(value: unknown): number {
|
|
115
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return CUSTOM_FPS_DEFAULT;
|
|
116
|
+
return Math.min(Math.max(Math.round(value), CUSTOM_FPS_MIN), CUSTOM_FPS_MAX);
|
|
117
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The remote-desktop quality ladder — shared, because both ends need the same table: the
|
|
3
|
+
* server turns a rung into ffmpeg args, and the client renders the picker and shows which rung
|
|
4
|
+
* is active. One definition so a label or a rung cannot drift between them.
|
|
5
|
+
*
|
|
6
|
+
* This is RustDesk's own set, ported rather than invented: `toolbarImageQuality` in
|
|
7
|
+
* `flutter/lib/common/widgets/toolbar.dart` builds exactly four radio items — Good image
|
|
8
|
+
* quality, Balanced, Optimize reaction time, Custom — and `Quality` in
|
|
9
|
+
* `libs/scrap/src/common/codec.rs` is the matching enum with the three ratios below.
|
|
10
|
+
*
|
|
11
|
+
* Two things follow from that, and both are the opposite of what this file used to do:
|
|
12
|
+
*
|
|
13
|
+
* 1. **A rung never changes the resolution.** It sets a bitrate ratio, and the stream stays at
|
|
14
|
+
* the host's native size. The old ladder capped height per rung (720p on `balanced`), which
|
|
15
|
+
* on a 3440x1440 host threw away 4.8x the pixels — and measured, it bought almost nothing:
|
|
16
|
+
* 720p used 1.17 Mbit/s against 1.26 Mbit/s for native at the same cap, because a desktop is
|
|
17
|
+
* mostly static and H.264 spends bits on *change*, not on area. Neither came close to the
|
|
18
|
+
* cap it was given, so the downscale was pure loss.
|
|
19
|
+
* 2. **There is no `auto` rung.** RustDesk has no such menu item because the adaptation is
|
|
20
|
+
* always running: the chosen quality is the *maximum* ratio (`video_qos.rs`: "user set image
|
|
21
|
+
* quality => update to the maximum ratio of the latest quality") and congestion lowers the
|
|
22
|
+
* effective ratio underneath it. A pin that switched adaptation *off* is what made a chosen
|
|
23
|
+
* rung flicker forever on a link that could not carry it.
|
|
24
|
+
*
|
|
25
|
+
* The *policy* built on top of this (how far the ratio may fall, and when it moves) is server
|
|
26
|
+
* side only, in `src/services/remote-desktop/remote-desktop-quality.ts`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export type QualityPresetId = "best" | "balanced" | "low";
|
|
30
|
+
|
|
31
|
+
/** One entry of the Image quality menu: a label and the bitrate ratio it asks for. Deliberately
|
|
32
|
+
* carries no resolution and no bitrate — the real bitrate depends on the capture size, so it
|
|
33
|
+
* can only be resolved once the display is known (`resolveQuality`). */
|
|
34
|
+
export interface QualityRung {
|
|
35
|
+
id: QualityPresetId;
|
|
36
|
+
label: string;
|
|
37
|
+
/** RustDesk's `BR_BEST` / `BR_BALANCED` / `BR_SPEED` from `codec.rs`, unchanged. */
|
|
38
|
+
ratio: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What the capture layer needs: a frame rate and an ffmpeg bitrate string. A rung plus a
|
|
42
|
+
* capture size resolves to one of these; so does the custom dialog's percentage. */
|
|
43
|
+
export interface QualityPreset {
|
|
44
|
+
fps: number;
|
|
45
|
+
/** ffmpeg bitrate string, used for both `-b:v` and `-maxrate`. */
|
|
46
|
+
bitrate: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const QUALITY_PRESETS: Record<QualityPresetId, QualityRung> = {
|
|
50
|
+
best: { id: "best", label: "Good image quality", ratio: 1.5 },
|
|
51
|
+
balanced: { id: "balanced", label: "Balanced", ratio: 0.67 },
|
|
52
|
+
low: { id: "low", label: "Optimize reaction time", ratio: 0.5 },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/* "Optimize reaction time" is RustDesk's own name for the 0.5 rung and it is now the right one,
|
|
56
|
+
* where on the old ladder it would have been backwards. There, the low rungs downscaled and the
|
|
57
|
+
* *high* rungs were the low-latency ones — measured 1440p60 `gap p50=16.6ms` against 720p30
|
|
58
|
+
* `p50=33.3ms`. With resolution out of the picture the only thing a lower rung changes is how
|
|
59
|
+
* many bytes have to cross the link per frame, so less really does mean sooner. */
|
|
60
|
+
|
|
61
|
+
/** RustDesk's menu order, which is best-first — not the worst-to-best order a ladder walks. */
|
|
62
|
+
export const QUALITY_PRESET_ORDER: readonly QualityPresetId[] = ["best", "balanced", "low"];
|
|
63
|
+
|
|
64
|
+
/** `Quality::default()` in `codec.rs` is `Balanced`. */
|
|
65
|
+
export const DEFAULT_PRESET_ID: QualityPresetId = "balanced";
|
|
66
|
+
|
|
67
|
+
/** `pub const FPS: u32 = 30` in `video_qos.rs` — what a named rung streams at. Only the custom
|
|
68
|
+
* dialog sets a frame rate directly; RustDesk otherwise adapts it, never exposes it. */
|
|
69
|
+
export const DEFAULT_FPS = 30;
|
|
70
|
+
|
|
71
|
+
/** Coerce whatever came over the wire into a real preset id, or null for garbage.
|
|
72
|
+
* `Object.hasOwn`, never `in`: `in` walks the prototype chain, so `"toString"` off the wire
|
|
73
|
+
* would pass as a preset id and `QUALITY_PRESETS["toString"]` would hand the capture pipeline
|
|
74
|
+
* a *function* where it expected a rung — nonsense ffmpeg args from one client string. */
|
|
75
|
+
export function parsePresetId(value: unknown): QualityPresetId | null {
|
|
76
|
+
return typeof value === "string" && Object.hasOwn(QUALITY_PRESETS, value)
|
|
77
|
+
? value as QualityPresetId
|
|
78
|
+
: null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** What the client can ask for. `custom` only means something together with the two numbers
|
|
82
|
+
* from its dialog, which is why it is a separate arm rather than a fourth rung. */
|
|
83
|
+
export type QualityChoice = QualityPresetId | "custom";
|
|
84
|
+
|
|
85
|
+
/** A custom rung: RustDesk's bitrate percentage + fps, at the host's native resolution — the
|
|
86
|
+
* same resolution every named rung uses (see `remote-desktop-custom-quality.ts`). */
|
|
87
|
+
export interface CustomQuality {
|
|
88
|
+
/** Bitrate percentage; the real bitrate comes from the capture size. */
|
|
89
|
+
percent: number;
|
|
90
|
+
fps: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Coerce a stored/untrusted value into a `QualityChoice`, falling back to the default rung.
|
|
94
|
+
* Separate from `parsePresetId` because the persisted device pref may legitimately be
|
|
95
|
+
* `"custom"`, and because localStorage is user-writable — a rung read back from it is
|
|
96
|
+
* untrusted input exactly like one off the wire, with the same `Object.hasOwn` hazard behind
|
|
97
|
+
* it. A pref left over from the old five-rung ladder (`"tiny"`, `"max"`, `"auto"`) parses as
|
|
98
|
+
* garbage and lands on the default, which is what should happen to a rung that no longer
|
|
99
|
+
* exists. */
|
|
100
|
+
export function parseQualityChoice(value: unknown): QualityChoice {
|
|
101
|
+
if (value === "custom") return value;
|
|
102
|
+
return parsePresetId(value) ?? DEFAULT_PRESET_ID;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Next value for the mobile toolbar's one-tap-per-hop button, in menu order. A dropdown in the
|
|
106
|
+
* thumb zone is worse than a button that cycles.
|
|
107
|
+
*
|
|
108
|
+
* `custom` is deliberately not in the cycle: it only means something together with the two
|
|
109
|
+
* numbers from its dialog, so a one-tap button that landed on it would change the stream to
|
|
110
|
+
* whatever was last typed there — surprising, and with no visible cause. */
|
|
111
|
+
export function nextQualityChoice(current: QualityChoice): QualityChoice {
|
|
112
|
+
if (current === "custom") return QUALITY_PRESET_ORDER[0]!;
|
|
113
|
+
const at = QUALITY_PRESET_ORDER.indexOf(current);
|
|
114
|
+
return QUALITY_PRESET_ORDER[(at + 1) % QUALITY_PRESET_ORDER.length]!;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Compact label for a toolbar button, where the full label does not fit. RustDesk has no such
|
|
118
|
+
* button, so these are the shortest thing that still distinguishes the four. */
|
|
119
|
+
const SHORT_LABELS: Record<QualityPresetId, string> = {
|
|
120
|
+
best: "Good",
|
|
121
|
+
balanced: "Balanced",
|
|
122
|
+
low: "Reaction",
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export function shortQualityLabel(choice: QualityChoice): string {
|
|
126
|
+
return choice === "custom" ? "Custom" : SHORT_LABELS[choice];
|
|
127
|
+
}
|
package/src/types/chat.ts
CHANGED
|
@@ -50,6 +50,10 @@ export interface AIProvider {
|
|
|
50
50
|
onToolApproval?: (callback: ToolApprovalHandler) => void;
|
|
51
51
|
abortQuery?(sessionId: string, source?: string): void;
|
|
52
52
|
getMessages?(sessionId: string): Promise<ChatMessage[]>;
|
|
53
|
+
/** Every message in the transcript, including the segments before each compaction.
|
|
54
|
+
* `getMessages` answers with the resumable *conversation*; this answers with the
|
|
55
|
+
* whole history. Only the search index asks for it — see `indexSession`. */
|
|
56
|
+
getFullMessages?(sessionId: string): Promise<ChatMessage[]>;
|
|
53
57
|
listSessionsByDir?(dir: string, opts?: { limit?: number; offset?: number }): Promise<SessionInfo[]>;
|
|
54
58
|
ensureProjectPath?(sessionId: string, path: string): void;
|
|
55
59
|
setForkSource?(sessionId: string, sourceSessionId: string): void;
|
package/src/types/git.ts
CHANGED
|
@@ -59,6 +59,24 @@ export interface GitDiffFile {
|
|
|
59
59
|
content: string;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Both sides of a single-file diff, as whole files.
|
|
64
|
+
*
|
|
65
|
+
* `binary` is decided from the bytes, and when it is true both sides come back
|
|
66
|
+
* **empty**: decoded, a PNG is megabytes of U+FFFD that no one can read and that
|
|
67
|
+
* JSON has to escape. The viewer asks again with `text=1` ("Open Anyway") when
|
|
68
|
+
* the user wants them anyway. A null size means the file does not exist on that
|
|
69
|
+
* side, which is how the binary view tells an added or deleted file from a
|
|
70
|
+
* changed one.
|
|
71
|
+
*/
|
|
72
|
+
export interface FileFullDiff {
|
|
73
|
+
original: string;
|
|
74
|
+
modified: string;
|
|
75
|
+
binary: boolean;
|
|
76
|
+
originalSize: number | null;
|
|
77
|
+
modifiedSize: number | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
62
80
|
export interface GitWorktree {
|
|
63
81
|
/** Absolute path to the worktree directory */
|
|
64
82
|
path: string;
|
|
@@ -79,3 +97,37 @@ export interface GitWorktree {
|
|
|
79
97
|
/** True if this worktree can be pruned (directory missing/stale) */
|
|
80
98
|
prunable: boolean;
|
|
81
99
|
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A whole branch's changes against a base, as one list.
|
|
103
|
+
*
|
|
104
|
+
* `mergeBase` is the commit every per-file diff has to be opened against — in
|
|
105
|
+
* three-dot mode it is where the two refs diverged, not `base` itself.
|
|
106
|
+
*/
|
|
107
|
+
export interface BranchDiff {
|
|
108
|
+
base: string;
|
|
109
|
+
head: string;
|
|
110
|
+
mode: "three-dot" | "two-dot";
|
|
111
|
+
mergeBase: string;
|
|
112
|
+
/**
|
|
113
|
+
* `head` resolved to a commit, which is what a file must be opened against.
|
|
114
|
+
* `head` itself is a ref name and moves: a commit landing between the list
|
|
115
|
+
* fetch and a file being opened would show the new tip beside counts and a
|
|
116
|
+
* blob id describing the old one.
|
|
117
|
+
*/
|
|
118
|
+
headCommit: string;
|
|
119
|
+
/** Files omitted from `files` because the list hit its cap; 0 when complete. */
|
|
120
|
+
omitted: number;
|
|
121
|
+
files: BranchDiffFile[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface BranchDiffFile {
|
|
125
|
+
path: string;
|
|
126
|
+
oldPath?: string;
|
|
127
|
+
status: "A" | "M" | "D" | "R" | "C" | "T";
|
|
128
|
+
additions: number;
|
|
129
|
+
deletions: number;
|
|
130
|
+
binary: boolean;
|
|
131
|
+
/** Head-side blob id; what a "reviewed" flag is remembered against. */
|
|
132
|
+
blob: string;
|
|
133
|
+
}
|