@hienlh/ppm 0.23.15 → 0.23.17
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.
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +1 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +4 -4
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/{accounts-settings-section-B5YXy0vd.js → accounts-settings-section-BqsI4Ixj.js} +1 -1
- package/dist/web/assets/accounts-settings-section-BqsI4Ixj.js.br +0 -0
- package/dist/web/assets/accounts-settings-section-BqsI4Ixj.js.gz +0 -0
- package/dist/web/assets/{agent-session-sheet-Bj42PwCK.js → agent-session-sheet-BmoNB8BG.js} +2 -2
- package/dist/web/assets/agent-session-sheet-BmoNB8BG.js.br +0 -0
- package/dist/web/assets/agent-session-sheet-BmoNB8BG.js.gz +0 -0
- package/dist/web/assets/{agent-session-window-content-LqL6fVPU.js → agent-session-window-content-rlCrYBYv.js} +1 -1
- package/dist/web/assets/agent-session-window-content-rlCrYBYv.js.br +0 -0
- package/dist/web/assets/agent-session-window-content-rlCrYBYv.js.gz +0 -0
- package/dist/web/assets/{ai-resource-editor-jVv2nvAG.js → ai-resource-editor-CUOR9Xtf.js} +1 -1
- package/dist/web/assets/ai-resource-editor-CUOR9Xtf.js.br +0 -0
- package/dist/web/assets/ai-resource-editor-CUOR9Xtf.js.gz +0 -0
- package/dist/web/assets/{ai-settings-section-C9Q-v8fY.js → ai-settings-section-CNdbcCaI.js} +1 -1
- package/dist/web/assets/ai-settings-section-CNdbcCaI.js.br +0 -0
- package/dist/web/assets/ai-settings-section-CNdbcCaI.js.gz +0 -0
- package/dist/web/assets/{ai-settings-section-CN5BR1ZZ.js → ai-settings-section-aXXTb0dN.js} +1 -1
- package/dist/web/assets/{android-tab-CwqiuEZP.js → android-tab-BsuTMKJw.js} +1 -1
- package/dist/web/assets/android-tab-BsuTMKJw.js.br +0 -0
- package/dist/web/assets/android-tab-BsuTMKJw.js.gz +0 -0
- package/dist/web/assets/{appearance-settings-section-D0JIfiRx.js → appearance-settings-section-Dk6d_fTo.js} +1 -1
- package/dist/web/assets/appearance-settings-section-Dk6d_fTo.js.br +0 -0
- package/dist/web/assets/appearance-settings-section-Dk6d_fTo.js.gz +0 -0
- package/dist/web/assets/{apps-panel-Cz_KJ37M.js → apps-panel-C20ys4Cy.js} +1 -1
- package/dist/web/assets/apps-panel-C20ys4Cy.js.br +0 -0
- package/dist/web/assets/apps-panel-C20ys4Cy.js.gz +0 -0
- package/dist/web/assets/{branch-review-tab-C3QNpsm7.js → branch-review-tab-CcvkQMRA.js} +1 -1
- package/dist/web/assets/branch-review-tab-CcvkQMRA.js.br +0 -0
- package/dist/web/assets/branch-review-tab-CcvkQMRA.js.gz +0 -0
- package/dist/web/assets/{chat-tab-D14CUl0J.js → chat-tab-BAh37UwG.js} +1 -1
- package/dist/web/assets/chat-tab-BAh37UwG.js.br +0 -0
- package/dist/web/assets/chat-tab-BAh37UwG.js.gz +0 -0
- package/dist/web/assets/chat-tab-D-cl4yMo.js +19 -0
- package/dist/web/assets/chat-tab-D-cl4yMo.js.br +0 -0
- package/dist/web/assets/chat-tab-D-cl4yMo.js.gz +0 -0
- package/dist/web/assets/{code-editor-B7gbH4-x.js → code-editor-DIGsPNPS.js} +4 -4
- package/dist/web/assets/code-editor-DIGsPNPS.js.br +0 -0
- package/dist/web/assets/{code-editor-B7gbH4-x.js.gz → code-editor-DIGsPNPS.js.gz} +0 -0
- package/dist/web/assets/{codex-reset-credit-button-DQDtyvnd.js → codex-reset-credit-button-C0a2svAw.js} +1 -1
- package/dist/web/assets/codex-reset-credit-button-C0a2svAw.js.br +0 -0
- package/dist/web/assets/codex-reset-credit-button-C0a2svAw.js.gz +0 -0
- package/dist/web/assets/{conflict-editor-ktOkrODA.js → conflict-editor-zORSNMk6.js} +1 -1
- package/dist/web/assets/conflict-editor-zORSNMk6.js.br +0 -0
- package/dist/web/assets/conflict-editor-zORSNMk6.js.gz +0 -0
- package/dist/web/assets/{connection-form-tab-CxvExR7T.js → connection-form-tab-BkE7wACA.js} +1 -1
- package/dist/web/assets/connection-form-tab-BkE7wACA.js.br +0 -0
- package/dist/web/assets/connection-form-tab-BkE7wACA.js.gz +0 -0
- package/dist/web/assets/{csv-preview-CBDMTEsF.js → csv-preview-CDUY0I94.js} +1 -1
- package/dist/web/assets/csv-preview-CDUY0I94.js.br +0 -0
- package/dist/web/assets/csv-preview-CDUY0I94.js.gz +0 -0
- package/dist/web/assets/{db-tab-parts-BgVpKbi6.js → db-tab-parts-tgVIgcs5.js} +1 -1
- package/dist/web/assets/db-tab-parts-tgVIgcs5.js.br +0 -0
- package/dist/web/assets/db-tab-parts-tgVIgcs5.js.gz +0 -0
- package/dist/web/assets/{design-settings-section-DPGw61pY.js → design-settings-section-CqFPwhis.js} +1 -1
- package/dist/web/assets/design-settings-section-CqFPwhis.js.br +0 -0
- package/dist/web/assets/design-settings-section-CqFPwhis.js.gz +0 -0
- package/dist/web/assets/{design-tab-BiCqHNl3.js → design-tab-CVTzge3p.js} +1 -1
- package/dist/web/assets/design-tab-CVTzge3p.js.br +0 -0
- package/dist/web/assets/design-tab-CVTzge3p.js.gz +0 -0
- package/dist/web/assets/{designs-sidebar-panel-pcJ2Rx5G.js → designs-sidebar-panel-DKTSE1LS.js} +1 -1
- package/dist/web/assets/designs-sidebar-panel-DKTSE1LS.js.br +0 -0
- package/dist/web/assets/designs-sidebar-panel-DKTSE1LS.js.gz +0 -0
- package/dist/web/assets/{diff-viewer-CLAvyqVG.js → diff-viewer-3VDHcDag.js} +1 -1
- package/dist/web/assets/diff-viewer-3VDHcDag.js.br +0 -0
- package/dist/web/assets/diff-viewer-3VDHcDag.js.gz +0 -0
- package/dist/web/assets/{docx-preview-XSkD2IL6.js → docx-preview-FgE_WU6t.js} +1 -1
- package/dist/web/assets/docx-preview-FgE_WU6t.js.br +0 -0
- package/dist/web/assets/docx-preview-FgE_WU6t.js.gz +0 -0
- package/dist/web/assets/{editor-dialog-cQvqVZAR.js → editor-dialog-BhK77FSu.js} +1 -1
- package/dist/web/assets/editor-dialog-BhK77FSu.js.br +0 -0
- package/dist/web/assets/editor-dialog-BhK77FSu.js.gz +0 -0
- package/dist/web/assets/{editor-language-service-6-7mS6_9.js → editor-language-service-BIN7M57r.js} +1 -1
- package/dist/web/assets/editor-language-service-BIN7M57r.js.br +4 -0
- package/dist/web/assets/editor-language-service-BIN7M57r.js.gz +0 -0
- package/dist/web/assets/{explorer-body-BlGDPmNa.js → explorer-body-DNsWiVO3.js} +1 -1
- package/dist/web/assets/explorer-body-DNsWiVO3.js.br +0 -0
- package/dist/web/assets/explorer-body-DNsWiVO3.js.gz +0 -0
- package/dist/web/assets/{explorer-window-content-BLWY8GJ_.js → explorer-window-content-DnOJYhRX.js} +1 -1
- package/dist/web/assets/explorer-window-content-DnOJYhRX.js.br +0 -0
- package/dist/web/assets/explorer-window-content-DnOJYhRX.js.gz +0 -0
- package/dist/web/assets/{extension-manager-section-NSEt5f4z.js → extension-manager-section-CJtRpUUs.js} +1 -1
- package/dist/web/assets/extension-manager-section-CJtRpUUs.js.br +0 -0
- package/dist/web/assets/extension-manager-section-CJtRpUUs.js.gz +0 -0
- package/dist/web/assets/{extension-webview-BkAEBWin.js → extension-webview-Bza7cPL1.js} +1 -1
- package/dist/web/assets/extension-webview-Bza7cPL1.js.br +0 -0
- package/dist/web/assets/extension-webview-Bza7cPL1.js.gz +0 -0
- package/dist/web/assets/{files-settings-section-Bq7liJ_r.js → files-settings-section-8rWMj1Oi.js} +1 -1
- package/dist/web/assets/files-settings-section-8rWMj1Oi.js.br +0 -0
- package/dist/web/assets/files-settings-section-8rWMj1Oi.js.gz +0 -0
- package/dist/web/assets/{general-settings-section-DvHROAMa.js → general-settings-section-joWQEgIn.js} +1 -1
- package/dist/web/assets/general-settings-section-joWQEgIn.js.br +0 -0
- package/dist/web/assets/general-settings-section-joWQEgIn.js.gz +0 -0
- package/dist/web/assets/{git-log-panel-Cu9kK48H.js → git-log-panel-BxYYWEiU.js} +1 -1
- package/dist/web/assets/git-log-panel-BxYYWEiU.js.br +0 -0
- package/dist/web/assets/git-log-panel-BxYYWEiU.js.gz +0 -0
- package/dist/web/assets/{git-review-tab-B_JT649W.js → git-review-tab-Btnvgk1e.js} +1 -1
- package/dist/web/assets/git-review-tab-Btnvgk1e.js.br +0 -0
- package/dist/web/assets/git-review-tab-Btnvgk1e.js.gz +0 -0
- package/dist/web/assets/{glide-data-grid-BlposOph.js → glide-data-grid-lCwaM1Gy.js} +1 -1
- package/dist/web/assets/glide-data-grid-lCwaM1Gy.js.br +0 -0
- package/dist/web/assets/glide-data-grid-lCwaM1Gy.js.gz +0 -0
- package/dist/web/assets/{grid-save-dialog-B0d0qhnN.js → grid-save-dialog-DoEEQ1vM.js} +1 -1
- package/dist/web/assets/grid-save-dialog-DoEEQ1vM.js.br +0 -0
- package/dist/web/assets/grid-save-dialog-DoEEQ1vM.js.gz +0 -0
- package/dist/web/assets/{group-chat-tab-jirUsGm-.js → group-chat-tab-B7Dwx9uL.js} +1 -1
- package/dist/web/assets/group-chat-tab-B7Dwx9uL.js.br +0 -0
- package/dist/web/assets/group-chat-tab-B7Dwx9uL.js.gz +0 -0
- package/dist/web/assets/{impexp-tab-HJVcOTYJ.js → impexp-tab-BtEDbQSn.js} +1 -1
- package/dist/web/assets/impexp-tab-BtEDbQSn.js.br +0 -0
- package/dist/web/assets/impexp-tab-BtEDbQSn.js.gz +0 -0
- package/dist/web/assets/index-BACBJm4i.css +1 -0
- package/dist/web/assets/index-BACBJm4i.css.br +0 -0
- package/dist/web/assets/index-BACBJm4i.css.gz +0 -0
- package/dist/web/assets/{index-DLB3j_P0.js → index-CPBujJc_.js} +5 -5
- package/dist/web/assets/index-CPBujJc_.js.br +0 -0
- package/dist/web/assets/index-CPBujJc_.js.gz +0 -0
- package/dist/web/assets/{jira-watcher-section-EQvdnETB.js → jira-watcher-section-mnly-maX.js} +1 -1
- package/dist/web/assets/{keybindings-store-CEy-Sdcc.js → keybindings-store-BSPTMRiv.js} +1 -1
- package/dist/web/assets/{keyboard-shortcuts-section-DN21gB-T.js → keyboard-shortcuts-section-DULhEWPg.js} +1 -1
- package/dist/web/assets/keyboard-shortcuts-section-DULhEWPg.js.br +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-DULhEWPg.js.gz +0 -0
- package/dist/web/assets/{language-servers-section-DOcZ72FJ.js → language-servers-section-CwBJsJ31.js} +2 -2
- package/dist/web/assets/language-servers-section-CwBJsJ31.js.br +0 -0
- package/dist/web/assets/language-servers-section-CwBJsJ31.js.gz +0 -0
- package/dist/web/assets/{logs-app-C7n9GSCn.js → logs-app-DatfI2Up.js} +1 -1
- package/dist/web/assets/logs-app-DatfI2Up.js.br +0 -0
- package/dist/web/assets/logs-app-DatfI2Up.js.gz +0 -0
- package/dist/web/assets/{logs-tab-CSOiexvx.js → logs-tab-an_9ujcz.js} +1 -1
- package/dist/web/assets/{logs-window-content-C4Qs6-0q.js → logs-window-content-Usr3JTmE.js} +1 -1
- package/dist/web/assets/{lsp-client-DpYmPhXX.js → lsp-client-tCCcXWsR.js} +1 -1
- package/dist/web/assets/lsp-client-tCCcXWsR.js.br +0 -0
- package/dist/web/assets/lsp-client-tCCcXWsR.js.gz +0 -0
- package/dist/web/assets/{markdown-renderer-CtEl6qAt.js → markdown-renderer-D0YUMQLE.js} +1 -1
- package/dist/web/assets/markdown-renderer-D0YUMQLE.js.br +0 -0
- package/dist/web/assets/markdown-renderer-D0YUMQLE.js.gz +0 -0
- package/dist/web/assets/{markdown-renderer-Df3r-CCb.js → markdown-renderer-DdzxNveP.js} +1 -1
- package/dist/web/assets/{mobile-explorer-sheet-B-rcKEL9.js → mobile-explorer-sheet-Bo27IRye.js} +1 -1
- package/dist/web/assets/mobile-explorer-sheet-Bo27IRye.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-Bo27IRye.js.gz +0 -0
- package/dist/web/assets/{new-design-dialog-DgztQK5R.js → new-design-dialog-DIOCWZW1.js} +1 -1
- package/dist/web/assets/new-design-dialog-DIOCWZW1.js.br +0 -0
- package/dist/web/assets/new-design-dialog-DIOCWZW1.js.gz +0 -0
- package/dist/web/assets/{notification-store-B92dvP_u.js → notification-store-CRPOp-6B.js} +1 -1
- package/dist/web/assets/{notifications-settings-section-JiGvaCFx.js → notifications-settings-section-DSSCTkfX.js} +1 -1
- package/dist/web/assets/notifications-settings-section-DSSCTkfX.js.br +0 -0
- package/dist/web/assets/notifications-settings-section-DSSCTkfX.js.gz +0 -0
- package/dist/web/assets/{ppmbot-settings-section-XfrZ8VoC.js → ppmbot-settings-section-BIYSlZu5.js} +1 -1
- package/dist/web/assets/ppmbot-settings-section-BIYSlZu5.js.br +0 -0
- package/dist/web/assets/ppmbot-settings-section-BIYSlZu5.js.gz +0 -0
- package/dist/web/assets/{problems-panel-CDzwSJn6.js → problems-panel-CH-00nA7.js} +1 -1
- package/dist/web/assets/problems-panel-CH-00nA7.js.br +0 -0
- package/dist/web/assets/problems-panel-CH-00nA7.js.gz +0 -0
- package/dist/web/assets/{process-table-C_B39GHm.js → process-table-DQO0g5do.js} +1 -1
- package/dist/web/assets/process-table-DQO0g5do.js.br +0 -0
- package/dist/web/assets/process-table-DQO0g5do.js.gz +0 -0
- package/dist/web/assets/{proxy-settings-section-CvaOvYIv.js → proxy-settings-section-Buo3hvQ4.js} +1 -1
- package/dist/web/assets/proxy-settings-section-Buo3hvQ4.js.br +0 -0
- package/dist/web/assets/proxy-settings-section-Buo3hvQ4.js.gz +0 -0
- package/dist/web/assets/{query-tab-B8aMpAbI.js → query-tab-CU0QVZpj.js} +1 -1
- package/dist/web/assets/query-tab-CU0QVZpj.js.br +0 -0
- package/dist/web/assets/query-tab-CU0QVZpj.js.gz +0 -0
- package/dist/web/assets/{remote-access-settings-section-Bv0eawR4.js → remote-access-settings-section-vlHMJPdY.js} +1 -1
- package/dist/web/assets/remote-access-settings-section-vlHMJPdY.js.br +0 -0
- package/dist/web/assets/remote-access-settings-section-vlHMJPdY.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-clipboard-notice-BpDakRY0.js → remote-desktop-clipboard-notice-DNqX7d1L.js} +1 -1
- package/dist/web/assets/remote-desktop-clipboard-notice-DNqX7d1L.js.br +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-DNqX7d1L.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-mobile-sheet-C5GQ3kzY.js → remote-desktop-mobile-sheet-Dz7tctpc.js} +2 -2
- package/dist/web/assets/remote-desktop-mobile-sheet-Dz7tctpc.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-Dz7tctpc.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-mobile-view-CZb5EMEK.js → remote-desktop-mobile-view-BqhQdShH.js} +1 -1
- package/dist/web/assets/remote-desktop-mobile-view-BqhQdShH.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-BqhQdShH.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-readiness-gate-CipNtLX5.js → remote-desktop-readiness-gate-rnTq4ZQS.js} +1 -1
- package/dist/web/assets/remote-desktop-readiness-gate-rnTq4ZQS.js.br +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-rnTq4ZQS.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-window-content-B7OXXvAM.js → remote-desktop-window-content-TqjHt0_5.js} +1 -1
- package/dist/web/assets/remote-desktop-window-content-TqjHt0_5.js.br +0 -0
- package/dist/web/assets/remote-desktop-window-content-TqjHt0_5.js.gz +0 -0
- package/dist/web/assets/{rename-object-dialog-B4W6S7Xg.js → rename-object-dialog-D0YpY40E.js} +1 -1
- package/dist/web/assets/rename-object-dialog-D0YpY40E.js.br +0 -0
- package/dist/web/assets/rename-object-dialog-D0YpY40E.js.gz +0 -0
- package/dist/web/assets/{review-code-B4osj0k1.js → review-code-DsF9921h.js} +1 -1
- package/dist/web/assets/review-code-DsF9921h.js.br +0 -0
- package/dist/web/assets/{review-code-B4osj0k1.js.gz → review-code-DsF9921h.js.gz} +0 -0
- package/dist/web/assets/{save-as-dialog-DpXv_f1d.js → save-as-dialog-niU7kVMJ.js} +1 -1
- package/dist/web/assets/save-as-dialog-niU7kVMJ.js.br +0 -0
- package/dist/web/assets/save-as-dialog-niU7kVMJ.js.gz +0 -0
- package/dist/web/assets/{schedules-settings-section-BbaPbc28.js → schedules-settings-section-Heuv2Kj4.js} +1 -1
- package/dist/web/assets/schedules-settings-section-Heuv2Kj4.js.br +0 -0
- package/dist/web/assets/schedules-settings-section-Heuv2Kj4.js.gz +0 -0
- package/dist/web/assets/{session-review-tab-Aq3FEooH.js → session-review-tab-Dob0FLvA.js} +1 -1
- package/dist/web/assets/session-review-tab-Dob0FLvA.js.br +0 -0
- package/dist/web/assets/session-review-tab-Dob0FLvA.js.gz +0 -0
- package/dist/web/assets/{settings-body-vXbOWchN.js → settings-body-5t6SQbLx.js} +1 -1
- package/dist/web/assets/settings-body-5t6SQbLx.js.br +0 -0
- package/dist/web/assets/settings-body-5t6SQbLx.js.gz +0 -0
- package/dist/web/assets/{settings-rows-DD7Gxmst.js → settings-rows-B0w15zot.js} +1 -1
- package/dist/web/assets/settings-rows-B0w15zot.js.br +0 -0
- package/dist/web/assets/settings-rows-B0w15zot.js.gz +0 -0
- package/dist/web/assets/{settings-section-content-DkUJFop6.js → settings-section-content-D41IJ62x.js} +2 -2
- package/dist/web/assets/settings-section-content-D41IJ62x.js.br +0 -0
- package/dist/web/assets/settings-section-content-D41IJ62x.js.gz +0 -0
- package/dist/web/assets/settings-section-content-DuVO6erO.js +1 -0
- package/dist/web/assets/{settings-tab-Cdv9_OIe.js → settings-tab-DGU5FVbO.js} +1 -1
- package/dist/web/assets/{settings-window-content-DLa6Ph4u.js → settings-window-content-Da5PY5MZ.js} +1 -1
- package/dist/web/assets/{slash-command-picker-lILYDfK3.js → slash-command-picker-DpZsPuNf.js} +1 -1
- package/dist/web/assets/slash-command-picker-DpZsPuNf.js.br +0 -0
- package/dist/web/assets/slash-command-picker-DpZsPuNf.js.gz +0 -0
- package/dist/web/assets/{sql-object-tab-Da42SMK5.js → sql-object-tab-BTS8aHGO.js} +1 -1
- package/dist/web/assets/sql-object-tab-BTS8aHGO.js.br +0 -0
- package/dist/web/assets/sql-object-tab-BTS8aHGO.js.gz +0 -0
- package/dist/web/assets/{sqlite-viewer-V3SKNukY.js → sqlite-viewer-CVo5eqWj.js} +1 -1
- package/dist/web/assets/sqlite-viewer-CVo5eqWj.js.br +0 -0
- package/dist/web/assets/sqlite-viewer-CVo5eqWj.js.gz +0 -0
- package/dist/web/assets/{structure-save-dialog-B635iyPI.js → structure-save-dialog-B9Q-Rhgj.js} +1 -1
- package/dist/web/assets/structure-save-dialog-B9Q-Rhgj.js.br +0 -0
- package/dist/web/assets/structure-save-dialog-B9Q-Rhgj.js.gz +0 -0
- package/dist/web/assets/{structure-tab-Co9d8ktU.js → structure-tab-_cQn0KG7.js} +1 -1
- package/dist/web/assets/structure-tab-_cQn0KG7.js.br +0 -0
- package/dist/web/assets/structure-tab-_cQn0KG7.js.gz +0 -0
- package/dist/web/assets/{system-monitor-body-DC0j8RqP.js → system-monitor-body-YS3i1PUs.js} +2 -2
- package/dist/web/assets/system-monitor-body-YS3i1PUs.js.br +0 -0
- package/dist/web/assets/system-monitor-body-YS3i1PUs.js.gz +0 -0
- package/dist/web/assets/{system-monitor-tab-DA8BcdXo.js → system-monitor-tab-D63xmd8U.js} +1 -1
- package/dist/web/assets/{system-monitor-window-content-BiuLo8XW.js → system-monitor-window-content-DkCsK8Ed.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-Crvt1r5F.js → tab-host-window-content-CrASOMX8.js} +1 -1
- package/dist/web/assets/tab-host-window-content-CrASOMX8.js.br +0 -0
- package/dist/web/assets/tab-host-window-content-CrASOMX8.js.gz +0 -0
- package/dist/web/assets/{table-tab-C2cYw7a0.js → table-tab-JlfqyPZR.js} +1 -1
- package/dist/web/assets/table-tab-JlfqyPZR.js.br +0 -0
- package/dist/web/assets/table-tab-JlfqyPZR.js.gz +0 -0
- package/dist/web/assets/{telegram-bot-token-field-BqNtOo5n.js → telegram-bot-token-field-ctRa2529.js} +1 -1
- package/dist/web/assets/telegram-bot-token-field-ctRa2529.js.br +0 -0
- package/dist/web/assets/telegram-bot-token-field-ctRa2529.js.gz +0 -0
- package/dist/web/assets/{terminal-tab-CrKxibGC.js → terminal-tab-BsXBBru0.js} +1 -1
- package/dist/web/assets/terminal-tab-BsXBBru0.js.br +0 -0
- package/dist/web/assets/terminal-tab-BsXBBru0.js.gz +0 -0
- package/dist/web/assets/{tool-cards-BRto67jM.js → tool-cards-BDzK03U4.js} +3 -3
- package/dist/web/assets/tool-cards-BDzK03U4.js.br +0 -0
- package/dist/web/assets/tool-cards-BDzK03U4.js.gz +0 -0
- package/dist/web/assets/{tools-settings-section-DEvZsrxq.js → tools-settings-section-DCG74pbd.js} +1 -1
- package/dist/web/assets/tools-settings-section-DCG74pbd.js.br +0 -0
- package/dist/web/assets/tools-settings-section-DCG74pbd.js.gz +0 -0
- package/dist/web/assets/{use-inline-blame-CRWJU6Rq.js → use-inline-blame-BOn_m8f-.js} +1 -1
- package/dist/web/assets/use-inline-blame-BOn_m8f-.js.br +0 -0
- package/dist/web/assets/use-inline-blame-BOn_m8f-.js.gz +0 -0
- package/dist/web/assets/{use-lsp-B-otjr-6.js → use-lsp-D144ftsf.js} +1 -1
- package/dist/web/assets/use-lsp-D144ftsf.js.br +0 -0
- package/dist/web/assets/use-lsp-D144ftsf.js.gz +0 -0
- package/dist/web/assets/{use-monaco-theme-BiDXRJ4e.js → use-monaco-theme-ArU1f9T6.js} +1 -1
- package/dist/web/assets/{use-process-kill-BW2UW0mZ.js → use-process-kill-BdmXHMFE.js} +1 -1
- package/dist/web/assets/use-process-kill-BdmXHMFE.js.br +0 -0
- package/dist/web/assets/use-process-kill-BdmXHMFE.js.gz +0 -0
- package/dist/web/assets/{use-sql-schema-info-D_Kc2FI7.js → use-sql-schema-info-BQXSMw6p.js} +1 -1
- package/dist/web/assets/use-sql-schema-info-BQXSMw6p.js.br +0 -0
- package/dist/web/assets/use-sql-schema-info-BQXSMw6p.js.gz +0 -0
- package/dist/web/assets/{use-websocket-n3Ud9vEA.js → use-websocket-rWdrMWa8.js} +1 -1
- package/dist/web/assets/{web-preview-tab-doNpegW3.js → web-preview-tab-B2OSrnl9.js} +1 -1
- package/dist/web/assets/web-preview-tab-B2OSrnl9.js.br +0 -0
- package/dist/web/assets/web-preview-tab-B2OSrnl9.js.gz +0 -0
- package/dist/web/index.html +2 -2
- package/dist/web/index.html.br +0 -0
- package/dist/web/index.html.gz +0 -0
- package/dist/web/sw.js +1 -1
- package/dist/web/sw.js.br +0 -0
- package/dist/web/sw.js.gz +0 -0
- package/docs/architecture/ai-chat-and-providers.md +22 -0
- package/package.json +1 -1
- package/src/providers/claude-agent-sdk.ts +37 -7
- package/src/providers/codex-app-server/codex-event-mapper.ts +3 -0
- package/src/providers/codex-app-server/codex-provider.ts +3 -21
- package/src/providers/codex-app-server/codex-rollout-items.ts +3 -0
- package/src/providers/codex-app-server/codex-thread-params.ts +1 -0
- package/src/providers/codex-app-server/codex-user-input.ts +41 -0
- package/src/server/ws/chat-token-rotation.ts +150 -0
- package/src/server/ws/chat.ts +28 -0
- package/src/services/account.service.ts +14 -0
- package/src/shared/chat-questions.ts +19 -0
- package/src/web/components/chat/async-question-card.tsx +24 -0
- package/src/web/components/chat/chat-reconnect-banner.tsx +54 -0
- package/src/web/components/chat/chat-tab.tsx +13 -15
- package/src/web/components/chat/message-events.tsx +6 -0
- package/src/web/components/chat/question-card.tsx +8 -10
- package/src/web/components/chat/tool-cards.tsx +4 -4
- package/src/web/hooks/use-reconnect-indicator.ts +49 -0
- package/dist/web/assets/accounts-settings-section-B5YXy0vd.js.br +0 -0
- package/dist/web/assets/accounts-settings-section-B5YXy0vd.js.gz +0 -0
- package/dist/web/assets/agent-session-sheet-Bj42PwCK.js.br +0 -0
- package/dist/web/assets/agent-session-sheet-Bj42PwCK.js.gz +0 -0
- package/dist/web/assets/agent-session-window-content-LqL6fVPU.js.br +0 -0
- package/dist/web/assets/agent-session-window-content-LqL6fVPU.js.gz +0 -0
- package/dist/web/assets/ai-resource-editor-jVv2nvAG.js.br +0 -0
- package/dist/web/assets/ai-resource-editor-jVv2nvAG.js.gz +0 -0
- package/dist/web/assets/ai-settings-section-C9Q-v8fY.js.br +0 -0
- package/dist/web/assets/ai-settings-section-C9Q-v8fY.js.gz +0 -0
- package/dist/web/assets/android-tab-CwqiuEZP.js.br +0 -0
- package/dist/web/assets/android-tab-CwqiuEZP.js.gz +0 -0
- package/dist/web/assets/appearance-settings-section-D0JIfiRx.js.br +0 -0
- package/dist/web/assets/appearance-settings-section-D0JIfiRx.js.gz +0 -0
- package/dist/web/assets/apps-panel-Cz_KJ37M.js.br +0 -0
- package/dist/web/assets/apps-panel-Cz_KJ37M.js.gz +0 -0
- package/dist/web/assets/branch-review-tab-C3QNpsm7.js.br +0 -0
- package/dist/web/assets/branch-review-tab-C3QNpsm7.js.gz +0 -0
- package/dist/web/assets/chat-tab-D14CUl0J.js.br +0 -0
- package/dist/web/assets/chat-tab-D14CUl0J.js.gz +0 -0
- package/dist/web/assets/chat-tab-D8iAVZKV.js +0 -16
- package/dist/web/assets/chat-tab-D8iAVZKV.js.br +0 -0
- package/dist/web/assets/chat-tab-D8iAVZKV.js.gz +0 -0
- package/dist/web/assets/code-editor-B7gbH4-x.js.br +0 -0
- package/dist/web/assets/codex-reset-credit-button-DQDtyvnd.js.br +0 -0
- package/dist/web/assets/codex-reset-credit-button-DQDtyvnd.js.gz +0 -0
- package/dist/web/assets/conflict-editor-ktOkrODA.js.br +0 -0
- package/dist/web/assets/conflict-editor-ktOkrODA.js.gz +0 -0
- package/dist/web/assets/connection-form-tab-CxvExR7T.js.br +0 -0
- package/dist/web/assets/connection-form-tab-CxvExR7T.js.gz +0 -0
- package/dist/web/assets/csv-preview-CBDMTEsF.js.br +0 -0
- package/dist/web/assets/csv-preview-CBDMTEsF.js.gz +0 -0
- package/dist/web/assets/db-tab-parts-BgVpKbi6.js.br +0 -0
- package/dist/web/assets/db-tab-parts-BgVpKbi6.js.gz +0 -0
- package/dist/web/assets/design-settings-section-DPGw61pY.js.br +0 -0
- package/dist/web/assets/design-settings-section-DPGw61pY.js.gz +0 -0
- package/dist/web/assets/design-tab-BiCqHNl3.js.br +0 -0
- package/dist/web/assets/design-tab-BiCqHNl3.js.gz +0 -0
- package/dist/web/assets/designs-sidebar-panel-pcJ2Rx5G.js.br +0 -0
- package/dist/web/assets/designs-sidebar-panel-pcJ2Rx5G.js.gz +0 -0
- package/dist/web/assets/diff-viewer-CLAvyqVG.js.br +0 -0
- package/dist/web/assets/diff-viewer-CLAvyqVG.js.gz +0 -0
- package/dist/web/assets/docx-preview-XSkD2IL6.js.br +0 -0
- package/dist/web/assets/docx-preview-XSkD2IL6.js.gz +0 -0
- package/dist/web/assets/editor-dialog-cQvqVZAR.js.br +0 -0
- package/dist/web/assets/editor-dialog-cQvqVZAR.js.gz +0 -0
- package/dist/web/assets/editor-language-service-6-7mS6_9.js.br +0 -0
- package/dist/web/assets/editor-language-service-6-7mS6_9.js.gz +0 -0
- package/dist/web/assets/explorer-body-BlGDPmNa.js.br +0 -0
- package/dist/web/assets/explorer-body-BlGDPmNa.js.gz +0 -0
- package/dist/web/assets/explorer-window-content-BLWY8GJ_.js.br +0 -0
- package/dist/web/assets/explorer-window-content-BLWY8GJ_.js.gz +0 -0
- package/dist/web/assets/extension-manager-section-NSEt5f4z.js.br +0 -0
- package/dist/web/assets/extension-manager-section-NSEt5f4z.js.gz +0 -0
- package/dist/web/assets/extension-webview-BkAEBWin.js.br +0 -0
- package/dist/web/assets/extension-webview-BkAEBWin.js.gz +0 -0
- package/dist/web/assets/files-settings-section-Bq7liJ_r.js.br +0 -0
- package/dist/web/assets/files-settings-section-Bq7liJ_r.js.gz +0 -0
- package/dist/web/assets/general-settings-section-DvHROAMa.js.br +0 -0
- package/dist/web/assets/general-settings-section-DvHROAMa.js.gz +0 -0
- package/dist/web/assets/git-log-panel-Cu9kK48H.js.br +0 -0
- package/dist/web/assets/git-log-panel-Cu9kK48H.js.gz +0 -0
- package/dist/web/assets/git-review-tab-B_JT649W.js.br +0 -0
- package/dist/web/assets/git-review-tab-B_JT649W.js.gz +0 -0
- package/dist/web/assets/glide-data-grid-BlposOph.js.br +0 -0
- package/dist/web/assets/glide-data-grid-BlposOph.js.gz +0 -0
- package/dist/web/assets/grid-save-dialog-B0d0qhnN.js.br +0 -0
- package/dist/web/assets/grid-save-dialog-B0d0qhnN.js.gz +0 -0
- package/dist/web/assets/group-chat-tab-jirUsGm-.js.br +0 -0
- package/dist/web/assets/group-chat-tab-jirUsGm-.js.gz +0 -0
- package/dist/web/assets/impexp-tab-HJVcOTYJ.js.br +0 -0
- package/dist/web/assets/impexp-tab-HJVcOTYJ.js.gz +0 -0
- package/dist/web/assets/index-DLB3j_P0.js.br +0 -0
- package/dist/web/assets/index-DLB3j_P0.js.gz +0 -0
- package/dist/web/assets/index-M3DUzop8.css +0 -1
- package/dist/web/assets/index-M3DUzop8.css.br +0 -0
- package/dist/web/assets/index-M3DUzop8.css.gz +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-DN21gB-T.js.br +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-DN21gB-T.js.gz +0 -0
- package/dist/web/assets/language-servers-section-DOcZ72FJ.js.br +0 -0
- package/dist/web/assets/language-servers-section-DOcZ72FJ.js.gz +0 -0
- package/dist/web/assets/logs-app-C7n9GSCn.js.br +0 -0
- package/dist/web/assets/logs-app-C7n9GSCn.js.gz +0 -0
- package/dist/web/assets/lsp-client-DpYmPhXX.js.br +0 -0
- package/dist/web/assets/lsp-client-DpYmPhXX.js.gz +0 -0
- package/dist/web/assets/markdown-renderer-CtEl6qAt.js.br +0 -0
- package/dist/web/assets/markdown-renderer-CtEl6qAt.js.gz +0 -0
- package/dist/web/assets/mobile-explorer-sheet-B-rcKEL9.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-B-rcKEL9.js.gz +0 -0
- package/dist/web/assets/new-design-dialog-DgztQK5R.js.br +0 -0
- package/dist/web/assets/new-design-dialog-DgztQK5R.js.gz +0 -0
- package/dist/web/assets/notifications-settings-section-JiGvaCFx.js.br +0 -0
- package/dist/web/assets/notifications-settings-section-JiGvaCFx.js.gz +0 -0
- package/dist/web/assets/ppmbot-settings-section-XfrZ8VoC.js.br +0 -0
- package/dist/web/assets/ppmbot-settings-section-XfrZ8VoC.js.gz +0 -0
- package/dist/web/assets/problems-panel-CDzwSJn6.js.br +0 -0
- package/dist/web/assets/problems-panel-CDzwSJn6.js.gz +0 -0
- package/dist/web/assets/process-table-C_B39GHm.js.br +0 -0
- package/dist/web/assets/process-table-C_B39GHm.js.gz +0 -0
- package/dist/web/assets/proxy-settings-section-CvaOvYIv.js.br +0 -0
- package/dist/web/assets/proxy-settings-section-CvaOvYIv.js.gz +0 -0
- package/dist/web/assets/query-tab-B8aMpAbI.js.br +0 -0
- package/dist/web/assets/query-tab-B8aMpAbI.js.gz +0 -0
- package/dist/web/assets/remote-access-settings-section-Bv0eawR4.js.br +0 -0
- package/dist/web/assets/remote-access-settings-section-Bv0eawR4.js.gz +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-BpDakRY0.js.br +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-BpDakRY0.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-C5GQ3kzY.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-C5GQ3kzY.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-CZb5EMEK.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-CZb5EMEK.js.gz +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-CipNtLX5.js.br +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-CipNtLX5.js.gz +0 -0
- package/dist/web/assets/remote-desktop-window-content-B7OXXvAM.js.br +0 -0
- package/dist/web/assets/remote-desktop-window-content-B7OXXvAM.js.gz +0 -0
- package/dist/web/assets/rename-object-dialog-B4W6S7Xg.js.br +0 -0
- package/dist/web/assets/rename-object-dialog-B4W6S7Xg.js.gz +0 -0
- package/dist/web/assets/review-code-B4osj0k1.js.br +0 -0
- package/dist/web/assets/save-as-dialog-DpXv_f1d.js.br +0 -5
- package/dist/web/assets/save-as-dialog-DpXv_f1d.js.gz +0 -0
- package/dist/web/assets/schedules-settings-section-BbaPbc28.js.br +0 -0
- package/dist/web/assets/schedules-settings-section-BbaPbc28.js.gz +0 -0
- package/dist/web/assets/session-review-tab-Aq3FEooH.js.br +0 -0
- package/dist/web/assets/session-review-tab-Aq3FEooH.js.gz +0 -0
- package/dist/web/assets/settings-body-vXbOWchN.js.br +0 -0
- package/dist/web/assets/settings-body-vXbOWchN.js.gz +0 -0
- package/dist/web/assets/settings-rows-DD7Gxmst.js.br +0 -0
- package/dist/web/assets/settings-rows-DD7Gxmst.js.gz +0 -0
- package/dist/web/assets/settings-section-content-CVtvJ6ze.js +0 -1
- package/dist/web/assets/settings-section-content-DkUJFop6.js.br +0 -0
- package/dist/web/assets/settings-section-content-DkUJFop6.js.gz +0 -0
- package/dist/web/assets/slash-command-picker-lILYDfK3.js.br +0 -0
- package/dist/web/assets/slash-command-picker-lILYDfK3.js.gz +0 -0
- package/dist/web/assets/sql-object-tab-Da42SMK5.js.br +0 -0
- package/dist/web/assets/sql-object-tab-Da42SMK5.js.gz +0 -0
- package/dist/web/assets/sqlite-viewer-V3SKNukY.js.br +0 -0
- package/dist/web/assets/sqlite-viewer-V3SKNukY.js.gz +0 -0
- package/dist/web/assets/structure-save-dialog-B635iyPI.js.br +0 -0
- package/dist/web/assets/structure-save-dialog-B635iyPI.js.gz +0 -0
- package/dist/web/assets/structure-tab-Co9d8ktU.js.br +0 -0
- package/dist/web/assets/structure-tab-Co9d8ktU.js.gz +0 -0
- package/dist/web/assets/system-monitor-body-DC0j8RqP.js.br +0 -0
- package/dist/web/assets/system-monitor-body-DC0j8RqP.js.gz +0 -0
- package/dist/web/assets/tab-host-window-content-Crvt1r5F.js.br +0 -0
- package/dist/web/assets/tab-host-window-content-Crvt1r5F.js.gz +0 -0
- package/dist/web/assets/table-tab-C2cYw7a0.js.br +0 -0
- package/dist/web/assets/table-tab-C2cYw7a0.js.gz +0 -0
- package/dist/web/assets/telegram-bot-token-field-BqNtOo5n.js.br +0 -0
- package/dist/web/assets/telegram-bot-token-field-BqNtOo5n.js.gz +0 -0
- package/dist/web/assets/terminal-tab-CrKxibGC.js.br +0 -0
- package/dist/web/assets/terminal-tab-CrKxibGC.js.gz +0 -0
- package/dist/web/assets/tool-cards-BRto67jM.js.br +0 -0
- package/dist/web/assets/tool-cards-BRto67jM.js.gz +0 -0
- package/dist/web/assets/tools-settings-section-DEvZsrxq.js.br +0 -0
- package/dist/web/assets/tools-settings-section-DEvZsrxq.js.gz +0 -0
- package/dist/web/assets/use-inline-blame-CRWJU6Rq.js.br +0 -0
- package/dist/web/assets/use-inline-blame-CRWJU6Rq.js.gz +0 -0
- package/dist/web/assets/use-lsp-B-otjr-6.js.br +0 -0
- package/dist/web/assets/use-lsp-B-otjr-6.js.gz +0 -0
- package/dist/web/assets/use-process-kill-BW2UW0mZ.js.br +0 -0
- package/dist/web/assets/use-process-kill-BW2UW0mZ.js.gz +0 -0
- package/dist/web/assets/use-sql-schema-info-D_Kc2FI7.js.br +0 -0
- package/dist/web/assets/use-sql-schema-info-D_Kc2FI7.js.gz +0 -0
- package/dist/web/assets/web-preview-tab-doNpegW3.js.br +0 -0
- package/dist/web/assets/web-preview-tab-doNpegW3.js.gz +0 -0
|
@@ -221,6 +221,30 @@ interface StreamingSession {
|
|
|
221
221
|
lastUserContent: string;
|
|
222
222
|
/** Latest user message images — updated on follow-ups for accurate retry */
|
|
223
223
|
lastUserImages?: Array<{ data: string; mediaType: string }>;
|
|
224
|
+
/** The sign-in this subprocess was started with — see `SubprocessCredential`. */
|
|
225
|
+
credential: SubprocessCredential;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The OAuth token a CLI subprocess holds, fixed for its whole life.
|
|
230
|
+
*
|
|
231
|
+
* The CLI cannot renew a token handed to it in `CLAUDE_CODE_OAUTH_TOKEN`: it carries no
|
|
232
|
+
* refresh token, and the CLI's own 401 recovery deliberately keeps a user-supplied token
|
|
233
|
+
* rather than adopting one from disk. Its only other renewal paths — the SDK's
|
|
234
|
+
* `getOAuthToken` callback and the rotated-token file — are enabled for Anthropic's own
|
|
235
|
+
* clients alone (the callback is gated on `CLAUDE_CODE_ENTRYPOINT` being Desktop, Cowork or
|
|
236
|
+
* VS Code). So when PPM renews an account's token, which revokes the previous one, every
|
|
237
|
+
* subprocess still holding it fails its next request. Recording the token per subprocess is
|
|
238
|
+
* what lets the chat layer renew while an account is quiet and restart idle subprocesses
|
|
239
|
+
* onto the new token between turns (`src/server/ws/chat-token-rotation.ts`).
|
|
240
|
+
*/
|
|
241
|
+
export interface SubprocessCredential {
|
|
242
|
+
accountId: string | null;
|
|
243
|
+
oauthToken: string;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function credentialOf(env: Record<string, string | undefined>, accountId: string | undefined): SubprocessCredential {
|
|
247
|
+
return { accountId: accountId ?? null, oauthToken: env.CLAUDE_CODE_OAUTH_TOKEN ?? "" };
|
|
224
248
|
}
|
|
225
249
|
|
|
226
250
|
/**
|
|
@@ -825,6 +849,11 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
825
849
|
return this.streamingSessions.has(sessionId);
|
|
826
850
|
}
|
|
827
851
|
|
|
852
|
+
/** Every session with a live subprocess, with the sign-in that subprocess holds. */
|
|
853
|
+
liveSubprocessCredentials(): Array<{ sessionId: string; credential: SubprocessCredential }> {
|
|
854
|
+
return [...this.streamingSessions].map(([sessionId, ss]) => ({ sessionId, credential: ss.credential }));
|
|
855
|
+
}
|
|
856
|
+
|
|
828
857
|
/**
|
|
829
858
|
* How long this session's prompt cache is worth holding a subprocess for.
|
|
830
859
|
*
|
|
@@ -1390,6 +1419,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1390
1419
|
if (latest) account = latest;
|
|
1391
1420
|
}
|
|
1392
1421
|
const queryEnv = this.buildQueryEnv(meta.projectPath, account);
|
|
1422
|
+
const queryCredential = credentialOf(queryEnv, account?.id);
|
|
1393
1423
|
|
|
1394
1424
|
// Pre-flight: warn if no credentials at all (avoids 2-minute silent timeout)
|
|
1395
1425
|
if (!account) {
|
|
@@ -1503,7 +1533,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1503
1533
|
// The query's one start line: a warm CLI and the MCP servers belong to it.
|
|
1504
1534
|
log.info(`query: session=${sessionId} isFirst=${isFirstMessage} fork=${shouldFork} cwd=${effectiveCwd} platform=${process.platform} accountMode=${!!account} permissionMode=${permissionMode} isBypass=${isBypass}`
|
|
1505
1535
|
+ ` cli=${spare ? `warm ageMs=${spare.ageMs}` : "cold"}${crashRetryCount > 0 ? ` crashRetry=${crashRetryCount}` : ""} mcp=${Object.keys(mcpServers).join(",") || "none"}`);
|
|
1506
|
-
this.streamingSessions.set(sessionId, { meta, query: initialQuery, controller: initialCtrl, lastUserContent: initContent, lastUserImages: initImages });
|
|
1536
|
+
this.streamingSessions.set(sessionId, { meta, query: initialQuery, controller: initialCtrl, lastUserContent: initContent, lastUserImages: initImages, credential: queryCredential });
|
|
1507
1537
|
this.activeQueries.set(sessionId, initialQuery);
|
|
1508
1538
|
let eventSource: AsyncIterable<any> = initialQuery;
|
|
1509
1539
|
log.debug(`session=${sessionId} query() created, waiting for first SDK event...`);
|
|
@@ -1531,7 +1561,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1531
1561
|
prompt: generator,
|
|
1532
1562
|
options: { ...opts, hooks: queryHooks, canUseTool } as any,
|
|
1533
1563
|
});
|
|
1534
|
-
this.streamingSessions.set(sessionId, { meta, query: rq, controller, lastUserContent: retry.lastUserContent, lastUserImages: retry.lastUserImages });
|
|
1564
|
+
this.streamingSessions.set(sessionId, { meta, query: rq, controller, lastUserContent: retry.lastUserContent, lastUserImages: retry.lastUserImages, credential: credentialOf(env, acc?.id) });
|
|
1535
1565
|
this.activeQueries.set(sessionId, rq);
|
|
1536
1566
|
return rq;
|
|
1537
1567
|
};
|
|
@@ -1600,7 +1630,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1600
1630
|
prompt: retryGen,
|
|
1601
1631
|
options: { ...retryOpts, hooks: queryHooks, canUseTool } as any,
|
|
1602
1632
|
});
|
|
1603
|
-
this.streamingSessions.set(sessionId, { meta, query: rq, controller: retryCtrl, lastUserContent: retry1.lastUserContent, lastUserImages: retry1.lastUserImages });
|
|
1633
|
+
this.streamingSessions.set(sessionId, { meta, query: rq, controller: retryCtrl, lastUserContent: retry1.lastUserContent, lastUserImages: retry1.lastUserImages, credential: queryCredential });
|
|
1604
1634
|
this.activeQueries.set(sessionId, rq);
|
|
1605
1635
|
eventSource = rq;
|
|
1606
1636
|
continue retryLoop;
|
|
@@ -1686,7 +1716,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1686
1716
|
prompt: earlyAuthGen,
|
|
1687
1717
|
options: { ...retryOpts, hooks: queryHooks, canUseTool } as any,
|
|
1688
1718
|
});
|
|
1689
|
-
this.streamingSessions.set(sessionId, { meta, query: rq, controller: earlyAuthCtrl, lastUserContent: retry2.lastUserContent, lastUserImages: retry2.lastUserImages });
|
|
1719
|
+
this.streamingSessions.set(sessionId, { meta, query: rq, controller: earlyAuthCtrl, lastUserContent: retry2.lastUserContent, lastUserImages: retry2.lastUserImages, credential: credentialOf(retryEnv, account?.id) });
|
|
1690
1720
|
this.activeQueries.set(sessionId, rq);
|
|
1691
1721
|
eventSource = rq;
|
|
1692
1722
|
continue retryLoop;
|
|
@@ -1913,7 +1943,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1913
1943
|
prompt: authRetryGen,
|
|
1914
1944
|
options: { ...retryOpts, hooks: queryHooks, canUseTool } as any,
|
|
1915
1945
|
});
|
|
1916
|
-
this.streamingSessions.set(sessionId, { meta, query: rq, controller: authRetryCtrl, lastUserContent: retry3.lastUserContent, lastUserImages: retry3.lastUserImages });
|
|
1946
|
+
this.streamingSessions.set(sessionId, { meta, query: rq, controller: authRetryCtrl, lastUserContent: retry3.lastUserContent, lastUserImages: retry3.lastUserImages, credential: credentialOf(retryEnv, account?.id) });
|
|
1917
1947
|
this.activeQueries.set(sessionId, rq);
|
|
1918
1948
|
eventSource = rq;
|
|
1919
1949
|
continue retryLoop;
|
|
@@ -1950,7 +1980,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1950
1980
|
prompt: ulRetryGen,
|
|
1951
1981
|
options: { ...retryOpts, hooks: queryHooks, canUseTool } as any,
|
|
1952
1982
|
});
|
|
1953
|
-
this.streamingSessions.set(sessionId, { meta, query: rq, controller: ulRetryCtrl, lastUserContent: retryU.lastUserContent, lastUserImages: retryU.lastUserImages });
|
|
1983
|
+
this.streamingSessions.set(sessionId, { meta, query: rq, controller: ulRetryCtrl, lastUserContent: retryU.lastUserContent, lastUserImages: retryU.lastUserImages, credential: credentialOf(ulRetryEnv, account?.id) });
|
|
1954
1984
|
this.activeQueries.set(sessionId, rq);
|
|
1955
1985
|
eventSource = rq;
|
|
1956
1986
|
continue retryLoop;
|
|
@@ -2171,7 +2201,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
2171
2201
|
prompt: authRetryGen2,
|
|
2172
2202
|
options: { ...retryOpts, hooks: queryHooks, canUseTool } as any,
|
|
2173
2203
|
});
|
|
2174
|
-
this.streamingSessions.set(sessionId, { meta, query: rq, controller: authRetryCtrl2, lastUserContent: retry6.lastUserContent, lastUserImages: retry6.lastUserImages });
|
|
2204
|
+
this.streamingSessions.set(sessionId, { meta, query: rq, controller: authRetryCtrl2, lastUserContent: retry6.lastUserContent, lastUserImages: retry6.lastUserImages, credential: credentialOf(retryEnv, account?.id) });
|
|
2175
2205
|
this.activeQueries.set(sessionId, rq);
|
|
2176
2206
|
eventSource = rq;
|
|
2177
2207
|
continue retryLoop;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ChatEvent } from "../provider.interface.ts";
|
|
2
2
|
import type { TurnUsage } from "../../shared/turn-usage.ts";
|
|
3
3
|
import { redactTruncate } from "./codex-redact.ts";
|
|
4
|
+
import { codexAsyncQuestionEvent } from "./codex-user-input.ts";
|
|
4
5
|
import { changeToToolUse, fileUpdateChanges } from "./codex-patch.ts";
|
|
5
6
|
import { parseSubagentActivity, subagentToolResult, subagentToolUse } from "./codex-subagent-thread.ts";
|
|
6
7
|
|
|
@@ -237,6 +238,8 @@ export function mapCodexEvent(notif: Notif, sessionId: string): ChatEvent[] {
|
|
|
237
238
|
|
|
238
239
|
case "item/completed": {
|
|
239
240
|
const item = asObj(p.item) as Item;
|
|
241
|
+
const question = codexAsyncQuestionEvent(item);
|
|
242
|
+
if (question) return [question];
|
|
240
243
|
if (item.type === "contextCompaction") return [{ type: "system", subtype: "compact_done" }];
|
|
241
244
|
const subagent = parseSubagentActivity(item);
|
|
242
245
|
// Answers the card opened at `started` — keyed on the thread, because the
|
|
@@ -51,6 +51,7 @@ import { getOrFetchUsage, registerUsageSource } from "../../services/provider-us
|
|
|
51
51
|
import { codexUsageSource } from "./codex-usage-source.ts";
|
|
52
52
|
import { AMBIENT_ACCOUNT_KEY } from "../../services/provider-usage/usage-source.ts";
|
|
53
53
|
import { redactTruncate } from "./codex-redact.ts";
|
|
54
|
+
import { buildUserInputResponse, normalizeCodexQuestions } from "./codex-user-input.ts";
|
|
54
55
|
import { localizeRollout } from "./codex-rollout-transfer.ts";
|
|
55
56
|
import { getRolloutMessagesAsync } from "./codex-history-async.ts";
|
|
56
57
|
import {
|
|
@@ -62,7 +63,6 @@ import type {
|
|
|
62
63
|
ModelListResponse,
|
|
63
64
|
ServerRequest,
|
|
64
65
|
JsonRpcNotification,
|
|
65
|
-
ToolRequestUserInputResponse,
|
|
66
66
|
Thread,
|
|
67
67
|
UserInput,
|
|
68
68
|
CodexSkill,
|
|
@@ -328,24 +328,6 @@ function approvalToolLabel(method: string, params: unknown): string {
|
|
|
328
328
|
return String(p.tool ?? "Tool");
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
function buildUserInputResponse(questions: unknown, data: unknown): ToolRequestUserInputResponse {
|
|
332
|
-
const answers: ToolRequestUserInputResponse["answers"] = {};
|
|
333
|
-
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
334
|
-
for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
|
|
335
|
-
answers[k] = { answers: Array.isArray(v) ? v.map(String) : [String(v)] };
|
|
336
|
-
}
|
|
337
|
-
return { answers };
|
|
338
|
-
}
|
|
339
|
-
const arr = Array.isArray(data) ? data : data != null ? [data] : [];
|
|
340
|
-
const qs = Array.isArray(questions) ? questions : [];
|
|
341
|
-
qs.forEach((q, i) => {
|
|
342
|
-
const qid = (q && typeof q === "object" && ((q as any).id ?? (q as any).questionId)) || String(i);
|
|
343
|
-
const a = arr[i];
|
|
344
|
-
answers[qid] = { answers: a != null ? [String(a)] : [] };
|
|
345
|
-
});
|
|
346
|
-
return { answers };
|
|
347
|
-
}
|
|
348
|
-
|
|
349
331
|
/**
|
|
350
332
|
* Codex provider — implements AIProvider directly (mirrors claude-agent-sdk),
|
|
351
333
|
* driving `codex app-server` over JSON-RPC. Per-session live subprocess map;
|
|
@@ -1160,7 +1142,7 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
1160
1142
|
const ppmReqId = crypto.randomUUID();
|
|
1161
1143
|
const questions = (req.params as { questions?: unknown })?.questions;
|
|
1162
1144
|
live.pendingApprovals.set(ppmReqId, { codexId: req.id, method, questions });
|
|
1163
|
-
live.channel.push({ type: "approval_request", requestId: ppmReqId, tool: "AskUserQuestion", input:
|
|
1145
|
+
live.channel.push({ type: "approval_request", requestId: ppmReqId, tool: "AskUserQuestion", input: { questions: normalizeCodexQuestions(questions) } });
|
|
1164
1146
|
return;
|
|
1165
1147
|
}
|
|
1166
1148
|
// permissions/* response is a granted-profile, not a decision → decline.
|
|
@@ -1174,7 +1156,7 @@ export class CodexAppServerProvider implements AIProvider {
|
|
|
1174
1156
|
if (!pending) continue;
|
|
1175
1157
|
live.pendingApprovals.delete(requestId);
|
|
1176
1158
|
if (pending.method === "item/tool/requestUserInput") {
|
|
1177
|
-
this.respondOnce(live, pending.codexId, buildUserInputResponse(pending.questions, data));
|
|
1159
|
+
this.respondOnce(live, pending.codexId, buildUserInputResponse(pending.questions, approved ? data : undefined));
|
|
1178
1160
|
} else if (isApprovalMethod(pending.method)) {
|
|
1179
1161
|
this.respondOnce(live, pending.codexId, decisionFor(pending.method as ApprovalMethod, approved));
|
|
1180
1162
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ChatEvent } from "../provider.interface.ts";
|
|
2
2
|
import { redactTruncate } from "./codex-redact.ts";
|
|
3
|
+
import { codexAsyncQuestionEvent } from "./codex-user-input.ts";
|
|
3
4
|
import { parseSubagentActivity, type SubagentActivity } from "./codex-subagent-thread.ts";
|
|
4
5
|
import { changeToToolUse, rolloutFileChanges } from "./codex-patch.ts";
|
|
5
6
|
|
|
@@ -161,6 +162,8 @@ export function mapRolloutItem(item: unknown): RolloutItemMapping {
|
|
|
161
162
|
return text ? { kind: "user", text } : { kind: "ignore" };
|
|
162
163
|
}
|
|
163
164
|
if (type === "AgentMessage") {
|
|
165
|
+
const question = codexAsyncQuestionEvent(it);
|
|
166
|
+
if (question) return { kind: "events", events: [question] };
|
|
164
167
|
const text = textFrom(it.content);
|
|
165
168
|
return text ? { kind: "assistant", text } : { kind: "ignore" };
|
|
166
169
|
}
|
|
@@ -116,6 +116,7 @@ export function dbToolsMcpEnv(access: DbToolsMcpAccess | undefined): Record<stri
|
|
|
116
116
|
export function buildThreadParams(input: ThreadParamsInput): CodexThreadParams {
|
|
117
117
|
const instructions = input.developerInstructions?.trim();
|
|
118
118
|
const config = {
|
|
119
|
+
"features.default_mode_request_user_input": true,
|
|
119
120
|
...(input.configOverrides?.config ?? {}),
|
|
120
121
|
...designMcpConfig(input.designMcp),
|
|
121
122
|
...tabToolsMcpConfig(input.tabToolsMcp),
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ChatEvent } from "../provider.interface.ts";
|
|
2
|
+
import { ASYNC_QUESTION_TOOL, type ChatQuestion } from "../../shared/chat-questions.ts";
|
|
3
|
+
import type { ToolRequestUserInputResponse } from "./codex-protocol.ts";
|
|
4
|
+
|
|
5
|
+
export function normalizeCodexQuestions(value: unknown): ChatQuestion[] {
|
|
6
|
+
if (!Array.isArray(value)) return [];
|
|
7
|
+
return value.flatMap((value, index) => {
|
|
8
|
+
if (!value || typeof value !== "object") return [];
|
|
9
|
+
const question = typeof value.question === "string" ? value.question : value.title;
|
|
10
|
+
if (typeof question !== "string" || !question.trim()) return [];
|
|
11
|
+
const options = Array.isArray(value.options) ? value.options.flatMap((option: unknown) => {
|
|
12
|
+
if (typeof option === "string") return [{ label: option }];
|
|
13
|
+
if (!option || typeof option !== "object" || !("label" in option) || typeof option.label !== "string") return [];
|
|
14
|
+
return [{ label: option.label, ...("description" in option && typeof option.description === "string" ? { description: option.description } : {}) }];
|
|
15
|
+
}) : [];
|
|
16
|
+
return [{ id: typeof value.id === "string" ? value.id : String(index), question,
|
|
17
|
+
...(typeof value.header === "string" ? { header: value.header } : {}), options }];
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function codexAsyncQuestionEvent(item: Record<string, unknown>): ChatEvent | null {
|
|
22
|
+
if (item.type !== "agentMessage" && item.type !== "AgentMessage") return null;
|
|
23
|
+
const questions = normalizeCodexQuestions(item.questions);
|
|
24
|
+
if (typeof item.id !== "string" || !questions.length) return null;
|
|
25
|
+
return { type: "tool_use", tool: ASYNC_QUESTION_TOOL, toolUseId: item.id, input: { questions } };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildUserInputResponse(questions: unknown, data: unknown): ToolRequestUserInputResponse {
|
|
29
|
+
const answers: ToolRequestUserInputResponse["answers"] = {};
|
|
30
|
+
if (!data || typeof data !== "object") return { answers };
|
|
31
|
+
normalizeCodexQuestions(questions).forEach((question, index) => {
|
|
32
|
+
const id = question.id!;
|
|
33
|
+
const value = Array.isArray(data) ? data[index]
|
|
34
|
+
: Object.hasOwn(data, id) ? (data as Record<string, unknown>)[id]
|
|
35
|
+
: Object.hasOwn(data, question.question) ? (data as Record<string, unknown>)[question.question] : undefined;
|
|
36
|
+
if (value != null) Object.defineProperty(answers, id, {
|
|
37
|
+
value: { answers: Array.isArray(value) ? value.map(String) : [String(value)] }, enumerable: true,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
return { answers };
|
|
41
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renews Claude sign-ins while nothing is using them, and moves idle chats onto the new token.
|
|
3
|
+
*
|
|
4
|
+
* Every chat subprocess is started with its account's access token in
|
|
5
|
+
* `CLAUDE_CODE_OAUTH_TOKEN` and cannot renew it (`SubprocessCredential` in
|
|
6
|
+
* `claude-agent-sdk.ts` says why). Tokens live eight hours, and renewing one revokes the
|
|
7
|
+
* previous token, so a renewal used to land in the middle of whatever turn happened to be
|
|
8
|
+
* running: its next request got a 401, the provider rebuilt the subprocess, and every
|
|
9
|
+
* subagent still working inside it died with it.
|
|
10
|
+
*
|
|
11
|
+
* So a token is renewed early — inside `QUIET_REFRESH_WINDOW_S` of expiry — at a moment
|
|
12
|
+
* when no turn, approval or background work is running on that account. Idle subprocesses
|
|
13
|
+
* still holding the old token are then dropped, the same way a model switch drops them, and
|
|
14
|
+
* their next message resumes on the new one. A subprocess that is busy is left alone and
|
|
15
|
+
* dropped once its turn is over. Only an account kept busy for the whole window still meets
|
|
16
|
+
* the point-of-use renewal mid-turn, and the provider's 401 recovery covers that, as before.
|
|
17
|
+
*
|
|
18
|
+
* Accounts no subprocess is using are not touched: they are renewed where they are next
|
|
19
|
+
* used, which is the rule `account.service.ts` follows on purpose.
|
|
20
|
+
*/
|
|
21
|
+
import { accountService } from "../../services/account.service.ts";
|
|
22
|
+
import { createLogger } from "../../services/logger.ts";
|
|
23
|
+
import type { SubprocessCredential } from "../../providers/claude-agent-sdk.ts";
|
|
24
|
+
|
|
25
|
+
const log = createLogger("token-rotation");
|
|
26
|
+
|
|
27
|
+
/** Renew this close to expiry, as soon as nothing is running on the account. */
|
|
28
|
+
export const QUIET_REFRESH_WINDOW_S = 2 * 3600;
|
|
29
|
+
/** Inside this the point-of-use renewal owns the token (`PREFLIGHT_REFRESH_BUFFER_S`). */
|
|
30
|
+
const POINT_OF_USE_BUFFER_S = 300;
|
|
31
|
+
const SWEEP_INTERVAL_MS = 60_000;
|
|
32
|
+
|
|
33
|
+
export interface LiveSubprocess {
|
|
34
|
+
sessionId: string;
|
|
35
|
+
credential: SubprocessCredential;
|
|
36
|
+
/** A turn, an approval or background work is running in it — dropping it would lose work. */
|
|
37
|
+
busy: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RenewalCandidate {
|
|
41
|
+
id: string;
|
|
42
|
+
expiresAt: number | null;
|
|
43
|
+
/** An OAuth account with a refresh token that the server has not rejected. */
|
|
44
|
+
renewable: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Accounts to renew now: in use by some subprocess, near expiry, and busy in none. */
|
|
48
|
+
export function accountsToRenewNow(candidates: RenewalCandidate[], live: LiveSubprocess[], nowS: number): string[] {
|
|
49
|
+
const inUse = new Set<string>();
|
|
50
|
+
const busy = new Set<string>();
|
|
51
|
+
for (const p of live) {
|
|
52
|
+
const id = p.credential.accountId;
|
|
53
|
+
if (!id || !p.credential.oauthToken) continue;
|
|
54
|
+
inUse.add(id);
|
|
55
|
+
if (p.busy) busy.add(id);
|
|
56
|
+
}
|
|
57
|
+
return candidates
|
|
58
|
+
.filter((c) => {
|
|
59
|
+
if (!c.renewable || c.expiresAt == null || !inUse.has(c.id) || busy.has(c.id)) return false;
|
|
60
|
+
const left = c.expiresAt - nowS;
|
|
61
|
+
return left <= QUIET_REFRESH_WINDOW_S && left > POINT_OF_USE_BUFFER_S;
|
|
62
|
+
})
|
|
63
|
+
.map((c) => c.id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Idle subprocesses still holding a token their account no longer has. */
|
|
67
|
+
export function staleIdleSessions(live: LiveSubprocess[], currentToken: (accountId: string) => string | null): string[] {
|
|
68
|
+
return live
|
|
69
|
+
.filter((p) => {
|
|
70
|
+
const id = p.credential.accountId;
|
|
71
|
+
if (p.busy || !id || !p.credential.oauthToken) return false;
|
|
72
|
+
const current = currentToken(id);
|
|
73
|
+
return current !== null && current !== p.credential.oauthToken;
|
|
74
|
+
})
|
|
75
|
+
.map((p) => p.sessionId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface TokenRotationDeps {
|
|
79
|
+
/** Every live Claude subprocess and whether work is running in it right now. */
|
|
80
|
+
liveSubprocesses: () => LiveSubprocess[];
|
|
81
|
+
/** Restart one idle subprocess; its next message resumes on the account's current token. */
|
|
82
|
+
dropIdle: (sessionId: string) => void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function currentOAuthToken(accountId: string): string | null {
|
|
86
|
+
const token = accountService.getWithTokens(accountId)?.accessToken;
|
|
87
|
+
return token && token.startsWith("sk-ant-oat") ? token : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Drop every idle subprocess whose token has been replaced. Safe to call at any time. */
|
|
91
|
+
export function dropStaleIdle(deps: TokenRotationDeps): void {
|
|
92
|
+
for (const sessionId of staleIdleSessions(deps.liveSubprocesses(), currentOAuthToken)) {
|
|
93
|
+
log.info(`session=${sessionId} restarting idle subprocess: its account's token was renewed`);
|
|
94
|
+
deps.dropIdle(sessionId);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** One pass: renew what is due and quiet, then move idle subprocesses onto new tokens. */
|
|
99
|
+
export async function sweepTokenRotation(deps: TokenRotationDeps, nowS = Math.floor(Date.now() / 1000)): Promise<void> {
|
|
100
|
+
const candidates: RenewalCandidate[] = accountService.list().map((a) => {
|
|
101
|
+
const acc = accountService.getWithTokens(a.id);
|
|
102
|
+
return {
|
|
103
|
+
id: a.id,
|
|
104
|
+
expiresAt: acc?.expiresAt ?? null,
|
|
105
|
+
renewable: !!acc && acc.accessToken.startsWith("sk-ant-oat") && acc.refreshToken !== "" && !acc.reauthRequired,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
for (const id of accountsToRenewNow(candidates, deps.liveSubprocesses(), nowS)) {
|
|
109
|
+
const left = (candidates.find((c) => c.id === id)?.expiresAt ?? nowS) - nowS;
|
|
110
|
+
log.info(`Renewing ${id} early while no turn runs on it (token expires in ${left}s)`);
|
|
111
|
+
try {
|
|
112
|
+
await accountService.refreshAccessToken(id, false, false, QUIET_REFRESH_WINDOW_S);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
// The point-of-use renewal tries again before the token is needed.
|
|
115
|
+
log.warn(`Early renewal failed for ${id}: ${(e as Error).message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
dropStaleIdle(deps);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Survive Bun --hot reloads: one timer and one listener, whichever module copy registered last.
|
|
122
|
+
const HOT_KEY = "__PPM_TOKEN_ROTATION__" as const;
|
|
123
|
+
const hot = ((globalThis as any)[HOT_KEY] ??= { stop: null, deps: null }) as {
|
|
124
|
+
stop: (() => void) | null;
|
|
125
|
+
deps: TokenRotationDeps | null;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** Run a sweep now with the registered chat layer, rather than waiting for the timer. */
|
|
129
|
+
export async function runTokenRotationSweep(): Promise<void> {
|
|
130
|
+
if (hot.deps) await sweepTokenRotation(hot.deps);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function registerTokenRotation(deps: TokenRotationDeps): () => void {
|
|
134
|
+
hot.stop?.();
|
|
135
|
+
hot.deps = deps;
|
|
136
|
+
let sweeping = false;
|
|
137
|
+
const sweep = () => {
|
|
138
|
+
if (sweeping) return;
|
|
139
|
+
sweeping = true;
|
|
140
|
+
sweepTokenRotation(deps)
|
|
141
|
+
.catch((e) => log.warn(`token rotation sweep failed: ${(e as Error).message}`))
|
|
142
|
+
.finally(() => { sweeping = false; });
|
|
143
|
+
};
|
|
144
|
+
const timer = setInterval(sweep, SWEEP_INTERVAL_MS);
|
|
145
|
+
(timer as { unref?: () => void }).unref?.();
|
|
146
|
+
// Any renewal — this sweep's, a turn's pre-flight, the usage poller's — strands idle subprocesses.
|
|
147
|
+
const unlisten = accountService.onTokenRotated(() => dropStaleIdle(deps));
|
|
148
|
+
hot.stop = () => { clearInterval(timer); unlisten(); };
|
|
149
|
+
return hot.stop;
|
|
150
|
+
}
|
package/src/server/ws/chat.ts
CHANGED
|
@@ -33,6 +33,8 @@ import { parseTabOpenResult, type TabOpenRequest } from "../../shared/tab-open-p
|
|
|
33
33
|
import { readLastTurnStop } from "../../services/session-trace/turn-stop-reader.ts";
|
|
34
34
|
import { describeTurnStop, type TurnStop } from "../../shared/turn-stop.ts";
|
|
35
35
|
import { createLogger } from "../../services/logger.ts";
|
|
36
|
+
import { dropStaleIdle, registerTokenRotation, type TokenRotationDeps } from "./chat-token-rotation.ts";
|
|
37
|
+
import type { SubprocessCredential } from "../../providers/claude-agent-sdk.ts";
|
|
36
38
|
|
|
37
39
|
const log = createLogger("chat");
|
|
38
40
|
const bgShellLog = createLogger("bg-shell");
|
|
@@ -436,6 +438,28 @@ registerMcpSignInSync({
|
|
|
436
438
|
),
|
|
437
439
|
});
|
|
438
440
|
|
|
441
|
+
const tokenRotationDeps: TokenRotationDeps = {
|
|
442
|
+
liveSubprocesses: () => {
|
|
443
|
+
const provider = providerRegistry.get("claude") as {
|
|
444
|
+
liveSubprocessCredentials?: () => Array<{ sessionId: string; credential: SubprocessCredential }>;
|
|
445
|
+
} | undefined;
|
|
446
|
+
return (provider?.liveSubprocessCredentials?.() ?? []).map(({ sessionId, credential }) => {
|
|
447
|
+
const entry = activeSessions.get(sessionId);
|
|
448
|
+
// The consumer loop outlives each turn, so a live subprocess between turns sits at
|
|
449
|
+
// `idle` with `isStreamingActive` still set. A subprocess with no entry here belongs to
|
|
450
|
+
// something else (a scheduled run) and is never ours to judge idle.
|
|
451
|
+
const busy = !entry || entry.phase !== "idle" || !!entry.pendingApprovalEvent || hasBackgroundWork(sessionId);
|
|
452
|
+
return { sessionId, credential, busy };
|
|
453
|
+
});
|
|
454
|
+
},
|
|
455
|
+
dropIdle: (sessionId) => dropIdleSubprocess(
|
|
456
|
+
sessionId,
|
|
457
|
+
"token_rotated",
|
|
458
|
+
"Subprocess restarted: its account's sign-in token was renewed, the next message resumes on the new one",
|
|
459
|
+
),
|
|
460
|
+
};
|
|
461
|
+
registerTokenRotation(tokenRotationDeps);
|
|
462
|
+
|
|
439
463
|
/**
|
|
440
464
|
* Sessions with a turn in flight, optionally narrowed to one project.
|
|
441
465
|
*
|
|
@@ -1253,6 +1277,10 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
|
|
|
1253
1277
|
broadcast(sessionId, { type: "compact_status", status: "done" });
|
|
1254
1278
|
}
|
|
1255
1279
|
setPhase(sessionId, "idle");
|
|
1280
|
+
// A renewal that landed while this turn ran left its subprocess holding a revoked
|
|
1281
|
+
// token; restart it now rather than let the next message open with a 401. Deferred so
|
|
1282
|
+
// the drop does not close the query from inside the loop that is reading it.
|
|
1283
|
+
setTimeout(() => dropStaleIdle(tokenRotationDeps), 0);
|
|
1256
1284
|
// Reset heartbeat tracking for next turn
|
|
1257
1285
|
firstEventReceived = false;
|
|
1258
1286
|
startTime = Date.now();
|
|
@@ -166,6 +166,17 @@ class AccountService {
|
|
|
166
166
|
private pendingStates = new Map<string, { verifier: string; createdAt: number }>();
|
|
167
167
|
/** Per-account mutex: dedup concurrent refresh calls so only one OAuth request fires at a time. */
|
|
168
168
|
private pendingRefreshes = new Map<string, Promise<void>>();
|
|
169
|
+
private tokenListeners = new Set<(accountId: string) => void>();
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Hear about every replaced access token. Renewing one revokes the previous token, so a
|
|
173
|
+
* Claude subprocess still holding it fails its next request — see
|
|
174
|
+
* `src/server/ws/chat-token-rotation.ts`, which moves idle chats onto the new one.
|
|
175
|
+
*/
|
|
176
|
+
onTokenRotated(listener: (accountId: string) => void): () => void {
|
|
177
|
+
this.tokenListeners.add(listener);
|
|
178
|
+
return () => { this.tokenListeners.delete(listener); };
|
|
179
|
+
}
|
|
169
180
|
|
|
170
181
|
private toAccount(row: AccountRow): Account {
|
|
171
182
|
let profileData: OAuthProfileData | null = null;
|
|
@@ -518,6 +529,9 @@ class AccountService {
|
|
|
518
529
|
reauth_required: 0,
|
|
519
530
|
...(parked ? {} : { status: "active" as const }),
|
|
520
531
|
});
|
|
532
|
+
for (const listener of this.tokenListeners) {
|
|
533
|
+
try { listener(id); } catch (e) { log.warn(`token rotation listener failed for ${id}: ${(e as Error).message}`); }
|
|
534
|
+
}
|
|
521
535
|
}
|
|
522
536
|
|
|
523
537
|
/**
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ChatQuestion {
|
|
2
|
+
id?: string;
|
|
3
|
+
question: string;
|
|
4
|
+
header?: string;
|
|
5
|
+
options: { label: string; description?: string }[];
|
|
6
|
+
multiSelect?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const ASYNC_QUESTION_TOOL = "RequestUserInput";
|
|
10
|
+
|
|
11
|
+
export function questionReplyPrefix(itemId: string): string {
|
|
12
|
+
return `Answers to Codex question ${encodeURIComponent(itemId)}:\n`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatQuestionReply(itemId: string, questions: ChatQuestion[], answers?: Record<string, string>): string {
|
|
16
|
+
return questionReplyPrefix(itemId) + (answers
|
|
17
|
+
? questions.map((question) => `${question.question}\n${answers[question.id ?? question.question] ?? ""}`).join("\n\n")
|
|
18
|
+
: "Skipped by the user.");
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import type { ChatMessage } from "../../../types/chat";
|
|
3
|
+
import { formatQuestionReply, questionReplyPrefix, type ChatQuestion } from "../../../shared/chat-questions";
|
|
4
|
+
import { QuestionCard } from "./question-card";
|
|
5
|
+
|
|
6
|
+
export const AsyncQuestionContext = createContext<{
|
|
7
|
+
messages: ChatMessage[];
|
|
8
|
+
onSend?: (content: string) => void;
|
|
9
|
+
}>({ messages: [] });
|
|
10
|
+
|
|
11
|
+
export function AsyncQuestionCard({ itemId, questions }: { itemId: string; questions: ChatQuestion[] }) {
|
|
12
|
+
const { messages, onSend } = useContext(AsyncQuestionContext);
|
|
13
|
+
const prefix = questionReplyPrefix(itemId);
|
|
14
|
+
const reply = messages.find((message) => message.role === "user" && message.content.startsWith(prefix));
|
|
15
|
+
if (reply || !onSend) return (
|
|
16
|
+
<div className="rounded-lg border border-border bg-surface p-3 text-sm whitespace-pre-wrap">
|
|
17
|
+
{reply ? reply.content.slice(prefix.length) : questions.map((question) => question.question).join("\n")}
|
|
18
|
+
<div className="mt-2 text-xs text-text-secondary">{reply ? "Response sent" : "Connect to answer this question"}</div>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
return <QuestionCard questions={questions} autoFocus={false}
|
|
22
|
+
onSubmit={(answers) => onSend(formatQuestionReply(itemId, questions, answers))}
|
|
23
|
+
onSkip={() => onSend(formatQuestionReply(itemId, questions))} />;
|
|
24
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Loader2, WifiOff } from "@/lib/icons";
|
|
2
|
+
import { useReconnectIndicator } from "@/hooks/use-reconnect-indicator";
|
|
3
|
+
|
|
4
|
+
interface ChatReconnectBannerProps {
|
|
5
|
+
isReconnecting: boolean;
|
|
6
|
+
onRetry: () => void;
|
|
7
|
+
/** Overrides for tests; production uses the hook's defaults. */
|
|
8
|
+
showDelayMs?: number;
|
|
9
|
+
lostAfterMs?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A small pill floating over the top of the chat while the socket reconnects.
|
|
14
|
+
*
|
|
15
|
+
* It deliberately covers nothing else: the messages on screen stay readable and
|
|
16
|
+
* scrollable, and anything sent meanwhile is queued and goes out on reconnect.
|
|
17
|
+
*/
|
|
18
|
+
export function ChatReconnectBanner({ isReconnecting, onRetry, showDelayMs, lostAfterMs }: ChatReconnectBannerProps) {
|
|
19
|
+
const { stage, restart } = useReconnectIndicator(isReconnecting, showDelayMs, lostAfterMs);
|
|
20
|
+
if (stage === "hidden") return null;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="pointer-events-none absolute inset-x-0 top-2 z-40 flex justify-center px-4">
|
|
24
|
+
<div
|
|
25
|
+
role="status"
|
|
26
|
+
aria-live="polite"
|
|
27
|
+
data-reconnect-stage={stage}
|
|
28
|
+
className="pointer-events-auto flex items-center gap-2 rounded-full border border-border bg-surface/95 px-3 py-1 text-xs text-text-secondary shadow-sm backdrop-blur-sm"
|
|
29
|
+
>
|
|
30
|
+
{stage === "reconnecting" ? (
|
|
31
|
+
<>
|
|
32
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
33
|
+
<span>Reconnecting…</span>
|
|
34
|
+
</>
|
|
35
|
+
) : (
|
|
36
|
+
<>
|
|
37
|
+
<WifiOff className="size-3.5 text-error" />
|
|
38
|
+
<span>Connection lost</span>
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
onClick={() => {
|
|
42
|
+
restart();
|
|
43
|
+
onRetry();
|
|
44
|
+
}}
|
|
45
|
+
className="-my-1 min-h-11 rounded-full px-2 font-medium text-primary hover:underline active:opacity-70 md:min-h-0 md:py-1"
|
|
46
|
+
>
|
|
47
|
+
Retry
|
|
48
|
+
</button>
|
|
49
|
+
</>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
|
2
|
+
import { AsyncQuestionContext } from "./async-question-card";
|
|
2
3
|
import { Loader2, Upload, X } from "@/lib/icons";
|
|
3
4
|
import { toast } from "sonner";
|
|
4
5
|
import { api, projectUrl } from "@/lib/api-client";
|
|
@@ -19,6 +20,7 @@ import { useChatAccountClaim } from "@/hooks/use-chat-account-claim";
|
|
|
19
20
|
import { startPrepare, getPrepare, isPrepared, forgetPrepare } from "@/lib/new-chat-prepare-client";
|
|
20
21
|
import { MessageList } from "./message-list";
|
|
21
22
|
import { BackgroundCommandBar } from "./background-command-bar";
|
|
23
|
+
import { ChatReconnectBanner } from "./chat-reconnect-banner";
|
|
22
24
|
import { RunningAgentsBar } from "./running-agents-bar";
|
|
23
25
|
import { AgentSessionProvider, normalizeProviderId } from "./agent-session-context";
|
|
24
26
|
import { McpSignInBar } from "@/components/mcp-auth/mcp-sign-in-bar";
|
|
@@ -473,6 +475,12 @@ function ChatTabContent({ metadata, tabId, onNewSession, onFork, historyFilter }
|
|
|
473
475
|
killBackgroundShell,
|
|
474
476
|
} = useChat(sessionId, providerId, projectName, handleSessionMigrated, observeAttempt, replyTransportOptions);
|
|
475
477
|
|
|
478
|
+
// Shared by the history bar's reload button and the reconnect pill's Retry.
|
|
479
|
+
const reloadChat = useCallback(() => {
|
|
480
|
+
if (!isConnected) reconnect();
|
|
481
|
+
refetchMessages();
|
|
482
|
+
}, [isConnected, reconnect, refetchMessages]);
|
|
483
|
+
|
|
476
484
|
const sessionChanges = useSessionFileChanges({ projectName, sessionId, messages, isStreaming });
|
|
477
485
|
const openReview = useCallback((path?: string) => {
|
|
478
486
|
if (!sessionId) return;
|
|
@@ -1283,15 +1291,8 @@ function ChatTabContent({ metadata, tabId, onNewSession, onFork, historyFilter }
|
|
|
1283
1291
|
</div>
|
|
1284
1292
|
)}
|
|
1285
1293
|
|
|
1286
|
-
{/* Reconnect overlay */}
|
|
1287
|
-
{isReconnecting
|
|
1288
|
-
<div className="absolute inset-0 z-50 flex items-center justify-center bg-background/60 backdrop-blur-sm">
|
|
1289
|
-
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
1290
|
-
<Loader2 className="size-4 animate-spin" />
|
|
1291
|
-
<span>Reconnecting...</span>
|
|
1292
|
-
</div>
|
|
1293
|
-
</div>
|
|
1294
|
-
)}
|
|
1294
|
+
{/* Reconnect status — a pill, not an overlay: the chat stays readable and usable */}
|
|
1295
|
+
<ChatReconnectBanner isReconnecting={isReconnecting} onRetry={reloadChat} />
|
|
1295
1296
|
|
|
1296
1297
|
{/* Edit-fork overlay — covers the chat while the new version forks + connects */}
|
|
1297
1298
|
{editForking && (
|
|
@@ -1315,7 +1316,7 @@ function ChatTabContent({ metadata, tabId, onNewSession, onFork, historyFilter }
|
|
|
1315
1316
|
<span>Starting conversation...</span>
|
|
1316
1317
|
</div>
|
|
1317
1318
|
</div>
|
|
1318
|
-
) : <SessionChangesContext.Provider value={sessionChangesValue}><MessageList
|
|
1319
|
+
) : <AsyncQuestionContext.Provider value={{ messages: renderedMessages, onSend: isConnected ? (content) => sendMessage(content, { permissionMode }) : undefined }}><SessionChangesContext.Provider value={sessionChangesValue}><MessageList
|
|
1319
1320
|
messages={renderedMessages}
|
|
1320
1321
|
onExpandCompact={expandCompact}
|
|
1321
1322
|
isCompactExpanded={isCompactExpanded}
|
|
@@ -1346,7 +1347,7 @@ function ChatTabContent({ metadata, tabId, onNewSession, onFork, historyFilter }
|
|
|
1346
1347
|
onDismissMessage={dismissMessage}
|
|
1347
1348
|
onClearErrors={clearErrors}
|
|
1348
1349
|
bashPartialOutput={bashPartialOutput}
|
|
1349
|
-
/></SessionChangesContext.Provider>}
|
|
1350
|
+
/></SessionChangesContext.Provider></AsyncQuestionContext.Provider>}
|
|
1350
1351
|
|
|
1351
1352
|
{/* Agents still working — pinned here so it is the last thing under the conversation */}
|
|
1352
1353
|
<RunningAgentsBar
|
|
@@ -1399,10 +1400,7 @@ function ChatTabContent({ metadata, tabId, onNewSession, onFork, historyFilter }
|
|
|
1399
1400
|
onSelectSession={handleSelectSession}
|
|
1400
1401
|
historyFilter={historyFilter}
|
|
1401
1402
|
isConnected={isConnected}
|
|
1402
|
-
onReload={
|
|
1403
|
-
if (!isConnected) reconnect();
|
|
1404
|
-
refetchMessages();
|
|
1405
|
-
}}
|
|
1403
|
+
onReload={reloadChat}
|
|
1406
1404
|
teamActivity={teamActivity}
|
|
1407
1405
|
teamMessages={teamMessages}
|
|
1408
1406
|
onTeamOpen={markTeamRead}
|