@hienlh/ppm 0.9.0-beta.9 → 0.9.1
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 +233 -0
- package/bun.lock +17 -0
- package/dist/web/assets/{_basePickBy-3Xe18azI.js → _basePickBy-5PGDJbfF.js} +1 -1
- package/dist/web/assets/{_baseUniq-Yy35llnn.js → _baseUniq-BT4Ow4Kk.js} +1 -1
- package/dist/web/assets/api-settings-BUvk6Saw.js +1 -0
- package/dist/web/assets/{arc-B9n1Gvb5.js → arc-BAOivWpI.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DqAZP_F6.js → architectureDiagram-2XIMDMQ5-Z-4eN4za.js} +1 -1
- package/dist/web/assets/arrow-up-BYhx9ckd.js +1 -0
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-h3cDF2vI.js → blockDiagram-WCTKOSBZ-BCLqzhuZ.js} +1 -1
- package/dist/web/assets/browser-tab-CrkhFCaw.js +1 -0
- package/dist/web/assets/{c4Diagram-IC4MRINW--pF1r5lr.js → c4Diagram-IC4MRINW-0Vp0Jeas.js} +1 -1
- package/dist/web/assets/channel-By7bn0Yq.js +1 -0
- package/dist/web/assets/chat-tab-C6jpiwh7.js +8 -0
- package/dist/web/assets/chevron-right-5HgK6l7K.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-C3aZvW7B.js → chunk-4BX2VUAB-D4tOov49.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-D5cABeB9.js → chunk-55IACEB6-DJ6BynZ4.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-CkFGv6Zs.js → chunk-7E7YKBS2-CiyUJxNI.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-Dvbyu4Zw.js → chunk-7R4GIKGN-Dv-4cAYn.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-CtqKiH4q.js → chunk-C72U2L5F-D21mS_6G.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-Cpr87sBR.js → chunk-EGIJ26TM-DzqmU2Z7.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-D23YVTOU.js → chunk-FMBD7UC4-DXncblvW.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-tDjHsAUs.js → chunk-GEFDOKGD-D-pKjlVd.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-DKikpoJM.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-C7vxA5i9.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-BBmymCjA.js → chunk-JSJVCQXG-99JzIdPr.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-DP36BDiU.js → chunk-KX2RTZJC-CRq1OBZv.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-Djw13C-3.js → chunk-KYZI473N-Bb0MCaIO.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-HG_eMj_C.js → chunk-L3YUKLVL-C7qGJrfV.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-C2UEioMs.js → chunk-MX3YWQON-BpS_PtKp.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-DXUTQ-BL.js → chunk-NQ4KR5QH-z_blpjxi.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-BsUWb9d0.js → chunk-O4XLMI2P-nDhi_cVu.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-rG0P22U9.js → chunk-OZEHJAEY-BXhYx3nO.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-DX0xW7kO.js → chunk-PQ6SQG4A-TF58UVMU.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-C7Gry6md.js → chunk-PU5JKC2W-ek7k4QVB.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-CMY0PkRK.js → chunk-R5LLSJPH-CFwSJijQ.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-CXuQvlyu.js → chunk-WL4C6EOR-ByUrSRin.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-DRJEb7Zb.js → chunk-XIRO2GV7-Djlmrely.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-BPEX8KhL.js → chunk-XPW4576I-BPQQBakK.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-Cb0iqycX.js → chunk-XZSTWKYB-DxAOx4hG.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-av5aeHLq.js → chunk-YBOYWFTD-rQG3QH5s.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-BA8Nj-_C.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-DjYu-6mn.js +1 -0
- package/dist/web/assets/clone-LRxlvnMj.js +1 -0
- package/dist/web/assets/code-editor-CBIPzlP2.js +2 -0
- package/dist/web/assets/columns-2-cEVJHYd7.js +1 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-qudEiMCT.js → cose-bilkent-S5V4N54A-B_AWZsOP.js} +1 -1
- package/dist/web/assets/createLucideIcon-PuMiQgHl.js +1 -0
- package/dist/web/assets/{csv-preview-DUbHtTAS.js → csv-preview-ncSOnJSC.js} +2 -2
- package/dist/web/assets/{dagre-BFcnKyBF.js → dagre-DHq9bhnd.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-C3O-MTLf.js → dagre-KLK3FWXG-BdJr7Byp.js} +1 -1
- package/dist/web/assets/database-viewer-BqOJR_zi.js +1 -0
- package/dist/web/assets/{diagram-E7M64L7V-DxPjK7_c.js → diagram-E7M64L7V-_db4pBVA.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-sqTog_XV.js → diagram-IFDJBPK2-xKoeuiJx.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-hzmp0GHK.js → diagram-P4PSJMXO-C8tjJsev.js} +1 -1
- package/dist/web/assets/diff-viewer-CcLyp4eY.js +4 -0
- package/dist/web/assets/{dist-CALwEtco.js → dist-DIV6WgAG.js} +1 -1
- package/dist/web/assets/{dist-DGDPTxs1.js → dist-ovWkrgO-.js} +1 -1
- package/dist/web/assets/{erDiagram-INFDFZHY-DLeYhAAT.js → erDiagram-INFDFZHY-BSh2z9Df.js} +1 -1
- package/dist/web/assets/extension-webview-NiZ7Ybvv.js +3 -0
- package/dist/web/assets/{flowDiagram-PKNHOUZH-CRxlE9Sr.js → flowDiagram-PKNHOUZH-oYaovqyp.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-BdjmoMLS.js → ganttDiagram-A5KZAMGK-DmL26q2P.js} +1 -1
- package/dist/web/assets/git-graph-CoTvMrIo.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BeHSX7kk.js → gitGraphDiagram-K3NZZRJ6-CMoukSrY.js} +1 -1
- package/dist/web/assets/{graphlib-Duh_bWLa.js → graphlib-BcsNnGcW.js} +1 -1
- package/dist/web/assets/index-C8byznLO.js +37 -0
- package/dist/web/assets/index-KwC2YrG4.css +2 -0
- package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-DWwumDkq.js +2 -0
- package/dist/web/assets/{isEmpty-B9L-Ge-H.js → isEmpty-bnrF3Qbc.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-Cu0Rt1Ok.js → ishikawaDiagram-PHBUUO56-D05_LyL7.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-CgDI-UG4.js → journeyDiagram-4ABVD52K-B_L20qMe.js} +1 -1
- package/dist/web/assets/jsx-runtime-kMwlnEGE.js +1 -0
- package/dist/web/assets/{kanban-definition-K7BYSVSG-h4g10UHL.js → kanban-definition-K7BYSVSG-CZ535BbZ.js} +1 -1
- package/dist/web/assets/keybindings-store-DPYzBe_M.js +1 -0
- package/dist/web/assets/{line-B75-Rx70.js → line-CVvo3dRu.js} +1 -1
- package/dist/web/assets/{linear-Bcjv9FQt.js → linear-DP4mkX3m.js} +1 -1
- package/dist/web/assets/{markdown-renderer-VIZB1GXE.js → markdown-renderer-DPLdR9xc.js} +5 -5
- package/dist/web/assets/{mermaid-parser.core-8u2leTXI.js → mermaid-parser.core-C7UwoIh6.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-BaOBwb-W.js → mindmap-definition-YRQLILUH-x0MTutJp.js} +1 -1
- package/dist/web/assets/{ordinal-LFEjVtwQ.js → ordinal-_K3x1fkz.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-At5Kz0KK.js → pieDiagram-SKSYHLDU-C1Gjrtzy.js} +1 -1
- package/dist/web/assets/postgres-viewer-BeiK4lCa.js +1 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-CdjGIDfw.js → quadrantDiagram-337W2JSQ-C8bzJCjQ.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-B9F_Cx_p.js → requirementDiagram-Z7DCOOCP-pQyah6WB.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-RolPi8bU.js → sankeyDiagram-WA2Y5GQK-T6RgG-N8.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-DM-tMAhx.js → sequenceDiagram-2WXFIKYE-BQDJ4CVs.js} +1 -1
- package/dist/web/assets/settings-tab-D3AvU4lu.js +1 -0
- package/dist/web/assets/sqlite-viewer-nA2sD4Yv.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-C4EMl6jf.js → stateDiagram-RAJIS63D-66vhiIuk.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-BGVqj_g9.js +1 -0
- package/dist/web/assets/tab-store-BOgTrqRr.js +1 -0
- package/dist/web/assets/table-DFevCOMd.js +1 -0
- package/dist/web/assets/tag-CXMT0QB6.js +1 -0
- package/dist/web/assets/{terminal-tab-XhKfb4ei.js → terminal-tab-BBi0pEji.js} +1 -1
- package/dist/web/assets/{timeline-definition-YZTLITO2-A4PN_Efm.js → timeline-definition-YZTLITO2-DwZqB3nn.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +1 -0
- package/dist/web/assets/{use-monaco-theme-0p0-84jJ.js → use-monaco-theme-B5pG2d1w.js} +1 -1
- package/dist/web/assets/{vennDiagram-LZ73GAT5-ywK7LMaH.js → vennDiagram-LZ73GAT5-s9Z71fz-.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-DylHYNtJ.js → xychartDiagram-JWTSCODW-DRa_TH4B.js} +1 -1
- package/dist/web/index.html +10 -9
- package/dist/web/monacoeditorwork/css.worker.bundle.js +122 -122
- package/dist/web/monacoeditorwork/editor.worker.bundle.js +78 -78
- package/dist/web/monacoeditorwork/html.worker.bundle.js +110 -110
- package/dist/web/monacoeditorwork/json.worker.bundle.js +108 -108
- package/dist/web/monacoeditorwork/ts.worker.bundle.js +81 -81
- package/dist/web/sw.js +1 -1
- package/docs/code-standards.md +128 -1
- package/docs/codebase-summary.md +79 -12
- package/docs/extension-development-guide.md +532 -0
- package/docs/project-changelog.md +51 -1
- package/docs/project-roadmap.md +9 -3
- package/docs/streaming-input-guide.md +267 -0
- package/docs/system-architecture.md +432 -3
- package/package.json +6 -3
- package/packages/ext-database/package.json +41 -0
- package/packages/ext-database/src/connection-tree.ts +142 -0
- package/packages/ext-database/src/extension.ts +346 -0
- package/packages/ext-database/src/query-panel.ts +120 -0
- package/packages/ext-database/src/table-viewer-panel.ts +410 -0
- package/packages/ext-database/tsconfig.json +8 -0
- package/packages/vscode-compat/package.json +16 -0
- package/packages/vscode-compat/src/commands.ts +39 -0
- package/packages/vscode-compat/src/context.ts +65 -0
- package/packages/vscode-compat/src/disposable.ts +21 -0
- package/packages/vscode-compat/src/env.ts +20 -0
- package/packages/vscode-compat/src/event-emitter.ts +28 -0
- package/packages/vscode-compat/src/index.ts +93 -0
- package/packages/vscode-compat/src/not-supported.ts +15 -0
- package/packages/vscode-compat/src/types.ts +167 -0
- package/packages/vscode-compat/src/uri.ts +65 -0
- package/packages/vscode-compat/src/window.ts +229 -0
- package/packages/vscode-compat/src/workspace.ts +76 -0
- package/packages/vscode-compat/tsconfig.json +10 -0
- package/snapshot-state.md +1526 -0
- package/src/cli/commands/autostart.ts +1 -1
- package/src/cli/commands/ext-cmd.ts +121 -0
- package/src/cli/commands/restart.ts +9 -1
- package/src/cli/commands/status.ts +19 -0
- package/src/index.ts +5 -3
- package/src/providers/claude-agent-sdk.ts +221 -17
- package/src/providers/cli-provider-base.ts +6 -0
- package/src/server/index.ts +55 -155
- package/src/server/routes/chat.ts +81 -11
- package/src/server/routes/extensions.ts +81 -0
- package/src/server/routes/project-scoped.ts +2 -0
- package/src/server/routes/settings.ts +27 -0
- package/src/server/routes/workspace.ts +35 -0
- package/src/server/ws/chat.ts +9 -3
- package/src/server/ws/extensions.ts +175 -0
- package/src/services/account-selector.service.ts +14 -5
- package/src/services/account.service.ts +7 -7
- package/src/services/claude-usage.service.ts +11 -11
- package/src/services/cloud-ws.service.ts +228 -0
- package/src/services/cloud.service.ts +1 -0
- package/src/services/contribution-registry.ts +110 -0
- package/src/services/db.service.ts +181 -4
- package/src/services/extension-host-worker.ts +160 -0
- package/src/services/extension-installer.ts +112 -0
- package/src/services/extension-manifest.ts +65 -0
- package/src/services/extension-rpc-handlers.ts +235 -0
- package/src/services/extension-rpc.ts +105 -0
- package/src/services/extension.service.ts +228 -0
- package/src/services/mcp-config.service.ts +15 -6
- package/src/services/supervisor.ts +271 -25
- package/src/types/api.ts +1 -0
- package/src/types/chat.ts +4 -0
- package/src/types/extension-messages.ts +64 -0
- package/src/types/extension.ts +131 -0
- package/src/web/app.tsx +69 -48
- package/src/web/components/chat/account-rotation-settings.tsx +163 -0
- package/src/web/components/chat/chat-history-bar.tsx +106 -10
- package/src/web/components/chat/chat-tab.tsx +15 -10
- package/src/web/components/chat/chat-welcome.tsx +148 -0
- package/src/web/components/chat/message-list.tsx +19 -6
- package/src/web/components/chat/session-picker.tsx +80 -32
- package/src/web/components/chat/usage-badge.tsx +68 -8
- package/src/web/components/extensions/extension-inputbox.tsx +92 -0
- package/src/web/components/extensions/extension-quickpick.tsx +194 -0
- package/src/web/components/extensions/extension-tree-view.tsx +240 -0
- package/src/web/components/extensions/extension-webview.tsx +83 -0
- package/src/web/components/layout/command-palette.tsx +22 -2
- package/src/web/components/layout/editor-panel.tsx +163 -18
- package/src/web/components/layout/mobile-nav.tsx +2 -1
- package/src/web/components/layout/sidebar.tsx +21 -3
- package/src/web/components/layout/status-bar.tsx +64 -0
- package/src/web/components/layout/tab-bar.tsx +2 -0
- package/src/web/components/layout/tab-content.tsx +5 -0
- package/src/web/components/layout/upgrade-banner.tsx +15 -5
- package/src/web/components/settings/change-password-section.tsx +128 -0
- package/src/web/components/settings/extension-manager-section.tsx +214 -0
- package/src/web/components/settings/settings-tab.tsx +9 -2
- package/src/web/components/shared/connection-lost-overlay.tsx +89 -0
- package/src/web/hooks/use-chat.ts +28 -0
- package/src/web/hooks/use-extension-ws.ts +181 -0
- package/src/web/hooks/use-global-keybindings.ts +18 -2
- package/src/web/hooks/use-server-reload.ts +9 -0
- package/src/web/hooks/use-url-sync.ts +173 -21
- package/src/web/stores/connection-store.ts +39 -0
- package/src/web/stores/extension-store.ts +204 -0
- package/src/web/stores/panel-store.ts +63 -9
- package/src/web/stores/panel-utils.ts +145 -3
- package/src/web/stores/settings-store.ts +7 -2
- package/src/web/stores/tab-store.ts +2 -1
- package/test-session-ops.mjs +444 -0
- package/test-tokens.mjs +212 -0
- package/tsconfig.json +3 -1
- package/dist/web/assets/api-settings-CEMxVMCV.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-CFzkFKEL.js +0 -1
- package/dist/web/assets/arrow-up--LjUXLEt.js +0 -1
- package/dist/web/assets/browser-tab-D1Zua62g.js +0 -1
- package/dist/web/assets/channel-C2fMafck.js +0 -1
- package/dist/web/assets/chat-tab-BnD27Vp9.js +0 -7
- package/dist/web/assets/chevron-right-CHnjJt4E.js +0 -1
- package/dist/web/assets/chunk-GLR3WWYH-DBdWQ3zy.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-BBw_z0fW.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-DFKFM_C1.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-Dp4Kk3Yb.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-D8IvcV_B.js +0 -1
- package/dist/web/assets/clone-B2hUek6n.js +0 -1
- package/dist/web/assets/code-editor-DGRg8stf.js +0 -2
- package/dist/web/assets/columns-2-DbesTfa7.js +0 -1
- package/dist/web/assets/database-viewer-DxCXZQcE.js +0 -1
- package/dist/web/assets/diff-viewer-C1sDJG35.js +0 -4
- package/dist/web/assets/git-graph-BDn-EiGE.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-CNlas3Rz.js +0 -1
- package/dist/web/assets/index-Bun94AK3.js +0 -37
- package/dist/web/assets/index-Db8uky1a.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-BDzTLc11.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-ZZmpgc6t.js +0 -2
- package/dist/web/assets/jsx-runtime-BRW_vwa9.js +0 -1
- package/dist/web/assets/keybindings-store-COmK4Dte.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-IVa5F-go.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-CvXHKAzp.js +0 -1
- package/dist/web/assets/postgres-viewer-CvQZ8gkh.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-Z-Tr5wtS.js +0 -1
- package/dist/web/assets/settings-tab-RCnvZ29H.js +0 -1
- package/dist/web/assets/sqlite-viewer-CEEm2W4C.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-B-UjZch3.js +0 -1
- package/dist/web/assets/tab-store-Bjh6bXFP.js +0 -1
- package/dist/web/assets/table-CQVQM2SB.js +0 -1
- package/dist/web/assets/tag-Q2dZiSPX.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-C9TYRE0k.js +0 -1
- /package/dist/web/assets/{api-client-BKIT_Qeg.js → api-client-BfBM3I7n.js} +0 -0
- /package/dist/web/assets/{array-DqLCdDFv.js → array-B9UHiPd-.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-CWPXKqbJ.js → cytoscape.esm-BW-DbntU.js} +0 -0
- /package/dist/web/assets/{defaultLocale-CrJzLgRD.js → defaultLocale-5eAKkKJC.js} +0 -0
- /package/dist/web/assets/{dist-Cep75xXf.js → dist-CSJdAyA9.js} +0 -0
- /package/dist/web/assets/{init-C0r9Gk5G.js → init-DlZdxViB.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-CGBoxvCD.js → isArrayLikeObject-B_v2FtYn.js} +0 -0
- /package/dist/web/assets/{katex-DzXRfQ_m.js → katex-Bqvo_ZG0.js} +0 -0
- /package/dist/web/assets/{lib-BeaDXEkP.js → lib-BQ34Db2e.js} +0 -0
- /package/dist/web/assets/{math-y9zN1W-N.js → math-069Z4SuC.js} +0 -0
- /package/dist/web/assets/{path-DIKpVbHL.js → path-6uRLdFF7.js} +0 -0
- /package/dist/web/assets/{rough.esm-nHaDi0Kw.js → rough.esm-JX0wREDd.js} +0 -0
- /package/dist/web/assets/{src-Dw4QhedI.js → src-BqX54PbV.js} +0 -0
- /package/dist/web/assets/{utils-DMiycH3O.js → utils-BNytJOb1.js} +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket handler for extension UI bridge.
|
|
3
|
+
* Routes messages between browser clients and the extension host.
|
|
4
|
+
*/
|
|
5
|
+
import { contributionRegistry } from "../../services/contribution-registry.ts";
|
|
6
|
+
import type { ExtServerMsg, ExtClientMsg } from "../../types/extension-messages.ts";
|
|
7
|
+
|
|
8
|
+
type ExtWsSocket = {
|
|
9
|
+
data: { type: string };
|
|
10
|
+
send: (data: string) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** All connected extension WS clients */
|
|
14
|
+
const clients = new Set<ExtWsSocket>();
|
|
15
|
+
|
|
16
|
+
/** Pending request resolvers for quickpick/inputbox responses from browser */
|
|
17
|
+
const pendingRequests = new Map<string, (value: unknown) => void>();
|
|
18
|
+
|
|
19
|
+
// --- Public API for extension service to push UI updates ---
|
|
20
|
+
|
|
21
|
+
/** Broadcast a message to all connected extension WS clients */
|
|
22
|
+
export function broadcastExtMsg(msg: ExtServerMsg): void {
|
|
23
|
+
const data = JSON.stringify(msg);
|
|
24
|
+
for (const ws of clients) {
|
|
25
|
+
try { ws.send(data); } catch {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Send a request to browser and wait for response (quickpick, inputbox, notification).
|
|
31
|
+
* The `trackingId` is the key used to match the response.
|
|
32
|
+
* Returns the resolved value or undefined on timeout.
|
|
33
|
+
*/
|
|
34
|
+
export function requestFromBrowser<T = unknown>(
|
|
35
|
+
msg: ExtServerMsg,
|
|
36
|
+
trackingId: string,
|
|
37
|
+
timeoutMs = 30_000,
|
|
38
|
+
): Promise<T | undefined> {
|
|
39
|
+
broadcastExtMsg(msg);
|
|
40
|
+
return new Promise<T | undefined>((resolve) => {
|
|
41
|
+
const timer = setTimeout(() => {
|
|
42
|
+
pendingRequests.delete(trackingId);
|
|
43
|
+
resolve(undefined);
|
|
44
|
+
}, timeoutMs);
|
|
45
|
+
pendingRequests.set(trackingId, (value) => {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
resolve(value as T);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Get the number of connected extension WS clients */
|
|
53
|
+
export function getExtClientCount(): number {
|
|
54
|
+
return clients.size;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- WS lifecycle handlers ---
|
|
58
|
+
|
|
59
|
+
function handleOpen(ws: ExtWsSocket): void {
|
|
60
|
+
clients.add(ws);
|
|
61
|
+
console.log(`[ExtWS] Client connected (${clients.size} total)`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function handleMessage(ws: ExtWsSocket, raw: string | Buffer): Promise<void> {
|
|
65
|
+
let msg: ExtClientMsg;
|
|
66
|
+
try {
|
|
67
|
+
msg = JSON.parse(typeof raw === "string" ? raw : raw.toString()) as ExtClientMsg;
|
|
68
|
+
} catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
switch (msg.type) {
|
|
73
|
+
case "ready": {
|
|
74
|
+
// Send current contributions on connect
|
|
75
|
+
const contributions = contributionRegistry.getAll();
|
|
76
|
+
ws.send(JSON.stringify({ type: "contributions:update", contributions } satisfies ExtServerMsg));
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
case "command:execute": {
|
|
81
|
+
try {
|
|
82
|
+
const { extensionService } = await import("../../services/extension.service.ts");
|
|
83
|
+
// Forward to extension host worker via RPC
|
|
84
|
+
if (extensionService["rpc"]) {
|
|
85
|
+
await extensionService["rpc"].sendRequest("ext:command:execute", msg.command, ...(msg.args ?? []));
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.error(`[ExtWS] command:execute error:`, e);
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case "tree:click": {
|
|
94
|
+
if (msg.command) {
|
|
95
|
+
try {
|
|
96
|
+
const { extensionService } = await import("../../services/extension.service.ts");
|
|
97
|
+
if (extensionService["rpc"]) {
|
|
98
|
+
await extensionService["rpc"].sendRequest("ext:command:execute", msg.command);
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.error(`[ExtWS] tree:click command error:`, e);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case "quickpick:resolve": {
|
|
108
|
+
const resolver = pendingRequests.get(msg.requestId);
|
|
109
|
+
if (resolver) {
|
|
110
|
+
pendingRequests.delete(msg.requestId);
|
|
111
|
+
resolver(msg.selected);
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case "inputbox:resolve": {
|
|
117
|
+
const resolver = pendingRequests.get(msg.requestId);
|
|
118
|
+
if (resolver) {
|
|
119
|
+
pendingRequests.delete(msg.requestId);
|
|
120
|
+
resolver(msg.value);
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case "notification:action": {
|
|
126
|
+
const resolver = pendingRequests.get(msg.id);
|
|
127
|
+
if (resolver) {
|
|
128
|
+
pendingRequests.delete(msg.id);
|
|
129
|
+
resolver(msg.action);
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case "webview:message": {
|
|
135
|
+
try {
|
|
136
|
+
const { extensionService } = await import("../../services/extension.service.ts");
|
|
137
|
+
if (extensionService["rpc"]) {
|
|
138
|
+
await extensionService["rpc"].sendRequest("ext:webview:message", msg.panelId, msg.message);
|
|
139
|
+
}
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error(`[ExtWS] webview:message error:`, e);
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
case "tree:expand": {
|
|
147
|
+
try {
|
|
148
|
+
const { extensionService } = await import("../../services/extension.service.ts");
|
|
149
|
+
if (extensionService["rpc"]) {
|
|
150
|
+
const result = await extensionService["rpc"].sendRequest<{ ok: boolean; items?: unknown[] }>(
|
|
151
|
+
"ext:tree:expand", msg.viewId, msg.itemId,
|
|
152
|
+
);
|
|
153
|
+
if (result?.ok && result.items) {
|
|
154
|
+
// Send children back to the requesting client (parentId distinguishes child updates from root updates)
|
|
155
|
+
ws.send(JSON.stringify({ type: "tree:update", viewId: msg.viewId, items: result.items as import("../../types/extension-messages.ts").TreeItemMsg[], parentId: msg.itemId } satisfies ExtServerMsg));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch (e) {
|
|
159
|
+
console.error(`[ExtWS] tree:expand error:`, e);
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function handleClose(ws: ExtWsSocket): void {
|
|
167
|
+
clients.delete(ws);
|
|
168
|
+
console.log(`[ExtWS] Client disconnected (${clients.size} remaining)`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export const extensionWebSocket = {
|
|
172
|
+
open: handleOpen,
|
|
173
|
+
message: handleMessage,
|
|
174
|
+
close: handleClose,
|
|
175
|
+
};
|
|
@@ -9,6 +9,8 @@ const MAX_RETRY_CONFIG_KEY = "account_max_retry";
|
|
|
9
9
|
const BACKOFF_BASE_MS = 1_000;
|
|
10
10
|
const BACKOFF_MAX_MS = 30 * 60_000;
|
|
11
11
|
const AUTH_BACKOFF_BASE_MS = 5 * 60_000; // 5min base for auth errors (longer than rate limits)
|
|
12
|
+
/** Skip accounts whose 5-hour utilization >= this threshold (proactive avoidance) */
|
|
13
|
+
const FIVE_HOUR_SKIP_THRESHOLD = 0.95;
|
|
12
14
|
|
|
13
15
|
class AccountSelectorService {
|
|
14
16
|
private cursor = 0;
|
|
@@ -74,18 +76,25 @@ class AccountSelectorService {
|
|
|
74
76
|
return null;
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
// Proactive: skip accounts whose 5-hour utilization >= 95%
|
|
80
|
+
const usable = active.filter((a) => {
|
|
81
|
+
const snap = getLatestSnapshotForAccount(a.id);
|
|
82
|
+
return !snap || (snap.five_hour_util ?? 0) < FIVE_HOUR_SKIP_THRESHOLD;
|
|
83
|
+
});
|
|
84
|
+
const candidates = usable.length > 0 ? usable : active; // fallback to all if every account is near limit
|
|
85
|
+
|
|
77
86
|
let pickedId: string;
|
|
78
87
|
const strategy = this.getStrategy();
|
|
79
88
|
if (strategy === "lowest-usage") {
|
|
80
|
-
pickedId = this.pickLowestUsage(
|
|
89
|
+
pickedId = this.pickLowestUsage(candidates);
|
|
81
90
|
} else if (strategy === "fill-first") {
|
|
82
|
-
const sorted = [...
|
|
91
|
+
const sorted = [...candidates].sort((a, b) => b.priority - a.priority || a.createdAt - b.createdAt);
|
|
83
92
|
pickedId = sorted[0]!.id;
|
|
84
93
|
} else {
|
|
85
94
|
// Round-robin
|
|
86
|
-
this.cursor = this.cursor %
|
|
87
|
-
pickedId =
|
|
88
|
-
this.cursor = (this.cursor + 1) %
|
|
95
|
+
this.cursor = this.cursor % candidates.length;
|
|
96
|
+
pickedId = candidates[this.cursor]!.id;
|
|
97
|
+
this.cursor = (this.cursor + 1) % candidates.length;
|
|
89
98
|
}
|
|
90
99
|
this._lastPickedId = pickedId;
|
|
91
100
|
const result = accountService.getWithTokens(pickedId);
|
|
@@ -139,7 +139,7 @@ class AccountService {
|
|
|
139
139
|
await this.refreshAccessToken(id, false);
|
|
140
140
|
return this.getWithTokens(id);
|
|
141
141
|
} catch (e) {
|
|
142
|
-
console.error(`[accounts] Pre-flight refresh failed for ${id}
|
|
142
|
+
console.error(`[accounts] Pre-flight refresh failed for ${id}: ${(e as Error).message ?? e}`);
|
|
143
143
|
return null;
|
|
144
144
|
}
|
|
145
145
|
}
|
|
@@ -623,13 +623,13 @@ class AccountService {
|
|
|
623
623
|
if (!row.id || !row.access_token) continue;
|
|
624
624
|
const hasRefresh = !!row.refresh_token && row.refresh_token !== "";
|
|
625
625
|
|
|
626
|
-
// Duplicate handling:
|
|
626
|
+
// Duplicate handling: update existing account tokens from import
|
|
627
627
|
const existingById = getAccountById(row.id);
|
|
628
628
|
const existingByEmail = row.email ? this.list().find((a) => a.email === row.email) : null;
|
|
629
629
|
const existing = existingById ?? (existingByEmail ? getAccountById(existingByEmail.id) : null);
|
|
630
630
|
if (existing) {
|
|
631
|
-
if (hasRefresh
|
|
632
|
-
//
|
|
631
|
+
if (hasRefresh) {
|
|
632
|
+
// Always update tokens when import has refresh token (handles expired/invalid tokens too)
|
|
633
633
|
let accessToken = row.access_token;
|
|
634
634
|
if (!looksEncrypted(accessToken)) accessToken = encrypt(accessToken);
|
|
635
635
|
const refreshToken = looksEncrypted(row.refresh_token) ? row.refresh_token : encrypt(row.refresh_token);
|
|
@@ -641,9 +641,9 @@ class AccountService {
|
|
|
641
641
|
});
|
|
642
642
|
imported++;
|
|
643
643
|
fullTransferIds.push(existing.id);
|
|
644
|
-
console.log(`[accounts]
|
|
644
|
+
console.log(`[accounts] Updated ${row.email ?? existing.id} tokens from import`);
|
|
645
645
|
}
|
|
646
|
-
continue; // skip if
|
|
646
|
+
continue; // skip if import doesn't have refresh token
|
|
647
647
|
}
|
|
648
648
|
|
|
649
649
|
// New account — insert
|
|
@@ -709,7 +709,7 @@ class AccountService {
|
|
|
709
709
|
try {
|
|
710
710
|
await this.refreshAccessToken(acc.id, false);
|
|
711
711
|
} catch (e) {
|
|
712
|
-
console.error(`[accounts] Auto-refresh failed for ${acc.id}
|
|
712
|
+
console.error(`[accounts] Auto-refresh failed for ${acc.id}: ${(e as Error).message ?? e}`);
|
|
713
713
|
}
|
|
714
714
|
}
|
|
715
715
|
};
|
|
@@ -273,28 +273,28 @@ export function getUsageForAccount(accountId: string): ClaudeUsage {
|
|
|
273
273
|
return row ? snapshotToUsage(row) : {};
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
/** Get usage for all accounts
|
|
276
|
+
/** Get usage for all accounts */
|
|
277
277
|
export function getAllAccountUsages(): AccountUsageEntry[] {
|
|
278
|
-
const
|
|
279
|
-
const accounts = accountService.list().filter(acc => {
|
|
280
|
-
// Exclude expired accounts without refresh token (temporary/invalid)
|
|
281
|
-
if (!accountService.hasRefreshToken(acc.id) && acc.expiresAt && acc.expiresAt < nowS) return false;
|
|
282
|
-
return true;
|
|
283
|
-
});
|
|
278
|
+
const accounts = accountService.list();
|
|
284
279
|
const snapshots = getAllLatestSnapshots();
|
|
285
280
|
const snapshotMap = new Map(snapshots.map(s => [s.account_id, s]));
|
|
286
|
-
|
|
281
|
+
const nowS = Math.floor(Date.now() / 1000);
|
|
282
|
+
const result: AccountUsageEntry[] = [];
|
|
283
|
+
for (const acc of accounts) {
|
|
287
284
|
const withTokens = accountService.getWithTokens(acc.id);
|
|
285
|
+
// Skip expired accounts without refresh token (temporary/disposable)
|
|
286
|
+
if (acc.expiresAt && acc.expiresAt < nowS && !withTokens?.refreshToken) continue;
|
|
288
287
|
const isOAuth = withTokens?.accessToken.startsWith("sk-ant-oat") ?? false;
|
|
289
288
|
const row = snapshotMap.get(acc.id);
|
|
290
|
-
|
|
289
|
+
result.push({
|
|
291
290
|
accountId: acc.id,
|
|
292
291
|
accountLabel: acc.label,
|
|
293
292
|
accountStatus: acc.status,
|
|
294
293
|
isOAuth,
|
|
295
294
|
usage: row ? snapshotToUsage(row) : {},
|
|
296
|
-
};
|
|
297
|
-
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
/** Get cached usage for active account (used by chat header) */
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud WebSocket client — persistent connection from supervisor to PPM Cloud.
|
|
3
|
+
* Auto-reconnects with exponential backoff + jitter. Queues messages when disconnected.
|
|
4
|
+
*/
|
|
5
|
+
import { appendFileSync } from "node:fs";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
|
|
9
|
+
// ─── Types (must match Cloud's ws-types.ts) ─────────
|
|
10
|
+
interface WsMessage {
|
|
11
|
+
type: string;
|
|
12
|
+
id?: string;
|
|
13
|
+
timestamp: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface HeartbeatMsg extends WsMessage {
|
|
17
|
+
type: "heartbeat";
|
|
18
|
+
tunnelUrl: string | null;
|
|
19
|
+
state: string;
|
|
20
|
+
appVersion: string;
|
|
21
|
+
availableVersion: string | null;
|
|
22
|
+
serverPid: number | null;
|
|
23
|
+
uptime: number;
|
|
24
|
+
deviceName?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface StateChangeMsg extends WsMessage {
|
|
28
|
+
type: "state_change";
|
|
29
|
+
from: string;
|
|
30
|
+
to: string;
|
|
31
|
+
reason: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface CommandAckMsg extends WsMessage {
|
|
35
|
+
type: "command_ack";
|
|
36
|
+
id: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface CommandResultMsg extends WsMessage {
|
|
40
|
+
type: "command_result";
|
|
41
|
+
id: string;
|
|
42
|
+
success: boolean;
|
|
43
|
+
error?: string;
|
|
44
|
+
data?: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type OutboundMsg = HeartbeatMsg | StateChangeMsg | CommandAckMsg | CommandResultMsg;
|
|
48
|
+
|
|
49
|
+
interface CommandMsg extends WsMessage {
|
|
50
|
+
type: "command";
|
|
51
|
+
id: string;
|
|
52
|
+
action: string;
|
|
53
|
+
params?: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type CommandHandler = (cmd: CommandMsg) => void;
|
|
57
|
+
|
|
58
|
+
// ─── Constants ──────────────────────────────────────
|
|
59
|
+
const BACKOFF_STEPS = [1000, 2000, 4000, 8000, 15000, 30000, 60000];
|
|
60
|
+
const MAX_QUEUE_SIZE = 50;
|
|
61
|
+
const HEARTBEAT_INTERVAL_MS = 60_000; // 60s via WS
|
|
62
|
+
|
|
63
|
+
// ─── State ──────────────────────────────────────────
|
|
64
|
+
let ws: WebSocket | null = null;
|
|
65
|
+
let connected = false;
|
|
66
|
+
let reconnecting = false;
|
|
67
|
+
let reconnectAttempt = 0;
|
|
68
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
69
|
+
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
70
|
+
let commandHandler: CommandHandler | null = null;
|
|
71
|
+
let outboundQueue: OutboundMsg[] = [];
|
|
72
|
+
let wsUrl = "";
|
|
73
|
+
let shouldConnect = false;
|
|
74
|
+
|
|
75
|
+
// Credentials for first-message auth
|
|
76
|
+
let deviceId = "";
|
|
77
|
+
let secretKey = "";
|
|
78
|
+
|
|
79
|
+
// For heartbeat payload
|
|
80
|
+
let getHeartbeatData: (() => HeartbeatMsg) | null = null;
|
|
81
|
+
|
|
82
|
+
// ─── Public API ─────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
export function connect(opts: {
|
|
85
|
+
cloudUrl: string;
|
|
86
|
+
deviceId: string;
|
|
87
|
+
secretKey: string;
|
|
88
|
+
heartbeatFn: () => HeartbeatMsg;
|
|
89
|
+
}): void {
|
|
90
|
+
// No secret_key in URL — auth via first message after connect
|
|
91
|
+
wsUrl = `${opts.cloudUrl.replace(/^http/, "ws")}/ws/device`;
|
|
92
|
+
deviceId = opts.deviceId;
|
|
93
|
+
secretKey = opts.secretKey;
|
|
94
|
+
getHeartbeatData = opts.heartbeatFn;
|
|
95
|
+
shouldConnect = true;
|
|
96
|
+
reconnectAttempt = 0;
|
|
97
|
+
doConnect();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function disconnect(): void {
|
|
101
|
+
shouldConnect = false;
|
|
102
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
103
|
+
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
|
|
104
|
+
if (ws) {
|
|
105
|
+
try { ws.close(1000, "shutdown"); } catch {}
|
|
106
|
+
ws = null;
|
|
107
|
+
}
|
|
108
|
+
connected = false;
|
|
109
|
+
outboundQueue = [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function send(msg: OutboundMsg): void {
|
|
113
|
+
if (connected && ws?.readyState === WebSocket.OPEN) {
|
|
114
|
+
ws.send(JSON.stringify(msg));
|
|
115
|
+
} else {
|
|
116
|
+
outboundQueue.push(msg);
|
|
117
|
+
if (outboundQueue.length > MAX_QUEUE_SIZE) outboundQueue.shift();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function onCommand(handler: CommandHandler): void {
|
|
122
|
+
commandHandler = handler;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function isConnected(): boolean {
|
|
126
|
+
return connected;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Internal ───────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
function doConnect(): void {
|
|
132
|
+
if (!shouldConnect || reconnecting) return;
|
|
133
|
+
reconnecting = true;
|
|
134
|
+
|
|
135
|
+
// Capture local ref — if a reconnect replaces `ws` before this socket's
|
|
136
|
+
// handlers fire, stale handlers must not reset module-level state.
|
|
137
|
+
let sock: WebSocket;
|
|
138
|
+
try {
|
|
139
|
+
sock = new WebSocket(wsUrl);
|
|
140
|
+
ws = sock;
|
|
141
|
+
} catch {
|
|
142
|
+
reconnecting = false;
|
|
143
|
+
scheduleReconnect("constructor");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
sock.onopen = () => {
|
|
148
|
+
if (ws !== sock) return; // stale — newer connection replaced us
|
|
149
|
+
reconnecting = false;
|
|
150
|
+
reconnectAttempt = 0;
|
|
151
|
+
log("INFO", "Cloud WS connected, sending auth");
|
|
152
|
+
|
|
153
|
+
// Send auth as first message — server must process this before any other msg
|
|
154
|
+
sock.send(JSON.stringify({
|
|
155
|
+
type: "auth",
|
|
156
|
+
deviceId,
|
|
157
|
+
secretKey,
|
|
158
|
+
timestamp: new Date().toISOString(),
|
|
159
|
+
version: 1,
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
// Delay setting connected + sending heartbeat to let server process auth.
|
|
163
|
+
// Server's authenticateDevice() is async (DB lookup), so messages sent
|
|
164
|
+
// immediately after auth arrive before authenticated=true → 4002 reject.
|
|
165
|
+
setTimeout(() => {
|
|
166
|
+
if (ws !== sock) return; // replaced during delay
|
|
167
|
+
connected = true;
|
|
168
|
+
|
|
169
|
+
// Flush queued messages
|
|
170
|
+
while (outboundQueue.length > 0 && connected) {
|
|
171
|
+
const msg = outboundQueue.shift()!;
|
|
172
|
+
sock.send(JSON.stringify(msg));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Send immediate heartbeat
|
|
176
|
+
if (getHeartbeatData) send(getHeartbeatData());
|
|
177
|
+
|
|
178
|
+
// Start periodic heartbeat
|
|
179
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
180
|
+
heartbeatTimer = setInterval(() => {
|
|
181
|
+
if (getHeartbeatData && connected) send(getHeartbeatData());
|
|
182
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
183
|
+
}, 500); // 500ms for DB auth round-trip
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
sock.onmessage = (event) => {
|
|
187
|
+
try {
|
|
188
|
+
const msg = JSON.parse(String(event.data)) as CommandMsg;
|
|
189
|
+
if (msg.type === "command" && commandHandler) {
|
|
190
|
+
commandHandler(msg);
|
|
191
|
+
}
|
|
192
|
+
} catch {} // ignore malformed
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
sock.onclose = (event) => {
|
|
196
|
+
if (ws !== sock) return; // stale — ignore close from replaced connection
|
|
197
|
+
log("WARN", `Cloud WS closed: code=${event.code} reason=${event.reason || ""}`);
|
|
198
|
+
connected = false;
|
|
199
|
+
reconnecting = false;
|
|
200
|
+
ws = null;
|
|
201
|
+
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
|
|
202
|
+
if (shouldConnect) scheduleReconnect("onclose");
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
sock.onerror = (event) => {
|
|
206
|
+
log("ERROR", `Cloud WS error: ${String(event)}`);
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function scheduleReconnect(source = "unknown"): void {
|
|
211
|
+
if (!shouldConnect || reconnectTimer) return;
|
|
212
|
+
const base = BACKOFF_STEPS[Math.min(reconnectAttempt, BACKOFF_STEPS.length - 1)]!;
|
|
213
|
+
// Add ±30% jitter to prevent thundering herd after Cloud deploy
|
|
214
|
+
const jitter = base * (0.7 + Math.random() * 0.6);
|
|
215
|
+
const delay = Math.round(jitter);
|
|
216
|
+
reconnectAttempt++;
|
|
217
|
+
log("WARN", `Cloud WS reconnect in ${delay}ms (attempt #${reconnectAttempt}) src=${source}`);
|
|
218
|
+
reconnectTimer = setTimeout(() => {
|
|
219
|
+
reconnectTimer = null;
|
|
220
|
+
doConnect();
|
|
221
|
+
}, delay);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function log(level: string, msg: string): void {
|
|
225
|
+
const ts = new Date().toISOString();
|
|
226
|
+
const logFile = resolve(process.env.PPM_HOME || resolve(homedir(), ".ppm"), "ppm.log");
|
|
227
|
+
try { appendFileSync(logFile, `[${ts}] [${level}] [cloud-ws] ${msg}\n`); } catch {}
|
|
228
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { ExtensionContributes, ContributedCommand, ContributedView, ContributedMenu } from "../types/extension.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory registry of all contribution points from enabled extensions.
|
|
5
|
+
* Populated when extensions activate, cleared when they deactivate.
|
|
6
|
+
*/
|
|
7
|
+
class ContributionRegistry {
|
|
8
|
+
private commands = new Map<string, ContributedCommand & { extId: string }>();
|
|
9
|
+
private views = new Map<string, Map<string, ContributedView & { extId: string }>>();
|
|
10
|
+
private configs = new Map<string, Record<string, unknown>>();
|
|
11
|
+
private menus = new Map<string, Array<ContributedMenu & { extId: string }>>();
|
|
12
|
+
|
|
13
|
+
register(extId: string, contributes: ExtensionContributes): void {
|
|
14
|
+
if (contributes.commands) {
|
|
15
|
+
for (const cmd of contributes.commands) {
|
|
16
|
+
this.commands.set(cmd.command, { ...cmd, extId });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (contributes.views) {
|
|
20
|
+
for (const [location, views] of Object.entries(contributes.views)) {
|
|
21
|
+
if (!this.views.has(location)) this.views.set(location, new Map());
|
|
22
|
+
const locationMap = this.views.get(location)!;
|
|
23
|
+
for (const view of views) {
|
|
24
|
+
locationMap.set(view.id, { ...view, extId });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (contributes.configuration?.properties) {
|
|
29
|
+
this.configs.set(extId, contributes.configuration.properties);
|
|
30
|
+
}
|
|
31
|
+
if (contributes.menus) {
|
|
32
|
+
for (const [location, items] of Object.entries(contributes.menus)) {
|
|
33
|
+
if (!this.menus.has(location)) this.menus.set(location, []);
|
|
34
|
+
const list = this.menus.get(location)!;
|
|
35
|
+
for (const item of items) {
|
|
36
|
+
list.push({ ...item, extId });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
unregister(extId: string): void {
|
|
43
|
+
for (const [key, cmd] of this.commands) {
|
|
44
|
+
if (cmd.extId === extId) this.commands.delete(key);
|
|
45
|
+
}
|
|
46
|
+
for (const [, locationMap] of this.views) {
|
|
47
|
+
for (const [key, view] of locationMap) {
|
|
48
|
+
if (view.extId === extId) locationMap.delete(key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const [location, items] of this.menus) {
|
|
52
|
+
this.menus.set(location, items.filter((m) => m.extId !== extId));
|
|
53
|
+
}
|
|
54
|
+
this.configs.delete(extId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getCommands(): Array<ContributedCommand & { extId: string }> {
|
|
58
|
+
return [...this.commands.values()];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getViews(location?: string): Array<ContributedView & { extId: string }> {
|
|
62
|
+
if (location) {
|
|
63
|
+
return [...(this.views.get(location)?.values() ?? [])];
|
|
64
|
+
}
|
|
65
|
+
const all: Array<ContributedView & { extId: string }> = [];
|
|
66
|
+
for (const locationMap of this.views.values()) {
|
|
67
|
+
all.push(...locationMap.values());
|
|
68
|
+
}
|
|
69
|
+
return all;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getViewLocations(): string[] {
|
|
73
|
+
return [...this.views.keys()];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getConfiguration(extId?: string): Record<string, Record<string, unknown>> {
|
|
77
|
+
if (extId) {
|
|
78
|
+
const cfg = this.configs.get(extId);
|
|
79
|
+
return cfg ? { [extId]: cfg } : {};
|
|
80
|
+
}
|
|
81
|
+
return Object.fromEntries(this.configs);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Get all contributions as a single object (for API responses) */
|
|
85
|
+
getAll() {
|
|
86
|
+
const viewsByLocation: Record<string, Array<ContributedView & { extId: string }>> = {};
|
|
87
|
+
for (const location of this.views.keys()) {
|
|
88
|
+
viewsByLocation[location] = this.getViews(location);
|
|
89
|
+
}
|
|
90
|
+
const menusByLocation: Record<string, Array<ContributedMenu & { extId: string }>> = {};
|
|
91
|
+
for (const [location, items] of this.menus) {
|
|
92
|
+
menusByLocation[location] = items;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
commands: this.getCommands(),
|
|
96
|
+
views: viewsByLocation,
|
|
97
|
+
menus: menusByLocation,
|
|
98
|
+
configuration: this.getConfiguration(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
clear(): void {
|
|
103
|
+
this.commands.clear();
|
|
104
|
+
this.views.clear();
|
|
105
|
+
this.configs.clear();
|
|
106
|
+
this.menus.clear();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const contributionRegistry = new ContributionRegistry();
|