@hienlh/ppm 0.23.21 → 0.23.23

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 (497) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/CLAUDE.md +6 -3
  3. package/README.md +1 -1
  4. package/assets/skills/ppm/SKILL.md +1 -1
  5. package/assets/skills/ppm/references/cli-reference.md +4 -4
  6. package/assets/skills/ppm/references/http-api.md +2 -1
  7. package/dist/web/assets/{accounts-settings-section-DD88DRdx.js → accounts-settings-section-Cg-LncY6.js} +1 -1
  8. package/dist/web/assets/accounts-settings-section-Cg-LncY6.js.br +0 -0
  9. package/dist/web/assets/accounts-settings-section-Cg-LncY6.js.gz +0 -0
  10. package/dist/web/assets/{agent-session-sheet-C0PhhLKR.js → agent-session-sheet-DMdV8GOu.js} +2 -2
  11. package/dist/web/assets/agent-session-sheet-DMdV8GOu.js.br +0 -0
  12. package/dist/web/assets/agent-session-sheet-DMdV8GOu.js.gz +0 -0
  13. package/dist/web/assets/{agent-session-window-content-D0XZgh4U.js → agent-session-window-content-CYg58J-V.js} +1 -1
  14. package/dist/web/assets/agent-session-window-content-CYg58J-V.js.br +0 -0
  15. package/dist/web/assets/agent-session-window-content-CYg58J-V.js.gz +0 -0
  16. package/dist/web/assets/{ai-resource-editor-CpfQB6A7.js → ai-resource-editor-BvEnn0Z_.js} +1 -1
  17. package/dist/web/assets/ai-resource-editor-BvEnn0Z_.js.br +0 -0
  18. package/dist/web/assets/ai-resource-editor-BvEnn0Z_.js.gz +0 -0
  19. package/dist/web/assets/{ai-settings-section-DqE67XNC.js → ai-settings-section-Bo-nf2rY.js} +1 -1
  20. package/dist/web/assets/ai-settings-section-DAqiu_11.js +1 -0
  21. package/dist/web/assets/ai-settings-section-DAqiu_11.js.br +0 -0
  22. package/dist/web/assets/ai-settings-section-DAqiu_11.js.gz +0 -0
  23. package/dist/web/assets/{android-tab-4Z4AgHz1.js → android-tab-C-wmlFc0.js} +1 -1
  24. package/dist/web/assets/android-tab-C-wmlFc0.js.br +0 -0
  25. package/dist/web/assets/android-tab-C-wmlFc0.js.gz +0 -0
  26. package/dist/web/assets/{appearance-settings-section-DjQjiJF7.js → appearance-settings-section-CYqX6BGh.js} +1 -1
  27. package/dist/web/assets/appearance-settings-section-CYqX6BGh.js.br +0 -0
  28. package/dist/web/assets/appearance-settings-section-CYqX6BGh.js.gz +0 -0
  29. package/dist/web/assets/{apps-panel-_lxY9xbc.js → apps-panel-D1VTBxgC.js} +1 -1
  30. package/dist/web/assets/apps-panel-D1VTBxgC.js.br +0 -0
  31. package/dist/web/assets/apps-panel-D1VTBxgC.js.gz +0 -0
  32. package/dist/web/assets/{branch-review-tab-BzrhnFSs.js → branch-review-tab-BG6vxSsC.js} +1 -1
  33. package/dist/web/assets/branch-review-tab-BG6vxSsC.js.br +0 -0
  34. package/dist/web/assets/branch-review-tab-BG6vxSsC.js.gz +0 -0
  35. package/dist/web/assets/chat-tab-BAwGLGyR.js +19 -0
  36. package/dist/web/assets/chat-tab-BAwGLGyR.js.br +0 -0
  37. package/dist/web/assets/chat-tab-BAwGLGyR.js.gz +0 -0
  38. package/dist/web/assets/{chat-tab-BKHepkYi.js → chat-tab-BJ0Mi2sd.js} +1 -1
  39. package/dist/web/assets/chat-tab-BJ0Mi2sd.js.br +0 -0
  40. package/dist/web/assets/chat-tab-BJ0Mi2sd.js.gz +0 -0
  41. package/dist/web/assets/{code-editor-COWmNXO4.js → code-editor-DuwbwTpM.js} +4 -4
  42. package/dist/web/assets/code-editor-DuwbwTpM.js.br +0 -0
  43. package/dist/web/assets/code-editor-DuwbwTpM.js.gz +0 -0
  44. package/dist/web/assets/{codex-reset-credit-button-BHt4yJet.js → codex-reset-credit-button-tIwfnf2G.js} +1 -1
  45. package/dist/web/assets/codex-reset-credit-button-tIwfnf2G.js.br +0 -0
  46. package/dist/web/assets/codex-reset-credit-button-tIwfnf2G.js.gz +0 -0
  47. package/dist/web/assets/{conflict-editor-tj-1gzKr.js → conflict-editor-D2n-hQqs.js} +1 -1
  48. package/dist/web/assets/conflict-editor-D2n-hQqs.js.br +0 -0
  49. package/dist/web/assets/conflict-editor-D2n-hQqs.js.gz +0 -0
  50. package/dist/web/assets/{connection-form-tab-BV2QX8Cf.js → connection-form-tab-DYqPNq_-.js} +1 -1
  51. package/dist/web/assets/connection-form-tab-DYqPNq_-.js.br +0 -0
  52. package/dist/web/assets/connection-form-tab-DYqPNq_-.js.gz +0 -0
  53. package/dist/web/assets/{csv-preview-xhE2esgz.js → csv-preview-Xh3Z6BS5.js} +1 -1
  54. package/dist/web/assets/csv-preview-Xh3Z6BS5.js.br +0 -0
  55. package/dist/web/assets/csv-preview-Xh3Z6BS5.js.gz +0 -0
  56. package/dist/web/assets/{db-tab-parts-C3MsjOID.js → db-tab-parts-Dqnb3GCP.js} +1 -1
  57. package/dist/web/assets/db-tab-parts-Dqnb3GCP.js.br +0 -0
  58. package/dist/web/assets/db-tab-parts-Dqnb3GCP.js.gz +0 -0
  59. package/dist/web/assets/{design-settings-section-C3LxYhIw.js → design-settings-section-DNNWgxF9.js} +1 -1
  60. package/dist/web/assets/design-settings-section-DNNWgxF9.js.br +0 -0
  61. package/dist/web/assets/design-settings-section-DNNWgxF9.js.gz +0 -0
  62. package/dist/web/assets/{design-tab-DYcvjdfb.js → design-tab-BoAdSpZZ.js} +1 -1
  63. package/dist/web/assets/design-tab-BoAdSpZZ.js.br +0 -0
  64. package/dist/web/assets/design-tab-BoAdSpZZ.js.gz +0 -0
  65. package/dist/web/assets/{designs-sidebar-panel-Dpr4PPFT.js → designs-sidebar-panel-BKw7HFAz.js} +1 -1
  66. package/dist/web/assets/designs-sidebar-panel-BKw7HFAz.js.br +0 -0
  67. package/dist/web/assets/designs-sidebar-panel-BKw7HFAz.js.gz +0 -0
  68. package/dist/web/assets/{diff-viewer-Fl_4jqr6.js → diff-viewer-CRmfCAZX.js} +1 -1
  69. package/dist/web/assets/diff-viewer-CRmfCAZX.js.br +0 -0
  70. package/dist/web/assets/diff-viewer-CRmfCAZX.js.gz +0 -0
  71. package/dist/web/assets/{docx-preview-bVE9shPX.js → docx-preview-zIrtL09u.js} +1 -1
  72. package/dist/web/assets/docx-preview-zIrtL09u.js.br +0 -0
  73. package/dist/web/assets/docx-preview-zIrtL09u.js.gz +0 -0
  74. package/dist/web/assets/{editor-dialog-eEOYWdbr.js → editor-dialog-jMYvXVla.js} +1 -1
  75. package/dist/web/assets/editor-dialog-jMYvXVla.js.br +0 -0
  76. package/dist/web/assets/editor-dialog-jMYvXVla.js.gz +0 -0
  77. package/dist/web/assets/{editor-language-service-D1_Ggq4s.js → editor-language-service-Yb-61Bro.js} +1 -1
  78. package/dist/web/assets/editor-language-service-Yb-61Bro.js.br +0 -0
  79. package/dist/web/assets/editor-language-service-Yb-61Bro.js.gz +0 -0
  80. package/dist/web/assets/{explorer-body-C0UOcBPJ.js → explorer-body-C6qKehmd.js} +1 -1
  81. package/dist/web/assets/explorer-body-C6qKehmd.js.br +0 -0
  82. package/dist/web/assets/explorer-body-C6qKehmd.js.gz +0 -0
  83. package/dist/web/assets/{explorer-window-content-Dxee7SuJ.js → explorer-window-content-B9P8grrY.js} +1 -1
  84. package/dist/web/assets/explorer-window-content-B9P8grrY.js.br +0 -0
  85. package/dist/web/assets/explorer-window-content-B9P8grrY.js.gz +0 -0
  86. package/dist/web/assets/{extension-manager-section-D8krnoj8.js → extension-manager-section-C45v1QtG.js} +1 -1
  87. package/dist/web/assets/extension-manager-section-C45v1QtG.js.br +0 -0
  88. package/dist/web/assets/extension-manager-section-C45v1QtG.js.gz +0 -0
  89. package/dist/web/assets/{extension-webview-CtIdV69T.js → extension-webview-DIxM-xkH.js} +1 -1
  90. package/dist/web/assets/extension-webview-DIxM-xkH.js.br +0 -0
  91. package/dist/web/assets/extension-webview-DIxM-xkH.js.gz +0 -0
  92. package/dist/web/assets/{files-settings-section-C8vtarR8.js → files-settings-section-DeVlzKXd.js} +1 -1
  93. package/dist/web/assets/files-settings-section-DeVlzKXd.js.br +0 -0
  94. package/dist/web/assets/files-settings-section-DeVlzKXd.js.gz +0 -0
  95. package/dist/web/assets/format-date-CkyHzZLA.js +1 -0
  96. package/dist/web/assets/{general-settings-section-Chu8Ut8h.js → general-settings-section-CLVDIEym.js} +1 -1
  97. package/dist/web/assets/general-settings-section-CLVDIEym.js.br +0 -0
  98. package/dist/web/assets/general-settings-section-CLVDIEym.js.gz +0 -0
  99. package/dist/web/assets/{git-log-panel-DG09p6k5.js → git-log-panel-D3Q9NRll.js} +1 -1
  100. package/dist/web/assets/git-log-panel-D3Q9NRll.js.br +0 -0
  101. package/dist/web/assets/git-log-panel-D3Q9NRll.js.gz +0 -0
  102. package/dist/web/assets/{git-review-tab-CSy8DjSC.js → git-review-tab-ORgiYbva.js} +1 -1
  103. package/dist/web/assets/git-review-tab-ORgiYbva.js.br +0 -0
  104. package/dist/web/assets/git-review-tab-ORgiYbva.js.gz +0 -0
  105. package/dist/web/assets/{glide-data-grid-BN1y4BT0.js → glide-data-grid-CtWl1BTk.js} +1 -1
  106. package/dist/web/assets/glide-data-grid-CtWl1BTk.js.br +0 -0
  107. package/dist/web/assets/glide-data-grid-CtWl1BTk.js.gz +0 -0
  108. package/dist/web/assets/{grid-save-dialog-CfSuWw2t.js → grid-save-dialog-eV25Mqpg.js} +1 -1
  109. package/dist/web/assets/grid-save-dialog-eV25Mqpg.js.br +0 -0
  110. package/dist/web/assets/grid-save-dialog-eV25Mqpg.js.gz +0 -0
  111. package/dist/web/assets/{group-chat-tab-B04QMp7A.js → group-chat-tab-CYmbYXN2.js} +1 -1
  112. package/dist/web/assets/group-chat-tab-CYmbYXN2.js.br +0 -0
  113. package/dist/web/assets/group-chat-tab-CYmbYXN2.js.gz +0 -0
  114. package/dist/web/assets/{impexp-tab-BRNouquR.js → impexp-tab-D2OKxpFq.js} +1 -1
  115. package/dist/web/assets/impexp-tab-D2OKxpFq.js.br +0 -0
  116. package/dist/web/assets/impexp-tab-D2OKxpFq.js.gz +0 -0
  117. package/dist/web/assets/{index-BpgSIM5B.js → index-D8QDpdgs.js} +7 -7
  118. package/dist/web/assets/index-D8QDpdgs.js.br +0 -0
  119. package/dist/web/assets/index-D8QDpdgs.js.gz +0 -0
  120. package/dist/web/assets/{jira-watcher-section-CJeS5r6D.js → jira-watcher-section-B3OaMlim.js} +1 -1
  121. package/dist/web/assets/{keybindings-store-Dvxq-vZD.js → keybindings-store-C_aRhBu4.js} +1 -1
  122. package/dist/web/assets/{keyboard-shortcuts-section-BKDyZHhK.js → keyboard-shortcuts-section-DXRyKrSy.js} +1 -1
  123. package/dist/web/assets/keyboard-shortcuts-section-DXRyKrSy.js.br +0 -0
  124. package/dist/web/assets/keyboard-shortcuts-section-DXRyKrSy.js.gz +0 -0
  125. package/dist/web/assets/{language-servers-section-CbbkezhA.js → language-servers-section-BTs9iuZc.js} +2 -2
  126. package/dist/web/assets/language-servers-section-BTs9iuZc.js.br +0 -0
  127. package/dist/web/assets/language-servers-section-BTs9iuZc.js.gz +0 -0
  128. package/dist/web/assets/{logs-app-Blr1-v4q.js → logs-app-BlhVTOYN.js} +1 -1
  129. package/dist/web/assets/logs-app-BlhVTOYN.js.br +0 -0
  130. package/dist/web/assets/logs-app-BlhVTOYN.js.gz +0 -0
  131. package/dist/web/assets/{logs-tab-CGPtn2tU.js → logs-tab-kqNARbz0.js} +1 -1
  132. package/dist/web/assets/{logs-window-content-DAczcNmh.js → logs-window-content-UUCJJCqG.js} +1 -1
  133. package/dist/web/assets/{lsp-client-Ds3U2O52.js → lsp-client-BTlK_8lb.js} +1 -1
  134. package/dist/web/assets/lsp-client-BTlK_8lb.js.br +0 -0
  135. package/dist/web/assets/lsp-client-BTlK_8lb.js.gz +0 -0
  136. package/dist/web/assets/{markdown-renderer-DbJVwfUH.js → markdown-renderer-ClGxDien.js} +1 -1
  137. package/dist/web/assets/{markdown-renderer-DzCPadpB.js → markdown-renderer-DpsToFFB.js} +1 -1
  138. package/dist/web/assets/markdown-renderer-DpsToFFB.js.br +0 -0
  139. package/dist/web/assets/markdown-renderer-DpsToFFB.js.gz +0 -0
  140. package/dist/web/assets/{mobile-explorer-sheet-BClCMXb8.js → mobile-explorer-sheet-CybJbScN.js} +1 -1
  141. package/dist/web/assets/mobile-explorer-sheet-CybJbScN.js.br +0 -0
  142. package/dist/web/assets/mobile-explorer-sheet-CybJbScN.js.gz +0 -0
  143. package/dist/web/assets/{new-design-dialog-HRP9H11v.js → new-design-dialog-9WTgcJoj.js} +1 -1
  144. package/dist/web/assets/new-design-dialog-9WTgcJoj.js.br +0 -0
  145. package/dist/web/assets/new-design-dialog-9WTgcJoj.js.gz +0 -0
  146. package/dist/web/assets/{notification-store-BomzYNQj.js → notification-store-CPVq5ec5.js} +1 -1
  147. package/dist/web/assets/notifications-settings-section-xrYII-pL.js +1 -0
  148. package/dist/web/assets/notifications-settings-section-xrYII-pL.js.br +0 -0
  149. package/dist/web/assets/notifications-settings-section-xrYII-pL.js.gz +0 -0
  150. package/dist/web/assets/{ppmbot-settings-section-BRowIIqc.js → ppmbot-settings-section-Bc46iDP9.js} +1 -1
  151. package/dist/web/assets/ppmbot-settings-section-Bc46iDP9.js.br +0 -0
  152. package/dist/web/assets/ppmbot-settings-section-Bc46iDP9.js.gz +0 -0
  153. package/dist/web/assets/{problems-panel-DdB10Ybd.js → problems-panel-D49-Xkzd.js} +1 -1
  154. package/dist/web/assets/problems-panel-D49-Xkzd.js.br +0 -0
  155. package/dist/web/assets/problems-panel-D49-Xkzd.js.gz +0 -0
  156. package/dist/web/assets/{process-table-CzLaWPe0.js → process-table-NxYTZ0n9.js} +1 -1
  157. package/dist/web/assets/process-table-NxYTZ0n9.js.br +0 -0
  158. package/dist/web/assets/process-table-NxYTZ0n9.js.gz +0 -0
  159. package/dist/web/assets/{proxy-settings-section-BCq7R5DA.js → proxy-settings-section-Exx7cQc4.js} +1 -1
  160. package/dist/web/assets/proxy-settings-section-Exx7cQc4.js.br +0 -0
  161. package/dist/web/assets/proxy-settings-section-Exx7cQc4.js.gz +0 -0
  162. package/dist/web/assets/{query-tab-DGvj_h6l.js → query-tab-BWtJPuxw.js} +1 -1
  163. package/dist/web/assets/query-tab-BWtJPuxw.js.br +0 -0
  164. package/dist/web/assets/{query-tab-DGvj_h6l.js.gz → query-tab-BWtJPuxw.js.gz} +0 -0
  165. package/dist/web/assets/{remote-access-settings-section-BrqOoOyk.js → remote-access-settings-section-C4K__DNW.js} +1 -1
  166. package/dist/web/assets/remote-access-settings-section-C4K__DNW.js.br +0 -0
  167. package/dist/web/assets/remote-access-settings-section-C4K__DNW.js.gz +0 -0
  168. package/dist/web/assets/{remote-desktop-clipboard-notice-5r6UbdlN.js → remote-desktop-clipboard-notice-DQyysBwL.js} +1 -1
  169. package/dist/web/assets/remote-desktop-clipboard-notice-DQyysBwL.js.br +0 -0
  170. package/dist/web/assets/remote-desktop-clipboard-notice-DQyysBwL.js.gz +0 -0
  171. package/dist/web/assets/{remote-desktop-mobile-sheet-B9Q4auqe.js → remote-desktop-mobile-sheet-DZtEFmgf.js} +2 -2
  172. package/dist/web/assets/remote-desktop-mobile-sheet-DZtEFmgf.js.br +0 -0
  173. package/dist/web/assets/remote-desktop-mobile-sheet-DZtEFmgf.js.gz +0 -0
  174. package/dist/web/assets/{remote-desktop-mobile-view-CL-jrkA8.js → remote-desktop-mobile-view-VW7HdPUX.js} +1 -1
  175. package/dist/web/assets/remote-desktop-mobile-view-VW7HdPUX.js.br +0 -0
  176. package/dist/web/assets/remote-desktop-mobile-view-VW7HdPUX.js.gz +0 -0
  177. package/dist/web/assets/{remote-desktop-readiness-gate-DXEf2Jnz.js → remote-desktop-readiness-gate-B3tuP9Kt.js} +1 -1
  178. package/dist/web/assets/remote-desktop-readiness-gate-B3tuP9Kt.js.br +0 -0
  179. package/dist/web/assets/remote-desktop-readiness-gate-B3tuP9Kt.js.gz +0 -0
  180. package/dist/web/assets/{remote-desktop-window-content-20P6-gKB.js → remote-desktop-window-content-n0NoDCaE.js} +1 -1
  181. package/dist/web/assets/remote-desktop-window-content-n0NoDCaE.js.br +0 -0
  182. package/dist/web/assets/remote-desktop-window-content-n0NoDCaE.js.gz +0 -0
  183. package/dist/web/assets/{rename-object-dialog-Bs7pUsUP.js → rename-object-dialog-rLTW4A55.js} +1 -1
  184. package/dist/web/assets/rename-object-dialog-rLTW4A55.js.br +0 -0
  185. package/dist/web/assets/rename-object-dialog-rLTW4A55.js.gz +0 -0
  186. package/dist/web/assets/{review-code-Bjo4sHN9.js → review-code-DNfsaNO1.js} +1 -1
  187. package/dist/web/assets/review-code-DNfsaNO1.js.br +0 -0
  188. package/dist/web/assets/review-code-DNfsaNO1.js.gz +0 -0
  189. package/dist/web/assets/{save-as-dialog-BHx0Wp2B.js → save-as-dialog-BSz8WrNZ.js} +1 -1
  190. package/dist/web/assets/save-as-dialog-BSz8WrNZ.js.br +0 -0
  191. package/dist/web/assets/save-as-dialog-BSz8WrNZ.js.gz +0 -0
  192. package/dist/web/assets/{schedules-settings-section-C6ATYDPK.js → schedules-settings-section-DqUZ-H2K.js} +1 -1
  193. package/dist/web/assets/schedules-settings-section-DqUZ-H2K.js.br +0 -0
  194. package/dist/web/assets/schedules-settings-section-DqUZ-H2K.js.gz +0 -0
  195. package/dist/web/assets/{session-review-tab-BPMUyNTx.js → session-review-tab-DKtrFb9y.js} +1 -1
  196. package/dist/web/assets/session-review-tab-DKtrFb9y.js.br +0 -0
  197. package/dist/web/assets/session-review-tab-DKtrFb9y.js.gz +0 -0
  198. package/dist/web/assets/{settings-body-Dy5D6Vu7.js → settings-body-C42xA9Dw.js} +1 -1
  199. package/dist/web/assets/settings-body-C42xA9Dw.js.br +0 -0
  200. package/dist/web/assets/settings-body-C42xA9Dw.js.gz +0 -0
  201. package/dist/web/assets/{settings-rows-BRoYE2To.js → settings-rows-C7ZatT9O.js} +1 -1
  202. package/dist/web/assets/settings-rows-C7ZatT9O.js.br +0 -0
  203. package/dist/web/assets/settings-rows-C7ZatT9O.js.gz +0 -0
  204. package/dist/web/assets/{settings-section-content-essUViKu.js → settings-section-content-B_iXJpcK.js} +2 -2
  205. package/dist/web/assets/settings-section-content-B_iXJpcK.js.br +0 -0
  206. package/dist/web/assets/settings-section-content-B_iXJpcK.js.gz +0 -0
  207. package/dist/web/assets/settings-section-content-rxfQEQ66.js +1 -0
  208. package/dist/web/assets/{settings-tab-Di1DUP1j.js → settings-tab-PRw6OpEb.js} +1 -1
  209. package/dist/web/assets/{settings-window-content-DYvYFPWy.js → settings-window-content-WITQsGfW.js} +1 -1
  210. package/dist/web/assets/{slash-command-picker-8da30mYP.js → slash-command-picker-DmDs8nOj.js} +1 -1
  211. package/dist/web/assets/slash-command-picker-DmDs8nOj.js.br +0 -0
  212. package/dist/web/assets/slash-command-picker-DmDs8nOj.js.gz +0 -0
  213. package/dist/web/assets/{sql-object-tab-DRIHVXuR.js → sql-object-tab-D6P4hzkb.js} +1 -1
  214. package/dist/web/assets/sql-object-tab-D6P4hzkb.js.br +0 -0
  215. package/dist/web/assets/sql-object-tab-D6P4hzkb.js.gz +0 -0
  216. package/dist/web/assets/{sqlite-viewer-G-Jl2yEr.js → sqlite-viewer-mUitnzHB.js} +1 -1
  217. package/dist/web/assets/sqlite-viewer-mUitnzHB.js.br +0 -0
  218. package/dist/web/assets/sqlite-viewer-mUitnzHB.js.gz +0 -0
  219. package/dist/web/assets/{structure-save-dialog-Cwikz0we.js → structure-save-dialog-CmbNlmBY.js} +1 -1
  220. package/dist/web/assets/structure-save-dialog-CmbNlmBY.js.br +0 -0
  221. package/dist/web/assets/structure-save-dialog-CmbNlmBY.js.gz +0 -0
  222. package/dist/web/assets/{structure-tab-CWEgdZPm.js → structure-tab-DCPNuFTe.js} +1 -1
  223. package/dist/web/assets/structure-tab-DCPNuFTe.js.br +0 -0
  224. package/dist/web/assets/structure-tab-DCPNuFTe.js.gz +0 -0
  225. package/dist/web/assets/{system-monitor-body-BDQm9-wb.js → system-monitor-body-BhxyqSqu.js} +2 -2
  226. package/dist/web/assets/system-monitor-body-BhxyqSqu.js.br +0 -0
  227. package/dist/web/assets/system-monitor-body-BhxyqSqu.js.gz +0 -0
  228. package/dist/web/assets/{system-monitor-tab-BouN21Yh.js → system-monitor-tab-B_miogQI.js} +1 -1
  229. package/dist/web/assets/{system-monitor-window-content-BRyWBSOb.js → system-monitor-window-content-8IogR6tR.js} +1 -1
  230. package/dist/web/assets/{tab-host-window-content-CZ55-bTz.js → tab-host-window-content-BWHQjgWZ.js} +1 -1
  231. package/dist/web/assets/tab-host-window-content-BWHQjgWZ.js.br +0 -0
  232. package/dist/web/assets/tab-host-window-content-BWHQjgWZ.js.gz +0 -0
  233. package/dist/web/assets/{table-tab-Bmn4UdBV.js → table-tab-E3YMueu_.js} +1 -1
  234. package/dist/web/assets/table-tab-E3YMueu_.js.br +0 -0
  235. package/dist/web/assets/table-tab-E3YMueu_.js.gz +0 -0
  236. package/dist/web/assets/{telegram-bot-token-field-fdHwRf39.js → telegram-bot-token-field-CwW24m62.js} +1 -1
  237. package/dist/web/assets/telegram-bot-token-field-CwW24m62.js.br +0 -0
  238. package/dist/web/assets/telegram-bot-token-field-CwW24m62.js.gz +0 -0
  239. package/dist/web/assets/{terminal-tab-D-qlZXLK.js → terminal-tab-BAe5cWEL.js} +1 -1
  240. package/dist/web/assets/terminal-tab-BAe5cWEL.js.br +0 -0
  241. package/dist/web/assets/terminal-tab-BAe5cWEL.js.gz +0 -0
  242. package/dist/web/assets/{tool-cards-BdXbQ9Fv.js → tool-cards-DrCiSH2r.js} +2 -2
  243. package/dist/web/assets/tool-cards-DrCiSH2r.js.br +0 -0
  244. package/dist/web/assets/tool-cards-DrCiSH2r.js.gz +0 -0
  245. package/dist/web/assets/{tools-settings-section-CcPaspbR.js → tools-settings-section-S-vBr2af.js} +1 -1
  246. package/dist/web/assets/tools-settings-section-S-vBr2af.js.br +0 -0
  247. package/dist/web/assets/tools-settings-section-S-vBr2af.js.gz +0 -0
  248. package/dist/web/assets/{use-inline-blame-Ck5XoSS3.js → use-inline-blame-BzVaaCCt.js} +1 -1
  249. package/dist/web/assets/use-inline-blame-BzVaaCCt.js.br +0 -0
  250. package/dist/web/assets/{use-inline-blame-Ck5XoSS3.js.gz → use-inline-blame-BzVaaCCt.js.gz} +0 -0
  251. package/dist/web/assets/{use-lsp-CGSqfFBk.js → use-lsp-BwDL9Fqk.js} +1 -1
  252. package/dist/web/assets/use-lsp-BwDL9Fqk.js.br +0 -0
  253. package/dist/web/assets/use-lsp-BwDL9Fqk.js.gz +0 -0
  254. package/dist/web/assets/{use-monaco-theme-BW05eZS4.js → use-monaco-theme-DljSc3Xo.js} +1 -1
  255. package/dist/web/assets/{use-process-kill-DJ_f7rCO.js → use-process-kill-YDcDqDtG.js} +1 -1
  256. package/dist/web/assets/use-process-kill-YDcDqDtG.js.br +0 -0
  257. package/dist/web/assets/use-process-kill-YDcDqDtG.js.gz +0 -0
  258. package/dist/web/assets/{use-sql-schema-info-wt320fVR.js → use-sql-schema-info-cAI0vn7d.js} +1 -1
  259. package/dist/web/assets/use-sql-schema-info-cAI0vn7d.js.br +0 -0
  260. package/dist/web/assets/use-sql-schema-info-cAI0vn7d.js.gz +0 -0
  261. package/dist/web/assets/{use-websocket-CTwe74Cb.js → use-websocket-Djo_ruX_.js} +1 -1
  262. package/dist/web/assets/{web-preview-tab-D4XayUEK.js → web-preview-tab-DXy0hRef.js} +1 -1
  263. package/dist/web/assets/web-preview-tab-DXy0hRef.js.br +0 -0
  264. package/dist/web/assets/web-preview-tab-DXy0hRef.js.gz +0 -0
  265. package/dist/web/index.html +2 -2
  266. package/dist/web/index.html.br +0 -0
  267. package/dist/web/index.html.gz +0 -0
  268. package/dist/web/sw.js +1 -1
  269. package/dist/web/sw.js.br +0 -0
  270. package/dist/web/sw.js.gz +0 -0
  271. package/docs/architecture/ai-chat-and-providers.md +29 -0
  272. package/docs/deployment-guide.md +39 -0
  273. package/docs/lessons-learned.md +14 -4
  274. package/package.json +1 -1
  275. package/scripts/mem-diag-report.ts +31 -0
  276. package/src/providers/claude-agent-sdk-query-options.ts +13 -2
  277. package/src/providers/claude-agent-sdk.ts +58 -18
  278. package/src/providers/claude-approval-queue.ts +75 -0
  279. package/src/providers/claude-effort-status.ts +211 -0
  280. package/src/providers/registry.ts +10 -1
  281. package/src/server/index.ts +7 -6
  282. package/src/server/middleware/access-log.ts +3 -0
  283. package/src/server/middleware/cors-headers.ts +44 -0
  284. package/src/server/routes/chat.ts +21 -1
  285. package/src/server/routes/database-impexp.ts +1 -1
  286. package/src/server/routes/design-preview.ts +2 -2
  287. package/src/server/routes/settings.ts +16 -0
  288. package/src/server/ws/chat-effort-status.ts +73 -0
  289. package/src/server/ws/chat.ts +33 -3
  290. package/src/services/compact-segment.ts +14 -19
  291. package/src/services/database/grid-export-tickets.ts +6 -2
  292. package/src/services/database/impexp/readers/file-text.ts +6 -3
  293. package/src/services/edge-forwarder.ts +12 -0
  294. package/src/services/file-chunks.ts +39 -0
  295. package/src/services/file-lines.ts +10 -17
  296. package/src/services/lsp/lsp-manager.ts +3 -0
  297. package/src/services/memory-diagnostics-native.ts +110 -0
  298. package/src/services/memory-diagnostics-report.ts +200 -0
  299. package/src/services/memory-diagnostics-requests.ts +95 -0
  300. package/src/services/memory-diagnostics-spawns.ts +242 -0
  301. package/src/services/memory-diagnostics.ts +121 -23
  302. package/src/services/memory-watchdog.ts +185 -0
  303. package/src/services/nested-subagent-spy.ts +3 -0
  304. package/src/services/process-thread-snapshot.ts +12 -2
  305. package/src/services/process-tree-memory.ts +161 -0
  306. package/src/services/read-lines.ts +16 -27
  307. package/src/services/supervisor.ts +1 -0
  308. package/src/services/terminal.service.ts +8 -0
  309. package/src/services/transcript-images-file.ts +7 -11
  310. package/src/shared/effort-status.ts +128 -0
  311. package/src/types/api.ts +3 -1
  312. package/src/types/chat.ts +8 -0
  313. package/src/web/components/chat/chat-scroll-nav.tsx +12 -8
  314. package/src/web/components/chat/chat-tab.tsx +15 -11
  315. package/src/web/components/chat/message-action-bar.tsx +19 -4
  316. package/src/web/components/chat/message-input.tsx +6 -0
  317. package/src/web/components/chat/message-list.tsx +55 -21
  318. package/src/web/components/chat/message-user-bubble.tsx +2 -2
  319. package/src/web/components/chat/model-thinking-selector-helpers.ts +79 -0
  320. package/src/web/components/chat/model-thinking-selector.tsx +36 -10
  321. package/src/web/components/chat/session-changes-context.tsx +29 -1
  322. package/src/web/components/layout/reparenting-tab.tsx +53 -14
  323. package/src/web/components/settings/ai-settings-section.tsx +32 -0
  324. package/src/web/components/settings/notifications-settings-section.tsx +14 -1
  325. package/src/web/hooks/use-chat.ts +16 -0
  326. package/src/web/hooks/use-effort-status.ts +46 -0
  327. package/src/web/hooks/use-relative-time-tick.ts +35 -0
  328. package/src/web/hooks/use-session-file-changes.ts +5 -1
  329. package/src/web/hooks/use-tab-overflow.ts +26 -13
  330. package/src/web/lib/format-date.ts +6 -1
  331. package/src/web/lib/web-push-client.ts +35 -5
  332. package/src/web/lib/web-push-support.ts +57 -4
  333. package/dist/web/assets/accounts-settings-section-DD88DRdx.js.br +0 -0
  334. package/dist/web/assets/accounts-settings-section-DD88DRdx.js.gz +0 -0
  335. package/dist/web/assets/agent-session-sheet-C0PhhLKR.js.br +0 -0
  336. package/dist/web/assets/agent-session-sheet-C0PhhLKR.js.gz +0 -0
  337. package/dist/web/assets/agent-session-window-content-D0XZgh4U.js.br +0 -0
  338. package/dist/web/assets/agent-session-window-content-D0XZgh4U.js.gz +0 -0
  339. package/dist/web/assets/ai-resource-editor-CpfQB6A7.js.br +0 -0
  340. package/dist/web/assets/ai-resource-editor-CpfQB6A7.js.gz +0 -0
  341. package/dist/web/assets/ai-settings-section-CQWBwWcM.js +0 -1
  342. package/dist/web/assets/ai-settings-section-CQWBwWcM.js.br +0 -0
  343. package/dist/web/assets/ai-settings-section-CQWBwWcM.js.gz +0 -0
  344. package/dist/web/assets/android-tab-4Z4AgHz1.js.br +0 -0
  345. package/dist/web/assets/android-tab-4Z4AgHz1.js.gz +0 -0
  346. package/dist/web/assets/appearance-settings-section-DjQjiJF7.js.br +0 -0
  347. package/dist/web/assets/appearance-settings-section-DjQjiJF7.js.gz +0 -0
  348. package/dist/web/assets/apps-panel-_lxY9xbc.js.br +0 -0
  349. package/dist/web/assets/apps-panel-_lxY9xbc.js.gz +0 -0
  350. package/dist/web/assets/branch-review-tab-BzrhnFSs.js.br +0 -0
  351. package/dist/web/assets/branch-review-tab-BzrhnFSs.js.gz +0 -0
  352. package/dist/web/assets/chat-tab-BKHepkYi.js.br +0 -0
  353. package/dist/web/assets/chat-tab-BKHepkYi.js.gz +0 -0
  354. package/dist/web/assets/chat-tab-Cv8gYzcA.js +0 -19
  355. package/dist/web/assets/chat-tab-Cv8gYzcA.js.br +0 -0
  356. package/dist/web/assets/chat-tab-Cv8gYzcA.js.gz +0 -0
  357. package/dist/web/assets/code-editor-COWmNXO4.js.br +0 -0
  358. package/dist/web/assets/code-editor-COWmNXO4.js.gz +0 -0
  359. package/dist/web/assets/codex-reset-credit-button-BHt4yJet.js.br +0 -0
  360. package/dist/web/assets/codex-reset-credit-button-BHt4yJet.js.gz +0 -0
  361. package/dist/web/assets/conflict-editor-tj-1gzKr.js.br +0 -0
  362. package/dist/web/assets/conflict-editor-tj-1gzKr.js.gz +0 -0
  363. package/dist/web/assets/connection-form-tab-BV2QX8Cf.js.br +0 -0
  364. package/dist/web/assets/connection-form-tab-BV2QX8Cf.js.gz +0 -0
  365. package/dist/web/assets/csv-preview-xhE2esgz.js.br +0 -0
  366. package/dist/web/assets/csv-preview-xhE2esgz.js.gz +0 -0
  367. package/dist/web/assets/db-tab-parts-C3MsjOID.js.br +0 -0
  368. package/dist/web/assets/db-tab-parts-C3MsjOID.js.gz +0 -0
  369. package/dist/web/assets/design-settings-section-C3LxYhIw.js.br +0 -0
  370. package/dist/web/assets/design-settings-section-C3LxYhIw.js.gz +0 -0
  371. package/dist/web/assets/design-tab-DYcvjdfb.js.br +0 -0
  372. package/dist/web/assets/design-tab-DYcvjdfb.js.gz +0 -0
  373. package/dist/web/assets/designs-sidebar-panel-Dpr4PPFT.js.br +0 -0
  374. package/dist/web/assets/designs-sidebar-panel-Dpr4PPFT.js.gz +0 -0
  375. package/dist/web/assets/diff-viewer-Fl_4jqr6.js.br +0 -0
  376. package/dist/web/assets/diff-viewer-Fl_4jqr6.js.gz +0 -0
  377. package/dist/web/assets/docx-preview-bVE9shPX.js.br +0 -0
  378. package/dist/web/assets/docx-preview-bVE9shPX.js.gz +0 -0
  379. package/dist/web/assets/editor-dialog-eEOYWdbr.js.br +0 -0
  380. package/dist/web/assets/editor-dialog-eEOYWdbr.js.gz +0 -0
  381. package/dist/web/assets/editor-language-service-D1_Ggq4s.js.br +0 -0
  382. package/dist/web/assets/editor-language-service-D1_Ggq4s.js.gz +0 -0
  383. package/dist/web/assets/explorer-body-C0UOcBPJ.js.br +0 -0
  384. package/dist/web/assets/explorer-body-C0UOcBPJ.js.gz +0 -0
  385. package/dist/web/assets/explorer-window-content-Dxee7SuJ.js.br +0 -0
  386. package/dist/web/assets/explorer-window-content-Dxee7SuJ.js.gz +0 -0
  387. package/dist/web/assets/extension-manager-section-D8krnoj8.js.br +0 -0
  388. package/dist/web/assets/extension-manager-section-D8krnoj8.js.gz +0 -0
  389. package/dist/web/assets/extension-webview-CtIdV69T.js.br +0 -0
  390. package/dist/web/assets/extension-webview-CtIdV69T.js.gz +0 -0
  391. package/dist/web/assets/files-settings-section-C8vtarR8.js.br +0 -0
  392. package/dist/web/assets/files-settings-section-C8vtarR8.js.gz +0 -0
  393. package/dist/web/assets/format-date-CSHaU2wN.js +0 -1
  394. package/dist/web/assets/general-settings-section-Chu8Ut8h.js.br +0 -0
  395. package/dist/web/assets/general-settings-section-Chu8Ut8h.js.gz +0 -0
  396. package/dist/web/assets/git-log-panel-DG09p6k5.js.br +0 -0
  397. package/dist/web/assets/git-log-panel-DG09p6k5.js.gz +0 -0
  398. package/dist/web/assets/git-review-tab-CSy8DjSC.js.br +0 -0
  399. package/dist/web/assets/git-review-tab-CSy8DjSC.js.gz +0 -0
  400. package/dist/web/assets/glide-data-grid-BN1y4BT0.js.br +0 -0
  401. package/dist/web/assets/glide-data-grid-BN1y4BT0.js.gz +0 -0
  402. package/dist/web/assets/grid-save-dialog-CfSuWw2t.js.br +0 -0
  403. package/dist/web/assets/grid-save-dialog-CfSuWw2t.js.gz +0 -0
  404. package/dist/web/assets/group-chat-tab-B04QMp7A.js.br +0 -0
  405. package/dist/web/assets/group-chat-tab-B04QMp7A.js.gz +0 -0
  406. package/dist/web/assets/impexp-tab-BRNouquR.js.br +0 -0
  407. package/dist/web/assets/impexp-tab-BRNouquR.js.gz +0 -0
  408. package/dist/web/assets/index-BpgSIM5B.js.br +0 -0
  409. package/dist/web/assets/index-BpgSIM5B.js.gz +0 -0
  410. package/dist/web/assets/keyboard-shortcuts-section-BKDyZHhK.js.br +0 -0
  411. package/dist/web/assets/keyboard-shortcuts-section-BKDyZHhK.js.gz +0 -0
  412. package/dist/web/assets/language-servers-section-CbbkezhA.js.br +0 -0
  413. package/dist/web/assets/language-servers-section-CbbkezhA.js.gz +0 -0
  414. package/dist/web/assets/logs-app-Blr1-v4q.js.br +0 -0
  415. package/dist/web/assets/logs-app-Blr1-v4q.js.gz +0 -0
  416. package/dist/web/assets/lsp-client-Ds3U2O52.js.br +0 -0
  417. package/dist/web/assets/lsp-client-Ds3U2O52.js.gz +0 -0
  418. package/dist/web/assets/markdown-renderer-DzCPadpB.js.br +0 -0
  419. package/dist/web/assets/markdown-renderer-DzCPadpB.js.gz +0 -0
  420. package/dist/web/assets/mobile-explorer-sheet-BClCMXb8.js.br +0 -0
  421. package/dist/web/assets/mobile-explorer-sheet-BClCMXb8.js.gz +0 -0
  422. package/dist/web/assets/new-design-dialog-HRP9H11v.js.br +0 -0
  423. package/dist/web/assets/new-design-dialog-HRP9H11v.js.gz +0 -0
  424. package/dist/web/assets/notifications-settings-section-CYTTQaNr.js +0 -1
  425. package/dist/web/assets/notifications-settings-section-CYTTQaNr.js.br +0 -0
  426. package/dist/web/assets/notifications-settings-section-CYTTQaNr.js.gz +0 -0
  427. package/dist/web/assets/ppmbot-settings-section-BRowIIqc.js.br +0 -0
  428. package/dist/web/assets/ppmbot-settings-section-BRowIIqc.js.gz +0 -0
  429. package/dist/web/assets/problems-panel-DdB10Ybd.js.br +0 -0
  430. package/dist/web/assets/problems-panel-DdB10Ybd.js.gz +0 -0
  431. package/dist/web/assets/process-table-CzLaWPe0.js.br +0 -0
  432. package/dist/web/assets/process-table-CzLaWPe0.js.gz +0 -0
  433. package/dist/web/assets/proxy-settings-section-BCq7R5DA.js.br +0 -0
  434. package/dist/web/assets/proxy-settings-section-BCq7R5DA.js.gz +0 -0
  435. package/dist/web/assets/query-tab-DGvj_h6l.js.br +0 -0
  436. package/dist/web/assets/remote-access-settings-section-BrqOoOyk.js.br +0 -0
  437. package/dist/web/assets/remote-access-settings-section-BrqOoOyk.js.gz +0 -0
  438. package/dist/web/assets/remote-desktop-clipboard-notice-5r6UbdlN.js.br +0 -0
  439. package/dist/web/assets/remote-desktop-clipboard-notice-5r6UbdlN.js.gz +0 -0
  440. package/dist/web/assets/remote-desktop-mobile-sheet-B9Q4auqe.js.br +0 -0
  441. package/dist/web/assets/remote-desktop-mobile-sheet-B9Q4auqe.js.gz +0 -0
  442. package/dist/web/assets/remote-desktop-mobile-view-CL-jrkA8.js.br +0 -0
  443. package/dist/web/assets/remote-desktop-mobile-view-CL-jrkA8.js.gz +0 -0
  444. package/dist/web/assets/remote-desktop-readiness-gate-DXEf2Jnz.js.br +0 -0
  445. package/dist/web/assets/remote-desktop-readiness-gate-DXEf2Jnz.js.gz +0 -0
  446. package/dist/web/assets/remote-desktop-window-content-20P6-gKB.js.br +0 -0
  447. package/dist/web/assets/remote-desktop-window-content-20P6-gKB.js.gz +0 -0
  448. package/dist/web/assets/rename-object-dialog-Bs7pUsUP.js.br +0 -0
  449. package/dist/web/assets/rename-object-dialog-Bs7pUsUP.js.gz +0 -0
  450. package/dist/web/assets/review-code-Bjo4sHN9.js.br +0 -0
  451. package/dist/web/assets/review-code-Bjo4sHN9.js.gz +0 -0
  452. package/dist/web/assets/save-as-dialog-BHx0Wp2B.js.br +0 -0
  453. package/dist/web/assets/save-as-dialog-BHx0Wp2B.js.gz +0 -0
  454. package/dist/web/assets/schedules-settings-section-C6ATYDPK.js.br +0 -0
  455. package/dist/web/assets/schedules-settings-section-C6ATYDPK.js.gz +0 -0
  456. package/dist/web/assets/session-review-tab-BPMUyNTx.js.br +0 -0
  457. package/dist/web/assets/session-review-tab-BPMUyNTx.js.gz +0 -0
  458. package/dist/web/assets/settings-body-Dy5D6Vu7.js.br +0 -0
  459. package/dist/web/assets/settings-body-Dy5D6Vu7.js.gz +0 -0
  460. package/dist/web/assets/settings-rows-BRoYE2To.js.br +0 -0
  461. package/dist/web/assets/settings-rows-BRoYE2To.js.gz +0 -0
  462. package/dist/web/assets/settings-section-content-C4DX7TYv.js +0 -1
  463. package/dist/web/assets/settings-section-content-essUViKu.js.br +0 -0
  464. package/dist/web/assets/settings-section-content-essUViKu.js.gz +0 -0
  465. package/dist/web/assets/slash-command-picker-8da30mYP.js.br +0 -0
  466. package/dist/web/assets/slash-command-picker-8da30mYP.js.gz +0 -0
  467. package/dist/web/assets/sql-object-tab-DRIHVXuR.js.br +0 -0
  468. package/dist/web/assets/sql-object-tab-DRIHVXuR.js.gz +0 -0
  469. package/dist/web/assets/sqlite-viewer-G-Jl2yEr.js.br +0 -0
  470. package/dist/web/assets/sqlite-viewer-G-Jl2yEr.js.gz +0 -0
  471. package/dist/web/assets/structure-save-dialog-Cwikz0we.js.br +0 -0
  472. package/dist/web/assets/structure-save-dialog-Cwikz0we.js.gz +0 -0
  473. package/dist/web/assets/structure-tab-CWEgdZPm.js.br +0 -0
  474. package/dist/web/assets/structure-tab-CWEgdZPm.js.gz +0 -0
  475. package/dist/web/assets/system-monitor-body-BDQm9-wb.js.br +0 -0
  476. package/dist/web/assets/system-monitor-body-BDQm9-wb.js.gz +0 -0
  477. package/dist/web/assets/tab-host-window-content-CZ55-bTz.js.br +0 -0
  478. package/dist/web/assets/tab-host-window-content-CZ55-bTz.js.gz +0 -0
  479. package/dist/web/assets/table-tab-Bmn4UdBV.js.br +0 -0
  480. package/dist/web/assets/table-tab-Bmn4UdBV.js.gz +0 -0
  481. package/dist/web/assets/telegram-bot-token-field-fdHwRf39.js.br +0 -0
  482. package/dist/web/assets/telegram-bot-token-field-fdHwRf39.js.gz +0 -0
  483. package/dist/web/assets/terminal-tab-D-qlZXLK.js.br +0 -0
  484. package/dist/web/assets/terminal-tab-D-qlZXLK.js.gz +0 -0
  485. package/dist/web/assets/tool-cards-BdXbQ9Fv.js.br +0 -0
  486. package/dist/web/assets/tool-cards-BdXbQ9Fv.js.gz +0 -0
  487. package/dist/web/assets/tools-settings-section-CcPaspbR.js.br +0 -0
  488. package/dist/web/assets/tools-settings-section-CcPaspbR.js.gz +0 -0
  489. package/dist/web/assets/use-inline-blame-Ck5XoSS3.js.br +0 -0
  490. package/dist/web/assets/use-lsp-CGSqfFBk.js.br +0 -0
  491. package/dist/web/assets/use-lsp-CGSqfFBk.js.gz +0 -0
  492. package/dist/web/assets/use-process-kill-DJ_f7rCO.js.br +0 -0
  493. package/dist/web/assets/use-process-kill-DJ_f7rCO.js.gz +0 -0
  494. package/dist/web/assets/use-sql-schema-info-wt320fVR.js.br +0 -0
  495. package/dist/web/assets/use-sql-schema-info-wt320fVR.js.gz +0 -0
  496. package/dist/web/assets/web-preview-tab-D4XayUEK.js.br +0 -0
  497. package/dist/web/assets/web-preview-tab-D4XayUEK.js.gz +0 -0
@@ -51,8 +51,8 @@ export function createDesignPreviewRoutes(now: () => number = Date.now) {
51
51
  const api = new Hono();
52
52
  const content = new Hono();
53
53
 
54
- // The app's global `cors()` answers `*`, and Hono copies headers set before a handler over
55
- // the handler's own response, so the value set below would be lost. Re-assert it after.
54
+ // Every response here must say `null`, error pages included; asserted after the handler so
55
+ // it holds whatever an inner layer set. The app's CORS layer leaves an existing value alone.
56
56
  content.use("*", async (c, next) => {
57
57
  await next();
58
58
  c.res.headers.set("Access-Control-Allow-Origin", "null");
@@ -314,6 +314,22 @@ settingsRoutes.get("/ai/providers/:id/models", async (c) => {
314
314
  }
315
315
  });
316
316
 
317
+ /**
318
+ * GET /settings/ai/providers/:id/effort-status — what overrides the default effort outside PPM for
319
+ * every project: the user settings and the environment PPM runs in. A project's own settings can
320
+ * add more, which the chat's picker reports. `status` is null for a provider nothing overrides.
321
+ */
322
+ settingsRoutes.get("/ai/providers/:id/effort-status", (c) => {
323
+ const id = c.req.param("id");
324
+ const provider = providerRegistry.get(id);
325
+ if (!provider) return c.json(err(`Provider "${id}" not found`), 404);
326
+ try {
327
+ return c.json(ok({ status: provider.estimateEffortStatus?.(undefined, c.req.query("model") || undefined) ?? null }));
328
+ } catch (e) {
329
+ return c.json(err((e as Error).message), 500);
330
+ }
331
+ });
332
+
317
333
  // ── Keybindings ──────────────────────────────────────────────────────
318
334
 
319
335
  const KEYBINDINGS_KEY = "keybindings";
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The effort a chat's turns really run at, kept beside the pick.
3
+ *
4
+ * PPM starts the CLI with the picked `--effort`, but `CLAUDE_CODE_EFFORT_LEVEL` (in the
5
+ * environment or a settings file's `env`), a `maxEffortLevel` cap, an organization limit and the
6
+ * model itself all outrank it there. So the session's CLI is asked at the start of every turn —
7
+ * a settings file edited since the last one shows up there too — and its answer goes to the
8
+ * clients. Before any turn has asked, what the settings files say stands in.
9
+ */
10
+ import { effortInEffect, effortSettingPlace, type EffortStatus } from "../../shared/effort-status.ts";
11
+
12
+ export interface EffortStatusSession {
13
+ providerId: string;
14
+ projectPath?: string;
15
+ /** The CLI's last answer for this session. */
16
+ effortStatus?: EffortStatus;
17
+ }
18
+
19
+ export interface EffortStatusProvider {
20
+ readEffortStatus?(sessionId: string): Promise<EffortStatus | null>;
21
+ estimateEffortStatus?(projectPath: string | undefined, model?: string): EffortStatus;
22
+ }
23
+
24
+ /** What a greeting carries: the CLI's answer, else the settings files; null for a provider nothing overrides. */
25
+ export function sessionEffortStatus(
26
+ provider: EffortStatusProvider | undefined,
27
+ session: EffortStatusSession,
28
+ model: string | undefined,
29
+ ): EffortStatus | null {
30
+ return session.effortStatus ?? provider?.estimateEffortStatus?.(session.projectPath, model) ?? null;
31
+ }
32
+
33
+ /** The session log's line for a turn whose effort is not the one it was started with. */
34
+ export function effortOverrideLogLine(status: EffortStatus): string | null {
35
+ const { observed } = status;
36
+ if (!observed?.requested) return null;
37
+ const { level, reason } = effortInEffect(observed.requested, status);
38
+ if (!reason) return null;
39
+ const by = reason === "env" && status.env
40
+ ? `CLAUDE_CODE_EFFORT_LEVEL=${status.env.value} in ${effortSettingPlace(status.env)}`
41
+ : reason === "cap" && status.cap
42
+ ? `maxEffortLevel ${status.cap.value} in ${effortSettingPlace(status.cap)}`
43
+ : `what ${observed.model ?? "the model"} allows`;
44
+ return `Effort ${observed.requested} was picked; Claude Code sends ${level ?? "no effort"} (${by})`;
45
+ }
46
+
47
+ /**
48
+ * Ask the session's CLI at a turn's start, and tell the clients when its answer changed. Never
49
+ * rejects: it runs unawaited beside the turn, where a rejection would reach the server's
50
+ * fatal-error handler for what is only a label.
51
+ */
52
+ export async function refreshEffortStatus(p: {
53
+ sessionId: string;
54
+ session: EffortStatusSession;
55
+ provider: EffortStatusProvider | undefined;
56
+ /** False once the session's entry was replaced while the CLI was answering. */
57
+ isCurrent: () => boolean;
58
+ broadcast: (event: unknown) => void;
59
+ log: (line: string) => void;
60
+ }): Promise<void> {
61
+ try {
62
+ const status = await p.provider?.readEffortStatus?.(p.sessionId);
63
+ if (!status || !p.isCurrent()) return;
64
+ const before = p.session.effortStatus;
65
+ p.session.effortStatus = status;
66
+ if (before && JSON.stringify(before) === JSON.stringify(status)) return;
67
+ p.broadcast({ type: "effort_status", status });
68
+ const line = effortOverrideLogLine(status);
69
+ if (line) p.log(line);
70
+ } catch {
71
+ // The status stays as it was; the next turn asks again.
72
+ }
73
+ }
@@ -24,6 +24,8 @@ import { isAsyncAgentLaunchAck, isTerminalAgentStatus } from "../../shared/backg
24
24
  import { cacheReleaseDelayMs, selectWarmIdleEvictions } from "../../services/subprocess-retention.ts";
25
25
  import { needsAuthServerNames } from "../../services/mcp-oauth/mcp-oauth-redirect.ts";
26
26
  import { mcpStatusEvent, registerMcpSignInSync } from "./chat-mcp-sign-in-sync.ts";
27
+ import { refreshEffortStatus, sessionEffortStatus } from "./chat-effort-status.ts";
28
+ import type { EffortStatus } from "../../shared/effort-status.ts";
27
29
  import { claudeTranscriptExists } from "../../services/claude-transcript-exists.ts";
28
30
  import { registerMemoryGauge } from "../../services/memory-diagnostics.ts";
29
31
  import { setTabOpenDelivery, tabOpenBroker } from "../../services/tab-tools-mcp/tab-open-broker.ts";
@@ -114,6 +116,11 @@ function resolveSessionEffort(sessionId: string): string | undefined {
114
116
  return getSessionEffort(sessionId) ?? sessionProviderConfig(sessionId)?.effort;
115
117
  }
116
118
 
119
+ /** What overrides the effort shown in session_state, if anything does. */
120
+ function resolveEffortStatus(sessionId: string, entry: SessionEntry): EffortStatus | null {
121
+ return sessionEffortStatus(providerRegistry.get(entry.providerId), entry, resolveSessionModel(sessionId));
122
+ }
123
+
117
124
  /** Whether thinking is effectively ON: per-session override wins, else provider config, else SDK default. */
118
125
  function resolveSessionThinkingEnabled(sessionId: string): boolean {
119
126
  return isThinkingEnabled(
@@ -196,6 +203,8 @@ interface SessionEntry {
196
203
  compactStatus?: "compacting" | null;
197
204
  /** MCP servers the subprocess reported as `needs-auth` at init — drives the chat's sign-in bar */
198
205
  mcpNeedsAuth?: string[];
206
+ /** What the subprocess said at the last turn's start about the effort it sends — see chat-effort-status.ts */
207
+ effortStatus?: EffortStatus;
199
208
  /** toolUseIds of Bash/Agent calls launched with run_in_background — their spy outlives the tool_result */
200
209
  backgroundToolUseIds?: Set<string>;
201
210
  /** Nested-agent children buffered into turnEvents this turn (see MAX_NESTED_TURN_EVENTS) */
@@ -957,9 +966,20 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
957
966
  // System events → transition connecting → thinking, forward compact events
958
967
  if (evType === "system") {
959
968
  const sub = (ev as any).subtype;
960
- if (sub === "init" && Array.isArray(ev.mcpServers)) {
961
- entry.mcpNeedsAuth = needsAuthServerNames(ev.mcpServers);
962
- broadcast(sessionId, mcpStatusEvent(entry.mcpNeedsAuth));
969
+ if (sub === "init") {
970
+ if (Array.isArray(ev.mcpServers)) {
971
+ entry.mcpNeedsAuth = needsAuthServerNames(ev.mcpServers);
972
+ broadcast(sessionId, mcpStatusEvent(entry.mcpNeedsAuth));
973
+ }
974
+ // Not awaited: the turn goes on while the CLI answers.
975
+ void refreshEffortStatus({
976
+ sessionId,
977
+ session: entry,
978
+ provider: providerRegistry.get(entry.providerId),
979
+ isCurrent: () => activeSessions.get(sessionId) === entry,
980
+ broadcast: (event) => broadcast(sessionId, event),
981
+ log: (line) => logSessionEvent(sessionId, "INFO", line),
982
+ });
963
983
  } else if (sub === "compacting") {
964
984
  entry.compactStatus = "compacting";
965
985
  log.debug(`session=${sessionId} compact_status=compacting (persisted on entry)`);
@@ -1196,6 +1216,9 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
1196
1216
  const stopped = turnStop ? describeTurnStop(turnStop) : null;
1197
1217
 
1198
1218
  const finalText = entry.finalText;
1219
+ // The consumer outlives the turn (Claude pushes a follow-up into the same stream), so
1220
+ // the next turn's answer must not start with this one's.
1221
+ entry.finalText = undefined;
1199
1222
  import("../../services/notification.service.ts").then(({ notificationService }) => {
1200
1223
  const project = entry.projectName || "Project";
1201
1224
  const session = chatService.getSession(sessionId);
@@ -1385,6 +1408,7 @@ export const chatWebSocket = {
1385
1408
  mcpNeedsAuth: existing.mcpNeedsAuth ?? [],
1386
1409
  model: resolveSessionModel(sessionId),
1387
1410
  effort: resolveSessionEffort(sessionId),
1411
+ effortStatus: resolveEffortStatus(sessionId, existing),
1388
1412
  thinking: resolveSessionThinkingEnabled(sessionId),
1389
1413
  promptCache: promptCacheSnapshot(sessionId, existing),
1390
1414
  turnStop: existing.phase === "idle" ? lastTurnStop(sessionId) : null,
@@ -1554,6 +1578,7 @@ export const chatWebSocket = {
1554
1578
  mcpNeedsAuth: entry.mcpNeedsAuth ?? [],
1555
1579
  model: resolveSessionModel(sessionId),
1556
1580
  effort: resolveSessionEffort(sessionId),
1581
+ effortStatus: resolveEffortStatus(sessionId, entry),
1557
1582
  thinking: resolveSessionThinkingEnabled(sessionId),
1558
1583
  promptCache: promptCacheSnapshot(sessionId, entry),
1559
1584
  turnStop: entry.phase === "idle" ? lastTurnStop(sessionId) : null,
@@ -1776,6 +1801,8 @@ export const chatWebSocket = {
1776
1801
  }
1777
1802
  entry.model = parsed.model;
1778
1803
  setSessionModel(sessionId, parsed.model);
1804
+ // Its levels and limits were the old model's; the settings files stand in until the next turn asks.
1805
+ entry.effortStatus = undefined;
1779
1806
  const provider = providerRegistry.get(providerId);
1780
1807
  const hasLiveStream = provider?.hasStreamingSession?.(sessionId) ?? false;
1781
1808
  // Only abort when idle between turns — never interrupt an active turn.
@@ -1795,6 +1822,7 @@ export const chatWebSocket = {
1795
1822
  mcpNeedsAuth: entry.mcpNeedsAuth ?? [],
1796
1823
  model: resolveSessionModel(sessionId),
1797
1824
  effort: resolveSessionEffort(sessionId),
1825
+ effortStatus: resolveEffortStatus(sessionId, entry),
1798
1826
  thinking: resolveSessionThinkingEnabled(sessionId),
1799
1827
  }));
1800
1828
  } else if (parsed.type === "set_effort") {
@@ -1822,6 +1850,7 @@ export const chatWebSocket = {
1822
1850
  mcpNeedsAuth: entry.mcpNeedsAuth ?? [],
1823
1851
  model: resolveSessionModel(sessionId),
1824
1852
  effort: resolveSessionEffort(sessionId),
1853
+ effortStatus: resolveEffortStatus(sessionId, entry),
1825
1854
  thinking: resolveSessionThinkingEnabled(sessionId),
1826
1855
  }));
1827
1856
  } else if (parsed.type === "set_thinking") {
@@ -1843,6 +1872,7 @@ export const chatWebSocket = {
1843
1872
  mcpNeedsAuth: entry.mcpNeedsAuth ?? [],
1844
1873
  model: resolveSessionModel(sessionId),
1845
1874
  effort: resolveSessionEffort(sessionId),
1875
+ effortStatus: resolveEffortStatus(sessionId, entry),
1846
1876
  thinking: resolveSessionThinkingEnabled(sessionId),
1847
1877
  }));
1848
1878
  } else if (parsed.type === "cancel") {
@@ -19,6 +19,7 @@
19
19
  import { statSync } from "node:fs";
20
20
  import type { ChatMessage } from "../types/chat.ts";
21
21
  import { parseJsonlTranscript } from "./jsonl-transcript-parser.ts";
22
+ import { readFileChunks } from "./file-chunks.ts";
22
23
 
23
24
  /**
24
25
  * How many bytes one expand may parse.
@@ -90,26 +91,20 @@ export async function compactSegmentWindow(filePath: string, beforeUuid?: string
90
91
  // instead would copy a long record once per chunk it spans.
91
92
  const pending: Uint8Array[] = [];
92
93
  let at = 0; // file offset of the first pending byte
93
- const reader = Bun.file(filePath).stream().getReader();
94
- try {
95
- for (;;) {
96
- const { done, value } = await reader.read();
97
- if (done) break;
98
- const lastNewline = value.lastIndexOf(NEWLINE);
99
- if (lastNewline === -1) {
100
- pending.push(value);
101
- continue;
102
- }
103
- const lines = Buffer.concat([...pending, value.subarray(0, lastNewline + 1)]);
104
- const stop = scan(lines, at);
105
- if (stop !== undefined) return window(stop);
106
- at += lines.length;
107
- pending.length = 0;
108
- pending.push(value.subarray(lastNewline + 1));
94
+ // Not `Bun.file().stream()`, which keeps about the file's size in native memory per read
95
+ // (see `file-chunks.ts`). Returning mid-loop closes the file.
96
+ for await (const value of readFileChunks(filePath)) {
97
+ const lastNewline = value.lastIndexOf(NEWLINE);
98
+ if (lastNewline === -1) {
99
+ pending.push(value);
100
+ continue;
109
101
  }
110
- } finally {
111
- // Not only `releaseLock`: a stream let go of before its end keeps the file open.
112
- await reader.cancel().catch(() => {});
102
+ const lines = Buffer.concat([...pending, value.subarray(0, lastNewline + 1)]);
103
+ const stop = scan(lines, at);
104
+ if (stop !== undefined) return window(stop);
105
+ at += lines.length;
106
+ pending.length = 0;
107
+ pending.push(value.subarray(lastNewline + 1));
113
108
  }
114
109
  // A last line with no newline yet is still a record the full parse would read.
115
110
  const stop = scan(Buffer.concat(pending), at);
@@ -15,8 +15,12 @@ import { GRID_EXPORT_TICKET_TTL_MS } from "../../shared/db-grid-export.ts";
15
15
  export interface ExportDownload {
16
16
  fileName: string;
17
17
  contentType: string;
18
- /** The file, read from the database as it is sent. */
19
- open(): ReadableStream<Uint8Array>;
18
+ /**
19
+ * The file, read from the database as it is sent — or, for a file already on disk, the file
20
+ * itself: a `Bun.file` body is sent by Bun directly, where `Bun.file().stream()` keeps about the
21
+ * file's size in native memory per download (see `file-chunks.ts`).
22
+ */
23
+ open(): ReadableStream<Uint8Array> | Blob;
20
24
  /** Lets go of the rows and the connection, when the download never starts. */
21
25
  abandon(): void;
22
26
  }
@@ -3,6 +3,7 @@
3
3
  * order mark, or UTF-16 with one — what PPM's "CSV for Excel" writes. Bytes that are not text in
4
4
  * that encoding fail the read: turned into "�" they would be imported as a different value.
5
5
  */
6
+ import { readFileChunks } from "../../../file-chunks.ts";
6
7
 
7
8
  /** The encoding the first bytes name, and how many of them the mark takes. */
8
9
  function encodingOf(head: Uint8Array): { encoding: "utf-8" | "utf-16le" | "utf-16be"; skip: number } {
@@ -30,7 +31,9 @@ function notText(encoding: string): FileTextError {
30
31
 
31
32
  /** The text of the file at `path`, in pieces as it is read. Stop ends it between two pieces. */
32
33
  export async function* fileText(path: string, signal?: AbortSignal): AsyncGenerator<string> {
33
- const reader = Bun.file(path).stream().getReader();
34
+ // Not `Bun.file().stream()`, which keeps about the file's size in native memory per read
35
+ // (see `file-chunks.ts`) — an import file can be hundreds of MB.
36
+ const chunks = readFileChunks(path);
34
37
  let decoder: TextDecoder | null = null;
35
38
  let encoding = "utf-8";
36
39
  let head: Uint8Array = new Uint8Array(0);
@@ -55,7 +58,7 @@ export async function* fileText(path: string, signal?: AbortSignal): AsyncGenera
55
58
  sliceStart = performance.now();
56
59
  }
57
60
  signal?.throwIfAborted();
58
- const { done, value } = await reader.read();
61
+ const { done, value } = await chunks.next();
59
62
  if (done) break;
60
63
  let bytes: Uint8Array = value;
61
64
  if (!decoder) {
@@ -75,6 +78,6 @@ export async function* fileText(path: string, signal?: AbortSignal): AsyncGenera
75
78
  if (rest) yield rest;
76
79
  } finally {
77
80
  // Read to its end or not, the file is closed.
78
- await reader.cancel().catch(() => {});
81
+ await chunks.return(undefined).catch(() => {});
79
82
  }
80
83
  }
@@ -21,6 +21,7 @@
21
21
  import net from "node:net";
22
22
  import { resolveTargetPort, _resetTargetCache } from "./edge-target-resolver.ts";
23
23
  import { createLogger, installFileLogSink } from "./logger.ts";
24
+ import { registerMemoryGauge, startMemoryDiagnostics } from "./memory-diagnostics.ts";
24
25
 
25
26
  const log = createLogger("edge");
26
27
 
@@ -42,6 +43,12 @@ const CONNECT_WINDOW_MS = 5000;
42
43
 
43
44
  const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
44
45
 
46
+ /** Client connections open right now, and ever: the memory watchdog's view of the edge's load. */
47
+ let openConnections = 0;
48
+ let totalConnections = 0;
49
+ registerMemoryGauge("edge.openConnections", () => openConnections);
50
+ registerMemoryGauge("edge.totalConnections", () => totalConnections);
51
+
45
52
  /** At most one warning a minute about connections dropped for want of a server. */
46
53
  const DROP_WARN_INTERVAL_MS = 60_000;
47
54
  let droppedSinceWarning = 0;
@@ -201,6 +208,9 @@ export function startEdgeForwarder(
201
208
  // allowHalfOpen keeps the other direction alive when one side sends FIN —
202
209
  // long-lived chat WS and SSE streams depend on it.
203
210
  const server = net.createServer({ allowHalfOpen: true }, (client) => {
211
+ openConnections++;
212
+ totalConnections++;
213
+ client.once("close", () => { openConnections--; });
204
214
  void handleConnection(client, connectWindowMs);
205
215
  });
206
216
  server.once("error", reject);
@@ -218,6 +228,8 @@ if (process.argv.includes("__edge__")) {
218
228
  // Lines go to ppm.log with a timestamp and level like every other process's. The
219
229
  // supervisor hands the log over as stderr, so the stderr echo is skipped (see logger.ts).
220
230
  installFileLogSink({ echo: "stderr" });
231
+ // Reads memory through the OS only: it spawns nothing, so the invariant above holds.
232
+ startMemoryDiagnostics({ role: "edge" });
221
233
  const idx = process.argv.indexOf("__edge__");
222
234
  const publicPort = parseInt(process.argv[idx + 1] ?? "", 10);
223
235
  const host = process.argv[idx + 2] ?? "0.0.0.0";
@@ -0,0 +1,39 @@
1
+ import { open } from "node:fs/promises";
2
+
3
+ /**
4
+ * A file's bytes, a piece at a time, through `node:fs/promises` — never `Bun.file().stream()`.
5
+ *
6
+ * On Bun 1.3.10 (Windows), every full read through `Bun.file(path).stream()` keeps roughly the
7
+ * file's size in native memory that is never handed back: a 30 MB transcript read 40 times took
8
+ * a process from 377 MB to 1,949 MB private, 38 MB a read, with the JS heap back at 18 MB. A
9
+ * chat tab opening reads the whole transcript this way (`readCompactions`), so a server whose
10
+ * tabs reconnected all day reached 25 GB committed and the machine ran out of memory. The same
11
+ * 40 reads through a `FileHandle` stayed flat (0.1 MB a read); so did `createReadStream` and
12
+ * `readSync`. Measured as private bytes (commit charge), which RSS and the heap both hide.
13
+ *
14
+ * Each chunk is a fresh buffer, so a caller may keep it after asking for the next one. Breaking
15
+ * out of the loop closes the file (the generator's `finally`), so an early stop leaks no handle.
16
+ *
17
+ * `fromByte` is inclusive, `toByte` exclusive; both are absolute offsets.
18
+ */
19
+ export async function* readFileChunks(
20
+ filePath: string,
21
+ opts: { fromByte?: number; toByte?: number; chunkBytes?: number } = {},
22
+ ): AsyncGenerator<Uint8Array> {
23
+ const chunkBytes = opts.chunkBytes ?? 512 * 1024;
24
+ let position = Math.max(0, opts.fromByte ?? 0);
25
+ const end = opts.toByte ?? Infinity;
26
+ const handle = await open(filePath, "r");
27
+ try {
28
+ while (position < end) {
29
+ const want = Math.min(chunkBytes, end - position);
30
+ const buf = new Uint8Array(want);
31
+ const { bytesRead } = await handle.read(buf, 0, want, position);
32
+ if (bytesRead === 0) break;
33
+ position += bytesRead;
34
+ yield bytesRead === want ? buf : buf.subarray(0, bytesRead);
35
+ }
36
+ } finally {
37
+ await handle.close().catch(() => {});
38
+ }
39
+ }
@@ -15,6 +15,7 @@
15
15
  * Neither cost was bounded: both grow with the file, and both were paid on the
16
16
  * one thread that also serves every other request.
17
17
  */
18
+ import { readFileChunks } from "./file-chunks.ts";
18
19
 
19
20
  /** How much of the end of a file is read to find the last few lines. */
20
21
  export const DEFAULT_TAIL_BYTES = 64 * 1024;
@@ -89,24 +90,16 @@ export async function countLines(filePath: string): Promise<number> {
89
90
  let count = 0;
90
91
  let lastByte = -1;
91
92
  let sinceYield = 0;
92
- // `getReader()` rather than `for await`: Bun's ReadableStream is async
93
- // iterable at runtime, but the DOM typings it is declared against do not say
94
- // so, and a cast to paper over that would outlive the reason for it.
95
- const reader = Bun.file(filePath).stream().getReader();
96
- try {
97
- for (;;) {
98
- const { done, value } = await reader.read();
99
- if (done || !value) break;
100
- for (let i = 0; i < value.length; i++) if (value[i] === 0x0a) count++;
101
- if (value.length > 0) lastByte = value[value.length - 1]!;
102
- sinceYield += value.length;
103
- if (sinceYield >= YIELD_EVERY_BYTES) {
104
- sinceYield = 0;
105
- await new Promise<void>((r) => setTimeout(r, 0));
106
- }
93
+ // Not `Bun.file().stream()`, which keeps about the file's size in native
94
+ // memory per read and never returns it (see `file-chunks.ts`).
95
+ for await (const value of readFileChunks(filePath)) {
96
+ for (let i = 0; i < value.length; i++) if (value[i] === 0x0a) count++;
97
+ if (value.length > 0) lastByte = value[value.length - 1]!;
98
+ sinceYield += value.length;
99
+ if (sinceYield >= YIELD_EVERY_BYTES) {
100
+ sinceYield = 0;
101
+ await new Promise<void>((r) => setTimeout(r, 0));
107
102
  }
108
- } finally {
109
- reader.releaseLock();
110
103
  }
111
104
  if (lastByte === -1) return 0; // empty file
112
105
  if (lastByte !== 0x0a) count++; // last line carries no newline
@@ -20,6 +20,7 @@ import { resolveBunPath } from "../autostart-generator.ts";
20
20
  import { createLogger } from "../logger.ts";
21
21
  import { canInstall, lspInstallDir, rustupServerPath } from "./lsp-install.ts";
22
22
  import { LspSession, type LspSessionState } from "./lsp-session.ts";
23
+ import { registerMemoryGauge } from "../memory-diagnostics.ts";
23
24
  import {
24
25
  LANGUAGE_SERVERS,
25
26
  ancestorDirs,
@@ -595,3 +596,5 @@ export class LspManager {
595
596
  }
596
597
 
597
598
  export const lspManager = new LspManager();
599
+
600
+ registerMemoryGauge("lsp.servers", () => lspManager["entries"].size);
@@ -0,0 +1,110 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+
3
+ /**
4
+ * What the OS says this process holds, beyond what `process.memoryUsage()` reports.
5
+ *
6
+ * `rss` is only what is resident right now. A leak of *committed* memory — an allocator
7
+ * segment reserved and never handed back, a pipe buffer per child — can sit at 30 GB of
8
+ * private bytes while RSS reads 1–2 GB, because Windows trims the working set and pages
9
+ * the rest out. That is exactly how the server's leak looked on 20/10/2026: mem-diag said
10
+ * 1 GB, Windows' low-memory detector named the same pid at 32 GB, and every new child
11
+ * process died of memory exhaustion. Private bytes are the number that runs the machine
12
+ * out of commit, so that is the one recorded here.
13
+ *
14
+ * Every field is `undefined` when this platform cannot measure it, never `0`.
15
+ */
16
+ export interface NativeMemory {
17
+ /** Windows `PrivateUsage` (commit charge); Linux `RssAnon + VmSwap`. */
18
+ privateMB?: number;
19
+ /** Windows `PeakWorkingSetSize`; Linux `VmHWM`. */
20
+ peakRssMB?: number;
21
+ /** Linux `VmSize` — address space, mostly reservations. */
22
+ virtualMB?: number;
23
+ /** Windows kernel handles; Linux open file descriptors. Climbs with leaked pipes and children. */
24
+ handles?: number;
25
+ /** Linux only: `Threads` in `/proc/self/status`. */
26
+ threads?: number;
27
+ }
28
+
29
+ const MB = 1024 * 1024;
30
+
31
+ type Win32 = {
32
+ K32GetProcessMemoryInfo: (process: bigint, counters: number, size: number) => number;
33
+ GetProcessHandleCount: (process: bigint, count: number) => number;
34
+ };
35
+
36
+ /** `(HANDLE)-1`, the pseudo handle for the calling process. A bigint, so it crosses FFI unrounded. */
37
+ const CURRENT_PROCESS = -1n;
38
+ /** `sizeof(PROCESS_MEMORY_COUNTERS_EX)` on 64-bit Windows: two DWORDs then nine SIZE_Ts. */
39
+ const COUNTERS_SIZE = 80;
40
+ const OFFSET_PEAK_WORKING_SET = 8;
41
+ const OFFSET_PRIVATE_USAGE = 72;
42
+
43
+ let win32: Win32 | null | undefined;
44
+
45
+ async function loadWin32(): Promise<Win32 | null> {
46
+ if (win32 !== undefined) return win32;
47
+ try {
48
+ const { dlopen, FFIType } = await import("bun:ffi");
49
+ win32 = dlopen("kernel32.dll", {
50
+ K32GetProcessMemoryInfo: { args: [FFIType.i64, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
51
+ GetProcessHandleCount: { args: [FFIType.i64, FFIType.ptr], returns: FFIType.i32 },
52
+ }).symbols as unknown as Win32;
53
+ } catch {
54
+ win32 = null;
55
+ }
56
+ return win32;
57
+ }
58
+
59
+ async function readWindows(): Promise<NativeMemory> {
60
+ const lib = await loadWin32();
61
+ if (!lib) return {};
62
+ const { ptr } = await import("bun:ffi");
63
+ const out: NativeMemory = {};
64
+
65
+ const counters = new Uint8Array(COUNTERS_SIZE);
66
+ const view = new DataView(counters.buffer);
67
+ view.setUint32(0, COUNTERS_SIZE, true);
68
+ if (lib.K32GetProcessMemoryInfo(CURRENT_PROCESS, ptr(counters), COUNTERS_SIZE) !== 0) {
69
+ out.privateMB = Math.round(Number(view.getBigUint64(OFFSET_PRIVATE_USAGE, true)) / MB);
70
+ out.peakRssMB = Math.round(Number(view.getBigUint64(OFFSET_PEAK_WORKING_SET, true)) / MB);
71
+ }
72
+
73
+ const handleCount = new Uint32Array(1);
74
+ if (lib.GetProcessHandleCount(CURRENT_PROCESS, ptr(handleCount)) !== 0) out.handles = handleCount[0];
75
+ return out;
76
+ }
77
+
78
+ /** Parse the `kB` lines of `/proc/self/status` that matter here. Exported for tests. */
79
+ export function parseProcStatus(text: string): NativeMemory {
80
+ const kb = (key: string): number | undefined => {
81
+ const m = text.match(new RegExp(`^${key}:\\s+(\\d+)\\s+kB`, "m"));
82
+ return m ? Number(m[1]) : undefined;
83
+ };
84
+ const anon = kb("RssAnon");
85
+ const swap = kb("VmSwap");
86
+ const hwm = kb("VmHWM");
87
+ const size = kb("VmSize");
88
+ const threads = text.match(/^Threads:\s+(\d+)/m);
89
+ return {
90
+ privateMB: anon === undefined ? undefined : Math.round((anon + (swap ?? 0)) / 1024),
91
+ peakRssMB: hwm === undefined ? undefined : Math.round(hwm / 1024),
92
+ virtualMB: size === undefined ? undefined : Math.round(size / 1024),
93
+ threads: threads ? Number(threads[1]) : undefined,
94
+ };
95
+ }
96
+
97
+ function readLinux(): NativeMemory {
98
+ const out = parseProcStatus(readFileSync("/proc/self/status", "utf8"));
99
+ try { out.handles = readdirSync("/proc/self/fd").length; } catch { /* fd count is optional */ }
100
+ return out;
101
+ }
102
+
103
+ /** One reading of this process's native memory. Never throws; an unreadable field is left out. */
104
+ export async function readNativeMemory(platform: NodeJS.Platform = process.platform): Promise<NativeMemory> {
105
+ try {
106
+ if (platform === "win32") return await readWindows();
107
+ if (platform === "linux") return readLinux();
108
+ } catch { /* fall through: no native reading this tick */ }
109
+ return {};
110
+ }