@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,2199 @@
1
+ /**
2
+ * Generate the complete webview HTML for the git graph panel.
3
+ * All JS + CSS is inlined since webview runs in an iframe sandbox.
4
+ */
5
+
6
+ export function getWebviewHtml(): string {
7
+ return `<!DOCTYPE html>
8
+ <html>
9
+ <head>
10
+ <meta charset="utf-8">
11
+ <style>
12
+ ${getStyles()}
13
+ </style>
14
+ </head>
15
+ <body>
16
+ <div id="app">
17
+ <header id="toolbar">
18
+ <div class="toolbar-left">
19
+ <div id="branch-selector" class="branch-dropdown">
20
+ <button id="branch-trigger" class="branch-trigger">All Branches</button>
21
+ <div id="branch-dropdown-menu" class="branch-dropdown-menu hidden">
22
+ <input id="branch-filter" type="text" placeholder="Filter branches..." class="branch-filter-input" />
23
+ <div id="branch-list" class="branch-list"></div>
24
+ </div>
25
+ </div>
26
+ <button id="btn-refresh" title="Refresh"></button>
27
+ <button id="btn-fetch" title="Fetch from remotes"></button>
28
+ </div>
29
+ <div class="toolbar-right">
30
+ <div class="worktree-dropdown">
31
+ <button id="btn-worktree" title="Worktrees"></button>
32
+ <div id="worktree-popover" class="worktree-popover hidden">
33
+ <div class="worktree-popover-header">
34
+ <span>Worktrees</span>
35
+ </div>
36
+ <div id="worktree-list" class="worktree-list"></div>
37
+ <div class="worktree-popover-footer">
38
+ <button id="wt-add" class="btn-sm">+ Add Worktree</button>
39
+ <button id="wt-prune" class="btn-sm secondary" title="Remove stale worktree entries">Prune</button>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ <button id="btn-find" title="Find (Ctrl+F)"></button>
44
+ <button id="btn-settings" title="Settings"></button>
45
+ </div>
46
+ </header>
47
+ <div id="find-bar" class="find-bar hidden">
48
+ <input id="find-input" type="text" placeholder="Search commits..." />
49
+ <span id="find-count"></span>
50
+ <button id="find-prev" title="Previous">&uarr;</button>
51
+ <button id="find-next" title="Next">&darr;</button>
52
+ <button id="find-close" title="Close">&times;</button>
53
+ </div>
54
+ <div id="graph-container">
55
+ <div id="graph-header" class="commit-row header-row">
56
+ <div class="col-graph">Graph<div class="graph-resize-handle" id="graph-resize-handle"></div></div>
57
+ <div class="col-message">Message</div>
58
+ <div class="col-author">Author</div>
59
+ <div class="col-date">Date</div>
60
+ <div class="col-hash">Hash</div>
61
+ </div>
62
+ <div id="commit-list-wrapper">
63
+ <div id="graph-svg-container"></div>
64
+ <div id="commit-list"></div>
65
+ </div>
66
+ <div id="loading" class="loading hidden">Loading...</div>
67
+ </div>
68
+ <div id="detail-panel" class="detail-panel hidden"></div>
69
+ <div id="settings-panel" class="settings-panel">
70
+ <div class="settings-header">
71
+ <h3>Git Graph Settings</h3>
72
+ <button id="settings-close" title="Close">&times;</button>
73
+ </div>
74
+ <div class="settings-body">
75
+ <details class="settings-section" open>
76
+ <summary>General</summary>
77
+ <div class="setting-row"><label>Max Commits</label><input type="number" id="s-maxCommits" min="10" max="10000" step="50"></div>
78
+ <div class="setting-row"><label>Show Tags</label><input type="checkbox" id="s-showTags"></div>
79
+ <div class="setting-row"><label>Show Stashes</label><input type="checkbox" id="s-showStashes"></div>
80
+ <div class="setting-row"><label>Show Remote Branches</label><input type="checkbox" id="s-showRemoteBranches"></div>
81
+ <div class="setting-row"><label>Graph Style</label><select id="s-graphStyle"><option value="rounded">Rounded</option><option value="angular">Angular</option></select></div>
82
+ <div class="setting-row"><label>First Parent Only</label><input type="checkbox" id="s-firstParentOnly"></div>
83
+ <div class="setting-row"><label>Date Format</label><select id="s-dateFormat"><option value="relative">Relative</option><option value="absolute">Absolute</option><option value="iso">ISO</option></select></div>
84
+ <div class="setting-row"><label>Commit Ordering</label><select id="s-commitOrdering"><option value="topo">Topological</option><option value="date">Date</option><option value="author-date">Author Date</option></select></div>
85
+ <div class="setting-row"><label>Auto Fetch Interval</label><select id="s-autoFetchInterval"><option value="0">Disabled</option><option value="10">10 seconds</option><option value="30">30 seconds</option><option value="60">1 minute</option><option value="120">2 minutes</option><option value="300">5 minutes</option></select></div>
86
+ </details>
87
+ <details class="settings-section" open>
88
+ <summary>User Details</summary>
89
+ <div class="setting-row"><label>Name</label><input type="text" id="s-userName" placeholder="user.name"></div>
90
+ <div class="setting-row"><label>Email</label><input type="text" id="s-userEmail" placeholder="user.email"></div>
91
+ <div class="setting-row" style="justify-content:flex-end"><button id="s-saveUser" class="btn-sm">Save User Details</button></div>
92
+ </details>
93
+ <details class="settings-section" open>
94
+ <summary>Remotes</summary>
95
+ <div id="s-remotes-list"></div>
96
+ <div class="add-remote-form">
97
+ <input type="text" id="s-newRemoteName" placeholder="Remote name">
98
+ <input type="text" id="s-newRemoteUrl" placeholder="Remote URL">
99
+ <button id="s-addRemote" class="btn-sm">Add Remote</button>
100
+ </div>
101
+ </details>
102
+ <details class="settings-section">
103
+ <summary>Issue Linking</summary>
104
+ <p style="font-size:11px;color:var(--subtext);margin-bottom:6px">Turn issue references in commit messages into clickable links.</p>
105
+ <div id="issue-rules-list"></div>
106
+ <button id="add-issue-rule" class="btn-sm" style="margin-top:6px">+ Add Rule</button>
107
+ </details>
108
+ <details class="settings-section">
109
+ <summary>Pull Request Creation</summary>
110
+ <div class="setting-row"><label>Provider</label><select id="pr-provider"><option value="">Disabled</option><option value="github">GitHub</option><option value="gitlab">GitLab</option><option value="bitbucket">Bitbucket</option><option value="custom">Custom</option></select></div>
111
+ <div id="pr-config" class="hidden">
112
+ <div class="setting-row"><label>Owner</label><input type="text" id="pr-owner" placeholder="owner or org"></div>
113
+ <div class="setting-row"><label>Repo</label><input type="text" id="pr-repo" placeholder="repository name"></div>
114
+ <div class="setting-row"><label>Target Branch</label><input type="text" id="pr-target" placeholder="main"></div>
115
+ <div class="setting-row"><label>URL Template</label><input type="text" id="pr-url-template" placeholder="https://..."></div>
116
+ <p style="font-size:10px;color:var(--subtext);margin:2px 0 4px">Variables: \${owner}, \${repo}, \${sourceBranch}, \${targetBranch}</p>
117
+ <div class="setting-row" style="justify-content:flex-end"><button id="pr-save" class="btn-sm">Save PR Config</button></div>
118
+ </div>
119
+ </details>
120
+ </div>
121
+ </div>
122
+ <div id="status-bar">
123
+ <span id="status-text">Loading repository...</span>
124
+ </div>
125
+ </div>
126
+ <div id="context-menu" class="context-menu hidden"></div>
127
+ <script>
128
+ ${getScript()}
129
+ </script>
130
+ </body>
131
+ </html>`;
132
+ }
133
+
134
+ function getStyles(): string {
135
+ return `
136
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
137
+ :root {
138
+ --bg: #ffffff; --surface: #f4f4f5; --text: #09090b; --subtext: #71717a; --subtle: #a1a1aa;
139
+ --border: #e4e4e7; --border2: #d4d4d8; --blue: #3b82f6; --red: #ef4444; --green: #22c55e;
140
+ --yellow: #eab308; --purple: #8b5cf6; --orange: #f97316;
141
+ --surface-hover: #f4f4f5; --selected: #eff6ff;
142
+ }
143
+ @media (prefers-color-scheme: dark) {
144
+ :root {
145
+ --bg: #09090b; --surface: #18181b; --text: #fafafa; --subtext: #a1a1aa; --subtle: #52525b;
146
+ --border: #27272a; --border2: #3f3f46; --selected: #1e293b; --surface-hover: #27272a;
147
+ }
148
+ }
149
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); font-size: 13px; overflow: hidden; height: 100vh; display: flex; flex-direction: column; }
150
+ #app { display: flex; flex-direction: column; height: 100vh; }
151
+
152
+ /* Toolbar */
153
+ #toolbar { display: flex; justify-content: space-between; align-items: center; padding: 6px 12px; border-bottom: 1px solid var(--border); background: var(--surface); flex-shrink: 0; }
154
+ .toolbar-left, .toolbar-right { display: flex; align-items: center; gap: 6px; }
155
+ select { background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 4px 8px; font-size: 12px; }
156
+ button { background: transparent; color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; min-width: 28px; min-height: 28px; transition: background 0.15s, border-color 0.15s; }
157
+ button:hover { background: var(--surface-hover); border-color: var(--border2); }
158
+ button:active { background: var(--surface); }
159
+ .btn-fetching { opacity: 0.6; pointer-events: none; }
160
+
161
+ /* Branch dropdown */
162
+ .branch-dropdown { position: relative; }
163
+ .branch-trigger { background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 4px 24px 4px 8px; font-size: 12px; cursor: pointer; min-width: 140px; text-align: left; position: relative; }
164
+ .branch-trigger::after { content: '\\25BC'; font-size: 8px; position: absolute; right: 8px; top: 50%; transform: translateY(-50%); color: var(--subtext); }
165
+ .branch-dropdown-menu { position: absolute; top: 100%; left: 0; z-index: 60; background: var(--surface); border: 1px solid var(--border2); border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); min-width: 220px; max-height: 300px; display: flex; flex-direction: column; margin-top: 2px; }
166
+ .branch-filter-input { padding: 6px 8px; border: none; border-bottom: 1px solid var(--border); background: var(--bg); color: var(--text); font-size: 12px; outline: none; border-radius: 6px 6px 0 0; }
167
+ .branch-list { overflow-y: auto; max-height: 250px; }
168
+ .branch-option { padding: 6px 10px; cursor: pointer; font-size: 12px; display: flex; align-items: center; gap: 6px; }
169
+ .branch-option:hover { background: var(--surface-hover); }
170
+ .branch-option.selected { background: var(--selected); font-weight: 600; }
171
+
172
+ /* Worktree popover */
173
+ .worktree-dropdown { position: relative; }
174
+ #btn-worktree { display: flex; align-items: center; gap: 4px; font-size: 12px; padding: 4px 8px; }
175
+ #btn-worktree .wt-count { background: var(--accent, #58a6ff); color: #fff; font-size: 10px; border-radius: 8px; padding: 0 5px; min-width: 16px; text-align: center; line-height: 16px; }
176
+ .worktree-popover { position: absolute; top: 100%; right: 0; z-index: 60; background: var(--surface); border: 1px solid var(--border2); border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.2); min-width: 300px; max-width: 400px; margin-top: 4px; display: flex; flex-direction: column; }
177
+ .worktree-popover-header { padding: 8px 12px; font-size: 12px; font-weight: 600; border-bottom: 1px solid var(--border); }
178
+ .worktree-list { overflow-y: auto; max-height: 240px; }
179
+ .wt-item { padding: 8px 12px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; font-size: 12px; }
180
+ .wt-item:last-child { border-bottom: none; }
181
+ .wt-item-info { flex: 1; min-width: 0; }
182
+ .wt-item-branch { font-weight: 600; display: flex; align-items: center; gap: 4px; }
183
+ .wt-item-path { font-size: 10px; color: var(--subtext); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
184
+ .wt-badge { font-size: 9px; padding: 1px 4px; border-radius: 4px; background: var(--border); color: var(--subtext); }
185
+ .wt-badge-current { background: var(--accent, #58a6ff); color: #fff; }
186
+ .wt-item-active { background: var(--selected); }
187
+ .wt-badge-locked { background: #d29922; color: #fff; }
188
+ .wt-item-actions { display: flex; gap: 4px; flex-shrink: 0; }
189
+ .wt-item-actions button { min-width: 24px; min-height: 24px; padding: 2px 6px; font-size: 10px; }
190
+ .worktree-popover-footer { padding: 8px 12px; border-top: 1px solid var(--border); display: flex; gap: 6px; }
191
+ .worktree-popover-footer .btn-sm { flex: 1; }
192
+ .wt-empty { padding: 16px; text-align: center; font-size: 11px; color: var(--subtext); }
193
+ @media (max-width: 768px) { .branch-option { padding: 10px 12px; min-height: 44px; } }
194
+
195
+ /* Find bar */
196
+ .find-bar { display: flex; align-items: center; gap: 6px; padding: 6px 12px; border-bottom: 1px solid var(--border); background: var(--surface); }
197
+ .find-bar input { flex: 1; background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 4px 8px; font-size: 12px; }
198
+ .find-bar input:focus { outline: none; border-color: var(--blue); }
199
+ #find-count { font-size: 11px; color: var(--subtext); min-width: 60px; }
200
+ .hidden { display: none !important; }
201
+
202
+ /* Graph container */
203
+ #graph-container { flex: 1; overflow-y: auto; overflow-x: hidden; }
204
+ .commit-row { display: flex; align-items: center; cursor: pointer; min-height: 28px; padding: 0 8px; }
205
+ .commit-row:hover { background: var(--surface-hover); }
206
+ .commit-row.selected { background: var(--selected); }
207
+ .commit-row.header-row { background: var(--surface); cursor: default; font-weight: 600; font-size: 11px; color: var(--subtext); text-transform: uppercase; letter-spacing: 0.5px; position: sticky; top: 0; z-index: 2; border-bottom: 1px solid var(--border); }
208
+ .commit-row.search-match { background: rgba(234, 179, 8, 0.15); }
209
+ .commit-row.virtual { opacity: 0.85; font-style: italic; }
210
+ .commit-row.virtual .col-message { color: var(--subtext); }
211
+ .file-clickable { cursor: pointer; border-radius: 3px; padding: 2px 4px; margin: 0 -4px; }
212
+ .file-clickable:hover { background: var(--surface-hover); }
213
+ .col-graph { width: var(--graph-col-w, 120px); min-width: var(--graph-col-w, 80px); overflow: hidden; flex-shrink: 0; position: relative; }
214
+ .graph-resize-handle { position: absolute; right: 0; top: 0; bottom: 0; width: 6px; cursor: col-resize; z-index: 3; background: transparent; }
215
+ .graph-resize-handle:hover, .graph-resize-handle.dragging { background: var(--blue); opacity: 0.5; }
216
+ .col-message { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 8px; }
217
+ .col-author { width: 120px; min-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--subtext); font-size: 12px; }
218
+ .col-date { width: 100px; min-width: 100px; color: var(--subtext); font-size: 12px; }
219
+ .col-hash { width: 70px; min-width: 70px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; color: var(--subtle); }
220
+
221
+ /* Ref badges */
222
+ .ref-badge { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-right: 4px; vertical-align: middle; }
223
+ .ref-head { background: var(--green); color: #fff; }
224
+ .ref-local { background: var(--blue); color: #fff; }
225
+ .ref-remote { background: var(--purple); color: #fff; }
226
+ .ref-tag { background: var(--yellow); color: #000; }
227
+
228
+ /* SVG graph — single SVG overlay */
229
+ #commit-list-wrapper { position: relative; }
230
+ #graph-svg-container { position: absolute; top: 0; left: 8px; z-index: 1; pointer-events: none; }
231
+ #graph-svg-container circle { pointer-events: auto; cursor: pointer; }
232
+ #graph-svg-container .line { stroke-width: 2; fill: none; }
233
+ #graph-svg-container .graphCurrent { fill: var(--bg); stroke-width: 2; }
234
+ #graph-svg-container .graphStashOuter { fill: none; stroke: #808080; stroke-width: 1.5; }
235
+ #graph-svg-container .graphStashInner { fill: #808080; }
236
+ .commit-row.graph-hover { background: var(--surface-hover); }
237
+
238
+ /* Detail panel */
239
+ .detail-panel { border-top: 1px solid var(--border2); background: var(--surface); max-height: 40vh; overflow-y: auto; padding: 12px 16px; flex-shrink: 0; }
240
+ .detail-panel h3 { font-size: 14px; margin-bottom: 8px; }
241
+ .detail-field { margin-bottom: 4px; font-size: 12px; }
242
+ .detail-field .label { color: var(--subtext); display: inline-block; width: 80px; }
243
+ .detail-message { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 8px; margin: 8px 0; font-size: 12px; white-space: pre-wrap; font-family: 'SF Mono', 'Fira Code', monospace; }
244
+ .file-list { margin-top: 8px; }
245
+ .file-item { display: flex; align-items: center; gap: 6px; padding: 2px 0; font-size: 12px; font-family: 'SF Mono', 'Fira Code', monospace; }
246
+ .file-status { display: inline-block; width: 16px; text-align: center; font-weight: 700; font-size: 11px; }
247
+ .file-status-A { color: var(--green); }
248
+ .file-status-M { color: var(--yellow); }
249
+ .file-status-D { color: var(--red); }
250
+ .file-status-R { color: var(--blue); }
251
+ .file-stat { color: var(--subtext); font-size: 11px; margin-left: auto; }
252
+ .file-stat .add { color: var(--green); }
253
+ .file-stat .del { color: var(--red); }
254
+
255
+ /* File view toggle */
256
+ .file-view-toggle { display: flex; gap: 2px; margin-bottom: 6px; }
257
+ .toggle-btn { padding: 2px 6px; font-size: 12px; min-width: 28px; min-height: 28px; border: 1px solid var(--border); border-radius: 4px; background: transparent; cursor: pointer; }
258
+ .toggle-btn.active { background: var(--surface-hover); border-color: var(--border2); }
259
+ .tree-dir { display: flex; align-items: center; gap: 4px; padding: 2px 0; font-size: 12px; color: var(--subtext); }
260
+ .tree-dir-name { font-weight: 500; color: var(--text); }
261
+ .tree-dir-count { font-size: 11px; color: var(--subtle); }
262
+
263
+ /* File actions */
264
+ .file-actions { display: flex; gap: 2px; margin-left: auto; flex-shrink: 0; }
265
+ .file-action-btn { min-width: 24px; min-height: 24px; padding: 0 4px; border: none; background: transparent; cursor: pointer; border-radius: 4px; font-size: 12px; color: var(--subtext); display: flex; align-items: center; justify-content: center; }
266
+ .file-action-btn:hover { background: var(--surface-hover); color: var(--text); }
267
+ .file-action-btn[data-action="discard"]:hover { color: var(--red); }
268
+ .section-actions { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
269
+ .commit-section { margin-top: 12px; border-top: 1px solid var(--border); padding-top: 12px; }
270
+ .commit-section textarea { width: 100%; background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 6px; padding: 8px; font-size: 12px; font-family: inherit; resize: vertical; min-height: 60px; }
271
+ .commit-section textarea:focus { outline: none; border-color: var(--blue); }
272
+ .commit-actions { display: flex; justify-content: flex-end; margin-top: 6px; gap: 6px; }
273
+ .btn-commit { background: var(--green); color: #fff; border-color: transparent; padding: 4px 16px; font-weight: 600; }
274
+ .btn-commit:disabled { opacity: 0.4; cursor: not-allowed; }
275
+ .btn-commit:hover:not(:disabled) { opacity: 0.9; }
276
+ @media (max-width: 768px) { .file-action-btn { min-width: 36px; min-height: 36px; } }
277
+
278
+ /* Status bar */
279
+ #status-bar { display: flex; align-items: center; padding: 4px 12px; border-top: 1px solid var(--border); background: var(--surface); font-size: 11px; color: var(--subtext); flex-shrink: 0; }
280
+
281
+ /* Context menu */
282
+ .context-menu { position: fixed; background: var(--surface); border: 1px solid var(--border2); border-radius: 6px; padding: 4px 0; min-width: 180px; z-index: 100; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
283
+ .ctx-item { padding: 6px 12px; cursor: pointer; font-size: 12px; display: flex; align-items: center; gap: 8px; }
284
+ .ctx-item:hover { background: var(--surface-hover); }
285
+ .ctx-item.destructive { color: var(--red); }
286
+ .ctx-separator { border-top: 1px solid var(--border); margin: 4px 0; }
287
+
288
+ /* Loading */
289
+ .loading { text-align: center; padding: 16px; color: var(--subtext); }
290
+
291
+ /* Settings panel */
292
+ .settings-panel { position: absolute; right: 0; top: 0; bottom: 0; width: 340px; background: var(--surface); border-left: 1px solid var(--border2); z-index: 50; overflow-y: auto; transform: translateX(100%); transition: transform 0.2s ease; display: flex; flex-direction: column; }
293
+ .settings-panel.open { transform: translateX(0); }
294
+ .settings-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
295
+ .settings-header h3 { font-size: 14px; font-weight: 600; }
296
+ .settings-body { flex: 1; overflow-y: auto; padding: 8px 0; }
297
+ .settings-section { border-bottom: 1px solid var(--border); padding: 8px 14px; }
298
+ .settings-section summary { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--subtext); cursor: pointer; padding: 4px 0; user-select: none; }
299
+ .settings-section[open] summary { margin-bottom: 6px; }
300
+ .setting-row { display: flex; align-items: center; justify-content: space-between; padding: 5px 0; font-size: 12px; gap: 8px; }
301
+ .setting-row label { flex: 1; min-width: 0; }
302
+ .setting-row input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--blue); flex-shrink: 0; }
303
+ .setting-row input[type="number"] { width: 72px; background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 3px 6px; font-size: 12px; }
304
+ .setting-row select { background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 3px 6px; font-size: 12px; min-width: 100px; }
305
+ .setting-row input[type="text"] { flex: 1; background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 3px 6px; font-size: 12px; }
306
+ .btn-sm { font-size: 11px; padding: 3px 10px; border-radius: 6px; min-width: 0; min-height: 0; }
307
+ .remote-item { padding: 6px 0; border-bottom: 1px solid var(--border); font-size: 12px; }
308
+ .remote-item:last-child { border-bottom: none; }
309
+ .remote-item .remote-name { font-weight: 600; margin-bottom: 2px; }
310
+ .remote-item .remote-url { color: var(--subtext); font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; word-break: break-all; }
311
+ .remote-actions { display: flex; gap: 4px; margin-top: 4px; }
312
+ .add-remote-form { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
313
+ .add-remote-form input { background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 4px 6px; font-size: 12px; }
314
+ .issue-rule-row { display: flex; gap: 4px; align-items: center; margin-bottom: 4px; }
315
+ .issue-rule-row input { flex: 1; background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 3px 6px; font-size: 11px; font-family: 'SF Mono', 'Fira Code', monospace; }
316
+ .issue-rule-row input.rule-error { border-color: var(--red); }
317
+ .issue-rule-row .rule-remove { min-width: 24px; min-height: 24px; padding: 0; font-size: 14px; color: var(--red); border: none; }
318
+ @media (max-width: 768px) { .settings-panel { width: 100%; } }
319
+
320
+ /* Dialog overlay */
321
+ .dialog-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 200; display: flex; align-items: center; justify-content: center; }
322
+ .dialog { background: var(--surface); border: 1px solid var(--border2); border-radius: 8px; padding: 16px; min-width: 300px; max-width: 400px; box-shadow: 0 8px 24px rgba(0,0,0,0.2); }
323
+ .dialog h3 { font-size: 14px; margin-bottom: 12px; }
324
+ .dialog p { font-size: 12px; color: var(--subtext); margin-bottom: 12px; }
325
+ .dialog p.warning { color: var(--red); font-weight: 600; }
326
+ .dialog input, .dialog select { width: 100%; background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 6px 8px; font-size: 12px; margin-bottom: 12px; }
327
+ .dialog input:focus, .dialog select:focus { outline: none; border-color: var(--blue); }
328
+ .dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
329
+ .dialog-actions button { min-width: 64px; }
330
+ .dialog-actions .btn-primary { background: var(--blue); color: #fff; border-color: transparent; }
331
+ .dialog-actions .btn-danger { background: var(--red); color: #fff; border-color: transparent; }
332
+
333
+ /* Links in commit messages */
334
+ .commit-link { color: var(--blue); text-decoration: none; cursor: pointer; }
335
+ .commit-link:hover { text-decoration: underline; }
336
+
337
+ /* Toast notifications */
338
+ .toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); padding: 8px 16px; border-radius: 6px; font-size: 12px; z-index: 300; animation: toast-in 0.3s ease; max-width: 80%; pointer-events: none; }
339
+ .toast-error { background: var(--red); color: #fff; }
340
+ .toast-success { background: var(--green); color: #fff; }
341
+ .toast-info { background: var(--blue); color: #fff; }
342
+ @keyframes toast-in { from { opacity: 0; transform: translateX(-50%) translateY(10px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }
343
+
344
+ /* Touch targets for mobile */
345
+ @media (max-width: 768px) {
346
+ .commit-row { min-height: 44px; }
347
+ .ctx-item { padding: 10px 16px; min-height: 44px; }
348
+ button { min-width: 44px; min-height: 44px; }
349
+ .col-author, .col-hash { display: none; }
350
+ .col-date { width: 60px; min-width: 60px; }
351
+ }
352
+ `;
353
+ }
354
+
355
+ function getScript(): string {
356
+ return `
357
+ const vscode = acquireVsCodeApi();
358
+ const SVG_NS = 'http://www.w3.org/2000/svg';
359
+ const NULL_VERTEX_ID = -1;
360
+ const GRAPH_COLORS = ['#4ec9b0','#569cd6','#c586c0','#ce9178','#dcdcaa','#4fc1ff','#d7ba7d','#9cdcfe','#b5cea8','#d16969'];
361
+ const graphConfig = {
362
+ colours: GRAPH_COLORS,
363
+ grid: { x: 16, y: 28, offsetX: 8, offsetY: 14, expandY: 60 },
364
+ style: 'rounded'
365
+ };
366
+
367
+ // --- State ---
368
+ const DEFAULT_SETTINGS = {
369
+ maxCommits: 300, showTags: true, showStashes: true, showRemoteBranches: true,
370
+ graphStyle: 'rounded', firstParentOnly: false, dateFormat: 'relative', commitOrdering: 'topo',
371
+ issueLinkingRules: [{ pattern: '#(\\\\d+)', url: '' }], prCreation: null,
372
+ autoFetchInterval: 0,
373
+ };
374
+
375
+ const state = {
376
+ repo: '',
377
+ commits: [],
378
+ branches: [],
379
+ tags: [],
380
+ remotes: [],
381
+ stashes: [],
382
+ currentBranch: '',
383
+ head: '',
384
+ selectedCommit: null,
385
+ expandedCommit: null,
386
+ maxCommits: 300,
387
+ loading: false,
388
+ uncommitted: null,
389
+ searchMatches: [],
390
+ searchIndex: -1,
391
+ settings: { ...DEFAULT_SETTINGS },
392
+ userDetails: { name: '', email: '' },
393
+ graphColWidth: null,
394
+ fileViewMode: 'list',
395
+ worktrees: [],
396
+ _lastDetail: null,
397
+ };
398
+
399
+ // --- SVG Icons ---
400
+ const ICONS = {
401
+ refresh: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg>',
402
+ download: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
403
+ search: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
404
+ settings: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"/></svg>',
405
+ folderOpen: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2l-1-5H4l-1 5a2 2 0 002 2z"/></svg>',
406
+ file: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>',
407
+ list: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>',
408
+ tree: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>',
409
+ plus: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
410
+ minus: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/></svg>',
411
+ x: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
412
+ fileOpen: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>',
413
+ gitBranch: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 01-9 9"/></svg>',
414
+ trash: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>',
415
+ };
416
+
417
+ // --- Toast notifications ---
418
+ function showToast(message, type) {
419
+ const existing = document.querySelector('.toast');
420
+ if (existing) existing.remove();
421
+ const el = document.createElement('div');
422
+ el.className = 'toast toast-' + (type || 'error');
423
+ el.textContent = message;
424
+ document.body.appendChild(el);
425
+ setTimeout(() => { if (el.parentNode) el.remove(); }, 4000);
426
+ }
427
+
428
+ // --- Init ---
429
+ document.getElementById('btn-refresh').innerHTML = ICONS.refresh;
430
+ document.getElementById('btn-fetch').innerHTML = ICONS.download;
431
+ document.getElementById('btn-find').innerHTML = ICONS.search;
432
+ document.getElementById('btn-settings').innerHTML = ICONS.settings;
433
+ document.getElementById('btn-worktree').innerHTML = ICONS.gitBranch + ' <span class="wt-count" style="display:none">0</span>';
434
+ vscode.postMessage({ command: 'ready' });
435
+
436
+ // --- Message handler ---
437
+ window.addEventListener('message', (event) => {
438
+ const msg = event.data;
439
+ switch (msg.command) {
440
+ case 'loadRepoInfo':
441
+ state.repo = msg.data.path;
442
+ state.branches = msg.data.branches;
443
+ state.tags = msg.data.tags;
444
+ state.remotes = msg.data.remotes;
445
+ state.stashes = msg.data.stashes;
446
+ state.head = msg.data.head;
447
+ state.currentBranch = msg.data.currentBranch;
448
+ renderBranchSelector();
449
+ updateStatus();
450
+ if (document.getElementById('settings-panel').classList.contains('open')) renderRemotesList();
451
+ break;
452
+ case 'loadCommits':
453
+ if (msg.append) {
454
+ state.commits = state.commits.concat(msg.data);
455
+ } else {
456
+ state.commits = msg.data;
457
+ }
458
+ renderCommitList();
459
+ updateStatus();
460
+ state.loading = false;
461
+ document.getElementById('loading').classList.add('hidden');
462
+ break;
463
+ case 'commitDetails':
464
+ renderDetailPanel(msg.data);
465
+ break;
466
+ case 'refresh':
467
+ state.commits = msg.data;
468
+ if (msg.repoInfo) {
469
+ state.branches = msg.repoInfo.branches;
470
+ state.tags = msg.repoInfo.tags;
471
+ state.remotes = msg.repoInfo.remotes;
472
+ state.stashes = msg.repoInfo.stashes;
473
+ state.head = msg.repoInfo.head;
474
+ state.currentBranch = msg.repoInfo.currentBranch;
475
+ renderBranchSelector();
476
+ }
477
+ renderCommitList();
478
+ updateStatus();
479
+ break;
480
+ case 'loadUncommitted':
481
+ state.uncommitted = msg.data;
482
+ renderCommitList();
483
+ if (state.selectedCommit === 'uncommitted') {
484
+ if (!msg.data || (msg.data.staged.length === 0 && msg.data.unstaged.length === 0)) {
485
+ state.selectedCommit = null;
486
+ state.expandedCommit = null;
487
+ document.getElementById('detail-panel').classList.add('hidden');
488
+ } else {
489
+ renderUncommittedDetail();
490
+ }
491
+ }
492
+ break;
493
+ case 'loadSettings':
494
+ state.settings = { ...DEFAULT_SETTINGS, ...msg.data };
495
+ state.maxCommits = state.settings.maxCommits;
496
+ applySettingsToUI();
497
+ break;
498
+ case 'loadUserDetails':
499
+ state.userDetails = msg.data;
500
+ document.getElementById('s-userName').value = msg.data.name;
501
+ document.getElementById('s-userEmail').value = msg.data.email;
502
+ break;
503
+ case 'loadOwnerRepo':
504
+ if (msg.data.owner) document.getElementById('pr-owner').value = msg.data.owner;
505
+ if (msg.data.repo) document.getElementById('pr-repo').value = msg.data.repo;
506
+ break;
507
+ case 'actionResult':
508
+ if (msg.action === 'fetch') {
509
+ fetchInProgress = false;
510
+ btnFetch.classList.remove('btn-fetching');
511
+ btnFetch.title = 'Fetch from remotes';
512
+ if (!msg.result.ok) {
513
+ document.getElementById('status-text').textContent = 'Fetch failed: ' + (msg.result.error || 'Unknown error');
514
+ }
515
+ } else if (!msg.result.ok && msg.action === 'createBranch' && msg.result.error && msg.result.error.includes('already exists')) {
516
+ // Extract branch name from error: "fatal: a branch named 'X' already exists"
517
+ const branchMatch = msg.result.error.match(/branch named '([^']+)'/);
518
+ const branchName = branchMatch ? branchMatch[1] : 'this branch';
519
+ showDialog({
520
+ title: 'Branch Already Exists',
521
+ message: 'A branch named <b>' + escHtml(branchName) + '</b> already exists, do you want to replace it with this new branch?',
522
+ rawMessage: true,
523
+ confirmLabel: 'Yes, replace the existing branch',
524
+ cancelLabel: 'No, choose another branch name',
525
+ onConfirm: () => gitAction('createBranch', { ...msg.args, force: true }),
526
+ });
527
+ } else if (!msg.result.ok) {
528
+ showToast('Git action failed: ' + (msg.result.error || 'Unknown error'), 'error');
529
+ }
530
+ // Refresh worktree list after worktree mutations
531
+ if (msg.result.ok && (msg.action === 'addWorktree' || msg.action === 'removeWorktree' || msg.action === 'pruneWorktrees')) {
532
+ vscode.postMessage({ command: 'requestWorktrees' });
533
+ }
534
+ break;
535
+ case 'loadWorktrees':
536
+ state.worktrees = msg.data || [];
537
+ renderWorktreeList();
538
+ break;
539
+ case 'error':
540
+ document.getElementById('status-text').textContent = 'Error: ' + msg.message;
541
+ break;
542
+ }
543
+ });
544
+
545
+ // --- File click delegation (opens diff tab) ---
546
+ document.getElementById('detail-panel').addEventListener('click', (e) => {
547
+ // File-level action buttons (stage/unstage/discard/open)
548
+ const actionBtn = e.target.closest('.file-action-btn');
549
+ if (actionBtn) {
550
+ e.stopPropagation();
551
+ const action = actionBtn.dataset.action;
552
+ const file = actionBtn.dataset.file;
553
+ if (action === 'open') {
554
+ vscode.postMessage({ command: 'openFile', filePath: file });
555
+ } else if (action === 'stage') {
556
+ vscode.postMessage({ command: 'gitAction', action: 'stage', args: { files: [file] } });
557
+ } else if (action === 'unstage') {
558
+ vscode.postMessage({ command: 'gitAction', action: 'unstage', args: { files: [file] } });
559
+ } else if (action === 'discard') {
560
+ showDialog({
561
+ title: 'Discard Changes',
562
+ message: 'Discard changes to "' + file + '"? This cannot be undone.',
563
+ destructive: true,
564
+ confirmLabel: 'Discard',
565
+ onConfirm: () => vscode.postMessage({ command: 'gitAction', action: 'discard', args: { files: [file] } }),
566
+ });
567
+ }
568
+ return;
569
+ }
570
+ // Section-level actions (Stage All / Unstage All)
571
+ const sectionBtn = e.target.closest('.section-action-btn');
572
+ if (sectionBtn) {
573
+ e.stopPropagation();
574
+ const action = sectionBtn.dataset.action;
575
+ if (action === 'stage-all') {
576
+ const files = state.uncommitted.unstaged.map(f => f.path);
577
+ vscode.postMessage({ command: 'gitAction', action: 'stage', args: { files } });
578
+ } else if (action === 'unstage-all') {
579
+ const files = state.uncommitted.staged.map(f => f.path);
580
+ vscode.postMessage({ command: 'gitAction', action: 'unstage', args: { files } });
581
+ }
582
+ return;
583
+ }
584
+ // Toggle buttons (tree/list view)
585
+ const toggleBtn = e.target.closest('.toggle-btn[data-view]');
586
+ if (toggleBtn) {
587
+ state.fileViewMode = toggleBtn.dataset.view;
588
+ if (state.selectedCommit === 'uncommitted') {
589
+ renderUncommittedDetail();
590
+ } else if (state._lastDetail) {
591
+ renderDetailPanel(state._lastDetail);
592
+ }
593
+ return;
594
+ }
595
+ // File click (opens diff)
596
+ const item = e.target.closest('.file-clickable');
597
+ if (!item) return;
598
+ const filePath = item.dataset.path;
599
+ const hash = item.dataset.hash;
600
+ const parentHash = item.dataset.parent || null;
601
+ if (filePath && hash) {
602
+ vscode.postMessage({ command: 'openDiff', filePath, hash, parentHash });
603
+ }
604
+ });
605
+
606
+ // --- Branch dropdown ---
607
+ let selectedBranch = 'all';
608
+ const branchTrigger = document.getElementById('branch-trigger');
609
+ const branchMenu = document.getElementById('branch-dropdown-menu');
610
+ const branchFilterInput = document.getElementById('branch-filter');
611
+ const branchListEl = document.getElementById('branch-list');
612
+
613
+ branchTrigger.addEventListener('click', (e) => {
614
+ e.stopPropagation();
615
+ const wasHidden = branchMenu.classList.contains('hidden');
616
+ branchMenu.classList.toggle('hidden');
617
+ if (wasHidden) {
618
+ branchFilterInput.value = '';
619
+ renderBranchOptions('');
620
+ branchFilterInput.focus();
621
+ }
622
+ });
623
+
624
+ document.addEventListener('click', (e) => {
625
+ if (!e.target.closest('#branch-selector')) branchMenu.classList.add('hidden');
626
+ });
627
+
628
+ branchFilterInput.addEventListener('input', () => {
629
+ renderBranchOptions(branchFilterInput.value.toLowerCase());
630
+ });
631
+ branchFilterInput.addEventListener('click', (e) => e.stopPropagation());
632
+
633
+ function renderBranchOptions(filter) {
634
+ const options = [{ name: 'all', label: 'All Branches', current: false }];
635
+ state.branches.forEach(b => {
636
+ if (b.remote && !state.settings.showRemoteBranches) return;
637
+ options.push({ name: b.name, label: (b.current ? '* ' : '') + b.name, current: b.current });
638
+ });
639
+ const filtered = filter ? options.filter(o => o.label.toLowerCase().includes(filter)) : options;
640
+ branchListEl.innerHTML = filtered.map(o =>
641
+ '<div class="branch-option' + (o.name === selectedBranch ? ' selected' : '') + '" data-branch="' + escHtml(o.name) + '">' + escHtml(o.label) + '</div>'
642
+ ).join('');
643
+ }
644
+
645
+ branchListEl.addEventListener('click', (e) => {
646
+ const opt = e.target.closest('.branch-option');
647
+ if (!opt) return;
648
+ const branch = opt.dataset.branch;
649
+ selectedBranch = branch;
650
+ branchTrigger.textContent = branch === 'all' ? 'All Branches' : branch;
651
+ branchMenu.classList.add('hidden');
652
+ state.commits = [];
653
+ document.getElementById('commit-list').innerHTML = '';
654
+ vscode.postMessage({ command: 'requestCommits', branch, maxCommits: state.maxCommits });
655
+ });
656
+
657
+ function renderBranchSelector() {
658
+ const branchNames = state.branches.map(b => b.name);
659
+ if (selectedBranch !== 'all' && !branchNames.includes(selectedBranch)) selectedBranch = 'all';
660
+ branchTrigger.textContent = selectedBranch === 'all' ? 'All Branches' : selectedBranch;
661
+ }
662
+
663
+ // --- Refresh ---
664
+ document.getElementById('btn-refresh').addEventListener('click', () => {
665
+ state.commits = [];
666
+ document.getElementById('commit-list').innerHTML = '';
667
+ vscode.postMessage({ command: 'requestRepoInfo' });
668
+ vscode.postMessage({ command: 'requestCommits', maxCommits: state.maxCommits });
669
+ });
670
+
671
+ // --- Fetch ---
672
+ const btnFetch = document.getElementById('btn-fetch');
673
+ let fetchInProgress = false;
674
+ let autoFetchTimer = null;
675
+
676
+ function doFetch() {
677
+ fetchInProgress = true;
678
+ btnFetch.classList.add('btn-fetching');
679
+ btnFetch.title = 'Fetching...';
680
+ vscode.postMessage({ command: 'gitAction', action: 'fetch', args: { prune: true } });
681
+ }
682
+
683
+ btnFetch.addEventListener('click', () => { if (!fetchInProgress) doFetch(); });
684
+
685
+ function startAutoFetch(intervalSec) {
686
+ stopAutoFetch();
687
+ if (!intervalSec || intervalSec <= 0) return;
688
+ const ms = Math.max(intervalSec, 10) * 1000;
689
+ autoFetchTimer = setInterval(() => { if (!fetchInProgress) doFetch(); }, ms);
690
+ }
691
+ function stopAutoFetch() {
692
+ if (autoFetchTimer) { clearInterval(autoFetchTimer); autoFetchTimer = null; }
693
+ }
694
+
695
+ // --- Worktree popover ---
696
+ const wtPopover = document.getElementById('worktree-popover');
697
+ const btnWorktree = document.getElementById('btn-worktree');
698
+
699
+ btnWorktree.addEventListener('click', (e) => {
700
+ e.stopPropagation();
701
+ const wasHidden = wtPopover.classList.contains('hidden');
702
+ wtPopover.classList.toggle('hidden');
703
+ if (wasHidden) vscode.postMessage({ command: 'requestWorktrees' });
704
+ });
705
+
706
+ document.addEventListener('click', (e) => {
707
+ if (!e.target.closest('.worktree-dropdown')) wtPopover.classList.add('hidden');
708
+ });
709
+
710
+ function renderWorktreeList() {
711
+ const listEl = document.getElementById('worktree-list');
712
+ const countEl = btnWorktree.querySelector('.wt-count');
713
+ const wts = state.worktrees;
714
+ if (countEl) {
715
+ countEl.textContent = wts.length;
716
+ countEl.style.display = wts.length > 1 ? '' : 'none';
717
+ }
718
+ if (!wts.length) {
719
+ listEl.innerHTML = '<div class="wt-empty">No worktrees found</div>';
720
+ return;
721
+ }
722
+ listEl.innerHTML = wts.map((wt, i) => {
723
+ const branchName = wt.branch || (wt.isDetached ? 'detached HEAD' : '(bare)');
724
+ const shortHash = wt.head ? wt.head.substring(0, 7) : '';
725
+ const isCurrent = wt.path === state.repo;
726
+ let badges = '';
727
+ if (isCurrent) badges += ' <span class="wt-badge wt-badge-current">current</span>';
728
+ if (wt.isMain && !isCurrent) badges += ' <span class="wt-badge">main</span>';
729
+ if (wt.locked) badges += ' <span class="wt-badge wt-badge-locked">locked</span>';
730
+ if (wt.prunable) badges += ' <span class="wt-badge">prunable</span>';
731
+ if (wt.isDetached) badges += ' <span class="wt-badge">detached</span>';
732
+ const actions = isCurrent ? ''
733
+ : '<button class="wt-open" data-idx="' + i + '" title="Open in PPM">' + ICONS.fileOpen + '</button>'
734
+ + (wt.isMain ? '' : '<button class="wt-remove" data-idx="' + i + '" title="Remove worktree">' + ICONS.trash + '</button>');
735
+ return '<div class="wt-item' + (isCurrent ? ' wt-item-active' : '') + '">'
736
+ + '<div class="wt-item-info">'
737
+ + '<div class="wt-item-branch">' + ICONS.gitBranch + ' ' + escHtml(branchName) + badges + '</div>'
738
+ + '<div class="wt-item-path" title="' + escHtml(wt.path) + '">' + escHtml(wt.path) + ' <span style="color:var(--subtle)">' + shortHash + '</span></div>'
739
+ + '</div>'
740
+ + (actions ? '<div class="wt-item-actions">' + actions + '</div>' : '')
741
+ + '</div>';
742
+ }).join('');
743
+
744
+ // Bind action buttons
745
+ listEl.querySelectorAll('.wt-open').forEach(btn => {
746
+ btn.addEventListener('click', (e) => {
747
+ e.stopPropagation();
748
+ const wt = state.worktrees[parseInt(btn.dataset.idx)];
749
+ if (wt) vscode.postMessage({ command: 'openWorktree', path: wt.path });
750
+ });
751
+ });
752
+ listEl.querySelectorAll('.wt-remove').forEach(btn => {
753
+ btn.addEventListener('click', (e) => {
754
+ e.stopPropagation();
755
+ const wt = state.worktrees[parseInt(btn.dataset.idx)];
756
+ if (!wt) return;
757
+ showDialog({
758
+ title: 'Remove Worktree',
759
+ message: 'Remove worktree at "' + wt.path + '"?',
760
+ destructive: true,
761
+ confirmLabel: 'Remove',
762
+ onConfirm: () => vscode.postMessage({ command: 'removeWorktree', path: wt.path }),
763
+ });
764
+ });
765
+ });
766
+ }
767
+
768
+ document.getElementById('wt-add').addEventListener('click', () => {
769
+ showCreateWorktreeDialog();
770
+ });
771
+
772
+ document.getElementById('wt-prune').addEventListener('click', () => {
773
+ showDialog({
774
+ title: 'Prune Worktrees',
775
+ message: 'Remove stale worktree entries (worktrees whose directories no longer exist)?',
776
+ confirmLabel: 'Prune',
777
+ onConfirm: () => vscode.postMessage({ command: 'pruneWorktrees' }),
778
+ });
779
+ });
780
+
781
+ function showCreateWorktreeDialog(startPoint) {
782
+ const overlay = document.createElement('div');
783
+ overlay.className = 'dialog-overlay';
784
+ const dialog = document.createElement('div');
785
+ dialog.className = 'dialog';
786
+ dialog.innerHTML = '<h3>Add Worktree</h3>'
787
+ + '<p style="font-size:12px;margin-bottom:8px">Path for the new worktree directory:</p>'
788
+ + '<input type="text" id="wt-dialog-path" placeholder="/path/to/worktree" style="width:100%;margin-bottom:8px" />'
789
+ + '<p style="font-size:12px;margin-bottom:4px">Branch:</p>'
790
+ + '<div style="display:flex;gap:8px;margin-bottom:6px">'
791
+ + '<label style="font-size:11px;display:flex;align-items:center;gap:4px"><input type="radio" name="wt-branch-mode" value="existing" checked /> Existing branch</label>'
792
+ + '<label style="font-size:11px;display:flex;align-items:center;gap:4px"><input type="radio" name="wt-branch-mode" value="new" /> New branch</label>'
793
+ + '</div>'
794
+ + '<input type="text" id="wt-dialog-branch" placeholder="Branch name" style="width:100%;margin-bottom:8px" />'
795
+ + (startPoint ? '<input type="hidden" id="wt-dialog-start" value="' + escHtml(startPoint) + '" />' : '<input type="text" id="wt-dialog-start" placeholder="Start point (commit/branch, optional)" style="width:100%;margin-bottom:8px" />');
796
+
797
+ const actions = document.createElement('div');
798
+ actions.className = 'dialog-actions';
799
+ const cancelBtn = document.createElement('button');
800
+ cancelBtn.textContent = 'Cancel';
801
+ cancelBtn.className = 'secondary';
802
+ cancelBtn.addEventListener('click', () => overlay.remove());
803
+ const confirmBtn = document.createElement('button');
804
+ confirmBtn.textContent = 'Create';
805
+ confirmBtn.className = 'btn-primary';
806
+ confirmBtn.addEventListener('click', () => {
807
+ const path = dialog.querySelector('#wt-dialog-path').value.trim();
808
+ if (!path) { showToast('Path is required', 'error'); return; }
809
+ const branch = dialog.querySelector('#wt-dialog-branch').value.trim();
810
+ const mode = dialog.querySelector('input[name="wt-branch-mode"]:checked').value;
811
+ const sp = dialog.querySelector('#wt-dialog-start');
812
+ const startPt = sp ? sp.value.trim() : '';
813
+ const msg = { command: 'addWorktree', path };
814
+ if (mode === 'new' && branch) { msg.newBranch = branch; }
815
+ else if (branch) { msg.branch = branch; }
816
+ if (startPt) msg.startPoint = startPt;
817
+ vscode.postMessage(msg);
818
+ overlay.remove();
819
+ });
820
+ actions.appendChild(cancelBtn);
821
+ actions.appendChild(confirmBtn);
822
+ dialog.appendChild(actions);
823
+ overlay.appendChild(dialog);
824
+ document.body.appendChild(overlay);
825
+ setTimeout(() => dialog.querySelector('#wt-dialog-path').focus(), 50);
826
+ overlay.addEventListener('keydown', (e) => {
827
+ if (e.key === 'Escape') overlay.remove();
828
+ if (e.key === 'Enter') confirmBtn.click();
829
+ });
830
+ }
831
+
832
+ // --- Graph column resize ---
833
+ {
834
+ const resizeHandle = document.getElementById('graph-resize-handle');
835
+ let resizing = false, startX = 0, startW = 0;
836
+ resizeHandle.addEventListener('pointerdown', (e) => {
837
+ e.preventDefault();
838
+ resizing = true;
839
+ startX = e.clientX;
840
+ startW = document.querySelector('.col-graph').offsetWidth;
841
+ resizeHandle.classList.add('dragging');
842
+ resizeHandle.setPointerCapture(e.pointerId);
843
+ });
844
+ resizeHandle.addEventListener('pointermove', (e) => {
845
+ if (!resizing) return;
846
+ const newW = Math.max(40, Math.min(400, startW + e.clientX - startX));
847
+ document.documentElement.style.setProperty('--graph-col-w', newW + 'px');
848
+ });
849
+ resizeHandle.addEventListener('pointerup', (e) => {
850
+ if (!resizing) return;
851
+ resizing = false;
852
+ resizeHandle.classList.remove('dragging');
853
+ const newW = Math.max(40, Math.min(400, startW + e.clientX - startX));
854
+ state.graphColWidth = newW;
855
+ document.documentElement.style.setProperty('--graph-col-w', newW + 'px');
856
+ });
857
+ resizeHandle.addEventListener('dblclick', () => {
858
+ state.graphColWidth = null;
859
+ graphRender(-1);
860
+ });
861
+ }
862
+
863
+ // --- Graph rendering (faithful port of vscode-git-graph graph.ts) ---
864
+
865
+ class GBranch {
866
+ constructor(colour) {
867
+ this._colour = colour;
868
+ this._end = 0;
869
+ this._lines = [];
870
+ this._numUncommitted = 0;
871
+ }
872
+ addLine(p1, p2, isCommitted, lockedFirst) {
873
+ this._lines.push({ p1, p2, lockedFirst });
874
+ if (isCommitted) {
875
+ if (p2.x === 0 && p2.y < this._numUncommitted) this._numUncommitted = p2.y;
876
+ } else {
877
+ this._numUncommitted++;
878
+ }
879
+ }
880
+ getColour() { return this._colour; }
881
+ getEnd() { return this._end; }
882
+ setEnd(end) { this._end = end; }
883
+
884
+ draw(svg, config, expandAt) {
885
+ const colour = config.colours[this._colour % config.colours.length];
886
+ const d = config.grid.y * (config.style === 'angular' ? 0.38 : 0.8);
887
+ const pxLines = [];
888
+ let curPath = '';
889
+
890
+ for (let i = 0; i < this._lines.length; i++) {
891
+ const ln = this._lines[i];
892
+ let x1 = ln.p1.x * config.grid.x + config.grid.offsetX;
893
+ let y1 = ln.p1.y * config.grid.y + config.grid.offsetY;
894
+ let x2 = ln.p2.x * config.grid.x + config.grid.offsetX;
895
+ let y2 = ln.p2.y * config.grid.y + config.grid.offsetY;
896
+
897
+ if (expandAt > -1) {
898
+ if (ln.p1.y > expandAt) {
899
+ y1 += config.grid.expandY; y2 += config.grid.expandY;
900
+ } else if (ln.p2.y > expandAt) {
901
+ if (x1 === x2) {
902
+ y2 += config.grid.expandY;
903
+ } else if (ln.lockedFirst) {
904
+ pxLines.push({ p1: { x: x1, y: y1 }, p2: { x: x2, y: y2 }, isC: i >= this._numUncommitted, lf: ln.lockedFirst });
905
+ pxLines.push({ p1: { x: x2, y: y1 + config.grid.y }, p2: { x: x2, y: y2 + config.grid.expandY }, isC: i >= this._numUncommitted, lf: ln.lockedFirst });
906
+ continue;
907
+ } else {
908
+ pxLines.push({ p1: { x: x1, y: y1 }, p2: { x: x1, y: y2 - config.grid.y + config.grid.expandY }, isC: i >= this._numUncommitted, lf: ln.lockedFirst });
909
+ y1 += config.grid.expandY; y2 += config.grid.expandY;
910
+ }
911
+ }
912
+ }
913
+ pxLines.push({ p1: { x: x1, y: y1 }, p2: { x: x2, y: y2 }, isC: i >= this._numUncommitted, lf: ln.lockedFirst });
914
+ }
915
+
916
+ // Simplify consecutive vertical segments
917
+ let si = 0;
918
+ while (si < pxLines.length - 1) {
919
+ const a = pxLines[si], b = pxLines[si + 1];
920
+ if (a.p1.x === a.p2.x && a.p2.x === b.p1.x && b.p1.x === b.p2.x && a.p2.y === b.p1.y && a.isC === b.isC) {
921
+ a.p2.y = b.p2.y;
922
+ pxLines.splice(si + 1, 1);
923
+ } else { si++; }
924
+ }
925
+
926
+ // Build SVG paths
927
+ for (let i = 0; i < pxLines.length; i++) {
928
+ const pl = pxLines[i];
929
+ const x1 = pl.p1.x, y1 = pl.p1.y, x2 = pl.p2.x, y2 = pl.p2.y;
930
+
931
+ if (curPath !== '' && i > 0 && pl.isC !== pxLines[i - 1].isC) {
932
+ GBranch._drawPath(svg, curPath, pxLines[i - 1].isC, colour);
933
+ curPath = '';
934
+ }
935
+ if (curPath === '' || (i > 0 && (x1 !== pxLines[i - 1].p2.x || y1 !== pxLines[i - 1].p2.y))) {
936
+ curPath += 'M' + x1.toFixed(0) + ',' + y1.toFixed(1);
937
+ }
938
+ if (x1 === x2) {
939
+ curPath += 'L' + x2.toFixed(0) + ',' + y2.toFixed(1);
940
+ } else if (config.style === 'angular') {
941
+ curPath += 'L' + (pl.lf ? (x2.toFixed(0) + ',' + (y2 - d).toFixed(1)) : (x1.toFixed(0) + ',' + (y1 + d).toFixed(1))) + 'L' + x2.toFixed(0) + ',' + y2.toFixed(1);
942
+ } else {
943
+ curPath += 'C' + x1.toFixed(0) + ',' + (y1 + d).toFixed(1) + ' ' + x2.toFixed(0) + ',' + (y2 - d).toFixed(1) + ' ' + x2.toFixed(0) + ',' + y2.toFixed(1);
944
+ }
945
+ }
946
+ if (curPath !== '') GBranch._drawPath(svg, curPath, pxLines[pxLines.length - 1].isC, colour);
947
+ }
948
+
949
+ static _drawPath(svg, path, isCommitted, colour) {
950
+ const line = document.createElementNS(SVG_NS, 'path');
951
+ line.setAttribute('class', 'line');
952
+ line.setAttribute('d', path);
953
+ line.setAttribute('stroke', isCommitted ? colour : '#808080');
954
+ if (!isCommitted) line.setAttribute('stroke-dasharray', '2');
955
+ svg.appendChild(line);
956
+ }
957
+ }
958
+
959
+ class GVertex {
960
+ constructor(id, isStash) {
961
+ this.id = id;
962
+ this.isStash = isStash;
963
+ this._x = 0;
964
+ this._children = [];
965
+ this._parents = [];
966
+ this._nextParent = 0;
967
+ this._onBranch = null;
968
+ this._isCommitted = true;
969
+ this._isCurrent = false;
970
+ this._nextX = 0;
971
+ this._connections = [];
972
+ }
973
+ addChild(v) { this._children.push(v); }
974
+ getChildren() { return this._children; }
975
+ addParent(v) { this._parents.push(v); }
976
+ getParents() { return this._parents; }
977
+ hasParents() { return this._parents.length > 0; }
978
+ getNextParent() { return this._nextParent < this._parents.length ? this._parents[this._nextParent] : null; }
979
+ registerParentProcessed() { this._nextParent++; }
980
+ isMerge() { return this._parents.length > 1; }
981
+
982
+ addToBranch(branch, x) { if (this._onBranch === null) { this._onBranch = branch; this._x = x; } }
983
+ isNotOnBranch() { return this._onBranch === null; }
984
+ isOnThisBranch(branch) { return this._onBranch === branch; }
985
+ getBranch() { return this._onBranch; }
986
+
987
+ getPoint() { return { x: this._x, y: this.id }; }
988
+ getNextPoint() { return { x: this._nextX, y: this.id }; }
989
+
990
+ getPointConnectingTo(vertex, onBranch) {
991
+ for (let i = 0; i < this._connections.length; i++) {
992
+ if (this._connections[i].connectsTo === vertex && this._connections[i].onBranch === onBranch) return { x: i, y: this.id };
993
+ }
994
+ return null;
995
+ }
996
+ registerUnavailablePoint(x, connectsTo, onBranch) {
997
+ if (x === this._nextX) { this._nextX = x + 1; this._connections[x] = { connectsTo, onBranch }; }
998
+ }
999
+
1000
+ getColour() { return this._onBranch !== null ? this._onBranch.getColour() : 0; }
1001
+ getIsCommitted() { return this._isCommitted; }
1002
+ setNotCommitted() { this._isCommitted = false; }
1003
+ setCurrent() { this._isCurrent = true; }
1004
+
1005
+ draw(svg, config, expandOffset, overListener, outListener) {
1006
+ if (this._onBranch === null) return;
1007
+ const colour = this._isCommitted ? config.colours[this._onBranch.getColour() % config.colours.length] : '#808080';
1008
+ const cx = (this._x * config.grid.x + config.grid.offsetX).toString();
1009
+ const cy = (this.id * config.grid.y + config.grid.offsetY + (expandOffset ? config.grid.expandY : 0)).toString();
1010
+
1011
+ const circle = document.createElementNS(SVG_NS, 'circle');
1012
+ circle.dataset.id = this.id.toString();
1013
+ circle.setAttribute('cx', cx);
1014
+ circle.setAttribute('cy', cy);
1015
+ circle.setAttribute('r', '4');
1016
+ if (this._isCurrent) {
1017
+ circle.setAttribute('class', 'graphCurrent');
1018
+ circle.setAttribute('stroke', colour);
1019
+ } else {
1020
+ circle.setAttribute('fill', colour);
1021
+ }
1022
+ svg.appendChild(circle);
1023
+
1024
+ if (this.isStash && !this._isCurrent) {
1025
+ circle.setAttribute('r', '4.5');
1026
+ circle.setAttribute('class', 'graphStashOuter');
1027
+ const inner = document.createElementNS(SVG_NS, 'circle');
1028
+ inner.setAttribute('cx', cx);
1029
+ inner.setAttribute('cy', cy);
1030
+ inner.setAttribute('r', '2');
1031
+ inner.setAttribute('class', 'graphStashInner');
1032
+ svg.appendChild(inner);
1033
+ }
1034
+
1035
+ circle.addEventListener('mouseover', overListener);
1036
+ circle.addEventListener('mouseout', outListener);
1037
+ }
1038
+ }
1039
+
1040
+ // --- Graph layout state ---
1041
+ let gVertices = [], gBranches = [], gAvailColours = [], gCommitLookup = {};
1042
+
1043
+ function graphLoadCommits(commits) {
1044
+ gVertices = []; gBranches = []; gAvailColours = [];
1045
+ if (commits.length === 0) return;
1046
+
1047
+ const stashHashes = new Set(state.stashes.map(s => s.hash));
1048
+ const nullVertex = new GVertex(NULL_VERTEX_ID, false);
1049
+ const lookup = {};
1050
+ for (let i = 0; i < commits.length; i++) {
1051
+ lookup[commits[i].hash] = i;
1052
+ gVertices.push(new GVertex(i, stashHashes.has(commits[i].hash)));
1053
+ }
1054
+ gCommitLookup = lookup;
1055
+
1056
+ for (let i = 0; i < commits.length; i++) {
1057
+ for (let j = 0; j < commits[i].parents.length; j++) {
1058
+ const ph = commits[i].parents[j];
1059
+ if (typeof lookup[ph] === 'number') {
1060
+ gVertices[i].addParent(gVertices[lookup[ph]]);
1061
+ gVertices[lookup[ph]].addChild(gVertices[i]);
1062
+ } else {
1063
+ gVertices[i].addParent(nullVertex);
1064
+ }
1065
+ }
1066
+ }
1067
+
1068
+ if (state.head && typeof lookup[state.head] === 'number') {
1069
+ gVertices[lookup[state.head]].setCurrent();
1070
+ }
1071
+
1072
+ let i = 0;
1073
+ while (i < gVertices.length) {
1074
+ if (gVertices[i].getNextParent() !== null || gVertices[i].isNotOnBranch()) {
1075
+ graphDeterminePath(i);
1076
+ } else { i++; }
1077
+ }
1078
+ }
1079
+
1080
+ function graphDeterminePath(startAt) {
1081
+ let i = startAt;
1082
+ let vertex = gVertices[i], parentVertex = gVertices[i].getNextParent(), curVertex;
1083
+ let lastPoint = vertex.isNotOnBranch() ? vertex.getNextPoint() : vertex.getPoint(), curPoint;
1084
+
1085
+ if (parentVertex !== null && parentVertex.id !== NULL_VERTEX_ID && vertex.isMerge() && !vertex.isNotOnBranch() && !parentVertex.isNotOnBranch()) {
1086
+ let foundPtp = false, pBranch = parentVertex.getBranch();
1087
+ for (i = startAt + 1; i < gVertices.length; i++) {
1088
+ curVertex = gVertices[i];
1089
+ curPoint = curVertex.getPointConnectingTo(parentVertex, pBranch);
1090
+ if (curPoint !== null) { foundPtp = true; } else { curPoint = curVertex.getNextPoint(); }
1091
+ pBranch.addLine(lastPoint, curPoint, vertex.getIsCommitted(), !foundPtp && curVertex !== parentVertex ? lastPoint.x < curPoint.x : true);
1092
+ curVertex.registerUnavailablePoint(curPoint.x, parentVertex, pBranch);
1093
+ lastPoint = curPoint;
1094
+ if (foundPtp) { vertex.registerParentProcessed(); break; }
1095
+ }
1096
+ } else {
1097
+ const branch = new GBranch(graphGetAvailableColour(startAt));
1098
+ vertex.addToBranch(branch, lastPoint.x);
1099
+ vertex.registerUnavailablePoint(lastPoint.x, vertex, branch);
1100
+ for (i = startAt + 1; i < gVertices.length; i++) {
1101
+ curVertex = gVertices[i];
1102
+ curPoint = parentVertex === curVertex && !parentVertex.isNotOnBranch() ? curVertex.getPoint() : curVertex.getNextPoint();
1103
+ branch.addLine(lastPoint, curPoint, vertex.getIsCommitted(), lastPoint.x < curPoint.x);
1104
+ curVertex.registerUnavailablePoint(curPoint.x, parentVertex, branch);
1105
+ lastPoint = curPoint;
1106
+ if (parentVertex === curVertex) {
1107
+ vertex.registerParentProcessed();
1108
+ const onBranch = !parentVertex.isNotOnBranch();
1109
+ parentVertex.addToBranch(branch, curPoint.x);
1110
+ vertex = parentVertex;
1111
+ parentVertex = vertex.getNextParent();
1112
+ if (parentVertex === null || onBranch) break;
1113
+ }
1114
+ }
1115
+ if (i === gVertices.length && parentVertex !== null && parentVertex.id === NULL_VERTEX_ID) {
1116
+ vertex.registerParentProcessed();
1117
+ }
1118
+ branch.setEnd(i);
1119
+ gBranches.push(branch);
1120
+ gAvailColours[branch.getColour()] = i;
1121
+ }
1122
+ }
1123
+
1124
+ function graphGetAvailableColour(startAt) {
1125
+ for (let i = 0; i < gAvailColours.length; i++) {
1126
+ if (startAt > gAvailColours[i]) return i;
1127
+ }
1128
+ gAvailColours.push(0);
1129
+ return gAvailColours.length - 1;
1130
+ }
1131
+
1132
+ function graphRender(expandIdx) {
1133
+ const container = document.getElementById('graph-svg-container');
1134
+ container.innerHTML = '';
1135
+ if (gVertices.length === 0) { if (state.graphColWidth === null) document.documentElement.style.setProperty('--graph-col-w', '40px'); return; }
1136
+
1137
+ // Detect mobile: match CSS breakpoint where row height changes to 44px
1138
+ const isMobile = window.matchMedia('(max-width: 768px)').matches;
1139
+ const cfg = isMobile
1140
+ ? { ...graphConfig, grid: { ...graphConfig.grid, y: 44, offsetY: 22 } }
1141
+ : graphConfig;
1142
+
1143
+ const svg = document.createElementNS(SVG_NS, 'svg');
1144
+ const group = document.createElementNS(SVG_NS, 'g');
1145
+
1146
+ for (let i = 0; i < gBranches.length; i++) gBranches[i].draw(group, cfg, expandIdx);
1147
+
1148
+ const overL = (e) => graphVertexOver(e), outL = (e) => graphVertexOut(e);
1149
+ for (let i = 0; i < gVertices.length; i++) {
1150
+ gVertices[i].draw(group, cfg, expandIdx > -1 && i > expandIdx, overL, outL);
1151
+ }
1152
+
1153
+ svg.appendChild(group);
1154
+
1155
+ let maxX = 0;
1156
+ for (let i = 0; i < gVertices.length; i++) {
1157
+ const p = gVertices[i].getNextPoint();
1158
+ if (p.x > maxX) maxX = p.x;
1159
+ }
1160
+ const w = 2 * cfg.grid.offsetX + Math.max(maxX - 1, 0) * cfg.grid.x;
1161
+ const h = gVertices.length * cfg.grid.y + cfg.grid.offsetY - cfg.grid.y / 2 + (expandIdx > -1 ? cfg.grid.expandY : 0);
1162
+
1163
+ const gw = Math.max(w, 40);
1164
+ svg.setAttribute('width', gw.toString());
1165
+ svg.setAttribute('height', h.toString());
1166
+ container.appendChild(svg);
1167
+ if (state.graphColWidth === null) document.documentElement.style.setProperty('--graph-col-w', gw + 'px');
1168
+ }
1169
+
1170
+ function graphVertexOver(e) {
1171
+ if (!e.target || !e.target.dataset || !e.target.dataset.id) return;
1172
+ const id = parseInt(e.target.dataset.id);
1173
+ if (id >= 0 && id < state.commits.length) {
1174
+ const rows = document.querySelectorAll('.commit-row:not(.header-row)');
1175
+ if (rows[id]) rows[id].classList.add('graph-hover');
1176
+ e.target.setAttribute('r', e.target.classList.contains('graphStashOuter') ? '5.5' : '5');
1177
+ }
1178
+ }
1179
+ function graphVertexOut(e) {
1180
+ if (!e.target || !e.target.dataset || !e.target.dataset.id) return;
1181
+ const id = parseInt(e.target.dataset.id);
1182
+ if (id >= 0) {
1183
+ const rows = document.querySelectorAll('.commit-row:not(.header-row)');
1184
+ if (rows[id]) rows[id].classList.remove('graph-hover');
1185
+ e.target.setAttribute('r', e.target.classList.contains('graphStashOuter') ? '4.5' : '4');
1186
+ }
1187
+ }
1188
+
1189
+ // --- Commit list ---
1190
+ function getDisplayCommits() {
1191
+ const u = state.uncommitted;
1192
+ if (!u || (u.staged.length === 0 && u.unstaged.length === 0)) return state.commits;
1193
+ const virtualCommit = {
1194
+ hash: 'uncommitted',
1195
+ parents: state.head ? [state.head] : [],
1196
+ author: '',
1197
+ authorEmail: '',
1198
+ authorDate: Math.floor(Date.now() / 1000),
1199
+ committer: '',
1200
+ committerEmail: '',
1201
+ commitDate: Math.floor(Date.now() / 1000),
1202
+ refs: [],
1203
+ message: 'Uncommitted Changes (' + (u.staged.length + u.unstaged.length) + ' files)',
1204
+ };
1205
+ return [virtualCommit, ...state.commits];
1206
+ }
1207
+
1208
+ function renderCommitList() {
1209
+ const container = document.getElementById('commit-list');
1210
+ container.innerHTML = '';
1211
+
1212
+ const displayCommits = getDisplayCommits();
1213
+ graphLoadCommits(displayCommits);
1214
+
1215
+ // Mark uncommitted vertex for dashed lines
1216
+ if (displayCommits.length > 0 && displayCommits[0].hash === 'uncommitted') {
1217
+ if (gVertices.length > 0) gVertices[0].setNotCommitted();
1218
+ }
1219
+
1220
+ displayCommits.forEach((commit, idx) => {
1221
+ const isVirtual = commit.hash === 'uncommitted';
1222
+ const row = document.createElement('div');
1223
+ row.className = 'commit-row' + (isVirtual ? ' virtual' : '');
1224
+ row.dataset.hash = commit.hash;
1225
+
1226
+ // Graph spacer column (SVG overlays this area)
1227
+ const graphCol = document.createElement('div');
1228
+ graphCol.className = 'col-graph';
1229
+
1230
+ // Message column with ref badges
1231
+ const msgCol = document.createElement('div');
1232
+ msgCol.className = 'col-message';
1233
+ let badges = '';
1234
+ if (commit.refs) {
1235
+ commit.refs.forEach(ref => {
1236
+ if (ref.type === 'tag' && !state.settings.showTags) return;
1237
+ if (ref.type === 'remote' && !state.settings.showRemoteBranches) return;
1238
+ badges += '<span class="ref-badge ref-' + ref.type + '">' + escHtml(ref.name) + '</span>';
1239
+ });
1240
+ }
1241
+ msgCol.innerHTML = badges + formatCommitMessage(commit.message);
1242
+
1243
+ // Attach context menu and double-click to ref badges
1244
+ msgCol.querySelectorAll('.ref-badge').forEach(badge => {
1245
+ const refName = badge.textContent;
1246
+ const refType = badge.className.includes('ref-head') ? 'head'
1247
+ : badge.className.includes('ref-remote') ? 'remote'
1248
+ : badge.className.includes('ref-tag') ? 'tag' : 'local';
1249
+ badge.style.cursor = 'pointer';
1250
+ badge.addEventListener('dblclick', (e) => {
1251
+ e.stopPropagation();
1252
+ gitAction('checkout', { target: refName });
1253
+ });
1254
+ badge.addEventListener('contextmenu', (e) => {
1255
+ e.preventDefault();
1256
+ e.stopPropagation();
1257
+ showBranchContextMenu(e.clientX, e.clientY, refName, refType, commit);
1258
+ });
1259
+ });
1260
+
1261
+ const authorCol = document.createElement('div');
1262
+ authorCol.className = 'col-author';
1263
+ authorCol.textContent = isVirtual ? '' : commit.author;
1264
+
1265
+ const dateCol = document.createElement('div');
1266
+ dateCol.className = 'col-date';
1267
+ dateCol.textContent = isVirtual ? 'now' : formatDate(commit.commitDate);
1268
+
1269
+ const hashCol = document.createElement('div');
1270
+ hashCol.className = 'col-hash';
1271
+ hashCol.textContent = isVirtual ? '...' : commit.hash.substring(0, 7);
1272
+
1273
+ row.appendChild(graphCol);
1274
+ row.appendChild(msgCol);
1275
+ row.appendChild(authorCol);
1276
+ row.appendChild(dateCol);
1277
+ row.appendChild(hashCol);
1278
+
1279
+ row.addEventListener('click', () => selectCommit(commit.hash));
1280
+ if (isVirtual) {
1281
+ row.addEventListener('contextmenu', (e) => {
1282
+ e.preventDefault();
1283
+ showUncommittedContextMenu(e.clientX, e.clientY);
1284
+ });
1285
+ setupLongPress(row, (x, y) => showUncommittedContextMenu(x, y));
1286
+ } else {
1287
+ row.addEventListener('contextmenu', (e) => {
1288
+ e.preventDefault();
1289
+ showCommitContextMenu(e.clientX, e.clientY, commit);
1290
+ });
1291
+ setupLongPress(row, (x, y) => showCommitContextMenu(x, y, commit));
1292
+ }
1293
+
1294
+ container.appendChild(row);
1295
+ });
1296
+
1297
+ graphRender(-1);
1298
+ }
1299
+
1300
+ function selectCommit(hash) {
1301
+ // Deselect previous
1302
+ document.querySelectorAll('.commit-row.selected').forEach(el => el.classList.remove('selected'));
1303
+
1304
+ if (state.selectedCommit === hash) {
1305
+ state.selectedCommit = null;
1306
+ state.expandedCommit = null;
1307
+ document.getElementById('detail-panel').classList.add('hidden');
1308
+ return;
1309
+ }
1310
+
1311
+ state.selectedCommit = hash;
1312
+ state.expandedCommit = hash;
1313
+ const row = document.querySelector('[data-hash="' + CSS.escape(hash) + '"]');
1314
+ if (row) row.classList.add('selected');
1315
+
1316
+ if (hash === 'uncommitted') {
1317
+ renderUncommittedDetail();
1318
+ return;
1319
+ }
1320
+ vscode.postMessage({ command: 'requestCommitDetails', hash });
1321
+ }
1322
+
1323
+ // --- File tree helpers ---
1324
+ function buildFileTree(files) {
1325
+ const root = { name: '', children: {}, files: [] };
1326
+ for (const f of files) {
1327
+ const parts = f.path.split('/');
1328
+ let node = root;
1329
+ for (let i = 0; i < parts.length - 1; i++) {
1330
+ if (!node.children[parts[i]]) node.children[parts[i]] = { name: parts[i], children: {}, files: [] };
1331
+ node = node.children[parts[i]];
1332
+ }
1333
+ node.files.push({ ...f, fileName: parts[parts.length - 1] });
1334
+ }
1335
+ return root;
1336
+ }
1337
+
1338
+ function countFiles(node) {
1339
+ let count = node.files.length;
1340
+ for (const child of Object.values(node.children)) count += countFiles(child);
1341
+ return count;
1342
+ }
1343
+
1344
+ function renderFileTree(node, depth, hash, parentHash, section) {
1345
+ let html = '';
1346
+ const dirs = Object.keys(node.children).sort();
1347
+ for (const dir of dirs) {
1348
+ const child = node.children[dir];
1349
+ html += '<div class="tree-dir" style="padding-left:' + (depth * 16) + 'px">';
1350
+ html += ICONS.folderOpen + ' <span class="tree-dir-name">' + escHtml(dir) + '/</span>';
1351
+ html += '<span class="tree-dir-count">(' + countFiles(child) + ')</span></div>';
1352
+ html += renderFileTree(child, depth + 1, hash, parentHash, section);
1353
+ }
1354
+ const sortedFiles = [...node.files].sort((a, b) => a.fileName.localeCompare(b.fileName));
1355
+ for (const f of sortedFiles) {
1356
+ html += '<div class="file-item file-clickable" style="padding-left:' + (depth * 16) + 'px" ';
1357
+ html += 'data-path="' + escHtml(f.path) + '" data-hash="' + escHtml(hash) + '" data-parent="' + escHtml(parentHash) + '">';
1358
+ html += '<span class="file-status file-status-' + escHtml(f.status) + '">' + escHtml(f.status) + '</span>';
1359
+ html += '<span class="file-name">' + escHtml(f.fileName) + '</span>';
1360
+ if (f.additions > 0 || f.deletions > 0) {
1361
+ html += '<span class="file-stat">';
1362
+ if (f.additions > 0) html += '<span class="add">+' + f.additions + '</span> ';
1363
+ if (f.deletions > 0) html += '<span class="del">-' + f.deletions + '</span>';
1364
+ html += '</span>';
1365
+ }
1366
+ if (section) html += renderFileActions(f, section);
1367
+ html += '</div>';
1368
+ }
1369
+ return html;
1370
+ }
1371
+
1372
+ function renderFileListHtml(files, hash, parentHash, section) {
1373
+ if (state.fileViewMode === 'tree') {
1374
+ return renderFileTree(buildFileTree(files), 0, hash, parentHash, section);
1375
+ }
1376
+ return files.map(f =>
1377
+ '<div class="file-item file-clickable" data-path="' + escHtml(f.path) + '" data-hash="' + escHtml(hash) + '" data-parent="' + escHtml(parentHash || '') + '">' +
1378
+ '<span class="file-status file-status-' + escHtml(f.status) + '">' + escHtml(f.status) + '</span>' +
1379
+ '<span class="file-name">' + escHtml(f.path) + '</span>' +
1380
+ '<span class="file-stat">' +
1381
+ (f.additions > 0 ? '<span class="add">+' + f.additions + '</span> ' : '') +
1382
+ (f.deletions > 0 ? '<span class="del">-' + f.deletions + '</span>' : '') +
1383
+ '</span>' +
1384
+ (section ? renderFileActions(f, section) : '') +
1385
+ '</div>'
1386
+ ).join('');
1387
+ }
1388
+
1389
+ function renderFileActions(file, section) {
1390
+ let html = '<span class="file-actions">';
1391
+ if (section === 'unstaged') {
1392
+ html += '<button class="file-action-btn" data-action="stage" data-file="' + escHtml(file.path) + '" title="Stage">' + ICONS.plus + '</button>';
1393
+ html += '<button class="file-action-btn" data-action="discard" data-file="' + escHtml(file.path) + '" title="Discard changes">' + ICONS.x + '</button>';
1394
+ } else if (section === 'staged') {
1395
+ html += '<button class="file-action-btn" data-action="unstage" data-file="' + escHtml(file.path) + '" title="Unstage">' + ICONS.minus + '</button>';
1396
+ }
1397
+ html += '<button class="file-action-btn" data-action="open" data-file="' + escHtml(file.path) + '" title="Open file">' + ICONS.fileOpen + '</button>';
1398
+ html += '</span>';
1399
+ return html;
1400
+ }
1401
+
1402
+ function fileViewToggleHtml() {
1403
+ return '<div class="file-view-toggle">' +
1404
+ '<button class="toggle-btn' + (state.fileViewMode === 'list' ? ' active' : '') + '" data-view="list" title="List view">' + ICONS.list + '</button>' +
1405
+ '<button class="toggle-btn' + (state.fileViewMode === 'tree' ? ' active' : '') + '" data-view="tree" title="Tree view">' + ICONS.tree + '</button>' +
1406
+ '</div>';
1407
+ }
1408
+
1409
+ function renderUncommittedDetail() {
1410
+ const panel = document.getElementById('detail-panel');
1411
+ panel.classList.remove('hidden');
1412
+ const u = state.uncommitted;
1413
+ if (!u) { panel.classList.add('hidden'); return; }
1414
+ let html = '<h3>Uncommitted Changes</h3>';
1415
+ if (u.staged.length > 0 || u.unstaged.length > 0) {
1416
+ html += fileViewToggleHtml();
1417
+ }
1418
+ if (u.staged.length > 0) {
1419
+ html += '<div class="file-list"><div class="section-actions"><strong>Staged (' + u.staged.length + '):</strong>';
1420
+ html += '<button class="btn-sm section-action-btn" data-action="unstage-all">Unstage All</button></div>';
1421
+ html += renderFileListHtml(u.staged, 'staged', state.head, 'staged');
1422
+ html += '</div>';
1423
+ }
1424
+ if (u.unstaged.length > 0) {
1425
+ html += '<div class="file-list"><div class="section-actions"><strong>Unstaged (' + u.unstaged.length + '):</strong>';
1426
+ html += '<button class="btn-sm section-action-btn" data-action="stage-all">Stage All</button></div>';
1427
+ html += renderFileListHtml(u.unstaged, 'uncommitted', state.head, 'unstaged');
1428
+ html += '</div>';
1429
+ }
1430
+ if (u.staged.length === 0 && u.unstaged.length === 0) {
1431
+ html += '<p>No uncommitted changes.</p>';
1432
+ }
1433
+ html += '<div class="commit-section">';
1434
+ html += '<textarea id="commit-message" placeholder="Commit message..." rows="3"></textarea>';
1435
+ html += '<div class="commit-actions"><button id="btn-commit" class="btn-sm btn-commit" disabled>Commit</button></div>';
1436
+ html += '</div>';
1437
+ panel.innerHTML = html;
1438
+ wireCommitControls();
1439
+ }
1440
+
1441
+ function wireCommitControls() {
1442
+ const textarea = document.getElementById('commit-message');
1443
+ const commitBtn = document.getElementById('btn-commit');
1444
+ if (!textarea || !commitBtn) return;
1445
+ const updateBtn = () => {
1446
+ const hasMsg = textarea.value.trim().length > 0;
1447
+ const hasStaged = state.uncommitted && state.uncommitted.staged.length > 0;
1448
+ commitBtn.disabled = !(hasMsg && hasStaged);
1449
+ };
1450
+ textarea.addEventListener('input', updateBtn);
1451
+ updateBtn();
1452
+ commitBtn.addEventListener('click', () => {
1453
+ const message = textarea.value.trim();
1454
+ if (!message) return;
1455
+ vscode.postMessage({ command: 'gitAction', action: 'commit', args: { message } });
1456
+ textarea.value = '';
1457
+ commitBtn.disabled = true;
1458
+ });
1459
+ }
1460
+
1461
+ // --- Detail panel ---
1462
+ function renderDetailPanel(detail) {
1463
+ state._lastDetail = detail;
1464
+ const panel = document.getElementById('detail-panel');
1465
+ panel.classList.remove('hidden');
1466
+
1467
+ let html = '<h3>Commit Details</h3>';
1468
+ html += '<div class="detail-field"><span class="label">Hash:</span> ' + escHtml(detail.hash) + '</div>';
1469
+ html += '<div class="detail-field"><span class="label">Author:</span> ' + escHtml(detail.author) + ' &lt;' + escHtml(detail.authorEmail) + '&gt;</div>';
1470
+ html += '<div class="detail-field"><span class="label">Date:</span> ' + new Date(detail.authorDate * 1000).toLocaleString() + '</div>';
1471
+ if (detail.committer !== detail.author) {
1472
+ html += '<div class="detail-field"><span class="label">Committer:</span> ' + escHtml(detail.committer) + ' &lt;' + escHtml(detail.committerEmail) + '&gt;</div>';
1473
+ }
1474
+ if (detail.parents.length > 0) {
1475
+ html += '<div class="detail-field"><span class="label">Parents:</span> ' + detail.parents.map(p => escHtml(p.substring(0, 7))).join(', ') + '</div>';
1476
+ }
1477
+ html += '<div class="detail-message">' + escHtml(detail.message) + '</div>';
1478
+
1479
+ if (detail.fileChanges && detail.fileChanges.length > 0) {
1480
+ html += '<div class="file-list">' + fileViewToggleHtml() + '<strong>Files changed (' + detail.fileChanges.length + '):</strong>';
1481
+ html += renderFileListHtml(detail.fileChanges, detail.hash, detail.parents[0] || '');
1482
+ html += '</div>';
1483
+ }
1484
+
1485
+ panel.innerHTML = html;
1486
+ }
1487
+
1488
+ // --- Context menu ---
1489
+ function showCommitContextMenu(x, y, commit) {
1490
+ const menu = document.getElementById('context-menu');
1491
+ const items = [
1492
+ { label: 'Copy Commit Hash', action: () => copyText(commit.hash) },
1493
+ { label: 'Copy Short Hash', action: () => copyText(commit.hash.substring(0, 7)) },
1494
+ { separator: true },
1495
+ { label: 'Checkout...', action: () => gitAction('checkout', { target: commit.hash }) },
1496
+ { label: 'Create Branch Here...', action: () => promptAndAction('Branch name:', (name) => gitAction('createBranch', { name, startPoint: commit.hash })) },
1497
+ { label: 'Create Tag Here...', action: () => promptAndAction('Tag name:', (name) => gitAction('createTag', { name, hash: commit.hash })) },
1498
+ { label: 'Create Worktree Here...', action: () => showCreateWorktreeDialog(commit.hash) },
1499
+ ];
1500
+ // Add "Create PR" if PR creation is configured and commit has a branch ref
1501
+ if (state.settings.prCreation && state.settings.prCreation.urlTemplate) {
1502
+ const branchRef = (commit.refs || []).find(r => r.type === 'local' || r.type === 'head');
1503
+ if (branchRef) {
1504
+ items.push({ separator: true });
1505
+ items.push({ label: 'Create Pull Request (' + branchRef.name + ')', action: () => openPrUrl(branchRef.name) });
1506
+ }
1507
+ }
1508
+ items.push(
1509
+ { separator: true },
1510
+ { label: 'Cherry-pick', action: () => gitAction('cherryPick', { hash: commit.hash }) },
1511
+ { label: 'Revert', action: () => gitAction('revert', { hash: commit.hash }) },
1512
+ { separator: true },
1513
+ { label: 'Reset Current Branch to Here...', destructive: true, action: () => promptResetMode(commit.hash) },
1514
+ );
1515
+
1516
+ let html = '';
1517
+ items.forEach((item, idx) => {
1518
+ if (item.separator) {
1519
+ html += '<div class="ctx-separator"></div>';
1520
+ } else {
1521
+ html += '<div class="ctx-item' + (item.destructive ? ' destructive' : '') + '" data-idx="' + idx + '">' + escHtml(item.label) + '</div>';
1522
+ }
1523
+ });
1524
+ menu.innerHTML = html;
1525
+
1526
+ // Position (clamp to viewport)
1527
+ menu.style.left = Math.min(x, window.innerWidth - 200) + 'px';
1528
+ menu.style.top = Math.min(y, window.innerHeight - 300) + 'px';
1529
+ menu.classList.remove('hidden');
1530
+
1531
+ // Bind click handlers
1532
+ menu.querySelectorAll('.ctx-item').forEach(el => {
1533
+ const idx = parseInt(el.dataset.idx);
1534
+ const item = items[idx];
1535
+ if (item && item.action) el.addEventListener('click', () => { hideContextMenu(); item.action(); });
1536
+ });
1537
+
1538
+ // Close on click outside
1539
+ setTimeout(() => document.addEventListener('click', hideContextMenu, { once: true }), 0);
1540
+ }
1541
+
1542
+ function showUncommittedContextMenu(x, y) {
1543
+ const menu = document.getElementById('context-menu');
1544
+ const items = [
1545
+ { label: 'Stash Uncommitted Changes...', action: () => {
1546
+ showDialog({
1547
+ title: 'Stash Changes',
1548
+ input: { placeholder: 'Stash message (optional)' },
1549
+ confirmLabel: 'Stash',
1550
+ onConfirm: (message) => gitAction('stashSave', message ? { message } : {}),
1551
+ });
1552
+ }},
1553
+ { label: 'Reset Uncommitted Changes...', destructive: true, action: () => {
1554
+ showDialog({
1555
+ title: 'Reset Changes',
1556
+ message: 'Reset all uncommitted changes. Staged changes will be unstaged.',
1557
+ select: { options: ['mixed', 'hard'], defaultValue: 'mixed', label: 'Reset mode:' },
1558
+ destructive: true,
1559
+ confirmLabel: 'Reset',
1560
+ onConfirm: (mode) => {
1561
+ if (mode === 'hard') {
1562
+ showDialog({
1563
+ title: 'Confirm Hard Reset',
1564
+ message: 'WARNING: --hard will permanently discard ALL uncommitted changes!',
1565
+ destructive: true,
1566
+ confirmLabel: 'Reset Hard',
1567
+ onConfirm: () => gitAction('reset', { mode: 'hard', hash: 'HEAD' }),
1568
+ });
1569
+ } else {
1570
+ gitAction('reset', { mode, hash: 'HEAD' });
1571
+ }
1572
+ },
1573
+ });
1574
+ }},
1575
+ { label: 'Clean Untracked Files...', destructive: true, action: () => {
1576
+ showDialog({
1577
+ title: 'Clean Untracked Files',
1578
+ message: 'Permanently delete all untracked files and directories. This cannot be undone!',
1579
+ destructive: true,
1580
+ confirmLabel: 'Clean',
1581
+ onConfirm: () => gitAction('clean', {}),
1582
+ });
1583
+ }},
1584
+ { separator: true },
1585
+ { label: 'Open Source Control View', action: () => vscode.postMessage({ command: 'openSourceControl' }) },
1586
+ ];
1587
+
1588
+ let html = '';
1589
+ items.forEach((item, idx) => {
1590
+ if (item.separator) {
1591
+ html += '<div class="ctx-separator"></div>';
1592
+ } else {
1593
+ html += '<div class="ctx-item' + (item.destructive ? ' destructive' : '') + '" data-idx="' + idx + '">' + escHtml(item.label) + '</div>';
1594
+ }
1595
+ });
1596
+ menu.innerHTML = html;
1597
+ menu.style.left = Math.min(x, window.innerWidth - 200) + 'px';
1598
+ menu.style.top = Math.min(y, window.innerHeight - 300) + 'px';
1599
+ menu.classList.remove('hidden');
1600
+ menu.querySelectorAll('.ctx-item').forEach(el => {
1601
+ const idx = parseInt(el.dataset.idx);
1602
+ const item = items[idx];
1603
+ if (item && item.action) el.addEventListener('click', () => { hideContextMenu(); item.action(); });
1604
+ });
1605
+ setTimeout(() => document.addEventListener('click', hideContextMenu, { once: true }), 0);
1606
+ }
1607
+
1608
+ function showBranchContextMenu(x, y, branchName, refType, commit) {
1609
+ const menu = document.getElementById('context-menu');
1610
+ const items = [];
1611
+
1612
+ if (refType === 'head' || refType === 'local') {
1613
+ items.push(
1614
+ { label: 'Checkout "' + branchName + '"', action: () => gitAction('checkout', { target: branchName }) },
1615
+ { label: 'Merge into current branch', action: () => showDialog({
1616
+ title: 'Merge "' + branchName + '"',
1617
+ message: 'Merge "' + branchName + '" into the current branch?',
1618
+ confirmLabel: 'Merge',
1619
+ onConfirm: () => gitAction('merge', { branch: branchName }),
1620
+ })
1621
+ },
1622
+ { label: 'Rebase onto "' + branchName + '"', action: () => showDialog({
1623
+ title: 'Rebase onto "' + branchName + '"',
1624
+ message: 'Rebase current branch onto "' + branchName + '"?',
1625
+ confirmLabel: 'Rebase',
1626
+ onConfirm: () => gitAction('rebase', { branch: branchName }),
1627
+ })
1628
+ },
1629
+ { separator: true },
1630
+ { label: 'Rename branch...', action: () => showDialog({
1631
+ title: 'Rename Branch',
1632
+ input: { placeholder: 'New branch name', defaultValue: branchName },
1633
+ confirmLabel: 'Rename',
1634
+ onConfirm: (newName) => { if (newName && newName !== branchName) gitAction('renameBranch', { oldName: branchName, newName }); },
1635
+ })
1636
+ },
1637
+ );
1638
+ if (refType !== 'head') {
1639
+ items.push(
1640
+ { label: 'Delete branch...', destructive: true, action: () => showDialog({
1641
+ title: 'Delete Branch',
1642
+ message: 'Delete local branch "' + branchName + '"?',
1643
+ destructive: true,
1644
+ confirmLabel: 'Delete',
1645
+ onConfirm: () => gitAction('deleteBranch', { name: branchName, force: false }),
1646
+ })
1647
+ },
1648
+ );
1649
+ }
1650
+ if (state.settings.prCreation && state.settings.prCreation.urlTemplate) {
1651
+ items.push({ separator: true });
1652
+ items.push({ label: 'Create Pull Request', action: () => openPrUrl(branchName) });
1653
+ }
1654
+ } else if (refType === 'remote') {
1655
+ items.push(
1656
+ { label: 'Checkout as local branch', action: () => gitAction('checkout', { target: branchName }) },
1657
+ { separator: true },
1658
+ { label: 'Delete remote branch...', destructive: true, action: () => showDialog({
1659
+ title: 'Delete Remote Branch',
1660
+ message: 'Delete remote branch "' + branchName + '"? This cannot be undone.',
1661
+ destructive: true,
1662
+ confirmLabel: 'Delete',
1663
+ onConfirm: () => {
1664
+ const parts = branchName.split('/');
1665
+ const remote = parts[0];
1666
+ const branch = parts.slice(1).join('/');
1667
+ gitAction('push', { remote, branch, force: false, delete: true });
1668
+ },
1669
+ })
1670
+ },
1671
+ );
1672
+ } else if (refType === 'tag') {
1673
+ items.push(
1674
+ { label: 'Checkout tag "' + branchName + '"', action: () => gitAction('checkout', { target: branchName }) },
1675
+ { separator: true },
1676
+ { label: 'Delete tag...', destructive: true, action: () => showDialog({
1677
+ title: 'Delete Tag',
1678
+ message: 'Delete tag "' + branchName + '"?',
1679
+ destructive: true,
1680
+ confirmLabel: 'Delete',
1681
+ onConfirm: () => gitAction('deleteTag', { name: branchName }),
1682
+ })
1683
+ },
1684
+ );
1685
+ }
1686
+
1687
+ let html = '';
1688
+ items.forEach((item, idx) => {
1689
+ if (item.separator) {
1690
+ html += '<div class="ctx-separator"></div>';
1691
+ } else {
1692
+ html += '<div class="ctx-item' + (item.destructive ? ' destructive' : '') + '" data-idx="' + idx + '">' + escHtml(item.label) + '</div>';
1693
+ }
1694
+ });
1695
+ menu.innerHTML = html;
1696
+ menu.style.left = Math.min(x, window.innerWidth - 220) + 'px';
1697
+ menu.style.top = Math.min(y, window.innerHeight - 300) + 'px';
1698
+ menu.classList.remove('hidden');
1699
+ menu.querySelectorAll('.ctx-item').forEach(el => {
1700
+ const idx = parseInt(el.dataset.idx);
1701
+ const item = items[idx];
1702
+ if (item && item.action) el.addEventListener('click', () => { hideContextMenu(); item.action(); });
1703
+ });
1704
+ setTimeout(() => document.addEventListener('click', hideContextMenu, { once: true }), 0);
1705
+ }
1706
+
1707
+ function hideContextMenu() {
1708
+ document.getElementById('context-menu').classList.add('hidden');
1709
+ }
1710
+
1711
+ function gitAction(action, args) {
1712
+ vscode.postMessage({ command: 'gitAction', action, args });
1713
+ }
1714
+
1715
+ function promptAndAction(title, callback) {
1716
+ showDialog({ title, input: { placeholder: title }, onConfirm: (val) => { if (val) callback(val); } });
1717
+ }
1718
+
1719
+ function promptResetMode(hash) {
1720
+ showDialog({
1721
+ title: 'Reset Current Branch',
1722
+ select: { options: ['soft', 'mixed', 'hard'], defaultValue: 'mixed', label: 'Reset mode:' },
1723
+ onConfirm: (mode) => {
1724
+ if (mode === 'hard') {
1725
+ showDialog({
1726
+ title: 'Confirm Hard Reset',
1727
+ message: 'WARNING: --hard will discard ALL uncommitted changes. This cannot be undone!',
1728
+ destructive: true,
1729
+ confirmLabel: 'Reset Hard',
1730
+ onConfirm: () => gitAction('reset', { mode, hash }),
1731
+ });
1732
+ } else {
1733
+ gitAction('reset', { mode, hash });
1734
+ }
1735
+ },
1736
+ });
1737
+ }
1738
+
1739
+ function copyText(text) {
1740
+ navigator.clipboard.writeText(text).catch(() => {});
1741
+ }
1742
+
1743
+ // --- Dialog system ---
1744
+ function showDialog(opts) {
1745
+ const overlay = document.createElement('div');
1746
+ overlay.className = 'dialog-overlay';
1747
+ const dialog = document.createElement('div');
1748
+ dialog.className = 'dialog';
1749
+ dialog.innerHTML = '<h3>' + escHtml(opts.title || 'Dialog') + '</h3>';
1750
+ if (opts.message) {
1751
+ const msgHtml = opts.rawMessage ? opts.message : escHtml(opts.message);
1752
+ dialog.innerHTML += '<p' + (opts.destructive ? ' class="warning"' : '') + '>' + msgHtml + '</p>';
1753
+ }
1754
+
1755
+ let inputEl = null;
1756
+ if (opts.input) {
1757
+ inputEl = document.createElement('input');
1758
+ inputEl.type = 'text';
1759
+ inputEl.placeholder = opts.input.placeholder || '';
1760
+ if (opts.input.defaultValue) inputEl.value = opts.input.defaultValue;
1761
+ dialog.appendChild(inputEl);
1762
+ }
1763
+ if (opts.select) {
1764
+ if (opts.select.label) dialog.innerHTML += '<p>' + escHtml(opts.select.label) + '</p>';
1765
+ inputEl = document.createElement('select');
1766
+ opts.select.options.forEach(o => {
1767
+ const opt = document.createElement('option');
1768
+ opt.value = o; opt.textContent = o;
1769
+ if (o === opts.select.defaultValue) opt.selected = true;
1770
+ inputEl.appendChild(opt);
1771
+ });
1772
+ dialog.appendChild(inputEl);
1773
+ }
1774
+
1775
+ const actions = document.createElement('div');
1776
+ actions.className = 'dialog-actions';
1777
+ const cancelBtn = document.createElement('button');
1778
+ cancelBtn.textContent = opts.cancelLabel || 'Cancel';
1779
+ cancelBtn.className = 'secondary';
1780
+ cancelBtn.addEventListener('click', () => overlay.remove());
1781
+ const confirmBtn = document.createElement('button');
1782
+ confirmBtn.textContent = opts.confirmLabel || 'OK';
1783
+ confirmBtn.className = opts.destructive ? 'btn-danger' : 'btn-primary';
1784
+ confirmBtn.addEventListener('click', () => {
1785
+ overlay.remove();
1786
+ if (opts.onConfirm) opts.onConfirm(inputEl ? inputEl.value : undefined);
1787
+ });
1788
+ actions.appendChild(cancelBtn);
1789
+ actions.appendChild(confirmBtn);
1790
+ dialog.appendChild(actions);
1791
+ overlay.appendChild(dialog);
1792
+ document.body.appendChild(overlay);
1793
+
1794
+ // Focus input and handle Enter/Escape
1795
+ if (inputEl) setTimeout(() => inputEl.focus(), 50);
1796
+ overlay.addEventListener('keydown', (e) => {
1797
+ if (e.key === 'Escape') overlay.remove();
1798
+ if (e.key === 'Enter') confirmBtn.click();
1799
+ });
1800
+ }
1801
+
1802
+ // --- Mobile long-press ---
1803
+ function setupLongPress(el, callback) {
1804
+ let timer = null;
1805
+ let startX = 0, startY = 0;
1806
+ el.addEventListener('touchstart', (e) => {
1807
+ startX = e.touches[0].clientX;
1808
+ startY = e.touches[0].clientY;
1809
+ timer = setTimeout(() => { e.preventDefault(); callback(startX, startY); }, 500);
1810
+ }, { passive: false });
1811
+ el.addEventListener('touchmove', (e) => {
1812
+ if (timer && (Math.abs(e.touches[0].clientX - startX) > 10 || Math.abs(e.touches[0].clientY - startY) > 10)) {
1813
+ clearTimeout(timer); timer = null;
1814
+ }
1815
+ }, { passive: true });
1816
+ el.addEventListener('touchend', () => { if (timer) { clearTimeout(timer); timer = null; } });
1817
+ el.addEventListener('touchcancel', () => { if (timer) { clearTimeout(timer); timer = null; } });
1818
+ }
1819
+
1820
+ // --- Text formatter (URLs, issues, commit hashes) ---
1821
+ function formatCommitMessage(msg) {
1822
+ let safe = escHtml(msg);
1823
+ // Apply issue linking rules from settings
1824
+ const rules = state.settings.issueLinkingRules || [];
1825
+ for (const rule of rules) {
1826
+ if (!rule.pattern) continue;
1827
+ if (rule.pattern.length > 200) continue; // ReDoS guard
1828
+ try {
1829
+ const re = new RegExp(rule.pattern, 'g');
1830
+ if (rule.url) {
1831
+ safe = safe.replace(re, function(match) {
1832
+ let href = rule.url;
1833
+ for (let i = 1; i < arguments.length - 2; i++) {
1834
+ if (typeof arguments[i] === 'string') href = href.split('$' + i).join(arguments[i]);
1835
+ }
1836
+ return '<a class="commit-link" href="' + escHtml(href) + '" target="_blank" title="' + escHtml(href) + '">' + match + '</a>';
1837
+ });
1838
+ } else {
1839
+ safe = safe.replace(re, '<span class="commit-link" title="$&">$&</span>');
1840
+ }
1841
+ } catch (e) { /* invalid regex — skip */ }
1842
+ }
1843
+ // Short commit hashes
1844
+ safe = safe.replace(/\\b([0-9a-f]{7,40})\\b/g, '<span class="commit-link" title="$1">$1</span>');
1845
+ // URLs — skip if already inside an <a> tag
1846
+ safe = safe.replace(/(<a[^>]*>.*?<\\/a>)|(https?:\\/\\/[^\\s<]+)/g, (m, linked, url) => {
1847
+ if (linked) return linked;
1848
+ return '<a class="commit-link" href="' + url + '" target="_blank">' + url + '</a>';
1849
+ });
1850
+ return safe;
1851
+ }
1852
+
1853
+ // --- Find widget ---
1854
+ const findBar = document.getElementById('find-bar');
1855
+ const findInput = document.getElementById('find-input');
1856
+
1857
+ document.getElementById('btn-find').addEventListener('click', toggleFind);
1858
+
1859
+ function toggleFind() {
1860
+ findBar.classList.toggle('hidden');
1861
+ if (!findBar.classList.contains('hidden')) findInput.focus();
1862
+ else clearSearch();
1863
+ }
1864
+
1865
+ findInput.addEventListener('input', () => doSearch(findInput.value));
1866
+ document.getElementById('find-next').addEventListener('click', () => navigateSearch(1));
1867
+ document.getElementById('find-prev').addEventListener('click', () => navigateSearch(-1));
1868
+ document.getElementById('find-close').addEventListener('click', () => { findBar.classList.add('hidden'); clearSearch(); });
1869
+
1870
+ function doSearch(query) {
1871
+ clearSearchHighlights();
1872
+ state.searchMatches = [];
1873
+ state.searchIndex = -1;
1874
+ if (!query.trim()) { document.getElementById('find-count').textContent = ''; return; }
1875
+ const q = query.toLowerCase();
1876
+ const displayCommits = getDisplayCommits();
1877
+ document.querySelectorAll('.commit-row:not(.header-row)').forEach((row, idx) => {
1878
+ const commit = displayCommits[idx];
1879
+ if (!commit || commit.hash === 'uncommitted') return;
1880
+ const match = commit.message.toLowerCase().includes(q) ||
1881
+ commit.author.toLowerCase().includes(q) ||
1882
+ commit.hash.toLowerCase().startsWith(q);
1883
+ if (match) { state.searchMatches.push(idx); row.classList.add('search-match'); }
1884
+ });
1885
+ document.getElementById('find-count').textContent = state.searchMatches.length + ' match(es)';
1886
+ if (state.searchMatches.length > 0) navigateSearch(0);
1887
+ }
1888
+
1889
+ function navigateSearch(dir) {
1890
+ if (state.searchMatches.length === 0) return;
1891
+ if (dir === 0) state.searchIndex = 0;
1892
+ else state.searchIndex = (state.searchIndex + dir + state.searchMatches.length) % state.searchMatches.length;
1893
+ const idx = state.searchMatches[state.searchIndex];
1894
+ const rows = document.querySelectorAll('.commit-row:not(.header-row)');
1895
+ if (rows[idx]) rows[idx].scrollIntoView({ block: 'center' });
1896
+ document.getElementById('find-count').textContent = (state.searchIndex + 1) + ' of ' + state.searchMatches.length;
1897
+ }
1898
+
1899
+ function clearSearch() {
1900
+ clearSearchHighlights();
1901
+ state.searchMatches = [];
1902
+ state.searchIndex = -1;
1903
+ findInput.value = '';
1904
+ document.getElementById('find-count').textContent = '';
1905
+ }
1906
+
1907
+ function clearSearchHighlights() {
1908
+ document.querySelectorAll('.search-match').forEach(el => el.classList.remove('search-match'));
1909
+ }
1910
+
1911
+ // --- Keyboard shortcuts ---
1912
+ document.addEventListener('keydown', (e) => {
1913
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f') { e.preventDefault(); toggleFind(); }
1914
+ if (e.key === 'Escape') {
1915
+ hideContextMenu();
1916
+ if (!findBar.classList.contains('hidden')) { findBar.classList.add('hidden'); clearSearch(); }
1917
+ else if (state.expandedCommit) { state.selectedCommit = null; state.expandedCommit = null; document.getElementById('detail-panel').classList.add('hidden'); document.querySelectorAll('.commit-row.selected').forEach(el => el.classList.remove('selected')); }
1918
+ }
1919
+ });
1920
+
1921
+ // --- Scroll to load more ---
1922
+ document.getElementById('graph-container').addEventListener('scroll', (e) => {
1923
+ const container = e.target;
1924
+ if (container.scrollTop + container.clientHeight >= container.scrollHeight - 50) {
1925
+ if (!state.loading && state.commits.length >= state.maxCommits) {
1926
+ state.loading = true;
1927
+ document.getElementById('loading').classList.remove('hidden');
1928
+ vscode.postMessage({ command: 'requestCommits', maxCommits: state.maxCommits, skip: state.commits.length });
1929
+ }
1930
+ }
1931
+ });
1932
+
1933
+ // --- Utilities ---
1934
+ function escHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;'); }
1935
+
1936
+ function formatDate(ts) {
1937
+ const fmt = state.settings.dateFormat;
1938
+ if (fmt === 'iso') return new Date(ts * 1000).toISOString().substring(0, 16).replace('T', ' ');
1939
+ if (fmt === 'absolute') return new Date(ts * 1000).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
1940
+ const now = Date.now() / 1000;
1941
+ const diff = now - ts;
1942
+ if (diff < 60) return 'just now';
1943
+ if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
1944
+ if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
1945
+ if (diff < 2592000) return Math.floor(diff / 86400) + 'd ago';
1946
+ if (diff < 31536000) return Math.floor(diff / 2592000) + 'mo ago';
1947
+ return Math.floor(diff / 31536000) + 'y ago';
1948
+ }
1949
+
1950
+ function updateStatus() {
1951
+ const parts = [];
1952
+ if (state.currentBranch) parts.push(state.currentBranch);
1953
+ parts.push(state.commits.length + ' commits');
1954
+ parts.push(state.branches.length + ' branches');
1955
+ parts.push(state.tags.length + ' tags');
1956
+ document.getElementById('status-text').textContent = parts.join(' | ');
1957
+ }
1958
+
1959
+ // --- Settings panel ---
1960
+ const settingsPanel = document.getElementById('settings-panel');
1961
+
1962
+ document.getElementById('btn-settings').addEventListener('click', () => {
1963
+ const isOpen = settingsPanel.classList.toggle('open');
1964
+ if (isOpen) {
1965
+ vscode.postMessage({ command: 'requestSettings' });
1966
+ vscode.postMessage({ command: 'requestUserDetails' });
1967
+ renderRemotesList();
1968
+ }
1969
+ });
1970
+ document.getElementById('settings-close').addEventListener('click', () => {
1971
+ settingsPanel.classList.remove('open');
1972
+ });
1973
+
1974
+ function applySettingsToUI() {
1975
+ const s = state.settings;
1976
+ document.getElementById('s-maxCommits').value = s.maxCommits;
1977
+ document.getElementById('s-showTags').checked = s.showTags;
1978
+ document.getElementById('s-showStashes').checked = s.showStashes;
1979
+ document.getElementById('s-showRemoteBranches').checked = s.showRemoteBranches;
1980
+ document.getElementById('s-graphStyle').value = s.graphStyle;
1981
+ document.getElementById('s-firstParentOnly').checked = s.firstParentOnly;
1982
+ document.getElementById('s-dateFormat').value = s.dateFormat;
1983
+ document.getElementById('s-commitOrdering').value = s.commitOrdering;
1984
+ document.getElementById('s-autoFetchInterval').value = s.autoFetchInterval || 0;
1985
+ graphConfig.style = s.graphStyle;
1986
+ startAutoFetch(s.autoFetchInterval);
1987
+ renderIssueRules();
1988
+ applyPrSettingsToUI();
1989
+ }
1990
+
1991
+ // General setting change handlers
1992
+ ['showTags', 'showStashes', 'showRemoteBranches', 'firstParentOnly'].forEach(key => {
1993
+ document.getElementById('s-' + key).addEventListener('change', (e) => {
1994
+ vscode.postMessage({ command: 'updateSetting', key, value: e.target.checked });
1995
+ state.settings[key] = e.target.checked;
1996
+ renderCommitList();
1997
+ });
1998
+ });
1999
+ ['graphStyle', 'dateFormat', 'commitOrdering'].forEach(key => {
2000
+ document.getElementById('s-' + key).addEventListener('change', (e) => {
2001
+ vscode.postMessage({ command: 'updateSetting', key, value: e.target.value });
2002
+ state.settings[key] = e.target.value;
2003
+ if (key === 'graphStyle') graphConfig.style = e.target.value;
2004
+ if (key === 'dateFormat') renderCommitList();
2005
+ });
2006
+ });
2007
+ document.getElementById('s-maxCommits').addEventListener('change', (e) => {
2008
+ const n = parseInt(e.target.value, 10);
2009
+ if (n > 0 && n <= 10000) {
2010
+ state.maxCommits = n;
2011
+ state.settings.maxCommits = n;
2012
+ vscode.postMessage({ command: 'updateSetting', key: 'maxCommits', value: n });
2013
+ }
2014
+ });
2015
+ document.getElementById('s-autoFetchInterval').addEventListener('change', (e) => {
2016
+ const val = parseInt(e.target.value, 10);
2017
+ state.settings.autoFetchInterval = val;
2018
+ vscode.postMessage({ command: 'updateSetting', key: 'autoFetchInterval', value: val });
2019
+ startAutoFetch(val);
2020
+ });
2021
+
2022
+ // User details
2023
+ document.getElementById('s-saveUser').addEventListener('click', () => {
2024
+ const name = document.getElementById('s-userName').value.trim();
2025
+ const email = document.getElementById('s-userEmail').value.trim();
2026
+ vscode.postMessage({ command: 'updateUserDetails', name, email });
2027
+ });
2028
+
2029
+ // Remotes
2030
+ function renderRemotesList() {
2031
+ const container = document.getElementById('s-remotes-list');
2032
+ if (state.remotes.length === 0) {
2033
+ container.innerHTML = '<p style="font-size:12px;color:var(--subtext)">No remotes configured.</p>';
2034
+ return;
2035
+ }
2036
+ container.innerHTML = state.remotes.map(r =>
2037
+ '<div class="remote-item">' +
2038
+ '<div class="remote-name">' + escHtml(r.name) + '</div>' +
2039
+ '<div class="remote-url">' + escHtml(r.fetchUrl) + '</div>' +
2040
+ '<div class="remote-actions">' +
2041
+ '<button class="btn-sm" data-edit-remote="' + escHtml(r.name) + '">Edit URL</button>' +
2042
+ '<button class="btn-sm" style="color:var(--red)" data-rm-remote="' + escHtml(r.name) + '">Remove</button>' +
2043
+ '</div>' +
2044
+ '</div>'
2045
+ ).join('');
2046
+ }
2047
+
2048
+ document.getElementById('s-remotes-list').addEventListener('click', (e) => {
2049
+ const editBtn = e.target.closest('[data-edit-remote]');
2050
+ if (editBtn) {
2051
+ const name = editBtn.dataset.editRemote;
2052
+ const remote = state.remotes.find(r => r.name === name);
2053
+ showDialog({
2054
+ title: 'Edit Remote URL: ' + name,
2055
+ input: { placeholder: 'New URL', defaultValue: remote ? remote.fetchUrl : '' },
2056
+ onConfirm: (url) => { if (url) vscode.postMessage({ command: 'editRemoteUrl', name, url }); },
2057
+ });
2058
+ return;
2059
+ }
2060
+ const rmBtn = e.target.closest('[data-rm-remote]');
2061
+ if (rmBtn) {
2062
+ const name = rmBtn.dataset.rmRemote;
2063
+ showDialog({
2064
+ title: 'Remove Remote',
2065
+ message: 'Remove remote "' + name + '"? This cannot be undone.',
2066
+ destructive: true,
2067
+ confirmLabel: 'Remove',
2068
+ onConfirm: () => vscode.postMessage({ command: 'removeRemote', name }),
2069
+ });
2070
+ }
2071
+ });
2072
+
2073
+ document.getElementById('s-addRemote').addEventListener('click', () => {
2074
+ const name = document.getElementById('s-newRemoteName').value.trim();
2075
+ const url = document.getElementById('s-newRemoteUrl').value.trim();
2076
+ if (name && url) {
2077
+ vscode.postMessage({ command: 'addRemote', name, url });
2078
+ document.getElementById('s-newRemoteName').value = '';
2079
+ document.getElementById('s-newRemoteUrl').value = '';
2080
+ }
2081
+ });
2082
+
2083
+ // --- Issue Linking ---
2084
+ function renderIssueRules() {
2085
+ const rules = state.settings.issueLinkingRules || [];
2086
+ const container = document.getElementById('issue-rules-list');
2087
+ container.innerHTML = rules.map((r, i) =>
2088
+ '<div class="issue-rule-row" data-idx="' + i + '">' +
2089
+ '<input type="text" class="rule-pattern" placeholder="Regex, e.g. #(\\d+)" value="' + escHtml(r.pattern) + '">' +
2090
+ '<input type="text" class="rule-url" placeholder="URL with $1, e.g. https://..." value="' + escHtml(r.url) + '">' +
2091
+ '<button class="rule-remove" title="Remove">&times;</button>' +
2092
+ '</div>'
2093
+ ).join('');
2094
+ }
2095
+
2096
+ let issueRuleDebounce = null;
2097
+ document.getElementById('issue-rules-list').addEventListener('input', (e) => {
2098
+ const row = e.target.closest('.issue-rule-row');
2099
+ if (!row) return;
2100
+ const idx = parseInt(row.dataset.idx);
2101
+ const rules = [...(state.settings.issueLinkingRules || [])];
2102
+ if (!rules[idx]) return;
2103
+ if (e.target.classList.contains('rule-pattern')) {
2104
+ rules[idx] = { ...rules[idx], pattern: e.target.value };
2105
+ try { new RegExp(e.target.value); e.target.classList.remove('rule-error'); }
2106
+ catch { e.target.classList.add('rule-error'); return; }
2107
+ }
2108
+ if (e.target.classList.contains('rule-url')) {
2109
+ rules[idx] = { ...rules[idx], url: e.target.value };
2110
+ }
2111
+ state.settings.issueLinkingRules = rules;
2112
+ clearTimeout(issueRuleDebounce);
2113
+ issueRuleDebounce = setTimeout(() => {
2114
+ vscode.postMessage({ command: 'updateSetting', key: 'issueLinkingRules', value: rules });
2115
+ }, 500);
2116
+ });
2117
+
2118
+ document.getElementById('issue-rules-list').addEventListener('click', (e) => {
2119
+ if (!e.target.closest('.rule-remove')) return;
2120
+ const row = e.target.closest('.issue-rule-row');
2121
+ const idx = parseInt(row.dataset.idx);
2122
+ const rules = [...(state.settings.issueLinkingRules || [])];
2123
+ rules.splice(idx, 1);
2124
+ state.settings.issueLinkingRules = rules;
2125
+ vscode.postMessage({ command: 'updateSetting', key: 'issueLinkingRules', value: rules });
2126
+ renderIssueRules();
2127
+ });
2128
+
2129
+ document.getElementById('add-issue-rule').addEventListener('click', () => {
2130
+ const rules = [...(state.settings.issueLinkingRules || []), { pattern: '', url: '' }];
2131
+ state.settings.issueLinkingRules = rules;
2132
+ vscode.postMessage({ command: 'updateSetting', key: 'issueLinkingRules', value: rules });
2133
+ renderIssueRules();
2134
+ });
2135
+
2136
+ // --- PR Creation ---
2137
+ const PR_TEMPLATES = {
2138
+ github: 'https://github.com/\${owner}/\${repo}/compare/\${targetBranch}...\${sourceBranch}?expand=1',
2139
+ gitlab: 'https://gitlab.com/\${owner}/\${repo}/-/merge_requests/new?source_branch=\${sourceBranch}&target_branch=\${targetBranch}',
2140
+ bitbucket: 'https://bitbucket.org/\${owner}/\${repo}/pull-requests/new?source=\${sourceBranch}&dest=\${targetBranch}',
2141
+ custom: '',
2142
+ };
2143
+
2144
+ document.getElementById('pr-provider').addEventListener('change', (e) => {
2145
+ const provider = e.target.value;
2146
+ const prConfig = document.getElementById('pr-config');
2147
+ if (!provider) {
2148
+ prConfig.classList.add('hidden');
2149
+ state.settings.prCreation = null;
2150
+ vscode.postMessage({ command: 'updateSetting', key: 'prCreation', value: null });
2151
+ return;
2152
+ }
2153
+ prConfig.classList.remove('hidden');
2154
+ document.getElementById('pr-url-template').value = PR_TEMPLATES[provider] || '';
2155
+ document.getElementById('pr-target').value = 'main';
2156
+ vscode.postMessage({ command: 'requestOwnerRepo' });
2157
+ });
2158
+
2159
+ document.getElementById('pr-save').addEventListener('click', () => {
2160
+ const provider = document.getElementById('pr-provider').value;
2161
+ if (!provider) return;
2162
+ const config = {
2163
+ provider,
2164
+ urlTemplate: document.getElementById('pr-url-template').value.trim(),
2165
+ owner: document.getElementById('pr-owner').value.trim(),
2166
+ repo: document.getElementById('pr-repo').value.trim(),
2167
+ defaultTargetBranch: document.getElementById('pr-target').value.trim() || 'main',
2168
+ };
2169
+ state.settings.prCreation = config;
2170
+ vscode.postMessage({ command: 'updateSetting', key: 'prCreation', value: config });
2171
+ });
2172
+
2173
+ function applyPrSettingsToUI() {
2174
+ const pr = state.settings.prCreation;
2175
+ if (!pr) {
2176
+ document.getElementById('pr-provider').value = '';
2177
+ document.getElementById('pr-config').classList.add('hidden');
2178
+ return;
2179
+ }
2180
+ document.getElementById('pr-provider').value = pr.provider;
2181
+ document.getElementById('pr-config').classList.remove('hidden');
2182
+ document.getElementById('pr-owner').value = pr.owner || '';
2183
+ document.getElementById('pr-repo').value = pr.repo || '';
2184
+ document.getElementById('pr-target').value = pr.defaultTargetBranch || 'main';
2185
+ document.getElementById('pr-url-template').value = pr.urlTemplate || '';
2186
+ }
2187
+
2188
+ function openPrUrl(sourceBranch) {
2189
+ const pr = state.settings.prCreation;
2190
+ if (!pr || !pr.urlTemplate) return;
2191
+ const url = pr.urlTemplate
2192
+ .replace(/\\$\\{owner\\}/g, encodeURIComponent(pr.owner))
2193
+ .replace(/\\$\\{repo\\}/g, encodeURIComponent(pr.repo))
2194
+ .replace(/\\$\\{sourceBranch\\}/g, encodeURIComponent(sourceBranch))
2195
+ .replace(/\\$\\{targetBranch\\}/g, encodeURIComponent(pr.defaultTargetBranch || 'main'));
2196
+ window.open(url, '_blank');
2197
+ }
2198
+ `;
2199
+ }