@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,616 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/api/android/*` — the Android emulator surface.
|
|
3
|
+
*
|
|
4
|
+
* Guard style mirrors `remote-desktop.ts`, and for the same reason: `authMiddleware` passes
|
|
5
|
+
* everything through when PPM auth is disabled, so a feature that spawns processes on the host
|
|
6
|
+
* and hands over control of them must enforce `auth.enabled` itself, plus an Origin check so a
|
|
7
|
+
* foreign page cannot drive it with ambient browser credentials.
|
|
8
|
+
*
|
|
9
|
+
* That Origin check compares **hostname only, not host:port** — deliberately, and copied from
|
|
10
|
+
* `remote-desktop.ts` where the reasoning is spelled out: Vite's dev proxy rewrites `Host`
|
|
11
|
+
* without adding `X-Forwarded-Host`, so a full-host comparison rejects every dev-mode request.
|
|
12
|
+
* It is worth being clear about what that costs: a page on a *different port* of the same
|
|
13
|
+
* hostname is not rejected. It is not a complete same-origin guarantee, and the nonce that
|
|
14
|
+
* Phase 2's WS session requires is what actually gates control.
|
|
15
|
+
*
|
|
16
|
+
* Anything that can take minutes (a cold boot) answers with an operation id rather than holding
|
|
17
|
+
* the request open — see `android-operations.ts`.
|
|
18
|
+
*/
|
|
19
|
+
import { Hono, type Context } from "hono";
|
|
20
|
+
import { ok, err } from "../../types/api.ts";
|
|
21
|
+
import { configService } from "../../services/config.service.ts";
|
|
22
|
+
import { isAndroidEmulatorEnabled } from "../../services/android/android-flag.ts";
|
|
23
|
+
import { androidCapabilities } from "../../services/android/android-capabilities.ts";
|
|
24
|
+
import { discoverSdk } from "../../services/android/sdk-discovery.ts";
|
|
25
|
+
import { listDevices, releaseOwnership } from "../../services/android/device-registry.ts";
|
|
26
|
+
import { EmulatorLimitError, emulatorLog, startEmulator, stopEmulator } from "../../services/android/emulator-launcher.ts";
|
|
27
|
+
import { getOperation } from "../../services/android/android-operations.ts";
|
|
28
|
+
import { mintAndroidNonce } from "../../services/android/android-nonce.ts";
|
|
29
|
+
import { listAvds } from "../../services/android/avd-list.ts";
|
|
30
|
+
import { abiMatchesHost, listSystemImages } from "../../services/android/system-images.ts";
|
|
31
|
+
import {
|
|
32
|
+
createAvd, deleteAvd, listDeviceProfiles, validateAvdName, wipeAvdData, type CreateAvdRequest,
|
|
33
|
+
} from "../../services/android/avd-manager.ts";
|
|
34
|
+
import { existsSync } from "node:fs";
|
|
35
|
+
import { basename, isAbsolute, relative, resolve } from "node:path";
|
|
36
|
+
import { findRunningByDeviceId, type DeviceEntry } from "../../services/android/device-registry.ts";
|
|
37
|
+
import type { RunningEmulator } from "../../services/android/emulator-discovery.ts";
|
|
38
|
+
import { connectToEmulator, getEmulatorStatus, type EmulatorChannel } from "../../services/android/android-grpc.ts";
|
|
39
|
+
import { getClipboard } from "../../services/android/android-input.ts";
|
|
40
|
+
import { MAX_CLIPBOARD_CHARS, screenshotFilename, setDeviceClipboard, takeScreenshot } from "../../services/android/android-screenshot.ts";
|
|
41
|
+
import { findProjectApks, installApk, stageApkUpload } from "../../services/android/android-apk.ts";
|
|
42
|
+
import { cancelOperation, createOperation, failOperation, finishOperation, registerCanceller, updateOperation } from "../../services/android/android-operations.ts";
|
|
43
|
+
|
|
44
|
+
export const androidRoutes = new Hono();
|
|
45
|
+
|
|
46
|
+
function androidConfig() {
|
|
47
|
+
return configService.get("android") ?? {};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Disabled-feature check only. Read-only diagnostics may pass this and nothing more. */
|
|
51
|
+
function assertEnabled(c: Context): Response | null {
|
|
52
|
+
if (!isAndroidEmulatorEnabled()) {
|
|
53
|
+
return c.json(err("android emulator support is disabled on this host (set ANDROID_EMULATOR_ENABLED=1)"), 404);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Full guard for anything that acts on the host. */
|
|
59
|
+
function assertControlAllowed(c: Context): Response | null {
|
|
60
|
+
const disabled = assertEnabled(c);
|
|
61
|
+
if (disabled) return disabled;
|
|
62
|
+
if (!configService.get("auth").enabled) {
|
|
63
|
+
return c.json(err("android emulator control requires PPM authentication to be enabled"), 403);
|
|
64
|
+
}
|
|
65
|
+
const origin = c.req.header("origin");
|
|
66
|
+
if (origin) {
|
|
67
|
+
let originHost: string | null = null;
|
|
68
|
+
try { originHost = new URL(origin).hostname; } catch { originHost = null; }
|
|
69
|
+
let requestHost: string | null = null;
|
|
70
|
+
try { requestHost = new URL(c.req.url).hostname; } catch { requestHost = null; }
|
|
71
|
+
if (!originHost || !requestHost || originHost !== requestHost) {
|
|
72
|
+
return c.json(err("cross-origin request rejected"), 403);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Where AVDs live for this host, resolved once per request rather than cached: a user can
|
|
79
|
+
* change the SDK path in Settings and the next call must see it. */
|
|
80
|
+
async function avdHome(): Promise<string> {
|
|
81
|
+
const sdk = await discoverSdk(androidConfig().sdk_root ?? null);
|
|
82
|
+
return sdk.avdHome;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
androidRoutes.get("/capabilities", async (c) => {
|
|
86
|
+
// Deliberately answerable while disabled: `enabled` is how the nav decides whether to show the
|
|
87
|
+
// entry at all, and the device list's requirement rows say why it is off.
|
|
88
|
+
const caps = await androidCapabilities({
|
|
89
|
+
enabled: isAndroidEmulatorEnabled(),
|
|
90
|
+
configuredSdkRoot: androidConfig().sdk_root ?? null,
|
|
91
|
+
});
|
|
92
|
+
return c.json(ok({
|
|
93
|
+
...caps,
|
|
94
|
+
authRequired: configService.get("auth").enabled,
|
|
95
|
+
// Whether the device list may offer "New device" at all: creating one is `avdmanager`, which
|
|
96
|
+
// ships with the command-line tools and is the one SDK piece an emulator install can lack.
|
|
97
|
+
canCreateAvd: !!caps.sdk.avdmanager.path,
|
|
98
|
+
}));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
androidRoutes.get("/devices", async (c) => {
|
|
102
|
+
const rejected = assertEnabled(c);
|
|
103
|
+
if (rejected) return rejected;
|
|
104
|
+
const devices = listDevices(await avdHome());
|
|
105
|
+
// `runtime.grpcPort` and pid stay server-side: the browser never dials the emulator itself.
|
|
106
|
+
return c.json(ok({
|
|
107
|
+
devices: devices.map((d) => ({
|
|
108
|
+
...d,
|
|
109
|
+
runtime: d.runtime
|
|
110
|
+
? {
|
|
111
|
+
deviceId: d.runtime.deviceId,
|
|
112
|
+
generation: d.runtime.generation,
|
|
113
|
+
adbSerial: d.runtime.adbSerial,
|
|
114
|
+
ownedByPpm: d.runtime.ownedByPpm,
|
|
115
|
+
}
|
|
116
|
+
: null,
|
|
117
|
+
})),
|
|
118
|
+
}));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
androidRoutes.post("/avds/:avdId/start", async (c) => {
|
|
122
|
+
const rejected = assertControlAllowed(c);
|
|
123
|
+
if (rejected) return rejected;
|
|
124
|
+
|
|
125
|
+
const home = await avdHome();
|
|
126
|
+
const device = listDevices(home).find((d) => d.avdId === c.req.param("avdId"));
|
|
127
|
+
if (!device) return c.json(err("no such AVD"), 404);
|
|
128
|
+
if (device.lockedByAnotherProcess) {
|
|
129
|
+
// Plan §5: report the lock, never delete it to force a start.
|
|
130
|
+
return c.json(err(`${device.name} is locked by another process — it is probably open in Android Studio`), 409);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const cfg = androidConfig();
|
|
134
|
+
const sdk = await discoverSdk(cfg.sdk_root ?? null);
|
|
135
|
+
if (!sdk.emulator.path) return c.json(err("no emulator binary found in the Android SDK"), 400);
|
|
136
|
+
|
|
137
|
+
let operation: ReturnType<typeof startEmulator>;
|
|
138
|
+
try {
|
|
139
|
+
operation = startEmulator({
|
|
140
|
+
avdName: device.name,
|
|
141
|
+
avdHome: home,
|
|
142
|
+
emulatorPath: sdk.emulator.path,
|
|
143
|
+
windowMode: cfg.window_mode ?? "no-window",
|
|
144
|
+
gpuMode: cfg.gpu_mode,
|
|
145
|
+
// The launcher counts, not this handler: an await between a count here and the start
|
|
146
|
+
// would let a concurrent request for another AVD take the same slot.
|
|
147
|
+
maxConcurrent: cfg.max_concurrent ?? 1,
|
|
148
|
+
});
|
|
149
|
+
} catch (e) {
|
|
150
|
+
if (e instanceof EmulatorLimitError) return c.json(err(e.message), 409);
|
|
151
|
+
throw e;
|
|
152
|
+
}
|
|
153
|
+
return c.json(ok({ operationId: operation.id, state: operation.state, detail: operation.detail }));
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
androidRoutes.get("/operations/:id", (c) => {
|
|
157
|
+
const rejected = assertEnabled(c);
|
|
158
|
+
if (rejected) return rejected;
|
|
159
|
+
const op = getOperation(c.req.param("id"));
|
|
160
|
+
if (!op) return c.json(err("no such operation"), 404);
|
|
161
|
+
return c.json(ok({
|
|
162
|
+
id: op.id, kind: op.kind, state: op.state, detail: op.detail,
|
|
163
|
+
startedAt: op.startedAt, endedAt: op.endedAt,
|
|
164
|
+
result: op.result, error: op.error,
|
|
165
|
+
}));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
androidRoutes.post("/devices/:deviceId/stop", async (c) => {
|
|
169
|
+
const rejected = assertControlAllowed(c);
|
|
170
|
+
if (rejected) return rejected;
|
|
171
|
+
|
|
172
|
+
const home = await avdHome();
|
|
173
|
+
const device = listDevices(home).find((d) => d.runtime?.deviceId === c.req.param("deviceId"));
|
|
174
|
+
if (!device?.runtime) return c.json(err("no such running device"), 404);
|
|
175
|
+
|
|
176
|
+
// Generation check: a stop issued against a device that has since restarted must not take
|
|
177
|
+
// down the new run. The client sends the generation it last saw.
|
|
178
|
+
const claimed = Number(c.req.query("generation"));
|
|
179
|
+
if (Number.isInteger(claimed) && claimed !== device.runtime.generation) {
|
|
180
|
+
return c.json(err("this device has restarted since you last saw it; refresh and try again"), 409);
|
|
181
|
+
}
|
|
182
|
+
if (!device.runtime.ownedByPpm) {
|
|
183
|
+
return c.json(err(`${device.name} was not started by PPM — disconnect instead of stopping it`), 403);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const outcome = await stopEmulator(device.name, { avdHome: home });
|
|
187
|
+
if (outcome === "not-owned") {
|
|
188
|
+
return c.json(err(`${device.name} was not started by PPM — disconnect instead of stopping it`), 403);
|
|
189
|
+
}
|
|
190
|
+
return c.json(ok({ outcome }));
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
androidRoutes.get("/devices/:deviceId/log", async (c) => {
|
|
194
|
+
const rejected = assertControlAllowed(c);
|
|
195
|
+
if (rejected) return rejected;
|
|
196
|
+
const device = listDevices(await avdHome()).find((d) => d.runtime?.deviceId === c.req.param("deviceId"));
|
|
197
|
+
if (!device) return c.json(err("no such running device"), 404);
|
|
198
|
+
return c.json(ok({ lines: emulatorLog(device.name) }));
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Mint the single-use nonce `/ws/android` requires as its first message.
|
|
203
|
+
*
|
|
204
|
+
* The device is named **here**, not on the WS URL, so the thing that decides which VM a socket
|
|
205
|
+
* drives never lands in a proxy or tunnel access log the way `?token=` does.
|
|
206
|
+
*/
|
|
207
|
+
androidRoutes.post("/devices/:deviceId/sessions", async (c) => {
|
|
208
|
+
const rejected = assertControlAllowed(c);
|
|
209
|
+
if (rejected) return rejected;
|
|
210
|
+
|
|
211
|
+
const deviceId = c.req.param("deviceId");
|
|
212
|
+
const device = listDevices(await avdHome()).find((d) => d.runtime?.deviceId === deviceId);
|
|
213
|
+
if (!device?.runtime) return c.json(err("no such running device"), 404);
|
|
214
|
+
|
|
215
|
+
return c.json(ok({
|
|
216
|
+
nonce: mintAndroidNonce(deviceId),
|
|
217
|
+
deviceId,
|
|
218
|
+
generation: device.runtime.generation,
|
|
219
|
+
hardwareKeyboard: device.hardwareKeyboard,
|
|
220
|
+
}));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
/* =============================================================================================
|
|
224
|
+
* Phase 3 — app tooling: install an APK, take a screenshot, read the log.
|
|
225
|
+
*
|
|
226
|
+
* Every one of these is a *typed* endpoint. There is no route here that takes a command, an adb
|
|
227
|
+
* argument or an RPC name from the browser (plan Phase 3: "API typed riêng, không mở generic
|
|
228
|
+
* shell/RPC proxy"), and each names its device explicitly so a host running two emulators can
|
|
229
|
+
* never install to the wrong one.
|
|
230
|
+
* ============================================================================================= */
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The one place a deviceId becomes a live gRPC channel, so every caller closes it the same way.
|
|
234
|
+
*
|
|
235
|
+
* A wedged emulator makes the RPC reject, and an unhandled rejection here is a bare 500 with
|
|
236
|
+
* nothing in it — so the failure is turned into the same `{ok:false, error}` shape every other
|
|
237
|
+
* route answers with, carrying gRPC's own words rather than "internal error".
|
|
238
|
+
*/
|
|
239
|
+
type DeviceOutcome<T> =
|
|
240
|
+
| { kind: "ok"; value: T }
|
|
241
|
+
| { kind: "no-device" }
|
|
242
|
+
| { kind: "failed"; message: string };
|
|
243
|
+
|
|
244
|
+
async function withDevice<T>(
|
|
245
|
+
deviceId: string,
|
|
246
|
+
fn: (ctx: { emulator: RunningEmulator; device: DeviceEntry; channel: EmulatorChannel }) => Promise<T>,
|
|
247
|
+
): Promise<DeviceOutcome<T>> {
|
|
248
|
+
const device = listDevices(await avdHome()).find((d) => d.runtime?.deviceId === deviceId);
|
|
249
|
+
const emulator = findRunningByDeviceId(deviceId);
|
|
250
|
+
if (!device?.runtime || !emulator) return { kind: "no-device" };
|
|
251
|
+
const channel = connectToEmulator(emulator);
|
|
252
|
+
try {
|
|
253
|
+
return { kind: "ok", value: await fn({ emulator, device, channel }) };
|
|
254
|
+
} catch (e) {
|
|
255
|
+
const err = e as { details?: string; message?: string };
|
|
256
|
+
return { kind: "failed", message: err.details || err.message || "the emulator did not answer" };
|
|
257
|
+
} finally {
|
|
258
|
+
channel.close();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Every device route answers a failure the same way, so none of them can forget one. */
|
|
263
|
+
function deviceProblem(c: Context, outcome: { kind: "no-device" } | { kind: "failed"; message: string }): Response {
|
|
264
|
+
if (outcome.kind === "no-device") return c.json(err("no such running device"), 404);
|
|
265
|
+
return c.json(err(outcome.message), 502);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
androidRoutes.get("/devices/:deviceId/screenshot", async (c) => {
|
|
269
|
+
const rejected = assertControlAllowed(c);
|
|
270
|
+
if (rejected) return rejected;
|
|
271
|
+
|
|
272
|
+
const outcome = await withDevice(c.req.param("deviceId"), async ({ device, channel }) => {
|
|
273
|
+
const shot = await takeScreenshot(channel);
|
|
274
|
+
return { shot, name: screenshotFilename(device.name) };
|
|
275
|
+
});
|
|
276
|
+
if (outcome.kind !== "ok") return deviceProblem(c, outcome);
|
|
277
|
+
const { shot, name } = outcome.value;
|
|
278
|
+
|
|
279
|
+
return new Response(shot.png as unknown as BodyInit, {
|
|
280
|
+
headers: {
|
|
281
|
+
"Content-Type": "image/png",
|
|
282
|
+
"Content-Length": String(shot.png.length),
|
|
283
|
+
// `attachment` so the browser saves it rather than replacing the tab with the image.
|
|
284
|
+
"Content-Disposition": `attachment; filename="${name}"`,
|
|
285
|
+
"Cache-Control": "no-store",
|
|
286
|
+
"X-Android-Screenshot-Size": `${shot.width}x${shot.height}`,
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
androidRoutes.get("/devices/:deviceId/clipboard", async (c) => {
|
|
292
|
+
const rejected = assertControlAllowed(c);
|
|
293
|
+
if (rejected) return rejected;
|
|
294
|
+
const outcome = await withDevice(c.req.param("deviceId"), ({ channel }) => getClipboard(channel));
|
|
295
|
+
if (outcome.kind !== "ok") return deviceProblem(c, outcome);
|
|
296
|
+
return c.json(ok({ text: outcome.value }));
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
androidRoutes.post("/devices/:deviceId/clipboard", async (c) => {
|
|
300
|
+
const rejected = assertControlAllowed(c);
|
|
301
|
+
if (rejected) return rejected;
|
|
302
|
+
|
|
303
|
+
const body = await c.req.json().catch(() => null) as { text?: unknown } | null;
|
|
304
|
+
if (typeof body?.text !== "string") return c.json(err("text is required"), 400);
|
|
305
|
+
if (body.text.length > MAX_CLIPBOARD_CHARS) return c.json(err("that text is too long for the clipboard"), 413);
|
|
306
|
+
|
|
307
|
+
// Never log the text: plan Phase 3 gate, "không log nội dung input/clipboard".
|
|
308
|
+
const outcome = await withDevice(c.req.param("deviceId"), async ({ channel }) => {
|
|
309
|
+
await setDeviceClipboard(channel, body.text as string);
|
|
310
|
+
return true;
|
|
311
|
+
});
|
|
312
|
+
if (outcome.kind !== "ok") return deviceProblem(c, outcome);
|
|
313
|
+
return c.json(ok({ set: true }));
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
androidRoutes.post("/operations/:id/cancel", (c) => {
|
|
317
|
+
const rejected = assertControlAllowed(c);
|
|
318
|
+
if (rejected) return rejected;
|
|
319
|
+
const op = getOperation(c.req.param("id"));
|
|
320
|
+
if (!op) return c.json(err("no such operation"), 404);
|
|
321
|
+
if (!cancelOperation(op.id)) {
|
|
322
|
+
return c.json(err(op.endedAt !== null ? "that operation has already finished" : "that operation cannot be cancelled"), 409);
|
|
323
|
+
}
|
|
324
|
+
return c.json(ok({ cancelled: true }));
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Begin an install and return an operation id.
|
|
329
|
+
*
|
|
330
|
+
* Shared by both entry points because everything after "there is a file on this host" is the
|
|
331
|
+
* same; they differ only in where the file came from and whether it is ours to delete.
|
|
332
|
+
*/
|
|
333
|
+
async function beginInstall(
|
|
334
|
+
deviceId: string,
|
|
335
|
+
apkPath: string,
|
|
336
|
+
label: string,
|
|
337
|
+
opts: { reinstall: boolean; allowDowngrade: boolean; cleanup?: () => void },
|
|
338
|
+
): Promise<{ operationId: string } | { error: string; status: 400 | 404 }> {
|
|
339
|
+
const device = listDevices(await avdHome()).find((d) => d.runtime?.deviceId === deviceId);
|
|
340
|
+
if (!device?.runtime) return { error: "no such running device", status: 404 };
|
|
341
|
+
|
|
342
|
+
// Plan §6: "install APK chỉ khi guest ready". Video is available before the guest has finished
|
|
343
|
+
// booting, so a tab can be perfectly alive while `pm` is not yet running — an install then
|
|
344
|
+
// fails with adb's own wording instead of a sentence that says to wait.
|
|
345
|
+
//
|
|
346
|
+
// `DeviceEntry.state` cannot answer this: `listDevices` is synchronous and sets `ready` for
|
|
347
|
+
// anything with a live process, so it never reports `booting` at all (a Phase 1 gap, noted in
|
|
348
|
+
// the Phase 3 report). `getStatus().booted` is the emulator's own answer, and costs one RPC.
|
|
349
|
+
const emulator = findRunningByDeviceId(deviceId);
|
|
350
|
+
if (emulator) {
|
|
351
|
+
const channel = connectToEmulator(emulator);
|
|
352
|
+
try {
|
|
353
|
+
const status = await getEmulatorStatus(channel);
|
|
354
|
+
if (!status.booted) {
|
|
355
|
+
return { error: `${device.name} has not finished booting — wait for Android to start`, status: 400 };
|
|
356
|
+
}
|
|
357
|
+
} catch {
|
|
358
|
+
// A channel that will not answer is not a reason to refuse: adb may well still work, and
|
|
359
|
+
// its own error is more specific than anything that could be invented here.
|
|
360
|
+
} finally {
|
|
361
|
+
channel.close();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const serial = device.runtime.adbSerial;
|
|
366
|
+
if (!serial) {
|
|
367
|
+
return { error: `${device.name} has no adb serial yet — wait for it to finish booting`, status: 400 };
|
|
368
|
+
}
|
|
369
|
+
const sdk = await discoverSdk(androidConfig().sdk_root ?? null);
|
|
370
|
+
if (!sdk.adb.path) return { error: "no adb found; install the Android SDK platform-tools", status: 400 };
|
|
371
|
+
|
|
372
|
+
const op = createOperation<{ message: string; code: string | null; package: string | null }>(
|
|
373
|
+
"android-install", `installing ${label} on ${device.name}`);
|
|
374
|
+
const controller = new AbortController();
|
|
375
|
+
registerCanceller(op.id, () => controller.abort());
|
|
376
|
+
updateOperation(op.id, { state: "running" });
|
|
377
|
+
|
|
378
|
+
void (async () => {
|
|
379
|
+
try {
|
|
380
|
+
const result = await installApk({
|
|
381
|
+
adbPath: sdk.adb.path!,
|
|
382
|
+
serial,
|
|
383
|
+
apkPath,
|
|
384
|
+
reinstall: opts.reinstall,
|
|
385
|
+
allowDowngrade: opts.allowDowngrade,
|
|
386
|
+
signal: controller.signal,
|
|
387
|
+
// adb's own progress lines ("Performing Streamed Install") are the only honest progress
|
|
388
|
+
// it offers; there is no percentage to report.
|
|
389
|
+
onProgress: (line) => updateOperation(op.id, { detail: line }),
|
|
390
|
+
});
|
|
391
|
+
if (result.ok) finishOperation(op.id, { message: result.message, code: null, package: null });
|
|
392
|
+
else failOperation(op.id, result.message);
|
|
393
|
+
} catch (e) {
|
|
394
|
+
failOperation(op.id, (e as Error).message);
|
|
395
|
+
} finally {
|
|
396
|
+
opts.cleanup?.();
|
|
397
|
+
}
|
|
398
|
+
})();
|
|
399
|
+
|
|
400
|
+
return { operationId: op.id };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Upload an APK and install it.
|
|
405
|
+
*
|
|
406
|
+
* The body is the file itself rather than a multipart form: `Bun.readableStreamToBlob` on a form
|
|
407
|
+
* would buffer the whole upload in memory before a byte reached disk, and these are hundreds of
|
|
408
|
+
* megabytes. Streaming it also makes cancel real — an aborted request rejects the read mid-file
|
|
409
|
+
* and `stageApkUpload` deletes the partial, which is the Phase 3 gate.
|
|
410
|
+
*/
|
|
411
|
+
androidRoutes.post("/devices/:deviceId/apk", async (c) => {
|
|
412
|
+
const rejected = assertControlAllowed(c);
|
|
413
|
+
if (rejected) return rejected;
|
|
414
|
+
|
|
415
|
+
const deviceId = c.req.param("deviceId");
|
|
416
|
+
if (!listDevices(await avdHome()).some((d) => d.runtime?.deviceId === deviceId)) {
|
|
417
|
+
return c.json(err("no such running device"), 404);
|
|
418
|
+
}
|
|
419
|
+
const body = c.req.raw.body;
|
|
420
|
+
if (!body) return c.json(err("no file in the request body"), 400);
|
|
421
|
+
|
|
422
|
+
let staged;
|
|
423
|
+
try {
|
|
424
|
+
staged = await stageApkUpload(body, { signal: c.req.raw.signal });
|
|
425
|
+
} catch (e) {
|
|
426
|
+
// An abort is the client's own doing: the partial is already deleted and there is usually
|
|
427
|
+
// nobody left to read the answer, so this is only a 400 for the case that is not an abort.
|
|
428
|
+
return c.json(err((e as Error).message), 400);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const filename = (c.req.query("filename") ?? "app.apk").split(/[\\/]/).pop() || "app.apk";
|
|
432
|
+
const started = await beginInstall(deviceId, staged.path, filename, {
|
|
433
|
+
reinstall: c.req.query("reinstall") !== "0",
|
|
434
|
+
allowDowngrade: c.req.query("downgrade") === "1",
|
|
435
|
+
cleanup: () => staged.discard(),
|
|
436
|
+
});
|
|
437
|
+
if ("error" in started) { staged.discard(); return c.json(err(started.error), started.status); }
|
|
438
|
+
return c.json(ok({ ...started, bytes: staged.bytes, filename }));
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Install a file that is already on this host, named by project rather than by path.
|
|
443
|
+
*
|
|
444
|
+
* The path is resolved *inside* the named project and rejected if it escapes — the same rule
|
|
445
|
+
* `extension-rpc-handlers.ts` applies, and the reason this takes `{project, path}` rather than
|
|
446
|
+
* an absolute path: an absolute path would make this route an arbitrary-file reader.
|
|
447
|
+
*/
|
|
448
|
+
androidRoutes.post("/devices/:deviceId/apk/project", async (c) => {
|
|
449
|
+
const rejected = assertControlAllowed(c);
|
|
450
|
+
if (rejected) return rejected;
|
|
451
|
+
|
|
452
|
+
const body = await c.req.json().catch(() => null) as { project?: unknown; path?: unknown; downgrade?: unknown } | null;
|
|
453
|
+
if (typeof body?.project !== "string" || typeof body?.path !== "string") {
|
|
454
|
+
return c.json(err("project and path are required"), 400);
|
|
455
|
+
}
|
|
456
|
+
const project = configService.get("projects").find((p) => p.name === body.project);
|
|
457
|
+
if (!project) return c.json(err("no such project"), 404);
|
|
458
|
+
|
|
459
|
+
const root = resolve(project.path);
|
|
460
|
+
const target = resolve(root, body.path);
|
|
461
|
+
const rel = relative(root, target);
|
|
462
|
+
if (rel.startsWith("..") || isAbsolute(rel)) return c.json(err("that path is outside the project"), 400);
|
|
463
|
+
if (!target.toLowerCase().endsWith(".apk")) return c.json(err("that is not an .apk file"), 400);
|
|
464
|
+
if (!existsSync(target)) return c.json(err("no such file"), 404);
|
|
465
|
+
|
|
466
|
+
const started = await beginInstall(c.req.param("deviceId"), target, basename(target), {
|
|
467
|
+
reinstall: true,
|
|
468
|
+
allowDowngrade: body.downgrade === true,
|
|
469
|
+
// Never a cleanup: this file belongs to the user's project, not to PPM.
|
|
470
|
+
});
|
|
471
|
+
if ("error" in started) return c.json(err(started.error), started.status);
|
|
472
|
+
return c.json(ok({ ...started, filename: basename(target) }));
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
/** Every .apk in a project, so the picker can offer them without a file browser. */
|
|
476
|
+
androidRoutes.get("/projects/:project/apks", async (c) => {
|
|
477
|
+
const rejected = assertControlAllowed(c);
|
|
478
|
+
if (rejected) return rejected;
|
|
479
|
+
const project = configService.get("projects").find((p) => p.name === c.req.param("project"));
|
|
480
|
+
if (!project) return c.json(err("no such project"), 404);
|
|
481
|
+
return c.json(ok({ apks: await findProjectApks(resolve(project.path)) }));
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
/* =============================================================================================
|
|
485
|
+
* Phase 4 — the device manager: create, wipe and delete an AVD.
|
|
486
|
+
*
|
|
487
|
+
* Three rules run through all of it, and each is a line in the plan:
|
|
488
|
+
*
|
|
489
|
+
* - **Typed wrappers, never a shell string.** The browser picks a device profile and a system
|
|
490
|
+
* image out of lists these routes produced; it never supplies a flag or a path.
|
|
491
|
+
* - **Destructive work only when the AVD is stopped**, and only after the caller has typed the
|
|
492
|
+
* name back. A confirmation the client could skip is not a confirmation, so the name is
|
|
493
|
+
* checked here rather than only in the dialog.
|
|
494
|
+
* - **PPM never accepts an SDK licence or starts a download.** Installing a system image is
|
|
495
|
+
* out of scope; these routes only ever offer what the host already has.
|
|
496
|
+
* ============================================================================================= */
|
|
497
|
+
|
|
498
|
+
androidRoutes.get("/system-images", async (c) => {
|
|
499
|
+
const rejected = assertEnabled(c);
|
|
500
|
+
if (rejected) return rejected;
|
|
501
|
+
const sdk = await discoverSdk(androidConfig().sdk_root ?? null);
|
|
502
|
+
const images = listSystemImages(sdk.root).map((i) => ({
|
|
503
|
+
...i,
|
|
504
|
+
hostCompatible: abiMatchesHost(i.abi),
|
|
505
|
+
// The absolute sysdir would name the user's home in a browser payload for no benefit.
|
|
506
|
+
sysdir: undefined,
|
|
507
|
+
}));
|
|
508
|
+
return c.json(ok({ images, canInstallMore: sdk.sdkmanager.path !== null }));
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
androidRoutes.get("/device-profiles", async (c) => {
|
|
512
|
+
const rejected = assertEnabled(c);
|
|
513
|
+
if (rejected) return rejected;
|
|
514
|
+
const sdk = await discoverSdk(androidConfig().sdk_root ?? null);
|
|
515
|
+
if (!sdk.avdmanager.path) {
|
|
516
|
+
return c.json(err("the SDK command-line tools are not installed — no device profiles to offer"), 400);
|
|
517
|
+
}
|
|
518
|
+
return c.json(ok({ profiles: await listDeviceProfiles(sdk.avdmanager.path) }));
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
androidRoutes.post("/avds", async (c) => {
|
|
522
|
+
const rejected = assertControlAllowed(c);
|
|
523
|
+
if (rejected) return rejected;
|
|
524
|
+
|
|
525
|
+
const body = await c.req.json().catch(() => null) as Partial<CreateAvdRequest> | null;
|
|
526
|
+
if (typeof body?.name !== "string" || typeof body?.systemImage !== "string"
|
|
527
|
+
|| typeof body?.deviceProfile !== "string") {
|
|
528
|
+
return c.json(err("name, systemImage and deviceProfile are required"), 400);
|
|
529
|
+
}
|
|
530
|
+
const nameProblem = validateAvdName(body.name);
|
|
531
|
+
if (nameProblem) return c.json(err(nameProblem), 400);
|
|
532
|
+
|
|
533
|
+
const sdk = await discoverSdk(androidConfig().sdk_root ?? null);
|
|
534
|
+
if (!sdk.avdmanager.path) {
|
|
535
|
+
return c.json(err("the SDK command-line tools are not installed — PPM cannot create an AVD"), 400);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const result = await createAvd({
|
|
539
|
+
name: body.name,
|
|
540
|
+
systemImage: body.systemImage,
|
|
541
|
+
deviceProfile: body.deviceProfile,
|
|
542
|
+
ramMb: numberOrUndefined(body.ramMb),
|
|
543
|
+
storageMb: numberOrUndefined(body.storageMb),
|
|
544
|
+
sdCardMb: numberOrUndefined(body.sdCardMb),
|
|
545
|
+
avdmanagerPath: sdk.avdmanager.path,
|
|
546
|
+
avdHome: sdk.avdHome,
|
|
547
|
+
images: listSystemImages(sdk.root),
|
|
548
|
+
});
|
|
549
|
+
if (!result.ok) return c.json(err(result.message), 400);
|
|
550
|
+
return c.json(ok({ avd: result.avd }));
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Both destructive routes share this: the AVD must exist, be stopped, and the caller must have
|
|
555
|
+
* typed its name back. Studio holding the lock counts as "not stopped" — plan §5, and the lock
|
|
556
|
+
* is reported, never deleted.
|
|
557
|
+
*/
|
|
558
|
+
async function assertDestructible(avdId: string, confirmName: unknown): Promise<
|
|
559
|
+
{ ok: true; device: DeviceEntry; dir: string } | { ok: false; error: string; status: 400 | 404 | 409 }
|
|
560
|
+
> {
|
|
561
|
+
const home = await avdHome();
|
|
562
|
+
const device = listDevices(home).find((d) => d.avdId === avdId);
|
|
563
|
+
if (!device) return { ok: false, error: "no such AVD", status: 404 };
|
|
564
|
+
if (device.runtime) {
|
|
565
|
+
return { ok: false, error: `${device.name} is running — stop it first`, status: 409 };
|
|
566
|
+
}
|
|
567
|
+
if (device.lockedByAnotherProcess) {
|
|
568
|
+
return { ok: false, error: `${device.name} is locked by another process — it is probably open in Android Studio`, status: 409 };
|
|
569
|
+
}
|
|
570
|
+
if (confirmName !== device.name) {
|
|
571
|
+
return { ok: false, error: `type ${device.name} to confirm`, status: 400 };
|
|
572
|
+
}
|
|
573
|
+
// The directory comes from the *listing*, not from joining the name onto the AVD home: the
|
|
574
|
+
// display name is read out of `config.ini` and need not match the directory it came from.
|
|
575
|
+
const summary = listAvds(home).find((a) => a.avdId === avdId);
|
|
576
|
+
if (!summary) return { ok: false, error: "no such AVD", status: 404 };
|
|
577
|
+
return { ok: true, device, dir: summary.dir };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
androidRoutes.post("/avds/:avdId/wipe", async (c) => {
|
|
581
|
+
const rejected = assertControlAllowed(c);
|
|
582
|
+
if (rejected) return rejected;
|
|
583
|
+
|
|
584
|
+
const body = await c.req.json().catch(() => null) as { confirmName?: unknown } | null;
|
|
585
|
+
const allowed = await assertDestructible(c.req.param("avdId"), body?.confirmName);
|
|
586
|
+
if (!allowed.ok) return c.json(err(allowed.error), allowed.status);
|
|
587
|
+
|
|
588
|
+
const outcome = wipeAvdData(allowed.dir);
|
|
589
|
+
if (!outcome.ok) return c.json(err(outcome.message), 400);
|
|
590
|
+
return c.json(ok({ removed: outcome.removed, freedBytes: outcome.freedBytes, message: outcome.message }));
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
androidRoutes.delete("/avds/:avdId", async (c) => {
|
|
594
|
+
const rejected = assertControlAllowed(c);
|
|
595
|
+
if (rejected) return rejected;
|
|
596
|
+
|
|
597
|
+
const body = await c.req.json().catch(() => null) as { confirmName?: unknown } | null;
|
|
598
|
+
const allowed = await assertDestructible(c.req.param("avdId"), body?.confirmName ?? c.req.query("confirmName"));
|
|
599
|
+
if (!allowed.ok) return c.json(err(allowed.error), allowed.status);
|
|
600
|
+
|
|
601
|
+
const sdk = await discoverSdk(androidConfig().sdk_root ?? null);
|
|
602
|
+
if (!sdk.avdmanager.path) {
|
|
603
|
+
return c.json(err("the SDK command-line tools are not installed — PPM cannot delete an AVD"), 400);
|
|
604
|
+
}
|
|
605
|
+
const result = await deleteAvd(sdk.avdmanager.path, allowed.device.name, sdk.avdHome);
|
|
606
|
+
if (!result.ok) return c.json(err(result.message), 400);
|
|
607
|
+
|
|
608
|
+
// Ownership is keyed by AVD name and would otherwise outlive the AVD, handing stop rights to
|
|
609
|
+
// whatever is created with that name next.
|
|
610
|
+
releaseOwnership(allowed.device.name);
|
|
611
|
+
return c.json(ok({ deleted: allowed.device.name }));
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
function numberOrUndefined(value: unknown): number | undefined {
|
|
615
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
616
|
+
}
|
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
} from "../../services/chat-search.service.ts";
|
|
39
39
|
import { compareSessionsByActivity, type ChatSearchResult, type ChatSearchResponse } from "../../types/chat.ts";
|
|
40
40
|
import { ok, err } from "../../types/api.ts";
|
|
41
|
+
import { VALID_PERMISSION_MODES } from "../../types/config.ts";
|
|
42
|
+
import { THINKING_ADAPTIVE, VALID_EFFORT_VALUES } from "../../providers/claude-agent-sdk-query-options.ts";
|
|
41
43
|
|
|
42
44
|
type Env = { Variables: { projectPath: string; projectName: string } };
|
|
43
45
|
|
|
@@ -351,6 +353,33 @@ chatRoutes.get("/sessions/:id/messages", async (c) => {
|
|
|
351
353
|
}
|
|
352
354
|
});
|
|
353
355
|
|
|
356
|
+
/**
|
|
357
|
+
* POST /chat/prewarm — a new-chat tab is open here: start the process its first message
|
|
358
|
+
* will run on, taking the picks that message will carry. The body is read the way the chat
|
|
359
|
+
* socket reads that message, since a spare is only used when the two agree. Answered at once;
|
|
360
|
+
* a request nothing can be started for (another provider, a small host, no claimed account)
|
|
361
|
+
* just starts nothing.
|
|
362
|
+
*/
|
|
363
|
+
chatRoutes.post("/prewarm", async (c) => {
|
|
364
|
+
const projectPath = c.get("projectPath");
|
|
365
|
+
const body = await c.req.json<Record<string, unknown>>().catch((): Record<string, unknown> => ({}));
|
|
366
|
+
const { providerId, accountId, permissionMode, model, effort, thinking } = body;
|
|
367
|
+
const opts = {
|
|
368
|
+
...(VALID_PERMISSION_MODES.includes(permissionMode as never) && { permissionMode: permissionMode as string }),
|
|
369
|
+
...(typeof model === "string" && model && { model }),
|
|
370
|
+
...(VALID_EFFORT_VALUES.includes(effort as never) && { effort: effort as string }),
|
|
371
|
+
...(typeof thinking === "boolean" && { thinkingBudget: thinking ? THINKING_ADAPTIVE : 0 }),
|
|
372
|
+
};
|
|
373
|
+
void chatService
|
|
374
|
+
.prewarm(typeof providerId === "string" ? providerId : undefined, {
|
|
375
|
+
projectPath,
|
|
376
|
+
accountId: typeof accountId === "string" ? accountId : undefined,
|
|
377
|
+
opts,
|
|
378
|
+
})
|
|
379
|
+
.catch((e) => console.warn(`[chat] prewarm failed: ${(e as Error).message}`));
|
|
380
|
+
return c.json(ok({ accepted: true }), 202);
|
|
381
|
+
});
|
|
382
|
+
|
|
354
383
|
/** POST /chat/sessions — create a new session for the project in context */
|
|
355
384
|
chatRoutes.post("/sessions", async (c) => {
|
|
356
385
|
try {
|
|
@@ -372,6 +401,8 @@ chatRoutes.post("/sessions", async (c) => {
|
|
|
372
401
|
projectName,
|
|
373
402
|
projectPath,
|
|
374
403
|
title: body.title,
|
|
404
|
+
// A design session spawns with its own instructions, so a spare would not fit it.
|
|
405
|
+
adoptWarmSpare: !isValidDesignSlug(designSlug),
|
|
375
406
|
});
|
|
376
407
|
if (body.clearedFrom) setSessionClearedFrom(session.id, body.clearedFrom);
|
|
377
408
|
if (isValidDesignSlug(designSlug)) setSessionDesignSlug(session.id, designSlug);
|