@hienlh/ppm 0.9.84 → 0.9.85
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 +13 -0
- package/bun.lock +259 -9
- package/dist/web/assets/{_basePickBy-5PGDJbfF.js → _basePickBy-D-bUmjma.js} +1 -1
- package/dist/web/assets/{_baseUniq-BT4Ow4Kk.js → _baseUniq-BnXXIfRB.js} +1 -1
- package/dist/web/assets/ai-settings-section-D6d-RmR6.js +1 -0
- package/dist/web/assets/{api-settings-Bn-bIxD1.js → api-settings-Qi2xRiHa.js} +1 -1
- package/dist/web/assets/{arc-BAOivWpI.js → arc-DB9vXGzd.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DpVzOETR.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Z-4eN4za.js → architectureDiagram-2XIMDMQ5-BBV25747.js} +1 -1
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-BCLqzhuZ.js → blockDiagram-WCTKOSBZ-BOTnY2Lq.js} +1 -1
- package/dist/web/assets/{c4Diagram-IC4MRINW-0Vp0Jeas.js → c4Diagram-IC4MRINW-D7QAUdHD.js} +1 -1
- package/dist/web/assets/channel-Cgy1thYT.js +1 -0
- package/dist/web/assets/chat-tab-DXBb9Y3U.js +10 -0
- package/dist/web/assets/check-ePA3ZvK4.js +1 -0
- package/dist/web/assets/chevron-down-EQA06nR-.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-D4tOov49.js → chunk-4BX2VUAB-BnOVw77D.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-DJ6BynZ4.js → chunk-55IACEB6-BftA8DxR.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-CiyUJxNI.js → chunk-7E7YKBS2-B0vnP8v3.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-Dv-4cAYn.js → chunk-7R4GIKGN-Czlaj26D.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-D21mS_6G.js → chunk-C72U2L5F-DpEbDtMo.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-DzqmU2Z7.js → chunk-EGIJ26TM-BWXe6lkx.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-DXncblvW.js → chunk-FMBD7UC4-DspqhPfk.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-D-pKjlVd.js → chunk-GEFDOKGD-D6HHRbYk.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-CxUl1sdz.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-DN7ebS2Y.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-99JzIdPr.js → chunk-JSJVCQXG-BC8wnMwf.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-CRq1OBZv.js → chunk-KX2RTZJC-D3VDtyvX.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-Bb0MCaIO.js → chunk-KYZI473N-Z-NBw_HS.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-C7qGJrfV.js → chunk-L3YUKLVL--RGkEh__.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-BpS_PtKp.js → chunk-MX3YWQON-2B76t_Kx.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-z_blpjxi.js → chunk-NQ4KR5QH-BekY3tEi.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-nDhi_cVu.js → chunk-O4XLMI2P-2CJLfx_1.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-BXhYx3nO.js → chunk-OZEHJAEY-sug_L09P.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-TF58UVMU.js → chunk-PQ6SQG4A-_fwPRLQy.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-ek7k4QVB.js → chunk-PU5JKC2W-BUaTFJVQ.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-C4La7oLj.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-CFwSJijQ.js → chunk-R5LLSJPH-C37xW0vj.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-ByUrSRin.js → chunk-WL4C6EOR-CCkt_MT6.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-Djlmrely.js → chunk-XIRO2GV7-Dz2LBq7Y.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-BPQQBakK.js → chunk-XPW4576I-DenTbBuj.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-DxAOx4hG.js → chunk-XZSTWKYB-Dbp1nUSQ.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-rQG3QH5s.js → chunk-YBOYWFTD-3OTKowjE.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-C3IyfqG-.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-Dcvhz2pb.js +1 -0
- package/dist/web/assets/clone--C7Tby8z.js +1 -0
- package/dist/web/assets/code-editor-Cr7JrBKC.js +8 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-B_AWZsOP.js → cose-bilkent-S5V4N54A-MbmGZnt0.js} +1 -1
- package/dist/web/assets/{csv-preview-D2pJJj3K.js → csv-preview-uZ_7b8I7.js} +1 -1
- package/dist/web/assets/{dagre-DHq9bhnd.js → dagre-CPhI6v-K.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-BdJr7Byp.js → dagre-KLK3FWXG-CmSE-oNj.js} +1 -1
- package/dist/web/assets/database-D1ToEV9d.js +1 -0
- package/dist/web/assets/{database-viewer-Camu01H4.js → database-viewer-5xljX0JI.js} +2 -2
- package/dist/web/assets/{diagram-E7M64L7V-_db4pBVA.js → diagram-E7M64L7V-B5XG3ZT7.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-xKoeuiJx.js → diagram-IFDJBPK2-BsP248aX.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-C8tjJsev.js → diagram-P4PSJMXO-Cna3408N.js} +1 -1
- package/dist/web/assets/diff-viewer-BBr6e_gb.js +4 -0
- package/dist/web/assets/dist-KUoHa6tg.js +1 -0
- package/dist/web/assets/{erDiagram-INFDFZHY-BSh2z9Df.js → erDiagram-INFDFZHY-B7SgktiR.js} +1 -1
- package/dist/web/assets/{extension-webview-pU1xJyoc.js → extension-webview-B0klBip8.js} +1 -1
- package/dist/web/assets/eye-CNcBU6Tx.js +1 -0
- package/dist/web/assets/{flowDiagram-PKNHOUZH-oYaovqyp.js → flowDiagram-PKNHOUZH-FOYZZ1OB.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-DmL26q2P.js → ganttDiagram-A5KZAMGK-CnHVYh9v.js} +1 -1
- package/dist/web/assets/git-graph-CDiwGa0g.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-DcPyMEIJ.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-CMoukSrY.js → gitGraphDiagram-K3NZZRJ6-0G9XxZay.js} +1 -1
- package/dist/web/assets/{graphlib-BcsNnGcW.js → graphlib-CNiBwlg_.js} +1 -1
- package/dist/web/assets/index-CkaCzNgO.css +2 -0
- package/dist/web/assets/index-Ic5uTu20.js +26 -0
- package/dist/web/assets/info-3K5VOQVL-Dw4O15cw.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-DFhmsucr.js +2 -0
- package/dist/web/assets/input-CcbTF6ih.js +45 -0
- package/dist/web/assets/{isEmpty-bnrF3Qbc.js → isEmpty-CcCb5n2-.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-D05_LyL7.js → ishikawaDiagram-PHBUUO56-D4QCzh5J.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-B_L20qMe.js → journeyDiagram-4ABVD52K-CnHYNfKW.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-CZ535BbZ.js → kanban-definition-K7BYSVSG-Bh_g3EVu.js} +1 -1
- package/dist/web/assets/keybindings-store-CxE6BlG2.js +1 -0
- package/dist/web/assets/{line-CVvo3dRu.js → line-6d3eBADm.js} +1 -1
- package/dist/web/assets/{linear-DP4mkX3m.js → linear-cA_2lQy7.js} +1 -1
- package/dist/web/assets/markdown-renderer-CZ07F7T6.js +306 -0
- package/dist/web/assets/{mermaid-parser.core-C7UwoIh6.js → mermaid-parser.core-C3kd7JXM.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-x0MTutJp.js → mindmap-definition-YRQLILUH-CYiUwhr_.js} +1 -1
- package/dist/web/assets/{ordinal-_K3x1fkz.js → ordinal-XHK5vIzZ.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-o3LmdL8H.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-BjNP0M3B.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-C1Gjrtzy.js → pieDiagram-SKSYHLDU-D0S7jeZA.js} +1 -1
- package/dist/web/assets/plus-Iso5r9vD.js +1 -0
- package/dist/web/assets/port-forwarding-tab-BPuSc6pI.js +1 -0
- package/dist/web/assets/{postgres-viewer-BQdPMowm.js → postgres-viewer-RldlAO_m.js} +3 -3
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-C8bzJCjQ.js → quadrantDiagram-337W2JSQ-0hNP63hW.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-gDgOiaME.js +1 -0
- package/dist/web/assets/refresh-cw-BgQzFNaG.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-pQyah6WB.js → requirementDiagram-Z7DCOOCP-BVnmqFbL.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-T6RgG-N8.js → sankeyDiagram-WA2Y5GQK-DVkYdCJb.js} +1 -1
- package/dist/web/assets/scroll-area-i4EZlOl_.js +1 -0
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-BQDJ4CVs.js → sequenceDiagram-2WXFIKYE-B80s7sOg.js} +1 -1
- package/dist/web/assets/settings-tab-BzSSN2BQ.js +1 -0
- package/dist/web/assets/{sql-query-editor-CY61vWBg.js → sql-query-editor-CjZ7Z6XL.js} +1 -1
- package/dist/web/assets/sqlite-viewer-CoyZOM_Y.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-66vhiIuk.js → stateDiagram-RAJIS63D-BPLXgXRR.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DksQJ7es.js +1 -0
- package/dist/web/assets/{terminal-tab-TIJmxHl6.js → terminal-tab-DjzD8GLn.js} +2 -2
- package/dist/web/assets/{timeline-definition-YZTLITO2-DwZqB3nn.js → timeline-definition-YZTLITO2-fa_51u1X.js} +1 -1
- package/dist/web/assets/trash-2-DYCa06CV.js +1 -0
- package/dist/web/assets/treemap-KZPCXAKY-DwFqAvnj.js +1 -0
- package/dist/web/assets/{use-monaco-theme-BHn-LEm7.js → use-monaco-theme-D9XFxQuU.js} +1 -1
- package/dist/web/assets/{vennDiagram-LZ73GAT5-s9Z71fz-.js → vennDiagram-LZ73GAT5-kX4jJn6W.js} +1 -1
- package/dist/web/assets/x-BXecj-16.js +1 -0
- package/dist/web/assets/{xychartDiagram-JWTSCODW-DRa_TH4B.js → xychartDiagram-JWTSCODW-Bzm5lZBs.js} +1 -1
- package/dist/web/index.html +22 -12
- package/dist/web/sw.js +1 -1
- package/package.json +9 -3
- package/src/server/index.ts +1 -1
- package/src/web/components/editor/code-editor.tsx +67 -4
- package/src/web/components/editor/save-as-dialog.tsx +75 -0
- package/src/web/components/layout/command-palette.tsx +2 -0
- package/src/web/components/layout/draggable-tab.tsx +120 -67
- package/src/web/components/layout/mobile-nav.tsx +69 -2
- package/src/web/components/layout/tab-bar.tsx +74 -1
- package/src/web/components/layout/upgrade-banner.tsx +3 -0
- 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-global-keybindings.ts +7 -0
- package/src/web/main.tsx +1 -0
- package/src/web/stores/keybindings-store.ts +1 -0
- package/src/web/stores/panel-utils.ts +13 -0
- package/src/web/stores/tab-store.ts +16 -0
- 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/channel-By7bn0Yq.js +0 -1
- package/dist/web/assets/chat-tab-CT2XUgsc.js +0 -10
- 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/diff-viewer-CTwcVIP_.js +0 -4
- package/dist/web/assets/dist-DIV6WgAG.js +0 -41
- 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/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/stateDiagram-v2-FVOUBMTO-BGVqj_g9.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/dist/web/assets/{api-client-BfBM3I7n.js → api-client-wQbeUyeh.js} +0 -0
- /package/dist/web/assets/{array-B9UHiPd-.js → array-X0JlPOfd.js} +0 -0
- /package/dist/web/assets/{arrow-up-BYhx9ckd.js → arrow-up-BigIMx-e.js} +0 -0
- /package/dist/web/assets/{chevron-right-4zq1jPv6.js → chevron-right-CXzzT44u.js} +0 -0
- /package/dist/web/assets/{columns-2-BoZAN-iw.js → columns-2-BZ9uqssV.js} +0 -0
- /package/dist/web/assets/{csv-parser-CNNw2RVA.js → csv-parser-CElqio6o.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-BW-DbntU.js → cytoscape.esm-BfIOPvwt.js} +0 -0
- /package/dist/web/assets/{defaultLocale-5eAKkKJC.js → defaultLocale-B6RGN4id.js} +0 -0
- /package/dist/web/assets/{dist-CSJdAyA9.js → dist-CK1enexV.js} +0 -0
- /package/dist/web/assets/{init-DlZdxViB.js → init-BmUWJJHz.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-B_v2FtYn.js → isArrayLikeObject-BrCM-iA1.js} +0 -0
- /package/dist/web/assets/{jsx-runtime-kMwlnEGE.js → jsx-runtime-R_NjdZtX.js} +0 -0
- /package/dist/web/assets/{katex-Bqvo_ZG0.js → katex-xQS_6bNb.js} +0 -0
- /package/dist/web/assets/{lib-DurwGtQO.js → lib-CfWBrYll.js} +0 -0
- /package/dist/web/assets/{math-069Z4SuC.js → math-CpLFzrfV.js} +0 -0
- /package/dist/web/assets/{path-6uRLdFF7.js → path-CoPyR7c2.js} +0 -0
- /package/dist/web/assets/{preload-helper-Bf_JiD2A.js → preload-helper-CH6UZRzu.js} +0 -0
- /package/dist/web/assets/{react-SKk5z-bm.js → react-j5zqhEum.js} +0 -0
- /package/dist/web/assets/{rough.esm-JX0wREDd.js → rough.esm-D5NinLFK.js} +0 -0
- /package/dist/web/assets/{sql-completion-provider-DM9Qov6L.js → sql-completion-provider-D0xutVaK.js} +0 -0
- /package/dist/web/assets/{square-oPKIkJiw.js → square-pfn_LYYy.js} +0 -0
- /package/dist/web/assets/{src-BqX54PbV.js → src-j04igtQ5.js} +0 -0
- /package/dist/web/assets/{table-DFevCOMd.js → table-CHv2x_qg.js} +0 -0
- /package/dist/web/assets/{tag-CXMT0QB6.js → tag-Bb_UFXt0.js} +0 -0
- /package/dist/web/assets/{text-wrap-BWNOVswA.js → text-wrap-D8BbQYTx.js} +0 -0
- /package/dist/web/assets/{utils-BNytJOb1.js → utils-CSCvNZxE.js} +0 -0
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Database,
|
|
9
9
|
Search,
|
|
10
10
|
FileCode,
|
|
11
|
+
FilePlus,
|
|
11
12
|
FolderOpen,
|
|
12
13
|
Loader2,
|
|
13
14
|
Globe,
|
|
@@ -159,6 +160,7 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
|
|
|
159
160
|
|
|
160
161
|
const builtIn: CommandItem[] = [
|
|
161
162
|
{ 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
|
+
{ 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")) },
|
|
162
164
|
{ id: "terminal", label: "New Terminal", icon: Terminal, action: openNewTab("terminal", "Terminal"), keywords: "bash shell console", group: "action", shortcut: formatShortcut(getBinding("open-terminal")) },
|
|
163
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")) },
|
|
164
166
|
{ id: "ports", label: "Port Forwarding", icon: Globe, action: openNewTab("ports", "Ports"), keywords: "web preview localhost port forward tunnel url", group: "action" },
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { useState, useRef, useEffect } from "react";
|
|
2
|
-
import { X } from "lucide-react";
|
|
2
|
+
import { X, Download } from "lucide-react";
|
|
3
3
|
import type { Tab, TabType } from "@/stores/tab-store";
|
|
4
4
|
import { cn } from "@/lib/utils";
|
|
5
5
|
import { isDarkColor } from "@/lib/color-utils";
|
|
6
6
|
import { notificationColor } from "@/stores/notification-store";
|
|
7
|
+
import {
|
|
8
|
+
ContextMenu,
|
|
9
|
+
ContextMenuContent,
|
|
10
|
+
ContextMenuItem,
|
|
11
|
+
ContextMenuSeparator,
|
|
12
|
+
ContextMenuTrigger,
|
|
13
|
+
} from "@/components/ui/context-menu";
|
|
7
14
|
|
|
8
15
|
interface DraggableTabProps {
|
|
9
16
|
tab: Tab;
|
|
@@ -23,11 +30,13 @@ interface DraggableTabProps {
|
|
|
23
30
|
tabRef: (el: HTMLButtonElement | null) => void;
|
|
24
31
|
/** If provided, double-clicking the title enters inline rename mode */
|
|
25
32
|
onRename?: (newTitle: string) => void;
|
|
33
|
+
/** Context menu action handler — receives action name */
|
|
34
|
+
onContextAction?: (action: string) => void;
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
export function DraggableTab({
|
|
29
38
|
tab, isActive, icon: Icon, showDropBefore, notificationType, onSelect, onClose,
|
|
30
|
-
onDragStart, onDragOver, onDragEnd, onTouchStart, onTouchMove, onTouchEnd, tabRef, onRename,
|
|
39
|
+
onDragStart, onDragOver, onDragEnd, onTouchStart, onTouchMove, onTouchEnd, tabRef, onRename, onContextAction,
|
|
31
40
|
}: DraggableTabProps) {
|
|
32
41
|
const [editing, setEditing] = useState(false);
|
|
33
42
|
const [editValue, setEditValue] = useState(tab.title);
|
|
@@ -56,76 +65,120 @@ export function DraggableTab({
|
|
|
56
65
|
}
|
|
57
66
|
: undefined;
|
|
58
67
|
|
|
68
|
+
const isFile = tab.type === "editor";
|
|
69
|
+
|
|
70
|
+
const tabButton = (
|
|
71
|
+
<button
|
|
72
|
+
ref={tabRef}
|
|
73
|
+
data-tab-item
|
|
74
|
+
draggable={!editing}
|
|
75
|
+
onClick={onSelect}
|
|
76
|
+
onAuxClick={(e) => { if (e.button === 1 && tab.closable) { e.preventDefault(); onClose(); } }}
|
|
77
|
+
onDragStart={onDragStart}
|
|
78
|
+
onDragOver={onDragOver}
|
|
79
|
+
onDragEnd={onDragEnd}
|
|
80
|
+
onTouchStart={onTouchStart}
|
|
81
|
+
onTouchMove={onTouchMove}
|
|
82
|
+
onTouchEnd={onTouchEnd}
|
|
83
|
+
style={colorStyle}
|
|
84
|
+
className={cn(
|
|
85
|
+
"group flex items-center gap-1 px-3 h-10 whitespace-nowrap text-xs transition-colors",
|
|
86
|
+
"border-b-2 -mb-px cursor-grab active:cursor-grabbing",
|
|
87
|
+
!colorStyle && (isActive
|
|
88
|
+
? "border-primary text-primary"
|
|
89
|
+
: "border-transparent text-text-secondary hover:text-foreground"),
|
|
90
|
+
colorStyle && "border-transparent",
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
<span className="relative">
|
|
94
|
+
<Icon className="size-4" />
|
|
95
|
+
{notificationType && !isActive && (
|
|
96
|
+
<span className={cn("absolute -top-1 -right-1 size-2 rounded-full", notificationColor(notificationType))} />
|
|
97
|
+
)}
|
|
98
|
+
</span>
|
|
99
|
+
{editing ? (
|
|
100
|
+
<input
|
|
101
|
+
ref={inputRef}
|
|
102
|
+
value={editValue}
|
|
103
|
+
onChange={(e) => setEditValue(e.target.value)}
|
|
104
|
+
onBlur={commitRename}
|
|
105
|
+
onKeyDown={(e) => {
|
|
106
|
+
if (e.key === "Enter") commitRename();
|
|
107
|
+
if (e.key === "Escape") setEditing(false);
|
|
108
|
+
e.stopPropagation();
|
|
109
|
+
}}
|
|
110
|
+
onClick={(e) => e.stopPropagation()}
|
|
111
|
+
className="max-w-[120px] bg-surface-elevated text-xs px-1 py-0.5 rounded border border-border outline-none focus:border-primary"
|
|
112
|
+
autoFocus
|
|
113
|
+
/>
|
|
114
|
+
) : (
|
|
115
|
+
<span
|
|
116
|
+
className="max-w-[120px] truncate"
|
|
117
|
+
onDoubleClick={(e) => {
|
|
118
|
+
if (onRename) { e.stopPropagation(); setEditing(true); }
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
{tab.title}
|
|
122
|
+
</span>
|
|
123
|
+
)}
|
|
124
|
+
{tab.closable && !editing && (
|
|
125
|
+
<span
|
|
126
|
+
role="button"
|
|
127
|
+
tabIndex={0}
|
|
128
|
+
onClick={(e) => { e.stopPropagation(); onClose(); }}
|
|
129
|
+
onKeyDown={(e) => { if (e.key === "Enter") { e.stopPropagation(); onClose(); } }}
|
|
130
|
+
className="ml-1 can-hover:opacity-0 can-hover:group-hover:opacity-100 rounded-sm hover:bg-surface-elevated p-0.5 transition-opacity"
|
|
131
|
+
>
|
|
132
|
+
<X className="size-3" />
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
135
|
+
</button>
|
|
136
|
+
);
|
|
137
|
+
|
|
59
138
|
return (
|
|
60
139
|
<div className="relative flex items-center">
|
|
61
140
|
{showDropBefore && (
|
|
62
141
|
<div className="absolute left-0 top-1 bottom-1 w-0.5 bg-primary rounded-full z-10" />
|
|
63
142
|
)}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
onClick={(e) => e.stopPropagation()}
|
|
104
|
-
className="max-w-[120px] bg-surface-elevated text-xs px-1 py-0.5 rounded border border-border outline-none focus:border-primary"
|
|
105
|
-
autoFocus
|
|
106
|
-
/>
|
|
107
|
-
) : (
|
|
108
|
-
<span
|
|
109
|
-
className="max-w-[120px] truncate"
|
|
110
|
-
onDoubleClick={(e) => {
|
|
111
|
-
if (onRename) { e.stopPropagation(); setEditing(true); }
|
|
112
|
-
}}
|
|
113
|
-
>
|
|
114
|
-
{tab.title}
|
|
115
|
-
</span>
|
|
116
|
-
)}
|
|
117
|
-
{tab.closable && !editing && (
|
|
118
|
-
<span
|
|
119
|
-
role="button"
|
|
120
|
-
tabIndex={0}
|
|
121
|
-
onClick={(e) => { e.stopPropagation(); onClose(); }}
|
|
122
|
-
onKeyDown={(e) => { if (e.key === "Enter") { e.stopPropagation(); onClose(); } }}
|
|
123
|
-
className="ml-1 can-hover:opacity-0 can-hover:group-hover:opacity-100 rounded-sm hover:bg-surface-elevated p-0.5 transition-opacity"
|
|
124
|
-
>
|
|
125
|
-
<X className="size-3" />
|
|
126
|
-
</span>
|
|
127
|
-
)}
|
|
128
|
-
</button>
|
|
143
|
+
{onContextAction ? (
|
|
144
|
+
<ContextMenu>
|
|
145
|
+
<ContextMenuTrigger asChild>
|
|
146
|
+
{tabButton}
|
|
147
|
+
</ContextMenuTrigger>
|
|
148
|
+
<ContextMenuContent>
|
|
149
|
+
{isFile && (
|
|
150
|
+
<>
|
|
151
|
+
<ContextMenuItem onClick={() => onContextAction("copy-path")}>
|
|
152
|
+
Copy Path
|
|
153
|
+
</ContextMenuItem>
|
|
154
|
+
<ContextMenuItem onClick={() => onContextAction("download")}>
|
|
155
|
+
<Download className="size-3.5 mr-2" />
|
|
156
|
+
Download
|
|
157
|
+
</ContextMenuItem>
|
|
158
|
+
<ContextMenuSeparator />
|
|
159
|
+
<ContextMenuItem onClick={() => onContextAction("rename")}>
|
|
160
|
+
Rename
|
|
161
|
+
</ContextMenuItem>
|
|
162
|
+
<ContextMenuItem variant="destructive" onClick={() => onContextAction("delete")}>
|
|
163
|
+
Delete
|
|
164
|
+
</ContextMenuItem>
|
|
165
|
+
<ContextMenuSeparator />
|
|
166
|
+
</>
|
|
167
|
+
)}
|
|
168
|
+
{tab.closable && (
|
|
169
|
+
<ContextMenuItem onClick={() => onContextAction("close")}>
|
|
170
|
+
Close
|
|
171
|
+
</ContextMenuItem>
|
|
172
|
+
)}
|
|
173
|
+
<ContextMenuItem onClick={() => onContextAction("close-others")}>
|
|
174
|
+
Close Others
|
|
175
|
+
</ContextMenuItem>
|
|
176
|
+
<ContextMenuItem onClick={() => onContextAction("close-right")}>
|
|
177
|
+
Close to the Right
|
|
178
|
+
</ContextMenuItem>
|
|
179
|
+
</ContextMenuContent>
|
|
180
|
+
</ContextMenu>
|
|
181
|
+
) : tabButton}
|
|
129
182
|
</div>
|
|
130
183
|
);
|
|
131
184
|
}
|
|
@@ -2,10 +2,11 @@ import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
|
|
2
2
|
import {
|
|
3
3
|
Terminal, MessageSquare, GitBranch, Database,
|
|
4
4
|
FileDiff, FileCode, Settings, Menu, X, ArrowLeft, ArrowRight, SplitSquareVertical, MoveVertical, Layers, Plus,
|
|
5
|
-
ChevronRight, Globe, Puzzle,
|
|
5
|
+
ChevronRight, Globe, Puzzle, Copy, Download, Pencil, Trash2,
|
|
6
6
|
} from "lucide-react";
|
|
7
7
|
import { usePanelStore } from "@/stores/panel-store";
|
|
8
8
|
import { useProjectStore, resolveOrder } from "@/stores/project-store";
|
|
9
|
+
import { useFileStore, type FileNode } from "@/stores/file-store";
|
|
9
10
|
import { findPanelPosition, MAX_ROWS } from "@/stores/panel-utils";
|
|
10
11
|
import { resolveProjectColor } from "@/lib/project-palette";
|
|
11
12
|
import { getProjectInitials } from "@/lib/project-avatar";
|
|
@@ -14,6 +15,8 @@ import { cn } from "@/lib/utils";
|
|
|
14
15
|
import { openCommandPalette } from "@/hooks/use-global-keybindings";
|
|
15
16
|
import { useNotificationStore, notificationColor } from "@/stores/notification-store";
|
|
16
17
|
import { useTabOverflow, getHiddenUnreadDirection } from "@/hooks/use-tab-overflow";
|
|
18
|
+
import { downloadFile } from "@/lib/file-download";
|
|
19
|
+
import { FileActions } from "@/components/explorer/file-actions";
|
|
17
20
|
|
|
18
21
|
const NEW_TAB_OPTIONS: { type: TabType; label: string }[] = [
|
|
19
22
|
{ type: "terminal", label: "Terminal" },
|
|
@@ -112,6 +115,28 @@ export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
|
|
|
112
115
|
usePanelStore.getState().moveTab(tabId, pid, targetPanelId);
|
|
113
116
|
}
|
|
114
117
|
|
|
118
|
+
const [fileActionState, setFileActionState] = useState<{ action: string; node: FileNode; tabId: string } | null>(null);
|
|
119
|
+
|
|
120
|
+
function handleFileAction(tab: Tab, action: string) {
|
|
121
|
+
const filePath = tab.metadata?.filePath as string | undefined;
|
|
122
|
+
const projectName = tab.metadata?.projectName as string | undefined;
|
|
123
|
+
switch (action) {
|
|
124
|
+
case "copy-path":
|
|
125
|
+
if (filePath) navigator.clipboard.writeText(filePath).catch(() => {});
|
|
126
|
+
break;
|
|
127
|
+
case "download":
|
|
128
|
+
if (filePath && projectName) downloadFile(projectName, filePath);
|
|
129
|
+
break;
|
|
130
|
+
case "rename":
|
|
131
|
+
case "delete":
|
|
132
|
+
if (filePath) {
|
|
133
|
+
setFileActionState({ action, tabId: tab.id, node: { name: tab.title, path: filePath, type: "file" } });
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
setMenuTabId(null);
|
|
138
|
+
}
|
|
139
|
+
|
|
115
140
|
const { activeProject: activeProjectForTab } = useProjectStore.getState();
|
|
116
141
|
function handleNewTab(type: TabType) {
|
|
117
142
|
const state = usePanelStore.getState();
|
|
@@ -269,13 +294,40 @@ export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
|
|
|
269
294
|
<div className="px-3 py-2 text-xs text-text-secondary border-b border-border truncate">
|
|
270
295
|
{menuTab.title}
|
|
271
296
|
</div>
|
|
297
|
+
{menuTab.type === "editor" && (
|
|
298
|
+
<>
|
|
299
|
+
<button onClick={() => handleFileAction(menuTab, "copy-path")}
|
|
300
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
|
|
301
|
+
<Copy className="size-4" /> Copy Path
|
|
302
|
+
</button>
|
|
303
|
+
<button onClick={() => handleFileAction(menuTab, "download")}
|
|
304
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
|
|
305
|
+
<Download className="size-4" /> Download
|
|
306
|
+
</button>
|
|
307
|
+
<button onClick={() => handleFileAction(menuTab, "rename")}
|
|
308
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
|
|
309
|
+
<Pencil className="size-4" /> Rename
|
|
310
|
+
</button>
|
|
311
|
+
<button onClick={() => handleFileAction(menuTab, "delete")}
|
|
312
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-error active:bg-surface-elevated">
|
|
313
|
+
<Trash2 className="size-4" /> Delete
|
|
314
|
+
</button>
|
|
315
|
+
<div className="h-px bg-border mx-2" />
|
|
316
|
+
</>
|
|
317
|
+
)}
|
|
318
|
+
{menuTab.closable && (
|
|
319
|
+
<button onClick={() => { usePanelStore.getState().closeTab(menuTabId!); setMenuTabId(null); }}
|
|
320
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
|
|
321
|
+
<X className="size-4" /> Close
|
|
322
|
+
</button>
|
|
323
|
+
)}
|
|
272
324
|
{menuTabIdx > 0 && (
|
|
273
325
|
<button onClick={() => { moveTabLeft(menuTabId!); setMenuTabId(null); }}
|
|
274
326
|
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
|
|
275
327
|
<ArrowLeft className="size-4" /> Move Left
|
|
276
328
|
</button>
|
|
277
329
|
)}
|
|
278
|
-
{menuTabIdx <
|
|
330
|
+
{menuTabIdx < menuTabPanelTabs.length - 1 && (
|
|
279
331
|
<button onClick={() => { moveTabRight(menuTabId!); setMenuTabId(null); }}
|
|
280
332
|
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-foreground active:bg-surface-elevated">
|
|
281
333
|
<ArrowRight className="size-4" /> Move Right
|
|
@@ -296,6 +348,21 @@ export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
|
|
|
296
348
|
</div>
|
|
297
349
|
</>
|
|
298
350
|
)}
|
|
351
|
+
|
|
352
|
+
{fileActionState && (
|
|
353
|
+
<FileActions
|
|
354
|
+
action={fileActionState.action}
|
|
355
|
+
node={fileActionState.node}
|
|
356
|
+
projectName={activeProjectForTab?.name ?? ""}
|
|
357
|
+
onClose={() => setFileActionState(null)}
|
|
358
|
+
onRefresh={() => {
|
|
359
|
+
if (activeProjectForTab) useFileStore.getState().fetchTree(activeProjectForTab.name);
|
|
360
|
+
if (fileActionState.action === "delete") {
|
|
361
|
+
usePanelStore.getState().closeTab(fileActionState.tabId);
|
|
362
|
+
}
|
|
363
|
+
}}
|
|
364
|
+
/>
|
|
365
|
+
)}
|
|
299
366
|
</nav>
|
|
300
367
|
);
|
|
301
368
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef, useCallback } from "react";
|
|
1
|
+
import { useEffect, useRef, useCallback, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Plus,
|
|
4
4
|
Terminal,
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { useTabStore, type TabType } from "@/stores/tab-store";
|
|
17
17
|
import { usePanelStore } from "@/stores/panel-store";
|
|
18
18
|
import { useProjectStore } from "@/stores/project-store";
|
|
19
|
+
import { useFileStore, type FileNode } from "@/stores/file-store";
|
|
19
20
|
import { useTabDrag } from "@/hooks/use-tab-drag";
|
|
20
21
|
import { useTouchTabDrag, wasTouchDragRecent } from "@/hooks/use-touch-tab-drag";
|
|
21
22
|
import { openCommandPalette } from "@/hooks/use-global-keybindings";
|
|
@@ -25,6 +26,8 @@ import { useTabOverflow, getHiddenUnreadDirection } from "@/hooks/use-tab-overfl
|
|
|
25
26
|
import { DraggableTab } from "./draggable-tab";
|
|
26
27
|
import { cn } from "@/lib/utils";
|
|
27
28
|
import type { Tab } from "@/stores/tab-store";
|
|
29
|
+
import { downloadFile } from "@/lib/file-download";
|
|
30
|
+
import { FileActions } from "@/components/explorer/file-actions";
|
|
28
31
|
|
|
29
32
|
const TAB_ICONS: Record<TabType, React.ElementType> = {
|
|
30
33
|
terminal: Terminal,
|
|
@@ -86,6 +89,56 @@ export function TabBar({ panelId }: TabBarProps) {
|
|
|
86
89
|
}
|
|
87
90
|
}, []);
|
|
88
91
|
|
|
92
|
+
// File action dialog state for tab context menu (rename/delete)
|
|
93
|
+
const [fileActionState, setFileActionState] = useState<{ action: string; node: FileNode; tabId: string } | null>(null);
|
|
94
|
+
|
|
95
|
+
/** Handle context menu actions on a tab */
|
|
96
|
+
const handleTabContextAction = useCallback((tab: Tab, action: string) => {
|
|
97
|
+
const panelState = usePanelStore.getState();
|
|
98
|
+
const pTabs = panelState.panels[effectivePanelId]?.tabs ?? [];
|
|
99
|
+
|
|
100
|
+
switch (action) {
|
|
101
|
+
case "close":
|
|
102
|
+
panelState.closeTab(tab.id, effectivePanelId);
|
|
103
|
+
break;
|
|
104
|
+
case "close-others":
|
|
105
|
+
for (const t of pTabs) {
|
|
106
|
+
if (t.id !== tab.id && t.closable) panelState.closeTab(t.id, effectivePanelId);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
case "close-right": {
|
|
110
|
+
const idx = pTabs.findIndex((t) => t.id === tab.id);
|
|
111
|
+
for (let i = idx + 1; i < pTabs.length; i++) {
|
|
112
|
+
if (pTabs[i]!.closable) panelState.closeTab(pTabs[i]!.id, effectivePanelId);
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case "copy-path": {
|
|
117
|
+
const filePath = tab.metadata?.filePath as string | undefined;
|
|
118
|
+
if (filePath) navigator.clipboard.writeText(filePath).catch(() => {});
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case "download": {
|
|
122
|
+
const filePath = tab.metadata?.filePath as string | undefined;
|
|
123
|
+
const projectName = tab.metadata?.projectName as string | undefined;
|
|
124
|
+
if (filePath && projectName) downloadFile(projectName, filePath);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case "rename":
|
|
128
|
+
case "delete": {
|
|
129
|
+
const filePath = tab.metadata?.filePath as string | undefined;
|
|
130
|
+
if (filePath) {
|
|
131
|
+
setFileActionState({
|
|
132
|
+
action,
|
|
133
|
+
tabId: tab.id,
|
|
134
|
+
node: { name: tab.title, path: filePath, type: "file" },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}, [effectivePanelId]);
|
|
141
|
+
|
|
89
142
|
/** Double-click on empty bar area → open command palette */
|
|
90
143
|
function handleBarDoubleClick(e: React.MouseEvent) {
|
|
91
144
|
// Only trigger if clicking directly on the bar or scroll container (not on a tab)
|
|
@@ -103,6 +156,7 @@ export function TabBar({ panelId }: TabBarProps) {
|
|
|
103
156
|
}
|
|
104
157
|
|
|
105
158
|
return (
|
|
159
|
+
<>
|
|
106
160
|
<div
|
|
107
161
|
className="hidden md:flex items-center h-10 border-b border-border bg-background relative"
|
|
108
162
|
onDragOver={handleDragOverBar}
|
|
@@ -160,6 +214,7 @@ export function TabBar({ panelId }: TabBarProps) {
|
|
|
160
214
|
else tabRefs.current.delete(tab.id);
|
|
161
215
|
}}
|
|
162
216
|
onRename={tab.type === "chat" ? (title) => handleRenameTab(tab, title) : undefined}
|
|
217
|
+
onContextAction={(action) => handleTabContextAction(tab, action)}
|
|
163
218
|
/>
|
|
164
219
|
);
|
|
165
220
|
})}
|
|
@@ -194,5 +249,23 @@ export function TabBar({ panelId }: TabBarProps) {
|
|
|
194
249
|
</button>
|
|
195
250
|
)}
|
|
196
251
|
</div>
|
|
252
|
+
|
|
253
|
+
{fileActionState && (
|
|
254
|
+
<FileActions
|
|
255
|
+
action={fileActionState.action}
|
|
256
|
+
node={fileActionState.node}
|
|
257
|
+
projectName={activeProject?.name ?? ""}
|
|
258
|
+
onClose={() => setFileActionState(null)}
|
|
259
|
+
onRefresh={() => {
|
|
260
|
+
if (activeProject) useFileStore.getState().fetchTree(activeProject.name);
|
|
261
|
+
// Close tab after file deletion (onRefresh only called on success)
|
|
262
|
+
if (fileActionState.action === "delete") {
|
|
263
|
+
usePanelStore.getState().closeTab(fileActionState.tabId, effectivePanelId);
|
|
264
|
+
}
|
|
265
|
+
}}
|
|
266
|
+
/>
|
|
267
|
+
)}
|
|
268
|
+
</>
|
|
197
269
|
);
|
|
198
270
|
}
|
|
271
|
+
|
|
@@ -96,6 +96,9 @@ export function UpgradeBanner({ onVisibilityChange }: UpgradeBannerProps) {
|
|
|
96
96
|
// No supervisor — manual restart needed
|
|
97
97
|
toast.info(data.message || "Upgrade installed. Restart PPM manually.");
|
|
98
98
|
setUpgrading(false);
|
|
99
|
+
if (availableVersion) {
|
|
100
|
+
sessionStorage.setItem(DISMISS_KEY_PREFIX + availableVersion, "1");
|
|
101
|
+
}
|
|
99
102
|
setDismissed(true);
|
|
100
103
|
}
|
|
101
104
|
} catch (e) {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, type ReactNode } from "react";
|
|
2
|
+
import mermaid from "mermaid";
|
|
3
|
+
import { useMdContext, FILE_EXT_RE, GLOB_CHARS_RE } from "./markdown-context";
|
|
4
|
+
import { useTabStore } from "@/stores/tab-store";
|
|
5
|
+
|
|
6
|
+
const MERMAID_KEYWORDS = /^(sequenceDiagram|flowchart|graph\s|classDiagram|stateDiagram|erDiagram|journey|gantt|pie|quadrantChart|requirementDiagram|gitGraph|mindmap|timeline|sankey|xychart|block-beta|packet-beta|architecture-beta|kanban)\b/;
|
|
7
|
+
|
|
8
|
+
let mermaidInitialized = false;
|
|
9
|
+
function ensureMermaidInit() {
|
|
10
|
+
if (mermaidInitialized) return;
|
|
11
|
+
mermaid.initialize({ startOnLoad: false, theme: "default", securityLevel: "loose", fontFamily: "ui-sans-serif, system-ui, sans-serif" });
|
|
12
|
+
mermaidInitialized = true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Extract plain text from a hast node tree */
|
|
16
|
+
function hastToText(node: any): string {
|
|
17
|
+
if (!node) return "";
|
|
18
|
+
if (node.type === "text") return node.value ?? "";
|
|
19
|
+
if (node.children) return node.children.map(hastToText).join("");
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Pre — code block wrapper with mermaid detection and action buttons */
|
|
24
|
+
export function MdPre({ children, node, ...rest }: any) {
|
|
25
|
+
const { codeActions, projectName, openDiagramOverlay } = useMdContext();
|
|
26
|
+
const openTab = useTabStore((s) => s.openTab);
|
|
27
|
+
|
|
28
|
+
const codeNode = node?.children?.[0];
|
|
29
|
+
const langClass = (codeNode?.properties?.className ?? []).find((c: string) => c.startsWith("language-"));
|
|
30
|
+
const lang = langClass?.replace("language-", "");
|
|
31
|
+
const text = hastToText(codeNode);
|
|
32
|
+
|
|
33
|
+
// Mermaid detection
|
|
34
|
+
if (lang === "mermaid" || (!lang && MERMAID_KEYWORDS.test(text.trim()))) {
|
|
35
|
+
return <MermaidDiagram source={text.trim()} />;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isBash = /^(bash|sh|shell|zsh)$/.test(lang || "") || (!lang && text.startsWith("$"));
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<pre {...rest} className={`relative group ${rest.className || ""}`}>
|
|
42
|
+
{children}
|
|
43
|
+
{codeActions && (
|
|
44
|
+
<div className="code-actions absolute top-1 right-1 flex gap-1">
|
|
45
|
+
<ActionBtn title="Copy" icon={<CopyIcon />} activeIcon={<CheckIcon />} onClick={() => navigator.clipboard.writeText(text)} />
|
|
46
|
+
{isBash && projectName && (
|
|
47
|
+
<ActionBtn
|
|
48
|
+
title="Run in terminal"
|
|
49
|
+
icon={<PlayIcon />}
|
|
50
|
+
onClick={() => {
|
|
51
|
+
navigator.clipboard.writeText(text.replace(/^\$\s*/gm, ""));
|
|
52
|
+
openTab({ type: "terminal", title: "Terminal", metadata: { projectName }, projectId: projectName, closable: true });
|
|
53
|
+
}}
|
|
54
|
+
/>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
</pre>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Code — inline code with file clicking; block code passes through */
|
|
63
|
+
export function MdCode({ className, children, node, ...rest }: any) {
|
|
64
|
+
const { openFileOrSearch } = useMdContext();
|
|
65
|
+
|
|
66
|
+
// Block code (has language/hljs class from rehype-highlight) — render as-is
|
|
67
|
+
if (className) return <code className={className} {...rest}>{children}</code>;
|
|
68
|
+
|
|
69
|
+
// Inline code — check for clickable file paths
|
|
70
|
+
const text = String(children ?? "").trim();
|
|
71
|
+
if (text && !text.includes(" ") && !GLOB_CHARS_RE.test(text) && FILE_EXT_RE.test(text)) {
|
|
72
|
+
return (
|
|
73
|
+
<code
|
|
74
|
+
onClick={() => openFileOrSearch(text)}
|
|
75
|
+
style={{ cursor: "pointer", textDecoration: "underline", textDecorationStyle: "dotted" as const }}
|
|
76
|
+
{...rest}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</code>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return <code {...rest}>{children}</code>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Mermaid diagram renderer with click-to-expand */
|
|
87
|
+
function MermaidDiagram({ source }: { source: string }) {
|
|
88
|
+
const { openDiagramOverlay } = useMdContext();
|
|
89
|
+
const [svg, setSvg] = useState<string | null>(null);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
ensureMermaidInit();
|
|
93
|
+
const id = `mermaid-${Math.random().toString(36).slice(2, 9)}`;
|
|
94
|
+
mermaid.render(id, source).then(({ svg }) => setSvg(svg)).catch(() => {});
|
|
95
|
+
}, [source]);
|
|
96
|
+
|
|
97
|
+
if (!svg) return <pre><code>{source}</code></pre>;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
className="mermaid-diagram group relative cursor-pointer rounded-lg border border-border bg-white dark:bg-zinc-50 p-3 overflow-x-auto my-2"
|
|
102
|
+
onClick={() => openDiagramOverlay(svg)}
|
|
103
|
+
>
|
|
104
|
+
<div dangerouslySetInnerHTML={{ __html: svg }} />
|
|
105
|
+
<div className="absolute top-2 right-2 flex items-center gap-1 px-2 py-1 rounded bg-black/60 text-white text-xs can-hover:opacity-0 can-hover:group-hover:opacity-100 transition-opacity pointer-events-none">
|
|
106
|
+
Click to expand
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Reusable code-block action button with optional active state */
|
|
113
|
+
function ActionBtn({ title, icon, activeIcon, onClick }: { title: string; icon: ReactNode; activeIcon?: ReactNode; onClick: () => void }) {
|
|
114
|
+
const [active, setActive] = useState(false);
|
|
115
|
+
return (
|
|
116
|
+
<button
|
|
117
|
+
className="flex items-center justify-center size-6 rounded bg-surface-elevated/80 hover:bg-surface-elevated text-text-secondary hover:text-text-primary transition-colors border border-border/50"
|
|
118
|
+
title={title}
|
|
119
|
+
onClick={() => { onClick(); if (activeIcon) { setActive(true); setTimeout(() => setActive(false), 2000); } }}
|
|
120
|
+
>
|
|
121
|
+
{active && activeIcon ? activeIcon : icon}
|
|
122
|
+
</button>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const CopyIcon = () => (
|
|
127
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
128
|
+
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
|
129
|
+
</svg>
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const CheckIcon = () => (
|
|
133
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
134
|
+
<polyline points="20 6 9 17 4 12" />
|
|
135
|
+
</svg>
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const PlayIcon = () => (
|
|
139
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
140
|
+
<polyline points="4 17 10 11 4 5" /><line x1="12" y1="19" x2="20" y2="19" />
|
|
141
|
+
</svg>
|
|
142
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
/** Common text file extensions that PPM can open as editor tabs */
|
|
4
|
+
const FILE_EXTS = "ts|tsx|js|jsx|mjs|cjs|py|json|md|mdx|yaml|yml|toml|css|scss|less|html|htm|sh|bash|zsh|go|rs|sql|rb|java|kt|swift|c|cpp|h|hpp|cs|vue|svelte|txt|env|cfg|conf|ini|xml|csv|log|dockerfile|makefile|gradle";
|
|
5
|
+
export const FILE_EXT_RE = new RegExp(`\\.(${FILE_EXTS})$`, "i");
|
|
6
|
+
/** Glob/regex chars that indicate a pattern, not a real file */
|
|
7
|
+
export const GLOB_CHARS_RE = /[*?{}\[\]]/;
|
|
8
|
+
/** Detect local absolute file paths (Unix or Windows) */
|
|
9
|
+
export const LOCAL_PATH_RE = /^(\/|[A-Za-z]:[/\\])/;
|
|
10
|
+
|
|
11
|
+
export interface MdContextValue {
|
|
12
|
+
projectName?: string;
|
|
13
|
+
codeActions: boolean;
|
|
14
|
+
openFileOrSearch: (path: string) => void;
|
|
15
|
+
openImageOverlay: (url: string, alt: string) => void;
|
|
16
|
+
openDiagramOverlay: (svg: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const MdContext = createContext<MdContextValue>(null!);
|
|
20
|
+
export const useMdContext = () => useContext(MdContext);
|