@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.
Files changed (199) 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 +13 -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-Camu01H4.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-pU1xJyoc.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-BQdPMowm.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-CY61vWBg.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-TIJmxHl6.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-BHn-LEm7.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/web/components/editor/code-editor.tsx +67 -4
  115. package/src/web/components/editor/save-as-dialog.tsx +75 -0
  116. package/src/web/components/layout/command-palette.tsx +2 -0
  117. package/src/web/components/layout/draggable-tab.tsx +120 -67
  118. package/src/web/components/layout/mobile-nav.tsx +69 -2
  119. package/src/web/components/layout/tab-bar.tsx +74 -1
  120. package/src/web/components/layout/upgrade-banner.tsx +3 -0
  121. package/src/web/components/shared/markdown-code-block.tsx +142 -0
  122. package/src/web/components/shared/markdown-context.ts +20 -0
  123. package/src/web/components/shared/markdown-renderer.tsx +113 -288
  124. package/src/web/hooks/use-global-keybindings.ts +7 -0
  125. package/src/web/main.tsx +1 -0
  126. package/src/web/stores/keybindings-store.ts +1 -0
  127. package/src/web/stores/panel-utils.ts +13 -0
  128. package/src/web/stores/tab-store.ts +16 -0
  129. package/src/web/styles/globals.css +6 -0
  130. package/.opencode/.env.example +0 -98
  131. package/.opencode/skills/ads-management/scripts/.env.example +0 -13
  132. package/.opencode/skills/ai-multimodal/.env.example +0 -230
  133. package/.opencode/skills/cip-design/.env.example +0 -6
  134. package/.opencode/skills/devops/.env.example +0 -76
  135. package/.opencode/skills/docs-seeker/.env.example +0 -15
  136. package/.opencode/skills/elevenlabs/.env.example +0 -3
  137. package/.opencode/skills/marketing-dashboard/.env.example +0 -15
  138. package/.opencode/skills/marketing-dashboard/app/.env.example +0 -2
  139. package/.opencode/skills/marketing-dashboard/server/.env.example +0 -2
  140. package/.opencode/skills/mcp-management/scripts/dist/analyze-tools.js +0 -70
  141. package/.opencode/skills/mcp-management/scripts/dist/cli.js +0 -160
  142. package/.opencode/skills/mcp-management/scripts/dist/mcp-client.js +0 -183
  143. package/.opencode/skills/payment-integration/scripts/.env.example +0 -20
  144. package/.opencode/skills/sequential-thinking/.env.example +0 -8
  145. package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +0 -1
  146. package/dist/web/assets/channel-By7bn0Yq.js +0 -1
  147. package/dist/web/assets/chat-tab-CT2XUgsc.js +0 -10
  148. package/dist/web/assets/chunk-GLR3WWYH-DKikpoJM.js +0 -2
  149. package/dist/web/assets/chunk-HHEYEP7N-C7vxA5i9.js +0 -1
  150. package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +0 -1
  151. package/dist/web/assets/classDiagram-VBA2DB6C-BA8Nj-_C.js +0 -1
  152. package/dist/web/assets/classDiagram-v2-RAHNMMFH-DjYu-6mn.js +0 -1
  153. package/dist/web/assets/clone-LRxlvnMj.js +0 -1
  154. package/dist/web/assets/code-editor-DQiPtcNd.js +0 -8
  155. package/dist/web/assets/diff-viewer-CTwcVIP_.js +0 -4
  156. package/dist/web/assets/dist-DIV6WgAG.js +0 -41
  157. package/dist/web/assets/git-graph-BnFbmpom.js +0 -1
  158. package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +0 -1
  159. package/dist/web/assets/index-CP9KnaGh.js +0 -30
  160. package/dist/web/assets/index-Cxz7oGXY.css +0 -2
  161. package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +0 -1
  162. package/dist/web/assets/infoDiagram-LFFYTUFH-DWwumDkq.js +0 -2
  163. package/dist/web/assets/keybindings-store-DdhEeehv.js +0 -1
  164. package/dist/web/assets/markdown-renderer-BjYurPV4.js +0 -326
  165. package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +0 -1
  166. package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +0 -1
  167. package/dist/web/assets/port-forwarding-tab-Bgr8dmsw.js +0 -1
  168. package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +0 -1
  169. package/dist/web/assets/settings-tab-BNoboN6E.js +0 -1
  170. package/dist/web/assets/sqlite-viewer-srSbGg1D.js +0 -1
  171. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-BGVqj_g9.js +0 -1
  172. package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +0 -1
  173. package/dist/web/assets/x-D2_KzIET.js +0 -1
  174. /package/dist/web/assets/{api-client-BfBM3I7n.js → api-client-wQbeUyeh.js} +0 -0
  175. /package/dist/web/assets/{array-B9UHiPd-.js → array-X0JlPOfd.js} +0 -0
  176. /package/dist/web/assets/{arrow-up-BYhx9ckd.js → arrow-up-BigIMx-e.js} +0 -0
  177. /package/dist/web/assets/{chevron-right-4zq1jPv6.js → chevron-right-CXzzT44u.js} +0 -0
  178. /package/dist/web/assets/{columns-2-BoZAN-iw.js → columns-2-BZ9uqssV.js} +0 -0
  179. /package/dist/web/assets/{csv-parser-CNNw2RVA.js → csv-parser-CElqio6o.js} +0 -0
  180. /package/dist/web/assets/{cytoscape.esm-BW-DbntU.js → cytoscape.esm-BfIOPvwt.js} +0 -0
  181. /package/dist/web/assets/{defaultLocale-5eAKkKJC.js → defaultLocale-B6RGN4id.js} +0 -0
  182. /package/dist/web/assets/{dist-CSJdAyA9.js → dist-CK1enexV.js} +0 -0
  183. /package/dist/web/assets/{init-DlZdxViB.js → init-BmUWJJHz.js} +0 -0
  184. /package/dist/web/assets/{isArrayLikeObject-B_v2FtYn.js → isArrayLikeObject-BrCM-iA1.js} +0 -0
  185. /package/dist/web/assets/{jsx-runtime-kMwlnEGE.js → jsx-runtime-R_NjdZtX.js} +0 -0
  186. /package/dist/web/assets/{katex-Bqvo_ZG0.js → katex-xQS_6bNb.js} +0 -0
  187. /package/dist/web/assets/{lib-DurwGtQO.js → lib-CfWBrYll.js} +0 -0
  188. /package/dist/web/assets/{math-069Z4SuC.js → math-CpLFzrfV.js} +0 -0
  189. /package/dist/web/assets/{path-6uRLdFF7.js → path-CoPyR7c2.js} +0 -0
  190. /package/dist/web/assets/{preload-helper-Bf_JiD2A.js → preload-helper-CH6UZRzu.js} +0 -0
  191. /package/dist/web/assets/{react-SKk5z-bm.js → react-j5zqhEum.js} +0 -0
  192. /package/dist/web/assets/{rough.esm-JX0wREDd.js → rough.esm-D5NinLFK.js} +0 -0
  193. /package/dist/web/assets/{sql-completion-provider-DM9Qov6L.js → sql-completion-provider-D0xutVaK.js} +0 -0
  194. /package/dist/web/assets/{square-oPKIkJiw.js → square-pfn_LYYy.js} +0 -0
  195. /package/dist/web/assets/{src-BqX54PbV.js → src-j04igtQ5.js} +0 -0
  196. /package/dist/web/assets/{table-DFevCOMd.js → table-CHv2x_qg.js} +0 -0
  197. /package/dist/web/assets/{tag-CXMT0QB6.js → tag-Bb_UFXt0.js} +0 -0
  198. /package/dist/web/assets/{text-wrap-BWNOVswA.js → text-wrap-D8BbQYTx.js} +0 -0
  199. /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
- <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
  }
@@ -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);