@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,47 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.22.0] - 2026-09-18
6
+
7
+ ### Added
8
+ - **The Git Graph's columns can be turned off** — right-click the table header, or Settings → Columns. Hiding one gives its width to the graph.
9
+ - **Review a whole branch against another as one list.** A feature branch with sixty commits could only be read one commit at a time, and the alternative — squashing them into one — is not a review. A new tab lists every file that differs between two refs, with the tree on the left, a diff on the right, and a per-file reviewed tick.
10
+ - Every file opens at the **merge base**, not at the base branch's tip. That is what a review means, and the Compare panel had been listing files one way and opening them the other, so the base branch's own commits rendered as deletions inside a branch review.
11
+ - A reviewed tick stores the file's head-side blob id rather than `true`. Keying it to the commit throws all progress away whenever a new one lands; a plain boolean keeps a file ticked after it has been rewritten, which marks unreviewed code as reviewed.
12
+ - **Remote Desktop works on Linux hosts.** It was Windows and macOS only. Which backend runs is chosen per *session* rather than per platform, because a Wayland session almost always runs XWayland too: XWayland sets `DISPLAY` and answers an X11 capture **successfully**, with a view of XWayland clients only — a black screen rather than an error. A session is looked up afresh every few seconds, so a desktop that starts after PPM does is found, and an X11 → Wayland re-login stops input being sent to a display that is gone.
13
+ - Keystrokes are sent as evdev codes rather than keysyms. Asking X for "the key that produces an a" answers with the physical Q on an AZERTY host, which is the wrong key on exactly the layouts where it matters.
14
+ - A host missing libX11 is told to install libX11, not libXtst: the capability probe reports each library separately, because `dlopen` resolves all symbols or none, and one absent optional symbol used to cost the host its mouse with a message pointing at the wrong package.
15
+ - **Event-loop lag is now measured, and attributed.** PPM serves every request, every WebSocket frame and all of the chat work from one process with one event loop, so anything synchronous holds up everything else for as long as it runs — which is why expanding a folder in the file explorer could sit on a spinner for seconds while a chat session was busy. That much was visible from outside (`/api/health`, which returns a literal, measured **18.9s** on a loaded instance against **0.12-0.65ms** on an idle one of the same binary), but it named no culprit, and nothing in PPM recorded it at all.
16
+ - A monitor samples the loop four times a second and records every tick that slips, together with the process CPU consumed across the same gap. That second number is the point: a stall we spent *running* is our own synchronous work and worth hunting in this code, while a stall we spent descheduled is the machine being oversubscribed — and no change to PPM fixes that one, moving work to a worker thread least of all, since there was no CPU to move it onto. The two are indistinguishable without it.
17
+ - Readable at `GET /api/system/event-loop`: how long the loop has been unavailable for, the worst single stall, and the recent ones with their cause and the resident set at the time.
18
+ - It holds its samples in memory and writes nothing to disk. `~/.ppm/ppm.log` is already 261 MB with 54% of its lines exact duplicates, and a diagnostic that feeds the problem it diagnoses is not one.
19
+
20
+ ### Fixed
21
+ - **Tooltips no longer appear under the pointer and stop the panel scrolling.** They opened with no delay, so merely crossing a toolbar fired one — and the box that appeared owned the wheel, because tooltip content is hoverable by default. They now wait half a second, close the moment the trigger is left, and ignore the pointer entirely.
22
+ - **The Git Graph is a list of commits again in a panel that is not wide.** With enough parallel branches the graph column grew to whatever the lanes needed and took the whole row: every commit message was zero pixels wide and the columns behind it were clipped off the edge with no way to scroll to them. The graph is now capped to what the panel can spare and pans sideways from a strip in its header, the message column has a floor it cannot go under, and a narrow panel drops date, hash and then changes instead of crushing what is left.
23
+ - **Discarding a hunk that had moved could discard the wrong one.** The Source Control panel sends back the position a hunk held when the list was drawn, and a file edited in the meantime moves them. Stage and unstage re-find the hunk by its content, which is right for them — but two identical blocks in one file have the same content, so discarding fell back to the first match and could throw away an edit the user never looked at. Discard now refuses rather than guesses, and says to reload and pick it again. It is the one of the three that cannot be undone.
24
+ - **A compacted chat could not be scrolled back into, permanently.** Transcripts over 50MB were refused outright and the route answered 403, which the browser had no way to explain — so a session with thirteen compactions, at 77MB, loaded nothing when you scrolled to the top, every time, for exactly the chats the feature exists for. The cap is 256MB, the message now quotes the size it actually enforces rather than a number from two limits ago, and the reader streams so the file never lands in memory whole.
25
+ - **Scrolling back through a compacted chat loads one segment at a time.** It used to answer with everything before the first boundary — 5626 messages at once on that same session, prepended into a view already holding 3553 nodes. Each click now opens the stretch behind the card it was on, and that stretch is headed by the previous boundary's card, so the walk keeps going. On a thread compacted more than once the old answer also skipped the middle stretch entirely, leaving it reachable from nothing at all.
26
+ - **Chat search missed most of a long session.** It indexed 179 of one session's 1084 messages, because the walk back through parent links stops at a compaction boundary — correct for the chat view, wrong for a search that is supposed to see the whole history.
27
+ - **A folder you expand in the explorer stays where it is.** Expanding a directory that was its parent's only child made the row you had just clicked *disappear*: it was joined onto its parent as one `.claude/agent-memory` line, and everything beneath it shifted a level left. VS Code does join single-child chains, but on the way *in* — you never see the intermediate row, so there is nothing there to vanish. PPM's version only joined once *both* rows were already expanded, so it fired one gesture too late, and the click that should have revealed a folder removed one instead. Every directory now gets its own row at its own depth, so the indentation reads as nesting again.
28
+ - **Gitignored files were unreadable in the explorer on the light themes.** The tree dims them, and it did that by fading the whole row to 40% opacity — which reads as one design decision covering both modes and is not one. Fading a near-white name on a dark panel keeps a usable gap; fading a near-black one on white collapses it. Measured against PPM's six built-in themes, a gitignored filename sat at **1.72–1.89:1 against the panel on the three light themes** and 2.07–2.23:1 on the dark ones: the first of those is not low contrast, it is a name that cannot be read, which is why a whole `.claude/` subtree looked blank on light and perfectly fine on dark. The state is now a *colour* rather than a fade — **3.59–5.21:1 across all six**, still clearly dimmer than an ordinary file — and the file icon keeps its own colours, because those glyphs are full-colour artwork with no `currentColor`, so any opacity on them is that same fade to the background.
29
+ - **A bug report no longer stalls the server for a third of a second, and anyone could ask for one.** `GET /api/logs/recent` returns the last 30 lines of `~/.ppm/ppm.log` so the login screen can show something useful when things go wrong. It did that by reading the whole file and splitting it: on the 276 MB log this was found on, **264 ms to read plus 92 ms to split — 356 ms of a completely blocked event loop and a 644 MB resident spike, to answer with 30 lines.** The cost grew with the log, and the route is registered *before* the auth middleware, so it could be asked for repeatedly by anyone who could reach the port. It now reads a bounded 64 KB from the end: **0.48 ms** on the same file, and the same on a file ten times the size.
30
+ - It also answers with 30 lines now rather than 29. A file ending in a newline splits to a trailing empty string, which counted against the total and then got trimmed away — the old spelling got away with it only because it always asked for 30.
31
+ - **Secrets were being written to `ppm.log` unredacted, by a second path nobody had noticed.** The supervisor spawns the server with `stdio: ["ignore", logFd, logFd]` where `logFd` is `ppm.log` itself, so every `console.log` already reached the log through file descriptor 1 — raw, with no timestamp and, critically, without passing through `redactSecrets()`, which only ever ran on the *appended* copy. The log held 1,395,012 lines of which only 532,792 carried the `[timestamp] [LEVEL]` prefix: the other **862,220 (62%) were the same events arriving unformatted and unredacted**, including lines matching `Token: <value>`. Where the console already reaches the file, the original call is now dropped and only the formatted, redacted line is written — one copy per event, and the safe one. Run from a terminal (`bun dev:server`) nothing changes: stdout is a tty, and both the terminal and the log get their line as before. Which copy is which cannot be worked out on Windows — `fstat` reports an inode of 0 for most handles there, so the two files never compare equal and both writers kept writing — so the supervisor now *declares* it to the child it wired up (`PPM_STDIO_IS_LOG`), and the child drops the variable once it has read it, since it spawns terminals and SDK children with its own environment.
32
+ - **`ppm.log` is now bounded.** It had reached 276 MB and was still growing. It is checked once a minute against a 20 MB cap and rotated through three generations, by the supervisor alone — it owns the descriptor every child was handed, and two processes truncating one file would race. Rotation *truncates in place* rather than renaming, which is the only thing that works here: the supervisor and every child hold descriptors opened on that inode with `O_APPEND`, and a rename would leave all of them writing into `ppm.log.1` for the rest of the process's life, so the log would appear to rotate once and then never grow again. Bounded where the truncate is permitted, which is the honest way to say it: a file another process holds open can fail the truncate with EBUSY — the usual case on Windows — and that used to land in a blanket `catch` and leave the log growing with no signal at all. It now warns once, naming the file and the error.
33
+ - **Opening a project no longer freezes the whole server for two to four seconds.** Registering a project's file watchers walked the directory tree with `readdirSync` and then called `fs.watch` on every directory it kept — both synchronous, both in one burst. On a 12,000-directory project that held the event loop for **2.95 seconds**: no request served, no WebSocket frame read, no chat token forwarded, for anyone, in any project. It was found by lining a lag sample up against `ppm.log` — a 2.95 s silence ending exactly on the `Started watching: … (12000 dirs)` line — and confirmed on two independent restarts at ratio 1.38, the ratio being over 1 because Bun runs those `readdirSync` calls on its file system thread pool while the main thread walks.
34
+ - The walk now hands the loop back every 64 directories. Measured on a 13,321-directory tree with a 5 ms timer running alongside: before, that timer fired **0 times** in 2.1 seconds; after, **174–189 times**, with the worst single pause down to **35–50 ms**. Wall time went from 2.1 s to 2.2–2.8 s, which is the trade being made on purpose — the walk is slower and the server stays answerable throughout.
35
+ - 64 was picked by sweeping it, not guessed: every 250 directories leaves a 134 ms pause, every 32 gets to 23 ms but costs 1.8 extra seconds of walking. The yield is `setImmediate`, which reaches the same 41 ms worst pause as `setTimeout(…, 0)` for 600 ms less wall time, and it has to be one of those two — `await Promise.resolve()` drains as a microtask and would leave the loop exactly as blocked while looking asynchronous.
36
+ - **What this changes for callers:** `startWatching()` no longer has the tree attached by the time it returns, so a change made in the window right after it is not reported. It returns a promise that resolves when coverage lands, and anything that touches the filesystem immediately afterwards has to await it. The walk also checks whether it has been closed after every pause — closing a project mid-walk used to be impossible and now is not, and a walk carrying on past it would attach watchers nothing will ever release.
37
+ - **Session debug info no longer holds the event loop while it counts a transcript.** `GET /chat/sessions/:id/debug` reports how many records a transcript has, and it worked that out by reading the whole file into a string and walking it with `charCodeAt` — 53 ms on a 35 MB transcript, up to ~95 ms at the 64 MB cap, during which nothing else in PPM could run. It now counts newline bytes off a stream. This is **not faster** (also 53 ms), which is the point worth stating: what changed is that the loop gets the thread back nine times during it, worst gap 11.8 ms, where before it got it back zero times. Measured with a 5 ms interval running alongside; awaiting the stream alone was *not* enough, because its chunks resolve as microtasks and a microtask queue drains without ever letting a timer run.
38
+ - **The file explorer stuttered when scrolled on a tablet or phone.** The tree was already virtualized, so this was never about how many rows exist — it was that the virtualizer's default mode ends every scroll event in a synchronous React re-render of every row on screen, *inside the scroll handler*. Measured over 1191 rows on a 6x-throttled CPU: 92ms of a 137ms long frame was the scroll listener, while style and layout together came to 4ms. The virtualizer now writes each row's position straight to the DOM and React only renders when the set of visible rows actually changes — the same division of labour VS Code's list has, where scrolling moves one container and leaves the rows alone.
39
+ - Frame time over the same scroll, same throttle: median **38.2ms → ~25ms**, frames missing 30fps **76 of 80 → 1**, long animation frames **2 → 0**. On a desktop it was already 60fps before and still is; this is a fix for the devices that were not.
40
+ - A row is also exactly the height the virtualizer is told it is now. 13px text with the inherited line-height made a 27.5px row against an estimate of 26, so every row scrolling into view corrected the total height underneath the scrollbar.
41
+ - And the row component's memo works: its action handler was rebuilt on every render, so the memo never once hit and rows re-rendered whatever had changed.
42
+ - **One context menu for the whole tree**, the way VS Code's explorer has a single `onContextMenu` rather than a menu per row. Every mounted row was constructing this menu's 39 items on every render, and subscribing to a fifth store purely to feed it; which row the menu belongs to is now resolved from whichever one the click or press landed on. Frame time went the rest of the way: median **~25ms → ~18.5ms**, and no frame in three runs missed 30fps.
43
+ - A bug the first of these shipped, found and fixed: expanding a folder left the rows below it drawn on top of each other, at the offsets they had before the expansion. Writing positions to the DOM means the virtualizer has to find each row's element, and it looks one up by the row's *index* while React tracks it by its *path* — so a row that only moved down the list was never re-registered, and the newly inserted rows took over the slots it had. Both now identify a row the same way.
44
+ - **Clicking a folder no longer throws away the request that was already fetching it.** The tree fetches one directory level at a time and quietly prefetches the level ahead, so the next click renders from memory — but a click arriving while that prefetch was still in the air *aborted* it and asked for the identical thing again, which is the click the prefetch existed to make instant. It waits for the answer already coming now. Nothing else changes: the URL and the response were always the same, so there was never anything to preempt.
45
+
5
46
  ## [0.21.0] - 2026-09-17
6
47
 
7
48
  ### Added
package/CLAUDE.md CHANGED
@@ -109,17 +109,32 @@ Related: `configService.save()`/`set()` throw when `load()` has not run, because
109
109
  - **A font stack is a wish list, and on Linux it usually loses**: `-apple-system`, `BlinkMacSystemFont` and `Segoe UI` all miss there, `Menlo, Monaco, Consolas` misses too, and the generic fallbacks resolve through fontconfig to Liberation Sans — or, for a bare `sans-serif`, to Liberation *Serif*. Nothing errors: the app was simply set in an Arial clone and the editor in whatever `fc-match monospace` answered, on the platform PPM is most often self-hosted on. The first entry of every stack is now a face `main.tsx` actually imports (Geist, Monaspace Argon, Monaspace Krypton via `@fontsource*`), and `tests/unit/web/editor-font.test.ts` pairs the two, because a stack naming a font nothing ships reads identically to the fixed version in review. Four things are load-bearing: ghost text may only use a *different Monaspace family*, never a different typeface, because Monaco places every character at column times character width and a family with another advance slides out of the grid; that override needs `!important`, since Monaco's own `.monaco-editor .ghost-text-decoration` rule is the same specificity and its stylesheet is appended by the AMD loader after the bundle; xterm bakes the cell size and glyph atlas from `ctx.font` at `open()`, so a terminal opened during the first paint keeps the fallback resolved then for the whole session unless it re-measures on `document.fonts.ready`; and the Nerd Font the terminal asks for first is now bundled as icons only, per block (the bullet below). Fonts cost nothing until used (`@font-face` is fetched per `unicode-range`, on demand), stay out of the SW `globPatterns`, and must stay out of `COMPRESSIBLE` in `precompress-web.ts` — `woff2` is already compressed.
110
110
  - **A project folder is often not the repository, and every git surface has to agree on which one is**: a workspace folder can be a container whose children are the checkouts, and `simple-git` in a directory with no `.git` does not report "wrong directory" — it fails with git's own wording, which the UI renders as "not a git repository" for every panel at once. The scope is one `?repo=` parameter resolved in a middleware in `src/server/routes/git.ts` and one hook, `src/web/hooks/use-git-repo.ts`, on the browser side; `GET /git/repos` is the discovery. Four things are load-bearing. The parameter is **omitted** when the repository *is* the project root, so every URL an ordinary project builds stays byte-identical and the common case cannot regress on this path. A `repo=` outside the project must be a **400, never a fallback** to the project folder — a fallback runs the command one directory up and answers with *a* history, which is indistinguishable from a working feature until somebody acts on it. Paths cross in **both** directions: git run inside the repository reports and expects repository-relative paths while a tab's `filePath` is project-relative, so `rebaseStatusPaths` moves the *tree decorations* into the project's terms and the panel's own copy stays as git reported it, because that copy is what stage/unstage/discard/hunks send straight back. And `useGitRepo`'s return value is `useMemo`d: call sites name `gitRepo` in a dependency array, so a fresh object literal per render turns one `useCallback` into an effect that refetches forever. Watch the panel's own early returns too — `fetchStatus` deliberately does nothing until a repository is resolved, so a spinner gated only on `loading && !status` waits for a fetch that will never run and the chooser never renders.
111
111
  - **An extension is handed a path, and it resolves the PPM project by matching that path exactly**: open a git-graph panel on a sub-repository and `resolveProjectName` falls through to the directory's basename, which names no registered project — so the tab it opens builds `/api/project/<basename>/…` and 404s, while the file path it carries is repository-relative where a tab's is project-relative. `packages/ext-git-graph/src/project-scope.ts` picks the innermost project *containing* the path and rebases the file onto it, and `resolveFileTab` does both at once because resolving them separately is how one of them gets forgotten. The spawn allowlist is not in the way: `assertSafePath` accepts any directory under a registered project, so a sub-repository is a legal cwd. Related trap when testing this: the extension host's `fetch` base URL is `http://localhost:${configService.get("port")}`, **not** the port the server was actually started on, so a dev instance whose config port disagrees has every extension `fetch` fail silently into its fallback.
112
- - **A file icon theme is four separate traps, and every one of them fails silently**: PPM draws the real `vscode-icons` artwork, vendored at build time by `bun scripts/gen-file-icons.ts` into `src/web/lib/file-icons.generated.ts` (the name → glyph tables) and `src/web/styles/file-icons.generated.css` (one `background-image` class per glyph). Both are committed; re-run the script after editing its lists, and `tests/unit/web/file-icons.test.ts` fails if the two files drift, because a name with no class renders a correctly-sized span of nothing. First trap: `width`/`height` **do not apply to a non-replaced inline element**, so the icon span needs `inline-block` — the trees are flex rows and blockify it for free, while the tab strip wraps its icon in a `<span class="relative">` for the notification dot and there the icon measured 0×0 and simply was not there. An inline `<svg>` would have worked without saying so, which is why this only broke in one place. Second: `vscode-icons-js` answers with the **light-theme** drawing for 143 glyphs and offers no way to ask for the other one (`getIconForFile("a.json")` is `file_type_light_json`, full stop), and those are darker ink meant for a light workbench — `file-type-light-toml` has no `fill` at all, i.e. black on black. The generator canonicalises the class to the theme-independent name and emits the light drawing as a `:root.light` override, which costs +1.2 KiB brotli for +100 KiB raw because the two bodies differ by a colour and brotli collapses the duplication. Third: the mapping package is versioned apart from the artwork and lags it, so `Dockerfile`, `bun.lock`, `.env.local`, `Rakefile` and every `.mts` file drew the blank-page default while the right glyph sat unused in the bundlehence `OVERRIDES`, checked against the collection at generation time. Fourth: a byte budget cuts by weight, not by worth. At 1800 bytes it dropped `file-type-reactts` (1833) and with it every `.tsx` file in a TypeScript repository, plus `.jsx`, `.go` and `.rs`. And measure the right number: the generated stylesheet is big enough that the bundler gives it its own chunk (`assets/file-icons-*.css`, 76 KiB brotli), which is outside the service worker's `globPatterns` — so it never shows up in `precache N entries` and only `precompress-web.ts`'s `.br` sibling tells you what it costs.
112
+ - **A file icon theme is five separate traps, and every one of them fails silently**: PPM draws the real `vscode-icons` artwork, vendored at build time by `bun scripts/gen-file-icons.ts` into `src/web/lib/file-icons.generated.ts` (the name → glyph tables) and `src/web/styles/file-icons.generated.css` (one `background-image` class per glyph). Both are committed; re-run the script after editing its lists, and `tests/unit/web/file-icons.test.ts` fails if the two files drift, because a name with no class renders a correctly-sized span of nothing. First trap: `width`/`height` **do not apply to a non-replaced inline element**, so the icon span needs `inline-block` — the trees are flex rows and blockify it for free, while the tab strip wraps its icon in a `<span class="relative">` for the notification dot and there the icon measured 0×0 and simply was not there. An inline `<svg>` would have worked without saying so, which is why this only broke in one place. Second: `vscode-icons-js` answers with the **light-theme** drawing for 143 glyphs and offers no way to ask for the other one (`getIconForFile("a.json")` is `file_type_light_json`, full stop), and those are darker ink meant for a light workbench — `file-type-light-toml` has no `fill` at all, i.e. black on black. The generator canonicalises the class to the theme-independent name and emits the light drawing as a `:root.light` override, which costs +1.2 KiB brotli for +100 KiB raw because the two bodies differ by a colour and brotli collapses the duplication. Third: the *mapping* is not the artwork's package. It is built from the extension's own `src/iconsManifest` (pinned tag + SHA-256 per file), because the third-party `vscode-icons-js` repackaging lags it badly — its table of multi-dot extensions holds **41** entries against the manifest's 127 — and it has never held `controller.ts`, `module.ts` or `service.ts` in any of its tables, which is why `app.controller.ts`, `app.module.ts` and `app.service.ts` drew the plain TypeScript glyph while the Nest artwork sat unused in the bundle. It is still read *second*, and that is not laziness: the manifest associates a great many types by **VS Code language id** and nothing else (the `dotenv` entry has `extensions: []`; `.cc`, `.htm`, `.conf`, `.kts` reach their icon only because the editor already knows which language owns them), so a manifest-only port silently lost 36 names that previously had an icon. Three consequences. The sources cascade **per key, not per table** — merging first and resolving after loses a key outright when the winning source names a glyph that is missing or over budget, which is how `cargo.lock` ended up blank while the second source's answer sat right there. The manifest's *language* layer ranks **below** `vscode-icons-js`'s flattening of VS Code's real associations, because `knownExtensions` turns "this language is known by .css" into "a .css file is this icon" and `tailwindcss` also claims `css` and sorts after it — every stylesheet in the app came out with the Tailwind logo. And the legacy tables answer with `file_type_light_*` names too, so the `light_` prefix has to be stripped there as well or trap two comes back through the side door. Fourth, and the reason `.service.ts` needs a project to answer it: `nest_*` and `ng_*` ship `disabled: true` upstream, behind two presets that are **mutually exclusive** they claim the same six suffixes (`module`, `service`, `guard`, `pipe`, `interceptor`, `controller`) and `ng_` sorts after `nest_`, so enabling both hands a NestJS repository the Angular artwork. They are emitted as *overlays* and `project-framework-store.ts` picks one per project from a `package.json` dependency; `FileIcon` subscribes rather than taking a prop, because two of the eight call sites go through `fileIconElement`, which hands out a component with no project in scope. Fifth: a byte budget cuts by weight, not by worth. At 1800 bytes it dropped `file-type-reactts` (1833) and with it every `.tsx` file in a TypeScript repository, plus `.jsx`, `.go` and `.rs`. And measure the right number: the generated stylesheet is big enough that the bundler gives it its own chunk (`assets/file-icons-*.css`, 382 KiB brotli for the full 1193-glyph port), which is outside the service worker's `globPatterns` — so it never shows up in `precache N entries` and only `precompress-web.ts`'s `.br` sibling tells you what it costs.
113
113
  - **Icons come from `@/lib/icons`, never from `lucide-react`, and mixing the two is invisible until you look twice**: the app's chrome is Fluent System Icons at 20px Regular (the set `miguelsolorio.fluent-icons` puts over VS Code's product icons), vendored by `bun scripts/gen-product-icons.ts` into `src/web/lib/icons.generated.tsx` and built on the one component in `src/web/lib/fluent-icon.tsx`. 207 of the 222 names resolve; the other 15 stay on lucide on purpose and `tests/unit/web/product-icons.test.ts` pins that list, because "fix the missing icon by falling back" is how an icon set stops being one. Four things to know. The two sets share almost no vocabulary — lucide's `X` is Fluent's `dismiss`, `Check` is `checkmark`, `Trash2` is `delete`, `Plus` is `add`, `RefreshCw` is `arrow-sync`, and only 72 names kebab-case straight across — so `MAP` in the script is a hand-checked table, and every entry is verified against the collection at generation time rather than trusted. The glyphs keep their **20px** viewBox rather than being re-boxed to lucide's 24: Fluent's outlines are drawn for that grid, and the app sizes icons with Tailwind `size-*` classes anyway, which beat the width/height attributes because they are CSS. `strokeWidth` is accepted and *dropped* — these are filled outlines with nothing to weight, and spreading it would put a dead attribute in the DOM and make React warn about `absoluteStrokeWidth` on every render. And the identifying hook is `data-icon="<name>"`, not a class: lucide put the same information in `class="lucide lucide-circle-check"`, which is what let a tool-card test assert "this card is spinning" by string-matching the markup — an attribute keeps that possible without offering stylesheets a class name to start depending on. Related and separate: the *file* icons are a different system (`@/lib/file-icons`, the entry above), because those are full-colour artwork with no `currentColor` to inherit.
114
- - **A banding rule qualified with an id outranks hover and selection**: `#commit-list .commit-row:nth-child(even)` is (1,2,0) against `.commit-row:hover` and `.commit-row.selected` at (0,2,0), so every *other* row silently stopped highlighting on hover and when selected — the kind of thing that reads as "sometimes it doesn't work". Zebra striping has to be written at the same specificity and earlier in the file, and a test asserts both. In the same stylesheet, the graph column's `@media (max-width: 640px)` block must come after the `@media (pointer: coarse)` block, which keeps all six columns and scrolls them sideways correct for a tablet and wrong for a phone.
114
+ - **A banding rule qualified with an id outranks hover and selection**: `#commit-list .commit-row:nth-child(even)` is (1,2,0) against `.commit-row:hover` and `.commit-row.selected` at (0,2,0), so every *other* row silently stopped highlighting on hover and when selected — the kind of thing that reads as "sometimes it doesn't work". Zebra striping has to be written at the same specificity and earlier in the file, and a test asserts both. In the same stylesheet, the phone block (`@media (max-width: 640px)`) must come after the `@media (pointer: coarse)` block and after the two width tiers (900px drops date and hash, 760px drops changes), because the later block is what decides a tie.
115
+ - **The Git Graph's column widths are one number owned by JS, and the message column is what pays for getting it wrong**: the commit row is a flexbox whose six other cells are all pinned by a `min-width`, so `.col-message` is the only thing that can shrink — and it was written `min-width: 0`. The graph column was then set to the SVG's *intrinsic* width (`28 + (lanes − 1) × 22`), with no relation to the panel, so from about 22 parallel lanes up in a 1020px panel the message column was zero pixels wide and the columns behind it were clipped away with `overflow-x: hidden` over them. The fix is a cap (`graphColCap()` measures the header's own cells, so a hidden column hands its pixels over without anything having to know which columns exist) plus a floor (`MESSAGE_MIN_W`, interpolated into both the CSS and the script from one constant). Three things are load-bearing. A cap without a **clip** is not a fix: the SVG is `position: absolute` and the rows are static, so it paints *above* them — an uncapped overlay draws branch lines through the messages, which is why `#graph-clip` exists and why the clip box is offset by the row's 6px padding while the 2px the drawing sits in from there moved onto the SVG (both on the box and it ends 2px into the message). The clip cannot carry the scrollbar either — it is as tall as the whole history, so `overflow-x: auto` on it puts the bar thousands of pixels below the viewport; the header's `#graph-hscroll` is a strip that scrolls nothing and whose `scrollLeft` is applied to the overlay as `--graph-pan-x`. And the cap is a share of the panel, so it needs a `ResizeObserver`: nothing re-renders the graph when a window is dragged, and the old cap then quietly outlives the width it was computed for. Verify with `bun tests/e2e/git-graph-narrow-layout.ts`, which measures a synthetic 40-branch history in headless Chrome — a CSS assertion cannot tell you that a column ended up 240px wide.
115
116
  - **`vscode.languages` and every editor API are absent for extensions**: no `activeTextEditor`, no `setDecorations`, no hover providers. Anything GitLens-shaped that wants to annotate code has to render its own view in a webview panel.
116
117
  - **Screen wake lock needs a secure context**: `navigator.wakeLock` does not exist at all over plain HTTP, so reaching PPM at `http://<lan-ip>:<port>` silently disables "keep screen awake" — the very setup (a tablet on the LAN) it was built for. Only HTTPS and `localhost` qualify. The lock is also released by the browser on *every* hide, so `src/web/hooks/use-wake-lock.ts` re-acquires on `visibilitychange` and drops its handle on hide rather than waiting for the `release` event, whose ordering against `visibilitychange` is undefined.
117
118
  - **Remote desktop on macOS (avfoundation)**: address the screen by **name** (`-i "Capture screen 0"`), never by index — the device list reorders at runtime when a Continuity Camera joins (index 3 became the iPhone). `-framerate` is ignored (the screen input delivers at display refresh, 120–165 fps, with a stuck pts) → `-use_wallclock_as_timestamps 1` + `-vf fps=30`. ffmpeg-avfoundation ignores SIGTERM/SIGINT → `proc.kill("SIGKILL")` or it leaks one process per session. No Screen Recording grant = **black frames, no error**; no Accessibility grant = silent no-op input — both are pre-flighted in `remote-desktop-requirements.ts`, never inferred from failures. TCC keys the grant to the `bun` executable.
118
119
  - **Remote desktop text injection on macOS**: `CGEventKeyboardSetUnicodeString` still carries a keycode, and IMEs that hook by key (OpenKey/EVKey) re-read keycode 0 as a literal "a" (`"日本"` → `"â"`); function/unassigned keycodes produce no text at all. The carrier is Space (`TEXT_CARRIER_KEYCODE`). Text is still subject to the host's active IME (Telex rewrote "World" → "ửold") on every OS.
120
+ - **Remote desktop on Linux is chosen per *session*, not per platform, and Wayland must win the tie**: `process.platform === "linux"` says nothing about how to capture or inject. A Wayland session almost always also runs XWayland, which sets `DISPLAY` and answers `x11grab` **successfully** — with a view of XWayland clients only, i.e. a black or half-empty screen rather than an error. So `remote-desktop-linux-session.ts` checks Wayland *before* X11, and `XDG_SESSION_TYPE` outranks both. It also has to *probe* rather than read the env: a PPM started by its systemd user unit has no `DISPLAY`/`XAUTHORITY` at all while a full desktop runs two feet away, so the fallbacks are the `X<N>` socket in `/tmp/.X11-unix`, the `wayland-N` socket in `$XDG_RUNTIME_DIR`, and for auth the `xauth_*` file Xorg was launched with (`-auth /run/user/1000/xauth_XXXXXX`) before `~/.Xauthority`. ffmpeg is then spawned with that env merged over `process.env`. Wayland *capture* is not implemented — it needs the desktop portal, and this is surfaced as an unmet `video` requirement the user can fix by picking X11 at login, never as "platform unsupported" (which would hide the entry and explain nothing).
121
+ - **Key injection on Linux goes through evdev codes, never keysyms**: `XKeysymToKeycode(XK_a)` answers "which key currently *produces* an a", which on an AZERTY host is the physical Q key — so a keysym table silently transposes the whole keyboard for anyone not on a US layout. `remote-desktop-evdev-key-map.ts` maps `KeyboardEvent.code` → evdev code, which names the physical switch just as `code` does; X11 keycode = evdev + 8 (`X11_KEYCODE_OFFSET`), verified against a live server for all 50 spot-checked keys. The same table feeds uinput, which wants the raw code. X11 also has no "type this string" call: `text()` uses the real keymap when the character is in it (all ASCII, no round trip) and otherwise borrows a keycode with no keysyms, binds it to `0x01000000|codepoint`, and needs a real ~12ms delay after the remap — the server applies it at once but the focused *client* only learns from its `MappingNotify`, so a key pressed sooner is read with the old mapping.
122
+ - **Xlib's default I/O error handler calls `exit()` on the PPM server**: a logout, a GPU reset or `systemctl restart sddm` makes libX11 print `XIO: fatal IO error` and terminate the *calling process* — every project, terminal and chat session gone because someone logged out of the desktop. `remote-desktop-x11.ts` replaces all three handlers (`XSetErrorHandler`, `XSetIOErrorHandler`, and libX11 ≥ 1.7's `XSetIOErrorExitHandler`, which cancels the exit that happens even when the first one returns) and holds the `JSCallback`s in module scope — a GC'd callback leaves Xlib calling a freed trampoline, i.e. a segfault instead of the crash it was installed to prevent. A connection that took an I/O error has no safe call left on it, not even `XCloseDisplay`, so it is dropped un-closed and the next caller reopens.
123
+ - **`xrandr` the binary and `libXrandr` the library are different packages**: this dev host has `libXrandr.so.2` and no `xrandr`, so anything that shells out to enumerate monitors reports "no displays" on a working desktop. `remote-desktop-displays.ts` reads `XRRGetMonitors` over FFI (`sizeof(XRRMonitorInfo)` = 56 on x86-64: name Atom at 0, primary at 8, x/y/w/h at 20/24/28/32) and falls back to one whole-root-window entry when the library is absent. X11 puts every monitor in one root coordinate space, so a per-monitor capture is a crop — `-video_size WxH -i :0.0+X,Y` — and `-i :0.0` alone grabs the union of all of them.
124
+ - **A uinput injector needs two devices, not one**: libinput classifies a device from the capabilities it declares, and one device advertising both a full keyboard and absolute axes is typically read as a *touchpad* — which then applies pointer acceleration and treats absolute coordinates as relative motion, so the pointer drifts instead of landing. `remote-desktop-input-uinput.ts` creates a pointer device (absolute axes + buttons + wheel + `INPUT_PROP_DIRECT`) and a keyboard device separately, declares every key it will ever send (`UI_SET_KEYBIT`; an undeclared key is dropped by the kernel silently), and `UI_DEV_DESTROY`s both on exit or they pile up in the compositor's device list on every restart. Derive the ioctl numbers from real headers — `_IOW` encodes the struct size, so a wrong `sizeof` yields a different request number and `ioctl` fails with `EINVAL` for no visible reason. uinput carries key *codes*, so there is no `text()` at all: a phone soft keyboard that reports no key codes cannot type on a Wayland host.
125
+ - **VAAPI is the only hardware H.264 an AMD Linux host has, and its args are position-sensitive**: `h264_amf` is Windows-only and `h264_qsv` is Intel-only, so before `h264_vaapi` was added to the candidate list every Radeon machine fell all the way back to software libx264. It is not a drop-in `-c:v` swap: `-vaapi_device` has to precede `-i` (it opens during input setup) and the frames must be uploaded in the *same* `-vf` chain (`format=nv12,hwupload`) — a second `-vf` replaces the first rather than combining — while `-pix_fmt yuv420p` has to be dropped, because a software pixel format on the output makes ffmpeg reject the hardware frame context. The capability probe test-encodes for real (`encoderWorks`), which is what stops `h264_nvenc` being chosen on this Intel-only host purely because the build lists it.
119
126
  - **A white screen has two causes and only one of them is catchable**: both start with a hashed chunk the browser asks for and the server does not have — `vite build` runs with `emptyOutDir`, so an upgrade deletes every previous hash while a tab is still naming them. `static.ts` used to SPA-fall-back on *any* missing path, so that request came back as **`200 text/html`**, which a module load refuses on its strict MIME check; `static-fallback.ts` now decides by `Sec-Fetch-Dest` (plus `/assets/` and `/monacoeditorwork/` as the prefix, because browsers omit Fetch Metadata on an insecure origin and PPM is usually reached over plain HTTP on a LAN). The path's *shape* cannot be the test: PPM's routes embed file paths, so `/project/x/editor/src/main.js` is a navigation whose extension is `.js`. Then: a **lazy** chunk failing throws inside `React.lazy`, and a throw past the last boundary unmounts the entire tree — `main.tsx` had no root boundary, so `#root` simply emptied. But the entry's own graph is **39 separately-hashed chunks** (rolldown names one of them `settings-tab-*` while the entry statically imports it, so the name says nothing), and one of those missing means the entry module *never executes*: no React, no boundary, no listener, nothing bundled to report it. That is what the inline watchdog in `index.html` is for, gated on the `__ppmEntryRan` flag `main.tsx` sets so the two can never both fire. The service worker made either one **permanent**: `CacheFirst` on `/assets/` had stored that `text/html` under the chunk's URL and content-hashed assets are never revalidated, so restoring the file on the server fixed nothing and only `caches.delete("ppm-assets")` did — hence the plugin refusing to store *or* serve HTML on both runtime caches, `cleanupOutdatedCaches()`, and the `skipWaiting`/`clients.claim()` that `registerType: "autoUpdate"` never actually implemented in `injectManifest` mode. Two things will hide the bug while you try to reproduce it: `Cache-Control: immutable` means a chunk already in the HTTP cache is served from disk however hard the server 404s, and `CacheFirst` means one already in `ppm-assets` is too — a cold origin (a different port) is the only reliable reproduction.
120
127
  - **A long-press must be disarmed on `touchcancel`, and leaving it out looks like nothing**: the pattern is a timer armed on `touchstart` and cleared on `touchmove`/`touchend`, which reads as complete. It is not — once the browser decides the gesture belongs to a scroll it fires **`touchcancel`** and then delivers *no further* `touchmove` or `touchend` to that element, so the timer survives the scroll and fires into it: a context menu over a list the finger is already moving, or a tab strip entering drag mode by itself. A movement tolerance cannot save it either, because the moves it would have measured are never delivered. It reproduces on a phone and nowhere else (a mouse has no touch events, and device emulation dispatches move/end faithfully), which is how it stayed live in **seven** separate hand-rolled long-presses at once — `git-status-panel.tsx`, `ui/adaptive-context-menu.tsx`, `os-explorer/use-coarse-long-press.ts`, `hooks/use-touch-tab-drag.ts`, `layout/mobile-drawer-tab-bar.tsx`, `layout/mobile-tab-switcher-sheet.tsx`, `layout/project-bottom-sheet.tsx`. `tests/unit/web/long-press-touchcancel.test.ts` fails any file that arms a timer from `onTouchStart` without naming `onTouchCancel`. Two related traps in the same shape: a hook that returns handlers has to *return* the cancel one and every forwarding component in between has to pass it on (`use-touch-tab-drag` → `tab-bar` → `draggable-tab` is three files for one prop), and the timer needs clearing on unmount — the git panel refreshes its file list on every save, so a row can vanish mid-press and open a menu belonging to no row.
121
128
  - **A blob URL is not stable for the life of the thing it previews**: the composer creates one from the image as pasted, downscales, then revokes the first and points the chip at the reduced copy — and `URL.revokeObjectURL` makes an open viewer render a *broken image* with no error, no console warning and nothing to say why. So `attachment-chips.tsx` keeps a `Map` of every preview URL it has handed to the lightbox against the attachment it belongs to: a URL that was replaced is followed, and one whose attachment is gone closes the viewer. Keyed by URL rather than by remembering one "currently viewing" id, because the overlay's arrow keys move between images without telling the component, and an id stops describing what is on screen the moment they are used. Related: the lightbox's gallery is collected from the **DOM** (`collectGallery`), so a row of thumbnails needs `GALLERY_ROOT_ATTR` on the container *and* `GALLERY_ITEM_ATTR` on each `<img>` — miss either and the viewer opens with dead arrows and no sign a second image was ever attached.
122
129
 
130
+ - **`preventDefault()` on the Ctrl+V keydown cancels the `paste` event, and that is the whole reason clipboard sync is shaped the way it is**: `use-remote-input-capture.ts` preventDefaults *every* keydown so no keystroke also acts on the browser — and `paste` is a default action of that keydown, so with the guard in place it simply never fires. Measured, not reasoned: with the exemption removed the keydown count still rises and the paste count stays flat. On an insecure origin (PPM's usual LAN deployment) `navigator.clipboard` does not exist at all, so that event is the *only* way to read the client's clipboard — hence Ctrl/Cmd+V is the one combo swallowed rather than forwarded, and the **server** delivers the paste keystroke itself once the clipboard write has landed. Forwarding it instead loses the race by construction: keydown order is Control, then V, then `paste`, so the host would be pasted before the text ever arrived. Three more measured details. A focused `<canvas tabindex=0>` **does** receive `paste` (the event targets the canvas; no wrapper needed). **Ctrl+Shift+V also fires `paste`**, which is what makes the two distinguishable — and the distinction is load-bearing, because a terminal pastes on Ctrl+Shift+V and reads a plain Ctrl+V as the literal quoted-insert `^V`, so the client sends a shift flag and `pasteComboCodes` presses what the user actually pressed. And Chromium reports a paste into the mobile hidden input as `beforeinput`/`insertFromPaste` with the text in **`data`** and a *null* `dataTransfer` — the opposite of what the spec's contenteditable case describes — so the portable source is `clipboardData` on the `paste` event, which is what `use-remote-desktop-virtual-keyboard.ts` listens for (and `preventDefault()` there is what stops the `beforeinput` double-insert). Host→client then needs `navigator.clipboard.writeText`, which is secure-context only *and* rejects on an unfocused document even where it exists, so the fallback is a real click running `execCommand("copy")` over a textarea that must be off-screen but still **rendered** — `display:none`/`visibility:hidden` give an empty selection, and `execCommand` then copies nothing while still returning true.
131
+
132
+ - **Owning a clipboard is not a one-shot call, which is why the host side shells out**: an X11 clipboard owner has to stay alive answering `SelectionRequest` events for as long as it holds the selection, so there is no "set the clipboard" Xlib call to FFI — `xclip`/`wl-copy` already fork a helper that does exactly that, and `remote-desktop-clipboard.ts` spawns them with `DISPLAY`/`XAUTHORITY` merged in (same as ffmpeg, since a systemd-started PPM has neither). The cost is a package the host may not have, and that is not hypothetical: this dev machine had **none** of xclip/xsel/wl-copy while running a perfectly good X11 desktop — the same trap as `xrandr` the binary vs `libXrandr`. So a missing tool is reported as `clipboard.available === false` carrying an install command, never as a silent no-op, and a paste still lands by *typing* the text as a fallback. It cannot be a `requirements` row though: that checklist only renders while video or input is unmet, so a clipboard row would be invisible on exactly the working hosts that are missing the tool — hence a separate `clipboard` field on the readiness response. Two more. On a **Wayland** session a working `xclip` is *worse* than none, because it addresses XWayland's own clipboard rather than the one the user sees, so the X pair is never even considered there. And the two directions feed each other unless the last synced text is remembered (client pastes T → host clipboard becomes T → the next read pushes T back → the client's own write re-arms it), which is what `lastClipboardText` is for. Windows goes through PowerShell with base64 on **both** sides, because `Get-Clipboard`/`Set-Clipboard` cross stdout/stdin in whatever console code page is active; `-Raw` plus a `$null` guard are needed too, or a multi-line clipboard arrives as an array of lines and an empty one makes `GetBytes` throw.
133
+
134
+ - **Every in-session setting is one of three kinds, and picking the wrong kind is how a menu item does nothing**: the viewer's toolbar (RustDesk's `_DisplayMenu`, ported) mixes settings that cost a **capture respawn**, settings that are **pure CSS**, and settings that are **only the absence of a listener** — and each has its own failure shape. (1) The remote cursor is burned into the frames by the grabber, so it is a startup flag with a different name per grabber (`-draw_mouse` for gdigrab/x11grab, `-capture_cursor` for avfoundation) and changing it means stop-wait-respawn, which is what `restartCapture` is for now that `applyPreset` is not the only thing that needs it. It rides the **`auth`** message rather than its own `cursor` one, or every connect would respawn ffmpeg ~400 ms after the first frame; only an explicit `cursor: false` hides it, so an older client still gets the pointer. (2) The scale modes are CSS, and `adaptive` is deliberately the one that computes *nothing* — `canvasCssSize` returns null and the canvas keeps `max-h-full max-w-full`, the path the viewer has always been on. The two traps there are both silent: `max-width: 100%` **beats** `width: 3440px`, so leaving the fit classes on makes `original`/`custom` no-ops (the mode switches, the menu ticks, the picture does not move), and centring an oversized child with `justify-content: center` puts half the overflow *before* the scroll origin where no scrollbar reaches it — `m-auto` is the fix, because auto margins centre only while there is free space. Overlays must also stay *outside* the scroll container or they scroll away with the picture. `tests/unit/web/remote-desktop-scale-layout.test.ts` pins all three. (3) Clipboard sync off is implemented by passing **no** clipboard hooks: `use-remote-input-capture.ts` then stops exempting Ctrl+V from `preventDefault` and forwards it as an ordinary keystroke, so the host pastes its own clipboard — which is exactly RustDesk's "Disable clipboard" behaviour and needs no server flag. It still needs one guard on the *inbound* path, though: the client's own `clipboardRead` is delayed 350 ms, so a host reply can land after the toggle went off, and "off" has to mean the host's text never reaches this device. Last, scale modes are desktop-only on purpose — the mobile viewer's pinch-zoom stage already owns the transform continuously, and its `object-contain` + `h-full w-full` canvas is load-bearing for hit-testing.
135
+
136
+ - **Reacting to a backpressure *spike* is what makes remote desktop stutter, and the higher-resolution rung is the smoother one**: both findings are counter-intuitive and both were measured on a 3440x1440 host. First, discarding one delta access unit makes every later delta in that GOP undecodable, so the client freezes until the next keyframe — which means a **120 ms** buffer spike bought a **502 ms** frozen picture, and a spike that short is a client GC pause or one wifi retransmit that would have drained by itself. `congestionState` therefore requires the socket to stay over threshold for `BACKPRESSURE_SUSTAIN_MS` before anything is dropped (same spike after: **20 ms**), and only sustained congestion feeds the adaptive ladder, because degrading a rung respawns ffmpeg for ~400 ms — a worse hitch than the one it was avoiding. The GOP is the other half of the same lever: it bounds every resync freeze, and halving it to half a second costs **no bitrate at all** (-0.0% at `balanced`, -0.1% at `high`, +0.3% at `tiny` against a full-motion source) because the encode is rate-capped, so extra keyframes redistribute the same budget rather than adding to it. Second, and the reason a "safe" default can read as lag: **1440p60 measured `gap p50=16.6 p95=22.7 max=30 ms` while 720p30 managed only `p50=33.3 p95=41.4 max=49 ms`** — the frame interval is half as long, so the same absolute jitter is a smaller fraction of it, and the expensive rung is the *smoother* one. A 30 fps ceiling cannot be fixed by sharpening the frames. Two traps that follow: a preset's `height` has to be a **ceiling** (`scale=-2:'min(ih,H)'`), or a 1440p rung on a 1080p host upscales and spends the whole rung's bitrate on interpolated pixels; and `auto` deliberately still stops at `balanced` (rising unprompted to 16 Mbit/s would manufacture congestion and spend a metered user's data), so the top rungs are reachable only by pinning them — the picker is the feature, not a fallback, and the pin is remembered with `persistDevicePref` (localStorage only, like `lspEnabled`) because the server round-trip would let a LAN desktop pin 1440p60 for a phone on mobile data. What comes back out of localStorage is untrusted the same way a rung off the wire is, hence `parseQualityChoice` rather than a cast — it lives in the shared ladder and not in the store because importing the store reads `localStorage` at module scope and throws under `bun:test`. Finally, the things that are **not** the bottleneck, so nobody re-measures them: QSV's `async_depth` (no effect — one AU per frame at cadence either way), x11grab at 3440x1440 with *software* swscale (holds both 30 and 60 fps on an i9-12900K), and moving the downscale onto the GPU with `scale_vaapi` (no latency win, marginally worse p95). The client is not either: `use-h264-canvas-decoder.ts` configures `optimizeForLatency` and draws straight from the `output` callback with no queue.
137
+
123
138
  - **A prompt's icons are Private Use Area codepoints, and a font that arrives late draws nothing at all**: a themed shell prompt takes its separators, git status and language badges from the PUA, so on a machine with no patched font every one of them was tofu while the `↑39 ↓15` between them rendered — those two are real Unicode, and that asymmetry is the whole diagnosis. `scripts/gen-nerd-font.ts` vendors the symbols-only face (version and SHA-256 both pinned; the single file from the repository tree is byte-identical to the one inside the release tarball, so no `tar`/`xz` is needed) and splits it into **fourteen** `@font-face`s, one per icon set. The split *is* the optimisation and it is load-bearing three times over: a face with no `unicode-range` is a candidate for all text and downloads unconditionally — all 1.14 MB of it; two faces claiming one codepoint fetch a second 500 KiB file to draw a glyph already in hand; and Vite's 4 KB `assetsInlineLimit` base64'd the four *smallest* faces straight into the entry CSS, which is the one file the SW precaches, so the rarest sets became an unconditional download for everyone (hence the function form excluding `woff2`, which made the boot shell 9 KiB smaller than before the font was added at all). The family name is PPM's own rather than `Symbols Nerd Font Mono` because a `@font-face` shadows a same-named system font *completely* — naming the real one would replace a full local install's 10,624 glyphs with this subset's — and it sits behind the installed names but ahead of Monaspace, which is safe only because it contains no letters or digits for ordinary text to stop at. Then the timing, measured rather than assumed: a canvas `fillText` **does** start the fetch (0 → 1 request; setting `ctx.font` alone does not), the load **does** fire `FontFaceSet`'s `loadingdone`, and the tofu it drew first is **permanent** — xterm bakes glyphs into a texture atlas and nothing repaints it, so ink was byte-identical 1.8 s after the face finished loading and only changed on a redraw. `use-terminal.ts` hangs `clearTextureAtlas()` + `refresh()` on `loadingdone` for the terminal's whole life; `document.fonts.ready` cannot do it, because it resolves long before a prompt first draws a PUA character and triggers the fetch. With the listener's body removed the glyph rasterisations after a face landed were +0, with it +14. Last trap, and it is the one that makes this untestable locally: after the stack is exhausted the browser asks **fontconfig** for any font with the codepoint, and a dev box with Nerd Fonts installed answers for all of them — so the webfont is never requested, a local screenshot proves nothing, and the bug reproduces only where no such font exists. Verify by forcing each face with `document.fonts.load()` and asserting the request that follows is that block's file.
124
139
 
125
140
  - **A `DropdownMenu` used as a row's context menu takes the row's tap away, and the adaptive one gives no tap back**: a dropdown trigger opens on *click*, so on a touch screen the gesture that opens it is the same one that would have activated the row — the Source Control panel's folder rows could not be expanded on a phone at all, because tapping one opened a menu over it. `@/components/ui/adaptive-context-menu` is the fix (bottom sheet + long-press below `md`, radix right-click above), but it only *suppresses* the click that follows a press: it has no tap handler of its own, so whatever the tap should do has to be a real button inside the trigger. That is also what removes the hand-rolled "was that a tap or a press?" timer, which is where the `touchcancel` class of bug lives. Two details when converting: mark a destructive item with `variant="destructive"` rather than a `text-destructive` class, because the class only reaches the radix half and the sheet takes its colour from the variant; and put `select-none` on the trigger's child, or a long press starts a text selection under the sheet it opens.
@@ -129,7 +144,17 @@ Related: `configService.save()`/`set()` throw when `load()` has not run, because
129
144
  - **An overlay on a vendored glyph assumes that glyph is empty, and an icon set migration silently breaks that**: the streaming chat tab draws three bouncing dots absolutely positioned over its 16px bubble icon, which was correct against lucide's hollow `MessageSquare`. Fluent's `chat` is the same bubble with **two message lines drawn inside it**, so after the migration the dots landed on the upper line in the same `currentColor` and the three fused into one lumpy bar — a static-looking smudge at the size a tab renders, reported as a broken animation rather than a wrong glyph, because the dots really were still bouncing. The fix is `chat-empty` (exported as `MessageCircle`), whose two subpaths are byte-identical to `chat`'s first two: same outline, nothing in it. Two things generalise. A glyph's *subpath count* is the cheap test for "does this draw anything inside itself" — `tests/unit/web/tab-streaming-indicator.test.ts` asserts the host glyph contributes exactly the outer edge and its hole, so a MAP edit or a regeneration cannot put a lined glyph back under the dots; and freezing a CSS animation with `animation-play-state: paused` plus a *negative* `animation-delay` renders any phase of its cycle, which is how a bounce this small is compared before and after at all. Note also that `isStreaming` is safe to branch an icon on only because both call sites read `sessionId` exclusively for `type === "chat"`.
130
145
 
131
146
  - **A Monarch `/$/` rule never runs, so a line-oriented grammar may not use states at all**: Monarch's per-line loop ends the moment the position reaches the end of the line, which means the idiomatic-looking `[/$/, { token: "", next: "@popall" }]` is dead code — the state entered mid-line is *never* left, and it carries into every following line. The `dotenv` grammar (`src/web/lib/monaco-dotenv-language.ts`, since Monaco ships no `.env` language and neither does VS Code) first used a `@value` state entered on `=`; from the first assignment on, the whole file tokenized as a value, so `#KEY=off` came back `string` — the one case the language existed for, still broken while every line *looked* coloured. Everything has to happen in `root`, the way Monaco's own `ini` does, and the test forbids a `next:` outright. Two consequences of statelessness worth knowing: a quoted value must be matched **whole** (`/"(?:[^"\\]|\\.)*"/`), which is the only way a `#` inside it is not read as a comment — at the cost of not colouring an interpolation inside quotes; and an unterminated quote falls through to the bare-value rule instead of swallowing the rest of the file. Separately, this grammar needs **no** `*-token-rules.ts` beside `diff-token-rules.ts`: its token names are Monaco's own (`comment`, `key`, `string`, `variable`, `keyword`, `delimiter`) and `monaco-adapter.ts` defines every PPM theme with `inherit: true`, so the colours arrive from the `vs`/`vs-dark` base and cannot drift from the app's themes — the test reads that pairing out of Monaco's own theme table rather than trusting it. Last trap, when verifying any of this in a browser: a standalone Monaco has **one global theme**, so two editors cannot show two themes at once (the second `create` silently retints the first), and sampling colours right after `setTheme` reads the *old* `mtk` class indices against the new palette — `string` measured as `#FF0000`. One editor, one reload per theme. Do not be misled by a `dotenv-*.js` chunk in the build output: that is shiki's TextMate grammar for a fenced `dotenv` block in markdown (`@shikijs/langs`, reached from `markdown-code-block.tsx`), a separate pipeline Monaco cannot read — there is no TextMate bridge in the editor, which is why a Monarch grammar had to be written even though the repository already ships a dotenv grammar.
147
+ - **An exclusive device grab is already taken, so blocking the host's input is an X grab and not evdev**: privacy mode's obvious implementation is `EVIOCGRAB` on every `/dev/input/event*` — and on this machine it returned **EBUSY on 3 of 4 devices** (`errno=16`, both keyboards and the wireless receiver; walking `/proc/*/fd` named the holder: **toshy**, a keyboard remapper, alongside Xorg). The grab is exclusive, so anything already holding it — toshy, kmonad, keyd, xremap — wins, and the first design would have reported "privacy mode on" while the local keyboard kept typing. Worse, the support probe only tested `devices[0]`, which happened to be the one device that *was* grabbable, so it answered "available" for a feature that could not work. The implementation is `XGrabKeyboard` + `XGrabPointer` with `owner_events=False`, plus **`XTestGrabControl(dpy, True)`** — that last call is the whole trick, because it makes a client's own XTEST events impervious to its own grab, so the remote session keeps injecting while the local user is locked out. Both grabs or neither: one of the two succeeding leaves the host half-controllable. Blanking is **DPMS**, which lives in **libXext**, not libX11 (`DPMSForceLevel(dpy, 3)`); it blanks the *output* and leaves the framebuffer, which is why x11grab keeps streaming a screen nobody local can see — and it needs re-asserting on a timer, because the injected XTEST events reset the idle timer that would otherwise hold it off. Probe DPMS for real (`DPMSQueryExtension` *and* `DPMSCapable`) rather than assuming the extension implies a capable output. Verify a grab from a *second* X connection (`XGrabKeyboard` there returns 1 = AlreadyGrabbed while it holds) — the process holding it cannot observe its own exclusivity.
148
+ - **The Ogg muxer buffers a whole second per page, which reads as audio latency and not as a setting**: host audio arrived ~2000 ms late, and none of the usual suspects were it — not the encoder (`libopus -application lowdelay -frame_duration 20`), not the socket, not the decoder. `-page_duration`'s default is **1 second**, so ffmpeg held ~50 Opus packets before emitting anything; `-page_duration 20000` (+ `-flush_packets 1`) took it to ~40 ms. It was found by *measuring page arrival times*, because every layer above and below looked correctly configured. Two more measured details that stop a re-investigation: a silent PulseAudio sink still produces packets (**3 bytes** each, against ~285 with real sound), so "packets are flowing" is not evidence of audio; and the demuxer's `concat` must **always copy** — returning a `subarray` view for the single-part case both pins the whole received chunk alive and hands out a packet pointing into a buffer that is about to be replaced. Detect pulse from `ffmpeg -demuxers` with a pattern that tolerates its real column layout (` D d pulse`): `/^\s*D\s+pulse\b/` requires whitespace immediately after the `D` and rejects every working build.
149
+ - **Changing the host's own resolution is a *sequence* of X calls, and the wrong order leaves a desktop with a dead strip**: the root window is a container for the CRTCs and neither may be outside the other, so any dimension that grows needs `XRRSetScreenSize` **before** `XRRSetCrtcConfig` (a CRTC past the screen bounds is refused) and trimming it down has to come **after** (a screen smaller than a live CRTC is equally invalid). A switch where one dimension grows and the other shrinks — 1920x1200 → 2560x1080 — needs both, which is why the decision is the pure `planResolutionSwitch` with its own test rather than an `if (growing)` pair: get it wrong and the mode applies, the root window keeps the wrong size, and nothing errors. Three more traps. The struct offsets are written out because a wrong one does not fail, it reads a neighbouring field and returns a plausible-looking mode list — check the read path against the kernel's own `modes` file under `/sys/class/drm/<card>-<connector>` (37 modes over XRandR vs 43 there on this host: X drops the interlaced/NTSC duplicates, and all 15 distinct sizes match). `XRROutputInfo.npreferred` is a **count** of preferred modes at the head of the list, not an index, so reading it as one marks the wrong mode native. And the menu must collapse to one row per *size* (37 timings → 15 rows), where the surviving mode has to be the **live** one when the host is on it — keeping the highest refresh instead leaves a host on 3440x1440@60 with nothing ticked while it also advertises @100. Restoring the original mode on teardown is not optional: this is the one session setting that outlives the session on the host's own screen. `-video_size` is fixed at spawn, so a switch is also a capture respawn.
150
+ - **RustDesk's "image quality" is bitrate and frame rate only — never resolution — and its Custom percentage is not a percentage of anything**: the menu is exactly four radio items (`toolbarImageQuality` in `flutter/lib/common/widgets/toolbar.dart`) — Good image quality, Balanced, Optimize reaction time, Custom — with no `auto` and no resolution, and `Quality` in `codec.rs` is the matching enum. PPM first shipped five rungs that *downscaled* beside a separate resolution menu, which is one menu too many and cost real picture quality: on a 3440x1440 host `balanced` streamed 720p, and measured, that downscale bought **1.17 Mbit/s against 1.26 Mbit/s** for native at the same cap — 7% of the bandwidth in exchange for 4.8x the pixels, because a desktop is mostly static and H.264 spends bits on *change* rather than on area. Neither came close to its cap, so it was pure loss. Dropping the scale filter also retires the upscale hazard it was guarding against, since nothing scales at all. The flicker that prompted this is worth recording precisely, because the plausible diagnosis was wrong: the old top rung asked for a flat 25 Mbit/s at 1440p60, and over a link that cannot carry that the capture pipe backs up and **ffmpeg throttles itself** — 22.9 of 60 fps arriving with gaps to **181 ms** over 4 Mbit/s, bursts and stalls that read as continuous flicker at that resolution, where the same rung on an unthrottled link held 60.0 fps at `p50=16.5ms`. It was *not* the frame-dropping path: `congestionState` never fired, because **`ws.getBufferedAmount()` is Bun's userspace queue and drains into the kernel socket buffer**, so it never reaches the 512 KB threshold — not even at 0.4 Mbit/s behind a relay applying real TCP backpressure. The keyframe rate is the tell (0.75/s against a 30-frame GOP is ffmpeg encoding at 22 fps, not a server discarding two thirds of its output), and the consequence is that the whole backpressure ladder hangs off a signal that is effectively inert on this host — anything built on it is unit-tested at best. With ratios instead, the top rung is `base_bitrate(3440,1440) x 1.5` = 6 Mbit/s and holds **30.0 fps with a 42 ms maximum gap over both a 5 Mbit/s and a 2 Mbit/s link**. A chosen rung is a **ceiling**, never a freeze (`video_qos.rs`: "user set image quality => update to the maximum ratio of the latest quality"), which is what the old `autoQuality = false` got backwards. `video_qos.rs` turns the slider into a *ratio* as `(percent * 2) / 100` clamped to 0.2–40.0, so the dialog's default **50% is ratio 1.0** and 100% is **double** the base rate; the named rungs are the same ratio under another name (BR_BEST 1.5, BR_BALANCED 0.67, BR_SPEED 0.5). What it multiplies is a **nearest-pixel-count lookup**, not a formula — a 13-row table from 640x480@400 kbps to 7680x4320@12000 kbps, scaled by the ratio of the real pixel count to the matched row's, giving `base_bitrate(3440,1440) = 4000` kbps. Three traps in the port, all in `src/shared/remote-desktop-custom-quality.ts`: the smallest row makes `baseBitrateKbps(0, 0)` round to **0** and ffmpeg refuses a zero bitrate, so it needs a floor; the custom rung has to keep the *display's own* height or it silently re-introduces the downscale the menu is not supposed to have (`effectivePreset()` overrides `fps` and `bitrate` and nothing else); and leaving custom for a named rung whose id is the one already in force must force a respawn, because `applyPreset` early-returns on an unchanged id and ffmpeg keeps running the custom numbers while the menu ticks a preset — verified over the real WS, not reasoned about. The **More** checkbox raises the ceiling from 100% to 2000%, so it decides what the percent is clamped to and has to be read out of storage *first*, and unticking it must re-clamp and re-commit or the host keeps encoding at a rate the dialog no longer shows. Ours debounces each commit by 400 ms because a bitrate change respawns ffmpeg here; RustDesk retunes its encoder in place and needs no such delay. "Optimize reaction time" is deliberately **not** copied onto a low rung: in this ladder the high rungs are the low-latency ones (see the backpressure entry above).
151
+ - **The scale slider is piecewise around a pivot, and the resolutions menu is four items that are usually all hidden**: `custom_scale_base.dart` maps the track so **100% sits at 1/3 of it** (5–1000% overall, a 0.006 detent snapping to the pivot, ±1 percentage point per nudge, 300 ms debounce) — laid out linearly, 100% lands a tenth of the way along where it cannot be hit. Then `_ResolutionsMenuState.build` gates the whole submenu on `keyboard && (virtualDisplay || resolutions.length > 1) && currentDisplay != all`, and every extra item on its own condition: **Original** only once the session has moved the host off it (`isOriginalResolutionSet && !isOriginalResolution`), the **custom w×h inputs** only for a *virtual* display, one shared divider only when something above it is visible, and **Fit local** only when `_getBestFitResolution()` finds a host mode matching the client's screen **exactly** — no nearest, no letterboxed approximation, because offering 1920x1080 to a 1366x768 client puts the picture back on the scaler the item exists to avoid. So on most client/host pairs Fit local is *absent*, which reads as a missing feature and is the port being faithful. Two details in `fitLocalMode`: the comparison is by **size**, not mode id, or a host on another timing of the size the client already has is offered a switch with nothing visible to show for it (this host sits on 3440x1440@60 while advertising @100, so it is the normal case); and the local screen is the client's **CSS** pixels — RustDesk's web branch divides physical by `scaleFactor` and a browser's `screen.width` already is that number, where physical pixels would land the picture at half size on any 2x display.
152
+ - **Virtual display cannot be done from userspace, so PPM does not pretend to offer it**: RustDesk's "virtual display" is a **signed IddCx driver** it ships and installs on Windows; on Linux the equivalent is `xf86-video-dummy` configured into `xorg.conf` and loaded *at X startup*, which no running process can add to a live server. There is no FFI call, no extension and no ffmpeg flag that creates a head — `XRRSetScreenSize` can resize the framebuffer past the panel but the CRTC still drives the one real output, so the extra area is never scanned out and is not a second display. The feature is therefore absent rather than stubbed; a menu item that silently does nothing is worse than no menu item.
153
+
132
154
  - **A filename column has to ellipsize from its *start*, and only `dir="rtl"` + `<bdi>` does it without leaving a mark**: `text-overflow: ellipsis` cuts the end, which in a list of source files is the only part that differs — `remote-desktop-capture.ts`, `remote-desktop-capture-input.ts` and `remote-desktop-encoder-args.ts` agree for **23 characters**, so seven Source Control rows all rendered as `remote-desktop-…` and the panel could not say which file it was listing. Three approaches fail before the fourth, and none of them fails loudly: splitting the name and pinning the tail beside an ellipsized head opens a ragged gap mid-row, because flex hands the head a fractional width while the ellipsis lands on a whole character — and `text-align` does **not** close it (measured; alignment does not apply to overflowing content); a fixed-length tail cuts mid-word into fragments that mean nothing (`…rgs.test.ts`); and the U+2066/U+2069 isolate *characters* render correctly but land in `textContent`, so a copied filename carries invisible codepoints. `<bdi>` inside a `dir="rtl"` box is the one that leaves the text clean (`StartEllipsis` in `git-status-panel.tsx`), and the isolate is load-bearing rather than tidy: outside one, the `.` of `.gitignore` is a neutral character at a run boundary, takes the paragraph's direction and renders as `gitignore.`. A dimmed path prefix in the same box has to be **one inline flow** inside the `<bdi>`, not two flex children, or the row's `gap` opens a space inside the path (`web/ components`). Related: the tree's own pure functions live in `src/web/lib/git-file-tree.ts` rather than in the panel, because importing the component reaches the zustand stores, which read `localStorage` at module scope and throw under `bun:test` — logic embedded in a component file here is untestable by construction, not merely awkward.
155
+ - **`opacity` is not a dimming level, it is a fade toward whatever is behind it — so one value cannot serve both modes**: the file explorer marked gitignored rows with `opacity-40`, which reads as a single decision and is not one. Measured against PPM's six built-in themes, that put a gitignored *filename* at **1.72–1.89:1** against the panel on the three light themes and 2.07–2.23:1 on the dark ones — unreadable on light, merely dim on dark, from the same class, so it reproduces only for whoever is on a light theme. A de-emphasised state has to be a **colour**: `--color-text-dim` is `color-mix(in srgb, var(--text-2) 50%, var(--text-3))`, which measures 3.59–5.21:1 across all six while staying visibly below `--text-2`. The file icon is then left alone entirely — those glyphs are `background-image` artwork with no `currentColor`, so an opacity on them is that same fade and nothing else, which is why the *folder* icon was the washed-out thing in the report. `tests/unit/web/explorer-ignored-contrast.test.ts` measures the hazard and the mitigation together and reads the mix out of `globals.css` rather than restating it, so the two cannot drift.
156
+ - **Half of VS Code's compact folders is worse than none of it**: joining a single-child chain into one `a/b/c` row only makes sense if it happens on the way *in*, before the intermediate row is ever drawn. PPM's `flatten-visible-tree.ts` required both paths to be in `expandedPaths`, so it fired one gesture too late: clicking a folder made the row you clicked vanish upward into its parent and shifted everything below it a level left. `depth` is the load-bearing part — it becomes `paddingLeft`, so a compacted chain indented its children by how many rows were above them rather than by where they actually sit. It also leaked into four other files as an `effectiveNode`/`effectivePath` shadowing the real one (focus, range selection, the drop target, the inline-create target). One row per directory, always; `tests/unit/web/explorer-row-per-directory.test.ts` pins it.
157
+
133
158
  ## UI Rules
134
159
 
135
160
  When creating or modifying any UI component, you MUST read and follow `docs/design-guidelines.md`, especially the **Mobile-First UI Rules** section. Key rules:
@@ -166,12 +191,12 @@ Before planning or implementing a new feature, read `docs/project-roadmap.md` to
166
191
  - `src/services/host-info/` — Per-OS drives/known-folders/pinned-folders providers (`/api/system/host`)
167
192
  - `src/services/system-metrics/` — Task Manager backend (`/api/system/resources*`): per-OS collectors, one long-lived PowerShell child on Windows (never spawn per tick — 32 MiB commit leak each), light/full SSE tiers with leases, guarded kill. UI in `src/web/components/system/` (floating window on desktop, tab on mobile)
168
193
  - `src/services/db-backup/` — verified `VACUUM INTO` snapshots of `ppm.db` (hourly + at start + before every migration), GFS retention, `ppm backup` / `ppm restore`
169
- - `src/services/remote-desktop/` — Remote Desktop: one ffmpeg per WS session (`remote-desktop-capture.ts` + per-platform `remote-desktop-capture-input.ts` + `remote-desktop-encoder-args.ts`) → Annex-B H.264 access units → WebCodecs; input via `remote-desktop-input.ts` facade over `remote-desktop-input-{win32,darwin}.ts` (bun:ffi, no helper binary); host checklist in `remote-desktop-requirements.ts` (ffmpeg install command, macOS TCC pre-flight); display list in `remote-desktop-displays.ts` (`CGGetActiveDisplayList` order = avfoundation `Capture screen N`, names/bounds via one JXA call). Routes `src/server/routes/remote-desktop.ts`, WS `src/server/ws/remote-desktop.ts`, UI `src/web/components/remote-desktop/`, e2e `tests/e2e/remote-desktop-e2e.mjs`
194
+ - `src/services/remote-desktop/` — Remote Desktop: one ffmpeg per WS session (`remote-desktop-capture.ts` + per-platform `remote-desktop-capture-input.ts` + `remote-desktop-encoder-args.ts`) → Annex-B H.264 access units → WebCodecs; input via `remote-desktop-input.ts` facade over `remote-desktop-input-{win32,darwin,linux,uinput}.ts` (bun:ffi, no helper binary); host checklist in `remote-desktop-requirements.ts` (ffmpeg install command, macOS TCC pre-flight, Linux session/XTEST/uinput rows); display list in `remote-desktop-displays.ts` (`CGGetActiveDisplayList` order = avfoundation `Capture screen N`, names/bounds via one JXA call; X11 monitors via `XRRGetMonitors`). Linux specifics: `remote-desktop-linux-session.ts` decides X11 vs Wayland and resolves `DISPLAY`/`XAUTHORITY`, `remote-desktop-x11.ts` is the shared Xlib connection, `remote-desktop-evdev-key-map.ts` is the one key table both Linux backends use. Clipboard sync is `remote-desktop-clipboard.ts` (host side, one helper process per read/write) and `src/web/components/remote-desktop/remote-desktop-clipboard-client.ts` (which key combos mean copy/paste, plus the insecure-origin copy fallback). The in-session controls are `remote-desktop-toolbar.tsx` (desktop, RustDesk's grouped menus) and `remote-desktop-mobile-toolbar.tsx` + `remote-desktop-mobile-settings.tsx` (thumb zone), with the scale maths in `remote-desktop-view-style.ts`. Host audio is `remote-desktop-audio.ts` (a second ffmpeg, Opus in Ogg) demuxed by `remote-desktop-ogg-opus.ts` and played by `use-opus-audio-player.ts`; privacy mode is `remote-desktop-privacy.ts` (X grabs + DPMS); host resolution is `remote-desktop-resolution.ts` (XRandR FFI) with the menu collapse and the fit-local decision in `remote-desktop-resolution-list.ts`; the custom image-quality maths ported from RustDesk's `video_qos.rs` are `src/shared/remote-desktop-custom-quality.ts` behind `remote-desktop-custom-quality-dialog.tsx`; screenshot/recording are `remote-desktop-recording.ts` + `use-remote-desktop-recorder.ts` (client-side `MediaRecorder` over the canvas). Codec choice comes from `workingEncoders()` in `media-transcode/ffmpeg-capabilities.ts`, which probes *every* candidate rather than short-circuiting. Routes `src/server/routes/remote-desktop.ts`, WS `src/server/ws/remote-desktop.ts`, UI `src/web/components/remote-desktop/`, e2e `tests/e2e/remote-desktop-e2e.mjs`
170
195
  - `src/services/git-hunks/` — hunk/line-level stage, unstage and discard (`/api/projects/:name/git/{hunks,stage-hunks,unstage-hunks,discard-hunks}`). `unified-diff.ts` holds the patch surgery (recomputed `@@` counts, unselected deletions demoted to context); the UI is `src/web/components/git/hunk-stage-dialog.tsx`
171
196
  - `src/services/git-repos/` — which repository a project's git surfaces run in, when the project folder is a container of checkouts rather than a repository itself. `git-repo-discovery.ts` walks the folder (`GET /git/repos`), the `?repo=` middleware in `src/server/routes/git.ts` scopes every git route to one of them, and the browser side is `src/web/lib/git-repo-scope.ts` (pure decisions and both directions of path rebasing), `src/web/stores/git-repo-store.ts` (the device-local choice), `src/web/hooks/use-git-repo.ts` (what every panel calls) and `src/web/components/git/git-repo-picker.tsx`. Extension panels are opened on the repository, so `packages/ext-git-graph/src/project-scope.ts` maps that path back to the containing project
172
197
  - `src/web/lib/icons.ts` + `src/web/lib/icons.generated.tsx` + `src/web/lib/fluent-icon.tsx` — the product icons (Fluent System Icons 20px Regular, MIT, microsoft/fluentui-system-icons) for the app's own chrome. Every UI icon is imported from `@/lib/icons`; regenerate with `bun scripts/gen-product-icons.ts` after editing its `MAP`
173
198
  - `src/server/routes/static-fallback.ts` + `src/web/components/root-error-boundary.tsx` + `src/web/lib/chunk-recovery.ts` + the inline watchdog in `src/web/index.html` — the four halves of not going blank when a chunk is missing: 404 instead of the shell, a boundary above `App`, one guarded purge-and-reload on `vite:preloadError`, and a dependency-free last resort for when the entry never ran at all
174
- - `src/web/lib/file-icons.tsx` + `src/web/lib/file-icons.generated.ts` + `src/web/styles/file-icons.generated.css` — the file icon theme (real `vscode-icons` artwork, MIT). `FileIcon` for a component slot, `fileIconElement` for the `icon: ElementType` slots (tab bar, command palette, both through `src/web/lib/tab-type-icons.ts`). Regenerate with `bun scripts/gen-file-icons.ts`
199
+ - `src/web/lib/file-icons.tsx` + `src/web/lib/file-icon-name.ts` + `src/web/lib/file-icons.generated.ts` + `src/web/styles/file-icons.generated.css` — the file icon theme (real `vscode-icons` artwork, MIT). `FileIcon` for a component slot, `fileIconElement` for the `icon: ElementType` slots (tab bar, command palette, both through `src/web/lib/tab-type-icons.ts`). Name resolution is the pure half in `file-icon-name.ts` (importable under `bun:test`); the framework preset comes from `src/web/stores/project-framework-store.ts`. Regenerate with `bun scripts/gen-file-icons.ts`
175
200
  - `src/services/lsp/` — language servers for the editor. `server-registry.ts` says which binary serves which language and how to find it (project `node_modules/.bin` before PATH), `lsp-protocol.ts` is the `Content-Length` framing, `lsp-session.ts` owns one child process, `lsp-manager.ts` decides how many exist and shares them by *server + root directory*. Bridge at `src/server/ws/lsp.ts` on `/ws/project/:name/lsp`, status at `/api/projects/:name/lsp/status`. Browser side in `src/web/lib/lsp/` (client, Monaco conversions, providers, shadow models, F12 navigation) driven by `src/web/hooks/use-lsp.ts`. All of it is behind the `lspEnabled` setting — off by default, per device, never on below the `md` breakpoint — and mounted by `src/web/components/editor/editor-language-service.tsx`, which `code-editor.tsx` may only reach through `lazy()`. Real-server check: `bun tests/e2e/lsp-e2e.ts`
176
201
  - `src/web/components/problems/` + `src/web/stores/problems-store.ts` — the Problems panel: a dock tab (`DOCK_ALLOWED_TAB_TYPES`) fed by every mounted editor's `useLsp`, opened from the status bar counts on desktop and the editor's LSP row on mobile. Scoped to the active project because `tab-pool` keeps other projects' editors mounted for keep-alive, so their servers keep publishing
177
202
  - `src/services/git-blame/` — `git blame --porcelain` for the editor's inline annotation (`/api/projects/:name/git/blame`); shared shape and formatting in `src/shared/blame.ts`, editor wiring in `src/web/hooks/use-inline-blame.ts`. The annotation's GitLens-style hover adds one `git show --unified=0` per commit *and line* (`/git/commit-line`, parsed by `commit-line-diff.ts`), with the markdown built in `src/web/lib/blame-hover.ts` and its buttons registered in `blame-hover-commands.ts`, which hand off to the Git Graph extension's own views over `ext:command:execute`
@@ -34,7 +34,7 @@ only the surrounding component is PPM's. The 15 names Fluent has no glyph for st
34
34
 
35
35
  ## File icons
36
36
 
37
- `src/web/styles/file-icons.generated.css` inlines 224 SVG drawings as data URIs and
37
+ `src/web/styles/file-icons.generated.css` inlines 1193 SVG drawings as data URIs and
38
38
  `src/web/lib/file-icons.generated.ts` holds the name-to-glyph tables. Both are emitted by
39
39
  `scripts/gen-file-icons.ts` from the packages below: the artwork comes from the icon
40
40
  collection, the extension-to-name mapping from `vscode-icons-js`. The drawings are
@@ -71,4 +71,4 @@ This skill covers the `ppm` CLI, its HTTP API, and its config DB. It does **not*
71
71
  - Third-party extensions (inspect via `ppm ext list`).
72
72
  - The Claude Agent SDK internals (separate skill).
73
73
 
74
- <!-- Generated for PPM v0.21.0 at build time. Re-run `ppm export skill --install` to refresh. -->
74
+ <!-- Generated for PPM v0.22.0 at build time. Re-run `ppm export skill --install` to refresh. -->
@@ -349,4 +349,4 @@ _Base URL: `http://localhost:8080` (default; override via `ppm config set port <
349
349
  - `ws://<host>/ws/terminal` — PTY terminal multiplexer
350
350
  - `ws://<host>/ws/extensions` — extension host channel
351
351
 
352
- <!-- Generated from src/server/routes/ for PPM v0.21.0 -->
352
+ <!-- Generated from src/server/routes/ for PPM v0.22.0 -->
package/bun.lock CHANGED
@@ -79,6 +79,7 @@
79
79
  "@vitejs/plugin-react": "^6.0.1",
80
80
  "concurrently": "^9.2.1",
81
81
  "esbuild": "^0.27.4",
82
+ "happy-dom": "^20.14.5",
82
83
  "subset-font": "^2.7.0",
83
84
  "tailwindcss": "^4.2.1",
84
85
  "vite": "^8.0.0",
@@ -848,6 +849,10 @@
848
849
 
849
850
  "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
850
851
 
852
+ "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
853
+
854
+ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
855
+
851
856
  "@uiw/codemirror-extensions-basic-setup": ["@uiw/codemirror-extensions-basic-setup@4.25.8", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-9Rr+liiBmK4xzZHszL+twNRJApthqmITBwDP3emNTtTrkBFN4gHlqfp+nodKmoVt1+bUH1qQCtyqt+7dbDTHiw=="],
852
857
 
853
858
  "@uiw/react-codemirror": ["@uiw/react-codemirror@4.25.8", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@codemirror/commands": "^6.1.0", "@codemirror/state": "^6.1.1", "@codemirror/theme-one-dark": "^6.0.0", "@uiw/codemirror-extensions-basic-setup": "4.25.8", "codemirror": "^6.0.0" }, "peerDependencies": { "@codemirror/view": ">=6.0.0", "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-A0aLOuJZm2yJ+U9GlMFwxwFciztjd5LhcAG4SMqFxdD58wH+sCQXuY4UU5J2hqgS390qAlShtUgREvJPUonbuQ=="],
@@ -952,6 +957,8 @@
952
957
 
953
958
  "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
954
959
 
960
+ "buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="],
961
+
955
962
  "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
956
963
 
957
964
  "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
@@ -1172,7 +1179,7 @@
1172
1179
 
1173
1180
  "enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
1174
1181
 
1175
- "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
1182
+ "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
1176
1183
 
1177
1184
  "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="],
1178
1185
 
@@ -1288,6 +1295,8 @@
1288
1295
 
1289
1296
  "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
1290
1297
 
1298
+ "happy-dom": ["happy-dom@20.14.5", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-x/RzkpWO40bTjIoT30iQtt64FLLmH/iRcUCN2X//bLx7H3ifkdfPXyqsro/OYtqzIAhiLMMA7mmiOR9C3NOKjQ=="],
1299
+
1291
1300
  "harfbuzzjs": ["harfbuzzjs@0.10.3", "", {}, "sha512-GJnLUrgLMadlMYrBGEXwYEimObbysy3prWT4HyPpFQERvgTU/OZ+ReUlEPOum6w4RBtFXzXiCCmECOr4sz3qwQ=="],
1292
1301
 
1293
1302
  "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
@@ -2112,6 +2121,8 @@
2112
2121
 
2113
2122
  "webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
2114
2123
 
2124
+ "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
2125
+
2115
2126
  "whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="],
2116
2127
 
2117
2128
  "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -2164,6 +2175,8 @@
2164
2175
 
2165
2176
  "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
2166
2177
 
2178
+ "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
2179
+
2167
2180
  "xmlbuilder": ["xmlbuilder@10.1.1", "", {}, "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg=="],
2168
2181
 
2169
2182
  "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
@@ -2248,6 +2261,8 @@
2248
2261
 
2249
2262
  "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
2250
2263
 
2264
+ "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
2265
+
2251
2266
  "path-scurry/lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="],
2252
2267
 
2253
2268
  "readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
@@ -0,0 +1 @@
1
+ import{o as e}from"./rolldown-runtime-FhOqtrmT.js";import{C as t,S as n}from"./vendor-markdown-CuWCHIVb.js";import"./vendor-ui-DmZ-Mb-H.js";import{Br as r,Tn as i,V as a,Xt as o,ct as s,dn as c,kr as l,ln as u,nt as d,or as f,qn as p,st as m,wr as h,xt as g}from"./icons-DQjqk0lJ.js";import{n as _,r as v}from"./api-client-C1ZwRL4Y.js";import{n as y}from"./utils-Bmf_8h7Y.js";import"./portal-container-context-BarKmT4S.js";import{n as b}from"./use-is-mobile-TkcwP1Gx.js";import{n as x,t as S}from"./button-B9CTrsEf.js";import{a as C,i as w,n as T,o as E,r as D,t as O}from"./dialog-B84_DJQ8.js";import{t as k}from"./mobile-bottom-sheet-C3TniEP-.js";import"./use-visual-viewport-Lp878uzs.js";import{t as A}from"./input-C7Q8FxeL.js";import{Cn as j,G as M,J as N,K as P,Qt as ee,Sn as F,W as te,Y as I,dn as ne,q as L,rt as R,wn as z}from"./index-DU68xk06.js";import{addAccount as B,deleteAccount as V,exchangeOAuthCode as H,getAISettings as U,getAccountSettings as W,getOAuthUrl as G,getUsageHistory as re,importAccounts as K,patchAccount as q,testAccountToken as ie,testExport as J,testRawToken as ae,updateAccountSettings as Y}from"./api-settings-_7Q-ULxp.js";import{a as oe,c as se,d as ce,f as le,i as ue,n as de,o as fe,r as pe,s as me,t as he,u as ge}from"./codex-daily-guard-BSccLRGT.js";var X=e(t(),1),Z=n(),_e=[`Mon`,`Tue`,`Wed`,`Thu`,`Fri`,`Sat`,`Sun`],Q=Array.from({length:24},(e,t)=>t);function ve(e,t){let n=Array.from({length:7},()=>Array.from({length:24},()=>({sum:0,count:0,avg:0})));for(let r of e){let e=t===`5h`?r.five_hour_util:r.weekly_util;if(e==null)continue;let i=new Date(r.recorded_at+(r.recorded_at.endsWith(`Z`)?``:`Z`)),a=(i.getDay()+6)%7,o=i.getHours();n[a][o].sum+=e,n[a][o].count+=1}for(let e of n)for(let t of e)t.avg=t.count>0?t.sum/t.count:0;return n}function ye(e){return e.map(e=>{let t=e.reduce((e,t)=>e+t.sum,0),n=e.reduce((e,t)=>e+t.count,0);return n>0?t/n:0})}function be(e){return Q.map(t=>{let n=0,r=0;for(let i of e)n+=i[t].sum,r+=i[t].count;return r>0?n/r:0})}function xe(e){return e===0?`bg-surface-elevated`:e<.3?`bg-success/30`:e<.5?`bg-success/60`:e<.7?`bg-warning/50`:e<.9?`bg-warning/80`:`bg-error/80`}function Se(e){return e<.3?`bg-success`:e<.7?`bg-warning`:`bg-error`}function Ce({accountId:e}){let[t,n]=(0,X.useState)(null),[r,i]=(0,X.useState)(!0),[a,o]=(0,X.useState)(`5h`);(0,X.useEffect)(()=>{i(!0),re(e).then(n).catch(()=>n([])).finally(()=>i(!1))},[e]);let s=(0,X.useMemo)(()=>t?ve(t,a):null,[t,a]),c=(0,X.useMemo)(()=>s?ye(s):[],[s]),l=(0,X.useMemo)(()=>s?be(s):[],[s]),d=Math.max(...c,.01);if(Math.max(...l,.01),r)return(0,Z.jsx)(`div`,{className:`flex items-center justify-center py-3`,children:(0,Z.jsx)(u,{className:`size-3 animate-spin text-text-subtle`})});if(!t||t.length===0)return(0,Z.jsx)(`div`,{className:`text-[10px] text-text-subtle py-2 text-center`,children:`No usage history yet`});let f=t.length,p=new Set(t.map(e=>new Date(e.recorded_at+(e.recorded_at.endsWith(`Z`)?``:`Z`)).toDateString())).size;return(0,Z.jsxs)(`div`,{className:`mt-2 space-y-2`,children:[(0,Z.jsxs)(`div`,{className:`flex items-center justify-between`,children:[(0,Z.jsx)(`span`,{className:`text-[10px] font-medium text-text-subtle`,children:`Usage Pattern (7d)`}),(0,Z.jsxs)(`div`,{className:`flex gap-0.5 text-[9px]`,children:[(0,Z.jsx)(`button`,{onClick:()=>o(`5h`),className:`px-1.5 py-0.5 rounded cursor-pointer transition-colors ${a===`5h`?`bg-primary/15 text-primary`:`text-text-subtle hover:text-text-secondary`}`,title:`5-hour rolling window limit — resets every 5 hours`,children:`5h`}),(0,Z.jsx)(`button`,{onClick:()=>o(`weekly`),className:`px-1.5 py-0.5 rounded cursor-pointer transition-colors ${a===`weekly`?`bg-primary/15 text-primary`:`text-text-subtle hover:text-text-secondary`}`,title:`Weekly limit — resets every 7 days`,children:`Wk`})]})]}),(0,Z.jsxs)(`p`,{className:`text-[9px] text-text-subtle leading-tight`,children:[`Avg `,a===`5h`?`5-hour`:`weekly`,` limit usage over `,p,`d (`,f,` samples). Higher % = closer to rate limit. Hover cells for details.`]}),(0,Z.jsxs)(`div`,{children:[(0,Z.jsx)(`span`,{className:`text-[9px] text-text-subtle`,children:`Avg usage by day of week`}),(0,Z.jsx)(`div`,{className:`flex flex-col gap-[2px] mt-0.5`,children:_e.map((e,t)=>{let n=c[t]??0;return(0,Z.jsxs)(`div`,{className:`flex items-center gap-1`,children:[(0,Z.jsx)(`span`,{className:`text-[8px] text-text-subtle w-5 shrink-0 text-right tabular-nums`,children:e}),(0,Z.jsx)(`div`,{className:`flex-1 h-2.5 bg-surface-elevated rounded-sm overflow-hidden`,children:(0,Z.jsx)(`div`,{className:`h-full rounded-sm transition-all ${Se(n)}`,style:{width:`${Math.round(n/d*100)}%`}})}),(0,Z.jsxs)(`span`,{className:`text-[8px] text-text-subtle w-6 shrink-0 text-right tabular-nums`,children:[Math.round(n*100),`%`]})]},e)})})]}),(0,Z.jsxs)(`div`,{children:[(0,Z.jsx)(`span`,{className:`text-[9px] text-text-subtle`,children:`Avg usage by hour (0h-23h)`}),(0,Z.jsx)(`div`,{className:`flex gap-[1px] mt-0.5`,children:Q.map(e=>{let t=l[e]??0;return(0,Z.jsxs)(`div`,{className:`flex-1 flex flex-col items-center gap-[1px]`,children:[(0,Z.jsx)(`div`,{className:`w-full aspect-square rounded-[2px] ${xe(t)}`,title:`${e}:00 — avg ${Math.round(t*100)}% usage`}),e%6==0&&(0,Z.jsx)(`span`,{className:`text-[7px] text-text-subtle tabular-nums`,children:e})]},e)})})]}),s&&(0,Z.jsxs)(`div`,{children:[(0,Z.jsx)(`span`,{className:`text-[9px] text-text-subtle`,children:`Day x Hour heatmap`}),(0,Z.jsxs)(`div`,{className:`flex flex-col gap-[1px] mt-0.5`,children:[_e.map((e,t)=>(0,Z.jsxs)(`div`,{className:`flex items-center gap-[1px]`,children:[(0,Z.jsx)(`span`,{className:`text-[7px] text-text-subtle w-4 shrink-0 text-right`,children:e.charAt(0)}),Q.map(n=>{let r=s[t][n];return(0,Z.jsx)(`div`,{className:`flex-1 aspect-square rounded-[1px] ${xe(r.avg)}`,title:`${e} ${n}:00 — ${r.count>0?`avg ${Math.round(r.avg*100)}% (${r.count} samples)`:`no data`}`},n)})]},e)),(0,Z.jsxs)(`div`,{className:`flex items-center gap-[1px]`,children:[(0,Z.jsx)(`span`,{className:`w-4 shrink-0`}),Q.map(e=>(0,Z.jsx)(`div`,{className:`flex-1 text-center`,children:e%6==0&&(0,Z.jsx)(`span`,{className:`text-[7px] text-text-subtle tabular-nums`,children:e})},e))]})]})]}),(0,Z.jsxs)(`div`,{className:`flex items-center gap-1.5 text-[8px] text-text-subtle`,children:[(0,Z.jsx)(`span`,{children:`Low`}),(0,Z.jsxs)(`div`,{className:`flex gap-[2px]`,children:[(0,Z.jsx)(`div`,{className:`size-2 rounded-[1px] bg-success/30`}),(0,Z.jsx)(`div`,{className:`size-2 rounded-[1px] bg-success/60`}),(0,Z.jsx)(`div`,{className:`size-2 rounded-[1px] bg-warning/50`}),(0,Z.jsx)(`div`,{className:`size-2 rounded-[1px] bg-warning/80`}),(0,Z.jsx)(`div`,{className:`size-2 rounded-[1px] bg-error/80`})]}),(0,Z.jsx)(`span`,{children:`High`}),(0,Z.jsx)(`span`,{className:`ml-1`,children:`|`}),(0,Z.jsx)(`div`,{className:`size-2 rounded-[1px] bg-surface-elevated border border-border/30`}),(0,Z.jsx)(`span`,{children:`No data`})]})]})}function we({profile:e,accountId:t,onClose:n}){let i=[[`Name`,e.account?.display_name],[`Email`,e.account?.email],[`Org`,e.organization?.name],[`Type`,e.organization?.organization_type],[`Tier`,e.organization?.rate_limit_tier],[`Status`,e.organization?.subscription_status]];return(0,Z.jsxs)(`section`,{className:`space-y-2 border-t border-border pt-3`,"data-testid":`account-profile-panel`,children:[(0,Z.jsxs)(`div`,{className:`flex items-center justify-between`,children:[(0,Z.jsx)(`h3`,{className:`text-sm font-medium`,children:`Profile`}),(0,Z.jsx)(`button`,{className:`p-2 text-muted-foreground hover:text-foreground cursor-pointer`,onClick:n,"aria-label":`Close profile`,children:(0,Z.jsx)(r,{className:`size-4`})})]}),(0,Z.jsx)(`div`,{className:`grid grid-cols-[80px_1fr] gap-x-3 gap-y-1 text-xs`,children:i.map(([e,t])=>t?(0,Z.jsxs)(`span`,{className:`contents`,children:[(0,Z.jsx)(`span`,{className:`text-muted-foreground`,children:e}),(0,Z.jsx)(`span`,{className:`break-all`,children:t})]},e):null)}),(0,Z.jsx)(Ce,{accountId:t})]})}function Te({display:e,onConfirm:t,onCancel:n}){let r=b(),i=(0,Z.jsxs)(`div`,{className:`space-y-4`,"data-testid":`account-delete-confirm`,children:[(0,Z.jsxs)(`p`,{className:`text-sm`,children:[`Remove `,(0,Z.jsx)(`span`,{className:`font-medium`,children:e}),`?`]}),(0,Z.jsx)(`p`,{className:`text-xs text-text-subtle`,children:`The stored token is deleted from this machine. Any export you already made keeps working.`}),(0,Z.jsxs)(`div`,{className:`flex flex-col-reverse md:flex-row gap-2 md:justify-end pt-2`,children:[(0,Z.jsx)(S,{variant:`outline`,onClick:n,className:`min-h-11`,children:`Cancel`}),(0,Z.jsx)(S,{variant:`destructive`,autoFocus:!1,onClick:t,className:`min-h-11`,children:`Remove account`})]})]});return r?(0,Z.jsx)(k,{open:!0,onClose:n,children:(0,Z.jsxs)(`div`,{className:`px-4 pb-4`,children:[(0,Z.jsx)(`h2`,{className:`text-base font-semibold mb-3`,children:`Remove account`}),i]})}):(0,Z.jsx)(O,{open:!0,onOpenChange:e=>{e||n()},children:(0,Z.jsxs)(T,{className:`sm:max-w-sm`,children:[(0,Z.jsx)(C,{children:(0,Z.jsx)(E,{className:`text-sm`,children:`Remove account`})}),i]})})}function Ee({open:e,onOpenChange:t,onSuccess:n}){let[r,i]=(0,X.useState)(``),[a,o]=(0,X.useState)(``),[s,c]=(0,X.useState)(!1),[l,d]=(0,X.useState)(null),[f,p]=(0,X.useState)(null),[m,h]=(0,X.useState)(``),[g,_]=(0,X.useState)(!1),[v,y]=(0,X.useState)(`idle`);function b(){p(null),h(``),y(`idle`),d(null)}function x(){t(!1),b(),i(``),o(``),d(null)}async function k(){_(!0),d(null);try{let{url:e,state:t}=await G();p(t),y(`waiting`),window.open(e,`_blank`)}catch(e){d(e.message)}_(!1)}async function j(){if(!(!m.trim()||!f)){_(!0),d(null);try{let e=m.trim();e.includes(`#`)&&(e=e.split(`#`)[0]??e);let t=await H(e,f);x(),n(t?.status===`disabled`?`Signed in. This account is still disabled — enable it to use it.`:`Account connected via OAuth!`)}catch(e){d(e.message)}_(!1)}}async function M(){if(r.trim()){c(!0),d(null);try{let e=await B({apiKey:r.trim(),label:a.trim()||void 0});x(),n(e?.status===`disabled`?`Token updated. This account is still disabled — enable it to use it.`:`Account added!`)}catch(e){d(e.message)}c(!1)}}let N=r.trim()?r.trim().startsWith(`sk-ant-oat`)?`OAuth token (Claude Max/Pro)`:r.trim().startsWith(`sk-ant-api`)?`API key`:`Unknown format`:``;return(0,Z.jsx)(O,{open:e,onOpenChange:e=>{e||x()},children:(0,Z.jsxs)(T,{className:`sm:max-w-md`,children:[(0,Z.jsxs)(C,{children:[(0,Z.jsx)(E,{className:`text-sm`,children:`Add Claude Account`}),(0,Z.jsx)(D,{className:`text-xs leading-relaxed`,children:`Connect via OAuth (recommended) or paste a token manually.`})]}),(0,Z.jsxs)(`div`,{className:`space-y-3`,children:[(0,Z.jsxs)(`div`,{className:`rounded-md border p-3 space-y-2`,children:[(0,Z.jsx)(`p`,{className:`text-[11px] font-medium`,children:`Recommended: Login with Claude`}),v===`idle`?(0,Z.jsx)(S,{size:`sm`,className:`w-full h-8 text-xs`,onClick:k,disabled:g,children:g?(0,Z.jsxs)(Z.Fragment,{children:[(0,Z.jsx)(u,{className:`size-3 animate-spin mr-1`}),` Opening...`]}):`Login with Claude`}):(0,Z.jsxs)(`div`,{className:`space-y-2`,children:[(0,Z.jsx)(`p`,{className:`text-[10px] text-muted-foreground`,children:`Authorize in the opened tab, then paste the code:`}),(0,Z.jsx)(A,{placeholder:`Paste code here...`,value:m,onChange:e=>h(e.target.value),className:`text-xs h-8 font-mono`,autoFocus:!0}),(0,Z.jsxs)(`div`,{className:`flex gap-1.5`,children:[(0,Z.jsx)(S,{size:`sm`,className:`flex-1 h-7 text-xs`,onClick:j,disabled:!m.trim()||g,children:g?(0,Z.jsxs)(Z.Fragment,{children:[(0,Z.jsx)(u,{className:`size-3 animate-spin mr-1`}),` Connecting...`]}):`Connect`}),(0,Z.jsx)(S,{size:`sm`,variant:`ghost`,className:`h-7 text-xs`,onClick:b,children:`Cancel`})]})]})]}),(0,Z.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,Z.jsx)(`div`,{className:`flex-1 border-t`}),(0,Z.jsx)(`span`,{className:`text-[10px] text-muted-foreground`,children:`or paste token`}),(0,Z.jsx)(`div`,{className:`flex-1 border-t`})]}),(0,Z.jsxs)(`div`,{className:`space-y-1.5`,children:[(0,Z.jsx)(R,{htmlFor:`add-token`,className:`text-xs`,children:`Token`}),(0,Z.jsx)(A,{id:`add-token`,type:`password`,placeholder:`sk-ant-...`,value:r,onChange:e=>i(e.target.value),className:`text-xs h-8 font-mono`}),N&&(0,Z.jsxs)(`p`,{className:`text-[10px] text-muted-foreground`,children:[`Detected: `,N]})]}),(0,Z.jsxs)(`div`,{className:`space-y-1.5`,children:[(0,Z.jsx)(R,{htmlFor:`add-label`,className:`text-xs`,children:`Label (optional)`}),(0,Z.jsx)(A,{id:`add-label`,placeholder:`e.g. Personal, Work`,value:a,onChange:e=>o(e.target.value),className:`text-xs h-8`})]})]}),l&&(0,Z.jsx)(`div`,{className:`text-[11px] p-2 rounded bg-error/10 text-error`,children:l}),(0,Z.jsxs)(w,{children:[(0,Z.jsx)(S,{size:`sm`,variant:`outline`,className:`text-xs h-7`,onClick:x,children:`Cancel`}),(0,Z.jsx)(S,{size:`sm`,className:`text-xs h-7`,onClick:M,disabled:!r.trim()||s,children:s?`Adding...`:`Add Token`})]})]})})}function De(e){let t=new Blob([e],{type:`application/json`}),n=document.createElement(`a`);n.href=URL.createObjectURL(t),n.download=`ppm-accounts-backup.json`,n.click(),URL.revokeObjectURL(n.href)}function Oe({open:e,onOpenChange:t,accounts:n,preselectId:r,onMessage:i}){let a=n.filter(e=>e.hasRefreshToken),[o,s]=(0,X.useState)(new Set),[l,f]=(0,X.useState)(``),[p,h]=(0,X.useState)(!1),[g,_]=(0,X.useState)(!1),[y,b]=(0,X.useState)(!1),[x,k]=(0,X.useState)(!1);e&&!x&&(s(r?new Set([r]):new Set(a.map(e=>e.id))),k(!0)),!e&&x&&k(!1);function j(){t(!1),f(``),h(!1),_(!1)}async function M(e){if(o.size===0)return;b(!0);let t=l.trim()||`ppm-hienlh`;try{let n={"Content-Type":`application/json`},r=v();r&&(n.Authorization=`Bearer ${r}`);let a=await fetch(`/api/accounts/export`,{method:`POST`,headers:n,body:JSON.stringify({password:t,accountIds:[...o],includeRefreshToken:p,refreshBeforeExport:g})});if(!a.ok){let e=await a.json();throw Error(e.error??`Export failed: ${a.status}`)}let s=await a.text();e&&await ee(s)?i?.(`Backup copied to clipboard!`):(De(s),i?.(`Backup downloaded.`)),j()}catch{}b(!1)}let N=o.size>0&&!y;return(0,Z.jsx)(O,{open:e,onOpenChange:e=>{e||j()},children:(0,Z.jsxs)(T,{className:`sm:max-w-md`,children:[(0,Z.jsxs)(C,{children:[(0,Z.jsxs)(E,{className:`text-sm flex items-center gap-1.5`,children:[(0,Z.jsx)(c,{className:`size-3.5`}),` Export Accounts`]}),(0,Z.jsx)(D,{className:`text-xs`,children:`Select accounts and set a password to protect the backup.`})]}),(0,Z.jsxs)(`div`,{className:`space-y-3`,children:[(0,Z.jsxs)(`div`,{className:`space-y-1`,children:[(0,Z.jsxs)(`div`,{className:`flex items-center justify-between mb-1`,children:[(0,Z.jsx)(`p`,{className:`text-[11px] font-medium text-muted-foreground`,children:`Accounts to export`}),(0,Z.jsx)(`button`,{className:`text-[10px] text-primary hover:underline cursor-pointer`,onClick:()=>s(o.size===a.length?new Set:new Set(a.map(e=>e.id))),children:o.size===a.length?`Deselect all`:`Select all`})]}),a.length===0?(0,Z.jsx)(`p`,{className:`text-[10px] text-muted-foreground p-2 border rounded`,children:`No exportable accounts.`}):(0,Z.jsx)(`div`,{className:`max-h-36 overflow-y-auto space-y-1 border rounded p-2`,children:a.map(e=>(0,Z.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,Z.jsx)(`input`,{type:`checkbox`,id:`exp-${e.id}`,checked:o.has(e.id),onChange:t=>{let n=new Set(o);t.target.checked?n.add(e.id):n.delete(e.id),s(n)},className:`size-3.5 accent-primary cursor-pointer`}),(0,Z.jsx)(`label`,{htmlFor:`exp-${e.id}`,className:`text-xs cursor-pointer truncate`,children:e.label??e.email??e.id.slice(0,8)})]},e.id))})]}),(0,Z.jsxs)(`div`,{className:`space-y-1.5`,children:[(0,Z.jsxs)(R,{className:`text-xs`,children:[`Password `,(0,Z.jsx)(`span`,{className:`text-muted-foreground font-normal`,children:`(optional)`})]}),(0,Z.jsx)(A,{type:`password`,placeholder:`Leave empty for default`,value:l,onChange:e=>f(e.target.value),className:`text-xs h-8`,autoComplete:`new-password`})]}),(0,Z.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,Z.jsx)(`input`,{type:`checkbox`,id:`exp-full`,checked:p,onChange:e=>h(e.target.checked),className:`size-3.5 accent-primary cursor-pointer`}),(0,Z.jsx)(`label`,{htmlFor:`exp-full`,className:`text-[11px] cursor-pointer`,children:`Include refresh tokens (full transfer)`})]}),(0,Z.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,Z.jsx)(`input`,{type:`checkbox`,id:`exp-refresh`,checked:g,onChange:e=>_(e.target.checked),className:`size-3.5 accent-primary cursor-pointer`}),(0,Z.jsx)(`label`,{htmlFor:`exp-refresh`,className:`text-[11px] cursor-pointer`,children:`Refresh tokens before export`})]}),p?(0,Z.jsxs)(`div`,{className:`rounded-md border border-error/30 bg-error/5 p-2.5`,children:[(0,Z.jsx)(`p`,{className:`text-[10px] font-medium text-error`,children:`Full transfer — source accounts will expire`}),(0,Z.jsx)(`p`,{className:`text-[10px] text-muted-foreground`,children:`Refresh tokens included. Source machine expires in ~1h after target refreshes.`})]}):g?(0,Z.jsx)(`div`,{className:`rounded-md border border-warning/30 bg-warning/5 p-2.5`,children:(0,Z.jsx)(`p`,{className:`text-[10px] font-medium text-warning`,children:`Refresh before export — invalidates previous shares`})}):(0,Z.jsx)(`div`,{className:`rounded-md border border-success/30 bg-success/5 p-2.5`,children:(0,Z.jsx)(`p`,{className:`text-[10px] font-medium text-success`,children:`Share current token (safe)`})}),(0,Z.jsx)(`p`,{className:`text-[10px] text-muted-foreground`,children:`Encrypted with AES-256-GCM + scrypt.`})]}),(0,Z.jsxs)(w,{className:`gap-1.5 flex-col sm:flex-row`,children:[(0,Z.jsx)(S,{size:`sm`,variant:`outline`,className:`text-xs h-7 cursor-pointer`,onClick:j,children:`Cancel`}),(0,Z.jsxs)(S,{size:`sm`,variant:`outline`,className:`text-xs h-7 cursor-pointer`,disabled:!N,onClick:()=>M(!0),children:[(0,Z.jsx)(d,{className:`size-3 mr-1`}),` Copy`]}),(0,Z.jsx)(S,{size:`sm`,className:`text-xs h-7 cursor-pointer`,disabled:!N,onClick:()=>M(!1),children:y?(0,Z.jsxs)(Z.Fragment,{children:[(0,Z.jsx)(u,{className:`size-3 animate-spin mr-1`}),` Exporting...`]}):(0,Z.jsxs)(Z.Fragment,{children:[(0,Z.jsx)(m,{className:`size-3 mr-1`}),` Download`]})})]})]})})}function ke({open:e,onOpenChange:t,onSuccess:n}){let[r,i]=(0,X.useState)(``),[a,o]=(0,X.useState)(``),[s,l]=(0,X.useState)(!1),[d,f]=(0,X.useState)(null);function p(){t(!1),i(``),o(``),f(null)}async function m(){if(r.trim()){l(!0),f(null);try{let e=await K({data:r.trim(),password:a.trim()||`ppm-hienlh`});p(),n(`Imported ${e.imported} account(s)`)}catch(e){f(e.message||`Import failed`)}l(!1)}}return(0,Z.jsx)(O,{open:e,onOpenChange:e=>{e||p()},children:(0,Z.jsxs)(T,{className:`sm:max-w-md`,children:[(0,Z.jsxs)(C,{children:[(0,Z.jsxs)(E,{className:`text-sm flex items-center gap-1.5`,children:[(0,Z.jsx)(c,{className:`size-3.5`}),` Import Accounts`]}),(0,Z.jsx)(D,{className:`text-xs`,children:`Paste backup data and enter the export password. Imported accounts are temporary (~1h).`})]}),(0,Z.jsxs)(`div`,{className:`space-y-3`,children:[(0,Z.jsxs)(`div`,{className:`space-y-1.5`,children:[(0,Z.jsx)(R,{className:`text-xs`,children:`Backup data`}),(0,Z.jsx)(`textarea`,{value:r,onChange:e=>i(e.target.value),placeholder:`Paste backup JSON here...`,rows:4,className:`w-full text-xs p-2 rounded border border-border bg-background font-mono resize-none focus:outline-none focus:ring-1 focus:ring-primary`})]}),(0,Z.jsxs)(`div`,{className:`space-y-1.5`,children:[(0,Z.jsxs)(R,{className:`text-xs`,children:[`Password `,(0,Z.jsx)(`span`,{className:`text-muted-foreground font-normal`,children:`(optional)`})]}),(0,Z.jsx)(A,{type:`password`,placeholder:`Leave empty for default`,value:a,onChange:e=>o(e.target.value),className:`text-xs h-8`,autoComplete:`current-password`})]})]}),d&&(0,Z.jsx)(`div`,{className:`text-[11px] p-2 rounded bg-error/10 text-error`,children:d}),(0,Z.jsxs)(w,{children:[(0,Z.jsx)(S,{size:`sm`,variant:`outline`,className:`text-xs h-7 cursor-pointer`,onClick:p,children:`Cancel`}),(0,Z.jsx)(S,{size:`sm`,className:`text-xs h-7 cursor-pointer`,disabled:!r.trim()||s,onClick:m,children:s?(0,Z.jsxs)(Z.Fragment,{children:[(0,Z.jsx)(u,{className:`size-3 animate-spin mr-1`}),` Importing...`]}):`Import`})]})]})})}var $=typeof window<`u`?window.matchMedia(`(min-width: 768px)`):null;function Ae(e){return $?.addEventListener(`change`,e),()=>$?.removeEventListener(`change`,e)}function je(){return $?.matches??!0}function Me(){let[e,t]=(0,X.useState)(null),[n,r]=(0,X.useState)(!0);return(0,X.useEffect)(()=>{r(!0),W().then(t).finally(()=>r(!1))},[]),n?(0,Z.jsx)(`p`,{className:`text-xs text-text-subtle py-4 text-center`,children:`Loading...`}):e?(0,Z.jsxs)(`div`,{className:`space-y-4`,children:[(0,Z.jsxs)(`div`,{className:`space-y-1.5`,children:[(0,Z.jsx)(`label`,{className:`text-xs font-medium text-text-primary`,children:`Rotation Strategy`}),(0,Z.jsxs)(te,{value:e.strategy,onValueChange:async e=>{t(await Y({strategy:e}))},children:[(0,Z.jsx)(L,{className:`w-full h-9 text-xs`,children:(0,Z.jsx)(N,{})}),(0,Z.jsxs)(M,{children:[(0,Z.jsx)(P,{value:`round-robin`,children:`Round-robin`}),(0,Z.jsx)(P,{value:`fill-first`,children:`Fill-first`}),(0,Z.jsx)(P,{value:`lowest-usage`,children:`Lowest usage`})]})]}),(0,Z.jsxs)(`p`,{className:`text-[10px] text-text-subtle`,children:[e.strategy===`round-robin`&&`Cycles through accounts evenly`,e.strategy===`fill-first`&&`Uses one account until its limit, then moves on`,e.strategy===`lowest-usage`&&`Picks the account with the lowest current usage`]})]}),(0,Z.jsxs)(`div`,{className:`space-y-1.5`,children:[(0,Z.jsx)(`label`,{className:`text-xs font-medium text-text-primary`,children:`Max Retry`}),(0,Z.jsx)(`input`,{type:`number`,min:0,value:e.maxRetry,className:`w-full h-9 text-xs border rounded-md px-3 bg-background`,onChange:async e=>{let n=parseInt(e.target.value,10);!isNaN(n)&&n>=0&&t(await Y({maxRetry:n}))}}),(0,Z.jsx)(`p`,{className:`text-[10px] text-text-subtle`,children:`How many accounts to try on failure. 0 = try all available accounts.`})]}),(0,Z.jsxs)(`div`,{className:`space-y-1.5 border-t border-border pt-3`,children:[(0,Z.jsxs)(`div`,{className:`flex items-center justify-between`,children:[(0,Z.jsx)(`label`,{className:`text-xs font-medium text-text-primary`,children:`Cooldown parking`}),(0,Z.jsx)(I,{checked:e.cooldownEnabled,onCheckedChange:async e=>{t(await Y({cooldownEnabled:e}))}})]}),(0,Z.jsx)(`p`,{className:`text-[10px] text-text-subtle`,children:`Park a failing account (rate limit / usage limit / auth error) until it recovers. Off: accounts stay selectable — rotation still skips accounts already ≥95% on the 5-hour limit.`})]}),(0,Z.jsxs)(`div`,{className:`flex items-center justify-between text-xs border-t border-border pt-3`,children:[(0,Z.jsx)(`span`,{className:`text-text-subtle`,children:`Active accounts`}),(0,Z.jsx)(`span`,{className:`font-medium text-text-primary`,children:e.activeCount})]})]}):(0,Z.jsx)(`p`,{className:`text-xs text-text-subtle py-4 text-center`,children:`Failed to load settings`})}function Ne({open:e,onOpenChange:t}){let n=(0,X.useSyncExternalStore)(Ae,je);return e?n?(0,Z.jsx)(O,{open:e,onOpenChange:t,children:(0,Z.jsxs)(T,{className:`sm:max-w-sm`,children:[(0,Z.jsx)(C,{children:(0,Z.jsxs)(E,{className:`text-sm flex items-center gap-2`,children:[(0,Z.jsx)(f,{className:`size-4`}),` Rotation & Retry`]})}),(0,Z.jsx)(Me,{})]})}):(0,Z.jsxs)(Z.Fragment,{children:[(0,Z.jsx)(`div`,{className:`fixed inset-0 z-50 transition-opacity duration-200 opacity-100`,onClick:()=>t(!1),style:{backgroundColor:`rgba(0,0,0,0.5)`}}),(0,Z.jsxs)(`div`,{className:y(`fixed bottom-0 left-0 right-0 z-50 bg-background rounded-t-2xl border-t border-border shadow-2xl`,`transition-transform duration-300 ease-out max-h-[85vh] overflow-y-auto`,`translate-y-0`),children:[(0,Z.jsx)(`div`,{className:`flex justify-center pt-3 pb-1`,children:(0,Z.jsx)(`div`,{className:`w-10 h-1 rounded-full bg-border`})}),(0,Z.jsxs)(`div`,{className:`flex items-center justify-between px-4 py-2 border-b border-border`,children:[(0,Z.jsxs)(`span`,{className:`text-sm font-semibold flex items-center gap-2`,children:[(0,Z.jsx)(f,{className:`size-4`}),` Rotation & Retry`]}),(0,Z.jsx)(`button`,{onClick:()=>t(!1),className:`flex items-center justify-center size-7 rounded-md hover:bg-surface-elevated transition-colors`,children:(0,Z.jsx)(r,{className:`size-4`})})]}),(0,Z.jsx)(`div`,{className:`px-4 py-4 pb-8`,children:(0,Z.jsx)(Me,{})})]})]}):null}var Pe=x(`inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2`,{variants:{variant:{default:`border-transparent bg-primary text-primary-foreground shadow`,secondary:`border-transparent bg-secondary text-secondary-foreground`,destructive:`border-transparent bg-destructive text-destructive-foreground shadow`,outline:`text-foreground`}},defaultVariants:{variant:`default`}});function Fe({className:e,variant:t,...n}){return(0,Z.jsx)(`div`,{className:y(Pe({variant:t}),e),...n})}function Ie(e){let t=Math.floor(Date.now()/1e3);return e>t?`${Math.floor((e-t)/60)}m`:`exp`}function Le({accounts:e}){let[t,n]=(0,X.useState)([]),[r,i]=(0,X.useState)(!1),[a,o]=(0,X.useState)(!1),[s,c]=(0,X.useState)(new Map),l=e.filter(e=>e.hasRefreshToken);async function d(){if(l.length!==0){i(!0);try{let e=await J(l.map(e=>e.id),a);n(t=>[...t,{round:t.length+1,time:new Date().toLocaleTimeString(),includeRefresh:a,items:e}])}catch{}i(!1)}}async function f(e,t){c(t=>new Map(t).set(e,{loading:!0}));try{let n=await ae(t);c(t=>new Map(t).set(e,{loading:!1,...n}))}catch(t){c(n=>new Map(n).set(e,{loading:!1,status:`error`,error:t.message}))}}function p(e,t){return[{key:`r${e.round}-pre-${t.id}`,label:`Pre-export`,token:t.preExportTokenFull,preview:t.preExportToken,expires:t.preExportExpires},{key:`r${e.round}-exp-${t.id}`,label:`Exported`,token:t.exportedTokenFull,preview:t.exportedToken,expires:t.exportedExpires},{key:`r${e.round}-post-${t.id}`,label:`Post-export`,token:t.postExportTokenFull,preview:t.postExportToken,expires:t.postExportExpires}]}function m(){for(let e of t)for(let t of e.items)for(let n of p(e,t))n.token&&f(n.key,n.token)}return(0,Z.jsxs)(`section`,{className:`border-t pt-4 space-y-2`,children:[(0,Z.jsxs)(`div`,{className:`flex items-center justify-between gap-2`,children:[(0,Z.jsx)(`p`,{className:`text-xs font-medium`,children:`Simulate export`}),t.length>0&&(0,Z.jsxs)(`span`,{className:`text-[10px] text-muted-foreground`,children:[t.length,` round(s)`]})]}),(0,Z.jsx)(`p`,{className:`text-[11px] text-muted-foreground`,children:`Every run appends a round. Earlier rounds are kept so tokens can be compared across runs.`}),l.length===0?(0,Z.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:`No OAuth account with a refresh token to export.`}):(0,Z.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,Z.jsxs)(`div`,{className:`flex items-center gap-1.5`,children:[(0,Z.jsx)(I,{id:`sim-include-refresh`,checked:a,onCheckedChange:o,className:`cursor-pointer`}),(0,Z.jsx)(R,{htmlFor:`sim-include-refresh`,className:`text-[11px] cursor-pointer`,children:`Include refresh tokens`})]}),(0,Z.jsx)(S,{size:`sm`,className:`h-7 text-xs cursor-pointer`,disabled:r,onClick:()=>void d(),children:r?(0,Z.jsxs)(Z.Fragment,{children:[(0,Z.jsx)(u,{className:`size-3 animate-spin mr-1`}),` Exporting...`]}):`Run export #${t.length+1}`})]}),t.length>0&&(0,Z.jsxs)(`div`,{className:`space-y-3`,children:[(0,Z.jsxs)(S,{size:`sm`,variant:`outline`,className:`w-full h-7 text-[11px] cursor-pointer`,onClick:m,children:[`Test all tokens (`,t.reduce((e,t)=>e+t.items.length*3,0),`)`]}),t.map(e=>(0,Z.jsxs)(`div`,{className:`space-y-1.5`,children:[(0,Z.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,Z.jsxs)(`p`,{className:`text-xs font-medium text-primary`,children:[`Round #`,e.round]}),(0,Z.jsx)(`span`,{className:`text-[10px] text-muted-foreground`,children:e.time}),(0,Z.jsx)(Fe,{variant:e.includeRefresh?`destructive`:`secondary`,className:`text-[9px] px-1 py-0`,children:e.includeRefresh?`with refresh`:`access only`})]}),e.items.map(t=>(0,Z.jsxs)(`div`,{className:`p-2 rounded-lg border bg-card space-y-1.5`,children:[(0,Z.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,Z.jsx)(`span`,{className:`text-[11px] font-medium`,children:t.label??t.email??t.id.slice(0,8)}),t.tokenChanged&&(0,Z.jsx)(Fe,{variant:`secondary`,className:`text-[9px] px-1 py-0`,children:`DB token changed`})]}),p(e,t).map(e=>{let t=s.get(e.key);return(0,Z.jsxs)(`div`,{className:`flex items-center gap-1.5 text-[11px]`,children:[(0,Z.jsx)(`span`,{className:`w-20 shrink-0 text-muted-foreground text-[10px]`,children:e.label}),(0,Z.jsx)(`code`,{className:`flex-1 truncate font-mono text-[10px] bg-muted px-1 rounded`,children:e.preview??`N/A`}),e.expires&&(0,Z.jsx)(`span`,{className:`text-[10px] text-muted-foreground shrink-0`,children:Ie(e.expires)}),e.token?(0,Z.jsx)(S,{size:`sm`,variant:`outline`,className:`h-6 text-[10px] px-1.5 cursor-pointer shrink-0`,disabled:t?.loading,onClick:()=>void f(e.key,e.token),children:t?.loading?(0,Z.jsx)(u,{className:`size-3 animate-spin`}):`Test`}):(0,Z.jsx)(`span`,{className:`text-[10px] text-muted-foreground`,children:`-`}),t&&!t.loading&&(0,Z.jsx)(`span`,{className:`text-[10px] font-medium shrink-0 ${t.status?.startsWith(`valid`)?`text-success`:`text-error`}`,children:t.status})]},e.key)})]},`r${e.round}-${t.id}`))]},e.round))]})]})}function Re(e){let t=Math.floor(Date.now()/1e3),n=Math.floor(Math.abs(e-t)/60);return e>t?`${n}m left`:`expired ${n}m ago`}function ze({open:e,onOpenChange:t,accounts:n}){let[r,i]=(0,X.useState)(new Map),[a,o]=(0,X.useState)(!1);async function s(e){i(t=>new Map(t).set(e,{loading:!0}));try{let t=await ie(e,a);i(n=>new Map(n).set(e,{loading:!1,result:t}))}catch(t){i(n=>new Map(n).set(e,{loading:!1,error:t.message}))}}function c(){for(let e of n.filter(e=>e.expiresAt!==null))s(e.id)}return(0,Z.jsx)(O,{open:e,onOpenChange:t,children:(0,Z.jsxs)(T,{className:`sm:max-w-xl max-h-[85vh] flex flex-col`,children:[(0,Z.jsxs)(C,{children:[(0,Z.jsxs)(E,{className:`text-sm flex items-center gap-1.5`,children:[(0,Z.jsx)(g,{className:`size-4`}),` Token Test`]}),(0,Z.jsx)(D,{className:`text-xs`,children:`Probe stored tokens for validity. Nothing is modified.`})]}),(0,Z.jsxs)(`div`,{className:`space-y-4 overflow-y-auto flex-1 pr-1`,children:[(0,Z.jsxs)(`section`,{className:`space-y-2`,children:[(0,Z.jsxs)(`div`,{className:`flex items-center justify-between gap-2`,children:[(0,Z.jsx)(`p`,{className:`text-xs font-medium`,children:`Current tokens`}),(0,Z.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,Z.jsxs)(`div`,{className:`flex items-center gap-1.5`,children:[(0,Z.jsx)(I,{id:`token-test-refresh`,checked:a,onCheckedChange:o,className:`cursor-pointer`}),(0,Z.jsx)(R,{htmlFor:`token-test-refresh`,className:`text-[11px] cursor-pointer`,children:`Also test refresh`})]}),(0,Z.jsx)(S,{size:`sm`,variant:`outline`,className:`h-7 text-xs cursor-pointer`,onClick:c,children:`Test all`})]})]}),n.length===0&&(0,Z.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:`No accounts to test.`}),(0,Z.jsx)(`div`,{className:`space-y-1.5`,children:n.map(e=>{let t=r.get(e.id),n=t?.result?.accessToken.status;return(0,Z.jsxs)(`div`,{className:`p-2 rounded border bg-card space-y-1`,children:[(0,Z.jsxs)(`div`,{className:`flex items-center justify-between gap-2`,children:[(0,Z.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,Z.jsx)(`span`,{className:`text-xs font-medium truncate block`,children:e.label??e.email??e.id.slice(0,8)}),e.expiresAt&&(0,Z.jsx)(`span`,{className:`text-[10px] text-muted-foreground`,children:Re(e.expiresAt)})]}),(0,Z.jsx)(S,{size:`sm`,variant:`outline`,className:`h-7 text-xs cursor-pointer shrink-0`,disabled:t?.loading,onClick:()=>void s(e.id),children:t?.loading?(0,Z.jsx)(u,{className:`size-3 animate-spin`}):`Test`})]}),t&&!t.loading&&(0,Z.jsxs)(`div`,{className:`text-[11px] pl-2 border-l-2 border-muted`,children:[t.error&&(0,Z.jsx)(`p`,{className:`text-error`,children:t.error}),n&&(0,Z.jsxs)(`span`,{className:n.startsWith(`valid`)?`text-success`:`text-error`,children:[n,t.result?.accessToken.code?` (${t.result.accessToken.code})`:``]})]})]},e.id)})})]}),(0,Z.jsx)(Le,{accounts:n})]}),(0,Z.jsx)(w,{children:(0,Z.jsx)(S,{size:`sm`,variant:`outline`,className:`text-xs cursor-pointer`,onClick:()=>t(!1),children:`Close`})})]})})}function Be(){let{usages:e,accounts:t,activeAccountId:n,initialLoading:r,refreshing:i,flashIds:a,reload:s}=pe(),[c,d]=(0,X.useState)(null),[h,_]=(0,X.useState)(null),[v,y]=(0,X.useState)(null),[b,x]=(0,X.useState)(null),[C,w]=(0,X.useState)(null),[T,E]=(0,X.useState)(null);function D(e){s(),e&&d(e)}async function O(e,t){_(e);try{await q(e,{status:t===`disabled`?`active`:`disabled`})}catch(e){d(e.message)}_(null),s()}async function k(){if(v){try{await V(v.id),d(`Account "${v.display}" removed.`),s()}catch(e){d(`Failed to remove: ${e.message}`)}y(null)}}let A=new Map(t.map(e=>[e.id,e])),j=e[0]?.usage.lastFetchedAt;return(0,Z.jsxs)(`div`,{className:`space-y-4`,children:[(0,Z.jsx)(me,{description:`Connect multiple Claude accounts. PPM rotates between them automatically to avoid rate limits.`,onRefresh:()=>void s(),refreshing:i,disabled:r,actions:(0,Z.jsxs)(Z.Fragment,{children:[(0,Z.jsxs)(S,{size:`sm`,className:`cursor-pointer gap-1.5`,onClick:()=>E(`add`),children:[(0,Z.jsx)(p,{className:`size-4`}),` Add account`]}),(0,Z.jsxs)(S,{size:`sm`,variant:`outline`,className:`cursor-pointer gap-1.5`,onClick:()=>{w(null),E(`export`)},children:[(0,Z.jsx)(m,{className:`size-4`}),` Export`]}),(0,Z.jsxs)(S,{size:`sm`,variant:`outline`,className:`cursor-pointer gap-1.5`,onClick:()=>E(`import`),children:[(0,Z.jsx)(l,{className:`size-4`}),` Import`]}),(0,Z.jsxs)(S,{size:`sm`,variant:`outline`,className:`cursor-pointer gap-1.5`,onClick:()=>E(`rotation`),children:[(0,Z.jsx)(f,{className:`size-4`}),` Rotation`]}),(0,Z.jsxs)(S,{size:`sm`,variant:`outline`,className:`cursor-pointer gap-1.5`,onClick:()=>E(`token-test`),children:[(0,Z.jsx)(g,{className:`size-4`}),` Token test`]})]})}),(0,Z.jsx)(se,{message:c,onDismiss:()=>d(null)}),(0,Z.jsxs)(`section`,{className:`space-y-2`,children:[(0,Z.jsxs)(`div`,{className:`flex items-center justify-between`,children:[(0,Z.jsx)(`h3`,{className:`text-sm font-medium`,children:`Accounts`}),j&&(0,Z.jsxs)(`span`,{className:`text-[10px] text-muted-foreground`,children:[`usage `,ce(new Date(j).getTime())]})]}),r?(0,Z.jsxs)(`div`,{className:`flex items-center gap-2 py-6 text-xs text-muted-foreground`,children:[(0,Z.jsx)(u,{className:`size-4 animate-spin`}),` Loading accounts...`]}):e.length===0?(0,Z.jsxs)(`div`,{className:`flex flex-col items-center gap-2 py-8 text-center`,children:[(0,Z.jsx)(o,{className:`size-5 text-muted-foreground`}),(0,Z.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:`No accounts connected yet.`}),(0,Z.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:`Add one to start using PPM.`})]}):(0,Z.jsx)(oe,{children:e.map(e=>(0,Z.jsx)(ue,{layout:`strip`,entry:e,isActive:e.accountId===n,accountInfo:A.get(e.accountId),onToggle:O,toggling:h===e.accountId,onDelete:(e,t)=>y({id:e,display:t}),onExport:e=>{w(e),E(`export`)},onViewProfile:(e,t)=>x({profile:e,accountId:t}),flash:a.has(e.accountId)},e.accountId))})]}),b&&(0,Z.jsx)(we,{profile:b.profile,accountId:b.accountId,onClose:()=>x(null)}),v&&(0,Z.jsx)(Te,{display:v.display,onConfirm:()=>void k(),onCancel:()=>y(null)}),(0,Z.jsx)(Ee,{open:T===`add`,onOpenChange:e=>E(e?`add`:null),onSuccess:D}),(0,Z.jsx)(Oe,{open:T===`export`,onOpenChange:e=>{E(e?`export`:null),e||w(null)},accounts:t,preselectId:C,onMessage:d}),(0,Z.jsx)(ke,{open:T===`import`,onOpenChange:e=>E(e?`import`:null),onSuccess:D}),(0,Z.jsx)(Ne,{open:T===`rotation`,onOpenChange:e=>E(e?`rotation`:null)}),(0,Z.jsx)(ze,{open:T===`token-test`,onOpenChange:e=>E(e?`token-test`:null),accounts:t})]})}function Ve({open:e,onOpenChange:t,label:n,onLabelChange:r,apiKey:a,onApiKeyChange:c,adding:l,onAddApiKey:d,deviceWaiting:f,onStartDevice:p,device:m,error:h,browser:g,onStartBrowser:_,loginStarting:v,callbackUrl:y,onCallbackUrlChange:b,submittingCallback:x,onSubmitCallback:k}){let j=v||f||!!g||l;return(0,Z.jsx)(O,{open:e,onOpenChange:t,children:(0,Z.jsxs)(T,{className:`sm:max-w-md max-h-[90dvh] overflow-y-auto`,children:[(0,Z.jsxs)(C,{children:[(0,Z.jsx)(E,{className:`text-sm`,children:`Add Codex Account`}),(0,Z.jsx)(D,{className:`text-xs leading-relaxed`,children:`Choose browser login, a device code, or an OpenAI API key.`})]}),(0,Z.jsxs)(`div`,{className:`space-y-3`,children:[(0,Z.jsxs)(`div`,{className:`space-y-1.5`,children:[(0,Z.jsx)(R,{htmlFor:`codex-label`,className:`text-xs`,children:`Label (optional)`}),(0,Z.jsx)(A,{id:`codex-label`,placeholder:`e.g. Personal, Work`,value:n,onChange:e=>r(e.target.value),disabled:j,className:`text-xs`})]}),(0,Z.jsxs)(`div`,{className:`rounded-md border p-3 space-y-2`,children:[(0,Z.jsx)(`p`,{className:`text-[11px] font-medium`,children:`Browser login`}),(0,Z.jsxs)(S,{size:`sm`,className:`w-full cursor-pointer gap-1.5`,onClick:_,disabled:j,children:[(0,Z.jsx)(s,{className:`size-4`}),` Sign in with browser`]}),g&&(0,Z.jsxs)(`div`,{className:`space-y-2 text-xs`,children:[(0,Z.jsxs)(`a`,{href:g.authUrl,target:`_blank`,rel:`noreferrer`,className:`inline-flex items-center gap-1 text-primary hover:underline`,children:[(0,Z.jsx)(s,{className:`size-3`}),` Open ChatGPT sign-in`]}),(0,Z.jsx)(`p`,{children:`Sign in, then copy the full localhost callback URL from the address bar and paste it below. On a phone, copy it even if the page fails to load.`}),(0,Z.jsx)(R,{htmlFor:`codex-callback-url`,children:`Callback URL`}),(0,Z.jsx)(A,{id:`codex-callback-url`,type:`password`,autoComplete:`off`,spellCheck:!1,placeholder:`http://localhost:.../auth/callback?...`,value:y,onChange:e=>b(e.target.value),disabled:x}),(0,Z.jsxs)(S,{size:`sm`,onClick:k,disabled:!y.trim()||x,children:[x&&(0,Z.jsx)(u,{className:`size-3 animate-spin`}),` Submit callback URL`]}),(0,Z.jsxs)(`p`,{role:`status`,className:`flex items-center gap-1.5 text-muted-foreground`,children:[(0,Z.jsx)(u,{className:`size-3 animate-spin`}),` Waiting for authorization. This may complete automatically on this computer.`]})]})]}),(0,Z.jsxs)(`div`,{className:`rounded-md border p-3 space-y-2`,children:[(0,Z.jsx)(`p`,{className:`text-[11px] font-medium`,children:`Device code`}),(0,Z.jsx)(S,{size:`sm`,className:`w-full cursor-pointer gap-1.5`,onClick:p,disabled:j,children:f?(0,Z.jsxs)(Z.Fragment,{children:[(0,Z.jsx)(u,{className:`size-4 animate-spin`}),` Waiting for authorization...`]}):(0,Z.jsxs)(Z.Fragment,{children:[(0,Z.jsx)(i,{className:`size-4`}),` Sign in with device code`]})}),m&&(0,Z.jsxs)(`div`,{className:`rounded-md border border-primary/40 bg-primary/10 p-2.5 space-y-1.5`,children:[(0,Z.jsx)(`p`,{className:`text-[11px]`,children:`Enter this code to authorize:`}),(0,Z.jsx)(`div`,{className:`font-mono text-lg tracking-widest`,children:m.userCode}),(0,Z.jsxs)(`a`,{href:m.verificationUrl,target:`_blank`,rel:`noreferrer`,className:`inline-flex items-center gap-1 text-primary hover:underline text-[11px]`,children:[(0,Z.jsx)(s,{className:`size-3`}),` `,m.verificationUrl]})]})]}),(0,Z.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,Z.jsx)(`div`,{className:`flex-1 border-t`}),(0,Z.jsx)(`span`,{className:`text-[10px] text-muted-foreground`,children:`or paste an API key`}),(0,Z.jsx)(`div`,{className:`flex-1 border-t`})]}),(0,Z.jsxs)(`div`,{className:`space-y-1.5`,children:[(0,Z.jsx)(R,{htmlFor:`codex-api-key`,className:`text-xs`,children:`OpenAI API key`}),(0,Z.jsx)(A,{id:`codex-api-key`,type:`password`,placeholder:`sk-...`,value:a,onChange:e=>c(e.target.value),className:`text-xs font-mono`})]})]}),v&&(0,Z.jsx)(`p`,{role:`status`,className:`text-xs`,children:`Starting login...`}),h&&(0,Z.jsx)(`div`,{role:`alert`,className:`text-[11px] p-2 rounded bg-error/10 text-error`,children:h}),(0,Z.jsxs)(w,{children:[(0,Z.jsx)(S,{size:`sm`,variant:`outline`,className:`text-xs cursor-pointer`,onClick:()=>t(!1),children:`Cancel`}),(0,Z.jsxs)(S,{size:`sm`,className:`text-xs cursor-pointer gap-1.5`,onClick:d,disabled:!a.trim()||j,children:[l?(0,Z.jsx)(u,{className:`size-3.5 animate-spin`}):(0,Z.jsx)(o,{className:`size-3.5`}),`Add key`]})]})]})})}function He({mode:e,onOpenChange:t,password:n,onPasswordChange:r,busy:i,onExport:a,onImport:o,error:s}){let d=(0,X.useRef)(null),f=e===`export`;return(0,Z.jsx)(O,{open:e!==null,onOpenChange:t,children:(0,Z.jsxs)(T,{className:`sm:max-w-md`,children:[(0,Z.jsxs)(C,{children:[(0,Z.jsxs)(E,{className:`text-sm flex items-center gap-1.5`,children:[(0,Z.jsx)(c,{className:`size-4`}),` `,f?`Export Codex Accounts`:`Import Codex Accounts`]}),(0,Z.jsx)(D,{className:`text-xs leading-relaxed`,children:f?`Writes a password-encrypted file containing each account's login.`:`Reads a backup file. The password must be the one it was exported with.`})]}),(0,Z.jsxs)(`div`,{className:`space-y-1.5`,children:[(0,Z.jsx)(R,{htmlFor:`codex-backup-password`,className:`text-xs`,children:`Password`}),(0,Z.jsx)(A,{id:`codex-backup-password`,type:`password`,placeholder:f?`Choose a password`:`Password used for the export`,value:n,onChange:e=>r(e.target.value),className:`text-xs`,autoComplete:f?`new-password`:`current-password`}),(0,Z.jsx)(`p`,{className:`text-[10px] text-muted-foreground`,children:`A backup holds full logins — keep the file and its password together.`})]}),s&&(0,Z.jsx)(`div`,{className:`text-[11px] p-2 rounded bg-error/10 text-error`,children:s}),(0,Z.jsx)(`input`,{ref:d,type:`file`,accept:`application/json,.json`,className:`hidden`,onChange:e=>{let t=e.target.files?.[0];e.target.value=``,t&&o(t)}}),(0,Z.jsxs)(w,{children:[(0,Z.jsx)(S,{size:`sm`,variant:`outline`,className:`text-xs cursor-pointer`,onClick:()=>t(!1),children:`Cancel`}),(0,Z.jsxs)(S,{size:`sm`,className:`text-xs cursor-pointer gap-1.5`,disabled:!n.trim()||i,onClick:()=>f?a():d.current?.click(),children:[i?(0,Z.jsx)(u,{className:`size-3.5 animate-spin`}):f?(0,Z.jsx)(m,{className:`size-3.5`}):(0,Z.jsx)(l,{className:`size-3.5`}),f?`Download backup`:`Choose file`]})]})]})})}var Ue=[{value:`round-robin`,label:`Round-robin`,hint:`Take the next account each time.`},{value:`fill-first`,label:`Fill-first`,hint:`Stay on one account until it runs out.`},{value:`lowest-usage`,label:`Lowest usage`,hint:`Pick whichever has the most left.`}];function We({open:e,onOpenChange:t,strategy:n,onChange:r}){return(0,Z.jsx)(O,{open:e,onOpenChange:t,children:(0,Z.jsxs)(T,{className:`sm:max-w-md`,children:[(0,Z.jsxs)(C,{children:[(0,Z.jsx)(E,{className:`text-sm`,children:`Codex Rotation`}),(0,Z.jsx)(D,{className:`text-xs leading-relaxed`,children:`How a new chat chooses among your Codex accounts.`})]}),(0,Z.jsx)(`div`,{className:`space-y-2`,children:Ue.map(e=>(0,Z.jsxs)(`button`,{type:`button`,onClick:()=>r(e.value),"aria-current":n===e.value?`true`:void 0,className:y(`w-full text-left rounded-md border px-3 py-2.5 min-h-11 cursor-pointer transition-colors`,n===e.value?`border-primary bg-primary/10`:`border-border hover:bg-accent/50 active:bg-accent`),children:[(0,Z.jsx)(`p`,{className:`text-sm font-medium`,children:e.label}),(0,Z.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:e.hint})]},e.value))}),(0,Z.jsx)(w,{children:(0,Z.jsx)(S,{size:`sm`,variant:`outline`,className:`text-xs cursor-pointer`,onClick:()=>t(!1),children:`Done`})})]})})}function Ge(e){return e==null?null:Math.round(e*100)}function Ke({usage:e}){let t=e.session==null&&e.weekly==null;return(0,Z.jsxs)(`div`,{className:`space-y-2`,children:[(t||e.session!=null)&&(0,Z.jsx)(ge,{label:`5-Hour Session`,pct:Ge(e.fiveHour),reset:le(e.session),resetsAt:e.session?.resetsAt}),(t||e.weekly!=null)&&(0,Z.jsx)(ge,{label:`Weekly`,pct:Ge(e.sevenDay),reset:le(e.weekly),resetsAt:e.weekly?.resetsAt})]})}var qe=2e3,Je=21e4;function Ye(e){let[t,n]=(0,X.useState)([]),[r,i]=(0,X.useState)(`round-robin`),[a,o]=(0,X.useState)({}),[s,c]=(0,X.useState)(!1),[l,u]=(0,X.useState)(new Set),[d,f]=(0,X.useState)(``),[p,m]=(0,X.useState)(``),[h,g]=(0,X.useState)(!1),[y,b]=(0,X.useState)(null),[x,S]=(0,X.useState)(null),[C,w]=(0,X.useState)(!1),[T,E]=(0,X.useState)(null),[D,O]=(0,X.useState)(!1),[k,A]=(0,X.useState)(``),[j,M]=(0,X.useState)(!1),[N,P]=(0,X.useState)(``),[ee,F]=(0,X.useState)(!1),[te,I]=(0,X.useState)(!1),[ne,L]=(0,X.useState)(null),R=(0,X.useRef)(null),z=(0,X.useRef)(null),B=(0,X.useRef)(0),V=(0,X.useRef)(!1),H=(0,X.useRef)(!1),U=(0,X.useRef)(!1),W=(0,X.useRef)(e);W.current=e;let G=(0,X.useCallback)(async()=>{c(!0);try{let e=await _.get(`/api/codex-accounts`);n(e.accounts),i(e.strategy),_.get(`/api/codex-accounts/usage`).then(o).catch(()=>{})}finally{c(!1)}},[]);(0,X.useEffect)(()=>{G()},[G]);let re=async()=>{if(!(!d.trim()||H.current||V.current||z.current)){H.current=!0,g(!0),b(null);try{await _.post(`/api/codex-accounts/api-key`,{apiKey:d.trim(),label:p.trim()||void 0}),f(``),m(``),e(),await G()}catch(e){b(e.message)}finally{H.current=!1,g(!1)}}},K=(0,X.useCallback)(()=>{B.current+=1,V.current=!1,U.current=!1,R.current?.(),R.current=null;let e=z.current;z.current=null,e&&_.del(`/api/codex-accounts/${e.method}-login/${e.id}`).catch(()=>{})},[]),q=(0,X.useCallback)(()=>{K(),S(null),w(!1),E(null),O(!1),A(``),M(!1)},[K]),ie=(0,X.useCallback)((e,t)=>{let n=Date.now()+(t===`browser`?61e4:Je),r=!1,i;R.current=()=>{r=!0,clearTimeout(i)};let a=async()=>{if(r)return;let o=null;try{o=await _.get(`/api/codex-accounts/${t}-login/${e}/status`)}catch{}if(!r){if(o?.state===`done`){q(),m(``),W.current(),await G();return}if(o?.state===`error`||Date.now()>n){q(),b(o?.state===`error`?o.error:`${t===`browser`?`Browser`:`Device`} login timed out.`);return}i=setTimeout(a,qe)}};a()},[q,G]);(0,X.useEffect)(()=>K,[K]);let J=async e=>{if(V.current||z.current||H.current)return;V.current=!0;let t=++B.current;O(!0),b(null);try{let n=await _.post(`/api/codex-accounts/${e}-login`,{label:p.trim()||void 0});if(t!==B.current){_.del(`/api/codex-accounts/${e}-login/${n.id}`).catch(()=>{});return}z.current={id:n.id,method:e},e===`device`?(S(n),w(!0)):E(n),ie(n.id,e)}catch(e){t===B.current&&b(e.message)}finally{t===B.current&&(V.current=!1,O(!1))}};return{accounts:t,strategy:r,usages:a,loading:s,apiKey:d,setApiKey:f,label:p,setLabel:m,adding:h,err:y,setErr:b,device:x,deviceWaiting:C,backupPassword:N,setBackupPassword:P,exporting:ee,importing:te,msg:ne,setMsg:L,toggling:l,load:G,addApiKey:re,startDevice:()=>J(`device`),startBrowser:()=>J(`browser`),browser:T,loginStarting:D,callbackUrl:k,setCallbackUrl:A,submittingCallback:j,submitCallback:async()=>{let e=z.current;if(e?.method!==`browser`||!k.trim()||U.current)return;let t=B.current;U.current=!0,M(!0),b(null);let n=k.trim();A(``);try{await _.post(`/api/codex-accounts/browser-login/${e.id}/callback`,{callbackUrl:n})}catch(e){t===B.current&&b(e.message)}finally{t===B.current&&(U.current=!1,M(!1))}},cancelLogin:q,remove:async e=>{await _.del(`/api/codex-accounts/${e}`),await G()},toggle:async(e,t)=>{let n=t===`disabled`?`active`:`disabled`;u(t=>new Set(t).add(e)),b(null);try{await _.patch(`/api/codex-accounts/${e}`,{status:n}),await G()}catch(e){b(e.message||`Could not change the account`)}finally{u(t=>{let n=new Set(t);return n.delete(e),n})}},toggleDailyGuard:async(e,t)=>{u(t=>new Set(t).add(e)),b(null);try{await _.patch(`/api/codex-accounts/${e}`,{dailyGuardEnabled:!t}),await G()}catch(e){b(e.message||`Could not change Daily guard`)}finally{u(t=>{let n=new Set(t);return n.delete(e),n})}},changeStrategy:async e=>{i(e);try{await _.put(`/api/codex-accounts/strategy`,{strategy:e})}catch{}},doExport:async()=>{if(!N.trim()){b(`Set a backup password first.`);return}F(!0),b(null),L(null);try{let t={"Content-Type":`application/json`},n=v();n&&(t.Authorization=`Bearer ${n}`);let r=await fetch(`/api/codex-accounts/export`,{method:`POST`,headers:t,body:JSON.stringify({password:N})});if(!r.ok){let e=await r.json().catch(()=>({}));throw Error(e.error??`Export failed: ${r.status}`)}let i=await r.text(),a=new Blob([i],{type:`application/json`}),o=document.createElement(`a`);o.href=URL.createObjectURL(a),o.download=`ppm-codex-accounts-backup.json`,o.click(),URL.revokeObjectURL(o.href),L(`Backup downloaded.`),e()}catch(e){b(e.message)}finally{F(!1)}},doImport:async t=>{if(!N.trim()){b(`Enter the backup password first.`);return}I(!0),b(null),L(null);try{let n=await t.text(),r=await _.post(`/api/codex-accounts/import`,{data:n,password:N});L(`Imported ${r.imported}${r.skipped?`, skipped ${r.skipped}`:``}.`),e(),await G()}catch(e){b(e.message)}finally{I(!1)}}}}function Xe(){let[e,t]=(0,X.useState)(null),n=Ye(()=>t(null));return(0,Z.jsxs)(`div`,{className:`space-y-4`,children:[(0,Z.jsx)(me,{description:(0,Z.jsxs)(Z.Fragment,{children:[`Each Codex account keeps its own login (`,(0,Z.jsx)(`code`,{children:`CODEX_HOME`}),`). One is picked per chat by the rotation setting.`]}),onRefresh:()=>void n.load(),refreshing:n.loading,actions:(0,Z.jsxs)(Z.Fragment,{children:[(0,Z.jsxs)(S,{size:`sm`,className:`cursor-pointer gap-1.5`,onClick:()=>t(`add`),children:[(0,Z.jsx)(p,{className:`size-4`}),` Add account`]}),(0,Z.jsxs)(S,{size:`sm`,variant:`outline`,className:`cursor-pointer gap-1.5`,onClick:()=>t(`export`),children:[(0,Z.jsx)(m,{className:`size-4`}),` Export`]}),(0,Z.jsxs)(S,{size:`sm`,variant:`outline`,className:`cursor-pointer gap-1.5`,onClick:()=>t(`import`),children:[(0,Z.jsx)(l,{className:`size-4`}),` Import`]}),(0,Z.jsxs)(S,{size:`sm`,variant:`outline`,className:`cursor-pointer gap-1.5`,onClick:()=>t(`rotation`),children:[(0,Z.jsx)(f,{className:`size-4`}),` Rotation`]})]})}),(0,Z.jsx)(se,{message:n.msg??n.err,onDismiss:()=>{n.setMsg(null),n.setErr(null)}}),(0,Z.jsxs)(`section`,{className:`space-y-2`,children:[(0,Z.jsxs)(`div`,{className:`flex items-center justify-between`,children:[(0,Z.jsx)(`h3`,{className:`text-sm font-medium`,children:`Codex Accounts`}),(0,Z.jsx)(`span`,{className:`text-[10px] text-muted-foreground capitalize`,children:n.strategy.replace(`-`,` `)})]}),n.loading&&n.accounts.length===0?(0,Z.jsxs)(`div`,{className:`flex items-center gap-2 py-6 text-xs text-muted-foreground`,children:[(0,Z.jsx)(u,{className:`size-4 animate-spin`}),` Loading accounts...`]}):n.accounts.length===0?(0,Z.jsxs)(`div`,{className:`flex flex-col items-center gap-2 py-8 text-center`,children:[(0,Z.jsx)(o,{className:`size-5 text-muted-foreground`}),(0,Z.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:`No Codex accounts yet.`}),(0,Z.jsxs)(`p`,{className:`text-xs text-muted-foreground`,children:[`Chats use your default `,(0,Z.jsx)(`code`,{children:`~/.codex`}),` login until you add one.`]})]}):(0,Z.jsx)(oe,{children:n.accounts.map(e=>{let t=n.usages[e.id]??{};return(0,Z.jsxs)(fe,{dense:!0,className:`min-w-[300px] shrink-0 snap-start`,"data-testid":`account-card`,"data-account-id":e.id,children:[(0,Z.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,Z.jsx)(`span`,{className:`text-sm font-medium truncate flex-1 min-w-0`,children:e.label}),(0,Z.jsx)(`span`,{className:`text-[10px] uppercase tracking-wide text-text-subtle border border-border rounded px-1 shrink-0`,children:e.type}),de(e.planType)&&(0,Z.jsx)(`span`,{className:`text-[10px] text-text-subtle shrink-0`,children:de(e.planType)}),(0,Z.jsx)(I,{checked:e.status!==`disabled`,onCheckedChange:()=>void n.toggle(e.id,e.status??`active`),disabled:n.toggling.has(e.id),"aria-label":e.status===`disabled`?`Enable account`:`Disable account`,className:`cursor-pointer shrink-0`}),(0,Z.jsxs)(F,{children:[(0,Z.jsx)(z,{asChild:!0,children:(0,Z.jsx)(`button`,{type:`button`,onClick:()=>void n.remove(e.id),"aria-label":`Remove account`,className:`p-2 rounded cursor-pointer text-text-subtle hover:text-error hover:bg-surface-elevated transition-colors shrink-0`,children:(0,Z.jsx)(h,{className:`size-4`})})}),(0,Z.jsx)(j,{side:`top`,children:`Remove account`})]})]}),(0,Z.jsx)(Ke,{usage:t}),t.session==null&&(()=>{let r=he(t.weekly);return r?(0,Z.jsxs)(`div`,{className:`flex min-h-12 items-center justify-between gap-3 rounded-md border px-2.5 py-1.5 ${e.dailyGuardEnabled&&r.blocked?`border-error/40 bg-error/5`:`border-border/50 bg-surface/40`}`,children:[(0,Z.jsxs)(`div`,{className:`min-w-0 space-y-0.5`,children:[(0,Z.jsxs)(`div`,{className:`flex items-center gap-1.5 text-xs font-medium ${e.dailyGuardEnabled&&r.blocked?`text-error`:`text-text-secondary`}`,children:[(0,Z.jsx)(`span`,{children:`Daily guard`}),(0,Z.jsxs)(F,{children:[(0,Z.jsx)(z,{asChild:!0,children:(0,Z.jsx)(`button`,{type:`button`,"aria-label":`About Daily guard`,className:`text-text-subtle hover:text-text-primary cursor-help`,children:(0,Z.jsx)(a,{className:`size-3.5`})})}),(0,Z.jsx)(j,{side:`top`,className:`max-w-64 text-xs leading-relaxed`,children:`Keeps a weekly-only Codex account on pace to last until its reset. Each day adds one seventh of the weekly quota. When usage reaches that day's cap, PPM pauses new turns until the next day. Turn it off any time to use the remaining quota freely.`})]})]}),(0,Z.jsx)(`p`,{className:`text-[10px] tabular-nums ${e.dailyGuardEnabled&&r.blocked?`text-error`:`text-text-subtle`}`,children:e.dailyGuardEnabled&&r.blocked?`${Math.round(r.used*100)}% used / ${Math.round(r.cap*100)}% daily cap. New turns paused.`:`Day ${r.day}/7, ${Math.round(r.cap*100)}% daily cap`})]}),(0,Z.jsxs)(`span`,{className:`hidden`,title:`Day ${r.day}/7: hold weekly usage at or below ${Math.round(r.cap*100)}% to last until reset.`,children:[`Daily guard `,e.dailyGuardEnabled?`· Day ${r.day}/7 cap ${Math.round(r.cap*100)}%`:``]}),(0,Z.jsx)(I,{checked:!!e.dailyGuardEnabled,onCheckedChange:()=>void n.toggleDailyGuard(e.id,!!e.dailyGuardEnabled),disabled:n.toggling.has(e.id),"aria-label":e.dailyGuardEnabled?`Disable Daily guard`:`Enable Daily guard`,className:`cursor-pointer shrink-0`})]}):null})()]},e.id)})})]}),(0,Z.jsx)(Ve,{open:e===`add`,onOpenChange:e=>{e||n.cancelLogin(),t(e?`add`:null)},label:n.label,onLabelChange:n.setLabel,apiKey:n.apiKey,onApiKeyChange:n.setApiKey,adding:n.adding,onAddApiKey:()=>void n.addApiKey(),deviceWaiting:n.deviceWaiting,onStartDevice:()=>void n.startDevice(),device:n.device,browser:n.browser,onStartBrowser:()=>void n.startBrowser(),loginStarting:n.loginStarting,callbackUrl:n.callbackUrl,onCallbackUrlChange:n.setCallbackUrl,submittingCallback:n.submittingCallback,onSubmitCallback:()=>void n.submitCallback(),error:n.err}),(0,Z.jsx)(He,{mode:e===`export`?`export`:e===`import`?`import`:null,onOpenChange:e=>{e||t(null)},password:n.backupPassword,onPasswordChange:n.setBackupPassword,busy:n.exporting||n.importing,onExport:()=>void n.doExport(),onImport:e=>void n.doImport(e),error:n.err}),(0,Z.jsx)(We,{open:e===`rotation`,onOpenChange:e=>t(e?`rotation`:null),strategy:n.strategy,onChange:e=>void n.changeStrategy(e)})]})}var Ze=[{id:`claude`,label:`Claude`},{id:`codex`,label:`Codex`}];function Qe(){let[e,t]=(0,X.useState)(null),[n,r]=(0,X.useState)(`claude`);(0,X.useEffect)(()=>{let e=!1;return U().then(n=>{e||t(new Set(Object.keys(n.providers??{})))}).catch(()=>{e||t(new Set([`claude`]))}),()=>{e=!0}},[]);let i=Ze.filter(t=>t.id===`claude`||e?.has(t.id)),a=i.length>1;return(0,Z.jsxs)(`div`,{className:`space-y-4`,"data-testid":`accounts-pane`,"data-provider":n,children:[a&&(0,Z.jsx)(`div`,{className:`flex gap-1 border-b border-border/50`,children:i.map(e=>(0,Z.jsxs)(`button`,{onClick:()=>r(e.id),"data-testid":`accounts-tab-${e.id}`,"aria-current":n===e.id?`page`:void 0,className:`flex items-center gap-1.5 px-3 py-2 text-xs rounded-t transition-colors cursor-pointer ${n===e.id?`text-primary border-b-2 border-primary font-medium`:`text-text-subtle hover:text-text-secondary`}`,children:[(0,Z.jsx)(ne,{providerId:e.id}),e.label]},e.id))}),e===null&&!a&&(0,Z.jsxs)(`div`,{className:`flex items-center gap-2 text-xs text-muted-foreground`,children:[(0,Z.jsx)(u,{className:`size-3 animate-spin`}),` Checking configured providers...`]}),n===`claude`?(0,Z.jsx)(Be,{}):(0,Z.jsx)(Xe,{})]})}export{Qe as AccountsSettingsSection};
@@ -1 +1 @@
1
- import{o as e}from"./rolldown-runtime-FhOqtrmT.js";import{C as t,S as n}from"./vendor-markdown-CuWCHIVb.js";import"./vendor-ui-BBdkypKW.js";import{Dr as r,dn as i,ln as a,r as o,tr as s}from"./icons-DQjqk0lJ.js";import{s as c}from"./api-client-bfB7cXRM.js";import"./vendor-mermaid-Cbig5zA6.js";import"./settings-store-DZPgPtEA.js";import{n as l}from"./utils-Bmf_8h7Y.js";import{G as u,H as d,U as f,V as p,W as m,c as h}from"./index-ZtfxZOyb.js";import{t as g}from"./use-monaco-theme-DBqbWjWX.js";var _=e(t(),1),v=n();function y({metadata:e}){let t=e?.filePath,n=e?.name,l=e?.resourceType??`skill`,y=e?.scope??`user`,x=e?.readOnly===!0,S=e?.shadowed===!0,C=e?.shadowedBy,w=e?.project??``,[T,E]=(0,_.useState)(``),[D,O]=(0,_.useState)(``),[k,A]=(0,_.useState)(!0),[j,M]=(0,_.useState)(null),[N,P]=(0,_.useState)(!1),F=g(),I=(0,_.useRef)(``),L=T!==D;(0,_.useEffect)(()=>{let e=!1;return A(!0),M(null),m(t,w).then(t=>{e||(E(t.content),O(t.content),I.current=t.content)}).catch(t=>{e||M(t.message)}).finally(()=>{e||A(!1)}),()=>{e=!0}},[t,w]);let R=(0,_.useCallback)(async()=>{if(x||N)return;let e=I.current;if(e!==D){P(!0);try{await u(t,e,w),O(e),E(e),c.success(`Saved`),f.getState().reload()}catch(e){c.error(e.message)}finally{P(!1)}}},[t,w,D,x,N]);(0,_.useEffect)(()=>{let e=e=>{(e.ctrlKey||e.metaKey)&&e.key.toLowerCase()===`s`&&(e.preventDefault(),R())};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[R]);let z=d[l];return(0,v.jsxs)(`div`,{className:`flex h-full flex-col`,children:[(0,v.jsxs)(`div`,{className:`flex items-center gap-2 border-b border-border px-3 py-2 shrink-0`,children:[(0,v.jsx)(z,{className:`size-4 text-text-subtle`}),(0,v.jsx)(`span`,{className:`text-sm font-semibold`,children:n}),(0,v.jsx)(p,{scope:y}),L&&!x&&(0,v.jsx)(`span`,{className:`size-1.5 rounded-full bg-primary`,title:`Unsaved changes`}),(0,v.jsx)(`div`,{className:`flex-1`}),(0,v.jsxs)(`span`,{className:`hidden sm:block text-[11px] text-text-subtle`,children:[l,` · markdown`]}),!x&&(0,v.jsxs)(v.Fragment,{children:[(0,v.jsxs)(`button`,{onClick:()=>E(D),disabled:!L||N,className:`flex items-center gap-1 rounded px-2 py-1 text-xs text-text-secondary hover:bg-surface-elevated disabled:opacity-40`,children:[(0,v.jsx)(r,{className:`size-3.5`}),` Revert`]}),(0,v.jsxs)(`button`,{onClick:()=>void R(),disabled:!L||N,className:`flex items-center gap-1 rounded bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-40`,children:[N?(0,v.jsx)(a,{className:`size-3.5 animate-spin`}):(0,v.jsx)(s,{className:`size-3.5`}),` Save`]})]})]}),x&&(0,v.jsx)(b,{icon:i,tone:`muted`,children:`Bundled resource — read-only. Duplicate it (from the AI Resources panel) to make an editable copy.`}),S&&C&&(0,v.jsxs)(b,{icon:o,tone:`warn`,children:[`This `,l,` is overridden by another “`,C.name,`” from a higher-priority source (`,C.source,`). It is currently inactive.`]}),(0,v.jsx)(`div`,{className:`min-h-0 flex-1`,children:k?(0,v.jsx)(`div`,{className:`flex h-full items-center justify-center`,children:(0,v.jsx)(a,{className:`size-5 animate-spin text-primary`})}):j?(0,v.jsx)(`div`,{className:`p-6 text-center text-sm text-destructive`,children:j}):(0,v.jsx)(h,{height:`100%`,language:`markdown`,theme:F,value:T,onChange:e=>{let t=e??``;I.current=t,E(t)},options:{readOnly:x,minimap:{enabled:!1},fontSize:13,wordWrap:`on`,scrollBeyondLastLine:!1,lineNumbers:`on`}})})]})}function b({icon:e,tone:t,children:n}){return(0,v.jsxs)(`div`,{className:l(`flex items-start gap-2 px-3 py-2 text-[12px] shrink-0 border-b border-border`,t===`warn`?`bg-amber-500/10 text-amber-300`:`bg-surface-elevated text-text-secondary`),children:[(0,v.jsx)(e,{className:`mt-0.5 size-3.5 shrink-0`}),(0,v.jsx)(`span`,{children:n})]})}export{y as AiResourceEditor};
1
+ import{o as e}from"./rolldown-runtime-FhOqtrmT.js";import{C as t,S as n}from"./vendor-markdown-CuWCHIVb.js";import"./vendor-ui-DmZ-Mb-H.js";import{Dr as r,dn as i,ln as a,r as o,tr as s}from"./icons-DQjqk0lJ.js";import{s as c}from"./api-client-C1ZwRL4Y.js";import"./settings-store-krC6bc79.js";import"./vendor-mermaid-BitiXJlL.js";import{n as l}from"./utils-Bmf_8h7Y.js";import{B as u,H as d,U as f,V as p,c as m,z as h}from"./index-DU68xk06.js";import{t as g}from"./use-monaco-theme-BIVYJBIn.js";var _=e(t(),1),v=n();function y({metadata:e}){let t=e?.filePath,n=e?.name,l=e?.resourceType??`skill`,y=e?.scope??`user`,x=e?.readOnly===!0,S=e?.shadowed===!0,C=e?.shadowedBy,w=e?.project??``,[T,E]=(0,_.useState)(``),[D,O]=(0,_.useState)(``),[k,A]=(0,_.useState)(!0),[j,M]=(0,_.useState)(null),[N,P]=(0,_.useState)(!1),F=g(),I=(0,_.useRef)(``),L=T!==D;(0,_.useEffect)(()=>{let e=!1;return A(!0),M(null),d(t,w).then(t=>{e||(E(t.content),O(t.content),I.current=t.content)}).catch(t=>{e||M(t.message)}).finally(()=>{e||A(!1)}),()=>{e=!0}},[t,w]);let R=(0,_.useCallback)(async()=>{if(x||N)return;let e=I.current;if(e!==D){P(!0);try{await f(t,e,w),O(e),E(e),c.success(`Saved`),p.getState().reload()}catch(e){c.error(e.message)}finally{P(!1)}}},[t,w,D,x,N]);(0,_.useEffect)(()=>{let e=e=>{(e.ctrlKey||e.metaKey)&&e.key.toLowerCase()===`s`&&(e.preventDefault(),R())};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[R]);let z=u[l];return(0,v.jsxs)(`div`,{className:`flex h-full flex-col`,children:[(0,v.jsxs)(`div`,{className:`flex items-center gap-2 border-b border-border px-3 py-2 shrink-0`,children:[(0,v.jsx)(z,{className:`size-4 text-text-subtle`}),(0,v.jsx)(`span`,{className:`text-sm font-semibold`,children:n}),(0,v.jsx)(h,{scope:y}),L&&!x&&(0,v.jsx)(`span`,{className:`size-1.5 rounded-full bg-primary`,title:`Unsaved changes`}),(0,v.jsx)(`div`,{className:`flex-1`}),(0,v.jsxs)(`span`,{className:`hidden sm:block text-[11px] text-text-subtle`,children:[l,` · markdown`]}),!x&&(0,v.jsxs)(v.Fragment,{children:[(0,v.jsxs)(`button`,{onClick:()=>E(D),disabled:!L||N,className:`flex items-center gap-1 rounded px-2 py-1 text-xs text-text-secondary hover:bg-surface-elevated disabled:opacity-40`,children:[(0,v.jsx)(r,{className:`size-3.5`}),` Revert`]}),(0,v.jsxs)(`button`,{onClick:()=>void R(),disabled:!L||N,className:`flex items-center gap-1 rounded bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-40`,children:[N?(0,v.jsx)(a,{className:`size-3.5 animate-spin`}):(0,v.jsx)(s,{className:`size-3.5`}),` Save`]})]})]}),x&&(0,v.jsx)(b,{icon:i,tone:`muted`,children:`Bundled resource — read-only. Duplicate it (from the AI Resources panel) to make an editable copy.`}),S&&C&&(0,v.jsxs)(b,{icon:o,tone:`warn`,children:[`This `,l,` is overridden by another “`,C.name,`” from a higher-priority source (`,C.source,`). It is currently inactive.`]}),(0,v.jsx)(`div`,{className:`min-h-0 flex-1`,children:k?(0,v.jsx)(`div`,{className:`flex h-full items-center justify-center`,children:(0,v.jsx)(a,{className:`size-5 animate-spin text-primary`})}):j?(0,v.jsx)(`div`,{className:`p-6 text-center text-sm text-destructive`,children:j}):(0,v.jsx)(m,{height:`100%`,language:`markdown`,theme:F,value:T,onChange:e=>{let t=e??``;I.current=t,E(t)},options:{readOnly:x,minimap:{enabled:!1},fontSize:13,wordWrap:`on`,scrollBeyondLastLine:!1,lineNumbers:`on`}})})]})}function b({icon:e,tone:t,children:n}){return(0,v.jsxs)(`div`,{className:l(`flex items-start gap-2 px-3 py-2 text-[12px] shrink-0 border-b border-border`,t===`warn`?`bg-amber-500/10 text-amber-300`:`bg-surface-elevated text-text-secondary`),children:[(0,v.jsx)(e,{className:`mt-0.5 size-3.5 shrink-0`}),(0,v.jsx)(`span`,{children:n})]})}export{y as AiResourceEditor};