@hienlh/ppm 0.9.85 → 0.9.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/web/assets/{_basePickBy-D-bUmjma.js → _basePickBy-Bj0dI1ei.js} +1 -1
- package/dist/web/assets/{_baseUniq-BnXXIfRB.js → _baseUniq-CyzdZeQH.js} +1 -1
- package/dist/web/assets/ai-settings-section-Bo9lCaTd.js +1 -0
- package/dist/web/assets/{api-settings-Qi2xRiHa.js → api-settings-CUxg9RE5.js} +1 -1
- package/dist/web/assets/{arc-DB9vXGzd.js → arc-CxgHJ7Z4.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DDFO_NKq.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-BBV25747.js → architectureDiagram-2XIMDMQ5-D16OotsC.js} +1 -1
- package/dist/web/assets/arrow-up-I9-21gkR.js +1 -0
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-BOTnY2Lq.js → blockDiagram-WCTKOSBZ-Ct57Wtfk.js} +1 -1
- package/dist/web/assets/{c4Diagram-IC4MRINW-D7QAUdHD.js → c4Diagram-IC4MRINW-BIymcNsg.js} +1 -1
- package/dist/web/assets/channel-wumTB1if.js +1 -0
- package/dist/web/assets/chat-tab-BEEd-Km4.js +10 -0
- package/dist/web/assets/chevron-right-DY_wImxB.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-BnOVw77D.js → chunk-4BX2VUAB-CENmY7Kw.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-BftA8DxR.js → chunk-55IACEB6-DhZGI1l3.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-B0vnP8v3.js → chunk-7E7YKBS2-DZcnC7Ow.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-Czlaj26D.js → chunk-7R4GIKGN-y8bfHEy-.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-DpEbDtMo.js → chunk-C72U2L5F-BHPkfQj2.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-BWXe6lkx.js → chunk-EGIJ26TM-nant2LXl.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-DspqhPfk.js → chunk-FMBD7UC4-Bog4cpN-.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-D6HHRbYk.js → chunk-GEFDOKGD-86LFbsAC.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-Re-5eSlQ.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-C45i5G_3.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-BC8wnMwf.js → chunk-JSJVCQXG-23eG9mgt.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-D3VDtyvX.js → chunk-KX2RTZJC-CHj8TnTB.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-Z-NBw_HS.js → chunk-KYZI473N-gqRLpJ4w.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL--RGkEh__.js → chunk-L3YUKLVL-DnSMmNFC.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-2B76t_Kx.js → chunk-MX3YWQON-B6g1ZH9X.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-BekY3tEi.js → chunk-NQ4KR5QH-DX32345Y.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-2CJLfx_1.js → chunk-O4XLMI2P-Vp_V4P-b.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-sug_L09P.js → chunk-OZEHJAEY-lKq2SWjA.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-_fwPRLQy.js → chunk-PQ6SQG4A-Bik13fTV.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-BUaTFJVQ.js → chunk-PU5JKC2W-DD95Rx35.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-N3VXx1VH.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-C37xW0vj.js → chunk-R5LLSJPH-dRhXRnrb.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-CCkt_MT6.js → chunk-WL4C6EOR-B1iIvLOG.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-Dz2LBq7Y.js → chunk-XIRO2GV7-DZBoNl1_.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-DenTbBuj.js → chunk-XPW4576I-CgLyyW03.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-Dbp1nUSQ.js → chunk-XZSTWKYB-DjV8xl5A.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-3OTKowjE.js → chunk-YBOYWFTD-D_ILLe6_.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-mr-Cb1me.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-BKe8_uda.js +1 -0
- package/dist/web/assets/clone--z5KLAuR.js +1 -0
- package/dist/web/assets/code-editor-Ij4p30cr.js +8 -0
- package/dist/web/assets/columns-2-IeETSfON.js +1 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-MbmGZnt0.js → cose-bilkent-S5V4N54A-BGNPFv3x.js} +1 -1
- package/dist/web/assets/{csv-preview-uZ_7b8I7.js → csv-preview-CwQnOa3E.js} +2 -2
- package/dist/web/assets/{dagre-CPhI6v-K.js → dagre-CkhlMHnx.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-CmSE-oNj.js → dagre-KLK3FWXG-Cnp996VG.js} +1 -1
- package/dist/web/assets/database-CgTomMxt.js +1 -0
- package/dist/web/assets/{database-viewer-5xljX0JI.js → database-viewer-C1UHSgft.js} +2 -2
- package/dist/web/assets/{diagram-E7M64L7V-B5XG3ZT7.js → diagram-E7M64L7V-BZF0tSOr.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-BsP248aX.js → diagram-IFDJBPK2-nUcO8sN8.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-Cna3408N.js → diagram-P4PSJMXO-CW0eCkwC.js} +1 -1
- package/dist/web/assets/diff-viewer-CVx5naBA.js +4 -0
- package/dist/web/assets/dist-CM0oD8tQ.js +1 -0
- package/dist/web/assets/{erDiagram-INFDFZHY-B7SgktiR.js → erDiagram-INFDFZHY-DSkriYZ9.js} +1 -1
- package/dist/web/assets/extension-webview-CHVVpV34.js +3 -0
- package/dist/web/assets/{flowDiagram-PKNHOUZH-FOYZZ1OB.js → flowDiagram-PKNHOUZH-CFYAfZBx.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-CnHVYh9v.js → ganttDiagram-A5KZAMGK-KSn4XAU4.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-OkvBPi6H.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-0G9XxZay.js → gitGraphDiagram-K3NZZRJ6-BMgjjVys.js} +1 -1
- package/dist/web/assets/{graphlib-CNiBwlg_.js → graphlib-BWe1iK_s.js} +1 -1
- package/dist/web/assets/index-OqgGFmh8.js +26 -0
- package/dist/web/assets/index-vA7juDri.css +2 -0
- package/dist/web/assets/info-3K5VOQVL-BDU2_bYD.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-Diq4Cyc3.js +2 -0
- package/dist/web/assets/input-BHj0veau.js +45 -0
- package/dist/web/assets/{isEmpty-CcCb5n2-.js → isEmpty-BfLnxq-B.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-D4QCzh5J.js → ishikawaDiagram-PHBUUO56-CiVEvp8o.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-CnHYNfKW.js → journeyDiagram-4ABVD52K-CG_v5Aho.js} +1 -1
- package/dist/web/assets/jsx-runtime-BRW_vwa9.js +1 -0
- package/dist/web/assets/{kanban-definition-K7BYSVSG-Bh_g3EVu.js → kanban-definition-K7BYSVSG-miB0-_Zq.js} +1 -1
- package/dist/web/assets/keybindings-store-BQxgPV5o.js +1 -0
- package/dist/web/assets/{line-6d3eBADm.js → line-CSuSrJ9J.js} +1 -1
- package/dist/web/assets/{linear-cA_2lQy7.js → linear-DFN_MPsw.js} +1 -1
- package/dist/web/assets/{markdown-renderer-CZ07F7T6.js → markdown-renderer-CRy8xw2B.js} +6 -6
- package/dist/web/assets/{mermaid-parser.core-C3kd7JXM.js → mermaid-parser.core-CFdP1Z5_.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-CYiUwhr_.js → mindmap-definition-YRQLILUH-pYPWwASE.js} +1 -1
- package/dist/web/assets/{ordinal-XHK5vIzZ.js → ordinal-DpFn432U.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-BwpIpYB3.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-BPgAfmes.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-D0S7jeZA.js → pieDiagram-SKSYHLDU-Dovdlvhu.js} +1 -1
- package/dist/web/assets/plus-DQGIb4mQ.js +1 -0
- package/dist/web/assets/port-forwarding-tab-Biua8ov5.js +1 -0
- package/dist/web/assets/{postgres-viewer-RldlAO_m.js → postgres-viewer-BcVjCAl4.js} +3 -3
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-0hNP63hW.js → quadrantDiagram-337W2JSQ-TXe6cU_F.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-TqxBkWx-.js +1 -0
- package/dist/web/assets/refresh-cw-Clk8fdUD.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-BVnmqFbL.js → requirementDiagram-Z7DCOOCP-CuiiuGS9.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-DVkYdCJb.js → sankeyDiagram-WA2Y5GQK-BbRmhv0t.js} +1 -1
- package/dist/web/assets/scroll-area-BpXCNme3.js +1 -0
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-B80s7sOg.js → sequenceDiagram-2WXFIKYE-B2D8IQDb.js} +1 -1
- package/dist/web/assets/settings-tab-C9X-N8hE.js +1 -0
- package/dist/web/assets/{sql-query-editor-CjZ7Z6XL.js → sql-query-editor-BFvRvJn0.js} +1 -1
- package/dist/web/assets/sqlite-viewer-CPfvwFl4.js +1 -0
- package/dist/web/assets/square-vBdqj0bF.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-BPLXgXRR.js → stateDiagram-RAJIS63D-ylr4HxPu.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-D6zvxf3M.js +1 -0
- package/dist/web/assets/table-Bi27fEaN.js +1 -0
- package/dist/web/assets/{terminal-tab-DjzD8GLn.js → terminal-tab-mWwk_weB.js} +2 -2
- package/dist/web/assets/text-wrap-D_OmSzhp.js +1 -0
- package/dist/web/assets/{timeline-definition-YZTLITO2-fa_51u1X.js → timeline-definition-YZTLITO2-pMv1grvM.js} +1 -1
- package/dist/web/assets/trash-2-CNuB-htI.js +1 -0
- package/dist/web/assets/treemap-KZPCXAKY-Kck06FKU.js +1 -0
- package/dist/web/assets/{use-monaco-theme-D9XFxQuU.js → use-monaco-theme-CPaeSMAA.js} +1 -1
- package/dist/web/assets/{vennDiagram-LZ73GAT5-kX4jJn6W.js → vennDiagram-LZ73GAT5-C-rkIUbo.js} +1 -1
- package/dist/web/assets/x-Dw3TjeY_.js +1 -0
- package/dist/web/assets/{xychartDiagram-JWTSCODW-Bzm5lZBs.js → xychartDiagram-JWTSCODW-CtpjAakO.js} +1 -1
- package/dist/web/index.html +18 -22
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +134 -11
- package/docs/extension-development-guide.md +98 -1
- package/docs/journals/260414-1400-ext-git-graph-port-complete.md +147 -0
- package/docs/journals/260414-1452-git-graph-faithful-port.md +144 -0
- package/docs/journals/260414-1810-git-graph-ui-improvements-complete.md +261 -0
- package/docs/journals/260414-2001-bundled-extensions.md +219 -0
- package/docs/project-changelog.md +63 -22
- package/docs/project-roadmap.md +1 -0
- package/docs/system-architecture.md +33 -5
- package/package.json +1 -1
- package/packages/ext-git-graph/package.json +30 -0
- package/packages/ext-git-graph/src/extension-integration.test.ts +230 -0
- package/packages/ext-git-graph/src/extension-parsers.test.ts +193 -0
- package/packages/ext-git-graph/src/extension.ts +800 -0
- package/packages/ext-git-graph/src/git-log-parser.test.ts +271 -0
- package/packages/ext-git-graph/src/git-log-parser.ts +38 -0
- package/packages/ext-git-graph/src/types.ts +181 -0
- package/packages/ext-git-graph/src/webview-html.test.ts +142 -0
- package/packages/ext-git-graph/src/webview-html.ts +2199 -0
- package/packages/vscode-compat/src/index.ts +4 -0
- package/packages/vscode-compat/src/process.ts +25 -0
- package/packages/vscode-compat/src/window.ts +10 -0
- package/src/cli/commands/ext-cmd.ts +3 -1
- package/src/server/ws/extensions.ts +6 -2
- package/src/services/contribution-registry.ts +14 -1
- package/src/services/extension-host-worker.ts +7 -3
- package/src/services/extension-manifest.ts +18 -1
- package/src/services/extension-rpc-handlers.ts +68 -2
- package/src/services/extension.service.ts +46 -6
- package/src/types/extension-messages.ts +2 -0
- package/src/types/extension.ts +8 -0
- package/src/web/components/editor/code-editor.tsx +16 -4
- package/src/web/components/extensions/extension-webview.tsx +111 -12
- package/src/web/components/layout/command-palette.tsx +41 -17
- package/src/web/components/layout/editor-panel.tsx +15 -4
- package/src/web/components/layout/mobile-nav.tsx +5 -5
- package/src/web/components/layout/tab-bar.tsx +2 -3
- package/src/web/components/layout/tab-content.tsx +12 -5
- package/src/web/components/settings/keyboard-shortcuts-section.tsx +46 -1
- package/src/web/hooks/use-extension-ws.ts +22 -4
- package/src/web/hooks/use-global-keybindings.ts +24 -2
- package/src/web/hooks/use-url-sync.ts +8 -3
- package/src/web/stores/keybindings-store.ts +2 -3
- package/src/web/stores/panel-store.ts +2 -2
- package/src/web/stores/panel-utils.ts +4 -2
- package/src/web/stores/tab-store.ts +1 -1
- package/dist/web/assets/ai-settings-section-D6d-RmR6.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-DpVzOETR.js +0 -1
- package/dist/web/assets/arrow-up-BigIMx-e.js +0 -1
- package/dist/web/assets/channel-Cgy1thYT.js +0 -1
- package/dist/web/assets/chat-tab-DXBb9Y3U.js +0 -10
- package/dist/web/assets/check-ePA3ZvK4.js +0 -1
- package/dist/web/assets/chevron-down-EQA06nR-.js +0 -1
- package/dist/web/assets/chevron-right-CXzzT44u.js +0 -1
- package/dist/web/assets/chunk-GLR3WWYH-CxUl1sdz.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-DN7ebS2Y.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-C4La7oLj.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-C3IyfqG-.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-Dcvhz2pb.js +0 -1
- package/dist/web/assets/clone--C7Tby8z.js +0 -1
- package/dist/web/assets/code-editor-Cr7JrBKC.js +0 -8
- package/dist/web/assets/columns-2-BZ9uqssV.js +0 -1
- package/dist/web/assets/createLucideIcon-PuMiQgHl.js +0 -1
- package/dist/web/assets/database-D1ToEV9d.js +0 -1
- package/dist/web/assets/diff-viewer-BBr6e_gb.js +0 -4
- package/dist/web/assets/dist-KUoHa6tg.js +0 -1
- package/dist/web/assets/extension-webview-B0klBip8.js +0 -3
- package/dist/web/assets/eye-CNcBU6Tx.js +0 -1
- package/dist/web/assets/git-graph-CDiwGa0g.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-DcPyMEIJ.js +0 -1
- package/dist/web/assets/index-CkaCzNgO.css +0 -2
- package/dist/web/assets/index-Ic5uTu20.js +0 -26
- package/dist/web/assets/info-3K5VOQVL-Dw4O15cw.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-DFhmsucr.js +0 -2
- package/dist/web/assets/input-CcbTF6ih.js +0 -45
- package/dist/web/assets/jsx-runtime-R_NjdZtX.js +0 -1
- package/dist/web/assets/keybindings-store-CxE6BlG2.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-o3LmdL8H.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-BjNP0M3B.js +0 -1
- package/dist/web/assets/plus-Iso5r9vD.js +0 -1
- package/dist/web/assets/port-forwarding-tab-BPuSc6pI.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-gDgOiaME.js +0 -1
- package/dist/web/assets/refresh-cw-BgQzFNaG.js +0 -1
- package/dist/web/assets/scroll-area-i4EZlOl_.js +0 -1
- package/dist/web/assets/settings-tab-BzSSN2BQ.js +0 -1
- package/dist/web/assets/sqlite-viewer-CoyZOM_Y.js +0 -1
- package/dist/web/assets/square-pfn_LYYy.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DksQJ7es.js +0 -1
- package/dist/web/assets/table-CHv2x_qg.js +0 -1
- package/dist/web/assets/tag-Bb_UFXt0.js +0 -1
- package/dist/web/assets/text-wrap-D8BbQYTx.js +0 -1
- package/dist/web/assets/trash-2-DYCa06CV.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-DwFqAvnj.js +0 -1
- package/dist/web/assets/x-BXecj-16.js +0 -1
- package/src/web/components/git/git-graph-branch-label.tsx +0 -124
- package/src/web/components/git/git-graph-constants.ts +0 -185
- package/src/web/components/git/git-graph-detail.tsx +0 -107
- package/src/web/components/git/git-graph-dialog.tsx +0 -72
- package/src/web/components/git/git-graph-row.tsx +0 -167
- package/src/web/components/git/git-graph-settings-dialog.tsx +0 -104
- package/src/web/components/git/git-graph-svg.tsx +0 -54
- package/src/web/components/git/git-graph-toolbar.tsx +0 -195
- package/src/web/components/git/git-graph.tsx +0 -193
- package/src/web/components/git/use-column-resize.ts +0 -33
- package/src/web/components/git/use-git-graph.ts +0 -201
- /package/dist/web/assets/{api-client-wQbeUyeh.js → api-client-BvxmRZUi.js} +0 -0
- /package/dist/web/assets/{array-X0JlPOfd.js → array-BFDiaBgf.js} +0 -0
- /package/dist/web/assets/{csv-parser-CElqio6o.js → csv-parser-i7fjqP2H.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-BfIOPvwt.js → cytoscape.esm-C8i2jUzT.js} +0 -0
- /package/dist/web/assets/{defaultLocale-B6RGN4id.js → defaultLocale-ZeknFqNe.js} +0 -0
- /package/dist/web/assets/{dist-CK1enexV.js → dist-DZmJeHOA.js} +0 -0
- /package/dist/web/assets/{init-BmUWJJHz.js → init-0VJVrkRJ.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-BrCM-iA1.js → isArrayLikeObject-ClzWCpcm.js} +0 -0
- /package/dist/web/assets/{katex-xQS_6bNb.js → katex-DR0kdMDv.js} +0 -0
- /package/dist/web/assets/{lib-CfWBrYll.js → lib-CeBVkQ-7.js} +0 -0
- /package/dist/web/assets/{math-CpLFzrfV.js → math-CRc16Nj6.js} +0 -0
- /package/dist/web/assets/{path-CoPyR7c2.js → path-INs8XTPH.js} +0 -0
- /package/dist/web/assets/{preload-helper-CH6UZRzu.js → preload-helper-mr3rCizq.js} +0 -0
- /package/dist/web/assets/{react-j5zqhEum.js → react-0tkk-ztn.js} +0 -0
- /package/dist/web/assets/{rough.esm-D5NinLFK.js → rough.esm-eLccZ4OJ.js} +0 -0
- /package/dist/web/assets/{sql-completion-provider-D0xutVaK.js → sql-completion-provider-B8uUWWej.js} +0 -0
- /package/dist/web/assets/{src-j04igtQ5.js → src-CqyWLlNZ.js} +0 -0
- /package/dist/web/assets/{utils-CSCvNZxE.js → utils-DX8jb5qv.js} +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { useRef, useEffect,
|
|
1
|
+
import { useRef, useEffect, useState } from "react";
|
|
2
2
|
import { useExtensionStore } from "@/stores/extension-store";
|
|
3
|
+
import { useTabStore } from "@/stores/tab-store";
|
|
4
|
+
import { Loader2 } from "lucide-react";
|
|
3
5
|
|
|
4
6
|
/** Inject acquireVsCodeApi() shim so extension webviews can postMessage to parent */
|
|
5
7
|
const VSCODE_API_SHIM = `<script>
|
|
@@ -22,49 +24,146 @@ interface ExtensionWebviewProps {
|
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* iframe-based webview container for extension-contributed webview panels.
|
|
25
|
-
*
|
|
27
|
+
* Matches panel by panelId (direct) or viewType (reload recovery).
|
|
26
28
|
*/
|
|
27
29
|
export function ExtensionWebview({ metadata }: ExtensionWebviewProps) {
|
|
28
30
|
const panelId = metadata?.panelId as string | undefined;
|
|
29
|
-
const
|
|
31
|
+
const viewType = metadata?.viewType as string | undefined;
|
|
32
|
+
const currentProject = useTabStore((s) => s.currentProject);
|
|
33
|
+
const projectName = (metadata?.projectName as string | undefined) || currentProject || undefined;
|
|
34
|
+
const [timedOut, setTimedOut] = useState(false);
|
|
35
|
+
|
|
36
|
+
// Match panel: prefer panelId (exact), fallback to viewType match (reload recovery)
|
|
37
|
+
const panel = useExtensionStore((s) => {
|
|
38
|
+
if (panelId && s.webviewPanels[panelId]) return s.webviewPanels[panelId];
|
|
39
|
+
if (viewType) {
|
|
40
|
+
// Find panel whose viewType matches (with or without .view suffix)
|
|
41
|
+
const fullViewType = viewType.includes(".") ? viewType : `${viewType}.view`;
|
|
42
|
+
return Object.values(s.webviewPanels).find(
|
|
43
|
+
(p) => p.viewType === viewType || p.viewType === fullViewType,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const resolvedPanelId = panel?.id ?? panelId;
|
|
30
50
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
31
51
|
|
|
32
52
|
// Inject acquireVsCodeApi shim + write HTML into iframe via srcdoc
|
|
33
53
|
const rawHtml = panel?.html ?? "";
|
|
34
54
|
const html = injectVscodeApiShim(rawHtml);
|
|
35
55
|
|
|
56
|
+
// On reload: resolve project path, then dispatch command with retry
|
|
57
|
+
// Retry needed because WS connection may not be ready on first attempt
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (panel || !viewType) return;
|
|
60
|
+
const command = viewType.includes(".") ? viewType : `${viewType}.view`;
|
|
61
|
+
let cancelled = false;
|
|
62
|
+
let resolvedArgs: unknown[] | null = null;
|
|
63
|
+
|
|
64
|
+
async function resolveArgs(): Promise<unknown[]> {
|
|
65
|
+
if (resolvedArgs) return resolvedArgs;
|
|
66
|
+
if (!projectName) return [];
|
|
67
|
+
try {
|
|
68
|
+
const res = await fetch("/api/projects");
|
|
69
|
+
const json = await res.json() as { ok: boolean; data?: { name: string; path: string }[] };
|
|
70
|
+
const match = json.data?.find((p) => p.name === projectName);
|
|
71
|
+
resolvedArgs = match ? [match.path] : [];
|
|
72
|
+
} catch {
|
|
73
|
+
resolvedArgs = [];
|
|
74
|
+
}
|
|
75
|
+
return resolvedArgs;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function attempt() {
|
|
79
|
+
const args = await resolveArgs();
|
|
80
|
+
if (cancelled) return;
|
|
81
|
+
window.dispatchEvent(new CustomEvent("ext:command:execute", {
|
|
82
|
+
detail: { command, args },
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// First attempt after short delay (let WS connect), then retry every 2s
|
|
87
|
+
const initialTimer = setTimeout(() => {
|
|
88
|
+
if (!cancelled) attempt();
|
|
89
|
+
}, 500);
|
|
90
|
+
const retryTimer = setInterval(() => {
|
|
91
|
+
if (!cancelled) attempt();
|
|
92
|
+
}, 2_000);
|
|
93
|
+
|
|
94
|
+
return () => { cancelled = true; clearTimeout(initialTimer); clearInterval(retryTimer); };
|
|
95
|
+
}, [panel, viewType, projectName]);
|
|
96
|
+
|
|
97
|
+
// When project switches while panel exists, re-dispatch command with new project path
|
|
98
|
+
const prevProjectRef = useRef(currentProject);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!panel || !viewType || !currentProject || currentProject === prevProjectRef.current) {
|
|
101
|
+
prevProjectRef.current = currentProject;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
prevProjectRef.current = currentProject;
|
|
105
|
+
// Resolve new project path and dispatch command
|
|
106
|
+
(async () => {
|
|
107
|
+
try {
|
|
108
|
+
const res = await fetch("/api/projects");
|
|
109
|
+
const json = await res.json() as { ok: boolean; data?: { name: string; path: string }[] };
|
|
110
|
+
const match = json.data?.find((p: { name: string }) => p.name === currentProject);
|
|
111
|
+
if (match) {
|
|
112
|
+
const command = viewType.includes(".") ? viewType : `${viewType}.view`;
|
|
113
|
+
window.dispatchEvent(new CustomEvent("ext:command:execute", {
|
|
114
|
+
detail: { command, args: [match.path] },
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
} catch {}
|
|
118
|
+
})();
|
|
119
|
+
}, [panel, viewType, currentProject]);
|
|
120
|
+
|
|
121
|
+
// Timeout: if panel doesn't appear within 10s, show error
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (panel) { setTimedOut(false); return; }
|
|
124
|
+
const timer = setTimeout(() => setTimedOut(true), 10_000);
|
|
125
|
+
return () => clearTimeout(timer);
|
|
126
|
+
}, [panel]);
|
|
127
|
+
|
|
36
128
|
// Listen for postMessage from iframe → forward to extension via WS bridge
|
|
37
129
|
useEffect(() => {
|
|
130
|
+
if (!resolvedPanelId) return;
|
|
38
131
|
const handler = (event: MessageEvent) => {
|
|
39
132
|
if (iframeRef.current && event.source === iframeRef.current.contentWindow) {
|
|
40
|
-
// Forward to server via custom event → picked up by useExtensionWs
|
|
41
133
|
window.dispatchEvent(new CustomEvent("ext:webview:send", {
|
|
42
|
-
detail: { panelId, message: event.data },
|
|
134
|
+
detail: { panelId: resolvedPanelId, message: event.data },
|
|
43
135
|
}));
|
|
44
136
|
}
|
|
45
137
|
};
|
|
46
138
|
window.addEventListener("message", handler);
|
|
47
139
|
return () => window.removeEventListener("message", handler);
|
|
48
|
-
}, [
|
|
140
|
+
}, [resolvedPanelId]);
|
|
49
141
|
|
|
50
142
|
// 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
143
|
useEffect(() => {
|
|
144
|
+
if (!resolvedPanelId) return;
|
|
54
145
|
const handler = (e: Event) => {
|
|
55
146
|
const { panelId: targetId, message } = (e as CustomEvent).detail;
|
|
56
|
-
if (targetId ===
|
|
147
|
+
if (targetId === resolvedPanelId) {
|
|
57
148
|
iframeRef.current?.contentWindow?.postMessage(message, "*");
|
|
58
149
|
}
|
|
59
150
|
};
|
|
60
151
|
window.addEventListener("ext:webview:message", handler);
|
|
61
152
|
return () => window.removeEventListener("ext:webview:message", handler);
|
|
62
|
-
}, [
|
|
153
|
+
}, [resolvedPanelId]);
|
|
63
154
|
|
|
155
|
+
// Loading state — waiting for extension to create the panel
|
|
64
156
|
if (!panel) {
|
|
65
157
|
return (
|
|
66
|
-
<div className="flex items-center justify-center h-full text-sm text-text-subtle">
|
|
67
|
-
|
|
158
|
+
<div className="flex flex-col items-center justify-center h-full gap-2 text-sm text-text-subtle">
|
|
159
|
+
{timedOut ? (
|
|
160
|
+
<span>Extension failed to load webview panel</span>
|
|
161
|
+
) : (
|
|
162
|
+
<>
|
|
163
|
+
<Loader2 className="size-5 animate-spin" />
|
|
164
|
+
<span>Loading extension...</span>
|
|
165
|
+
</>
|
|
166
|
+
)}
|
|
68
167
|
</div>
|
|
69
168
|
);
|
|
70
169
|
}
|
|
@@ -2,8 +2,9 @@ import { useState, useEffect, useRef, useMemo, useCallback } from "react";
|
|
|
2
2
|
import {
|
|
3
3
|
Terminal,
|
|
4
4
|
MessageSquare,
|
|
5
|
-
GitBranch,
|
|
6
5
|
GitCommitHorizontal,
|
|
6
|
+
GitBranch,
|
|
7
|
+
Puzzle,
|
|
7
8
|
Settings,
|
|
8
9
|
Database,
|
|
9
10
|
Search,
|
|
@@ -13,7 +14,8 @@ import {
|
|
|
13
14
|
Loader2,
|
|
14
15
|
Globe,
|
|
15
16
|
Mic,
|
|
16
|
-
|
|
17
|
+
RefreshCw,
|
|
18
|
+
Plus,
|
|
17
19
|
} from "lucide-react";
|
|
18
20
|
import { useTabStore, type TabType } from "@/stores/tab-store";
|
|
19
21
|
import { useProjectStore } from "@/stores/project-store";
|
|
@@ -38,6 +40,19 @@ interface CommandItem {
|
|
|
38
40
|
|
|
39
41
|
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent);
|
|
40
42
|
|
|
43
|
+
/** Map extension icon string names to lucide components */
|
|
44
|
+
const EXT_ICON_MAP: Record<string, React.ElementType> = {
|
|
45
|
+
"git-branch": GitBranch,
|
|
46
|
+
"database": Database,
|
|
47
|
+
"refresh": RefreshCw,
|
|
48
|
+
"plus": Plus,
|
|
49
|
+
"terminal": Terminal,
|
|
50
|
+
"settings": Settings,
|
|
51
|
+
"search": Search,
|
|
52
|
+
"file-code": FileCode,
|
|
53
|
+
"globe": Globe,
|
|
54
|
+
};
|
|
55
|
+
|
|
41
56
|
/** Format a keybinding combo for display (e.g. "Mod+G" → "⌘G" on Mac, "Ctrl+G" on others) */
|
|
42
57
|
function formatShortcut(combo: string): string {
|
|
43
58
|
if (!combo) return "";
|
|
@@ -162,7 +177,6 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
|
|
|
162
177
|
{ id: "chat", label: "New AI Chat", icon: MessageSquare, action: openNewTab("chat", "AI Chat"), keywords: "ai assistant claude", group: "action", shortcut: formatShortcut(getBinding("open-chat")) },
|
|
163
178
|
{ id: "new-file", label: "New File", icon: FilePlus, action: () => { useTabStore.getState().openNewFile(); onClose(); }, keywords: "create untitled blank empty", group: "action", shortcut: formatShortcut(getBinding("new-file")) },
|
|
164
179
|
{ id: "terminal", label: "New Terminal", icon: Terminal, action: openNewTab("terminal", "Terminal"), keywords: "bash shell console", group: "action", shortcut: formatShortcut(getBinding("open-terminal")) },
|
|
165
|
-
{ 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")) },
|
|
166
180
|
{ id: "ports", label: "Port Forwarding", icon: Globe, action: openNewTab("ports", "Ports"), keywords: "web preview localhost port forward tunnel url", group: "action" },
|
|
167
181
|
{ id: "postgres", label: "PostgreSQL", icon: Database, action: openNewTab("postgres", "PostgreSQL"), keywords: "database pg sql query", group: "action" },
|
|
168
182
|
{ id: "voice-input", label: "Voice Input", icon: Mic, action: () => { window.dispatchEvent(new CustomEvent("toggle-voice-input")); onClose(); }, keywords: "speech microphone dictate voice", group: "action", shortcut: formatShortcut(getBinding("voice-input")) },
|
|
@@ -180,20 +194,30 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
|
|
|
180
194
|
},
|
|
181
195
|
];
|
|
182
196
|
|
|
183
|
-
// Append extension-contributed commands
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
// Append extension-contributed commands (with keybinding shortcuts, respecting user overrides)
|
|
198
|
+
const extKbs = extContributions?.keybindings ?? [];
|
|
199
|
+
const extCmds: CommandItem[] = (extContributions?.commands ?? []).map((cmd) => {
|
|
200
|
+
const kb = extKbs.find((k) => k.command === cmd.command);
|
|
201
|
+
const overrideCombo = kb ? getBinding(`ext:${kb.command}`) : "";
|
|
202
|
+
const shortcutCombo = overrideCombo || (kb ? ((isMac && kb.mac) ? kb.mac : kb.key) : "");
|
|
203
|
+
return {
|
|
204
|
+
id: `ext:${cmd.command}`,
|
|
205
|
+
label: cmd.title,
|
|
206
|
+
hint: cmd.category,
|
|
207
|
+
icon: (cmd.icon && EXT_ICON_MAP[cmd.icon]) || Puzzle,
|
|
208
|
+
group: "action" as const,
|
|
209
|
+
keywords: `extension ${cmd.command} ${cmd.category ?? ""}`,
|
|
210
|
+
shortcut: shortcutCombo ? formatShortcut(shortcutCombo) : undefined,
|
|
211
|
+
action: () => {
|
|
212
|
+
const args: unknown[] = [];
|
|
213
|
+
if (activeProject?.path) args.push(activeProject.path);
|
|
214
|
+
window.dispatchEvent(new CustomEvent("ext:command:execute", {
|
|
215
|
+
detail: { command: cmd.command, args },
|
|
216
|
+
}));
|
|
217
|
+
onClose();
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
});
|
|
197
221
|
|
|
198
222
|
return [...builtIn, ...extCmds];
|
|
199
223
|
}, [activeProject, openTab, onClose, setSidebarActiveTab, sidebarCollapsed, toggleSidebar, getBinding, extContributions]);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Suspense, lazy, useEffect, useState, useCallback } from "react";
|
|
2
|
-
import { ChevronDown, ChevronUp, Loader2, Terminal, MessageSquare,
|
|
2
|
+
import { ChevronDown, ChevronUp, Loader2, Terminal, MessageSquare, FilePlus, Pin, PinOff } from "lucide-react";
|
|
3
3
|
import { usePanelStore } from "@/stores/panel-store";
|
|
4
4
|
import { useProjectStore } from "@/stores/project-store";
|
|
5
|
-
import type
|
|
5
|
+
import { useTabStore, type TabType } from "@/stores/tab-store";
|
|
6
6
|
import { api, projectUrl } from "@/lib/api-client";
|
|
7
7
|
import type { SessionInfo } from "../../../types/chat";
|
|
8
8
|
import { TabBar } from "./tab-bar";
|
|
@@ -12,7 +12,7 @@ import { cn } from "@/lib/utils";
|
|
|
12
12
|
const QUICK_OPEN_TABS: { type: TabType; label: string; icon: React.ElementType }[] = [
|
|
13
13
|
{ type: "terminal", label: "Terminal", icon: Terminal },
|
|
14
14
|
{ type: "chat", label: "AI Chat", icon: MessageSquare },
|
|
15
|
-
{ type: "
|
|
15
|
+
{ type: "editor", label: "New File", icon: FilePlus },
|
|
16
16
|
];
|
|
17
17
|
|
|
18
18
|
const TAB_COMPONENTS: Record<TabType, React.LazyExoticComponent<React.ComponentType<{ metadata?: Record<string, unknown>; tabId?: string }>>> = {
|
|
@@ -22,10 +22,10 @@ const TAB_COMPONENTS: Record<TabType, React.LazyExoticComponent<React.ComponentT
|
|
|
22
22
|
database: lazy(() => import("@/components/database/database-viewer").then((m) => ({ default: m.DatabaseViewer }))),
|
|
23
23
|
sqlite: lazy(() => import("@/components/sqlite/sqlite-viewer").then((m) => ({ default: m.SqliteViewer }))),
|
|
24
24
|
postgres: lazy(() => import("@/components/postgres/postgres-viewer").then((m) => ({ default: m.PostgresViewer }))),
|
|
25
|
-
"git-graph": lazy(() => import("@/components/git/git-graph").then((m) => ({ default: m.GitGraph }))),
|
|
26
25
|
"git-diff": lazy(() => import("@/components/editor/diff-viewer").then((m) => ({ default: m.DiffViewer }))),
|
|
27
26
|
settings: lazy(() => import("@/components/settings/settings-tab").then((m) => ({ default: m.SettingsTab }))),
|
|
28
27
|
ports: lazy(() => import("@/components/ports/port-forwarding-tab").then((m) => ({ default: m.PortForwardingTab }))),
|
|
28
|
+
extension: lazy(() => import("@/components/extensions/extension-webview").then((m) => ({ default: m.ExtensionWebview }))),
|
|
29
29
|
"extension-webview": lazy(() => import("@/components/extensions/extension-webview").then((m) => ({ default: m.ExtensionWebview }))),
|
|
30
30
|
};
|
|
31
31
|
|
|
@@ -62,6 +62,13 @@ export function EditorPanel({ panelId, projectName }: EditorPanelProps) {
|
|
|
62
62
|
panel.tabs.map((tab) => {
|
|
63
63
|
const Component = TAB_COMPONENTS[tab.type];
|
|
64
64
|
const isActive = tab.id === panel.activeTabId;
|
|
65
|
+
if (!Component) {
|
|
66
|
+
return (
|
|
67
|
+
<div key={tab.id} className={isActive ? "h-full w-full flex items-center justify-center text-muted-foreground" : "hidden"}>
|
|
68
|
+
Unknown tab type: {tab.type}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
65
72
|
return (
|
|
66
73
|
<div key={tab.id} className={isActive ? "h-full w-full" : "hidden"}>
|
|
67
74
|
<Suspense fallback={<div className="flex items-center justify-center h-full"><Loader2 className="size-6 animate-spin text-primary" /></div>}>
|
|
@@ -143,6 +150,10 @@ function EmptyPanel({ panelId }: { panelId: string }) {
|
|
|
143
150
|
}, [activeProject?.name]);
|
|
144
151
|
|
|
145
152
|
function openTab(type: TabType) {
|
|
153
|
+
if (type === "editor") {
|
|
154
|
+
useTabStore.getState().openNewFile();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
146
157
|
const needsProject = type !== "settings";
|
|
147
158
|
const metadata = needsProject && activeProject ? { projectName: activeProject.name } : undefined;
|
|
148
159
|
usePanelStore.getState().openTab(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
|
2
2
|
import {
|
|
3
|
-
Terminal, MessageSquare,
|
|
3
|
+
Terminal, MessageSquare, Database,
|
|
4
4
|
FileDiff, FileCode, Settings, Menu, X, ArrowLeft, ArrowRight, SplitSquareVertical, MoveVertical, Layers, Plus,
|
|
5
5
|
ChevronRight, Globe, Puzzle, Copy, Download, Pencil, Trash2,
|
|
6
6
|
} from "lucide-react";
|
|
@@ -21,14 +21,14 @@ import { FileActions } from "@/components/explorer/file-actions";
|
|
|
21
21
|
const NEW_TAB_OPTIONS: { type: TabType; label: string }[] = [
|
|
22
22
|
{ type: "terminal", label: "Terminal" },
|
|
23
23
|
{ type: "chat", label: "AI Chat" },
|
|
24
|
-
{ type: "git-graph", label: "Git Graph" },
|
|
25
24
|
{ type: "settings", label: "Settings" },
|
|
26
25
|
];
|
|
27
26
|
const NEW_TAB_LABELS: Partial<Record<TabType, string>> = Object.fromEntries(NEW_TAB_OPTIONS.map((o) => [o.type, o.label]));
|
|
28
27
|
|
|
29
28
|
const TAB_ICONS: Record<TabType, React.ElementType> = {
|
|
30
29
|
terminal: Terminal, chat: MessageSquare, editor: FileCode, database: Database, sqlite: Database, postgres: Database,
|
|
31
|
-
"git-
|
|
30
|
+
"git-diff": FileDiff, settings: Settings, ports: Globe,
|
|
31
|
+
extension: Puzzle,
|
|
32
32
|
"extension-webview": Puzzle,
|
|
33
33
|
};
|
|
34
34
|
|
|
@@ -141,7 +141,7 @@ export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
|
|
|
141
141
|
function handleNewTab(type: TabType) {
|
|
142
142
|
const state = usePanelStore.getState();
|
|
143
143
|
const firstPanelId = state.grid[0]?.[0] ?? state.focusedPanelId;
|
|
144
|
-
const needsProject = type === "git-
|
|
144
|
+
const needsProject = type === "git-diff" || type === "terminal" || type === "chat";
|
|
145
145
|
const metadata = needsProject ? { projectName: activeProjectForTab?.name } : undefined;
|
|
146
146
|
state.openTab(
|
|
147
147
|
{ type, title: NEW_TAB_LABELS[type] ?? type, metadata, projectId: activeProjectForTab?.name ?? null, closable: true },
|
|
@@ -211,7 +211,7 @@ export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
|
|
|
211
211
|
<div className="flex-1 min-w-0 relative flex items-center h-12 -ml-4">
|
|
212
212
|
<div ref={mobileScrollRef} className="flex-1 min-w-0 flex items-center h-12 overflow-x-auto scrollbar-none pl-4">
|
|
213
213
|
{tabs.map((tab) => {
|
|
214
|
-
const Icon = TAB_ICONS[tab.type];
|
|
214
|
+
const Icon = TAB_ICONS[tab.type] || Puzzle;
|
|
215
215
|
const isActive = tab.id === activeTabId;
|
|
216
216
|
const sessionId = tab.type === "chat" ? (tab.metadata?.sessionId as string) : undefined;
|
|
217
217
|
const entry = sessionId ? notifications.get(sessionId) : undefined;
|
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
Plus,
|
|
4
4
|
Terminal,
|
|
5
5
|
MessageSquare,
|
|
6
|
-
GitBranch,
|
|
7
6
|
FileDiff,
|
|
8
7
|
Settings,
|
|
9
8
|
FileCode,
|
|
@@ -36,10 +35,10 @@ const TAB_ICONS: Record<TabType, React.ElementType> = {
|
|
|
36
35
|
database: Database,
|
|
37
36
|
sqlite: Database,
|
|
38
37
|
postgres: Database,
|
|
39
|
-
"git-graph": GitBranch,
|
|
40
38
|
"git-diff": FileDiff,
|
|
41
39
|
settings: Settings,
|
|
42
40
|
ports: Globe,
|
|
41
|
+
extension: Puzzle,
|
|
43
42
|
"extension-webview": Puzzle,
|
|
44
43
|
};
|
|
45
44
|
|
|
@@ -194,7 +193,7 @@ export function TabBar({ panelId }: TabBarProps) {
|
|
|
194
193
|
key={tab.id}
|
|
195
194
|
tab={tab}
|
|
196
195
|
isActive={tab.id === activeTabId}
|
|
197
|
-
icon={TAB_ICONS[tab.type]}
|
|
196
|
+
icon={TAB_ICONS[tab.type] || Puzzle}
|
|
198
197
|
showDropBefore={dropIndex === i}
|
|
199
198
|
notificationType={notiType}
|
|
200
199
|
onSelect={() => {
|
|
@@ -33,11 +33,6 @@ const TAB_COMPONENTS: Record<TabType, React.LazyExoticComponent<React.ComponentT
|
|
|
33
33
|
default: m.PostgresViewer,
|
|
34
34
|
})),
|
|
35
35
|
),
|
|
36
|
-
"git-graph": lazy(() =>
|
|
37
|
-
import("@/components/git/git-graph").then((m) => ({
|
|
38
|
-
default: m.GitGraph,
|
|
39
|
-
})),
|
|
40
|
-
),
|
|
41
36
|
"git-diff": lazy(() =>
|
|
42
37
|
import("@/components/editor/diff-viewer").then((m) => ({
|
|
43
38
|
default: m.DiffViewer,
|
|
@@ -53,6 +48,11 @@ const TAB_COMPONENTS: Record<TabType, React.LazyExoticComponent<React.ComponentT
|
|
|
53
48
|
default: m.PortForwardingTab,
|
|
54
49
|
})),
|
|
55
50
|
),
|
|
51
|
+
extension: lazy(() =>
|
|
52
|
+
import("@/components/extensions/extension-webview").then((m) => ({
|
|
53
|
+
default: m.ExtensionWebview,
|
|
54
|
+
})),
|
|
55
|
+
),
|
|
56
56
|
"extension-webview": lazy(() =>
|
|
57
57
|
import("@/components/extensions/extension-webview").then((m) => ({
|
|
58
58
|
default: m.ExtensionWebview,
|
|
@@ -84,6 +84,13 @@ export function TabContent() {
|
|
|
84
84
|
{tabs.map((tab) => {
|
|
85
85
|
const Component = TAB_COMPONENTS[tab.type];
|
|
86
86
|
const isActive = tab.id === activeTabId;
|
|
87
|
+
if (!Component) {
|
|
88
|
+
return (
|
|
89
|
+
<div key={tab.id} className={isActive ? "h-full w-full flex items-center justify-center text-muted-foreground" : "hidden"}>
|
|
90
|
+
Unknown tab type: {tab.type}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
87
94
|
return (
|
|
88
95
|
<div
|
|
89
96
|
key={tab.id}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
-
import { RotateCcw, AlertTriangle, Lock } from "lucide-react";
|
|
2
|
+
import { RotateCcw, AlertTriangle, Lock, Puzzle } from "lucide-react";
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
4
|
import {
|
|
5
5
|
KEY_ACTIONS,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
comboFromEvent,
|
|
9
9
|
type KeyCategory,
|
|
10
10
|
} from "@/stores/keybindings-store";
|
|
11
|
+
import { useExtensionStore } from "@/stores/extension-store";
|
|
11
12
|
|
|
12
13
|
const CATEGORIES: { key: KeyCategory; label: string }[] = [
|
|
13
14
|
{ key: "general", label: "General" },
|
|
@@ -104,6 +105,9 @@ function ShortcutBadge({
|
|
|
104
105
|
|
|
105
106
|
export function KeyboardShortcutsSection() {
|
|
106
107
|
const { getBinding, resetBinding, resetAll, overrides } = useKeybindingsStore();
|
|
108
|
+
const extContributions = useExtensionStore((s) => s.contributions);
|
|
109
|
+
const extKeybindings = extContributions?.keybindings ?? [];
|
|
110
|
+
const extCommands = extContributions?.commands ?? [];
|
|
107
111
|
|
|
108
112
|
return (
|
|
109
113
|
<div className="space-y-3">
|
|
@@ -177,6 +181,47 @@ export function KeyboardShortcutsSection() {
|
|
|
177
181
|
</div>
|
|
178
182
|
);
|
|
179
183
|
})}
|
|
184
|
+
|
|
185
|
+
{/* Extension-contributed keybindings */}
|
|
186
|
+
{extKeybindings.length > 0 && (
|
|
187
|
+
<div className="space-y-1">
|
|
188
|
+
<span className="text-[10px] font-medium text-muted-foreground uppercase tracking-wide">
|
|
189
|
+
Extensions
|
|
190
|
+
</span>
|
|
191
|
+
<div className="space-y-0.5">
|
|
192
|
+
{extKeybindings.map((kb) => {
|
|
193
|
+
const cmd = extCommands.find((c) => c.command === kb.command);
|
|
194
|
+
const label = cmd?.title ?? kb.command;
|
|
195
|
+
const actionId = `ext:${kb.command}`;
|
|
196
|
+
const currentCombo = getBinding(actionId) || kb.key;
|
|
197
|
+
const isOverridden = actionId in overrides;
|
|
198
|
+
return (
|
|
199
|
+
<div
|
|
200
|
+
key={actionId}
|
|
201
|
+
className="flex items-center justify-between py-1 px-1 rounded hover:bg-surface-elevated/50 transition-colors"
|
|
202
|
+
>
|
|
203
|
+
<div className="flex items-center gap-1.5 min-w-0">
|
|
204
|
+
<Puzzle className="size-3 text-muted-foreground shrink-0" />
|
|
205
|
+
<span className="text-xs text-foreground">{label}</span>
|
|
206
|
+
</div>
|
|
207
|
+
<div className="flex items-center gap-1 shrink-0 ml-2">
|
|
208
|
+
<ShortcutBadge actionId={actionId} combo={currentCombo} />
|
|
209
|
+
{isOverridden && (
|
|
210
|
+
<button
|
|
211
|
+
onClick={() => resetBinding(actionId)}
|
|
212
|
+
className="flex items-center justify-center size-5 rounded text-muted-foreground hover:text-foreground hover:bg-surface-elevated transition-colors"
|
|
213
|
+
title="Reset to default"
|
|
214
|
+
>
|
|
215
|
+
<RotateCcw className="size-3" />
|
|
216
|
+
</button>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
})}
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
180
225
|
</div>
|
|
181
226
|
);
|
|
182
227
|
}
|
|
@@ -111,7 +111,8 @@ export function useExtensionWs(enabled = true) {
|
|
|
111
111
|
});
|
|
112
112
|
break;
|
|
113
113
|
|
|
114
|
-
case "webview:create":
|
|
114
|
+
case "webview:create": {
|
|
115
|
+
const viewTypeSlug = msg.viewType.replace(/\.view$/, "");
|
|
115
116
|
store.addWebviewPanel({
|
|
116
117
|
id: msg.panelId,
|
|
117
118
|
extensionId: msg.extensionId,
|
|
@@ -119,15 +120,18 @@ export function useExtensionWs(enabled = true) {
|
|
|
119
120
|
title: msg.title,
|
|
120
121
|
html: "",
|
|
121
122
|
});
|
|
122
|
-
// Open a tab
|
|
123
|
+
// Open a tab — use stable viewType slug as identifier (survives reload)
|
|
124
|
+
// Include projectName so reload can resolve project path for re-trigger
|
|
125
|
+
const currentProject = useTabStore.getState().currentProject;
|
|
123
126
|
useTabStore.getState().openTab({
|
|
124
|
-
type: "extension
|
|
127
|
+
type: "extension",
|
|
125
128
|
title: msg.title,
|
|
126
129
|
projectId: null,
|
|
127
130
|
closable: true,
|
|
128
|
-
metadata: { panelId: msg.panelId, extensionId: msg.extensionId },
|
|
131
|
+
metadata: { viewType: viewTypeSlug, panelId: msg.panelId, extensionId: msg.extensionId, ...(currentProject && { projectName: currentProject }) },
|
|
129
132
|
});
|
|
130
133
|
break;
|
|
134
|
+
}
|
|
131
135
|
|
|
132
136
|
case "webview:html":
|
|
133
137
|
store.updateWebviewPanel(msg.panelId, { html: msg.html });
|
|
@@ -142,6 +146,20 @@ export function useExtensionWs(enabled = true) {
|
|
|
142
146
|
detail: { panelId: msg.panelId, message: msg.message },
|
|
143
147
|
}));
|
|
144
148
|
break;
|
|
149
|
+
|
|
150
|
+
case "tab:open":
|
|
151
|
+
useTabStore.getState().openTab({
|
|
152
|
+
type: (msg as any).tabType,
|
|
153
|
+
title: (msg as any).title,
|
|
154
|
+
projectId: (msg as any).projectId ?? null,
|
|
155
|
+
closable: (msg as any).closable ?? true,
|
|
156
|
+
metadata: (msg as any).metadata,
|
|
157
|
+
});
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
case "project:switch":
|
|
161
|
+
useTabStore.getState().switchProject((msg as any).projectName);
|
|
162
|
+
break;
|
|
145
163
|
}
|
|
146
164
|
});
|
|
147
165
|
|
|
@@ -2,7 +2,8 @@ import { useEffect, useState, useCallback } from "react";
|
|
|
2
2
|
import { useTabStore } from "@/stores/tab-store";
|
|
3
3
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
4
4
|
import { useProjectStore } from "@/stores/project-store";
|
|
5
|
-
import { useKeybindingsStore } from "@/stores/keybindings-store";
|
|
5
|
+
import { useKeybindingsStore, parseCombo, eventMatchesCombo } from "@/stores/keybindings-store";
|
|
6
|
+
import { useExtensionStore } from "@/stores/extension-store";
|
|
6
7
|
|
|
7
8
|
/** Dispatch this event to open the command palette from anywhere, optionally with initial query */
|
|
8
9
|
export function openCommandPalette(initialQuery?: string) {
|
|
@@ -112,7 +113,6 @@ export function useGlobalKeybindings() {
|
|
|
112
113
|
const tabShortcuts: { action: string; type: string; title: string }[] = [
|
|
113
114
|
{ action: "open-chat", type: "chat", title: "AI Chat" },
|
|
114
115
|
{ action: "open-terminal", type: "terminal", title: "Terminal" },
|
|
115
|
-
{ action: "open-git-graph", type: "git-graph", title: "Git Graph" },
|
|
116
116
|
];
|
|
117
117
|
for (const s of tabShortcuts) {
|
|
118
118
|
if (match(e, s.action)) {
|
|
@@ -176,6 +176,28 @@ export function useGlobalKeybindings() {
|
|
|
176
176
|
return;
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
+
|
|
180
|
+
// Extension-contributed keybindings (with user override support)
|
|
181
|
+
const extKbs = useExtensionStore.getState().contributions?.keybindings;
|
|
182
|
+
if (extKbs) {
|
|
183
|
+
const mac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent);
|
|
184
|
+
const { getBinding: getBind } = useKeybindingsStore.getState();
|
|
185
|
+
for (const kb of extKbs) {
|
|
186
|
+
// User override via "ext:<command>" key, fallback to extension default
|
|
187
|
+
const overrideCombo = getBind(`ext:${kb.command}`);
|
|
188
|
+
const combo = overrideCombo || ((mac && kb.mac) ? kb.mac : kb.key);
|
|
189
|
+
if (combo && eventMatchesCombo(e, parseCombo(combo))) {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
const project = useProjectStore.getState().activeProject;
|
|
192
|
+
const args: unknown[] = [];
|
|
193
|
+
if (project?.path) args.push(project.path);
|
|
194
|
+
window.dispatchEvent(new CustomEvent("ext:command:execute", {
|
|
195
|
+
detail: { command: kb.command, args },
|
|
196
|
+
}));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
179
201
|
}
|
|
180
202
|
|
|
181
203
|
// Custom event listener for programmatic opening
|
|
@@ -14,7 +14,8 @@ export interface UrlState {
|
|
|
14
14
|
|
|
15
15
|
const VALID_TAB_TYPES: TabType[] = [
|
|
16
16
|
"terminal", "chat", "editor", "database", "sqlite",
|
|
17
|
-
"postgres", "git-
|
|
17
|
+
"postgres", "git-diff", "settings", "ports",
|
|
18
|
+
"extension",
|
|
18
19
|
];
|
|
19
20
|
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
@@ -108,7 +109,6 @@ function buildMetadataFromUrl(
|
|
|
108
109
|
return { ...(sessionId && { sessionId }), ...(providerId && { providerId }), projectName };
|
|
109
110
|
}
|
|
110
111
|
case "terminal": return { terminalIndex: parseInt(identifier ?? "1", 10), projectName };
|
|
111
|
-
case "git-graph": return { projectName };
|
|
112
112
|
case "git-diff": return identifier ? { filePath: identifier, projectName } : null;
|
|
113
113
|
case "settings": return {};
|
|
114
114
|
case "database": {
|
|
@@ -121,6 +121,7 @@ function buildMetadataFromUrl(
|
|
|
121
121
|
return connId ? { connectionId: connId, tableName: tableName ?? "" } : null;
|
|
122
122
|
}
|
|
123
123
|
case "ports": return null;
|
|
124
|
+
case "extension": return identifier ? { viewType: identifier, projectName } : null;
|
|
124
125
|
default: return null;
|
|
125
126
|
}
|
|
126
127
|
}
|
|
@@ -130,13 +131,17 @@ function buildTitleFromUrl(type: TabType, identifier: string | null): string {
|
|
|
130
131
|
case "editor": return identifier?.split("/").pop() ?? "File";
|
|
131
132
|
case "chat": return "Chat";
|
|
132
133
|
case "terminal": return `Terminal ${identifier ?? "1"}`;
|
|
133
|
-
case "git-graph": return "Git Graph";
|
|
134
134
|
case "git-diff": return identifier?.split("/").pop() ?? "Diff";
|
|
135
135
|
case "settings": return "Settings";
|
|
136
136
|
case "database": return identifier ?? "Database";
|
|
137
137
|
case "sqlite": return identifier?.split("/").pop() ?? "SQLite";
|
|
138
138
|
case "postgres": return identifier ?? "PostgreSQL";
|
|
139
139
|
case "ports": return "Ports";
|
|
140
|
+
case "extension": {
|
|
141
|
+
if (!identifier) return "Extension";
|
|
142
|
+
// "git-graph" → "Git Graph"
|
|
143
|
+
return identifier.split("-").map(w => (w[0]?.toUpperCase() ?? "") + w.slice(1)).join(" ");
|
|
144
|
+
}
|
|
140
145
|
default: return type;
|
|
141
146
|
}
|
|
142
147
|
}
|