@hienlh/ppm 0.23.11 → 0.23.12
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 +18 -0
- package/CLAUDE.md +13 -2
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +4 -4
- package/assets/skills/ppm/references/http-api.md +37 -1
- package/bun.lock +34 -0
- package/dist/web/assets/accounts-settings-section-CGe5ikvs.js +1 -0
- package/dist/web/assets/accounts-settings-section-CGe5ikvs.js.br +0 -0
- package/dist/web/assets/accounts-settings-section-CGe5ikvs.js.gz +0 -0
- package/dist/web/assets/{agent-session-sheet-CBMsnl_p.js → agent-session-sheet-vBhfHYrC.js} +2 -2
- package/dist/web/assets/agent-session-sheet-vBhfHYrC.js.br +0 -0
- package/dist/web/assets/agent-session-sheet-vBhfHYrC.js.gz +0 -0
- package/dist/web/assets/{agent-session-window-content-D9C9FA7E.js → agent-session-window-content-BxIxDN5i.js} +1 -1
- package/dist/web/assets/agent-session-window-content-BxIxDN5i.js.br +0 -0
- package/dist/web/assets/agent-session-window-content-BxIxDN5i.js.gz +0 -0
- package/dist/web/assets/{ai-resource-editor-DhUVgDEG.js → ai-resource-editor-Y8clBPRv.js} +1 -1
- package/dist/web/assets/ai-resource-editor-Y8clBPRv.js.br +0 -0
- package/dist/web/assets/ai-resource-editor-Y8clBPRv.js.gz +0 -0
- package/dist/web/assets/{ai-settings-section-DOckWvp4.js → ai-settings-section-BB8vscte.js} +1 -1
- package/dist/web/assets/{ai-settings-section-CETpSpYd.js → ai-settings-section-Ck8uN_Bw.js} +1 -1
- package/dist/web/assets/ai-settings-section-Ck8uN_Bw.js.br +0 -0
- package/dist/web/assets/ai-settings-section-Ck8uN_Bw.js.gz +0 -0
- package/dist/web/assets/android-tab-BIOUVVoW.js +2 -0
- package/dist/web/assets/android-tab-BIOUVVoW.js.br +0 -0
- package/dist/web/assets/android-tab-BIOUVVoW.js.gz +0 -0
- package/dist/web/assets/{api-settings-JIgKfcCx.js → api-settings-D1axn9vm.js} +1 -1
- package/dist/web/assets/{appearance-settings-section-BJUiLUb7.js → appearance-settings-section-Bv5_n9lH.js} +1 -1
- package/dist/web/assets/appearance-settings-section-Bv5_n9lH.js.br +0 -0
- package/dist/web/assets/appearance-settings-section-Bv5_n9lH.js.gz +0 -0
- package/dist/web/assets/{apps-panel-1VElRgV3.js → apps-panel-DXHF0QEv.js} +1 -1
- package/dist/web/assets/apps-panel-DXHF0QEv.js.br +0 -0
- package/dist/web/assets/apps-panel-DXHF0QEv.js.gz +0 -0
- package/dist/web/assets/{audio-preview-DYkyHu5X.js → audio-preview-DpjOKEm4.js} +1 -1
- package/dist/web/assets/audio-preview-DpjOKEm4.js.br +0 -0
- package/dist/web/assets/audio-preview-DpjOKEm4.js.gz +0 -0
- package/dist/web/assets/branch-review-tab-4dgMNadQ.js +1 -0
- package/dist/web/assets/branch-review-tab-4dgMNadQ.js.br +0 -0
- package/dist/web/assets/branch-review-tab-4dgMNadQ.js.gz +0 -0
- package/dist/web/assets/chat-tab-Bbe_77oJ.js +15 -0
- package/dist/web/assets/chat-tab-Bbe_77oJ.js.br +0 -0
- package/dist/web/assets/chat-tab-Bbe_77oJ.js.gz +0 -0
- package/dist/web/assets/chat-tab-D3i5O3dO.js +1 -0
- package/dist/web/assets/chat-tab-D3i5O3dO.js.br +0 -0
- package/dist/web/assets/chat-tab-D3i5O3dO.js.gz +0 -0
- package/dist/web/assets/code-editor-DXsIWoQ1.js +7 -0
- package/dist/web/assets/code-editor-DXsIWoQ1.js.br +0 -0
- package/dist/web/assets/code-editor-DXsIWoQ1.js.gz +0 -0
- package/dist/web/assets/{codex-reset-credit-button-Fr45FUxi.js → codex-reset-credit-button-DRhievFQ.js} +1 -1
- package/dist/web/assets/codex-reset-credit-button-DRhievFQ.js.br +0 -0
- package/dist/web/assets/codex-reset-credit-button-DRhievFQ.js.gz +0 -0
- package/dist/web/assets/{conflict-editor-B-wdjSXI.js → conflict-editor-D_RImo_V.js} +3 -3
- package/dist/web/assets/conflict-editor-D_RImo_V.js.br +0 -0
- package/dist/web/assets/conflict-editor-D_RImo_V.js.gz +0 -0
- package/dist/web/assets/{csv-preview-B6RodObB.js → csv-preview-BuFdfLo3.js} +1 -1
- package/dist/web/assets/csv-preview-BuFdfLo3.js.br +0 -0
- package/dist/web/assets/csv-preview-BuFdfLo3.js.gz +0 -0
- package/dist/web/assets/{database-viewer-CdPBUQPY.js → database-viewer-ChcbYn9E.js} +1 -1
- package/dist/web/assets/database-viewer-ChcbYn9E.js.br +0 -0
- package/dist/web/assets/database-viewer-ChcbYn9E.js.gz +0 -0
- package/dist/web/assets/{design-settings-section-BpCpaBwL.js → design-settings-section-B_xOf51h.js} +1 -1
- package/dist/web/assets/design-settings-section-B_xOf51h.js.br +0 -0
- package/dist/web/assets/design-settings-section-B_xOf51h.js.gz +0 -0
- package/dist/web/assets/{design-skill-suggestion-DkT2E2IV.js → design-skill-suggestion-BbEDg15Z.js} +1 -1
- package/dist/web/assets/design-skill-suggestion-BbEDg15Z.js.br +0 -0
- package/dist/web/assets/design-skill-suggestion-BbEDg15Z.js.gz +0 -0
- package/dist/web/assets/{design-tab-BZb9eemI.js → design-tab-BQSYJ4cS.js} +6 -6
- package/dist/web/assets/design-tab-BQSYJ4cS.js.br +0 -0
- package/dist/web/assets/design-tab-BQSYJ4cS.js.gz +0 -0
- package/dist/web/assets/{designs-sidebar-panel-CF4tp6j-.js → designs-sidebar-panel-AiPaYfSU.js} +1 -1
- package/dist/web/assets/designs-sidebar-panel-AiPaYfSU.js.br +0 -0
- package/dist/web/assets/designs-sidebar-panel-AiPaYfSU.js.gz +0 -0
- package/dist/web/assets/{diff-viewer-BqEqwSw6.js → diff-viewer-BZ6glGK-.js} +1 -1
- package/dist/web/assets/diff-viewer-BZ6glGK-.js.br +0 -0
- package/dist/web/assets/diff-viewer-BZ6glGK-.js.gz +0 -0
- package/dist/web/assets/{edit-diff-preview-DD4HayXY.js → edit-diff-preview-D-FNLOU2.js} +1 -1
- package/dist/web/assets/edit-diff-preview-D-FNLOU2.js.br +0 -0
- package/dist/web/assets/edit-diff-preview-D-FNLOU2.js.gz +0 -0
- package/dist/web/assets/{editor-language-service-CkA5sEdV.js → editor-language-service-4sbsdqBV.js} +1 -1
- package/dist/web/assets/editor-language-service-4sbsdqBV.js.br +0 -0
- package/dist/web/assets/editor-language-service-4sbsdqBV.js.gz +0 -0
- package/dist/web/assets/explorer-body-DbgrG1fe.js +2 -0
- package/dist/web/assets/explorer-body-DbgrG1fe.js.br +0 -0
- package/dist/web/assets/explorer-body-DbgrG1fe.js.gz +0 -0
- package/dist/web/assets/{explorer-window-content-CRJ2eMZ4.js → explorer-window-content-BCjWvHpp.js} +1 -1
- package/dist/web/assets/explorer-window-content-BCjWvHpp.js.br +0 -0
- package/dist/web/assets/explorer-window-content-BCjWvHpp.js.gz +0 -0
- package/dist/web/assets/{extension-manager-section-DwQTdCII.js → extension-manager-section-DiMF4RnT.js} +1 -1
- package/dist/web/assets/extension-manager-section-DiMF4RnT.js.br +0 -0
- package/dist/web/assets/extension-manager-section-DiMF4RnT.js.gz +0 -0
- package/dist/web/assets/{extension-webview-CMq-VTLu.js → extension-webview-Coti6Vfw.js} +1 -1
- package/dist/web/assets/extension-webview-Coti6Vfw.js.br +0 -0
- package/dist/web/assets/extension-webview-Coti6Vfw.js.gz +0 -0
- package/dist/web/assets/file-store-Cm7-pHux.js +1 -0
- package/dist/web/assets/file-store-Cm7-pHux.js.br +0 -0
- package/dist/web/assets/file-store-Cm7-pHux.js.gz +0 -0
- package/dist/web/assets/{files-settings-section-B2lcyh-6.js → files-settings-section-ZTVwOwx0.js} +1 -1
- package/dist/web/assets/files-settings-section-ZTVwOwx0.js.br +0 -0
- package/dist/web/assets/files-settings-section-ZTVwOwx0.js.gz +0 -0
- package/dist/web/assets/{general-settings-section-Doepm2sH.js → general-settings-section-B7Hjx_fb.js} +1 -1
- package/dist/web/assets/general-settings-section-B7Hjx_fb.js.br +0 -0
- package/dist/web/assets/general-settings-section-B7Hjx_fb.js.gz +0 -0
- package/dist/web/assets/{git-log-panel-CJakNMDc.js → git-log-panel-CbcJ_YEH.js} +1 -1
- package/dist/web/assets/git-log-panel-CbcJ_YEH.js.br +0 -0
- package/dist/web/assets/git-log-panel-CbcJ_YEH.js.gz +0 -0
- package/dist/web/assets/{glide-data-grid-C_SeOhSq.js → glide-data-grid-BfByGSf1.js} +3 -3
- package/dist/web/assets/glide-data-grid-BfByGSf1.js.br +0 -0
- package/dist/web/assets/glide-data-grid-BfByGSf1.js.gz +0 -0
- package/dist/web/assets/group-chat-tab-DCo9OP3D.js +1 -0
- package/dist/web/assets/group-chat-tab-DCo9OP3D.js.br +0 -0
- package/dist/web/assets/group-chat-tab-DCo9OP3D.js.gz +0 -0
- package/dist/web/assets/{image-preview-C1Frlx22.js → image-preview-Cjlps7Pt.js} +1 -1
- package/dist/web/assets/index-CyDLBddf.js +52 -0
- package/dist/web/assets/index-CyDLBddf.js.br +0 -0
- package/dist/web/assets/index-CyDLBddf.js.gz +0 -0
- package/dist/web/assets/index-Dh5apC3I.css +1 -0
- package/dist/web/assets/index-Dh5apC3I.css.br +0 -0
- package/dist/web/assets/index-Dh5apC3I.css.gz +0 -0
- package/dist/web/assets/{jira-watcher-section-BmkIGQFV.js → jira-watcher-section-Dz_-sUC2.js} +1 -1
- package/dist/web/assets/keybindings-store-BTq5Z7XP.js +1 -0
- package/dist/web/assets/{keyboard-shortcuts-section-BgZe3QDr.js → keyboard-shortcuts-section-Ba5NYwlG.js} +1 -1
- package/dist/web/assets/keyboard-shortcuts-section-Ba5NYwlG.js.br +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-Ba5NYwlG.js.gz +0 -0
- package/dist/web/assets/{language-servers-section-CCMzgOaE.js → language-servers-section-DJJ7SFXo.js} +2 -2
- package/dist/web/assets/language-servers-section-DJJ7SFXo.js.br +0 -0
- package/dist/web/assets/language-servers-section-DJJ7SFXo.js.gz +0 -0
- package/dist/web/assets/{lsp-client-B10suuaZ.js → lsp-client-Dr3vQ1bb.js} +1 -1
- package/dist/web/assets/lsp-client-Dr3vQ1bb.js.br +0 -0
- package/dist/web/assets/lsp-client-Dr3vQ1bb.js.gz +0 -0
- package/dist/web/assets/{markdown-renderer-AHHFS3jc.js → markdown-renderer-CmnqFfTU.js} +3 -3
- package/dist/web/assets/markdown-renderer-CmnqFfTU.js.br +0 -0
- package/dist/web/assets/markdown-renderer-CmnqFfTU.js.gz +0 -0
- package/dist/web/assets/markdown-renderer-LJdVMbGl.js +1 -0
- package/dist/web/assets/{mobile-explorer-sheet-CqCsLWSr.js → mobile-explorer-sheet-BiIhZtJ2.js} +1 -1
- package/dist/web/assets/mobile-explorer-sheet-BiIhZtJ2.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-BiIhZtJ2.js.gz +0 -0
- package/dist/web/assets/new-design-dialog-DI_hxvFV.js +1 -0
- package/dist/web/assets/new-design-dialog-DI_hxvFV.js.br +0 -0
- package/dist/web/assets/new-design-dialog-DI_hxvFV.js.gz +0 -0
- package/dist/web/assets/notification-store-9lBSplOV.js +1 -0
- package/dist/web/assets/{notifications-settings-section-CXgzH_bK.js → notifications-settings-section-0s-2OKRv.js} +1 -1
- package/dist/web/assets/notifications-settings-section-0s-2OKRv.js.br +0 -0
- package/dist/web/assets/notifications-settings-section-0s-2OKRv.js.gz +0 -0
- package/dist/web/assets/{overview-panel-BYB8mgRE.js → overview-panel-3htxfZS2.js} +1 -1
- package/dist/web/assets/overview-panel-3htxfZS2.js.br +0 -0
- package/dist/web/assets/overview-panel-3htxfZS2.js.gz +0 -0
- package/dist/web/assets/panel-store-B6PoCwBA.js +1 -0
- package/dist/web/assets/panel-store-B6PoCwBA.js.br +0 -0
- package/dist/web/assets/panel-store-B6PoCwBA.js.gz +0 -0
- package/dist/web/assets/{performance-panel-Dpuq3g0W.js → performance-panel-V8gpnnf7.js} +2 -2
- package/dist/web/assets/performance-panel-V8gpnnf7.js.br +0 -0
- package/dist/web/assets/performance-panel-V8gpnnf7.js.gz +0 -0
- package/dist/web/assets/{postgres-viewer-DW0Gkmow.js → postgres-viewer-sWS-ogTC.js} +1 -1
- package/dist/web/assets/postgres-viewer-sWS-ogTC.js.br +0 -0
- package/dist/web/assets/postgres-viewer-sWS-ogTC.js.gz +0 -0
- package/dist/web/assets/{ppmbot-settings-section-DFtlGoW6.js → ppmbot-settings-section-CT8vvlzk.js} +1 -1
- package/dist/web/assets/ppmbot-settings-section-CT8vvlzk.js.br +0 -0
- package/dist/web/assets/ppmbot-settings-section-CT8vvlzk.js.gz +0 -0
- package/dist/web/assets/{pptx-export-CNyG32Xr.js → pptx-export-Ugjv0hso.js} +4 -4
- package/dist/web/assets/pptx-export-Ugjv0hso.js.br +0 -0
- package/dist/web/assets/pptx-export-Ugjv0hso.js.gz +0 -0
- package/dist/web/assets/{problems-panel-V5zqpuXq.js → problems-panel-Gcy79pSS.js} +1 -1
- package/dist/web/assets/problems-panel-Gcy79pSS.js.br +0 -0
- package/dist/web/assets/problems-panel-Gcy79pSS.js.gz +0 -0
- package/dist/web/assets/{process-row-format-CiJJSf_V.js → process-row-format-u6y7lOee.js} +1 -1
- package/dist/web/assets/process-table-BUTIp9Oy.js +1 -0
- package/dist/web/assets/process-table-BUTIp9Oy.js.br +0 -0
- package/dist/web/assets/process-table-BUTIp9Oy.js.gz +0 -0
- package/dist/web/assets/{proxy-settings-section-B7AVlTfY.js → proxy-settings-section-Buv_8y54.js} +1 -1
- package/dist/web/assets/proxy-settings-section-Buv_8y54.js.br +0 -0
- package/dist/web/assets/proxy-settings-section-Buv_8y54.js.gz +0 -0
- package/dist/web/assets/{query-audit-section-gHmRfWKr.js → query-audit-section-N2DQtgJx.js} +1 -1
- package/dist/web/assets/query-audit-section-N2DQtgJx.js.br +0 -0
- package/dist/web/assets/query-audit-section-N2DQtgJx.js.gz +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-CRvJqmaz.js +1 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-CRvJqmaz.js.br +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-CRvJqmaz.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-BoYINLbJ.js +2 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-BoYINLbJ.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-BoYINLbJ.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-DPdP0yYu.js +1 -0
- package/dist/web/assets/remote-desktop-mobile-view-DPdP0yYu.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-DPdP0yYu.js.gz +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-BeQ_Jrp7.js +1 -0
- package/dist/web/assets/remote-desktop-readiness-gate-BeQ_Jrp7.js.br +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-BeQ_Jrp7.js.gz +0 -0
- package/dist/web/assets/remote-desktop-settings-section-Bs4sHS3l.js +1 -0
- package/dist/web/assets/remote-desktop-settings-section-Bs4sHS3l.js.br +0 -0
- package/dist/web/assets/remote-desktop-settings-section-Bs4sHS3l.js.gz +0 -0
- package/dist/web/assets/remote-desktop-window-content-DDr__JxQ.js +1 -0
- package/dist/web/assets/remote-desktop-window-content-DDr__JxQ.js.br +0 -0
- package/dist/web/assets/remote-desktop-window-content-DDr__JxQ.js.gz +0 -0
- package/dist/web/assets/{run-in-terminal-DKBcEGVu.js → run-in-terminal-DCBNtYHQ.js} +1 -1
- package/dist/web/assets/{schedules-settings-section-sYDAsBMO.js → schedules-settings-section-_cHX2qOa.js} +1 -1
- package/dist/web/assets/schedules-settings-section-_cHX2qOa.js.br +0 -0
- package/dist/web/assets/schedules-settings-section-_cHX2qOa.js.gz +0 -0
- package/dist/web/assets/{send-to-chat-CCdOrKc7.js → send-to-chat-dNqNV7Y4.js} +1 -1
- package/dist/web/assets/send-to-chat-dNqNV7Y4.js.br +0 -0
- package/dist/web/assets/send-to-chat-dNqNV7Y4.js.gz +0 -0
- package/dist/web/assets/{services-panel-CKa2aP1d.js → services-panel-Dm54cnW3.js} +1 -1
- package/dist/web/assets/services-panel-Dm54cnW3.js.br +0 -0
- package/dist/web/assets/services-panel-Dm54cnW3.js.gz +0 -0
- package/dist/web/assets/settings-body-DRRQNaDA.js +1 -0
- package/dist/web/assets/settings-body-DRRQNaDA.js.br +0 -0
- package/dist/web/assets/settings-body-DRRQNaDA.js.gz +0 -0
- package/dist/web/assets/settings-section-content-DRaHfvm8.js +1 -0
- package/dist/web/assets/settings-section-content-U3mIx6hF.js +2 -0
- package/dist/web/assets/settings-section-content-U3mIx6hF.js.br +0 -0
- package/dist/web/assets/settings-section-content-U3mIx6hF.js.gz +0 -0
- package/dist/web/assets/{settings-store-DfuiLq_9.js → settings-store-Cyuo8ceh.js} +2 -2
- package/dist/web/assets/settings-store-Cyuo8ceh.js.br +0 -0
- package/dist/web/assets/settings-store-Cyuo8ceh.js.gz +0 -0
- package/dist/web/assets/settings-tab-CB_BhHTK.js +1 -0
- package/dist/web/assets/settings-window-content-CgZIMNX8.js +1 -0
- package/dist/web/assets/{slash-command-picker-CLevA_Gf.js → slash-command-picker-CQlIqMW0.js} +1 -1
- package/dist/web/assets/slash-command-picker-CQlIqMW0.js.br +0 -0
- package/dist/web/assets/slash-command-picker-CQlIqMW0.js.gz +0 -0
- package/dist/web/assets/{sql-query-editor-Db8b0jEr.js → sql-query-editor--Z4bcqd8.js} +1 -1
- package/dist/web/assets/sql-query-editor--Z4bcqd8.js.br +0 -0
- package/dist/web/assets/sql-query-editor--Z4bcqd8.js.gz +0 -0
- package/dist/web/assets/{sqlite-viewer-VzYLMqkD.js → sqlite-viewer-CSmX3qaj.js} +1 -1
- package/dist/web/assets/sqlite-viewer-CSmX3qaj.js.br +0 -0
- package/dist/web/assets/sqlite-viewer-CSmX3qaj.js.gz +0 -0
- package/dist/web/assets/{system-monitor-body-BR60W_mG.js → system-monitor-body-LannpU6H.js} +2 -2
- package/dist/web/assets/system-monitor-body-LannpU6H.js.br +0 -0
- package/dist/web/assets/system-monitor-body-LannpU6H.js.gz +0 -0
- package/dist/web/assets/system-monitor-tab-BfVucwEF.js +1 -0
- package/dist/web/assets/{system-monitor-window-content-BLw1QBXo.js → system-monitor-window-content-kLNW9Ln3.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-BL61SB5m.js → tab-host-window-content-II_FtOYM.js} +1 -1
- package/dist/web/assets/tab-host-window-content-II_FtOYM.js.br +0 -0
- package/dist/web/assets/tab-host-window-content-II_FtOYM.js.gz +0 -0
- package/dist/web/assets/{tab-store-CbPh8CAX.js → tab-store-DCpc0g0Q.js} +1 -1
- package/dist/web/assets/{terminal-tab-B4y6at4h.js → terminal-tab-CTe2polt.js} +1 -1
- package/dist/web/assets/terminal-tab-CTe2polt.js.br +0 -0
- package/dist/web/assets/terminal-tab-CTe2polt.js.gz +0 -0
- package/dist/web/assets/{tool-cards-K0r_yxI0.js → tool-cards-rxLXbBSm.js} +5 -5
- package/dist/web/assets/tool-cards-rxLXbBSm.js.br +0 -0
- package/dist/web/assets/tool-cards-rxLXbBSm.js.gz +0 -0
- package/dist/web/assets/use-h264-canvas-decoder-BfO9QrlH.js +1 -0
- package/dist/web/assets/use-h264-canvas-decoder-BfO9QrlH.js.br +0 -0
- package/dist/web/assets/use-h264-canvas-decoder-BfO9QrlH.js.gz +0 -0
- package/dist/web/assets/{use-inline-blame-BK23icsE.js → use-inline-blame-CxZ_QWzS.js} +3 -3
- package/dist/web/assets/use-inline-blame-CxZ_QWzS.js.br +0 -0
- package/dist/web/assets/use-inline-blame-CxZ_QWzS.js.gz +0 -0
- package/dist/web/assets/{use-lsp-CRdCPCKW.js → use-lsp-gCNE5A9K.js} +1 -1
- package/dist/web/assets/use-lsp-gCNE5A9K.js.br +0 -0
- package/dist/web/assets/use-lsp-gCNE5A9K.js.gz +0 -0
- package/dist/web/assets/{use-monaco-theme-CpXT2uh3.js → use-monaco-theme-DvNGcCvz.js} +1 -1
- package/dist/web/assets/{use-process-kill-BWYX03KF.js → use-process-kill-Cbiu1mbY.js} +1 -1
- package/dist/web/assets/use-process-kill-Cbiu1mbY.js.br +0 -0
- package/dist/web/assets/use-process-kill-Cbiu1mbY.js.gz +0 -0
- package/dist/web/assets/{use-websocket-CiWQKhHr.js → use-websocket-AC7EG1LK.js} +1 -1
- package/dist/web/assets/video-preview-C2Y61WUE.js +1 -0
- package/dist/web/assets/video-preview-C2Y61WUE.js.br +0 -0
- package/dist/web/assets/video-preview-C2Y61WUE.js.gz +0 -0
- package/dist/web/assets/{voice-settings-section-Ceihggqf.js → voice-settings-section-DCXZeLYI.js} +1 -1
- package/dist/web/assets/voice-settings-section-DCXZeLYI.js.br +0 -0
- package/dist/web/assets/voice-settings-section-DCXZeLYI.js.gz +0 -0
- package/dist/web/assets/{window-pip-registry-mDgwpAwP.js → window-pip-registry-9Y9LQQYS.js} +1 -1
- package/dist/web/index.html +50 -7
- package/dist/web/index.html.br +0 -0
- package/dist/web/index.html.gz +0 -0
- package/dist/web/sw.js +1 -1
- package/dist/web/sw.js.br +0 -0
- package/dist/web/sw.js.gz +0 -0
- package/docs/architecture/android-emulator-phase0-report-260921.md +310 -0
- package/docs/architecture/android-emulator-phase2-report-260921.md +163 -0
- package/docs/architecture/android-emulator-phase3-report-260921.md +203 -0
- package/docs/architecture/android-emulator-phase4-report-260921.md +191 -0
- package/docs/architecture/android-emulator-plan-260920.md +376 -0
- package/docs/architecture/plugins-and-tracing.md +48 -7
- package/package.json +4 -2
- package/scripts/gen-android-proto.ts +37 -0
- package/scripts/release.sh +3 -1
- package/src/cli/commands/chat-cmd.ts +2 -2
- package/src/providers/claude-agent-sdk-query-options.ts +28 -0
- package/src/providers/claude-agent-sdk.ts +153 -71
- package/src/providers/claude-warm-spare.ts +176 -0
- package/src/providers/provider.interface.ts +1 -0
- package/src/server/index.ts +53 -0
- package/src/server/middleware/gzip-json.ts +13 -1
- package/src/server/routes/android.ts +616 -0
- package/src/server/routes/chat.ts +31 -0
- package/src/server/routes/files.ts +52 -5
- package/src/server/routes/remote-desktop.ts +95 -2
- package/src/server/routes/trace.ts +66 -0
- package/src/server/ws/android.ts +101 -0
- package/src/server/ws/chat.ts +12 -17
- package/src/server/ws/global.ts +13 -1
- package/src/server/ws/remote-desktop.ts +6 -1
- package/src/services/android/android-apk.ts +387 -0
- package/src/services/android/android-capabilities.ts +154 -0
- package/src/services/android/android-flag.ts +12 -0
- package/src/services/android/android-grpc.ts +124 -0
- package/src/services/android/android-input.ts +129 -0
- package/src/services/android/android-logcat.ts +169 -0
- package/src/services/android/android-nonce.ts +50 -0
- package/src/services/android/android-operations.ts +124 -0
- package/src/services/android/android-screenshot.ts +53 -0
- package/src/services/android/android-session.ts +510 -0
- package/src/services/android/android-video.ts +495 -0
- package/src/services/android/avd-list.ts +131 -0
- package/src/services/android/avd-manager.ts +397 -0
- package/src/services/android/device-registry.ts +223 -0
- package/src/services/android/emulator-discovery.ts +160 -0
- package/src/services/android/emulator-launcher.ts +290 -0
- package/src/services/android/proto/emulator-controller-descriptor.ts +10 -0
- package/src/services/android/proto/emulator_controller.proto +1791 -0
- package/src/services/android/sdk-discovery.ts +178 -0
- package/src/services/android/system-images.ts +124 -0
- package/src/services/chat.service.ts +81 -4
- package/src/services/config.service.ts +1 -1
- package/src/services/file-filter.service.ts +2 -61
- package/src/services/file-glob.ts +67 -0
- package/src/services/file-index/file-index-runner.ts +169 -0
- package/src/services/file-index/file-index-worker.ts +73 -0
- package/src/services/file-index/index-search.ts +152 -0
- package/src/services/file-index/index-walk.ts +109 -0
- package/src/services/file-list-index.service.ts +189 -70
- package/src/services/file-watcher/linux-inotify.ts +238 -0
- package/src/services/file-watcher/watch-tree.ts +37 -6
- package/src/services/file-watcher.service.ts +40 -8
- package/src/services/file.service.ts +17 -6
- package/src/services/group-chat/group-chat.service.ts +1 -1
- package/src/services/jira-debug-session.service.ts +1 -1
- package/src/services/ppmbot/ppmbot-delegation.ts +1 -1
- package/src/services/ppmbot/ppmbot-service.ts +1 -1
- package/src/services/project-content-search.service.ts +45 -11
- package/src/services/proxy-agent-turn.ts +7 -3
- package/src/services/remote-desktop/mediamtx-catalog.ts +89 -0
- package/src/services/remote-desktop/mediamtx-config.ts +91 -0
- package/src/services/remote-desktop/mediamtx-install.service.ts +191 -0
- package/src/services/remote-desktop/mediamtx-paths.ts +51 -0
- package/src/services/remote-desktop/mediamtx-process.ts +152 -0
- package/src/services/remote-desktop/remote-desktop-capture-wayland.ts +312 -0
- package/src/services/remote-desktop/remote-desktop-capture.ts +60 -4
- package/src/services/remote-desktop/remote-desktop-portal.ts +529 -0
- package/src/services/remote-desktop/remote-desktop-requirements.ts +56 -20
- package/src/services/remote-desktop/remote-desktop-session.ts +77 -3
- package/src/services/remote-desktop/remote-desktop-whep-registry.ts +36 -0
- package/src/services/scheduler-runner.ts +2 -3
- package/src/services/session-trace/browser-trace-ingest.ts +131 -0
- package/src/services/session-trace/session-trace-cleanup.ts +65 -0
- package/src/services/session-trace/session-trace-db.ts +89 -0
- package/src/services/session-trace/session-trace-store.ts +132 -0
- package/src/services/session-trace/trace-coalescer.ts +86 -0
- package/src/services/session-trace/trace-recorder.ts +304 -0
- package/src/services/session-trace/trace-writer.ts +83 -0
- package/src/shared/android-avd.ts +53 -0
- package/src/shared/android-protocol.ts +184 -0
- package/src/shared/file-index-limits.ts +17 -0
- package/src/shared/session-trace.ts +61 -0
- package/src/types/chat.ts +17 -0
- package/src/types/config.ts +27 -0
- package/src/types/project.ts +9 -0
- package/src/web/app.tsx +5 -0
- package/src/web/components/android/android-apk-upload.ts +75 -0
- package/src/web/components/android/android-avd-create-dialog.tsx +305 -0
- package/src/web/components/android/android-avd-destroy-dialog.tsx +151 -0
- package/src/web/components/android/android-clipboard-panel.tsx +94 -0
- package/src/web/components/android/android-controls.tsx +128 -0
- package/src/web/components/android/android-coords.ts +99 -0
- package/src/web/components/android/android-device-list.tsx +329 -0
- package/src/web/components/android/android-install-panel.tsx +280 -0
- package/src/web/components/android/android-log-filter.ts +75 -0
- package/src/web/components/android/android-logcat-panel.tsx +214 -0
- package/src/web/components/android/android-screenshot-download.ts +56 -0
- package/src/web/components/android/android-tab.tsx +66 -0
- package/src/web/components/android/android-tools-drawer.tsx +76 -0
- package/src/web/components/android/android-viewer.tsx +200 -0
- package/src/web/components/android/android-ws-url.ts +21 -0
- package/src/web/components/android/open-android.ts +21 -0
- package/src/web/components/android/use-android-available.ts +29 -0
- package/src/web/components/android/use-android-keyboard.ts +111 -0
- package/src/web/components/android/use-android-session.ts +277 -0
- package/src/web/components/android/use-android-touch.ts +123 -0
- package/src/web/components/chat/chat-tab.tsx +28 -2
- package/src/web/components/chat/message-input.tsx +3 -5
- package/src/web/components/editor/compare-picker.tsx +12 -2
- package/src/web/components/explorer/file-tree.tsx +6 -7
- package/src/web/components/explorer/use-tree-row-dnd.ts +0 -2
- package/src/web/components/floating-window/window-content-registry.ts +8 -8
- package/src/web/components/layout/command-palette.tsx +31 -9
- package/src/web/components/layout/mobile-drawer.tsx +8 -1
- package/src/web/components/layout/nav-section-rail.tsx +8 -1
- package/src/web/components/layout/reparenting-tab.tsx +2 -2
- package/src/web/components/layout/tab-content.tsx +5 -0
- package/src/web/components/layout/tab-pool.tsx +31 -20
- package/src/web/components/remote-desktop/use-h264-canvas-decoder.ts +23 -1
- package/src/web/components/remote-desktop/use-remote-desktop-connection.ts +64 -6
- package/src/web/components/remote-desktop/use-webrtc-canvas-video.ts +255 -0
- package/src/web/components/root-error-boundary.tsx +5 -0
- package/src/web/components/settings/remote-desktop-settings-section.tsx +176 -0
- package/src/web/components/settings/settings-categories.ts +3 -0
- package/src/web/components/settings/settings-section-content.tsx +25 -18
- package/src/web/hooks/use-chat-prewarm.ts +53 -0
- package/src/web/hooks/use-chat.ts +18 -4
- package/src/web/hooks/use-file-index-invalidation.ts +7 -5
- package/src/web/hooks/use-global-events.ts +12 -1
- package/src/web/hooks/use-remote-file-search.ts +39 -0
- package/src/web/hooks/use-ui-preload.ts +38 -0
- package/src/web/index.html +43 -0
- package/src/web/lib/chunk-recovery.ts +5 -1
- package/src/web/lib/device-id.ts +39 -0
- package/src/web/lib/feature-badges.ts +2 -0
- package/src/web/lib/lazy-with-preload.tsx +43 -0
- package/src/web/lib/preload-ui.ts +60 -0
- package/src/web/lib/score-file-search.ts +27 -14
- package/src/web/lib/tab-type-icons.ts +2 -1
- package/src/web/lib/trace-boot.ts +8 -0
- package/src/web/lib/trace-client.ts +516 -0
- package/src/web/main.tsx +12 -0
- package/src/web/stores/file-store.ts +124 -18
- package/src/web/stores/panel-utils.ts +3 -0
- package/src/web/stores/tab-store.ts +2 -1
- package/src/web/stores/window-panel-persistence.ts +3 -0
- package/vite.config.ts +3 -0
- package/dist/web/assets/accounts-settings-section-DEIm-W-D.js +0 -1
- package/dist/web/assets/accounts-settings-section-DEIm-W-D.js.br +0 -0
- package/dist/web/assets/accounts-settings-section-DEIm-W-D.js.gz +0 -0
- package/dist/web/assets/agent-session-sheet-CBMsnl_p.js.br +0 -0
- package/dist/web/assets/agent-session-sheet-CBMsnl_p.js.gz +0 -0
- package/dist/web/assets/agent-session-window-content-D9C9FA7E.js.br +0 -0
- package/dist/web/assets/agent-session-window-content-D9C9FA7E.js.gz +0 -0
- package/dist/web/assets/ai-resource-editor-DhUVgDEG.js.br +0 -0
- package/dist/web/assets/ai-resource-editor-DhUVgDEG.js.gz +0 -0
- package/dist/web/assets/ai-settings-section-CETpSpYd.js.br +0 -0
- package/dist/web/assets/ai-settings-section-CETpSpYd.js.gz +0 -0
- package/dist/web/assets/appearance-settings-section-BJUiLUb7.js.br +0 -0
- package/dist/web/assets/appearance-settings-section-BJUiLUb7.js.gz +0 -0
- package/dist/web/assets/apps-panel-1VElRgV3.js.br +0 -0
- package/dist/web/assets/apps-panel-1VElRgV3.js.gz +0 -0
- package/dist/web/assets/audio-preview-DYkyHu5X.js.br +0 -0
- package/dist/web/assets/audio-preview-DYkyHu5X.js.gz +0 -0
- package/dist/web/assets/branch-review-tab-DhTMSpSU.js +0 -1
- package/dist/web/assets/branch-review-tab-DhTMSpSU.js.br +0 -0
- package/dist/web/assets/branch-review-tab-DhTMSpSU.js.gz +0 -0
- package/dist/web/assets/chat-tab-Cg3BKugo.js +0 -15
- package/dist/web/assets/chat-tab-Cg3BKugo.js.br +0 -0
- package/dist/web/assets/chat-tab-Cg3BKugo.js.gz +0 -0
- package/dist/web/assets/chat-tab-tbUDK__Y.js +0 -1
- package/dist/web/assets/chat-tab-tbUDK__Y.js.br +0 -0
- package/dist/web/assets/chat-tab-tbUDK__Y.js.gz +0 -0
- package/dist/web/assets/code-editor-6ZiQehbr.js +0 -7
- package/dist/web/assets/code-editor-6ZiQehbr.js.br +0 -0
- package/dist/web/assets/code-editor-6ZiQehbr.js.gz +0 -0
- package/dist/web/assets/codex-reset-credit-button-Fr45FUxi.js.br +0 -0
- package/dist/web/assets/codex-reset-credit-button-Fr45FUxi.js.gz +0 -0
- package/dist/web/assets/conflict-editor-B-wdjSXI.js.br +0 -0
- package/dist/web/assets/conflict-editor-B-wdjSXI.js.gz +0 -0
- package/dist/web/assets/csv-preview-B6RodObB.js.br +0 -0
- package/dist/web/assets/csv-preview-B6RodObB.js.gz +0 -0
- package/dist/web/assets/database-viewer-CdPBUQPY.js.br +0 -0
- package/dist/web/assets/database-viewer-CdPBUQPY.js.gz +0 -0
- package/dist/web/assets/design-settings-section-BpCpaBwL.js.br +0 -0
- package/dist/web/assets/design-settings-section-BpCpaBwL.js.gz +0 -0
- package/dist/web/assets/design-skill-suggestion-DkT2E2IV.js.br +0 -0
- package/dist/web/assets/design-skill-suggestion-DkT2E2IV.js.gz +0 -0
- package/dist/web/assets/design-tab-BZb9eemI.js.br +0 -0
- package/dist/web/assets/design-tab-BZb9eemI.js.gz +0 -0
- package/dist/web/assets/designs-sidebar-panel-CF4tp6j-.js.br +0 -0
- package/dist/web/assets/designs-sidebar-panel-CF4tp6j-.js.gz +0 -0
- package/dist/web/assets/diff-viewer-BqEqwSw6.js.br +0 -0
- package/dist/web/assets/diff-viewer-BqEqwSw6.js.gz +0 -0
- package/dist/web/assets/edit-diff-preview-DD4HayXY.js.br +0 -0
- package/dist/web/assets/edit-diff-preview-DD4HayXY.js.gz +0 -0
- package/dist/web/assets/editor-language-service-CkA5sEdV.js.br +0 -0
- package/dist/web/assets/editor-language-service-CkA5sEdV.js.gz +0 -0
- package/dist/web/assets/explorer-body-Ds7oKuz7.js +0 -2
- package/dist/web/assets/explorer-body-Ds7oKuz7.js.br +0 -0
- package/dist/web/assets/explorer-body-Ds7oKuz7.js.gz +0 -0
- package/dist/web/assets/explorer-window-content-CRJ2eMZ4.js.br +0 -0
- package/dist/web/assets/explorer-window-content-CRJ2eMZ4.js.gz +0 -0
- package/dist/web/assets/extension-manager-section-DwQTdCII.js.br +0 -0
- package/dist/web/assets/extension-manager-section-DwQTdCII.js.gz +0 -0
- package/dist/web/assets/extension-webview-CMq-VTLu.js.br +0 -0
- package/dist/web/assets/extension-webview-CMq-VTLu.js.gz +0 -0
- package/dist/web/assets/file-store-DY1_0nzY.js +0 -1
- package/dist/web/assets/file-store-DY1_0nzY.js.br +0 -0
- package/dist/web/assets/file-store-DY1_0nzY.js.gz +0 -0
- package/dist/web/assets/files-settings-section-B2lcyh-6.js.br +0 -0
- package/dist/web/assets/files-settings-section-B2lcyh-6.js.gz +0 -0
- package/dist/web/assets/general-settings-section-Doepm2sH.js.br +0 -0
- package/dist/web/assets/general-settings-section-Doepm2sH.js.gz +0 -0
- package/dist/web/assets/git-log-panel-CJakNMDc.js.br +0 -0
- package/dist/web/assets/git-log-panel-CJakNMDc.js.gz +0 -0
- package/dist/web/assets/glide-data-grid-C_SeOhSq.js.br +0 -0
- package/dist/web/assets/glide-data-grid-C_SeOhSq.js.gz +0 -0
- package/dist/web/assets/group-chat-tab--by1xCQW.js +0 -1
- package/dist/web/assets/group-chat-tab--by1xCQW.js.br +0 -0
- package/dist/web/assets/group-chat-tab--by1xCQW.js.gz +0 -0
- package/dist/web/assets/index-DhWjx5iR.js +0 -52
- package/dist/web/assets/index-DhWjx5iR.js.br +0 -0
- package/dist/web/assets/index-DhWjx5iR.js.gz +0 -0
- package/dist/web/assets/index-oIvYSKTT.css +0 -1
- package/dist/web/assets/index-oIvYSKTT.css.br +0 -0
- package/dist/web/assets/index-oIvYSKTT.css.gz +0 -0
- package/dist/web/assets/keybindings-store-30LYZU0l.js +0 -1
- package/dist/web/assets/keyboard-shortcuts-section-BgZe3QDr.js.br +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-BgZe3QDr.js.gz +0 -0
- package/dist/web/assets/language-servers-section-CCMzgOaE.js.br +0 -0
- package/dist/web/assets/language-servers-section-CCMzgOaE.js.gz +0 -0
- package/dist/web/assets/lsp-client-B10suuaZ.js.br +0 -0
- package/dist/web/assets/lsp-client-B10suuaZ.js.gz +0 -0
- package/dist/web/assets/markdown-renderer-AHHFS3jc.js.br +0 -0
- package/dist/web/assets/markdown-renderer-AHHFS3jc.js.gz +0 -0
- package/dist/web/assets/markdown-renderer-DxnXtBoj.js +0 -1
- package/dist/web/assets/mobile-explorer-sheet-CqCsLWSr.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-CqCsLWSr.js.gz +0 -0
- package/dist/web/assets/new-design-dialog-B9tgnQXq.js +0 -1
- package/dist/web/assets/new-design-dialog-B9tgnQXq.js.br +0 -0
- package/dist/web/assets/new-design-dialog-B9tgnQXq.js.gz +0 -0
- package/dist/web/assets/notification-store-HUVzO_bp.js +0 -1
- package/dist/web/assets/notifications-settings-section-CXgzH_bK.js.br +0 -0
- package/dist/web/assets/notifications-settings-section-CXgzH_bK.js.gz +0 -0
- package/dist/web/assets/overview-panel-BYB8mgRE.js.br +0 -0
- package/dist/web/assets/overview-panel-BYB8mgRE.js.gz +0 -0
- package/dist/web/assets/panel-store-CWHPmHwj.js +0 -1
- package/dist/web/assets/panel-store-CWHPmHwj.js.br +0 -0
- package/dist/web/assets/panel-store-CWHPmHwj.js.gz +0 -0
- package/dist/web/assets/performance-panel-Dpuq3g0W.js.br +0 -0
- package/dist/web/assets/performance-panel-Dpuq3g0W.js.gz +0 -0
- package/dist/web/assets/postgres-viewer-DW0Gkmow.js.br +0 -0
- package/dist/web/assets/postgres-viewer-DW0Gkmow.js.gz +0 -0
- package/dist/web/assets/ppmbot-settings-section-DFtlGoW6.js.br +0 -0
- package/dist/web/assets/ppmbot-settings-section-DFtlGoW6.js.gz +0 -0
- package/dist/web/assets/pptx-export-CNyG32Xr.js.br +0 -0
- package/dist/web/assets/pptx-export-CNyG32Xr.js.gz +0 -0
- package/dist/web/assets/problems-panel-V5zqpuXq.js.br +0 -0
- package/dist/web/assets/problems-panel-V5zqpuXq.js.gz +0 -0
- package/dist/web/assets/process-table-DAj46HIl.js +0 -1
- package/dist/web/assets/process-table-DAj46HIl.js.br +0 -0
- package/dist/web/assets/process-table-DAj46HIl.js.gz +0 -0
- package/dist/web/assets/proxy-settings-section-B7AVlTfY.js.br +0 -0
- package/dist/web/assets/proxy-settings-section-B7AVlTfY.js.gz +0 -0
- package/dist/web/assets/query-audit-section-gHmRfWKr.js.br +0 -0
- package/dist/web/assets/query-audit-section-gHmRfWKr.js.gz +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-Cgoznh8L.js +0 -1
- package/dist/web/assets/remote-desktop-clipboard-notice-Cgoznh8L.js.br +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-Cgoznh8L.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-X12QbPhO.js +0 -2
- package/dist/web/assets/remote-desktop-mobile-sheet-X12QbPhO.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-X12QbPhO.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-DsSHawSK.js +0 -1
- package/dist/web/assets/remote-desktop-mobile-view-DsSHawSK.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-DsSHawSK.js.gz +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-CHiQEd9Q.js +0 -1
- package/dist/web/assets/remote-desktop-readiness-gate-CHiQEd9Q.js.br +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-CHiQEd9Q.js.gz +0 -0
- package/dist/web/assets/remote-desktop-window-content-BnznIby3.js +0 -1
- package/dist/web/assets/remote-desktop-window-content-BnznIby3.js.br +0 -0
- package/dist/web/assets/remote-desktop-window-content-BnznIby3.js.gz +0 -0
- package/dist/web/assets/schedules-settings-section-sYDAsBMO.js.br +0 -0
- package/dist/web/assets/schedules-settings-section-sYDAsBMO.js.gz +0 -0
- package/dist/web/assets/send-to-chat-CCdOrKc7.js.br +0 -0
- package/dist/web/assets/send-to-chat-CCdOrKc7.js.gz +0 -0
- package/dist/web/assets/services-panel-CKa2aP1d.js.br +0 -0
- package/dist/web/assets/services-panel-CKa2aP1d.js.gz +0 -0
- package/dist/web/assets/settings-body-CEIF8lkl.js +0 -2
- package/dist/web/assets/settings-body-CEIF8lkl.js.br +0 -0
- package/dist/web/assets/settings-body-CEIF8lkl.js.gz +0 -0
- package/dist/web/assets/settings-store-DfuiLq_9.js.br +0 -0
- package/dist/web/assets/settings-store-DfuiLq_9.js.gz +0 -0
- package/dist/web/assets/settings-tab-Du5AUPni.js +0 -1
- package/dist/web/assets/settings-window-content-BTflfLez.js +0 -1
- package/dist/web/assets/slash-command-picker-CLevA_Gf.js.br +0 -0
- package/dist/web/assets/slash-command-picker-CLevA_Gf.js.gz +0 -0
- package/dist/web/assets/sql-query-editor-Db8b0jEr.js.br +0 -0
- package/dist/web/assets/sql-query-editor-Db8b0jEr.js.gz +0 -0
- package/dist/web/assets/sqlite-viewer-VzYLMqkD.js.br +0 -0
- package/dist/web/assets/sqlite-viewer-VzYLMqkD.js.gz +0 -0
- package/dist/web/assets/system-monitor-body-BR60W_mG.js.br +0 -0
- package/dist/web/assets/system-monitor-body-BR60W_mG.js.gz +0 -0
- package/dist/web/assets/system-monitor-tab-BYpantE2.js +0 -1
- package/dist/web/assets/tab-host-window-content-BL61SB5m.js.br +0 -0
- package/dist/web/assets/tab-host-window-content-BL61SB5m.js.gz +0 -0
- package/dist/web/assets/terminal-tab-B4y6at4h.js.br +0 -0
- package/dist/web/assets/terminal-tab-B4y6at4h.js.gz +0 -0
- package/dist/web/assets/tool-cards-K0r_yxI0.js.br +0 -0
- package/dist/web/assets/tool-cards-K0r_yxI0.js.gz +0 -0
- package/dist/web/assets/use-inline-blame-BK23icsE.js.br +0 -0
- package/dist/web/assets/use-inline-blame-BK23icsE.js.gz +0 -0
- package/dist/web/assets/use-lsp-CRdCPCKW.js.br +0 -0
- package/dist/web/assets/use-lsp-CRdCPCKW.js.gz +0 -0
- package/dist/web/assets/use-process-kill-BWYX03KF.js.br +0 -0
- package/dist/web/assets/use-process-kill-BWYX03KF.js.gz +0 -0
- package/dist/web/assets/video-preview-DugVkMB7.js +0 -1
- package/dist/web/assets/video-preview-DugVkMB7.js.br +0 -0
- package/dist/web/assets/video-preview-DugVkMB7.js.gz +0 -0
- package/dist/web/assets/voice-settings-section-Ceihggqf.js.br +0 -0
- package/dist/web/assets/voice-settings-section-Ceihggqf.js.gz +0 -0
- /package/dist/web/assets/{__vite-browser-external-B_XMEguz.js → __vite-browser-external-C5mXParC.js} +0 -0
- /package/dist/web/assets/{agent-session-sheet-store-DO682WSg.js → agent-session-sheet-store-_OeezCXB.js} +0 -0
- /package/dist/web/assets/{api-settings-CpO8SPk9.js → api-settings-Cc_CpfmC.js} +0 -0
- /package/dist/web/assets/{api-settings-CpO8SPk9.js.br → api-settings-Cc_CpfmC.js.br} +0 -0
- /package/dist/web/assets/{api-settings-CpO8SPk9.js.gz → api-settings-Cc_CpfmC.js.gz} +0 -0
- /package/dist/web/assets/{api-themes-CJipDH_l.js → api-themes-BtMBSjuM.js} +0 -0
- /package/dist/web/assets/{can-open-in-ppm-Bw_xiOXX.js → can-open-in-ppm-DeE2-kd5.js} +0 -0
- /package/dist/web/assets/{can-open-in-ppm-Bw_xiOXX.js.br → can-open-in-ppm-DeE2-kd5.js.br} +0 -0
- /package/dist/web/assets/{can-open-in-ppm-Bw_xiOXX.js.gz → can-open-in-ppm-DeE2-kd5.js.gz} +0 -0
- /package/dist/web/assets/{csv-parser-DcRWxcNV.js → csv-parser-CoxPdyLw.js} +0 -0
- /package/dist/web/assets/{csv-parser-DcRWxcNV.js.br → csv-parser-CoxPdyLw.js.br} +0 -0
- /package/dist/web/assets/{csv-parser-DcRWxcNV.js.gz → csv-parser-CoxPdyLw.js.gz} +0 -0
- /package/dist/web/assets/{design-export-client-CHffN3DM.js → design-export-client-BfdZB7xK.js} +0 -0
- /package/dist/web/assets/{design-export-client-CHffN3DM.js.br → design-export-client-BfdZB7xK.js.br} +0 -0
- /package/dist/web/assets/{design-export-client-CHffN3DM.js.gz → design-export-client-BfdZB7xK.js.gz} +0 -0
- /package/dist/web/assets/{design-responsive-dialog-DSCpv6O1.js → design-responsive-dialog-BiYy3ibS.js} +0 -0
- /package/dist/web/assets/{design-responsive-dialog-DSCpv6O1.js.br → design-responsive-dialog-BiYy3ibS.js.br} +0 -0
- /package/dist/web/assets/{design-responsive-dialog-DSCpv6O1.js.gz → design-responsive-dialog-BiYy3ibS.js.gz} +0 -0
- /package/dist/web/assets/{design-system-status-label-B_0I_3a8.js → design-system-status-label-ekiLyjKn.js} +0 -0
- /package/dist/web/assets/{docx-preview-B3eJyY2y.js → docx-preview-Dt6RQpfA.js} +0 -0
- /package/dist/web/assets/{docx-preview-B3eJyY2y.js.br → docx-preview-Dt6RQpfA.js.br} +0 -0
- /package/dist/web/assets/{docx-preview-B3eJyY2y.js.gz → docx-preview-Dt6RQpfA.js.gz} +0 -0
- /package/dist/web/assets/{editor-font-BNwo7zo5.js → editor-font-CBszn6vt.js} +0 -0
- /package/dist/web/assets/{format-bytes-CRZfxO2J.js → format-bytes-FQmpZ25A.js} +0 -0
- /package/dist/web/assets/{image-gallery-DC3gRAqk.js → image-gallery-B16hO6Ab.js} +0 -0
- /package/dist/web/assets/{image-preview-geometry-DN3DZQp0.js → image-preview-geometry-D4Ihz4UV.js} +0 -0
- /package/dist/web/assets/{lib-B9k5tn9s.js → lib-6APiJkU3.js} +0 -0
- /package/dist/web/assets/{lib-B9k5tn9s.js.br → lib-6APiJkU3.js.br} +0 -0
- /package/dist/web/assets/{lib-B9k5tn9s.js.gz → lib-6APiJkU3.js.gz} +0 -0
- /package/dist/web/assets/{libesm-5f7T4AHA.js → libesm-5wREk7JN.js} +0 -0
- /package/dist/web/assets/{libesm-5f7T4AHA.js.br → libesm-5wREk7JN.js.br} +0 -0
- /package/dist/web/assets/{libesm-5f7T4AHA.js.gz → libesm-5wREk7JN.js.gz} +0 -0
- /package/dist/web/assets/{media-url-CCGOIxzS.js → media-url-CrT-qulZ.js} +0 -0
- /package/dist/web/assets/{metric-chart-canvas-CjFbZuhk.js → metric-chart-canvas-D7f-LQ8x.js} +0 -0
- /package/dist/web/assets/{metric-chart-canvas-CjFbZuhk.js.br → metric-chart-canvas-D7f-LQ8x.js.br} +0 -0
- /package/dist/web/assets/{metric-chart-canvas-CjFbZuhk.js.gz → metric-chart-canvas-D7f-LQ8x.js.gz} +0 -0
- /package/dist/web/assets/{monaco-dotenv-language-Bwq3gQsi.js → monaco-dotenv-language-DbSss7YR.js} +0 -0
- /package/dist/web/assets/{onboarding-entry-CuH1Kqwu.js → onboarding-entry-CGFdGLLP.js} +0 -0
- /package/dist/web/assets/{onboarding-entry-CuH1Kqwu.js.br → onboarding-entry-CGFdGLLP.js.br} +0 -0
- /package/dist/web/assets/{onboarding-entry-CuH1Kqwu.js.gz → onboarding-entry-CGFdGLLP.js.gz} +0 -0
- /package/dist/web/assets/{pdf-preview-X8bdpfE9.js → pdf-preview-B8A4T-ES.js} +0 -0
- /package/dist/web/assets/{pdf-preview-X8bdpfE9.js.br → pdf-preview-B8A4T-ES.js.br} +0 -0
- /package/dist/web/assets/{pdf-preview-X8bdpfE9.js.gz → pdf-preview-B8A4T-ES.js.gz} +0 -0
- /package/dist/web/assets/{separator-DMu7VFQa.js → separator-dHj3Km--.js} +0 -0
- /package/dist/web/assets/{sortable-header-CipIP7aH.js → sortable-header-BOa8o7NJ.js} +0 -0
- /package/dist/web/assets/{sortable-header-CipIP7aH.js.br → sortable-header-BOa8o7NJ.js.br} +0 -0
- /package/dist/web/assets/{sortable-header-CipIP7aH.js.gz → sortable-header-BOa8o7NJ.js.gz} +0 -0
- /package/dist/web/assets/{use-blob-url-DjuRQWnR.js → use-blob-url-BC69n15x.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-Be4L1NHd.js → vendor-xterm-CfasyE0m.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-Be4L1NHd.js.br → vendor-xterm-CfasyE0m.js.br} +0 -0
- /package/dist/web/assets/{vendor-xterm-Be4L1NHd.js.gz → vendor-xterm-CfasyE0m.js.gz} +0 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One live viewing session per *device*, shared by every viewer looking at it.
|
|
3
|
+
*
|
|
4
|
+
* The pipeline is per device rather than per socket deliberately: a second viewer must not cost
|
|
5
|
+
* a second gRPC stream and a second ffmpeg for the same pixels. What is per socket is the
|
|
6
|
+
* *controller lease* — plan §5 — because two people sending touches into one guest at once is
|
|
7
|
+
* not a feature, it is a fight.
|
|
8
|
+
*
|
|
9
|
+
* Two generations ride on every frame and are checked on every input, and they answer different
|
|
10
|
+
* questions (see `src/shared/android-protocol.ts`):
|
|
11
|
+
* - `sessionGeneration` changes when the lease moves. Input from a client that has been
|
|
12
|
+
* superseded but does not know it yet is dropped rather than applied.
|
|
13
|
+
* - `geometryGeneration` changes when the emulator changes frame size or rotation. Input
|
|
14
|
+
* carrying an old geometry describes a screen that no longer exists, so it is rejected
|
|
15
|
+
* instead of being mapped onto the new one.
|
|
16
|
+
*
|
|
17
|
+
* A viewer that stops sending heartbeats loses the lease after `LEASE_EXPIRY_MS` — a laptop lid
|
|
18
|
+
* closed mid-session must not hold the device hostage — but it keeps *watching*, because losing
|
|
19
|
+
* a network for four seconds should not close the tab's picture.
|
|
20
|
+
*/
|
|
21
|
+
import { connectToEmulator, getEmulatorStatus, type EmulatorChannel } from "./android-grpc.ts";
|
|
22
|
+
import { startVideoPipeline, type FrameGeometry, type RunningPipeline } from "./android-video.ts";
|
|
23
|
+
import {
|
|
24
|
+
releaseAllTouches, sendHardwareKey, sendKey, sendText, sendTouch, setRotation,
|
|
25
|
+
type HardwareKey,
|
|
26
|
+
} from "./android-input.ts";
|
|
27
|
+
import { subscribeLogcat, closeDeviceLogcat, LOGCAT_RING_SIZE } from "./android-logcat.ts";
|
|
28
|
+
import type { RunningEmulator } from "./emulator-discovery.ts";
|
|
29
|
+
import {
|
|
30
|
+
encodeVideoFrame, type AndroidClientMessage, type AndroidGeometry, type AndroidLogEntry,
|
|
31
|
+
type AndroidQuality, type AndroidServerMessage,
|
|
32
|
+
} from "../../shared/android-protocol.ts";
|
|
33
|
+
|
|
34
|
+
/** Plan §5: heartbeat every 5s, lease gone after 15s of silence. */
|
|
35
|
+
const LEASE_EXPIRY_MS = 15_000;
|
|
36
|
+
const LEASE_SWEEP_MS = 2_500;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Stop queueing delta frames for a viewer whose socket is this far behind.
|
|
40
|
+
*
|
|
41
|
+
* Deliberately a single threshold and not a ladder. `ws.getBufferedAmount()` is Bun's *userspace*
|
|
42
|
+
* queue and drains into the kernel socket buffer, so it was measured effectively inert even at
|
|
43
|
+
* 0.4 Mbit/s over a real relay (CLAUDE.md, remote desktop). Anything more elaborate built on it
|
|
44
|
+
* would be untestable by construction; this exists only so a genuinely stuck socket cannot grow
|
|
45
|
+
* without bound. Keyframes are never dropped — dropping one costs the client the whole GOP.
|
|
46
|
+
*/
|
|
47
|
+
const MAX_SOCKET_BACKLOG_BYTES = 4 * 1024 * 1024;
|
|
48
|
+
|
|
49
|
+
/** Coalescing window for log entries. One WS frame per gRPC message would swamp the video. */
|
|
50
|
+
const LOGCAT_FLUSH_MS = 120;
|
|
51
|
+
|
|
52
|
+
export interface AndroidSocket {
|
|
53
|
+
send(data: string | Uint8Array): number;
|
|
54
|
+
close(code?: number, reason?: string): void;
|
|
55
|
+
getBufferedAmount?(): number;
|
|
56
|
+
/** True once the socket's close has been handled. A socket can close while its session is
|
|
57
|
+
* still starting, and its close handler then had no viewer to remove. */
|
|
58
|
+
isClosed?(): boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface Viewer {
|
|
62
|
+
id: string;
|
|
63
|
+
socket: AndroidSocket;
|
|
64
|
+
lastSeen: number;
|
|
65
|
+
sequence: number;
|
|
66
|
+
/** A viewer cannot decode anything until a keyframe arrives, so deltas before it are noise. */
|
|
67
|
+
sawKeyframe: boolean;
|
|
68
|
+
visible: boolean;
|
|
69
|
+
/** Set while this viewer has the log panel open; calling it stops the subscription. */
|
|
70
|
+
logcatOff: (() => void) | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface DeviceSession {
|
|
74
|
+
deviceId: string;
|
|
75
|
+
emulator: RunningEmulator;
|
|
76
|
+
channel: EmulatorChannel;
|
|
77
|
+
pipeline: RunningPipeline;
|
|
78
|
+
viewers: Map<string, Viewer>;
|
|
79
|
+
controllerId: string | null;
|
|
80
|
+
sessionGeneration: number;
|
|
81
|
+
geometryGeneration: number;
|
|
82
|
+
deviceWidth: number;
|
|
83
|
+
deviceHeight: number;
|
|
84
|
+
quality: AndroidQuality;
|
|
85
|
+
/** Last codec string announced, so a change is sent once rather than per keyframe. */
|
|
86
|
+
codec: string | null;
|
|
87
|
+
sweep: ReturnType<typeof setInterval>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const sessions = new Map<string, DeviceSession>();
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Sessions still starting, by device. `openDeviceSession` awaits twice before there is a session
|
|
94
|
+
* to put in `sessions`, so without this two viewers arriving together each started a gRPC stream
|
|
95
|
+
* and an ffmpeg, and the second replaced the first in the map — leaving a pipeline running that
|
|
96
|
+
* nothing could reach or stop.
|
|
97
|
+
*/
|
|
98
|
+
const opening = new Map<string, Promise<DeviceSession>>();
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Viewers still waiting on a session that is starting, by device. Every waiter resumes from the
|
|
102
|
+
* same promise, one after the other, so one whose socket closed in the meantime must not take an
|
|
103
|
+
* empty session down while another is about to attach to it.
|
|
104
|
+
*/
|
|
105
|
+
const waiting = new Map<string, number>();
|
|
106
|
+
|
|
107
|
+
function openShared(opts: AttachOptions, open: typeof openDeviceSession): Promise<DeviceSession> {
|
|
108
|
+
const pending = opening.get(opts.deviceId);
|
|
109
|
+
if (pending) return pending;
|
|
110
|
+
const started: Promise<DeviceSession> = open(opts.deviceId, opts.emulator, opts.quality)
|
|
111
|
+
.then((session) => {
|
|
112
|
+
sessions.set(opts.deviceId, session);
|
|
113
|
+
return session;
|
|
114
|
+
})
|
|
115
|
+
.finally(() => {
|
|
116
|
+
if (opening.get(opts.deviceId) === started) opening.delete(opts.deviceId);
|
|
117
|
+
});
|
|
118
|
+
opening.set(opts.deviceId, started);
|
|
119
|
+
return started;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function send(viewer: Viewer, message: AndroidServerMessage): void {
|
|
123
|
+
try { viewer.socket.send(JSON.stringify(message)); } catch { /* closing */ }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function geometryMessage(s: DeviceSession): AndroidGeometry {
|
|
127
|
+
const g = s.pipeline.geometry;
|
|
128
|
+
return {
|
|
129
|
+
width: g.width,
|
|
130
|
+
height: g.height,
|
|
131
|
+
deviceWidth: s.deviceWidth,
|
|
132
|
+
deviceHeight: s.deviceHeight,
|
|
133
|
+
rotation: g.rotation,
|
|
134
|
+
generation: s.geometryGeneration,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------------------------
|
|
139
|
+
// The device session
|
|
140
|
+
// ---------------------------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
async function openDeviceSession(
|
|
143
|
+
deviceId: string,
|
|
144
|
+
emulator: RunningEmulator,
|
|
145
|
+
quality: AndroidQuality,
|
|
146
|
+
): Promise<DeviceSession> {
|
|
147
|
+
const channel = connectToEmulator(emulator);
|
|
148
|
+
const status = await getEmulatorStatus(channel);
|
|
149
|
+
|
|
150
|
+
// Placeholders until the pipeline resolves — it is the thing that knows the real geometry and
|
|
151
|
+
// the real codec string, and its callbacks fire during its own startup, before either exists.
|
|
152
|
+
let self: DeviceSession | null = null;
|
|
153
|
+
let pipelineRef: RunningPipeline | null = null;
|
|
154
|
+
|
|
155
|
+
const pipeline = await startVideoPipeline({
|
|
156
|
+
emulator,
|
|
157
|
+
quality,
|
|
158
|
+
onAccessUnit: (au, ptsMs) => {
|
|
159
|
+
if (!self) return;
|
|
160
|
+
// A keyframe may carry a new SPS — a rung switch and a rotation both respawn the encoder,
|
|
161
|
+
// and the profile/level are encoder-default, so the client must be reconfigured before it
|
|
162
|
+
// decodes anything from the new bitstream.
|
|
163
|
+
if (au.isKey) {
|
|
164
|
+
const codec = pipelineRef?.codecString() ?? null;
|
|
165
|
+
if (codec && codec !== self.codec) {
|
|
166
|
+
self.codec = codec;
|
|
167
|
+
for (const viewer of self.viewers.values()) send(viewer, { type: "codec", codec });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const viewer of self.viewers.values()) {
|
|
171
|
+
if (!viewer.visible) continue;
|
|
172
|
+
if (au.isKey) viewer.sawKeyframe = true;
|
|
173
|
+
else if (!viewer.sawKeyframe) continue;
|
|
174
|
+
if (!au.isKey && (viewer.socket.getBufferedAmount?.() ?? 0) > MAX_SOCKET_BACKLOG_BYTES) continue;
|
|
175
|
+
const frame = encodeVideoFrame({
|
|
176
|
+
keyframe: au.isKey,
|
|
177
|
+
codecConfig: false,
|
|
178
|
+
sessionGeneration: self.sessionGeneration,
|
|
179
|
+
geometryGeneration: self.geometryGeneration,
|
|
180
|
+
sequence: viewer.sequence++,
|
|
181
|
+
ptsMs,
|
|
182
|
+
}, au.bytes);
|
|
183
|
+
try { viewer.socket.send(frame); } catch { /* closing */ }
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
onGeometry: (g: FrameGeometry) => {
|
|
187
|
+
if (!self) return; // the first call is the startup one, already known
|
|
188
|
+
self.geometryGeneration = (self.geometryGeneration + 1) & 0xffff;
|
|
189
|
+
self.codec = null;
|
|
190
|
+
// Every viewer's decoder is now holding parameters for a size that no longer exists.
|
|
191
|
+
for (const viewer of self.viewers.values()) {
|
|
192
|
+
viewer.sawKeyframe = false;
|
|
193
|
+
send(viewer, { type: "geometry", geometry: geometryMessage(self) });
|
|
194
|
+
}
|
|
195
|
+
void g;
|
|
196
|
+
},
|
|
197
|
+
onError: (message) => {
|
|
198
|
+
if (!self) return;
|
|
199
|
+
for (const viewer of self.viewers.values()) send(viewer, { type: "error", message });
|
|
200
|
+
},
|
|
201
|
+
}).catch((e) => {
|
|
202
|
+
channel.close();
|
|
203
|
+
throw e;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
pipelineRef = pipeline;
|
|
207
|
+
|
|
208
|
+
const session: DeviceSession = {
|
|
209
|
+
deviceId,
|
|
210
|
+
emulator,
|
|
211
|
+
channel,
|
|
212
|
+
pipeline,
|
|
213
|
+
viewers: new Map(),
|
|
214
|
+
controllerId: null,
|
|
215
|
+
sessionGeneration: 1,
|
|
216
|
+
geometryGeneration: 1,
|
|
217
|
+
// The guest's own size, which the frame is a scaled copy of. Falls back to the frame's own
|
|
218
|
+
// size when the AVD does not declare one, so the viewer's coordinate maths always has a base.
|
|
219
|
+
deviceWidth: status.displayWidth ?? pipeline.geometry.width,
|
|
220
|
+
deviceHeight: status.displayHeight ?? pipeline.geometry.height,
|
|
221
|
+
quality,
|
|
222
|
+
codec: pipeline.codecString(),
|
|
223
|
+
sweep: setInterval(() => sweepLease(session), LEASE_SWEEP_MS),
|
|
224
|
+
};
|
|
225
|
+
self = session;
|
|
226
|
+
return session;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function closeDeviceSession(session: DeviceSession): Promise<void> {
|
|
230
|
+
clearInterval(session.sweep);
|
|
231
|
+
// Only what is this session's own: the device may already have a newer one, and both the map
|
|
232
|
+
// entry and the logcat stream are keyed by device.
|
|
233
|
+
if (sessions.get(session.deviceId) === session) {
|
|
234
|
+
sessions.delete(session.deviceId);
|
|
235
|
+
// The logcat stream holds this same channel, so it has to go before the channel is closed —
|
|
236
|
+
// and it would not be dropped by the last viewer leaving if that viewer died without a close.
|
|
237
|
+
closeDeviceLogcat(session.deviceId);
|
|
238
|
+
}
|
|
239
|
+
await session.pipeline.stop().catch(() => {});
|
|
240
|
+
// Leave no finger down on a guest PPM is walking away from: the emulator does expire a touch
|
|
241
|
+
// slot on its own, but only after 120 seconds.
|
|
242
|
+
await releaseAllTouches(session.channel).catch(() => {});
|
|
243
|
+
session.channel.close();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** A controller that has stopped heartbeating loses the lease; it keeps watching. */
|
|
247
|
+
function sweepLease(session: DeviceSession): void {
|
|
248
|
+
const controller = session.controllerId ? session.viewers.get(session.controllerId) : undefined;
|
|
249
|
+
if (!controller) return;
|
|
250
|
+
if (Date.now() - controller.lastSeen <= LEASE_EXPIRY_MS) return;
|
|
251
|
+
void releaseAllTouches(session.channel).catch(() => {});
|
|
252
|
+
send(controller, { type: "controller", controller: false, reason: "lease expired" });
|
|
253
|
+
session.controllerId = null;
|
|
254
|
+
session.sessionGeneration = (session.sessionGeneration + 1) & 0xffff;
|
|
255
|
+
// Hand it to whoever has been alive most recently rather than leaving the device unusable.
|
|
256
|
+
const next = [...session.viewers.values()]
|
|
257
|
+
.filter((v) => v.id !== controller.id && Date.now() - v.lastSeen <= LEASE_EXPIRY_MS)
|
|
258
|
+
.sort((a, b) => b.lastSeen - a.lastSeen)[0];
|
|
259
|
+
if (next) {
|
|
260
|
+
session.controllerId = next.id;
|
|
261
|
+
send(next, { type: "controller", controller: true, reason: "the previous controller went away" });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function grantControl(session: DeviceSession, viewer: Viewer): void {
|
|
266
|
+
if (session.controllerId === viewer.id) return;
|
|
267
|
+
const previous = session.controllerId ? session.viewers.get(session.controllerId) : undefined;
|
|
268
|
+
session.controllerId = viewer.id;
|
|
269
|
+
session.sessionGeneration = (session.sessionGeneration + 1) & 0xffff;
|
|
270
|
+
// The outgoing controller may have fingers down; nothing else will ever release them.
|
|
271
|
+
void releaseAllTouches(session.channel).catch(() => {});
|
|
272
|
+
if (previous) send(previous, { type: "controller", controller: false, reason: "another viewer took control" });
|
|
273
|
+
send(viewer, { type: "controller", controller: true });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---------------------------------------------------------------------------------------------
|
|
277
|
+
// The per-socket viewer
|
|
278
|
+
// ---------------------------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
export interface AndroidViewerSession {
|
|
281
|
+
handleClientMessage(text: string): Promise<void>;
|
|
282
|
+
close(): void;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface AttachOptions {
|
|
286
|
+
deviceId: string;
|
|
287
|
+
emulator: RunningEmulator;
|
|
288
|
+
quality: AndroidQuality;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** `open` is what a test stands in for: the real one needs a running emulator. */
|
|
292
|
+
export async function attachAndroidViewer(
|
|
293
|
+
socket: AndroidSocket,
|
|
294
|
+
opts: AttachOptions,
|
|
295
|
+
open: typeof openDeviceSession = openDeviceSession,
|
|
296
|
+
): Promise<AndroidViewerSession> {
|
|
297
|
+
// Counted in this function rather than a helper of its own: an `await` on a helper would put
|
|
298
|
+
// a tick between the count going down and the check below, and every waiter would then see an
|
|
299
|
+
// empty count before the live one had attached.
|
|
300
|
+
let session: DeviceSession;
|
|
301
|
+
const known = sessions.get(opts.deviceId);
|
|
302
|
+
if (known) {
|
|
303
|
+
session = known;
|
|
304
|
+
} else {
|
|
305
|
+
waiting.set(opts.deviceId, (waiting.get(opts.deviceId) ?? 0) + 1);
|
|
306
|
+
try {
|
|
307
|
+
session = await openShared(opts, open);
|
|
308
|
+
} finally {
|
|
309
|
+
const left = (waiting.get(opts.deviceId) ?? 1) - 1;
|
|
310
|
+
if (left > 0) waiting.set(opts.deviceId, left);
|
|
311
|
+
else waiting.delete(opts.deviceId);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Closed while the session was starting: nothing else will ever remove this viewer, so it is
|
|
316
|
+
// never added. The session goes too if nobody is on it and nobody else is still waiting for
|
|
317
|
+
// it, or a device with no viewer left keeps its ffmpeg encoding for good.
|
|
318
|
+
if (socket.isClosed?.()) {
|
|
319
|
+
if (session.viewers.size === 0 && !waiting.has(opts.deviceId)) void closeDeviceSession(session);
|
|
320
|
+
return { async handleClientMessage() {}, close() {} };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const viewer: Viewer = {
|
|
324
|
+
id: crypto.randomUUID(),
|
|
325
|
+
socket,
|
|
326
|
+
lastSeen: Date.now(),
|
|
327
|
+
sequence: 0,
|
|
328
|
+
sawKeyframe: false,
|
|
329
|
+
visible: true,
|
|
330
|
+
logcatOff: null,
|
|
331
|
+
};
|
|
332
|
+
session.viewers.set(viewer.id, viewer);
|
|
333
|
+
|
|
334
|
+
// First one in gets the lease; later ones watch until they ask for it.
|
|
335
|
+
if (!session.controllerId) session.controllerId = viewer.id;
|
|
336
|
+
|
|
337
|
+
send(viewer, {
|
|
338
|
+
type: "ready",
|
|
339
|
+
sessionId: viewer.id,
|
|
340
|
+
sessionGeneration: session.sessionGeneration,
|
|
341
|
+
geometry: geometryMessage(session),
|
|
342
|
+
controller: session.controllerId === viewer.id,
|
|
343
|
+
codec: session.codec,
|
|
344
|
+
encoder: session.pipeline.encoder,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const isController = () => session.controllerId === viewer.id;
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Logcat is delivered in batches on a timer rather than per gRPC message.
|
|
351
|
+
*
|
|
352
|
+
* A device that is logging hard produces hundreds of entries a second, and one WS frame each
|
|
353
|
+
* would both flood the socket and starve the video frames sharing it. `LOGCAT_FLUSH_MS` is the
|
|
354
|
+
* coalescing window; a viewer whose socket is already backed up is skipped entirely, because
|
|
355
|
+
* the ring buffer means the entries are not lost — they are re-sent from the backlog when the
|
|
356
|
+
* panel is reopened.
|
|
357
|
+
*/
|
|
358
|
+
let logBatch: AndroidLogEntry[] = [];
|
|
359
|
+
let logTimer: ReturnType<typeof setTimeout> | null = null;
|
|
360
|
+
|
|
361
|
+
function flushLog(): void {
|
|
362
|
+
logTimer = null;
|
|
363
|
+
const entries = logBatch;
|
|
364
|
+
logBatch = [];
|
|
365
|
+
if (entries.length === 0) return;
|
|
366
|
+
if ((viewer.socket.getBufferedAmount?.() ?? 0) > MAX_SOCKET_BACKLOG_BYTES) return;
|
|
367
|
+
send(viewer, { type: "log", entries });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function startLogcat(): void {
|
|
371
|
+
if (viewer.logcatOff) return;
|
|
372
|
+
viewer.logcatOff = subscribeLogcat(session.deviceId, session.channel, (entries) => {
|
|
373
|
+
logBatch.push(...entries);
|
|
374
|
+
// Never let the pending batch outgrow the ring it came from.
|
|
375
|
+
if (logBatch.length > LOGCAT_RING_SIZE) logBatch = logBatch.slice(-LOGCAT_RING_SIZE);
|
|
376
|
+
if (!logTimer) logTimer = setTimeout(flushLog, LOGCAT_FLUSH_MS);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function stopLogcat(): void {
|
|
381
|
+
viewer.logcatOff?.();
|
|
382
|
+
viewer.logcatOff = null;
|
|
383
|
+
if (logTimer) { clearTimeout(logTimer); logTimer = null; }
|
|
384
|
+
logBatch = [];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function handle(message: AndroidClientMessage): Promise<void> {
|
|
388
|
+
viewer.lastSeen = Date.now();
|
|
389
|
+
|
|
390
|
+
switch (message.type) {
|
|
391
|
+
case "heartbeat":
|
|
392
|
+
send(viewer, { type: "heartbeat" });
|
|
393
|
+
return;
|
|
394
|
+
|
|
395
|
+
case "visibility":
|
|
396
|
+
viewer.visible = message.visible;
|
|
397
|
+
// The pipeline only pauses when *nobody* is watching: another viewer's tab being open is
|
|
398
|
+
// reason enough to keep encoding.
|
|
399
|
+
session.pipeline.setPaused(![...session.viewers.values()].some((v) => v.visible));
|
|
400
|
+
if (message.visible) viewer.sawKeyframe = false; // resync from the next keyframe
|
|
401
|
+
return;
|
|
402
|
+
|
|
403
|
+
case "take-control":
|
|
404
|
+
grantControl(session, viewer);
|
|
405
|
+
return;
|
|
406
|
+
|
|
407
|
+
case "quality":
|
|
408
|
+
if (!isController()) return;
|
|
409
|
+
if (message.quality === session.quality) return;
|
|
410
|
+
session.quality = message.quality;
|
|
411
|
+
session.codec = null;
|
|
412
|
+
await session.pipeline.setQuality(message.quality);
|
|
413
|
+
for (const v of session.viewers.values()) {
|
|
414
|
+
v.sawKeyframe = false;
|
|
415
|
+
send(v, { type: "quality", quality: message.quality });
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
|
|
419
|
+
case "input-reset":
|
|
420
|
+
if (!isController()) return;
|
|
421
|
+
await releaseAllTouches(session.channel);
|
|
422
|
+
return;
|
|
423
|
+
|
|
424
|
+
case "logcat":
|
|
425
|
+
// Above the lease gate on purpose: reading the log is not controlling the device, and a
|
|
426
|
+
// second person watching a crash while someone else drives is the normal case. Putting
|
|
427
|
+
// this in the switch below would silently refuse every viewer without the lease.
|
|
428
|
+
if (message.subscribe) startLogcat(); else stopLogcat();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Everything below this line is input, and input needs the lease.
|
|
433
|
+
if (!isController()) {
|
|
434
|
+
send(viewer, { type: "controller", controller: false, reason: "another viewer holds control" });
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
switch (message.type) {
|
|
439
|
+
case "touch":
|
|
440
|
+
// Coordinates describe a geometry; if it has changed they describe nothing.
|
|
441
|
+
if (message.geometryGeneration !== session.geometryGeneration) return;
|
|
442
|
+
await sendTouch(session.channel, message.touches);
|
|
443
|
+
return;
|
|
444
|
+
case "key":
|
|
445
|
+
if (message.geometryGeneration !== session.geometryGeneration) return;
|
|
446
|
+
await sendKey(session.channel, message.key, message.action);
|
|
447
|
+
return;
|
|
448
|
+
case "text":
|
|
449
|
+
case "paste":
|
|
450
|
+
// `sendText` picks the path: ASCII types, anything else goes via the clipboard, because
|
|
451
|
+
// `sendKey.text` silently drops every non-ASCII character (measured in Phase 0).
|
|
452
|
+
await sendText(session.channel, message.text);
|
|
453
|
+
return;
|
|
454
|
+
case "hardware":
|
|
455
|
+
await sendHardwareKey(session.channel, message.key as HardwareKey);
|
|
456
|
+
return;
|
|
457
|
+
case "rotate":
|
|
458
|
+
await setRotation(session.channel, message.rotation);
|
|
459
|
+
// Nothing is announced here: the next frame carries the emulator's own rotation and the
|
|
460
|
+
// pipeline's `onGeometry` is what tells every viewer.
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const viewerSession: AndroidViewerSession = {
|
|
466
|
+
async handleClientMessage(text: string) {
|
|
467
|
+
let message: AndroidClientMessage;
|
|
468
|
+
try { message = JSON.parse(text); } catch { return; }
|
|
469
|
+
if (!message || typeof message.type !== "string") return;
|
|
470
|
+
try {
|
|
471
|
+
await handle(message);
|
|
472
|
+
} catch (e) {
|
|
473
|
+
send(viewer, { type: "error", message: (e as Error).message });
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
close() {
|
|
478
|
+
stopLogcat();
|
|
479
|
+
session.viewers.delete(viewer.id);
|
|
480
|
+
if (session.controllerId === viewer.id) {
|
|
481
|
+
session.controllerId = null;
|
|
482
|
+
void releaseAllTouches(session.channel).catch(() => {});
|
|
483
|
+
// Hand the lease on rather than leaving the device uncontrollable for 15 seconds.
|
|
484
|
+
const next = [...session.viewers.values()].sort((a, b) => b.lastSeen - a.lastSeen)[0];
|
|
485
|
+
if (next) {
|
|
486
|
+
session.controllerId = next.id;
|
|
487
|
+
session.sessionGeneration = (session.sessionGeneration + 1) & 0xffff;
|
|
488
|
+
send(next, { type: "controller", controller: true, reason: "the previous controller disconnected" });
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (session.viewers.size === 0) void closeDeviceSession(session);
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
return viewerSession;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** The emulator keeps running; only PPM's view of it is torn down. */
|
|
498
|
+
export async function closeAllAndroidSessions(): Promise<void> {
|
|
499
|
+
await Promise.all([...sessions.values()].map((s) => closeDeviceSession(s)));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
let sweepRegistered = false;
|
|
503
|
+
export function registerAndroidExitSweep(): void {
|
|
504
|
+
if (sweepRegistered) return;
|
|
505
|
+
sweepRegistered = true;
|
|
506
|
+
// ffmpeg would otherwise outlive PPM: it is spawned detached from any shell job control.
|
|
507
|
+
for (const signal of ["exit", "SIGINT", "SIGTERM"] as const) {
|
|
508
|
+
process.on(signal, () => { void closeAllAndroidSessions(); });
|
|
509
|
+
}
|
|
510
|
+
}
|