@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,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gRPC RGB888 frames -> ffmpeg -> H.264 access units.
|
|
3
|
+
*
|
|
4
|
+
* This is ADR-B option 1, chosen on Phase 0 measurements: grpc-js under Bun sustained 40 fps and
|
|
5
|
+
* 297 MiB/s at full resolution with zero dropped frames, so the transport is not the bottleneck
|
|
6
|
+
* the plan feared. Six things here come from that work or from the proto, and each one is a
|
|
7
|
+
* silent bug if removed:
|
|
8
|
+
*
|
|
9
|
+
* 1. **No `vflip`, despite what the proto says.** `emulator_controller.proto` states the pixel
|
|
10
|
+
* buffer runs "left to right and bottom up", and both the plan and the Phase 0 report took
|
|
11
|
+
* that at its word. It is wrong on emulator 36.5.10: matched row by row against
|
|
12
|
+
* `adb exec-out screencap`, which is unambiguously top-down, the gRPC buffer agrees at a mean
|
|
13
|
+
* difference of **1.21** per pixel and disagrees at **37.15** when flipped. A `vflip` here
|
|
14
|
+
* would render every screen upside down, with no error from anything.
|
|
15
|
+
* 2. **The requested size is a bounding box, and the emulator decides the real one.** The proto
|
|
16
|
+
* is explicit: "The returned image will never exceed the given width, but can be less",
|
|
17
|
+
* scaled "while maintaining the aspect ratio of the device". Phase 0 asked 720x1600 and got
|
|
18
|
+
* exactly that only because it matched the device's aspect. Every frame carries the real
|
|
19
|
+
* `format.width/height/rotation` as **output** fields, so those are read rather than assumed
|
|
20
|
+
* — which is also what makes rotation work without guessing what it does to the geometry.
|
|
21
|
+
* 3. **The box is square, and it is opened exactly once.** A portrait box would collapse a
|
|
22
|
+
* landscape guest into a sliver (1600x720 inside a 720x1600 box is 720x324), so the ceiling
|
|
23
|
+
* is applied to the *long* edge by asking for `maxHeight x maxHeight` — rotation-invariant,
|
|
24
|
+
* so the picture keeps its resolution when the guest turns. It is opened at the **highest**
|
|
25
|
+
* rung's ceiling and never reopened, because a cancelled `streamScreenshot` cannot be
|
|
26
|
+
* replaced: measured on emulator 36.5.10, every stream opened after one has been cancelled
|
|
27
|
+
* delivers exactly **one** frame and then nothing, on a fresh channel as well as the same
|
|
28
|
+
* one. So a rung change is an ffmpeg `scale` and a respawned encoder, never a new stream.
|
|
29
|
+
* 4. **Pacing uses an accumulating deadline.** "Has one interval passed since the last frame I
|
|
30
|
+
* took" turns a 40 fps source into 20 fps at a 30 fps target — measured 242 frames over 12 s
|
|
31
|
+
* instead of 360. `nextDue = max(now, nextDue + interval)` is what gives the right count.
|
|
32
|
+
* 5. **A frame once started is written whole.** rawvideo carries no frame header, so a short
|
|
33
|
+
* write desynchronises the decoder permanently. Only a frame not yet begun may be dropped.
|
|
34
|
+
* 6. **`flush()` is what actually writes.** Bun's stdin is a FileSink whose `write()` only
|
|
35
|
+
* buffers and returns the byte count immediately, so a write loop alone proves nothing
|
|
36
|
+
* reached ffmpeg.
|
|
37
|
+
*/
|
|
38
|
+
import { AccessUnitAssembler, type AccessUnit } from "../remote-desktop/access-unit-assembler.ts";
|
|
39
|
+
import { avc1CodecString } from "../remote-desktop/avc1-codec-string.ts";
|
|
40
|
+
import { getFfmpegCapabilities, workingEncoders } from "../media-transcode/ffmpeg-capabilities.ts";
|
|
41
|
+
import { connectToEmulator, type EmulatorChannel } from "./android-grpc.ts";
|
|
42
|
+
import type { RunningEmulator } from "./emulator-discovery.ts";
|
|
43
|
+
import { ANDROID_QUALITY_PRESETS, type AndroidQuality } from "../../shared/android-protocol.ts";
|
|
44
|
+
|
|
45
|
+
// The H.264 helpers above live under remote-desktop/ and are imported rather than moved: they
|
|
46
|
+
// are pure, dependency-free bitstream utilities, and relocating them would touch nine files
|
|
47
|
+
// across a working, well-tested feature for no behavioural gain (plan §8 says extract them only
|
|
48
|
+
// "if needed"). If a third consumer ever appears, that is the moment to give them a neutral home.
|
|
49
|
+
|
|
50
|
+
/** The picture the client actually receives: the encoded size, which may be a scaled copy of
|
|
51
|
+
* what the emulator sends, plus the guest's own orientation. */
|
|
52
|
+
export interface FrameGeometry {
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
rotation: 0 | 90 | 180 | 270;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The one box ever requested from the emulator: the highest rung's ceiling, so every rung is
|
|
59
|
+
* reachable by scaling down and the stream never has to be reopened. */
|
|
60
|
+
export const STREAM_BOX = Math.max(...Object.values(ANDROID_QUALITY_PRESETS).map((p) => p.maxHeight));
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* How long after the last real change the pipeline keeps sending at the rung's full rate, and
|
|
64
|
+
* what it falls back to once the guest screen has gone still.
|
|
65
|
+
*
|
|
66
|
+
* Repeating a motionless picture is not free: measured on this host with the screen untouched,
|
|
67
|
+
* one viewer held ffmpeg at **55% of one core** — the rgb24 frame is rescaled and uploaded every
|
|
68
|
+
* time, whether or not a pixel moved. It cannot simply stop, though; see `lastFrame`. Ten frames
|
|
69
|
+
* a second is the compromise, measured on the same screen: **11.7 frames/s into ffmpeg for 23%
|
|
70
|
+
* of a core**, while the client still receives 24.9 access units a second, because the rawvideo
|
|
71
|
+
* demuxer declares 25 fps and ffmpeg's default CFR sync pads the output back up to it — and a
|
|
72
|
+
* duplicate costs the encoder almost nothing, since the expensive half happens per *input*
|
|
73
|
+
* frame. A viewer joining a still device therefore still has a keyframe inside a second.
|
|
74
|
+
*/
|
|
75
|
+
const IDLE_AFTER_MS = 1000;
|
|
76
|
+
const IDLE_FPS = 10;
|
|
77
|
+
|
|
78
|
+
/** Even dimensions only: H.264 chroma is subsampled and an odd size is rejected outright. */
|
|
79
|
+
function evenDown(n: number): number {
|
|
80
|
+
return Math.max(2, n % 2 === 0 ? n : n - 1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Fit a frame under a rung's ceiling, applied to the **long** edge so a rotation does not change
|
|
85
|
+
* how much detail a rung buys. Never upscales: a rung above the source is the source.
|
|
86
|
+
*/
|
|
87
|
+
export function fitLongEdge(width: number, height: number, ceiling: number): { width: number; height: number } {
|
|
88
|
+
const longEdge = Math.max(width, height);
|
|
89
|
+
if (longEdge <= ceiling) return { width: evenDown(width), height: evenDown(height) };
|
|
90
|
+
const scale = ceiling / longEdge;
|
|
91
|
+
return { width: evenDown(Math.round(width * scale)), height: evenDown(Math.round(height * scale)) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface VideoPipelineOptions {
|
|
95
|
+
emulator: RunningEmulator;
|
|
96
|
+
quality: AndroidQuality;
|
|
97
|
+
onAccessUnit: (au: AccessUnit, ptsMs: number) => void;
|
|
98
|
+
/** Called once at startup and again whenever the emulator changes the frame size or rotation. */
|
|
99
|
+
onGeometry: (geometry: FrameGeometry) => void;
|
|
100
|
+
onError: (message: string) => void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface RunningPipeline {
|
|
104
|
+
readonly geometry: FrameGeometry;
|
|
105
|
+
/** ffmpeg's encoder name, for display. Not what the browser's decoder is configured with. */
|
|
106
|
+
readonly encoder: string;
|
|
107
|
+
/** The current bitstream's WebCodecs codec string, null until its first keyframe. */
|
|
108
|
+
codecString(): string | null;
|
|
109
|
+
readonly fps: number;
|
|
110
|
+
stop(): Promise<void>;
|
|
111
|
+
/** Frames stop being requested while hidden; the emulator and the session stay up. */
|
|
112
|
+
setPaused(paused: boolean): void;
|
|
113
|
+
/** Switch rung. Respawns the encoder; the emulator's stream is never touched. */
|
|
114
|
+
setQuality(quality: AndroidQuality): Promise<void>;
|
|
115
|
+
/** Frames the emulator sent, and frames that survived pacing and reached ffmpeg. The gap
|
|
116
|
+
* between the two IS the pacing, so a stall shows up as which of them stopped moving. */
|
|
117
|
+
stats(): { sourceFrames: number; fedFrames: number };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The proto's `SkinRotation` in the terms the viewer uses. */
|
|
121
|
+
const ROTATION_DEGREES: Record<string, 0 | 90 | 180 | 270> = {
|
|
122
|
+
PORTRAIT: 0, LANDSCAPE: 90, REVERSE_PORTRAIT: 180, REVERSE_LANDSCAPE: 270,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
function rotationOf(format: any): 0 | 90 | 180 | 270 {
|
|
126
|
+
const r = format?.rotation?.rotation;
|
|
127
|
+
if (typeof r === "string" && r in ROTATION_DEGREES) return ROTATION_DEGREES[r]!;
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* ffmpeg arguments per encoder. VAAPI is not a drop-in `-c:v` swap: `-vaapi_device` must precede
|
|
133
|
+
* `-i` because it opens during input setup, the upload must be in the SAME `-vf` chain (a second
|
|
134
|
+
* `-vf` replaces the first), and `-pix_fmt yuv420p` has to go or the hardware frame context is
|
|
135
|
+
* rejected. Measured on an Intel UHD 770: VAAPI held 33% of a core against libx264's 64-80%.
|
|
136
|
+
*/
|
|
137
|
+
export function encoderArgs(opts: {
|
|
138
|
+
encoder: string; width: number; height: number; fps: number; bitrate: string;
|
|
139
|
+
/** Encoded size, when it differs from the source. Omit for no scaling. */
|
|
140
|
+
outWidth?: number; outHeight?: number;
|
|
141
|
+
vaapiDevice?: string;
|
|
142
|
+
}): string[] {
|
|
143
|
+
const { encoder, width, height, fps, bitrate } = opts;
|
|
144
|
+
const input = [
|
|
145
|
+
"-f", "rawvideo", "-pix_fmt", "rgb24", "-s", `${width}x${height}`,
|
|
146
|
+
// `-framerate` is **not** redundant beside the wallclock timestamps, and leaving it out costs
|
|
147
|
+
// one frame in six. The rawvideo demuxer assumes **25 fps** when nothing says otherwise, and
|
|
148
|
+
// ffmpeg's default CFR sync then conforms the output to that rate — measured, feeding a real
|
|
149
|
+
// 30 fps produced 151 access units from 180 frames, dropped unevenly, which is exactly what
|
|
150
|
+
// dragging on the device looked like. Nothing warns: the picture is correct, there is simply
|
|
151
|
+
// less of it. With the rate declared, 180 frames in gave 180 access units out.
|
|
152
|
+
"-framerate", String(fps),
|
|
153
|
+
"-use_wallclock_as_timestamps", "1", "-i", "pipe:0",
|
|
154
|
+
];
|
|
155
|
+
// An exact size rather than a filter expression: the pipeline always knows both, because a
|
|
156
|
+
// geometry change and a rung change each respawn the encoder. `scale=w:h` with numbers cannot
|
|
157
|
+
// be got subtly wrong the way `min(ih,H)` can when the frame is landscape.
|
|
158
|
+
const scale = opts.outWidth && opts.outHeight && (opts.outWidth !== width || opts.outHeight !== height)
|
|
159
|
+
? `scale=${opts.outWidth}:${opts.outHeight},` : "";
|
|
160
|
+
// Half-second GOP: it bounds every resync freeze and every late joiner's wait for a first
|
|
161
|
+
// decodable frame, and on a rate-capped encode costs no measurable bitrate — the same finding
|
|
162
|
+
// the Remote Desktop ladder is built on.
|
|
163
|
+
const gop = ["-g", String(Math.max(1, Math.round(fps / 2)))];
|
|
164
|
+
const out = ["-f", "h264", "pipe:1"];
|
|
165
|
+
|
|
166
|
+
if (encoder === "h264_vaapi") {
|
|
167
|
+
return [
|
|
168
|
+
"-vaapi_device", opts.vaapiDevice ?? "/dev/dri/renderD128",
|
|
169
|
+
...input,
|
|
170
|
+
"-vf", `${scale}format=nv12,hwupload`,
|
|
171
|
+
"-c:v", "h264_vaapi", "-b:v", bitrate, ...gop, ...out,
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
if (encoder === "h264_qsv") {
|
|
175
|
+
return [...input, "-vf", `${scale}format=nv12`,
|
|
176
|
+
"-c:v", "h264_qsv", "-b:v", bitrate, "-low_power", "1", ...gop, ...out];
|
|
177
|
+
}
|
|
178
|
+
if (encoder === "h264_nvenc") {
|
|
179
|
+
return [...input, "-vf", `${scale}format=yuv420p`,
|
|
180
|
+
"-c:v", "h264_nvenc", "-b:v", bitrate, "-preset", "p1", "-tune", "ll", ...gop, ...out];
|
|
181
|
+
}
|
|
182
|
+
return [...input, "-vf", `${scale}format=yuv420p`,
|
|
183
|
+
"-c:v", encoder, "-b:v", bitrate, "-preset", "veryfast", "-tune", "zerolatency", ...gop, ...out];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Hardware first, software last — but only among encoders that really encode on this host. */
|
|
187
|
+
export function pickEncoder(available: string[]): string {
|
|
188
|
+
for (const preferred of ["h264_vaapi", "h264_qsv", "h264_nvenc", "h264_amf"]) {
|
|
189
|
+
if (available.includes(preferred)) return preferred;
|
|
190
|
+
}
|
|
191
|
+
return available[0] ?? "libx264";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Named so `ReturnType<typeof spawnFfmpeg>` keeps the narrowed `stdin`/`stdout` types. Written
|
|
195
|
+
* inline the union comes back — `stdout` widens to `number | ReadableStream` and `.getReader()`
|
|
196
|
+
* stops type-checking, the same trap `host-info/spawn-runner.ts` records in CLAUDE.md. */
|
|
197
|
+
function spawnFfmpeg(ffmpeg: string, args: string[]) {
|
|
198
|
+
return Bun.spawn([ffmpeg, "-hide_banner", "-loglevel", "error", ...args], {
|
|
199
|
+
stdin: "pipe", stdout: "pipe", stderr: "pipe", windowsHide: true,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** One ffmpeg, locked to one frame size. A geometry change means a new one of these. */
|
|
204
|
+
/**
|
|
205
|
+
* Hand one whole raw frame to the encoder's stdin.
|
|
206
|
+
*
|
|
207
|
+
* One `write()`, then `flush()` — **never** a partial-write loop over the return value.
|
|
208
|
+
* `FileSink.write()` takes the whole chunk and answers a number that is *not* how much of this
|
|
209
|
+
* chunk it consumed: measured against `cat` with one real 4,924,800-byte frame, it returned
|
|
210
|
+
* 475072 eleven times while having already buffered everything, so a loop doing `off += n`
|
|
211
|
+
* re-sent the tail ten more times and `cat` received **28,043,840 bytes — 5.7x the frame**.
|
|
212
|
+
* ffmpeg's rawvideo input is then offset for good: the *first* picture decodes perfectly and
|
|
213
|
+
* every one after it is drawn rolled, with no error from ffmpeg, from the sink, or from
|
|
214
|
+
* anything else. It is its own function only so a test can measure the bytes that come out the
|
|
215
|
+
* other end (`tests/unit/android/android-video-frame-write.test.ts`) — a frame counter cannot
|
|
216
|
+
* see this bug.
|
|
217
|
+
*/
|
|
218
|
+
export async function writeFrameToSink(
|
|
219
|
+
sink: { write: (c: Uint8Array) => unknown; flush: () => unknown },
|
|
220
|
+
frame: Uint8Array,
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
sink.write(frame);
|
|
223
|
+
await sink.flush();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
class Encoder {
|
|
227
|
+
private readonly proc: ReturnType<typeof spawnFfmpeg>;
|
|
228
|
+
private readonly assembler = new AccessUnitAssembler();
|
|
229
|
+
private readonly sink: { write(c: Uint8Array): number | Promise<number>; flush(): number | Promise<number> };
|
|
230
|
+
private inFlight: Promise<void> = Promise.resolve();
|
|
231
|
+
private pending: Buffer | null = null;
|
|
232
|
+
private stopped = false;
|
|
233
|
+
readonly outPump: Promise<void>;
|
|
234
|
+
|
|
235
|
+
constructor(
|
|
236
|
+
ffmpeg: string,
|
|
237
|
+
args: string[],
|
|
238
|
+
private readonly onAccessUnit: (au: AccessUnit) => void,
|
|
239
|
+
private readonly onError: (m: string) => void,
|
|
240
|
+
) {
|
|
241
|
+
this.proc = spawnFfmpeg(ffmpeg, args);
|
|
242
|
+
this.sink = this.proc.stdin as never;
|
|
243
|
+
|
|
244
|
+
// Manual pull loop, NOT `for await (...) { break }`. Breaking out of a for-await cancels the
|
|
245
|
+
// underlying reader, which segfaults Bun 1.3.x on Windows the moment a consumer goes away —
|
|
246
|
+
// the hazard `remote-desktop-capture.ts` documents at length and guards the same way.
|
|
247
|
+
// Teardown is `kill()` only; the pending read then resolves `done: true` and this exits.
|
|
248
|
+
const outReader = this.proc.stdout.getReader();
|
|
249
|
+
this.outPump = (async () => {
|
|
250
|
+
for (;;) {
|
|
251
|
+
const { done, value } = await outReader.read();
|
|
252
|
+
if (done) return;
|
|
253
|
+
if (!value) continue;
|
|
254
|
+
for (const au of this.assembler.push(value)) this.onAccessUnit(au);
|
|
255
|
+
}
|
|
256
|
+
})().catch((e) => {
|
|
257
|
+
if (!this.stopped) this.onError(`encoder output failed: ${(e as Error).message}`);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const errReader = this.proc.stderr.getReader();
|
|
261
|
+
void (async () => {
|
|
262
|
+
for (;;) {
|
|
263
|
+
const { done, value } = await errReader.read();
|
|
264
|
+
if (done) return;
|
|
265
|
+
if (!value) continue;
|
|
266
|
+
const text = new TextDecoder().decode(value).trim();
|
|
267
|
+
if (text && !this.stopped) this.onError(`encoder: ${text.split("\n").slice(-1)[0]}`);
|
|
268
|
+
}
|
|
269
|
+
})().catch(() => { /* closed with the process */ });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The `avc1.PPCCLL` string this encoder's real bitstream implies, null before its first
|
|
273
|
+
* keyframe. Derived, never guessed: `encoderArgs` sets no profile or level. */
|
|
274
|
+
codecString(): string | null {
|
|
275
|
+
const sps = this.assembler.cachedSps();
|
|
276
|
+
return sps ? avc1CodecString(sps) : null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Queue a frame. Only a frame that has not started being written may be superseded. */
|
|
280
|
+
feed(buf: Buffer): void {
|
|
281
|
+
if (this.stopped) return;
|
|
282
|
+
this.pending = buf;
|
|
283
|
+
this.inFlight = this.inFlight.then(async () => {
|
|
284
|
+
while (this.pending && !this.stopped) {
|
|
285
|
+
const frame = this.pending;
|
|
286
|
+
this.pending = null;
|
|
287
|
+
try {
|
|
288
|
+
await writeFrameToSink(this.sink, frame);
|
|
289
|
+
} catch { /* the encoder is gone; stop() reports it */ }
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async stop(): Promise<void> {
|
|
295
|
+
if (this.stopped) return;
|
|
296
|
+
this.stopped = true;
|
|
297
|
+
// Kill FIRST, then wait. Waiting on the write chain before killing makes teardown hostage to
|
|
298
|
+
// an ffmpeg that has stopped reading its stdin: `flush()` would never resolve and the process
|
|
299
|
+
// would outlive PPM. Killing first can only turn a pending write into an EPIPE, which the
|
|
300
|
+
// write loop already swallows.
|
|
301
|
+
try { this.proc.stdin.end(); } catch { /* already closed */ }
|
|
302
|
+
try { this.proc.kill(); } catch { /* already exited */ }
|
|
303
|
+
await Promise.race([this.inFlight.catch(() => {}), Bun.sleep(1000)]);
|
|
304
|
+
await Promise.race([this.outPump, Bun.sleep(1000)]);
|
|
305
|
+
await Promise.race([this.proc.exited, Bun.sleep(2000)]);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function startVideoPipeline(opts: VideoPipelineOptions): Promise<RunningPipeline> {
|
|
310
|
+
// The binary the capability check found, not a bare "ffmpeg": that check also looks outside
|
|
311
|
+
// PATH (Homebrew's prefixes, WinGet's links), which a service-started PPM often lacks.
|
|
312
|
+
const found = (await getFfmpegCapabilities()).ffmpeg;
|
|
313
|
+
if (!found) throw new Error("ffmpeg is not installed on this host");
|
|
314
|
+
const ffmpeg: string = found;
|
|
315
|
+
const encoderName = pickEncoder(await workingEncoders());
|
|
316
|
+
const channel: EmulatorChannel = connectToEmulator(opts.emulator);
|
|
317
|
+
const startedAt = performance.now();
|
|
318
|
+
|
|
319
|
+
let quality = opts.quality;
|
|
320
|
+
let preset = ANDROID_QUALITY_PRESETS[quality];
|
|
321
|
+
/** What the emulator is sending, before any scaling of ours. */
|
|
322
|
+
let source: FrameGeometry | null = null;
|
|
323
|
+
/** What the client receives. */
|
|
324
|
+
let geometry: FrameGeometry | null = null;
|
|
325
|
+
let encoder: Encoder | null = null;
|
|
326
|
+
let stopped = false;
|
|
327
|
+
let paused = false;
|
|
328
|
+
let nextDue = 0;
|
|
329
|
+
let sourceFrames = 0;
|
|
330
|
+
let fedFrames = 0;
|
|
331
|
+
/**
|
|
332
|
+
* The newest frame the emulator has sent, kept so the pacer can send it again.
|
|
333
|
+
*
|
|
334
|
+
* `streamScreenshot` is **change-driven**: the emulator sends a frame when the screen changes
|
|
335
|
+
* and then nothing at all — measured, a device sitting on its launcher produced *2 frames in
|
|
336
|
+
* 15 seconds*. Feeding ffmpeg only on arrival therefore stops the H.264 stream completely on a
|
|
337
|
+
* still screen, so a viewer that joins then waits on "Waiting for the first frame…" until
|
|
338
|
+
* something moves in the guest, and one already watching cannot tell a frozen picture from a
|
|
339
|
+
* dropped connection. The fix is to keep feeding the last frame: the GOP is half a second, so
|
|
340
|
+
* a viewer joining a motionless device has a keyframe within 500 ms.
|
|
341
|
+
*/
|
|
342
|
+
let lastFrame: Buffer | null = null;
|
|
343
|
+
let lastChangeAt = 0;
|
|
344
|
+
let pacer: ReturnType<typeof setInterval> | null = null;
|
|
345
|
+
|
|
346
|
+
const emitAu = (au: AccessUnit) => opts.onAccessUnit(au, performance.now() - startedAt);
|
|
347
|
+
|
|
348
|
+
/** Tear down the encoder and start one for the current source and rung. */
|
|
349
|
+
function respawnEncoder(src: FrameGeometry): FrameGeometry {
|
|
350
|
+
const out = fitLongEdge(src.width, src.height, preset.maxHeight);
|
|
351
|
+
const previous = encoder;
|
|
352
|
+
encoder = null;
|
|
353
|
+
try {
|
|
354
|
+
encoder = new Encoder(
|
|
355
|
+
ffmpeg,
|
|
356
|
+
encoderArgs({
|
|
357
|
+
encoder: encoderName,
|
|
358
|
+
width: src.width, height: src.height,
|
|
359
|
+
outWidth: out.width, outHeight: out.height,
|
|
360
|
+
fps: preset.fps, bitrate: preset.bitrate,
|
|
361
|
+
}),
|
|
362
|
+
emitAu,
|
|
363
|
+
(m) => { if (!stopped) opts.onError(m); },
|
|
364
|
+
);
|
|
365
|
+
} catch (e) {
|
|
366
|
+
// Bun.spawn throws synchronously on a binary it cannot run, and this runs inside the gRPC
|
|
367
|
+
// stream's frame handler, where a throw would escape as an uncaught exception.
|
|
368
|
+
if (!stopped) opts.onError(`could not start ffmpeg: ${(e as Error).message}`);
|
|
369
|
+
}
|
|
370
|
+
nextDue = 0;
|
|
371
|
+
startPacer(); // the rung may have changed the frame rate
|
|
372
|
+
// Drained in the background: its remaining access units describe a picture the client has
|
|
373
|
+
// already been told is gone.
|
|
374
|
+
if (previous) void previous.stop();
|
|
375
|
+
return { width: out.width, height: out.height, rotation: src.rotation };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Hand the newest frame to the encoder if the rung's cadence allows one now. */
|
|
379
|
+
function feedNow(): void {
|
|
380
|
+
if (stopped || paused || !lastFrame) return;
|
|
381
|
+
const now = performance.now();
|
|
382
|
+
if (nextDue === 0) nextDue = now;
|
|
383
|
+
if (now < nextDue) return;
|
|
384
|
+
const fps = now - lastChangeAt > IDLE_AFTER_MS ? Math.min(IDLE_FPS, preset.fps) : preset.fps;
|
|
385
|
+
nextDue = Math.max(now, nextDue + 1000 / fps);
|
|
386
|
+
fedFrames++;
|
|
387
|
+
encoder?.feed(lastFrame);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Ticks at *half* the frame interval on purpose. A tick landing just before the next frame is
|
|
392
|
+
* due does nothing, so a tick exactly one period long would halve the rate whenever the two
|
|
393
|
+
* fall out of phase; at half a period the cadence holds.
|
|
394
|
+
*/
|
|
395
|
+
function startPacer(): void {
|
|
396
|
+
if (pacer) clearInterval(pacer);
|
|
397
|
+
pacer = setInterval(feedNow, Math.max(5, Math.round(500 / preset.fps)));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function stopPacer(): void {
|
|
401
|
+
if (pacer) clearInterval(pacer);
|
|
402
|
+
pacer = null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function onFrame(img: any): void {
|
|
406
|
+
if (stopped || paused) return;
|
|
407
|
+
const buf: Buffer | undefined = img?.image;
|
|
408
|
+
const width = Number(img?.format?.width ?? 0);
|
|
409
|
+
const height = Number(img?.format?.height ?? 0);
|
|
410
|
+
if (!buf || width <= 0 || height <= 0) return;
|
|
411
|
+
// The declared size and the payload must agree before ffmpeg ever sees it: a short buffer
|
|
412
|
+
// desynchronises the raw stream permanently, with no error from anything.
|
|
413
|
+
if (buf.length !== width * height * 3) return;
|
|
414
|
+
sourceFrames++;
|
|
415
|
+
|
|
416
|
+
const rotation = rotationOf(img.format);
|
|
417
|
+
if (!source || source.width !== width || source.height !== height || source.rotation !== rotation) {
|
|
418
|
+
source = { width, height, rotation };
|
|
419
|
+
geometry = respawnEncoder(source);
|
|
420
|
+
opts.onGeometry(geometry);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const now = performance.now();
|
|
424
|
+
// Coming out of idle, the next slot was scheduled at the *idle* rate and may be up to 100 ms
|
|
425
|
+
// away — that delay would land on the first frame after a touch, which is the one that must
|
|
426
|
+
// not be late. Re-arm so it goes out now.
|
|
427
|
+
if (now - lastChangeAt > IDLE_AFTER_MS) nextDue = 0;
|
|
428
|
+
lastChangeAt = now;
|
|
429
|
+
lastFrame = buf;
|
|
430
|
+
feedNow(); // a change goes out at once rather than waiting for the next tick
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Opened once for the life of the session. See rule 3 in this file's header: a cancelled
|
|
434
|
+
// screenshot stream cannot be replaced on this emulator, so nothing here ever cancels it
|
|
435
|
+
// except `stop()`.
|
|
436
|
+
const call = channel.client.streamScreenshot(
|
|
437
|
+
{ format: "RGB888", width: STREAM_BOX, height: STREAM_BOX },
|
|
438
|
+
channel.metadata,
|
|
439
|
+
);
|
|
440
|
+
call.on("data", onFrame);
|
|
441
|
+
call.on("error", (e: any) => {
|
|
442
|
+
// code 1 is CANCELLED, which is how stop() ends the stream.
|
|
443
|
+
if (!stopped && e?.code !== 1) {
|
|
444
|
+
opts.onError(`frame stream failed: ${e?.details ?? e?.message ?? e}`);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Resolve only once the emulator has told us what it is actually sending — the session needs a
|
|
449
|
+
// real geometry to put in its `ready` message, and there is nothing to guess it from.
|
|
450
|
+
await new Promise<void>((resolve, reject) => {
|
|
451
|
+
const deadline = setTimeout(() => reject(new Error("no frame from the emulator within 10s")), 10_000);
|
|
452
|
+
const poll = setInterval(() => {
|
|
453
|
+
if (geometry) { clearInterval(poll); clearTimeout(deadline); resolve(); }
|
|
454
|
+
if (stopped) { clearInterval(poll); clearTimeout(deadline); reject(new Error("stopped")); }
|
|
455
|
+
}, 25);
|
|
456
|
+
}).catch(async (e) => {
|
|
457
|
+
stopped = true;
|
|
458
|
+
stopPacer();
|
|
459
|
+
try { call.cancel(); } catch { /* already gone */ }
|
|
460
|
+
channel.close();
|
|
461
|
+
await encoder?.stop();
|
|
462
|
+
throw e;
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
get geometry() { return geometry!; },
|
|
467
|
+
encoder: encoderName,
|
|
468
|
+
codecString: () => encoder?.codecString() ?? null,
|
|
469
|
+
stats: () => ({ sourceFrames, fedFrames }),
|
|
470
|
+
get fps() { return preset.fps; },
|
|
471
|
+
|
|
472
|
+
setPaused(p: boolean) {
|
|
473
|
+
paused = p;
|
|
474
|
+
// Resuming re-arms the pacer so the first frame back is not judged against a stale deadline.
|
|
475
|
+
if (!p) nextDue = 0;
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
async setQuality(next: AndroidQuality) {
|
|
479
|
+
if (next === quality || stopped || !source) return;
|
|
480
|
+
quality = next;
|
|
481
|
+
preset = ANDROID_QUALITY_PRESETS[next];
|
|
482
|
+
geometry = respawnEncoder(source);
|
|
483
|
+
opts.onGeometry(geometry);
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
async stop() {
|
|
487
|
+
if (stopped) return;
|
|
488
|
+
stopped = true;
|
|
489
|
+
stopPacer();
|
|
490
|
+
try { call.cancel(); } catch { /* already gone */ }
|
|
491
|
+
channel.close();
|
|
492
|
+
await encoder?.stop();
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AVDs this host has, and enough of each one's config to say whether it will behave.
|
|
3
|
+
*
|
|
4
|
+
* Read straight from the AVD home rather than by shelling out to `emulator -list-avds`: that
|
|
5
|
+
* command answers names only, and the setup screen needs the API level, ABI and — the one that
|
|
6
|
+
* actually bites — whether the hardware keyboard is on.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { join, resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
export interface AvdSummary {
|
|
13
|
+
/** Opaque, stable id issued by PPM. Derived from the canonical config path so it survives a
|
|
14
|
+
* rename of the display name and never collides across two AVD homes. */
|
|
15
|
+
avdId: string;
|
|
16
|
+
name: string;
|
|
17
|
+
/** Directory holding config.ini, i.e. `<avd home>/<name>.avd`. */
|
|
18
|
+
dir: string;
|
|
19
|
+
apiLevel: number | null;
|
|
20
|
+
abi: string | null;
|
|
21
|
+
tag: string | null;
|
|
22
|
+
deviceProfile: string | null;
|
|
23
|
+
displayWidth: number | null;
|
|
24
|
+
displayHeight: number | null;
|
|
25
|
+
displayDensity: number | null;
|
|
26
|
+
/**
|
|
27
|
+
* `hw.keyboard`. When this is off, gRPC touch works perfectly, `sendKey` returns OK, and not
|
|
28
|
+
* one keystroke reaches the guest — no error anywhere. `avdmanager` creates AVDs with it off;
|
|
29
|
+
* Android Studio creates them with it on. Measured in Phase 0; surfaced so the UI can warn.
|
|
30
|
+
*/
|
|
31
|
+
hardwareKeyboard: boolean;
|
|
32
|
+
/** True when another process holds this AVD's lock — usually Android Studio running it. */
|
|
33
|
+
lockedByAnotherProcess: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseIni(text: string): Record<string, string> {
|
|
37
|
+
const out: Record<string, string> = {};
|
|
38
|
+
for (const line of text.split("\n")) {
|
|
39
|
+
const t = line.trim();
|
|
40
|
+
if (!t || t.startsWith("#")) continue;
|
|
41
|
+
const eq = t.indexOf("=");
|
|
42
|
+
if (eq > 0) out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function num(v: string | undefined): number | null {
|
|
48
|
+
if (v === undefined) return null;
|
|
49
|
+
const n = Number(v);
|
|
50
|
+
return Number.isFinite(n) ? n : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Stable opaque id for an AVD, from its canonical directory. */
|
|
54
|
+
export function avdIdFor(avdDir: string): string {
|
|
55
|
+
return createHash("sha256").update(resolve(avdDir)).digest("hex").slice(0, 16);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* API level out of `image.sysdir.1`, e.g. `system-images/android-35/google_apis.../x86_64/`.
|
|
60
|
+
* `target=android-35` is not always present in configs written by newer tooling.
|
|
61
|
+
*/
|
|
62
|
+
function apiLevelFrom(cfg: Record<string, string>): number | null {
|
|
63
|
+
const target = cfg["target"];
|
|
64
|
+
const fromTarget = target ? /android-(\d+)/.exec(target)?.[1] : undefined;
|
|
65
|
+
if (fromTarget) return Number(fromTarget);
|
|
66
|
+
const sysdir = cfg["image.sysdir.1"];
|
|
67
|
+
const fromSysdir = sysdir ? /android-(\d+)/.exec(sysdir)?.[1] : undefined;
|
|
68
|
+
return fromSysdir ? Number(fromSysdir) : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Is this AVD locked by a process that is actually alive?
|
|
73
|
+
*
|
|
74
|
+
* Presence of a lock file is **not** the answer, measured: `multiinstance.lock` is a zero-byte
|
|
75
|
+
* marker that survives a clean shutdown, so treating it as a lock refuses every start after the
|
|
76
|
+
* first. The real lock is `hardware-qemu.ini.lock`, which holds the emulator's pid (verified
|
|
77
|
+
* against the live process), so the pid is what gets checked. A lock naming a dead pid is stale
|
|
78
|
+
* and ignored.
|
|
79
|
+
*
|
|
80
|
+
* PPM only ever *reports* this — plan §5 is explicit that a lock is never deleted to force a
|
|
81
|
+
* start, which is what would corrupt an image Android Studio is using.
|
|
82
|
+
*/
|
|
83
|
+
function isLocked(avdDir: string): boolean {
|
|
84
|
+
for (const lock of ["hardware-qemu.ini.lock", "userdata-qemu.img.lock"]) {
|
|
85
|
+
const path = join(avdDir, lock);
|
|
86
|
+
if (!existsSync(path)) continue;
|
|
87
|
+
let raw: string;
|
|
88
|
+
try { raw = readFileSync(path, "utf8"); } catch { continue; }
|
|
89
|
+
// The emulator writes the pid **NUL-terminated** — measured, `hardware-qemu.ini.lock` is the
|
|
90
|
+
// 7 bytes `519772\0`. A NUL is not whitespace, so `trim()` keeps it and `Number()` answers
|
|
91
|
+
// NaN: read that way, every running emulator reports itself unlocked and the "open in
|
|
92
|
+
// Android Studio" warning never appears. Take the leading digits instead.
|
|
93
|
+
const digits = /^\s*(\d+)/.exec(raw);
|
|
94
|
+
if (!digits) continue;
|
|
95
|
+
const pid = Number(digits[1]);
|
|
96
|
+
if (!Number.isInteger(pid) || pid <= 0) continue;
|
|
97
|
+
try { process.kill(pid, 0); return true; } catch { /* stale lock, owner is gone */ }
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function listAvds(avdHome: string): AvdSummary[] {
|
|
103
|
+
let entries: string[];
|
|
104
|
+
try { entries = readdirSync(avdHome); } catch { return []; }
|
|
105
|
+
|
|
106
|
+
const avds: AvdSummary[] = [];
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (!entry.endsWith(".avd")) continue;
|
|
109
|
+
const dir = join(avdHome, entry);
|
|
110
|
+
const configPath = join(dir, "config.ini");
|
|
111
|
+
let cfg: Record<string, string>;
|
|
112
|
+
try { cfg = parseIni(readFileSync(configPath, "utf8")); } catch { continue; }
|
|
113
|
+
|
|
114
|
+
const name = cfg["AvdId"] ?? entry.slice(0, -".avd".length);
|
|
115
|
+
avds.push({
|
|
116
|
+
avdId: avdIdFor(dir),
|
|
117
|
+
name,
|
|
118
|
+
dir,
|
|
119
|
+
apiLevel: apiLevelFrom(cfg),
|
|
120
|
+
abi: cfg["abi.type"] ?? null,
|
|
121
|
+
tag: cfg["tag.id"] ?? null,
|
|
122
|
+
deviceProfile: cfg["hw.device.name"] ?? null,
|
|
123
|
+
displayWidth: num(cfg["hw.lcd.width"]),
|
|
124
|
+
displayHeight: num(cfg["hw.lcd.height"]),
|
|
125
|
+
displayDensity: num(cfg["hw.lcd.density"]),
|
|
126
|
+
hardwareKeyboard: (cfg["hw.keyboard"] ?? "no").trim() === "yes",
|
|
127
|
+
lockedByAnotherProcess: isLocked(dir),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return avds.sort((a, b) => a.name.localeCompare(b.name));
|
|
131
|
+
}
|