@hienlh/ppm 0.9.84 → 0.9.86

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