@hienlh/ppm 0.23.0 → 0.23.2

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 (382) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/CLAUDE.md +1 -0
  3. package/assets/skills/ppm/SKILL.md +1 -1
  4. package/assets/skills/ppm/references/http-api.md +1 -1
  5. package/dist/web/assets/accounts-settings-section-Cs7BC5O1.js +1 -0
  6. package/dist/web/assets/accounts-settings-section-Cs7BC5O1.js.br +0 -0
  7. package/dist/web/assets/accounts-settings-section-Cs7BC5O1.js.gz +0 -0
  8. package/dist/web/assets/{ai-resource-editor-DoCiQDKY.js → ai-resource-editor-TCREp5Ml.js} +1 -1
  9. package/dist/web/assets/ai-resource-editor-TCREp5Ml.js.br +0 -0
  10. package/dist/web/assets/ai-resource-editor-TCREp5Ml.js.gz +0 -0
  11. package/dist/web/assets/{ai-settings-section-0x6GFJ56.js → ai-settings-section-B7Un4KiW.js} +1 -1
  12. package/dist/web/assets/ai-settings-section-B7Un4KiW.js.br +0 -0
  13. package/dist/web/assets/ai-settings-section-B7Un4KiW.js.gz +0 -0
  14. package/dist/web/assets/{ai-settings-section-Cp6jcnTn.js → ai-settings-section-BlKlcihJ.js} +1 -1
  15. package/dist/web/assets/{appearance-settings-section-mRiGj3_g.js → appearance-settings-section-DACtiDor.js} +1 -1
  16. package/dist/web/assets/appearance-settings-section-DACtiDor.js.br +0 -0
  17. package/dist/web/assets/appearance-settings-section-DACtiDor.js.gz +0 -0
  18. package/dist/web/assets/{audio-preview-CO7Ia2N5.js → audio-preview-DXtE1Lpz.js} +1 -1
  19. package/dist/web/assets/audio-preview-DXtE1Lpz.js.br +0 -0
  20. package/dist/web/assets/audio-preview-DXtE1Lpz.js.gz +0 -0
  21. package/dist/web/assets/{branch-review-tab-F3CALiEa.js → branch-review-tab-vHJuFXtB.js} +1 -1
  22. package/dist/web/assets/branch-review-tab-vHJuFXtB.js.br +0 -0
  23. package/dist/web/assets/branch-review-tab-vHJuFXtB.js.gz +0 -0
  24. package/dist/web/assets/chat-tab-Ba3NvUgt.js +1 -0
  25. package/dist/web/assets/chat-tab-Ba3NvUgt.js.br +0 -0
  26. package/dist/web/assets/chat-tab-Ba3NvUgt.js.gz +0 -0
  27. package/dist/web/assets/chat-tab-DeISufm0.js +15 -0
  28. package/dist/web/assets/chat-tab-DeISufm0.js.br +0 -0
  29. package/dist/web/assets/chat-tab-DeISufm0.js.gz +0 -0
  30. package/dist/web/assets/{code-editor-FsR6KpZa.js → code-editor-BCwiLVyc.js} +3 -3
  31. package/dist/web/assets/code-editor-BCwiLVyc.js.br +0 -0
  32. package/dist/web/assets/code-editor-BCwiLVyc.js.gz +0 -0
  33. package/dist/web/assets/codex-sign-in-again-dialog-DcIOYhAe.js +1 -0
  34. package/dist/web/assets/codex-sign-in-again-dialog-DcIOYhAe.js.br +0 -0
  35. package/dist/web/assets/codex-sign-in-again-dialog-DcIOYhAe.js.gz +0 -0
  36. package/dist/web/assets/{conflict-editor-GGqIz6tt.js → conflict-editor-0YEDQ--c.js} +1 -1
  37. package/dist/web/assets/conflict-editor-0YEDQ--c.js.br +0 -0
  38. package/dist/web/assets/conflict-editor-0YEDQ--c.js.gz +0 -0
  39. package/dist/web/assets/{context-menu-CVO3HtIt.js → context-menu-CjYTq8DI.js} +1 -1
  40. package/dist/web/assets/context-menu-CjYTq8DI.js.br +0 -0
  41. package/dist/web/assets/context-menu-CjYTq8DI.js.gz +0 -0
  42. package/dist/web/assets/{csv-preview-ycnxJ_6J.js → csv-preview-CYxznB1I.js} +1 -1
  43. package/dist/web/assets/csv-preview-CYxznB1I.js.br +0 -0
  44. package/dist/web/assets/csv-preview-CYxznB1I.js.gz +0 -0
  45. package/dist/web/assets/{database-viewer-y6URJv8N.js → database-viewer-eP3gCxGb.js} +1 -1
  46. package/dist/web/assets/database-viewer-eP3gCxGb.js.br +0 -0
  47. package/dist/web/assets/database-viewer-eP3gCxGb.js.gz +0 -0
  48. package/dist/web/assets/{design-responsive-dialog-D0QYfX7v.js → design-responsive-dialog-CcJETHO2.js} +1 -1
  49. package/dist/web/assets/design-responsive-dialog-CcJETHO2.js.br +0 -0
  50. package/dist/web/assets/design-responsive-dialog-CcJETHO2.js.gz +0 -0
  51. package/dist/web/assets/{design-tab-DgCTcwot.js → design-tab-B7aVMMfj.js} +1 -1
  52. package/dist/web/assets/design-tab-B7aVMMfj.js.br +0 -0
  53. package/dist/web/assets/design-tab-B7aVMMfj.js.gz +0 -0
  54. package/dist/web/assets/{designs-sidebar-panel-Dtuph5jh.js → designs-sidebar-panel-8KI79o1u.js} +1 -1
  55. package/dist/web/assets/designs-sidebar-panel-8KI79o1u.js.br +0 -0
  56. package/dist/web/assets/designs-sidebar-panel-8KI79o1u.js.gz +0 -0
  57. package/dist/web/assets/{dialog-CaShS4f5.js → dialog-tzBRkGeW.js} +1 -1
  58. package/dist/web/assets/dialog-tzBRkGeW.js.br +0 -0
  59. package/dist/web/assets/dialog-tzBRkGeW.js.gz +0 -0
  60. package/dist/web/assets/{diff-viewer-ciBkExM7.js → diff-viewer-B_K7VQup.js} +1 -1
  61. package/dist/web/assets/diff-viewer-B_K7VQup.js.br +0 -0
  62. package/dist/web/assets/diff-viewer-B_K7VQup.js.gz +0 -0
  63. package/dist/web/assets/{docx-preview-C8K6QzOW.js → docx-preview-C-UKGMXR.js} +1 -1
  64. package/dist/web/assets/docx-preview-C-UKGMXR.js.br +0 -0
  65. package/dist/web/assets/docx-preview-C-UKGMXR.js.gz +0 -0
  66. package/dist/web/assets/{editor-language-service-CkoaAHjF.js → editor-language-service-DYBxZX66.js} +1 -1
  67. package/dist/web/assets/editor-language-service-DYBxZX66.js.br +0 -0
  68. package/dist/web/assets/editor-language-service-DYBxZX66.js.gz +0 -0
  69. package/dist/web/assets/{explorer-body-C5gXPGOl.js → explorer-body-C0syNltJ.js} +1 -1
  70. package/dist/web/assets/explorer-body-C0syNltJ.js.br +0 -0
  71. package/dist/web/assets/explorer-body-C0syNltJ.js.gz +0 -0
  72. package/dist/web/assets/{explorer-window-content-8EBl9Vkx.js → explorer-window-content-CfIomwTq.js} +1 -1
  73. package/dist/web/assets/explorer-window-content-CfIomwTq.js.br +0 -0
  74. package/dist/web/assets/explorer-window-content-CfIomwTq.js.gz +0 -0
  75. package/dist/web/assets/{extension-manager-section-DOwZYx8O.js → extension-manager-section-CL7pl3re.js} +1 -1
  76. package/dist/web/assets/extension-manager-section-CL7pl3re.js.br +0 -0
  77. package/dist/web/assets/extension-manager-section-CL7pl3re.js.gz +0 -0
  78. package/dist/web/assets/{extension-webview-CdEva8pD.js → extension-webview-FHTB0jh1.js} +1 -1
  79. package/dist/web/assets/extension-webview-FHTB0jh1.js.br +0 -0
  80. package/dist/web/assets/extension-webview-FHTB0jh1.js.gz +0 -0
  81. package/dist/web/assets/{files-settings-section-DVemQIZ2.js → files-settings-section-C574RfCl.js} +1 -1
  82. package/dist/web/assets/files-settings-section-C574RfCl.js.br +0 -0
  83. package/dist/web/assets/files-settings-section-C574RfCl.js.gz +0 -0
  84. package/dist/web/assets/{general-settings-section-DiY_5C93.js → general-settings-section--DSUTrEP.js} +1 -1
  85. package/dist/web/assets/general-settings-section--DSUTrEP.js.br +0 -0
  86. package/dist/web/assets/general-settings-section--DSUTrEP.js.gz +0 -0
  87. package/dist/web/assets/{git-log-panel-Cq0WohOO.js → git-log-panel-BzwEbRwN.js} +1 -1
  88. package/dist/web/assets/git-log-panel-BzwEbRwN.js.br +0 -0
  89. package/dist/web/assets/git-log-panel-BzwEbRwN.js.gz +0 -0
  90. package/dist/web/assets/{glide-data-grid-DG9AnCmP.js → glide-data-grid-CUNWBFfN.js} +1 -1
  91. package/dist/web/assets/glide-data-grid-CUNWBFfN.js.br +0 -0
  92. package/dist/web/assets/glide-data-grid-CUNWBFfN.js.gz +0 -0
  93. package/dist/web/assets/{group-chat-tab-DxbDrFJl.js → group-chat-tab-Dp1p4Jdu.js} +1 -1
  94. package/dist/web/assets/group-chat-tab-Dp1p4Jdu.js.br +0 -0
  95. package/dist/web/assets/group-chat-tab-Dp1p4Jdu.js.gz +0 -0
  96. package/dist/web/assets/icons-CO_II6D-.js +1 -0
  97. package/dist/web/assets/icons-CO_II6D-.js.br +0 -0
  98. package/dist/web/assets/icons-CO_II6D-.js.gz +0 -0
  99. package/dist/web/assets/{image-preview-eAv91-an.js → image-preview-78Fguk1f.js} +1 -1
  100. package/dist/web/assets/{index-BVKT5UCu.js → index-CvUGUp7d.js} +6 -6
  101. package/dist/web/assets/index-CvUGUp7d.js.br +0 -0
  102. package/dist/web/assets/index-CvUGUp7d.js.gz +0 -0
  103. package/dist/web/assets/index-DMA-So69.css +1 -0
  104. package/dist/web/assets/index-DMA-So69.css.br +0 -0
  105. package/dist/web/assets/index-DMA-So69.css.gz +0 -0
  106. package/dist/web/assets/{jira-watcher-section-Cr8gSVGY.js → jira-watcher-section-DoXl0GAq.js} +1 -1
  107. package/dist/web/assets/jira-watcher-section-DoXl0GAq.js.br +0 -0
  108. package/dist/web/assets/jira-watcher-section-DoXl0GAq.js.gz +0 -0
  109. package/dist/web/assets/{keybindings-store-Ch1mWz0q.js → keybindings-store-BqGm9AgI.js} +1 -1
  110. package/dist/web/assets/{keyboard-shortcuts-section-BvC6m_C2.js → keyboard-shortcuts-section-BogwnAdr.js} +1 -1
  111. package/dist/web/assets/keyboard-shortcuts-section-BogwnAdr.js.br +0 -0
  112. package/dist/web/assets/keyboard-shortcuts-section-BogwnAdr.js.gz +0 -0
  113. package/dist/web/assets/{markdown-renderer-56wMzGkr.js → markdown-renderer-C33dsNJ_.js} +1 -1
  114. package/dist/web/assets/{markdown-renderer-8JlfrfnD.js → markdown-renderer-CZiECUJp.js} +1 -1
  115. package/dist/web/assets/markdown-renderer-CZiECUJp.js.br +0 -0
  116. package/dist/web/assets/markdown-renderer-CZiECUJp.js.gz +0 -0
  117. package/dist/web/assets/{mobile-explorer-sheet-cJfocgE7.js → mobile-explorer-sheet-BOgo9tbK.js} +1 -1
  118. package/dist/web/assets/mobile-explorer-sheet-BOgo9tbK.js.br +0 -0
  119. package/dist/web/assets/mobile-explorer-sheet-BOgo9tbK.js.gz +0 -0
  120. package/dist/web/assets/{new-design-dialog-BO_D3nex.js → new-design-dialog-DcoejI1s.js} +1 -1
  121. package/dist/web/assets/new-design-dialog-DcoejI1s.js.br +0 -0
  122. package/dist/web/assets/new-design-dialog-DcoejI1s.js.gz +0 -0
  123. package/dist/web/assets/{notification-store-DNWazI7j.js → notification-store-D88Y9kF_.js} +1 -1
  124. package/dist/web/assets/{notifications-settings-section-DaMh3khs.js → notifications-settings-section-Bfxqz65i.js} +1 -1
  125. package/dist/web/assets/notifications-settings-section-Bfxqz65i.js.br +0 -0
  126. package/dist/web/assets/notifications-settings-section-Bfxqz65i.js.gz +0 -0
  127. package/dist/web/assets/{onboarding-entry-CwkNJ69U.js → onboarding-entry-CcvySZ_l.js} +1 -1
  128. package/dist/web/assets/onboarding-entry-CcvySZ_l.js.br +0 -0
  129. package/dist/web/assets/onboarding-entry-CcvySZ_l.js.gz +0 -0
  130. package/dist/web/assets/{onboarding-quick-orientation-u7yM68d3.js → onboarding-quick-orientation-CT4PylG0.js} +1 -1
  131. package/dist/web/assets/onboarding-quick-orientation-CT4PylG0.js.br +0 -0
  132. package/dist/web/assets/onboarding-quick-orientation-CT4PylG0.js.gz +0 -0
  133. package/dist/web/assets/{pdf-preview-CaxTidh0.js → pdf-preview-BWim6mu1.js} +1 -1
  134. package/dist/web/assets/pdf-preview-BWim6mu1.js.br +0 -0
  135. package/dist/web/assets/pdf-preview-BWim6mu1.js.gz +0 -0
  136. package/dist/web/assets/{postgres-viewer-CJOOLdgO.js → postgres-viewer-HwlL1UTJ.js} +1 -1
  137. package/dist/web/assets/postgres-viewer-HwlL1UTJ.js.br +0 -0
  138. package/dist/web/assets/postgres-viewer-HwlL1UTJ.js.gz +0 -0
  139. package/dist/web/assets/{ppmbot-settings-section-rNBdWG7F.js → ppmbot-settings-section-DF0kqdyO.js} +1 -1
  140. package/dist/web/assets/ppmbot-settings-section-DF0kqdyO.js.br +0 -0
  141. package/dist/web/assets/ppmbot-settings-section-DF0kqdyO.js.gz +0 -0
  142. package/dist/web/assets/{problems-panel-BTmOGMcL.js → problems-panel-DRcq5ChN.js} +1 -1
  143. package/dist/web/assets/problems-panel-DRcq5ChN.js.br +0 -0
  144. package/dist/web/assets/problems-panel-DRcq5ChN.js.gz +0 -0
  145. package/dist/web/assets/{process-table-Bc9WEBsS.js → process-table-Ds0S_YK-.js} +1 -1
  146. package/dist/web/assets/process-table-Ds0S_YK-.js.br +0 -0
  147. package/dist/web/assets/process-table-Ds0S_YK-.js.gz +0 -0
  148. package/dist/web/assets/{proxy-settings-section-Bo-FPk6Y.js → proxy-settings-section-Q-ttqVOa.js} +1 -1
  149. package/dist/web/assets/proxy-settings-section-Q-ttqVOa.js.br +0 -0
  150. package/dist/web/assets/proxy-settings-section-Q-ttqVOa.js.gz +0 -0
  151. package/dist/web/assets/{query-audit-section-CQfzKAf9.js → query-audit-section-Cn11xqgz.js} +1 -1
  152. package/dist/web/assets/query-audit-section-Cn11xqgz.js.br +0 -0
  153. package/dist/web/assets/query-audit-section-Cn11xqgz.js.gz +0 -0
  154. package/dist/web/assets/{remote-desktop-clipboard-notice-DgeuoZ4n.js → remote-desktop-clipboard-notice-CvPPtrRq.js} +1 -1
  155. package/dist/web/assets/remote-desktop-clipboard-notice-CvPPtrRq.js.br +0 -0
  156. package/dist/web/assets/remote-desktop-clipboard-notice-CvPPtrRq.js.gz +0 -0
  157. package/dist/web/assets/{remote-desktop-mobile-sheet-BpP7jI0p.js → remote-desktop-mobile-sheet-B-TT9Fdt.js} +2 -2
  158. package/dist/web/assets/remote-desktop-mobile-sheet-B-TT9Fdt.js.br +0 -0
  159. package/dist/web/assets/remote-desktop-mobile-sheet-B-TT9Fdt.js.gz +0 -0
  160. package/dist/web/assets/{remote-desktop-mobile-view-Byn0lFCF.js → remote-desktop-mobile-view-hUnYxXDk.js} +1 -1
  161. package/dist/web/assets/remote-desktop-mobile-view-hUnYxXDk.js.br +0 -0
  162. package/dist/web/assets/remote-desktop-mobile-view-hUnYxXDk.js.gz +0 -0
  163. package/dist/web/assets/{remote-desktop-readiness-gate-B2mXZ9vo.js → remote-desktop-readiness-gate-CUmaeiX9.js} +1 -1
  164. package/dist/web/assets/remote-desktop-readiness-gate-CUmaeiX9.js.br +0 -0
  165. package/dist/web/assets/remote-desktop-readiness-gate-CUmaeiX9.js.gz +0 -0
  166. package/dist/web/assets/{remote-desktop-window-content-CWt3VVjm.js → remote-desktop-window-content-DEPUOOGw.js} +1 -1
  167. package/dist/web/assets/remote-desktop-window-content-DEPUOOGw.js.br +0 -0
  168. package/dist/web/assets/remote-desktop-window-content-DEPUOOGw.js.gz +0 -0
  169. package/dist/web/assets/{schedules-settings-section-Bx9Rm-NB.js → schedules-settings-section-Bv_wrzDp.js} +1 -1
  170. package/dist/web/assets/schedules-settings-section-Bv_wrzDp.js.br +0 -0
  171. package/dist/web/assets/schedules-settings-section-Bv_wrzDp.js.gz +0 -0
  172. package/dist/web/assets/{settings-body-DU3LZkOy.js → settings-body-C5_chEpq.js} +2 -2
  173. package/dist/web/assets/settings-body-C5_chEpq.js.br +0 -0
  174. package/dist/web/assets/settings-body-C5_chEpq.js.gz +0 -0
  175. package/dist/web/assets/{settings-tab-Q5Ai_XB7.js → settings-tab-CogpEbq5.js} +1 -1
  176. package/dist/web/assets/{settings-window-content-DNg-jjm4.js → settings-window-content-BNRirQDZ.js} +1 -1
  177. package/dist/web/assets/{sql-query-editor-BMNsmnfi.js → sql-query-editor-Bg3lOrmt.js} +1 -1
  178. package/dist/web/assets/sql-query-editor-Bg3lOrmt.js.br +0 -0
  179. package/dist/web/assets/sql-query-editor-Bg3lOrmt.js.gz +0 -0
  180. package/dist/web/assets/{sqlite-viewer-DBdxQgCD.js → sqlite-viewer-DMKtDkI6.js} +1 -1
  181. package/dist/web/assets/sqlite-viewer-DMKtDkI6.js.br +0 -0
  182. package/dist/web/assets/sqlite-viewer-DMKtDkI6.js.gz +0 -0
  183. package/dist/web/assets/{system-monitor-body-BEuqhoiF.js → system-monitor-body-Dst39LA7.js} +2 -2
  184. package/dist/web/assets/system-monitor-body-Dst39LA7.js.br +0 -0
  185. package/dist/web/assets/system-monitor-body-Dst39LA7.js.gz +0 -0
  186. package/dist/web/assets/system-monitor-tab-CEwME1_C.js +1 -0
  187. package/dist/web/assets/{system-monitor-window-content-Dr6EgqpJ.js → system-monitor-window-content-DQRBjn9z.js} +1 -1
  188. package/dist/web/assets/{tab-host-window-content-BhS39QWy.js → tab-host-window-content-Dlunzrfh.js} +1 -1
  189. package/dist/web/assets/tab-host-window-content-Dlunzrfh.js.br +0 -0
  190. package/dist/web/assets/tab-host-window-content-Dlunzrfh.js.gz +0 -0
  191. package/dist/web/assets/{team-member-sheet-C5-PeHDb.js → team-member-sheet-CPZGU9j5.js} +2 -2
  192. package/dist/web/assets/team-member-sheet-CPZGU9j5.js.br +0 -0
  193. package/dist/web/assets/team-member-sheet-CPZGU9j5.js.gz +0 -0
  194. package/dist/web/assets/{team-member-window-content-CZjHuvdw.js → team-member-window-content-CwI9JD_H.js} +1 -1
  195. package/dist/web/assets/{team-member-window-content-CZjHuvdw.js.br → team-member-window-content-CwI9JD_H.js.br} +0 -0
  196. package/dist/web/assets/team-member-window-content-CwI9JD_H.js.gz +0 -0
  197. package/dist/web/assets/{terminal-tab-B0w7Jsbs.js → terminal-tab-BcQXleSa.js} +1 -1
  198. package/dist/web/assets/terminal-tab-BcQXleSa.js.br +0 -0
  199. package/dist/web/assets/terminal-tab-BcQXleSa.js.gz +0 -0
  200. package/dist/web/assets/{tool-cards-CmdjXi2c.js → tool-cards-DsQdMorI.js} +2 -2
  201. package/dist/web/assets/tool-cards-DsQdMorI.js.br +0 -0
  202. package/dist/web/assets/tool-cards-DsQdMorI.js.gz +0 -0
  203. package/dist/web/assets/{use-inline-blame-BSb807l9.js → use-inline-blame--fS-pzzX.js} +1 -1
  204. package/dist/web/assets/use-inline-blame--fS-pzzX.js.br +0 -0
  205. package/dist/web/assets/use-inline-blame--fS-pzzX.js.gz +0 -0
  206. package/dist/web/assets/{use-lsp-DhBjkGWr.js → use-lsp-DK26Eter.js} +1 -1
  207. package/dist/web/assets/use-lsp-DK26Eter.js.br +0 -0
  208. package/dist/web/assets/use-lsp-DK26Eter.js.gz +0 -0
  209. package/dist/web/assets/{use-monaco-theme-CNEk66US.js → use-monaco-theme-BVatH9yZ.js} +1 -1
  210. package/dist/web/assets/{use-websocket-Bki-B4fj.js → use-websocket-C1mloX0R.js} +1 -1
  211. package/dist/web/assets/{video-preview-T1SMFp03.js → video-preview-C48Go7JZ.js} +1 -1
  212. package/dist/web/assets/video-preview-C48Go7JZ.js.br +0 -0
  213. package/dist/web/assets/video-preview-C48Go7JZ.js.gz +0 -0
  214. package/dist/web/index.html +6 -6
  215. package/dist/web/index.html.br +0 -0
  216. package/dist/web/index.html.gz +0 -0
  217. package/dist/web/sw.js +1 -1
  218. package/dist/web/sw.js.br +0 -0
  219. package/dist/web/sw.js.gz +0 -0
  220. package/package.json +1 -1
  221. package/src/providers/codex-app-server/codex-auth-failure.ts +51 -0
  222. package/src/providers/codex-app-server/codex-event-mapper.ts +8 -0
  223. package/src/providers/codex-app-server/codex-provider.ts +97 -19
  224. package/src/providers/codex-app-server/codex-usage-source.ts +14 -1
  225. package/src/server/routes/codex-accounts.ts +18 -6
  226. package/src/services/codex-account-auth-state.ts +58 -0
  227. package/src/services/codex-account-login.ts +42 -9
  228. package/src/services/codex-account.service.ts +42 -7
  229. package/src/services/proxy-agent-anthropic-bridge.ts +2 -2
  230. package/src/services/proxy-agent-bridge.ts +2 -2
  231. package/src/services/proxy-agent-turn.ts +80 -20
  232. package/src/services/proxy-image-bridge.ts +3 -3
  233. package/src/services/proxy.service.ts +1 -1
  234. package/src/web/components/chat/codex-usage-panel.tsx +13 -1
  235. package/src/web/components/chat/usage-badge.tsx +14 -0
  236. package/src/web/components/settings/accounts/account-add-dialog.tsx +10 -3
  237. package/src/web/components/settings/accounts/account-card.tsx +15 -4
  238. package/src/web/components/settings/accounts/account-hint.tsx +19 -2
  239. package/src/web/components/settings/accounts/claude-accounts-section.tsx +10 -2
  240. package/src/web/components/settings/accounts/codex-accounts-section.tsx +18 -2
  241. package/src/web/components/settings/accounts/codex-add-account-dialog.tsx +50 -33
  242. package/src/web/components/settings/accounts/codex-sign-in-again-dialog.tsx +47 -0
  243. package/src/web/components/settings/accounts/use-codex-accounts.ts +18 -4
  244. package/src/web/lib/fluent-icon.tsx +38 -2
  245. package/dist/web/assets/accounts-settings-section-CVJ7hQom.js +0 -1
  246. package/dist/web/assets/accounts-settings-section-CVJ7hQom.js.br +0 -0
  247. package/dist/web/assets/accounts-settings-section-CVJ7hQom.js.gz +0 -0
  248. package/dist/web/assets/ai-resource-editor-DoCiQDKY.js.br +0 -0
  249. package/dist/web/assets/ai-resource-editor-DoCiQDKY.js.gz +0 -0
  250. package/dist/web/assets/ai-settings-section-0x6GFJ56.js.br +0 -0
  251. package/dist/web/assets/ai-settings-section-0x6GFJ56.js.gz +0 -0
  252. package/dist/web/assets/appearance-settings-section-mRiGj3_g.js.br +0 -0
  253. package/dist/web/assets/appearance-settings-section-mRiGj3_g.js.gz +0 -0
  254. package/dist/web/assets/audio-preview-CO7Ia2N5.js.br +0 -0
  255. package/dist/web/assets/audio-preview-CO7Ia2N5.js.gz +0 -0
  256. package/dist/web/assets/branch-review-tab-F3CALiEa.js.br +0 -0
  257. package/dist/web/assets/branch-review-tab-F3CALiEa.js.gz +0 -0
  258. package/dist/web/assets/chat-tab-D6BFnkCj.js +0 -1
  259. package/dist/web/assets/chat-tab-D6BFnkCj.js.br +0 -0
  260. package/dist/web/assets/chat-tab-D6BFnkCj.js.gz +0 -0
  261. package/dist/web/assets/chat-tab-IXHMUKMW.js +0 -15
  262. package/dist/web/assets/chat-tab-IXHMUKMW.js.br +0 -0
  263. package/dist/web/assets/chat-tab-IXHMUKMW.js.gz +0 -0
  264. package/dist/web/assets/code-editor-FsR6KpZa.js.br +0 -0
  265. package/dist/web/assets/code-editor-FsR6KpZa.js.gz +0 -0
  266. package/dist/web/assets/codex-daily-guard-Bn8jfcei.js +0 -1
  267. package/dist/web/assets/codex-daily-guard-Bn8jfcei.js.br +0 -0
  268. package/dist/web/assets/codex-daily-guard-Bn8jfcei.js.gz +0 -0
  269. package/dist/web/assets/conflict-editor-GGqIz6tt.js.br +0 -0
  270. package/dist/web/assets/conflict-editor-GGqIz6tt.js.gz +0 -0
  271. package/dist/web/assets/context-menu-CVO3HtIt.js.br +0 -0
  272. package/dist/web/assets/context-menu-CVO3HtIt.js.gz +0 -0
  273. package/dist/web/assets/csv-preview-ycnxJ_6J.js.br +0 -0
  274. package/dist/web/assets/csv-preview-ycnxJ_6J.js.gz +0 -0
  275. package/dist/web/assets/database-viewer-y6URJv8N.js.br +0 -0
  276. package/dist/web/assets/database-viewer-y6URJv8N.js.gz +0 -0
  277. package/dist/web/assets/design-responsive-dialog-D0QYfX7v.js.br +0 -0
  278. package/dist/web/assets/design-responsive-dialog-D0QYfX7v.js.gz +0 -0
  279. package/dist/web/assets/design-tab-DgCTcwot.js.br +0 -0
  280. package/dist/web/assets/design-tab-DgCTcwot.js.gz +0 -0
  281. package/dist/web/assets/designs-sidebar-panel-Dtuph5jh.js.br +0 -0
  282. package/dist/web/assets/designs-sidebar-panel-Dtuph5jh.js.gz +0 -0
  283. package/dist/web/assets/dialog-CaShS4f5.js.br +0 -0
  284. package/dist/web/assets/dialog-CaShS4f5.js.gz +0 -0
  285. package/dist/web/assets/diff-viewer-ciBkExM7.js.br +0 -0
  286. package/dist/web/assets/diff-viewer-ciBkExM7.js.gz +0 -0
  287. package/dist/web/assets/docx-preview-C8K6QzOW.js.br +0 -0
  288. package/dist/web/assets/docx-preview-C8K6QzOW.js.gz +0 -0
  289. package/dist/web/assets/editor-language-service-CkoaAHjF.js.br +0 -0
  290. package/dist/web/assets/editor-language-service-CkoaAHjF.js.gz +0 -0
  291. package/dist/web/assets/explorer-body-C5gXPGOl.js.br +0 -0
  292. package/dist/web/assets/explorer-body-C5gXPGOl.js.gz +0 -0
  293. package/dist/web/assets/explorer-window-content-8EBl9Vkx.js.br +0 -0
  294. package/dist/web/assets/explorer-window-content-8EBl9Vkx.js.gz +0 -0
  295. package/dist/web/assets/extension-manager-section-DOwZYx8O.js.br +0 -0
  296. package/dist/web/assets/extension-manager-section-DOwZYx8O.js.gz +0 -0
  297. package/dist/web/assets/extension-webview-CdEva8pD.js.br +0 -0
  298. package/dist/web/assets/extension-webview-CdEva8pD.js.gz +0 -0
  299. package/dist/web/assets/files-settings-section-DVemQIZ2.js.br +0 -0
  300. package/dist/web/assets/files-settings-section-DVemQIZ2.js.gz +0 -0
  301. package/dist/web/assets/general-settings-section-DiY_5C93.js.br +0 -0
  302. package/dist/web/assets/general-settings-section-DiY_5C93.js.gz +0 -0
  303. package/dist/web/assets/git-log-panel-Cq0WohOO.js.br +0 -0
  304. package/dist/web/assets/git-log-panel-Cq0WohOO.js.gz +0 -0
  305. package/dist/web/assets/glide-data-grid-DG9AnCmP.js.br +0 -0
  306. package/dist/web/assets/glide-data-grid-DG9AnCmP.js.gz +0 -0
  307. package/dist/web/assets/group-chat-tab-DxbDrFJl.js.br +0 -0
  308. package/dist/web/assets/group-chat-tab-DxbDrFJl.js.gz +0 -0
  309. package/dist/web/assets/icons-C9ZwXgel.js +0 -1
  310. package/dist/web/assets/icons-C9ZwXgel.js.br +0 -0
  311. package/dist/web/assets/icons-C9ZwXgel.js.gz +0 -0
  312. package/dist/web/assets/index-BVKT5UCu.js.br +0 -0
  313. package/dist/web/assets/index-BVKT5UCu.js.gz +0 -0
  314. package/dist/web/assets/index-CzYrKDUF.css +0 -1
  315. package/dist/web/assets/index-CzYrKDUF.css.br +0 -0
  316. package/dist/web/assets/index-CzYrKDUF.css.gz +0 -0
  317. package/dist/web/assets/jira-watcher-section-Cr8gSVGY.js.br +0 -0
  318. package/dist/web/assets/jira-watcher-section-Cr8gSVGY.js.gz +0 -0
  319. package/dist/web/assets/keyboard-shortcuts-section-BvC6m_C2.js.br +0 -0
  320. package/dist/web/assets/keyboard-shortcuts-section-BvC6m_C2.js.gz +0 -0
  321. package/dist/web/assets/markdown-renderer-8JlfrfnD.js.br +0 -0
  322. package/dist/web/assets/markdown-renderer-8JlfrfnD.js.gz +0 -0
  323. package/dist/web/assets/mobile-explorer-sheet-cJfocgE7.js.br +0 -0
  324. package/dist/web/assets/mobile-explorer-sheet-cJfocgE7.js.gz +0 -0
  325. package/dist/web/assets/new-design-dialog-BO_D3nex.js.br +0 -0
  326. package/dist/web/assets/new-design-dialog-BO_D3nex.js.gz +0 -0
  327. package/dist/web/assets/notifications-settings-section-DaMh3khs.js.br +0 -0
  328. package/dist/web/assets/notifications-settings-section-DaMh3khs.js.gz +0 -0
  329. package/dist/web/assets/onboarding-entry-CwkNJ69U.js.br +0 -0
  330. package/dist/web/assets/onboarding-entry-CwkNJ69U.js.gz +0 -0
  331. package/dist/web/assets/onboarding-quick-orientation-u7yM68d3.js.br +0 -0
  332. package/dist/web/assets/onboarding-quick-orientation-u7yM68d3.js.gz +0 -0
  333. package/dist/web/assets/pdf-preview-CaxTidh0.js.br +0 -0
  334. package/dist/web/assets/pdf-preview-CaxTidh0.js.gz +0 -0
  335. package/dist/web/assets/postgres-viewer-CJOOLdgO.js.br +0 -0
  336. package/dist/web/assets/postgres-viewer-CJOOLdgO.js.gz +0 -0
  337. package/dist/web/assets/ppmbot-settings-section-rNBdWG7F.js.br +0 -0
  338. package/dist/web/assets/ppmbot-settings-section-rNBdWG7F.js.gz +0 -0
  339. package/dist/web/assets/problems-panel-BTmOGMcL.js.br +0 -0
  340. package/dist/web/assets/problems-panel-BTmOGMcL.js.gz +0 -0
  341. package/dist/web/assets/process-table-Bc9WEBsS.js.br +0 -0
  342. package/dist/web/assets/process-table-Bc9WEBsS.js.gz +0 -0
  343. package/dist/web/assets/proxy-settings-section-Bo-FPk6Y.js.br +0 -0
  344. package/dist/web/assets/proxy-settings-section-Bo-FPk6Y.js.gz +0 -0
  345. package/dist/web/assets/query-audit-section-CQfzKAf9.js.br +0 -0
  346. package/dist/web/assets/query-audit-section-CQfzKAf9.js.gz +0 -0
  347. package/dist/web/assets/remote-desktop-clipboard-notice-DgeuoZ4n.js.br +0 -0
  348. package/dist/web/assets/remote-desktop-clipboard-notice-DgeuoZ4n.js.gz +0 -0
  349. package/dist/web/assets/remote-desktop-mobile-sheet-BpP7jI0p.js.br +0 -0
  350. package/dist/web/assets/remote-desktop-mobile-sheet-BpP7jI0p.js.gz +0 -0
  351. package/dist/web/assets/remote-desktop-mobile-view-Byn0lFCF.js.br +0 -0
  352. package/dist/web/assets/remote-desktop-mobile-view-Byn0lFCF.js.gz +0 -0
  353. package/dist/web/assets/remote-desktop-readiness-gate-B2mXZ9vo.js.br +0 -0
  354. package/dist/web/assets/remote-desktop-readiness-gate-B2mXZ9vo.js.gz +0 -0
  355. package/dist/web/assets/remote-desktop-window-content-CWt3VVjm.js.br +0 -0
  356. package/dist/web/assets/remote-desktop-window-content-CWt3VVjm.js.gz +0 -0
  357. package/dist/web/assets/schedules-settings-section-Bx9Rm-NB.js.br +0 -0
  358. package/dist/web/assets/schedules-settings-section-Bx9Rm-NB.js.gz +0 -0
  359. package/dist/web/assets/settings-body-DU3LZkOy.js.br +0 -0
  360. package/dist/web/assets/settings-body-DU3LZkOy.js.gz +0 -0
  361. package/dist/web/assets/sql-query-editor-BMNsmnfi.js.br +0 -0
  362. package/dist/web/assets/sql-query-editor-BMNsmnfi.js.gz +0 -0
  363. package/dist/web/assets/sqlite-viewer-DBdxQgCD.js.br +0 -0
  364. package/dist/web/assets/sqlite-viewer-DBdxQgCD.js.gz +0 -0
  365. package/dist/web/assets/system-monitor-body-BEuqhoiF.js.br +0 -0
  366. package/dist/web/assets/system-monitor-body-BEuqhoiF.js.gz +0 -0
  367. package/dist/web/assets/system-monitor-tab-BAU1kzbY.js +0 -1
  368. package/dist/web/assets/tab-host-window-content-BhS39QWy.js.br +0 -0
  369. package/dist/web/assets/tab-host-window-content-BhS39QWy.js.gz +0 -0
  370. package/dist/web/assets/team-member-sheet-C5-PeHDb.js.br +0 -0
  371. package/dist/web/assets/team-member-sheet-C5-PeHDb.js.gz +0 -0
  372. package/dist/web/assets/team-member-window-content-CZjHuvdw.js.gz +0 -0
  373. package/dist/web/assets/terminal-tab-B0w7Jsbs.js.br +0 -0
  374. package/dist/web/assets/terminal-tab-B0w7Jsbs.js.gz +0 -0
  375. package/dist/web/assets/tool-cards-CmdjXi2c.js.br +0 -0
  376. package/dist/web/assets/tool-cards-CmdjXi2c.js.gz +0 -0
  377. package/dist/web/assets/use-inline-blame-BSb807l9.js.br +0 -0
  378. package/dist/web/assets/use-inline-blame-BSb807l9.js.gz +0 -0
  379. package/dist/web/assets/use-lsp-DhBjkGWr.js.br +0 -0
  380. package/dist/web/assets/use-lsp-DhBjkGWr.js.gz +0 -0
  381. package/dist/web/assets/video-preview-T1SMFp03.js.br +0 -0
  382. package/dist/web/assets/video-preview-T1SMFp03.js.gz +0 -0
@@ -16,6 +16,8 @@ import { configService } from "./config.service.ts";
16
16
  import { encrypt, decrypt } from "../lib/account-crypto.ts";
17
17
  import { getOrFetchUsage } from "./provider-usage/usage-registry.ts";
18
18
  import { isCodexAccountUsageLimited } from "./codex-account-cooldown.ts";
19
+ import { isCodexAccountAuthFailed } from "./codex-account-auth-state.ts";
20
+ import { codexSignedOutMessage } from "../providers/codex-app-server/codex-auth-failure.ts";
19
21
  import { dailyGuardMessage, dailyGuardState } from "../shared/codex-daily-guard.ts";
20
22
  import type { UsageInfo } from "../providers/provider.interface.ts";
21
23
 
@@ -149,6 +151,35 @@ export function setCodexDailyGuard(id: string, enabled: boolean): CodexAccount |
149
151
 
150
152
  export class CodexDailyGuardError extends Error {}
151
153
 
154
+ /** Every managed account that could serve is signed out. */
155
+ export class CodexSignedOutError extends Error {}
156
+
157
+ /**
158
+ * Accounts that can be handed a turn at all: switched on, and not known to be signed out.
159
+ *
160
+ * Both exclusions are hard. Off is the user's say-so; signed out is an account that will
161
+ * refuse every turn after ~24s of codex retrying, so handing it back "because nothing else
162
+ * was left" buys a slow failure instead of a fast one.
163
+ */
164
+ function servableAccounts(): CodexAccount[] {
165
+ return listCodexAccounts().filter((a) => a.status !== "disabled" && !isCodexAccountAuthFailed(a.id));
166
+ }
167
+
168
+ /**
169
+ * Throw when accounts are enabled but every one of them is signed out.
170
+ *
171
+ * Without this the selector finds nothing, the caller falls back to the ambient ~/.codex
172
+ * login — usually absent or stale on a machine that manages accounts — and the turn fails
173
+ * with an error that names neither account.
174
+ */
175
+ function assertSomeAccountSignedIn(): void {
176
+ const enabled = listCodexAccounts().filter((a) => a.status !== "disabled");
177
+ if (enabled.length === 0 || enabled.some((a) => !isCodexAccountAuthFailed(a.id))) return;
178
+ throw new CodexSignedOutError(enabled.length === 1
179
+ ? codexSignedOutMessage(enabled[0]!.label)
180
+ : `Every Codex account is signed out (${enabled.map((a) => a.label).join(", ")}). Press "Sign in again" on their cards in Settings → Accounts → Codex.`);
181
+ }
182
+
152
183
  /** Remove the account row and its CODEX_HOME dir. */
153
184
  export function removeCodexAccount(id: string): void {
154
185
  const acct = getCodexAccount(id);
@@ -242,7 +273,7 @@ export function selectCodexAccount(opts?: { strategy?: CodexStrategy; usageOf?:
242
273
  // `exclude` is hard for the same reason: its only caller is the rotation away from an
243
274
  // account that just refused the turn, and falling back to it would rotate in a circle.
244
275
  const excluded = new Set(opts?.exclude ?? []);
245
- const all = listCodexAccounts().filter((a) => a.status !== "disabled" && !excluded.has(a.id));
276
+ const all = servableAccounts().filter((a) => !excluded.has(a.id));
246
277
  if (all.length === 0) return null;
247
278
  if (all.length === 1) return all[0]!;
248
279
  const strategy = opts?.strategy ?? getCodexStrategy();
@@ -276,7 +307,7 @@ export function peekCodexAccount(): CodexAccount | null {
276
307
  // Status and the quota park are the two things this can filter on without going async, so
277
308
  // it does both. Usage lives behind a fetch and stays out of reach here; the consuming pick
278
309
  // endpoint reads it instead.
279
- const accts = withQuotaLeft(listCodexAccounts().filter((a) => a.status !== "disabled"));
310
+ const accts = withQuotaLeft(servableAccounts());
280
311
  if (accts.length === 0) return null;
281
312
  if (accts.length === 1) return accts[0]!;
282
313
  return getCodexStrategy() === "fill-first" ? accts[0]! : null;
@@ -287,6 +318,11 @@ export async function resolveCodexAccountForSession(sessionId?: string): Promise
287
318
  // Session-less callers (the model list) have nothing sticky to honour and fall
288
319
  // straight through to the configured strategy.
289
320
  const sticky = sessionId ? getSessionCodexAccount(sessionId) : null;
321
+ // A signed-out account lets go of its sessions too: it would refuse the turn after
322
+ // codex's full retry loop, and there is no quota reset to wait for.
323
+ assertSomeAccountSignedIn();
324
+ const keepable = (a: CodexAccount | null): a is CodexAccount =>
325
+ !!a && a.status !== "disabled" && !isCodexAccountUsageLimited(a.id) && !isCodexAccountAuthFailed(a.id);
290
326
  // A binding is held for the prompt cache, not honoured unconditionally: an account the
291
327
  // user has switched off has to let go of the sessions sitting on it, or turning it off
292
328
  // would do nothing for exactly the conversations already using it. An account parked for
@@ -296,7 +332,7 @@ export async function resolveCodexAccountForSession(sessionId?: string): Promise
296
332
  // account able to serve it cannot. Parked accounts count as unable for the same reason
297
333
  // they are skipped below, so a pool of two where one is out of quota resolves without a
298
334
  // network read at all.
299
- const usable = listCodexAccounts().filter((a) => a.status !== "disabled" && !isCodexAccountUsageLimited(a.id));
335
+ const usable = servableAccounts().filter((a) => !isCodexAccountUsageLimited(a.id));
300
336
  const guarded = usable.filter((a) => a.dailyGuardEnabled);
301
337
  if (guarded.length > 0) {
302
338
  const usages = await getAllCodexUsages();
@@ -309,8 +345,7 @@ export async function resolveCodexAccountForSession(sessionId?: string): Promise
309
345
  });
310
346
  if (blocked.length > 0) {
311
347
  const stickyAccount = sticky ? getCodexAccount(sticky) : null;
312
- if (stickyAccount && !blocked.includes(stickyAccount.id)
313
- && stickyAccount.status !== "disabled" && !isCodexAccountUsageLimited(stickyAccount.id)) return stickyAccount;
348
+ if (keepable(stickyAccount) && !blocked.includes(stickyAccount.id)) return stickyAccount;
314
349
  if (blocked.length === usable.length && sessionId) {
315
350
  const account = getCodexAccount(blocked[0]!);
316
351
  const usage = account ? usages[account.id] : undefined;
@@ -321,13 +356,13 @@ export async function resolveCodexAccountForSession(sessionId?: string): Promise
321
356
  }
322
357
  if (sticky) {
323
358
  const a = getCodexAccount(sticky);
324
- if (a && a.status !== "disabled" && !isCodexAccountUsageLimited(a.id)) return a;
359
+ if (keepable(a)) return a;
325
360
  }
326
361
  return selectCodexAccount({ usageOf: (id) => codexUsageLevel(usages[id]) });
327
362
  }
328
363
  if (sticky) {
329
364
  const a = getCodexAccount(sticky);
330
- if (a && a.status !== "disabled" && !isCodexAccountUsageLimited(a.id)) return a;
365
+ if (keepable(a)) return a;
331
366
  }
332
367
  if (usable.length <= 1) return selectCodexAccount();
333
368
  // Usage is read for every strategy, not just lowest-usage: it is also what lets the
@@ -7,7 +7,7 @@
7
7
  * setting that base to `…/proxy/<provider>`.
8
8
  */
9
9
  import {
10
- startAgentTurn, usageOf, resolveProvider, proxyableProviderIds,
10
+ startAgentTurn, usageOf, resolveProvider, proxyableProviderIds, turnFailureResponse,
11
11
  } from "./proxy-agent-turn.ts";
12
12
  import {
13
13
  buildPromptFromAnthropicMessages, hasUnsupportedAnthropicBlocks, messageResponse, anthropicError,
@@ -99,6 +99,6 @@ export async function forwardAgentMessages(
99
99
  ? await runStreaming(providerId, body)
100
100
  : await runNonStreaming(providerId, body);
101
101
  } catch (e) {
102
- return anthropicError(502, (e as Error).message);
102
+ return turnFailureResponse(e, anthropicError);
103
103
  }
104
104
  }
@@ -15,7 +15,7 @@ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
15
15
  import { tmpdir } from "node:os";
16
16
  import { join } from "node:path";
17
17
  import {
18
- startAgentTurn, usageOf, resolveProvider, proxyableProviderIds,
18
+ startAgentTurn, usageOf, resolveProvider, proxyableProviderIds, turnFailureResponse,
19
19
  } from "./proxy-agent-turn.ts";
20
20
  import { decodeImagePayload } from "./proxy-image-bridge.ts";
21
21
  import {
@@ -140,7 +140,7 @@ export async function forwardAgentChatCompletions(
140
140
  ? await runStreaming(providerId, body)
141
141
  : await runNonStreaming(providerId, body);
142
142
  } catch (e) {
143
- return openAiError(502, (e as Error).message);
143
+ return turnFailureResponse(e, openAiError);
144
144
  }
145
145
  }
146
146
 
@@ -34,6 +34,54 @@ const PROXY_PERMISSION_MODE = "plan";
34
34
  const FIRST_EVENT_TIMEOUT_MS = 90_000;
35
35
  const IDLE_TIMEOUT_MS = 300_000;
36
36
 
37
+ /**
38
+ * How many proxy turns may run at once, across every provider and dialect.
39
+ *
40
+ * Every turn is its own agent subprocess (codex: `bun x @openai/codex app-server`), and a
41
+ * client that retries on failure multiplies them: a batch script at concurrency 4 against
42
+ * a signed-out account kept a dozen app-servers starting and failing at once, and the
43
+ * server stopped answering its own health check until the supervisor killed it, over and
44
+ * over. Past this many, a request is refused at once with 429 + Retry-After — which every
45
+ * OpenAI/Anthropic SDK already backs off on — instead of queueing more subprocesses.
46
+ */
47
+ export const MAX_CONCURRENT_PROXY_TURNS = 4;
48
+ const BUSY_RETRY_AFTER_S = 5;
49
+ let activeProxyTurns = 0;
50
+
51
+ /** Refused because {@link MAX_CONCURRENT_PROXY_TURNS} turns are already running. */
52
+ export class ProxyBusyError extends Error {}
53
+
54
+ /** Current number of running proxy turns (for tests and diagnostics). */
55
+ export function activeProxyTurnCount(): number {
56
+ return activeProxyTurns;
57
+ }
58
+
59
+ /** Take a turn slot, or throw {@link ProxyBusyError}. The returned release is idempotent. */
60
+ function acquireTurnSlot(): () => void {
61
+ if (activeProxyTurns >= MAX_CONCURRENT_PROXY_TURNS) {
62
+ throw new ProxyBusyError(`Proxy busy: ${MAX_CONCURRENT_PROXY_TURNS} requests are already running. Retry in ${BUSY_RETRY_AFTER_S}s.`);
63
+ }
64
+ activeProxyTurns++;
65
+ let released = false;
66
+ return () => {
67
+ if (released) return;
68
+ released = true;
69
+ activeProxyTurns--;
70
+ };
71
+ }
72
+
73
+ /**
74
+ * The error response for a turn that could not run, in the caller's dialect.
75
+ * A busy refusal is a 429 with Retry-After so SDKs back off; anything else is a 502.
76
+ */
77
+ export function turnFailureResponse(e: unknown, render: (status: number, message: string) => Response): Response {
78
+ const message = (e as Error)?.message ?? String(e);
79
+ if (!(e instanceof ProxyBusyError)) return render(502, message);
80
+ const res = render(429, message);
81
+ res.headers.set("Retry-After", String(BUSY_RETRY_AFTER_S));
82
+ return res;
83
+ }
84
+
37
85
  /** Providers that exist in the registry but must never be exposed over HTTP. */
38
86
  const NOT_PROXYABLE = new Set(["mock"]);
39
87
 
@@ -133,26 +181,38 @@ export async function startAgentTurn(providerId: string, req: TurnRequest): Prom
133
181
  // instructions lead the turn instead.
134
182
  const message = req.systemPrompt ? `${req.systemPrompt}\n\n${req.prompt}` : req.prompt;
135
183
 
136
- const session = await provider.createSession({
137
- projectPath: proxyWorkspace(),
138
- title: `[API] ${providerId}`,
139
- });
140
-
141
- const events = provider.sendMessage(session.id, message, {
142
- permissionMode: PROXY_PERMISSION_MODE,
143
- ...(req.model ? { model: req.model } : {}),
144
- ...(req.imagePaths?.length ? { imagePaths: req.imagePaths } : {}),
145
- });
146
-
147
- return {
148
- events: withTimeout(events),
149
- cleanup: async () => {
150
- // abortQuery is what kills the runtime; deleteSession alone may only drop
151
- // the record, depending on the provider.
152
- try { provider.abortQuery?.(session.id, "proxy"); } catch { /* best effort */ }
153
- try { await provider.deleteSession(session.id); } catch { /* best effort */ }
154
- },
155
- };
184
+ // Held until cleanup, which every caller runs on every path — including a streaming
185
+ // response whose body outlives this function.
186
+ const release = acquireTurnSlot();
187
+ try {
188
+ const session = await provider.createSession({
189
+ projectPath: proxyWorkspace(),
190
+ title: `[API] ${providerId}`,
191
+ });
192
+
193
+ const events = provider.sendMessage(session.id, message, {
194
+ permissionMode: PROXY_PERMISSION_MODE,
195
+ ...(req.model ? { model: req.model } : {}),
196
+ ...(req.imagePaths?.length ? { imagePaths: req.imagePaths } : {}),
197
+ });
198
+
199
+ return {
200
+ events: withTimeout(events),
201
+ cleanup: async () => {
202
+ try {
203
+ // abortQuery is what kills the runtime; deleteSession alone may only drop
204
+ // the record, depending on the provider.
205
+ try { provider.abortQuery?.(session.id, "proxy"); } catch { /* best effort */ }
206
+ try { await provider.deleteSession(session.id); } catch { /* best effort */ }
207
+ } finally {
208
+ release();
209
+ }
210
+ },
211
+ };
212
+ } catch (e) {
213
+ release();
214
+ throw e;
215
+ }
156
216
  }
157
217
 
158
218
  /** `<provider>/v1/models` in OpenAI's list shape, so clients can discover models. */
@@ -21,7 +21,7 @@ import { mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, wr
21
21
  import { tmpdir } from "node:os";
22
22
  import { join, extname } from "node:path";
23
23
  import { resolveCodexAccountForSession } from "./codex-account.service.ts";
24
- import { startAgentTurn, resolveProvider } from "./proxy-agent-turn.ts";
24
+ import { startAgentTurn, resolveProvider, turnFailureResponse } from "./proxy-agent-turn.ts";
25
25
  import { openAiError } from "./proxy-openai-format.ts";
26
26
 
27
27
  /** Providers whose agent can actually produce an image. */
@@ -149,7 +149,7 @@ export async function forwardImageGeneration(providerId: string, body: ImageGene
149
149
  const produced = await runImageTurn(providerId, generationPrompt(body), body.model, []);
150
150
  return imagesResponse(produced.slice(0, Math.min(Math.max(body.n ?? 1, 1), 4)));
151
151
  } catch (e) {
152
- return openAiError(502, (e as Error).message);
152
+ return turnFailureResponse(e, openAiError);
153
153
  }
154
154
  }
155
155
 
@@ -179,7 +179,7 @@ export async function forwardImageEdit(providerId: string, body: ImageEditBody):
179
179
  const produced = await runImageTurn(providerId, prompt, body.model, [path]);
180
180
  return imagesResponse(produced.slice(0, Math.min(Math.max(body.n ?? 1, 1), 4)));
181
181
  } catch (e) {
182
- return openAiError(502, (e as Error).message);
182
+ return turnFailureResponse(e, openAiError);
183
183
  } finally {
184
184
  try { rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ }
185
185
  }
@@ -198,7 +198,7 @@ class ProxyService {
198
198
  insertProxyRequest({
199
199
  endpoint, model: parsed.model, accountLabel: providerId,
200
200
  callerIp: caller?.callerIp, callerUa: caller?.callerUa,
201
- status: response.ok ? "success" : "error", durationMs,
201
+ status: response.ok ? "success" : response.status === 429 ? "rate_limited" : "error", durationMs,
202
202
  });
203
203
  console.log(`[proxy] POST ${endpoint} → ${response.status} ${durationMs}ms caller=${caller?.callerIp ?? "unknown"}`);
204
204
  return response;
@@ -25,7 +25,8 @@ import { codexPlanLabel } from "../../../shared/codex-plan-label.ts";
25
25
  import { dailyGuardState } from "../../../shared/codex-daily-guard.ts";
26
26
  import { UsagePanelShell } from "./usage-panel-shell";
27
27
 
28
- interface CodexAccount { id: string; label: string; type: string; planType?: string | null; status?: "active" | "disabled"; dailyGuardEnabled?: boolean }
28
+ import { CODEX_SIGNED_OUT_HINT, type CodexAccount } from "@/components/settings/accounts/use-codex-accounts";
29
+ import { CodexSignInAgainDialog } from "@/components/settings/accounts/codex-sign-in-again-dialog";
29
30
  type Usage = Pick<UsageInfo, "fiveHour" | "sevenDay" | "session" | "weekly">;
30
31
 
31
32
  /** Matches the whole-percent figure the bars show, so a refusal agrees with the card. */
@@ -66,6 +67,8 @@ export function CodexUsagePanel({ onClose, usage, onReload, onSelectAccount, sel
66
67
  const [selectingId, setSelectingId] = useState<string | null>(null);
67
68
  const [togglingId, setTogglingId] = useState<string | null>(null);
68
69
  const [panelError, setPanelError] = useState<string | null>(null);
70
+ /** The signed-out account whose "Sign in again" chip was pressed. */
71
+ const [signInAgain, setSignInAgain] = useState<CodexAccount | null>(null);
69
72
 
70
73
  const load = useCallback(async () => {
71
74
  setLoading(true);
@@ -98,6 +101,7 @@ export function CodexUsagePanel({ onClose, usage, onReload, onSelectAccount, sel
98
101
  */
99
102
  const unselectableReason = useCallback((account: CodexAccount, u: Usage): string | null => {
100
103
  if (account.status === "disabled") return "Disabled";
104
+ if (account.signedOut) return "Signed out — sign in again first";
101
105
  if (atCap(u.fiveHour)) return `5-hour limit reached${u.session?.resetsAt ? ` — resets ${formatResetTime(u.session)}` : ""}`;
102
106
  if (atCap(u.sevenDay)) return `Weekly limit reached${u.weekly?.resetsAt ? ` — resets ${formatResetTime(u.weekly)}` : ""}`;
103
107
  return null;
@@ -175,6 +179,7 @@ export function CodexUsagePanel({ onClose, usage, onReload, onSelectAccount, sel
175
179
  );
176
180
 
177
181
  return (
182
+ <>
178
183
  <UsagePanelShell
179
184
  title="Codex Usage"
180
185
  onClose={onClose}
@@ -205,9 +210,16 @@ export function CodexUsagePanel({ onClose, usage, onReload, onSelectAccount, sel
205
210
  dailyGuard={guard ? { enabled: !!a.dailyGuardEnabled, state: guard } : undefined}
206
211
  onDailyGuardToggle={guard ? () => void handleDailyGuardToggle(a.id, !!a.dailyGuardEnabled) : undefined}
207
212
  dailyGuardToggling={togglingId === a.id}
213
+ reauthHint={a.signedOut ? CODEX_SIGNED_OUT_HINT : undefined}
214
+ // Only a ChatGPT sign-in can be renewed in place; a dead API key is replaced.
215
+ onReauth={a.type === "chatgpt" ? () => setSignInAgain(a) : undefined}
208
216
  />
209
217
  );
210
218
  })}
211
219
  </UsagePanelShell>
220
+ {signInAgain && (
221
+ <CodexSignInAgainDialog account={signInAgain} onClose={() => setSignInAgain(null)} onDone={() => void load()} />
222
+ )}
223
+ </>
212
224
  );
213
225
  }
@@ -25,6 +25,7 @@ import { AccountBucketRow } from "@/components/settings/accounts/account-bucket-
25
25
  import { useAccountsData } from "@/components/settings/accounts/use-accounts-data";
26
26
  import { formatLastUpdated, formatResetTime, pctColor } from "@/components/settings/accounts/account-usage-format";
27
27
  import { UsagePanelShell } from "./usage-panel-shell";
28
+ import { AddAccountDialog } from "@/components/settings/accounts/account-add-dialog";
28
29
 
29
30
  /** Matches the whole-percent figure the usage bars show, so a refusal agrees with the card. */
30
31
  function atCap(util: number | null | undefined): boolean {
@@ -91,6 +92,8 @@ export function UsageDetailPanel({
91
92
  // One strip for anything that went wrong in this panel — a refused switch or a refused
92
93
  // toggle. Two separate messages in two places would be harder to notice, not clearer.
93
94
  const [panelError, setPanelError] = useState<string | null>(null);
95
+ /** Label of the account whose "Sign in again" chip was pressed; opens the sign-in dialog. */
96
+ const [signInAgainFor, setSignInAgainFor] = useState<string | null>(null);
94
97
 
95
98
  if (!visible) return null;
96
99
 
@@ -159,6 +162,7 @@ export function UsageDetailPanel({
159
162
  const hasCost = usage.queryCostUsd != null || usage.totalCostUsd != null;
160
163
 
161
164
  return (
165
+ <>
162
166
  <UsagePanelShell
163
167
  title="Usage"
164
168
  meta={lastFetchedAt && (
@@ -220,9 +224,19 @@ export function UsageDetailPanel({
220
224
  selecting={selectingId === entry.accountId}
221
225
  onToggle={handleToggle}
222
226
  toggling={togglingId === entry.accountId}
227
+ onReauth={() => setSignInAgainFor(entry.accountLabel ?? entry.accountId.slice(0, 8))}
223
228
  />
224
229
  );
225
230
  })}
226
231
  </UsagePanelShell>
232
+ {signInAgainFor && (
233
+ <AddAccountDialog
234
+ open
235
+ onOpenChange={(v) => { if (!v) setSignInAgainFor(null); }}
236
+ onSuccess={(msg) => { setSignInAgainFor(null); void reload(); if (msg) setPanelError(msg); }}
237
+ signInAgainFor={signInAgainFor}
238
+ />
239
+ )}
240
+ </>
227
241
  );
228
242
  }
@@ -12,9 +12,14 @@ interface AddAccountDialogProps {
12
12
  open: boolean;
13
13
  onOpenChange: (open: boolean) => void;
14
14
  onSuccess: (msg?: string) => void;
15
+ /**
16
+ * The label of an account being signed in again. Only the wording changes: the add flow
17
+ * already updates an account it matches by email or profile id, clearing its re-auth flag.
18
+ */
19
+ signInAgainFor?: string;
15
20
  }
16
21
 
17
- export function AddAccountDialog({ open, onOpenChange, onSuccess }: AddAccountDialogProps) {
22
+ export function AddAccountDialog({ open, onOpenChange, onSuccess, signInAgainFor }: AddAccountDialogProps) {
18
23
  const [newToken, setNewToken] = useState("");
19
24
  const [newLabel, setNewLabel] = useState("");
20
25
  const [adding, setAdding] = useState(false);
@@ -100,9 +105,11 @@ export function AddAccountDialog({ open, onOpenChange, onSuccess }: AddAccountDi
100
105
  <Dialog open={open} onOpenChange={(v) => { if (!v) handleClose(); }}>
101
106
  <DialogContent className="sm:max-w-md">
102
107
  <DialogHeader>
103
- <DialogTitle className="text-sm">Add Claude Account</DialogTitle>
108
+ <DialogTitle className="text-sm">{signInAgainFor ? `Sign in again — ${signInAgainFor}` : "Add Claude Account"}</DialogTitle>
104
109
  <DialogDescription className="text-xs leading-relaxed">
105
- Connect via OAuth (recommended) or paste a token manually.
110
+ {signInAgainFor
111
+ ? "Log in with the same Claude account. PPM matches it by email and renews the existing card instead of adding a new one."
112
+ : "Connect via OAuth (recommended) or paste a token manually."}
106
113
  </DialogDescription>
107
114
  </DialogHeader>
108
115
  <div className="space-y-3">
@@ -60,6 +60,15 @@ export interface AccountCardProps {
60
60
  dailyGuard?: { enabled: boolean; state: DailyGuardState };
61
61
  onDailyGuardToggle?: () => void;
62
62
  dailyGuardToggling?: boolean;
63
+ /**
64
+ * The server has seen this account refused for a revoked or expired login, explained in
65
+ * the provider's own terms. For accounts with no `accountInfo` to carry `reauthRequired`
66
+ * (Codex): its usage bars still show the last reading, so without this the card looked
67
+ * healthy while every turn on it failed.
68
+ */
69
+ reauthHint?: string;
70
+ /** Start signing this account in again. Makes the "Sign in again"/"Expired" chip a button. */
71
+ onReauth?: (id: string) => void;
63
72
  }
64
73
 
65
74
  // Fixed widths so a row scrolls instead of squeezing. Two of them: a read-only card holds a
@@ -70,7 +79,7 @@ const STRIP_WIDTH = { readOnly: "min-w-[220px]", withActions: "min-w-[300px]" }
70
79
  export function AccountCard({
71
80
  entry, isActive, accountInfo, onToggle, toggling, onDelete, onExport, onViewProfile, flash,
72
81
  onSelect, unselectableReason, selecting, planLabel, dailyGuard, onDailyGuardToggle, dailyGuardToggling,
73
- layout = "list",
82
+ reauthHint, onReauth, layout = "list",
74
83
  }: AccountCardProps) {
75
84
  const { usage } = entry;
76
85
  const hasBuckets = usage.session || usage.weekly || usage.weeklyOpus || usage.weeklySonnet;
@@ -83,7 +92,7 @@ export function AccountCard({
83
92
  // Distinct from isExpired: this account still holds a refresh token, it is just one the
84
93
  // server will not honour. Dim it like an expired card, but keep every control — signing
85
94
  // in again goes through the same add flow, and delete has to stay reachable.
86
- const needsReauth = !!accountInfo?.reauthRequired;
95
+ const needsReauth = !!accountInfo?.reauthRequired || !!reauthHint;
87
96
  // A sign-in dies on a fixed schedule regardless of use, so the warning has to lead the
88
97
  // last stretch of it rather than appear once it is already too late.
89
98
  const grantExpiresAtMs = accountInfo?.grantExpiresAt ? accountInfo.grantExpiresAt * 1000 : null;
@@ -166,7 +175,8 @@ export function AccountCard({
166
175
  {isExpired && (
167
176
  <AccountHint
168
177
  className="text-[10px] text-error shrink-0 font-medium"
169
- hint="This token has expired and carries no refresh token, so nothing can renew it. Add the account again."
178
+ hint="This token has expired and carries no refresh token, so nothing can renew it. Sign in again to replace it."
179
+ onClick={onReauth ? () => onReauth(entry.accountId) : undefined}
170
180
  >
171
181
  Expired
172
182
  </AccountHint>
@@ -174,7 +184,8 @@ export function AccountCard({
174
184
  {needsReauth && !isExpired && (
175
185
  <AccountHint
176
186
  className="text-[10px] text-error shrink-0 font-medium"
177
- hint="Anthropic rejected this account's refresh token, so no turn can run on it. Signing in again is the only thing that restores it."
187
+ hint={reauthHint ?? "Anthropic rejected this account's refresh token, so no turn can run on it. Signing in again is the only thing that restores it."}
188
+ onClick={onReauth ? () => onReauth(entry.accountId) : undefined}
178
189
  >
179
190
  Sign in again
180
191
  </AccountHint>
@@ -15,13 +15,30 @@ export interface AccountHintProps {
15
15
  hint: ReactNode;
16
16
  className?: string;
17
17
  children: ReactNode;
18
+ /**
19
+ * Makes the chip an action as well as an explanation — "Sign in again" should do what it
20
+ * says rather than only describe it. The tooltip still opens on hover/focus.
21
+ */
22
+ onClick?: () => void;
18
23
  }
19
24
 
20
- export function AccountHint({ hint, className, children }: AccountHintProps) {
25
+ export function AccountHint({ hint, className, children, onClick }: AccountHintProps) {
21
26
  return (
22
27
  <Tooltip>
23
28
  <TooltipTrigger asChild>
24
- <button type="button" className={["cursor-help text-left", className].filter(Boolean).join(" ")}>
29
+ <button
30
+ type="button"
31
+ onClick={onClick}
32
+ className={[
33
+ // The invisible `after` box stretches the touch target to ~44px without growing
34
+ // the card header the chip sits in.
35
+ onClick
36
+ ? "relative cursor-pointer rounded px-1 -mx-1 underline decoration-dotted underline-offset-2 hover:bg-error/10 after:absolute after:-inset-x-2 after:-inset-y-3 after:content-['']"
37
+ : "cursor-help",
38
+ "text-left",
39
+ className,
40
+ ].filter(Boolean).join(" ")}
41
+ >
25
42
  {children}
26
43
  </button>
27
44
  </TooltipTrigger>
@@ -40,6 +40,8 @@ export function ClaudeAccountsSection() {
40
40
  const [profileView, setProfileView] = useState<{ profile: OAuthProfileData; accountId: string } | null>(null);
41
41
  const [exportPreselect, setExportPreselect] = useState<string | null>(null);
42
42
  const [dialog, setDialog] = useState<"add" | "export" | "import" | "rotation" | "token-test" | null>(null);
43
+ /** Label of the account whose "Sign in again" chip opened the add dialog. */
44
+ const [signInAgainFor, setSignInAgainFor] = useState<string | undefined>(undefined);
43
45
 
44
46
  function handleSuccess(msg?: string) {
45
47
  void reload();
@@ -84,7 +86,7 @@ export function ClaudeAccountsSection() {
84
86
  refreshing={refreshing}
85
87
  disabled={initialLoading}
86
88
  actions={<>
87
- <Button size="sm" className="cursor-pointer gap-1.5" onClick={() => setDialog("add")}>
89
+ <Button size="sm" className="cursor-pointer gap-1.5" onClick={() => { setSignInAgainFor(undefined); setDialog("add"); }}>
88
90
  <Plus className="size-4" /> Add account
89
91
  </Button>
90
92
  <Button size="sm" variant="outline" className="cursor-pointer gap-1.5" onClick={() => { setExportPreselect(null); setDialog("export"); }}>
@@ -138,6 +140,7 @@ export function ClaudeAccountsSection() {
138
140
  onDelete={(id, display) => setDeleteTarget({ id, display })}
139
141
  onExport={(id) => { setExportPreselect(id); setDialog("export"); }}
140
142
  onViewProfile={(profile, accountId) => setProfileView({ profile, accountId })}
143
+ onReauth={() => { setSignInAgainFor(entry.accountLabel ?? entry.accountId.slice(0, 8)); setDialog("add"); }}
141
144
  flash={flashIds.has(entry.accountId)}
142
145
  />
143
146
  ))}
@@ -161,7 +164,12 @@ export function ClaudeAccountsSection() {
161
164
  />
162
165
  )}
163
166
 
164
- <AddAccountDialog open={dialog === "add"} onOpenChange={(v) => setDialog(v ? "add" : null)} onSuccess={handleSuccess} />
167
+ <AddAccountDialog
168
+ open={dialog === "add"}
169
+ onOpenChange={(v) => { setDialog(v ? "add" : null); if (!v) setSignInAgainFor(undefined); }}
170
+ onSuccess={handleSuccess}
171
+ signInAgainFor={signInAgainFor}
172
+ />
165
173
  <ExportAccountsDialog
166
174
  open={dialog === "export"}
167
175
  onOpenChange={(v) => { setDialog(v ? "export" : null); if (!v) setExportPreselect(null); }}
@@ -25,7 +25,9 @@ import { CodexAddAccountDialog } from "./codex-add-account-dialog";
25
25
  import { CodexBackupDialog } from "./codex-backup-dialog";
26
26
  import { CodexRotationDialog } from "./codex-rotation-dialog";
27
27
  import { CodexUsageRows } from "./codex-usage-rows";
28
- import { useCodexAccounts } from "./use-codex-accounts";
28
+ import { CODEX_SIGNED_OUT_HINT, useCodexAccounts, type CodexAccount } from "./use-codex-accounts";
29
+ import { CodexSignInAgainDialog } from "./codex-sign-in-again-dialog";
30
+ import { AccountHint } from "./account-hint";
29
31
  import { codexPlanLabel } from "../../../../shared/codex-plan-label.ts";
30
32
  import { dailyGuardState } from "../../../../shared/codex-daily-guard.ts";
31
33
 
@@ -34,6 +36,8 @@ import { dailyGuardState } from "../../../../shared/codex-daily-guard.ts";
34
36
  export function CodexAccountsSection() {
35
37
  const [dialog, setDialog] = useState<"add" | "export" | "import" | "rotation" | null>(null);
36
38
  const c = useCodexAccounts(() => setDialog(null));
39
+ /** The signed-out account whose "Sign in again" chip was pressed. */
40
+ const [signInAgain, setSignInAgain] = useState<CodexAccount | null>(null);
37
41
 
38
42
  return (
39
43
  <div className="space-y-4">
@@ -88,12 +92,21 @@ export function CodexAccountsSection() {
88
92
  key={a.id}
89
93
  dense
90
94
  // Matches the Claude card that also carries a control beside the name.
91
- className="min-w-[300px] shrink-0 snap-start"
95
+ className={`min-w-[300px] shrink-0 snap-start${a.signedOut ? " opacity-60" : ""}`}
92
96
  data-testid="account-card"
93
97
  data-account-id={a.id}
94
98
  >
95
99
  <div className="flex items-center gap-2">
96
100
  <span className="text-sm font-medium truncate flex-1 min-w-0">{a.label}</span>
101
+ {a.signedOut && (
102
+ <AccountHint
103
+ className="text-[10px] text-error shrink-0 font-medium"
104
+ hint={CODEX_SIGNED_OUT_HINT}
105
+ onClick={a.type === "chatgpt" ? () => setSignInAgain(a) : undefined}
106
+ >
107
+ Sign in again
108
+ </AccountHint>
109
+ )}
97
110
  <span className="text-[10px] uppercase tracking-wide text-text-subtle border border-border rounded px-1 shrink-0">
98
111
  {a.type}
99
112
  </span>
@@ -199,6 +212,9 @@ export function CodexAccountsSection() {
199
212
  onImport={(f) => void c.doImport(f)}
200
213
  error={c.err}
201
214
  />
215
+ {signInAgain && (
216
+ <CodexSignInAgainDialog account={signInAgain} onClose={() => setSignInAgain(null)} onDone={() => void c.load()} />
217
+ )}
202
218
  <CodexRotationDialog
203
219
  open={dialog === "rotation"}
204
220
  onOpenChange={(v) => setDialog(v ? "rotation" : null)}