@hienlh/ppm 0.19.12 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (535) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/CLAUDE.md +8 -1
  3. package/assets/skills/ppm/SKILL.md +1 -1
  4. package/assets/skills/ppm/references/http-api.md +4 -1
  5. package/dist/web/assets/accounts-settings-section-BQyru31C.js +1 -0
  6. package/dist/web/assets/{ai-resource-editor-CqL-X2zu.js → ai-resource-editor-BgYIPGVP.js} +1 -1
  7. package/dist/web/assets/{ai-settings-section-CcqokdzF.js → ai-settings-section-Cywmtnps.js} +1 -1
  8. package/dist/web/assets/ai-settings-section-iBRCQvBR.js +1 -0
  9. package/dist/web/assets/{angular-html-C8nsn2VC.js → angular-html-BtJLNcMw.js} +1 -1
  10. package/dist/web/assets/{angular-ts-Caw9nRCH.js → angular-ts-CQiu5F_W.js} +1 -1
  11. package/dist/web/assets/api-client-QfreGirP.js +1 -0
  12. package/dist/web/assets/api-settings-D18Q4Ocp.js +1 -0
  13. package/dist/web/assets/{api-themes-BoOJPqwM.js → api-themes-sSv-6OLk.js} +1 -1
  14. package/dist/web/assets/{apl-Dhu1LORF.js → apl-CKZvgs-w.js} +1 -1
  15. package/dist/web/assets/{appearance-settings-section-2hZBnpPS.js → appearance-settings-section-mhu9T6Fk.js} +1 -1
  16. package/dist/web/assets/architecture-PBZL5I3N-DfR8k8Bc.js +1 -0
  17. package/dist/web/assets/{astro-DwkX9sLZ.js → astro-D0b8ssLD.js} +1 -1
  18. package/dist/web/assets/{audio-preview-CkGZO8Uy.js → audio-preview-B4E5_7TW.js} +1 -1
  19. package/dist/web/assets/{blade-D1fC9Osx.js → blade-DOIpINLw.js} +1 -1
  20. package/dist/web/assets/{bsl-pur-PRip.js → bsl-CJ7_JlQ8.js} +1 -1
  21. package/dist/web/assets/{button-2WCAS8dI.js → button-DX32ffge.js} +1 -1
  22. package/dist/web/assets/{cairo-DDJs9amR.js → cairo-5CpV2T1P.js} +1 -1
  23. package/dist/web/assets/chat-tab-Bs9OD6ss.js +15 -0
  24. package/dist/web/assets/{cobol-Bo0usdoM.js → cobol-BreMVrtc.js} +1 -1
  25. package/dist/web/assets/code-editor-Cv_pzHo7.js +7 -0
  26. package/dist/web/assets/{coffee-DBBsbX1e.js → coffee-D7DoKh5b.js} +1 -1
  27. package/dist/web/assets/{conflict-editor-NqFAJP47.js → conflict-editor-BnWaMSEa.js} +1 -1
  28. package/dist/web/assets/{context-menu-C13b1uT3.js → context-menu-CJL8L98s.js} +1 -1
  29. package/dist/web/assets/{cpp-6CTO78A-.js → cpp-BnWeWAoE.js} +1 -1
  30. package/dist/web/assets/{crystal-DAlvpAP7.js → crystal-D2pJbFUn.js} +1 -1
  31. package/dist/web/assets/{csv-preview-Dvriy18_.js → csv-preview-DG4YAabr.js} +1 -1
  32. package/dist/web/assets/{data-grid-overlay-editor-iRrpAV0e.js → data-grid-overlay-editor-CbBkhhJa.js} +1 -1
  33. package/dist/web/assets/{database-viewer-Bs2RMm5a.js → database-viewer-B-wA2qT-.js} +1 -1
  34. package/dist/web/assets/diff-viewer-DRtzdfXk.js +4 -0
  35. package/dist/web/assets/{docx-preview-Bgw838ry.js → docx-preview-Xy7SZ7Vf.js} +1 -1
  36. package/dist/web/assets/{edge-D0tZrpC7.js → edge-aqGMtbY3.js} +1 -1
  37. package/dist/web/assets/{edit-diff-preview-DUgmjNre.js → edit-diff-preview-6tzdk7Pn.js} +1 -1
  38. package/dist/web/assets/{elixir-C1E6-o4G.js → elixir-CsYMBLc5.js} +1 -1
  39. package/dist/web/assets/{elm-BKyTfeQX.js → elm-BU8C2gFp.js} +1 -1
  40. package/dist/web/assets/{erb-Cdnr4BMI.js → erb-Z_E49ab8.js} +1 -1
  41. package/dist/web/assets/{erlang-N1237mFq.js → erlang-BRqOjXH9.js} +1 -1
  42. package/dist/web/assets/{esm-ClNnmD9t.js → esm-B-vXxHUd.js} +1 -1
  43. package/dist/web/assets/{explorer-body-DfWunSf9.js → explorer-body-uh8oJp6J.js} +2 -2
  44. package/dist/web/assets/explorer-window-content-DLn_4egF.js +1 -0
  45. package/dist/web/assets/{extension-manager-section-Zc9yag2c.js → extension-manager-section-C_ZGYvWf.js} +1 -1
  46. package/dist/web/assets/{extension-webview-B5FayDPz.js → extension-webview-DbFWxJpj.js} +1 -1
  47. package/dist/web/assets/{files-settings-section-CxAtO3GL.js → files-settings-section-BrUbJglA.js} +1 -1
  48. package/dist/web/assets/{fortran-fixed-form-D_f9tVv0.js → fortran-fixed-form-CWbxmDGR.js} +1 -1
  49. package/dist/web/assets/{fsharp-DEJmdhc2.js → fsharp-DKMYbq6I.js} +1 -1
  50. package/dist/web/assets/{gdresource-Dmmrx2gP.js → gdresource-B3yFau4V.js} +1 -1
  51. package/dist/web/assets/{general-settings-section-9DfKrBns.js → general-settings-section-B9ZPZVso.js} +1 -1
  52. package/dist/web/assets/{git-commit-qwJ867ig.js → git-commit-BW0p3Ekk.js} +1 -1
  53. package/dist/web/assets/{git-log-panel-BsmDZurR.js → git-log-panel-C0jnhmgC.js} +1 -1
  54. package/dist/web/assets/{git-rebase-Dkv8Zw86.js → git-rebase-DrvmQCS7.js} +1 -1
  55. package/dist/web/assets/gitGraph-HDMCJU4V-CPd78aaT.js +1 -0
  56. package/dist/web/assets/{glide-data-grid-BuYwXIoS.js → glide-data-grid-CZVKIum-.js} +6 -6
  57. package/dist/web/assets/{glimmer-js-CcrXn8lQ.js → glimmer-js-BY1XApJJ.js} +1 -1
  58. package/dist/web/assets/{glimmer-ts-CLab3lU7.js → glimmer-ts-DPZtxF8N.js} +1 -1
  59. package/dist/web/assets/{glsl-DXhkG9dU.js → glsl-DZ8G_Hvf.js} +1 -1
  60. package/dist/web/assets/{graphql-Cuqsx8r3.js → graphql-Ck06uCZy.js} +1 -1
  61. package/dist/web/assets/group-chat-tab-DnPtmbPc.js +1 -0
  62. package/dist/web/assets/{hack-C_CBVdBc.js → hack-pLogDwAs.js} +1 -1
  63. package/dist/web/assets/{haml-CBWfzZpv.js → haml-CBqeCQ2A.js} +1 -1
  64. package/dist/web/assets/{handlebars-4e8OR-VI.js → handlebars-B66JmVtp.js} +1 -1
  65. package/dist/web/assets/{html-C5X4urn_.js → html-Dk4wmfwC.js} +1 -1
  66. package/dist/web/assets/{html-derivative-nH-jXfsL.js → html-derivative-CJWqfN51.js} +1 -1
  67. package/dist/web/assets/{http-CQgaG7eu.js → http-CexhotTh.js} +1 -1
  68. package/dist/web/assets/{hurl-B8gidJhu.js → hurl-BScKFk3Z.js} +1 -1
  69. package/dist/web/assets/{hxml-BAK94GX8.js → hxml-DAle7vtu.js} +1 -1
  70. package/dist/web/assets/{image-preview-1kGj4W9R.js → image-preview-fuU4h2nr.js} +1 -1
  71. package/dist/web/assets/index-CHQAPZIX.js +52 -0
  72. package/dist/web/assets/index-CoaVwAF6.css +2 -0
  73. package/dist/web/assets/info-3K5VOQVL-BciNds6R.js +1 -0
  74. package/dist/web/assets/{jinja-BuAjvj8M.js → jinja-yHpyD80t.js} +1 -1
  75. package/dist/web/assets/{jira-watcher-section-rJVz40wR.js → jira-watcher-section-Bjldyhru.js} +1 -1
  76. package/dist/web/assets/{jison-Dhhs26-L.js → jison-BY6WLJVu.js} +1 -1
  77. package/dist/web/assets/{julia-gZavIyll.js → julia-Dd_iYQDT.js} +1 -1
  78. package/dist/web/assets/{just-DDHrFReO.js → just-BTuUKpxF.js} +1 -1
  79. package/dist/web/assets/keybindings-store-DBVXZKp8.js +1 -0
  80. package/dist/web/assets/{keyboard-shortcuts-section-Dv4e3M8h.js → keyboard-shortcuts-section-D7jaCPrv.js} +1 -1
  81. package/dist/web/assets/{latex-BdGZa8ov.js → latex-Dt3OUlMZ.js} +1 -1
  82. package/dist/web/assets/{liquid-urFfd9Fr.js → liquid-BxPJU2ix.js} +1 -1
  83. package/dist/web/assets/{lua-CcS6tnB5.js → lua-DEGIvn7d.js} +1 -1
  84. package/dist/web/assets/{markdown-renderer-CEloCR06.js → markdown-renderer-B4B5oxQS.js} +3 -3
  85. package/dist/web/assets/markdown-renderer-DwHh-jd_.js +1 -0
  86. package/dist/web/assets/{marko-CSQqk4oG.js → marko-BDxwBJur.js} +1 -1
  87. package/dist/web/assets/{mdc-CYIe80m_.js → mdc-B8aXPTg7.js} +1 -1
  88. package/dist/web/assets/{media-url-DBmHJRft.js → media-url-BMeBaB0q.js} +1 -1
  89. package/dist/web/assets/{mobile-bottom-sheet-B3uDlD79.js → mobile-bottom-sheet-CmySDB6I.js} +1 -1
  90. package/dist/web/assets/mobile-explorer-sheet-JJxZYWkV.js +1 -0
  91. package/dist/web/assets/{nextflow-C-W8vX8I.js → nextflow-DBVZnnT_.js} +1 -1
  92. package/dist/web/assets/{nginx-COGi5you.js → nginx-Crpyd-Do.js} +1 -1
  93. package/dist/web/assets/{nim-CPitYGel.js → nim-BSqTBYDj.js} +1 -1
  94. package/dist/web/assets/notification-store-CCWNUHsS.js +1 -0
  95. package/dist/web/assets/{notifications-settings-section-B2GkfB0l.js → notifications-settings-section-DbbX7-Qp.js} +1 -1
  96. package/dist/web/assets/{number-overlay-editor-C_eD-Bdr.js → number-overlay-editor-DRjTIcPH.js} +1 -1
  97. package/dist/web/assets/{overview-panel-Cm2HRtEz.js → overview-panel-DiiA3B_Z.js} +1 -1
  98. package/dist/web/assets/packet-RMMSAZCW-ClD3l2Bs.js +1 -0
  99. package/dist/web/assets/{panel-store-BRrnDO_i.js → panel-store-BmdcSBNb.js} +1 -1
  100. package/dist/web/assets/{pdf-preview-Dmz8jkZZ.js → pdf-preview-DxNjWa1v.js} +1 -1
  101. package/dist/web/assets/{perl-B0yW3hbb.js → perl-BU0kbB-K.js} +1 -1
  102. package/dist/web/assets/{php-BNMYN5Jp.js → php-D9n1ikHN.js} +1 -1
  103. package/dist/web/assets/pie-UPGHQEXC-PU6kD0rW.js +1 -0
  104. package/dist/web/assets/{postgres-viewer-BtZbfC3f.js → postgres-viewer-BScvk5ev.js} +3 -3
  105. package/dist/web/assets/{ppmbot-settings-section-OYfVG5-l.js → ppmbot-settings-section-jFA9Ncgo.js} +1 -1
  106. package/dist/web/assets/{process-table-DRu3NyO_.js → process-table-BexeYWQm.js} +1 -1
  107. package/dist/web/assets/{project-store-BuY7rxn_.js → project-store-wWZ6bjwq.js} +1 -1
  108. package/dist/web/assets/{proxy-settings-section-xMbrcyr8.js → proxy-settings-section-C_seKq56.js} +1 -1
  109. package/dist/web/assets/{pug-CP5fxsxD.js → pug-BOpaczDr.js} +1 -1
  110. package/dist/web/assets/{qml-CHGpQosY.js → qml-B-cQXhrg.js} +1 -1
  111. package/dist/web/assets/{query-audit-section-CC5Mf6qp.js → query-audit-section-BWXJmPDV.js} +1 -1
  112. package/dist/web/assets/radar-KQ55EAFF-4LzjL0Zt.js +1 -0
  113. package/dist/web/assets/{razor-Cfe0AYo4.js → razor-DFB0pNCh.js} +1 -1
  114. package/dist/web/assets/remote-desktop-mobile-sheet-B--PeL5i.js +2 -0
  115. package/dist/web/assets/{remote-desktop-mobile-view-B4BSHIxU.js → remote-desktop-mobile-view-jgn8nmQa.js} +1 -1
  116. package/dist/web/assets/{remote-desktop-readiness-gate-C0d0PSX4.js → remote-desktop-readiness-gate-CjDth-W_.js} +1 -1
  117. package/dist/web/assets/{remote-desktop-window-content-DjeCZzqq.js → remote-desktop-window-content-CrHAUA03.js} +1 -1
  118. package/dist/web/assets/{rst-DxSSGshb.js → rst-DAXedsgm.js} +1 -1
  119. package/dist/web/assets/{ruby-rbK5SsJa.js → ruby-BCO4polC.js} +1 -1
  120. package/dist/web/assets/{run-in-terminal-DHAvC1AG.js → run-in-terminal-wHKwAnWP.js} +1 -1
  121. package/dist/web/assets/{sas-DDM28fbb.js → sas-DfB5dcFC.js} +1 -1
  122. package/dist/web/assets/schedules-settings-section-DMFn3mYL.js +1 -0
  123. package/dist/web/assets/{scss-TDl-6ro4.js → scss-BknIVgn7.js} +1 -1
  124. package/dist/web/assets/{send-to-chat-BOyUWNKw.js → send-to-chat-BI7-fFpG.js} +1 -1
  125. package/dist/web/assets/{separator-BzGoTlR7.js → separator-W_hJcXPk.js} +1 -1
  126. package/dist/web/assets/{settings-body-CXTQwhbM.js → settings-body-Dz2vPTEP.js} +2 -2
  127. package/dist/web/assets/settings-store-CzLnTR8p.js +2 -0
  128. package/dist/web/assets/settings-tab-B5QbsvS4.js +1 -0
  129. package/dist/web/assets/settings-window-content-iM2bPoX5.js +1 -0
  130. package/dist/web/assets/{shaderlab-CCvmDgGS.js → shaderlab-CPu31TpY.js} +1 -1
  131. package/dist/web/assets/{shellsession-BzKS9yKh.js → shellsession-CFX4c_TM.js} +1 -1
  132. package/dist/web/assets/{soy-QfO35nDh.js → soy-rz9hilgm.js} +1 -1
  133. package/dist/web/assets/{sparql-D29RDfp2.js → sparql-Bo8ZDeqE.js} +1 -1
  134. package/dist/web/assets/{sql-query-editor-B-VwfyTf.js → sql-query-editor-C0mGgLfX.js} +1 -1
  135. package/dist/web/assets/{sqlite-viewer-CpUhDEhN.js → sqlite-viewer-Bs3RKf1X.js} +1 -1
  136. package/dist/web/assets/{stata-Be6j_SNv.js → stata-B5J-QMyE.js} +1 -1
  137. package/dist/web/assets/{streaming-store-9_9j70zt.js → streaming-store-9XVmAMfV.js} +1 -1
  138. package/dist/web/assets/{surrealql-CykFjgNG.js → surrealql-Caw1ADnG.js} +1 -1
  139. package/dist/web/assets/{svelte-CdBF8z4-.js → svelte-D3oMVcxz.js} +1 -1
  140. package/dist/web/assets/{system-monitor-body-DzWXd1wM.js → system-monitor-body-DvUmEdNe.js} +2 -2
  141. package/dist/web/assets/system-monitor-tab-CHZ5F_ZE.js +1 -0
  142. package/dist/web/assets/{system-monitor-window-content-DIIP3dff.js → system-monitor-window-content-DjzqoeCN.js} +1 -1
  143. package/dist/web/assets/{tab-host-window-content-CLdcz84K.js → tab-host-window-content-DKeAr5mD.js} +1 -1
  144. package/dist/web/assets/{tab-store-PZsP5ZwG.js → tab-store-1xYJb0gM.js} +1 -1
  145. package/dist/web/assets/team-member-sheet-Cl9ZB3dl.js +2 -0
  146. package/dist/web/assets/team-member-window-content-DfP7WdFD.js +1 -0
  147. package/dist/web/assets/{templ-BVHE2CNb.js → templ-BAdNyIOL.js} +1 -1
  148. package/dist/web/assets/{terminal-tab-BeKLpaPM.js → terminal-tab-J1Fqa_rm.js} +2 -2
  149. package/dist/web/assets/{tex-C8XwrbBw.js → tex-IQ8mELOR.js} +1 -1
  150. package/dist/web/assets/tool-cards-DQghSuKX.js +9 -0
  151. package/dist/web/assets/treemap-KZPCXAKY-DiumLNMg.js +1 -0
  152. package/dist/web/assets/{ts-tags-BIHk5AM0.js → ts-tags-D1DDO2jl.js} +1 -1
  153. package/dist/web/assets/{twig-YcE6Q2Ox.js → twig-C8Fd5RvV.js} +1 -1
  154. package/dist/web/assets/use-accounts-data-KWcMgRlc.js +1 -0
  155. package/dist/web/assets/{use-blob-url-De-ZtcpX.js → use-blob-url-CrwoZJUJ.js} +1 -1
  156. package/dist/web/assets/use-inline-blame-C1G-_NjC.js +8 -0
  157. package/dist/web/assets/{use-monaco-theme-Kj2OGDlm.js → use-monaco-theme-aa4-wjKQ.js} +1 -1
  158. package/dist/web/assets/{use-open-team-member-DWhhWdmJ.js → use-open-team-member-CSUU7Z8N.js} +1 -1
  159. package/dist/web/assets/{use-remote-desktop-display-choice-8AkPpM_0.js → use-remote-desktop-display-choice-B-TpQ-Ua.js} +1 -1
  160. package/dist/web/assets/{use-websocket-kR1omKO1.js → use-websocket-BBslmv3D.js} +1 -1
  161. package/dist/web/assets/{vendor-mermaid-CKKtYuTV.js → vendor-mermaid-CfY1-PkA.js} +1 -1
  162. package/dist/web/assets/{video-preview-BdV7LjbZ.js → video-preview-Bd0lcK3d.js} +1 -1
  163. package/dist/web/assets/{vue-q7m01V0h.js → vue-EvccXlkP.js} +1 -1
  164. package/dist/web/assets/{vue-html-D1y9feOj.js → vue-html-Dg0xUJCh.js} +1 -1
  165. package/dist/web/assets/{vue-vine-DrIGs03v.js → vue-vine-BdrMg72k.js} +1 -1
  166. package/dist/web/assets/{window-pip-registry-CSLhJfpL.js → window-pip-registry-CrCYYTTf.js} +1 -1
  167. package/dist/web/assets/{window-store-DGKf9rPG.js → window-store-D0HbQ1pM.js} +1 -1
  168. package/dist/web/assets/ws-auth-D111jj32.js +1 -0
  169. package/dist/web/assets/{xml-oyvc1920.js → xml-MmDclzxK.js} +1 -1
  170. package/dist/web/assets/{xsl-BbIGdNPF.js → xsl-DQEFs6Dd.js} +1 -1
  171. package/dist/web/index.html +23 -23
  172. package/dist/web/sw.js +1 -1
  173. package/docs/extension-development-guide.md +47 -0
  174. package/docs/project-roadmap.md +3 -1
  175. package/package.json +2 -2
  176. package/packages/ext-git-graph/package.json +23 -5
  177. package/packages/ext-git-graph/src/blame-parser.test.ts +154 -0
  178. package/packages/ext-git-graph/src/blame-parser.ts +148 -0
  179. package/packages/ext-git-graph/src/blame-view.ts +389 -0
  180. package/packages/ext-git-graph/src/commit-search.test.ts +118 -0
  181. package/packages/ext-git-graph/src/commit-search.ts +102 -0
  182. package/packages/ext-git-graph/src/compare-args.test.ts +111 -0
  183. package/packages/ext-git-graph/src/compare-args.ts +109 -0
  184. package/packages/ext-git-graph/src/compare-view.ts +343 -0
  185. package/packages/ext-git-graph/src/extension-integration.test.ts +6 -3
  186. package/packages/ext-git-graph/src/extension.ts +202 -249
  187. package/packages/ext-git-graph/src/file-history-parser.test.ts +102 -0
  188. package/packages/ext-git-graph/src/file-history-parser.ts +89 -0
  189. package/packages/ext-git-graph/src/file-history-view.ts +386 -0
  190. package/packages/ext-git-graph/src/git-exec.ts +122 -0
  191. package/packages/ext-git-graph/src/git-state.ts +81 -0
  192. package/packages/ext-git-graph/src/panel-nav.ts +65 -0
  193. package/packages/ext-git-graph/src/panel-registry.ts +72 -0
  194. package/packages/ext-git-graph/src/ppm-api.ts +48 -0
  195. package/packages/ext-git-graph/src/rebase-todo.test.ts +196 -0
  196. package/packages/ext-git-graph/src/rebase-todo.ts +119 -0
  197. package/packages/ext-git-graph/src/rebase-view.ts +483 -0
  198. package/packages/ext-git-graph/src/reflog-parser.test.ts +93 -0
  199. package/packages/ext-git-graph/src/reflog-parser.ts +86 -0
  200. package/packages/ext-git-graph/src/reflog-view.ts +375 -0
  201. package/packages/ext-git-graph/src/register-view-command.ts +47 -0
  202. package/packages/ext-git-graph/src/submodule-parser.test.ts +79 -0
  203. package/packages/ext-git-graph/src/submodule-parser.ts +77 -0
  204. package/packages/ext-git-graph/src/types.ts +10 -1
  205. package/packages/ext-git-graph/src/webview-html.ts +438 -3
  206. package/packages/ext-git-graph/src/webview-shell.ts +236 -0
  207. package/src/providers/claude-agent-sdk.ts +7 -0
  208. package/src/providers/codex-app-server/codex-provider.ts +89 -1
  209. package/src/server/index.ts +1 -1
  210. package/src/server/routes/accounts.ts +25 -1
  211. package/src/server/routes/chat.ts +29 -1
  212. package/src/server/routes/codex-accounts.ts +41 -1
  213. package/src/server/routes/git.ts +97 -0
  214. package/src/server/routes/settings.ts +1 -0
  215. package/src/server/routes/teams.ts +14 -3
  216. package/src/services/account-selector.service.ts +125 -20
  217. package/src/services/account.service.ts +176 -54
  218. package/src/services/autostart-generator.ts +12 -1
  219. package/src/services/codex-account.service.ts +87 -13
  220. package/src/services/db.service.ts +58 -3
  221. package/src/services/extension.service.ts +55 -5
  222. package/src/services/git-blame/blame-porcelain.ts +89 -0
  223. package/src/services/git-blame/git-blame.service.ts +90 -0
  224. package/src/services/git-hunks/git-hunks.service.ts +264 -0
  225. package/src/services/git-hunks/unified-diff.ts +359 -0
  226. package/src/services/picked-account-binding.ts +57 -0
  227. package/src/services/team-member-activity/member-transcript-tail.ts +64 -0
  228. package/src/shared/blame.ts +79 -0
  229. package/src/web/components/chat/chat-history-bar.tsx +34 -5
  230. package/src/web/components/chat/chat-tab.tsx +120 -3
  231. package/src/web/components/chat/codex-usage-panel.tsx +132 -14
  232. package/src/web/components/chat/team-member-window-content.tsx +8 -39
  233. package/src/web/components/chat/usage-badge.tsx +176 -52
  234. package/src/web/components/editor/code-editor.tsx +33 -1
  235. package/src/web/components/editor/diff-viewer.tsx +83 -8
  236. package/src/web/components/editor/editor-toolbar.tsx +25 -1
  237. package/src/web/components/git/git-status-panel.tsx +46 -0
  238. package/src/web/components/git/hunk-selection.ts +132 -0
  239. package/src/web/components/git/hunk-stage-dialog.tsx +419 -0
  240. package/src/web/components/settings/accounts/account-bucket-row.tsx +19 -3
  241. package/src/web/components/settings/accounts/account-card.tsx +153 -33
  242. package/src/web/components/settings/accounts/account-hint.tsx +33 -0
  243. package/src/web/components/settings/accounts/account-usage-format.ts +26 -0
  244. package/src/web/components/settings/accounts/codex-accounts-section.tsx +24 -9
  245. package/src/web/components/settings/accounts/codex-usage-rows.tsx +2 -2
  246. package/src/web/components/settings/accounts/use-accounts-data.ts +32 -2
  247. package/src/web/components/settings/accounts/use-codex-accounts.ts +24 -2
  248. package/src/web/hooks/use-chat.ts +27 -0
  249. package/src/web/hooks/use-inline-blame.ts +175 -0
  250. package/src/web/hooks/use-member-transcript-tail.ts +95 -0
  251. package/src/web/lib/api-settings.ts +24 -0
  252. package/src/web/stores/settings-store.ts +12 -0
  253. package/dist/web/assets/accounts-settings-section-Cd1Nh04o.js +0 -1
  254. package/dist/web/assets/ai-settings-section-OaYGT5OF.js +0 -1
  255. package/dist/web/assets/api-client-BogqsFna.js +0 -1
  256. package/dist/web/assets/api-settings-BVfemzZW.js +0 -1
  257. package/dist/web/assets/architecture-PBZL5I3N-BZBxtErN.js +0 -1
  258. package/dist/web/assets/chat-tab-Cc1W2pTu.js +0 -15
  259. package/dist/web/assets/code-editor-gLRyCPWE.js +0 -7
  260. package/dist/web/assets/diff-viewer-ix1Ko0xk.js +0 -4
  261. package/dist/web/assets/explorer-window-content-B7HmteDI.js +0 -1
  262. package/dist/web/assets/gitGraph-HDMCJU4V-BHxJbXfV.js +0 -1
  263. package/dist/web/assets/group-chat-tab-DmZ1vLk-.js +0 -1
  264. package/dist/web/assets/index-N-pnuwFP.css +0 -2
  265. package/dist/web/assets/index-yV5RgBDh.js +0 -52
  266. package/dist/web/assets/info-3K5VOQVL-DI4p3vbH.js +0 -1
  267. package/dist/web/assets/keybindings-store-88_Zkn5F.js +0 -1
  268. package/dist/web/assets/markdown-renderer-UhszMWSn.js +0 -1
  269. package/dist/web/assets/mobile-explorer-sheet-D9cqcU3X.js +0 -1
  270. package/dist/web/assets/notification-store-B6X6RYqQ.js +0 -1
  271. package/dist/web/assets/packet-RMMSAZCW-YNQWTi_-.js +0 -1
  272. package/dist/web/assets/pie-UPGHQEXC-E3Wiw76n.js +0 -1
  273. package/dist/web/assets/radar-KQ55EAFF-BPQJUKON.js +0 -1
  274. package/dist/web/assets/remote-desktop-mobile-sheet-DefltAPl.js +0 -2
  275. package/dist/web/assets/schedules-settings-section-D76Ob6tI.js +0 -1
  276. package/dist/web/assets/settings-store-DWUx3865.js +0 -2
  277. package/dist/web/assets/settings-tab-DToHQXfC.js +0 -1
  278. package/dist/web/assets/settings-window-content-B2rlq-Dv.js +0 -1
  279. package/dist/web/assets/system-monitor-tab-CuDpwAZU.js +0 -1
  280. package/dist/web/assets/team-member-sheet-DLrT_Kxo.js +0 -2
  281. package/dist/web/assets/team-member-window-content-S8cdLcg3.js +0 -1
  282. package/dist/web/assets/tool-cards-CLxrQlUs.js +0 -9
  283. package/dist/web/assets/treemap-KZPCXAKY-C2CvKBQR.js +0 -1
  284. package/dist/web/assets/use-accounts-data-snYPJED_.js +0 -1
  285. package/dist/web/assets/ws-auth-ZSZkAT6p.js +0 -1
  286. /package/dist/web/assets/{abap-Cq9nYQyd.js → abap-D-o-bvtI.js} +0 -0
  287. /package/dist/web/assets/{actionscript-3-DL8rUIy0.js → actionscript-3-D2tIzAm8.js} +0 -0
  288. /package/dist/web/assets/{ada-BY8TLuhY.js → ada-DxatBcxS.js} +0 -0
  289. /package/dist/web/assets/{andromeeda-CyVfHsS0.js → andromeeda-D0LK9YJ3.js} +0 -0
  290. /package/dist/web/assets/{apache-DTQUy73x.js → apache-B7uMPNiW.js} +0 -0
  291. /package/dist/web/assets/{apex-CiSZngrZ.js → apex-B7GiXrAr.js} +0 -0
  292. /package/dist/web/assets/{api-client-oY1tC3tM.js → api-client-CEYCBt_0.js} +0 -0
  293. /package/dist/web/assets/{applescript-CzuRDd3h.js → applescript-D_SIFkKe.js} +0 -0
  294. /package/dist/web/assets/{apply-theme-r8tKDrCR.js → apply-theme-BVXAUYoN.js} +0 -0
  295. /package/dist/web/assets/{ara-CSFV93kR.js → ara-C3iECgdg.js} +0 -0
  296. /package/dist/web/assets/{asciidoc-hOQNnXhz.js → asciidoc-gc8z_MMY.js} +0 -0
  297. /package/dist/web/assets/{asm-DdG53NGR.js → asm-27qeoPOI.js} +0 -0
  298. /package/dist/web/assets/{aurora-x-DJ7ORZG6.js → aurora-x-C5J9fWe1.js} +0 -0
  299. /package/dist/web/assets/{awk-B3FFpX_J.js → awk-B7GBzF92.js} +0 -0
  300. /package/dist/web/assets/{ayu-dark-D_P1CfTe.js → ayu-dark-XlnQhuLL.js} +0 -0
  301. /package/dist/web/assets/{ayu-light-Cz3YLUqH.js → ayu-light-DKLLilkU.js} +0 -0
  302. /package/dist/web/assets/{ayu-mirage-JcMq5tgJ.js → ayu-mirage-Dv-XOprJ.js} +0 -0
  303. /package/dist/web/assets/{ballerina-7Hs4Rfpk.js → ballerina-DACRT-sT.js} +0 -0
  304. /package/dist/web/assets/{bat-COgQdnFA.js → bat-BlQCq2XE.js} +0 -0
  305. /package/dist/web/assets/{beancount-BJ3CutBG.js → beancount-DYDztRI0.js} +0 -0
  306. /package/dist/web/assets/{berry-D7so2Cui.js → berry-BE5YKqvx.js} +0 -0
  307. /package/dist/web/assets/{bibtex-Ct3acHa1.js → bibtex-CAm7mgEm.js} +0 -0
  308. /package/dist/web/assets/{bicep-jXDwCQcm.js → bicep-BKgAbfVa.js} +0 -0
  309. /package/dist/web/assets/{bird2-CjLD2cna.js → bird2-yrBNbW1G.js} +0 -0
  310. /package/dist/web/assets/{c-CdgZojnL.js → c-txZFr4RM.js} +0 -0
  311. /package/dist/web/assets/{c3-DtpZYego.js → c3-D_DrzBUd.js} +0 -0
  312. /package/dist/web/assets/{cadence-CPLkixoh.js → cadence-CUFCEgG9.js} +0 -0
  313. /package/dist/web/assets/{can-open-in-ppm-BntPywsy.js → can-open-in-ppm-_yqa69dV.js} +0 -0
  314. /package/dist/web/assets/{catppuccin-frappe-Xt0_qJa9.js → catppuccin-frappe-Dn32MuhA.js} +0 -0
  315. /package/dist/web/assets/{catppuccin-latte-CJnbLec5.js → catppuccin-latte-Dewo7yDf.js} +0 -0
  316. /package/dist/web/assets/{catppuccin-macchiato-BPHP1wYB.js → catppuccin-macchiato-D8WOWZeo.js} +0 -0
  317. /package/dist/web/assets/{catppuccin-mocha-ssWJodGc.js → catppuccin-mocha-b0md48XM.js} +0 -0
  318. /package/dist/web/assets/{clarity-DB0408F6.js → clarity-Ct3UqLBj.js} +0 -0
  319. /package/dist/web/assets/{clojure-dpRPlAO4.js → clojure-CwP5aPEo.js} +0 -0
  320. /package/dist/web/assets/{cmake-r2VkeX9y.js → cmake-D5RF49e5.js} +0 -0
  321. /package/dist/web/assets/{codeowners-v5nQqHiB.js → codeowners-Cl5897L_.js} +0 -0
  322. /package/dist/web/assets/{codeql-BSSEhKRR.js → codeql-BKh1RCg0.js} +0 -0
  323. /package/dist/web/assets/{common-lisp-BXb2MMMA.js → common-lisp-BhHuD6t0.js} +0 -0
  324. /package/dist/web/assets/{coq-BjBNi73X.js → coq-Dco3qwD8.js} +0 -0
  325. /package/dist/web/assets/{csharp-CdPjZRbV.js → csharp-Dpjw9ZA9.js} +0 -0
  326. /package/dist/web/assets/{css-BuxbQOHL.js → css-CPe4cabH.js} +0 -0
  327. /package/dist/web/assets/{csv-Dj-eoZwH.js → csv-BAcTN8Kt.js} +0 -0
  328. /package/dist/web/assets/{csv-parser-CBbALsFU.js → csv-parser-DbxGKdMG.js} +0 -0
  329. /package/dist/web/assets/{cue-Dz73VksN.js → cue-Cr6y60TA.js} +0 -0
  330. /package/dist/web/assets/{cypher-CkVkDywZ.js → cypher-DWtqXXFP.js} +0 -0
  331. /package/dist/web/assets/{d-oU51Pbpf.js → d-BASiTZk4.js} +0 -0
  332. /package/dist/web/assets/{dark-plus-DcwvjjfO.js → dark-plus-56kmnrcl.js} +0 -0
  333. /package/dist/web/assets/{dart-DGvu8mcX.js → dart-C5mZnjoE.js} +0 -0
  334. /package/dist/web/assets/{data-grid-types-B6kUrF-M.js → data-grid-types-DeY60CLU.js} +0 -0
  335. /package/dist/web/assets/{dax-BmbjKQ90.js → dax-B4I2e7d8.js} +0 -0
  336. /package/dist/web/assets/{desktop-IRBSiFfh.js → desktop-DekGg0BT.js} +0 -0
  337. /package/dist/web/assets/{diff-Bdik2JR8.js → diff-CWnJBd73.js} +0 -0
  338. /package/dist/web/assets/{dist-Bv4zQpS1.js → dist-B0J2eYBz.js} +0 -0
  339. /package/dist/web/assets/{dist-C8YPBIOb.js → dist-BKOM_AoT.js} +0 -0
  340. /package/dist/web/assets/{docker-Bgdl0H8I.js → docker-DevchDxK.js} +0 -0
  341. /package/dist/web/assets/{dotenv-COe5NdnA.js → dotenv-DCvtdaGJ.js} +0 -0
  342. /package/dist/web/assets/{dracula-DjURaSAa.js → dracula-8U6Vkk3l.js} +0 -0
  343. /package/dist/web/assets/{dracula-soft-Do7d0bo-.js → dracula-soft-Bfeew5Wg.js} +0 -0
  344. /package/dist/web/assets/{dream-maker-DYwQvJIj.js → dream-maker-DDTJVuoy.js} +0 -0
  345. /package/dist/web/assets/{emacs-lisp-BXjlHyDW.js → emacs-lisp-CEHJqI4m.js} +0 -0
  346. /package/dist/web/assets/{everforest-dark-h8Q2loiX.js → everforest-dark-CHHLt7cq.js} +0 -0
  347. /package/dist/web/assets/{everforest-light-CdsJiL-9.js → everforest-light-TcTbK1sM.js} +0 -0
  348. /package/dist/web/assets/{fennel-D0CBtOvY.js → fennel-Bh4vsA1A.js} +0 -0
  349. /package/dist/web/assets/{fish-DzayfrMj.js → fish-D0T0fILG.js} +0 -0
  350. /package/dist/web/assets/{fluent-CatjkUfx.js → fluent-CgXDXdyy.js} +0 -0
  351. /package/dist/web/assets/{format-bytes-BulRSULX.js → format-bytes-Bc6SqF7M.js} +0 -0
  352. /package/dist/web/assets/{fortran-free-form-DM04ud0b.js → fortran-free-form-CUg3Xhz4.js} +0 -0
  353. /package/dist/web/assets/{gdscript-BLN5iZwf.js → gdscript-Cx056p_k.js} +0 -0
  354. /package/dist/web/assets/{gdshader-BK-1iZQs.js → gdshader-Dj1qP4I5.js} +0 -0
  355. /package/dist/web/assets/{genie-BGPOFWDN.js → genie-SBt15wn7.js} +0 -0
  356. /package/dist/web/assets/{gherkin-CdVOJ_JY.js → gherkin-bTJiHIrB.js} +0 -0
  357. /package/dist/web/assets/{github-dark-B_xW8TaY.js → github-dark-BOPhyugs.js} +0 -0
  358. /package/dist/web/assets/{github-dark-default-h_iKq7CX.js → github-dark-default-Cktqe6NF.js} +0 -0
  359. /package/dist/web/assets/{github-dark-dimmed-nKDYE62T.js → github-dark-dimmed-CeWfII_B.js} +0 -0
  360. /package/dist/web/assets/{github-dark-high-contrast-l8I771QT.js → github-dark-high-contrast-CTx8LpdV.js} +0 -0
  361. /package/dist/web/assets/{github-light-DNhjkg8Z.js → github-light-DVsO5Un1.js} +0 -0
  362. /package/dist/web/assets/{github-light-default-DQ06uS3g.js → github-light-default-BiyPxicr.js} +0 -0
  363. /package/dist/web/assets/{github-light-high-contrast-qc2sZCVN.js → github-light-high-contrast-BPlnke0a.js} +0 -0
  364. /package/dist/web/assets/{gleam-DPPatkNH.js → gleam-CWSxvD0C.js} +0 -0
  365. /package/dist/web/assets/{gn-BnzGQTy5.js → gn-D7NUVJuv.js} +0 -0
  366. /package/dist/web/assets/{gnuplot-Ct1leoo3.js → gnuplot-c5uXmJKc.js} +0 -0
  367. /package/dist/web/assets/{go-qysCdbrn.js → go-CRsHi4Cw.js} +0 -0
  368. /package/dist/web/assets/{groovy-BkWIdrRQ.js → groovy-DpV0RA5r.js} +0 -0
  369. /package/dist/web/assets/{gruvbox-dark-hard-L6Iiybbl.js → gruvbox-dark-hard-DD9fguUN.js} +0 -0
  370. /package/dist/web/assets/{gruvbox-dark-medium-DbD3yl0b.js → gruvbox-dark-medium-BvcwpYJ-.js} +0 -0
  371. /package/dist/web/assets/{gruvbox-dark-soft-Ban2cHK3.js → gruvbox-dark-soft-48dCxX9M.js} +0 -0
  372. /package/dist/web/assets/{gruvbox-light-hard-CK_SjS3e.js → gruvbox-light-hard-CjJ994Ip.js} +0 -0
  373. /package/dist/web/assets/{gruvbox-light-medium-Bjgvhc2w.js → gruvbox-light-medium-DQJQZ_rV.js} +0 -0
  374. /package/dist/web/assets/{gruvbox-light-soft-EWnyYGUu.js → gruvbox-light-soft-AqsC9mKw.js} +0 -0
  375. /package/dist/web/assets/{haskell-rT1N9nYr.js → haskell-B5V7yvmh.js} +0 -0
  376. /package/dist/web/assets/{haxe-Bbc91axo.js → haxe-LWTKfPKr.js} +0 -0
  377. /package/dist/web/assets/{hcl-D-3qJA4v.js → hcl-QmNZdAtI.js} +0 -0
  378. /package/dist/web/assets/{hjson-DWtewcyQ.js → hjson-CboJsAzK.js} +0 -0
  379. /package/dist/web/assets/{hlsl-C7t6hCrO.js → hlsl-Bd-DO_WL.js} +0 -0
  380. /package/dist/web/assets/{horizon-zJAGUQ3j.js → horizon-CU8Bi_ss.js} +0 -0
  381. /package/dist/web/assets/{horizon-bright-CRNyrJy8.js → horizon-bright-iSQ3_3jP.js} +0 -0
  382. /package/dist/web/assets/{houston-BFKa4W1-.js → houston-CTXv3Puz.js} +0 -0
  383. /package/dist/web/assets/{hy-W1hNjMBM.js → hy-Cd4hTidD.js} +0 -0
  384. /package/dist/web/assets/{image-gallery-BycaVWOu.js → image-gallery-J8h8IY0c.js} +0 -0
  385. /package/dist/web/assets/{imba-CPphaV-V.js → imba-BipCHEY2.js} +0 -0
  386. /package/dist/web/assets/{ini-CayaZ4vW.js → ini-Dd1KkMrB.js} +0 -0
  387. /package/dist/web/assets/{java-BPAE_9Ej.js → java-rGr0OQD8.js} +0 -0
  388. /package/dist/web/assets/{javascript-C5SKIVqe.js → javascript-DHhCH2TN.js} +0 -0
  389. /package/dist/web/assets/{json-C-0EUYmu.js → json-CMMLBCUf.js} +0 -0
  390. /package/dist/web/assets/{json5-BqVetEZL.js → json5-mPOwTOsB.js} +0 -0
  391. /package/dist/web/assets/{jsonc-DnjIzkUB.js → jsonc-Bie0E6D3.js} +0 -0
  392. /package/dist/web/assets/{jsonl-rbV-3EqO.js → jsonl-CpqHmtC1.js} +0 -0
  393. /package/dist/web/assets/{jsonnet-9hvXw4qr.js → jsonnet-D21Ms51e.js} +0 -0
  394. /package/dist/web/assets/{jssm-1ifXyIhu.js → jssm-BXnl1nur.js} +0 -0
  395. /package/dist/web/assets/{jsx-hPumywTY.js → jsx-Chnii_vB.js} +0 -0
  396. /package/dist/web/assets/{kanagawa-dragon-BCPjmoMO.js → kanagawa-dragon-DhxR0UjL.js} +0 -0
  397. /package/dist/web/assets/{kanagawa-lotus-DN2VNjy7.js → kanagawa-lotus-fupRg5wp.js} +0 -0
  398. /package/dist/web/assets/{kanagawa-wave-B8RZEsgc.js → kanagawa-wave-DSWbRS9M.js} +0 -0
  399. /package/dist/web/assets/{kdl-CkvjWQIa.js → kdl-DirGHqUy.js} +0 -0
  400. /package/dist/web/assets/{kotlin-D8cnRi5o.js → kotlin-DkhUVMDC.js} +0 -0
  401. /package/dist/web/assets/{kusto-CmeeBEJG.js → kusto-DU7OpovF.js} +0 -0
  402. /package/dist/web/assets/{laserwave--rtV8Mv9.js → laserwave-iLKb1ZCT.js} +0 -0
  403. /package/dist/web/assets/{lean-CS-Pyi42.js → lean-Cuvh_QAP.js} +0 -0
  404. /package/dist/web/assets/{less-Ddi_7qGm.js → less-B5tQZOdI.js} +0 -0
  405. /package/dist/web/assets/{lib-DIrxSyLb.js → lib-el9rA30D.js} +0 -0
  406. /package/dist/web/assets/{libesm-CYLrAeW3.js → libesm-CdttTIAd.js} +0 -0
  407. /package/dist/web/assets/{light-plus-CrEjpkkX.js → light-plus-DiHU2KL5.js} +0 -0
  408. /package/dist/web/assets/{llvm-Cm7ZXmAO.js → llvm-sCBTKCKN.js} +0 -0
  409. /package/dist/web/assets/{log-lW2sYxtp.js → log-DtyR34Ie.js} +0 -0
  410. /package/dist/web/assets/{logo-B-PiRb53.js → logo-CP91aQyG.js} +0 -0
  411. /package/dist/web/assets/{luau-8St0pC-R.js → luau-nfGo2ulP.js} +0 -0
  412. /package/dist/web/assets/{make-DbNdhH54.js → make-XbDn9GHe.js} +0 -0
  413. /package/dist/web/assets/{markdown-CYocYSGd.js → markdown-BB1_3xLr.js} +0 -0
  414. /package/dist/web/assets/{material-theme-CVBF1Cbi.js → material-theme-CIx_sxAy.js} +0 -0
  415. /package/dist/web/assets/{material-theme-darker-HFrlNZD8.js → material-theme-darker-DsQeueDD.js} +0 -0
  416. /package/dist/web/assets/{material-theme-lighter-BM-uz2ft.js → material-theme-lighter-x_lMi74j.js} +0 -0
  417. /package/dist/web/assets/{material-theme-ocean-PbvUez8m.js → material-theme-ocean-Bkgh25P8.js} +0 -0
  418. /package/dist/web/assets/{material-theme-palenight-B22Ck6qd.js → material-theme-palenight-CoOjdbti.js} +0 -0
  419. /package/dist/web/assets/{matlab-GFFNzucw.js → matlab-D6xZas_8.js} +0 -0
  420. /package/dist/web/assets/{mdx-BVnJzMDx.js → mdx-Cl5_YNoz.js} +0 -0
  421. /package/dist/web/assets/{mermaid-BlCmSTCM.js → mermaid-8Eow4jz_.js} +0 -0
  422. /package/dist/web/assets/{min-dark-B0PwlAfC.js → min-dark-D4DhjgRE.js} +0 -0
  423. /package/dist/web/assets/{min-light-C_H2ABKu.js → min-light-DzAckd0t.js} +0 -0
  424. /package/dist/web/assets/{mipsasm-C3rf2fo6.js → mipsasm-zI_zu0tj.js} +0 -0
  425. /package/dist/web/assets/{mojo-CDRuzM3o.js → mojo-DQnGGPHf.js} +0 -0
  426. /package/dist/web/assets/{monokai-KiebNy_Y.js → monokai-CrFoD2VD.js} +0 -0
  427. /package/dist/web/assets/{moonbit-CdVUHoWl.js → moonbit-DdphIGs6.js} +0 -0
  428. /package/dist/web/assets/{move-Dq9iKZRY.js → move-BUlkn0xW.js} +0 -0
  429. /package/dist/web/assets/{narrat-7y5L5sGM.js → narrat-CbOBdtCn.js} +0 -0
  430. /package/dist/web/assets/{nextflow-groovy-Cwcbkcrx.js → nextflow-groovy-CeVLUTuQ.js} +0 -0
  431. /package/dist/web/assets/{night-owl-TBgLPzj4.js → night-owl-Bm4LhUR1.js} +0 -0
  432. /package/dist/web/assets/{night-owl-light-Dehb9My2.js → night-owl-light-DtcBiMEf.js} +0 -0
  433. /package/dist/web/assets/{nix-BKj-gzoX.js → nix-DrdwQ70g.js} +0 -0
  434. /package/dist/web/assets/{nord-BF_r8PeX.js → nord-B0Head1v.js} +0 -0
  435. /package/dist/web/assets/{nushell-CeAjk9_E.js → nushell-CmUdltKn.js} +0 -0
  436. /package/dist/web/assets/{objective-c-Xl1OMcDp.js → objective-c-BGDR33XN.js} +0 -0
  437. /package/dist/web/assets/{objective-cpp-C4ZgzrRq.js → objective-cpp-Buc1Zhd0.js} +0 -0
  438. /package/dist/web/assets/{ocaml-CGj8IYTq.js → ocaml-xdzl9rRD.js} +0 -0
  439. /package/dist/web/assets/{odin-C4veqvW2.js → odin-DiUFngnd.js} +0 -0
  440. /package/dist/web/assets/{one-dark-pro-gAcWejsV.js → one-dark-pro-BURxmIp3.js} +0 -0
  441. /package/dist/web/assets/{one-light-B7E-UUXy.js → one-light-BMRMInAj.js} +0 -0
  442. /package/dist/web/assets/{openscad-C-wN9BEv.js → openscad-DAgLA5J3.js} +0 -0
  443. /package/dist/web/assets/{pascal-C8WBtxMs.js → pascal-CoDZxcRB.js} +0 -0
  444. /package/dist/web/assets/{pkl-BBfF4G2A.js → pkl-DzKM6BuM.js} +0 -0
  445. /package/dist/web/assets/{plastic-DFOAb89P.js → plastic-vF15LmNS.js} +0 -0
  446. /package/dist/web/assets/{plsql-BS2CEMMV.js → plsql-C1v07teA.js} +0 -0
  447. /package/dist/web/assets/{po-BKJDtPmy.js → po-CLLC8BuA.js} +0 -0
  448. /package/dist/web/assets/{poimandres-Cg3hL13e.js → poimandres-Uq0xysxM.js} +0 -0
  449. /package/dist/web/assets/{polar-B9ebiXUU.js → polar-2-luY1Pc.js} +0 -0
  450. /package/dist/web/assets/{portal-container-context-a399Hd9F.js → portal-container-context-DKymYOY-.js} +0 -0
  451. /package/dist/web/assets/{postcss-B5REwNY_.js → postcss-lbFhe0rz.js} +0 -0
  452. /package/dist/web/assets/{powerquery-stc0Gzkf.js → powerquery-RL8O9Wie.js} +0 -0
  453. /package/dist/web/assets/{powershell-jgd0ahJN.js → powershell-upFWhb-b.js} +0 -0
  454. /package/dist/web/assets/{prisma-CWuZjRhI.js → prisma-DBtYaWFk.js} +0 -0
  455. /package/dist/web/assets/{prolog-D5zNFE63.js → prolog-D8jSMykJ.js} +0 -0
  456. /package/dist/web/assets/{proto-B-6RrU7n.js → proto-BM4eadF7.js} +0 -0
  457. /package/dist/web/assets/{puppet-DONP8TDR.js → puppet-BQDQ3Snu.js} +0 -0
  458. /package/dist/web/assets/{purescript-DDJITf-A.js → purescript-Cj2aTX4s.js} +0 -0
  459. /package/dist/web/assets/{python-bmWy9j1-.js → python-BYpqq6u4.js} +0 -0
  460. /package/dist/web/assets/{qmldir-CKDwMt7X.js → qmldir-4uiTcwAu.js} +0 -0
  461. /package/dist/web/assets/{qss-DptQQzBn.js → qss-D5xB-Mqo.js} +0 -0
  462. /package/dist/web/assets/{r-BorKpJ8r.js → r-P6Ujy8rf.js} +0 -0
  463. /package/dist/web/assets/{racket-BaHpYVsZ.js → racket-COKgCFuM.js} +0 -0
  464. /package/dist/web/assets/{raku-DFJIV4k_.js → raku-z14Q9i7K.js} +0 -0
  465. /package/dist/web/assets/{react-X4jUntEf.js → react-D1Yg256j.js} +0 -0
  466. /package/dist/web/assets/{red-70oJGrkb.js → red-ObAvgvQ5.js} +0 -0
  467. /package/dist/web/assets/{reg-C4g235Dv.js → reg-Qv4f0JFA.js} +0 -0
  468. /package/dist/web/assets/{regexp-Cw1UVmti.js → regexp-CEOVGIzy.js} +0 -0
  469. /package/dist/web/assets/{rel-DeLhIz5L.js → rel-cWOyTUwd.js} +0 -0
  470. /package/dist/web/assets/{resolve-theme-b_yGlUFQ.js → resolve-theme-CMp5xxCW.js} +0 -0
  471. /package/dist/web/assets/{riscv-ChnTiBRK.js → riscv-BZDwx-lv.js} +0 -0
  472. /package/dist/web/assets/{ron-C5IWeqDn.js → ron-BpjosPS0.js} +0 -0
  473. /package/dist/web/assets/{rose-pine-DCR0ohYW.js → rose-pine-DhxfJWcm.js} +0 -0
  474. /package/dist/web/assets/{rose-pine-dawn-DvGZs1PR.js → rose-pine-dawn-BbGt9Wy_.js} +0 -0
  475. /package/dist/web/assets/{rose-pine-moon-BWACHjeS.js → rose-pine-moon-dj4Zxgik.js} +0 -0
  476. /package/dist/web/assets/{rosmsg-CQlIVazE.js → rosmsg-EciOaHo2.js} +0 -0
  477. /package/dist/web/assets/{rust-DGGVzWI8.js → rust-C_eH59O_.js} +0 -0
  478. /package/dist/web/assets/{sass-bj1C0rFI.js → sass-Dos3R5lL.js} +0 -0
  479. /package/dist/web/assets/{scala-MU1tLKDi.js → scala-8lVKsTvf.js} +0 -0
  480. /package/dist/web/assets/{scheme-CeGXtv7t.js → scheme-Bd3VdaZj.js} +0 -0
  481. /package/dist/web/assets/{sdbl-D0HLSGKp.js → sdbl-C4Yrjlgh.js} +0 -0
  482. /package/dist/web/assets/{shellscript-DkYiDvh1.js → shellscript-OFZbPKZH.js} +0 -0
  483. /package/dist/web/assets/{slack-dark-YqeHiGWH.js → slack-dark-aL6vlxZs.js} +0 -0
  484. /package/dist/web/assets/{slack-ochin-9Oe0DljJ.js → slack-ochin-BKt1E7sL.js} +0 -0
  485. /package/dist/web/assets/{smalltalk-CaaxedR7.js → smalltalk-D6qVBL1U.js} +0 -0
  486. /package/dist/web/assets/{snazzy-light-C8s2FHNC.js → snazzy-light-BrWerT0t.js} +0 -0
  487. /package/dist/web/assets/{solarized-dark-0ryY6oN3.js → solarized-dark-CzxTJgVQ.js} +0 -0
  488. /package/dist/web/assets/{solarized-light-bltgZeNh.js → solarized-light-BecaDAnG.js} +0 -0
  489. /package/dist/web/assets/{solidity-DOf5HmLZ.js → solidity-C9t7pZ84.js} +0 -0
  490. /package/dist/web/assets/{splunk-D1u4WHDW.js → splunk-CtxfFKtF.js} +0 -0
  491. /package/dist/web/assets/{sql-G8uQZljX.js → sql-7FE_Duz0.js} +0 -0
  492. /package/dist/web/assets/{ssh-config-rbXvly7Q.js → ssh-config-3Fr37ACC.js} +0 -0
  493. /package/dist/web/assets/{stylus-Cii1iADy.js → stylus-BP4mE1sO.js} +0 -0
  494. /package/dist/web/assets/{swift-DpIgonN4.js → swift-0uqTFouD.js} +0 -0
  495. /package/dist/web/assets/{synthwave-84-DjGLzU2b.js → synthwave-84-DQaNM3DI.js} +0 -0
  496. /package/dist/web/assets/{system-verilog-_Z0M0Hs1.js → system-verilog-DUIVrZHZ.js} +0 -0
  497. /package/dist/web/assets/{systemd-BhyDCh9P.js → systemd-CtNo_WIh.js} +0 -0
  498. /package/dist/web/assets/{talonscript-CNchgCRD.js → talonscript-B0HXaCVT.js} +0 -0
  499. /package/dist/web/assets/{tasl-m--W7Pyr.js → tasl-Cb7RAl11.js} +0 -0
  500. /package/dist/web/assets/{tcl-gY-M_2BC.js → tcl-B79aC6l5.js} +0 -0
  501. /package/dist/web/assets/{terraform-CB6CzMwo.js → terraform-75jJDjpW.js} +0 -0
  502. /package/dist/web/assets/{tokyo-night-COh9oRYi.js → tokyo-night-Cz9Uh9qb.js} +0 -0
  503. /package/dist/web/assets/{toml-Bg2bsY-v.js → toml-CD6y9A-z.js} +0 -0
  504. /package/dist/web/assets/{tsv-DceGDiJC.js → tsv-CXJMbBrb.js} +0 -0
  505. /package/dist/web/assets/{tsx-EccDYa7i.js → tsx-BuYN0I2t.js} +0 -0
  506. /package/dist/web/assets/{turtle-C_l2JhvX.js → turtle-DWH-Rjse.js} +0 -0
  507. /package/dist/web/assets/{typescript-C1ytmMcr.js → typescript-BPd-hsrR.js} +0 -0
  508. /package/dist/web/assets/{typespec-De2ztzG5.js → typespec-B0wvVykx.js} +0 -0
  509. /package/dist/web/assets/{typst-DNcS7gSr.js → typst-DWG2c48-.js} +0 -0
  510. /package/dist/web/assets/{use-is-mobile-CYW3FJFE.js → use-is-mobile-D1weanbj.js} +0 -0
  511. /package/dist/web/assets/{use-visual-viewport-D9O48HQt.js → use-visual-viewport-Bfl3Bxxb.js} +0 -0
  512. /package/dist/web/assets/{utils-C9OjCrou.js → utils-IrnQujwR.js} +0 -0
  513. /package/dist/web/assets/{v-DSMUsdMO.js → v-Ch6E4Egd.js} +0 -0
  514. /package/dist/web/assets/{vala-BEazLpkO.js → vala-CGVKwbPx.js} +0 -0
  515. /package/dist/web/assets/{vb-CV9pEJzW.js → vb-BIEbSxyT.js} +0 -0
  516. /package/dist/web/assets/{vendor-xterm-cuGQmUjN.js → vendor-xterm-YQlzIWv2.js} +0 -0
  517. /package/dist/web/assets/{verilog-DhXkLVMk.js → verilog-A-3pYPad.js} +0 -0
  518. /package/dist/web/assets/{vesper-n4IltYq-.js → vesper-CK1naZAV.js} +0 -0
  519. /package/dist/web/assets/{vhdl-CePjFbvs.js → vhdl-CVawmgMg.js} +0 -0
  520. /package/dist/web/assets/{viml-ChndFsm5.js → viml-CiWjaqBY.js} +0 -0
  521. /package/dist/web/assets/{vitesse-black-BytVdvhC.js → vitesse-black-PYXW4-BO.js} +0 -0
  522. /package/dist/web/assets/{vitesse-dark-83Wech0N.js → vitesse-dark-BU5s4AuG.js} +0 -0
  523. /package/dist/web/assets/{vitesse-light-Xg4wbeMO.js → vitesse-light-DgkIT-N7.js} +0 -0
  524. /package/dist/web/assets/{vyper-eW1NC3FG.js → vyper-D8cRDPp3.js} +0 -0
  525. /package/dist/web/assets/{wasm-BucxKwBZ.js → wasm-C02wlRbP.js} +0 -0
  526. /package/dist/web/assets/{wasm-Bhucqu1z.js → wasm-ClFJgzmT.js} +0 -0
  527. /package/dist/web/assets/{wenyan-BJFBuXuH.js → wenyan-B8plxIr8.js} +0 -0
  528. /package/dist/web/assets/{wgsl-6z4FUyMK.js → wgsl-CVujtm3V.js} +0 -0
  529. /package/dist/web/assets/{wikitext-BoDU6teJ.js → wikitext-CmcG9B69.js} +0 -0
  530. /package/dist/web/assets/{wit-DerMPpgt.js → wit--gl2Jium.js} +0 -0
  531. /package/dist/web/assets/{wolfram-D78v0Ap-.js → wolfram-6T_1D-NS.js} +0 -0
  532. /package/dist/web/assets/{x-YDu4m2Iq.js → x-CxoBA0po.js} +0 -0
  533. /package/dist/web/assets/{yaml-C4TIgIw4.js → yaml-CIVV9uA1.js} +0 -0
  534. /package/dist/web/assets/{zenscript-BydW-dLC.js → zenscript-CoT4yNdA.js} +0 -0
  535. /package/dist/web/assets/{zig-b9lTqL5E.js → zig-BWs2Qo3G.js} +0 -0
@@ -23,6 +23,12 @@ export interface Account {
23
23
  lastUsedAt: number | null;
24
24
  profileData: OAuthProfileData | null;
25
25
  createdAt: number;
26
+ /** When the current OAuth grant was issued. Null for rows that predate the column. */
27
+ grantedAt: number | null;
28
+ /** Refresh-token expiry as reported by the token endpoint, when it reports one. */
29
+ refreshExpiresAt: number | null;
30
+ /** The OAuth server rejected the refresh token; nothing but a fresh sign-in clears it. */
31
+ reauthRequired: boolean;
26
32
  }
27
33
 
28
34
  export interface AccountWithTokens extends Account {
@@ -69,6 +75,72 @@ const OAUTH_TOKEN_URL = "https://api.anthropic.com/v1/oauth/token";
69
75
  const OAUTH_SCOPE = "org:create_api_key user:profile user:inference";
70
76
  const OAUTH_PLATFORM_REDIRECT = "https://platform.claude.com/oauth/code/callback";
71
77
 
78
+ /**
79
+ * Anthropic invalidates the entire refresh-token family this long after the original
80
+ * grant, whether or not the token kept rotating in between. Measured twice on a live
81
+ * install: developers@ signed in 2026-08-11 and was rejected 2026-09-08 (28d 2h);
82
+ * victor@ signed in 2026-08-18 and was rejected 2026-09-15 (27d 15h). Both died on the
83
+ * first use of a token minted hours earlier, so this is grant age, not token age.
84
+ *
85
+ * Only a fallback: if the token endpoint reports the refresh token's own expiry we use
86
+ * that instead, because a guessed constant silently rots if Anthropic changes it.
87
+ */
88
+ const GRANT_LIFETIME_S = 28 * 86400;
89
+
90
+ /**
91
+ * How close to expiry a token may be before use forces a refresh. Claude Code uses
92
+ * 5 minutes (`g0e = 300000` in its bundle) and refreshes at the point of use rather
93
+ * than on a timer; PPM now does the same.
94
+ *
95
+ * A buffer this small can hand a long turn a token that lapses mid-run. That is
96
+ * survivable here, and only here, because the 401 path in claude-agent-sdk.ts force-
97
+ * refreshes and retries the turn instead of failing it. Shrink that safety net and this
98
+ * number has to grow again.
99
+ */
100
+ const PREFLIGHT_REFRESH_BUFFER_S = 300;
101
+
102
+ /** Response fields we already understand; anything else is worth learning about. */
103
+ const KNOWN_TOKEN_FIELDS = new Set([
104
+ "access_token", "refresh_token", "expires_in", "token_type", "scope", "account", "organization",
105
+ "refresh_token_expires_in", "refresh_expires_in", "refresh_token_expires_at", "refresh_expires_at",
106
+ ]);
107
+ const loggedUnknownTokenFields = new Set<string>();
108
+
109
+ /**
110
+ * Log response fields we do not consume, once per field name.
111
+ *
112
+ * The refresh-token expiry is the one number that would let PPM warn before a grant
113
+ * dies, and Claude Code stores it (`claudeAiOauth.refreshTokenExpiresAt`), but its own
114
+ * wire format is not public. Rather than hard-code a guess, read whichever field shows
115
+ * up and surface the rest so an unknown name is a log line instead of a silent miss.
116
+ */
117
+ function noteUnknownTokenFields(data: Record<string, unknown>, context: string): void {
118
+ for (const key of Object.keys(data)) {
119
+ if (KNOWN_TOKEN_FIELDS.has(key) || loggedUnknownTokenFields.has(key)) continue;
120
+ loggedUnknownTokenFields.add(key);
121
+ const value = data[key];
122
+ const shown = typeof value === "string" && /token|secret|key/i.test(key)
123
+ ? `<redacted len=${value.length}>`
124
+ : JSON.stringify(value);
125
+ console.log(`[accounts] ${context} response carries unhandled field "${key}" = ${shown}`);
126
+ }
127
+ }
128
+
129
+ /** Pull the refresh token's own expiry out of a token response, in whatever shape it arrives. */
130
+ function readRefreshExpiry(data: Record<string, unknown>): number | null {
131
+ const nowS = Math.floor(Date.now() / 1000);
132
+ for (const key of ["refresh_token_expires_in", "refresh_expires_in"]) {
133
+ const v = data[key];
134
+ if (typeof v === "number" && Number.isFinite(v) && v > 0) return nowS + Math.floor(v);
135
+ }
136
+ for (const key of ["refresh_token_expires_at", "refresh_expires_at"]) {
137
+ const v = data[key];
138
+ // Milliseconds if it is far past any plausible epoch-seconds value.
139
+ if (typeof v === "number" && Number.isFinite(v) && v > 0) return v > 1e11 ? Math.floor(v / 1000) : Math.floor(v);
140
+ }
141
+ return null;
142
+ }
143
+
72
144
  // Survive Bun --hot reloads: persist timer ref across module re-evaluations
73
145
  const ACCT_HOT_KEY = "__PPM_ACCT_REFRESH__" as const;
74
146
  const acctHotState = ((globalThis as any)[ACCT_HOT_KEY] ??= {
@@ -97,9 +169,25 @@ class AccountService {
97
169
  lastUsedAt: row.last_used_at,
98
170
  profileData,
99
171
  createdAt: row.created_at,
172
+ grantedAt: row.granted_at,
173
+ refreshExpiresAt: row.refresh_expires_at,
174
+ reauthRequired: row.reauth_required === 1,
100
175
  };
101
176
  }
102
177
 
178
+ /**
179
+ * When this account's refresh token family is expected to die, or null if unknowable.
180
+ *
181
+ * Prefers whatever the server reported. Falls back to grant age, which is only
182
+ * available for accounts signed in since PPM started recording it — an older row
183
+ * reads as "unknown" rather than being given a fabricated date.
184
+ */
185
+ grantExpiresAt(acc: Account): number | null {
186
+ if (acc.refreshExpiresAt) return acc.refreshExpiresAt;
187
+ if (acc.grantedAt) return acc.grantedAt + GRANT_LIFETIME_S;
188
+ return null;
189
+ }
190
+
103
191
  private toAccountWithTokens(row: AccountRow): AccountWithTokens {
104
192
  return {
105
193
  ...this.toAccount(row),
@@ -144,18 +232,25 @@ class AccountService {
144
232
  * the two because its callers are background loops with nobody to tell; the one caller
145
233
  * that has a user in front of it (`PATCH /api/accounts/:id`) uses this.
146
234
  */
147
- async ensureFreshTokenChecked(id: string): Promise<{ account: AccountWithTokens | null; rejected: boolean }> {
235
+ async ensureFreshTokenChecked(
236
+ id: string,
237
+ opts?: { retryRejected?: boolean },
238
+ ): Promise<{ account: AccountWithTokens | null; rejected: boolean }> {
148
239
  const acc = this.getWithTokens(id);
149
240
  if (!acc) return { account: null, rejected: false };
150
241
  // Only OAuth tokens need refresh
151
242
  if (!acc.accessToken.startsWith("sk-ant-oat")) return { account: acc, rejected: false };
152
243
  if (!acc.expiresAt) return { account: acc, rejected: false };
244
+ // A grant the server already rejected cannot come back on its own, and this method is
245
+ // on the usage poller's five-minute path. Callers acting on a deliberate user gesture
246
+ // (enable, test token) pass retryRejected so a false positive is always one click from
247
+ // being re-tested.
248
+ if (acc.reauthRequired && !opts?.retryRejected) return { account: null, rejected: true };
153
249
  const nowS = Math.floor(Date.now() / 1000);
154
- const REFRESH_BUFFER_S = 3600; // 1 hour refresh proactively before expiry
155
- if (acc.expiresAt - nowS > REFRESH_BUFFER_S) return { account: acc, rejected: false }; // still fresh
250
+ if (acc.expiresAt - nowS > PREFLIGHT_REFRESH_BUFFER_S) return { account: acc, rejected: false }; // still fresh
156
251
  try {
157
- console.log(`[accounts] Pre-flight refresh for ${acc.email ?? id} (expires in ${acc.expiresAt - nowS}s, buffer=${REFRESH_BUFFER_S}s)`);
158
- await this.refreshAccessToken(id, false, false, REFRESH_BUFFER_S);
252
+ console.log(`[accounts] Pre-flight refresh for ${acc.email ?? id} (expires in ${acc.expiresAt - nowS}s, buffer=${PREFLIGHT_REFRESH_BUFFER_S}s)`);
253
+ await this.refreshAccessToken(id, false, false, PREFLIGHT_REFRESH_BUFFER_S);
159
254
  return { account: this.getWithTokens(id), rejected: false };
160
255
  } catch (e) {
161
256
  const msg = (e as Error).message ?? String(e);
@@ -186,13 +281,24 @@ class AccountService {
186
281
  accessToken: string;
187
282
  refreshToken: string;
188
283
  expiresAt: number;
284
+ refreshExpiresAt?: number | null;
189
285
  label?: string;
190
286
  profileData?: OAuthProfileData;
191
287
  }): Account {
288
+ const grantedAt = Math.floor(Date.now() / 1000);
192
289
  // Check for duplicate — update existing account tokens instead of creating new
193
290
  const dup = this.findDuplicate(params.email, params.profileData);
194
291
  if (dup) {
195
292
  this.updateTokens(dup.id, params.accessToken, params.refreshToken, params.expiresAt);
293
+ // Signing in again starts a brand new grant, so the 28-day clock restarts here.
294
+ // This is the branch that matters in practice: re-authenticating an account that
295
+ // already exists is how every expiry is recovered from, and it is exactly where
296
+ // created_at stays frozen at the row's original insert.
297
+ updateAccount(dup.id, {
298
+ granted_at: grantedAt,
299
+ refresh_expires_at: params.refreshExpiresAt ?? null,
300
+ reauth_required: 0,
301
+ });
196
302
  if (params.profileData) {
197
303
  updateAccount(dup.id, { profile_json: JSON.stringify(params.profileData) });
198
304
  }
@@ -217,6 +323,10 @@ class AccountService {
217
323
  total_requests: 0,
218
324
  last_used_at: null,
219
325
  profile_json: params.profileData ? JSON.stringify(params.profileData) : null,
326
+ granted_at: grantedAt,
327
+ refresh_expires_at: params.refreshExpiresAt ?? null,
328
+ reauth_required: 0,
329
+ last_refresh_attempt_at: null,
220
330
  });
221
331
  return this.toAccount(getAccountById(id)!);
222
332
  }
@@ -347,6 +457,11 @@ class AccountService {
347
457
  total_requests: 0,
348
458
  last_used_at: null,
349
459
  profile_json: info.profileData ? JSON.stringify(info.profileData) : null,
460
+ // An API key is not an OAuth grant: it has no family to expire and no sign-in to date.
461
+ granted_at: null,
462
+ refresh_expires_at: null,
463
+ reauth_required: 0,
464
+ last_refresh_attempt_at: null,
350
465
  });
351
466
  return this.toAccount(getAccountById(id)!);
352
467
  }
@@ -366,6 +481,10 @@ class AccountService {
366
481
  // The cooldown clears either way — a fresh token is what ends one, and leaving the
367
482
  // timestamp on a parked account would resurrect it the moment the park is lifted.
368
483
  cooldown_until: null,
484
+ // Holding live tokens is the only thing that clears a re-auth demand. Every door
485
+ // that lands here — sign-in, import, a successful refresh — has just proved the
486
+ // credential works, which is exactly the condition the flag was tracking.
487
+ reauth_required: 0,
369
488
  ...(parked ? {} : { status: "active" as const }),
370
489
  });
371
490
  }
@@ -515,6 +634,7 @@ class AccountService {
515
634
  accessToken: tokens.accessToken,
516
635
  refreshToken: tokens.refreshToken,
517
636
  expiresAt: tokens.expiresAt,
637
+ refreshExpiresAt: tokens.refreshExpiresAt,
518
638
  label,
519
639
  profileData,
520
640
  });
@@ -533,6 +653,7 @@ class AccountService {
533
653
  accessToken: tokens.accessToken,
534
654
  refreshToken: tokens.refreshToken,
535
655
  expiresAt: tokens.expiresAt,
656
+ refreshExpiresAt: tokens.refreshExpiresAt,
536
657
  profileData,
537
658
  });
538
659
  }
@@ -541,6 +662,7 @@ class AccountService {
541
662
  accessToken: string;
542
663
  refreshToken: string;
543
664
  expiresAt: number;
665
+ refreshExpiresAt: number | null;
544
666
  email: string;
545
667
  }> {
546
668
  const body: Record<string, string> = {
@@ -560,16 +682,18 @@ class AccountService {
560
682
  const text = await res.text();
561
683
  throw new Error(`OAuth token exchange failed: ${res.status} ${text}`);
562
684
  }
563
- const data = await res.json() as {
685
+ const data = await res.json() as Record<string, unknown> & {
564
686
  access_token: string;
565
687
  refresh_token: string;
566
688
  expires_in: number;
567
689
  account?: { email_address?: string };
568
690
  };
691
+ noteUnknownTokenFields(data, "authorization_code");
569
692
  return {
570
693
  accessToken: data.access_token,
571
694
  refreshToken: data.refresh_token,
572
695
  expiresAt: Math.floor(Date.now() / 1000) + data.expires_in,
696
+ refreshExpiresAt: readRefreshExpiry(data),
573
697
  email: data.account?.email_address ?? "",
574
698
  };
575
699
  }
@@ -606,15 +730,18 @@ class AccountService {
606
730
 
607
731
  /**
608
732
  * POST the refresh grant, retrying transient failures with backoff.
609
- * A network blip (e.g. waking from sleep) must not cost a whole refresh cycle: this
610
- * runs at or past access-token expiry, so a dropped attempt leaves the account unable
611
- * to serve a turn until the next sweep.
612
733
  *
613
- * It is *not* that the refresh token is about to lapse with it. Measured against a
614
- * live install's history: 324 refreshes, zero `invalid_grant`, and one that succeeded
615
- * after a 58-hour gap 50 hours past the 8-hour access-token expiry. Refresh tokens
616
- * outlive their access tokens by days, which is what makes freezing a parked account
617
- * (see `startAutoRefresh`) safe rather than a slow way to kill it.
734
+ * Retrying matters more now than it used to. This runs at the point of use, inside a
735
+ * five-minute window before the token is needed, so a network blip that burns the
736
+ * attempt costs the turn rather than merely deferring to the next sweep there is no
737
+ * next sweep.
738
+ *
739
+ * An earlier version of this comment claimed refresh tokens outlive their access tokens
740
+ * by days, citing 324 refreshes with zero `invalid_grant`. That held only inside one
741
+ * grant's lifetime. Across grants it is wrong in the way that matters: the family is
742
+ * revoked on a fixed schedule from sign-in (GRANT_LIFETIME_S), and when it goes, the
743
+ * refresh token is rejected on its first use while the access token minted alongside it
744
+ * is still valid. No amount of retrying recovers that — only a new sign-in does.
618
745
  */
619
746
  private async postRefreshGrant(refreshToken: string, label: string): Promise<Response> {
620
747
  const backoffMs = [2_000, 6_000];
@@ -661,11 +788,13 @@ class AccountService {
661
788
  console.log(`[accounts] Token for ${account.email ?? accountId} is already fresh (expires in ${account.expiresAt - nowS}s, threshold=${freshThresholdS}s) — skipping OAuth refresh`);
662
789
  return;
663
790
  }
791
+ updateAccount(accountId, { last_refresh_attempt_at: nowS });
664
792
  const res = await this.postRefreshGrant(account.refreshToken, account.email ?? accountId);
665
793
  if (!res.ok) {
666
794
  const errorBody = await res.text().catch(() => "");
667
795
  console.error(`[accounts] Refresh failed for ${accountId}: ${res.status} ${errorBody}`);
668
- if (errorBody.includes("invalid_grant") || errorBody.includes("invalid_request")) {
796
+ const rejected = errorBody.includes("invalid_grant") || errorBody.includes("invalid_request");
797
+ if (rejected) {
669
798
  // Another session/process may have refreshed (and rotated) the token between our read
670
799
  // and this OAuth call, making this failure stale. Detect that by comparing against the
671
800
  // expiry we read — testing "is it fresh" instead would swallow genuine rejections
@@ -680,24 +809,35 @@ class AccountService {
680
809
  // permanently with no recovery path (esp. for parked/disabled accounts). Preserve
681
810
  // it so re-enable / re-import / re-sync can restore access.
682
811
  console.warn(`[accounts] Refresh token rejected for ${account.email ?? accountId} — preserving token for recovery (not clearing)`);
812
+ // Anthropic ends the refresh-token family a fixed time after the grant, so a
813
+ // rejection here is terminal: every subsequent attempt returns the same 400.
814
+ // Recording it stops the retry loop that otherwise runs every few minutes for
815
+ // days — one live install logged 17,510 of them — and gives the UI something to
816
+ // show besides an account that still claims to be active.
817
+ updateAccount(accountId, { reauth_required: 1 });
683
818
  }
684
819
  if (disableOnFail) {
685
820
  this.setDisabled(accountId);
686
821
  }
687
822
  throw new Error(`Token refresh failed for account ${accountId}: ${res.status} ${errorBody}`);
688
823
  }
689
- const data = await res.json() as {
824
+ const data = await res.json() as Record<string, unknown> & {
690
825
  access_token: string;
691
826
  refresh_token?: string;
692
827
  expires_in: number;
693
828
  };
694
- console.log(`[accounts] Token refreshed for ${account.email ?? accountId} (expires_in=${data.expires_in}s, new_refresh=${!!data.refresh_token})`);
829
+ noteUnknownTokenFields(data, "refresh_token");
830
+ const refreshExpiresAt = readRefreshExpiry(data);
831
+ console.log(`[accounts] Token refreshed for ${account.email ?? accountId} (expires_in=${data.expires_in}s, new_refresh=${!!data.refresh_token}, refresh_expires_at=${refreshExpiresAt ?? "not reported"})`);
695
832
  this.updateTokens(
696
833
  accountId,
697
834
  data.access_token,
698
835
  data.refresh_token ?? account.refreshToken,
699
836
  Math.floor(Date.now() / 1000) + data.expires_in,
700
837
  );
838
+ // Only overwrite when the server said something. A rotation that reports nothing
839
+ // must not erase an expiry an earlier response did report.
840
+ if (refreshExpiresAt) updateAccount(accountId, { refresh_expires_at: refreshExpiresAt });
701
841
  }
702
842
 
703
843
  // ---------------------------------------------------------------------------
@@ -816,6 +956,12 @@ class AccountService {
816
956
  total_requests: row.total_requests ?? 0,
817
957
  last_used_at: row.last_used_at,
818
958
  profile_json: row.profile_json ?? null,
959
+ // A backup written before these columns existed has none of this; null means
960
+ // "unknown", which is what the import genuinely knows.
961
+ granted_at: row.granted_at ?? null,
962
+ refresh_expires_at: row.refresh_expires_at ?? null,
963
+ reauth_required: 0,
964
+ last_refresh_attempt_at: null,
819
965
  });
820
966
  imported++;
821
967
  if (hasRefresh && importedStatus !== "disabled") fullTransferIds.push(row.id);
@@ -846,43 +992,21 @@ class AccountService {
846
992
  // Auto-refresh background timer
847
993
  // ---------------------------------------------------------------------------
848
994
 
849
- startAutoRefresh(): void {
995
+ startAccountMaintenance(): void {
850
996
  if (acctHotState.refreshTimer) return;
851
997
  const CHECK_INTERVAL_MS = 5 * 60_000;
852
- // Refresh a full hour early: Anthropic's refresh token lapses soon after the access
853
- // token expires, so waiting until the last minutes leaves no room to recover from a
854
- // network outage. An hour of headroom gives ~12 retry ticks before the token is lost.
855
- const REFRESH_BUFFER_S = 60 * 60;
856
998
 
857
- const refreshExpiring = async () => {
858
- const accounts = this.list();
859
- const nowS = Math.floor(Date.now() / 1000);
860
- for (const acc of accounts) {
861
- // Skip disabled accounts: this timer exists to keep the chat rotation supplied and
862
- // a parked account is not in it. Refreshing rotates the OAuth refresh token; when
863
- // the same account is shared across machines the rotation races and one side gets
864
- // invalid_grant, killing an account the user only meant to park.
865
- //
866
- // A preference, not a guarantee, and worth knowing before relying on it: the usage
867
- // poller runs on the same 5-minute cadence with the same 1-hour buffer and does not
868
- // skip parked accounts — deliberately, because a token nothing ever refreshes does
869
- // eventually die. So while polling is on, a parked account's token still rotates
870
- // here-or-there; this skip only declines to be the one that does it. Enabling the
871
- // account proves the token either way (`PATCH /api/accounts/:id`).
872
- if (acc.status === "disabled") continue;
873
- if (!acc.expiresAt) continue;
874
- if (acc.expiresAt - nowS > REFRESH_BUFFER_S) continue;
875
- // Skip temporary accounts (no refresh token) — they can't be refreshed
876
- const withTokens = this.getWithTokens(acc.id);
877
- if (!withTokens?.refreshToken) continue;
878
- console.log(`[accounts] Auto-refreshing token for ${acc.email ?? acc.id}`);
879
- try {
880
- await this.refreshAccessToken(acc.id, false, false, REFRESH_BUFFER_S);
881
- } catch (e) {
882
- console.error(`[accounts] Auto-refresh failed for ${acc.id}: ${(e as Error).message ?? e}`);
883
- }
884
- }
885
- };
999
+ // There is deliberately no token-refresh sweep here any more.
1000
+ //
1001
+ // It used to refresh every account an hour before expiry, which rotated an idle
1002
+ // account's token three or four times a day for nothing. That bought no safety: the
1003
+ // refresh-token family dies on grant age (see GRANT_LIFETIME_S), not on idleness, so
1004
+ // keeping a parked token warm never extended its life — it only widened the window in
1005
+ // which two holders of the same credential could rotate over each other.
1006
+ //
1007
+ // Claude Code refreshes where the token is consumed and nowhere else, and PPM now
1008
+ // matches: ensureFreshTokenChecked() on the turn path, the proxy, and the usage
1009
+ // poller. What survives on this timer is the one job no request path covers.
886
1010
 
887
1011
  // Cleanup: auto-delete expired temporary accounts (no refresh token) after 7 days
888
1012
  const TEMP_EXPIRY_DAYS = 7;
@@ -902,10 +1026,8 @@ class AccountService {
902
1026
  };
903
1027
 
904
1028
  // Run immediately on startup, then every 5 minutes
905
- refreshExpiring().catch(() => {});
906
1029
  cleanupExpiredTemporary();
907
1030
  acctHotState.refreshTimer = setInterval(() => {
908
- refreshExpiring().catch(() => {});
909
1031
  cleanupExpiredTemporary();
910
1032
  }, CHECK_INTERVAL_MS);
911
1033
 
@@ -914,7 +1036,7 @@ class AccountService {
914
1036
  }
915
1037
  }
916
1038
 
917
- stopAutoRefresh(): void {
1039
+ stopAccountMaintenance(): void {
918
1040
  if (acctHotState.refreshTimer) {
919
1041
  clearInterval(acctHotState.refreshTimer);
920
1042
  acctHotState.refreshTimer = null;
@@ -140,7 +140,18 @@ export function getServicePath(): string {
140
140
  export function generateSystemdService(config: AutoStartConfig): string {
141
141
  const cmd = buildExecCommand(config);
142
142
  const execStart = cmd.map(shellEscape).join(" ");
143
- const bunDir = isCompiledBinary() ? "" : resolve(resolveBunPath(), "..");
143
+ // Bun is a runtime dependency even when PPM itself is a compiled binary: the extension
144
+ // installer spawns bare `bun add` / `bun remove` (extension-installer.ts), and a systemd
145
+ // user unit inherits a PATH that does not include ~/.bun/bin. Skipping this line whenever
146
+ // PPM was not started *by* bun left extension install/search failing with
147
+ // `Executable not found in $PATH: "bun"`. The launchd plist already carries a PATH through
148
+ // for the same reason.
149
+ let bunDir = "";
150
+ try {
151
+ bunDir = resolve(resolveBunPath(), "..");
152
+ } catch {
153
+ // No bun installed — there is no directory to add, and the feature that needs it says so.
154
+ }
144
155
 
145
156
  // Build PATH with bun directory prepended
146
157
  const envPath = bunDir
@@ -21,12 +21,16 @@ export type CodexStrategy = "round-robin" | "fill-first" | "lowest-usage";
21
21
 
22
22
  export type CodexAccountType = "apiKey" | "chatgpt";
23
23
 
24
+ /** Two states only — nothing here parks an account in cooldown the way Claude's rotation does. */
25
+ export type CodexAccountStatus = "active" | "disabled";
26
+
24
27
  export interface CodexAccount {
25
28
  id: string;
26
29
  label: string;
27
30
  type: CodexAccountType;
28
31
  home: string;
29
32
  planType?: string | null;
33
+ status: CodexAccountStatus;
30
34
  addedAt: string;
31
35
  }
32
36
 
@@ -38,10 +42,21 @@ export type CodexCreds =
38
42
  interface Row {
39
43
  id: string; label: string | null; type: string; home: string;
40
44
  plan_type: string | null; creds_enc: string | null; added_at: string;
45
+ status?: string | null;
41
46
  }
42
47
 
43
48
  function rowToAccount(r: Row): CodexAccount {
44
- return { id: r.id, label: r.label ?? r.id.slice(0, 8), type: r.type as CodexAccountType, home: r.home, planType: r.plan_type, addedAt: r.added_at };
49
+ return {
50
+ id: r.id,
51
+ label: r.label ?? r.id.slice(0, 8),
52
+ type: r.type as CodexAccountType,
53
+ home: r.home,
54
+ planType: r.plan_type,
55
+ // A row written before the column existed reads as null; treat that as enabled, which
56
+ // is what it effectively was.
57
+ status: r.status === "disabled" ? "disabled" : "active",
58
+ addedAt: r.added_at,
59
+ };
45
60
  }
46
61
 
47
62
  /** CODEX_HOME directory for an account id. */
@@ -102,6 +117,24 @@ export function updateCodexAccountMeta(id: string, meta: { label?: string; planT
102
117
  getDb().query(`UPDATE codex_accounts SET ${sets.join(", ")} WHERE id = ?`).run(...vals as any[]);
103
118
  }
104
119
 
120
+ /**
121
+ * Switch an account on or off.
122
+ *
123
+ * Deliberately no token proof on the way in, unlike the Claude path: that check exists
124
+ * because an Anthropic OAuth account can hold a refresh token the server has to exercise
125
+ * before trusting. A Codex account is a CODEX_HOME directory the app-server authenticates
126
+ * against when it spawns, so there is nothing here to prove in advance — copying that check
127
+ * across would buy a slow button and no safety.
128
+ *
129
+ * Disabling never touches credentials. Off and removed are different things, and the account
130
+ * has to come back exactly as it was.
131
+ */
132
+ export function setCodexAccountStatus(id: string, status: CodexAccountStatus): CodexAccount | null {
133
+ if (!getCodexAccount(id)) return null;
134
+ getDb().query("UPDATE codex_accounts SET status = ? WHERE id = ?").run(status, id);
135
+ return getCodexAccount(id);
136
+ }
137
+
105
138
  /** Remove the account row and its CODEX_HOME dir. */
106
139
  export function removeCodexAccount(id: string): void {
107
140
  const acct = getCodexAccount(id);
@@ -126,16 +159,48 @@ export function setCodexStrategy(strategy: CodexStrategy): void {
126
159
  configService.save();
127
160
  }
128
161
 
162
+ /** Five-hour utilisation at or above which an account is skipped while others have room. */
163
+ const FIVE_HOUR_SKIP_THRESHOLD = 0.95;
164
+
165
+ /**
166
+ * Drop accounts with no five-hour room left, unless that would leave nothing.
167
+ *
168
+ * Soft on purpose, mirroring the Claude selector: a throttled turn beats no turn, so when
169
+ * every account is at its cap the caller still gets one back. `usageOf` is supplied by the
170
+ * caller rather than fetched here — usage lives behind an async provider layer, and pulling
171
+ * it inside a synchronous pick would put a network round-trip on every account selection.
172
+ * With no `usageOf` there is nothing to judge and every account stays a candidate.
173
+ */
174
+ function withFiveHourRoom(
175
+ accts: CodexAccount[],
176
+ usageOf?: (id: string) => number,
177
+ ): CodexAccount[] {
178
+ if (!usageOf) return accts;
179
+ const room = accts.filter((a) => {
180
+ const util = usageOf(a.id);
181
+ return !Number.isFinite(util) ? true : util < FIVE_HOUR_SKIP_THRESHOLD;
182
+ });
183
+ return room.length > 0 ? room : accts;
184
+ }
185
+
129
186
  /**
130
187
  * Pick an account by strategy. `usageOf` (optional, from P4) enables lowest-usage;
131
188
  * without it lowest-usage falls back to round-robin.
189
+ *
190
+ * `usageOf` also drives the five-hour skip, so a caller that supplies it gets an account
191
+ * with room rather than merely the least-used one among several that are all capped.
132
192
  */
133
193
  export function selectCodexAccount(opts?: { strategy?: CodexStrategy; usageOf?: (id: string) => number }): CodexAccount | null {
134
- const accts = listCodexAccounts();
135
- if (accts.length === 0) return null;
136
- if (accts.length === 1) return accts[0]!;
194
+ // Disabled is a hard exclusion with no fallback, unlike the five-hour skip below. Being
195
+ // capped means a slower turn; being switched off is the user saying don't use this, and
196
+ // handing it back anyway because nothing else was left would ignore them.
197
+ const all = listCodexAccounts().filter((a) => a.status !== "disabled");
198
+ if (all.length === 0) return null;
199
+ if (all.length === 1) return all[0]!;
137
200
  const strategy = opts?.strategy ?? getCodexStrategy();
138
201
  const usageOf = opts?.usageOf;
202
+ const accts = withFiveHourRoom(all, usageOf);
203
+ if (accts.length === 1) return accts[0]!;
139
204
  if (strategy === "fill-first") return accts[0]!;
140
205
  if (strategy === "lowest-usage" && usageOf) {
141
206
  return accts.reduce((best, a) => (usageOf(a.id) < usageOf(best.id) ? a : best), accts[0]!);
@@ -160,7 +225,9 @@ export function selectCodexAccount(opts?: { strategy?: CodexStrategy; usageOf?:
160
225
  * null rather than guessing at one.
161
226
  */
162
227
  export function peekCodexAccount(): CodexAccount | null {
163
- const accts = listCodexAccounts();
228
+ // Status is the one thing this can filter on without going async, so it does. Usage lives
229
+ // behind a fetch and stays out of reach here; the consuming pick endpoint reads it instead.
230
+ const accts = listCodexAccounts().filter((a) => a.status !== "disabled");
164
231
  if (accts.length === 0) return null;
165
232
  if (accts.length === 1) return accts[0]!;
166
233
  return getCodexStrategy() === "fill-first" ? accts[0]! : null;
@@ -171,14 +238,21 @@ export async function resolveCodexAccountForSession(sessionId?: string): Promise
171
238
  // Session-less callers (the model list) have nothing sticky to honour and fall
172
239
  // straight through to the configured strategy.
173
240
  const sticky = sessionId ? getSessionCodexAccount(sessionId) : null;
174
- if (sticky) { const a = getCodexAccount(sticky); if (a) return a; }
175
- if (getCodexStrategy() === "lowest-usage" && listCodexAccounts().length > 1) {
176
- const usages = await getAllCodexUsages();
177
- // A failed usage fetch (broken/expired account) yields {} treat as +Infinity
178
- // so lowest-usage de-prioritizes it instead of preferring it as "0% used".
179
- return selectCodexAccount({ strategy: "lowest-usage", usageOf: (id) => usages[id]?.fiveHour ?? Number.POSITIVE_INFINITY });
180
- }
181
- return selectCodexAccount();
241
+ // A binding is held for the prompt cache, not honoured unconditionally: an account the
242
+ // user has switched off has to let go of the sessions sitting on it, or turning it off
243
+ // would do nothing for exactly the conversations already using it.
244
+ if (sticky) { const a = getCodexAccount(sticky); if (a && a.status !== "disabled") return a; }
245
+ if (listCodexAccounts().filter((a) => a.status !== "disabled").length <= 1) return selectCodexAccount();
246
+ // Usage is read for every strategy, not just lowest-usage: it is also what lets the
247
+ // selector skip an account with no five-hour room left. Reading it only for lowest-usage
248
+ // left that skip inert under round-robin, which is the default — sessions kept landing on
249
+ // accounts that were already capped. The read is served from the shared usage cache, so
250
+ // this costs a lookup rather than a request per session start.
251
+ //
252
+ // A failed usage fetch (broken/expired account) yields {} → treat as +Infinity so
253
+ // lowest-usage de-prioritizes it instead of preferring it as "0% used".
254
+ const usages = await getAllCodexUsages();
255
+ return selectCodexAccount({ usageOf: (id) => usages[id]?.fiveHour ?? Number.POSITIVE_INFINITY });
182
256
  }
183
257
 
184
258
  // ── Usage (per account) ──