@hienlh/ppm 0.18.14 → 0.18.15
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/.rdtest2-cf.out +0 -0
- package/CHANGELOG.md +18 -0
- package/CLAUDE.md +3 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +22 -4
- package/assets/skills/ppm/references/http-api.md +8 -1
- package/dist/web/assets/{adaptive-context-menu-DCZtgOL7.js → adaptive-context-menu-ChBZWLLq.js} +1 -1
- package/dist/web/assets/{ai-resource-editor-D5CPBg06.js → ai-resource-editor-C8aAY8Po.js} +1 -1
- package/dist/web/assets/{ai-settings-section-DY-t1bK8.js → ai-settings-section-ERu1WiZc.js} +1 -1
- package/dist/web/assets/{angular-html-CTvM4ulF.js → angular-html-CXgHIQ2U.js} +1 -1
- package/dist/web/assets/{angular-ts-B_vSkjco.js → angular-ts-C8renMV9.js} +1 -1
- package/dist/web/assets/api-client-WpZ2FDHg.js +1 -0
- package/dist/web/assets/api-settings-BgSEE6et.js +1 -0
- package/dist/web/assets/{api-settings-B2iNXQ0c.js → api-settings-R59c66DE.js} +1 -1
- package/dist/web/assets/{api-themes-DrWnJbhs.js → api-themes-DKxrwJda.js} +1 -1
- package/dist/web/assets/{apl-DywzTry0.js → apl-DkGH8laI.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-CCalSKAt.js +1 -0
- package/dist/web/assets/{astro-1JaRgaSb.js → astro-tHUd8Zlg.js} +1 -1
- package/dist/web/assets/{audio-preview-DAuGvOiK.js → audio-preview-B_C6Dpvz.js} +1 -1
- package/dist/web/assets/{blade-CDeeJWBb.js → blade-BbKpjIYK.js} +1 -1
- package/dist/web/assets/{bsl-BfgoPVYR.js → bsl-DA0UYto0.js} +1 -1
- package/dist/web/assets/{button-tNy0ba3y.js → button-DBSpDPS0.js} +1 -1
- package/dist/web/assets/{cairo-C26gNwWW.js → cairo-79ayucbB.js} +1 -1
- package/dist/web/assets/chat-tab-BraiJsYG.js +15 -0
- package/dist/web/assets/{cobol-D92nBnON.js → cobol-DSS2EOYo.js} +1 -1
- package/dist/web/assets/code-editor-lhmLNhXu.js +10 -0
- package/dist/web/assets/{coffee-CIWH40R1.js → coffee-DvSxq2iD.js} +1 -1
- package/dist/web/assets/{conflict-editor-CDp0I3mD.js → conflict-editor-C4VCv51R.js} +1 -1
- package/dist/web/assets/{context-menu-BO1c9jKy.js → context-menu-BpwUny-o.js} +1 -1
- package/dist/web/assets/{cpp-DOrBqXCn.js → cpp-BhWZ-FSn.js} +1 -1
- package/dist/web/assets/{crystal-BNKqmHux.js → crystal-yP2OeF-f.js} +1 -1
- package/dist/web/assets/{csv-preview-D4svNDAq.js → csv-preview-D46W_icb.js} +1 -1
- package/dist/web/assets/{data-grid-overlay-editor-BqV0AxZg.js → data-grid-overlay-editor-DeGkPYtA.js} +1 -1
- package/dist/web/assets/{database-viewer-BCvv_Bwf.js → database-viewer-DFI4YtrH.js} +1 -1
- package/dist/web/assets/{dialog-DcLGDwaX.js → dialog-RIpeb87v.js} +1 -1
- package/dist/web/assets/{diff-viewer-D41fCFAi.js → diff-viewer-eaNOLcBL.js} +1 -1
- package/dist/web/assets/{docx-preview-DfaQn9v7.js → docx-preview-Cbj-pOK3.js} +1 -1
- package/dist/web/assets/{edge-Dbjp42Pt.js → edge-D3K9cd-p.js} +1 -1
- package/dist/web/assets/{edit-diff-preview-BqcUuFCc.js → edit-diff-preview-DDqy3KLA.js} +1 -1
- package/dist/web/assets/{elixir-na3b1pYy.js → elixir-D4PBmsLp.js} +1 -1
- package/dist/web/assets/{elm-_Nalil1R.js → elm-DSpXjuaH.js} +1 -1
- package/dist/web/assets/{erb-CW_7dubf.js → erb-CaYZMTJW.js} +1 -1
- package/dist/web/assets/{erlang-DmZ7Zzb0.js → erlang-DX5BFqJj.js} +1 -1
- package/dist/web/assets/{esm-CwyBetO1.js → esm-Cv7HsFnk.js} +1 -1
- package/dist/web/assets/explorer-body-BxbSttfl.js +2 -0
- package/dist/web/assets/explorer-window-content-DUqQCC7A.js +1 -0
- package/dist/web/assets/{extension-store-DUaZINyC.js → extension-store-B1EClUAr.js} +1 -1
- package/dist/web/assets/{extension-webview-BW_mccb3.js → extension-webview-BaGvDk8v.js} +1 -1
- package/dist/web/assets/{file-store-yReVbR8q.js → file-store-m9crh6uq.js} +1 -1
- package/dist/web/assets/{fortran-fixed-form-DB2A8L4-.js → fortran-fixed-form-B0bAufUe.js} +1 -1
- package/dist/web/assets/{fsharp-BYHrti7z.js → fsharp-C2AL2b1M.js} +1 -1
- package/dist/web/assets/{gdresource-D5GBin40.js → gdresource-DRQsDMNI.js} +1 -1
- package/dist/web/assets/{git-commit-CUiOBoIL.js → git-commit-GtBdnrzk.js} +1 -1
- package/dist/web/assets/{git-log-panel-C1QDkmgl.js → git-log-panel-hwV1flSO.js} +1 -1
- package/dist/web/assets/{git-rebase-DOxGnpdw.js → git-rebase-h_0CGA6F.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-DAUrJCqi.js +1 -0
- package/dist/web/assets/glide-data-grid-YsN-xm6x.js +138 -0
- package/dist/web/assets/{glimmer-js-Begy4UT7.js → glimmer-js-Bxwu93bS.js} +1 -1
- package/dist/web/assets/{glimmer-ts-rPApRY8N.js → glimmer-ts-CIUYdR4E.js} +1 -1
- package/dist/web/assets/{glsl-wrbPvrBL.js → glsl-CfxLI5jn.js} +1 -1
- package/dist/web/assets/{graphql-Cu4-NR__.js → graphql-CstqDPde.js} +1 -1
- package/dist/web/assets/group-chat-tab-B7mJO1gG.js +1 -0
- package/dist/web/assets/{hack-Ddo93XKD.js → hack-Bjqnssb5.js} +1 -1
- package/dist/web/assets/{haml-BFX_1i8j.js → haml-lVc-DfbM.js} +1 -1
- package/dist/web/assets/hand-x0ejq3fS.js +1 -0
- package/dist/web/assets/{handlebars-5gFKA4H5.js → handlebars-Yy6fPQI_.js} +1 -1
- package/dist/web/assets/{html-derivative-e5JLAFZF.js → html-derivative-CchzCY1z.js} +1 -1
- package/dist/web/assets/{html-C06I-zeO.js → html-lqtcy6-2.js} +1 -1
- package/dist/web/assets/{http-CrAAhSU7.js → http-ChclRrko.js} +1 -1
- package/dist/web/assets/{hurl-T1V3SX9T.js → hurl-CoOK7sXa.js} +1 -1
- package/dist/web/assets/{hxml-B056LLKN.js → hxml-fZSZ7Iyt.js} +1 -1
- package/dist/web/assets/{image-preview-DWyuCYT3.js → image-preview-ClAcqDlL.js} +1 -1
- package/dist/web/assets/index-CtJbMNNI.css +2 -0
- package/dist/web/assets/index-H_gScEOk.js +52 -0
- package/dist/web/assets/info-3K5VOQVL-Cx1ZfJbp.js +1 -0
- package/dist/web/assets/{input-DouJ9Zvt.js → input-DXPgUtxi.js} +1 -1
- package/dist/web/assets/{jinja-DGdtRh43.js → jinja-Y0w_KBdE.js} +1 -1
- package/dist/web/assets/{jison-Ah6QobPS.js → jison-BILj3F2e.js} +1 -1
- package/dist/web/assets/{julia-BJeGOK0K.js → julia-DRfOPDts.js} +1 -1
- package/dist/web/assets/{just-iw9oRs7j.js → just-n3U_oODO.js} +1 -1
- package/dist/web/assets/keybindings-store-BqctreaI.js +1 -0
- package/dist/web/assets/{keybindings-store-0Wqfa7Qp.js → keybindings-store-D-CHdq-G.js} +1 -1
- package/dist/web/assets/keyboard-DBBaoQSu.js +1 -0
- package/dist/web/assets/{latex-DHjj5hyN.js → latex-D6gztEOL.js} +1 -1
- package/dist/web/assets/{liquid-B-FFecDc.js → liquid-DPqCEVb9.js} +1 -1
- package/dist/web/assets/{lua-DDianJp_.js → lua-I40KPUm-.js} +1 -1
- package/dist/web/assets/markdown-renderer-BwOkiZC1.js +1 -0
- package/dist/web/assets/{markdown-renderer-BHiUhQN8.js → markdown-renderer-oJ9jPdQ2.js} +3 -3
- package/dist/web/assets/{marko-BW5SBVng.js → marko-DMjsThSW.js} +1 -1
- package/dist/web/assets/{mdc-B9gL9bjl.js → mdc-CN2alsbo.js} +1 -1
- package/dist/web/assets/{media-url-DUK-k0bB.js → media-url-COUvasjn.js} +1 -1
- package/dist/web/assets/mobile-bottom-sheet-Cj1YBq-c.js +1 -0
- package/dist/web/assets/mobile-explorer-sheet-D1nM0Ik1.js +1 -0
- package/dist/web/assets/{nextflow-dTluY6qy.js → nextflow-CGa4JeZ6.js} +1 -1
- package/dist/web/assets/{nginx-DlwghGI-.js → nginx-CmqrIj84.js} +1 -1
- package/dist/web/assets/{nim-N9jddb5D.js → nim-CqhmTeMa.js} +1 -1
- package/dist/web/assets/notification-store-n-XEQ5pH.js +1 -0
- package/dist/web/assets/{number-overlay-editor-BVXM7aAs.js → number-overlay-editor-BEzJ1YeD.js} +1 -1
- package/dist/web/assets/{overview-panel-DCCDmtCQ.js → overview-panel-Bf9Gn8qV.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-Bqwiy7dz.js +1 -0
- package/dist/web/assets/{panel-store-B4rijFKN.js → panel-store-UWCH6B-_.js} +1 -1
- package/dist/web/assets/{pdf-preview-D7yLmAZW.js → pdf-preview-BMHM5cR2.js} +1 -1
- package/dist/web/assets/{perl-CkmpYKa_.js → perl-BOwpb6Tb.js} +1 -1
- package/dist/web/assets/{php-BwZ3KDPo.js → php-wT9fVxxu.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-BXSJ2God.js +1 -0
- package/dist/web/assets/{postgres-viewer--CRN1fvO.js → postgres-viewer-D8rfEY5b.js} +3 -3
- package/dist/web/assets/process-table-CsQ-hDJr.js +1 -0
- package/dist/web/assets/{project-store-BuY7rxn_.js → project-store-CKAOr23O.js} +1 -1
- package/dist/web/assets/{pug-B3rBPqQo.js → pug-DdlNnj-p.js} +1 -1
- package/dist/web/assets/{qml-BfDbWFmg.js → qml-C5zBdydf.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-D6XrM7HH.js +1 -0
- package/dist/web/assets/{razor-B1ww1r38.js → razor-DqcJX4iq.js} +1 -1
- package/dist/web/assets/remote-desktop-mobile-sheet-CTiipn4k.js +2 -0
- package/dist/web/assets/remote-desktop-mobile-view-Cf5B0dKo.js +1 -0
- package/dist/web/assets/remote-desktop-stats-overlay-vhJBIW7s.js +1 -0
- package/dist/web/assets/remote-desktop-window-content-wCJUfI6G.js +1 -0
- package/dist/web/assets/{rst-C0MSq6m-.js → rst-DiMiMYUO.js} +1 -1
- package/dist/web/assets/{ruby-ClIZ1wb3.js → ruby-DK62iflA.js} +1 -1
- package/dist/web/assets/{run-in-terminal-4efbW2Xv.js → run-in-terminal-DJSL6nwZ.js} +1 -1
- package/dist/web/assets/{sas-DR0j3o4_.js → sas-6QPGJRa5.js} +1 -1
- package/dist/web/assets/{scroll-area-ZP8wcwTI.js → scroll-area-DXGXeiRz.js} +1 -1
- package/dist/web/assets/{scss-Vh3JLayR.js → scss-DmUqh48_.js} +1 -1
- package/dist/web/assets/{send-to-chat-BNrEpAyC.js → send-to-chat-BOony0sO.js} +1 -1
- package/dist/web/assets/settings-store-BpcY5aYE.js +2 -0
- package/dist/web/assets/settings-tab-BVqRO01j.js +1 -0
- package/dist/web/assets/settings-tab-FH581A_i.js +3 -0
- package/dist/web/assets/{shaderlab-0rHRnQ18.js → shaderlab-DopFR1mi.js} +1 -1
- package/dist/web/assets/{shellsession-Dxdw6D8c.js → shellsession-Bp_TGuXb.js} +1 -1
- package/dist/web/assets/{soy-BHlSNj1y.js → soy-DEB9KEFb.js} +1 -1
- package/dist/web/assets/{sparql-B2QaPpni.js → sparql-C8lEwO1p.js} +1 -1
- package/dist/web/assets/{sql-query-editor-R_iKjPP9.js → sql-query-editor-MMfk0Kbz.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-b2onA0SP.js → sqlite-viewer-vglzrgnZ.js} +1 -1
- package/dist/web/assets/{stata-CwIxjsEJ.js → stata-DVep4apN.js} +1 -1
- package/dist/web/assets/{surrealql-C6vo-rp-.js → surrealql-fSlJApOA.js} +1 -1
- package/dist/web/assets/{svelte-LLdJ6rlH.js → svelte-DezWBEDi.js} +1 -1
- package/dist/web/assets/{switch-DV-BDjyk.js → switch-DqL_ZjB_.js} +1 -1
- package/dist/web/assets/system-monitor-body-BYUfyF8f.js +2 -0
- package/dist/web/assets/system-monitor-tab-CKGXB8lH.js +1 -0
- package/dist/web/assets/{system-monitor-window-content-BPif0azo.js → system-monitor-window-content-CiJ2yIsr.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-BnAN6m4K.js → tab-host-window-content-DYlXR0cg.js} +1 -1
- package/dist/web/assets/{tab-store-tFnyPmm4.js → tab-store-D2thn4Vv.js} +1 -1
- package/dist/web/assets/team-member-sheet-Bnygu7Xw.js +2 -0
- package/dist/web/assets/{team-member-window-content-BSyU-5Uc.js → team-member-window-content-qrgFTC96.js} +1 -1
- package/dist/web/assets/{templ-CJGvhEP5.js → templ-D0zB42a9.js} +1 -1
- package/dist/web/assets/{terminal-tab-Ncor0Rfu.js → terminal-tab-D_-R5rGm.js} +3 -3
- package/dist/web/assets/{tex-psXWhc4t.js → tex-BJiPijcH.js} +1 -1
- package/dist/web/assets/{tool-cards-DdVP1wrD.js → tool-cards-BW7Xc20J.js} +4 -4
- package/dist/web/assets/treemap-KZPCXAKY-BobWhcHN.js +1 -0
- package/dist/web/assets/{ts-tags-D5Ar_ZCO.js → ts-tags-Dcr4u6y5.js} +1 -1
- package/dist/web/assets/{twig-vJgGC-qY.js → twig-B-SQU0bg.js} +1 -1
- package/dist/web/assets/{use-blob-url-BOVGWg9F.js → use-blob-url-BtPlqwLr.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-BshnLXoY.js → use-monaco-theme-DQY1ZSaf.js} +1 -1
- package/dist/web/assets/{use-open-team-member-CY3wOdWL.js → use-open-team-member-C5LQ_TCM.js} +1 -1
- package/dist/web/assets/use-visual-viewport-pVxLfE5B.js +1 -0
- package/dist/web/assets/{use-websocket-B7xjNsyD.js → use-websocket-l39Odhlp.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-999ksD45.js → vendor-mermaid-CtDsSxbM.js} +1 -1
- package/dist/web/assets/{video-preview-7F72sRwf.js → video-preview-C5AhkIDl.js} +1 -1
- package/dist/web/assets/{vue-BWngFyav.js → vue-Cw10Sfbh.js} +1 -1
- package/dist/web/assets/{vue-html-BN5ZLu-H.js → vue-html-Cc5C_Z_w.js} +1 -1
- package/dist/web/assets/{vue-vine-BZHnO8yV.js → vue-vine-BuNMlnvu.js} +1 -1
- package/dist/web/assets/{window-pip-registry-BnYU83LE.js → window-pip-registry-C7oz3Dfy.js} +1 -1
- package/dist/web/assets/window-store-B5ZswdYg.js +1 -0
- package/dist/web/assets/{xml-DS2beZHp.js → xml-DqT1wkky.js} +1 -1
- package/dist/web/assets/{xsl-Ciiku1Bq.js → xsl-DLkge3A2.js} +1 -1
- package/dist/web/index.html +45 -43
- package/dist/web/sw.js +1 -1
- package/docs/architecture/ai-chat-and-providers.md +3 -0
- package/package.json +1 -1
- package/packages/vscode-compat/node_modules/.bin/tsc.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsc.exe +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.exe +0 -0
- package/src/cli/commands/backup-cmd.ts +119 -0
- package/src/index.ts +3 -0
- package/src/providers/claude-agent-sdk.ts +29 -1
- package/src/server/index.ts +42 -15
- package/src/server/routes/accounts.ts +30 -2
- package/src/server/routes/chat.ts +8 -1
- package/src/server/routes/named-tunnel.ts +5 -2
- package/src/server/routes/remote-desktop.ts +64 -0
- package/src/server/routes/settings.ts +1 -0
- package/src/server/ws/chat.ts +126 -7
- package/src/server/ws/remote-desktop.ts +86 -0
- package/src/services/account.service.ts +106 -23
- package/src/services/db-backup/db-backup-paths.ts +66 -0
- package/src/services/db-backup/db-backup-restore.ts +105 -0
- package/src/services/db-backup/db-backup-retention.ts +96 -0
- package/src/services/db-backup/db-backup-sync.ts +113 -0
- package/src/services/db-backup/db-backup.service.ts +61 -0
- package/src/services/db.service.ts +29 -1
- package/src/services/edge-forwarder.ts +191 -191
- package/src/services/fs-credential-path-guard.ts +19 -1
- package/src/services/named-tunnel/cloudflared-login.service.ts +2 -2
- package/src/services/nested-subagent-spy.ts +206 -0
- package/src/services/remote-desktop/access-unit-assembler.ts +90 -0
- package/src/services/remote-desktop/avc1-codec-string.ts +18 -0
- package/src/services/remote-desktop/nal-splitter.ts +100 -0
- package/src/services/remote-desktop/remote-desktop-capture.ts +120 -0
- package/src/services/remote-desktop/remote-desktop-encoder-args.ts +42 -0
- package/src/services/remote-desktop/remote-desktop-flag.ts +10 -0
- package/src/services/remote-desktop/remote-desktop-input.ts +217 -0
- package/src/services/remote-desktop/remote-desktop-nonce.ts +38 -0
- package/src/services/remote-desktop/remote-desktop-session.ts +189 -0
- package/src/services/remote-desktop/remote-desktop-vk-map.ts +41 -0
- package/src/services/subagent-transcript-merger.ts +92 -75
- package/src/services/subprocess-retention.ts +29 -6
- package/src/services/supervisor.ts +58 -0
- package/src/services/team-member-activity/subagent-transcript-index.ts +56 -0
- package/src/services/windows-process-tree.ts +351 -351
- package/src/types/chat.ts +2 -0
- package/src/web/app.tsx +8 -0
- package/src/web/components/chat/account-dialogs.tsx +11 -4
- package/src/web/components/chat/question-card.tsx +9 -2
- package/src/web/components/chat/usage-badge.tsx +17 -3
- package/src/web/components/floating-window/window-content-registry.ts +2 -0
- package/src/web/components/floating-window/window-store-types.ts +1 -1
- package/src/web/components/layout/mobile-drawer.tsx +38 -27
- package/src/web/components/layout/nav-section-rail.tsx +10 -2
- package/src/web/components/remote-desktop/open-remote-desktop.ts +26 -0
- package/src/web/components/remote-desktop/remote-desktop-coords.ts +98 -0
- package/src/web/components/remote-desktop/remote-desktop-frame-timestamp.ts +21 -0
- package/src/web/components/remote-desktop/remote-desktop-gesture-classifiers.ts +56 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-key-bar.tsx +131 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-sheet.tsx +43 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-toolbar.tsx +79 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-view.tsx +190 -0
- package/src/web/components/remote-desktop/remote-desktop-stats-overlay.tsx +54 -0
- package/src/web/components/remote-desktop/remote-desktop-stats.ts +37 -0
- package/src/web/components/remote-desktop/remote-desktop-touch-gesture-tracker.ts +195 -0
- package/src/web/components/remote-desktop/remote-desktop-window-content.tsx +72 -0
- package/src/web/components/remote-desktop/remote-desktop-ws-url.ts +22 -0
- package/src/web/components/remote-desktop/use-h264-canvas-decoder.ts +175 -0
- package/src/web/components/remote-desktop/use-remote-desktop-available.ts +29 -0
- package/src/web/components/remote-desktop/use-remote-desktop-connection.ts +147 -0
- package/src/web/components/remote-desktop/use-remote-desktop-mobile-open-state.ts +18 -0
- package/src/web/components/remote-desktop/use-remote-desktop-touch.ts +224 -0
- package/src/web/components/remote-desktop/use-remote-desktop-virtual-keyboard.ts +50 -0
- package/src/web/components/remote-desktop/use-remote-input-capture.ts +92 -0
- package/src/web/components/tunnels/named-tunnel/named-tunnel-copy.ts +59 -59
- package/src/web/components/tunnels/named-tunnel/named-tunnel-section.tsx +2 -2
- package/src/web/hooks/use-notification-badge.ts +19 -11
- package/src/web/lib/feature-badges.ts +2 -0
- package/src/web/stores/settings-store.ts +13 -0
- package/src/web/stores/streaming-store.ts +21 -2
- package/dist/web/assets/api-client-BogqsFna.js +0 -1
- package/dist/web/assets/api-settings-DRF9oYkn.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-nw5-j_I_.js +0 -1
- package/dist/web/assets/chat-tab-IcnU3nqQ.js +0 -15
- package/dist/web/assets/code-editor-Dno5Srkm.js +0 -10
- package/dist/web/assets/explorer-body-DB1L30pa.js +0 -2
- package/dist/web/assets/explorer-window-content-Cwlt3C3I.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-C1xf87gN.js +0 -1
- package/dist/web/assets/glide-data-grid-Dz8lx6n2.js +0 -138
- package/dist/web/assets/group-chat-tab-DFwr-pig.js +0 -1
- package/dist/web/assets/index-DWpNR3nB.css +0 -2
- package/dist/web/assets/index-oUKV5nIA.js +0 -52
- package/dist/web/assets/info-3K5VOQVL-C2z7FTDe.js +0 -1
- package/dist/web/assets/keybindings-store-d54pv2NK.js +0 -1
- package/dist/web/assets/markdown-renderer-CnE36_gd.js +0 -1
- package/dist/web/assets/mobile-bottom-sheet-CJ7ApOkI.js +0 -1
- package/dist/web/assets/mobile-explorer-sheet-BzjlS1Xn.js +0 -1
- package/dist/web/assets/notification-store-C7UBqxYP.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-DQUxN0wU.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-BOr9ek4Q.js +0 -1
- package/dist/web/assets/process-table-CXC10GSy.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-DSErxZFb.js +0 -1
- package/dist/web/assets/settings-store-OLJE7dPH.js +0 -2
- package/dist/web/assets/settings-tab-B1rvHFc6.js +0 -3
- package/dist/web/assets/settings-tab-BUlmPJ2w.js +0 -1
- package/dist/web/assets/system-monitor-body-DPYQlBEt.js +0 -2
- package/dist/web/assets/system-monitor-tab-CPQdWv27.js +0 -1
- package/dist/web/assets/team-member-sheet-Bu2wG1sm.js +0 -2
- package/dist/web/assets/treemap-KZPCXAKY-C17p0LQw.js +0 -1
- package/dist/web/assets/window-store-CQ1lghvv.js +0 -1
- /package/dist/web/assets/{abap-CjzrhAS2.js → abap-CKgBNk5K.js} +0 -0
- /package/dist/web/assets/{actionscript-3-B59Kbl-9.js → actionscript-3-oARhAY6c.js} +0 -0
- /package/dist/web/assets/{ada-B7gItKAE.js → ada-2vi8ASy3.js} +0 -0
- /package/dist/web/assets/{andromeeda-Dy5zcy0Q.js → andromeeda-B3vMjPKd.js} +0 -0
- /package/dist/web/assets/{apache-B6T9jrc0.js → apache-DnIEr338.js} +0 -0
- /package/dist/web/assets/{apex-CTsIxDQw.js → apex-B7Qs5AY5.js} +0 -0
- /package/dist/web/assets/{api-client-oY1tC3tM.js → api-client-BkGKTjsq.js} +0 -0
- /package/dist/web/assets/{applescript-DuycKmmb.js → applescript-EbQNUCWb.js} +0 -0
- /package/dist/web/assets/{apply-theme-r8tKDrCR.js → apply-theme-Dd_cqGa-.js} +0 -0
- /package/dist/web/assets/{ara-CISL3599.js → ara-CtHdhFRw.js} +0 -0
- /package/dist/web/assets/{asciidoc-BXyE2Lpr.js → asciidoc-BH6qin_O.js} +0 -0
- /package/dist/web/assets/{asm-y258phtP.js → asm-D1XU1ACk.js} +0 -0
- /package/dist/web/assets/{aurora-x-Dq3Jc78M.js → aurora-x-BkhcxkuS.js} +0 -0
- /package/dist/web/assets/{awk-DxL6XSJv.js → awk-DijzB6o7.js} +0 -0
- /package/dist/web/assets/{ayu-dark-DjoK6Evo.js → ayu-dark-tysG77WC.js} +0 -0
- /package/dist/web/assets/{ayu-light-CKKXBLMI.js → ayu-light-Dg0WMETC.js} +0 -0
- /package/dist/web/assets/{ayu-mirage-DGXSrrXB.js → ayu-mirage--RsJCjC4.js} +0 -0
- /package/dist/web/assets/{ballerina-DZSXFmUf.js → ballerina-DT_5-l82.js} +0 -0
- /package/dist/web/assets/{bat-BNsZk6nY.js → bat-BRpDlkv0.js} +0 -0
- /package/dist/web/assets/{beancount-cPl4gJct.js → beancount--8Df-WGG.js} +0 -0
- /package/dist/web/assets/{berry-DGAodERk.js → berry-Bc9fo35J.js} +0 -0
- /package/dist/web/assets/{bibtex-DftVoRif.js → bibtex-CwyBpp9X.js} +0 -0
- /package/dist/web/assets/{bicep-Suioyyzh.js → bicep-DiyH2Bs7.js} +0 -0
- /package/dist/web/assets/{bird2-DjU24I2P.js → bird2-CHgK4a2A.js} +0 -0
- /package/dist/web/assets/{c-DFi8Y_nN.js → c-BM1qL6-f.js} +0 -0
- /package/dist/web/assets/{c3-1eSHnH5V.js → c3-D2oOjrP7.js} +0 -0
- /package/dist/web/assets/{cadence-DLLsFwQP.js → cadence-nS45wqF6.js} +0 -0
- /package/dist/web/assets/{can-open-in-ppm-in727eFQ.js → can-open-in-ppm-DwP9KU8o.js} +0 -0
- /package/dist/web/assets/{catppuccin-frappe-DDj0JEiY.js → catppuccin-frappe-BpnJHiCj.js} +0 -0
- /package/dist/web/assets/{catppuccin-latte-DPyANHyR.js → catppuccin-latte-Bb95Wj3h.js} +0 -0
- /package/dist/web/assets/{catppuccin-macchiato-CO-h8VTT.js → catppuccin-macchiato-Btx8c17W.js} +0 -0
- /package/dist/web/assets/{catppuccin-mocha-BCmbN2IZ.js → catppuccin-mocha-B4DZOUGI.js} +0 -0
- /package/dist/web/assets/{clarity-C0pYsZjs.js → clarity-1ps69jU6.js} +0 -0
- /package/dist/web/assets/{clipboard-DBLGKMrH.js → clipboard-CrHX8AYw.js} +0 -0
- /package/dist/web/assets/{clojure-JFr6s8iZ.js → clojure-BklC0hKV.js} +0 -0
- /package/dist/web/assets/{cmake-EEcLEgVy.js → cmake-B573LYAQ.js} +0 -0
- /package/dist/web/assets/{codeowners-Deh3Lx0u.js → codeowners-BoYeX89M.js} +0 -0
- /package/dist/web/assets/{codeql-C317igT5.js → codeql-7Dhf5Udk.js} +0 -0
- /package/dist/web/assets/{common-lisp-DepbATZs.js → common-lisp-m1nxlthv.js} +0 -0
- /package/dist/web/assets/{coq-MxgxU5ya.js → coq-DNhGmd4a.js} +0 -0
- /package/dist/web/assets/{csharp-eEFXdsAq.js → csharp-P-nAurtS.js} +0 -0
- /package/dist/web/assets/{css-Db-syWW9.js → css-LYbSeh3-.js} +0 -0
- /package/dist/web/assets/{csv-DHeYcbaq.js → csv-DHZZZm7X.js} +0 -0
- /package/dist/web/assets/{csv-parser-_JWMLGl3.js → csv-parser-CY9Ggtg5.js} +0 -0
- /package/dist/web/assets/{cue-BmUNowrt.js → cue-D8yLUOle.js} +0 -0
- /package/dist/web/assets/{cypher-XjXHgCNS.js → cypher-9cbW8RYA.js} +0 -0
- /package/dist/web/assets/{d-BWJnZtig.js → d-ZihzhxqI.js} +0 -0
- /package/dist/web/assets/{dark-plus-Db7umas1.js → dark-plus-C8xIA1u3.js} +0 -0
- /package/dist/web/assets/{dart-DSI4HYh1.js → dart-CBm6jKc7.js} +0 -0
- /package/dist/web/assets/{data-grid-types-CfVHh9gv.js → data-grid-types-DkEQ-Na9.js} +0 -0
- /package/dist/web/assets/{dax-yw1JsAni.js → dax-B3cx6YeV.js} +0 -0
- /package/dist/web/assets/{desktop-C2QO4RvD.js → desktop-C03SpXth.js} +0 -0
- /package/dist/web/assets/{diff-Drt8aEyp.js → diff-D8LnnvFY.js} +0 -0
- /package/dist/web/assets/{dist-DNRvhVdz.js → dist-C4XoS_zo.js} +0 -0
- /package/dist/web/assets/{dist-VBxOHLmN.js → dist-Do3fljss.js} +0 -0
- /package/dist/web/assets/{docker-ByDKbmLl.js → docker-DwFiOQWW.js} +0 -0
- /package/dist/web/assets/{dotenv-BkfOLytR.js → dotenv-CVJ_vFSQ.js} +0 -0
- /package/dist/web/assets/{dracula-DAmrm-Un.js → dracula-DMw8UACY.js} +0 -0
- /package/dist/web/assets/{dracula-soft-BUOEnKqR.js → dracula-soft-BnB3nDrA.js} +0 -0
- /package/dist/web/assets/{dream-maker-DQBIfSiO.js → dream-maker-2riXUQXo.js} +0 -0
- /package/dist/web/assets/{emacs-lisp-Blh5kBD3.js → emacs-lisp-BQdYaiB5.js} +0 -0
- /package/dist/web/assets/{everforest-dark-CEzhDxHq.js → everforest-dark-C2FPIWdj.js} +0 -0
- /package/dist/web/assets/{everforest-light-CPk_5Khm.js → everforest-light-DMOWf9sD.js} +0 -0
- /package/dist/web/assets/{fennel-B1MT_EBZ.js → fennel-JtSHp0vW.js} +0 -0
- /package/dist/web/assets/{fish-C_HGcewt.js → fish-CflDcRfe.js} +0 -0
- /package/dist/web/assets/{fluent-Dp1Iclmx.js → fluent-D4eu6Vac.js} +0 -0
- /package/dist/web/assets/{format-bytes-53WBtyi2.js → format-bytes-DR3228bu.js} +0 -0
- /package/dist/web/assets/{fortran-free-form-B_-UA4XC.js → fortran-free-form-hO4RuC9Z.js} +0 -0
- /package/dist/web/assets/{gdscript-Bm_JpjS_.js → gdscript-D7n3a8VM.js} +0 -0
- /package/dist/web/assets/{gdshader-C_PNmfYU.js → gdshader-BhUmrlzG.js} +0 -0
- /package/dist/web/assets/{genie-B5rmR2TF.js → genie-VbyftqBp.js} +0 -0
- /package/dist/web/assets/{gherkin-BTJ34TJ9.js → gherkin-BZzhvUUb.js} +0 -0
- /package/dist/web/assets/{github-dark-DOkNVj9H.js → github-dark-C4q41woW.js} +0 -0
- /package/dist/web/assets/{github-dark-default-DTsmiyVo.js → github-dark-default-CMNi7dQX.js} +0 -0
- /package/dist/web/assets/{github-dark-dimmed-CjbxAe9s.js → github-dark-dimmed-BRf_-LlY.js} +0 -0
- /package/dist/web/assets/{github-dark-high-contrast-r_M8F_k7.js → github-dark-high-contrast-xanKBsRI.js} +0 -0
- /package/dist/web/assets/{github-light-BzfxyMyb.js → github-light-BEQeNQCs.js} +0 -0
- /package/dist/web/assets/{github-light-default-BOqiBIy5.js → github-light-default-ceQrtdXg.js} +0 -0
- /package/dist/web/assets/{github-light-high-contrast-DQzBrDM4.js → github-light-high-contrast-doKbXzgW.js} +0 -0
- /package/dist/web/assets/{gleam-BWmuAtD8.js → gleam-Chzqf4fT.js} +0 -0
- /package/dist/web/assets/{gn-CNQPjiZW.js → gn-CCN_-6fp.js} +0 -0
- /package/dist/web/assets/{gnuplot-BlHjcPMe.js → gnuplot-CIplgwwP.js} +0 -0
- /package/dist/web/assets/{go-Bn8Ba9uz.js → go-Cjj-9dRs.js} +0 -0
- /package/dist/web/assets/{groovy-C1Y1_F4V.js → groovy-PqD4K7X1.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-hard-DueOsHav.js → gruvbox-dark-hard-BAbpUSF-.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-medium-DaQjD538.js → gruvbox-dark-medium-BWcWXDBB.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-soft-CbbX2WOm.js → gruvbox-dark-soft-Dkx1okkk.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-hard-E2M70ljZ.js → gruvbox-light-hard-WVsDK0MM.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-medium-B_DDLY6h.js → gruvbox-light-medium-tiR9t7gr.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-soft-2SZmyC1F.js → gruvbox-light-soft-HzHohaX6.js} +0 -0
- /package/dist/web/assets/{haskell-D8DeoXtG.js → haskell-B1eIZfr6.js} +0 -0
- /package/dist/web/assets/{haxe-BmZm7B0W.js → haxe-BcDdi5xr.js} +0 -0
- /package/dist/web/assets/{hcl-CKnBM3ng.js → hcl-Dq-HKVni.js} +0 -0
- /package/dist/web/assets/{hjson-Ct9t4-ci.js → hjson-Dr9D9WVU.js} +0 -0
- /package/dist/web/assets/{hlsl-BZSASBqS.js → hlsl-Bo8t2FuZ.js} +0 -0
- /package/dist/web/assets/{horizon-CzX1AZ08.js → horizon-DWG5IXc-.js} +0 -0
- /package/dist/web/assets/{horizon-bright-CCVNeRIP.js → horizon-bright-DFr9wTTx.js} +0 -0
- /package/dist/web/assets/{houston-DBlJN7xc.js → houston-NAVNhkup.js} +0 -0
- /package/dist/web/assets/{hy-Dyc6h6Xb.js → hy-DnyvUk3B.js} +0 -0
- /package/dist/web/assets/{image-gallery-DujlrjOC.js → image-gallery-DoFdNF9i.js} +0 -0
- /package/dist/web/assets/{imba-Bauru7s2.js → imba-CPbR5W_A.js} +0 -0
- /package/dist/web/assets/{ini-SjdwtaTp.js → ini-lc8_Lt0y.js} +0 -0
- /package/dist/web/assets/{java-D2r0ocpf.js → java-B0KCo2vK.js} +0 -0
- /package/dist/web/assets/{javascript-y-lof7iP.js → javascript-YqEbNRpd.js} +0 -0
- /package/dist/web/assets/{json-DO4LSx5A.js → json-Co0WtOlh.js} +0 -0
- /package/dist/web/assets/{json5-BEwWf-W3.js → json5-2Jhb0QUN.js} +0 -0
- /package/dist/web/assets/{jsonc-OBEFv7gP.js → jsonc-e72F11nU.js} +0 -0
- /package/dist/web/assets/{jsonl-DOyW4Mg3.js → jsonl-Bff8IPye.js} +0 -0
- /package/dist/web/assets/{jsonnet-BL5R2CpP.js → jsonnet-DRCkb-_H.js} +0 -0
- /package/dist/web/assets/{jssm-ksPusAyg.js → jssm-C6UiwVEm.js} +0 -0
- /package/dist/web/assets/{jsx-B6XEu6O-.js → jsx-u1aZlnfA.js} +0 -0
- /package/dist/web/assets/{kanagawa-dragon-JBVe_ji_.js → kanagawa-dragon-De-RQFV3.js} +0 -0
- /package/dist/web/assets/{kanagawa-lotus-ChprO68_.js → kanagawa-lotus-K8elE4QU.js} +0 -0
- /package/dist/web/assets/{kanagawa-wave-D_uB_R0Q.js → kanagawa-wave-BrStl_zx.js} +0 -0
- /package/dist/web/assets/{kdl-pf5B8N_m.js → kdl-CLCfMeZy.js} +0 -0
- /package/dist/web/assets/{kotlin-BdF0METC.js → kotlin-DEEC3pNh.js} +0 -0
- /package/dist/web/assets/{kusto-BneVP-OU.js → kusto-DTqRhCup.js} +0 -0
- /package/dist/web/assets/{laserwave-C4-UPIyG.js → laserwave-DsgpUSbA.js} +0 -0
- /package/dist/web/assets/{lean-BN6Jdymq.js → lean-Dl9MI0Y3.js} +0 -0
- /package/dist/web/assets/{less-Cx-fdlDC.js → less-Bg21rao6.js} +0 -0
- /package/dist/web/assets/{lib-CMuBa85c.js → lib-CXUwxw1M.js} +0 -0
- /package/dist/web/assets/{libesm-BB74PRsH.js → libesm-CVkncAQz.js} +0 -0
- /package/dist/web/assets/{light-plus-DFnnn_iL.js → light-plus-DGNbbK1V.js} +0 -0
- /package/dist/web/assets/{llvm-BGxtW0Nw.js → llvm-91_ZX3p-.js} +0 -0
- /package/dist/web/assets/{loader-circle-Dpqcv0eS.js → loader-circle-DouKgIXy.js} +0 -0
- /package/dist/web/assets/{lock-DldbT31x.js → lock-CNxUGpTk.js} +0 -0
- /package/dist/web/assets/{log-kQb4eJnj.js → log-DSXzbq8u.js} +0 -0
- /package/dist/web/assets/{logo-DqAzaq7s.js → logo-BQRNd4tc.js} +0 -0
- /package/dist/web/assets/{luau-Dc2cp-zS.js → luau-BE-TMpiZ.js} +0 -0
- /package/dist/web/assets/{make-DYg9dwaJ.js → make-ePYGKTpc.js} +0 -0
- /package/dist/web/assets/{markdown-O4EPpS3K.js → markdown-Cst3wgMW.js} +0 -0
- /package/dist/web/assets/{material-theme-ClP8yfQ2.js → material-theme-9XyvnNLb.js} +0 -0
- /package/dist/web/assets/{material-theme-darker-LOK55hGu.js → material-theme-darker-H1VN4MTS.js} +0 -0
- /package/dist/web/assets/{material-theme-lighter-j72tzZqi.js → material-theme-lighter-DAreBbuF.js} +0 -0
- /package/dist/web/assets/{material-theme-ocean-C19LKClA.js → material-theme-ocean-BpTR-3II.js} +0 -0
- /package/dist/web/assets/{material-theme-palenight-DJ5tN2ax.js → material-theme-palenight-DUNzX711.js} +0 -0
- /package/dist/web/assets/{matlab-DQNZ_NU8.js → matlab-BpYNufBU.js} +0 -0
- /package/dist/web/assets/{mdx-CJayB7XK.js → mdx-BYjfe1DK.js} +0 -0
- /package/dist/web/assets/{mermaid-Cb0TRX92.js → mermaid-DPCcIW9B.js} +0 -0
- /package/dist/web/assets/{min-dark-CWM8Bo-m.js → min-dark-DnGOAGCo.js} +0 -0
- /package/dist/web/assets/{min-light-BBR1sjS2.js → min-light-CwV-o31y.js} +0 -0
- /package/dist/web/assets/{mipsasm-Jc7u7YSS.js → mipsasm-DdeCPC97.js} +0 -0
- /package/dist/web/assets/{mojo-CTB0cutd.js → mojo-BmxxZ3iM.js} +0 -0
- /package/dist/web/assets/{monitor-4vWfqzuN.js → monitor-C835ucWH.js} +0 -0
- /package/dist/web/assets/{monokai-CvM_YNKx.js → monokai-Cx6cQ-MA.js} +0 -0
- /package/dist/web/assets/{moonbit-CbFEfYYM.js → moonbit-D2-Gj5vj.js} +0 -0
- /package/dist/web/assets/{move-BT38p0kT.js → move-DSWGoQa8.js} +0 -0
- /package/dist/web/assets/{narrat-55QZ_kOG.js → narrat-D9TnZSSu.js} +0 -0
- /package/dist/web/assets/{nextflow-groovy-Bg8peyqg.js → nextflow-groovy-BbL-8PwD.js} +0 -0
- /package/dist/web/assets/{night-owl-DzS77iPK.js → night-owl-CZitKm5d.js} +0 -0
- /package/dist/web/assets/{night-owl-light-DER7fGnx.js → night-owl-light-DJVX1Pno.js} +0 -0
- /package/dist/web/assets/{nix-BSI8YesN.js → nix-R6hr3oln.js} +0 -0
- /package/dist/web/assets/{nord-9bn7iNWo.js → nord-ChGHya3l.js} +0 -0
- /package/dist/web/assets/{nushell-qDVZn_lw.js → nushell-QQWB-GWx.js} +0 -0
- /package/dist/web/assets/{objective-c-Blf-ATos.js → objective-c-C0pZ6y73.js} +0 -0
- /package/dist/web/assets/{objective-cpp-Bz8bAq47.js → objective-cpp-BdzVM31a.js} +0 -0
- /package/dist/web/assets/{ocaml-B9heAsc3.js → ocaml-D4KktNlS.js} +0 -0
- /package/dist/web/assets/{odin-BaFYnOwO.js → odin-Y-fypmGT.js} +0 -0
- /package/dist/web/assets/{one-dark-pro-D6B_hSe-.js → one-dark-pro-BwecIxQL.js} +0 -0
- /package/dist/web/assets/{one-light-Ba-f0wyk.js → one-light-Cz7UVzLU.js} +0 -0
- /package/dist/web/assets/{openscad-UvYyL2MK.js → openscad-DtRe3qYD.js} +0 -0
- /package/dist/web/assets/{pascal-B4-qvEoG.js → pascal-Ck6I1fGG.js} +0 -0
- /package/dist/web/assets/{pkl-B146nqGx.js → pkl-DdFK94VW.js} +0 -0
- /package/dist/web/assets/{plastic-L38t-Id3.js → plastic-BQSAzZwR.js} +0 -0
- /package/dist/web/assets/{play-CpCAoQRZ.js → play-CJlWpVMw.js} +0 -0
- /package/dist/web/assets/{plsql-ItOG5Cca.js → plsql-DjXg_CP0.js} +0 -0
- /package/dist/web/assets/{po-imiCTaNX.js → po-Bw2SuM4r.js} +0 -0
- /package/dist/web/assets/{poimandres-Bu6sIba9.js → poimandres-YMK6c5VN.js} +0 -0
- /package/dist/web/assets/{polar-CTeGoFxV.js → polar-BMu17-O0.js} +0 -0
- /package/dist/web/assets/{portal-container-context-a399Hd9F.js → portal-container-context-DSGdlXrJ.js} +0 -0
- /package/dist/web/assets/{postcss-jvFe6GNE.js → postcss-DWib10RZ.js} +0 -0
- /package/dist/web/assets/{powerquery-D9Dw1hJp.js → powerquery-Dht4CK0b.js} +0 -0
- /package/dist/web/assets/{powershell-CZqvYQtg.js → powershell-DvrCg8ji.js} +0 -0
- /package/dist/web/assets/{prisma-B4D2nO_7.js → prisma-DbodYvCM.js} +0 -0
- /package/dist/web/assets/{prolog-CQbyUcCF.js → prolog-DZ-m6ysB.js} +0 -0
- /package/dist/web/assets/{proto-Cwpo0cW1.js → proto-HHpG3tGO.js} +0 -0
- /package/dist/web/assets/{puppet-CKxCwBod.js → puppet-BljHe_75.js} +0 -0
- /package/dist/web/assets/{purescript-B-MyXMMp.js → purescript-DzCiFR0B.js} +0 -0
- /package/dist/web/assets/{python-D6QbSR2S.js → python-B5BQ27cC.js} +0 -0
- /package/dist/web/assets/{qmldir-BpTSVcw_.js → qmldir-CYlcXrnn.js} +0 -0
- /package/dist/web/assets/{qss-DBc4gbpe.js → qss-Ce7T_TEg.js} +0 -0
- /package/dist/web/assets/{r-i9jMMi0s.js → r-Bku-2Jp_.js} +0 -0
- /package/dist/web/assets/{racket-BWNaxU7C.js → racket-DtfSL3WR.js} +0 -0
- /package/dist/web/assets/{raku-fqTZLn7L.js → raku-CnDLsBJr.js} +0 -0
- /package/dist/web/assets/{react-X4jUntEf.js → react-Dfio3PfK.js} +0 -0
- /package/dist/web/assets/{red-D-JEcuzA.js → red-vbqcE9b-.js} +0 -0
- /package/dist/web/assets/{refresh-cw-B9iv7csb.js → refresh-cw-84mz8aOE.js} +0 -0
- /package/dist/web/assets/{reg-BbirlU1S.js → reg-DIDLT8-A.js} +0 -0
- /package/dist/web/assets/{regexp-D8VTFrtg.js → regexp-CB4h-VPu.js} +0 -0
- /package/dist/web/assets/{rel-ByQGKiNM.js → rel-CiQEnWXa.js} +0 -0
- /package/dist/web/assets/{resolve-theme-Cqb9zWYc.js → resolve-theme-DPTQHAy1.js} +0 -0
- /package/dist/web/assets/{riscv-B5nqCfLE.js → riscv-R78iq5Rf.js} +0 -0
- /package/dist/web/assets/{ron-DOVh_iZR.js → ron-DhYENxc4.js} +0 -0
- /package/dist/web/assets/{rose-pine-jp51ZUzi.js → rose-pine-5wBeyjfJ.js} +0 -0
- /package/dist/web/assets/{rose-pine-dawn-B7hkmmWy.js → rose-pine-dawn-FpKtPCuH.js} +0 -0
- /package/dist/web/assets/{rose-pine-moon-Ff063yLj.js → rose-pine-moon-BwcIYc7C.js} +0 -0
- /package/dist/web/assets/{rosmsg-Cyk7Il8z.js → rosmsg-DWIGAYMl.js} +0 -0
- /package/dist/web/assets/{rotate-ccw-DnuPzJRE.js → rotate-ccw-CNTk2J7l.js} +0 -0
- /package/dist/web/assets/{rust-DRNM5epF.js → rust-DU_ofKgt.js} +0 -0
- /package/dist/web/assets/{sass-D_cB0-M1.js → sass-U9SUuYEy.js} +0 -0
- /package/dist/web/assets/{save-DxonD4y1.js → save-CtW9ojKk.js} +0 -0
- /package/dist/web/assets/{scala-Crl-1kKS.js → scala-op4gjA6j.js} +0 -0
- /package/dist/web/assets/{scheme-DCaWyEJ0.js → scheme-tEjkR8rL.js} +0 -0
- /package/dist/web/assets/{sdbl-BbXNqwtX.js → sdbl-C6SSRmZg.js} +0 -0
- /package/dist/web/assets/{send-B-t5Mz26.js → send-C_sLFt56.js} +0 -0
- /package/dist/web/assets/{shallow-4zAfs2KW.js → shallow-C1YoJQA-.js} +0 -0
- /package/dist/web/assets/{shellscript-BAgksBhW.js → shellscript-Bf2KiwQj.js} +0 -0
- /package/dist/web/assets/{shield-check-CdTiK1j2.js → shield-check-Du6HKYn9.js} +0 -0
- /package/dist/web/assets/{shield-off-DuetkXKM.js → shield-off-CWKb1MQL.js} +0 -0
- /package/dist/web/assets/{slack-dark-GB-ElUW_.js → slack-dark-oGgCey8r.js} +0 -0
- /package/dist/web/assets/{slack-ochin-tU6CFD3S.js → slack-ochin-CemlB8cE.js} +0 -0
- /package/dist/web/assets/{smalltalk-DSVGUbtL.js → smalltalk-CTE-6jrZ.js} +0 -0
- /package/dist/web/assets/{snazzy-light-DZhi3PoS.js → snazzy-light-DoEIGnmz.js} +0 -0
- /package/dist/web/assets/{solarized-dark-D6AMCco7.js → solarized-dark-CStiJyBg.js} +0 -0
- /package/dist/web/assets/{solarized-light-DepuTWSE.js → solarized-light-CfuNycS9.js} +0 -0
- /package/dist/web/assets/{solidity-CsxN618l.js → solidity-DL6sO5rd.js} +0 -0
- /package/dist/web/assets/{splunk-CF1m0Kkv.js → splunk-yK4ZUFBB.js} +0 -0
- /package/dist/web/assets/{sql-DvyMDgrB.js → sql-Lw03eoN8.js} +0 -0
- /package/dist/web/assets/{ssh-config-a8sDo-Q8.js → ssh-config-DeRp25TR.js} +0 -0
- /package/dist/web/assets/{stylus-DCJ-6AjT.js → stylus-D8OMsPh-.js} +0 -0
- /package/dist/web/assets/{swift-c-ZFxyPP.js → swift-nsl9stPV.js} +0 -0
- /package/dist/web/assets/{synthwave-84-9NPgBcuv.js → synthwave-84-exAgsgpf.js} +0 -0
- /package/dist/web/assets/{system-verilog-DPD02IOo.js → system-verilog-DBWf00T6.js} +0 -0
- /package/dist/web/assets/{systemd-BUr5HRtU.js → systemd-nhu27Dw5.js} +0 -0
- /package/dist/web/assets/{table-FLU8ei8b.js → table-GAR4upb6.js} +0 -0
- /package/dist/web/assets/{talonscript-BNBQV5YJ.js → talonscript-CNOEcGDc.js} +0 -0
- /package/dist/web/assets/{tasl-BETjpSyu.js → tasl-CjYZenSH.js} +0 -0
- /package/dist/web/assets/{tcl-Clp_AZiC.js → tcl-BVA09RDz.js} +0 -0
- /package/dist/web/assets/{terraform-B0kZ0lim.js → terraform-C5EloH1q.js} +0 -0
- /package/dist/web/assets/{text-wrap-B05VGRa-.js → text-wrap-D71Jqz5U.js} +0 -0
- /package/dist/web/assets/{tokyo-night-oOJ0a7Dk.js → tokyo-night-D764DdhO.js} +0 -0
- /package/dist/web/assets/{toml-DOPCSumy.js → toml-CCDYblSB.js} +0 -0
- /package/dist/web/assets/{trash-2-CTXlTflF.js → trash-2-C08fhIg1.js} +0 -0
- /package/dist/web/assets/{triangle-alert-BRq0VAfd.js → triangle-alert-85BQgs3T.js} +0 -0
- /package/dist/web/assets/{tsv-D6qUGf84.js → tsv-BZ4sA05e.js} +0 -0
- /package/dist/web/assets/{tsx-B6qYhT6O.js → tsx-DPLBzAt5.js} +0 -0
- /package/dist/web/assets/{turtle-C0Rjzw73.js → turtle-CwGHCq40.js} +0 -0
- /package/dist/web/assets/{typescript-BiydShEu.js → typescript-CUH0vBAl.js} +0 -0
- /package/dist/web/assets/{typespec-DjmF6Aww.js → typespec-Bq31Vq4u.js} +0 -0
- /package/dist/web/assets/{typst-CPuscfcL.js → typst-MD9BR9W5.js} +0 -0
- /package/dist/web/assets/{use-is-mobile-b94q8RvF.js → use-is-mobile-DUuki97q.js} +0 -0
- /package/dist/web/assets/{utils-C9OjCrou.js → utils-u2fbYPrY.js} +0 -0
- /package/dist/web/assets/{v-DE4vpLLQ.js → v-DZV7IxIT.js} +0 -0
- /package/dist/web/assets/{vala-CmUF2pS5.js → vala-urd1zi9S.js} +0 -0
- /package/dist/web/assets/{vb-BvcRoX4b.js → vb-DIZU3zbJ.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-K8kpLxdb.js → vendor-xterm-DCj0zgSC.js} +0 -0
- /package/dist/web/assets/{verilog-BGIzyH3R.js → verilog-Ndz_ciTA.js} +0 -0
- /package/dist/web/assets/{vesper-CG7IrHQQ.js → vesper-C86M7LJp.js} +0 -0
- /package/dist/web/assets/{vhdl-DMSumomw.js → vhdl-_klwDUjd.js} +0 -0
- /package/dist/web/assets/{viml-Drt7AEmU.js → viml-DzRXWoQh.js} +0 -0
- /package/dist/web/assets/{vitesse-black-DmsjfN0b.js → vitesse-black-TWZ20_3U.js} +0 -0
- /package/dist/web/assets/{vitesse-dark-Dl55_WeJ.js → vitesse-dark-Dl7R6fy9.js} +0 -0
- /package/dist/web/assets/{vitesse-light-Crbv9aGg.js → vitesse-light-Cs0Dt2_n.js} +0 -0
- /package/dist/web/assets/{vyper-CB7psEJj.js → vyper-qnCVj8ia.js} +0 -0
- /package/dist/web/assets/{wasm-Dck8KPgP.js → wasm-Ckx9jBPe.js} +0 -0
- /package/dist/web/assets/{wasm-BY1tuM5m.js → wasm-Dy-SJ3W9.js} +0 -0
- /package/dist/web/assets/{wenyan-DhewQ68b.js → wenyan-DTEy2Lx-.js} +0 -0
- /package/dist/web/assets/{wgsl-DlmiP456.js → wgsl-DC7n63Sy.js} +0 -0
- /package/dist/web/assets/{wikitext-cQCnqNgE.js → wikitext-1zFoZiW6.js} +0 -0
- /package/dist/web/assets/{wit-S5MaCngB.js → wit-DtTfCRph.js} +0 -0
- /package/dist/web/assets/{wolfram-Bh8n_9zr.js → wolfram-B2Bgu1KL.js} +0 -0
- /package/dist/web/assets/{x-YDu4m2Iq.js → x-CeN0Qybc.js} +0 -0
- /package/dist/web/assets/{yaml-BoK_17l9.js → yaml-B-Jxk0IQ.js} +0 -0
- /package/dist/web/assets/{zenscript-DB3ZFwNJ.js → zenscript-BWRfvNER.js} +0 -0
- /package/dist/web/assets/{zig-Bv1BWmfT.js → zig-CkwuvETm.js} +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Groups the raw NAL units from `NalSplitter` into WebCodecs-ready access units.
|
|
3
|
+
*
|
|
4
|
+
* `VideoDecoder` decodes one access unit (one frame) per `EncodedVideoChunk`, not one
|
|
5
|
+
* NAL — feeding SPS/PPS/slice as three separate chunks throws or corrupts decode. libx264
|
|
6
|
+
* (no B-frames, `-bf 0`) emits at most one slice NAL per frame, optionally preceded by
|
|
7
|
+
* SPS/PPS on a keyframe, so an AU boundary is simply "the next VCL NAL after we already
|
|
8
|
+
* buffered one" — everything non-VCL (SPS/PPS/AUD/SEI) that arrived first attaches to the
|
|
9
|
+
* AU it precedes.
|
|
10
|
+
*/
|
|
11
|
+
import { NalSplitter, nalType, isVclNal, NAL_TYPE_IDR, NAL_TYPE_SPS, NAL_TYPE_PPS } from "./nal-splitter.ts";
|
|
12
|
+
|
|
13
|
+
export interface AccessUnit {
|
|
14
|
+
/** Annex-B encoded bytes (start codes + NAL payloads) — one `EncodedVideoChunk` worth. */
|
|
15
|
+
bytes: Uint8Array;
|
|
16
|
+
isKey: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const START_CODE = Uint8Array.of(0, 0, 0, 1);
|
|
20
|
+
|
|
21
|
+
function encodeAnnexB(nals: Uint8Array[]): Uint8Array {
|
|
22
|
+
let total = 0;
|
|
23
|
+
for (const n of nals) total += START_CODE.length + n.length;
|
|
24
|
+
const out = new Uint8Array(total);
|
|
25
|
+
let offset = 0;
|
|
26
|
+
for (const n of nals) {
|
|
27
|
+
out.set(START_CODE, offset);
|
|
28
|
+
offset += START_CODE.length;
|
|
29
|
+
out.set(n, offset);
|
|
30
|
+
offset += n.length;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class AccessUnitAssembler {
|
|
36
|
+
private readonly splitter = new NalSplitter();
|
|
37
|
+
private pending: Uint8Array[] = [];
|
|
38
|
+
private pendingHasVcl = false;
|
|
39
|
+
private sps: Uint8Array | null = null;
|
|
40
|
+
private pps: Uint8Array | null = null;
|
|
41
|
+
|
|
42
|
+
/** Feed a raw ffmpeg stdout chunk; returns zero or more completed access units. */
|
|
43
|
+
push(chunk: Uint8Array): AccessUnit[] {
|
|
44
|
+
const aus: AccessUnit[] = [];
|
|
45
|
+
for (const nal of this.splitter.push(chunk)) {
|
|
46
|
+
const type = nalType(nal);
|
|
47
|
+
if (type === NAL_TYPE_SPS) this.sps = nal;
|
|
48
|
+
else if (type === NAL_TYPE_PPS) this.pps = nal;
|
|
49
|
+
|
|
50
|
+
const vcl = isVclNal(nal);
|
|
51
|
+
if (vcl && this.pendingHasVcl) {
|
|
52
|
+
const au = this.flush();
|
|
53
|
+
if (au) aus.push(au);
|
|
54
|
+
}
|
|
55
|
+
this.pending.push(nal);
|
|
56
|
+
if (vcl) this.pendingHasVcl = true;
|
|
57
|
+
}
|
|
58
|
+
return aus;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** SPS bytes seen so far (NAL header byte included) — used to derive the avc1 codec
|
|
62
|
+
* string. Null until the encoder has emitted its first keyframe. */
|
|
63
|
+
cachedSps(): Uint8Array | null {
|
|
64
|
+
return this.sps;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private flush(): AccessUnit | null {
|
|
68
|
+
if (this.pending.length === 0) {
|
|
69
|
+
this.pendingHasVcl = false;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
let nals = this.pending;
|
|
73
|
+
const isKey = nals.some((n) => nalType(n) === NAL_TYPE_IDR);
|
|
74
|
+
if (isKey) {
|
|
75
|
+
// A late-joining client (or a client that missed the very first AU) needs SPS+PPS
|
|
76
|
+
// concatenated into the same key chunk it decodes — three separate chunks is not
|
|
77
|
+
// decodable Annex-B input for WebCodecs. Normally libx264 already includes them; this
|
|
78
|
+
// is a defensive backfill from the cache, not the common path.
|
|
79
|
+
const hasSps = nals.some((n) => nalType(n) === NAL_TYPE_SPS);
|
|
80
|
+
const hasPps = nals.some((n) => nalType(n) === NAL_TYPE_PPS);
|
|
81
|
+
const prefix: Uint8Array[] = [];
|
|
82
|
+
if (!hasSps && this.sps) prefix.push(this.sps);
|
|
83
|
+
if (!hasPps && this.pps) prefix.push(this.pps);
|
|
84
|
+
if (prefix.length > 0) nals = [...prefix, ...nals];
|
|
85
|
+
}
|
|
86
|
+
this.pending = [];
|
|
87
|
+
this.pendingHasVcl = false;
|
|
88
|
+
return { bytes: encodeAnnexB(nals), isKey };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive the WebCodecs `avc1.PPCCLL` codec string from a real SPS NAL rather than
|
|
3
|
+
* guessing. `encoderArgs()` in `ffmpeg-capabilities.ts` sets no `-profile`/`-level`, so the
|
|
4
|
+
* actual encoded profile/level is encoder-default and unknown until the bitstream exists —
|
|
5
|
+
* a hardcoded guess can mismatch and make `VideoDecoder.configure()` throw or refuse.
|
|
6
|
+
*
|
|
7
|
+
* SPS layout (after the 1-byte NAL header at index 0): profile_idc, then the
|
|
8
|
+
* constraint-flags/reserved byte, then level_idc — always the first three bytes of the
|
|
9
|
+
* RBSP, unaffected by emulation-prevention bytes (those only appear deeper in the SPS).
|
|
10
|
+
*/
|
|
11
|
+
export function avc1CodecString(sps: Uint8Array): string | null {
|
|
12
|
+
if (sps.length < 4) return null;
|
|
13
|
+
const profileIdc = sps[1]!;
|
|
14
|
+
const constraintFlags = sps[2]!;
|
|
15
|
+
const levelIdc = sps[3]!;
|
|
16
|
+
const hex = (n: number) => n.toString(16).padStart(2, "0");
|
|
17
|
+
return `avc1.${hex(profileIdc)}${hex(constraintFlags)}${hex(levelIdc)}`;
|
|
18
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental Annex-B start-code splitter.
|
|
3
|
+
*
|
|
4
|
+
* ffmpeg's `-f h264` stdout is a raw Annex-B byte stream: NAL units separated by
|
|
5
|
+
* `00 00 01` or `00 00 00 01` start codes, with no framing that respects our read
|
|
6
|
+
* chunk boundaries. A NAL can straddle two `stdout` reads, so this holds the tail
|
|
7
|
+
* of the last incomplete NAL across calls to `push()` rather than assuming a full
|
|
8
|
+
* NAL always arrives in one chunk.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** NAL unit types relevant to access-unit assembly (ITU-T H.264 Table 7-1). */
|
|
12
|
+
export const NAL_TYPE_SLICE_NON_IDR = 1;
|
|
13
|
+
export const NAL_TYPE_IDR = 5;
|
|
14
|
+
export const NAL_TYPE_SPS = 7;
|
|
15
|
+
export const NAL_TYPE_PPS = 8;
|
|
16
|
+
|
|
17
|
+
/** Low 5 bits of the NAL header byte (the unit's first byte) is `nal_unit_type`. */
|
|
18
|
+
export function nalType(nal: Uint8Array): number {
|
|
19
|
+
return (nal[0] ?? 0) & 0x1f;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A slice NAL starts a new access unit; SPS/PPS/AUD/SEI attach to whichever AU follows them. */
|
|
23
|
+
export function isVclNal(nal: Uint8Array): boolean {
|
|
24
|
+
const t = nalType(nal);
|
|
25
|
+
return t === NAL_TYPE_SLICE_NON_IDR || t === NAL_TYPE_IDR;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** `Bun.spawn` stdout chunks type as `Uint8Array<ArrayBufferLike>` (could in principle back
|
|
29
|
+
* onto a SharedArrayBuffer), while `new Uint8Array(n)` types as the narrower
|
|
30
|
+
* `Uint8Array<ArrayBuffer>` — pin everything here to the wider type so a plain chunk and our
|
|
31
|
+
* own freshly-allocated buffers can always mix without a type error. */
|
|
32
|
+
export type Bytes = Uint8Array<ArrayBufferLike>;
|
|
33
|
+
|
|
34
|
+
function concat(a: Bytes, b: Bytes): Bytes {
|
|
35
|
+
if (a.length === 0) return b;
|
|
36
|
+
if (b.length === 0) return a;
|
|
37
|
+
const out = new Uint8Array(a.length + b.length);
|
|
38
|
+
out.set(a, 0);
|
|
39
|
+
out.set(b, a.length);
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Index of the next `00 00 01` marker at or after `from`, or -1. This always finds the
|
|
44
|
+
* *last* 3 bytes of a start code — a 4-byte `00 00 00 01` form has its 3-byte suffix match
|
|
45
|
+
* here too (at index+1), and H.264 emulation prevention guarantees `00 00 01`/`00 00 00`
|
|
46
|
+
* never occurs inside real RBSP payload data, so there is no false-positive risk scanning
|
|
47
|
+
* straight through NAL bodies. */
|
|
48
|
+
function findStartCode(buf: Bytes, from: number): number {
|
|
49
|
+
for (let i = from; i + 2 < buf.length; i++) {
|
|
50
|
+
if (buf[i] === 0 && buf[i + 1] === 0 && buf[i + 2] === 1) return i;
|
|
51
|
+
}
|
|
52
|
+
return -1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Bytes to skip past a marker found by `findStartCode` to reach the payload — always 3
|
|
56
|
+
* (the marker itself), regardless of how many extra leading zero bytes preceded it. */
|
|
57
|
+
const MARKER_LEN = 3;
|
|
58
|
+
|
|
59
|
+
/** Guard against an unbounded buffer if ffmpeg ever emits a byte stream with no start
|
|
60
|
+
* code at all (would indicate a broken pipe/format, not a real Annex-B stream). */
|
|
61
|
+
const MAX_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
62
|
+
|
|
63
|
+
export class NalSplitter {
|
|
64
|
+
private buffer: Bytes = new Uint8Array(0);
|
|
65
|
+
|
|
66
|
+
/** Feed a raw stdout chunk; returns zero or more complete NAL payloads (start code
|
|
67
|
+
* stripped). Incomplete trailing bytes are retained for the next call. */
|
|
68
|
+
push(chunk: Bytes): Bytes[] {
|
|
69
|
+
this.buffer = concat(this.buffer, chunk);
|
|
70
|
+
const nals: Bytes[] = [];
|
|
71
|
+
|
|
72
|
+
let start = findStartCode(this.buffer, 0);
|
|
73
|
+
if (start === -1) {
|
|
74
|
+
if (this.buffer.length > MAX_BUFFER_BYTES) this.buffer = new Uint8Array(0);
|
|
75
|
+
return nals;
|
|
76
|
+
}
|
|
77
|
+
// Drop any garbage before the first start code (should not happen with a clean
|
|
78
|
+
// ffmpeg pipe, but a partial NAL from before this splitter attached must not corrupt
|
|
79
|
+
// the first parsed unit).
|
|
80
|
+
for (;;) {
|
|
81
|
+
const nextStart = findStartCode(this.buffer, start + MARKER_LEN);
|
|
82
|
+
if (nextStart === -1) {
|
|
83
|
+
this.buffer = this.buffer.slice(start);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
// `nextStart` only marks the *last* 3 bytes of the following start code — a 4-byte
|
|
87
|
+
// `00 00 00 01` form leaves its leading zero sitting right before `nextStart`, which
|
|
88
|
+
// would otherwise get appended to this NAL's payload as a spurious trailing byte.
|
|
89
|
+
// Trim any such zero bytes back to the real payload end (standard Annex-B handling of
|
|
90
|
+
// `leading_zero_8bits`/`trailing_zero_8bits`).
|
|
91
|
+
let payloadEnd = nextStart;
|
|
92
|
+
const payloadStart = start + MARKER_LEN;
|
|
93
|
+
while (payloadEnd > payloadStart && this.buffer[payloadEnd - 1] === 0) payloadEnd--;
|
|
94
|
+
const payload = this.buffer.slice(payloadStart, payloadEnd);
|
|
95
|
+
if (payload.length > 0) nals.push(payload);
|
|
96
|
+
start = nextStart;
|
|
97
|
+
}
|
|
98
|
+
return nals;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn ffmpeg (`gdigrab` → H.264 Annex-B on `pipe:1`) and turn its stdout into access units.
|
|
3
|
+
*
|
|
4
|
+
* Teardown is `proc.kill()` only — never `reader.cancel()`, never `for await (chunk of
|
|
5
|
+
* proc.stdout) { ... break }`. Both of those cancel the underlying stream reader, which
|
|
6
|
+
* segfaults Bun 1.3.x on Windows the instant a consumer goes away (see
|
|
7
|
+
* `transcode-stream.ts:130-148` and `tests/integration/transcode-stream-client-disconnect.test.ts`
|
|
8
|
+
* — that guard targets a `Response` body; the WS rewrite here re-creates the same trap one
|
|
9
|
+
* layer down if the pump loop is written as `for await`). `stop()` only calls `proc.kill()`;
|
|
10
|
+
* the in-flight `reader.read()` then resolves with `done: true` and the pump loop exits on
|
|
11
|
+
* its own — it is never cancelled from outside.
|
|
12
|
+
*/
|
|
13
|
+
import { getFfmpegCapabilities } from "../media-transcode/ffmpeg-capabilities.ts";
|
|
14
|
+
import { captureEncoderArgs, CAPTURE_FRAMERATE } from "./remote-desktop-encoder-args.ts";
|
|
15
|
+
import { AccessUnitAssembler, type AccessUnit } from "./access-unit-assembler.ts";
|
|
16
|
+
|
|
17
|
+
export class CaptureUnavailableError extends Error {
|
|
18
|
+
constructor(msg = "ffmpeg is not installed (gdigrab capture requires it)") {
|
|
19
|
+
super(msg);
|
|
20
|
+
this.name = "CaptureUnavailableError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Build the gdigrab argv; pure so it can be asserted on without spawning a process.
|
|
25
|
+
* `-fflags nobuffer -flags low_delay` + `-flush_packets 1` stop ffmpeg from holding frames in
|
|
26
|
+
* its demux/mux buffers before emitting — on a fast/LAN transport that buffering is a big slice
|
|
27
|
+
* of the felt lag. `encoder` selects the H.264 encoder args (hardware NVENC/QSV/AMF when the
|
|
28
|
+
* capability probe found one, else libx264). */
|
|
29
|
+
export function buildCaptureArgs(ffmpeg: string, encoder: string = "libx264"): string[] {
|
|
30
|
+
return [
|
|
31
|
+
ffmpeg, "-hide_banner", "-loglevel", "error", "-nostdin",
|
|
32
|
+
"-fflags", "nobuffer", "-flags", "low_delay",
|
|
33
|
+
"-f", "gdigrab", "-framerate", String(CAPTURE_FRAMERATE), "-i", "desktop",
|
|
34
|
+
"-vf", "scale=-2:720",
|
|
35
|
+
...captureEncoderArgs(encoder),
|
|
36
|
+
"-flush_packets", "1",
|
|
37
|
+
"-f", "h264", "pipe:1",
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CaptureHandle {
|
|
42
|
+
/** SPS bytes cached from the stream (for the avc1 codec string), null before the encoder's
|
|
43
|
+
* first keyframe has been parsed. */
|
|
44
|
+
cachedSps(): Uint8Array | null;
|
|
45
|
+
/** Kill ffmpeg. Idempotent, safe to call from multiple teardown paths. */
|
|
46
|
+
stop(): void;
|
|
47
|
+
/** True once `stop()` has run or the process exited on its own. */
|
|
48
|
+
isStopped(): boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface StartCaptureOptions {
|
|
52
|
+
onAccessUnit: (au: AccessUnit) => void;
|
|
53
|
+
/** Called once the process exits, whether via `stop()` or on its own (crash/killed
|
|
54
|
+
* externally) — lets the session registry clean up without polling. `reason` is a short
|
|
55
|
+
* stderr tail, present only when ffmpeg died on its own (e.g. gdigrab access denied on a
|
|
56
|
+
* disconnected session) so the caller can surface *why* to the client instead of a bare
|
|
57
|
+
* disconnect. */
|
|
58
|
+
onExit?: (code: number | null, reason?: string) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Start gdigrab capture. Rejects immediately if ffmpeg is not on PATH. */
|
|
62
|
+
export async function startCapture(opts: StartCaptureOptions): Promise<CaptureHandle> {
|
|
63
|
+
const caps = await getFfmpegCapabilities();
|
|
64
|
+
if (!caps.ffmpeg) throw new CaptureUnavailableError();
|
|
65
|
+
|
|
66
|
+
const proc = Bun.spawn(buildCaptureArgs(caps.ffmpeg, caps.encoder ?? "libx264"), {
|
|
67
|
+
stdout: "pipe",
|
|
68
|
+
stderr: "pipe",
|
|
69
|
+
stdin: "ignore",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
let stopped = false;
|
|
73
|
+
const stop = () => {
|
|
74
|
+
if (stopped) return;
|
|
75
|
+
stopped = true;
|
|
76
|
+
if (!proc.killed) proc.kill();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Drain stderr so ffmpeg never blocks on a full pipe; keep a short tail for diagnostics.
|
|
80
|
+
const stderrTail = new Response(proc.stderr).text().catch(() => "");
|
|
81
|
+
proc.exited.then(async (code) => {
|
|
82
|
+
// Read `stopped` BEFORE overwriting it: this is the only reliable "did WE ask ffmpeg to
|
|
83
|
+
// die" signal. `proc.killed` is NOT reliable for that — Bun sets it `true` even when
|
|
84
|
+
// ffmpeg exits on its own with a nonzero code (verified: gdigrab "access denied" on a
|
|
85
|
+
// disconnected session exits with `killed: true` despite `stop()` never having been
|
|
86
|
+
// called), which previously suppressed this warning — and any client-facing error —
|
|
87
|
+
// for exactly the crash this exists to report.
|
|
88
|
+
const diedOnItsOwn = !stopped;
|
|
89
|
+
stopped = true;
|
|
90
|
+
let reason: string | undefined;
|
|
91
|
+
if (diedOnItsOwn && code !== 0 && code !== null) {
|
|
92
|
+
reason = (await stderrTail).trim().split("\n").slice(-3).join(" | ");
|
|
93
|
+
console.warn(`[remote-desktop] ffmpeg exited ${code}: ${reason}`);
|
|
94
|
+
}
|
|
95
|
+
opts.onExit?.(code, reason);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const assembler = new AccessUnitAssembler();
|
|
99
|
+
|
|
100
|
+
// Manual pull loop — the only safe way to drain this stdout (see file header). Runs
|
|
101
|
+
// detached from the caller; it terminates itself once `proc.kill()` resolves the pending
|
|
102
|
+
// read with `done: true`, never via an external cancel.
|
|
103
|
+
const reader = proc.stdout.getReader();
|
|
104
|
+
(async () => {
|
|
105
|
+
for (;;) {
|
|
106
|
+
const { done, value } = await reader.read();
|
|
107
|
+
if (done) return;
|
|
108
|
+
if (!value) continue;
|
|
109
|
+
for (const au of assembler.push(value)) opts.onAccessUnit(au);
|
|
110
|
+
}
|
|
111
|
+
})().catch((e) => {
|
|
112
|
+
console.error(`[remote-desktop] capture pump failed: ${(e as Error).message}`);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
cachedSps: () => assembler.cachedSps(),
|
|
117
|
+
stop,
|
|
118
|
+
isStopped: () => stopped,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture-tuned libx264 args — deliberately NOT `encoderArgs()` from `ffmpeg-capabilities.ts`.
|
|
3
|
+
* That table is tuned for "watch a file" (nvenc VBR, libx264 `-crf 23 -maxrate 8M -bufsize
|
|
4
|
+
* 16M`), which fights a real-time capture: large bufsize/VBR inflates latency spikes exactly
|
|
5
|
+
* where `-tune zerolatency` is trying to remove them.
|
|
6
|
+
*
|
|
7
|
+
* libx264 is forced (never the hardware picks from `ffmpeg-capabilities.ts`) so profile/level
|
|
8
|
+
* behavior is deterministic across machines — the actual codec string is still derived from
|
|
9
|
+
* the real SPS bytes at runtime (`avc1-codec-string.ts`), this just keeps that derivation
|
|
10
|
+
* predictable during development.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** 30fps + a 1s keyframe interval (30 frames): smoother than 15fps and bounds how long a
|
|
14
|
+
* dropped/late-joining client waits to resync (see WS backpressure handling). */
|
|
15
|
+
export const CAPTURE_FRAMERATE = 30;
|
|
16
|
+
export const CAPTURE_GOP_FRAMES = 30;
|
|
17
|
+
|
|
18
|
+
/** ffmpeg output args for the H.264 encode step (goes after `-i desktop -vf ...`).
|
|
19
|
+
* Prefers hardware NVENC (verified to work even under an RDP session on this class of GPU) —
|
|
20
|
+
* hardware encode adds ~5–15ms vs 30–100ms for libx264, the dominant slice of the felt lag on
|
|
21
|
+
* a fast/LAN transport. Falls back to a low-latency libx264 profile when no hardware H.264
|
|
22
|
+
* encoder is available. The avc1 codec string is derived from the real SPS at runtime, so the
|
|
23
|
+
* encoder's actual profile/level is handled regardless of which branch runs. */
|
|
24
|
+
export function captureEncoderArgs(encoder: string = "libx264"): string[] {
|
|
25
|
+
const common = ["-b:v", "6M", "-maxrate", "6M", "-g", String(CAPTURE_GOP_FRAMES), "-bf", "0", "-pix_fmt", "yuv420p"];
|
|
26
|
+
switch (encoder) {
|
|
27
|
+
case "h264_nvenc":
|
|
28
|
+
// p2 + `-tune ll` (low latency), CBR, no frame reordering, minimal output delay/VBV.
|
|
29
|
+
return ["-c:v", "h264_nvenc", "-preset", "p2", "-tune", "ll", "-rc", "cbr",
|
|
30
|
+
"-zerolatency", "1", "-delay", "0", "-bufsize", "512k", ...common];
|
|
31
|
+
case "h264_qsv":
|
|
32
|
+
return ["-c:v", "h264_qsv", "-preset", "veryfast", "-low_power", "1", "-bufsize", "512k", ...common];
|
|
33
|
+
case "h264_amf":
|
|
34
|
+
return ["-c:v", "h264_amf", "-quality", "speed", "-rc", "cbr", "-bufsize", "512k", ...common];
|
|
35
|
+
default:
|
|
36
|
+
// libx264 low-latency. `-tune zerolatency` enables sliced-threads (multi-slice NALs);
|
|
37
|
+
// access-unit-assembler.ts assumes one slice/frame, so force it back with sliced-threads=0.
|
|
38
|
+
return ["-c:v", "libx264", "-preset", "veryfast", "-tune", "zerolatency",
|
|
39
|
+
"-profile:v", "main", "-level", "4.0", "-x264-params", "sliced-threads=0:slices=1",
|
|
40
|
+
"-bufsize", "1M", ...common];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature flag gate. A live video+input channel to the host must never be reachable just
|
|
3
|
+
* because the code shipped — default OFF until phase-07 hardening (host-approval prompt,
|
|
4
|
+
* signed input path) lands. Every entry point (REST routes, WS upgrade) checks this
|
|
5
|
+
* independently rather than trusting a single call site.
|
|
6
|
+
*/
|
|
7
|
+
export function isRemoteDesktopEnabled(): boolean {
|
|
8
|
+
const v = process.env.REMOTE_DESKTOP_ENABLED;
|
|
9
|
+
return v === "1" || v === "true";
|
|
10
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mouse/keyboard injection via Bun FFI straight into `user32.dll` `SendInput` — no separate
|
|
3
|
+
* helper process (drops the Rust-binary build step + unsigned-exe/SmartScreen risk the phase
|
|
4
|
+
* plan flagged for a bundled injector; phase 02's SYSTEM-session helper is a different
|
|
5
|
+
* process/token anyway, so nothing here is meant to be reused as-is).
|
|
6
|
+
*
|
|
7
|
+
* Normal desktop only, current interactive session, no SYSTEM/UAC/lock-screen support.
|
|
8
|
+
*
|
|
9
|
+
* `bun:ffi` is only ever `import()`-ed and `dlopen`-ed lazily from inside `loadUser32()`, so
|
|
10
|
+
* merely importing this module (routing, types) on Linux/macOS test runners never touches
|
|
11
|
+
* `user32.dll`.
|
|
12
|
+
*/
|
|
13
|
+
import { codeToVk, MODIFIER_VK_CODES } from "./remote-desktop-vk-map.ts";
|
|
14
|
+
|
|
15
|
+
export class RemoteInputUnavailableError extends Error {
|
|
16
|
+
constructor(msg = "Remote input injection is only available on Windows") {
|
|
17
|
+
super(msg);
|
|
18
|
+
this.name = "RemoteInputUnavailableError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const INPUT_MOUSE = 0;
|
|
23
|
+
const INPUT_KEYBOARD = 1;
|
|
24
|
+
const MOUSEEVENTF_MOVE = 0x0001;
|
|
25
|
+
const MOUSEEVENTF_LEFTDOWN = 0x0002;
|
|
26
|
+
const MOUSEEVENTF_LEFTUP = 0x0004;
|
|
27
|
+
const MOUSEEVENTF_RIGHTDOWN = 0x0008;
|
|
28
|
+
const MOUSEEVENTF_RIGHTUP = 0x0010;
|
|
29
|
+
const MOUSEEVENTF_ABSOLUTE = 0x8000;
|
|
30
|
+
const MOUSEEVENTF_VIRTUALDESK = 0x4000;
|
|
31
|
+
const MOUSEEVENTF_WHEEL = 0x0800;
|
|
32
|
+
/** One notch of a real mouse wheel — `mouseData` for `MOUSEEVENTF_WHEEL` is a signed multiple
|
|
33
|
+
* of this constant, not a raw pixel delta. */
|
|
34
|
+
const WHEEL_DELTA = 120;
|
|
35
|
+
const KEYEVENTF_KEYUP = 0x0002;
|
|
36
|
+
/** `((DPI_AWARENESS_CONTEXT)-4)` — PER_MONITOR_AWARE_V2. Not a real pointer; the x64 ABI
|
|
37
|
+
* passes it in the same register/slot as a pointer arg would, bit pattern only. */
|
|
38
|
+
const DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4n;
|
|
39
|
+
/** Each `INPUT` struct is 40 bytes on x64 (8-byte `type` slot + 32-byte union). */
|
|
40
|
+
const INPUT_STRUCT_SIZE = 40;
|
|
41
|
+
/** Desktop access rights for input injection. `SendInput` silently no-ops when the calling
|
|
42
|
+
* thread is not attached to the *input* desktop; attaching needs the READ/WRITE/SWITCH rights
|
|
43
|
+
* plus JOURNALPLAYBACK so the desktop accepts injected input. These MUST be the real DESKTOP_*
|
|
44
|
+
* bit values — an invalid mask (e.g. an out-of-range bit) makes `OpenInputDesktop` return NULL
|
|
45
|
+
* and the attach silently fails, leaving SendInput pointed at the wrong desktop. */
|
|
46
|
+
const DESKTOP_READOBJECTS = 0x0001;
|
|
47
|
+
const DESKTOP_JOURNALPLAYBACK = 0x0020;
|
|
48
|
+
const DESKTOP_WRITEOBJECTS = 0x0080;
|
|
49
|
+
const DESKTOP_SWITCHDESKTOP = 0x0100;
|
|
50
|
+
const INPUT_DESKTOP_ACCESS =
|
|
51
|
+
DESKTOP_READOBJECTS | DESKTOP_JOURNALPLAYBACK | DESKTOP_WRITEOBJECTS | DESKTOP_SWITCHDESKTOP; // 0x1A1
|
|
52
|
+
|
|
53
|
+
/** WINSTA_ALL_ACCESS — enough to open the interactive window station and make it this process's
|
|
54
|
+
* station so its input desktop becomes reachable. */
|
|
55
|
+
const WINSTA_ALL_ACCESS = 0x37f;
|
|
56
|
+
|
|
57
|
+
type FfiModule = typeof import("bun:ffi");
|
|
58
|
+
type User32Symbols = {
|
|
59
|
+
SendInput: (count: number, ptr: number | bigint, size: number) => number;
|
|
60
|
+
SetProcessDpiAwarenessContext: (ctx: bigint) => boolean;
|
|
61
|
+
OpenInputDesktop: (flags: number, inherit: boolean, access: number) => number | bigint;
|
|
62
|
+
SetThreadDesktop: (hdesk: number | bigint) => boolean;
|
|
63
|
+
CloseDesktop: (hdesk: number | bigint) => boolean;
|
|
64
|
+
OpenWindowStationW: (name: number | bigint, inherit: boolean, access: number) => number | bigint;
|
|
65
|
+
SetProcessWindowStation: (hwinsta: number | bigint) => boolean;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let ffiModule: FfiModule | null = null;
|
|
69
|
+
let user32: User32Symbols | null = null;
|
|
70
|
+
let dpiAwarenessAttempted = false;
|
|
71
|
+
|
|
72
|
+
async function loadUser32(): Promise<{ ffi: FfiModule; lib: User32Symbols }> {
|
|
73
|
+
if (process.platform !== "win32") throw new RemoteInputUnavailableError();
|
|
74
|
+
if (!ffiModule) ffiModule = await import("bun:ffi");
|
|
75
|
+
const { dlopen, FFIType } = ffiModule;
|
|
76
|
+
if (!user32) {
|
|
77
|
+
user32 = dlopen("user32.dll", {
|
|
78
|
+
SendInput: { args: [FFIType.u32, FFIType.ptr, FFIType.i32], returns: FFIType.u32 },
|
|
79
|
+
SetProcessDpiAwarenessContext: { args: [FFIType.i64], returns: FFIType.bool },
|
|
80
|
+
OpenInputDesktop: { args: [FFIType.u32, FFIType.bool, FFIType.u32], returns: FFIType.ptr },
|
|
81
|
+
SetThreadDesktop: { args: [FFIType.ptr], returns: FFIType.bool },
|
|
82
|
+
CloseDesktop: { args: [FFIType.ptr], returns: FFIType.bool },
|
|
83
|
+
OpenWindowStationW: { args: [FFIType.ptr, FFIType.bool, FFIType.u32], returns: FFIType.ptr },
|
|
84
|
+
SetProcessWindowStation: { args: [FFIType.ptr], returns: FFIType.bool },
|
|
85
|
+
}).symbols as unknown as User32Symbols;
|
|
86
|
+
}
|
|
87
|
+
if (!dpiAwarenessAttempted) {
|
|
88
|
+
dpiAwarenessAttempted = true;
|
|
89
|
+
// A second call to this in the same process returns E_ACCESSDENIED — swallow it, it just
|
|
90
|
+
// means something else (or a previous session) already set it.
|
|
91
|
+
try { user32.SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); } catch { /* already set */ }
|
|
92
|
+
}
|
|
93
|
+
return { ffi: ffiModule, lib: user32 };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function writeMouseInput(dv: DataView, offset: number, xAbs: number, yAbs: number, flags: number, mouseData = 0): void {
|
|
97
|
+
dv.setUint32(offset, INPUT_MOUSE, true);
|
|
98
|
+
dv.setInt32(offset + 8, xAbs, true);
|
|
99
|
+
dv.setInt32(offset + 12, yAbs, true);
|
|
100
|
+
dv.setInt32(offset + 16, mouseData, true); // wheel delta for MOUSEEVENTF_WHEEL, unused otherwise
|
|
101
|
+
dv.setUint32(offset + 20, flags >>> 0, true);
|
|
102
|
+
dv.setUint32(offset + 24, 0, true); // time: 0 lets the system supply it
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function writeKeyInput(dv: DataView, offset: number, vk: number, keyUp: boolean): void {
|
|
106
|
+
dv.setUint32(offset, INPUT_KEYBOARD, true);
|
|
107
|
+
dv.setUint16(offset + 8, vk, true);
|
|
108
|
+
dv.setUint16(offset + 10, 0, true); // wScan — unused, we inject by VK not scan code
|
|
109
|
+
dv.setUint32(offset + 12, keyUp ? KEYEVENTF_KEYUP : 0, true);
|
|
110
|
+
dv.setUint32(offset + 16, 0, true); // time
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Handle of the desktop this thread is currently attached to, so we can close the previous one
|
|
114
|
+
* when the active input desktop changes (lock/unlock/UAC switches it). One handle in flight. */
|
|
115
|
+
let attachedDesktop: number | bigint | null = null;
|
|
116
|
+
|
|
117
|
+
/** Attach the calling thread to the *current* input desktop before injecting. Re-run every send
|
|
118
|
+
* because the input desktop changes on lock/unlock; `CreateProcessAsUser`/process start only
|
|
119
|
+
* fixes the initial desktop, not later switches. Best-effort: on failure we still try SendInput
|
|
120
|
+
* (it just no-ops), and log once rather than per-event spam. */
|
|
121
|
+
/** Attach the PROCESS to the interactive window station ("winsta0") once, so its input desktop
|
|
122
|
+
* is reachable. Without this, a PPM process launched from a non-interactive station (a service,
|
|
123
|
+
* or some spawners) can't OpenInputDesktop at all — SendInput then goes nowhere. No-op when the
|
|
124
|
+
* process is already on winsta0 (the normal case for PPM started inside the user's session). */
|
|
125
|
+
let stationEnsured = false;
|
|
126
|
+
function ensureInteractiveStation(ffi: FfiModule, lib: User32Symbols): void {
|
|
127
|
+
if (stationEnsured) return;
|
|
128
|
+
stationEnsured = true;
|
|
129
|
+
const name = "winsta0";
|
|
130
|
+
const wide = new Uint16Array(name.length + 1);
|
|
131
|
+
for (let i = 0; i < name.length; i++) wide[i] = name.charCodeAt(i);
|
|
132
|
+
const hwinsta = lib.OpenWindowStationW(ffi.ptr(wide), false, WINSTA_ALL_ACCESS);
|
|
133
|
+
if (!hwinsta) return; // already on it, or no access — OpenInputDesktop below will tell us
|
|
134
|
+
lib.SetProcessWindowStation(hwinsta);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function attachToInputDesktop(ffi: FfiModule, lib: User32Symbols): void {
|
|
138
|
+
ensureInteractiveStation(ffi, lib);
|
|
139
|
+
const hdesk = lib.OpenInputDesktop(0, false, INPUT_DESKTOP_ACCESS);
|
|
140
|
+
if (!hdesk) return; // couldn't open the input desktop (e.g. secure/lock desktop); leave thread as-is
|
|
141
|
+
const ok = lib.SetThreadDesktop(hdesk);
|
|
142
|
+
if (ok) {
|
|
143
|
+
if (attachedDesktop && attachedDesktop !== hdesk) {
|
|
144
|
+
try { lib.CloseDesktop(attachedDesktop); } catch { /* ignore */ }
|
|
145
|
+
}
|
|
146
|
+
attachedDesktop = hdesk;
|
|
147
|
+
} else {
|
|
148
|
+
// SetThreadDesktop fails if this thread owns windows/hooks — shouldn't for the server thread.
|
|
149
|
+
try { lib.CloseDesktop(hdesk); } catch { /* ignore */ }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function sendRaw(buf: Uint8Array, count: number): Promise<void> {
|
|
154
|
+
const { ffi, lib } = await loadUser32();
|
|
155
|
+
attachToInputDesktop(ffi, lib);
|
|
156
|
+
const sent = lib.SendInput(count, ffi.ptr(buf), INPUT_STRUCT_SIZE);
|
|
157
|
+
if (sent !== count) console.warn(`[remote-desktop] SendInput accepted ${sent}/${count} events`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Move the cursor and, if this is a click, press/release the button — one absolute move
|
|
161
|
+
* per call so the button acts at the position the client actually clicked. `xFrac`/`yFrac`
|
|
162
|
+
* are 0..1 fractions of the capture (client already dropped devicePixelRatio; only the
|
|
163
|
+
* canvas-relative fraction is meaningful for host coordinates). */
|
|
164
|
+
export async function injectPointer(
|
|
165
|
+
xFrac: number,
|
|
166
|
+
yFrac: number,
|
|
167
|
+
button: "left" | "right" | null,
|
|
168
|
+
down: boolean | null,
|
|
169
|
+
): Promise<void> {
|
|
170
|
+
const xAbs = Math.round(Math.min(Math.max(xFrac, 0), 1) * 65535);
|
|
171
|
+
const yAbs = Math.round(Math.min(Math.max(yFrac, 0), 1) * 65535);
|
|
172
|
+
let flags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK;
|
|
173
|
+
if (button === "left") flags |= down ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP;
|
|
174
|
+
else if (button === "right") flags |= down ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP;
|
|
175
|
+
|
|
176
|
+
const buf = new Uint8Array(INPUT_STRUCT_SIZE);
|
|
177
|
+
writeMouseInput(new DataView(buf.buffer), 0, xAbs, yAbs, flags);
|
|
178
|
+
await sendRaw(buf, 1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Scroll the wheel. `deltaY` is in `WHEEL_DELTA` (120) units — positive rotates the wheel
|
|
182
|
+
* forward (scrolls up/away from the user), negative rotates backward (scrolls down). The
|
|
183
|
+
* client accumulates raw two-finger-drag pixels into this unit before sending, so this layer
|
|
184
|
+
* stays a dumb relay: round + clamp to a safe i16 range so a runaway client value can't wrap
|
|
185
|
+
* into a huge or negative-looking mouseData field. */
|
|
186
|
+
export async function injectWheel(deltaY: number): Promise<void> {
|
|
187
|
+
const clamped = Math.max(-32768, Math.min(32767, Math.round(deltaY)));
|
|
188
|
+
if (clamped === 0) return;
|
|
189
|
+
const buf = new Uint8Array(INPUT_STRUCT_SIZE);
|
|
190
|
+
writeMouseInput(new DataView(buf.buffer), 0, 0, 0, MOUSEEVENTF_WHEEL, clamped);
|
|
191
|
+
await sendRaw(buf, 1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Send a keydown/keyup by physical key code. Returns false for an unmapped code (caller
|
|
195
|
+
* should just drop it rather than injecting a wrong key). */
|
|
196
|
+
export async function injectKey(code: string, down: boolean): Promise<boolean> {
|
|
197
|
+
const vk = codeToVk(code);
|
|
198
|
+
if (vk === null) return false;
|
|
199
|
+
const buf = new Uint8Array(INPUT_STRUCT_SIZE);
|
|
200
|
+
writeKeyInput(new DataView(buf.buffer), 0, vk, !down);
|
|
201
|
+
await sendRaw(buf, 1);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Force keyup for every modifier VK, regardless of tracked state — the backstop for a lost
|
|
206
|
+
* keyup (blur, WS drop) leaving Shift/Ctrl/Alt/Win logically held on the host. */
|
|
207
|
+
export async function releaseAllModifiers(): Promise<void> {
|
|
208
|
+
if (process.platform !== "win32") return;
|
|
209
|
+
const buf = new Uint8Array(INPUT_STRUCT_SIZE * MODIFIER_VK_CODES.length);
|
|
210
|
+
const dv = new DataView(buf.buffer);
|
|
211
|
+
MODIFIER_VK_CODES.forEach((vk, i) => writeKeyInput(dv, i * INPUT_STRUCT_SIZE, vk, true));
|
|
212
|
+
try { await sendRaw(buf, MODIFIER_VK_CODES.length); } catch { /* best-effort */ }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function isInputAvailable(): boolean {
|
|
216
|
+
return process.platform === "win32";
|
|
217
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-use, short-TTL connection nonce — mirrors `download-token.service.ts`'s
|
|
3
|
+
* consume-once pattern.
|
|
4
|
+
*
|
|
5
|
+
* The PPM static bearer token already travels as `?token=` on every WS upgrade
|
|
6
|
+
* (`isWsUpgradeAuthorized`, `src/server/index.ts`) and proves nothing beyond "holds the one
|
|
7
|
+
* reusable app token" — it is not a real per-session grant. `POST /api/remote-desktop/session`
|
|
8
|
+
* mints one of these after re-checking `auth.enabled`; the client then presents it via the WS
|
|
9
|
+
* subprotocol header (never the query string) and it is consumed exactly once, at upgrade
|
|
10
|
+
* time, in `src/server/index.ts`.
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUIDv7 } from "bun";
|
|
13
|
+
|
|
14
|
+
const TTL_MS = 30_000;
|
|
15
|
+
const nonces = new Map<string, number>();
|
|
16
|
+
|
|
17
|
+
/** Mint a nonce; caller must have already verified auth is enabled + a valid request. */
|
|
18
|
+
export function mintRemoteDesktopNonce(): string {
|
|
19
|
+
const token = randomUUIDv7();
|
|
20
|
+
nonces.set(token, Date.now());
|
|
21
|
+
cleanup();
|
|
22
|
+
return token;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Validate + consume. Returns false for missing/expired/already-used nonces. */
|
|
26
|
+
export function consumeRemoteDesktopNonce(token: string): boolean {
|
|
27
|
+
const mintedAt = nonces.get(token);
|
|
28
|
+
if (mintedAt === undefined) return false;
|
|
29
|
+
nonces.delete(token);
|
|
30
|
+
return Date.now() - mintedAt <= TTL_MS;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function cleanup(): void {
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
for (const [token, mintedAt] of nonces) {
|
|
36
|
+
if (now - mintedAt > TTL_MS) nonces.delete(token);
|
|
37
|
+
}
|
|
38
|
+
}
|