@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,194 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useMemo, useCallback } from "react";
|
|
2
|
+
import { Check, Search } from "lucide-react";
|
|
3
|
+
import { useExtensionStore, type QuickPickItemUI } from "@/stores/extension-store";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
/** Modal overlay for extension-driven QuickPick selection */
|
|
7
|
+
export function ExtensionQuickPick() {
|
|
8
|
+
const quickPick = useExtensionStore((s) => s.quickPick);
|
|
9
|
+
const resolveQuickPick = useExtensionStore((s) => s.resolveQuickPick);
|
|
10
|
+
|
|
11
|
+
if (!quickPick) return null;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<QuickPickModal
|
|
15
|
+
items={quickPick.items}
|
|
16
|
+
options={quickPick.options}
|
|
17
|
+
onSelect={(selected) => resolveQuickPick(selected)}
|
|
18
|
+
onCancel={() => resolveQuickPick(undefined)}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function QuickPickModal({
|
|
24
|
+
items,
|
|
25
|
+
options,
|
|
26
|
+
onSelect,
|
|
27
|
+
onCancel,
|
|
28
|
+
}: {
|
|
29
|
+
items: QuickPickItemUI[];
|
|
30
|
+
options: { placeholder?: string; canPickMany?: boolean };
|
|
31
|
+
onSelect: (selected: QuickPickItemUI[]) => void;
|
|
32
|
+
onCancel: () => void;
|
|
33
|
+
}) {
|
|
34
|
+
const [query, setQuery] = useState("");
|
|
35
|
+
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
36
|
+
const [picked, setPicked] = useState<Set<number>>(() => {
|
|
37
|
+
const initial = new Set<number>();
|
|
38
|
+
items.forEach((item, i) => { if (item.picked) initial.add(i); });
|
|
39
|
+
return initial;
|
|
40
|
+
});
|
|
41
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
42
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
43
|
+
const canPickMany = options.canPickMany ?? false;
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
requestAnimationFrame(() => inputRef.current?.focus());
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const filtered = useMemo(() => {
|
|
50
|
+
if (!query.trim()) return items.map((item, i) => ({ item, originalIdx: i }));
|
|
51
|
+
const q = query.toLowerCase();
|
|
52
|
+
return items
|
|
53
|
+
.map((item, i) => ({ item, originalIdx: i }))
|
|
54
|
+
.filter(({ item }) =>
|
|
55
|
+
item.label.toLowerCase().includes(q) ||
|
|
56
|
+
item.description?.toLowerCase().includes(q) ||
|
|
57
|
+
item.detail?.toLowerCase().includes(q),
|
|
58
|
+
);
|
|
59
|
+
}, [items, query]);
|
|
60
|
+
|
|
61
|
+
// Clamp selection when filter changes
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
setSelectedIdx((prev) => Math.min(prev, Math.max(filtered.length - 1, 0)));
|
|
64
|
+
}, [filtered.length]);
|
|
65
|
+
|
|
66
|
+
// Scroll selected into view
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const el = listRef.current?.children[selectedIdx] as HTMLElement | undefined;
|
|
69
|
+
el?.scrollIntoView({ block: "nearest" });
|
|
70
|
+
}, [selectedIdx]);
|
|
71
|
+
|
|
72
|
+
const confirmSelection = useCallback(() => {
|
|
73
|
+
if (canPickMany) {
|
|
74
|
+
const selected = items.filter((_, i) => picked.has(i));
|
|
75
|
+
onSelect(selected);
|
|
76
|
+
} else if (filtered[selectedIdx]) {
|
|
77
|
+
onSelect([filtered[selectedIdx].item]);
|
|
78
|
+
}
|
|
79
|
+
}, [canPickMany, picked, items, filtered, selectedIdx, onSelect]);
|
|
80
|
+
|
|
81
|
+
function handleKeyDown(e: React.KeyboardEvent) {
|
|
82
|
+
switch (e.key) {
|
|
83
|
+
case "ArrowDown":
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
if (filtered.length > 0) setSelectedIdx((i) => (i + 1) % filtered.length);
|
|
86
|
+
break;
|
|
87
|
+
case "ArrowUp":
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
if (filtered.length > 0) setSelectedIdx((i) => (i - 1 + filtered.length) % filtered.length);
|
|
90
|
+
break;
|
|
91
|
+
case " ":
|
|
92
|
+
if (canPickMany && filtered[selectedIdx]) {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
const origIdx = filtered[selectedIdx].originalIdx;
|
|
95
|
+
setPicked((prev) => {
|
|
96
|
+
const next = new Set(prev);
|
|
97
|
+
if (next.has(origIdx)) next.delete(origIdx); else next.add(origIdx);
|
|
98
|
+
return next;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
case "Enter":
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
confirmSelection();
|
|
105
|
+
break;
|
|
106
|
+
case "Escape":
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
onCancel();
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="fixed inset-0 z-50 flex items-end md:items-start justify-center md:pt-[20vh]" onClick={onCancel}>
|
|
115
|
+
<div className="fixed inset-0 bg-black/50" />
|
|
116
|
+
<div
|
|
117
|
+
className="relative z-10 w-full max-w-md rounded-t-xl md:rounded-xl border border-border bg-background shadow-2xl overflow-hidden max-h-[80vh] md:max-h-none"
|
|
118
|
+
onClick={(e) => e.stopPropagation()}
|
|
119
|
+
onKeyDown={handleKeyDown}
|
|
120
|
+
>
|
|
121
|
+
{/* Search input */}
|
|
122
|
+
<div className="flex items-center gap-2 border-b border-border px-3 py-2.5">
|
|
123
|
+
<Search className="size-4 text-text-subtle shrink-0" />
|
|
124
|
+
<input
|
|
125
|
+
ref={inputRef}
|
|
126
|
+
type="text"
|
|
127
|
+
value={query}
|
|
128
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
129
|
+
placeholder={options.placeholder ?? "Select an item..."}
|
|
130
|
+
className="flex-1 bg-transparent text-sm text-text-primary outline-none placeholder:text-text-subtle"
|
|
131
|
+
/>
|
|
132
|
+
<kbd className="hidden sm:inline-flex items-center rounded border border-border bg-surface px-1.5 py-0.5 text-[10px] text-text-subtle font-mono">
|
|
133
|
+
ESC
|
|
134
|
+
</kbd>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Results */}
|
|
138
|
+
<div ref={listRef} className="max-h-72 overflow-y-auto py-1">
|
|
139
|
+
{filtered.length === 0 ? (
|
|
140
|
+
<p className="px-3 py-4 text-sm text-text-subtle text-center">No matching items</p>
|
|
141
|
+
) : (
|
|
142
|
+
filtered.map(({ item, originalIdx }, i) => (
|
|
143
|
+
<button
|
|
144
|
+
key={originalIdx}
|
|
145
|
+
onClick={() => {
|
|
146
|
+
if (canPickMany) {
|
|
147
|
+
setPicked((prev) => {
|
|
148
|
+
const next = new Set(prev);
|
|
149
|
+
if (next.has(originalIdx)) next.delete(originalIdx); else next.add(originalIdx);
|
|
150
|
+
return next;
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
onSelect([item]);
|
|
154
|
+
}
|
|
155
|
+
}}
|
|
156
|
+
className={cn(
|
|
157
|
+
"flex items-center gap-3 w-full px-3 py-2 text-sm text-left transition-colors",
|
|
158
|
+
i === selectedIdx ? "bg-accent/15 text-text-primary" : "text-text-secondary hover:bg-surface-elevated",
|
|
159
|
+
)}
|
|
160
|
+
>
|
|
161
|
+
{canPickMany && (
|
|
162
|
+
<span className={cn(
|
|
163
|
+
"size-4 shrink-0 rounded border flex items-center justify-center",
|
|
164
|
+
picked.has(originalIdx) ? "bg-primary border-primary text-primary-foreground" : "border-border",
|
|
165
|
+
)}>
|
|
166
|
+
{picked.has(originalIdx) && <Check className="size-3" />}
|
|
167
|
+
</span>
|
|
168
|
+
)}
|
|
169
|
+
<div className="flex-1 min-w-0">
|
|
170
|
+
<div className="truncate">{item.label}</div>
|
|
171
|
+
{item.description && <span className="text-xs text-text-subtle ml-2">{item.description}</span>}
|
|
172
|
+
{item.detail && <div className="text-xs text-text-subtle truncate mt-0.5">{item.detail}</div>}
|
|
173
|
+
</div>
|
|
174
|
+
</button>
|
|
175
|
+
))
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Footer hint for multi-select */}
|
|
180
|
+
{canPickMany && (
|
|
181
|
+
<div className="flex items-center justify-between border-t border-border px-3 py-1.5">
|
|
182
|
+
<span className="text-[10px] text-text-subtle">{picked.size} selected</span>
|
|
183
|
+
<button
|
|
184
|
+
onClick={confirmSelection}
|
|
185
|
+
className="text-xs text-primary hover:text-primary/80 font-medium"
|
|
186
|
+
>
|
|
187
|
+
Confirm
|
|
188
|
+
</button>
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { useCallback, useState, useRef, useMemo } from "react";
|
|
2
|
+
import { ChevronRight, ChevronDown, RefreshCw, Pencil, Trash2, Plus, Search } from "lucide-react";
|
|
3
|
+
import { useExtensionStore, type TreeItemUI, type TreeItemAction } from "@/stores/extension-store";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
interface ExtensionTreeViewProps {
|
|
7
|
+
viewId: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Dispatch a tree:expand request to fetch children from the server */
|
|
12
|
+
function requestTreeExpand(viewId: string, itemId: string) {
|
|
13
|
+
window.dispatchEvent(new CustomEvent("ext:tree:expand", {
|
|
14
|
+
detail: { viewId, itemId },
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Dispatch a command:execute request via the WS bridge */
|
|
19
|
+
function executeCommand(command: string, args?: unknown[]) {
|
|
20
|
+
window.dispatchEvent(new CustomEvent("ext:command:execute", {
|
|
21
|
+
detail: { command, args },
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ACTION_ICONS: Record<string, React.ElementType> = {
|
|
26
|
+
refresh: RefreshCw,
|
|
27
|
+
edit: Pencil,
|
|
28
|
+
trash: Trash2,
|
|
29
|
+
plus: Plus,
|
|
30
|
+
search: Search,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Generic TreeView renderer for extension-contributed tree data */
|
|
34
|
+
export function ExtensionTreeView({ viewId, className }: ExtensionTreeViewProps) {
|
|
35
|
+
const items = useExtensionStore((s) => s.treeViews[viewId]) ?? [];
|
|
36
|
+
const contributions = useExtensionStore((s) => s.contributions);
|
|
37
|
+
|
|
38
|
+
// Find view name & header actions from contributions
|
|
39
|
+
const viewMeta = useMemo(() => {
|
|
40
|
+
if (!contributions) return { name: viewId, headerActions: [] };
|
|
41
|
+
// Find view name
|
|
42
|
+
let name = viewId;
|
|
43
|
+
const views = contributions.views;
|
|
44
|
+
if (views) {
|
|
45
|
+
for (const group of Object.values(views)) {
|
|
46
|
+
const found = group.find((v) => v.id === viewId);
|
|
47
|
+
if (found) { name = found.name; break; }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Find header actions from menus.view/title
|
|
51
|
+
const headerActions: { command: string; title: string; icon?: string }[] = [];
|
|
52
|
+
const viewTitleMenus = contributions.menus?.["view/title"];
|
|
53
|
+
if (viewTitleMenus) {
|
|
54
|
+
for (const menu of viewTitleMenus) {
|
|
55
|
+
// Check "when" clause: view == viewId
|
|
56
|
+
if (menu.when) {
|
|
57
|
+
const match = menu.when.match(/view\s*==\s*(\S+)/);
|
|
58
|
+
if (match && match[1] !== viewId) continue;
|
|
59
|
+
}
|
|
60
|
+
// Find command title
|
|
61
|
+
const cmd = contributions.commands?.find((c) => c.command === menu.command);
|
|
62
|
+
if (cmd) headerActions.push({ command: cmd.command, title: cmd.title, icon: cmd.icon });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { name, headerActions };
|
|
66
|
+
}, [contributions, viewId]);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className={cn("flex flex-col h-full", className)}>
|
|
70
|
+
{/* Header — matches built-in DatabaseSidebar header */}
|
|
71
|
+
<div className="flex items-center justify-between px-3 py-2 border-b border-border shrink-0">
|
|
72
|
+
<span className="text-[10px] font-semibold text-text-subtle uppercase tracking-wider">
|
|
73
|
+
{viewMeta.name}
|
|
74
|
+
</span>
|
|
75
|
+
<div className="flex items-center gap-0.5">
|
|
76
|
+
{viewMeta.headerActions.map((action) => {
|
|
77
|
+
const iconName = action.icon ?? inferIcon(action.command);
|
|
78
|
+
const Icon = ACTION_ICONS[iconName] ?? RefreshCw;
|
|
79
|
+
return (
|
|
80
|
+
<button
|
|
81
|
+
key={action.command}
|
|
82
|
+
onClick={() => executeCommand(action.command)}
|
|
83
|
+
className="flex items-center justify-center size-5 rounded hover:bg-surface-elevated transition-colors text-text-subtle hover:text-foreground"
|
|
84
|
+
title={action.title}
|
|
85
|
+
>
|
|
86
|
+
<Icon className="size-3.5" />
|
|
87
|
+
</button>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Tree content */}
|
|
94
|
+
<div className="flex-1 overflow-y-auto min-h-0 py-1" role="tree" aria-label={viewId}>
|
|
95
|
+
{items.length === 0 ? (
|
|
96
|
+
<p className="px-4 py-6 text-xs text-text-subtle text-center">No items</p>
|
|
97
|
+
) : (
|
|
98
|
+
items.map((item) => (
|
|
99
|
+
<TreeNode key={item.id} item={item} depth={0} viewId={viewId} />
|
|
100
|
+
))
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Infer icon name from command ID */
|
|
108
|
+
function inferIcon(command: string): string {
|
|
109
|
+
if (command.includes("refresh")) return "refresh";
|
|
110
|
+
if (command.includes("add") || command.includes("create") || command.includes("new")) return "plus";
|
|
111
|
+
if (command.includes("delete") || command.includes("remove")) return "trash";
|
|
112
|
+
if (command.includes("edit") || command.includes("update")) return "edit";
|
|
113
|
+
if (command.includes("search") || command.includes("find")) return "search";
|
|
114
|
+
return "refresh";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function TreeNode({ item, depth, viewId }: { item: TreeItemUI; depth: number; viewId: string }) {
|
|
118
|
+
const [expanded, setExpanded] = useState(false);
|
|
119
|
+
const hasChildren = item.collapsibleState !== "none";
|
|
120
|
+
const childrenLoaded = useRef(false);
|
|
121
|
+
|
|
122
|
+
// When children arrive from server, auto-expand once
|
|
123
|
+
if (item.children && item.children.length > 0 && !childrenLoaded.current) {
|
|
124
|
+
childrenLoaded.current = true;
|
|
125
|
+
if (!expanded) setExpanded(true);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const handleToggle = useCallback(() => {
|
|
129
|
+
if (!hasChildren) return;
|
|
130
|
+
const willExpand = !expanded;
|
|
131
|
+
setExpanded(willExpand);
|
|
132
|
+
if (willExpand && (!item.children || item.children.length === 0)) {
|
|
133
|
+
requestTreeExpand(viewId, item.id);
|
|
134
|
+
}
|
|
135
|
+
}, [hasChildren, expanded, item.id, item.children, viewId]);
|
|
136
|
+
|
|
137
|
+
const handleClick = useCallback(() => {
|
|
138
|
+
handleToggle();
|
|
139
|
+
if (item.command) {
|
|
140
|
+
executeCommand(item.command, item.commandArgs);
|
|
141
|
+
}
|
|
142
|
+
}, [handleToggle, item.command, item.commandArgs]);
|
|
143
|
+
|
|
144
|
+
const paddingLeft = 8 + depth * 16;
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div role="treeitem" aria-expanded={hasChildren ? expanded : undefined}>
|
|
148
|
+
<div
|
|
149
|
+
className={cn("group/node flex items-center gap-1 py-1 hover:bg-surface-elevated transition-colors")}
|
|
150
|
+
style={{ paddingLeft, paddingRight: 8 }}
|
|
151
|
+
>
|
|
152
|
+
{/* Expand chevron */}
|
|
153
|
+
<button
|
|
154
|
+
onClick={handleToggle}
|
|
155
|
+
className="shrink-0 text-text-subtle hover:text-foreground transition-colors"
|
|
156
|
+
>
|
|
157
|
+
{hasChildren ? (
|
|
158
|
+
expanded ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />
|
|
159
|
+
) : (
|
|
160
|
+
<span className="size-3" />
|
|
161
|
+
)}
|
|
162
|
+
</button>
|
|
163
|
+
|
|
164
|
+
{/* Color dot */}
|
|
165
|
+
{item.color && (
|
|
166
|
+
<span
|
|
167
|
+
className="shrink-0 size-2 rounded-full border border-border"
|
|
168
|
+
style={{ backgroundColor: item.color }}
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
{/* Label — click to toggle or execute command */}
|
|
173
|
+
<button
|
|
174
|
+
className="flex-1 text-left text-xs truncate hover:text-primary transition-colors"
|
|
175
|
+
onClick={handleClick}
|
|
176
|
+
>
|
|
177
|
+
{item.label}
|
|
178
|
+
</button>
|
|
179
|
+
|
|
180
|
+
{/* Description (column type info) */}
|
|
181
|
+
{item.description && (
|
|
182
|
+
<span className="shrink-0 ml-1 text-text-subtle text-[10px]">{item.description}</span>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
{/* Badge (PG/DB) */}
|
|
186
|
+
{item.badge && (
|
|
187
|
+
<span className="shrink-0 text-[9px] text-text-subtle uppercase px-1 rounded bg-surface-elevated">
|
|
188
|
+
{item.badge}
|
|
189
|
+
</span>
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
{/* Action buttons (visible on hover) */}
|
|
193
|
+
{item.actions && item.actions.length > 0 && (
|
|
194
|
+
<div className="hidden group-hover/node:flex items-center gap-0.5 shrink-0">
|
|
195
|
+
{item.actions.map((action) => (
|
|
196
|
+
<ActionButton key={action.command} action={action} />
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{/* Children */}
|
|
203
|
+
{hasChildren && expanded && item.children && (
|
|
204
|
+
<div role="group">
|
|
205
|
+
{item.children.map((child) => (
|
|
206
|
+
<TreeNode key={child.id} item={child} depth={depth + 1} viewId={viewId} />
|
|
207
|
+
))}
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function ActionButton({ action }: { action: TreeItemAction }) {
|
|
215
|
+
const [spinning, setSpinning] = useState(false);
|
|
216
|
+
const Icon = ACTION_ICONS[action.icon] ?? RefreshCw;
|
|
217
|
+
const isTrash = action.icon === "trash";
|
|
218
|
+
|
|
219
|
+
const handleClick = useCallback((e: React.MouseEvent) => {
|
|
220
|
+
e.stopPropagation();
|
|
221
|
+
if (action.icon === "refresh") {
|
|
222
|
+
setSpinning(true);
|
|
223
|
+
setTimeout(() => setSpinning(false), 1000);
|
|
224
|
+
}
|
|
225
|
+
executeCommand(action.command, action.commandArgs);
|
|
226
|
+
}, [action]);
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<button
|
|
230
|
+
onClick={handleClick}
|
|
231
|
+
className={cn(
|
|
232
|
+
"p-0.5 text-text-subtle transition-colors",
|
|
233
|
+
isTrash ? "hover:text-red-500" : "hover:text-foreground",
|
|
234
|
+
)}
|
|
235
|
+
title={action.tooltip}
|
|
236
|
+
>
|
|
237
|
+
<Icon className={cn("size-3", spinning && "animate-spin")} />
|
|
238
|
+
</button>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useRef, useEffect, useCallback } from "react";
|
|
2
|
+
import { useExtensionStore } from "@/stores/extension-store";
|
|
3
|
+
|
|
4
|
+
/** Inject acquireVsCodeApi() shim so extension webviews can postMessage to parent */
|
|
5
|
+
const VSCODE_API_SHIM = `<script>
|
|
6
|
+
function acquireVsCodeApi(){return{postMessage:function(m){window.parent.postMessage(m,"*")},getState:function(){try{return JSON.parse(sessionStorage.getItem("vscode-state")||"null")}catch{return null}},setState:function(s){sessionStorage.setItem("vscode-state",JSON.stringify(s));return s}}}
|
|
7
|
+
</script>`;
|
|
8
|
+
|
|
9
|
+
function injectVscodeApiShim(html: string): string {
|
|
10
|
+
if (!html) return html;
|
|
11
|
+
// Insert shim right after <head> tag (or at start if no <head>)
|
|
12
|
+
const headIdx = html.indexOf("<head>");
|
|
13
|
+
if (headIdx !== -1) {
|
|
14
|
+
return html.slice(0, headIdx + 6) + VSCODE_API_SHIM + html.slice(headIdx + 6);
|
|
15
|
+
}
|
|
16
|
+
return VSCODE_API_SHIM + html;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ExtensionWebviewProps {
|
|
20
|
+
metadata?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* iframe-based webview container for extension-contributed webview panels.
|
|
25
|
+
* Renders as a tab component in the editor panel system.
|
|
26
|
+
*/
|
|
27
|
+
export function ExtensionWebview({ metadata }: ExtensionWebviewProps) {
|
|
28
|
+
const panelId = metadata?.panelId as string | undefined;
|
|
29
|
+
const panel = useExtensionStore((s) => panelId ? s.webviewPanels[panelId] : undefined);
|
|
30
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
31
|
+
|
|
32
|
+
// Inject acquireVsCodeApi shim + write HTML into iframe via srcdoc
|
|
33
|
+
const rawHtml = panel?.html ?? "";
|
|
34
|
+
const html = injectVscodeApiShim(rawHtml);
|
|
35
|
+
|
|
36
|
+
// Listen for postMessage from iframe → forward to extension via WS bridge
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const handler = (event: MessageEvent) => {
|
|
39
|
+
if (iframeRef.current && event.source === iframeRef.current.contentWindow) {
|
|
40
|
+
// Forward to server via custom event → picked up by useExtensionWs
|
|
41
|
+
window.dispatchEvent(new CustomEvent("ext:webview:send", {
|
|
42
|
+
detail: { panelId, message: event.data },
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
window.addEventListener("message", handler);
|
|
47
|
+
return () => window.removeEventListener("message", handler);
|
|
48
|
+
}, [panelId]);
|
|
49
|
+
|
|
50
|
+
// Listen for server→webview messages (dispatched by useExtensionWs)
|
|
51
|
+
// targetOrigin "*" is safe here because sandbox omits allow-same-origin,
|
|
52
|
+
// so iframe origin is opaque "null". MUST restrict if allow-same-origin is ever added.
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const handler = (e: Event) => {
|
|
55
|
+
const { panelId: targetId, message } = (e as CustomEvent).detail;
|
|
56
|
+
if (targetId === panelId) {
|
|
57
|
+
iframeRef.current?.contentWindow?.postMessage(message, "*");
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
window.addEventListener("ext:webview:message", handler);
|
|
61
|
+
return () => window.removeEventListener("ext:webview:message", handler);
|
|
62
|
+
}, [panelId]);
|
|
63
|
+
|
|
64
|
+
if (!panel) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex items-center justify-center h-full text-sm text-text-subtle">
|
|
67
|
+
Webview panel not found
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="h-full w-full relative">
|
|
74
|
+
<iframe
|
|
75
|
+
ref={iframeRef}
|
|
76
|
+
srcDoc={html}
|
|
77
|
+
sandbox="allow-scripts"
|
|
78
|
+
className="w-full h-full border-0 bg-white dark:bg-zinc-900"
|
|
79
|
+
title={panel.title}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -12,12 +12,14 @@ import {
|
|
|
12
12
|
Loader2,
|
|
13
13
|
Globe,
|
|
14
14
|
Mic,
|
|
15
|
+
Puzzle,
|
|
15
16
|
} from "lucide-react";
|
|
16
17
|
import { useTabStore, type TabType } from "@/stores/tab-store";
|
|
17
18
|
import { useProjectStore } from "@/stores/project-store";
|
|
18
19
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
19
20
|
import { useKeybindingsStore } from "@/stores/keybindings-store";
|
|
20
21
|
import { useFileStore, type FileNode } from "@/stores/file-store";
|
|
22
|
+
import { useExtensionStore } from "@/stores/extension-store";
|
|
21
23
|
import { api } from "@/lib/api-client";
|
|
22
24
|
import { basename } from "@/lib/utils";
|
|
23
25
|
|
|
@@ -103,6 +105,7 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
|
|
|
103
105
|
const sidebarCollapsed = useSettingsStore((s) => s.sidebarCollapsed);
|
|
104
106
|
const toggleSidebar = useSettingsStore((s) => s.toggleSidebar);
|
|
105
107
|
const getBinding = useKeybindingsStore((s) => s.getBinding);
|
|
108
|
+
const extContributions = useExtensionStore((s) => s.contributions);
|
|
106
109
|
|
|
107
110
|
// Fetch filesystem files when path query changes directory
|
|
108
111
|
const fetchFsFiles = useCallback(async (dir: string) => {
|
|
@@ -154,7 +157,7 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
|
|
|
154
157
|
onClose();
|
|
155
158
|
};
|
|
156
159
|
|
|
157
|
-
|
|
160
|
+
const builtIn: CommandItem[] = [
|
|
158
161
|
{ id: "chat", label: "New AI Chat", icon: MessageSquare, action: openNewTab("chat", "AI Chat"), keywords: "ai assistant claude", group: "action", shortcut: formatShortcut(getBinding("open-chat")) },
|
|
159
162
|
{ id: "terminal", label: "New Terminal", icon: Terminal, action: openNewTab("terminal", "Terminal"), keywords: "bash shell console", group: "action", shortcut: formatShortcut(getBinding("open-terminal")) },
|
|
160
163
|
{ id: "git-graph", label: "Git Graph", icon: GitBranch, action: openNewTab("git-graph", "Git Graph"), keywords: "branch history log", group: "action", shortcut: formatShortcut(getBinding("open-git-graph")) },
|
|
@@ -174,7 +177,24 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
|
|
|
174
177
|
shortcut: formatShortcut(getBinding("open-settings")),
|
|
175
178
|
},
|
|
176
179
|
];
|
|
177
|
-
|
|
180
|
+
|
|
181
|
+
// Append extension-contributed commands
|
|
182
|
+
const extCmds: CommandItem[] = (extContributions?.commands ?? []).map((cmd) => ({
|
|
183
|
+
id: `ext:${cmd.command}`,
|
|
184
|
+
label: cmd.title,
|
|
185
|
+
hint: cmd.category,
|
|
186
|
+
icon: Puzzle,
|
|
187
|
+
group: "action" as const,
|
|
188
|
+
keywords: `extension ${cmd.command} ${cmd.category ?? ""}`,
|
|
189
|
+
action: () => {
|
|
190
|
+
// Phase 4: execute via WS bridge
|
|
191
|
+
console.log("[CmdPalette] ext command:", cmd.command);
|
|
192
|
+
onClose();
|
|
193
|
+
},
|
|
194
|
+
}));
|
|
195
|
+
|
|
196
|
+
return [...builtIn, ...extCmds];
|
|
197
|
+
}, [activeProject, openTab, onClose, setSidebarActiveTab, sidebarCollapsed, toggleSidebar, getBinding, extContributions]);
|
|
178
198
|
|
|
179
199
|
// File commands — derived from file store tree (project files)
|
|
180
200
|
const fileCommands = useMemo<CommandItem[]>(() => {
|