@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.
Files changed (641) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/CLAUDE.md +13 -2
  3. package/assets/skills/ppm/SKILL.md +1 -1
  4. package/assets/skills/ppm/references/cli-reference.md +4 -4
  5. package/assets/skills/ppm/references/http-api.md +37 -1
  6. package/bun.lock +34 -0
  7. package/dist/web/assets/accounts-settings-section-CGe5ikvs.js +1 -0
  8. package/dist/web/assets/accounts-settings-section-CGe5ikvs.js.br +0 -0
  9. package/dist/web/assets/accounts-settings-section-CGe5ikvs.js.gz +0 -0
  10. package/dist/web/assets/{agent-session-sheet-CBMsnl_p.js → agent-session-sheet-vBhfHYrC.js} +2 -2
  11. package/dist/web/assets/agent-session-sheet-vBhfHYrC.js.br +0 -0
  12. package/dist/web/assets/agent-session-sheet-vBhfHYrC.js.gz +0 -0
  13. package/dist/web/assets/{agent-session-window-content-D9C9FA7E.js → agent-session-window-content-BxIxDN5i.js} +1 -1
  14. package/dist/web/assets/agent-session-window-content-BxIxDN5i.js.br +0 -0
  15. package/dist/web/assets/agent-session-window-content-BxIxDN5i.js.gz +0 -0
  16. package/dist/web/assets/{ai-resource-editor-DhUVgDEG.js → ai-resource-editor-Y8clBPRv.js} +1 -1
  17. package/dist/web/assets/ai-resource-editor-Y8clBPRv.js.br +0 -0
  18. package/dist/web/assets/ai-resource-editor-Y8clBPRv.js.gz +0 -0
  19. package/dist/web/assets/{ai-settings-section-DOckWvp4.js → ai-settings-section-BB8vscte.js} +1 -1
  20. package/dist/web/assets/{ai-settings-section-CETpSpYd.js → ai-settings-section-Ck8uN_Bw.js} +1 -1
  21. package/dist/web/assets/ai-settings-section-Ck8uN_Bw.js.br +0 -0
  22. package/dist/web/assets/ai-settings-section-Ck8uN_Bw.js.gz +0 -0
  23. package/dist/web/assets/android-tab-BIOUVVoW.js +2 -0
  24. package/dist/web/assets/android-tab-BIOUVVoW.js.br +0 -0
  25. package/dist/web/assets/android-tab-BIOUVVoW.js.gz +0 -0
  26. package/dist/web/assets/{api-settings-JIgKfcCx.js → api-settings-D1axn9vm.js} +1 -1
  27. package/dist/web/assets/{appearance-settings-section-BJUiLUb7.js → appearance-settings-section-Bv5_n9lH.js} +1 -1
  28. package/dist/web/assets/appearance-settings-section-Bv5_n9lH.js.br +0 -0
  29. package/dist/web/assets/appearance-settings-section-Bv5_n9lH.js.gz +0 -0
  30. package/dist/web/assets/{apps-panel-1VElRgV3.js → apps-panel-DXHF0QEv.js} +1 -1
  31. package/dist/web/assets/apps-panel-DXHF0QEv.js.br +0 -0
  32. package/dist/web/assets/apps-panel-DXHF0QEv.js.gz +0 -0
  33. package/dist/web/assets/{audio-preview-DYkyHu5X.js → audio-preview-DpjOKEm4.js} +1 -1
  34. package/dist/web/assets/audio-preview-DpjOKEm4.js.br +0 -0
  35. package/dist/web/assets/audio-preview-DpjOKEm4.js.gz +0 -0
  36. package/dist/web/assets/branch-review-tab-4dgMNadQ.js +1 -0
  37. package/dist/web/assets/branch-review-tab-4dgMNadQ.js.br +0 -0
  38. package/dist/web/assets/branch-review-tab-4dgMNadQ.js.gz +0 -0
  39. package/dist/web/assets/chat-tab-Bbe_77oJ.js +15 -0
  40. package/dist/web/assets/chat-tab-Bbe_77oJ.js.br +0 -0
  41. package/dist/web/assets/chat-tab-Bbe_77oJ.js.gz +0 -0
  42. package/dist/web/assets/chat-tab-D3i5O3dO.js +1 -0
  43. package/dist/web/assets/chat-tab-D3i5O3dO.js.br +0 -0
  44. package/dist/web/assets/chat-tab-D3i5O3dO.js.gz +0 -0
  45. package/dist/web/assets/code-editor-DXsIWoQ1.js +7 -0
  46. package/dist/web/assets/code-editor-DXsIWoQ1.js.br +0 -0
  47. package/dist/web/assets/code-editor-DXsIWoQ1.js.gz +0 -0
  48. package/dist/web/assets/{codex-reset-credit-button-Fr45FUxi.js → codex-reset-credit-button-DRhievFQ.js} +1 -1
  49. package/dist/web/assets/codex-reset-credit-button-DRhievFQ.js.br +0 -0
  50. package/dist/web/assets/codex-reset-credit-button-DRhievFQ.js.gz +0 -0
  51. package/dist/web/assets/{conflict-editor-B-wdjSXI.js → conflict-editor-D_RImo_V.js} +3 -3
  52. package/dist/web/assets/conflict-editor-D_RImo_V.js.br +0 -0
  53. package/dist/web/assets/conflict-editor-D_RImo_V.js.gz +0 -0
  54. package/dist/web/assets/{csv-preview-B6RodObB.js → csv-preview-BuFdfLo3.js} +1 -1
  55. package/dist/web/assets/csv-preview-BuFdfLo3.js.br +0 -0
  56. package/dist/web/assets/csv-preview-BuFdfLo3.js.gz +0 -0
  57. package/dist/web/assets/{database-viewer-CdPBUQPY.js → database-viewer-ChcbYn9E.js} +1 -1
  58. package/dist/web/assets/database-viewer-ChcbYn9E.js.br +0 -0
  59. package/dist/web/assets/database-viewer-ChcbYn9E.js.gz +0 -0
  60. package/dist/web/assets/{design-settings-section-BpCpaBwL.js → design-settings-section-B_xOf51h.js} +1 -1
  61. package/dist/web/assets/design-settings-section-B_xOf51h.js.br +0 -0
  62. package/dist/web/assets/design-settings-section-B_xOf51h.js.gz +0 -0
  63. package/dist/web/assets/{design-skill-suggestion-DkT2E2IV.js → design-skill-suggestion-BbEDg15Z.js} +1 -1
  64. package/dist/web/assets/design-skill-suggestion-BbEDg15Z.js.br +0 -0
  65. package/dist/web/assets/design-skill-suggestion-BbEDg15Z.js.gz +0 -0
  66. package/dist/web/assets/{design-tab-BZb9eemI.js → design-tab-BQSYJ4cS.js} +6 -6
  67. package/dist/web/assets/design-tab-BQSYJ4cS.js.br +0 -0
  68. package/dist/web/assets/design-tab-BQSYJ4cS.js.gz +0 -0
  69. package/dist/web/assets/{designs-sidebar-panel-CF4tp6j-.js → designs-sidebar-panel-AiPaYfSU.js} +1 -1
  70. package/dist/web/assets/designs-sidebar-panel-AiPaYfSU.js.br +0 -0
  71. package/dist/web/assets/designs-sidebar-panel-AiPaYfSU.js.gz +0 -0
  72. package/dist/web/assets/{diff-viewer-BqEqwSw6.js → diff-viewer-BZ6glGK-.js} +1 -1
  73. package/dist/web/assets/diff-viewer-BZ6glGK-.js.br +0 -0
  74. package/dist/web/assets/diff-viewer-BZ6glGK-.js.gz +0 -0
  75. package/dist/web/assets/{edit-diff-preview-DD4HayXY.js → edit-diff-preview-D-FNLOU2.js} +1 -1
  76. package/dist/web/assets/edit-diff-preview-D-FNLOU2.js.br +0 -0
  77. package/dist/web/assets/edit-diff-preview-D-FNLOU2.js.gz +0 -0
  78. package/dist/web/assets/{editor-language-service-CkA5sEdV.js → editor-language-service-4sbsdqBV.js} +1 -1
  79. package/dist/web/assets/editor-language-service-4sbsdqBV.js.br +0 -0
  80. package/dist/web/assets/editor-language-service-4sbsdqBV.js.gz +0 -0
  81. package/dist/web/assets/explorer-body-DbgrG1fe.js +2 -0
  82. package/dist/web/assets/explorer-body-DbgrG1fe.js.br +0 -0
  83. package/dist/web/assets/explorer-body-DbgrG1fe.js.gz +0 -0
  84. package/dist/web/assets/{explorer-window-content-CRJ2eMZ4.js → explorer-window-content-BCjWvHpp.js} +1 -1
  85. package/dist/web/assets/explorer-window-content-BCjWvHpp.js.br +0 -0
  86. package/dist/web/assets/explorer-window-content-BCjWvHpp.js.gz +0 -0
  87. package/dist/web/assets/{extension-manager-section-DwQTdCII.js → extension-manager-section-DiMF4RnT.js} +1 -1
  88. package/dist/web/assets/extension-manager-section-DiMF4RnT.js.br +0 -0
  89. package/dist/web/assets/extension-manager-section-DiMF4RnT.js.gz +0 -0
  90. package/dist/web/assets/{extension-webview-CMq-VTLu.js → extension-webview-Coti6Vfw.js} +1 -1
  91. package/dist/web/assets/extension-webview-Coti6Vfw.js.br +0 -0
  92. package/dist/web/assets/extension-webview-Coti6Vfw.js.gz +0 -0
  93. package/dist/web/assets/file-store-Cm7-pHux.js +1 -0
  94. package/dist/web/assets/file-store-Cm7-pHux.js.br +0 -0
  95. package/dist/web/assets/file-store-Cm7-pHux.js.gz +0 -0
  96. package/dist/web/assets/{files-settings-section-B2lcyh-6.js → files-settings-section-ZTVwOwx0.js} +1 -1
  97. package/dist/web/assets/files-settings-section-ZTVwOwx0.js.br +0 -0
  98. package/dist/web/assets/files-settings-section-ZTVwOwx0.js.gz +0 -0
  99. package/dist/web/assets/{general-settings-section-Doepm2sH.js → general-settings-section-B7Hjx_fb.js} +1 -1
  100. package/dist/web/assets/general-settings-section-B7Hjx_fb.js.br +0 -0
  101. package/dist/web/assets/general-settings-section-B7Hjx_fb.js.gz +0 -0
  102. package/dist/web/assets/{git-log-panel-CJakNMDc.js → git-log-panel-CbcJ_YEH.js} +1 -1
  103. package/dist/web/assets/git-log-panel-CbcJ_YEH.js.br +0 -0
  104. package/dist/web/assets/git-log-panel-CbcJ_YEH.js.gz +0 -0
  105. package/dist/web/assets/{glide-data-grid-C_SeOhSq.js → glide-data-grid-BfByGSf1.js} +3 -3
  106. package/dist/web/assets/glide-data-grid-BfByGSf1.js.br +0 -0
  107. package/dist/web/assets/glide-data-grid-BfByGSf1.js.gz +0 -0
  108. package/dist/web/assets/group-chat-tab-DCo9OP3D.js +1 -0
  109. package/dist/web/assets/group-chat-tab-DCo9OP3D.js.br +0 -0
  110. package/dist/web/assets/group-chat-tab-DCo9OP3D.js.gz +0 -0
  111. package/dist/web/assets/{image-preview-C1Frlx22.js → image-preview-Cjlps7Pt.js} +1 -1
  112. package/dist/web/assets/index-CyDLBddf.js +52 -0
  113. package/dist/web/assets/index-CyDLBddf.js.br +0 -0
  114. package/dist/web/assets/index-CyDLBddf.js.gz +0 -0
  115. package/dist/web/assets/index-Dh5apC3I.css +1 -0
  116. package/dist/web/assets/index-Dh5apC3I.css.br +0 -0
  117. package/dist/web/assets/index-Dh5apC3I.css.gz +0 -0
  118. package/dist/web/assets/{jira-watcher-section-BmkIGQFV.js → jira-watcher-section-Dz_-sUC2.js} +1 -1
  119. package/dist/web/assets/keybindings-store-BTq5Z7XP.js +1 -0
  120. package/dist/web/assets/{keyboard-shortcuts-section-BgZe3QDr.js → keyboard-shortcuts-section-Ba5NYwlG.js} +1 -1
  121. package/dist/web/assets/keyboard-shortcuts-section-Ba5NYwlG.js.br +0 -0
  122. package/dist/web/assets/keyboard-shortcuts-section-Ba5NYwlG.js.gz +0 -0
  123. package/dist/web/assets/{language-servers-section-CCMzgOaE.js → language-servers-section-DJJ7SFXo.js} +2 -2
  124. package/dist/web/assets/language-servers-section-DJJ7SFXo.js.br +0 -0
  125. package/dist/web/assets/language-servers-section-DJJ7SFXo.js.gz +0 -0
  126. package/dist/web/assets/{lsp-client-B10suuaZ.js → lsp-client-Dr3vQ1bb.js} +1 -1
  127. package/dist/web/assets/lsp-client-Dr3vQ1bb.js.br +0 -0
  128. package/dist/web/assets/lsp-client-Dr3vQ1bb.js.gz +0 -0
  129. package/dist/web/assets/{markdown-renderer-AHHFS3jc.js → markdown-renderer-CmnqFfTU.js} +3 -3
  130. package/dist/web/assets/markdown-renderer-CmnqFfTU.js.br +0 -0
  131. package/dist/web/assets/markdown-renderer-CmnqFfTU.js.gz +0 -0
  132. package/dist/web/assets/markdown-renderer-LJdVMbGl.js +1 -0
  133. package/dist/web/assets/{mobile-explorer-sheet-CqCsLWSr.js → mobile-explorer-sheet-BiIhZtJ2.js} +1 -1
  134. package/dist/web/assets/mobile-explorer-sheet-BiIhZtJ2.js.br +0 -0
  135. package/dist/web/assets/mobile-explorer-sheet-BiIhZtJ2.js.gz +0 -0
  136. package/dist/web/assets/new-design-dialog-DI_hxvFV.js +1 -0
  137. package/dist/web/assets/new-design-dialog-DI_hxvFV.js.br +0 -0
  138. package/dist/web/assets/new-design-dialog-DI_hxvFV.js.gz +0 -0
  139. package/dist/web/assets/notification-store-9lBSplOV.js +1 -0
  140. package/dist/web/assets/{notifications-settings-section-CXgzH_bK.js → notifications-settings-section-0s-2OKRv.js} +1 -1
  141. package/dist/web/assets/notifications-settings-section-0s-2OKRv.js.br +0 -0
  142. package/dist/web/assets/notifications-settings-section-0s-2OKRv.js.gz +0 -0
  143. package/dist/web/assets/{overview-panel-BYB8mgRE.js → overview-panel-3htxfZS2.js} +1 -1
  144. package/dist/web/assets/overview-panel-3htxfZS2.js.br +0 -0
  145. package/dist/web/assets/overview-panel-3htxfZS2.js.gz +0 -0
  146. package/dist/web/assets/panel-store-B6PoCwBA.js +1 -0
  147. package/dist/web/assets/panel-store-B6PoCwBA.js.br +0 -0
  148. package/dist/web/assets/panel-store-B6PoCwBA.js.gz +0 -0
  149. package/dist/web/assets/{performance-panel-Dpuq3g0W.js → performance-panel-V8gpnnf7.js} +2 -2
  150. package/dist/web/assets/performance-panel-V8gpnnf7.js.br +0 -0
  151. package/dist/web/assets/performance-panel-V8gpnnf7.js.gz +0 -0
  152. package/dist/web/assets/{postgres-viewer-DW0Gkmow.js → postgres-viewer-sWS-ogTC.js} +1 -1
  153. package/dist/web/assets/postgres-viewer-sWS-ogTC.js.br +0 -0
  154. package/dist/web/assets/postgres-viewer-sWS-ogTC.js.gz +0 -0
  155. package/dist/web/assets/{ppmbot-settings-section-DFtlGoW6.js → ppmbot-settings-section-CT8vvlzk.js} +1 -1
  156. package/dist/web/assets/ppmbot-settings-section-CT8vvlzk.js.br +0 -0
  157. package/dist/web/assets/ppmbot-settings-section-CT8vvlzk.js.gz +0 -0
  158. package/dist/web/assets/{pptx-export-CNyG32Xr.js → pptx-export-Ugjv0hso.js} +4 -4
  159. package/dist/web/assets/pptx-export-Ugjv0hso.js.br +0 -0
  160. package/dist/web/assets/pptx-export-Ugjv0hso.js.gz +0 -0
  161. package/dist/web/assets/{problems-panel-V5zqpuXq.js → problems-panel-Gcy79pSS.js} +1 -1
  162. package/dist/web/assets/problems-panel-Gcy79pSS.js.br +0 -0
  163. package/dist/web/assets/problems-panel-Gcy79pSS.js.gz +0 -0
  164. package/dist/web/assets/{process-row-format-CiJJSf_V.js → process-row-format-u6y7lOee.js} +1 -1
  165. package/dist/web/assets/process-table-BUTIp9Oy.js +1 -0
  166. package/dist/web/assets/process-table-BUTIp9Oy.js.br +0 -0
  167. package/dist/web/assets/process-table-BUTIp9Oy.js.gz +0 -0
  168. package/dist/web/assets/{proxy-settings-section-B7AVlTfY.js → proxy-settings-section-Buv_8y54.js} +1 -1
  169. package/dist/web/assets/proxy-settings-section-Buv_8y54.js.br +0 -0
  170. package/dist/web/assets/proxy-settings-section-Buv_8y54.js.gz +0 -0
  171. package/dist/web/assets/{query-audit-section-gHmRfWKr.js → query-audit-section-N2DQtgJx.js} +1 -1
  172. package/dist/web/assets/query-audit-section-N2DQtgJx.js.br +0 -0
  173. package/dist/web/assets/query-audit-section-N2DQtgJx.js.gz +0 -0
  174. package/dist/web/assets/remote-desktop-clipboard-notice-CRvJqmaz.js +1 -0
  175. package/dist/web/assets/remote-desktop-clipboard-notice-CRvJqmaz.js.br +0 -0
  176. package/dist/web/assets/remote-desktop-clipboard-notice-CRvJqmaz.js.gz +0 -0
  177. package/dist/web/assets/remote-desktop-mobile-sheet-BoYINLbJ.js +2 -0
  178. package/dist/web/assets/remote-desktop-mobile-sheet-BoYINLbJ.js.br +0 -0
  179. package/dist/web/assets/remote-desktop-mobile-sheet-BoYINLbJ.js.gz +0 -0
  180. package/dist/web/assets/remote-desktop-mobile-view-DPdP0yYu.js +1 -0
  181. package/dist/web/assets/remote-desktop-mobile-view-DPdP0yYu.js.br +0 -0
  182. package/dist/web/assets/remote-desktop-mobile-view-DPdP0yYu.js.gz +0 -0
  183. package/dist/web/assets/remote-desktop-readiness-gate-BeQ_Jrp7.js +1 -0
  184. package/dist/web/assets/remote-desktop-readiness-gate-BeQ_Jrp7.js.br +0 -0
  185. package/dist/web/assets/remote-desktop-readiness-gate-BeQ_Jrp7.js.gz +0 -0
  186. package/dist/web/assets/remote-desktop-settings-section-Bs4sHS3l.js +1 -0
  187. package/dist/web/assets/remote-desktop-settings-section-Bs4sHS3l.js.br +0 -0
  188. package/dist/web/assets/remote-desktop-settings-section-Bs4sHS3l.js.gz +0 -0
  189. package/dist/web/assets/remote-desktop-window-content-DDr__JxQ.js +1 -0
  190. package/dist/web/assets/remote-desktop-window-content-DDr__JxQ.js.br +0 -0
  191. package/dist/web/assets/remote-desktop-window-content-DDr__JxQ.js.gz +0 -0
  192. package/dist/web/assets/{run-in-terminal-DKBcEGVu.js → run-in-terminal-DCBNtYHQ.js} +1 -1
  193. package/dist/web/assets/{schedules-settings-section-sYDAsBMO.js → schedules-settings-section-_cHX2qOa.js} +1 -1
  194. package/dist/web/assets/schedules-settings-section-_cHX2qOa.js.br +0 -0
  195. package/dist/web/assets/schedules-settings-section-_cHX2qOa.js.gz +0 -0
  196. package/dist/web/assets/{send-to-chat-CCdOrKc7.js → send-to-chat-dNqNV7Y4.js} +1 -1
  197. package/dist/web/assets/send-to-chat-dNqNV7Y4.js.br +0 -0
  198. package/dist/web/assets/send-to-chat-dNqNV7Y4.js.gz +0 -0
  199. package/dist/web/assets/{services-panel-CKa2aP1d.js → services-panel-Dm54cnW3.js} +1 -1
  200. package/dist/web/assets/services-panel-Dm54cnW3.js.br +0 -0
  201. package/dist/web/assets/services-panel-Dm54cnW3.js.gz +0 -0
  202. package/dist/web/assets/settings-body-DRRQNaDA.js +1 -0
  203. package/dist/web/assets/settings-body-DRRQNaDA.js.br +0 -0
  204. package/dist/web/assets/settings-body-DRRQNaDA.js.gz +0 -0
  205. package/dist/web/assets/settings-section-content-DRaHfvm8.js +1 -0
  206. package/dist/web/assets/settings-section-content-U3mIx6hF.js +2 -0
  207. package/dist/web/assets/settings-section-content-U3mIx6hF.js.br +0 -0
  208. package/dist/web/assets/settings-section-content-U3mIx6hF.js.gz +0 -0
  209. package/dist/web/assets/{settings-store-DfuiLq_9.js → settings-store-Cyuo8ceh.js} +2 -2
  210. package/dist/web/assets/settings-store-Cyuo8ceh.js.br +0 -0
  211. package/dist/web/assets/settings-store-Cyuo8ceh.js.gz +0 -0
  212. package/dist/web/assets/settings-tab-CB_BhHTK.js +1 -0
  213. package/dist/web/assets/settings-window-content-CgZIMNX8.js +1 -0
  214. package/dist/web/assets/{slash-command-picker-CLevA_Gf.js → slash-command-picker-CQlIqMW0.js} +1 -1
  215. package/dist/web/assets/slash-command-picker-CQlIqMW0.js.br +0 -0
  216. package/dist/web/assets/slash-command-picker-CQlIqMW0.js.gz +0 -0
  217. package/dist/web/assets/{sql-query-editor-Db8b0jEr.js → sql-query-editor--Z4bcqd8.js} +1 -1
  218. package/dist/web/assets/sql-query-editor--Z4bcqd8.js.br +0 -0
  219. package/dist/web/assets/sql-query-editor--Z4bcqd8.js.gz +0 -0
  220. package/dist/web/assets/{sqlite-viewer-VzYLMqkD.js → sqlite-viewer-CSmX3qaj.js} +1 -1
  221. package/dist/web/assets/sqlite-viewer-CSmX3qaj.js.br +0 -0
  222. package/dist/web/assets/sqlite-viewer-CSmX3qaj.js.gz +0 -0
  223. package/dist/web/assets/{system-monitor-body-BR60W_mG.js → system-monitor-body-LannpU6H.js} +2 -2
  224. package/dist/web/assets/system-monitor-body-LannpU6H.js.br +0 -0
  225. package/dist/web/assets/system-monitor-body-LannpU6H.js.gz +0 -0
  226. package/dist/web/assets/system-monitor-tab-BfVucwEF.js +1 -0
  227. package/dist/web/assets/{system-monitor-window-content-BLw1QBXo.js → system-monitor-window-content-kLNW9Ln3.js} +1 -1
  228. package/dist/web/assets/{tab-host-window-content-BL61SB5m.js → tab-host-window-content-II_FtOYM.js} +1 -1
  229. package/dist/web/assets/tab-host-window-content-II_FtOYM.js.br +0 -0
  230. package/dist/web/assets/tab-host-window-content-II_FtOYM.js.gz +0 -0
  231. package/dist/web/assets/{tab-store-CbPh8CAX.js → tab-store-DCpc0g0Q.js} +1 -1
  232. package/dist/web/assets/{terminal-tab-B4y6at4h.js → terminal-tab-CTe2polt.js} +1 -1
  233. package/dist/web/assets/terminal-tab-CTe2polt.js.br +0 -0
  234. package/dist/web/assets/terminal-tab-CTe2polt.js.gz +0 -0
  235. package/dist/web/assets/{tool-cards-K0r_yxI0.js → tool-cards-rxLXbBSm.js} +5 -5
  236. package/dist/web/assets/tool-cards-rxLXbBSm.js.br +0 -0
  237. package/dist/web/assets/tool-cards-rxLXbBSm.js.gz +0 -0
  238. package/dist/web/assets/use-h264-canvas-decoder-BfO9QrlH.js +1 -0
  239. package/dist/web/assets/use-h264-canvas-decoder-BfO9QrlH.js.br +0 -0
  240. package/dist/web/assets/use-h264-canvas-decoder-BfO9QrlH.js.gz +0 -0
  241. package/dist/web/assets/{use-inline-blame-BK23icsE.js → use-inline-blame-CxZ_QWzS.js} +3 -3
  242. package/dist/web/assets/use-inline-blame-CxZ_QWzS.js.br +0 -0
  243. package/dist/web/assets/use-inline-blame-CxZ_QWzS.js.gz +0 -0
  244. package/dist/web/assets/{use-lsp-CRdCPCKW.js → use-lsp-gCNE5A9K.js} +1 -1
  245. package/dist/web/assets/use-lsp-gCNE5A9K.js.br +0 -0
  246. package/dist/web/assets/use-lsp-gCNE5A9K.js.gz +0 -0
  247. package/dist/web/assets/{use-monaco-theme-CpXT2uh3.js → use-monaco-theme-DvNGcCvz.js} +1 -1
  248. package/dist/web/assets/{use-process-kill-BWYX03KF.js → use-process-kill-Cbiu1mbY.js} +1 -1
  249. package/dist/web/assets/use-process-kill-Cbiu1mbY.js.br +0 -0
  250. package/dist/web/assets/use-process-kill-Cbiu1mbY.js.gz +0 -0
  251. package/dist/web/assets/{use-websocket-CiWQKhHr.js → use-websocket-AC7EG1LK.js} +1 -1
  252. package/dist/web/assets/video-preview-C2Y61WUE.js +1 -0
  253. package/dist/web/assets/video-preview-C2Y61WUE.js.br +0 -0
  254. package/dist/web/assets/video-preview-C2Y61WUE.js.gz +0 -0
  255. package/dist/web/assets/{voice-settings-section-Ceihggqf.js → voice-settings-section-DCXZeLYI.js} +1 -1
  256. package/dist/web/assets/voice-settings-section-DCXZeLYI.js.br +0 -0
  257. package/dist/web/assets/voice-settings-section-DCXZeLYI.js.gz +0 -0
  258. package/dist/web/assets/{window-pip-registry-mDgwpAwP.js → window-pip-registry-9Y9LQQYS.js} +1 -1
  259. package/dist/web/index.html +50 -7
  260. package/dist/web/index.html.br +0 -0
  261. package/dist/web/index.html.gz +0 -0
  262. package/dist/web/sw.js +1 -1
  263. package/dist/web/sw.js.br +0 -0
  264. package/dist/web/sw.js.gz +0 -0
  265. package/docs/architecture/android-emulator-phase0-report-260921.md +310 -0
  266. package/docs/architecture/android-emulator-phase2-report-260921.md +163 -0
  267. package/docs/architecture/android-emulator-phase3-report-260921.md +203 -0
  268. package/docs/architecture/android-emulator-phase4-report-260921.md +191 -0
  269. package/docs/architecture/android-emulator-plan-260920.md +376 -0
  270. package/docs/architecture/plugins-and-tracing.md +48 -7
  271. package/package.json +4 -2
  272. package/scripts/gen-android-proto.ts +37 -0
  273. package/scripts/release.sh +3 -1
  274. package/src/cli/commands/chat-cmd.ts +2 -2
  275. package/src/providers/claude-agent-sdk-query-options.ts +28 -0
  276. package/src/providers/claude-agent-sdk.ts +153 -71
  277. package/src/providers/claude-warm-spare.ts +176 -0
  278. package/src/providers/provider.interface.ts +1 -0
  279. package/src/server/index.ts +53 -0
  280. package/src/server/middleware/gzip-json.ts +13 -1
  281. package/src/server/routes/android.ts +616 -0
  282. package/src/server/routes/chat.ts +31 -0
  283. package/src/server/routes/files.ts +52 -5
  284. package/src/server/routes/remote-desktop.ts +95 -2
  285. package/src/server/routes/trace.ts +66 -0
  286. package/src/server/ws/android.ts +101 -0
  287. package/src/server/ws/chat.ts +12 -17
  288. package/src/server/ws/global.ts +13 -1
  289. package/src/server/ws/remote-desktop.ts +6 -1
  290. package/src/services/android/android-apk.ts +387 -0
  291. package/src/services/android/android-capabilities.ts +154 -0
  292. package/src/services/android/android-flag.ts +12 -0
  293. package/src/services/android/android-grpc.ts +124 -0
  294. package/src/services/android/android-input.ts +129 -0
  295. package/src/services/android/android-logcat.ts +169 -0
  296. package/src/services/android/android-nonce.ts +50 -0
  297. package/src/services/android/android-operations.ts +124 -0
  298. package/src/services/android/android-screenshot.ts +53 -0
  299. package/src/services/android/android-session.ts +510 -0
  300. package/src/services/android/android-video.ts +495 -0
  301. package/src/services/android/avd-list.ts +131 -0
  302. package/src/services/android/avd-manager.ts +397 -0
  303. package/src/services/android/device-registry.ts +223 -0
  304. package/src/services/android/emulator-discovery.ts +160 -0
  305. package/src/services/android/emulator-launcher.ts +290 -0
  306. package/src/services/android/proto/emulator-controller-descriptor.ts +10 -0
  307. package/src/services/android/proto/emulator_controller.proto +1791 -0
  308. package/src/services/android/sdk-discovery.ts +178 -0
  309. package/src/services/android/system-images.ts +124 -0
  310. package/src/services/chat.service.ts +81 -4
  311. package/src/services/config.service.ts +1 -1
  312. package/src/services/file-filter.service.ts +2 -61
  313. package/src/services/file-glob.ts +67 -0
  314. package/src/services/file-index/file-index-runner.ts +169 -0
  315. package/src/services/file-index/file-index-worker.ts +73 -0
  316. package/src/services/file-index/index-search.ts +152 -0
  317. package/src/services/file-index/index-walk.ts +109 -0
  318. package/src/services/file-list-index.service.ts +189 -70
  319. package/src/services/file-watcher/linux-inotify.ts +238 -0
  320. package/src/services/file-watcher/watch-tree.ts +37 -6
  321. package/src/services/file-watcher.service.ts +40 -8
  322. package/src/services/file.service.ts +17 -6
  323. package/src/services/group-chat/group-chat.service.ts +1 -1
  324. package/src/services/jira-debug-session.service.ts +1 -1
  325. package/src/services/ppmbot/ppmbot-delegation.ts +1 -1
  326. package/src/services/ppmbot/ppmbot-service.ts +1 -1
  327. package/src/services/project-content-search.service.ts +45 -11
  328. package/src/services/proxy-agent-turn.ts +7 -3
  329. package/src/services/remote-desktop/mediamtx-catalog.ts +89 -0
  330. package/src/services/remote-desktop/mediamtx-config.ts +91 -0
  331. package/src/services/remote-desktop/mediamtx-install.service.ts +191 -0
  332. package/src/services/remote-desktop/mediamtx-paths.ts +51 -0
  333. package/src/services/remote-desktop/mediamtx-process.ts +152 -0
  334. package/src/services/remote-desktop/remote-desktop-capture-wayland.ts +312 -0
  335. package/src/services/remote-desktop/remote-desktop-capture.ts +60 -4
  336. package/src/services/remote-desktop/remote-desktop-portal.ts +529 -0
  337. package/src/services/remote-desktop/remote-desktop-requirements.ts +56 -20
  338. package/src/services/remote-desktop/remote-desktop-session.ts +77 -3
  339. package/src/services/remote-desktop/remote-desktop-whep-registry.ts +36 -0
  340. package/src/services/scheduler-runner.ts +2 -3
  341. package/src/services/session-trace/browser-trace-ingest.ts +131 -0
  342. package/src/services/session-trace/session-trace-cleanup.ts +65 -0
  343. package/src/services/session-trace/session-trace-db.ts +89 -0
  344. package/src/services/session-trace/session-trace-store.ts +132 -0
  345. package/src/services/session-trace/trace-coalescer.ts +86 -0
  346. package/src/services/session-trace/trace-recorder.ts +304 -0
  347. package/src/services/session-trace/trace-writer.ts +83 -0
  348. package/src/shared/android-avd.ts +53 -0
  349. package/src/shared/android-protocol.ts +184 -0
  350. package/src/shared/file-index-limits.ts +17 -0
  351. package/src/shared/session-trace.ts +61 -0
  352. package/src/types/chat.ts +17 -0
  353. package/src/types/config.ts +27 -0
  354. package/src/types/project.ts +9 -0
  355. package/src/web/app.tsx +5 -0
  356. package/src/web/components/android/android-apk-upload.ts +75 -0
  357. package/src/web/components/android/android-avd-create-dialog.tsx +305 -0
  358. package/src/web/components/android/android-avd-destroy-dialog.tsx +151 -0
  359. package/src/web/components/android/android-clipboard-panel.tsx +94 -0
  360. package/src/web/components/android/android-controls.tsx +128 -0
  361. package/src/web/components/android/android-coords.ts +99 -0
  362. package/src/web/components/android/android-device-list.tsx +329 -0
  363. package/src/web/components/android/android-install-panel.tsx +280 -0
  364. package/src/web/components/android/android-log-filter.ts +75 -0
  365. package/src/web/components/android/android-logcat-panel.tsx +214 -0
  366. package/src/web/components/android/android-screenshot-download.ts +56 -0
  367. package/src/web/components/android/android-tab.tsx +66 -0
  368. package/src/web/components/android/android-tools-drawer.tsx +76 -0
  369. package/src/web/components/android/android-viewer.tsx +200 -0
  370. package/src/web/components/android/android-ws-url.ts +21 -0
  371. package/src/web/components/android/open-android.ts +21 -0
  372. package/src/web/components/android/use-android-available.ts +29 -0
  373. package/src/web/components/android/use-android-keyboard.ts +111 -0
  374. package/src/web/components/android/use-android-session.ts +277 -0
  375. package/src/web/components/android/use-android-touch.ts +123 -0
  376. package/src/web/components/chat/chat-tab.tsx +28 -2
  377. package/src/web/components/chat/message-input.tsx +3 -5
  378. package/src/web/components/editor/compare-picker.tsx +12 -2
  379. package/src/web/components/explorer/file-tree.tsx +6 -7
  380. package/src/web/components/explorer/use-tree-row-dnd.ts +0 -2
  381. package/src/web/components/floating-window/window-content-registry.ts +8 -8
  382. package/src/web/components/layout/command-palette.tsx +31 -9
  383. package/src/web/components/layout/mobile-drawer.tsx +8 -1
  384. package/src/web/components/layout/nav-section-rail.tsx +8 -1
  385. package/src/web/components/layout/reparenting-tab.tsx +2 -2
  386. package/src/web/components/layout/tab-content.tsx +5 -0
  387. package/src/web/components/layout/tab-pool.tsx +31 -20
  388. package/src/web/components/remote-desktop/use-h264-canvas-decoder.ts +23 -1
  389. package/src/web/components/remote-desktop/use-remote-desktop-connection.ts +64 -6
  390. package/src/web/components/remote-desktop/use-webrtc-canvas-video.ts +255 -0
  391. package/src/web/components/root-error-boundary.tsx +5 -0
  392. package/src/web/components/settings/remote-desktop-settings-section.tsx +176 -0
  393. package/src/web/components/settings/settings-categories.ts +3 -0
  394. package/src/web/components/settings/settings-section-content.tsx +25 -18
  395. package/src/web/hooks/use-chat-prewarm.ts +53 -0
  396. package/src/web/hooks/use-chat.ts +18 -4
  397. package/src/web/hooks/use-file-index-invalidation.ts +7 -5
  398. package/src/web/hooks/use-global-events.ts +12 -1
  399. package/src/web/hooks/use-remote-file-search.ts +39 -0
  400. package/src/web/hooks/use-ui-preload.ts +38 -0
  401. package/src/web/index.html +43 -0
  402. package/src/web/lib/chunk-recovery.ts +5 -1
  403. package/src/web/lib/device-id.ts +39 -0
  404. package/src/web/lib/feature-badges.ts +2 -0
  405. package/src/web/lib/lazy-with-preload.tsx +43 -0
  406. package/src/web/lib/preload-ui.ts +60 -0
  407. package/src/web/lib/score-file-search.ts +27 -14
  408. package/src/web/lib/tab-type-icons.ts +2 -1
  409. package/src/web/lib/trace-boot.ts +8 -0
  410. package/src/web/lib/trace-client.ts +516 -0
  411. package/src/web/main.tsx +12 -0
  412. package/src/web/stores/file-store.ts +124 -18
  413. package/src/web/stores/panel-utils.ts +3 -0
  414. package/src/web/stores/tab-store.ts +2 -1
  415. package/src/web/stores/window-panel-persistence.ts +3 -0
  416. package/vite.config.ts +3 -0
  417. package/dist/web/assets/accounts-settings-section-DEIm-W-D.js +0 -1
  418. package/dist/web/assets/accounts-settings-section-DEIm-W-D.js.br +0 -0
  419. package/dist/web/assets/accounts-settings-section-DEIm-W-D.js.gz +0 -0
  420. package/dist/web/assets/agent-session-sheet-CBMsnl_p.js.br +0 -0
  421. package/dist/web/assets/agent-session-sheet-CBMsnl_p.js.gz +0 -0
  422. package/dist/web/assets/agent-session-window-content-D9C9FA7E.js.br +0 -0
  423. package/dist/web/assets/agent-session-window-content-D9C9FA7E.js.gz +0 -0
  424. package/dist/web/assets/ai-resource-editor-DhUVgDEG.js.br +0 -0
  425. package/dist/web/assets/ai-resource-editor-DhUVgDEG.js.gz +0 -0
  426. package/dist/web/assets/ai-settings-section-CETpSpYd.js.br +0 -0
  427. package/dist/web/assets/ai-settings-section-CETpSpYd.js.gz +0 -0
  428. package/dist/web/assets/appearance-settings-section-BJUiLUb7.js.br +0 -0
  429. package/dist/web/assets/appearance-settings-section-BJUiLUb7.js.gz +0 -0
  430. package/dist/web/assets/apps-panel-1VElRgV3.js.br +0 -0
  431. package/dist/web/assets/apps-panel-1VElRgV3.js.gz +0 -0
  432. package/dist/web/assets/audio-preview-DYkyHu5X.js.br +0 -0
  433. package/dist/web/assets/audio-preview-DYkyHu5X.js.gz +0 -0
  434. package/dist/web/assets/branch-review-tab-DhTMSpSU.js +0 -1
  435. package/dist/web/assets/branch-review-tab-DhTMSpSU.js.br +0 -0
  436. package/dist/web/assets/branch-review-tab-DhTMSpSU.js.gz +0 -0
  437. package/dist/web/assets/chat-tab-Cg3BKugo.js +0 -15
  438. package/dist/web/assets/chat-tab-Cg3BKugo.js.br +0 -0
  439. package/dist/web/assets/chat-tab-Cg3BKugo.js.gz +0 -0
  440. package/dist/web/assets/chat-tab-tbUDK__Y.js +0 -1
  441. package/dist/web/assets/chat-tab-tbUDK__Y.js.br +0 -0
  442. package/dist/web/assets/chat-tab-tbUDK__Y.js.gz +0 -0
  443. package/dist/web/assets/code-editor-6ZiQehbr.js +0 -7
  444. package/dist/web/assets/code-editor-6ZiQehbr.js.br +0 -0
  445. package/dist/web/assets/code-editor-6ZiQehbr.js.gz +0 -0
  446. package/dist/web/assets/codex-reset-credit-button-Fr45FUxi.js.br +0 -0
  447. package/dist/web/assets/codex-reset-credit-button-Fr45FUxi.js.gz +0 -0
  448. package/dist/web/assets/conflict-editor-B-wdjSXI.js.br +0 -0
  449. package/dist/web/assets/conflict-editor-B-wdjSXI.js.gz +0 -0
  450. package/dist/web/assets/csv-preview-B6RodObB.js.br +0 -0
  451. package/dist/web/assets/csv-preview-B6RodObB.js.gz +0 -0
  452. package/dist/web/assets/database-viewer-CdPBUQPY.js.br +0 -0
  453. package/dist/web/assets/database-viewer-CdPBUQPY.js.gz +0 -0
  454. package/dist/web/assets/design-settings-section-BpCpaBwL.js.br +0 -0
  455. package/dist/web/assets/design-settings-section-BpCpaBwL.js.gz +0 -0
  456. package/dist/web/assets/design-skill-suggestion-DkT2E2IV.js.br +0 -0
  457. package/dist/web/assets/design-skill-suggestion-DkT2E2IV.js.gz +0 -0
  458. package/dist/web/assets/design-tab-BZb9eemI.js.br +0 -0
  459. package/dist/web/assets/design-tab-BZb9eemI.js.gz +0 -0
  460. package/dist/web/assets/designs-sidebar-panel-CF4tp6j-.js.br +0 -0
  461. package/dist/web/assets/designs-sidebar-panel-CF4tp6j-.js.gz +0 -0
  462. package/dist/web/assets/diff-viewer-BqEqwSw6.js.br +0 -0
  463. package/dist/web/assets/diff-viewer-BqEqwSw6.js.gz +0 -0
  464. package/dist/web/assets/edit-diff-preview-DD4HayXY.js.br +0 -0
  465. package/dist/web/assets/edit-diff-preview-DD4HayXY.js.gz +0 -0
  466. package/dist/web/assets/editor-language-service-CkA5sEdV.js.br +0 -0
  467. package/dist/web/assets/editor-language-service-CkA5sEdV.js.gz +0 -0
  468. package/dist/web/assets/explorer-body-Ds7oKuz7.js +0 -2
  469. package/dist/web/assets/explorer-body-Ds7oKuz7.js.br +0 -0
  470. package/dist/web/assets/explorer-body-Ds7oKuz7.js.gz +0 -0
  471. package/dist/web/assets/explorer-window-content-CRJ2eMZ4.js.br +0 -0
  472. package/dist/web/assets/explorer-window-content-CRJ2eMZ4.js.gz +0 -0
  473. package/dist/web/assets/extension-manager-section-DwQTdCII.js.br +0 -0
  474. package/dist/web/assets/extension-manager-section-DwQTdCII.js.gz +0 -0
  475. package/dist/web/assets/extension-webview-CMq-VTLu.js.br +0 -0
  476. package/dist/web/assets/extension-webview-CMq-VTLu.js.gz +0 -0
  477. package/dist/web/assets/file-store-DY1_0nzY.js +0 -1
  478. package/dist/web/assets/file-store-DY1_0nzY.js.br +0 -0
  479. package/dist/web/assets/file-store-DY1_0nzY.js.gz +0 -0
  480. package/dist/web/assets/files-settings-section-B2lcyh-6.js.br +0 -0
  481. package/dist/web/assets/files-settings-section-B2lcyh-6.js.gz +0 -0
  482. package/dist/web/assets/general-settings-section-Doepm2sH.js.br +0 -0
  483. package/dist/web/assets/general-settings-section-Doepm2sH.js.gz +0 -0
  484. package/dist/web/assets/git-log-panel-CJakNMDc.js.br +0 -0
  485. package/dist/web/assets/git-log-panel-CJakNMDc.js.gz +0 -0
  486. package/dist/web/assets/glide-data-grid-C_SeOhSq.js.br +0 -0
  487. package/dist/web/assets/glide-data-grid-C_SeOhSq.js.gz +0 -0
  488. package/dist/web/assets/group-chat-tab--by1xCQW.js +0 -1
  489. package/dist/web/assets/group-chat-tab--by1xCQW.js.br +0 -0
  490. package/dist/web/assets/group-chat-tab--by1xCQW.js.gz +0 -0
  491. package/dist/web/assets/index-DhWjx5iR.js +0 -52
  492. package/dist/web/assets/index-DhWjx5iR.js.br +0 -0
  493. package/dist/web/assets/index-DhWjx5iR.js.gz +0 -0
  494. package/dist/web/assets/index-oIvYSKTT.css +0 -1
  495. package/dist/web/assets/index-oIvYSKTT.css.br +0 -0
  496. package/dist/web/assets/index-oIvYSKTT.css.gz +0 -0
  497. package/dist/web/assets/keybindings-store-30LYZU0l.js +0 -1
  498. package/dist/web/assets/keyboard-shortcuts-section-BgZe3QDr.js.br +0 -0
  499. package/dist/web/assets/keyboard-shortcuts-section-BgZe3QDr.js.gz +0 -0
  500. package/dist/web/assets/language-servers-section-CCMzgOaE.js.br +0 -0
  501. package/dist/web/assets/language-servers-section-CCMzgOaE.js.gz +0 -0
  502. package/dist/web/assets/lsp-client-B10suuaZ.js.br +0 -0
  503. package/dist/web/assets/lsp-client-B10suuaZ.js.gz +0 -0
  504. package/dist/web/assets/markdown-renderer-AHHFS3jc.js.br +0 -0
  505. package/dist/web/assets/markdown-renderer-AHHFS3jc.js.gz +0 -0
  506. package/dist/web/assets/markdown-renderer-DxnXtBoj.js +0 -1
  507. package/dist/web/assets/mobile-explorer-sheet-CqCsLWSr.js.br +0 -0
  508. package/dist/web/assets/mobile-explorer-sheet-CqCsLWSr.js.gz +0 -0
  509. package/dist/web/assets/new-design-dialog-B9tgnQXq.js +0 -1
  510. package/dist/web/assets/new-design-dialog-B9tgnQXq.js.br +0 -0
  511. package/dist/web/assets/new-design-dialog-B9tgnQXq.js.gz +0 -0
  512. package/dist/web/assets/notification-store-HUVzO_bp.js +0 -1
  513. package/dist/web/assets/notifications-settings-section-CXgzH_bK.js.br +0 -0
  514. package/dist/web/assets/notifications-settings-section-CXgzH_bK.js.gz +0 -0
  515. package/dist/web/assets/overview-panel-BYB8mgRE.js.br +0 -0
  516. package/dist/web/assets/overview-panel-BYB8mgRE.js.gz +0 -0
  517. package/dist/web/assets/panel-store-CWHPmHwj.js +0 -1
  518. package/dist/web/assets/panel-store-CWHPmHwj.js.br +0 -0
  519. package/dist/web/assets/panel-store-CWHPmHwj.js.gz +0 -0
  520. package/dist/web/assets/performance-panel-Dpuq3g0W.js.br +0 -0
  521. package/dist/web/assets/performance-panel-Dpuq3g0W.js.gz +0 -0
  522. package/dist/web/assets/postgres-viewer-DW0Gkmow.js.br +0 -0
  523. package/dist/web/assets/postgres-viewer-DW0Gkmow.js.gz +0 -0
  524. package/dist/web/assets/ppmbot-settings-section-DFtlGoW6.js.br +0 -0
  525. package/dist/web/assets/ppmbot-settings-section-DFtlGoW6.js.gz +0 -0
  526. package/dist/web/assets/pptx-export-CNyG32Xr.js.br +0 -0
  527. package/dist/web/assets/pptx-export-CNyG32Xr.js.gz +0 -0
  528. package/dist/web/assets/problems-panel-V5zqpuXq.js.br +0 -0
  529. package/dist/web/assets/problems-panel-V5zqpuXq.js.gz +0 -0
  530. package/dist/web/assets/process-table-DAj46HIl.js +0 -1
  531. package/dist/web/assets/process-table-DAj46HIl.js.br +0 -0
  532. package/dist/web/assets/process-table-DAj46HIl.js.gz +0 -0
  533. package/dist/web/assets/proxy-settings-section-B7AVlTfY.js.br +0 -0
  534. package/dist/web/assets/proxy-settings-section-B7AVlTfY.js.gz +0 -0
  535. package/dist/web/assets/query-audit-section-gHmRfWKr.js.br +0 -0
  536. package/dist/web/assets/query-audit-section-gHmRfWKr.js.gz +0 -0
  537. package/dist/web/assets/remote-desktop-clipboard-notice-Cgoznh8L.js +0 -1
  538. package/dist/web/assets/remote-desktop-clipboard-notice-Cgoznh8L.js.br +0 -0
  539. package/dist/web/assets/remote-desktop-clipboard-notice-Cgoznh8L.js.gz +0 -0
  540. package/dist/web/assets/remote-desktop-mobile-sheet-X12QbPhO.js +0 -2
  541. package/dist/web/assets/remote-desktop-mobile-sheet-X12QbPhO.js.br +0 -0
  542. package/dist/web/assets/remote-desktop-mobile-sheet-X12QbPhO.js.gz +0 -0
  543. package/dist/web/assets/remote-desktop-mobile-view-DsSHawSK.js +0 -1
  544. package/dist/web/assets/remote-desktop-mobile-view-DsSHawSK.js.br +0 -0
  545. package/dist/web/assets/remote-desktop-mobile-view-DsSHawSK.js.gz +0 -0
  546. package/dist/web/assets/remote-desktop-readiness-gate-CHiQEd9Q.js +0 -1
  547. package/dist/web/assets/remote-desktop-readiness-gate-CHiQEd9Q.js.br +0 -0
  548. package/dist/web/assets/remote-desktop-readiness-gate-CHiQEd9Q.js.gz +0 -0
  549. package/dist/web/assets/remote-desktop-window-content-BnznIby3.js +0 -1
  550. package/dist/web/assets/remote-desktop-window-content-BnznIby3.js.br +0 -0
  551. package/dist/web/assets/remote-desktop-window-content-BnznIby3.js.gz +0 -0
  552. package/dist/web/assets/schedules-settings-section-sYDAsBMO.js.br +0 -0
  553. package/dist/web/assets/schedules-settings-section-sYDAsBMO.js.gz +0 -0
  554. package/dist/web/assets/send-to-chat-CCdOrKc7.js.br +0 -0
  555. package/dist/web/assets/send-to-chat-CCdOrKc7.js.gz +0 -0
  556. package/dist/web/assets/services-panel-CKa2aP1d.js.br +0 -0
  557. package/dist/web/assets/services-panel-CKa2aP1d.js.gz +0 -0
  558. package/dist/web/assets/settings-body-CEIF8lkl.js +0 -2
  559. package/dist/web/assets/settings-body-CEIF8lkl.js.br +0 -0
  560. package/dist/web/assets/settings-body-CEIF8lkl.js.gz +0 -0
  561. package/dist/web/assets/settings-store-DfuiLq_9.js.br +0 -0
  562. package/dist/web/assets/settings-store-DfuiLq_9.js.gz +0 -0
  563. package/dist/web/assets/settings-tab-Du5AUPni.js +0 -1
  564. package/dist/web/assets/settings-window-content-BTflfLez.js +0 -1
  565. package/dist/web/assets/slash-command-picker-CLevA_Gf.js.br +0 -0
  566. package/dist/web/assets/slash-command-picker-CLevA_Gf.js.gz +0 -0
  567. package/dist/web/assets/sql-query-editor-Db8b0jEr.js.br +0 -0
  568. package/dist/web/assets/sql-query-editor-Db8b0jEr.js.gz +0 -0
  569. package/dist/web/assets/sqlite-viewer-VzYLMqkD.js.br +0 -0
  570. package/dist/web/assets/sqlite-viewer-VzYLMqkD.js.gz +0 -0
  571. package/dist/web/assets/system-monitor-body-BR60W_mG.js.br +0 -0
  572. package/dist/web/assets/system-monitor-body-BR60W_mG.js.gz +0 -0
  573. package/dist/web/assets/system-monitor-tab-BYpantE2.js +0 -1
  574. package/dist/web/assets/tab-host-window-content-BL61SB5m.js.br +0 -0
  575. package/dist/web/assets/tab-host-window-content-BL61SB5m.js.gz +0 -0
  576. package/dist/web/assets/terminal-tab-B4y6at4h.js.br +0 -0
  577. package/dist/web/assets/terminal-tab-B4y6at4h.js.gz +0 -0
  578. package/dist/web/assets/tool-cards-K0r_yxI0.js.br +0 -0
  579. package/dist/web/assets/tool-cards-K0r_yxI0.js.gz +0 -0
  580. package/dist/web/assets/use-inline-blame-BK23icsE.js.br +0 -0
  581. package/dist/web/assets/use-inline-blame-BK23icsE.js.gz +0 -0
  582. package/dist/web/assets/use-lsp-CRdCPCKW.js.br +0 -0
  583. package/dist/web/assets/use-lsp-CRdCPCKW.js.gz +0 -0
  584. package/dist/web/assets/use-process-kill-BWYX03KF.js.br +0 -0
  585. package/dist/web/assets/use-process-kill-BWYX03KF.js.gz +0 -0
  586. package/dist/web/assets/video-preview-DugVkMB7.js +0 -1
  587. package/dist/web/assets/video-preview-DugVkMB7.js.br +0 -0
  588. package/dist/web/assets/video-preview-DugVkMB7.js.gz +0 -0
  589. package/dist/web/assets/voice-settings-section-Ceihggqf.js.br +0 -0
  590. package/dist/web/assets/voice-settings-section-Ceihggqf.js.gz +0 -0
  591. /package/dist/web/assets/{__vite-browser-external-B_XMEguz.js → __vite-browser-external-C5mXParC.js} +0 -0
  592. /package/dist/web/assets/{agent-session-sheet-store-DO682WSg.js → agent-session-sheet-store-_OeezCXB.js} +0 -0
  593. /package/dist/web/assets/{api-settings-CpO8SPk9.js → api-settings-Cc_CpfmC.js} +0 -0
  594. /package/dist/web/assets/{api-settings-CpO8SPk9.js.br → api-settings-Cc_CpfmC.js.br} +0 -0
  595. /package/dist/web/assets/{api-settings-CpO8SPk9.js.gz → api-settings-Cc_CpfmC.js.gz} +0 -0
  596. /package/dist/web/assets/{api-themes-CJipDH_l.js → api-themes-BtMBSjuM.js} +0 -0
  597. /package/dist/web/assets/{can-open-in-ppm-Bw_xiOXX.js → can-open-in-ppm-DeE2-kd5.js} +0 -0
  598. /package/dist/web/assets/{can-open-in-ppm-Bw_xiOXX.js.br → can-open-in-ppm-DeE2-kd5.js.br} +0 -0
  599. /package/dist/web/assets/{can-open-in-ppm-Bw_xiOXX.js.gz → can-open-in-ppm-DeE2-kd5.js.gz} +0 -0
  600. /package/dist/web/assets/{csv-parser-DcRWxcNV.js → csv-parser-CoxPdyLw.js} +0 -0
  601. /package/dist/web/assets/{csv-parser-DcRWxcNV.js.br → csv-parser-CoxPdyLw.js.br} +0 -0
  602. /package/dist/web/assets/{csv-parser-DcRWxcNV.js.gz → csv-parser-CoxPdyLw.js.gz} +0 -0
  603. /package/dist/web/assets/{design-export-client-CHffN3DM.js → design-export-client-BfdZB7xK.js} +0 -0
  604. /package/dist/web/assets/{design-export-client-CHffN3DM.js.br → design-export-client-BfdZB7xK.js.br} +0 -0
  605. /package/dist/web/assets/{design-export-client-CHffN3DM.js.gz → design-export-client-BfdZB7xK.js.gz} +0 -0
  606. /package/dist/web/assets/{design-responsive-dialog-DSCpv6O1.js → design-responsive-dialog-BiYy3ibS.js} +0 -0
  607. /package/dist/web/assets/{design-responsive-dialog-DSCpv6O1.js.br → design-responsive-dialog-BiYy3ibS.js.br} +0 -0
  608. /package/dist/web/assets/{design-responsive-dialog-DSCpv6O1.js.gz → design-responsive-dialog-BiYy3ibS.js.gz} +0 -0
  609. /package/dist/web/assets/{design-system-status-label-B_0I_3a8.js → design-system-status-label-ekiLyjKn.js} +0 -0
  610. /package/dist/web/assets/{docx-preview-B3eJyY2y.js → docx-preview-Dt6RQpfA.js} +0 -0
  611. /package/dist/web/assets/{docx-preview-B3eJyY2y.js.br → docx-preview-Dt6RQpfA.js.br} +0 -0
  612. /package/dist/web/assets/{docx-preview-B3eJyY2y.js.gz → docx-preview-Dt6RQpfA.js.gz} +0 -0
  613. /package/dist/web/assets/{editor-font-BNwo7zo5.js → editor-font-CBszn6vt.js} +0 -0
  614. /package/dist/web/assets/{format-bytes-CRZfxO2J.js → format-bytes-FQmpZ25A.js} +0 -0
  615. /package/dist/web/assets/{image-gallery-DC3gRAqk.js → image-gallery-B16hO6Ab.js} +0 -0
  616. /package/dist/web/assets/{image-preview-geometry-DN3DZQp0.js → image-preview-geometry-D4Ihz4UV.js} +0 -0
  617. /package/dist/web/assets/{lib-B9k5tn9s.js → lib-6APiJkU3.js} +0 -0
  618. /package/dist/web/assets/{lib-B9k5tn9s.js.br → lib-6APiJkU3.js.br} +0 -0
  619. /package/dist/web/assets/{lib-B9k5tn9s.js.gz → lib-6APiJkU3.js.gz} +0 -0
  620. /package/dist/web/assets/{libesm-5f7T4AHA.js → libesm-5wREk7JN.js} +0 -0
  621. /package/dist/web/assets/{libesm-5f7T4AHA.js.br → libesm-5wREk7JN.js.br} +0 -0
  622. /package/dist/web/assets/{libesm-5f7T4AHA.js.gz → libesm-5wREk7JN.js.gz} +0 -0
  623. /package/dist/web/assets/{media-url-CCGOIxzS.js → media-url-CrT-qulZ.js} +0 -0
  624. /package/dist/web/assets/{metric-chart-canvas-CjFbZuhk.js → metric-chart-canvas-D7f-LQ8x.js} +0 -0
  625. /package/dist/web/assets/{metric-chart-canvas-CjFbZuhk.js.br → metric-chart-canvas-D7f-LQ8x.js.br} +0 -0
  626. /package/dist/web/assets/{metric-chart-canvas-CjFbZuhk.js.gz → metric-chart-canvas-D7f-LQ8x.js.gz} +0 -0
  627. /package/dist/web/assets/{monaco-dotenv-language-Bwq3gQsi.js → monaco-dotenv-language-DbSss7YR.js} +0 -0
  628. /package/dist/web/assets/{onboarding-entry-CuH1Kqwu.js → onboarding-entry-CGFdGLLP.js} +0 -0
  629. /package/dist/web/assets/{onboarding-entry-CuH1Kqwu.js.br → onboarding-entry-CGFdGLLP.js.br} +0 -0
  630. /package/dist/web/assets/{onboarding-entry-CuH1Kqwu.js.gz → onboarding-entry-CGFdGLLP.js.gz} +0 -0
  631. /package/dist/web/assets/{pdf-preview-X8bdpfE9.js → pdf-preview-B8A4T-ES.js} +0 -0
  632. /package/dist/web/assets/{pdf-preview-X8bdpfE9.js.br → pdf-preview-B8A4T-ES.js.br} +0 -0
  633. /package/dist/web/assets/{pdf-preview-X8bdpfE9.js.gz → pdf-preview-B8A4T-ES.js.gz} +0 -0
  634. /package/dist/web/assets/{separator-DMu7VFQa.js → separator-dHj3Km--.js} +0 -0
  635. /package/dist/web/assets/{sortable-header-CipIP7aH.js → sortable-header-BOa8o7NJ.js} +0 -0
  636. /package/dist/web/assets/{sortable-header-CipIP7aH.js.br → sortable-header-BOa8o7NJ.js.br} +0 -0
  637. /package/dist/web/assets/{sortable-header-CipIP7aH.js.gz → sortable-header-BOa8o7NJ.js.gz} +0 -0
  638. /package/dist/web/assets/{use-blob-url-DjuRQWnR.js → use-blob-url-BC69n15x.js} +0 -0
  639. /package/dist/web/assets/{vendor-xterm-Be4L1NHd.js → vendor-xterm-CfasyE0m.js} +0 -0
  640. /package/dist/web/assets/{vendor-xterm-Be4L1NHd.js.br → vendor-xterm-CfasyE0m.js.br} +0 -0
  641. /package/dist/web/assets/{vendor-xterm-Be4L1NHd.js.gz → vendor-xterm-CfasyE0m.js.gz} +0 -0
@@ -0,0 +1,495 @@
1
+ /**
2
+ * gRPC RGB888 frames -> ffmpeg -> H.264 access units.
3
+ *
4
+ * This is ADR-B option 1, chosen on Phase 0 measurements: grpc-js under Bun sustained 40 fps and
5
+ * 297 MiB/s at full resolution with zero dropped frames, so the transport is not the bottleneck
6
+ * the plan feared. Six things here come from that work or from the proto, and each one is a
7
+ * silent bug if removed:
8
+ *
9
+ * 1. **No `vflip`, despite what the proto says.** `emulator_controller.proto` states the pixel
10
+ * buffer runs "left to right and bottom up", and both the plan and the Phase 0 report took
11
+ * that at its word. It is wrong on emulator 36.5.10: matched row by row against
12
+ * `adb exec-out screencap`, which is unambiguously top-down, the gRPC buffer agrees at a mean
13
+ * difference of **1.21** per pixel and disagrees at **37.15** when flipped. A `vflip` here
14
+ * would render every screen upside down, with no error from anything.
15
+ * 2. **The requested size is a bounding box, and the emulator decides the real one.** The proto
16
+ * is explicit: "The returned image will never exceed the given width, but can be less",
17
+ * scaled "while maintaining the aspect ratio of the device". Phase 0 asked 720x1600 and got
18
+ * exactly that only because it matched the device's aspect. Every frame carries the real
19
+ * `format.width/height/rotation` as **output** fields, so those are read rather than assumed
20
+ * — which is also what makes rotation work without guessing what it does to the geometry.
21
+ * 3. **The box is square, and it is opened exactly once.** A portrait box would collapse a
22
+ * landscape guest into a sliver (1600x720 inside a 720x1600 box is 720x324), so the ceiling
23
+ * is applied to the *long* edge by asking for `maxHeight x maxHeight` — rotation-invariant,
24
+ * so the picture keeps its resolution when the guest turns. It is opened at the **highest**
25
+ * rung's ceiling and never reopened, because a cancelled `streamScreenshot` cannot be
26
+ * replaced: measured on emulator 36.5.10, every stream opened after one has been cancelled
27
+ * delivers exactly **one** frame and then nothing, on a fresh channel as well as the same
28
+ * one. So a rung change is an ffmpeg `scale` and a respawned encoder, never a new stream.
29
+ * 4. **Pacing uses an accumulating deadline.** "Has one interval passed since the last frame I
30
+ * took" turns a 40 fps source into 20 fps at a 30 fps target — measured 242 frames over 12 s
31
+ * instead of 360. `nextDue = max(now, nextDue + interval)` is what gives the right count.
32
+ * 5. **A frame once started is written whole.** rawvideo carries no frame header, so a short
33
+ * write desynchronises the decoder permanently. Only a frame not yet begun may be dropped.
34
+ * 6. **`flush()` is what actually writes.** Bun's stdin is a FileSink whose `write()` only
35
+ * buffers and returns the byte count immediately, so a write loop alone proves nothing
36
+ * reached ffmpeg.
37
+ */
38
+ import { AccessUnitAssembler, type AccessUnit } from "../remote-desktop/access-unit-assembler.ts";
39
+ import { avc1CodecString } from "../remote-desktop/avc1-codec-string.ts";
40
+ import { getFfmpegCapabilities, workingEncoders } from "../media-transcode/ffmpeg-capabilities.ts";
41
+ import { connectToEmulator, type EmulatorChannel } from "./android-grpc.ts";
42
+ import type { RunningEmulator } from "./emulator-discovery.ts";
43
+ import { ANDROID_QUALITY_PRESETS, type AndroidQuality } from "../../shared/android-protocol.ts";
44
+
45
+ // The H.264 helpers above live under remote-desktop/ and are imported rather than moved: they
46
+ // are pure, dependency-free bitstream utilities, and relocating them would touch nine files
47
+ // across a working, well-tested feature for no behavioural gain (plan §8 says extract them only
48
+ // "if needed"). If a third consumer ever appears, that is the moment to give them a neutral home.
49
+
50
+ /** The picture the client actually receives: the encoded size, which may be a scaled copy of
51
+ * what the emulator sends, plus the guest's own orientation. */
52
+ export interface FrameGeometry {
53
+ width: number;
54
+ height: number;
55
+ rotation: 0 | 90 | 180 | 270;
56
+ }
57
+
58
+ /** The one box ever requested from the emulator: the highest rung's ceiling, so every rung is
59
+ * reachable by scaling down and the stream never has to be reopened. */
60
+ export const STREAM_BOX = Math.max(...Object.values(ANDROID_QUALITY_PRESETS).map((p) => p.maxHeight));
61
+
62
+ /**
63
+ * How long after the last real change the pipeline keeps sending at the rung's full rate, and
64
+ * what it falls back to once the guest screen has gone still.
65
+ *
66
+ * Repeating a motionless picture is not free: measured on this host with the screen untouched,
67
+ * one viewer held ffmpeg at **55% of one core** — the rgb24 frame is rescaled and uploaded every
68
+ * time, whether or not a pixel moved. It cannot simply stop, though; see `lastFrame`. Ten frames
69
+ * a second is the compromise, measured on the same screen: **11.7 frames/s into ffmpeg for 23%
70
+ * of a core**, while the client still receives 24.9 access units a second, because the rawvideo
71
+ * demuxer declares 25 fps and ffmpeg's default CFR sync pads the output back up to it — and a
72
+ * duplicate costs the encoder almost nothing, since the expensive half happens per *input*
73
+ * frame. A viewer joining a still device therefore still has a keyframe inside a second.
74
+ */
75
+ const IDLE_AFTER_MS = 1000;
76
+ const IDLE_FPS = 10;
77
+
78
+ /** Even dimensions only: H.264 chroma is subsampled and an odd size is rejected outright. */
79
+ function evenDown(n: number): number {
80
+ return Math.max(2, n % 2 === 0 ? n : n - 1);
81
+ }
82
+
83
+ /**
84
+ * Fit a frame under a rung's ceiling, applied to the **long** edge so a rotation does not change
85
+ * how much detail a rung buys. Never upscales: a rung above the source is the source.
86
+ */
87
+ export function fitLongEdge(width: number, height: number, ceiling: number): { width: number; height: number } {
88
+ const longEdge = Math.max(width, height);
89
+ if (longEdge <= ceiling) return { width: evenDown(width), height: evenDown(height) };
90
+ const scale = ceiling / longEdge;
91
+ return { width: evenDown(Math.round(width * scale)), height: evenDown(Math.round(height * scale)) };
92
+ }
93
+
94
+ export interface VideoPipelineOptions {
95
+ emulator: RunningEmulator;
96
+ quality: AndroidQuality;
97
+ onAccessUnit: (au: AccessUnit, ptsMs: number) => void;
98
+ /** Called once at startup and again whenever the emulator changes the frame size or rotation. */
99
+ onGeometry: (geometry: FrameGeometry) => void;
100
+ onError: (message: string) => void;
101
+ }
102
+
103
+ export interface RunningPipeline {
104
+ readonly geometry: FrameGeometry;
105
+ /** ffmpeg's encoder name, for display. Not what the browser's decoder is configured with. */
106
+ readonly encoder: string;
107
+ /** The current bitstream's WebCodecs codec string, null until its first keyframe. */
108
+ codecString(): string | null;
109
+ readonly fps: number;
110
+ stop(): Promise<void>;
111
+ /** Frames stop being requested while hidden; the emulator and the session stay up. */
112
+ setPaused(paused: boolean): void;
113
+ /** Switch rung. Respawns the encoder; the emulator's stream is never touched. */
114
+ setQuality(quality: AndroidQuality): Promise<void>;
115
+ /** Frames the emulator sent, and frames that survived pacing and reached ffmpeg. The gap
116
+ * between the two IS the pacing, so a stall shows up as which of them stopped moving. */
117
+ stats(): { sourceFrames: number; fedFrames: number };
118
+ }
119
+
120
+ /** The proto's `SkinRotation` in the terms the viewer uses. */
121
+ const ROTATION_DEGREES: Record<string, 0 | 90 | 180 | 270> = {
122
+ PORTRAIT: 0, LANDSCAPE: 90, REVERSE_PORTRAIT: 180, REVERSE_LANDSCAPE: 270,
123
+ };
124
+
125
+ function rotationOf(format: any): 0 | 90 | 180 | 270 {
126
+ const r = format?.rotation?.rotation;
127
+ if (typeof r === "string" && r in ROTATION_DEGREES) return ROTATION_DEGREES[r]!;
128
+ return 0;
129
+ }
130
+
131
+ /**
132
+ * ffmpeg arguments per encoder. VAAPI is not a drop-in `-c:v` swap: `-vaapi_device` must precede
133
+ * `-i` because it opens during input setup, the upload must be in the SAME `-vf` chain (a second
134
+ * `-vf` replaces the first), and `-pix_fmt yuv420p` has to go or the hardware frame context is
135
+ * rejected. Measured on an Intel UHD 770: VAAPI held 33% of a core against libx264's 64-80%.
136
+ */
137
+ export function encoderArgs(opts: {
138
+ encoder: string; width: number; height: number; fps: number; bitrate: string;
139
+ /** Encoded size, when it differs from the source. Omit for no scaling. */
140
+ outWidth?: number; outHeight?: number;
141
+ vaapiDevice?: string;
142
+ }): string[] {
143
+ const { encoder, width, height, fps, bitrate } = opts;
144
+ const input = [
145
+ "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", `${width}x${height}`,
146
+ // `-framerate` is **not** redundant beside the wallclock timestamps, and leaving it out costs
147
+ // one frame in six. The rawvideo demuxer assumes **25 fps** when nothing says otherwise, and
148
+ // ffmpeg's default CFR sync then conforms the output to that rate — measured, feeding a real
149
+ // 30 fps produced 151 access units from 180 frames, dropped unevenly, which is exactly what
150
+ // dragging on the device looked like. Nothing warns: the picture is correct, there is simply
151
+ // less of it. With the rate declared, 180 frames in gave 180 access units out.
152
+ "-framerate", String(fps),
153
+ "-use_wallclock_as_timestamps", "1", "-i", "pipe:0",
154
+ ];
155
+ // An exact size rather than a filter expression: the pipeline always knows both, because a
156
+ // geometry change and a rung change each respawn the encoder. `scale=w:h` with numbers cannot
157
+ // be got subtly wrong the way `min(ih,H)` can when the frame is landscape.
158
+ const scale = opts.outWidth && opts.outHeight && (opts.outWidth !== width || opts.outHeight !== height)
159
+ ? `scale=${opts.outWidth}:${opts.outHeight},` : "";
160
+ // Half-second GOP: it bounds every resync freeze and every late joiner's wait for a first
161
+ // decodable frame, and on a rate-capped encode costs no measurable bitrate — the same finding
162
+ // the Remote Desktop ladder is built on.
163
+ const gop = ["-g", String(Math.max(1, Math.round(fps / 2)))];
164
+ const out = ["-f", "h264", "pipe:1"];
165
+
166
+ if (encoder === "h264_vaapi") {
167
+ return [
168
+ "-vaapi_device", opts.vaapiDevice ?? "/dev/dri/renderD128",
169
+ ...input,
170
+ "-vf", `${scale}format=nv12,hwupload`,
171
+ "-c:v", "h264_vaapi", "-b:v", bitrate, ...gop, ...out,
172
+ ];
173
+ }
174
+ if (encoder === "h264_qsv") {
175
+ return [...input, "-vf", `${scale}format=nv12`,
176
+ "-c:v", "h264_qsv", "-b:v", bitrate, "-low_power", "1", ...gop, ...out];
177
+ }
178
+ if (encoder === "h264_nvenc") {
179
+ return [...input, "-vf", `${scale}format=yuv420p`,
180
+ "-c:v", "h264_nvenc", "-b:v", bitrate, "-preset", "p1", "-tune", "ll", ...gop, ...out];
181
+ }
182
+ return [...input, "-vf", `${scale}format=yuv420p`,
183
+ "-c:v", encoder, "-b:v", bitrate, "-preset", "veryfast", "-tune", "zerolatency", ...gop, ...out];
184
+ }
185
+
186
+ /** Hardware first, software last — but only among encoders that really encode on this host. */
187
+ export function pickEncoder(available: string[]): string {
188
+ for (const preferred of ["h264_vaapi", "h264_qsv", "h264_nvenc", "h264_amf"]) {
189
+ if (available.includes(preferred)) return preferred;
190
+ }
191
+ return available[0] ?? "libx264";
192
+ }
193
+
194
+ /** Named so `ReturnType<typeof spawnFfmpeg>` keeps the narrowed `stdin`/`stdout` types. Written
195
+ * inline the union comes back — `stdout` widens to `number | ReadableStream` and `.getReader()`
196
+ * stops type-checking, the same trap `host-info/spawn-runner.ts` records in CLAUDE.md. */
197
+ function spawnFfmpeg(ffmpeg: string, args: string[]) {
198
+ return Bun.spawn([ffmpeg, "-hide_banner", "-loglevel", "error", ...args], {
199
+ stdin: "pipe", stdout: "pipe", stderr: "pipe", windowsHide: true,
200
+ });
201
+ }
202
+
203
+ /** One ffmpeg, locked to one frame size. A geometry change means a new one of these. */
204
+ /**
205
+ * Hand one whole raw frame to the encoder's stdin.
206
+ *
207
+ * One `write()`, then `flush()` — **never** a partial-write loop over the return value.
208
+ * `FileSink.write()` takes the whole chunk and answers a number that is *not* how much of this
209
+ * chunk it consumed: measured against `cat` with one real 4,924,800-byte frame, it returned
210
+ * 475072 eleven times while having already buffered everything, so a loop doing `off += n`
211
+ * re-sent the tail ten more times and `cat` received **28,043,840 bytes — 5.7x the frame**.
212
+ * ffmpeg's rawvideo input is then offset for good: the *first* picture decodes perfectly and
213
+ * every one after it is drawn rolled, with no error from ffmpeg, from the sink, or from
214
+ * anything else. It is its own function only so a test can measure the bytes that come out the
215
+ * other end (`tests/unit/android/android-video-frame-write.test.ts`) — a frame counter cannot
216
+ * see this bug.
217
+ */
218
+ export async function writeFrameToSink(
219
+ sink: { write: (c: Uint8Array) => unknown; flush: () => unknown },
220
+ frame: Uint8Array,
221
+ ): Promise<void> {
222
+ sink.write(frame);
223
+ await sink.flush();
224
+ }
225
+
226
+ class Encoder {
227
+ private readonly proc: ReturnType<typeof spawnFfmpeg>;
228
+ private readonly assembler = new AccessUnitAssembler();
229
+ private readonly sink: { write(c: Uint8Array): number | Promise<number>; flush(): number | Promise<number> };
230
+ private inFlight: Promise<void> = Promise.resolve();
231
+ private pending: Buffer | null = null;
232
+ private stopped = false;
233
+ readonly outPump: Promise<void>;
234
+
235
+ constructor(
236
+ ffmpeg: string,
237
+ args: string[],
238
+ private readonly onAccessUnit: (au: AccessUnit) => void,
239
+ private readonly onError: (m: string) => void,
240
+ ) {
241
+ this.proc = spawnFfmpeg(ffmpeg, args);
242
+ this.sink = this.proc.stdin as never;
243
+
244
+ // Manual pull loop, NOT `for await (...) { break }`. Breaking out of a for-await cancels the
245
+ // underlying reader, which segfaults Bun 1.3.x on Windows the moment a consumer goes away —
246
+ // the hazard `remote-desktop-capture.ts` documents at length and guards the same way.
247
+ // Teardown is `kill()` only; the pending read then resolves `done: true` and this exits.
248
+ const outReader = this.proc.stdout.getReader();
249
+ this.outPump = (async () => {
250
+ for (;;) {
251
+ const { done, value } = await outReader.read();
252
+ if (done) return;
253
+ if (!value) continue;
254
+ for (const au of this.assembler.push(value)) this.onAccessUnit(au);
255
+ }
256
+ })().catch((e) => {
257
+ if (!this.stopped) this.onError(`encoder output failed: ${(e as Error).message}`);
258
+ });
259
+
260
+ const errReader = this.proc.stderr.getReader();
261
+ void (async () => {
262
+ for (;;) {
263
+ const { done, value } = await errReader.read();
264
+ if (done) return;
265
+ if (!value) continue;
266
+ const text = new TextDecoder().decode(value).trim();
267
+ if (text && !this.stopped) this.onError(`encoder: ${text.split("\n").slice(-1)[0]}`);
268
+ }
269
+ })().catch(() => { /* closed with the process */ });
270
+ }
271
+
272
+ /** The `avc1.PPCCLL` string this encoder's real bitstream implies, null before its first
273
+ * keyframe. Derived, never guessed: `encoderArgs` sets no profile or level. */
274
+ codecString(): string | null {
275
+ const sps = this.assembler.cachedSps();
276
+ return sps ? avc1CodecString(sps) : null;
277
+ }
278
+
279
+ /** Queue a frame. Only a frame that has not started being written may be superseded. */
280
+ feed(buf: Buffer): void {
281
+ if (this.stopped) return;
282
+ this.pending = buf;
283
+ this.inFlight = this.inFlight.then(async () => {
284
+ while (this.pending && !this.stopped) {
285
+ const frame = this.pending;
286
+ this.pending = null;
287
+ try {
288
+ await writeFrameToSink(this.sink, frame);
289
+ } catch { /* the encoder is gone; stop() reports it */ }
290
+ }
291
+ });
292
+ }
293
+
294
+ async stop(): Promise<void> {
295
+ if (this.stopped) return;
296
+ this.stopped = true;
297
+ // Kill FIRST, then wait. Waiting on the write chain before killing makes teardown hostage to
298
+ // an ffmpeg that has stopped reading its stdin: `flush()` would never resolve and the process
299
+ // would outlive PPM. Killing first can only turn a pending write into an EPIPE, which the
300
+ // write loop already swallows.
301
+ try { this.proc.stdin.end(); } catch { /* already closed */ }
302
+ try { this.proc.kill(); } catch { /* already exited */ }
303
+ await Promise.race([this.inFlight.catch(() => {}), Bun.sleep(1000)]);
304
+ await Promise.race([this.outPump, Bun.sleep(1000)]);
305
+ await Promise.race([this.proc.exited, Bun.sleep(2000)]);
306
+ }
307
+ }
308
+
309
+ export async function startVideoPipeline(opts: VideoPipelineOptions): Promise<RunningPipeline> {
310
+ // The binary the capability check found, not a bare "ffmpeg": that check also looks outside
311
+ // PATH (Homebrew's prefixes, WinGet's links), which a service-started PPM often lacks.
312
+ const found = (await getFfmpegCapabilities()).ffmpeg;
313
+ if (!found) throw new Error("ffmpeg is not installed on this host");
314
+ const ffmpeg: string = found;
315
+ const encoderName = pickEncoder(await workingEncoders());
316
+ const channel: EmulatorChannel = connectToEmulator(opts.emulator);
317
+ const startedAt = performance.now();
318
+
319
+ let quality = opts.quality;
320
+ let preset = ANDROID_QUALITY_PRESETS[quality];
321
+ /** What the emulator is sending, before any scaling of ours. */
322
+ let source: FrameGeometry | null = null;
323
+ /** What the client receives. */
324
+ let geometry: FrameGeometry | null = null;
325
+ let encoder: Encoder | null = null;
326
+ let stopped = false;
327
+ let paused = false;
328
+ let nextDue = 0;
329
+ let sourceFrames = 0;
330
+ let fedFrames = 0;
331
+ /**
332
+ * The newest frame the emulator has sent, kept so the pacer can send it again.
333
+ *
334
+ * `streamScreenshot` is **change-driven**: the emulator sends a frame when the screen changes
335
+ * and then nothing at all — measured, a device sitting on its launcher produced *2 frames in
336
+ * 15 seconds*. Feeding ffmpeg only on arrival therefore stops the H.264 stream completely on a
337
+ * still screen, so a viewer that joins then waits on "Waiting for the first frame…" until
338
+ * something moves in the guest, and one already watching cannot tell a frozen picture from a
339
+ * dropped connection. The fix is to keep feeding the last frame: the GOP is half a second, so
340
+ * a viewer joining a motionless device has a keyframe within 500 ms.
341
+ */
342
+ let lastFrame: Buffer | null = null;
343
+ let lastChangeAt = 0;
344
+ let pacer: ReturnType<typeof setInterval> | null = null;
345
+
346
+ const emitAu = (au: AccessUnit) => opts.onAccessUnit(au, performance.now() - startedAt);
347
+
348
+ /** Tear down the encoder and start one for the current source and rung. */
349
+ function respawnEncoder(src: FrameGeometry): FrameGeometry {
350
+ const out = fitLongEdge(src.width, src.height, preset.maxHeight);
351
+ const previous = encoder;
352
+ encoder = null;
353
+ try {
354
+ encoder = new Encoder(
355
+ ffmpeg,
356
+ encoderArgs({
357
+ encoder: encoderName,
358
+ width: src.width, height: src.height,
359
+ outWidth: out.width, outHeight: out.height,
360
+ fps: preset.fps, bitrate: preset.bitrate,
361
+ }),
362
+ emitAu,
363
+ (m) => { if (!stopped) opts.onError(m); },
364
+ );
365
+ } catch (e) {
366
+ // Bun.spawn throws synchronously on a binary it cannot run, and this runs inside the gRPC
367
+ // stream's frame handler, where a throw would escape as an uncaught exception.
368
+ if (!stopped) opts.onError(`could not start ffmpeg: ${(e as Error).message}`);
369
+ }
370
+ nextDue = 0;
371
+ startPacer(); // the rung may have changed the frame rate
372
+ // Drained in the background: its remaining access units describe a picture the client has
373
+ // already been told is gone.
374
+ if (previous) void previous.stop();
375
+ return { width: out.width, height: out.height, rotation: src.rotation };
376
+ }
377
+
378
+ /** Hand the newest frame to the encoder if the rung's cadence allows one now. */
379
+ function feedNow(): void {
380
+ if (stopped || paused || !lastFrame) return;
381
+ const now = performance.now();
382
+ if (nextDue === 0) nextDue = now;
383
+ if (now < nextDue) return;
384
+ const fps = now - lastChangeAt > IDLE_AFTER_MS ? Math.min(IDLE_FPS, preset.fps) : preset.fps;
385
+ nextDue = Math.max(now, nextDue + 1000 / fps);
386
+ fedFrames++;
387
+ encoder?.feed(lastFrame);
388
+ }
389
+
390
+ /**
391
+ * Ticks at *half* the frame interval on purpose. A tick landing just before the next frame is
392
+ * due does nothing, so a tick exactly one period long would halve the rate whenever the two
393
+ * fall out of phase; at half a period the cadence holds.
394
+ */
395
+ function startPacer(): void {
396
+ if (pacer) clearInterval(pacer);
397
+ pacer = setInterval(feedNow, Math.max(5, Math.round(500 / preset.fps)));
398
+ }
399
+
400
+ function stopPacer(): void {
401
+ if (pacer) clearInterval(pacer);
402
+ pacer = null;
403
+ }
404
+
405
+ function onFrame(img: any): void {
406
+ if (stopped || paused) return;
407
+ const buf: Buffer | undefined = img?.image;
408
+ const width = Number(img?.format?.width ?? 0);
409
+ const height = Number(img?.format?.height ?? 0);
410
+ if (!buf || width <= 0 || height <= 0) return;
411
+ // The declared size and the payload must agree before ffmpeg ever sees it: a short buffer
412
+ // desynchronises the raw stream permanently, with no error from anything.
413
+ if (buf.length !== width * height * 3) return;
414
+ sourceFrames++;
415
+
416
+ const rotation = rotationOf(img.format);
417
+ if (!source || source.width !== width || source.height !== height || source.rotation !== rotation) {
418
+ source = { width, height, rotation };
419
+ geometry = respawnEncoder(source);
420
+ opts.onGeometry(geometry);
421
+ }
422
+
423
+ const now = performance.now();
424
+ // Coming out of idle, the next slot was scheduled at the *idle* rate and may be up to 100 ms
425
+ // away — that delay would land on the first frame after a touch, which is the one that must
426
+ // not be late. Re-arm so it goes out now.
427
+ if (now - lastChangeAt > IDLE_AFTER_MS) nextDue = 0;
428
+ lastChangeAt = now;
429
+ lastFrame = buf;
430
+ feedNow(); // a change goes out at once rather than waiting for the next tick
431
+ }
432
+
433
+ // Opened once for the life of the session. See rule 3 in this file's header: a cancelled
434
+ // screenshot stream cannot be replaced on this emulator, so nothing here ever cancels it
435
+ // except `stop()`.
436
+ const call = channel.client.streamScreenshot(
437
+ { format: "RGB888", width: STREAM_BOX, height: STREAM_BOX },
438
+ channel.metadata,
439
+ );
440
+ call.on("data", onFrame);
441
+ call.on("error", (e: any) => {
442
+ // code 1 is CANCELLED, which is how stop() ends the stream.
443
+ if (!stopped && e?.code !== 1) {
444
+ opts.onError(`frame stream failed: ${e?.details ?? e?.message ?? e}`);
445
+ }
446
+ });
447
+
448
+ // Resolve only once the emulator has told us what it is actually sending — the session needs a
449
+ // real geometry to put in its `ready` message, and there is nothing to guess it from.
450
+ await new Promise<void>((resolve, reject) => {
451
+ const deadline = setTimeout(() => reject(new Error("no frame from the emulator within 10s")), 10_000);
452
+ const poll = setInterval(() => {
453
+ if (geometry) { clearInterval(poll); clearTimeout(deadline); resolve(); }
454
+ if (stopped) { clearInterval(poll); clearTimeout(deadline); reject(new Error("stopped")); }
455
+ }, 25);
456
+ }).catch(async (e) => {
457
+ stopped = true;
458
+ stopPacer();
459
+ try { call.cancel(); } catch { /* already gone */ }
460
+ channel.close();
461
+ await encoder?.stop();
462
+ throw e;
463
+ });
464
+
465
+ return {
466
+ get geometry() { return geometry!; },
467
+ encoder: encoderName,
468
+ codecString: () => encoder?.codecString() ?? null,
469
+ stats: () => ({ sourceFrames, fedFrames }),
470
+ get fps() { return preset.fps; },
471
+
472
+ setPaused(p: boolean) {
473
+ paused = p;
474
+ // Resuming re-arms the pacer so the first frame back is not judged against a stale deadline.
475
+ if (!p) nextDue = 0;
476
+ },
477
+
478
+ async setQuality(next: AndroidQuality) {
479
+ if (next === quality || stopped || !source) return;
480
+ quality = next;
481
+ preset = ANDROID_QUALITY_PRESETS[next];
482
+ geometry = respawnEncoder(source);
483
+ opts.onGeometry(geometry);
484
+ },
485
+
486
+ async stop() {
487
+ if (stopped) return;
488
+ stopped = true;
489
+ stopPacer();
490
+ try { call.cancel(); } catch { /* already gone */ }
491
+ channel.close();
492
+ await encoder?.stop();
493
+ },
494
+ };
495
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The AVDs this host has, and enough of each one's config to say whether it will behave.
3
+ *
4
+ * Read straight from the AVD home rather than by shelling out to `emulator -list-avds`: that
5
+ * command answers names only, and the setup screen needs the API level, ABI and — the one that
6
+ * actually bites — whether the hardware keyboard is on.
7
+ */
8
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
9
+ import { createHash } from "node:crypto";
10
+ import { join, resolve } from "node:path";
11
+
12
+ export interface AvdSummary {
13
+ /** Opaque, stable id issued by PPM. Derived from the canonical config path so it survives a
14
+ * rename of the display name and never collides across two AVD homes. */
15
+ avdId: string;
16
+ name: string;
17
+ /** Directory holding config.ini, i.e. `<avd home>/<name>.avd`. */
18
+ dir: string;
19
+ apiLevel: number | null;
20
+ abi: string | null;
21
+ tag: string | null;
22
+ deviceProfile: string | null;
23
+ displayWidth: number | null;
24
+ displayHeight: number | null;
25
+ displayDensity: number | null;
26
+ /**
27
+ * `hw.keyboard`. When this is off, gRPC touch works perfectly, `sendKey` returns OK, and not
28
+ * one keystroke reaches the guest — no error anywhere. `avdmanager` creates AVDs with it off;
29
+ * Android Studio creates them with it on. Measured in Phase 0; surfaced so the UI can warn.
30
+ */
31
+ hardwareKeyboard: boolean;
32
+ /** True when another process holds this AVD's lock — usually Android Studio running it. */
33
+ lockedByAnotherProcess: boolean;
34
+ }
35
+
36
+ function parseIni(text: string): Record<string, string> {
37
+ const out: Record<string, string> = {};
38
+ for (const line of text.split("\n")) {
39
+ const t = line.trim();
40
+ if (!t || t.startsWith("#")) continue;
41
+ const eq = t.indexOf("=");
42
+ if (eq > 0) out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();
43
+ }
44
+ return out;
45
+ }
46
+
47
+ function num(v: string | undefined): number | null {
48
+ if (v === undefined) return null;
49
+ const n = Number(v);
50
+ return Number.isFinite(n) ? n : null;
51
+ }
52
+
53
+ /** Stable opaque id for an AVD, from its canonical directory. */
54
+ export function avdIdFor(avdDir: string): string {
55
+ return createHash("sha256").update(resolve(avdDir)).digest("hex").slice(0, 16);
56
+ }
57
+
58
+ /**
59
+ * API level out of `image.sysdir.1`, e.g. `system-images/android-35/google_apis.../x86_64/`.
60
+ * `target=android-35` is not always present in configs written by newer tooling.
61
+ */
62
+ function apiLevelFrom(cfg: Record<string, string>): number | null {
63
+ const target = cfg["target"];
64
+ const fromTarget = target ? /android-(\d+)/.exec(target)?.[1] : undefined;
65
+ if (fromTarget) return Number(fromTarget);
66
+ const sysdir = cfg["image.sysdir.1"];
67
+ const fromSysdir = sysdir ? /android-(\d+)/.exec(sysdir)?.[1] : undefined;
68
+ return fromSysdir ? Number(fromSysdir) : null;
69
+ }
70
+
71
+ /**
72
+ * Is this AVD locked by a process that is actually alive?
73
+ *
74
+ * Presence of a lock file is **not** the answer, measured: `multiinstance.lock` is a zero-byte
75
+ * marker that survives a clean shutdown, so treating it as a lock refuses every start after the
76
+ * first. The real lock is `hardware-qemu.ini.lock`, which holds the emulator's pid (verified
77
+ * against the live process), so the pid is what gets checked. A lock naming a dead pid is stale
78
+ * and ignored.
79
+ *
80
+ * PPM only ever *reports* this — plan §5 is explicit that a lock is never deleted to force a
81
+ * start, which is what would corrupt an image Android Studio is using.
82
+ */
83
+ function isLocked(avdDir: string): boolean {
84
+ for (const lock of ["hardware-qemu.ini.lock", "userdata-qemu.img.lock"]) {
85
+ const path = join(avdDir, lock);
86
+ if (!existsSync(path)) continue;
87
+ let raw: string;
88
+ try { raw = readFileSync(path, "utf8"); } catch { continue; }
89
+ // The emulator writes the pid **NUL-terminated** — measured, `hardware-qemu.ini.lock` is the
90
+ // 7 bytes `519772\0`. A NUL is not whitespace, so `trim()` keeps it and `Number()` answers
91
+ // NaN: read that way, every running emulator reports itself unlocked and the "open in
92
+ // Android Studio" warning never appears. Take the leading digits instead.
93
+ const digits = /^\s*(\d+)/.exec(raw);
94
+ if (!digits) continue;
95
+ const pid = Number(digits[1]);
96
+ if (!Number.isInteger(pid) || pid <= 0) continue;
97
+ try { process.kill(pid, 0); return true; } catch { /* stale lock, owner is gone */ }
98
+ }
99
+ return false;
100
+ }
101
+
102
+ export function listAvds(avdHome: string): AvdSummary[] {
103
+ let entries: string[];
104
+ try { entries = readdirSync(avdHome); } catch { return []; }
105
+
106
+ const avds: AvdSummary[] = [];
107
+ for (const entry of entries) {
108
+ if (!entry.endsWith(".avd")) continue;
109
+ const dir = join(avdHome, entry);
110
+ const configPath = join(dir, "config.ini");
111
+ let cfg: Record<string, string>;
112
+ try { cfg = parseIni(readFileSync(configPath, "utf8")); } catch { continue; }
113
+
114
+ const name = cfg["AvdId"] ?? entry.slice(0, -".avd".length);
115
+ avds.push({
116
+ avdId: avdIdFor(dir),
117
+ name,
118
+ dir,
119
+ apiLevel: apiLevelFrom(cfg),
120
+ abi: cfg["abi.type"] ?? null,
121
+ tag: cfg["tag.id"] ?? null,
122
+ deviceProfile: cfg["hw.device.name"] ?? null,
123
+ displayWidth: num(cfg["hw.lcd.width"]),
124
+ displayHeight: num(cfg["hw.lcd.height"]),
125
+ displayDensity: num(cfg["hw.lcd.density"]),
126
+ hardwareKeyboard: (cfg["hw.keyboard"] ?? "no").trim() === "yes",
127
+ lockedByAnotherProcess: isLocked(dir),
128
+ });
129
+ }
130
+ return avds.sort((a, b) => a.name.localeCompare(b.name));
131
+ }