@hienlh/ppm 0.9.84 → 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/260413-1354-new-file-editor-tab/reports/code-reviewer-260413-1420-new-file-tab-review.md +210 -0
- package/CHANGELOG.md +23 -0
- package/bun.lock +259 -9
- package/dist/web/assets/{_basePickBy-5PGDJbfF.js → _basePickBy-Bj0dI1ei.js} +1 -1
- package/dist/web/assets/{_baseUniq-BT4Ow4Kk.js → _baseUniq-CyzdZeQH.js} +1 -1
- package/dist/web/assets/ai-settings-section-Bo9lCaTd.js +1 -0
- package/dist/web/assets/{api-settings-Bn-bIxD1.js → api-settings-CUxg9RE5.js} +1 -1
- package/dist/web/assets/{arc-BAOivWpI.js → arc-CxgHJ7Z4.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DDFO_NKq.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Z-4eN4za.js → architectureDiagram-2XIMDMQ5-D16OotsC.js} +1 -1
- package/dist/web/assets/arrow-up-I9-21gkR.js +1 -0
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-BCLqzhuZ.js → blockDiagram-WCTKOSBZ-Ct57Wtfk.js} +1 -1
- package/dist/web/assets/{c4Diagram-IC4MRINW-0Vp0Jeas.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-D4tOov49.js → chunk-4BX2VUAB-CENmY7Kw.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-DJ6BynZ4.js → chunk-55IACEB6-DhZGI1l3.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-CiyUJxNI.js → chunk-7E7YKBS2-DZcnC7Ow.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-Dv-4cAYn.js → chunk-7R4GIKGN-y8bfHEy-.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-D21mS_6G.js → chunk-C72U2L5F-BHPkfQj2.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-DzqmU2Z7.js → chunk-EGIJ26TM-nant2LXl.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-DXncblvW.js → chunk-FMBD7UC4-Bog4cpN-.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-D-pKjlVd.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-99JzIdPr.js → chunk-JSJVCQXG-23eG9mgt.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-CRq1OBZv.js → chunk-KX2RTZJC-CHj8TnTB.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-Bb0MCaIO.js → chunk-KYZI473N-gqRLpJ4w.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-C7qGJrfV.js → chunk-L3YUKLVL-DnSMmNFC.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-BpS_PtKp.js → chunk-MX3YWQON-B6g1ZH9X.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-z_blpjxi.js → chunk-NQ4KR5QH-DX32345Y.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-nDhi_cVu.js → chunk-O4XLMI2P-Vp_V4P-b.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-BXhYx3nO.js → chunk-OZEHJAEY-lKq2SWjA.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-TF58UVMU.js → chunk-PQ6SQG4A-Bik13fTV.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-ek7k4QVB.js → chunk-PU5JKC2W-DD95Rx35.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-N3VXx1VH.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-CFwSJijQ.js → chunk-R5LLSJPH-dRhXRnrb.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-ByUrSRin.js → chunk-WL4C6EOR-B1iIvLOG.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-Djlmrely.js → chunk-XIRO2GV7-DZBoNl1_.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-BPQQBakK.js → chunk-XPW4576I-CgLyyW03.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-DxAOx4hG.js → chunk-XZSTWKYB-DjV8xl5A.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-rQG3QH5s.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-B_AWZsOP.js → cose-bilkent-S5V4N54A-BGNPFv3x.js} +1 -1
- package/dist/web/assets/{csv-preview-D2pJJj3K.js → csv-preview-CwQnOa3E.js} +2 -2
- package/dist/web/assets/{dagre-DHq9bhnd.js → dagre-CkhlMHnx.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-BdJr7Byp.js → dagre-KLK3FWXG-Cnp996VG.js} +1 -1
- package/dist/web/assets/database-CgTomMxt.js +1 -0
- package/dist/web/assets/{database-viewer-Camu01H4.js → database-viewer-C1UHSgft.js} +2 -2
- package/dist/web/assets/{diagram-E7M64L7V-_db4pBVA.js → diagram-E7M64L7V-BZF0tSOr.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-xKoeuiJx.js → diagram-IFDJBPK2-nUcO8sN8.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-C8tjJsev.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-BSh2z9Df.js → erDiagram-INFDFZHY-DSkriYZ9.js} +1 -1
- package/dist/web/assets/extension-webview-CHVVpV34.js +3 -0
- package/dist/web/assets/{flowDiagram-PKNHOUZH-oYaovqyp.js → flowDiagram-PKNHOUZH-CFYAfZBx.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-DmL26q2P.js → ganttDiagram-A5KZAMGK-KSn4XAU4.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-OkvBPi6H.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-CMoukSrY.js → gitGraphDiagram-K3NZZRJ6-BMgjjVys.js} +1 -1
- package/dist/web/assets/{graphlib-BcsNnGcW.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-bnrF3Qbc.js → isEmpty-BfLnxq-B.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-D05_LyL7.js → ishikawaDiagram-PHBUUO56-CiVEvp8o.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-B_L20qMe.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-CZ535BbZ.js → kanban-definition-K7BYSVSG-miB0-_Zq.js} +1 -1
- package/dist/web/assets/keybindings-store-BQxgPV5o.js +1 -0
- package/dist/web/assets/{line-CVvo3dRu.js → line-CSuSrJ9J.js} +1 -1
- package/dist/web/assets/{linear-DP4mkX3m.js → linear-DFN_MPsw.js} +1 -1
- package/dist/web/assets/markdown-renderer-CRy8xw2B.js +306 -0
- package/dist/web/assets/{mermaid-parser.core-C7UwoIh6.js → mermaid-parser.core-CFdP1Z5_.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-x0MTutJp.js → mindmap-definition-YRQLILUH-pYPWwASE.js} +1 -1
- package/dist/web/assets/{ordinal-_K3x1fkz.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-C1Gjrtzy.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-BQdPMowm.js → postgres-viewer-BcVjCAl4.js} +3 -3
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-C8bzJCjQ.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-pQyah6WB.js → requirementDiagram-Z7DCOOCP-CuiiuGS9.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-T6RgG-N8.js → sankeyDiagram-WA2Y5GQK-BbRmhv0t.js} +1 -1
- package/dist/web/assets/scroll-area-BpXCNme3.js +1 -0
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-BQDJ4CVs.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-CY61vWBg.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-66vhiIuk.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-TIJmxHl6.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-DwZqB3nn.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-BHn-LEm7.js → use-monaco-theme-CPaeSMAA.js} +1 -1
- package/dist/web/assets/{vennDiagram-LZ73GAT5-s9Z71fz-.js → vennDiagram-LZ73GAT5-C-rkIUbo.js} +1 -1
- package/dist/web/assets/x-Dw3TjeY_.js +1 -0
- package/dist/web/assets/{xychartDiagram-JWTSCODW-DRa_TH4B.js → xychartDiagram-JWTSCODW-CtpjAakO.js} +1 -1
- package/dist/web/index.html +18 -12
- 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 +9 -3
- 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/index.ts +1 -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 +83 -8
- package/src/web/components/editor/save-as-dialog.tsx +75 -0
- package/src/web/components/extensions/extension-webview.tsx +111 -12
- package/src/web/components/layout/command-palette.tsx +43 -17
- package/src/web/components/layout/draggable-tab.tsx +120 -67
- package/src/web/components/layout/editor-panel.tsx +15 -4
- package/src/web/components/layout/mobile-nav.tsx +74 -7
- package/src/web/components/layout/tab-bar.tsx +76 -4
- package/src/web/components/layout/tab-content.tsx +12 -5
- package/src/web/components/layout/upgrade-banner.tsx +3 -0
- package/src/web/components/settings/keyboard-shortcuts-section.tsx +46 -1
- package/src/web/components/shared/markdown-code-block.tsx +142 -0
- package/src/web/components/shared/markdown-context.ts +20 -0
- package/src/web/components/shared/markdown-renderer.tsx +113 -288
- package/src/web/hooks/use-extension-ws.ts +22 -4
- package/src/web/hooks/use-global-keybindings.ts +31 -2
- package/src/web/hooks/use-url-sync.ts +8 -3
- package/src/web/main.tsx +1 -0
- package/src/web/stores/keybindings-store.ts +3 -3
- package/src/web/stores/panel-store.ts +2 -2
- package/src/web/stores/panel-utils.ts +17 -2
- package/src/web/stores/tab-store.ts +17 -1
- package/src/web/styles/globals.css +6 -0
- package/.opencode/.env.example +0 -98
- package/.opencode/skills/ads-management/scripts/.env.example +0 -13
- package/.opencode/skills/ai-multimodal/.env.example +0 -230
- package/.opencode/skills/cip-design/.env.example +0 -6
- package/.opencode/skills/devops/.env.example +0 -76
- package/.opencode/skills/docs-seeker/.env.example +0 -15
- package/.opencode/skills/elevenlabs/.env.example +0 -3
- package/.opencode/skills/marketing-dashboard/.env.example +0 -15
- package/.opencode/skills/marketing-dashboard/app/.env.example +0 -2
- package/.opencode/skills/marketing-dashboard/server/.env.example +0 -2
- package/.opencode/skills/mcp-management/scripts/dist/analyze-tools.js +0 -70
- package/.opencode/skills/mcp-management/scripts/dist/cli.js +0 -160
- package/.opencode/skills/mcp-management/scripts/dist/mcp-client.js +0 -183
- package/.opencode/skills/payment-integration/scripts/.env.example +0 -20
- package/.opencode/skills/sequential-thinking/.env.example +0 -8
- package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +0 -1
- package/dist/web/assets/arrow-up-BYhx9ckd.js +0 -1
- package/dist/web/assets/channel-By7bn0Yq.js +0 -1
- package/dist/web/assets/chat-tab-CT2XUgsc.js +0 -10
- package/dist/web/assets/chevron-right-4zq1jPv6.js +0 -1
- package/dist/web/assets/chunk-GLR3WWYH-DKikpoJM.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-C7vxA5i9.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-BA8Nj-_C.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-DjYu-6mn.js +0 -1
- package/dist/web/assets/clone-LRxlvnMj.js +0 -1
- package/dist/web/assets/code-editor-DQiPtcNd.js +0 -8
- package/dist/web/assets/columns-2-BoZAN-iw.js +0 -1
- package/dist/web/assets/createLucideIcon-PuMiQgHl.js +0 -1
- package/dist/web/assets/diff-viewer-CTwcVIP_.js +0 -4
- package/dist/web/assets/dist-DIV6WgAG.js +0 -41
- package/dist/web/assets/extension-webview-pU1xJyoc.js +0 -3
- package/dist/web/assets/git-graph-BnFbmpom.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +0 -1
- package/dist/web/assets/index-CP9KnaGh.js +0 -30
- package/dist/web/assets/index-Cxz7oGXY.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-DWwumDkq.js +0 -2
- package/dist/web/assets/jsx-runtime-kMwlnEGE.js +0 -1
- package/dist/web/assets/keybindings-store-DdhEeehv.js +0 -1
- package/dist/web/assets/markdown-renderer-BjYurPV4.js +0 -326
- package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +0 -1
- package/dist/web/assets/port-forwarding-tab-Bgr8dmsw.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +0 -1
- package/dist/web/assets/settings-tab-BNoboN6E.js +0 -1
- package/dist/web/assets/sqlite-viewer-srSbGg1D.js +0 -1
- package/dist/web/assets/square-oPKIkJiw.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-BGVqj_g9.js +0 -1
- package/dist/web/assets/table-DFevCOMd.js +0 -1
- package/dist/web/assets/tag-CXMT0QB6.js +0 -1
- package/dist/web/assets/text-wrap-BWNOVswA.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +0 -1
- package/dist/web/assets/x-D2_KzIET.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-BfBM3I7n.js → api-client-BvxmRZUi.js} +0 -0
- /package/dist/web/assets/{array-B9UHiPd-.js → array-BFDiaBgf.js} +0 -0
- /package/dist/web/assets/{csv-parser-CNNw2RVA.js → csv-parser-i7fjqP2H.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-BW-DbntU.js → cytoscape.esm-C8i2jUzT.js} +0 -0
- /package/dist/web/assets/{defaultLocale-5eAKkKJC.js → defaultLocale-ZeknFqNe.js} +0 -0
- /package/dist/web/assets/{dist-CSJdAyA9.js → dist-DZmJeHOA.js} +0 -0
- /package/dist/web/assets/{init-DlZdxViB.js → init-0VJVrkRJ.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-B_v2FtYn.js → isArrayLikeObject-ClzWCpcm.js} +0 -0
- /package/dist/web/assets/{katex-Bqvo_ZG0.js → katex-DR0kdMDv.js} +0 -0
- /package/dist/web/assets/{lib-DurwGtQO.js → lib-CeBVkQ-7.js} +0 -0
- /package/dist/web/assets/{math-069Z4SuC.js → math-CRc16Nj6.js} +0 -0
- /package/dist/web/assets/{path-6uRLdFF7.js → path-INs8XTPH.js} +0 -0
- /package/dist/web/assets/{preload-helper-Bf_JiD2A.js → preload-helper-mr3rCizq.js} +0 -0
- /package/dist/web/assets/{react-SKk5z-bm.js → react-0tkk-ztn.js} +0 -0
- /package/dist/web/assets/{rough.esm-JX0wREDd.js → rough.esm-eLccZ4OJ.js} +0 -0
- /package/dist/web/assets/{sql-completion-provider-DM9Qov6L.js → sql-completion-provider-B8uUWWej.js} +0 -0
- /package/dist/web/assets/{src-BqX54PbV.js → src-CqyWLlNZ.js} +0 -0
- /package/dist/web/assets/{utils-BNytJOb1.js → utils-DX8jb5qv.js} +0 -0
|
@@ -21,11 +21,13 @@ export {
|
|
|
21
21
|
type RpcClient,
|
|
22
22
|
} from "./types.ts";
|
|
23
23
|
export { type ExtensionContext, Memento } from "./context.ts";
|
|
24
|
+
export { ProcessService, type SpawnResult, type SpawnOptions } from "./process.ts";
|
|
24
25
|
|
|
25
26
|
// Service imports
|
|
26
27
|
import { CommandService } from "./commands.ts";
|
|
27
28
|
import { WindowService } from "./window.ts";
|
|
28
29
|
import { WorkspaceService } from "./workspace.ts";
|
|
30
|
+
import { ProcessService } from "./process.ts";
|
|
29
31
|
import { createExtensionContext, type ExtensionContext } from "./context.ts";
|
|
30
32
|
import { createEnvNamespace } from "./env.ts";
|
|
31
33
|
import { createNotSupported } from "./not-supported.ts";
|
|
@@ -55,12 +57,14 @@ export function createVscodeCompat(options: CreateVscodeCompatOptions) {
|
|
|
55
57
|
const commands = new CommandService(rpc, extensionId);
|
|
56
58
|
const window = new WindowService(rpc, extensionId);
|
|
57
59
|
const workspace = new WorkspaceService(rpc, extensionId);
|
|
60
|
+
const process = new ProcessService(rpc);
|
|
58
61
|
|
|
59
62
|
return {
|
|
60
63
|
// Active API namespaces
|
|
61
64
|
commands,
|
|
62
65
|
window,
|
|
63
66
|
workspace,
|
|
67
|
+
process,
|
|
64
68
|
env: createEnvNamespace(options.appName ?? "PPM", options.machineId ?? "ppm-local"),
|
|
65
69
|
|
|
66
70
|
// Classes & utilities
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RpcClient } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export interface SpawnResult {
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
exitCode: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SpawnOptions {
|
|
10
|
+
timeout?: number;
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Process namespace — spawn subprocesses via RPC to main process */
|
|
15
|
+
export class ProcessService {
|
|
16
|
+
private rpc: RpcClient;
|
|
17
|
+
|
|
18
|
+
constructor(rpc: RpcClient) {
|
|
19
|
+
this.rpc = rpc;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async spawn(cmd: string, args: string[], cwd: string, options?: SpawnOptions): Promise<SpawnResult> {
|
|
23
|
+
return this.rpc.request<SpawnResult>("process:spawn", cmd, args, cwd, options);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -184,6 +184,16 @@ export class WindowService {
|
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
// --- Open PPM Tab ---
|
|
188
|
+
|
|
189
|
+
async openTab(tabType: string, title: string, projectId: string | null, metadata?: Record<string, unknown>): Promise<void> {
|
|
190
|
+
await this.rpc.request("window:openTab", tabType, title, projectId, metadata);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async switchProject(projectName: string): Promise<void> {
|
|
194
|
+
await this.rpc.request("window:switchProject", projectName);
|
|
195
|
+
}
|
|
196
|
+
|
|
187
197
|
// --- Webview Panel ---
|
|
188
198
|
|
|
189
199
|
createWebviewPanel(viewType: string, title: string, showOptions: ViewColumn): unknown {
|
|
@@ -63,6 +63,7 @@ export function registerExtCommands(program: Command): void {
|
|
|
63
63
|
.description("List installed extensions")
|
|
64
64
|
.action(async () => {
|
|
65
65
|
const { extensionService } = await import("../../services/extension.service.ts");
|
|
66
|
+
await extensionService.discover(); // populate bundledIds for source column
|
|
66
67
|
const extensions = extensionService.list();
|
|
67
68
|
if (extensions.length === 0) {
|
|
68
69
|
console.log(`${C.dim}No extensions installed.${C.reset}`);
|
|
@@ -71,10 +72,11 @@ export function registerExtCommands(program: Command): void {
|
|
|
71
72
|
const rows = extensions.map((e) => [
|
|
72
73
|
e.id,
|
|
73
74
|
e.version,
|
|
75
|
+
extensionService.isBundled(e.id) ? `${C.cyan}bundled${C.reset}` : "user",
|
|
74
76
|
e.enabled ? `${C.green}enabled${C.reset}` : `${C.dim}disabled${C.reset}`,
|
|
75
77
|
e.activated ? `${C.green}active${C.reset}` : `${C.dim}inactive${C.reset}`,
|
|
76
78
|
]);
|
|
77
|
-
printTable(["ID", "Version", "Enabled", "Status"], rows);
|
|
79
|
+
printTable(["ID", "Version", "Source", "Enabled", "Status"], rows);
|
|
78
80
|
});
|
|
79
81
|
|
|
80
82
|
ext
|
package/src/server/index.ts
CHANGED
|
@@ -535,7 +535,7 @@ if (process.argv.includes("__serve__")) {
|
|
|
535
535
|
|
|
536
536
|
if (url.pathname.startsWith("/ws/project/")) {
|
|
537
537
|
const parts = url.pathname.split("/");
|
|
538
|
-
const projectName = parts[3] ?? "";
|
|
538
|
+
const projectName = decodeURIComponent(parts[3] ?? "");
|
|
539
539
|
const wsType = parts[4] ?? "";
|
|
540
540
|
const id = parts[5] ?? "";
|
|
541
541
|
|
|
@@ -80,9 +80,13 @@ async function handleMessage(ws: ExtWsSocket, raw: string | Buffer): Promise<voi
|
|
|
80
80
|
case "command:execute": {
|
|
81
81
|
try {
|
|
82
82
|
const { extensionService } = await import("../../services/extension.service.ts");
|
|
83
|
-
// Forward to extension host worker via RPC
|
|
84
83
|
if (extensionService["rpc"]) {
|
|
85
|
-
await extensionService["rpc"].sendRequest
|
|
84
|
+
const result = await extensionService["rpc"].sendRequest<{ ok: boolean; error?: string }>(
|
|
85
|
+
"ext:command:execute", msg.command, ...(msg.args ?? []),
|
|
86
|
+
);
|
|
87
|
+
if (!result?.ok) {
|
|
88
|
+
console.error(`[ExtWS] command:execute failed: ${result?.error ?? "unknown"}`);
|
|
89
|
+
}
|
|
86
90
|
}
|
|
87
91
|
} catch (e) {
|
|
88
92
|
console.error(`[ExtWS] command:execute error:`, e);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionContributes, ContributedCommand, ContributedView, ContributedMenu } from "../types/extension.ts";
|
|
1
|
+
import type { ExtensionContributes, ContributedCommand, ContributedView, ContributedMenu, ContributedKeybinding } from "../types/extension.ts";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* In-memory registry of all contribution points from enabled extensions.
|
|
@@ -9,6 +9,7 @@ class ContributionRegistry {
|
|
|
9
9
|
private views = new Map<string, Map<string, ContributedView & { extId: string }>>();
|
|
10
10
|
private configs = new Map<string, Record<string, unknown>>();
|
|
11
11
|
private menus = new Map<string, Array<ContributedMenu & { extId: string }>>();
|
|
12
|
+
private keybindings: Array<ContributedKeybinding & { extId: string }> = [];
|
|
12
13
|
|
|
13
14
|
register(extId: string, contributes: ExtensionContributes): void {
|
|
14
15
|
if (contributes.commands) {
|
|
@@ -37,6 +38,11 @@ class ContributionRegistry {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
}
|
|
41
|
+
if (contributes.keybindings) {
|
|
42
|
+
for (const kb of contributes.keybindings) {
|
|
43
|
+
this.keybindings.push({ ...kb, extId });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
unregister(extId: string): void {
|
|
@@ -51,6 +57,7 @@ class ContributionRegistry {
|
|
|
51
57
|
for (const [location, items] of this.menus) {
|
|
52
58
|
this.menus.set(location, items.filter((m) => m.extId !== extId));
|
|
53
59
|
}
|
|
60
|
+
this.keybindings = this.keybindings.filter((kb) => kb.extId !== extId);
|
|
54
61
|
this.configs.delete(extId);
|
|
55
62
|
}
|
|
56
63
|
|
|
@@ -73,6 +80,10 @@ class ContributionRegistry {
|
|
|
73
80
|
return [...this.views.keys()];
|
|
74
81
|
}
|
|
75
82
|
|
|
83
|
+
getKeybindings(): Array<ContributedKeybinding & { extId: string }> {
|
|
84
|
+
return [...this.keybindings];
|
|
85
|
+
}
|
|
86
|
+
|
|
76
87
|
getConfiguration(extId?: string): Record<string, Record<string, unknown>> {
|
|
77
88
|
if (extId) {
|
|
78
89
|
const cfg = this.configs.get(extId);
|
|
@@ -95,6 +106,7 @@ class ContributionRegistry {
|
|
|
95
106
|
commands: this.getCommands(),
|
|
96
107
|
views: viewsByLocation,
|
|
97
108
|
menus: menusByLocation,
|
|
109
|
+
keybindings: this.getKeybindings(),
|
|
98
110
|
configuration: this.getConfiguration(),
|
|
99
111
|
};
|
|
100
112
|
}
|
|
@@ -104,6 +116,7 @@ class ContributionRegistry {
|
|
|
104
116
|
this.views.clear();
|
|
105
117
|
this.configs.clear();
|
|
106
118
|
this.menus.clear();
|
|
119
|
+
this.keybindings = [];
|
|
107
120
|
}
|
|
108
121
|
}
|
|
109
122
|
|
|
@@ -101,13 +101,17 @@ rpc.onRequest("ext:deactivate", async (params) => {
|
|
|
101
101
|
|
|
102
102
|
rpc.onRequest("ext:command:execute", async (params) => {
|
|
103
103
|
const [command, ...args] = params as [string, ...unknown[]];
|
|
104
|
-
for (const [, ext] of activeExtensions) {
|
|
104
|
+
for (const [extId, ext] of activeExtensions) {
|
|
105
105
|
if (ext.commands) {
|
|
106
|
+
const hasLocal = (ext.commands as any).localHandlers?.has(command);
|
|
107
|
+
if (!hasLocal) continue;
|
|
106
108
|
try {
|
|
107
109
|
const result = await (ext.commands as any).executeCommand(command, ...args);
|
|
108
110
|
return { ok: true, result };
|
|
109
|
-
} catch {
|
|
110
|
-
|
|
111
|
+
} catch (e) {
|
|
112
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
113
|
+
console.error(`[ExtHost] Command "${command}" in ${extId} threw:`, msg);
|
|
114
|
+
return { ok: false, error: msg };
|
|
111
115
|
}
|
|
112
116
|
}
|
|
113
117
|
}
|
|
@@ -38,7 +38,7 @@ export function readManifestAt(dir: string): ExtensionManifest | null {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/** Scan extensions directory for all valid manifests */
|
|
41
|
+
/** Scan extensions directory (node_modules) for all valid manifests */
|
|
42
42
|
export async function discoverManifests(extensionsDir: string): Promise<ExtensionManifest[]> {
|
|
43
43
|
const manifests: ExtensionManifest[] = [];
|
|
44
44
|
if (!existsSync(extensionsDir)) return manifests;
|
|
@@ -63,3 +63,20 @@ export async function discoverManifests(extensionsDir: string): Promise<Extensio
|
|
|
63
63
|
}
|
|
64
64
|
return manifests;
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
export type BundledManifest = ExtensionManifest & { _dir: string };
|
|
68
|
+
|
|
69
|
+
/** Scan packages directory for bundled extensions (ext-* dirs) */
|
|
70
|
+
export async function discoverBundledManifests(packagesDir: string): Promise<BundledManifest[]> {
|
|
71
|
+
const manifests: BundledManifest[] = [];
|
|
72
|
+
if (!existsSync(packagesDir)) return manifests;
|
|
73
|
+
|
|
74
|
+
const entries = await readdir(packagesDir, { withFileTypes: true });
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
if (!entry.isDirectory() || !entry.name.startsWith("ext-")) continue;
|
|
77
|
+
const dir = resolve(packagesDir, entry.name);
|
|
78
|
+
const manifest = readManifestAt(dir);
|
|
79
|
+
if (manifest) manifests.push({ ...manifest, _dir: dir });
|
|
80
|
+
}
|
|
81
|
+
return manifests;
|
|
82
|
+
}
|
|
@@ -112,6 +112,22 @@ export function registerVscodeCompatHandlers(rpc: RpcChannel): void {
|
|
|
112
112
|
return { ok: true };
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
+
// --- open PPM tab (generic, any extension can use) ---
|
|
116
|
+
rpc.onRequest("window:openTab", async (params) => {
|
|
117
|
+
const [tabType, title, projectId, metadata] = params as [
|
|
118
|
+
string, string, string | null, Record<string, unknown> | undefined,
|
|
119
|
+
];
|
|
120
|
+
broadcastExtMsg({ type: "tab:open", tabType, title, projectId, closable: true, metadata });
|
|
121
|
+
return { ok: true };
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// --- switch PPM project ---
|
|
125
|
+
rpc.onRequest("window:switchProject", async (params) => {
|
|
126
|
+
const [projectName] = params as [string];
|
|
127
|
+
broadcastExtMsg({ type: "project:switch", projectName });
|
|
128
|
+
return { ok: true };
|
|
129
|
+
});
|
|
130
|
+
|
|
115
131
|
// --- tree views (forwarded to browser via WS bridge) ---
|
|
116
132
|
rpc.onRequest("window:tree:update", async (params) => {
|
|
117
133
|
const [viewId, items] = params as [string, unknown[]];
|
|
@@ -155,9 +171,11 @@ export function registerVscodeCompatHandlers(rpc: RpcChannel): void {
|
|
|
155
171
|
async function assertSafePath(filePath: string): Promise<string> {
|
|
156
172
|
const { resolve, relative } = await import("node:path");
|
|
157
173
|
const resolved = resolve(filePath);
|
|
158
|
-
// Allow: CWD
|
|
174
|
+
// Allow: CWD, ~/.ppm/extensions/, and all registered project paths
|
|
159
175
|
const { getPpmDir } = await import("./ppm-dir.ts");
|
|
160
|
-
const
|
|
176
|
+
const { configService } = await import("./config.service.ts");
|
|
177
|
+
const projectPaths = configService.get("projects").map((p: { path: string }) => resolve(p.path));
|
|
178
|
+
const allowedRoots = [resolve(process.cwd()), resolve(getPpmDir(), "extensions"), ...projectPaths];
|
|
161
179
|
const isSafe = allowedRoots.some((root) => {
|
|
162
180
|
const rel = relative(root, resolved);
|
|
163
181
|
return !rel.startsWith("..") && !rel.startsWith("/");
|
|
@@ -221,6 +239,54 @@ export function registerVscodeCompatHandlers(rpc: RpcChannel): void {
|
|
|
221
239
|
}
|
|
222
240
|
return results;
|
|
223
241
|
});
|
|
242
|
+
|
|
243
|
+
// --- process spawn (for extensions needing subprocess access) ---
|
|
244
|
+
|
|
245
|
+
const ALLOWED_SPAWN_COMMANDS = new Set(["git", "node", "bun", "npx", "sqlite3"]);
|
|
246
|
+
const BLOCKED_ENV_KEYS = new Set(["PATH", "HOME", "LD_PRELOAD", "DYLD_INSERT_LIBRARIES", "LD_LIBRARY_PATH"]);
|
|
247
|
+
|
|
248
|
+
rpc.onRequest("process:spawn", async (params) => {
|
|
249
|
+
const [cmd, args, cwd, options] = params as [string, string[], string, { timeout?: number; env?: Record<string, string> }?];
|
|
250
|
+
|
|
251
|
+
// Security: command allowlist
|
|
252
|
+
const baseName = cmd.split("/").pop() || cmd;
|
|
253
|
+
if (!ALLOWED_SPAWN_COMMANDS.has(baseName)) {
|
|
254
|
+
throw new Error(`process:spawn: command "${cmd}" not allowed. Allowed: ${[...ALLOWED_SPAWN_COMMANDS].join(", ")}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Security: CWD must be within allowed roots
|
|
258
|
+
const safeCwd = await assertSafePath(cwd);
|
|
259
|
+
|
|
260
|
+
// Security: block dangerous env overrides
|
|
261
|
+
const safeEnv = { ...process.env };
|
|
262
|
+
if (options?.env) {
|
|
263
|
+
for (const [key, val] of Object.entries(options.env)) {
|
|
264
|
+
if (!BLOCKED_ENV_KEYS.has(key)) safeEnv[key] = val;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const timeout = options?.timeout ?? 30_000;
|
|
269
|
+
const proc = Bun.spawn([cmd, ...args], {
|
|
270
|
+
cwd: safeCwd,
|
|
271
|
+
stdout: "pipe",
|
|
272
|
+
stderr: "pipe",
|
|
273
|
+
env: safeEnv,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const timer = setTimeout(() => { try { proc.kill(); } catch {} }, timeout);
|
|
277
|
+
try {
|
|
278
|
+
const [stdout, stderr] = await Promise.all([
|
|
279
|
+
new Response(proc.stdout).text(),
|
|
280
|
+
new Response(proc.stderr).text(),
|
|
281
|
+
]);
|
|
282
|
+
const exitCode = await proc.exited;
|
|
283
|
+
clearTimeout(timer);
|
|
284
|
+
return { stdout, stderr, exitCode };
|
|
285
|
+
} catch (e) {
|
|
286
|
+
clearTimeout(timer);
|
|
287
|
+
throw new Error(`process:spawn failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
224
290
|
}
|
|
225
291
|
|
|
226
292
|
/** Get a nested value from an object by dot-separated key */
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import type { ExtensionManifest, ExtensionInfo, RpcMessage } from "../types/extension.ts";
|
|
4
|
-
import { getExtensions, getExtensionById, insertExtension, getExtensionStorage, setExtensionStorageValue } from "./db.service.ts";
|
|
4
|
+
import { getExtensions, getExtensionById, insertExtension, updateExtension, getExtensionStorage, setExtensionStorageValue } from "./db.service.ts";
|
|
5
5
|
import { contributionRegistry } from "./contribution-registry.ts";
|
|
6
6
|
import { RpcChannel } from "./extension-rpc.ts";
|
|
7
|
-
import { parseManifest, discoverManifests } from "./extension-manifest.ts";
|
|
7
|
+
import { parseManifest, discoverManifests, discoverBundledManifests } from "./extension-manifest.ts";
|
|
8
8
|
import { installExtension, removeExtension, devLinkExtension, ensureExtensionsDir } from "./extension-installer.ts";
|
|
9
9
|
import { registerVscodeCompatHandlers } from "./extension-rpc-handlers.ts";
|
|
10
10
|
import { getPpmDir } from "./ppm-dir.ts";
|
|
@@ -15,6 +15,8 @@ class ExtensionService {
|
|
|
15
15
|
private activatedIds = new Set<string>();
|
|
16
16
|
private workerReady = false;
|
|
17
17
|
private installing = new Set<string>();
|
|
18
|
+
private extensionPaths = new Map<string, string>();
|
|
19
|
+
private bundledIds = new Set<string>();
|
|
18
20
|
|
|
19
21
|
// --- Worker lifecycle ---
|
|
20
22
|
|
|
@@ -54,6 +56,8 @@ class ExtensionService {
|
|
|
54
56
|
if (this.worker) { this.worker.terminate(); this.worker = null; }
|
|
55
57
|
this.workerReady = false;
|
|
56
58
|
this.activatedIds.clear();
|
|
59
|
+
this.extensionPaths.clear();
|
|
60
|
+
this.bundledIds.clear();
|
|
57
61
|
contributionRegistry.clear();
|
|
58
62
|
}
|
|
59
63
|
|
|
@@ -65,7 +69,29 @@ class ExtensionService {
|
|
|
65
69
|
|
|
66
70
|
async discover(): Promise<ExtensionManifest[]> {
|
|
67
71
|
ensureExtensionsDir(resolve(getPpmDir(), "extensions"));
|
|
68
|
-
|
|
72
|
+
|
|
73
|
+
// Discover bundled extensions from packages/ext-*
|
|
74
|
+
const bundledDir = resolve(import.meta.dir, "../../packages");
|
|
75
|
+
const bundled = await discoverBundledManifests(bundledDir);
|
|
76
|
+
for (const m of bundled) {
|
|
77
|
+
this.extensionPaths.set(m.id, m._dir);
|
|
78
|
+
this.bundledIds.add(m.id);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Discover user-installed extensions
|
|
82
|
+
const userExtDir = resolve(getPpmDir(), "extensions");
|
|
83
|
+
const userManifests = await discoverManifests(userExtDir);
|
|
84
|
+
for (const m of userManifests) {
|
|
85
|
+
this.extensionPaths.set(m.id, resolve(userExtDir, "node_modules", m.id));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Merge: user overrides bundled if same id (strip _dir to avoid leaking paths)
|
|
89
|
+
const byId = new Map(bundled.map((m) => {
|
|
90
|
+
const { _dir, ...manifest } = m;
|
|
91
|
+
return [m.id, manifest as ExtensionManifest];
|
|
92
|
+
}));
|
|
93
|
+
for (const m of userManifests) byId.set(m.id, m);
|
|
94
|
+
return [...byId.values()];
|
|
69
95
|
}
|
|
70
96
|
|
|
71
97
|
async install(name: string): Promise<ExtensionManifest> {
|
|
@@ -79,6 +105,9 @@ class ExtensionService {
|
|
|
79
105
|
}
|
|
80
106
|
|
|
81
107
|
async remove(id: string): Promise<void> {
|
|
108
|
+
if (this.bundledIds.has(id)) {
|
|
109
|
+
throw new Error(`Cannot remove bundled extension "${id}". Use 'ppm ext disable ${id}' instead.`);
|
|
110
|
+
}
|
|
82
111
|
if (this.activatedIds.has(id)) await this.deactivate(id);
|
|
83
112
|
await removeExtension(id, resolve(getPpmDir(), "extensions"));
|
|
84
113
|
contributionRegistry.unregister(id);
|
|
@@ -92,7 +121,8 @@ class ExtensionService {
|
|
|
92
121
|
if (!row.enabled) throw new Error(`Extension ${id} is disabled`);
|
|
93
122
|
|
|
94
123
|
const manifest: ExtensionManifest = JSON.parse(row.manifest);
|
|
95
|
-
const extDir =
|
|
124
|
+
const extDir = this.extensionPaths.get(id)
|
|
125
|
+
?? resolve(resolve(getPpmDir(), "extensions"), "node_modules", id);
|
|
96
126
|
const entryPath = resolve(extDir, manifest.main);
|
|
97
127
|
if (!existsSync(entryPath)) throw new Error(`Entry point not found: ${entryPath}`);
|
|
98
128
|
|
|
@@ -166,7 +196,6 @@ class ExtensionService {
|
|
|
166
196
|
async setEnabled(id: string, enabled: boolean): Promise<void> {
|
|
167
197
|
const row = getExtensionById(id);
|
|
168
198
|
if (!row) throw new Error(`Extension ${id} not found`);
|
|
169
|
-
const { updateExtension } = await import("./db.service.ts");
|
|
170
199
|
updateExtension(id, { enabled: enabled ? 1 : 0 });
|
|
171
200
|
if (enabled && !this.activatedIds.has(id)) await this.activate(id);
|
|
172
201
|
else if (!enabled && this.activatedIds.has(id)) await this.deactivate(id);
|
|
@@ -187,12 +216,22 @@ class ExtensionService {
|
|
|
187
216
|
ensureExtensionsDir(resolve(getPpmDir(), "extensions"));
|
|
188
217
|
const manifests = await this.discover();
|
|
189
218
|
for (const m of manifests) {
|
|
190
|
-
|
|
219
|
+
const existing = getExtensionById(m.id);
|
|
220
|
+
if (!existing) {
|
|
191
221
|
insertExtension({
|
|
192
222
|
id: m.id, version: m.version,
|
|
193
223
|
display_name: m.displayName ?? null, description: m.description ?? null,
|
|
194
224
|
icon: m.icon ?? null, enabled: 1, manifest: JSON.stringify(m),
|
|
195
225
|
});
|
|
226
|
+
} else {
|
|
227
|
+
// Always sync manifest from disk so new contributes (keybindings, etc.) are picked up
|
|
228
|
+
updateExtension(m.id, {
|
|
229
|
+
version: m.version,
|
|
230
|
+
display_name: m.displayName ?? null,
|
|
231
|
+
description: m.description ?? null,
|
|
232
|
+
icon: m.icon ?? null,
|
|
233
|
+
manifest: JSON.stringify(m),
|
|
234
|
+
});
|
|
196
235
|
}
|
|
197
236
|
}
|
|
198
237
|
for (const row of getExtensions()) {
|
|
@@ -211,6 +250,7 @@ class ExtensionService {
|
|
|
211
250
|
}
|
|
212
251
|
|
|
213
252
|
isActivated(id: string): boolean { return this.activatedIds.has(id); }
|
|
253
|
+
isBundled(id: string): boolean { return this.bundledIds.has(id); }
|
|
214
254
|
getExtensionsDir(): string { return resolve(getPpmDir(), "extensions"); }
|
|
215
255
|
|
|
216
256
|
/** Push current contributions to all connected browser clients */
|
|
@@ -49,6 +49,8 @@ export type ExtServerMsg =
|
|
|
49
49
|
| { type: "webview:html"; panelId: string; html: string }
|
|
50
50
|
| { type: "webview:dispose"; panelId: string }
|
|
51
51
|
| { type: "webview:postMessage"; panelId: string; message: unknown }
|
|
52
|
+
| { type: "tab:open"; tabType: string; title: string; projectId: string | null; closable?: boolean; metadata?: Record<string, unknown> }
|
|
53
|
+
| { type: "project:switch"; projectName: string }
|
|
52
54
|
| { type: "contributions:update"; contributions: ExtensionContributes };
|
|
53
55
|
|
|
54
56
|
// --- Client → Server messages ---
|
package/src/types/extension.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface ExtensionContributes {
|
|
|
23
23
|
views?: Record<string, ContributedView[]>;
|
|
24
24
|
configuration?: { properties?: Record<string, ConfigProperty> };
|
|
25
25
|
menus?: Record<string, ContributedMenu[]>;
|
|
26
|
+
keybindings?: ContributedKeybinding[];
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export interface ContributedCommand {
|
|
@@ -52,6 +53,13 @@ export interface ContributedMenu {
|
|
|
52
53
|
group?: string;
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
export interface ContributedKeybinding {
|
|
57
|
+
command: string;
|
|
58
|
+
key: string;
|
|
59
|
+
mac?: string;
|
|
60
|
+
when?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
/** Runtime extension info returned by API */
|
|
56
64
|
export interface ExtensionInfo {
|
|
57
65
|
id: string;
|
|
@@ -4,12 +4,14 @@ import type * as MonacoType from "monaco-editor";
|
|
|
4
4
|
import { MarkdownRenderer } from "@/components/shared/markdown-renderer";
|
|
5
5
|
import { api, projectUrl, getAuthToken } from "@/lib/api-client";
|
|
6
6
|
import { useTabStore } from "@/stores/tab-store";
|
|
7
|
+
import { usePanelStore } from "@/stores/panel-store";
|
|
7
8
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
8
9
|
import { basename } from "@/lib/utils";
|
|
9
10
|
import { useMonacoTheme } from "@/lib/use-monaco-theme";
|
|
10
11
|
import { Loader2, FileWarning, ExternalLink, Play, Database } from "lucide-react";
|
|
11
12
|
import { EditorBreadcrumb } from "./editor-breadcrumb";
|
|
12
13
|
import { EditorToolbar } from "./editor-toolbar";
|
|
14
|
+
import { SaveAsDialog } from "./save-as-dialog";
|
|
13
15
|
import { lazy, Suspense } from "react";
|
|
14
16
|
import { createSqlCompletionProvider, clearCompletionCache, type SchemaInfo } from "../database/sql-completion-provider";
|
|
15
17
|
import { useConnections, type Connection } from "../database/use-connections";
|
|
@@ -63,6 +65,10 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
63
65
|
const { wordWrap, toggleWordWrap } = useSettingsStore();
|
|
64
66
|
const monacoTheme = useMonacoTheme();
|
|
65
67
|
|
|
68
|
+
const isUntitled = metadata?.isUntitled === true;
|
|
69
|
+
const savedContent = metadata?.unsavedContent as string | undefined;
|
|
70
|
+
const [showSaveAs, setShowSaveAs] = useState(false);
|
|
71
|
+
|
|
66
72
|
const ownTab = tabs.find((t) => t.id === tabId);
|
|
67
73
|
const ext = filePath ? getFileExt(filePath) : "";
|
|
68
74
|
const isImage = IMAGE_EXTS.has(ext);
|
|
@@ -198,6 +204,13 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
198
204
|
// Load file content
|
|
199
205
|
useEffect(() => {
|
|
200
206
|
if (inlineContent != null) { setLoading(false); return; }
|
|
207
|
+
if (isUntitled) {
|
|
208
|
+
setContent(savedContent ?? "");
|
|
209
|
+
latestContentRef.current = savedContent ?? "";
|
|
210
|
+
setLoading(false);
|
|
211
|
+
if (savedContent) setUnsaved(true);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
201
214
|
if (!filePath) return;
|
|
202
215
|
if (!isExternalFile && !projectName) return;
|
|
203
216
|
if (isImage || isPdf) { setLoading(false); return; }
|
|
@@ -223,12 +236,14 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
223
236
|
});
|
|
224
237
|
|
|
225
238
|
return () => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current); };
|
|
226
|
-
}, [filePath, projectName, isImage, isPdf, isExternalFile]);
|
|
239
|
+
}, [filePath, projectName, isImage, isPdf, isExternalFile, isUntitled]);
|
|
227
240
|
|
|
228
241
|
// Update tab title unsaved indicator
|
|
229
242
|
useEffect(() => {
|
|
230
243
|
if (!ownTab) return;
|
|
231
|
-
const baseName =
|
|
244
|
+
const baseName = isUntitled
|
|
245
|
+
? `Untitled-${metadata?.untitledNumber ?? 1}`
|
|
246
|
+
: (filePath ? basename(filePath) : "Untitled");
|
|
232
247
|
const newTitle = unsaved ? `${baseName} \u25CF` : baseName;
|
|
233
248
|
if (ownTab.title !== newTitle) updateTab(ownTab.id, { title: newTitle });
|
|
234
249
|
}, [unsaved]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
@@ -255,9 +270,39 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
255
270
|
latestContentRef.current = val;
|
|
256
271
|
setUnsaved(true);
|
|
257
272
|
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
258
|
-
|
|
273
|
+
if (isUntitled) {
|
|
274
|
+
// Persist to metadata for localStorage survival
|
|
275
|
+
saveTimerRef.current = setTimeout(() => {
|
|
276
|
+
if (tabId) updateTab(tabId, { metadata: { ...metadata, unsavedContent: latestContentRef.current } });
|
|
277
|
+
}, 2000);
|
|
278
|
+
} else {
|
|
279
|
+
saveTimerRef.current = setTimeout(() => saveFile(latestContentRef.current), 1000);
|
|
280
|
+
}
|
|
259
281
|
}
|
|
260
282
|
|
|
283
|
+
// Save As completion — transitions untitled → saved file
|
|
284
|
+
const handleSaveAs = useCallback(async (targetPath: string, savedText: string) => {
|
|
285
|
+
try {
|
|
286
|
+
// Clear any pending metadata persistence timer to prevent race condition
|
|
287
|
+
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
288
|
+
await api.put("/api/fs/write", { path: targetPath, content: savedText });
|
|
289
|
+
if (tabId) {
|
|
290
|
+
// Close old untitled tab and open as proper file tab
|
|
291
|
+
const { closeTab, openTab } = usePanelStore.getState();
|
|
292
|
+
closeTab(tabId);
|
|
293
|
+
openTab({
|
|
294
|
+
type: "editor",
|
|
295
|
+
title: basename(targetPath),
|
|
296
|
+
projectId: null,
|
|
297
|
+
metadata: { filePath: targetPath },
|
|
298
|
+
closable: true,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
setUnsaved(false);
|
|
302
|
+
setShowSaveAs(false);
|
|
303
|
+
} catch { /* silent — user can retry */ }
|
|
304
|
+
}, [tabId]);
|
|
305
|
+
|
|
261
306
|
// Jump to line when metadata.lineNumber is set (e.g. from search panel)
|
|
262
307
|
const lineNumber = metadata?.lineNumber as number | undefined;
|
|
263
308
|
const handleEditorMount: OnMount = useCallback((editor, monaco) => {
|
|
@@ -270,6 +315,13 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
270
315
|
editor.focus();
|
|
271
316
|
}, 100);
|
|
272
317
|
}
|
|
318
|
+
// Ctrl+S → Save As for untitled tabs
|
|
319
|
+
if (isUntitled) {
|
|
320
|
+
editor.addCommand(
|
|
321
|
+
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
|
|
322
|
+
() => setShowSaveAs(true),
|
|
323
|
+
);
|
|
324
|
+
}
|
|
273
325
|
editor.addCommand(
|
|
274
326
|
monaco.KeyMod.Alt | monaco.KeyCode.KeyZ,
|
|
275
327
|
() => useSettingsStore.getState().toggleWordWrap(),
|
|
@@ -288,22 +340,28 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
288
340
|
);
|
|
289
341
|
}
|
|
290
342
|
|
|
291
|
-
// Register CodeLens for inline Run buttons on .sql files
|
|
343
|
+
// Register CodeLens for inline Run buttons on .sql files (scoped to this editor's model)
|
|
292
344
|
if (isSql) {
|
|
293
345
|
codeLensDisposable.current.forEach((d) => d.dispose());
|
|
294
346
|
codeLensDisposable.current = [];
|
|
295
347
|
|
|
348
|
+
const thisModel = editor.getModel();
|
|
296
349
|
const cmdId = editor.addCommand(0, (_accessor: unknown, sql: string) => {
|
|
297
350
|
if (sql) runSqlRef.current(sql);
|
|
298
351
|
});
|
|
299
352
|
|
|
300
|
-
if (cmdId) {
|
|
353
|
+
if (cmdId && thisModel) {
|
|
301
354
|
const provider = monaco.languages.registerCodeLensProvider("sql", {
|
|
302
355
|
provideCodeLenses: (model: MonacoType.editor.ITextModel) => {
|
|
356
|
+
// Only provide lenses for THIS editor's model, not all SQL models
|
|
357
|
+
if (model !== thisModel) return { lenses: [], dispose: () => {} };
|
|
358
|
+
|
|
303
359
|
const lenses: MonacoType.languages.CodeLens[] = [];
|
|
304
|
-
const
|
|
360
|
+
const text = model.getValue();
|
|
361
|
+
const lines = text.split("\n");
|
|
305
362
|
let stmtStartLine = -1;
|
|
306
363
|
let stmtLines: string[] = [];
|
|
364
|
+
let dollarBlock = false; // Track DO $$ ... $$ blocks
|
|
307
365
|
|
|
308
366
|
const addLens = (line: number, stmt: string) => {
|
|
309
367
|
const trimmed = stmt.trim();
|
|
@@ -322,7 +380,13 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
322
380
|
stmtLines = [];
|
|
323
381
|
}
|
|
324
382
|
stmtLines.push(lines[i]!);
|
|
325
|
-
|
|
383
|
+
|
|
384
|
+
// Detect $$ dollar-quoted block start/end
|
|
385
|
+
const dollarMatches = (trimmed.match(/\$\$/g) || []).length;
|
|
386
|
+
if (dollarMatches % 2 === 1) dollarBlock = !dollarBlock;
|
|
387
|
+
|
|
388
|
+
// Only split on ; when NOT inside a $$ block
|
|
389
|
+
if (!dollarBlock && trimmed.endsWith(";")) {
|
|
326
390
|
addLens(stmtStartLine, stmtLines.join("\n"));
|
|
327
391
|
stmtStartLine = -1;
|
|
328
392
|
stmtLines = [];
|
|
@@ -339,7 +403,7 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
339
403
|
}
|
|
340
404
|
}, [sqlSchemaInfo]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
341
405
|
|
|
342
|
-
if (!inlineContent && (!filePath || (!isExternalFile && !projectName))) {
|
|
406
|
+
if (!inlineContent && !isUntitled && (!filePath || (!isExternalFile && !projectName))) {
|
|
343
407
|
return (
|
|
344
408
|
<div className="flex items-center justify-center h-full text-text-secondary text-sm">
|
|
345
409
|
No file selected.
|
|
@@ -476,6 +540,17 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
476
540
|
/>
|
|
477
541
|
</div>
|
|
478
542
|
)}
|
|
543
|
+
|
|
544
|
+
{/* Save As dialog for untitled tabs */}
|
|
545
|
+
{showSaveAs && (
|
|
546
|
+
<SaveAsDialog
|
|
547
|
+
open={showSaveAs}
|
|
548
|
+
defaultName={`Untitled-${metadata?.untitledNumber ?? 1}`}
|
|
549
|
+
content={latestContentRef.current}
|
|
550
|
+
onSave={handleSaveAs}
|
|
551
|
+
onCancel={() => setShowSaveAs(false)}
|
|
552
|
+
/>
|
|
553
|
+
)}
|
|
479
554
|
</div>
|
|
480
555
|
);
|
|
481
556
|
}
|