@hienlh/ppm 0.9.0-beta.9 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude.bak/agent-memory/tester/MEMORY.md +3 -0
- package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +32 -0
- package/CHANGELOG.md +240 -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-CjUzlPYv.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-moB4W7-w.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-aQQZUc2m.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-ChyP1N3c.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-ktwO5JbX.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-Bx1TlP6q.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-BIrGMX6e.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-C6KLr58u.js +37 -0
- package/dist/web/assets/index-DpBKDbIW.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-D3Y5c5uS.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-A7J2gdKT.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-C9-Acry_.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-C17exmRv.js +1 -0
- package/dist/web/assets/sqlite-viewer-Dr5oWCWA.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-CpyKvyfC.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-BjPAik5w.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/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/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/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-input.tsx +29 -29
- 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 +83 -10
- 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/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,214 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { Plus, Trash2, Power, PowerOff, Puzzle, FolderSymlink, Loader2 } from "lucide-react";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { Input } from "@/components/ui/input";
|
|
5
|
+
import { api } from "@/lib/api-client";
|
|
6
|
+
import { toast } from "sonner";
|
|
7
|
+
|
|
8
|
+
interface ExtensionInfo {
|
|
9
|
+
id: string;
|
|
10
|
+
version: string;
|
|
11
|
+
displayName: string;
|
|
12
|
+
description: string;
|
|
13
|
+
icon: string;
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
activated: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ExtensionManagerSection() {
|
|
19
|
+
const [extensions, setExtensions] = useState<ExtensionInfo[]>([]);
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
const [installName, setInstallName] = useState("");
|
|
22
|
+
const [installing, setInstalling] = useState(false);
|
|
23
|
+
const [devPath, setDevPath] = useState("");
|
|
24
|
+
const [showDevLink, setShowDevLink] = useState(false);
|
|
25
|
+
const [togglingId, setTogglingId] = useState<string | null>(null);
|
|
26
|
+
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
27
|
+
|
|
28
|
+
const fetchExtensions = useCallback(async () => {
|
|
29
|
+
try {
|
|
30
|
+
const data = await api.get<ExtensionInfo[]>("/api/extensions");
|
|
31
|
+
setExtensions(data);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error("Failed to load extensions:", e);
|
|
34
|
+
} finally {
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
useEffect(() => { fetchExtensions(); }, [fetchExtensions]);
|
|
40
|
+
|
|
41
|
+
const handleInstall = async () => {
|
|
42
|
+
const name = installName.trim();
|
|
43
|
+
if (!name) return;
|
|
44
|
+
setInstalling(true);
|
|
45
|
+
try {
|
|
46
|
+
await api.post("/api/extensions/install", { name });
|
|
47
|
+
toast.success(`Installed ${name}`);
|
|
48
|
+
setInstallName("");
|
|
49
|
+
fetchExtensions();
|
|
50
|
+
} catch (e: any) {
|
|
51
|
+
toast.error(e.message || "Install failed");
|
|
52
|
+
} finally {
|
|
53
|
+
setInstalling(false);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleToggle = async (ext: ExtensionInfo) => {
|
|
58
|
+
setTogglingId(ext.id);
|
|
59
|
+
try {
|
|
60
|
+
await api.patch(`/api/extensions/${ext.id}`, { enabled: !ext.enabled });
|
|
61
|
+
fetchExtensions();
|
|
62
|
+
} catch (e: any) {
|
|
63
|
+
toast.error(e.message || "Toggle failed");
|
|
64
|
+
} finally {
|
|
65
|
+
setTogglingId(null);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const handleRemove = async (id: string) => {
|
|
70
|
+
setDeletingId(id);
|
|
71
|
+
try {
|
|
72
|
+
await api.del(`/api/extensions/${id}`);
|
|
73
|
+
toast.success(`Removed ${id}`);
|
|
74
|
+
fetchExtensions();
|
|
75
|
+
} catch (e: any) {
|
|
76
|
+
toast.error(e.message || "Remove failed");
|
|
77
|
+
} finally {
|
|
78
|
+
setDeletingId(null);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (loading) {
|
|
83
|
+
return (
|
|
84
|
+
<div className="flex items-center justify-center py-8">
|
|
85
|
+
<Loader2 className="size-4 animate-spin text-muted-foreground" />
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="space-y-4">
|
|
92
|
+
{/* Install */}
|
|
93
|
+
<section className="space-y-2">
|
|
94
|
+
<h3 className="text-xs font-medium text-muted-foreground">Install Extension</h3>
|
|
95
|
+
<div className="flex gap-1.5">
|
|
96
|
+
<Input
|
|
97
|
+
value={installName}
|
|
98
|
+
onChange={(e) => setInstallName(e.target.value)}
|
|
99
|
+
onKeyDown={(e) => { if (e.key === "Enter") handleInstall(); }}
|
|
100
|
+
placeholder="npm package name (e.g. @ppm/ext-database)"
|
|
101
|
+
className="h-8 text-xs flex-1"
|
|
102
|
+
/>
|
|
103
|
+
<Button
|
|
104
|
+
variant="outline"
|
|
105
|
+
size="sm"
|
|
106
|
+
className="h-8 text-xs px-3 gap-1 cursor-pointer"
|
|
107
|
+
disabled={!installName.trim() || installing}
|
|
108
|
+
onClick={handleInstall}
|
|
109
|
+
>
|
|
110
|
+
{installing ? <Loader2 className="size-3 animate-spin" /> : <Plus className="size-3" />}
|
|
111
|
+
Install
|
|
112
|
+
</Button>
|
|
113
|
+
</div>
|
|
114
|
+
<button
|
|
115
|
+
onClick={() => setShowDevLink(!showDevLink)}
|
|
116
|
+
className="text-[11px] text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
|
117
|
+
>
|
|
118
|
+
{showDevLink ? "Hide" : "Dev link local extension..."}
|
|
119
|
+
</button>
|
|
120
|
+
{showDevLink && (
|
|
121
|
+
<div className="flex gap-1.5">
|
|
122
|
+
<Input
|
|
123
|
+
value={devPath}
|
|
124
|
+
onChange={(e) => setDevPath(e.target.value)}
|
|
125
|
+
placeholder="Local path (e.g. ./packages/ext-database)"
|
|
126
|
+
className="h-8 text-xs flex-1"
|
|
127
|
+
/>
|
|
128
|
+
<Button
|
|
129
|
+
variant="outline"
|
|
130
|
+
size="sm"
|
|
131
|
+
className="h-8 text-xs px-3 gap-1 cursor-pointer"
|
|
132
|
+
disabled={!devPath.trim()}
|
|
133
|
+
onClick={async () => {
|
|
134
|
+
try {
|
|
135
|
+
await api.post("/api/extensions/dev-link", { path: devPath.trim() });
|
|
136
|
+
toast.success("Dev-linked successfully");
|
|
137
|
+
setDevPath("");
|
|
138
|
+
fetchExtensions();
|
|
139
|
+
} catch (e: any) {
|
|
140
|
+
toast.error(e.message || "Dev link failed");
|
|
141
|
+
}
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<FolderSymlink className="size-3" />
|
|
145
|
+
Link
|
|
146
|
+
</Button>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</section>
|
|
150
|
+
|
|
151
|
+
{/* Extension list */}
|
|
152
|
+
<section className="space-y-2">
|
|
153
|
+
<h3 className="text-xs font-medium text-muted-foreground">
|
|
154
|
+
Installed ({extensions.length})
|
|
155
|
+
</h3>
|
|
156
|
+
{extensions.length === 0 ? (
|
|
157
|
+
<p className="text-[11px] text-muted-foreground py-4 text-center">
|
|
158
|
+
No extensions installed
|
|
159
|
+
</p>
|
|
160
|
+
) : (
|
|
161
|
+
<div className="space-y-1">
|
|
162
|
+
{extensions.map((ext) => (
|
|
163
|
+
<div
|
|
164
|
+
key={ext.id}
|
|
165
|
+
className="flex items-center gap-2 px-2.5 py-2 rounded-lg bg-muted/50 hover:bg-muted transition-colors"
|
|
166
|
+
>
|
|
167
|
+
<div className="size-8 rounded-md bg-background flex items-center justify-center shrink-0">
|
|
168
|
+
<Puzzle className="size-4 text-muted-foreground" />
|
|
169
|
+
</div>
|
|
170
|
+
<div className="flex-1 min-w-0">
|
|
171
|
+
<p className="text-xs font-medium truncate">{ext.displayName || ext.id}</p>
|
|
172
|
+
<p className="text-[11px] text-muted-foreground truncate">
|
|
173
|
+
{ext.id} v{ext.version}
|
|
174
|
+
{ext.activated && <span className="ml-1 text-green-500">active</span>}
|
|
175
|
+
</p>
|
|
176
|
+
</div>
|
|
177
|
+
<Button
|
|
178
|
+
variant="ghost"
|
|
179
|
+
size="icon"
|
|
180
|
+
className="size-7 shrink-0 cursor-pointer"
|
|
181
|
+
title={ext.enabled ? "Disable" : "Enable"}
|
|
182
|
+
disabled={togglingId === ext.id}
|
|
183
|
+
onClick={() => handleToggle(ext)}
|
|
184
|
+
>
|
|
185
|
+
{togglingId === ext.id ? (
|
|
186
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
187
|
+
) : ext.enabled ? (
|
|
188
|
+
<Power className="size-3.5 text-green-500" />
|
|
189
|
+
) : (
|
|
190
|
+
<PowerOff className="size-3.5 text-muted-foreground" />
|
|
191
|
+
)}
|
|
192
|
+
</Button>
|
|
193
|
+
<Button
|
|
194
|
+
variant="ghost"
|
|
195
|
+
size="icon"
|
|
196
|
+
className="size-7 shrink-0 cursor-pointer text-destructive hover:text-destructive"
|
|
197
|
+
title="Remove"
|
|
198
|
+
disabled={deletingId === ext.id}
|
|
199
|
+
onClick={() => handleRemove(ext.id)}
|
|
200
|
+
>
|
|
201
|
+
{deletingId === ext.id ? (
|
|
202
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
203
|
+
) : (
|
|
204
|
+
<Trash2 className="size-3.5" />
|
|
205
|
+
)}
|
|
206
|
+
</Button>
|
|
207
|
+
</div>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
</section>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback, useRef } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Moon, Sun, Monitor, Bell, BellOff, Check, ChevronRight, ArrowLeft,
|
|
4
|
-
Bot, BellRing, Keyboard, Globe, Plug,
|
|
4
|
+
Bot, BellRing, Keyboard, Globe, Plug, Puzzle,
|
|
5
5
|
} from "lucide-react";
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
7
|
import { Input } from "@/components/ui/input";
|
|
@@ -14,6 +14,8 @@ import { KeyboardShortcutsSection } from "./keyboard-shortcuts-section";
|
|
|
14
14
|
import { TelegramSettingsSection } from "./telegram-settings-section";
|
|
15
15
|
import { ProxySettingsSection } from "./proxy-settings-section";
|
|
16
16
|
import { McpSettingsSection } from "./mcp-settings-section";
|
|
17
|
+
import { ExtensionManagerSection } from "./extension-manager-section";
|
|
18
|
+
import { ChangePasswordSection } from "./change-password-section";
|
|
17
19
|
import { usePushNotification } from "@/hooks/use-push-notification";
|
|
18
20
|
|
|
19
21
|
const THEME_OPTIONS: { value: Theme; label: string; icon: React.ElementType }[] = [
|
|
@@ -26,7 +28,7 @@ const pushSupported = "PushManager" in window && "serviceWorker" in navigator;
|
|
|
26
28
|
const isIosNonPwa = /iPhone|iPad/.test(navigator.userAgent) &&
|
|
27
29
|
!window.matchMedia("(display-mode: standalone)").matches;
|
|
28
30
|
|
|
29
|
-
type SettingsCategory = "ai" | "notifications" | "proxy" | "shortcuts" | "mcp";
|
|
31
|
+
type SettingsCategory = "ai" | "notifications" | "proxy" | "shortcuts" | "mcp" | "extensions";
|
|
30
32
|
|
|
31
33
|
const CATEGORIES: { value: SettingsCategory; label: string; subtitle: string; icon: React.ElementType }[] = [
|
|
32
34
|
{ value: "ai", label: "AI Provider", subtitle: "Model, execution mode, limits", icon: Bot },
|
|
@@ -34,6 +36,7 @@ const CATEGORIES: { value: SettingsCategory; label: string; subtitle: string; ic
|
|
|
34
36
|
{ value: "proxy", label: "API Proxy", subtitle: "Expose accounts as Anthropic API", icon: Globe },
|
|
35
37
|
{ value: "shortcuts", label: "Keyboard Shortcuts", subtitle: "Customize key bindings", icon: Keyboard },
|
|
36
38
|
{ value: "mcp", label: "MCP Servers", subtitle: "Model Context Protocol tools", icon: Plug },
|
|
39
|
+
{ value: "extensions", label: "Extensions", subtitle: "Install and manage extensions", icon: Puzzle },
|
|
37
40
|
];
|
|
38
41
|
|
|
39
42
|
export function SettingsTab() {
|
|
@@ -87,6 +90,7 @@ export function SettingsTab() {
|
|
|
87
90
|
{activeCategory === "proxy" && <ProxySettingsSection />}
|
|
88
91
|
{activeCategory === "shortcuts" && <KeyboardShortcutsSection />}
|
|
89
92
|
{activeCategory === "mcp" && <McpSettingsSection />}
|
|
93
|
+
{activeCategory === "extensions" && <ExtensionManagerSection />}
|
|
90
94
|
</div>
|
|
91
95
|
</ScrollArea>
|
|
92
96
|
</div>
|
|
@@ -128,6 +132,9 @@ export function SettingsTab() {
|
|
|
128
132
|
</p>
|
|
129
133
|
</section>
|
|
130
134
|
|
|
135
|
+
{/* Security: Change Password */}
|
|
136
|
+
<ChangePasswordSection />
|
|
137
|
+
|
|
131
138
|
{/* Quick: Theme */}
|
|
132
139
|
<section className="space-y-2">
|
|
133
140
|
<h3 className="text-xs font-medium text-muted-foreground">Theme</h3>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { WifiOff, ServerOff, RefreshCw } from "lucide-react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useConnectionStore } from "@/stores/connection-store";
|
|
4
|
+
|
|
5
|
+
const CLOUD_URL = "https://ppm.hienle.tech";
|
|
6
|
+
|
|
7
|
+
function isTunnelDomain(): boolean {
|
|
8
|
+
return window.location.hostname.endsWith(".trycloudflare.com");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ConnectionLostOverlay() {
|
|
12
|
+
const showOverlay = useConnectionStore((s) => s.showOverlay);
|
|
13
|
+
const [retrying, setRetrying] = useState(false);
|
|
14
|
+
|
|
15
|
+
if (!showOverlay) return null;
|
|
16
|
+
|
|
17
|
+
const isTunnel = isTunnelDomain();
|
|
18
|
+
|
|
19
|
+
async function handleRetry() {
|
|
20
|
+
setRetrying(true);
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch("/api/health", { cache: "no-store" });
|
|
23
|
+
if (res.ok) {
|
|
24
|
+
useConnectionStore.getState().markUp();
|
|
25
|
+
if ("caches" in window) {
|
|
26
|
+
const keys = await caches.keys();
|
|
27
|
+
await Promise.all(keys.map((k) => caches.delete(k)));
|
|
28
|
+
}
|
|
29
|
+
window.location.reload();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// still down
|
|
34
|
+
}
|
|
35
|
+
setRetrying(false);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const Icon = isTunnel ? WifiOff : ServerOff;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="fixed inset-0 z-[200] bg-background/95 backdrop-blur-sm flex items-center justify-center p-4">
|
|
42
|
+
<div className="max-w-sm w-full text-center space-y-6">
|
|
43
|
+
<div className="flex justify-center">
|
|
44
|
+
<div className="rounded-full bg-destructive/10 p-4">
|
|
45
|
+
<Icon className="h-10 w-10 text-destructive" />
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div className="space-y-2">
|
|
50
|
+
<h2 className="text-xl font-semibold text-foreground">
|
|
51
|
+
{isTunnel ? "Connection Lost" : "Server Unreachable"}
|
|
52
|
+
</h2>
|
|
53
|
+
<p className="text-sm text-muted-foreground leading-relaxed">
|
|
54
|
+
{isTunnel
|
|
55
|
+
? "The tunnel appears to have closed. The server may have restarted with a new URL."
|
|
56
|
+
: "Cannot connect to the PPM server. It may have stopped or is restarting."}
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div className="flex flex-col gap-3">
|
|
61
|
+
{isTunnel && (
|
|
62
|
+
<a
|
|
63
|
+
href={CLOUD_URL}
|
|
64
|
+
target="_blank"
|
|
65
|
+
rel="noopener noreferrer"
|
|
66
|
+
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
|
|
67
|
+
>
|
|
68
|
+
Open PPM Cloud
|
|
69
|
+
</a>
|
|
70
|
+
)}
|
|
71
|
+
<button
|
|
72
|
+
onClick={handleRetry}
|
|
73
|
+
disabled={retrying}
|
|
74
|
+
className="inline-flex items-center justify-center gap-2 rounded-md border border-border px-4 py-2.5 text-sm font-medium text-foreground hover:bg-accent transition-colors disabled:opacity-50"
|
|
75
|
+
>
|
|
76
|
+
<RefreshCw className={`h-4 w-4 ${retrying ? "animate-spin" : ""}`} />
|
|
77
|
+
{retrying ? "Retrying…" : "Retry Connection"}
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{!isTunnel && (
|
|
82
|
+
<p className="text-xs text-muted-foreground">
|
|
83
|
+
If the server was stopped, run <code className="bg-muted px-1 py-0.5 rounded text-[11px]">ppm start</code> to restart it.
|
|
84
|
+
</p>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -22,6 +22,7 @@ interface UseChatReturn {
|
|
|
22
22
|
connectingElapsed: number;
|
|
23
23
|
pendingApproval: ApprovalRequest | null;
|
|
24
24
|
contextWindowPct: number | null;
|
|
25
|
+
compactStatus: "compacting" | null;
|
|
25
26
|
sessionTitle: string | null;
|
|
26
27
|
/** When CLI provider assigns a different session ID, this holds the new ID */
|
|
27
28
|
migratedSessionId: string | null;
|
|
@@ -51,6 +52,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
51
52
|
const [connectingElapsed, setConnectingElapsed] = useState(0);
|
|
52
53
|
const [pendingApproval, setPendingApproval] = useState<ApprovalRequest | null>(null);
|
|
53
54
|
const [contextWindowPct, setContextWindowPct] = useState<number | null>(null);
|
|
55
|
+
const [compactStatus, setCompactStatus] = useState<"compacting" | null>(null);
|
|
54
56
|
const [sessionTitle, setSessionTitle] = useState<string | null>(null);
|
|
55
57
|
const [isConnected, setIsConnected] = useState(false);
|
|
56
58
|
const [migratedSessionId, setMigratedSessionId] = useState<string | null>(null);
|
|
@@ -121,6 +123,17 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
121
123
|
break;
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
case "account_retry": {
|
|
127
|
+
// Update streaming account to the new one being tried
|
|
128
|
+
if (ev.accountId && ev.accountLabel) {
|
|
129
|
+
streamingAccountRef.current = { accountId: ev.accountId, accountLabel: ev.accountLabel };
|
|
130
|
+
}
|
|
131
|
+
// Surface retry as a system-level event in the stream
|
|
132
|
+
streamingEventsRef.current.push(ev as ChatEvent);
|
|
133
|
+
syncMessages();
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
124
137
|
case "text": {
|
|
125
138
|
const pid = ev.parentToolUseId as string | undefined;
|
|
126
139
|
if (pid && routeToParent(ev as ChatEvent, pid)) {
|
|
@@ -259,6 +272,19 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
259
272
|
return;
|
|
260
273
|
}
|
|
261
274
|
|
|
275
|
+
// Handle compact status events
|
|
276
|
+
if ((data as any).type === "compact_status") {
|
|
277
|
+
const status = (data as any).status;
|
|
278
|
+
if (status === "compacting") {
|
|
279
|
+
setCompactStatus("compacting");
|
|
280
|
+
} else if (status === "done") {
|
|
281
|
+
setCompactStatus(null);
|
|
282
|
+
// Refresh messages to show compacted history
|
|
283
|
+
refetchRef.current?.();
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
262
288
|
// Handle phase transitions from BE
|
|
263
289
|
if ((data as any).type === "phase_changed") {
|
|
264
290
|
const p = (data as any).phase as SessionPhase;
|
|
@@ -351,6 +377,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
351
377
|
setPhase("idle");
|
|
352
378
|
phaseRef.current = "idle";
|
|
353
379
|
setPendingApproval(null);
|
|
380
|
+
setCompactStatus(null);
|
|
354
381
|
streamingContentRef.current = "";
|
|
355
382
|
streamingEventsRef.current = [];
|
|
356
383
|
setIsConnected(false);
|
|
@@ -539,6 +566,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
539
566
|
connectingElapsed,
|
|
540
567
|
pendingApproval,
|
|
541
568
|
contextWindowPct,
|
|
569
|
+
compactStatus,
|
|
542
570
|
sessionTitle,
|
|
543
571
|
migratedSessionId,
|
|
544
572
|
sendMessage,
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { useEffect, useCallback, useRef } from "react";
|
|
2
|
+
import { WsClient } from "@/lib/ws-client";
|
|
3
|
+
import { useExtensionStore } from "@/stores/extension-store";
|
|
4
|
+
import { useTabStore } from "@/stores/tab-store";
|
|
5
|
+
import { getAuthToken } from "@/lib/api-client";
|
|
6
|
+
import type { ExtServerMsg, ExtClientMsg } from "../../types/extension-messages.ts";
|
|
7
|
+
import { toast } from "sonner";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hook that manages the WebSocket connection for extension UI bridge.
|
|
11
|
+
* Dispatches server messages into the extension Zustand store.
|
|
12
|
+
* Only connects when `enabled` is true (after auth).
|
|
13
|
+
*/
|
|
14
|
+
export function useExtensionWs(enabled = true) {
|
|
15
|
+
const clientRef = useRef<WsClient | null>(null);
|
|
16
|
+
|
|
17
|
+
const send = useCallback((msg: ExtClientMsg) => {
|
|
18
|
+
clientRef.current?.send(JSON.stringify(msg));
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!enabled) return;
|
|
23
|
+
|
|
24
|
+
// Pass auth token as query param for WS auth
|
|
25
|
+
const token = getAuthToken();
|
|
26
|
+
const wsUrl = token ? `/ws/extensions?token=${encodeURIComponent(token)}` : "/ws/extensions";
|
|
27
|
+
const client = new WsClient(wsUrl);
|
|
28
|
+
clientRef.current = client;
|
|
29
|
+
|
|
30
|
+
client.onMessage((event) => {
|
|
31
|
+
let msg: ExtServerMsg;
|
|
32
|
+
try {
|
|
33
|
+
msg = JSON.parse(event.data) as ExtServerMsg;
|
|
34
|
+
} catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const store = useExtensionStore.getState();
|
|
39
|
+
|
|
40
|
+
switch (msg.type) {
|
|
41
|
+
case "contributions:update":
|
|
42
|
+
store.setContributions(msg.contributions);
|
|
43
|
+
break;
|
|
44
|
+
|
|
45
|
+
case "statusbar:update":
|
|
46
|
+
store.addStatusBarItem(msg.item);
|
|
47
|
+
break;
|
|
48
|
+
|
|
49
|
+
case "statusbar:remove":
|
|
50
|
+
store.removeStatusBarItem(msg.itemId);
|
|
51
|
+
break;
|
|
52
|
+
|
|
53
|
+
case "tree:update":
|
|
54
|
+
if (msg.parentId) {
|
|
55
|
+
store.updateTreeChildren(msg.viewId, msg.parentId, msg.items);
|
|
56
|
+
} else {
|
|
57
|
+
store.updateTree(msg.viewId, msg.items);
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
|
|
61
|
+
case "tree:refresh":
|
|
62
|
+
store.removeTree(msg.viewId);
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case "notification": {
|
|
66
|
+
const toastFn = msg.level === "error" ? toast.error
|
|
67
|
+
: msg.level === "warn" ? toast.warning
|
|
68
|
+
: toast.info;
|
|
69
|
+
if (msg.actions && msg.actions.length > 0) {
|
|
70
|
+
const toastOpts: Record<string, unknown> = {
|
|
71
|
+
action: {
|
|
72
|
+
label: msg.actions[0],
|
|
73
|
+
onClick: () => send({ type: "notification:action", id: msg.id, action: msg.actions![0] ?? null }),
|
|
74
|
+
},
|
|
75
|
+
onDismiss: () => send({ type: "notification:action", id: msg.id, action: null }),
|
|
76
|
+
};
|
|
77
|
+
// Support a second action button via cancel
|
|
78
|
+
if (msg.actions.length > 1) {
|
|
79
|
+
toastOpts.cancel = {
|
|
80
|
+
label: msg.actions[1],
|
|
81
|
+
onClick: () => send({ type: "notification:action", id: msg.id, action: msg.actions![1] ?? null }),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
toastFn(msg.message, toastOpts);
|
|
85
|
+
} else {
|
|
86
|
+
toastFn(msg.message);
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case "quickpick:show":
|
|
92
|
+
store.showQuickPick(
|
|
93
|
+
msg.items,
|
|
94
|
+
msg.options,
|
|
95
|
+
).then((selected) => {
|
|
96
|
+
send({
|
|
97
|
+
type: "quickpick:resolve",
|
|
98
|
+
requestId: msg.requestId,
|
|
99
|
+
selected: selected ?? null,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
break;
|
|
103
|
+
|
|
104
|
+
case "inputbox:show":
|
|
105
|
+
store.showInputBox(msg.options).then((value) => {
|
|
106
|
+
send({
|
|
107
|
+
type: "inputbox:resolve",
|
|
108
|
+
requestId: msg.requestId,
|
|
109
|
+
value: value ?? null,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
break;
|
|
113
|
+
|
|
114
|
+
case "webview:create":
|
|
115
|
+
store.addWebviewPanel({
|
|
116
|
+
id: msg.panelId,
|
|
117
|
+
extensionId: msg.extensionId,
|
|
118
|
+
viewType: msg.viewType,
|
|
119
|
+
title: msg.title,
|
|
120
|
+
html: "",
|
|
121
|
+
});
|
|
122
|
+
// Open a tab to display the webview panel
|
|
123
|
+
useTabStore.getState().openTab({
|
|
124
|
+
type: "extension-webview",
|
|
125
|
+
title: msg.title,
|
|
126
|
+
projectId: null,
|
|
127
|
+
closable: true,
|
|
128
|
+
metadata: { panelId: msg.panelId, extensionId: msg.extensionId },
|
|
129
|
+
});
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
case "webview:html":
|
|
133
|
+
store.updateWebviewPanel(msg.panelId, { html: msg.html });
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
case "webview:dispose":
|
|
137
|
+
store.removeWebviewPanel(msg.panelId);
|
|
138
|
+
break;
|
|
139
|
+
|
|
140
|
+
case "webview:postMessage":
|
|
141
|
+
window.dispatchEvent(new CustomEvent("ext:webview:message", {
|
|
142
|
+
detail: { panelId: msg.panelId, message: msg.message },
|
|
143
|
+
}));
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Listen for iframe→server messages (dispatched by ExtensionWebview component)
|
|
149
|
+
const webviewSendHandler = (e: Event) => {
|
|
150
|
+
const { panelId, message } = (e as CustomEvent).detail;
|
|
151
|
+
client.send(JSON.stringify({ type: "webview:message", panelId, message }));
|
|
152
|
+
};
|
|
153
|
+
window.addEventListener("ext:webview:send", webviewSendHandler);
|
|
154
|
+
|
|
155
|
+
// Listen for tree:expand requests (dispatched by ExtensionTreeView component)
|
|
156
|
+
const treeExpandHandler = (e: Event) => {
|
|
157
|
+
const { viewId, itemId } = (e as CustomEvent).detail;
|
|
158
|
+
client.send(JSON.stringify({ type: "tree:expand", viewId, itemId }));
|
|
159
|
+
};
|
|
160
|
+
window.addEventListener("ext:tree:expand", treeExpandHandler);
|
|
161
|
+
|
|
162
|
+
// Listen for command:execute requests (dispatched by StatusBar / TreeView)
|
|
163
|
+
const commandHandler = (e: Event) => {
|
|
164
|
+
const { command, args } = (e as CustomEvent).detail;
|
|
165
|
+
client.send(JSON.stringify({ type: "command:execute", command, args }));
|
|
166
|
+
};
|
|
167
|
+
window.addEventListener("ext:command:execute", commandHandler);
|
|
168
|
+
|
|
169
|
+
client.connect();
|
|
170
|
+
|
|
171
|
+
return () => {
|
|
172
|
+
window.removeEventListener("ext:webview:send", webviewSendHandler);
|
|
173
|
+
window.removeEventListener("ext:tree:expand", treeExpandHandler);
|
|
174
|
+
window.removeEventListener("ext:command:execute", commandHandler);
|
|
175
|
+
client.disconnect();
|
|
176
|
+
clientRef.current = null;
|
|
177
|
+
};
|
|
178
|
+
}, [send, enabled]);
|
|
179
|
+
|
|
180
|
+
return { send };
|
|
181
|
+
}
|
|
@@ -21,6 +21,7 @@ export function useGlobalKeybindings() {
|
|
|
21
21
|
|
|
22
22
|
useEffect(() => {
|
|
23
23
|
let lastShiftUp = 0;
|
|
24
|
+
let shiftAlone = false; // true if Shift was pressed without any other key
|
|
24
25
|
const { matchesEvent } = useKeybindingsStore.getState();
|
|
25
26
|
|
|
26
27
|
let composing = false;
|
|
@@ -28,9 +29,24 @@ export function useGlobalKeybindings() {
|
|
|
28
29
|
function onCompositionEnd() { composing = false; }
|
|
29
30
|
|
|
30
31
|
function handler(e: KeyboardEvent) {
|
|
32
|
+
// Track whether Shift is pressed alone (not as a modifier for another key)
|
|
33
|
+
if (e.type === "keydown" && e.key === "Shift") {
|
|
34
|
+
shiftAlone = true;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Any non-Shift keydown while Shift is held means Shift is used as modifier
|
|
38
|
+
if (e.type === "keydown" && e.shiftKey) {
|
|
39
|
+
shiftAlone = false;
|
|
40
|
+
}
|
|
41
|
+
// Any non-Shift key resets the double-tap timer (user is typing, not double-tapping)
|
|
42
|
+
if (e.type === "keydown" && e.key !== "Shift") {
|
|
43
|
+
lastShiftUp = 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
// Double-Shift detection (on keyup to avoid repeats) — always active
|
|
32
|
-
//
|
|
33
|
-
|
|
47
|
+
// Only counts if Shift was pressed alone (not used as modifier e.g. Shift+T for uppercase)
|
|
48
|
+
// Also skip during IME composition (e.g. Vietnamese Telex) to prevent false triggers
|
|
49
|
+
if (e.type === "keyup" && e.key === "Shift" && shiftAlone && !e.ctrlKey && !e.metaKey && !e.altKey && !composing && !e.isComposing) {
|
|
34
50
|
const now = Date.now();
|
|
35
51
|
if (now - lastShiftUp < 400) {
|
|
36
52
|
lastShiftUp = 0;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
|
+
import { useConnectionStore } from "@/stores/connection-store";
|
|
2
3
|
|
|
3
4
|
const POLL_NORMAL_MS = 10_000; // 10s when server is up
|
|
4
5
|
const POLL_DOWN_MS = 2_000; // 2s when server is down (waiting for it to come back)
|
|
@@ -8,17 +9,21 @@ const POLL_DOWN_MS = 2_000; // 2s when server is down (waiting for it to com
|
|
|
8
9
|
* When the server goes down and comes back up (restart/stop+start),
|
|
9
10
|
* clears all browser/SW caches and reloads the page so the user
|
|
10
11
|
* always gets fresh assets.
|
|
12
|
+
*
|
|
13
|
+
* Also updates the connection store to drive the ConnectionLostOverlay.
|
|
11
14
|
*/
|
|
12
15
|
export function useServerReload() {
|
|
13
16
|
useEffect(() => {
|
|
14
17
|
let serverWasDown = false;
|
|
15
18
|
let timer: ReturnType<typeof setTimeout>;
|
|
19
|
+
const { markDown, markUp } = useConnectionStore.getState();
|
|
16
20
|
|
|
17
21
|
async function check() {
|
|
18
22
|
try {
|
|
19
23
|
const res = await fetch("/api/health", { cache: "no-store" });
|
|
20
24
|
if (res.ok && serverWasDown) {
|
|
21
25
|
// Server came back — clear caches then reload
|
|
26
|
+
markUp();
|
|
22
27
|
if ("caches" in window) {
|
|
23
28
|
const keys = await caches.keys();
|
|
24
29
|
await Promise.all(keys.map((k) => caches.delete(k)));
|
|
@@ -26,9 +31,13 @@ export function useServerReload() {
|
|
|
26
31
|
window.location.reload();
|
|
27
32
|
return;
|
|
28
33
|
}
|
|
34
|
+
if (res.ok) {
|
|
35
|
+
markUp();
|
|
36
|
+
}
|
|
29
37
|
serverWasDown = false;
|
|
30
38
|
} catch {
|
|
31
39
|
serverWasDown = true;
|
|
40
|
+
markDown();
|
|
32
41
|
}
|
|
33
42
|
timer = setTimeout(check, serverWasDown ? POLL_DOWN_MS : POLL_NORMAL_MS);
|
|
34
43
|
}
|