@hienlh/ppm 0.9.83 → 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 +20 -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-0P_zRC9w.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-Cx0GpRyC.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-DlCLiEGU.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-Dxx-QZaI.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-yfmxGQ5e.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-7qyyJae5.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/server/routes/settings.ts +6 -3
- package/src/services/cloud.service.ts +35 -4
- 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/git/git-status-panel.tsx +7 -1
- 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/sidebar.tsx +11 -1
- 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/git-status-store.ts +55 -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/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +0 -1
- package/dist/web/assets/channel-By7bn0Yq.js +0 -1
- package/dist/web/assets/chat-tab-DBJJz0Dm.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-BvSFsrGo.js +0 -8
- package/dist/web/assets/diff-viewer-BmBJq4gO.js +0 -4
- package/dist/web/assets/dist-DIV6WgAG.js +0 -41
- package/dist/web/assets/git-graph-BAlhf058.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +0 -1
- package/dist/web/assets/index-BDAqXmpQ.js +0 -30
- package/dist/web/assets/index-BYXjCNlK.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-L7UlPjK0.js +0 -1
- package/dist/web/assets/markdown-renderer-CYs_lrjt.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-CSHJ7gxM.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +0 -1
- package/dist/web/assets/settings-tab-Dcr7lDcJ.js +0 -1
- package/dist/web/assets/sqlite-viewer-B0052fC-.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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
|
4
|
+
} from "@/components/ui/dialog";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Input } from "@/components/ui/input";
|
|
7
|
+
import { FileBrowserPicker } from "@/components/ui/file-browser-picker";
|
|
8
|
+
import { useProjectStore } from "@/stores/project-store";
|
|
9
|
+
|
|
10
|
+
interface SaveAsDialogProps {
|
|
11
|
+
open: boolean;
|
|
12
|
+
defaultName: string;
|
|
13
|
+
content: string;
|
|
14
|
+
onSave: (fullPath: string, content: string) => void;
|
|
15
|
+
onCancel: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function SaveAsDialog({ open, defaultName, content, onSave, onCancel }: SaveAsDialogProps) {
|
|
19
|
+
const [filename, setFilename] = useState(defaultName);
|
|
20
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
21
|
+
const [error, setError] = useState("");
|
|
22
|
+
const activeProject = useProjectStore((s) => s.activeProject);
|
|
23
|
+
|
|
24
|
+
const validateAndProceed = useCallback(() => {
|
|
25
|
+
const trimmed = filename.trim();
|
|
26
|
+
if (!trimmed) { setError("Filename cannot be empty"); return; }
|
|
27
|
+
if (/[/\\]/.test(trimmed)) { setError("Filename cannot contain / or \\"); return; }
|
|
28
|
+
setError("");
|
|
29
|
+
setShowPicker(true);
|
|
30
|
+
}, [filename]);
|
|
31
|
+
|
|
32
|
+
const handleFolderSelect = useCallback((dirPath: string) => {
|
|
33
|
+
const sep = dirPath.includes("\\") ? "\\" : "/";
|
|
34
|
+
const fullPath = dirPath.endsWith(sep) ? `${dirPath}${filename.trim()}` : `${dirPath}${sep}${filename.trim()}`;
|
|
35
|
+
onSave(fullPath, content);
|
|
36
|
+
}, [filename, content, onSave]);
|
|
37
|
+
|
|
38
|
+
if (showPicker) {
|
|
39
|
+
return (
|
|
40
|
+
<FileBrowserPicker
|
|
41
|
+
open
|
|
42
|
+
mode="folder"
|
|
43
|
+
root={activeProject?.path}
|
|
44
|
+
title={`Save "${filename.trim()}" to...`}
|
|
45
|
+
onSelect={handleFolderSelect}
|
|
46
|
+
onCancel={() => setShowPicker(false)}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Dialog open={open} onOpenChange={(v) => { if (!v) onCancel(); }}>
|
|
53
|
+
<DialogContent className="sm:max-w-md">
|
|
54
|
+
<DialogHeader>
|
|
55
|
+
<DialogTitle>Save As</DialogTitle>
|
|
56
|
+
</DialogHeader>
|
|
57
|
+
<div className="flex flex-col gap-2 py-2">
|
|
58
|
+
<label className="text-sm text-muted-foreground">Filename</label>
|
|
59
|
+
<Input
|
|
60
|
+
value={filename}
|
|
61
|
+
onChange={(e) => { setFilename(e.target.value); setError(""); }}
|
|
62
|
+
onKeyDown={(e) => { if (e.key === "Enter") validateAndProceed(); }}
|
|
63
|
+
placeholder="e.g. my-file.ts"
|
|
64
|
+
autoFocus
|
|
65
|
+
/>
|
|
66
|
+
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
67
|
+
</div>
|
|
68
|
+
<DialogFooter>
|
|
69
|
+
<Button variant="outline" onClick={onCancel}>Cancel</Button>
|
|
70
|
+
<Button onClick={validateAndProceed}>Choose Folder...</Button>
|
|
71
|
+
</DialogFooter>
|
|
72
|
+
</DialogContent>
|
|
73
|
+
</Dialog>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -18,6 +18,7 @@ import { basename } from "@/lib/utils";
|
|
|
18
18
|
import { useTabStore } from "@/stores/tab-store";
|
|
19
19
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
20
20
|
import { useProjectStore } from "@/stores/project-store";
|
|
21
|
+
import { useGitStatusStore } from "@/stores/git-status-store";
|
|
21
22
|
import { GitWorktreePanel } from "./git-worktree-panel";
|
|
22
23
|
import { Button } from "@/components/ui/button";
|
|
23
24
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
@@ -122,6 +123,7 @@ export function GitStatusPanel({ metadata, tabId, onNavigate }: GitStatusPanelPr
|
|
|
122
123
|
const activeProjectPath = useProjectStore((s) =>
|
|
123
124
|
s.projects.find((p) => p.name === projectName)?.path,
|
|
124
125
|
);
|
|
126
|
+
const setGitChangesCount = useGitStatusStore((s) => s.setCount);
|
|
125
127
|
|
|
126
128
|
const fetchStatus = useCallback(async () => {
|
|
127
129
|
if (!projectName) return;
|
|
@@ -131,13 +133,17 @@ export function GitStatusPanel({ metadata, tabId, onNavigate }: GitStatusPanelPr
|
|
|
131
133
|
`${projectUrl(projectName)}/git/status`,
|
|
132
134
|
);
|
|
133
135
|
setStatus(data);
|
|
136
|
+
setGitChangesCount(
|
|
137
|
+
projectName,
|
|
138
|
+
data.staged.length + data.unstaged.length + data.untracked.length,
|
|
139
|
+
);
|
|
134
140
|
setError(null);
|
|
135
141
|
} catch (e) {
|
|
136
142
|
setError(e instanceof Error ? e.message : "Failed to fetch status");
|
|
137
143
|
} finally {
|
|
138
144
|
setLoading(false);
|
|
139
145
|
}
|
|
140
|
-
}, [projectName]);
|
|
146
|
+
}, [projectName, setGitChangesCount]);
|
|
141
147
|
|
|
142
148
|
useEffect(() => {
|
|
143
149
|
fetchStatus();
|
|
@@ -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
|
}
|
|
@@ -9,6 +9,7 @@ import { SettingsTab } from "@/components/settings/settings-tab";
|
|
|
9
9
|
import { DatabaseSidebar } from "@/components/database/database-sidebar";
|
|
10
10
|
import { SearchPanel } from "@/components/explorer/search-panel";
|
|
11
11
|
import { ExtensionTreeView } from "@/components/extensions/extension-tree-view";
|
|
12
|
+
import { useGitStatusStore, useGitChangesPoller } from "@/stores/git-status-store";
|
|
12
13
|
import { cn } from "@/lib/utils";
|
|
13
14
|
|
|
14
15
|
const BUILTIN_TABS: { id: SidebarActiveTab; label: string; icon: React.ElementType }[] = [
|
|
@@ -66,6 +67,10 @@ export function Sidebar() {
|
|
|
66
67
|
const sidebarActiveTab = useSettingsStore((s) => s.sidebarActiveTab);
|
|
67
68
|
const setSidebarActiveTab = useSettingsStore((s) => s.setSidebarActiveTab);
|
|
68
69
|
const contributions = useExtensionStore((s) => s.contributions);
|
|
70
|
+
const gitChangesCount = useGitStatusStore((s) =>
|
|
71
|
+
activeProject?.name ? (s.counts.get(activeProject.name) ?? 0) : 0,
|
|
72
|
+
);
|
|
73
|
+
useGitChangesPoller(activeProject?.name, sidebarActiveTab === "git");
|
|
69
74
|
|
|
70
75
|
// Build tabs list: built-in + extension-contributed sidebar views
|
|
71
76
|
const TABS = useMemo(() => {
|
|
@@ -108,13 +113,18 @@ export function Sidebar() {
|
|
|
108
113
|
key={tab.id}
|
|
109
114
|
onClick={() => setSidebarActiveTab(tab.id)}
|
|
110
115
|
className={cn(
|
|
111
|
-
"flex-1 flex items-center justify-center gap-1.5 h-full text-xs transition-colors border-b-2 -mb-px",
|
|
116
|
+
"flex-1 flex items-center justify-center gap-1.5 h-full text-xs transition-colors border-b-2 -mb-px relative",
|
|
112
117
|
isActive
|
|
113
118
|
? "border-primary text-primary font-medium"
|
|
114
119
|
: "border-transparent text-text-secondary hover:text-foreground",
|
|
115
120
|
)}
|
|
116
121
|
>
|
|
117
122
|
<Icon className="size-3.5" title={tab.label} />
|
|
123
|
+
{tab.id === "git" && gitChangesCount > 0 && (
|
|
124
|
+
<span className="absolute top-1 right-1 min-w-[16px] h-4 px-1 flex items-center justify-center rounded-full bg-primary text-primary-foreground text-[10px] font-medium leading-none">
|
|
125
|
+
{gitChangesCount > 99 ? "99+" : gitChangesCount}
|
|
126
|
+
</span>
|
|
127
|
+
)}
|
|
118
128
|
</button>
|
|
119
129
|
);
|
|
120
130
|
})}
|
|
@@ -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) {
|