@hienlh/ppm 0.21.0 → 0.22.0

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 (606) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/CLAUDE.md +29 -4
  3. package/THIRD-PARTY-NOTICES.md +1 -1
  4. package/assets/skills/ppm/SKILL.md +1 -1
  5. package/assets/skills/ppm/references/http-api.md +1 -1
  6. package/bun.lock +16 -1
  7. package/dist/web/assets/accounts-settings-section-BVr9-FZJ.js +1 -0
  8. package/dist/web/assets/{ai-resource-editor-F09pf8Hd.js → ai-resource-editor-CdDpOwjh.js} +1 -1
  9. package/dist/web/assets/ai-settings-section-B1hQyjAd.js +1 -0
  10. package/dist/web/assets/ai-settings-section-X2UOfSaS.js +1 -0
  11. package/dist/web/assets/{angular-html-DkR4iCth.js → angular-html-G66FNlyn.js} +1 -1
  12. package/dist/web/assets/{angular-ts-DcyL4Chx.js → angular-ts-B-PnUMyt.js} +1 -1
  13. package/dist/web/assets/{api-client-bfB7cXRM.js → api-client-C1ZwRL4Y.js} +1 -1
  14. package/dist/web/assets/api-client-pQL53cDQ.js +1 -0
  15. package/dist/web/assets/{api-settings-BcNjZIvG.js → api-settings-_7Q-ULxp.js} +1 -1
  16. package/dist/web/assets/{api-themes-DBfyVUtX.js → api-themes-DRAERo4Q.js} +1 -1
  17. package/dist/web/assets/{apl-BVlrVtjt.js → apl-BmDqqwE8.js} +1 -1
  18. package/dist/web/assets/{appearance-settings-section-whgnq3ss.js → appearance-settings-section-DLoPK0LD.js} +1 -1
  19. package/dist/web/assets/architecture-PBZL5I3N-Dx6nBEKT.js +1 -0
  20. package/dist/web/assets/{astro-MjJlozLj.js → astro-BfwMNfxr.js} +1 -1
  21. package/dist/web/assets/{audio-preview-Bc54GCAV.js → audio-preview-DsYvgKqj.js} +1 -1
  22. package/dist/web/assets/{blade-DnJWi1fz.js → blade-BsFV4pnN.js} +1 -1
  23. package/dist/web/assets/branch-review-tab-G84Ojzae.js +1 -0
  24. package/dist/web/assets/{bsl-DCmJxSIz.js → bsl-Cw9Outng.js} +1 -1
  25. package/dist/web/assets/{button-VW4QGKCn.js → button-B9CTrsEf.js} +1 -1
  26. package/dist/web/assets/{cairo-C8dDenub.js → cairo-DOK5Wqnd.js} +1 -1
  27. package/dist/web/assets/chat-tab-DQJfjm8E.js +15 -0
  28. package/dist/web/assets/{cobol-DPx_nifl.js → cobol-DRaRy4FK.js} +1 -1
  29. package/dist/web/assets/code-editor-D04iUPsZ.js +7 -0
  30. package/dist/web/assets/{codex-daily-guard-CfybHv_c.js → codex-daily-guard-BSccLRGT.js} +1 -1
  31. package/dist/web/assets/{coffee-DvFzqhgN.js → coffee-DvXuBmEJ.js} +1 -1
  32. package/dist/web/assets/{conflict-editor-DenQUOT2.js → conflict-editor-C4ay676e.js} +2 -2
  33. package/dist/web/assets/context-menu-C6pIsf83.js +1 -0
  34. package/dist/web/assets/{cpp-BLJN0umi.js → cpp-zCmm8rKA.js} +1 -1
  35. package/dist/web/assets/{crystal-BXR-9fGw.js → crystal-CdQx6lB6.js} +1 -1
  36. package/dist/web/assets/{csv-preview-CClrJOuR.js → csv-preview-DbwEmG7o.js} +1 -1
  37. package/dist/web/assets/{data-grid-overlay-editor-D4fhcMfT.js → data-grid-overlay-editor-6xUCeLHh.js} +1 -1
  38. package/dist/web/assets/{database-viewer-DItjm8wB.js → database-viewer-S6LsALjX.js} +1 -1
  39. package/dist/web/assets/{dialog-DQTn55Mt.js → dialog-B84_DJQ8.js} +1 -1
  40. package/dist/web/assets/diff-viewer-3oBA7vgQ.js +4 -0
  41. package/dist/web/assets/{docx-preview-DYBkiUaE.js → docx-preview-CXuk-emm.js} +1 -1
  42. package/dist/web/assets/{edge-rWOlCmXT.js → edge-E8QLt7oi.js} +1 -1
  43. package/dist/web/assets/{edit-diff-preview-COtmo54G.js → edit-diff-preview-BGGGecVw.js} +1 -1
  44. package/dist/web/assets/{editor-language-service-DxkZ51E5.js → editor-language-service-DJmaKVY0.js} +1 -1
  45. package/dist/web/assets/{elixir-E7Y82okW.js → elixir-DPb_-72P.js} +1 -1
  46. package/dist/web/assets/{elm-DTPGbc-w.js → elm-BMzJY5b0.js} +1 -1
  47. package/dist/web/assets/{erb-DHqLHrE6.js → erb-AABQNtMK.js} +1 -1
  48. package/dist/web/assets/{erlang-Byg0nNiV.js → erlang-CEIKOkfP.js} +1 -1
  49. package/dist/web/assets/{esm-D_DmOM7n.js → esm-B9RVMpwV.js} +1 -1
  50. package/dist/web/assets/explorer-body-BQ0jyAGv.js +2 -0
  51. package/dist/web/assets/explorer-window-content-jdkWt34J.js +1 -0
  52. package/dist/web/assets/{extension-manager-section-BhwB6nqr.js → extension-manager-section-DG1guILi.js} +1 -1
  53. package/dist/web/assets/{extension-webview-DI4VL2tJ.js → extension-webview-BGSDUbdK.js} +1 -1
  54. package/dist/web/assets/file-icons-BXdvggK2.js +1 -0
  55. package/dist/web/assets/file-icons.generated-BtBErEwb.css +1 -0
  56. package/dist/web/assets/file-store-ojgMXgAw.js +1 -0
  57. package/dist/web/assets/files-settings-section-Gnyd7han.js +1 -0
  58. package/dist/web/assets/{format-file-meta-DOpzJJhS.js → format-file-meta-C77h04cZ.js} +1 -1
  59. package/dist/web/assets/{fortran-fixed-form-CY1n0Xf6.js → fortran-fixed-form-CrXvLHyi.js} +1 -1
  60. package/dist/web/assets/{fsharp-_vEDSCJz.js → fsharp-DCI4qAz_.js} +1 -1
  61. package/dist/web/assets/{gdresource-B6XT0wAC.js → gdresource-Vb2kO1MG.js} +1 -1
  62. package/dist/web/assets/{general-settings-section-BcxwiZ0X.js → general-settings-section-V6Khh15V.js} +1 -1
  63. package/dist/web/assets/{git-commit-Wk9ovq59.js → git-commit-BMI_OWn-.js} +1 -1
  64. package/dist/web/assets/git-log-panel-Bi513qjT.js +1 -0
  65. package/dist/web/assets/{git-rebase-DSarGPvK.js → git-rebase-D-d_2iqZ.js} +1 -1
  66. package/dist/web/assets/{git-repo-store-Dy69YCHN.js → git-repo-store-88Z8v-pF.js} +1 -1
  67. package/dist/web/assets/gitGraph-HDMCJU4V-C6qRruHI.js +1 -0
  68. package/dist/web/assets/{glide-data-grid-BvgAqSUr.js → glide-data-grid-DnQwYSmQ.js} +7 -7
  69. package/dist/web/assets/{glimmer-js-CkYI35_P.js → glimmer-js-BmNNiR0Z.js} +1 -1
  70. package/dist/web/assets/{glimmer-ts-BqmIZcds.js → glimmer-ts-DQE2Ufha.js} +1 -1
  71. package/dist/web/assets/{glsl-Be-yBCF5.js → glsl-BcIDsxRJ.js} +1 -1
  72. package/dist/web/assets/{graphql-a5DrENHY.js → graphql-DGKSLmGH.js} +1 -1
  73. package/dist/web/assets/group-chat-tab-DpczdwyV.js +1 -0
  74. package/dist/web/assets/{hack-BLAg20aD.js → hack-YiC8bEIb.js} +1 -1
  75. package/dist/web/assets/{haml-Dj6bGOQH.js → haml-DjblxAsz.js} +1 -1
  76. package/dist/web/assets/{handlebars-dmEa36Oh.js → handlebars-BQepOXaU.js} +1 -1
  77. package/dist/web/assets/{html-yacZYzSE.js → html-Cayj_2GP.js} +1 -1
  78. package/dist/web/assets/{html-derivative-lLi3-RNT.js → html-derivative-Weq79bL6.js} +1 -1
  79. package/dist/web/assets/{http-1KaxcE4e.js → http-BeLfdGdz.js} +1 -1
  80. package/dist/web/assets/{hurl-ST-dEEZR.js → hurl-D9rypCLK.js} +1 -1
  81. package/dist/web/assets/{hxml-CpDtuq__.js → hxml-clG8mmSH.js} +1 -1
  82. package/dist/web/assets/{image-preview-CJVxL0uH.js → image-preview-CzCc4_Do.js} +1 -1
  83. package/dist/web/assets/image-preview-geometry-9lgeLYB3.js +1 -0
  84. package/dist/web/assets/index-BH99TIE0.css +1 -0
  85. package/dist/web/assets/index-DU68xk06.js +52 -0
  86. package/dist/web/assets/info-3K5VOQVL-BOSpdwlz.js +1 -0
  87. package/dist/web/assets/{jinja-Cjw-evAn.js → jinja-BShBWfs5.js} +1 -1
  88. package/dist/web/assets/{jira-watcher-section-DIC1yIV3.js → jira-watcher-section-f2HUbGDY.js} +1 -1
  89. package/dist/web/assets/{jison-DfATMYoY.js → jison-dN2Nf_Dr.js} +1 -1
  90. package/dist/web/assets/{julia-DDCaxiqT.js → julia-5U0vR-uJ.js} +1 -1
  91. package/dist/web/assets/{just-CJL529u6.js → just-BMppVRy6.js} +1 -1
  92. package/dist/web/assets/keybindings-store-DCRsnKg5.js +1 -0
  93. package/dist/web/assets/{keyboard-shortcuts-section-BW03P3AF.js → keyboard-shortcuts-section-BpvVEw_Y.js} +1 -1
  94. package/dist/web/assets/{latex-Zc4EpLpW.js → latex-DSSOKjis.js} +1 -1
  95. package/dist/web/assets/{liquid-B_6KMNYS.js → liquid-C24dKoyI.js} +1 -1
  96. package/dist/web/assets/{lua-BtS2idK7.js → lua-DNOsnyhn.js} +1 -1
  97. package/dist/web/assets/markdown-renderer-Bcaf9_bA.js +1 -0
  98. package/dist/web/assets/markdown-renderer-DGJp8HC3.js +3 -0
  99. package/dist/web/assets/{marko-Wk6LBnck.js → marko-26MHO_Q9.js} +1 -1
  100. package/dist/web/assets/{mdc-cRaXBrzL.js → mdc-Dhw9mGrC.js} +1 -1
  101. package/dist/web/assets/{media-url-DL8OCGGg.js → media-url-yfqHpPz0.js} +1 -1
  102. package/dist/web/assets/{mobile-bottom-sheet-CoUauvLF.js → mobile-bottom-sheet-C3TniEP-.js} +1 -1
  103. package/dist/web/assets/mobile-explorer-sheet-Bol3XAO1.js +1 -0
  104. package/dist/web/assets/{nextflow-Bos2wvDV.js → nextflow-xH4oJTex.js} +1 -1
  105. package/dist/web/assets/{nginx-DMlOwzdr.js → nginx-CMz4w0M8.js} +1 -1
  106. package/dist/web/assets/{nim-D2dZQA88.js → nim-D8FnIYsx.js} +1 -1
  107. package/dist/web/assets/notification-store-BXt54VIn.js +1 -0
  108. package/dist/web/assets/{notifications-settings-section-DPUROMw-.js → notifications-settings-section-DqlPDfNX.js} +1 -1
  109. package/dist/web/assets/{number-overlay-editor-DZ2i_Bnb.js → number-overlay-editor-CRGwULhR.js} +1 -1
  110. package/dist/web/assets/{overview-panel-R146OIom.js → overview-panel-Di-3aV9p.js} +1 -1
  111. package/dist/web/assets/packet-RMMSAZCW-BGIE1QdU.js +1 -0
  112. package/dist/web/assets/panel-store-CboGJAwh.js +1 -0
  113. package/dist/web/assets/{pdf-preview-BWUPGLp6.js → pdf-preview-CmBI6E7d.js} +1 -1
  114. package/dist/web/assets/{perl-C5-Z6fPn.js → perl-D9iNWYwb.js} +1 -1
  115. package/dist/web/assets/{php-BXNNhkMH.js → php-Bw3Qo0Zv.js} +1 -1
  116. package/dist/web/assets/pie-UPGHQEXC-Dg-QTbXX.js +1 -0
  117. package/dist/web/assets/{postgres-viewer-CjOvaruz.js → postgres-viewer-8ZAqEALv.js} +3 -3
  118. package/dist/web/assets/{ppmbot-settings-section-DrvWh8l8.js → ppmbot-settings-section-DKJUA3Kq.js} +1 -1
  119. package/dist/web/assets/problems-panel-CFcoUx8h.js +1 -0
  120. package/dist/web/assets/{process-table-BRMS1MrX.js → process-table-EKr2dqG6.js} +1 -1
  121. package/dist/web/assets/project-framework-store-B70Q3dNM.js +1 -0
  122. package/dist/web/assets/project-store-B87hLRSk.js +1 -0
  123. package/dist/web/assets/{proxy-settings-section-Bv0Ny-Yy.js → proxy-settings-section-B7qRVl8w.js} +1 -1
  124. package/dist/web/assets/{pug-C1zf1yWy.js → pug-fT5JC6Vm.js} +1 -1
  125. package/dist/web/assets/{qml-OW58jGpD.js → qml-P_wEDB1w.js} +1 -1
  126. package/dist/web/assets/{query-audit-section-BcHJZcVC.js → query-audit-section-By6qyqdB.js} +1 -1
  127. package/dist/web/assets/radar-KQ55EAFF-DZ6IVu2s.js +1 -0
  128. package/dist/web/assets/{razor-DzjSaobM.js → razor-Drrt2_Ph.js} +1 -1
  129. package/dist/web/assets/remote-desktop-clipboard-notice-0WDYsyCA.js +1 -0
  130. package/dist/web/assets/remote-desktop-mobile-sheet-COYDg16H.js +2 -0
  131. package/dist/web/assets/remote-desktop-mobile-view-2DqSuS2U.js +1 -0
  132. package/dist/web/assets/{remote-desktop-readiness-gate-BTzss2wX.js → remote-desktop-readiness-gate-Dm_qMJMH.js} +1 -1
  133. package/dist/web/assets/remote-desktop-window-content-UodhsF7W.js +1 -0
  134. package/dist/web/assets/{rst-RfuihOUW.js → rst-Dr2mW4-h.js} +1 -1
  135. package/dist/web/assets/{ruby-CNO3pli1.js → ruby-C6EpUsTY.js} +1 -1
  136. package/dist/web/assets/{run-in-terminal-Cb_hVU8x.js → run-in-terminal-CdefEe_R.js} +1 -1
  137. package/dist/web/assets/{sas-DZY5BtXx.js → sas-D8ZEeP6v.js} +1 -1
  138. package/dist/web/assets/{schedules-settings-section-DO8xAOVj.js → schedules-settings-section-bG2lYfGv.js} +1 -1
  139. package/dist/web/assets/{scroll-area-LtNV8g5o.js → scroll-area-BzeiOd-B.js} +1 -1
  140. package/dist/web/assets/{scss--4g-REcN.js → scss-ITL7jQ1f.js} +1 -1
  141. package/dist/web/assets/{send-to-chat-Cnpru1M0.js → send-to-chat-F207yH3u.js} +1 -1
  142. package/dist/web/assets/{separator-Cds1yxy8.js → separator-TRMsJUYe.js} +1 -1
  143. package/dist/web/assets/settings-body-C06bFarv.js +2 -0
  144. package/dist/web/assets/settings-store-krC6bc79.js +2 -0
  145. package/dist/web/assets/settings-tab-34RSaASd.js +1 -0
  146. package/dist/web/assets/settings-window-content-Dr_2lmHH.js +1 -0
  147. package/dist/web/assets/{shaderlab-Cu4sBiVn.js → shaderlab-mW-IIlhk.js} +1 -1
  148. package/dist/web/assets/{shellsession-DWzJfEv7.js → shellsession-DhNl5STt.js} +1 -1
  149. package/dist/web/assets/{soy-BnqE-5Ff.js → soy-SdY0Qjtr.js} +1 -1
  150. package/dist/web/assets/{sparql-Ctg__NhZ.js → sparql-ftqZo2i-.js} +1 -1
  151. package/dist/web/assets/{sql-query-editor-D9k19T_M.js → sql-query-editor-BgRH0cHD.js} +1 -1
  152. package/dist/web/assets/{sqlite-viewer-DoV9hnWJ.js → sqlite-viewer-DUxGslpa.js} +1 -1
  153. package/dist/web/assets/{stata-Qk2h66XW.js → stata-Bj7Yjb4b.js} +1 -1
  154. package/dist/web/assets/{surrealql-8QEX7EfH.js → surrealql-CLhYGAfB.js} +1 -1
  155. package/dist/web/assets/{svelte-D_mubC5-.js → svelte-DTAlRHhK.js} +1 -1
  156. package/dist/web/assets/system-monitor-body-IZuLn7Qh.js +2 -0
  157. package/dist/web/assets/system-monitor-tab-DiSA490Z.js +1 -0
  158. package/dist/web/assets/{system-monitor-window-content-qXLcicFM.js → system-monitor-window-content-CEaFISgj.js} +1 -1
  159. package/dist/web/assets/{tab-host-window-content-7fPZNEni.js → tab-host-window-content-D0TN8Wrq.js} +1 -1
  160. package/dist/web/assets/{tab-store-DGJGnbwv.js → tab-store-OJa1GPCq.js} +1 -1
  161. package/dist/web/assets/team-member-sheet-DDPNcCjd.js +2 -0
  162. package/dist/web/assets/{team-member-window-content-BgvJFypM.js → team-member-window-content-DCiW_1-E.js} +1 -1
  163. package/dist/web/assets/{templ-C1jV0fsO.js → templ-BMfhACxb.js} +1 -1
  164. package/dist/web/assets/{terminal-tab-Cs0sGljx.js → terminal-tab-4ypP0NDY.js} +3 -3
  165. package/dist/web/assets/{tex-D_StFXy_.js → tex-BcssM-QA.js} +1 -1
  166. package/dist/web/assets/tool-cards-Dd6E7Q3k.js +9 -0
  167. package/dist/web/assets/treemap-KZPCXAKY-CE30wCXa.js +1 -0
  168. package/dist/web/assets/{ts-tags-B5m47rEm.js → ts-tags-CrR5IYI8.js} +1 -1
  169. package/dist/web/assets/{twig-C_br6GIl.js → twig-DchfUmyI.js} +1 -1
  170. package/dist/web/assets/{use-blob-url-C-SGi_Hb.js → use-blob-url-CPtCcY3i.js} +1 -1
  171. package/dist/web/assets/{use-inline-blame-B4trxGcR.js → use-inline-blame-vrfFHuaR.js} +1 -1
  172. package/dist/web/assets/{use-lsp-CV0vzILO.js → use-lsp-CsuZu49X.js} +1 -1
  173. package/dist/web/assets/{use-monaco-theme-DBqbWjWX.js → use-monaco-theme-BIVYJBIn.js} +1 -1
  174. package/dist/web/assets/{use-open-team-member-DENsjp0s.js → use-open-team-member-C3mrUzIC.js} +1 -1
  175. package/dist/web/assets/{use-websocket-LgXiX1qN.js → use-websocket-BMbufhX7.js} +1 -1
  176. package/dist/web/assets/{vendor-mermaid-Cbig5zA6.js → vendor-mermaid-BitiXJlL.js} +1 -1
  177. package/dist/web/assets/{vendor-ui-BBdkypKW.js → vendor-ui-DmZ-Mb-H.js} +1 -1
  178. package/dist/web/assets/video-preview-Byj8P_jA.js +1 -0
  179. package/dist/web/assets/{vue-DQwCnLdr.js → vue-1EL6Pw1h.js} +1 -1
  180. package/dist/web/assets/{vue-html-T_EoGpPJ.js → vue-html-BPQNwPC6.js} +1 -1
  181. package/dist/web/assets/{vue-vine-DmxLqX50.js → vue-vine-Bb_dsWLt.js} +1 -1
  182. package/dist/web/assets/{window-pip-registry-3l4hoxuD.js → window-pip-registry-yD6esSju.js} +1 -1
  183. package/dist/web/assets/ws-auth-CEYVQ8NH.js +1 -0
  184. package/dist/web/assets/{xml-Bs6Bw8is.js → xml-dxkLNA0K.js} +1 -1
  185. package/dist/web/assets/{xsl-f5ZNhRCc.js → xsl-Bw2adwrl.js} +1 -1
  186. package/dist/web/index.html +27 -26
  187. package/dist/web/sw.js +1 -1
  188. package/docs/architecture/plugins-and-tracing.md +285 -0
  189. package/docs/lessons-learned.md +31 -0
  190. package/docs/system-architecture.md +1 -0
  191. package/package.json +2 -1
  192. package/packages/ext-git-graph/src/compare-diff-base.test.ts +173 -0
  193. package/packages/ext-git-graph/src/compare-view.ts +22 -3
  194. package/packages/ext-git-graph/src/extension.ts +6 -0
  195. package/packages/ext-git-graph/src/types.ts +19 -0
  196. package/packages/ext-git-graph/src/webview-html.test.ts +134 -1
  197. package/packages/ext-git-graph/src/webview-html.ts +277 -27
  198. package/scripts/gen-file-icons.ts +493 -242
  199. package/src/providers/claude-agent-sdk.ts +42 -1
  200. package/src/providers/codex-app-server/codex-history.ts +77 -23
  201. package/src/server/index.ts +42 -7
  202. package/src/server/routes/chat.ts +15 -11
  203. package/src/server/routes/git.ts +83 -3
  204. package/src/server/routes/remote-desktop.ts +7 -2
  205. package/src/server/ws/global.ts +1 -1
  206. package/src/server/ws/remote-desktop.ts +12 -5
  207. package/src/services/binary-content.ts +25 -0
  208. package/src/services/chat-search.service.ts +1 -1
  209. package/src/services/chat.service.ts +9 -0
  210. package/src/services/event-loop-lag.ts +208 -0
  211. package/src/services/file-lines.ts +114 -0
  212. package/src/services/file-watcher/recreated-dir-poller.ts +108 -6
  213. package/src/services/file-watcher/watch-tree.ts +131 -10
  214. package/src/services/file-watcher.service.ts +73 -23
  215. package/src/services/git-branch-diff/branch-diff-parse.ts +151 -0
  216. package/src/services/git-branch-diff/branch-diff.service.ts +79 -0
  217. package/src/services/git-hunks/git-hunks.service.ts +26 -11
  218. package/src/services/git-hunks/unified-diff.ts +15 -1
  219. package/src/services/git.service.ts +74 -24
  220. package/src/services/jsonl-transcript-parser.ts +138 -6
  221. package/src/services/log-rotate.ts +142 -0
  222. package/src/services/media-transcode/ffmpeg-capabilities.ts +72 -2
  223. package/src/services/remote-desktop/remote-desktop-audio.ts +193 -0
  224. package/src/services/remote-desktop/remote-desktop-capture-input.ts +87 -14
  225. package/src/services/remote-desktop/remote-desktop-capture.ts +40 -7
  226. package/src/services/remote-desktop/remote-desktop-clipboard.ts +166 -0
  227. package/src/services/remote-desktop/remote-desktop-displays.ts +77 -1
  228. package/src/services/remote-desktop/remote-desktop-encoder-args.ts +25 -6
  229. package/src/services/remote-desktop/remote-desktop-evdev-key-map.ts +69 -0
  230. package/src/services/remote-desktop/remote-desktop-input-linux.ts +253 -0
  231. package/src/services/remote-desktop/remote-desktop-input-uinput.ts +242 -0
  232. package/src/services/remote-desktop/remote-desktop-input.ts +40 -8
  233. package/src/services/remote-desktop/remote-desktop-linux-session.ts +146 -0
  234. package/src/services/remote-desktop/remote-desktop-ogg-opus.ts +144 -0
  235. package/src/services/remote-desktop/remote-desktop-privacy.ts +163 -0
  236. package/src/services/remote-desktop/remote-desktop-quality.ts +138 -0
  237. package/src/services/remote-desktop/remote-desktop-requirements.ts +158 -5
  238. package/src/services/remote-desktop/remote-desktop-resolution.ts +302 -0
  239. package/src/services/remote-desktop/remote-desktop-session.ts +523 -21
  240. package/src/services/remote-desktop/remote-desktop-x11.ts +250 -0
  241. package/src/services/supervisor.ts +47 -4
  242. package/src/shared/remote-desktop-custom-quality.ts +117 -0
  243. package/src/shared/remote-desktop-quality.ts +127 -0
  244. package/src/types/chat.ts +4 -0
  245. package/src/types/git.ts +52 -0
  246. package/src/web/components/branch-review/branch-review-tab.tsx +360 -0
  247. package/src/web/components/branch-review/branch-review-tree-row.tsx +138 -0
  248. package/src/web/components/chat/attachment-chips.tsx +30 -6
  249. package/src/web/components/chat/chat-tab.tsx +12 -1
  250. package/src/web/components/chat/message-events.tsx +273 -0
  251. package/src/web/components/chat/message-list.tsx +107 -861
  252. package/src/web/components/chat/message-markdown.tsx +34 -0
  253. package/src/web/components/chat/message-media.tsx +174 -0
  254. package/src/web/components/chat/message-user-bubble.tsx +399 -0
  255. package/src/web/components/chat/provider-selector.tsx +30 -22
  256. package/src/web/components/chat/session-history-list.tsx +2 -2
  257. package/src/web/components/chat/session-list-panel.tsx +3 -2
  258. package/src/web/components/editor/binary-diff-view.tsx +212 -0
  259. package/src/web/components/editor/diff-viewer.tsx +76 -3
  260. package/src/web/components/explorer/file-tree.tsx +235 -134
  261. package/src/web/components/explorer/flatten-visible-tree.ts +35 -40
  262. package/src/web/components/explorer/tree-node.tsx +69 -69
  263. package/src/web/components/explorer/use-tree-row-dnd.ts +3 -4
  264. package/src/web/components/git/git-status-panel.tsx +25 -0
  265. package/src/web/components/layout/draggable-tab.tsx +4 -2
  266. package/src/web/components/layout/nav-section-rail.tsx +12 -3
  267. package/src/web/components/layout/status-bar.tsx +10 -6
  268. package/src/web/components/layout/tab-content.tsx +5 -0
  269. package/src/web/components/layout/tab-pool.tsx +1 -0
  270. package/src/web/components/layout/upgrade-button.tsx +5 -1
  271. package/src/web/components/remote-desktop/remote-desktop-clipboard-client.ts +67 -0
  272. package/src/web/components/remote-desktop/remote-desktop-clipboard-notice.tsx +127 -0
  273. package/src/web/components/remote-desktop/remote-desktop-codec-labels.ts +28 -0
  274. package/src/web/components/remote-desktop/remote-desktop-custom-quality-dialog.tsx +144 -0
  275. package/src/web/components/remote-desktop/remote-desktop-mobile-key-bar.tsx +13 -2
  276. package/src/web/components/remote-desktop/remote-desktop-mobile-settings.tsx +232 -0
  277. package/src/web/components/remote-desktop/remote-desktop-mobile-toolbar.tsx +21 -8
  278. package/src/web/components/remote-desktop/remote-desktop-mobile-view.tsx +82 -3
  279. package/src/web/components/remote-desktop/remote-desktop-recording.ts +76 -0
  280. package/src/web/components/remote-desktop/remote-desktop-resolution-list.ts +100 -0
  281. package/src/web/components/remote-desktop/remote-desktop-toolbar.tsx +497 -0
  282. package/src/web/components/remote-desktop/remote-desktop-view-style.ts +145 -0
  283. package/src/web/components/remote-desktop/remote-desktop-window-content.tsx +126 -35
  284. package/src/web/components/remote-desktop/use-h264-canvas-decoder.ts +11 -0
  285. package/src/web/components/remote-desktop/use-opus-audio-player.ts +148 -0
  286. package/src/web/components/remote-desktop/use-remote-desktop-connection.ts +284 -1
  287. package/src/web/components/remote-desktop/use-remote-desktop-readiness.ts +75 -1
  288. package/src/web/components/remote-desktop/use-remote-desktop-recorder.ts +103 -0
  289. package/src/web/components/remote-desktop/use-remote-desktop-virtual-keyboard.ts +19 -2
  290. package/src/web/components/remote-desktop/use-remote-input-capture.ts +39 -3
  291. package/src/web/components/settings/theme-picker.tsx +2 -1
  292. package/src/web/components/ui/tooltip.tsx +15 -2
  293. package/src/web/hooks/use-delayed-flag.ts +35 -0
  294. package/src/web/lib/branch-review-state.ts +175 -0
  295. package/src/web/lib/file-icon-name.ts +62 -0
  296. package/src/web/lib/file-icons.generated.ts +3287 -25
  297. package/src/web/lib/file-icons.tsx +71 -73
  298. package/src/web/lib/flatten-expansions.ts +18 -5
  299. package/src/web/lib/message-ordinals.ts +23 -0
  300. package/src/web/lib/provider-logos.tsx +143 -0
  301. package/src/web/lib/tab-type-icons.ts +11 -0
  302. package/src/web/stores/file-store.ts +73 -35
  303. package/src/web/stores/panel-utils.ts +5 -0
  304. package/src/web/stores/project-framework-store.ts +82 -0
  305. package/src/web/stores/project-store.ts +7 -1
  306. package/src/web/stores/settings-store.ts +128 -0
  307. package/src/web/stores/tab-store.ts +1 -0
  308. package/src/web/stores/window-panel-persistence.ts +1 -0
  309. package/src/web/styles/file-icons.generated.css +3282 -234
  310. package/src/web/styles/globals.css +26 -0
  311. package/dist/web/assets/accounts-settings-section-BuxD9zH3.js +0 -1
  312. package/dist/web/assets/ai-settings-section-Dd9tOUp0.js +0 -1
  313. package/dist/web/assets/ai-settings-section-FrU2X8Fu.js +0 -1
  314. package/dist/web/assets/api-client-BUCo1lms.js +0 -1
  315. package/dist/web/assets/architecture-PBZL5I3N-5K0wEkAn.js +0 -1
  316. package/dist/web/assets/chat-tab-Cr6_KSGO.js +0 -15
  317. package/dist/web/assets/code-editor-DfmlGamv.js +0 -7
  318. package/dist/web/assets/context-menu-DOODKZdF.js +0 -1
  319. package/dist/web/assets/diff-viewer-BpNj7pN9.js +0 -4
  320. package/dist/web/assets/explorer-body-D-5z464e.js +0 -2
  321. package/dist/web/assets/explorer-window-content-7-1wuRul.js +0 -1
  322. package/dist/web/assets/file-icons-EzPL0q-k.js +0 -1
  323. package/dist/web/assets/file-icons.generated-CfaJuvZu.css +0 -1
  324. package/dist/web/assets/file-store-DMABxOkV.js +0 -1
  325. package/dist/web/assets/files-settings-section-CZzABuKz.js +0 -1
  326. package/dist/web/assets/git-log-panel-CRN4JVOx.js +0 -1
  327. package/dist/web/assets/gitGraph-HDMCJU4V-DkVT2MAM.js +0 -1
  328. package/dist/web/assets/group-chat-tab-DFa5mAx5.js +0 -1
  329. package/dist/web/assets/index-D7DbXD3G.css +0 -1
  330. package/dist/web/assets/index-ZtfxZOyb.js +0 -52
  331. package/dist/web/assets/info-3K5VOQVL-BBsmKGzA.js +0 -1
  332. package/dist/web/assets/keybindings-store-tVfhPANS.js +0 -1
  333. package/dist/web/assets/markdown-renderer-CvL1pWeK.js +0 -3
  334. package/dist/web/assets/markdown-renderer-sJNNVhlS.js +0 -1
  335. package/dist/web/assets/mobile-explorer-sheet-CZ0HZ2E_.js +0 -1
  336. package/dist/web/assets/notification-store-BZbsPnW0.js +0 -1
  337. package/dist/web/assets/packet-RMMSAZCW-DwmYPOlx.js +0 -1
  338. package/dist/web/assets/panel-store-BVLBRn6M.js +0 -1
  339. package/dist/web/assets/pie-UPGHQEXC-D0oogIFi.js +0 -1
  340. package/dist/web/assets/problems-panel-B7BWVhdy.js +0 -1
  341. package/dist/web/assets/project-store-BhMbVln3.js +0 -1
  342. package/dist/web/assets/radar-KQ55EAFF-CQIJOQGa.js +0 -1
  343. package/dist/web/assets/remote-desktop-mobile-sheet-ByjaeZKX.js +0 -2
  344. package/dist/web/assets/remote-desktop-mobile-view-D13mtr9F.js +0 -1
  345. package/dist/web/assets/remote-desktop-window-content-CbqTCaC6.js +0 -1
  346. package/dist/web/assets/settings-body-Rnu-FcX-.js +0 -2
  347. package/dist/web/assets/settings-store-DZPgPtEA.js +0 -2
  348. package/dist/web/assets/settings-tab-C6PHxs0Z.js +0 -1
  349. package/dist/web/assets/settings-window-content-CQ4gT2qj.js +0 -1
  350. package/dist/web/assets/system-monitor-body-2vQ1kMxE.js +0 -2
  351. package/dist/web/assets/system-monitor-tab-krAcvwF7.js +0 -1
  352. package/dist/web/assets/team-member-sheet-2FnRmk1a.js +0 -2
  353. package/dist/web/assets/tool-cards-DU_S2o51.js +0 -9
  354. package/dist/web/assets/treemap-KZPCXAKY-BhEtjljr.js +0 -1
  355. package/dist/web/assets/use-remote-desktop-display-choice-BK_f787P.js +0 -1
  356. package/dist/web/assets/video-preview-BDNwumbW.js +0 -1
  357. package/dist/web/assets/ws-auth-DEAJiGCR.js +0 -1
  358. /package/dist/web/assets/{abap-tkptwpsK.js → abap-IN2yINdN.js} +0 -0
  359. /package/dist/web/assets/{actionscript-3-Di9TMGwg.js → actionscript-3-C_t772eR.js} +0 -0
  360. /package/dist/web/assets/{ada-CB9F079D.js → ada-DPme3emm.js} +0 -0
  361. /package/dist/web/assets/{andromeeda-Dcl8jpb7.js → andromeeda-DSV4i4mZ.js} +0 -0
  362. /package/dist/web/assets/{apache-D0Nk0qsH.js → apache-CWr8s8I8.js} +0 -0
  363. /package/dist/web/assets/{apex-mQIoWncD.js → apex-Z20JnDF5.js} +0 -0
  364. /package/dist/web/assets/{applescript-C6ljbcsI.js → applescript-BKNPu3ga.js} +0 -0
  365. /package/dist/web/assets/{apply-theme-BwIMNXyX.js → apply-theme-CbR31K6I.js} +0 -0
  366. /package/dist/web/assets/{ara-WFqRPGPe.js → ara-hWdY3O96.js} +0 -0
  367. /package/dist/web/assets/{asciidoc-bNd8w4nf.js → asciidoc-eynhq496.js} +0 -0
  368. /package/dist/web/assets/{asm-BQ1_F580.js → asm-Bu02Kq4P.js} +0 -0
  369. /package/dist/web/assets/{aurora-x-Ciqs_Dbu.js → aurora-x-BHBGXbIZ.js} +0 -0
  370. /package/dist/web/assets/{awk-GCR-6KZV.js → awk-B4ojezyv.js} +0 -0
  371. /package/dist/web/assets/{ayu-dark-DxJXuj99.js → ayu-dark-D0GQzFi9.js} +0 -0
  372. /package/dist/web/assets/{ayu-light-jvSyJWDV.js → ayu-light-BVYb5Yn8.js} +0 -0
  373. /package/dist/web/assets/{ayu-mirage-Cg3BPO6H.js → ayu-mirage-BGFW6oNb.js} +0 -0
  374. /package/dist/web/assets/{ballerina-BPz4CrX6.js → ballerina-B6Og3IKT.js} +0 -0
  375. /package/dist/web/assets/{bat-CSTs2hIG.js → bat-D8YFILfx.js} +0 -0
  376. /package/dist/web/assets/{beancount-DMateAZX.js → beancount-BjlbBBuX.js} +0 -0
  377. /package/dist/web/assets/{berry-9lXg0GRM.js → berry-Cz1ygeoj.js} +0 -0
  378. /package/dist/web/assets/{bibtex-DIbqmvFg.js → bibtex-CVkJceJh.js} +0 -0
  379. /package/dist/web/assets/{bicep-02WGsoJ8.js → bicep-DrhYI5GX.js} +0 -0
  380. /package/dist/web/assets/{bird2-BY0tbu3k.js → bird2-BV-KJtmc.js} +0 -0
  381. /package/dist/web/assets/{c-jMBX_9Je.js → c-CuLlvxbG.js} +0 -0
  382. /package/dist/web/assets/{c3-B8JVbNjW.js → c3-CI7-1CJ0.js} +0 -0
  383. /package/dist/web/assets/{cadence-DUIf1tvL.js → cadence-DjxhNP3Q.js} +0 -0
  384. /package/dist/web/assets/{can-open-in-ppm-DGEc0s4P.js → can-open-in-ppm-cv5--yHX.js} +0 -0
  385. /package/dist/web/assets/{catppuccin-frappe-FhUCVxex.js → catppuccin-frappe-CEozpJRh.js} +0 -0
  386. /package/dist/web/assets/{catppuccin-latte-CzfrtmgL.js → catppuccin-latte-C4sXQFMc.js} +0 -0
  387. /package/dist/web/assets/{catppuccin-macchiato-6xL0lOtj.js → catppuccin-macchiato-Ci-fDhXy.js} +0 -0
  388. /package/dist/web/assets/{catppuccin-mocha-D39-xS3P.js → catppuccin-mocha-CH7YXw8d.js} +0 -0
  389. /package/dist/web/assets/{clarity-D8TeGvoT.js → clarity-BWrJ7UmT.js} +0 -0
  390. /package/dist/web/assets/{clojure-Dx_acSX7.js → clojure-C94fh2hb.js} +0 -0
  391. /package/dist/web/assets/{cmake-CyeYPq8o.js → cmake-CtvCkMsG.js} +0 -0
  392. /package/dist/web/assets/{codeowners-DrTI0lmI.js → codeowners-CJnubqzH.js} +0 -0
  393. /package/dist/web/assets/{codeql-CPkFxVW-.js → codeql-BOoP5HY2.js} +0 -0
  394. /package/dist/web/assets/{common-lisp-B0k-3LTj.js → common-lisp-B9nlMFJd.js} +0 -0
  395. /package/dist/web/assets/{coq-uygpo7to.js → coq-CgzIQiSH.js} +0 -0
  396. /package/dist/web/assets/{csharp-DKUYGfSp.js → csharp-4xAIx3jw.js} +0 -0
  397. /package/dist/web/assets/{css-DoOUp7v4.js → css-D4YoLYGn.js} +0 -0
  398. /package/dist/web/assets/{csv-B3YE13gi.js → csv-C2v-ZakW.js} +0 -0
  399. /package/dist/web/assets/{csv-parser-DXePqmXe.js → csv-parser-_oDBUkwQ.js} +0 -0
  400. /package/dist/web/assets/{cue-DViO2wvn.js → cue-Bbznh_an.js} +0 -0
  401. /package/dist/web/assets/{cypher-4BMCIW5N.js → cypher-BKhVEARF.js} +0 -0
  402. /package/dist/web/assets/{d-BT_sTHas.js → d-D6IhWopl.js} +0 -0
  403. /package/dist/web/assets/{dark-plus-DVVXu-1N.js → dark-plus-xAWu790O.js} +0 -0
  404. /package/dist/web/assets/{dart-DHz6f6_6.js → dart-Bj6S9O1j.js} +0 -0
  405. /package/dist/web/assets/{data-grid-types-OgUxMLdQ.js → data-grid-types-C5bthITf.js} +0 -0
  406. /package/dist/web/assets/{dax-ChYCjweK.js → dax-CF2zvaqq.js} +0 -0
  407. /package/dist/web/assets/{desktop-D6Lt4p04.js → desktop-CnmyxMKE.js} +0 -0
  408. /package/dist/web/assets/{diff-D8TDe4hP.js → diff-Qi8WuJsI.js} +0 -0
  409. /package/dist/web/assets/{dist-DOTlFQJA.js → dist-Bjseolz4.js} +0 -0
  410. /package/dist/web/assets/{dist-CrBOCtCW.js → dist-Czq7BUMy.js} +0 -0
  411. /package/dist/web/assets/{docker-Bsp0YbPx.js → docker-vLbbNP5d.js} +0 -0
  412. /package/dist/web/assets/{dotenv-B_7giSY_.js → dotenv-DJMvvNzn.js} +0 -0
  413. /package/dist/web/assets/{dracula-TrQL2iB5.js → dracula-CtAh2rhB.js} +0 -0
  414. /package/dist/web/assets/{dracula-soft-n2XOlgh-.js → dracula-soft-CXtuAAf6.js} +0 -0
  415. /package/dist/web/assets/{dream-maker-CdIEdhpT.js → dream-maker-D5YQyK49.js} +0 -0
  416. /package/dist/web/assets/{editor-font-BDrdiGf1.js → editor-font-ClTC9gaD.js} +0 -0
  417. /package/dist/web/assets/{emacs-lisp-CSbixs6u.js → emacs-lisp-CGTjbiqF.js} +0 -0
  418. /package/dist/web/assets/{everforest-dark-BuZahZYp.js → everforest-dark-D59p7vAQ.js} +0 -0
  419. /package/dist/web/assets/{everforest-light-DPH53z5H.js → everforest-light-B3Qtud3V.js} +0 -0
  420. /package/dist/web/assets/{fennel-B1-K_hlJ.js → fennel-BCyHNUwf.js} +0 -0
  421. /package/dist/web/assets/{fish-Cy12Dbua.js → fish-DDDlLV2G.js} +0 -0
  422. /package/dist/web/assets/{fluent-B_82ys8E.js → fluent-By54R9tl.js} +0 -0
  423. /package/dist/web/assets/{format-bytes-D5sBgIqU.js → format-bytes-D1AQyUCt.js} +0 -0
  424. /package/dist/web/assets/{fortran-free-form-BRQdNzZF.js → fortran-free-form-tP8mRym_.js} +0 -0
  425. /package/dist/web/assets/{gdscript-XBDmLUm9.js → gdscript-nHC2z6Te.js} +0 -0
  426. /package/dist/web/assets/{gdshader-DhVfB3u4.js → gdshader-K-cSmPlW.js} +0 -0
  427. /package/dist/web/assets/{genie-X3K29Y49.js → genie-BAcRhvEe.js} +0 -0
  428. /package/dist/web/assets/{gherkin-DKIx-6cX.js → gherkin-CcifxMLb.js} +0 -0
  429. /package/dist/web/assets/{github-dark-Bf7zdp5s.js → github-dark-67KKlMdB.js} +0 -0
  430. /package/dist/web/assets/{github-dark-default-DqsBBV11.js → github-dark-default-tja8pidR.js} +0 -0
  431. /package/dist/web/assets/{github-dark-dimmed-X2gqRz6w.js → github-dark-dimmed-CtjhYQtx.js} +0 -0
  432. /package/dist/web/assets/{github-dark-high-contrast-Z7tvbGX4.js → github-dark-high-contrast-Bf-LPFgn.js} +0 -0
  433. /package/dist/web/assets/{github-light-BKxt-7zZ.js → github-light-CbJxE7n3.js} +0 -0
  434. /package/dist/web/assets/{github-light-default-B0liF4d4.js → github-light-default-DRQmdHxt.js} +0 -0
  435. /package/dist/web/assets/{github-light-high-contrast-F4encJwr.js → github-light-high-contrast-CJkDgTeM.js} +0 -0
  436. /package/dist/web/assets/{gleam-Pz2bHgBB.js → gleam-D4LCl2ri.js} +0 -0
  437. /package/dist/web/assets/{gn-CgmdNsPU.js → gn-COY8zZmC.js} +0 -0
  438. /package/dist/web/assets/{gnuplot-qLNF5yZ9.js → gnuplot-ROk1VitR.js} +0 -0
  439. /package/dist/web/assets/{go-CSDUNhE7.js → go-H30c3p0C.js} +0 -0
  440. /package/dist/web/assets/{groovy-B3NEnOHF.js → groovy-DBLCaelV.js} +0 -0
  441. /package/dist/web/assets/{gruvbox-dark-hard-DcoftiUX.js → gruvbox-dark-hard-DJy4irps.js} +0 -0
  442. /package/dist/web/assets/{gruvbox-dark-medium-D1wVSuD8.js → gruvbox-dark-medium-Dhfw1QTO.js} +0 -0
  443. /package/dist/web/assets/{gruvbox-dark-soft-DWpunymH.js → gruvbox-dark-soft-BncJU-8w.js} +0 -0
  444. /package/dist/web/assets/{gruvbox-light-hard-rHMvyUdS.js → gruvbox-light-hard-Cq12_YvM.js} +0 -0
  445. /package/dist/web/assets/{gruvbox-light-medium-3O2KiXO7.js → gruvbox-light-medium-DqVja5_v.js} +0 -0
  446. /package/dist/web/assets/{gruvbox-light-soft-lNlXJPwH.js → gruvbox-light-soft-IvwhveGF.js} +0 -0
  447. /package/dist/web/assets/{haskell-aaG3aC0h.js → haskell-DA1JV-MC.js} +0 -0
  448. /package/dist/web/assets/{haxe-CPcHhUDd.js → haxe-V9RTRc5E.js} +0 -0
  449. /package/dist/web/assets/{hcl-CMQNlR_4.js → hcl-hr1lyZ10.js} +0 -0
  450. /package/dist/web/assets/{hjson-DeuklPFj.js → hjson-D3vW3i7X.js} +0 -0
  451. /package/dist/web/assets/{hlsl-BI3hlsaM.js → hlsl-BB85gfJT.js} +0 -0
  452. /package/dist/web/assets/{horizon-C7-gOcjs.js → horizon-DU8OOnj8.js} +0 -0
  453. /package/dist/web/assets/{horizon-bright-Cxt9bRW7.js → horizon-bright-DNMUclW6.js} +0 -0
  454. /package/dist/web/assets/{houston-DbF2G7iD.js → houston-DAiZKf6T.js} +0 -0
  455. /package/dist/web/assets/{hy-DRG6Hs2I.js → hy-C6Zv0LMe.js} +0 -0
  456. /package/dist/web/assets/{image-gallery-CObzTisL.js → image-gallery-CGvlv8zR.js} +0 -0
  457. /package/dist/web/assets/{imba-CxKv_2TW.js → imba-Didnp6Y-.js} +0 -0
  458. /package/dist/web/assets/{ini-BiHsyCdO.js → ini-m0T7q4gQ.js} +0 -0
  459. /package/dist/web/assets/{input-D-dR8RI6.js → input-C7Q8FxeL.js} +0 -0
  460. /package/dist/web/assets/{java-D3RZzgDB.js → java-Buy07jGn.js} +0 -0
  461. /package/dist/web/assets/{javascript-4FmSqG-T.js → javascript-Cjq3CbRL.js} +0 -0
  462. /package/dist/web/assets/{json-CjB236_E.js → json-BSVu8IqD.js} +0 -0
  463. /package/dist/web/assets/{json5-bkfO-Rco.js → json5-NN_Vb7e6.js} +0 -0
  464. /package/dist/web/assets/{jsonc-BwaTeLyv.js → jsonc-BdyZhBUw.js} +0 -0
  465. /package/dist/web/assets/{jsonl-CfxNSwGd.js → jsonl-BsDSNvwi.js} +0 -0
  466. /package/dist/web/assets/{jsonnet-CQETCYrp.js → jsonnet-Gm963F-U.js} +0 -0
  467. /package/dist/web/assets/{jssm-D5o3Zzdz.js → jssm-Ds3bKWgJ.js} +0 -0
  468. /package/dist/web/assets/{jsx-BAMILPLo.js → jsx-BT5mW58X.js} +0 -0
  469. /package/dist/web/assets/{kanagawa-dragon-BhGVxu3R.js → kanagawa-dragon-BvNvf7xd.js} +0 -0
  470. /package/dist/web/assets/{kanagawa-lotus-FH4Y59ro.js → kanagawa-lotus-CuSiliSn.js} +0 -0
  471. /package/dist/web/assets/{kanagawa-wave-eilvP9MB.js → kanagawa-wave-C6g-d6jT.js} +0 -0
  472. /package/dist/web/assets/{kdl-BQ-yvXUK.js → kdl-Bx4kZef-.js} +0 -0
  473. /package/dist/web/assets/{kotlin-BuCx0-VT.js → kotlin-qvaVvCcm.js} +0 -0
  474. /package/dist/web/assets/{kusto-DAN_n_Cy.js → kusto-BPLyNCN6.js} +0 -0
  475. /package/dist/web/assets/{laserwave-Cq2BlxrT.js → laserwave-Sf8ZEhWP.js} +0 -0
  476. /package/dist/web/assets/{lean-Dhq7uXKw.js → lean-DGe6uqgk.js} +0 -0
  477. /package/dist/web/assets/{less-CxaMQb8j.js → less-CqM85x-9.js} +0 -0
  478. /package/dist/web/assets/{lib-BVwmtv2i.js → lib-CD6pOS4u.js} +0 -0
  479. /package/dist/web/assets/{libesm-DdtEjhIi.js → libesm-CPh1Av0r.js} +0 -0
  480. /package/dist/web/assets/{light-plus-B4sMAend.js → light-plus-DBtKwhFA.js} +0 -0
  481. /package/dist/web/assets/{llvm-DQ6-B_6Y.js → llvm-Dpbq3gky.js} +0 -0
  482. /package/dist/web/assets/{log-Bb_vqvu7.js → log-qKMcPm9S.js} +0 -0
  483. /package/dist/web/assets/{logo-BzfYv3A_.js → logo-DFo7i5KH.js} +0 -0
  484. /package/dist/web/assets/{luau-BnAeNR77.js → luau-DGlqWFyL.js} +0 -0
  485. /package/dist/web/assets/{make-B6IFuxpC.js → make-CixdvUQe.js} +0 -0
  486. /package/dist/web/assets/{markdown-Z0xi77Fn.js → markdown-CmRnbngE.js} +0 -0
  487. /package/dist/web/assets/{material-theme-1scy1SYn.js → material-theme-WDUbafB-.js} +0 -0
  488. /package/dist/web/assets/{material-theme-darker-G-JykXQP.js → material-theme-darker-u8y2xpOk.js} +0 -0
  489. /package/dist/web/assets/{material-theme-lighter-BtKkCMVq.js → material-theme-lighter-cyQZK3Tg.js} +0 -0
  490. /package/dist/web/assets/{material-theme-ocean-irrDC89L.js → material-theme-ocean-LBAVvw7k.js} +0 -0
  491. /package/dist/web/assets/{material-theme-palenight-Eq5_K4LM.js → material-theme-palenight-BJqhBxL0.js} +0 -0
  492. /package/dist/web/assets/{matlab-BE3oCXZC.js → matlab-B38sOJee.js} +0 -0
  493. /package/dist/web/assets/{mdx-DYiuFTGw.js → mdx-C8QrE_IW.js} +0 -0
  494. /package/dist/web/assets/{mermaid-DVlaEvtw.js → mermaid-BQ-eeU8A.js} +0 -0
  495. /package/dist/web/assets/{min-dark-DSkeTpQa.js → min-dark-D4gNa5tv.js} +0 -0
  496. /package/dist/web/assets/{min-light-CFv2Q0uF.js → min-light-Dfbhn_PL.js} +0 -0
  497. /package/dist/web/assets/{mipsasm-CiAFuZQ2.js → mipsasm-bLOhJkdm.js} +0 -0
  498. /package/dist/web/assets/{mojo-ARshcaSQ.js → mojo-B0BCOMQW.js} +0 -0
  499. /package/dist/web/assets/{monaco-dotenv-language-BDYnKcnr.js → monaco-dotenv-language-uB-wok6C.js} +0 -0
  500. /package/dist/web/assets/{monokai-CPTV-SOg.js → monokai-Bdw_07OM.js} +0 -0
  501. /package/dist/web/assets/{moonbit-D57B0dZr.js → moonbit-BFdGaeuk.js} +0 -0
  502. /package/dist/web/assets/{move-qgy3G_o9.js → move-CyXQFGPM.js} +0 -0
  503. /package/dist/web/assets/{narrat-CZ3rC-oc.js → narrat-9DvTD3_3.js} +0 -0
  504. /package/dist/web/assets/{nextflow-groovy-BxxIOcI4.js → nextflow-groovy-C4VY3kNY.js} +0 -0
  505. /package/dist/web/assets/{night-owl-B1ryGCda.js → night-owl-feYm66V0.js} +0 -0
  506. /package/dist/web/assets/{night-owl-light-BZvNnvMF.js → night-owl-light-DALqFjvx.js} +0 -0
  507. /package/dist/web/assets/{nix-BVBQi8yv.js → nix-C1kPA9Sw.js} +0 -0
  508. /package/dist/web/assets/{nord-3NXw613h.js → nord-1dvrj1Al.js} +0 -0
  509. /package/dist/web/assets/{nushell-BMOLRalb.js → nushell-DSQ_9Uqj.js} +0 -0
  510. /package/dist/web/assets/{objective-c-AxruEUgh.js → objective-c-Dhcn5ZK8.js} +0 -0
  511. /package/dist/web/assets/{objective-cpp-cFT0qRsD.js → objective-cpp-CGVYCSrp.js} +0 -0
  512. /package/dist/web/assets/{ocaml-_8JH6O8J.js → ocaml-rxXW21An.js} +0 -0
  513. /package/dist/web/assets/{odin-nwYvR0O6.js → odin-ClSJX2DE.js} +0 -0
  514. /package/dist/web/assets/{one-dark-pro-C1LaYJaH.js → one-dark-pro-3gbWDfFW.js} +0 -0
  515. /package/dist/web/assets/{one-light-B7rMc__c.js → one-light-C2q5ZZAw.js} +0 -0
  516. /package/dist/web/assets/{openscad-Cwes8bKO.js → openscad-eHXeafBl.js} +0 -0
  517. /package/dist/web/assets/{pascal-DMt-6tdZ.js → pascal-kKha3Txs.js} +0 -0
  518. /package/dist/web/assets/{pkl-QBTyYQmB.js → pkl-CTiPytg-.js} +0 -0
  519. /package/dist/web/assets/{plastic-BTSUp8Fk.js → plastic-D1Ygezi4.js} +0 -0
  520. /package/dist/web/assets/{plsql-DQv3IFXP.js → plsql-D_gbgZIr.js} +0 -0
  521. /package/dist/web/assets/{po-CNwZx4tW.js → po-BkhbDUnq.js} +0 -0
  522. /package/dist/web/assets/{poimandres-8Os7Qequ.js → poimandres-Do6NywrA.js} +0 -0
  523. /package/dist/web/assets/{polar-b4yK8oD8.js → polar-B3B23wca.js} +0 -0
  524. /package/dist/web/assets/{postcss-sFctwpxx.js → postcss-Dv_YBvWd.js} +0 -0
  525. /package/dist/web/assets/{powerquery-Cg_WrtuN.js → powerquery-fKJWI-Sh.js} +0 -0
  526. /package/dist/web/assets/{powershell-C7Ifz95h.js → powershell-B4Y7KIgx.js} +0 -0
  527. /package/dist/web/assets/{prisma-Z52HUJzQ.js → prisma-J--XdTXX.js} +0 -0
  528. /package/dist/web/assets/{prolog-DLqW-zR1.js → prolog-BBTg4Vb3.js} +0 -0
  529. /package/dist/web/assets/{proto-BOIzxCCJ.js → proto-DWlum3T8.js} +0 -0
  530. /package/dist/web/assets/{puppet-De7TjZIf.js → puppet-ZiUfc9JV.js} +0 -0
  531. /package/dist/web/assets/{purescript-R76ou1WT.js → purescript-CmT8SCHB.js} +0 -0
  532. /package/dist/web/assets/{python-Dprm0tNP.js → python-BURpj-fq.js} +0 -0
  533. /package/dist/web/assets/{qmldir-Clp_jpNw.js → qmldir-CpaiJywQ.js} +0 -0
  534. /package/dist/web/assets/{qss-eQPYYzSo.js → qss-C-zdz0i5.js} +0 -0
  535. /package/dist/web/assets/{r-CCcRHIf5.js → r-K56BeS-s.js} +0 -0
  536. /package/dist/web/assets/{racket-CzE472yp.js → racket-gkn6Z_Jw.js} +0 -0
  537. /package/dist/web/assets/{raku-DQwXrTSi.js → raku-C0ZBuKlx.js} +0 -0
  538. /package/dist/web/assets/{red-KCS6wyop.js → red-BWZ6x-3p.js} +0 -0
  539. /package/dist/web/assets/{reg-CcrAddUh.js → reg-Dt_EsyGR.js} +0 -0
  540. /package/dist/web/assets/{regexp-DDbud-Ri.js → regexp-CT6BLG2C.js} +0 -0
  541. /package/dist/web/assets/{rel-99GDgaHp.js → rel-MJ2LcyzJ.js} +0 -0
  542. /package/dist/web/assets/{resolve-theme-CW2niGV2.js → resolve-theme-B_ukrg_8.js} +0 -0
  543. /package/dist/web/assets/{riscv-Dm2eNv0R.js → riscv-BZYWzMAA.js} +0 -0
  544. /package/dist/web/assets/{ron-CzFt0ygH.js → ron-CLbwHWMn.js} +0 -0
  545. /package/dist/web/assets/{rose-pine-Ck6lCrCP.js → rose-pine-BPiI_daR.js} +0 -0
  546. /package/dist/web/assets/{rose-pine-dawn-vtALRJNj.js → rose-pine-dawn-BjkJc7dW.js} +0 -0
  547. /package/dist/web/assets/{rose-pine-moon-DPto8qXw.js → rose-pine-moon-BQSxkt7g.js} +0 -0
  548. /package/dist/web/assets/{rosmsg-NtS6429H.js → rosmsg-yt4151pJ.js} +0 -0
  549. /package/dist/web/assets/{rust-BjIXvF9h.js → rust-1dAcengL.js} +0 -0
  550. /package/dist/web/assets/{sass-CQ23wf5E.js → sass-DUgtQJsg.js} +0 -0
  551. /package/dist/web/assets/{scala-LfKLM143.js → scala-CxoGYpS0.js} +0 -0
  552. /package/dist/web/assets/{scheme-D3SLBXlF.js → scheme-B2K3u9e3.js} +0 -0
  553. /package/dist/web/assets/{sdbl-ODRd2rGN.js → sdbl-C0Ygk0n6.js} +0 -0
  554. /package/dist/web/assets/{shellscript-3KB1G_oA.js → shellscript-C2CvhqMk.js} +0 -0
  555. /package/dist/web/assets/{slack-dark-DTtyWmIy.js → slack-dark-DYdgzRGj.js} +0 -0
  556. /package/dist/web/assets/{slack-ochin-Bu0PApk3.js → slack-ochin-X-WD0WCg.js} +0 -0
  557. /package/dist/web/assets/{smalltalk-haMT-Sbe.js → smalltalk-Dp7g_zb9.js} +0 -0
  558. /package/dist/web/assets/{snazzy-light-DXJzefE9.js → snazzy-light-DWHWr-a6.js} +0 -0
  559. /package/dist/web/assets/{solarized-dark-B6LyYa18.js → solarized-dark-CxdLsjvb.js} +0 -0
  560. /package/dist/web/assets/{solarized-light-QLADebkv.js → solarized-light-Cz9rAvG5.js} +0 -0
  561. /package/dist/web/assets/{solidity-mzfq1aDc.js → solidity-D33gHHdn.js} +0 -0
  562. /package/dist/web/assets/{splunk-CyH4RWBp.js → splunk-BFbOroYw.js} +0 -0
  563. /package/dist/web/assets/{sql-CWVlT5D3.js → sql-ChpxvmdY.js} +0 -0
  564. /package/dist/web/assets/{ssh-config-DNvQJ1Qx.js → ssh-config-CUuSQ6JG.js} +0 -0
  565. /package/dist/web/assets/{streaming-store-uDu3PoN1.js → streaming-store-BYukdB56.js} +0 -0
  566. /package/dist/web/assets/{stylus-ByQQdtmf.js → stylus-DDihP0_W.js} +0 -0
  567. /package/dist/web/assets/{swift-D1cpY4ow.js → swift-XoalUn4x.js} +0 -0
  568. /package/dist/web/assets/{synthwave-84-BDF_-QiW.js → synthwave-84-DjkG0Bff.js} +0 -0
  569. /package/dist/web/assets/{system-verilog-BYrkMydC.js → system-verilog-f_pLKMYd.js} +0 -0
  570. /package/dist/web/assets/{systemd-CQm3zyK0.js → systemd-DjDcLfxp.js} +0 -0
  571. /package/dist/web/assets/{talonscript-DMW5CzE1.js → talonscript-BpUzYcDl.js} +0 -0
  572. /package/dist/web/assets/{tasl-BhFzMCr2.js → tasl-DP-cUbg8.js} +0 -0
  573. /package/dist/web/assets/{tcl-D7VEAF-9.js → tcl-BHoy-dfE.js} +0 -0
  574. /package/dist/web/assets/{terraform-DdT5A0Vy.js → terraform-vCVxN25B.js} +0 -0
  575. /package/dist/web/assets/{tokyo-night-WGLMM6Q9.js → tokyo-night-D0u4xZba.js} +0 -0
  576. /package/dist/web/assets/{toml-CZ7ipXHZ.js → toml-BS8D0__A.js} +0 -0
  577. /package/dist/web/assets/{tsv-B1zVvoTr.js → tsv-DCb-NQh3.js} +0 -0
  578. /package/dist/web/assets/{tsx-D4HwRUBq.js → tsx-DF8YeRgG.js} +0 -0
  579. /package/dist/web/assets/{turtle-B4r8KEJH.js → turtle-Bhx1GlIy.js} +0 -0
  580. /package/dist/web/assets/{typescript-BnHhxMtp.js → typescript-CAVD2Wb6.js} +0 -0
  581. /package/dist/web/assets/{typespec-DP2q46GT.js → typespec-D7mchdY_.js} +0 -0
  582. /package/dist/web/assets/{typst-DFsD_yaD.js → typst-CeXzq6Yw.js} +0 -0
  583. /package/dist/web/assets/{use-is-mobile-BQ9Bv5Ga.js → use-is-mobile-TkcwP1Gx.js} +0 -0
  584. /package/dist/web/assets/{use-visual-viewport-CbwnktSG.js → use-visual-viewport-Lp878uzs.js} +0 -0
  585. /package/dist/web/assets/{v-Bji8hSVT.js → v-DhzO54je.js} +0 -0
  586. /package/dist/web/assets/{vala-D3TIgGmU.js → vala-Dw_RAk3E.js} +0 -0
  587. /package/dist/web/assets/{vb-DvcxFS2q.js → vb-CiXdNh2Y.js} +0 -0
  588. /package/dist/web/assets/{vendor-xterm-BqPDsE9b.js → vendor-xterm-BHEY6Ctu.js} +0 -0
  589. /package/dist/web/assets/{verilog-Dy7cF35d.js → verilog-fITdxqwV.js} +0 -0
  590. /package/dist/web/assets/{vesper-BzjwUv9J.js → vesper-B1wm_At7.js} +0 -0
  591. /package/dist/web/assets/{vhdl-DuFnFNmW.js → vhdl-ebHjqKp9.js} +0 -0
  592. /package/dist/web/assets/{viml-yAPgKlbz.js → viml-Dgmnoxoo.js} +0 -0
  593. /package/dist/web/assets/{vitesse-black-oSSQdyIZ.js → vitesse-black-BZ5gMb7l.js} +0 -0
  594. /package/dist/web/assets/{vitesse-dark-BZxwPihB.js → vitesse-dark-Lij7kkOt.js} +0 -0
  595. /package/dist/web/assets/{vitesse-light-uIigycvt.js → vitesse-light-DW6Wq7VC.js} +0 -0
  596. /package/dist/web/assets/{vyper-CX3NAxm5.js → vyper-6FxDnVNg.js} +0 -0
  597. /package/dist/web/assets/{wasm-6mEOOvVa.js → wasm-BDxaNPCj.js} +0 -0
  598. /package/dist/web/assets/{wasm-D9td37qO.js → wasm-DKhAtzfe.js} +0 -0
  599. /package/dist/web/assets/{wenyan-lwNIHgk6.js → wenyan-C0k0DYG_.js} +0 -0
  600. /package/dist/web/assets/{wgsl-DQAzCfog.js → wgsl-Y2zagbdn.js} +0 -0
  601. /package/dist/web/assets/{wikitext-DTXmt5NE.js → wikitext-C4QGWzkY.js} +0 -0
  602. /package/dist/web/assets/{wit-Bdw_7WDH.js → wit-CQESzjHy.js} +0 -0
  603. /package/dist/web/assets/{wolfram-DqyHDZ07.js → wolfram-B-ia69Ia.js} +0 -0
  604. /package/dist/web/assets/{yaml-CPXhdByd.js → yaml-CEYYTQPm.js} +0 -0
  605. /package/dist/web/assets/{zenscript-6s_i5zek.js → zenscript-BggWXIeJ.js} +0 -0
  606. /package/dist/web/assets/{zig-B2htezJ_.js → zig-CbaUfBQe.js} +0 -0
@@ -10,7 +10,45 @@ import type { ChatEvent, ChatMessage } from "../types/chat.ts";
10
10
  import { stringifyToolResultContent } from "../shared/tool-result-content.ts";
11
11
  import { stripSharedContext } from "../shared/provider-context.ts";
12
12
 
13
- const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
13
+ /**
14
+ * How large a transcript this module will look at, for the callers that go
15
+ * through `validateJsonlPath`.
16
+ *
17
+ * It was 50MB and it silently killed the whole expand-compact path for exactly
18
+ * the sessions that need it — a transcript with thirteen compactions in it had
19
+ * reached 77MB, so scrolling to the top of that chat answered 403 and loaded
20
+ * nothing, forever.
21
+ *
22
+ * It is a sanity bound and not a memory bound, and it is worth being exact
23
+ * about why, because the obvious reading is wrong. Measured on this repo's own
24
+ * 88.6MB transcript: the reader streams, the parsed array retains **11.1MB**
25
+ * (6974 messages, no single string over 200KB) — and RSS still rises by 355MB.
26
+ * That is `JSON.parse` garbage, not live objects: `Bun.gc(true)` does not give
27
+ * it back and neither does dropping the array, because the allocator keeps the
28
+ * pages. It also does not accumulate — four transcripts of 89/83/55/48MB in a
29
+ * row plateau at 390–466MB rather than summing — so the cost of reading one of
30
+ * these is a one-time high-water mark, reusable by the next.
31
+ *
32
+ * The number that actually bounds memory is therefore the *file* size, at about
33
+ * 4.3× transiently, which is what `FULL_PARSE_MAX_BYTES` below is for.
34
+ */
35
+ const MAX_FILE_SIZE = 256 * 1024 * 1024; // 256MB
36
+
37
+ /**
38
+ * How many bytes a *whole-history* parse may read.
39
+ *
40
+ * `validateJsonlPath`'s bound never applied here: the search indexer reaches
41
+ * `parseJsonlTranscript` through `getFullMessages`, which does no validation at
42
+ * all, and it asks for every segment rather than one. At the measured 4.3×
43
+ * transient cost that is ~550MB of allocator growth at this bound, and nothing
44
+ * above it — a bigger transcript is read from `fullParseWindow` onwards
45
+ * instead of whole, so the cost stops rising with the file.
46
+ *
47
+ * Set above the largest transcript on this machine (88.8MB of 1666) with room
48
+ * to spare, so the windowing is for the pathological case and not for the
49
+ * sessions this file's history is about.
50
+ */
51
+ export const FULL_PARSE_MAX_BYTES = 128 * 1024 * 1024; // 128MB
14
52
  const TEAMMATE_MSG_RE = /<teammate-message[^>]*>[\s\S]*?<\/teammate-message>/g;
15
53
 
16
54
  /** Strip SDK teammate-message XML tags from assistant text */
@@ -197,11 +235,41 @@ export function nestChildEventsAcrossMessages(messages: { content: string; event
197
235
  }
198
236
  }
199
237
 
238
+ /**
239
+ * How much of a transcript's tail a whole-history parse may read, as a byte
240
+ * offset to start at. `0` means the file is small enough to read entirely.
241
+ *
242
+ * A window rather than a refusal, because refusing is not the mitigation it
243
+ * looks like: `getFullMessages` falls back to the SDK's own walk, and that
244
+ * reads the same file — measured 3.1s on a 128MB fixture the guard had just
245
+ * declined to open. It also leaves the session unsearchable, which is the
246
+ * opposite of what the indexer was widened to do. Reading the tail bounds the
247
+ * work at `maxBytes` and indexes the newest part of the session, which is the
248
+ * part a search is most likely reaching for.
249
+ *
250
+ * Answers on `stat` alone, never by opening the file: the caller asks this once
251
+ * per stale session. A file that has vanished reads as `0` — let the parse
252
+ * report that in its own words rather than inventing a window into nothing.
253
+ */
254
+ export function fullParseWindow(filePath: string, maxBytes: number = FULL_PARSE_MAX_BYTES): number {
255
+ try {
256
+ const { size } = statSync(filePath);
257
+ return size > maxBytes ? size - maxBytes : 0;
258
+ } catch {
259
+ return 0;
260
+ }
261
+ }
262
+
200
263
  /**
201
264
  * Validate JSONL path — must be under ~/.claude/ (prevents arbitrary file reads).
202
265
  * Throws Error with descriptive message. Returns resolved realpath on success.
266
+ *
267
+ * `maxBytes` is a parameter only so a test can assert the boundary against a
268
+ * one-byte-over fixture: a real 256MB transcript is not worth committing, and a
269
+ * bound asserted nowhere is how this one came to say 50MB while rejecting at a
270
+ * different number.
203
271
  */
204
- export function validateJsonlPath(inputPath: string): string {
272
+ export function validateJsonlPath(inputPath: string, maxBytes = MAX_FILE_SIZE): string {
205
273
  if (!inputPath) throw new Error("jsonlPath is required");
206
274
  // Reject obvious traversal attempts before resolution
207
275
  if (inputPath.includes("\0")) throw new Error("Invalid path: denied");
@@ -227,16 +295,70 @@ export function validateJsonlPath(inputPath: string): string {
227
295
 
228
296
  const stat = statSync(real);
229
297
  if (!stat.isFile()) throw new Error("Not a regular file");
230
- if (stat.size > MAX_FILE_SIZE) {
231
- throw new Error(`File too large: ${Math.round(stat.size / 1024 / 1024)}MB exceeds 50MB limit`);
298
+ // Interpolated, never spelled out: the literal "50MB" outlived the 50MB cap
299
+ // and sent the next reader looking in the wrong place for a file that was
300
+ // rejected at 256MB.
301
+ if (stat.size > maxBytes) {
302
+ throw new Error(
303
+ `File too large: ${Math.round(stat.size / 1024 / 1024)}MB exceeds ` +
304
+ `${Math.round(maxBytes / 1024 / 1024)}MB limit`,
305
+ );
232
306
  }
233
307
  return real;
234
308
  }
235
309
 
310
+ /**
311
+ * Yield a file's lines without holding the file in memory.
312
+ *
313
+ * `Bun.file().text()` plus `split("\n")` costs the whole transcript twice over —
314
+ * 277MB resident for a 77MB file, which is what forced a cap low enough to
315
+ * reject real sessions. Peak here is one chunk plus one line.
316
+ */
317
+ async function* readLines(filePath: string, fromByte = 0): AsyncGenerator<string> {
318
+ const decoder = new TextDecoder();
319
+ // Explicit reader rather than `for await` over the stream: the DOM lib's
320
+ // ReadableStream is not typed as async-iterable, and the `finally` is what
321
+ // releases it when a caller breaks early on `beforeUuid`.
322
+ // One byte *before* the offset, so the first line the split produces is
323
+ // always the tail of the record the offset fell inside — the empty string
324
+ // when it fell exactly on a newline. That makes the drop below unconditional
325
+ // instead of a boundary case, and a boundary case here silently costs a whole
326
+ // record rather than a fragment.
327
+ const file = fromByte > 0 ? Bun.file(filePath).slice(fromByte - 1) : Bun.file(filePath);
328
+ const reader = file.stream().getReader();
329
+ // A fragment is not always unparseable — the tail of one record is sometimes
330
+ // a complete smaller document — so it has to go by position rather than be
331
+ // left for `JSON.parse` to reject, or it enters as an invented message.
332
+ let dropPartial = fromByte > 0;
333
+ let buffered = "";
334
+ try {
335
+ while (true) {
336
+ const { done, value } = await reader.read();
337
+ if (done) break;
338
+ buffered += decoder.decode(value, { stream: true });
339
+ const lines = buffered.split("\n");
340
+ buffered = lines.pop() ?? "";
341
+ for (const line of lines) {
342
+ if (dropPartial) { dropPartial = false; continue; }
343
+ yield line;
344
+ }
345
+ }
346
+ } finally {
347
+ reader.releaseLock();
348
+ }
349
+ buffered += decoder.decode();
350
+ if (buffered && !dropPartial) yield buffered;
351
+ }
352
+
236
353
  /**
237
354
  * Read a JSONL transcript file, parse entries, apply merge/nest pipeline, return ChatMessage[].
238
355
  * Applies the same logic as ClaudeAgentSdkProvider.getMessages() but reads from file directly.
239
356
  *
357
+ * @param opts.oneSegment Return only the stretch since the previous compaction,
358
+ * rather than everything before `beforeUuid`.
359
+ * @param opts.fromByte Start reading at this offset instead of at the start,
360
+ * dropping the partial record it lands in. `fullParseWindow`
361
+ * is what computes it; nothing else should.
240
362
  * @param beforeUuid If provided, stop parsing at the line with this uuid (exclusive).
241
363
  * Used for the expand-compact feature: Claude's compact summary references
242
364
  * the CURRENT session file (pre+summary+post), so we truncate at the
@@ -245,10 +367,10 @@ export function validateJsonlPath(inputPath: string): string {
245
367
  export async function parseJsonlTranscript(
246
368
  filePath: string,
247
369
  beforeUuid?: string,
370
+ opts?: { oneSegment?: boolean; fromByte?: number },
248
371
  ): Promise<ChatMessage[]> {
249
- const text = await Bun.file(filePath).text();
250
372
  const parsed: ChatMessage[] = [];
251
- for (const line of text.split("\n")) {
373
+ for await (const line of readLines(filePath, opts?.fromByte ?? 0)) {
252
374
  const trimmed = line.trim();
253
375
  if (!trimmed) continue;
254
376
  let entry: any;
@@ -258,6 +380,16 @@ export async function parseJsonlTranscript(
258
380
  continue; // skip malformed lines defensively
259
381
  }
260
382
  if (beforeUuid && entry.uuid === beforeUuid) break; // stop at compact boundary (exclusive)
383
+ // One segment only: drop everything collected so far each time another
384
+ // compaction is passed, so what survives is the stretch between the last
385
+ // compaction and `beforeUuid`. Without this a single expand answers with
386
+ // the whole history before the boundary — 5626 messages on a session with
387
+ // thirteen compactions, prepended into a view already carrying 3553 DOM
388
+ // nodes. The summary that resets it is itself a `user` record, so it is
389
+ // pushed straight back and becomes the segment's first message — which is
390
+ // what carries the transcript path, and therefore what lets the next
391
+ // scroll expand the segment before it.
392
+ if (opts?.oneSegment && entry.isCompactSummary) parsed.length = 0;
261
393
  if (entry.type !== "user" && entry.type !== "assistant") continue;
262
394
  if (!entry.uuid || !entry.message) continue;
263
395
  parsed.push(parseSessionMessage(entry));
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Keeping `ppm.log` bounded, and keeping one line out of it twice.
3
+ *
4
+ * Two separate defects sat in the same file. On this machine it had reached
5
+ * **276 MB / 1,395,012 lines**, and of those only 532,792 carried the
6
+ * `[timestamp] [LEVEL]` prefix — the other 862,220 (62%) were the *same events*
7
+ * arriving by a second route.
8
+ *
9
+ * The second route is `supervisor.ts` spawning the server with
10
+ * `stdio: ["ignore", logFd, logFd]`, where `logFd` is `ppm.log` itself. So the
11
+ * server's own `console.log` already lands in the log through fd 1, and
12
+ * `setupLogFile()` then appends a formatted copy of the same line. That is the
13
+ * duplication — and it is not a harmless one, because only the appended copy
14
+ * goes through `redactSecrets()`. The raw stdout copy does not, which is how
15
+ * two lines matching `Token: <value>` are sitting in the log right now. The
16
+ * redaction was never wrong; it only ever covered one of the two doors.
17
+ *
18
+ * `fdWritesTo` is how a writer notices that its own stdout already reaches the
19
+ * file it was about to append to, so it can stop doing one of the two.
20
+ */
21
+
22
+ import { fstatSync, statSync, copyFileSync, truncateSync, renameSync, rmSync, existsSync } from "node:fs";
23
+
24
+ /** Rotate once the log passes this. */
25
+ export const MAX_LOG_BYTES = 20 * 1024 * 1024;
26
+
27
+ /** How many previous logs to keep (`ppm.log.1` … `ppm.log.3`). */
28
+ export const LOG_GENERATIONS = 3;
29
+
30
+ /**
31
+ * Set by the supervisor on any child it hands `stdio: [\"ignore\", logFd, logFd]`.
32
+ *
33
+ * The inode comparison below cannot answer that question on Windows: `fstat` reports an inode
34
+ * of 0 for most handles there, so `fdWritesTo` says "not the same file" and both writers keep
35
+ * writing — which means the duplicate, **unredacted** stdout copy this module exists to remove
36
+ * was still being written on the one platform PPM is most often installed on. Detection cannot
37
+ * be made to work there, but the process doing the wiring knows for certain, so it says so.
38
+ */
39
+ export const STDIO_IS_LOG_ENV = "PPM_STDIO_IS_LOG";
40
+
41
+ /**
42
+ * Whether console output on `fd` already reaches `filePath` — by what the spawner declared,
43
+ * or, for a process nobody declared anything to, by inode.
44
+ */
45
+ export function stdioIsLogFile(fd: number, filePath: string, env: NodeJS.ProcessEnv = process.env): boolean {
46
+ return env[STDIO_IS_LOG_ENV] === "1" || fdWritesTo(fd, filePath);
47
+ }
48
+
49
+ /**
50
+ * Drop the marker once it has been read.
51
+ *
52
+ * It is inherited like any other variable, and PPM spawns terminals, SDK children and
53
+ * `ppm` CLI invocations with its own environment — every one of which would otherwise start
54
+ * life believing its stdout is the log file and silently drop it.
55
+ */
56
+ export function consumeStdioIsLogEnv(env: NodeJS.ProcessEnv = process.env): void {
57
+ delete env[STDIO_IS_LOG_ENV];
58
+ }
59
+
60
+ /**
61
+ * Whether writing to `fd` lands in `filePath` — same inode, same device.
62
+ *
63
+ * Answers "is my stdout already this log file?" for a process the supervisor did not label.
64
+ *
65
+ * Returns false whenever it cannot be sure, which on Windows is always — see
66
+ * `STDIO_IS_LOG_ENV`, which is how the supervisor's children get a real answer there.
67
+ */
68
+ export function fdWritesTo(fd: number, filePath: string): boolean {
69
+ try {
70
+ const a = fstatSync(fd);
71
+ if (a.ino === 0) return false;
72
+ const b = statSync(filePath);
73
+ return a.ino === b.ino && a.dev === b.dev;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Truncate the log in place once it is oversized, keeping N previous copies.
81
+ *
82
+ * In place, and that is the whole design constraint: both the supervisor and
83
+ * the server child hold file descriptors opened on this inode with `O_APPEND`.
84
+ * Renaming the file would leave every one of those descriptors writing into the
85
+ * renamed file for the rest of the process's life — the log would appear to
86
+ * rotate and then never grow again, while `ppm.log.1` quietly became the real
87
+ * log. Copying the contents out and truncating the original keeps the inode,
88
+ * so an `O_APPEND` writer simply resumes at offset 0.
89
+ *
90
+ * The cost is the one `logrotate` calls `copytruncate` and accepts for the same
91
+ * reason: a line written between the copy and the truncate is lost. The window
92
+ * is a few milliseconds, once per `maxBytes` of log.
93
+ *
94
+ * Returns whether it rotated.
95
+ */
96
+ export function rotateIfOversized(
97
+ filePath: string,
98
+ maxBytes: number = MAX_LOG_BYTES,
99
+ generations: number = LOG_GENERATIONS,
100
+ ): boolean {
101
+ let size: number;
102
+ try {
103
+ size = statSync(filePath).size;
104
+ } catch {
105
+ return false; // no log yet
106
+ }
107
+ if (size <= maxBytes) return false;
108
+
109
+ try {
110
+ // Oldest first, or a shift would overwrite the generation it is about to move.
111
+ rmSync(`${filePath}.${generations}`, { force: true });
112
+ for (let i = generations - 1; i >= 1; i--) {
113
+ const from = `${filePath}.${i}`;
114
+ if (existsSync(from)) renameSync(from, `${filePath}.${i + 1}`);
115
+ }
116
+ copyFileSync(filePath, `${filePath}.1`);
117
+ truncateSync(filePath, 0);
118
+ return true;
119
+ } catch (e) {
120
+ // A log that cannot be rotated must not take the process with it — but it must not do so
121
+ // in silence either. `truncateSync` on a file the supervisor and the server child both
122
+ // hold open fails with EBUSY on Windows, and a blanket `catch` turned "this log is
123
+ // unbounded from here on" into no signal at all. Once per process, because the caller is
124
+ // a one-minute timer and the second failure says nothing the first did not.
125
+ if (!rotateFailureReported) {
126
+ rotateFailureReported = true;
127
+ console.warn(
128
+ `[log-rotate] Could not rotate ${filePath} (${(e as Error).message}) — it will keep `
129
+ + "growing past its cap. On Windows this is usually another process holding the file open.",
130
+ );
131
+ }
132
+ return false;
133
+ }
134
+ }
135
+
136
+ /** One warning per process; see the `catch` above. Exported for tests, which need to be able
137
+ * to observe the *first* failure rather than whichever one happens to run first. */
138
+ export function resetRotateFailureWarning(): void {
139
+ rotateFailureReported = false;
140
+ }
141
+
142
+ let rotateFailureReported = false;
@@ -16,6 +16,7 @@
16
16
  * grepping `ffmpeg -encoders`: the list only says the build was compiled with
17
17
  * NVENC/QSV support, not that a compatible GPU/driver is present.
18
18
  */
19
+ import { readdirSync } from "node:fs";
19
20
 
20
21
  export interface FfmpegCapabilities {
21
22
  ffmpeg: string | null;
@@ -29,10 +30,38 @@ function encoderCandidates(): string[] {
29
30
  switch (process.platform) {
30
31
  case "win32": return ["h264_nvenc", "h264_qsv", "h264_amf", "libx264"];
31
32
  case "darwin": return ["h264_videotoolbox", "libx264"];
32
- default: return ["h264_nvenc", "h264_qsv", "libx264"];
33
+ // VAAPI sits after QSV (Intel does better through QSV) but before libx264, because it is
34
+ // the only hardware H.264 path an AMD Linux host has — `h264_amf` is Windows-only, so
35
+ // without VAAPI every Radeon machine fell all the way back to software encoding.
36
+ default: return ["h264_nvenc", "h264_qsv", "h264_vaapi", "libx264"];
33
37
  }
34
38
  }
35
39
 
40
+ /** The DRM render node VAAPI should use, or null when the host exposes none (no GPU driver,
41
+ * a container without `/dev/dri`). Picks the lowest `renderD*` rather than hardcoding
42
+ * `renderD128`: it is only the first node by convention and a host whose first GPU is not
43
+ * render-capable starts at `renderD129`. */
44
+ function vaapiRenderNode(): string | null {
45
+ try {
46
+ const nodes = readdirSync("/dev/dri").filter((n) => n.startsWith("renderD")).sort();
47
+ return nodes[0] ? `/dev/dri/${nodes[0]}` : null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /** Args VAAPI needs *before* `-i` (it opens the device during input setup), empty for every
54
+ * other encoder. Separate from `encoderArgs` because argv position matters here. */
55
+ export function encoderDeviceArgs(encoder: string): string[] {
56
+ if (encoder !== "h264_vaapi") return [];
57
+ const node = vaapiRenderNode();
58
+ return node ? ["-vaapi_device", node] : [];
59
+ }
60
+
61
+ /** VAAPI encodes from GPU-resident frames, so the chain has to convert and upload first.
62
+ * Callers that build their own `-vf` must append this rather than passing a second `-vf`. */
63
+ export const VAAPI_UPLOAD_FILTER = "format=nv12,hwupload";
64
+
36
65
  /** Per-encoder quality/speed flags tuned for "watch a file inside the IDE" latency. */
37
66
  export function encoderArgs(encoder: string): string[] {
38
67
  switch (encoder) {
@@ -40,6 +69,8 @@ export function encoderArgs(encoder: string): string[] {
40
69
  case "h264_qsv": return ["-c:v", "h264_qsv", "-preset", "veryfast", "-global_quality", "26"];
41
70
  case "h264_amf": return ["-c:v", "h264_amf", "-quality", "speed", "-rc", "cqp", "-qp_i", "24", "-qp_p", "26"];
42
71
  case "h264_videotoolbox": return ["-c:v", "h264_videotoolbox", "-b:v", "6M", "-realtime", "1"];
72
+ // No `-crf`/`-global_quality`: VAAPI's rate control is `-qp` on this encoder.
73
+ case "h264_vaapi": return ["-c:v", "h264_vaapi", "-qp", "26"];
43
74
  default: return ["-c:v", "libx264", "-preset", "veryfast", "-crf", "23", "-maxrate", "8M", "-bufsize", "16M"];
44
75
  }
45
76
  }
@@ -50,8 +81,12 @@ async function encoderWorks(ffmpeg: string, encoder: string): Promise<boolean> {
50
81
  [
51
82
  ffmpeg, "-hide_banner", "-loglevel", "error", "-nostdin",
52
83
  // 256px: hardware encoders reject frames below ~145×49 (NVENC "Frame Dimension less than minimum").
84
+ ...encoderDeviceArgs(encoder),
53
85
  "-f", "lavfi", "-i", "color=c=black:s=256x256:r=30:d=0.2",
54
- "-frames:v", "3", "-pix_fmt", "yuv420p", ...encoderArgs(encoder),
86
+ "-frames:v", "3",
87
+ // VAAPI wants hardware frames and rejects a software `-pix_fmt` on the output.
88
+ ...(encoder === "h264_vaapi" ? ["-vf", VAAPI_UPLOAD_FILTER] : ["-pix_fmt", "yuv420p"]),
89
+ ...encoderArgs(encoder),
55
90
  "-f", "null", "-",
56
91
  ],
57
92
  { stdout: "ignore", stderr: "ignore", stdin: "ignore" },
@@ -124,7 +159,42 @@ export function getFfmpegCapabilities(): Promise<FfmpegCapabilities> {
124
159
  return cached;
125
160
  }
126
161
 
162
+ let cachedWorking: Promise<string[]> | null = null;
163
+
164
+ /**
165
+ * **Every** candidate encoder that actually encodes on this host, in preference order — not
166
+ * just the first one, which is what `getFfmpegCapabilities().encoder` stops at.
167
+ *
168
+ * Separate call, and separately cached, on purpose: `detect()` short-circuits because playing a
169
+ * video inside the IDE has no use for the also-works list, and probing the rest costs one real
170
+ * encode each (plus a 10s timeout apiece against a hung driver). Only the remote-desktop codec
171
+ * picker wants the full list, so only it pays.
172
+ */
173
+ export function workingEncoders(): Promise<string[]> {
174
+ if (!cachedWorking) {
175
+ cachedWorking = (async () => {
176
+ const { ffmpeg } = await getFfmpegCapabilities();
177
+ if (!ffmpeg) return [];
178
+ const found: string[] = [];
179
+ for (const candidate of encoderCandidates()) {
180
+ if (await encoderWorks(ffmpeg, candidate)) found.push(candidate);
181
+ }
182
+ return found;
183
+ })().then((list) => {
184
+ // An empty list means either no ffmpeg or no working encoder; both are conditions the
185
+ // user can fix from the checklist, so neither is remembered.
186
+ if (list.length === 0) cachedWorking = null;
187
+ return list;
188
+ }).catch((e) => {
189
+ cachedWorking = null;
190
+ throw e;
191
+ });
192
+ }
193
+ return cachedWorking;
194
+ }
195
+
127
196
  /** Test hook: drop the cache so a different PATH can be probed. */
128
197
  export function resetFfmpegCapabilitiesCache(): void {
129
198
  cached = null;
199
+ cachedWorking = null;
130
200
  }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Host audio → client: one ffmpeg per session capturing what the host is *playing* (not its
3
+ * microphone) and encoding it as Opus in Ogg, which `remote-desktop-ogg-opus.ts` unwraps into
4
+ * packets the browser's `AudioDecoder` takes directly.
5
+ *
6
+ * The hard part is not the encode, it is that only one of the three platforms can capture its
7
+ * own output without help:
8
+ *
9
+ * - **Linux**: PulseAudio and PipeWire both expose every sink's *monitor* as a capture source,
10
+ * and `@DEFAULT_MONITOR@` resolves to the current default sink's — so this follows the user
11
+ * switching from speakers to headphones with no re-detection. Needs ffmpeg built with
12
+ * `pulse` (checked, not assumed: a minimal build has ALSA only, and `-f pulse` on one of
13
+ * those fails at spawn with "Unknown input format").
14
+ * - **Windows**: ffmpeg has no WASAPI loopback input. The only route is DirectShow, and a
15
+ * loopback DirectShow device is something the host must already have — "Stereo Mix", which
16
+ * most drivers ship disabled, or a virtual cable. So this reports unavailable with the
17
+ * device name to enable rather than spawning an ffmpeg that exits instantly.
18
+ * - **macOS**: avfoundation can only capture real inputs; there is no system-audio device at
19
+ * all without a loopback driver (BlackHole). Same treatment.
20
+ *
21
+ * `-application lowdelay` + `-frame_duration 20` keeps the encoder's own delay at one frame,
22
+ * and `-page_duration 20000` is load-bearing: the Ogg muxer's default is **one second**, which
23
+ * measured 2000 ms between writes on this host — two seconds of audio latency against video
24
+ * arriving in twenty milliseconds. With it, pages land every ~40 ms.
25
+ */
26
+ import { existsSync } from "node:fs";
27
+ import { getFfmpegCapabilities } from "../media-transcode/ffmpeg-capabilities.ts";
28
+ import { detectLinuxSession, linuxSessionEnv } from "./remote-desktop-linux-session.ts";
29
+ import { OggOpusDemuxer } from "./remote-desktop-ogg-opus.ts";
30
+
31
+ /** Opus always decodes at 48 kHz whatever the source rate was. */
32
+ export const AUDIO_SAMPLE_RATE = 48_000;
33
+ const AUDIO_BITRATE = "96k";
34
+ /** One Opus frame. Also the Ogg page duration, in microseconds, for the reason above. */
35
+ const FRAME_MS = 20;
36
+
37
+ export interface AudioSupport {
38
+ available: boolean;
39
+ /** Why not, and what the user can do about it. Null when audio works. */
40
+ reason: string | null;
41
+ }
42
+
43
+ /** The ffmpeg input for this host's own output, or null when it has none. */
44
+ export function audioInputArgs(
45
+ platform: NodeJS.Platform = process.platform,
46
+ hasPulse = pulseAvailable(),
47
+ ): string[] | null {
48
+ if (platform !== "linux" || !hasPulse) return null;
49
+ return ["-f", "pulse", "-i", "@DEFAULT_MONITOR@"];
50
+ }
51
+
52
+ /** Is there a PulseAudio/PipeWire server to talk to? The socket, not `pactl`: the binary is a
53
+ * separate package (the same `xrandr` vs `libXrandr` trap), and a PPM started by its systemd
54
+ * unit has `XDG_RUNTIME_DIR` even when it has no `DISPLAY`. */
55
+ export function pulseAvailable(runtimeDir = process.env.XDG_RUNTIME_DIR): boolean {
56
+ if (!runtimeDir) return false;
57
+ return existsSync(`${runtimeDir}/pulse/native`);
58
+ }
59
+
60
+ /** Whether this ffmpeg build has the `pulse` demuxer at all — a minimal build has ALSA only,
61
+ * and `-f pulse` there fails at spawn rather than degrading. */
62
+ export async function ffmpegHasPulse(ffmpeg: string): Promise<boolean> {
63
+ try {
64
+ const proc = Bun.spawn([ffmpeg, "-hide_banner", "-demuxers"], {
65
+ stdout: "pipe", stderr: "ignore", stdin: "ignore",
66
+ });
67
+ const text = await new Response(proc.stdout).text();
68
+ await proc.exited;
69
+ // `ffmpeg -demuxers` writes " D d pulse Pulse audio input" — flags, then a
70
+ // *space-separated* name. Anchoring on the flag column rather than on the whole line keeps
71
+ // this working for both the `-demuxers` and `-devices` spellings (" DE pulse").
72
+ return /^\s*D\S*\s+(?:\S+\s+)?pulse\s/m.test(text);
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ /** Can this host stream its own audio, and if not, what should the user do. */
79
+ export async function audioSupport(platform: NodeJS.Platform = process.platform): Promise<AudioSupport> {
80
+ if (platform === "win32") {
81
+ return {
82
+ available: false,
83
+ reason: "Windows has no loopback capture ffmpeg can use — enable \"Stereo Mix\" in Sound "
84
+ + "settings, or install a virtual audio cable.",
85
+ };
86
+ }
87
+ if (platform === "darwin") {
88
+ return {
89
+ available: false,
90
+ reason: "macOS has no system-audio input — install a loopback driver such as BlackHole.",
91
+ };
92
+ }
93
+ if (platform !== "linux") return { available: false, reason: "Not supported on this platform." };
94
+ if (!pulseAvailable()) {
95
+ return { available: false, reason: "No PulseAudio/PipeWire server is running on the host." };
96
+ }
97
+ const { ffmpeg } = await getFfmpegCapabilities();
98
+ if (!ffmpeg) return { available: false, reason: "ffmpeg is not installed." };
99
+ if (!await ffmpegHasPulse(ffmpeg)) {
100
+ return { available: false, reason: "This ffmpeg build has no PulseAudio support." };
101
+ }
102
+ return { available: true, reason: null };
103
+ }
104
+
105
+ /** Full argv, pure so it can be asserted without spawning. */
106
+ export function buildAudioArgs(ffmpeg: string, input: string[]): string[] {
107
+ return [
108
+ ffmpeg, "-hide_banner", "-loglevel", "error", "-nostdin",
109
+ "-fflags", "nobuffer", "-flags", "low_delay",
110
+ ...input,
111
+ "-c:a", "libopus", "-b:a", AUDIO_BITRATE,
112
+ // `lowdelay` disables the encoder's look-ahead; without it Opus adds its own ~100ms.
113
+ "-application", "lowdelay", "-frame_duration", String(FRAME_MS),
114
+ "-ar", String(AUDIO_SAMPLE_RATE), "-ac", "2",
115
+ "-page_duration", String(FRAME_MS * 1000),
116
+ "-flush_packets", "1", "-f", "ogg", "pipe:1",
117
+ ];
118
+ }
119
+
120
+ export interface AudioHandle {
121
+ stop(): void;
122
+ }
123
+
124
+ export interface StartAudioOptions {
125
+ /** One decoded Opus packet, ready for `AudioDecoder`. */
126
+ onPacket: (packet: Uint8Array) => void;
127
+ /** Channel layout, once the stream header has been parsed. Fires before the first packet. */
128
+ onStreamInfo: (info: { channels: number; sampleRate: number }) => void;
129
+ onExit?: (code: number | null, reason?: string) => void;
130
+ }
131
+
132
+ /** Start capturing host audio, or return null when this host cannot (see `audioSupport`).
133
+ * Never throws for an unsupported host: audio is an extra, and a session must not fail to
134
+ * start because the machine has no loopback device. */
135
+ export async function startAudioCapture(opts: StartAudioOptions): Promise<AudioHandle | null> {
136
+ const { ffmpeg } = await getFfmpegCapabilities();
137
+ if (!ffmpeg) return null;
138
+ const support = await audioSupport();
139
+ if (!support.available) return null;
140
+ const input = audioInputArgs();
141
+ if (!input) return null;
142
+
143
+ const session = process.platform === "linux" ? detectLinuxSession() : null;
144
+ const proc = Bun.spawn(buildAudioArgs(ffmpeg, input), {
145
+ stdout: "pipe",
146
+ stderr: "pipe",
147
+ stdin: "ignore",
148
+ ...(session ? { env: { ...process.env, ...linuxSessionEnv(session) } } : {}),
149
+ });
150
+
151
+ let stopped = false;
152
+ const stop = () => {
153
+ if (stopped) return;
154
+ stopped = true;
155
+ // SIGKILL for the same reason the video capture uses it — see `remote-desktop-capture.ts`.
156
+ if (!proc.killed) proc.kill("SIGKILL");
157
+ };
158
+
159
+ const stderrTail = new Response(proc.stderr).text().catch(() => "");
160
+ proc.exited.then(async (code) => {
161
+ const diedOnItsOwn = !stopped;
162
+ stopped = true;
163
+ let reason: string | undefined;
164
+ if (diedOnItsOwn && code !== 0 && code !== null) {
165
+ reason = (await stderrTail).trim().split("\n").slice(-2).join(" | ");
166
+ console.warn(`[remote-desktop] audio ffmpeg exited ${code}: ${reason}`);
167
+ }
168
+ opts.onExit?.(code, reason);
169
+ });
170
+
171
+ const demuxer = new OggOpusDemuxer();
172
+ let announced = false;
173
+ // Manual pull loop, never `for await`: cancelling this reader is what segfaults Bun on
174
+ // Windows (see `remote-desktop-capture.ts`'s header). `stop()` kills the process and the
175
+ // pending `read()` resolves done.
176
+ const reader = proc.stdout.getReader();
177
+ (async () => {
178
+ for (;;) {
179
+ const { done, value } = await reader.read();
180
+ if (done) return;
181
+ if (!value) continue;
182
+ const packets = demuxer.push(value);
183
+ const info = demuxer.info;
184
+ if (!announced && info) {
185
+ announced = true;
186
+ opts.onStreamInfo({ channels: info.channels, sampleRate: AUDIO_SAMPLE_RATE });
187
+ }
188
+ for (const p of packets) opts.onPacket(p);
189
+ }
190
+ })();
191
+
192
+ return { stop };
193
+ }