@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,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install an APK on this device: either one the browser uploads, or one already on the host.
|
|
3
|
+
*
|
|
4
|
+
* Both paths end in the same typed endpoint and the same operation poll, and neither of them
|
|
5
|
+
* ever names an adb argument — the browser says "this device, this file" and nothing more.
|
|
6
|
+
*
|
|
7
|
+
* The project path exists because the common case is not a download: it is an APK Gradle just
|
|
8
|
+
* wrote into the project PPM already has open, and making the user find it on disk, upload it
|
|
9
|
+
* back to the machine it is already on, and wait for 80 MB to cross the LAN twice would be
|
|
10
|
+
* absurd.
|
|
11
|
+
*/
|
|
12
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
13
|
+
import { api, getAuthToken } from "@/lib/api-client";
|
|
14
|
+
import { Button } from "@/components/ui/button";
|
|
15
|
+
import { Loader2, TriangleAlert, Check, Upload, X, RefreshCw } from "@/lib/icons";
|
|
16
|
+
import { useProjectStore } from "@/stores/project-store";
|
|
17
|
+
import { cn } from "@/lib/utils";
|
|
18
|
+
import { uploadApk, type ApkUploadHandle } from "./android-apk-upload";
|
|
19
|
+
|
|
20
|
+
const POLL_MS = 800;
|
|
21
|
+
|
|
22
|
+
interface ProjectApk { path: string; bytes: number; modifiedAt: number }
|
|
23
|
+
interface OperationView { state: string; detail: string; error: string | null }
|
|
24
|
+
|
|
25
|
+
export interface AndroidInstallPanelProps {
|
|
26
|
+
deviceId: string;
|
|
27
|
+
deviceName: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type Phase =
|
|
31
|
+
| { kind: "idle" }
|
|
32
|
+
| { kind: "uploading"; fraction: number; filename: string }
|
|
33
|
+
| { kind: "installing"; operationId: string; detail: string; filename: string }
|
|
34
|
+
| { kind: "done"; filename: string }
|
|
35
|
+
| { kind: "failed"; message: string };
|
|
36
|
+
|
|
37
|
+
export function AndroidInstallPanel({ deviceId, deviceName }: AndroidInstallPanelProps) {
|
|
38
|
+
const projects = useProjectStore((s) => s.projects);
|
|
39
|
+
const [projectName, setProjectName] = useState<string>("");
|
|
40
|
+
const [apks, setApks] = useState<ProjectApk[] | null>(null);
|
|
41
|
+
const [scanning, setScanning] = useState(false);
|
|
42
|
+
const [allowDowngrade, setAllowDowngrade] = useState(false);
|
|
43
|
+
const [phase, setPhase] = useState<Phase>({ kind: "idle" });
|
|
44
|
+
|
|
45
|
+
const fileRef = useRef<HTMLInputElement | null>(null);
|
|
46
|
+
const uploadRef = useRef<ApkUploadHandle | null>(null);
|
|
47
|
+
// Read by the poll loop, which is created once and would otherwise see the phase from the
|
|
48
|
+
// render that started it.
|
|
49
|
+
const phaseRef = useRef<Phase>(phase);
|
|
50
|
+
phaseRef.current = phase;
|
|
51
|
+
|
|
52
|
+
const busy = phase.kind === "uploading" || phase.kind === "installing";
|
|
53
|
+
|
|
54
|
+
/** Poll one install to its end. Cancel is a separate typed call, not an abort of this. */
|
|
55
|
+
const watchOperation = useCallback(async (operationId: string, filename: string) => {
|
|
56
|
+
for (;;) {
|
|
57
|
+
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
58
|
+
// Stop if this install is no longer the one on screen — cancelled, or replaced by a second
|
|
59
|
+
// one the user started. Checking only the *kind* would let two loops write over each other.
|
|
60
|
+
const now = phaseRef.current;
|
|
61
|
+
if (now.kind !== "installing" || now.operationId !== operationId) return;
|
|
62
|
+
let op: OperationView;
|
|
63
|
+
try {
|
|
64
|
+
op = await api.get<OperationView>(`/api/android/operations/${encodeURIComponent(operationId)}`);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
setPhase({ kind: "failed", message: (e as Error).message });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (op.state === "succeeded") { setPhase({ kind: "done", filename }); return; }
|
|
70
|
+
if (op.state === "failed") { setPhase({ kind: "failed", message: op.error ?? "the install failed" }); return; }
|
|
71
|
+
if (op.state === "cancelled") { setPhase({ kind: "idle" }); return; }
|
|
72
|
+
setPhase({ kind: "installing", operationId, detail: op.detail, filename });
|
|
73
|
+
}
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const onFile = useCallback((file: File) => {
|
|
77
|
+
setPhase({ kind: "uploading", fraction: 0, filename: file.name });
|
|
78
|
+
const handle = uploadApk({
|
|
79
|
+
deviceId, file, allowDowngrade, token: getAuthToken(),
|
|
80
|
+
onProgress: (fraction) => setPhase({ kind: "uploading", fraction, filename: file.name }),
|
|
81
|
+
});
|
|
82
|
+
uploadRef.current = handle;
|
|
83
|
+
void handle.done
|
|
84
|
+
.then((operationId) => {
|
|
85
|
+
uploadRef.current = null;
|
|
86
|
+
setPhase({ kind: "installing", operationId, detail: "installing…", filename: file.name });
|
|
87
|
+
void watchOperation(operationId, file.name);
|
|
88
|
+
})
|
|
89
|
+
.catch((e: Error) => {
|
|
90
|
+
uploadRef.current = null;
|
|
91
|
+
// A cancel is the user's own doing and needs no red box.
|
|
92
|
+
setPhase(e.message === "cancelled" ? { kind: "idle" } : { kind: "failed", message: e.message });
|
|
93
|
+
});
|
|
94
|
+
}, [deviceId, allowDowngrade, watchOperation]);
|
|
95
|
+
|
|
96
|
+
const installFromProject = useCallback(async (path: string) => {
|
|
97
|
+
const filename = path.split("/").pop() ?? path;
|
|
98
|
+
setPhase({ kind: "installing", operationId: "", detail: "starting…", filename });
|
|
99
|
+
try {
|
|
100
|
+
const { operationId } = await api.post<{ operationId: string }>(
|
|
101
|
+
`/api/android/devices/${encodeURIComponent(deviceId)}/apk/project`,
|
|
102
|
+
{ project: projectName, path, downgrade: allowDowngrade },
|
|
103
|
+
);
|
|
104
|
+
setPhase({ kind: "installing", operationId, detail: "installing…", filename });
|
|
105
|
+
void watchOperation(operationId, filename);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
setPhase({ kind: "failed", message: (e as Error).message });
|
|
108
|
+
}
|
|
109
|
+
}, [deviceId, projectName, allowDowngrade, watchOperation]);
|
|
110
|
+
|
|
111
|
+
const cancel = useCallback(() => {
|
|
112
|
+
if (phase.kind === "uploading") { uploadRef.current?.cancel(); return; }
|
|
113
|
+
if (phase.kind === "installing" && phase.operationId) {
|
|
114
|
+
void api.post(`/api/android/operations/${encodeURIComponent(phase.operationId)}/cancel`, {})
|
|
115
|
+
.catch(() => { /* it finished first; the poll will say so */ });
|
|
116
|
+
setPhase({ kind: "idle" });
|
|
117
|
+
}
|
|
118
|
+
}, [phase]);
|
|
119
|
+
|
|
120
|
+
const scan = useCallback(async (name: string) => {
|
|
121
|
+
if (!name) { setApks(null); return; }
|
|
122
|
+
setScanning(true);
|
|
123
|
+
try {
|
|
124
|
+
const res = await api.get<{ apks: ProjectApk[] }>(`/api/android/projects/${encodeURIComponent(name)}/apks`);
|
|
125
|
+
setApks(res.apks);
|
|
126
|
+
} catch {
|
|
127
|
+
setApks([]);
|
|
128
|
+
} finally {
|
|
129
|
+
setScanning(false);
|
|
130
|
+
}
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
useEffect(() => { void scan(projectName); }, [projectName, scan]);
|
|
134
|
+
useEffect(() => () => uploadRef.current?.cancel(), []);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className="flex h-full min-h-0 flex-col gap-3 overflow-auto p-3">
|
|
138
|
+
<p className="text-xs text-muted-foreground">Installing on {deviceName}.</p>
|
|
139
|
+
|
|
140
|
+
{/* --- upload ------------------------------------------------------------------------ */}
|
|
141
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
142
|
+
<input
|
|
143
|
+
ref={fileRef}
|
|
144
|
+
type="file"
|
|
145
|
+
accept=".apk,application/vnd.android.package-archive"
|
|
146
|
+
className="hidden"
|
|
147
|
+
onChange={(e) => {
|
|
148
|
+
const file = e.target.files?.[0];
|
|
149
|
+
e.target.value = ""; // so choosing the same file twice fires again
|
|
150
|
+
if (file) onFile(file);
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
<Button className="h-11 md:h-9" disabled={busy} onClick={() => fileRef.current?.click()}>
|
|
154
|
+
<Upload /> Choose an APK
|
|
155
|
+
</Button>
|
|
156
|
+
<label className="flex min-h-11 cursor-pointer items-center gap-2 text-sm md:min-h-9">
|
|
157
|
+
<input
|
|
158
|
+
type="checkbox"
|
|
159
|
+
className="size-4"
|
|
160
|
+
checked={allowDowngrade}
|
|
161
|
+
disabled={busy}
|
|
162
|
+
onChange={(e) => setAllowDowngrade(e.target.checked)}
|
|
163
|
+
/>
|
|
164
|
+
Allow downgrade
|
|
165
|
+
</label>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<PhaseView phase={phase} onCancel={cancel} onDismiss={() => setPhase({ kind: "idle" })} />
|
|
169
|
+
|
|
170
|
+
{/* --- from a project ---------------------------------------------------------------- */}
|
|
171
|
+
<div className="flex flex-col gap-2 border-t pt-3">
|
|
172
|
+
<div className="flex items-center gap-2">
|
|
173
|
+
<label className="text-sm" htmlFor="android-apk-project">From a project</label>
|
|
174
|
+
<select
|
|
175
|
+
id="android-apk-project"
|
|
176
|
+
className="h-11 min-w-0 flex-1 rounded-md border bg-background px-2 text-sm md:h-9"
|
|
177
|
+
value={projectName}
|
|
178
|
+
onChange={(e) => setProjectName(e.target.value)}
|
|
179
|
+
>
|
|
180
|
+
<option value="">Choose a project…</option>
|
|
181
|
+
{projects.map((p) => <option key={p.name} value={p.name}>{p.name}</option>)}
|
|
182
|
+
</select>
|
|
183
|
+
{projectName && (
|
|
184
|
+
<Button variant="ghost" size="icon" className="size-11 md:size-9" disabled={scanning}
|
|
185
|
+
onClick={() => void scan(projectName)} title="Rescan" aria-label="Rescan">
|
|
186
|
+
{scanning ? <Loader2 className="animate-spin" /> : <RefreshCw />}
|
|
187
|
+
</Button>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{scanning && <p className="text-sm text-muted-foreground">Looking for APKs…</p>}
|
|
192
|
+
{!scanning && apks?.length === 0 && (
|
|
193
|
+
<p className="text-sm text-muted-foreground">
|
|
194
|
+
No .apk files in that project yet — build one and press rescan.
|
|
195
|
+
</p>
|
|
196
|
+
)}
|
|
197
|
+
{!scanning && apks && apks.length > 0 && (
|
|
198
|
+
<ul className="flex flex-col gap-1">
|
|
199
|
+
{apks.map((apk) => (
|
|
200
|
+
<li key={apk.path}>
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
disabled={busy}
|
|
204
|
+
onClick={() => void installFromProject(apk.path)}
|
|
205
|
+
className={cn(
|
|
206
|
+
"flex min-h-11 w-full flex-col items-start rounded-md border px-3 py-2 text-left text-sm md:min-h-9",
|
|
207
|
+
busy ? "opacity-50" : "hover:bg-accent",
|
|
208
|
+
)}
|
|
209
|
+
>
|
|
210
|
+
{/* Filenames differ at the end, so the head is what gets cut (CLAUDE.md). */}
|
|
211
|
+
<span dir="rtl" className="w-full truncate"><bdi>{apk.path}</bdi></span>
|
|
212
|
+
<span className="text-xs text-muted-foreground">
|
|
213
|
+
{formatBytes(apk.bytes)} · {new Date(apk.modifiedAt).toLocaleString()}
|
|
214
|
+
</span>
|
|
215
|
+
</button>
|
|
216
|
+
</li>
|
|
217
|
+
))}
|
|
218
|
+
</ul>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function PhaseView({ phase, onCancel, onDismiss }: { phase: Phase; onCancel: () => void; onDismiss: () => void }) {
|
|
226
|
+
if (phase.kind === "idle") return null;
|
|
227
|
+
|
|
228
|
+
if (phase.kind === "uploading" || phase.kind === "installing") {
|
|
229
|
+
const percent = phase.kind === "uploading" ? Math.round(phase.fraction * 100) : null;
|
|
230
|
+
const detail = phase.kind === "installing" ? phase.detail : "";
|
|
231
|
+
return (
|
|
232
|
+
<div className="flex flex-col gap-2 rounded-md border p-3">
|
|
233
|
+
<div className="flex items-center gap-2 text-sm">
|
|
234
|
+
<Loader2 className="size-4 shrink-0 animate-spin" />
|
|
235
|
+
<span dir="rtl" className="min-w-0 flex-1 truncate"><bdi>{phase.filename}</bdi></span>
|
|
236
|
+
<Button variant="ghost" size="icon" className="size-9" onClick={onCancel}
|
|
237
|
+
title="Cancel" aria-label="Cancel">
|
|
238
|
+
<X />
|
|
239
|
+
</Button>
|
|
240
|
+
</div>
|
|
241
|
+
{percent !== null ? (
|
|
242
|
+
<>
|
|
243
|
+
<div className="h-1.5 overflow-hidden rounded-full bg-muted">
|
|
244
|
+
<div className="h-full bg-primary transition-[width]" style={{ width: `${percent}%` }} />
|
|
245
|
+
</div>
|
|
246
|
+
<p className="text-xs text-muted-foreground">Uploading — {percent}%</p>
|
|
247
|
+
</>
|
|
248
|
+
) : (
|
|
249
|
+
// adb has no percentage to report, so the panel does not invent one.
|
|
250
|
+
<p className="text-xs text-muted-foreground">{detail}</p>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (phase.kind === "done") {
|
|
257
|
+
return (
|
|
258
|
+
<div className="flex items-center gap-2 rounded-md border border-emerald-600/40 bg-emerald-600/10 p-3 text-sm">
|
|
259
|
+
<Check className="size-4 shrink-0 text-emerald-600 dark:text-emerald-400" />
|
|
260
|
+
<span dir="rtl" className="min-w-0 flex-1 truncate"><bdi>{phase.filename}</bdi></span>
|
|
261
|
+
<span className="shrink-0 text-muted-foreground">installed</span>
|
|
262
|
+
<Button variant="ghost" size="icon" className="size-9" onClick={onDismiss} aria-label="Dismiss"><X /></Button>
|
|
263
|
+
</div>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<div className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 p-3 text-sm">
|
|
269
|
+
<TriangleAlert className="mt-0.5 size-4 shrink-0 text-destructive" />
|
|
270
|
+
<span className="min-w-0 flex-1">{phase.message}</span>
|
|
271
|
+
<Button variant="ghost" size="icon" className="size-9" onClick={onDismiss} aria-label="Dismiss"><X /></Button>
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function formatBytes(bytes: number): string {
|
|
277
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
278
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
279
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
280
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the log panel shows, as pure functions.
|
|
3
|
+
*
|
|
4
|
+
* Separate from the component because a zustand-touching or DOM-touching module cannot be loaded
|
|
5
|
+
* under `bun:test` (the stores read `localStorage` at import time), and these three decisions —
|
|
6
|
+
* which level counts, what the search matches, how the ring is bounded — are exactly the ones
|
|
7
|
+
* worth pinning.
|
|
8
|
+
*/
|
|
9
|
+
import type { AndroidLogEntry, AndroidLogLevel } from "../../../shared/android-protocol";
|
|
10
|
+
|
|
11
|
+
/** Ascending severity. The panel's picker is a *minimum*, so the order is the whole mechanism. */
|
|
12
|
+
export const LOG_LEVELS: AndroidLogLevel[] = ["verbose", "debug", "info", "warn", "error", "fatal"];
|
|
13
|
+
|
|
14
|
+
const RANK = new Map(LOG_LEVELS.map((l, i) => [l, i]));
|
|
15
|
+
|
|
16
|
+
export function meetsLevel(level: AndroidLogLevel, minimum: AndroidLogLevel): boolean {
|
|
17
|
+
return (RANK.get(level) ?? 0) >= (RANK.get(minimum) ?? 0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LogFilter {
|
|
21
|
+
minimum: AndroidLogLevel;
|
|
22
|
+
/** Matched case-insensitively against the tag and the message, never against the pid. */
|
|
23
|
+
text: string;
|
|
24
|
+
/** Only entries from this pid. 0 means every process. */
|
|
25
|
+
pid: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const EMPTY_FILTER: LogFilter = { minimum: "verbose", text: "", pid: 0 };
|
|
29
|
+
|
|
30
|
+
export function matchesFilter(entry: AndroidLogEntry, filter: LogFilter): boolean {
|
|
31
|
+
if (!meetsLevel(entry.level, filter.minimum)) return false;
|
|
32
|
+
if (filter.pid !== 0 && entry.pid !== filter.pid) return false;
|
|
33
|
+
const needle = filter.text.trim().toLowerCase();
|
|
34
|
+
if (needle.length === 0) return true;
|
|
35
|
+
return entry.tag.toLowerCase().includes(needle) || entry.message.toLowerCase().includes(needle);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Append a batch and keep the newest `max`.
|
|
40
|
+
*
|
|
41
|
+
* Returns the same array reference when nothing arrived, so a React state setter given this can
|
|
42
|
+
* bail out instead of re-rendering — a device logging nothing must not cost a render a tick.
|
|
43
|
+
*/
|
|
44
|
+
export function appendBounded(
|
|
45
|
+
existing: AndroidLogEntry[],
|
|
46
|
+
incoming: AndroidLogEntry[],
|
|
47
|
+
max: number,
|
|
48
|
+
): AndroidLogEntry[] {
|
|
49
|
+
if (incoming.length === 0) return existing;
|
|
50
|
+
// A reconnection replays the server's whole ring, so the same ids arrive twice. Dropping by id
|
|
51
|
+
// is cheaper than it looks: the overlap is always at the head, and ids are monotonic per
|
|
52
|
+
// device, so a single comparison against the last id we hold answers it for the common case.
|
|
53
|
+
const lastId = existing.length > 0 ? existing[existing.length - 1]!.id : 0;
|
|
54
|
+
const fresh = incoming[0]!.id > lastId ? incoming : incoming.filter((e) => e.id > lastId);
|
|
55
|
+
if (fresh.length === 0) return existing;
|
|
56
|
+
const merged = existing.concat(fresh);
|
|
57
|
+
return merged.length > max ? merged.slice(merged.length - max) : merged;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `13:09:42.640`, local time — the date is in the header, and a log row has no room for it. */
|
|
61
|
+
export function formatLogTime(timestamp: number): string {
|
|
62
|
+
const d = new Date(timestamp);
|
|
63
|
+
const p = (n: number, w = 2) => String(n).padStart(w, "0");
|
|
64
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One line per entry, in logcat's own `threadtime` shape, for copy and for a saved file. */
|
|
68
|
+
export function formatLogText(entries: AndroidLogEntry[]): string {
|
|
69
|
+
const letter: Record<AndroidLogLevel, string> = {
|
|
70
|
+
verbose: "V", debug: "D", info: "I", warn: "W", error: "E", fatal: "F",
|
|
71
|
+
};
|
|
72
|
+
return entries
|
|
73
|
+
.map((e) => `${formatLogTime(e.timestamp)} ${String(e.pid).padStart(5)} ${String(e.tid).padStart(5)} ${letter[e.level]} ${e.tag}: ${e.message}`)
|
|
74
|
+
.join("\n");
|
|
75
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The device's log, live.
|
|
3
|
+
*
|
|
4
|
+
* Three decisions in here are about cost rather than looks:
|
|
5
|
+
*
|
|
6
|
+
* - **Nothing is stored.** The plan is explicit that logcat never reaches the database. The
|
|
7
|
+
* ring lives in this component's state and dies with it; the server holds a second, larger
|
|
8
|
+
* one so reopening the panel is not an empty box.
|
|
9
|
+
* - **The subscription follows the panel.** Mounting turns the device's log stream on and
|
|
10
|
+
* unmounting turns it off, which is the plan's gate ("logs hidden ngừng subscription") — so
|
|
11
|
+
* a viewer left open overnight is not also running a logcat nobody is reading.
|
|
12
|
+
* - **Following is suspended the moment you scroll up.** Auto-scrolling a list somebody is
|
|
13
|
+
* reading is the one behaviour that makes a log viewer useless, and a "follow" checkbox they
|
|
14
|
+
* have to find first is not much better.
|
|
15
|
+
*/
|
|
16
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
17
|
+
import { Button } from "@/components/ui/button";
|
|
18
|
+
import { Input } from "@/components/ui/input";
|
|
19
|
+
import { ArrowDown, Copy, Check, Pause, Play, Search, Trash2 } from "@/lib/icons";
|
|
20
|
+
import { cn } from "@/lib/utils";
|
|
21
|
+
import {
|
|
22
|
+
appendBounded, EMPTY_FILTER, formatLogText, formatLogTime, LOG_LEVELS, matchesFilter,
|
|
23
|
+
type LogFilter,
|
|
24
|
+
} from "./android-log-filter";
|
|
25
|
+
import type { AndroidLogEntry, AndroidLogLevel } from "../../../shared/android-protocol";
|
|
26
|
+
|
|
27
|
+
/** The client keeps fewer than the server's 2000: this is what one person scrolls, not storage. */
|
|
28
|
+
const CLIENT_RING = 1_000;
|
|
29
|
+
/** Rendering every row of a busy log is what makes the panel stutter, not receiving them. */
|
|
30
|
+
const MAX_ROWS = 400;
|
|
31
|
+
|
|
32
|
+
export interface AndroidLogcatPanelProps {
|
|
33
|
+
setLogcat: (on: boolean) => void;
|
|
34
|
+
onLog: (listener: (entries: AndroidLogEntry[]) => void) => () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const LEVEL_CLASS: Record<AndroidLogLevel, string> = {
|
|
38
|
+
verbose: "text-muted-foreground",
|
|
39
|
+
debug: "text-muted-foreground",
|
|
40
|
+
info: "text-foreground",
|
|
41
|
+
warn: "text-amber-600 dark:text-amber-400",
|
|
42
|
+
error: "text-destructive",
|
|
43
|
+
fatal: "text-destructive font-semibold",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const LEVEL_LETTER: Record<AndroidLogLevel, string> = {
|
|
47
|
+
verbose: "V", debug: "D", info: "I", warn: "W", error: "E", fatal: "F",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function AndroidLogcatPanel({ setLogcat, onLog }: AndroidLogcatPanelProps) {
|
|
51
|
+
const [entries, setEntries] = useState<AndroidLogEntry[]>([]);
|
|
52
|
+
const [filter, setFilter] = useState<LogFilter>(EMPTY_FILTER);
|
|
53
|
+
const [paused, setPaused] = useState(false);
|
|
54
|
+
const [copied, setCopied] = useState(false);
|
|
55
|
+
const [following, setFollowing] = useState(true);
|
|
56
|
+
|
|
57
|
+
const listRef = useRef<HTMLDivElement | null>(null);
|
|
58
|
+
const pausedRef = useRef(false);
|
|
59
|
+
pausedRef.current = paused;
|
|
60
|
+
|
|
61
|
+
// Turning the feed on is this panel existing, and turning it off is it going away. The gate
|
|
62
|
+
// asks for exactly that, and a separate toggle would let the two disagree.
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
setLogcat(true);
|
|
65
|
+
return () => setLogcat(false);
|
|
66
|
+
}, [setLogcat]);
|
|
67
|
+
|
|
68
|
+
useEffect(() => onLog((batch) => {
|
|
69
|
+
// Pause freezes the *view*: the stream keeps running on the host so nothing is missed, but
|
|
70
|
+
// a log that scrolls while you are reading it is the thing pause exists to stop.
|
|
71
|
+
if (pausedRef.current) return;
|
|
72
|
+
setEntries((current) => appendBounded(current, batch, CLIENT_RING));
|
|
73
|
+
}), [onLog]);
|
|
74
|
+
|
|
75
|
+
const visible = useMemo(() => {
|
|
76
|
+
const matched = entries.filter((e) => matchesFilter(e, filter));
|
|
77
|
+
return matched.length > MAX_ROWS ? matched.slice(matched.length - MAX_ROWS) : matched;
|
|
78
|
+
}, [entries, filter]);
|
|
79
|
+
|
|
80
|
+
// Scroll after the rows are in the DOM, and only while the reader has not scrolled away.
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!following) return;
|
|
83
|
+
const el = listRef.current;
|
|
84
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
85
|
+
}, [visible, following]);
|
|
86
|
+
|
|
87
|
+
const onScroll = useCallback(() => {
|
|
88
|
+
const el = listRef.current;
|
|
89
|
+
if (!el) return;
|
|
90
|
+
// 24px of slack: a list that is one subpixel off the bottom must not read as "scrolled up".
|
|
91
|
+
setFollowing(el.scrollHeight - el.scrollTop - el.clientHeight < 24);
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
const copyAll = useCallback(async () => {
|
|
95
|
+
const text = formatLogText(visible);
|
|
96
|
+
try {
|
|
97
|
+
// `navigator.clipboard` does not exist on an insecure origin, which is PPM's usual LAN
|
|
98
|
+
// deployment — the textarea fallback is the only thing that works there.
|
|
99
|
+
if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(text);
|
|
100
|
+
else copyViaTextarea(text);
|
|
101
|
+
setCopied(true);
|
|
102
|
+
setTimeout(() => setCopied(false), 1500);
|
|
103
|
+
} catch {
|
|
104
|
+
copyViaTextarea(text);
|
|
105
|
+
}
|
|
106
|
+
}, [visible]);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="flex h-full min-h-0 flex-col bg-background">
|
|
110
|
+
<div className="flex flex-wrap items-center gap-2 border-b p-2">
|
|
111
|
+
<div className="relative min-w-40 flex-1">
|
|
112
|
+
<Search className="pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
113
|
+
<Input
|
|
114
|
+
className="h-11 pl-8 md:h-9"
|
|
115
|
+
placeholder="Filter tag or message"
|
|
116
|
+
value={filter.text}
|
|
117
|
+
onChange={(e) => setFilter((f) => ({ ...f, text: e.target.value }))}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* A native select so a phone gets its own picker (design guidelines §8). */}
|
|
122
|
+
<label className="sr-only" htmlFor="android-log-level">Minimum level</label>
|
|
123
|
+
<select
|
|
124
|
+
id="android-log-level"
|
|
125
|
+
className="h-11 rounded-md border bg-background px-2 text-sm md:h-9"
|
|
126
|
+
value={filter.minimum}
|
|
127
|
+
onChange={(e) => setFilter((f) => ({ ...f, minimum: e.target.value as AndroidLogLevel }))}
|
|
128
|
+
>
|
|
129
|
+
{LOG_LEVELS.map((l) => (
|
|
130
|
+
<option key={l} value={l}>{l[0]!.toUpperCase() + l.slice(1)}</option>
|
|
131
|
+
))}
|
|
132
|
+
</select>
|
|
133
|
+
|
|
134
|
+
<Button variant={paused ? "default" : "ghost"} size="icon" className="size-11 md:size-9"
|
|
135
|
+
onClick={() => setPaused((p) => !p)}
|
|
136
|
+
title={paused ? "Resume" : "Pause the view"} aria-label={paused ? "Resume" : "Pause the view"}
|
|
137
|
+
aria-pressed={paused}>
|
|
138
|
+
{paused ? <Play /> : <Pause />}
|
|
139
|
+
</Button>
|
|
140
|
+
<Button variant="ghost" size="icon" className="size-11 md:size-9"
|
|
141
|
+
onClick={() => setEntries([])} title="Clear the view" aria-label="Clear the view">
|
|
142
|
+
<Trash2 />
|
|
143
|
+
</Button>
|
|
144
|
+
<Button variant="ghost" size="icon" className="size-11 md:size-9"
|
|
145
|
+
onClick={() => void copyAll()} title="Copy what is shown" aria-label="Copy what is shown">
|
|
146
|
+
{copied ? <Check /> : <Copy />}
|
|
147
|
+
</Button>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div
|
|
151
|
+
ref={listRef}
|
|
152
|
+
onScroll={onScroll}
|
|
153
|
+
className="min-h-0 flex-1 overflow-auto px-2 py-1 font-mono text-[11px] leading-[1.45]"
|
|
154
|
+
>
|
|
155
|
+
{visible.length === 0 ? (
|
|
156
|
+
<p className="px-2 py-6 text-center text-sm text-muted-foreground">
|
|
157
|
+
{entries.length === 0 ? "Waiting for the device to log something…" : "Nothing matches that filter."}
|
|
158
|
+
</p>
|
|
159
|
+
) : visible.map((e) => (
|
|
160
|
+
<div key={e.id} className="flex gap-2 whitespace-pre-wrap break-words">
|
|
161
|
+
<span className="shrink-0 tabular-nums text-muted-foreground">{formatLogTime(e.timestamp)}</span>
|
|
162
|
+
<span className={cn("w-3 shrink-0 text-center font-semibold", LEVEL_CLASS[e.level])}>
|
|
163
|
+
{LEVEL_LETTER[e.level]}
|
|
164
|
+
</span>
|
|
165
|
+
{e.tag && (
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
className="shrink-0 max-w-40 truncate text-left text-muted-foreground hover:underline"
|
|
169
|
+
title={`Filter by ${e.tag}`}
|
|
170
|
+
onClick={() => setFilter((f) => ({ ...f, text: e.tag }))}
|
|
171
|
+
>
|
|
172
|
+
{e.tag}
|
|
173
|
+
</button>
|
|
174
|
+
)}
|
|
175
|
+
<span className={cn("min-w-0", LEVEL_CLASS[e.level])}>{e.message}</span>
|
|
176
|
+
</div>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div className="flex items-center gap-2 border-t px-2 py-1 text-xs text-muted-foreground">
|
|
181
|
+
<span>{visible.length === entries.length ? `${entries.length} lines` : `${visible.length} of ${entries.length} lines`}</span>
|
|
182
|
+
{paused && <span className="text-amber-600 dark:text-amber-400">paused</span>}
|
|
183
|
+
<span className="flex-1" />
|
|
184
|
+
{!following && (
|
|
185
|
+
<Button variant="ghost" size="sm" className="h-8" onClick={() => setFollowing(true)}>
|
|
186
|
+
<ArrowDown /> Follow
|
|
187
|
+
</Button>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The insecure-origin copy fallback.
|
|
196
|
+
*
|
|
197
|
+
* The textarea must be rendered — `display:none` and `visibility:hidden` both give an empty
|
|
198
|
+
* selection, and `execCommand` then copies nothing while still returning true (CLAUDE.md, remote
|
|
199
|
+
* desktop clipboard).
|
|
200
|
+
*/
|
|
201
|
+
function copyViaTextarea(text: string): void {
|
|
202
|
+
const area = document.createElement("textarea");
|
|
203
|
+
area.value = text;
|
|
204
|
+
area.style.position = "fixed";
|
|
205
|
+
area.style.left = "-9999px";
|
|
206
|
+
area.setAttribute("readonly", "");
|
|
207
|
+
document.body.appendChild(area);
|
|
208
|
+
try {
|
|
209
|
+
area.select();
|
|
210
|
+
document.execCommand("copy");
|
|
211
|
+
} finally {
|
|
212
|
+
area.remove();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saving a screenshot to the browser's downloads.
|
|
3
|
+
*
|
|
4
|
+
* The route needs the `Authorization` header, so a plain `<a href>` cannot fetch it — the bytes
|
|
5
|
+
* come back through `fetch` and are handed to an anchor as a blob URL. Kept out of the component
|
|
6
|
+
* so the two easy things to get wrong are testable: reading the server's filename out of
|
|
7
|
+
* `Content-Disposition`, and revoking the URL (a blob URL revoked too early gives the browser a
|
|
8
|
+
* broken download with no error, CLAUDE.md).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** The server names the file; this only has to survive a header it did not write. */
|
|
12
|
+
export function filenameFromDisposition(header: string | null, fallback: string): string {
|
|
13
|
+
if (!header) return fallback;
|
|
14
|
+
// RFC 5987 `filename*=UTF-8''...` wins over the plain form when both are present.
|
|
15
|
+
const extended = header.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
|
|
16
|
+
if (extended?.[1]) {
|
|
17
|
+
try { return decodeURIComponent(extended[1].trim()) || fallback; } catch { /* malformed */ }
|
|
18
|
+
}
|
|
19
|
+
const quoted = header.match(/filename\s*=\s*"([^"]*)"/i) ?? header.match(/filename\s*=\s*([^;]+)/i);
|
|
20
|
+
const name = quoted?.[1]?.trim();
|
|
21
|
+
return name && name.length > 0 ? name : fallback;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function screenshotUrl(deviceId: string): string {
|
|
25
|
+
return `/api/android/devices/${encodeURIComponent(deviceId)}/screenshot`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Hand a blob to the browser as a download. Exported so the save can be tested without a fetch. */
|
|
29
|
+
export function saveBlob(blob: Blob, filename: string): void {
|
|
30
|
+
const url = URL.createObjectURL(blob);
|
|
31
|
+
const anchor = document.createElement("a");
|
|
32
|
+
anchor.href = url;
|
|
33
|
+
anchor.download = filename;
|
|
34
|
+
document.body.appendChild(anchor);
|
|
35
|
+
anchor.click();
|
|
36
|
+
anchor.remove();
|
|
37
|
+
// Revoking synchronously races the download in Safari and Firefox; a tick is enough and the
|
|
38
|
+
// page holds nothing else on it.
|
|
39
|
+
setTimeout(() => URL.revokeObjectURL(url), 10_000);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function downloadScreenshot(deviceId: string, token: string | null, fallbackName: string): Promise<void> {
|
|
43
|
+
const res = await fetch(screenshotUrl(deviceId), {
|
|
44
|
+
headers: token ? { Authorization: `Bearer ${token}`, "x-ppm-client": "web" } : { "x-ppm-client": "web" },
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
// The route answers PPM's envelope on failure and raw PNG on success.
|
|
48
|
+
let message = `the screenshot failed with HTTP ${res.status}`;
|
|
49
|
+
try {
|
|
50
|
+
const body = await res.json() as { error?: string };
|
|
51
|
+
if (body?.error) message = body.error;
|
|
52
|
+
} catch { /* not JSON */ }
|
|
53
|
+
throw new Error(message);
|
|
54
|
+
}
|
|
55
|
+
saveBlob(await res.blob(), filenameFromDisposition(res.headers.get("content-disposition"), fallbackName));
|
|
56
|
+
}
|