@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.
Files changed (189) hide show
  1. package/260413-1354-new-file-editor-tab/reports/code-reviewer-260413-1420-new-file-tab-review.md +210 -0
  2. package/CHANGELOG.md +20 -0
  3. package/bun.lock +259 -9
  4. package/dist/web/assets/{_basePickBy-5PGDJbfF.js → _basePickBy-D-bUmjma.js} +1 -1
  5. package/dist/web/assets/{_baseUniq-BT4Ow4Kk.js → _baseUniq-BnXXIfRB.js} +1 -1
  6. package/dist/web/assets/ai-settings-section-D6d-RmR6.js +1 -0
  7. package/dist/web/assets/{api-settings-Bn-bIxD1.js → api-settings-Qi2xRiHa.js} +1 -1
  8. package/dist/web/assets/{arc-BAOivWpI.js → arc-DB9vXGzd.js} +1 -1
  9. package/dist/web/assets/architecture-PBZL5I3N-DpVzOETR.js +1 -0
  10. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Z-4eN4za.js → architectureDiagram-2XIMDMQ5-BBV25747.js} +1 -1
  11. package/dist/web/assets/{blockDiagram-WCTKOSBZ-BCLqzhuZ.js → blockDiagram-WCTKOSBZ-BOTnY2Lq.js} +1 -1
  12. package/dist/web/assets/{c4Diagram-IC4MRINW-0Vp0Jeas.js → c4Diagram-IC4MRINW-D7QAUdHD.js} +1 -1
  13. package/dist/web/assets/channel-Cgy1thYT.js +1 -0
  14. package/dist/web/assets/chat-tab-DXBb9Y3U.js +10 -0
  15. package/dist/web/assets/check-ePA3ZvK4.js +1 -0
  16. package/dist/web/assets/chevron-down-EQA06nR-.js +1 -0
  17. package/dist/web/assets/{chunk-4BX2VUAB-D4tOov49.js → chunk-4BX2VUAB-BnOVw77D.js} +1 -1
  18. package/dist/web/assets/{chunk-55IACEB6-DJ6BynZ4.js → chunk-55IACEB6-BftA8DxR.js} +1 -1
  19. package/dist/web/assets/{chunk-7E7YKBS2-CiyUJxNI.js → chunk-7E7YKBS2-B0vnP8v3.js} +1 -1
  20. package/dist/web/assets/{chunk-7R4GIKGN-Dv-4cAYn.js → chunk-7R4GIKGN-Czlaj26D.js} +2 -2
  21. package/dist/web/assets/{chunk-C72U2L5F-D21mS_6G.js → chunk-C72U2L5F-DpEbDtMo.js} +1 -1
  22. package/dist/web/assets/{chunk-EGIJ26TM-DzqmU2Z7.js → chunk-EGIJ26TM-BWXe6lkx.js} +1 -1
  23. package/dist/web/assets/{chunk-FMBD7UC4-DXncblvW.js → chunk-FMBD7UC4-DspqhPfk.js} +1 -1
  24. package/dist/web/assets/{chunk-GEFDOKGD-D-pKjlVd.js → chunk-GEFDOKGD-D6HHRbYk.js} +1 -1
  25. package/dist/web/assets/chunk-GLR3WWYH-CxUl1sdz.js +2 -0
  26. package/dist/web/assets/chunk-HHEYEP7N-DN7ebS2Y.js +1 -0
  27. package/dist/web/assets/{chunk-JSJVCQXG-99JzIdPr.js → chunk-JSJVCQXG-BC8wnMwf.js} +1 -1
  28. package/dist/web/assets/{chunk-KX2RTZJC-CRq1OBZv.js → chunk-KX2RTZJC-D3VDtyvX.js} +1 -1
  29. package/dist/web/assets/{chunk-KYZI473N-Bb0MCaIO.js → chunk-KYZI473N-Z-NBw_HS.js} +1 -1
  30. package/dist/web/assets/{chunk-L3YUKLVL-C7qGJrfV.js → chunk-L3YUKLVL--RGkEh__.js} +1 -1
  31. package/dist/web/assets/{chunk-MX3YWQON-BpS_PtKp.js → chunk-MX3YWQON-2B76t_Kx.js} +1 -1
  32. package/dist/web/assets/{chunk-NQ4KR5QH-z_blpjxi.js → chunk-NQ4KR5QH-BekY3tEi.js} +1 -1
  33. package/dist/web/assets/{chunk-O4XLMI2P-nDhi_cVu.js → chunk-O4XLMI2P-2CJLfx_1.js} +1 -1
  34. package/dist/web/assets/{chunk-OZEHJAEY-BXhYx3nO.js → chunk-OZEHJAEY-sug_L09P.js} +1 -1
  35. package/dist/web/assets/{chunk-PQ6SQG4A-TF58UVMU.js → chunk-PQ6SQG4A-_fwPRLQy.js} +1 -1
  36. package/dist/web/assets/{chunk-PU5JKC2W-ek7k4QVB.js → chunk-PU5JKC2W-BUaTFJVQ.js} +1 -1
  37. package/dist/web/assets/chunk-QZHKN3VN-C4La7oLj.js +1 -0
  38. package/dist/web/assets/{chunk-R5LLSJPH-CFwSJijQ.js → chunk-R5LLSJPH-C37xW0vj.js} +1 -1
  39. package/dist/web/assets/{chunk-WL4C6EOR-ByUrSRin.js → chunk-WL4C6EOR-CCkt_MT6.js} +1 -1
  40. package/dist/web/assets/{chunk-XIRO2GV7-Djlmrely.js → chunk-XIRO2GV7-Dz2LBq7Y.js} +1 -1
  41. package/dist/web/assets/{chunk-XPW4576I-BPQQBakK.js → chunk-XPW4576I-DenTbBuj.js} +1 -1
  42. package/dist/web/assets/{chunk-XZSTWKYB-DxAOx4hG.js → chunk-XZSTWKYB-Dbp1nUSQ.js} +1 -1
  43. package/dist/web/assets/{chunk-YBOYWFTD-rQG3QH5s.js → chunk-YBOYWFTD-3OTKowjE.js} +1 -1
  44. package/dist/web/assets/classDiagram-VBA2DB6C-C3IyfqG-.js +1 -0
  45. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Dcvhz2pb.js +1 -0
  46. package/dist/web/assets/clone--C7Tby8z.js +1 -0
  47. package/dist/web/assets/code-editor-Cr7JrBKC.js +8 -0
  48. package/dist/web/assets/{cose-bilkent-S5V4N54A-B_AWZsOP.js → cose-bilkent-S5V4N54A-MbmGZnt0.js} +1 -1
  49. package/dist/web/assets/{csv-preview-D2pJJj3K.js → csv-preview-uZ_7b8I7.js} +1 -1
  50. package/dist/web/assets/{dagre-DHq9bhnd.js → dagre-CPhI6v-K.js} +1 -1
  51. package/dist/web/assets/{dagre-KLK3FWXG-BdJr7Byp.js → dagre-KLK3FWXG-CmSE-oNj.js} +1 -1
  52. package/dist/web/assets/database-D1ToEV9d.js +1 -0
  53. package/dist/web/assets/{database-viewer-0P_zRC9w.js → database-viewer-5xljX0JI.js} +2 -2
  54. package/dist/web/assets/{diagram-E7M64L7V-_db4pBVA.js → diagram-E7M64L7V-B5XG3ZT7.js} +1 -1
  55. package/dist/web/assets/{diagram-IFDJBPK2-xKoeuiJx.js → diagram-IFDJBPK2-BsP248aX.js} +1 -1
  56. package/dist/web/assets/{diagram-P4PSJMXO-C8tjJsev.js → diagram-P4PSJMXO-Cna3408N.js} +1 -1
  57. package/dist/web/assets/diff-viewer-BBr6e_gb.js +4 -0
  58. package/dist/web/assets/dist-KUoHa6tg.js +1 -0
  59. package/dist/web/assets/{erDiagram-INFDFZHY-BSh2z9Df.js → erDiagram-INFDFZHY-B7SgktiR.js} +1 -1
  60. package/dist/web/assets/{extension-webview-Cx0GpRyC.js → extension-webview-B0klBip8.js} +1 -1
  61. package/dist/web/assets/eye-CNcBU6Tx.js +1 -0
  62. package/dist/web/assets/{flowDiagram-PKNHOUZH-oYaovqyp.js → flowDiagram-PKNHOUZH-FOYZZ1OB.js} +1 -1
  63. package/dist/web/assets/{ganttDiagram-A5KZAMGK-DmL26q2P.js → ganttDiagram-A5KZAMGK-CnHVYh9v.js} +1 -1
  64. package/dist/web/assets/git-graph-CDiwGa0g.js +1 -0
  65. package/dist/web/assets/gitGraph-HDMCJU4V-DcPyMEIJ.js +1 -0
  66. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-CMoukSrY.js → gitGraphDiagram-K3NZZRJ6-0G9XxZay.js} +1 -1
  67. package/dist/web/assets/{graphlib-BcsNnGcW.js → graphlib-CNiBwlg_.js} +1 -1
  68. package/dist/web/assets/index-CkaCzNgO.css +2 -0
  69. package/dist/web/assets/index-Ic5uTu20.js +26 -0
  70. package/dist/web/assets/info-3K5VOQVL-Dw4O15cw.js +1 -0
  71. package/dist/web/assets/infoDiagram-LFFYTUFH-DFhmsucr.js +2 -0
  72. package/dist/web/assets/input-CcbTF6ih.js +45 -0
  73. package/dist/web/assets/{isEmpty-bnrF3Qbc.js → isEmpty-CcCb5n2-.js} +1 -1
  74. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-D05_LyL7.js → ishikawaDiagram-PHBUUO56-D4QCzh5J.js} +1 -1
  75. package/dist/web/assets/{journeyDiagram-4ABVD52K-B_L20qMe.js → journeyDiagram-4ABVD52K-CnHYNfKW.js} +1 -1
  76. package/dist/web/assets/{kanban-definition-K7BYSVSG-CZ535BbZ.js → kanban-definition-K7BYSVSG-Bh_g3EVu.js} +1 -1
  77. package/dist/web/assets/keybindings-store-CxE6BlG2.js +1 -0
  78. package/dist/web/assets/{line-CVvo3dRu.js → line-6d3eBADm.js} +1 -1
  79. package/dist/web/assets/{linear-DP4mkX3m.js → linear-cA_2lQy7.js} +1 -1
  80. package/dist/web/assets/markdown-renderer-CZ07F7T6.js +306 -0
  81. package/dist/web/assets/{mermaid-parser.core-C7UwoIh6.js → mermaid-parser.core-C3kd7JXM.js} +2 -2
  82. package/dist/web/assets/{mindmap-definition-YRQLILUH-x0MTutJp.js → mindmap-definition-YRQLILUH-CYiUwhr_.js} +1 -1
  83. package/dist/web/assets/{ordinal-_K3x1fkz.js → ordinal-XHK5vIzZ.js} +1 -1
  84. package/dist/web/assets/packet-RMMSAZCW-o3LmdL8H.js +1 -0
  85. package/dist/web/assets/pie-UPGHQEXC-BjNP0M3B.js +1 -0
  86. package/dist/web/assets/{pieDiagram-SKSYHLDU-C1Gjrtzy.js → pieDiagram-SKSYHLDU-D0S7jeZA.js} +1 -1
  87. package/dist/web/assets/plus-Iso5r9vD.js +1 -0
  88. package/dist/web/assets/port-forwarding-tab-BPuSc6pI.js +1 -0
  89. package/dist/web/assets/{postgres-viewer-DlCLiEGU.js → postgres-viewer-RldlAO_m.js} +3 -3
  90. package/dist/web/assets/{quadrantDiagram-337W2JSQ-C8bzJCjQ.js → quadrantDiagram-337W2JSQ-0hNP63hW.js} +1 -1
  91. package/dist/web/assets/radar-KQ55EAFF-gDgOiaME.js +1 -0
  92. package/dist/web/assets/refresh-cw-BgQzFNaG.js +1 -0
  93. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-pQyah6WB.js → requirementDiagram-Z7DCOOCP-BVnmqFbL.js} +1 -1
  94. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-T6RgG-N8.js → sankeyDiagram-WA2Y5GQK-DVkYdCJb.js} +1 -1
  95. package/dist/web/assets/scroll-area-i4EZlOl_.js +1 -0
  96. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-BQDJ4CVs.js → sequenceDiagram-2WXFIKYE-B80s7sOg.js} +1 -1
  97. package/dist/web/assets/settings-tab-BzSSN2BQ.js +1 -0
  98. package/dist/web/assets/{sql-query-editor-Dxx-QZaI.js → sql-query-editor-CjZ7Z6XL.js} +1 -1
  99. package/dist/web/assets/sqlite-viewer-CoyZOM_Y.js +1 -0
  100. package/dist/web/assets/{stateDiagram-RAJIS63D-66vhiIuk.js → stateDiagram-RAJIS63D-BPLXgXRR.js} +1 -1
  101. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DksQJ7es.js +1 -0
  102. package/dist/web/assets/{terminal-tab-yfmxGQ5e.js → terminal-tab-DjzD8GLn.js} +2 -2
  103. package/dist/web/assets/{timeline-definition-YZTLITO2-DwZqB3nn.js → timeline-definition-YZTLITO2-fa_51u1X.js} +1 -1
  104. package/dist/web/assets/trash-2-DYCa06CV.js +1 -0
  105. package/dist/web/assets/treemap-KZPCXAKY-DwFqAvnj.js +1 -0
  106. package/dist/web/assets/{use-monaco-theme-7qyyJae5.js → use-monaco-theme-D9XFxQuU.js} +1 -1
  107. package/dist/web/assets/{vennDiagram-LZ73GAT5-s9Z71fz-.js → vennDiagram-LZ73GAT5-kX4jJn6W.js} +1 -1
  108. package/dist/web/assets/x-BXecj-16.js +1 -0
  109. package/dist/web/assets/{xychartDiagram-JWTSCODW-DRa_TH4B.js → xychartDiagram-JWTSCODW-Bzm5lZBs.js} +1 -1
  110. package/dist/web/index.html +22 -12
  111. package/dist/web/sw.js +1 -1
  112. package/package.json +9 -3
  113. package/src/server/index.ts +1 -1
  114. package/src/server/routes/settings.ts +6 -3
  115. package/src/services/cloud.service.ts +35 -4
  116. package/src/web/components/editor/code-editor.tsx +67 -4
  117. package/src/web/components/editor/save-as-dialog.tsx +75 -0
  118. package/src/web/components/git/git-status-panel.tsx +7 -1
  119. package/src/web/components/layout/command-palette.tsx +2 -0
  120. package/src/web/components/layout/draggable-tab.tsx +120 -67
  121. package/src/web/components/layout/mobile-nav.tsx +69 -2
  122. package/src/web/components/layout/sidebar.tsx +11 -1
  123. package/src/web/components/layout/tab-bar.tsx +74 -1
  124. package/src/web/components/layout/upgrade-banner.tsx +3 -0
  125. package/src/web/components/shared/markdown-code-block.tsx +142 -0
  126. package/src/web/components/shared/markdown-context.ts +20 -0
  127. package/src/web/components/shared/markdown-renderer.tsx +113 -288
  128. package/src/web/hooks/use-global-keybindings.ts +7 -0
  129. package/src/web/main.tsx +1 -0
  130. package/src/web/stores/git-status-store.ts +55 -0
  131. package/src/web/stores/keybindings-store.ts +1 -0
  132. package/src/web/stores/panel-utils.ts +13 -0
  133. package/src/web/stores/tab-store.ts +16 -0
  134. package/src/web/styles/globals.css +6 -0
  135. package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +0 -1
  136. package/dist/web/assets/channel-By7bn0Yq.js +0 -1
  137. package/dist/web/assets/chat-tab-DBJJz0Dm.js +0 -10
  138. package/dist/web/assets/chunk-GLR3WWYH-DKikpoJM.js +0 -2
  139. package/dist/web/assets/chunk-HHEYEP7N-C7vxA5i9.js +0 -1
  140. package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +0 -1
  141. package/dist/web/assets/classDiagram-VBA2DB6C-BA8Nj-_C.js +0 -1
  142. package/dist/web/assets/classDiagram-v2-RAHNMMFH-DjYu-6mn.js +0 -1
  143. package/dist/web/assets/clone-LRxlvnMj.js +0 -1
  144. package/dist/web/assets/code-editor-BvSFsrGo.js +0 -8
  145. package/dist/web/assets/diff-viewer-BmBJq4gO.js +0 -4
  146. package/dist/web/assets/dist-DIV6WgAG.js +0 -41
  147. package/dist/web/assets/git-graph-BAlhf058.js +0 -1
  148. package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +0 -1
  149. package/dist/web/assets/index-BDAqXmpQ.js +0 -30
  150. package/dist/web/assets/index-BYXjCNlK.css +0 -2
  151. package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +0 -1
  152. package/dist/web/assets/infoDiagram-LFFYTUFH-DWwumDkq.js +0 -2
  153. package/dist/web/assets/keybindings-store-L7UlPjK0.js +0 -1
  154. package/dist/web/assets/markdown-renderer-CYs_lrjt.js +0 -326
  155. package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +0 -1
  156. package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +0 -1
  157. package/dist/web/assets/port-forwarding-tab-CSHJ7gxM.js +0 -1
  158. package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +0 -1
  159. package/dist/web/assets/settings-tab-Dcr7lDcJ.js +0 -1
  160. package/dist/web/assets/sqlite-viewer-B0052fC-.js +0 -1
  161. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-BGVqj_g9.js +0 -1
  162. package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +0 -1
  163. package/dist/web/assets/x-D2_KzIET.js +0 -1
  164. /package/dist/web/assets/{api-client-BfBM3I7n.js → api-client-wQbeUyeh.js} +0 -0
  165. /package/dist/web/assets/{array-B9UHiPd-.js → array-X0JlPOfd.js} +0 -0
  166. /package/dist/web/assets/{arrow-up-BYhx9ckd.js → arrow-up-BigIMx-e.js} +0 -0
  167. /package/dist/web/assets/{chevron-right-4zq1jPv6.js → chevron-right-CXzzT44u.js} +0 -0
  168. /package/dist/web/assets/{columns-2-BoZAN-iw.js → columns-2-BZ9uqssV.js} +0 -0
  169. /package/dist/web/assets/{csv-parser-CNNw2RVA.js → csv-parser-CElqio6o.js} +0 -0
  170. /package/dist/web/assets/{cytoscape.esm-BW-DbntU.js → cytoscape.esm-BfIOPvwt.js} +0 -0
  171. /package/dist/web/assets/{defaultLocale-5eAKkKJC.js → defaultLocale-B6RGN4id.js} +0 -0
  172. /package/dist/web/assets/{dist-CSJdAyA9.js → dist-CK1enexV.js} +0 -0
  173. /package/dist/web/assets/{init-DlZdxViB.js → init-BmUWJJHz.js} +0 -0
  174. /package/dist/web/assets/{isArrayLikeObject-B_v2FtYn.js → isArrayLikeObject-BrCM-iA1.js} +0 -0
  175. /package/dist/web/assets/{jsx-runtime-kMwlnEGE.js → jsx-runtime-R_NjdZtX.js} +0 -0
  176. /package/dist/web/assets/{katex-Bqvo_ZG0.js → katex-xQS_6bNb.js} +0 -0
  177. /package/dist/web/assets/{lib-DurwGtQO.js → lib-CfWBrYll.js} +0 -0
  178. /package/dist/web/assets/{math-069Z4SuC.js → math-CpLFzrfV.js} +0 -0
  179. /package/dist/web/assets/{path-6uRLdFF7.js → path-CoPyR7c2.js} +0 -0
  180. /package/dist/web/assets/{preload-helper-Bf_JiD2A.js → preload-helper-CH6UZRzu.js} +0 -0
  181. /package/dist/web/assets/{react-SKk5z-bm.js → react-j5zqhEum.js} +0 -0
  182. /package/dist/web/assets/{rough.esm-JX0wREDd.js → rough.esm-D5NinLFK.js} +0 -0
  183. /package/dist/web/assets/{sql-completion-provider-DM9Qov6L.js → sql-completion-provider-D0xutVaK.js} +0 -0
  184. /package/dist/web/assets/{square-oPKIkJiw.js → square-pfn_LYYy.js} +0 -0
  185. /package/dist/web/assets/{src-BqX54PbV.js → src-j04igtQ5.js} +0 -0
  186. /package/dist/web/assets/{table-DFevCOMd.js → table-CHv2x_qg.js} +0 -0
  187. /package/dist/web/assets/{tag-CXMT0QB6.js → tag-Bb_UFXt0.js} +0 -0
  188. /package/dist/web/assets/{text-wrap-BWNOVswA.js → text-wrap-D8BbQYTx.js} +0 -0
  189. /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
- <button
65
- ref={tabRef}
66
- data-tab-item
67
- draggable={!editing}
68
- onClick={onSelect}
69
- onAuxClick={(e) => { if (e.button === 1 && tab.closable) { e.preventDefault(); onClose(); } }}
70
- onDragStart={onDragStart}
71
- onDragOver={onDragOver}
72
- onDragEnd={onDragEnd}
73
- onTouchStart={onTouchStart}
74
- onTouchMove={onTouchMove}
75
- onTouchEnd={onTouchEnd}
76
- style={colorStyle}
77
- className={cn(
78
- "group flex items-center gap-1 px-3 h-10 whitespace-nowrap text-xs transition-colors",
79
- "border-b-2 -mb-px cursor-grab active:cursor-grabbing",
80
- !colorStyle && (isActive
81
- ? "border-primary text-primary"
82
- : "border-transparent text-text-secondary hover:text-foreground"),
83
- colorStyle && "border-transparent",
84
- )}
85
- >
86
- <span className="relative">
87
- <Icon className="size-4" />
88
- {notificationType && !isActive && (
89
- <span className={cn("absolute -top-1 -right-1 size-2 rounded-full", notificationColor(notificationType))} />
90
- )}
91
- </span>
92
- {editing ? (
93
- <input
94
- ref={inputRef}
95
- value={editValue}
96
- onChange={(e) => setEditValue(e.target.value)}
97
- onBlur={commitRename}
98
- onKeyDown={(e) => {
99
- if (e.key === "Enter") commitRename();
100
- if (e.key === "Escape") setEditing(false);
101
- e.stopPropagation();
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 < tabs.length - 1 && (
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) {