@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
@@ -0,0 +1,800 @@
1
+ /**
2
+ * @ppm/ext-git-graph — Git Graph extension for PPM.
3
+ * Visualizes git commit history as an interactive graph in a webview.
4
+ */
5
+ import type { ExtensionContext } from "@ppm/vscode-compat";
6
+ import type { SpawnResult } from "@ppm/vscode-compat/src/process.ts";
7
+ import type { GitGraphSettings, WebviewToExt, Worktree } from "./types.ts";
8
+ import { DEFAULT_SETTINGS } from "./types.ts";
9
+ import { getWebviewHtml } from "./webview-html.ts";
10
+
11
+ interface VscodeApi {
12
+ commands: {
13
+ registerCommand(command: string, callback: (...args: unknown[]) => unknown): { dispose(): void };
14
+ };
15
+ window: {
16
+ showErrorMessage(message: string, ...items: string[]): Promise<string | undefined>;
17
+ showInformationMessage(message: string, ...items: string[]): Promise<string | undefined>;
18
+ openTab(tabType: string, title: string, projectId: string | null, metadata?: Record<string, unknown>): Promise<void>;
19
+ switchProject(projectName: string): Promise<void>;
20
+ createWebviewPanel(viewType: string, title: string, showOptions: unknown): {
21
+ webview: {
22
+ html: string;
23
+ onDidReceiveMessage: (listener: (msg: unknown) => void) => { dispose(): void };
24
+ postMessage(message: unknown): Promise<boolean>;
25
+ };
26
+ onDidDispose: (listener: () => void) => { dispose(): void };
27
+ dispose(): void;
28
+ };
29
+ };
30
+ process: {
31
+ spawn(cmd: string, args: string[], cwd: string, options?: { timeout?: number; env?: Record<string, string> }): Promise<SpawnResult>;
32
+ };
33
+ ViewColumn: { Active: number };
34
+ }
35
+
36
+ let baseUrl = "";
37
+
38
+ // Track active panel state for reuse across project switches
39
+ let activePanel: ReturnType<VscodeApi["window"]["createWebviewPanel"]> | null = null;
40
+ let activeProjectPath = "";
41
+
42
+ function getSettings(context: ExtensionContext): GitGraphSettings {
43
+ return { ...DEFAULT_SETTINGS, ...(context.globalState.get<Partial<GitGraphSettings>>("settings") || {}) };
44
+ }
45
+
46
+ const VALID_SETTING_KEYS = new Set<string>([
47
+ "maxCommits", "showTags", "showStashes", "showRemoteBranches", "graphStyle",
48
+ "firstParentOnly", "dateFormat", "commitOrdering", "issueLinkingRules", "prCreation",
49
+ "autoFetchInterval",
50
+ ]);
51
+
52
+ async function saveSetting(context: ExtensionContext, key: string, value: unknown): Promise<GitGraphSettings> {
53
+ if (!VALID_SETTING_KEYS.has(key)) throw new Error(`Invalid setting key: ${key}`);
54
+ const settings = getSettings(context);
55
+ (settings as any)[key] = value;
56
+ await context.globalState.update("settings", settings);
57
+ return settings;
58
+ }
59
+
60
+ export function activate(context: ExtensionContext, vscode: VscodeApi): void {
61
+ baseUrl = (globalThis as any).__PPM_BASE_URL__ || "";
62
+
63
+ context.subscriptions.push(
64
+ vscode.commands.registerCommand("git-graph.view", async (...args: unknown[]) => {
65
+ const projectPath = args[0] as string | undefined;
66
+ const resolvedPath = projectPath || await resolveProjectPath();
67
+ if (!resolvedPath) {
68
+ await vscode.window.showErrorMessage("Git Graph: No project selected. Open a project first, then try again.");
69
+ return;
70
+ }
71
+ await openGitGraph(vscode, context, resolvedPath);
72
+ }),
73
+ );
74
+
75
+ console.log("[ext-git-graph] activated");
76
+ }
77
+
78
+ export function deactivate(): void {
79
+ console.log("[ext-git-graph] deactivated");
80
+ }
81
+
82
+ /** Resolve project path from PPM API as fallback */
83
+ async function resolveProjectPath(): Promise<string | null> {
84
+ try {
85
+ const res = await fetch(`${baseUrl}/api/projects`);
86
+ const json = await res.json() as { ok: boolean; data?: { name: string; path: string }[] };
87
+ if (!json.ok || !json.data || json.data.length === 0) return null;
88
+ // Single project — safe to auto-select
89
+ if (json.data.length === 1) return json.data[0].path;
90
+ // Multiple projects — cannot guess which is active, return null
91
+ return null;
92
+ } catch {}
93
+ return null;
94
+ }
95
+
96
+ /** Resolve project name from path via PPM API */
97
+ async function resolveProjectName(projectPath: string): Promise<string> {
98
+ try {
99
+ const res = await fetch(`${baseUrl}/api/projects`);
100
+ const json = await res.json() as { ok: boolean; data?: { name: string; path: string }[] };
101
+ if (json.ok && json.data) {
102
+ const match = json.data.find((p) => p.path === projectPath);
103
+ if (match) return match.name;
104
+ }
105
+ } catch {}
106
+ // Fallback to directory name
107
+ return projectPath.split(/[\\/]/).filter(Boolean).pop() || "project";
108
+ }
109
+
110
+ /** Spawn git and return result */
111
+ async function spawnGit(
112
+ vscode: VscodeApi,
113
+ args: string[],
114
+ cwd: string,
115
+ timeout = 30_000,
116
+ ): Promise<SpawnResult> {
117
+ return vscode.process.spawn("git", args, cwd, {
118
+ timeout,
119
+ env: { GIT_TERMINAL_PROMPT: "0" },
120
+ });
121
+ }
122
+
123
+ function openGitGraph(
124
+ vscode: VscodeApi,
125
+ context: ExtensionContext,
126
+ projectPath: string,
127
+ ): void {
128
+ // If panel exists and project changed, reload data in the existing panel
129
+ if (activePanel && activeProjectPath !== projectPath) {
130
+ activeProjectPath = projectPath;
131
+ reloadPanelData(vscode, activePanel, projectPath, context);
132
+ return;
133
+ }
134
+ // If panel exists with same project, nothing to do
135
+ if (activePanel) return;
136
+
137
+ activeProjectPath = projectPath;
138
+ const dirName = projectPath.split(/[\\/]/).filter(Boolean).pop() || "Git Graph";
139
+ const panel = vscode.window.createWebviewPanel(
140
+ "git-graph.view",
141
+ `Git Graph: ${dirName}`,
142
+ vscode.ViewColumn.Active,
143
+ );
144
+ activePanel = panel;
145
+
146
+ panel.webview.html = getWebviewHtml();
147
+
148
+ const msgDisposable = panel.webview.onDidReceiveMessage(async (raw: unknown) => {
149
+ const msg = raw as WebviewToExt;
150
+ // Always use activeProjectPath (not closure) so project switches take effect
151
+ const pp = activeProjectPath;
152
+ try {
153
+ switch (msg.command) {
154
+ case "ready":
155
+ await handleRepoInfo(vscode, panel, pp);
156
+ await handleRequestCommits(vscode, panel, pp, context);
157
+ handleUncommittedStatus(vscode, panel, pp); // fire-and-forget
158
+ handleWorktrees(vscode, panel, pp); // fire-and-forget
159
+ break;
160
+ case "requestRepoInfo":
161
+ await handleRepoInfo(vscode, panel, pp);
162
+ break;
163
+ case "requestCommits":
164
+ await handleRequestCommits(vscode, panel, pp, context, msg.maxCommits, msg.skip, msg.branch);
165
+ break;
166
+ case "requestCommitDetails":
167
+ await handleCommitDetails(vscode, panel, pp, msg.hash);
168
+ break;
169
+ case "requestUncommitted":
170
+ await handleUncommittedStatus(vscode, panel, pp);
171
+ break;
172
+ case "openDiff": {
173
+ assertSafeFilePaths([msg.filePath], pp);
174
+ const fileName = msg.filePath.split(/[\\/]/).pop() || msg.filePath;
175
+ const projectName = await resolveProjectName(pp);
176
+ await vscode.window.openTab("git-diff", `${fileName} (${msg.hash.substring(0, 7)})`, projectName, {
177
+ projectName,
178
+ filePath: msg.filePath,
179
+ ...(msg.parentHash ? { ref1: msg.parentHash } : {}),
180
+ ...(msg.hash !== "uncommitted" && msg.hash !== "staged" ? { ref2: msg.hash } : {}),
181
+ });
182
+ break;
183
+ }
184
+ case "requestSettings":
185
+ await panel.webview.postMessage({ command: "loadSettings", data: getSettings(context) });
186
+ break;
187
+ case "updateSetting": {
188
+ const updated = await saveSetting(context, msg.key, msg.value);
189
+ await panel.webview.postMessage({ command: "loadSettings", data: updated });
190
+ if (["maxCommits", "firstParentOnly", "commitOrdering"].includes(msg.key)) {
191
+ await handleRequestCommits(vscode, panel, pp, context, updated.maxCommits);
192
+ }
193
+ break;
194
+ }
195
+ case "requestUserDetails": {
196
+ const [nameResult, emailResult] = await Promise.all([
197
+ spawnGit(vscode, ["config", "user.name"], pp),
198
+ spawnGit(vscode, ["config", "user.email"], pp),
199
+ ]);
200
+ await panel.webview.postMessage({
201
+ command: "loadUserDetails",
202
+ data: { name: nameResult.stdout.trim(), email: emailResult.stdout.trim() },
203
+ });
204
+ break;
205
+ }
206
+ case "updateUserDetails": {
207
+ if (msg.name !== undefined) await spawnGit(vscode, ["config", "user.name", msg.name], pp);
208
+ if (msg.email !== undefined) await spawnGit(vscode, ["config", "user.email", msg.email], pp);
209
+ const [n, e] = await Promise.all([
210
+ spawnGit(vscode, ["config", "user.name"], pp),
211
+ spawnGit(vscode, ["config", "user.email"], pp),
212
+ ]);
213
+ await panel.webview.postMessage({ command: "loadUserDetails", data: { name: n.stdout.trim(), email: e.stdout.trim() } });
214
+ break;
215
+ }
216
+ case "addRemote": {
217
+ const remoteUrl = String(msg.url || "");
218
+ if (!remoteUrl || remoteUrl.startsWith("-")) throw new Error("Invalid remote URL");
219
+ await spawnGit(vscode, ["remote", "add", assertValidRemote(msg.name), remoteUrl], pp);
220
+ await handleRepoInfo(vscode, panel, pp);
221
+ break;
222
+ }
223
+ case "removeRemote":
224
+ await spawnGit(vscode, ["remote", "remove", assertValidRemote(msg.name)], pp);
225
+ await handleRepoInfo(vscode, panel, pp);
226
+ break;
227
+ case "editRemoteUrl": {
228
+ const editUrl = String(msg.url || "");
229
+ if (!editUrl || editUrl.startsWith("-")) throw new Error("Invalid remote URL");
230
+ await spawnGit(vscode, ["remote", "set-url", assertValidRemote(msg.name), editUrl], pp);
231
+ await handleRepoInfo(vscode, panel, pp);
232
+ break;
233
+ }
234
+ case "requestOwnerRepo": {
235
+ const result = await spawnGit(vscode, ["remote", "get-url", "origin"], pp);
236
+ const url = result.stdout.trim();
237
+ const match = url.match(/[/:]([^/]+)\/([^/.]+?)(?:\.git)?$/);
238
+ await panel.webview.postMessage({
239
+ command: "loadOwnerRepo",
240
+ data: match ? { owner: match[1], repo: match[2] } : { owner: "", repo: "" },
241
+ });
242
+ break;
243
+ }
244
+ case "gitAction":
245
+ if (msg.args?.files && Array.isArray(msg.args.files)) {
246
+ assertSafeFilePaths(msg.args.files as string[], pp);
247
+ }
248
+ if (msg.action === "discard") {
249
+ await handleDiscard(vscode, panel, pp, context, msg.args);
250
+ } else {
251
+ await handleGitAction(vscode, panel, pp, context, msg.action, msg.args);
252
+ }
253
+ break;
254
+ case "openFile": {
255
+ assertSafeFilePaths([msg.filePath], pp);
256
+ const projectName = await resolveProjectName(pp);
257
+ await vscode.window.openTab("editor", msg.filePath, projectName, {
258
+ projectName,
259
+ filePath: msg.filePath,
260
+ });
261
+ break;
262
+ }
263
+ case "requestWorktrees":
264
+ await handleWorktrees(vscode, panel, pp);
265
+ break;
266
+ case "addWorktree": {
267
+ const addArgs = ["worktree", "add"];
268
+ if (msg.newBranch) {
269
+ addArgs.push("-b", assertValidRef(msg.newBranch, "newBranch"));
270
+ }
271
+ addArgs.push(msg.path);
272
+ if (msg.branch) addArgs.push(assertValidRef(msg.branch, "branch"));
273
+ if (msg.startPoint) addArgs.push(assertValidHash(msg.startPoint));
274
+ const addResult = await spawnGit(vscode, addArgs, pp);
275
+ await panel.webview.postMessage({
276
+ command: "actionResult", action: "addWorktree",
277
+ result: { ok: addResult.exitCode === 0, error: addResult.exitCode !== 0 ? addResult.stderr.trim() : undefined },
278
+ });
279
+ if (addResult.exitCode === 0) await handleWorktrees(vscode, panel, pp);
280
+ break;
281
+ }
282
+ case "removeWorktree": {
283
+ const rmArgs = ["worktree", "remove", ...(msg.force ? ["--force"] : []), msg.path];
284
+ const rmResult = await spawnGit(vscode, rmArgs, pp);
285
+ await panel.webview.postMessage({
286
+ command: "actionResult", action: "removeWorktree",
287
+ result: { ok: rmResult.exitCode === 0, error: rmResult.exitCode !== 0 ? rmResult.stderr.trim() : undefined },
288
+ });
289
+ if (rmResult.exitCode === 0) await handleWorktrees(vscode, panel, pp);
290
+ break;
291
+ }
292
+ case "pruneWorktrees": {
293
+ const pruneResult = await spawnGit(vscode, ["worktree", "prune"], pp);
294
+ await panel.webview.postMessage({
295
+ command: "actionResult", action: "pruneWorktrees",
296
+ result: { ok: pruneResult.exitCode === 0, error: pruneResult.exitCode !== 0 ? pruneResult.stderr.trim() : undefined },
297
+ });
298
+ if (pruneResult.exitCode === 0) await handleWorktrees(vscode, panel, pp);
299
+ break;
300
+ }
301
+ case "openWorktree": {
302
+ // Find project matching worktree path and switch to it
303
+ try {
304
+ const res = await fetch(`${baseUrl}/api/projects`);
305
+ const json = await res.json() as { ok: boolean; data?: { name: string; path: string }[] };
306
+ const match = json.data?.find((p) => p.path === msg.path);
307
+ if (match) {
308
+ await vscode.window.switchProject(match.name);
309
+ } else {
310
+ // Worktree not registered — offer to add it as a project
311
+ const dirName = msg.path.split(/[\\/]/).filter(Boolean).pop() || "worktree";
312
+ const answer = await vscode.window.showInformationMessage(
313
+ `Worktree "${dirName}" is not registered as a project. Add it?`,
314
+ "Yes, add project", "Cancel",
315
+ );
316
+ if (answer === "Yes, add project") {
317
+ const addRes = await fetch(`${baseUrl}/api/projects`, {
318
+ method: "POST",
319
+ headers: { "Content-Type": "application/json" },
320
+ body: JSON.stringify({ path: msg.path, name: dirName }),
321
+ });
322
+ const addJson = await addRes.json() as { ok: boolean; data?: { name: string } };
323
+ if (addJson.ok) {
324
+ const name = addJson.data?.name || dirName;
325
+ await vscode.window.switchProject(name);
326
+ } else {
327
+ await vscode.window.showErrorMessage("Failed to add project");
328
+ }
329
+ }
330
+ }
331
+ } catch {
332
+ await vscode.window.showErrorMessage("Failed to look up projects");
333
+ }
334
+ break;
335
+ }
336
+ case "openSourceControl": {
337
+ await vscode.window.showInformationMessage("Open the Source Control panel from the sidebar.");
338
+ break;
339
+ }
340
+ }
341
+ } catch (e) {
342
+ const errMsg = e instanceof Error ? e.message : String(e);
343
+ await panel.webview.postMessage({ command: "error", message: errMsg });
344
+ }
345
+ });
346
+
347
+ context.subscriptions.push(msgDisposable);
348
+
349
+ // Poll uncommitted changes every 5 seconds
350
+ let disposed = false;
351
+ const uncommittedPollTimer = setInterval(() => {
352
+ if (!disposed) handleUncommittedStatus(vscode, panel, activeProjectPath);
353
+ }, 5_000);
354
+
355
+ panel.onDidDispose(() => {
356
+ disposed = true;
357
+ activePanel = null;
358
+ activeProjectPath = "";
359
+ clearInterval(uncommittedPollTimer);
360
+ msgDisposable.dispose();
361
+ });
362
+ }
363
+
364
+ /** Reload all data in existing panel for a new project path */
365
+ async function reloadPanelData(
366
+ vscode: VscodeApi,
367
+ panel: ReturnType<VscodeApi["window"]["createWebviewPanel"]>,
368
+ projectPath: string,
369
+ context: ExtensionContext,
370
+ ): Promise<void> {
371
+ await handleRepoInfo(vscode, panel, projectPath);
372
+ await handleRequestCommits(vscode, panel, projectPath, context);
373
+ handleUncommittedStatus(vscode, panel, projectPath);
374
+ handleWorktrees(vscode, panel, projectPath);
375
+ }
376
+
377
+ async function handleRepoInfo(
378
+ vscode: VscodeApi,
379
+ panel: ReturnType<VscodeApi["window"]["createWebviewPanel"]>,
380
+ projectPath: string,
381
+ ): Promise<void> {
382
+ const [branchResult, tagResult, remoteResult, stashResult, headResult, headHashResult] = await Promise.all([
383
+ spawnGit(vscode, ["branch", "-a", "--format=%(refname:short)|%(objectname:short)|%(HEAD)"], projectPath),
384
+ spawnGit(vscode, ["tag", "-l", "--format=%(refname:short)|%(objectname:short)"], projectPath),
385
+ spawnGit(vscode, ["remote", "-v"], projectPath),
386
+ spawnGit(vscode, ["stash", "list", "--format=%gd|%H|%s"], projectPath),
387
+ spawnGit(vscode, ["rev-parse", "--abbrev-ref", "HEAD"], projectPath),
388
+ spawnGit(vscode, ["rev-parse", "HEAD"], projectPath),
389
+ ]);
390
+
391
+ const branches = parseBranches(branchResult.stdout);
392
+ const tags = parseTags(tagResult.stdout);
393
+ const remotes = parseRemotes(remoteResult.stdout);
394
+ const stashes = parseStashes(stashResult.stdout);
395
+ const currentBranch = headResult.stdout.trim();
396
+ const headHash = headHashResult.stdout.trim();
397
+
398
+ await panel.webview.postMessage({
399
+ command: "loadRepoInfo",
400
+ data: { path: projectPath, branches, tags, remotes, stashes, head: headHash, currentBranch },
401
+ });
402
+ }
403
+
404
+ async function handleRequestCommits(
405
+ vscode: VscodeApi,
406
+ panel: ReturnType<VscodeApi["window"]["createWebviewPanel"]>,
407
+ projectPath: string,
408
+ context?: ExtensionContext,
409
+ maxCommits = 300,
410
+ skip = 0,
411
+ branch?: string,
412
+ ): Promise<void> {
413
+ const { parseGitLog } = await import("./git-log-parser.ts");
414
+ const settings = context ? getSettings(context) : DEFAULT_SETTINGS;
415
+ const orderFlag = settings.commitOrdering === "date" ? "--date-order"
416
+ : settings.commitOrdering === "author-date" ? "--author-date-order"
417
+ : "--topo-order";
418
+ const args = [
419
+ "log",
420
+ `--format=%H%n%P%n%an%n%ae%n%at%n%cn%n%ce%n%ct%n%D%n%s%n<END_COMMIT>`,
421
+ orderFlag,
422
+ `-n`, String(maxCommits),
423
+ ];
424
+ if (settings.firstParentOnly) args.push("--first-parent");
425
+ if (skip > 0) args.push(`--skip=${skip}`);
426
+ if (branch && branch !== "all") {
427
+ args.push(branch);
428
+ } else {
429
+ args.push("--all");
430
+ }
431
+
432
+ const result = await spawnGit(vscode, args, projectPath);
433
+ const commits = parseGitLog(result.stdout);
434
+
435
+ await panel.webview.postMessage({
436
+ command: "loadCommits",
437
+ data: commits,
438
+ append: skip > 0,
439
+ });
440
+ }
441
+
442
+ async function handleCommitDetails(
443
+ vscode: VscodeApi,
444
+ panel: ReturnType<VscodeApi["window"]["createWebviewPanel"]>,
445
+ projectPath: string,
446
+ hash: string,
447
+ ): Promise<void> {
448
+ const result = await spawnGit(vscode, [
449
+ "show", "--numstat", "--format=%H%n%P%n%an%n%ae%n%at%n%cn%n%ce%n%ct%n%B%n<END_MSG>", hash,
450
+ ], projectPath);
451
+
452
+ const detail = parseCommitDetail(result.stdout);
453
+ await panel.webview.postMessage({ command: "commitDetails", data: detail });
454
+ }
455
+
456
+ async function handleUncommittedStatus(
457
+ vscode: VscodeApi,
458
+ panel: ReturnType<VscodeApi["window"]["createWebviewPanel"]>,
459
+ projectPath: string,
460
+ ): Promise<void> {
461
+ try {
462
+ const result = await spawnGit(vscode, ["status", "--porcelain=v1", "-u"], projectPath, 10_000);
463
+ if (result.exitCode !== 0 || !result.stdout.trim()) {
464
+ await panel.webview.postMessage({ command: "loadUncommitted", data: null });
465
+ return;
466
+ }
467
+ const staged: import("./types.ts").FileChange[] = [];
468
+ const unstaged: import("./types.ts").FileChange[] = [];
469
+ for (const line of result.stdout.split("\n").filter(Boolean)) {
470
+ if (staged.length + unstaged.length >= 500) break; // cap total
471
+ const x = line[0]; // staged status
472
+ const y = line[1]; // unstaged status
473
+ const filePath = line.substring(3);
474
+ if (x !== " " && x !== "?") {
475
+ staged.push({ path: filePath, status: mapStatusCode(x), additions: 0, deletions: 0 });
476
+ }
477
+ if (y !== " " && y !== "?") {
478
+ unstaged.push({ path: filePath, status: mapStatusCode(y), additions: 0, deletions: 0 });
479
+ }
480
+ if (x === "?" && y === "?") {
481
+ unstaged.push({ path: filePath, status: "A", additions: 0, deletions: 0 });
482
+ }
483
+ }
484
+ await panel.webview.postMessage({
485
+ command: "loadUncommitted",
486
+ data: { staged, unstaged },
487
+ });
488
+ } catch {
489
+ await panel.webview.postMessage({ command: "loadUncommitted", data: null });
490
+ }
491
+ }
492
+
493
+ async function handleWorktrees(
494
+ vscode: VscodeApi,
495
+ panel: ReturnType<VscodeApi["window"]["createWebviewPanel"]>,
496
+ projectPath: string,
497
+ ): Promise<void> {
498
+ const result = await spawnGit(vscode, ["worktree", "list", "--porcelain"], projectPath, 10_000);
499
+ if (result.exitCode !== 0) {
500
+ await panel.webview.postMessage({ command: "loadWorktrees", data: [] });
501
+ return;
502
+ }
503
+ const worktrees: Worktree[] = [];
504
+ let current: Partial<Worktree> = {};
505
+ for (const line of result.stdout.split("\n")) {
506
+ if (line.startsWith("worktree ")) {
507
+ if (current.path) worktrees.push(current as Worktree);
508
+ current = { path: line.slice(9), branch: "", head: "", isMain: false, isDetached: false, locked: false, prunable: false };
509
+ } else if (line.startsWith("HEAD ")) {
510
+ current.head = line.slice(5);
511
+ } else if (line.startsWith("branch ")) {
512
+ current.branch = line.slice(7).replace(/^refs\/heads\//, "");
513
+ } else if (line === "detached") {
514
+ current.isDetached = true;
515
+ } else if (line === "bare") {
516
+ // skip bare entries
517
+ } else if (line.startsWith("locked")) {
518
+ current.locked = true;
519
+ if (line.length > 7) current.lockReason = line.slice(7);
520
+ } else if (line.startsWith("prunable")) {
521
+ current.prunable = true;
522
+ }
523
+ }
524
+ if (current.path) worktrees.push(current as Worktree);
525
+ // Mark first worktree as main
526
+ if (worktrees.length > 0) worktrees[0].isMain = true;
527
+ await panel.webview.postMessage({ command: "loadWorktrees", data: worktrees });
528
+ }
529
+
530
+ function mapStatusCode(code: string): "A" | "M" | "D" | "R" {
531
+ if (code === "A" || code === "?") return "A";
532
+ if (code === "D") return "D";
533
+ if (code === "R") return "R";
534
+ return "M";
535
+ }
536
+
537
+ async function handleGitAction(
538
+ vscode: VscodeApi,
539
+ panel: ReturnType<VscodeApi["window"]["createWebviewPanel"]>,
540
+ projectPath: string,
541
+ context: ExtensionContext,
542
+ action: string,
543
+ args: Record<string, unknown>,
544
+ ): Promise<void> {
545
+ const gitArgs = buildGitActionArgs(action, args);
546
+ const result = await spawnGit(vscode, gitArgs, projectPath);
547
+ const ok = result.exitCode === 0;
548
+
549
+ await panel.webview.postMessage({
550
+ command: "actionResult",
551
+ action,
552
+ args,
553
+ result: { ok, error: ok ? undefined : result.stderr.trim() },
554
+ });
555
+
556
+ // Refresh after action
557
+ if (ok) {
558
+ await handleRepoInfo(vscode, panel, projectPath);
559
+ await handleRequestCommits(vscode, panel, projectPath, context);
560
+ handleUncommittedStatus(vscode, panel, projectPath); // fire-and-forget
561
+ }
562
+ }
563
+
564
+ async function handleDiscard(
565
+ vscode: VscodeApi,
566
+ panel: ReturnType<VscodeApi["window"]["createWebviewPanel"]>,
567
+ projectPath: string,
568
+ context: ExtensionContext,
569
+ args: Record<string, unknown>,
570
+ ): Promise<void> {
571
+ const files = (args.files as string[] | undefined) || [];
572
+ if (!files.length) throw new Error("No files to discard");
573
+
574
+ // Determine tracked vs untracked
575
+ const statusResult = await spawnGit(vscode, ["status", "--porcelain=v1"], projectPath, 10_000);
576
+ const untracked = new Set<string>();
577
+ for (const line of statusResult.stdout.split("\n").filter(Boolean)) {
578
+ if (line.startsWith("??")) untracked.add(line.substring(3).trim());
579
+ }
580
+
581
+ const trackedFiles = files.filter((f) => !untracked.has(f));
582
+ const untrackedFiles = files.filter((f) => untracked.has(f));
583
+ const errors: string[] = [];
584
+
585
+ if (trackedFiles.length > 0) {
586
+ const r = await spawnGit(vscode, ["checkout", "--", ...trackedFiles], projectPath);
587
+ if (r.exitCode !== 0) errors.push(r.stderr.trim());
588
+ }
589
+ if (untrackedFiles.length > 0) {
590
+ const r = await spawnGit(vscode, ["clean", "-f", "--", ...untrackedFiles], projectPath);
591
+ if (r.exitCode !== 0) errors.push(r.stderr.trim());
592
+ }
593
+
594
+ const ok = errors.length === 0;
595
+ await panel.webview.postMessage({
596
+ command: "actionResult",
597
+ action: "discard",
598
+ result: { ok, error: ok ? undefined : errors.join("; ") },
599
+ });
600
+
601
+ // Always refresh to show current state (even on partial failure some files may have been discarded)
602
+ await handleRepoInfo(vscode, panel, projectPath);
603
+ await handleRequestCommits(vscode, panel, projectPath, context);
604
+ handleUncommittedStatus(vscode, panel, projectPath);
605
+ }
606
+
607
+ // --- Parsers ---
608
+
609
+ function parseBranches(stdout: string): import("./types.ts").Branch[] {
610
+ return stdout.trim().split("\n").filter(Boolean).map((line) => {
611
+ const [name, hash, head] = line.split("|");
612
+ const remote = name.includes("/") ? name.split("/")[0] : undefined;
613
+ return { name, hash, current: head === "*", remote };
614
+ });
615
+ }
616
+
617
+ function parseTags(stdout: string): import("./types.ts").Tag[] {
618
+ return stdout.trim().split("\n").filter(Boolean).map((line) => {
619
+ const [name, hash] = line.split("|");
620
+ return { name, hash };
621
+ });
622
+ }
623
+
624
+ function parseRemotes(stdout: string): import("./types.ts").Remote[] {
625
+ const map = new Map<string, { fetchUrl: string; pushUrl: string }>();
626
+ for (const line of stdout.trim().split("\n").filter(Boolean)) {
627
+ const match = line.match(/^(\S+)\s+(\S+)\s+\((\w+)\)$/);
628
+ if (!match) continue;
629
+ const [, name, url, type] = match;
630
+ if (!map.has(name)) map.set(name, { fetchUrl: "", pushUrl: "" });
631
+ const entry = map.get(name)!;
632
+ if (type === "fetch") entry.fetchUrl = url;
633
+ else entry.pushUrl = url;
634
+ }
635
+ return [...map.entries()].map(([name, urls]) => ({ name, ...urls }));
636
+ }
637
+
638
+ function parseStashes(stdout: string): import("./types.ts").Stash[] {
639
+ return stdout.trim().split("\n").filter(Boolean).map((line, i) => {
640
+ const parts = line.split("|");
641
+ const [, hash, ...messageParts] = parts;
642
+ return { index: i, hash, message: messageParts.join("|") };
643
+ });
644
+ }
645
+
646
+ function parseCommitDetail(stdout: string): import("./types.ts").CommitDetail {
647
+ const [headerBlock, rest] = stdout.split("<END_MSG>");
648
+ const lines = headerBlock.trim().split("\n");
649
+ const hash = lines[0];
650
+ const parents = lines[1] ? lines[1].split(" ").filter(Boolean) : [];
651
+ const author = lines[2];
652
+ const authorEmail = lines[3];
653
+ const authorDate = parseInt(lines[4], 10);
654
+ const committer = lines[5];
655
+ const committerEmail = lines[6];
656
+ const commitDate = parseInt(lines[7], 10);
657
+ const message = lines.slice(8).join("\n").trim();
658
+
659
+ // Parse --numstat output for file changes (format: "adds\tdels\tpath")
660
+ const fileChanges: import("./types.ts").FileChange[] = [];
661
+ if (rest) {
662
+ for (const line of rest.trim().split("\n").filter(Boolean)) {
663
+ const numstatMatch = line.match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
664
+ if (numstatMatch) {
665
+ const additions = numstatMatch[1] === "-" ? 0 : parseInt(numstatMatch[1], 10);
666
+ const deletions = numstatMatch[2] === "-" ? 0 : parseInt(numstatMatch[2], 10);
667
+ let filePath = numstatMatch[3];
668
+ let oldPath: string | undefined;
669
+ // Renamed files: "old => new" or "{prefix/old => prefix/new}"
670
+ const renameMatch = filePath.match(/^(.+)\{(.+) => (.+)\}(.*)$/) || filePath.match(/^(.+) => (.+)$/);
671
+ let status: "A" | "M" | "D" | "R" = "M";
672
+ if (renameMatch) {
673
+ status = "R";
674
+ if (renameMatch.length === 5) {
675
+ oldPath = renameMatch[1] + renameMatch[2] + renameMatch[4];
676
+ filePath = renameMatch[1] + renameMatch[3] + renameMatch[4];
677
+ } else {
678
+ oldPath = renameMatch[1];
679
+ filePath = renameMatch[2];
680
+ }
681
+ } else if (additions > 0 && deletions === 0) {
682
+ status = "A";
683
+ } else if (deletions > 0 && additions === 0) {
684
+ status = "D";
685
+ }
686
+ fileChanges.push({ path: filePath, oldPath, status, additions, deletions });
687
+ }
688
+ }
689
+ }
690
+
691
+ return { hash, parents, author, authorEmail, authorDate, committer, committerEmail, commitDate, message, fileChanges };
692
+ }
693
+
694
+ // --- Input validation for git actions ---
695
+
696
+ function assertValidHash(value: unknown): string {
697
+ const s = String(value || "");
698
+ if (s === "HEAD") return s;
699
+ if (!/^[0-9a-f]{4,40}$/i.test(s)) throw new Error(`Invalid commit hash: "${s}"`);
700
+ return s;
701
+ }
702
+
703
+ function assertValidRef(value: unknown, label: string): string {
704
+ const s = String(value || "");
705
+ if (!s || /[\x00-\x1f\x7f~^:?*[\]\\]/.test(s) || s.startsWith("-") || s.includes("..")) {
706
+ throw new Error(`Invalid git ref for ${label}: "${s}"`);
707
+ }
708
+ return s;
709
+ }
710
+
711
+ function assertValidRemote(value: unknown): string {
712
+ const s = String(value || "");
713
+ if (!s || /[\x00-\x1f\x7f]/.test(s) || s.startsWith("-")) {
714
+ throw new Error(`Invalid remote name: "${s}"`);
715
+ }
716
+ return s;
717
+ }
718
+
719
+ /** Validate file paths are relative and don't escape the project root */
720
+ function assertSafeFilePaths(files: string[], projectPath: string): void {
721
+ const { resolve, normalize } = require("path");
722
+ const root = normalize(projectPath) + "/";
723
+ for (const f of files) {
724
+ if (!f || f.startsWith("-") || f.startsWith("/") || /[\x00-\x1f\x7f]/.test(f)) {
725
+ throw new Error(`Invalid file path: "${f}"`);
726
+ }
727
+ const resolved = normalize(resolve(projectPath, f));
728
+ if (!resolved.startsWith(root) && resolved !== normalize(projectPath)) {
729
+ throw new Error(`File path escapes project root: "${f}"`);
730
+ }
731
+ }
732
+ }
733
+
734
+ function buildGitActionArgs(action: string, args: Record<string, unknown>): string[] {
735
+ const VALID_RESET_MODES = ["soft", "mixed", "hard"];
736
+
737
+ switch (action) {
738
+ case "checkout": return ["checkout", assertValidRef(args.target, "target")];
739
+ case "createBranch": return ["branch", ...(args.force ? ["-f"] : []), assertValidRef(args.name, "name"), ...(args.startPoint ? [assertValidHash(args.startPoint)] : [])];
740
+ case "deleteBranch": return ["branch", args.force ? "-D" : "-d", assertValidRef(args.name, "name")];
741
+ case "merge": {
742
+ const mergeArgs = ["merge", assertValidRef(args.branch, "branch")];
743
+ if (args.noFf) mergeArgs.push("--no-ff");
744
+ if (args.squash) mergeArgs.push("--squash");
745
+ return mergeArgs;
746
+ }
747
+ case "rebase": return ["rebase", assertValidRef(args.branch, "branch")];
748
+ case "cherryPick": return ["cherry-pick", assertValidHash(args.hash)];
749
+ case "revert": return ["revert", assertValidHash(args.hash)];
750
+ case "reset": {
751
+ const mode = VALID_RESET_MODES.includes(String(args.mode)) ? String(args.mode) : "mixed";
752
+ return ["reset", `--${mode}`, assertValidHash(args.hash)];
753
+ }
754
+ case "stashSave": return ["stash", "push", ...(args.message ? ["-m", String(args.message)] : [])];
755
+ case "stashPop": return ["stash", "pop", ...(args.stashRef ? [assertValidRef(args.stashRef, "stashRef")] : [])];
756
+ case "stashDrop": return ["stash", "drop", ...(args.stashRef ? [assertValidRef(args.stashRef, "stashRef")] : [])];
757
+ case "fetch": return ["fetch", ...(args.remote ? [assertValidRemote(args.remote)] : []), ...(args.prune ? ["--prune"] : [])];
758
+ case "pull": return ["pull", ...(args.remote ? [assertValidRemote(args.remote)] : []), ...(args.branch ? [assertValidRef(args.branch, "branch")] : [])];
759
+ case "renameBranch": {
760
+ const oldName = assertValidRef(args.oldName, "oldName");
761
+ const newName = assertValidRef(args.newName, "newName");
762
+ return ["branch", "-m", oldName, newName];
763
+ }
764
+ case "push": {
765
+ const pushArgs = ["push"];
766
+ if (args.remote) pushArgs.push(assertValidRemote(args.remote));
767
+ if (args.delete && args.branch) {
768
+ pushArgs.push("--delete", assertValidRef(args.branch, "branch"));
769
+ } else {
770
+ if (args.branch) pushArgs.push(assertValidRef(args.branch, "branch"));
771
+ if (args.force) pushArgs.push("--force");
772
+ }
773
+ return pushArgs;
774
+ }
775
+ case "createTag": {
776
+ const tagArgs = ["tag", assertValidRef(args.name, "name")];
777
+ if (args.hash) tagArgs.push(assertValidHash(args.hash));
778
+ if (args.message) tagArgs.push("-m", String(args.message));
779
+ return tagArgs;
780
+ }
781
+ case "deleteTag": return ["tag", "-d", assertValidRef(args.name, "name")];
782
+ case "stage": {
783
+ const files = args.files as string[] | undefined;
784
+ if (!files?.length) throw new Error("No files to stage");
785
+ return ["add", "--", ...files];
786
+ }
787
+ case "unstage": {
788
+ const files = args.files as string[] | undefined;
789
+ if (!files?.length) throw new Error("No files to unstage");
790
+ return ["restore", "--staged", "--", ...files];
791
+ }
792
+ case "commit": {
793
+ const message = String(args.message || "").trim();
794
+ if (!message) throw new Error("Commit message required");
795
+ return ["commit", "-m", message];
796
+ }
797
+ case "clean": return ["clean", "-fd"];
798
+ default: throw new Error(`Unknown git action: ${action}`);
799
+ }
800
+ }