@hienlh/ppm 0.22.8 → 0.22.10
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 +10 -0
- package/CLAUDE.md +3 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +11 -1
- package/bun.lock +10 -10
- package/dist/web/assets/accounts-settings-section-BYz8j7Fq.js +1 -0
- package/dist/web/assets/accounts-settings-section-BYz8j7Fq.js.br +0 -0
- package/dist/web/assets/accounts-settings-section-BYz8j7Fq.js.gz +0 -0
- package/dist/web/assets/{ai-resource-editor-CtvWAxMW.js → ai-resource-editor-K7Q5d1jo.js} +1 -1
- package/dist/web/assets/ai-resource-editor-K7Q5d1jo.js.br +0 -0
- package/dist/web/assets/ai-resource-editor-K7Q5d1jo.js.gz +0 -0
- package/dist/web/assets/ai-settings-section-ChEBg9Be.js +1 -0
- package/dist/web/assets/ai-settings-section-CyWSTidP.js +1 -0
- package/dist/web/assets/ai-settings-section-CyWSTidP.js.br +0 -0
- package/dist/web/assets/ai-settings-section-CyWSTidP.js.gz +0 -0
- package/dist/web/assets/{appearance-settings-section-DRWpbQqj.js → appearance-settings-section-DH0DxaKv.js} +1 -1
- package/dist/web/assets/appearance-settings-section-DH0DxaKv.js.br +0 -0
- package/dist/web/assets/appearance-settings-section-DH0DxaKv.js.gz +0 -0
- package/dist/web/assets/{branch-review-tab-B3BMVCdK.js → branch-review-tab-BjPYFJ7n.js} +1 -1
- package/dist/web/assets/branch-review-tab-BjPYFJ7n.js.br +0 -0
- package/dist/web/assets/branch-review-tab-BjPYFJ7n.js.gz +0 -0
- package/dist/web/assets/chat-tab-BFrvTu8k.js +15 -0
- package/dist/web/assets/chat-tab-BFrvTu8k.js.br +0 -0
- package/dist/web/assets/chat-tab-BFrvTu8k.js.gz +0 -0
- package/dist/web/assets/code-editor-CTTLNDyE.js +7 -0
- package/dist/web/assets/code-editor-CTTLNDyE.js.br +0 -0
- package/dist/web/assets/code-editor-CTTLNDyE.js.gz +0 -0
- package/dist/web/assets/{codex-daily-guard-DjLeZ_hE.js → codex-daily-guard-PsB0g8B0.js} +1 -1
- package/dist/web/assets/codex-daily-guard-PsB0g8B0.js.br +0 -0
- package/dist/web/assets/codex-daily-guard-PsB0g8B0.js.gz +0 -0
- package/dist/web/assets/{conflict-editor-Dx8MnO6j.js → conflict-editor-BoLsr5fL.js} +1 -1
- package/dist/web/assets/conflict-editor-BoLsr5fL.js.br +0 -0
- package/dist/web/assets/conflict-editor-BoLsr5fL.js.gz +0 -0
- package/dist/web/assets/{csv-preview-U-Fy9KPT.js → csv-preview-C-7PUd6W.js} +1 -1
- package/dist/web/assets/csv-preview-C-7PUd6W.js.br +0 -0
- package/dist/web/assets/csv-preview-C-7PUd6W.js.gz +0 -0
- package/dist/web/assets/database-viewer-BJgZ-0s2.js +1 -0
- package/dist/web/assets/database-viewer-BJgZ-0s2.js.br +0 -0
- package/dist/web/assets/database-viewer-BJgZ-0s2.js.gz +0 -0
- package/dist/web/assets/{diff-viewer-Bv2J64t8.js → diff-viewer-i0_wCJL2.js} +1 -1
- package/dist/web/assets/diff-viewer-i0_wCJL2.js.br +0 -0
- package/dist/web/assets/diff-viewer-i0_wCJL2.js.gz +0 -0
- package/dist/web/assets/{editor-language-service-C1J6iorq.js → editor-language-service-DDhZjZqB.js} +1 -1
- package/dist/web/assets/editor-language-service-DDhZjZqB.js.br +0 -0
- package/dist/web/assets/editor-language-service-DDhZjZqB.js.gz +0 -0
- package/dist/web/assets/explorer-body-DeIIj3uF.js +2 -0
- package/dist/web/assets/explorer-body-DeIIj3uF.js.br +0 -0
- package/dist/web/assets/explorer-body-DeIIj3uF.js.gz +0 -0
- package/dist/web/assets/{explorer-window-content-CZIwX6cE.js → explorer-window-content-D0i22zgM.js} +1 -1
- package/dist/web/assets/explorer-window-content-D0i22zgM.js.br +0 -0
- package/dist/web/assets/explorer-window-content-D0i22zgM.js.gz +0 -0
- package/dist/web/assets/{extension-manager-section-DGSKz5iJ.js → extension-manager-section-D8TSwRG8.js} +1 -1
- package/dist/web/assets/extension-manager-section-D8TSwRG8.js.br +0 -0
- package/dist/web/assets/extension-manager-section-D8TSwRG8.js.gz +0 -0
- package/dist/web/assets/{extension-webview-FuuXP0Aw.js → extension-webview-CfzJpLQR.js} +1 -1
- package/dist/web/assets/extension-webview-CfzJpLQR.js.br +0 -0
- package/dist/web/assets/extension-webview-CfzJpLQR.js.gz +0 -0
- package/dist/web/assets/{files-settings-section-CGd2yfzl.js → files-settings-section-Ne10pnQU.js} +1 -1
- package/dist/web/assets/files-settings-section-Ne10pnQU.js.br +0 -0
- package/dist/web/assets/files-settings-section-Ne10pnQU.js.gz +0 -0
- package/dist/web/assets/{general-settings-section-CcG10nJq.js → general-settings-section-fbSqpiKY.js} +1 -1
- package/dist/web/assets/general-settings-section-fbSqpiKY.js.br +0 -0
- package/dist/web/assets/general-settings-section-fbSqpiKY.js.gz +0 -0
- package/dist/web/assets/{git-log-panel-B-aSlSlu.js → git-log-panel-9mRsulRQ.js} +1 -1
- package/dist/web/assets/git-log-panel-9mRsulRQ.js.br +0 -0
- package/dist/web/assets/git-log-panel-9mRsulRQ.js.gz +0 -0
- package/dist/web/assets/{glide-data-grid-usB_4gTg.js → glide-data-grid-OhxZFlYg.js} +3 -3
- package/dist/web/assets/glide-data-grid-OhxZFlYg.js.br +0 -0
- package/dist/web/assets/glide-data-grid-OhxZFlYg.js.gz +0 -0
- package/dist/web/assets/group-chat-tab-4DdMvOkG.js +1 -0
- package/dist/web/assets/group-chat-tab-4DdMvOkG.js.br +0 -0
- package/dist/web/assets/group-chat-tab-4DdMvOkG.js.gz +0 -0
- package/dist/web/assets/index-C3v3QRVR.js +52 -0
- package/dist/web/assets/index-C3v3QRVR.js.br +0 -0
- package/dist/web/assets/index-C3v3QRVR.js.gz +0 -0
- package/dist/web/assets/index-aXJy_SvC.css +1 -0
- package/dist/web/assets/index-aXJy_SvC.css.br +0 -0
- package/dist/web/assets/index-aXJy_SvC.css.gz +0 -0
- package/dist/web/assets/{jira-watcher-section-cDMBzIR7.js → jira-watcher-section-PYPh3xGU.js} +1 -1
- package/dist/web/assets/jira-watcher-section-PYPh3xGU.js.br +0 -0
- package/dist/web/assets/jira-watcher-section-PYPh3xGU.js.gz +0 -0
- package/dist/web/assets/keybindings-store-DhAtn4Pd.js +1 -0
- package/dist/web/assets/{keyboard-shortcuts-section-CIcdYToP.js → keyboard-shortcuts-section-CM3pNiBR.js} +1 -1
- package/dist/web/assets/keyboard-shortcuts-section-CM3pNiBR.js.br +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-CM3pNiBR.js.gz +0 -0
- package/dist/web/assets/{markdown-renderer-BzX5WysW.js → markdown-renderer-Df1vynCr.js} +1 -1
- package/dist/web/assets/{markdown-renderer-KTNg80_A.js → markdown-renderer-DqNZUt1S.js} +3 -3
- package/dist/web/assets/markdown-renderer-DqNZUt1S.js.br +0 -0
- package/dist/web/assets/{markdown-renderer-KTNg80_A.js.gz → markdown-renderer-DqNZUt1S.js.gz} +0 -0
- package/dist/web/assets/{mobile-explorer-sheet-cnwXe-Uz.js → mobile-explorer-sheet-BSuzC_08.js} +1 -1
- package/dist/web/assets/mobile-explorer-sheet-BSuzC_08.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-BSuzC_08.js.gz +0 -0
- package/dist/web/assets/notification-store-DbKwLWwl.js +1 -0
- package/dist/web/assets/{ppmbot-settings-section-CDp69sHV.js → ppmbot-settings-section-CY_yAGFM.js} +1 -1
- package/dist/web/assets/ppmbot-settings-section-CY_yAGFM.js.br +0 -0
- package/dist/web/assets/ppmbot-settings-section-CY_yAGFM.js.gz +0 -0
- package/dist/web/assets/{problems-panel-BiE3c-31.js → problems-panel-CJNasbnt.js} +1 -1
- package/dist/web/assets/problems-panel-CJNasbnt.js.br +0 -0
- package/dist/web/assets/problems-panel-CJNasbnt.js.gz +0 -0
- package/dist/web/assets/{process-table-DcEq9HCl.js → process-table-1CN3rBVp.js} +1 -1
- package/dist/web/assets/process-table-1CN3rBVp.js.br +0 -0
- package/dist/web/assets/process-table-1CN3rBVp.js.gz +0 -0
- package/dist/web/assets/{proxy-settings-section-P_ULFRpZ.js → proxy-settings-section-CqcvneFB.js} +1 -1
- package/dist/web/assets/proxy-settings-section-CqcvneFB.js.br +0 -0
- package/dist/web/assets/proxy-settings-section-CqcvneFB.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-clipboard-notice-COjdmADR.js → remote-desktop-clipboard-notice-BysFGa_4.js} +1 -1
- package/dist/web/assets/remote-desktop-clipboard-notice-BysFGa_4.js.br +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-BysFGa_4.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-mobile-sheet-lCU5l2w4.js → remote-desktop-mobile-sheet-DLW_0HDz.js} +2 -2
- package/dist/web/assets/remote-desktop-mobile-sheet-DLW_0HDz.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-DLW_0HDz.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-mobile-view-Bo-wgZPC.js → remote-desktop-mobile-view-Df6SfRtI.js} +1 -1
- package/dist/web/assets/remote-desktop-mobile-view-Df6SfRtI.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-Df6SfRtI.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-readiness-gate-Cqz8kqWG.js → remote-desktop-readiness-gate-LugXhLJ3.js} +1 -1
- package/dist/web/assets/remote-desktop-readiness-gate-LugXhLJ3.js.br +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-LugXhLJ3.js.gz +0 -0
- package/dist/web/assets/remote-desktop-window-content-k-pIAIDq.js +1 -0
- package/dist/web/assets/remote-desktop-window-content-k-pIAIDq.js.br +0 -0
- package/dist/web/assets/remote-desktop-window-content-k-pIAIDq.js.gz +0 -0
- package/dist/web/assets/{schedules-settings-section-DHs84DvG.js → schedules-settings-section-ByCm7IOg.js} +1 -1
- package/dist/web/assets/schedules-settings-section-ByCm7IOg.js.br +0 -0
- package/dist/web/assets/schedules-settings-section-ByCm7IOg.js.gz +0 -0
- package/dist/web/assets/{settings-body-CLt6iutX.js → settings-body-Dwt_rUSa.js} +2 -2
- package/dist/web/assets/settings-body-Dwt_rUSa.js.br +0 -0
- package/dist/web/assets/settings-body-Dwt_rUSa.js.gz +0 -0
- package/dist/web/assets/settings-store-m6qOXoy4.js +2 -0
- package/dist/web/assets/settings-store-m6qOXoy4.js.br +0 -0
- package/dist/web/assets/settings-store-m6qOXoy4.js.gz +0 -0
- package/dist/web/assets/{settings-tab-DjOxh-vW.js → settings-tab-Dvj9UfzL.js} +1 -1
- package/dist/web/assets/{settings-window-content-BxzSerga.js → settings-window-content-Di-eONmj.js} +1 -1
- package/dist/web/assets/{sql-query-editor-4Okd-7ss.js → sql-query-editor-DgwwJxwJ.js} +1 -1
- package/dist/web/assets/sql-query-editor-DgwwJxwJ.js.br +0 -0
- package/dist/web/assets/sql-query-editor-DgwwJxwJ.js.gz +0 -0
- package/dist/web/assets/{sqlite-viewer-BqDUckIJ.js → sqlite-viewer-gxiJZ3kk.js} +1 -1
- package/dist/web/assets/sqlite-viewer-gxiJZ3kk.js.br +0 -0
- package/dist/web/assets/sqlite-viewer-gxiJZ3kk.js.gz +0 -0
- package/dist/web/assets/{system-monitor-body-Ssmt-r5H.js → system-monitor-body-BJrVntEe.js} +2 -2
- package/dist/web/assets/system-monitor-body-BJrVntEe.js.br +0 -0
- package/dist/web/assets/system-monitor-body-BJrVntEe.js.gz +0 -0
- package/dist/web/assets/{system-monitor-tab-DNe62hWL.js → system-monitor-tab-Uz9A5I07.js} +1 -1
- package/dist/web/assets/{system-monitor-window-content-C6vnW_pQ.js → system-monitor-window-content-9cKlCVN0.js} +1 -1
- package/dist/web/assets/{tab-host-window-content--BVyPR1f.js → tab-host-window-content-D3M-lRyv.js} +1 -1
- package/dist/web/assets/tab-host-window-content-D3M-lRyv.js.br +0 -0
- package/dist/web/assets/tab-host-window-content-D3M-lRyv.js.gz +0 -0
- package/dist/web/assets/{team-member-sheet-CE2eHuFX.js → team-member-sheet-D66xVR6G.js} +2 -2
- package/dist/web/assets/team-member-sheet-D66xVR6G.js.br +0 -0
- package/dist/web/assets/team-member-sheet-D66xVR6G.js.gz +0 -0
- package/dist/web/assets/{team-member-window-content-B1sg0R4H.js → team-member-window-content-D6SxpWVr.js} +1 -1
- package/dist/web/assets/team-member-window-content-D6SxpWVr.js.br +0 -0
- package/dist/web/assets/team-member-window-content-D6SxpWVr.js.gz +0 -0
- package/dist/web/assets/{terminal-tab-BrdS4bAy.js → terminal-tab-DUfOyHxp.js} +3 -3
- package/dist/web/assets/terminal-tab-DUfOyHxp.js.br +0 -0
- package/dist/web/assets/terminal-tab-DUfOyHxp.js.gz +0 -0
- package/dist/web/assets/{tool-cards-DzYJVhjl.js → tool-cards-O9N-oJuB.js} +3 -3
- package/dist/web/assets/tool-cards-O9N-oJuB.js.br +0 -0
- package/dist/web/assets/tool-cards-O9N-oJuB.js.gz +0 -0
- package/dist/web/assets/{use-inline-blame-CKu-Sd-l.js → use-inline-blame-B40AsaN2.js} +1 -1
- package/dist/web/assets/use-inline-blame-B40AsaN2.js.br +0 -0
- package/dist/web/assets/use-inline-blame-B40AsaN2.js.gz +0 -0
- package/dist/web/assets/{use-lsp-BaG0NUXY.js → use-lsp-B30TcKvt.js} +1 -1
- package/dist/web/assets/use-lsp-B30TcKvt.js.br +0 -0
- package/dist/web/assets/use-lsp-B30TcKvt.js.gz +0 -0
- package/dist/web/assets/{use-monaco-theme-DvWcXiCH.js → use-monaco-theme-DGi9_j5M.js} +1 -1
- package/dist/web/assets/{use-websocket-Dj0e6N6t.js → use-websocket-DdGdvJc5.js} +1 -1
- package/dist/web/assets/video-preview-RS5jKlrC.js +1 -0
- package/dist/web/assets/video-preview-RS5jKlrC.js.br +0 -0
- package/dist/web/assets/video-preview-RS5jKlrC.js.gz +0 -0
- package/dist/web/index.html +3 -3
- 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/codebase-summary.md +1 -1
- package/docs/project-roadmap.md +1 -1
- package/package.json +2 -2
- package/src/providers/claude-agent-sdk.ts +87 -8
- package/src/server/index.ts +9 -0
- package/src/server/routes/mcp-auth.ts +117 -0
- package/src/server/routes/settings.ts +2 -0
- package/src/server/ws/chat-mcp-sign-in-sync.ts +58 -0
- package/src/server/ws/chat.ts +35 -1
- package/src/services/mcp-oauth/mcp-control-query.ts +58 -0
- package/src/services/mcp-oauth/mcp-oauth-flows.ts +271 -0
- package/src/services/mcp-oauth/mcp-oauth-redirect.ts +68 -0
- package/src/services/mcp-oauth/mcp-status-probe.ts +76 -0
- package/src/web/components/ai-resources/ai-resources-panel.tsx +4 -0
- package/src/web/components/chat/chat-tab.tsx +5 -0
- package/src/web/components/mcp-auth/claude-ai-connector.ts +11 -0
- package/src/web/components/mcp-auth/mcp-server-name.ts +10 -0
- package/src/web/components/mcp-auth/mcp-sign-in-bar.tsx +74 -0
- package/src/web/components/mcp-auth/mcp-sign-in-dialog.tsx +196 -0
- package/src/web/components/mcp-auth/mcp-sign-in-list.tsx +110 -0
- package/src/web/components/mcp-auth/use-mcp-sign-in-flow.ts +120 -0
- package/src/web/components/settings/ai-settings-section.tsx +11 -0
- package/src/web/hooks/use-chat.ts +14 -0
- package/src/web/lib/api-mcp-auth.ts +54 -0
- package/src/web/stores/settings-store.ts +34 -0
- package/dist/web/assets/accounts-settings-section-Dk9qWWTy.js +0 -1
- package/dist/web/assets/accounts-settings-section-Dk9qWWTy.js.br +0 -0
- package/dist/web/assets/accounts-settings-section-Dk9qWWTy.js.gz +0 -0
- package/dist/web/assets/ai-resource-editor-CtvWAxMW.js.br +0 -0
- package/dist/web/assets/ai-resource-editor-CtvWAxMW.js.gz +0 -0
- package/dist/web/assets/ai-settings-section-BxN-zFfm.js +0 -1
- package/dist/web/assets/ai-settings-section-CfI9nNSa.js +0 -1
- package/dist/web/assets/ai-settings-section-CfI9nNSa.js.br +0 -0
- package/dist/web/assets/ai-settings-section-CfI9nNSa.js.gz +0 -0
- package/dist/web/assets/appearance-settings-section-DRWpbQqj.js.br +0 -0
- package/dist/web/assets/appearance-settings-section-DRWpbQqj.js.gz +0 -0
- package/dist/web/assets/branch-review-tab-B3BMVCdK.js.br +0 -0
- package/dist/web/assets/branch-review-tab-B3BMVCdK.js.gz +0 -0
- package/dist/web/assets/chat-tab-B0g-TzDa.js +0 -15
- package/dist/web/assets/chat-tab-B0g-TzDa.js.br +0 -0
- package/dist/web/assets/chat-tab-B0g-TzDa.js.gz +0 -0
- package/dist/web/assets/code-editor-DXra_Rnt.js +0 -7
- package/dist/web/assets/code-editor-DXra_Rnt.js.br +0 -0
- package/dist/web/assets/code-editor-DXra_Rnt.js.gz +0 -0
- package/dist/web/assets/codex-daily-guard-DjLeZ_hE.js.br +0 -0
- package/dist/web/assets/codex-daily-guard-DjLeZ_hE.js.gz +0 -0
- package/dist/web/assets/conflict-editor-Dx8MnO6j.js.br +0 -0
- package/dist/web/assets/conflict-editor-Dx8MnO6j.js.gz +0 -0
- package/dist/web/assets/csv-preview-U-Fy9KPT.js.br +0 -0
- package/dist/web/assets/csv-preview-U-Fy9KPT.js.gz +0 -0
- package/dist/web/assets/database-viewer-v5S-zzB6.js +0 -1
- package/dist/web/assets/database-viewer-v5S-zzB6.js.br +0 -0
- package/dist/web/assets/database-viewer-v5S-zzB6.js.gz +0 -0
- package/dist/web/assets/diff-viewer-Bv2J64t8.js.br +0 -0
- package/dist/web/assets/diff-viewer-Bv2J64t8.js.gz +0 -0
- package/dist/web/assets/editor-language-service-C1J6iorq.js.br +0 -0
- package/dist/web/assets/editor-language-service-C1J6iorq.js.gz +0 -0
- package/dist/web/assets/explorer-body-BpTOY2kH.js +0 -2
- package/dist/web/assets/explorer-body-BpTOY2kH.js.br +0 -0
- package/dist/web/assets/explorer-body-BpTOY2kH.js.gz +0 -0
- package/dist/web/assets/explorer-window-content-CZIwX6cE.js.br +0 -0
- package/dist/web/assets/explorer-window-content-CZIwX6cE.js.gz +0 -0
- package/dist/web/assets/extension-manager-section-DGSKz5iJ.js.br +0 -0
- package/dist/web/assets/extension-manager-section-DGSKz5iJ.js.gz +0 -0
- package/dist/web/assets/extension-webview-FuuXP0Aw.js.br +0 -0
- package/dist/web/assets/extension-webview-FuuXP0Aw.js.gz +0 -0
- package/dist/web/assets/files-settings-section-CGd2yfzl.js.br +0 -0
- package/dist/web/assets/files-settings-section-CGd2yfzl.js.gz +0 -0
- package/dist/web/assets/general-settings-section-CcG10nJq.js.br +0 -0
- package/dist/web/assets/general-settings-section-CcG10nJq.js.gz +0 -0
- package/dist/web/assets/git-log-panel-B-aSlSlu.js.br +0 -0
- package/dist/web/assets/git-log-panel-B-aSlSlu.js.gz +0 -0
- package/dist/web/assets/glide-data-grid-usB_4gTg.js.br +0 -0
- package/dist/web/assets/glide-data-grid-usB_4gTg.js.gz +0 -0
- package/dist/web/assets/group-chat-tab-BAMi_dhb.js +0 -1
- package/dist/web/assets/group-chat-tab-BAMi_dhb.js.br +0 -0
- package/dist/web/assets/group-chat-tab-BAMi_dhb.js.gz +0 -0
- package/dist/web/assets/index-CoTvom0K.js +0 -52
- package/dist/web/assets/index-CoTvom0K.js.br +0 -0
- package/dist/web/assets/index-CoTvom0K.js.gz +0 -0
- package/dist/web/assets/index-DLn9-Lrb.css +0 -1
- package/dist/web/assets/index-DLn9-Lrb.css.br +0 -0
- package/dist/web/assets/index-DLn9-Lrb.css.gz +0 -0
- package/dist/web/assets/jira-watcher-section-cDMBzIR7.js.br +0 -3
- package/dist/web/assets/jira-watcher-section-cDMBzIR7.js.gz +0 -0
- package/dist/web/assets/keybindings-store-D7nwnInl.js +0 -1
- package/dist/web/assets/keyboard-shortcuts-section-CIcdYToP.js.br +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-CIcdYToP.js.gz +0 -0
- package/dist/web/assets/markdown-renderer-KTNg80_A.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-cnwXe-Uz.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-cnwXe-Uz.js.gz +0 -0
- package/dist/web/assets/notification-store-Bayjh6Js.js +0 -1
- package/dist/web/assets/ppmbot-settings-section-CDp69sHV.js.br +0 -0
- package/dist/web/assets/ppmbot-settings-section-CDp69sHV.js.gz +0 -0
- package/dist/web/assets/problems-panel-BiE3c-31.js.br +0 -0
- package/dist/web/assets/problems-panel-BiE3c-31.js.gz +0 -0
- package/dist/web/assets/process-table-DcEq9HCl.js.br +0 -0
- package/dist/web/assets/process-table-DcEq9HCl.js.gz +0 -0
- package/dist/web/assets/proxy-settings-section-P_ULFRpZ.js.br +0 -0
- package/dist/web/assets/proxy-settings-section-P_ULFRpZ.js.gz +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-COjdmADR.js.br +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-COjdmADR.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-lCU5l2w4.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-lCU5l2w4.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-Bo-wgZPC.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-Bo-wgZPC.js.gz +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-Cqz8kqWG.js.br +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-Cqz8kqWG.js.gz +0 -0
- package/dist/web/assets/remote-desktop-window-content-R-aRW07B.js +0 -1
- package/dist/web/assets/remote-desktop-window-content-R-aRW07B.js.br +0 -0
- package/dist/web/assets/remote-desktop-window-content-R-aRW07B.js.gz +0 -0
- package/dist/web/assets/schedules-settings-section-DHs84DvG.js.br +0 -0
- package/dist/web/assets/schedules-settings-section-DHs84DvG.js.gz +0 -0
- package/dist/web/assets/settings-body-CLt6iutX.js.br +0 -0
- package/dist/web/assets/settings-body-CLt6iutX.js.gz +0 -0
- package/dist/web/assets/settings-store-tWrnlyEW.js +0 -2
- package/dist/web/assets/settings-store-tWrnlyEW.js.br +0 -0
- package/dist/web/assets/settings-store-tWrnlyEW.js.gz +0 -0
- package/dist/web/assets/sql-query-editor-4Okd-7ss.js.br +0 -0
- package/dist/web/assets/sql-query-editor-4Okd-7ss.js.gz +0 -0
- package/dist/web/assets/sqlite-viewer-BqDUckIJ.js.br +0 -0
- package/dist/web/assets/sqlite-viewer-BqDUckIJ.js.gz +0 -0
- package/dist/web/assets/system-monitor-body-Ssmt-r5H.js.br +0 -0
- package/dist/web/assets/system-monitor-body-Ssmt-r5H.js.gz +0 -0
- package/dist/web/assets/tab-host-window-content--BVyPR1f.js.br +0 -0
- package/dist/web/assets/tab-host-window-content--BVyPR1f.js.gz +0 -0
- package/dist/web/assets/team-member-sheet-CE2eHuFX.js.br +0 -0
- package/dist/web/assets/team-member-sheet-CE2eHuFX.js.gz +0 -0
- package/dist/web/assets/team-member-window-content-B1sg0R4H.js.br +0 -0
- package/dist/web/assets/team-member-window-content-B1sg0R4H.js.gz +0 -0
- package/dist/web/assets/terminal-tab-BrdS4bAy.js.br +0 -0
- package/dist/web/assets/terminal-tab-BrdS4bAy.js.gz +0 -0
- package/dist/web/assets/tool-cards-DzYJVhjl.js.br +0 -0
- package/dist/web/assets/tool-cards-DzYJVhjl.js.gz +0 -0
- package/dist/web/assets/use-inline-blame-CKu-Sd-l.js.br +0 -0
- package/dist/web/assets/use-inline-blame-CKu-Sd-l.js.gz +0 -0
- package/dist/web/assets/use-lsp-BaG0NUXY.js.br +0 -0
- package/dist/web/assets/use-lsp-BaG0NUXY.js.gz +0 -0
- package/dist/web/assets/video-preview-ZyRR49Lr.js +0 -1
- package/dist/web/assets/video-preview-ZyRR49Lr.js.br +0 -0
- package/dist/web/assets/video-preview-ZyRR49Lr.js.gz +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign in to one MCP server. Shared by the chat's sign-in bar and the AI Resources list.
|
|
3
|
+
* Dialog on desktop, bottom sheet on mobile.
|
|
4
|
+
*/
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { CheckCircle2, ExternalLink, Loader2, TriangleAlert } from "@/lib/icons";
|
|
7
|
+
import { Button } from "@/components/ui/button";
|
|
8
|
+
import { Input } from "@/components/ui/input";
|
|
9
|
+
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
10
|
+
import { BottomSheet } from "@/components/ui/mobile-bottom-sheet";
|
|
11
|
+
import { useIsMobile } from "@/hooks/use-is-mobile";
|
|
12
|
+
import { useMcpSignInFlow } from "./use-mcp-sign-in-flow";
|
|
13
|
+
import { CLAUDE_AI_CONNECTORS_URL, isClaudeAiConnector } from "./claude-ai-connector";
|
|
14
|
+
|
|
15
|
+
interface McpSignInDialogProps {
|
|
16
|
+
/** Server to sign in to; null keeps the dialog closed. */
|
|
17
|
+
serverName: string | null;
|
|
18
|
+
/** Project whose MCP configuration applies — matters for project-scoped servers. */
|
|
19
|
+
project?: string;
|
|
20
|
+
onClose: () => void;
|
|
21
|
+
onSignedIn?: (serverName: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function McpSignInDialog({ serverName, project, onClose, onSignedIn }: McpSignInDialogProps) {
|
|
25
|
+
const isMobile = useIsMobile();
|
|
26
|
+
const open = serverName !== null;
|
|
27
|
+
const title = serverName ? `Sign in to ${serverName}` : "Sign in";
|
|
28
|
+
const connector = serverName !== null && isClaudeAiConnector(serverName);
|
|
29
|
+
// The flow lives here, above the dialog/sheet switch: the two shells are different
|
|
30
|
+
// trees, and crossing the breakpoint mid sign-in (rotating a large phone) would
|
|
31
|
+
// otherwise remount the body, cancel the flow and kill the CLI's callback listener.
|
|
32
|
+
const flow = useMcpSignInFlow(connector ? null : serverName, project, onSignedIn);
|
|
33
|
+
const body = !serverName ? null : connector
|
|
34
|
+
? <ClaudeAiConnectorBody onClose={onClose} />
|
|
35
|
+
: <SignInBody key={serverName} serverName={serverName} flowState={flow} onClose={onClose} />;
|
|
36
|
+
|
|
37
|
+
if (isMobile) {
|
|
38
|
+
return (
|
|
39
|
+
<BottomSheet open={open} onClose={onClose} className="popover-solid">
|
|
40
|
+
<div className="px-4 pb-4 pt-1">
|
|
41
|
+
<h2 className="mb-3 text-base font-semibold">{title}</h2>
|
|
42
|
+
{body}
|
|
43
|
+
</div>
|
|
44
|
+
</BottomSheet>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Dialog open={open} onOpenChange={(o) => { if (!o) onClose(); }}>
|
|
50
|
+
<DialogContent className="sm:max-w-md">
|
|
51
|
+
<DialogHeader>
|
|
52
|
+
<DialogTitle>{title}</DialogTitle>
|
|
53
|
+
<DialogDescription>Grant PPM's Claude sessions access to this MCP server.</DialogDescription>
|
|
54
|
+
</DialogHeader>
|
|
55
|
+
{body}
|
|
56
|
+
</DialogContent>
|
|
57
|
+
</Dialog>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function SignInBody({ serverName, flowState, onClose }: {
|
|
62
|
+
serverName: string;
|
|
63
|
+
flowState: ReturnType<typeof useMcpSignInFlow>;
|
|
64
|
+
onClose: () => void;
|
|
65
|
+
}) {
|
|
66
|
+
const { flow, starting, busy, error, retry, submitCallback, confirm } = flowState;
|
|
67
|
+
const [pasted, setPasted] = useState("");
|
|
68
|
+
|
|
69
|
+
if (starting || (!flow && !error)) {
|
|
70
|
+
return <StatusLine icon={<Loader2 className="size-4 animate-spin" />} text="Preparing the sign-in…" />;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!flow || flow.status === "failed" || flow.status === "expired" || flow.status === "cancelled") {
|
|
74
|
+
return (
|
|
75
|
+
<div className="space-y-3">
|
|
76
|
+
<ErrorLine text={flow?.error ?? error ?? "The sign-in did not finish."} />
|
|
77
|
+
<div className="flex gap-2">
|
|
78
|
+
<Button className="h-11 flex-1 md:h-9" onClick={() => void retry()}>Try again</Button>
|
|
79
|
+
<Button variant="outline" className="h-11 md:h-9" onClick={onClose}>Close</Button>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (flow.status === "done") {
|
|
86
|
+
return (
|
|
87
|
+
<div className="space-y-3">
|
|
88
|
+
<StatusLine
|
|
89
|
+
icon={<CheckCircle2 className="size-4 text-success" />}
|
|
90
|
+
text={`Signed in. ${serverName} is available to Claude — open chats reconnect it on their own.`}
|
|
91
|
+
/>
|
|
92
|
+
<Button className="h-11 w-full md:h-9" onClick={onClose}>Done</Button>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const shownError = flow.error ?? error;
|
|
98
|
+
return (
|
|
99
|
+
<div className="space-y-4">
|
|
100
|
+
{flow.authUrl && (
|
|
101
|
+
<Button asChild className="h-11 w-full md:h-9">
|
|
102
|
+
{/* A real link, not window.open(): a popup opened after an await is blocked. */}
|
|
103
|
+
<a href={flow.authUrl} target="_blank" rel="noopener noreferrer">
|
|
104
|
+
<ExternalLink className="size-4" /> Open sign-in page
|
|
105
|
+
</a>
|
|
106
|
+
</Button>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
{flow.callbackExpected ? (
|
|
110
|
+
<StatusLine
|
|
111
|
+
icon={<Loader2 className="size-4 animate-spin" />}
|
|
112
|
+
text={flow.redirectScheme === "custom"
|
|
113
|
+
? "Waiting for you to approve access. The page returns to PPM by itself."
|
|
114
|
+
: "Waiting for you to approve access in the browser…"}
|
|
115
|
+
/>
|
|
116
|
+
) : (
|
|
117
|
+
<div className="space-y-2">
|
|
118
|
+
<p className="text-sm text-text-secondary">Grant access on the page that opens, then come back here.</p>
|
|
119
|
+
<Button variant="outline" className="h-11 w-full md:h-9" disabled={busy} onClick={() => void confirm()}>
|
|
120
|
+
{busy ? <Loader2 className="size-4 animate-spin" /> : null} I've granted access
|
|
121
|
+
</Button>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{flow.callbackExpected && (
|
|
126
|
+
<form
|
|
127
|
+
className="space-y-2 rounded-md border border-border p-3"
|
|
128
|
+
onSubmit={(e) => { e.preventDefault(); if (pasted.trim()) void submitCallback(pasted.trim()); }}
|
|
129
|
+
>
|
|
130
|
+
<p className="text-xs text-text-secondary">
|
|
131
|
+
{flow.redirectScheme === "custom" ? (
|
|
132
|
+
<>Did the page not come back to PPM? Copy the address the browser ended on after approving and paste it here.</>
|
|
133
|
+
) : (
|
|
134
|
+
<>
|
|
135
|
+
Not on the machine running PPM? After approving, the browser lands on a <code>localhost</code> page
|
|
136
|
+
that does not load. Copy that page's address and paste it here.
|
|
137
|
+
</>
|
|
138
|
+
)}
|
|
139
|
+
</p>
|
|
140
|
+
<div className="flex gap-2">
|
|
141
|
+
<Input
|
|
142
|
+
value={pasted}
|
|
143
|
+
onChange={(e) => setPasted(e.target.value)}
|
|
144
|
+
placeholder={flow.redirectScheme === "custom" ? "https://…/callback?code=…" : "http://localhost:…/callback?code=…"}
|
|
145
|
+
className="h-11 flex-1 md:h-9"
|
|
146
|
+
autoComplete="off"
|
|
147
|
+
spellCheck={false}
|
|
148
|
+
/>
|
|
149
|
+
<Button type="submit" variant="outline" className="h-11 md:h-9" disabled={busy || !pasted.trim()}>
|
|
150
|
+
{busy ? <Loader2 className="size-4 animate-spin" /> : "Submit"}
|
|
151
|
+
</Button>
|
|
152
|
+
</div>
|
|
153
|
+
</form>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{shownError && <ErrorLine text={shownError} />}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function ClaudeAiConnectorBody({ onClose }: { onClose: () => void }) {
|
|
162
|
+
return (
|
|
163
|
+
<div className="space-y-3">
|
|
164
|
+
<p className="text-sm text-text-secondary">
|
|
165
|
+
This connector belongs to your claude.ai account. Connect it in claude.ai's connector settings,
|
|
166
|
+
then start a new chat to use it here.
|
|
167
|
+
</p>
|
|
168
|
+
<div className="flex gap-2">
|
|
169
|
+
<Button asChild className="h-11 flex-1 md:h-9">
|
|
170
|
+
<a href={CLAUDE_AI_CONNECTORS_URL} target="_blank" rel="noopener noreferrer">
|
|
171
|
+
<ExternalLink className="size-4" /> Open claude.ai connectors
|
|
172
|
+
</a>
|
|
173
|
+
</Button>
|
|
174
|
+
<Button variant="outline" className="h-11 md:h-9" onClick={onClose}>Close</Button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function StatusLine({ icon, text }: { icon: React.ReactNode; text: string }) {
|
|
181
|
+
return (
|
|
182
|
+
<div className="flex items-start gap-2 text-sm text-text-secondary">
|
|
183
|
+
<span className="mt-0.5 shrink-0">{icon}</span>
|
|
184
|
+
<span>{text}</span>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function ErrorLine({ text }: { text: string }) {
|
|
190
|
+
return (
|
|
191
|
+
<div className="flex items-start gap-2 text-sm text-error">
|
|
192
|
+
<TriangleAlert className="mt-0.5 size-4 shrink-0" />
|
|
193
|
+
<span className="min-w-0 break-words">{text}</span>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { KeyRound, Loader2, RefreshCw } from "@/lib/icons";
|
|
3
|
+
import { getMcpAuthStatus, type McpServerState } from "@/lib/api-mcp-auth";
|
|
4
|
+
import { McpSignInDialog } from "./mcp-sign-in-dialog";
|
|
5
|
+
import { isClaudeAiConnector } from "./claude-ai-connector";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Where a server was configured, in words. "dynamic" — what the CLI calls every server
|
|
9
|
+
* PPM hands it — says nothing to the reader, so it is left out.
|
|
10
|
+
*/
|
|
11
|
+
function originLabel(s: McpServerState): string | null {
|
|
12
|
+
if (isClaudeAiConnector(s.name, s.scope)) return "claude.ai connector";
|
|
13
|
+
if (s.source === "plugin") return "Plugin";
|
|
14
|
+
if (s.scope && s.scope !== "dynamic") return s.scope;
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface McpSignInListProps {
|
|
19
|
+
/** Project whose MCP configuration applies; undefined = the home directory's. */
|
|
20
|
+
project?: string;
|
|
21
|
+
/** The panel's search box — the list filters with everything else. */
|
|
22
|
+
search?: string;
|
|
23
|
+
/** Keep the heading and say so when nothing needs a sign-in (Settings), instead of rendering nothing. */
|
|
24
|
+
showWhenEmpty?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* MCP servers that are configured but waiting for a sign-in, as a chat in `project` would
|
|
29
|
+
* see them. Covers every source Claude loads — PPM's own list, ~/.claude.json and plugins —
|
|
30
|
+
* which is why it asks the server instead of reading the PPM list next to it.
|
|
31
|
+
*/
|
|
32
|
+
export function McpSignInList({ project, search = "", showWhenEmpty = false }: McpSignInListProps) {
|
|
33
|
+
const [servers, setServers] = useState<McpServerState[]>([]);
|
|
34
|
+
const [loading, setLoading] = useState(true);
|
|
35
|
+
const [signingIn, setSigningIn] = useState<string | null>(null);
|
|
36
|
+
|
|
37
|
+
// Only the latest request may write: switching projects quickly must not leave the
|
|
38
|
+
// previous project's answer on screen.
|
|
39
|
+
const seqRef = useRef(0);
|
|
40
|
+
const load = useCallback(async (fresh = false) => {
|
|
41
|
+
const seq = ++seqRef.current;
|
|
42
|
+
setLoading(true);
|
|
43
|
+
try {
|
|
44
|
+
const next = await getMcpAuthStatus(project, fresh);
|
|
45
|
+
if (seq === seqRef.current) setServers(next);
|
|
46
|
+
} catch {
|
|
47
|
+
// A failed probe hides the list rather than showing a stale one.
|
|
48
|
+
if (seq === seqRef.current) setServers([]);
|
|
49
|
+
} finally {
|
|
50
|
+
if (seq === seqRef.current) setLoading(false);
|
|
51
|
+
}
|
|
52
|
+
}, [project]);
|
|
53
|
+
|
|
54
|
+
useEffect(() => { void load(); }, [load]);
|
|
55
|
+
|
|
56
|
+
const q = search.trim().toLowerCase();
|
|
57
|
+
const pending = servers.filter((s) => s.status === "needs-auth" && (!q || s.name.toLowerCase().includes(q)));
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
{(pending.length > 0 || showWhenEmpty) && (
|
|
62
|
+
<div className="pb-1">
|
|
63
|
+
<div className="flex items-center gap-1 px-2 py-1 text-[10px] font-semibold uppercase tracking-wide text-text-subtle">
|
|
64
|
+
<span>Needs sign-in</span>
|
|
65
|
+
<span className="text-text-subtle/60">{pending.length}</span>
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
onClick={() => void load(true)}
|
|
69
|
+
disabled={loading}
|
|
70
|
+
className="ml-auto flex size-6 items-center justify-center rounded hover:bg-surface-elevated disabled:opacity-50 max-md:size-9"
|
|
71
|
+
aria-label="Check again"
|
|
72
|
+
title="Check again"
|
|
73
|
+
>
|
|
74
|
+
{loading ? <Loader2 className="size-3 animate-spin" /> : <RefreshCw className="size-3" />}
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
{pending.length === 0 && (
|
|
78
|
+
<p className="px-2 py-1.5 text-[11px] text-text-subtle">
|
|
79
|
+
{loading ? "Checking MCP servers…" : "No MCP server is waiting for a sign-in."}
|
|
80
|
+
</p>
|
|
81
|
+
)}
|
|
82
|
+
{pending.map((s) => (
|
|
83
|
+
<div key={s.name} className="flex items-center gap-2 px-2 py-1.5">
|
|
84
|
+
<KeyRound className="size-4 shrink-0 text-warning" />
|
|
85
|
+
<span className="min-w-0 flex-1">
|
|
86
|
+
<span className="block truncate text-[13px] font-medium">{s.name}</span>
|
|
87
|
+
{originLabel(s) && (
|
|
88
|
+
<span className="block truncate text-[11px] text-text-subtle">{originLabel(s)}</span>
|
|
89
|
+
)}
|
|
90
|
+
</span>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
onClick={() => setSigningIn(s.name)}
|
|
94
|
+
className="shrink-0 rounded-md bg-primary px-2.5 py-1 text-[11px] font-medium text-primary-foreground hover:bg-primary/90 max-md:min-h-9 max-md:px-3"
|
|
95
|
+
>
|
|
96
|
+
{isClaudeAiConnector(s.name, s.scope) ? "Connect" : "Sign in"}
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
<McpSignInDialog
|
|
103
|
+
serverName={signingIn}
|
|
104
|
+
project={project}
|
|
105
|
+
onClose={() => setSigningIn(null)}
|
|
106
|
+
onSignedIn={() => void load(true)}
|
|
107
|
+
/>
|
|
108
|
+
</>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
startMcpAuth, getMcpAuthFlow, submitMcpAuthCallback, confirmMcpAuth, cancelMcpAuth,
|
|
4
|
+
type McpAuthFlow,
|
|
5
|
+
} from "@/lib/api-mcp-auth";
|
|
6
|
+
import { useSettingsStore } from "@/stores/settings-store";
|
|
7
|
+
|
|
8
|
+
const POLL_MS = 2_000;
|
|
9
|
+
|
|
10
|
+
function isOpen(flow: McpAuthFlow | null): boolean {
|
|
11
|
+
return flow?.status === "waiting" || flow?.status === "completing";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Drives one MCP sign-in while `serverName` is set: starts the flow, polls it until it
|
|
16
|
+
* settles, and cancels it if the caller goes away first — an abandoned flow would otherwise
|
|
17
|
+
* hold a Claude subprocess open for its full ten minutes.
|
|
18
|
+
*/
|
|
19
|
+
export function useMcpSignInFlow(
|
|
20
|
+
serverName: string | null,
|
|
21
|
+
project: string | undefined,
|
|
22
|
+
onSignedIn?: (serverName: string) => void,
|
|
23
|
+
) {
|
|
24
|
+
const [flow, setFlow] = useState<McpAuthFlow | null>(null);
|
|
25
|
+
const [starting, setStarting] = useState(false);
|
|
26
|
+
const [busy, setBusy] = useState(false);
|
|
27
|
+
const [error, setError] = useState<string | null>(null);
|
|
28
|
+
const flowRef = useRef<McpAuthFlow | null>(null);
|
|
29
|
+
const onSignedInRef = useRef(onSignedIn);
|
|
30
|
+
onSignedInRef.current = onSignedIn;
|
|
31
|
+
const reportedRef = useRef<string | null>(null);
|
|
32
|
+
|
|
33
|
+
const apply = useCallback((next: McpAuthFlow) => {
|
|
34
|
+
const current = flowRef.current;
|
|
35
|
+
// A poll that left before a submit settled the flow can land after it; an outcome
|
|
36
|
+
// never goes back to "waiting".
|
|
37
|
+
if (current?.id === next.id && !isOpen(current) && isOpen(next)) return;
|
|
38
|
+
flowRef.current = next;
|
|
39
|
+
setFlow(next);
|
|
40
|
+
if (next.status === "done" && reportedRef.current !== next.id) {
|
|
41
|
+
reportedRef.current = next.id;
|
|
42
|
+
// Signed in, so no longer a server the user chose to ignore: if it ever needs a
|
|
43
|
+
// sign-in again, the chat bar should say so.
|
|
44
|
+
useSettingsStore.getState().undismissMcpSignIn(next.serverName);
|
|
45
|
+
onSignedInRef.current?.(next.serverName);
|
|
46
|
+
}
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
// Bumped by every start and every teardown, so a start that resolves late — StrictMode
|
|
50
|
+
// runs the opening effect twice, and the server cancels the first flow when the second
|
|
51
|
+
// begins — can neither overwrite the live flow nor outlive the dialog.
|
|
52
|
+
const genRef = useRef(0);
|
|
53
|
+
|
|
54
|
+
const start = useCallback(async () => {
|
|
55
|
+
if (!serverName) return;
|
|
56
|
+
const gen = ++genRef.current;
|
|
57
|
+
setStarting(true);
|
|
58
|
+
setError(null);
|
|
59
|
+
setFlow(null);
|
|
60
|
+
flowRef.current = null;
|
|
61
|
+
try {
|
|
62
|
+
const next = await startMcpAuth(serverName, project);
|
|
63
|
+
if (gen !== genRef.current) {
|
|
64
|
+
if (isOpen(next)) void cancelMcpAuth(next.id).catch(() => {});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
apply(next);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
if (gen === genRef.current) setError((e as Error).message);
|
|
70
|
+
} finally {
|
|
71
|
+
if (gen === genRef.current) setStarting(false);
|
|
72
|
+
}
|
|
73
|
+
}, [serverName, project, apply]);
|
|
74
|
+
|
|
75
|
+
// Start when opened; cancel whatever is still open when closed or unmounted.
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!serverName) return;
|
|
78
|
+
void start();
|
|
79
|
+
return () => {
|
|
80
|
+
genRef.current++;
|
|
81
|
+
const open = flowRef.current;
|
|
82
|
+
if (open && isOpen(open)) void cancelMcpAuth(open.id).catch(() => {});
|
|
83
|
+
flowRef.current = null;
|
|
84
|
+
};
|
|
85
|
+
}, [serverName, start]);
|
|
86
|
+
|
|
87
|
+
const flowId = flow?.id;
|
|
88
|
+
const polling = isOpen(flow);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!flowId || !polling) return;
|
|
91
|
+
const timer = setInterval(() => {
|
|
92
|
+
getMcpAuthFlow(flowId).then(apply).catch((e) => {
|
|
93
|
+
// The server no longer knows this flow (restarted, or it was cancelled elsewhere):
|
|
94
|
+
// end it here too, so the dialog offers "Try again" instead of spinning forever.
|
|
95
|
+
const current = flowRef.current;
|
|
96
|
+
if (current?.id === flowId) apply({ ...current, status: "failed", authUrl: undefined, error: (e as Error).message });
|
|
97
|
+
});
|
|
98
|
+
}, POLL_MS);
|
|
99
|
+
return () => clearInterval(timer);
|
|
100
|
+
}, [flowId, polling, apply]);
|
|
101
|
+
|
|
102
|
+
const run = useCallback(async (action: (id: string) => Promise<McpAuthFlow>) => {
|
|
103
|
+
const current = flowRef.current;
|
|
104
|
+
if (!current) return;
|
|
105
|
+
setBusy(true);
|
|
106
|
+
setError(null);
|
|
107
|
+
try {
|
|
108
|
+
apply(await action(current.id));
|
|
109
|
+
} catch (e) {
|
|
110
|
+
setError((e as Error).message);
|
|
111
|
+
} finally {
|
|
112
|
+
setBusy(false);
|
|
113
|
+
}
|
|
114
|
+
}, [apply]);
|
|
115
|
+
|
|
116
|
+
const submitCallback = useCallback((url: string) => run((id) => submitMcpAuthCallback(id, url)), [run]);
|
|
117
|
+
const confirm = useCallback(() => run(confirmMcpAuth), [run]);
|
|
118
|
+
|
|
119
|
+
return { flow, starting, busy, error, retry: start, submitCallback, confirm };
|
|
120
|
+
}
|
|
@@ -15,6 +15,7 @@ import { api } from "@/lib/api-client";
|
|
|
15
15
|
import { ProviderBadge } from "@/components/chat/provider-selector";
|
|
16
16
|
import { openSettings } from "./open-settings";
|
|
17
17
|
import { CodexContextSettings } from "./codex-context-settings";
|
|
18
|
+
import { McpSignInList } from "@/components/mcp-auth/mcp-sign-in-list";
|
|
18
19
|
import type { ModelOption } from "../../../types/chat";
|
|
19
20
|
|
|
20
21
|
const EFFORT_OPTIONS = [
|
|
@@ -394,6 +395,16 @@ export function AISettingsSection({ compact }: { compact?: boolean } = {}) {
|
|
|
394
395
|
onCheckedChange={(v) => handleSave("inherit_claude_mcp", v)}
|
|
395
396
|
/>
|
|
396
397
|
</div>
|
|
398
|
+
|
|
399
|
+
<div className={fieldGap}>
|
|
400
|
+
<Label className={compact ? labelSize : undefined}>MCP sign-in</Label>
|
|
401
|
+
<p className={`${compact ? "text-[9px]" : "text-[11px]"} text-muted-foreground`}>
|
|
402
|
+
Servers that use OAuth stay unavailable to Claude until someone signs in. Project-scoped servers are listed in the project's AI Resources panel.
|
|
403
|
+
</p>
|
|
404
|
+
<div className="-mx-2">
|
|
405
|
+
<McpSignInList showWhenEmpty />
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
397
408
|
</>
|
|
398
409
|
)}
|
|
399
410
|
|
|
@@ -65,6 +65,8 @@ interface UseChatReturn {
|
|
|
65
65
|
pendingApproval: ApprovalRequest | null;
|
|
66
66
|
contextWindowPct: number | null;
|
|
67
67
|
compactStatus: "compacting" | null;
|
|
68
|
+
/** MCP servers this session's subprocess reported as needing a sign-in. */
|
|
69
|
+
mcpNeedsAuth: string[];
|
|
68
70
|
statusMessage: string | null;
|
|
69
71
|
sessionTitle: string | null;
|
|
70
72
|
/**
|
|
@@ -150,6 +152,8 @@ export function useChat(
|
|
|
150
152
|
const [pendingApproval, setPendingApproval] = useState<ApprovalRequest | null>(null);
|
|
151
153
|
const [contextWindowPct, setContextWindowPct] = useState<number | null>(null);
|
|
152
154
|
const [compactStatus, setCompactStatus] = useState<"compacting" | null>(null);
|
|
155
|
+
/** MCP servers this session's subprocess reported as needing a sign-in. */
|
|
156
|
+
const [mcpNeedsAuth, setMcpNeedsAuth] = useState<string[]>([]);
|
|
153
157
|
const [backgroundShells, setBackgroundShells] = useState<BackgroundShell[]>([]);
|
|
154
158
|
const backgroundShellsRef = useRef<BackgroundShell[]>([]);
|
|
155
159
|
const [statusMessage, setStatusMessage] = useState<string | null>(null);
|
|
@@ -862,6 +866,12 @@ export function useChat(
|
|
|
862
866
|
return;
|
|
863
867
|
}
|
|
864
868
|
|
|
869
|
+
if ((data as any).type === "mcp_status") {
|
|
870
|
+
const list = (data as any).needsAuth;
|
|
871
|
+
setMcpNeedsAuth(Array.isArray(list) ? list : []);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
865
875
|
// Handle compact status events
|
|
866
876
|
if ((data as any).type === "compact_status") {
|
|
867
877
|
const status = (data as any).status;
|
|
@@ -951,6 +961,7 @@ export function useChat(
|
|
|
951
961
|
// Sync compact indicator from authoritative server state (covers reconnect).
|
|
952
962
|
// state.compactStatus is "compacting" | null — treat undefined as null for back-compat.
|
|
953
963
|
setCompactStatus(state.compactStatus === "compacting" ? "compacting" : null);
|
|
964
|
+
setMcpNeedsAuth(Array.isArray(state.mcpNeedsAuth) ? state.mcpNeedsAuth : []);
|
|
954
965
|
// If idle, refetch history (completed turns) and hide overlay.
|
|
955
966
|
// Skip when nothing could have changed: the phase was already idle locally
|
|
956
967
|
// and the full transcript finished loading moments ago — on boot the WS
|
|
@@ -1109,6 +1120,8 @@ export function useChat(
|
|
|
1109
1120
|
setPendingApproval(null);
|
|
1110
1121
|
if (approvalToastRef.current != null) { toast.dismiss(approvalToastRef.current); approvalToastRef.current = null; }
|
|
1111
1122
|
setCompactStatus(null);
|
|
1123
|
+
// Another session's subprocess saw another MCP state; its session_state brings its own.
|
|
1124
|
+
setMcpNeedsAuth([]);
|
|
1112
1125
|
// Clear ephemeral pre-compact expansions on session change
|
|
1113
1126
|
setExpansions(new Map());
|
|
1114
1127
|
// Drop the previous session's version groups. Keeping them would let the
|
|
@@ -1516,6 +1529,7 @@ export function useChat(
|
|
|
1516
1529
|
pendingApproval,
|
|
1517
1530
|
contextWindowPct,
|
|
1518
1531
|
compactStatus,
|
|
1532
|
+
mcpNeedsAuth,
|
|
1519
1533
|
statusMessage,
|
|
1520
1534
|
sessionTitle,
|
|
1521
1535
|
/** Account the server last reported for this session — beats the polled usage label. */
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { api } from "./api-client";
|
|
2
|
+
|
|
3
|
+
export type McpAuthFlowStatus = "waiting" | "completing" | "done" | "failed" | "expired" | "cancelled";
|
|
4
|
+
|
|
5
|
+
export interface McpAuthFlow {
|
|
6
|
+
id: string;
|
|
7
|
+
serverName: string;
|
|
8
|
+
status: McpAuthFlowStatus;
|
|
9
|
+
/** Open this to sign in. Absent once the flow has settled. */
|
|
10
|
+
authUrl?: string;
|
|
11
|
+
/** False for a claude.ai connector: nothing redirects back, the user confirms instead. */
|
|
12
|
+
callbackExpected: boolean;
|
|
13
|
+
/** "localhost": the browser returns to the host machine's loopback, which only works on that machine. */
|
|
14
|
+
redirectScheme?: "localhost" | "custom";
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface McpServerState {
|
|
19
|
+
name: string;
|
|
20
|
+
status: string;
|
|
21
|
+
error?: string;
|
|
22
|
+
scope?: string;
|
|
23
|
+
source?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getMcpAuthStatus(project?: string, fresh = false): Promise<McpServerState[]> {
|
|
27
|
+
const params = new URLSearchParams();
|
|
28
|
+
if (project) params.set("project", project);
|
|
29
|
+
if (fresh) params.set("fresh", "1");
|
|
30
|
+
const qs = params.toString();
|
|
31
|
+
return api.get<McpServerState[]>(`/api/mcp-auth/status${qs ? `?${qs}` : ""}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function startMcpAuth(server: string, project?: string): Promise<McpAuthFlow> {
|
|
35
|
+
// The origin lets the server have the sign-in return to PPM itself, which is what makes
|
|
36
|
+
// it work from a phone; the server only uses it when it matches the request's host.
|
|
37
|
+
return api.post<McpAuthFlow>("/api/mcp-auth/start", { server, project, origin: window.location.origin });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getMcpAuthFlow(id: string): Promise<McpAuthFlow> {
|
|
41
|
+
return api.get<McpAuthFlow>(`/api/mcp-auth/flows/${encodeURIComponent(id)}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function submitMcpAuthCallback(id: string, url: string): Promise<McpAuthFlow> {
|
|
45
|
+
return api.post<McpAuthFlow>(`/api/mcp-auth/flows/${encodeURIComponent(id)}/callback`, { url });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function confirmMcpAuth(id: string): Promise<McpAuthFlow> {
|
|
49
|
+
return api.post<McpAuthFlow>(`/api/mcp-auth/flows/${encodeURIComponent(id)}/confirm`, {});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function cancelMcpAuth(id: string): Promise<void> {
|
|
53
|
+
return api.del(`/api/mcp-auth/flows/${encodeURIComponent(id)}`);
|
|
54
|
+
}
|
|
@@ -65,6 +65,10 @@ interface SettingsState {
|
|
|
65
65
|
/** User ticked "don't show again" on the remote-desktop warning that precedes every open
|
|
66
66
|
* (`remote-desktop-warning-gate.tsx`); once true the viewer connects straight away. */
|
|
67
67
|
remoteDesktopWarningDismissed: boolean;
|
|
68
|
+
/** MCP servers hidden from the chat's sign-in bar. The bar comes back only for a server
|
|
69
|
+
* outside this list; a successful sign-in takes the server off it, so a later expiry
|
|
70
|
+
* is announced again. Synced like other UI prefs. */
|
|
71
|
+
mcpSignInDismissed: string[];
|
|
68
72
|
/** Hold a screen wake lock while any chat turn is running, so a propped-up tablet does not
|
|
69
73
|
* dim mid-answer. Defaults on; see `hooks/use-wake-lock.ts`. */
|
|
70
74
|
keepScreenAwake: boolean;
|
|
@@ -148,6 +152,8 @@ interface SettingsState {
|
|
|
148
152
|
setExplorerSkin: (pref: ExplorerSkinPref) => void;
|
|
149
153
|
toggleRemoteDesktopStatsVisible: () => void;
|
|
150
154
|
setRemoteDesktopWarningDismissed: (dismissed: boolean) => void;
|
|
155
|
+
dismissMcpSignIn: (serverNames: string[]) => void;
|
|
156
|
+
undismissMcpSignIn: (serverName: string) => void;
|
|
151
157
|
setKeepScreenAwake: (enabled: boolean) => void;
|
|
152
158
|
fetchServerInfo: () => Promise<void>;
|
|
153
159
|
/** Re-push the in-memory theme selection to the server (see the action for why). */
|
|
@@ -177,6 +183,7 @@ interface PersistedSettings {
|
|
|
177
183
|
explorerSkin?: ExplorerSkinPref;
|
|
178
184
|
remoteDesktopStatsVisible?: boolean;
|
|
179
185
|
remoteDesktopWarningDismissed?: boolean;
|
|
186
|
+
mcpSignInDismissed?: string[];
|
|
180
187
|
keepScreenAwake?: boolean;
|
|
181
188
|
remoteDesktopQuality?: QualityChoice;
|
|
182
189
|
remoteDesktopViewStyle?: ViewStyle;
|
|
@@ -242,6 +249,15 @@ const DEFAULT_DB_EXPANDED: DbSidebarExpanded = { conns: [], groups: ["__ungroupe
|
|
|
242
249
|
const DB_EXPANDED_CAPS = { conns: 200, groups: 200, tables: 500 } as const;
|
|
243
250
|
|
|
244
251
|
/** Coerce stored/server data into a valid expansion set; null when unusable. */
|
|
252
|
+
/** Same bound as the server's validator for the pref. */
|
|
253
|
+
const MAX_NAME_LIST = 200;
|
|
254
|
+
|
|
255
|
+
/** A list of names read back from storage or the server, or null when it is not one. */
|
|
256
|
+
function sanitizeNameList(value: unknown): string[] | null {
|
|
257
|
+
if (!Array.isArray(value)) return null;
|
|
258
|
+
return value.filter((s): s is string => typeof s === "string" && s.length > 0 && s.length <= 200).slice(-MAX_NAME_LIST);
|
|
259
|
+
}
|
|
260
|
+
|
|
245
261
|
function sanitizeDbExpanded(value: unknown): DbSidebarExpanded | null {
|
|
246
262
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
247
263
|
const v = value as Record<string, unknown>;
|
|
@@ -356,6 +372,8 @@ function applyServerUiPrefs(data: Record<string, unknown>) {
|
|
|
356
372
|
}
|
|
357
373
|
if (typeof data.remoteDesktopStatsVisible === "boolean") patch.remoteDesktopStatsVisible = data.remoteDesktopStatsVisible;
|
|
358
374
|
if (typeof data.remoteDesktopWarningDismissed === "boolean") patch.remoteDesktopWarningDismissed = data.remoteDesktopWarningDismissed;
|
|
375
|
+
const mcpDismissed = sanitizeNameList(data.mcpSignInDismissed);
|
|
376
|
+
if (mcpDismissed) patch.mcpSignInDismissed = mcpDismissed;
|
|
359
377
|
if (typeof data.keepScreenAwake === "boolean") patch.keepScreenAwake = data.keepScreenAwake;
|
|
360
378
|
const dbExpanded = sanitizeDbExpanded(data.dbSidebarExpanded);
|
|
361
379
|
if (dbExpanded) patch.dbSidebarExpanded = dbExpanded;
|
|
@@ -402,6 +420,7 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
|
|
402
420
|
explorerSkin: (_initial.explorerSkin === "windows" || _initial.explorerSkin === "macos") ? _initial.explorerSkin : "auto",
|
|
403
421
|
remoteDesktopStatsVisible: _initial.remoteDesktopStatsVisible ?? false,
|
|
404
422
|
remoteDesktopWarningDismissed: _initial.remoteDesktopWarningDismissed ?? false,
|
|
423
|
+
mcpSignInDismissed: sanitizeNameList(_initial.mcpSignInDismissed) ?? [],
|
|
405
424
|
keepScreenAwake: _initial.keepScreenAwake ?? true,
|
|
406
425
|
deviceName: null,
|
|
407
426
|
version: null,
|
|
@@ -592,6 +611,21 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
|
|
592
611
|
set({ remoteDesktopWarningDismissed: dismissed });
|
|
593
612
|
},
|
|
594
613
|
|
|
614
|
+
dismissMcpSignIn: (serverNames) => {
|
|
615
|
+
const current = get().mcpSignInDismissed;
|
|
616
|
+
const next = [...current, ...serverNames.filter((n) => !current.includes(n))].slice(-MAX_NAME_LIST);
|
|
617
|
+
persistUiPref({ mcpSignInDismissed: next });
|
|
618
|
+
set({ mcpSignInDismissed: next });
|
|
619
|
+
},
|
|
620
|
+
|
|
621
|
+
undismissMcpSignIn: (serverName) => {
|
|
622
|
+
const current = get().mcpSignInDismissed;
|
|
623
|
+
if (!current.includes(serverName)) return;
|
|
624
|
+
const next = current.filter((n) => n !== serverName);
|
|
625
|
+
persistUiPref({ mcpSignInDismissed: next });
|
|
626
|
+
set({ mcpSignInDismissed: next });
|
|
627
|
+
},
|
|
628
|
+
|
|
595
629
|
setKeepScreenAwake: (enabled) => {
|
|
596
630
|
persistUiPref({ keepScreenAwake: enabled });
|
|
597
631
|
set({ keepScreenAwake: enabled });
|