@hienlh/ppm 0.9.85 → 0.9.87

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 (243) hide show
  1. package/260415-0932-git-graph-stash-rebase-conflicts/reports/code-reviewer-260415-1020-stash-rebase-conflicts.md +288 -0
  2. package/260415-0932-git-graph-stash-rebase-conflicts/reports/tester-260415-1020-build-check.md +117 -0
  3. package/260415-1150-ext-silent-failure-debugging/reports/code-reviewer-260415-1159-ext-error-reporting-review.md +205 -0
  4. package/260415-1150-ext-silent-failure-debugging/reports/docs-manager-260415-1206-ext-error-reporting.md +99 -0
  5. package/260415-1150-ext-silent-failure-debugging/reports/tester-260415-1159-extension-error-reporting.md +174 -0
  6. package/CHANGELOG.md +24 -0
  7. package/dist/web/assets/{_basePickBy-D-bUmjma.js → _basePickBy-Bj0dI1ei.js} +1 -1
  8. package/dist/web/assets/{_baseUniq-BnXXIfRB.js → _baseUniq-CyzdZeQH.js} +1 -1
  9. package/dist/web/assets/ai-settings-section-Bo9lCaTd.js +1 -0
  10. package/dist/web/assets/{api-settings-Qi2xRiHa.js → api-settings-CUxg9RE5.js} +1 -1
  11. package/dist/web/assets/{arc-DB9vXGzd.js → arc-CxgHJ7Z4.js} +1 -1
  12. package/dist/web/assets/architecture-PBZL5I3N-DDFO_NKq.js +1 -0
  13. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-BBV25747.js → architectureDiagram-2XIMDMQ5-D16OotsC.js} +1 -1
  14. package/dist/web/assets/arrow-up-I9-21gkR.js +1 -0
  15. package/dist/web/assets/{blockDiagram-WCTKOSBZ-BOTnY2Lq.js → blockDiagram-WCTKOSBZ-Ct57Wtfk.js} +1 -1
  16. package/dist/web/assets/{c4Diagram-IC4MRINW-D7QAUdHD.js → c4Diagram-IC4MRINW-BIymcNsg.js} +1 -1
  17. package/dist/web/assets/channel-wumTB1if.js +1 -0
  18. package/dist/web/assets/chat-tab-R4gKsnxD.js +10 -0
  19. package/dist/web/assets/chevron-right-DY_wImxB.js +1 -0
  20. package/dist/web/assets/{chunk-4BX2VUAB-BnOVw77D.js → chunk-4BX2VUAB-CENmY7Kw.js} +1 -1
  21. package/dist/web/assets/{chunk-55IACEB6-BftA8DxR.js → chunk-55IACEB6-DhZGI1l3.js} +1 -1
  22. package/dist/web/assets/{chunk-7E7YKBS2-B0vnP8v3.js → chunk-7E7YKBS2-DZcnC7Ow.js} +1 -1
  23. package/dist/web/assets/{chunk-7R4GIKGN-Czlaj26D.js → chunk-7R4GIKGN-y8bfHEy-.js} +2 -2
  24. package/dist/web/assets/{chunk-C72U2L5F-DpEbDtMo.js → chunk-C72U2L5F-BHPkfQj2.js} +1 -1
  25. package/dist/web/assets/{chunk-EGIJ26TM-BWXe6lkx.js → chunk-EGIJ26TM-nant2LXl.js} +1 -1
  26. package/dist/web/assets/{chunk-FMBD7UC4-DspqhPfk.js → chunk-FMBD7UC4-Bog4cpN-.js} +1 -1
  27. package/dist/web/assets/{chunk-GEFDOKGD-D6HHRbYk.js → chunk-GEFDOKGD-86LFbsAC.js} +1 -1
  28. package/dist/web/assets/chunk-GLR3WWYH-Re-5eSlQ.js +2 -0
  29. package/dist/web/assets/chunk-HHEYEP7N-C45i5G_3.js +1 -0
  30. package/dist/web/assets/{chunk-JSJVCQXG-BC8wnMwf.js → chunk-JSJVCQXG-23eG9mgt.js} +1 -1
  31. package/dist/web/assets/{chunk-KX2RTZJC-D3VDtyvX.js → chunk-KX2RTZJC-CHj8TnTB.js} +1 -1
  32. package/dist/web/assets/{chunk-KYZI473N-Z-NBw_HS.js → chunk-KYZI473N-gqRLpJ4w.js} +1 -1
  33. package/dist/web/assets/{chunk-L3YUKLVL--RGkEh__.js → chunk-L3YUKLVL-DnSMmNFC.js} +1 -1
  34. package/dist/web/assets/{chunk-MX3YWQON-2B76t_Kx.js → chunk-MX3YWQON-B6g1ZH9X.js} +1 -1
  35. package/dist/web/assets/{chunk-NQ4KR5QH-BekY3tEi.js → chunk-NQ4KR5QH-DX32345Y.js} +1 -1
  36. package/dist/web/assets/{chunk-O4XLMI2P-2CJLfx_1.js → chunk-O4XLMI2P-Vp_V4P-b.js} +1 -1
  37. package/dist/web/assets/{chunk-OZEHJAEY-sug_L09P.js → chunk-OZEHJAEY-lKq2SWjA.js} +1 -1
  38. package/dist/web/assets/{chunk-PQ6SQG4A-_fwPRLQy.js → chunk-PQ6SQG4A-Bik13fTV.js} +1 -1
  39. package/dist/web/assets/{chunk-PU5JKC2W-BUaTFJVQ.js → chunk-PU5JKC2W-DD95Rx35.js} +1 -1
  40. package/dist/web/assets/chunk-QZHKN3VN-N3VXx1VH.js +1 -0
  41. package/dist/web/assets/{chunk-R5LLSJPH-C37xW0vj.js → chunk-R5LLSJPH-dRhXRnrb.js} +1 -1
  42. package/dist/web/assets/{chunk-WL4C6EOR-CCkt_MT6.js → chunk-WL4C6EOR-B1iIvLOG.js} +1 -1
  43. package/dist/web/assets/{chunk-XIRO2GV7-Dz2LBq7Y.js → chunk-XIRO2GV7-DZBoNl1_.js} +1 -1
  44. package/dist/web/assets/{chunk-XPW4576I-DenTbBuj.js → chunk-XPW4576I-CgLyyW03.js} +1 -1
  45. package/dist/web/assets/{chunk-XZSTWKYB-Dbp1nUSQ.js → chunk-XZSTWKYB-DjV8xl5A.js} +1 -1
  46. package/dist/web/assets/{chunk-YBOYWFTD-3OTKowjE.js → chunk-YBOYWFTD-D_ILLe6_.js} +1 -1
  47. package/dist/web/assets/classDiagram-VBA2DB6C-mr-Cb1me.js +1 -0
  48. package/dist/web/assets/classDiagram-v2-RAHNMMFH-BKe8_uda.js +1 -0
  49. package/dist/web/assets/clone--z5KLAuR.js +1 -0
  50. package/dist/web/assets/code-editor-Br0vzTOy.js +8 -0
  51. package/dist/web/assets/columns-2-IeETSfON.js +1 -0
  52. package/dist/web/assets/conflict-editor-BPgCjnNz.js +19 -0
  53. package/dist/web/assets/{cose-bilkent-S5V4N54A-MbmGZnt0.js → cose-bilkent-S5V4N54A-BGNPFv3x.js} +1 -1
  54. package/dist/web/assets/{csv-preview-uZ_7b8I7.js → csv-preview-BZRICDP0.js} +2 -2
  55. package/dist/web/assets/{dagre-CPhI6v-K.js → dagre-CkhlMHnx.js} +1 -1
  56. package/dist/web/assets/{dagre-KLK3FWXG-CmSE-oNj.js → dagre-KLK3FWXG-Cnp996VG.js} +1 -1
  57. package/dist/web/assets/database-CgTomMxt.js +1 -0
  58. package/dist/web/assets/{database-viewer-5xljX0JI.js → database-viewer-DaUoQ-oR.js} +2 -2
  59. package/dist/web/assets/{diagram-E7M64L7V-B5XG3ZT7.js → diagram-E7M64L7V-BZF0tSOr.js} +1 -1
  60. package/dist/web/assets/{diagram-IFDJBPK2-BsP248aX.js → diagram-IFDJBPK2-nUcO8sN8.js} +1 -1
  61. package/dist/web/assets/{diagram-P4PSJMXO-Cna3408N.js → diagram-P4PSJMXO-CW0eCkwC.js} +1 -1
  62. package/dist/web/assets/diff-viewer-BzvK3gAE.js +4 -0
  63. package/dist/web/assets/dist-CM0oD8tQ.js +1 -0
  64. package/dist/web/assets/{erDiagram-INFDFZHY-B7SgktiR.js → erDiagram-INFDFZHY-DSkriYZ9.js} +1 -1
  65. package/dist/web/assets/extension-webview-CGepEw-b.js +3 -0
  66. package/dist/web/assets/{flowDiagram-PKNHOUZH-FOYZZ1OB.js → flowDiagram-PKNHOUZH-CFYAfZBx.js} +1 -1
  67. package/dist/web/assets/{ganttDiagram-A5KZAMGK-CnHVYh9v.js → ganttDiagram-A5KZAMGK-KSn4XAU4.js} +1 -1
  68. package/dist/web/assets/gitGraph-HDMCJU4V-OkvBPi6H.js +1 -0
  69. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-0G9XxZay.js → gitGraphDiagram-K3NZZRJ6-BMgjjVys.js} +1 -1
  70. package/dist/web/assets/{graphlib-CNiBwlg_.js → graphlib-BWe1iK_s.js} +1 -1
  71. package/dist/web/assets/index-CKsEzQ4f.js +26 -0
  72. package/dist/web/assets/index-Chf0otez.css +2 -0
  73. package/dist/web/assets/info-3K5VOQVL-BDU2_bYD.js +1 -0
  74. package/dist/web/assets/infoDiagram-LFFYTUFH-Diq4Cyc3.js +2 -0
  75. package/dist/web/assets/input-BHj0veau.js +45 -0
  76. package/dist/web/assets/{isEmpty-CcCb5n2-.js → isEmpty-BfLnxq-B.js} +1 -1
  77. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-D4QCzh5J.js → ishikawaDiagram-PHBUUO56-CiVEvp8o.js} +1 -1
  78. package/dist/web/assets/{journeyDiagram-4ABVD52K-CnHYNfKW.js → journeyDiagram-4ABVD52K-CG_v5Aho.js} +1 -1
  79. package/dist/web/assets/jsx-runtime-BRW_vwa9.js +1 -0
  80. package/dist/web/assets/{kanban-definition-K7BYSVSG-Bh_g3EVu.js → kanban-definition-K7BYSVSG-miB0-_Zq.js} +1 -1
  81. package/dist/web/assets/keybindings-store-D5zgHod8.js +1 -0
  82. package/dist/web/assets/{line-6d3eBADm.js → line-CSuSrJ9J.js} +1 -1
  83. package/dist/web/assets/{linear-cA_2lQy7.js → linear-DFN_MPsw.js} +1 -1
  84. package/dist/web/assets/{markdown-renderer-CZ07F7T6.js → markdown-renderer-DSYnGywb.js} +6 -6
  85. package/dist/web/assets/{mermaid-parser.core-C3kd7JXM.js → mermaid-parser.core-CFdP1Z5_.js} +2 -2
  86. package/dist/web/assets/{mindmap-definition-YRQLILUH-CYiUwhr_.js → mindmap-definition-YRQLILUH-pYPWwASE.js} +1 -1
  87. package/dist/web/assets/{ordinal-XHK5vIzZ.js → ordinal-DpFn432U.js} +1 -1
  88. package/dist/web/assets/packet-RMMSAZCW-BwpIpYB3.js +1 -0
  89. package/dist/web/assets/pie-UPGHQEXC-BPgAfmes.js +1 -0
  90. package/dist/web/assets/{pieDiagram-SKSYHLDU-D0S7jeZA.js → pieDiagram-SKSYHLDU-Dovdlvhu.js} +1 -1
  91. package/dist/web/assets/plus-DQGIb4mQ.js +1 -0
  92. package/dist/web/assets/port-forwarding-tab-vmqDKmk2.js +1 -0
  93. package/dist/web/assets/{postgres-viewer-RldlAO_m.js → postgres-viewer-0lIAosrr.js} +3 -3
  94. package/dist/web/assets/{quadrantDiagram-337W2JSQ-0hNP63hW.js → quadrantDiagram-337W2JSQ-TXe6cU_F.js} +1 -1
  95. package/dist/web/assets/radar-KQ55EAFF-TqxBkWx-.js +1 -0
  96. package/dist/web/assets/refresh-cw-Clk8fdUD.js +1 -0
  97. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-BVnmqFbL.js → requirementDiagram-Z7DCOOCP-CuiiuGS9.js} +1 -1
  98. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-DVkYdCJb.js → sankeyDiagram-WA2Y5GQK-BbRmhv0t.js} +1 -1
  99. package/dist/web/assets/scroll-area-BpXCNme3.js +1 -0
  100. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-B80s7sOg.js → sequenceDiagram-2WXFIKYE-B2D8IQDb.js} +1 -1
  101. package/dist/web/assets/settings-tab-CMnv1fce.js +1 -0
  102. package/dist/web/assets/{sql-query-editor-CjZ7Z6XL.js → sql-query-editor-Bc2hAwqT.js} +1 -1
  103. package/dist/web/assets/sqlite-viewer-B60MS2Dy.js +1 -0
  104. package/dist/web/assets/square-vBdqj0bF.js +1 -0
  105. package/dist/web/assets/{stateDiagram-RAJIS63D-BPLXgXRR.js → stateDiagram-RAJIS63D-ylr4HxPu.js} +1 -1
  106. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-D6zvxf3M.js +1 -0
  107. package/dist/web/assets/table-Bi27fEaN.js +1 -0
  108. package/dist/web/assets/{terminal-tab-DjzD8GLn.js → terminal-tab-CCJoLstH.js} +2 -2
  109. package/dist/web/assets/text-wrap-D_OmSzhp.js +1 -0
  110. package/dist/web/assets/{timeline-definition-YZTLITO2-fa_51u1X.js → timeline-definition-YZTLITO2-pMv1grvM.js} +1 -1
  111. package/dist/web/assets/trash-2-CNuB-htI.js +1 -0
  112. package/dist/web/assets/treemap-KZPCXAKY-Kck06FKU.js +1 -0
  113. package/dist/web/assets/{use-monaco-theme-D9XFxQuU.js → use-monaco-theme-BJK48EmK.js} +1 -1
  114. package/dist/web/assets/{vennDiagram-LZ73GAT5-kX4jJn6W.js → vennDiagram-LZ73GAT5-C-rkIUbo.js} +1 -1
  115. package/dist/web/assets/x-Dw3TjeY_.js +1 -0
  116. package/dist/web/assets/{xychartDiagram-JWTSCODW-Bzm5lZBs.js → xychartDiagram-JWTSCODW-CtpjAakO.js} +1 -1
  117. package/dist/web/index.html +18 -22
  118. package/dist/web/sw.js +1 -1
  119. package/docs/codebase-summary.md +169 -13
  120. package/docs/extension-development-guide.md +98 -1
  121. package/docs/journals/260414-1400-ext-git-graph-port-complete.md +147 -0
  122. package/docs/journals/260414-1452-git-graph-faithful-port.md +144 -0
  123. package/docs/journals/260414-1810-git-graph-ui-improvements-complete.md +261 -0
  124. package/docs/journals/260414-2001-bundled-extensions.md +219 -0
  125. package/docs/project-changelog.md +123 -21
  126. package/docs/project-roadmap.md +4 -2
  127. package/docs/system-architecture.md +77 -6
  128. package/package.json +1 -1
  129. package/packages/ext-git-graph/package.json +30 -0
  130. package/packages/ext-git-graph/src/extension-integration.test.ts +230 -0
  131. package/packages/ext-git-graph/src/extension-parsers.test.ts +193 -0
  132. package/packages/ext-git-graph/src/extension.ts +921 -0
  133. package/packages/ext-git-graph/src/git-log-parser.test.ts +271 -0
  134. package/packages/ext-git-graph/src/git-log-parser.ts +38 -0
  135. package/packages/ext-git-graph/src/types.ts +192 -0
  136. package/packages/ext-git-graph/src/webview-html.test.ts +142 -0
  137. package/packages/ext-git-graph/src/webview-html.ts +2417 -0
  138. package/packages/vscode-compat/src/index.ts +4 -0
  139. package/packages/vscode-compat/src/process.ts +25 -0
  140. package/packages/vscode-compat/src/window.ts +10 -0
  141. package/src/cli/commands/ext-cmd.ts +3 -1
  142. package/src/server/ws/extensions.ts +34 -4
  143. package/src/services/contribution-registry.ts +14 -1
  144. package/src/services/extension-host-worker.ts +12 -3
  145. package/src/services/extension-manifest.ts +18 -1
  146. package/src/services/extension-rpc-handlers.ts +68 -2
  147. package/src/services/extension.service.ts +63 -9
  148. package/src/types/extension-messages.ts +3 -1
  149. package/src/types/extension.ts +8 -0
  150. package/src/web/components/editor/code-editor.tsx +16 -4
  151. package/src/web/components/editor/conflict-editor.tsx +368 -0
  152. package/src/web/components/extensions/extension-webview.tsx +153 -12
  153. package/src/web/components/layout/command-palette.tsx +41 -17
  154. package/src/web/components/layout/editor-panel.tsx +16 -4
  155. package/src/web/components/layout/mobile-nav.tsx +6 -5
  156. package/src/web/components/layout/tab-bar.tsx +3 -3
  157. package/src/web/components/layout/tab-content.tsx +17 -5
  158. package/src/web/components/settings/keyboard-shortcuts-section.tsx +46 -1
  159. package/src/web/hooks/use-extension-ws.ts +30 -4
  160. package/src/web/hooks/use-global-keybindings.ts +24 -2
  161. package/src/web/hooks/use-url-sync.ts +8 -3
  162. package/src/web/stores/extension-store.ts +8 -0
  163. package/src/web/stores/keybindings-store.ts +2 -3
  164. package/src/web/stores/panel-store.ts +2 -2
  165. package/src/web/stores/panel-utils.ts +6 -2
  166. package/src/web/stores/tab-store.ts +3 -2
  167. package/dist/web/assets/ai-settings-section-D6d-RmR6.js +0 -1
  168. package/dist/web/assets/architecture-PBZL5I3N-DpVzOETR.js +0 -1
  169. package/dist/web/assets/arrow-up-BigIMx-e.js +0 -1
  170. package/dist/web/assets/channel-Cgy1thYT.js +0 -1
  171. package/dist/web/assets/chat-tab-DXBb9Y3U.js +0 -10
  172. package/dist/web/assets/check-ePA3ZvK4.js +0 -1
  173. package/dist/web/assets/chevron-down-EQA06nR-.js +0 -1
  174. package/dist/web/assets/chevron-right-CXzzT44u.js +0 -1
  175. package/dist/web/assets/chunk-GLR3WWYH-CxUl1sdz.js +0 -2
  176. package/dist/web/assets/chunk-HHEYEP7N-DN7ebS2Y.js +0 -1
  177. package/dist/web/assets/chunk-QZHKN3VN-C4La7oLj.js +0 -1
  178. package/dist/web/assets/classDiagram-VBA2DB6C-C3IyfqG-.js +0 -1
  179. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Dcvhz2pb.js +0 -1
  180. package/dist/web/assets/clone--C7Tby8z.js +0 -1
  181. package/dist/web/assets/code-editor-Cr7JrBKC.js +0 -8
  182. package/dist/web/assets/columns-2-BZ9uqssV.js +0 -1
  183. package/dist/web/assets/createLucideIcon-PuMiQgHl.js +0 -1
  184. package/dist/web/assets/database-D1ToEV9d.js +0 -1
  185. package/dist/web/assets/diff-viewer-BBr6e_gb.js +0 -4
  186. package/dist/web/assets/dist-KUoHa6tg.js +0 -1
  187. package/dist/web/assets/extension-webview-B0klBip8.js +0 -3
  188. package/dist/web/assets/eye-CNcBU6Tx.js +0 -1
  189. package/dist/web/assets/git-graph-CDiwGa0g.js +0 -1
  190. package/dist/web/assets/gitGraph-HDMCJU4V-DcPyMEIJ.js +0 -1
  191. package/dist/web/assets/index-CkaCzNgO.css +0 -2
  192. package/dist/web/assets/index-Ic5uTu20.js +0 -26
  193. package/dist/web/assets/info-3K5VOQVL-Dw4O15cw.js +0 -1
  194. package/dist/web/assets/infoDiagram-LFFYTUFH-DFhmsucr.js +0 -2
  195. package/dist/web/assets/input-CcbTF6ih.js +0 -45
  196. package/dist/web/assets/jsx-runtime-R_NjdZtX.js +0 -1
  197. package/dist/web/assets/keybindings-store-CxE6BlG2.js +0 -1
  198. package/dist/web/assets/packet-RMMSAZCW-o3LmdL8H.js +0 -1
  199. package/dist/web/assets/pie-UPGHQEXC-BjNP0M3B.js +0 -1
  200. package/dist/web/assets/plus-Iso5r9vD.js +0 -1
  201. package/dist/web/assets/port-forwarding-tab-BPuSc6pI.js +0 -1
  202. package/dist/web/assets/radar-KQ55EAFF-gDgOiaME.js +0 -1
  203. package/dist/web/assets/refresh-cw-BgQzFNaG.js +0 -1
  204. package/dist/web/assets/scroll-area-i4EZlOl_.js +0 -1
  205. package/dist/web/assets/settings-tab-BzSSN2BQ.js +0 -1
  206. package/dist/web/assets/sqlite-viewer-CoyZOM_Y.js +0 -1
  207. package/dist/web/assets/square-pfn_LYYy.js +0 -1
  208. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DksQJ7es.js +0 -1
  209. package/dist/web/assets/table-CHv2x_qg.js +0 -1
  210. package/dist/web/assets/tag-Bb_UFXt0.js +0 -1
  211. package/dist/web/assets/text-wrap-D8BbQYTx.js +0 -1
  212. package/dist/web/assets/trash-2-DYCa06CV.js +0 -1
  213. package/dist/web/assets/treemap-KZPCXAKY-DwFqAvnj.js +0 -1
  214. package/dist/web/assets/x-BXecj-16.js +0 -1
  215. package/src/web/components/git/git-graph-branch-label.tsx +0 -124
  216. package/src/web/components/git/git-graph-constants.ts +0 -185
  217. package/src/web/components/git/git-graph-detail.tsx +0 -107
  218. package/src/web/components/git/git-graph-dialog.tsx +0 -72
  219. package/src/web/components/git/git-graph-row.tsx +0 -167
  220. package/src/web/components/git/git-graph-settings-dialog.tsx +0 -104
  221. package/src/web/components/git/git-graph-svg.tsx +0 -54
  222. package/src/web/components/git/git-graph-toolbar.tsx +0 -195
  223. package/src/web/components/git/git-graph.tsx +0 -193
  224. package/src/web/components/git/use-column-resize.ts +0 -33
  225. package/src/web/components/git/use-git-graph.ts +0 -201
  226. /package/dist/web/assets/{api-client-wQbeUyeh.js → api-client-BvxmRZUi.js} +0 -0
  227. /package/dist/web/assets/{array-X0JlPOfd.js → array-BFDiaBgf.js} +0 -0
  228. /package/dist/web/assets/{csv-parser-CElqio6o.js → csv-parser-i7fjqP2H.js} +0 -0
  229. /package/dist/web/assets/{cytoscape.esm-BfIOPvwt.js → cytoscape.esm-C8i2jUzT.js} +0 -0
  230. /package/dist/web/assets/{defaultLocale-B6RGN4id.js → defaultLocale-ZeknFqNe.js} +0 -0
  231. /package/dist/web/assets/{dist-CK1enexV.js → dist-DZmJeHOA.js} +0 -0
  232. /package/dist/web/assets/{init-BmUWJJHz.js → init-0VJVrkRJ.js} +0 -0
  233. /package/dist/web/assets/{isArrayLikeObject-BrCM-iA1.js → isArrayLikeObject-ClzWCpcm.js} +0 -0
  234. /package/dist/web/assets/{katex-xQS_6bNb.js → katex-DR0kdMDv.js} +0 -0
  235. /package/dist/web/assets/{lib-CfWBrYll.js → lib-DSLzfeW0.js} +0 -0
  236. /package/dist/web/assets/{math-CpLFzrfV.js → math-CRc16Nj6.js} +0 -0
  237. /package/dist/web/assets/{path-CoPyR7c2.js → path-INs8XTPH.js} +0 -0
  238. /package/dist/web/assets/{preload-helper-CH6UZRzu.js → preload-helper-mr3rCizq.js} +0 -0
  239. /package/dist/web/assets/{react-j5zqhEum.js → react-0tkk-ztn.js} +0 -0
  240. /package/dist/web/assets/{rough.esm-D5NinLFK.js → rough.esm-eLccZ4OJ.js} +0 -0
  241. /package/dist/web/assets/{sql-completion-provider-D0xutVaK.js → sql-completion-provider-B8uUWWej.js} +0 -0
  242. /package/dist/web/assets/{src-j04igtQ5.js → src-CqyWLlNZ.js} +0 -0
  243. /package/dist/web/assets/{utils-CSCvNZxE.js → utils-DX8jb5qv.js} +0 -0
@@ -0,0 +1,2417 @@
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="stash-dropdown">
31
+ <button id="btn-stash" title="Stashes"></button>
32
+ <div id="stash-popover" class="stash-popover hidden">
33
+ <div class="stash-popover-header"><span>Stashes</span></div>
34
+ <div id="stash-list" class="stash-list"></div>
35
+ <div class="stash-popover-footer">
36
+ <button id="stash-save" class="btn-sm">+ Stash Changes</button>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ <div class="worktree-dropdown">
41
+ <button id="btn-worktree" title="Worktrees"></button>
42
+ <div id="worktree-popover" class="worktree-popover hidden">
43
+ <div class="worktree-popover-header">
44
+ <span>Worktrees</span>
45
+ </div>
46
+ <div id="worktree-list" class="worktree-list"></div>
47
+ <div class="worktree-popover-footer">
48
+ <button id="wt-add" class="btn-sm">+ Add Worktree</button>
49
+ <button id="wt-prune" class="btn-sm secondary" title="Remove stale worktree entries">Prune</button>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ <button id="btn-find" title="Find (Ctrl+F)"></button>
54
+ <button id="btn-settings" title="Settings"></button>
55
+ </div>
56
+ </header>
57
+ <div id="find-bar" class="find-bar hidden">
58
+ <input id="find-input" type="text" placeholder="Search commits..." />
59
+ <span id="find-count"></span>
60
+ <button id="find-prev" title="Previous">&uarr;</button>
61
+ <button id="find-next" title="Next">&darr;</button>
62
+ <button id="find-close" title="Close">&times;</button>
63
+ </div>
64
+ <div id="graph-container">
65
+ <div id="graph-header" class="commit-row header-row">
66
+ <div class="col-graph">Graph<div class="graph-resize-handle" id="graph-resize-handle"></div></div>
67
+ <div class="col-message">Message</div>
68
+ <div class="col-author">Author</div>
69
+ <div class="col-date">Date</div>
70
+ <div class="col-hash">Hash</div>
71
+ </div>
72
+ <div id="commit-list-wrapper">
73
+ <div id="graph-svg-container"></div>
74
+ <div id="commit-list"></div>
75
+ </div>
76
+ <div id="loading" class="loading hidden">Loading...</div>
77
+ </div>
78
+ <div id="detail-panel" class="detail-panel hidden"></div>
79
+ <div id="settings-panel" class="settings-panel">
80
+ <div class="settings-header">
81
+ <h3>Git Graph Settings</h3>
82
+ <button id="settings-close" title="Close">&times;</button>
83
+ </div>
84
+ <div class="settings-body">
85
+ <details class="settings-section" open>
86
+ <summary>General</summary>
87
+ <div class="setting-row"><label>Max Commits</label><input type="number" id="s-maxCommits" min="10" max="10000" step="50"></div>
88
+ <div class="setting-row"><label>Show Tags</label><input type="checkbox" id="s-showTags"></div>
89
+ <div class="setting-row"><label>Show Stashes</label><input type="checkbox" id="s-showStashes"></div>
90
+ <div class="setting-row"><label>Show Remote Branches</label><input type="checkbox" id="s-showRemoteBranches"></div>
91
+ <div class="setting-row"><label>Graph Style</label><select id="s-graphStyle"><option value="rounded">Rounded</option><option value="angular">Angular</option></select></div>
92
+ <div class="setting-row"><label>First Parent Only</label><input type="checkbox" id="s-firstParentOnly"></div>
93
+ <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>
94
+ <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>
95
+ <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>
96
+ </details>
97
+ <details class="settings-section" open>
98
+ <summary>User Details</summary>
99
+ <div class="setting-row"><label>Name</label><input type="text" id="s-userName" placeholder="user.name"></div>
100
+ <div class="setting-row"><label>Email</label><input type="text" id="s-userEmail" placeholder="user.email"></div>
101
+ <div class="setting-row" style="justify-content:flex-end"><button id="s-saveUser" class="btn-sm">Save User Details</button></div>
102
+ </details>
103
+ <details class="settings-section" open>
104
+ <summary>Remotes</summary>
105
+ <div id="s-remotes-list"></div>
106
+ <div class="add-remote-form">
107
+ <input type="text" id="s-newRemoteName" placeholder="Remote name">
108
+ <input type="text" id="s-newRemoteUrl" placeholder="Remote URL">
109
+ <button id="s-addRemote" class="btn-sm">Add Remote</button>
110
+ </div>
111
+ </details>
112
+ <details class="settings-section">
113
+ <summary>Issue Linking</summary>
114
+ <p style="font-size:11px;color:var(--subtext);margin-bottom:6px">Turn issue references in commit messages into clickable links.</p>
115
+ <div id="issue-rules-list"></div>
116
+ <button id="add-issue-rule" class="btn-sm" style="margin-top:6px">+ Add Rule</button>
117
+ </details>
118
+ <details class="settings-section">
119
+ <summary>Pull Request Creation</summary>
120
+ <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>
121
+ <div id="pr-config" class="hidden">
122
+ <div class="setting-row"><label>Owner</label><input type="text" id="pr-owner" placeholder="owner or org"></div>
123
+ <div class="setting-row"><label>Repo</label><input type="text" id="pr-repo" placeholder="repository name"></div>
124
+ <div class="setting-row"><label>Target Branch</label><input type="text" id="pr-target" placeholder="main"></div>
125
+ <div class="setting-row"><label>URL Template</label><input type="text" id="pr-url-template" placeholder="https://..."></div>
126
+ <p style="font-size:10px;color:var(--subtext);margin:2px 0 4px">Variables: \${owner}, \${repo}, \${sourceBranch}, \${targetBranch}</p>
127
+ <div class="setting-row" style="justify-content:flex-end"><button id="pr-save" class="btn-sm">Save PR Config</button></div>
128
+ </div>
129
+ </details>
130
+ </div>
131
+ </div>
132
+ <div id="status-bar">
133
+ <span id="status-text">Loading repository...</span>
134
+ </div>
135
+ </div>
136
+ <div id="context-menu" class="context-menu hidden"></div>
137
+ <script>
138
+ ${getScript()}
139
+ </script>
140
+ </body>
141
+ </html>`;
142
+ }
143
+
144
+ function getStyles(): string {
145
+ return `
146
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
147
+ :root {
148
+ --bg: #ffffff; --surface: #f4f4f5; --text: #09090b; --subtext: #71717a; --subtle: #a1a1aa;
149
+ --border: #e4e4e7; --border2: #d4d4d8; --blue: #3b82f6; --red: #ef4444; --green: #22c55e;
150
+ --yellow: #eab308; --purple: #8b5cf6; --orange: #f97316;
151
+ --surface-hover: #f4f4f5; --selected: #eff6ff;
152
+ }
153
+ @media (prefers-color-scheme: dark) {
154
+ :root {
155
+ --bg: #09090b; --surface: #18181b; --text: #fafafa; --subtext: #a1a1aa; --subtle: #52525b;
156
+ --border: #27272a; --border2: #3f3f46; --selected: #1e293b; --surface-hover: #27272a;
157
+ }
158
+ }
159
+ 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; }
160
+ #app { display: flex; flex-direction: column; height: 100vh; }
161
+
162
+ /* Toolbar */
163
+ #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; }
164
+ .toolbar-left, .toolbar-right { display: flex; align-items: center; gap: 6px; }
165
+ select { background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 4px 8px; font-size: 12px; }
166
+ 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; }
167
+ button:hover { background: var(--surface-hover); border-color: var(--border2); }
168
+ button:active { background: var(--surface); }
169
+ .btn-fetching { opacity: 0.6; pointer-events: none; }
170
+
171
+ /* Branch dropdown */
172
+ .branch-dropdown { position: relative; }
173
+ .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; }
174
+ .branch-trigger::after { content: '\\25BC'; font-size: 8px; position: absolute; right: 8px; top: 50%; transform: translateY(-50%); color: var(--subtext); }
175
+ .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; }
176
+ .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; }
177
+ .branch-list { overflow-y: auto; max-height: 250px; }
178
+ .branch-option { padding: 6px 10px; cursor: pointer; font-size: 12px; display: flex; align-items: center; gap: 6px; }
179
+ .branch-option:hover { background: var(--surface-hover); }
180
+ .branch-option.selected { background: var(--selected); font-weight: 600; }
181
+
182
+ /* Worktree popover */
183
+ .worktree-dropdown { position: relative; }
184
+ #btn-worktree { display: flex; align-items: center; gap: 4px; font-size: 12px; padding: 4px 8px; }
185
+ #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; }
186
+ .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; }
187
+ .worktree-popover-header { padding: 8px 12px; font-size: 12px; font-weight: 600; border-bottom: 1px solid var(--border); }
188
+ .worktree-list { overflow-y: auto; max-height: 240px; }
189
+ .wt-item { padding: 8px 12px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; font-size: 12px; }
190
+ .wt-item:last-child { border-bottom: none; }
191
+ .wt-item-info { flex: 1; min-width: 0; }
192
+ .wt-item-branch { font-weight: 600; display: flex; align-items: center; gap: 4px; }
193
+ .wt-item-path { font-size: 10px; color: var(--subtext); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
194
+ .wt-badge { font-size: 9px; padding: 1px 4px; border-radius: 4px; background: var(--border); color: var(--subtext); }
195
+ .wt-badge-current { background: var(--accent, #58a6ff); color: #fff; }
196
+ .wt-item-active { background: var(--selected); }
197
+ .wt-badge-locked { background: #d29922; color: #fff; }
198
+ .wt-item-actions { display: flex; gap: 4px; flex-shrink: 0; }
199
+ .wt-item-actions button { min-width: 24px; min-height: 24px; padding: 2px 6px; font-size: 10px; }
200
+ .worktree-popover-footer { padding: 8px 12px; border-top: 1px solid var(--border); display: flex; gap: 6px; }
201
+ .worktree-popover-footer .btn-sm { flex: 1; }
202
+ .wt-empty { padding: 16px; text-align: center; font-size: 11px; color: var(--subtext); }
203
+ @media (max-width: 768px) { .branch-option { padding: 10px 12px; min-height: 44px; } }
204
+
205
+ /* Stash popover */
206
+ .stash-dropdown { position: relative; }
207
+ #btn-stash { display: flex; align-items: center; gap: 4px; font-size: 12px; padding: 4px 8px; }
208
+ #btn-stash .stash-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; }
209
+ .stash-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; }
210
+ .stash-popover-header { padding: 8px 12px; font-size: 12px; font-weight: 600; border-bottom: 1px solid var(--border); }
211
+ .stash-list { overflow-y: auto; max-height: 240px; }
212
+ .stash-item { padding: 8px 12px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; font-size: 12px; }
213
+ .stash-item:last-child { border-bottom: none; }
214
+ .stash-item-info { flex: 1; min-width: 0; }
215
+ .stash-item-ref { font-weight: 600; font-size: 11px; color: var(--subtext); }
216
+ .stash-item-msg { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
217
+ .stash-item-actions { display: flex; gap: 4px; flex-shrink: 0; }
218
+ .stash-item-actions button { min-width: 24px; min-height: 24px; padding: 2px 6px; font-size: 10px; }
219
+ .stash-empty { padding: 16px; text-align: center; font-size: 11px; color: var(--subtext); }
220
+ .stash-popover-footer { padding: 8px 12px; border-top: 1px solid var(--border); display: flex; gap: 6px; }
221
+ .stash-popover-footer .btn-sm { flex: 1; }
222
+
223
+ /* Merge/rebase banner */
224
+ .merge-banner { padding: 8px 12px; background: rgba(133, 77, 14, 0.12); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; font-size: 12px; flex-shrink: 0; }
225
+ .merge-banner .banner-icon { color: #eab308; }
226
+ .merge-banner .banner-text { flex: 1; }
227
+ .merge-banner .banner-actions { display: flex; gap: 4px; }
228
+ .merge-banner .banner-actions button { font-size: 11px; padding: 2px 8px; }
229
+ .merge-banner .btn-continue { background: var(--green); color: #fff; border-color: transparent; }
230
+ .merge-banner .btn-abort { background: var(--red); color: #fff; border-color: transparent; }
231
+
232
+ /* Conflict section */
233
+ .conflict-header { padding: 4px 0; font-size: 12px; font-weight: 600; color: var(--red); display: flex; align-items: center; gap: 4px; }
234
+ .file-status-U { color: var(--red); font-weight: bold; }
235
+
236
+ /* Find bar */
237
+ .find-bar { display: flex; align-items: center; gap: 6px; padding: 6px 12px; border-bottom: 1px solid var(--border); background: var(--surface); }
238
+ .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; }
239
+ .find-bar input:focus { outline: none; border-color: var(--blue); }
240
+ #find-count { font-size: 11px; color: var(--subtext); min-width: 60px; }
241
+ .hidden { display: none !important; }
242
+
243
+ /* Graph container */
244
+ #graph-container { flex: 1; overflow-y: auto; overflow-x: hidden; }
245
+ .commit-row { display: flex; align-items: center; cursor: pointer; min-height: 28px; padding: 0 8px; }
246
+ .commit-row:hover { background: var(--surface-hover); }
247
+ .commit-row.selected { background: var(--selected); }
248
+ .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); }
249
+ .commit-row.search-match { background: rgba(234, 179, 8, 0.15); }
250
+ .commit-row.virtual { opacity: 0.85; font-style: italic; }
251
+ .commit-row.virtual .col-message { color: var(--subtext); }
252
+ .file-clickable { cursor: pointer; border-radius: 3px; padding: 2px 4px; margin: 0 -4px; }
253
+ .file-clickable:hover { background: var(--surface-hover); }
254
+ .col-graph { width: var(--graph-col-w, 120px); min-width: var(--graph-col-w, 80px); overflow: hidden; flex-shrink: 0; position: relative; }
255
+ .graph-resize-handle { position: absolute; right: 0; top: 0; bottom: 0; width: 6px; cursor: col-resize; z-index: 3; background: transparent; }
256
+ .graph-resize-handle:hover, .graph-resize-handle.dragging { background: var(--blue); opacity: 0.5; }
257
+ .col-message { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 8px; }
258
+ .col-author { width: 120px; min-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--subtext); font-size: 12px; }
259
+ .col-date { width: 100px; min-width: 100px; color: var(--subtext); font-size: 12px; }
260
+ .col-hash { width: 70px; min-width: 70px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; color: var(--subtle); }
261
+
262
+ /* Ref badges */
263
+ .ref-badge { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-right: 4px; vertical-align: middle; }
264
+ .ref-head { background: var(--green); color: #fff; }
265
+ .ref-local { background: var(--blue); color: #fff; }
266
+ .ref-remote { background: var(--purple); color: #fff; }
267
+ .ref-tag { background: var(--yellow); color: #000; }
268
+
269
+ /* SVG graph — single SVG overlay */
270
+ #commit-list-wrapper { position: relative; }
271
+ #graph-svg-container { position: absolute; top: 0; left: 8px; z-index: 1; pointer-events: none; }
272
+ #graph-svg-container circle { pointer-events: auto; cursor: pointer; }
273
+ #graph-svg-container .line { stroke-width: 2; fill: none; }
274
+ #graph-svg-container .graphCurrent { fill: var(--bg); stroke-width: 2; }
275
+ #graph-svg-container .graphStashOuter { fill: none; stroke: #808080; stroke-width: 1.5; }
276
+ #graph-svg-container .graphStashInner { fill: #808080; }
277
+ .commit-row.graph-hover { background: var(--surface-hover); }
278
+
279
+ /* Detail panel */
280
+ .detail-panel { border-top: 1px solid var(--border2); background: var(--surface); max-height: 40vh; overflow-y: auto; padding: 12px 16px; flex-shrink: 0; }
281
+ .detail-panel h3 { font-size: 14px; margin-bottom: 8px; }
282
+ .detail-field { margin-bottom: 4px; font-size: 12px; }
283
+ .detail-field .label { color: var(--subtext); display: inline-block; width: 80px; }
284
+ .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; }
285
+ .file-list { margin-top: 8px; }
286
+ .file-item { display: flex; align-items: center; gap: 6px; padding: 2px 0; font-size: 12px; font-family: 'SF Mono', 'Fira Code', monospace; }
287
+ .file-status { display: inline-block; width: 16px; text-align: center; font-weight: 700; font-size: 11px; }
288
+ .file-status-A { color: var(--green); }
289
+ .file-status-M { color: var(--yellow); }
290
+ .file-status-D { color: var(--red); }
291
+ .file-status-R { color: var(--blue); }
292
+ .file-stat { color: var(--subtext); font-size: 11px; margin-left: auto; }
293
+ .file-stat .add { color: var(--green); }
294
+ .file-stat .del { color: var(--red); }
295
+
296
+ /* File view toggle */
297
+ .file-view-toggle { display: flex; gap: 2px; margin-bottom: 6px; }
298
+ .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; }
299
+ .toggle-btn.active { background: var(--surface-hover); border-color: var(--border2); }
300
+ .tree-dir { display: flex; align-items: center; gap: 4px; padding: 2px 0; font-size: 12px; color: var(--subtext); }
301
+ .tree-dir-name { font-weight: 500; color: var(--text); }
302
+ .tree-dir-count { font-size: 11px; color: var(--subtle); }
303
+
304
+ /* File actions */
305
+ .file-actions { display: flex; gap: 2px; margin-left: auto; flex-shrink: 0; }
306
+ .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; }
307
+ .file-action-btn:hover { background: var(--surface-hover); color: var(--text); }
308
+ .file-action-btn[data-action="discard"]:hover { color: var(--red); }
309
+ .section-actions { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
310
+ .commit-section { margin-top: 12px; border-top: 1px solid var(--border); padding-top: 12px; }
311
+ .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; }
312
+ .commit-section textarea:focus { outline: none; border-color: var(--blue); }
313
+ .commit-actions { display: flex; justify-content: flex-end; margin-top: 6px; gap: 6px; }
314
+ .btn-commit { background: var(--green); color: #fff; border-color: transparent; padding: 4px 16px; font-weight: 600; }
315
+ .btn-commit:disabled { opacity: 0.4; cursor: not-allowed; }
316
+ .btn-commit:hover:not(:disabled) { opacity: 0.9; }
317
+ @media (max-width: 768px) { .file-action-btn { min-width: 36px; min-height: 36px; } }
318
+
319
+ /* Status bar */
320
+ #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; }
321
+
322
+ /* Context menu */
323
+ .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); }
324
+ .ctx-item { padding: 6px 12px; cursor: pointer; font-size: 12px; display: flex; align-items: center; gap: 8px; }
325
+ .ctx-item:hover { background: var(--surface-hover); }
326
+ .ctx-item.destructive { color: var(--red); }
327
+ .ctx-separator { border-top: 1px solid var(--border); margin: 4px 0; }
328
+
329
+ /* Loading */
330
+ .loading { text-align: center; padding: 16px; color: var(--subtext); }
331
+
332
+ /* Settings panel */
333
+ .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; }
334
+ .settings-panel.open { transform: translateX(0); }
335
+ .settings-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
336
+ .settings-header h3 { font-size: 14px; font-weight: 600; }
337
+ .settings-body { flex: 1; overflow-y: auto; padding: 8px 0; }
338
+ .settings-section { border-bottom: 1px solid var(--border); padding: 8px 14px; }
339
+ .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; }
340
+ .settings-section[open] summary { margin-bottom: 6px; }
341
+ .setting-row { display: flex; align-items: center; justify-content: space-between; padding: 5px 0; font-size: 12px; gap: 8px; }
342
+ .setting-row label { flex: 1; min-width: 0; }
343
+ .setting-row input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--blue); flex-shrink: 0; }
344
+ .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; }
345
+ .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; }
346
+ .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; }
347
+ .btn-sm { font-size: 11px; padding: 3px 10px; border-radius: 6px; min-width: 0; min-height: 0; }
348
+ .remote-item { padding: 6px 0; border-bottom: 1px solid var(--border); font-size: 12px; }
349
+ .remote-item:last-child { border-bottom: none; }
350
+ .remote-item .remote-name { font-weight: 600; margin-bottom: 2px; }
351
+ .remote-item .remote-url { color: var(--subtext); font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; word-break: break-all; }
352
+ .remote-actions { display: flex; gap: 4px; margin-top: 4px; }
353
+ .add-remote-form { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
354
+ .add-remote-form input { background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 4px 6px; font-size: 12px; }
355
+ .issue-rule-row { display: flex; gap: 4px; align-items: center; margin-bottom: 4px; }
356
+ .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; }
357
+ .issue-rule-row input.rule-error { border-color: var(--red); }
358
+ .issue-rule-row .rule-remove { min-width: 24px; min-height: 24px; padding: 0; font-size: 14px; color: var(--red); border: none; }
359
+ @media (max-width: 768px) { .settings-panel { width: 100%; } }
360
+
361
+ /* Dialog overlay */
362
+ .dialog-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 200; display: flex; align-items: center; justify-content: center; }
363
+ .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); }
364
+ .dialog h3 { font-size: 14px; margin-bottom: 12px; }
365
+ .dialog p { font-size: 12px; color: var(--subtext); margin-bottom: 12px; }
366
+ .dialog p.warning { color: var(--red); font-weight: 600; }
367
+ .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; }
368
+ .dialog input:focus, .dialog select:focus { outline: none; border-color: var(--blue); }
369
+ .dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
370
+ .dialog-actions button { min-width: 64px; }
371
+ .dialog-actions .btn-primary { background: var(--blue); color: #fff; border-color: transparent; }
372
+ .dialog-actions .btn-danger { background: var(--red); color: #fff; border-color: transparent; }
373
+
374
+ /* Links in commit messages */
375
+ .commit-link { color: var(--blue); text-decoration: none; cursor: pointer; }
376
+ .commit-link:hover { text-decoration: underline; }
377
+
378
+ /* Toast notifications */
379
+ .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; }
380
+ .toast-error { background: var(--red); color: #fff; }
381
+ .toast-success { background: var(--green); color: #fff; }
382
+ .toast-info { background: var(--blue); color: #fff; }
383
+ @keyframes toast-in { from { opacity: 0; transform: translateX(-50%) translateY(10px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }
384
+
385
+ /* Touch targets for mobile */
386
+ @media (max-width: 768px) {
387
+ .commit-row { min-height: 44px; }
388
+ .ctx-item { padding: 10px 16px; min-height: 44px; }
389
+ button { min-width: 44px; min-height: 44px; }
390
+ .col-author, .col-hash { display: none; }
391
+ .col-date { width: 60px; min-width: 60px; }
392
+ }
393
+ `;
394
+ }
395
+
396
+ function getScript(): string {
397
+ return `
398
+ const vscode = acquireVsCodeApi();
399
+ const SVG_NS = 'http://www.w3.org/2000/svg';
400
+ const NULL_VERTEX_ID = -1;
401
+ const GRAPH_COLORS = ['#4ec9b0','#569cd6','#c586c0','#ce9178','#dcdcaa','#4fc1ff','#d7ba7d','#9cdcfe','#b5cea8','#d16969'];
402
+ const graphConfig = {
403
+ colours: GRAPH_COLORS,
404
+ grid: { x: 16, y: 28, offsetX: 8, offsetY: 14, expandY: 60 },
405
+ style: 'rounded'
406
+ };
407
+
408
+ // --- State ---
409
+ const DEFAULT_SETTINGS = {
410
+ maxCommits: 300, showTags: true, showStashes: true, showRemoteBranches: true,
411
+ graphStyle: 'rounded', firstParentOnly: false, dateFormat: 'relative', commitOrdering: 'topo',
412
+ issueLinkingRules: [{ pattern: '#(\\\\d+)', url: '' }], prCreation: null,
413
+ autoFetchInterval: 0,
414
+ };
415
+
416
+ const state = {
417
+ repo: '',
418
+ commits: [],
419
+ branches: [],
420
+ tags: [],
421
+ remotes: [],
422
+ stashes: [],
423
+ currentBranch: '',
424
+ head: '',
425
+ selectedCommit: null,
426
+ expandedCommit: null,
427
+ maxCommits: 300,
428
+ loading: false,
429
+ uncommitted: null,
430
+ searchMatches: [],
431
+ searchIndex: -1,
432
+ settings: { ...DEFAULT_SETTINGS },
433
+ userDetails: { name: '', email: '' },
434
+ graphColWidth: null,
435
+ fileViewMode: 'list',
436
+ worktrees: [],
437
+ mergeState: null,
438
+ _lastDetail: null,
439
+ };
440
+
441
+ // --- SVG Icons ---
442
+ const ICONS = {
443
+ 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>',
444
+ 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>',
445
+ 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>',
446
+ 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>',
447
+ 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>',
448
+ 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>',
449
+ 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>',
450
+ 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>',
451
+ 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>',
452
+ 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>',
453
+ 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>',
454
+ 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>',
455
+ 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>',
456
+ 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>',
457
+ archive: '<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="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>',
458
+ };
459
+
460
+ // --- Toast notifications ---
461
+ function showToast(message, type) {
462
+ const existing = document.querySelector('.toast');
463
+ if (existing) existing.remove();
464
+ const el = document.createElement('div');
465
+ el.className = 'toast toast-' + (type || 'error');
466
+ el.textContent = message;
467
+ document.body.appendChild(el);
468
+ setTimeout(() => { if (el.parentNode) el.remove(); }, 4000);
469
+ }
470
+
471
+ // --- Init ---
472
+ document.getElementById('btn-refresh').innerHTML = ICONS.refresh;
473
+ document.getElementById('btn-fetch').innerHTML = ICONS.download;
474
+ document.getElementById('btn-find').innerHTML = ICONS.search;
475
+ document.getElementById('btn-settings').innerHTML = ICONS.settings;
476
+ document.getElementById('btn-worktree').innerHTML = ICONS.gitBranch + ' <span class="wt-count" style="display:none">0</span>';
477
+ document.getElementById('btn-stash').innerHTML = ICONS.archive + ' <span class="stash-count" style="display:none">0</span>';
478
+ vscode.postMessage({ command: 'ready' });
479
+
480
+ // --- Message handler ---
481
+ window.addEventListener('message', (event) => {
482
+ const msg = event.data;
483
+ switch (msg.command) {
484
+ case 'loadRepoInfo':
485
+ state.repo = msg.data.path;
486
+ state.branches = msg.data.branches;
487
+ state.tags = msg.data.tags;
488
+ state.remotes = msg.data.remotes;
489
+ state.stashes = msg.data.stashes;
490
+ state.head = msg.data.head;
491
+ state.currentBranch = msg.data.currentBranch;
492
+ renderBranchSelector();
493
+ updateStatus();
494
+ if (document.getElementById('settings-panel').classList.contains('open')) renderRemotesList();
495
+ break;
496
+ case 'loadCommits':
497
+ if (msg.append) {
498
+ state.commits = state.commits.concat(msg.data);
499
+ } else {
500
+ state.commits = msg.data;
501
+ }
502
+ renderCommitList();
503
+ updateStatus();
504
+ state.loading = false;
505
+ document.getElementById('loading').classList.add('hidden');
506
+ break;
507
+ case 'commitDetails':
508
+ renderDetailPanel(msg.data);
509
+ break;
510
+ case 'refresh':
511
+ state.commits = msg.data;
512
+ if (msg.repoInfo) {
513
+ state.branches = msg.repoInfo.branches;
514
+ state.tags = msg.repoInfo.tags;
515
+ state.remotes = msg.repoInfo.remotes;
516
+ state.stashes = msg.repoInfo.stashes;
517
+ state.head = msg.repoInfo.head;
518
+ state.currentBranch = msg.repoInfo.currentBranch;
519
+ renderBranchSelector();
520
+ }
521
+ renderCommitList();
522
+ updateStatus();
523
+ break;
524
+ case 'loadUncommitted':
525
+ state.uncommitted = msg.data;
526
+ state.mergeState = msg.data?.mergeState || null;
527
+ renderCommitList();
528
+ renderMergeBanner();
529
+ if (state.selectedCommit === 'uncommitted') {
530
+ const u = msg.data;
531
+ if (!u || (u.staged.length === 0 && u.unstaged.length === 0 && (!u.conflicted || u.conflicted.length === 0))) {
532
+ state.selectedCommit = null;
533
+ state.expandedCommit = null;
534
+ document.getElementById('detail-panel').classList.add('hidden');
535
+ } else {
536
+ renderUncommittedDetail();
537
+ }
538
+ }
539
+ break;
540
+ case 'loadSettings':
541
+ state.settings = { ...DEFAULT_SETTINGS, ...msg.data };
542
+ state.maxCommits = state.settings.maxCommits;
543
+ applySettingsToUI();
544
+ break;
545
+ case 'loadUserDetails':
546
+ state.userDetails = msg.data;
547
+ document.getElementById('s-userName').value = msg.data.name;
548
+ document.getElementById('s-userEmail').value = msg.data.email;
549
+ break;
550
+ case 'loadOwnerRepo':
551
+ if (msg.data.owner) document.getElementById('pr-owner').value = msg.data.owner;
552
+ if (msg.data.repo) document.getElementById('pr-repo').value = msg.data.repo;
553
+ break;
554
+ case 'actionResult':
555
+ if (msg.action === 'fetch') {
556
+ fetchInProgress = false;
557
+ btnFetch.classList.remove('btn-fetching');
558
+ btnFetch.title = 'Fetch from remotes';
559
+ if (!msg.result.ok) {
560
+ document.getElementById('status-text').textContent = 'Fetch failed: ' + (msg.result.error || 'Unknown error');
561
+ }
562
+ } else if (!msg.result.ok && msg.action === 'createBranch' && msg.result.error && msg.result.error.includes('already exists')) {
563
+ // Extract branch name from error: "fatal: a branch named 'X' already exists"
564
+ const branchMatch = msg.result.error.match(/branch named '([^']+)'/);
565
+ const branchName = branchMatch ? branchMatch[1] : 'this branch';
566
+ showDialog({
567
+ title: 'Branch Already Exists',
568
+ message: 'A branch named <b>' + escHtml(branchName) + '</b> already exists, do you want to replace it with this new branch?',
569
+ rawMessage: true,
570
+ confirmLabel: 'Yes, replace the existing branch',
571
+ cancelLabel: 'No, choose another branch name',
572
+ onConfirm: () => gitAction('createBranch', { ...msg.args, force: true }),
573
+ });
574
+ } else if (!msg.result.ok) {
575
+ showToast('Git action failed: ' + (msg.result.error || 'Unknown error'), 'error');
576
+ }
577
+ // Refresh worktree list after worktree mutations
578
+ if (msg.result.ok && (msg.action === 'addWorktree' || msg.action === 'removeWorktree' || msg.action === 'pruneWorktrees')) {
579
+ vscode.postMessage({ command: 'requestWorktrees' });
580
+ }
581
+ // Refresh stash list after stash mutations
582
+ if (msg.result.ok && ['stashSave','stashPop','stashDrop','stashApply'].includes(msg.action)) {
583
+ vscode.postMessage({ command: 'requestStashes' });
584
+ }
585
+ break;
586
+ case 'loadWorktrees':
587
+ state.worktrees = msg.data || [];
588
+ renderWorktreeList();
589
+ break;
590
+ case 'loadStashes':
591
+ state.stashes = msg.data || [];
592
+ renderStashList();
593
+ break;
594
+ case 'error':
595
+ document.getElementById('status-text').textContent = 'Error: ' + msg.message;
596
+ break;
597
+ }
598
+ });
599
+
600
+ // --- File click delegation (opens diff tab) ---
601
+ document.getElementById('detail-panel').addEventListener('click', (e) => {
602
+ // File-level action buttons (stage/unstage/discard/open)
603
+ const actionBtn = e.target.closest('.file-action-btn');
604
+ if (actionBtn) {
605
+ e.stopPropagation();
606
+ const action = actionBtn.dataset.action;
607
+ const file = actionBtn.dataset.file;
608
+ if (action === 'open') {
609
+ vscode.postMessage({ command: 'openFile', filePath: file });
610
+ } else if (action === 'open-conflict') {
611
+ vscode.postMessage({ command: 'openConflictFile', filePath: file });
612
+ } else if (action === 'stage') {
613
+ vscode.postMessage({ command: 'gitAction', action: 'stage', args: { files: [file] } });
614
+ } else if (action === 'unstage') {
615
+ vscode.postMessage({ command: 'gitAction', action: 'unstage', args: { files: [file] } });
616
+ } else if (action === 'discard') {
617
+ showDialog({
618
+ title: 'Discard Changes',
619
+ message: 'Discard changes to "' + file + '"? This cannot be undone.',
620
+ destructive: true,
621
+ confirmLabel: 'Discard',
622
+ onConfirm: () => vscode.postMessage({ command: 'gitAction', action: 'discard', args: { files: [file] } }),
623
+ });
624
+ }
625
+ return;
626
+ }
627
+ // Section-level actions (Stage All / Unstage All)
628
+ const sectionBtn = e.target.closest('.section-action-btn');
629
+ if (sectionBtn) {
630
+ e.stopPropagation();
631
+ const action = sectionBtn.dataset.action;
632
+ if (action === 'stage-all') {
633
+ const files = state.uncommitted.unstaged.map(f => f.path);
634
+ vscode.postMessage({ command: 'gitAction', action: 'stage', args: { files } });
635
+ } else if (action === 'unstage-all') {
636
+ const files = state.uncommitted.staged.map(f => f.path);
637
+ vscode.postMessage({ command: 'gitAction', action: 'unstage', args: { files } });
638
+ }
639
+ return;
640
+ }
641
+ // Toggle buttons (tree/list view)
642
+ const toggleBtn = e.target.closest('.toggle-btn[data-view]');
643
+ if (toggleBtn) {
644
+ state.fileViewMode = toggleBtn.dataset.view;
645
+ if (state.selectedCommit === 'uncommitted') {
646
+ renderUncommittedDetail();
647
+ } else if (state._lastDetail) {
648
+ renderDetailPanel(state._lastDetail);
649
+ }
650
+ return;
651
+ }
652
+ // File click (opens diff)
653
+ const item = e.target.closest('.file-clickable');
654
+ if (!item) return;
655
+ const filePath = item.dataset.path;
656
+ const hash = item.dataset.hash;
657
+ const parentHash = item.dataset.parent || null;
658
+ if (filePath && hash) {
659
+ vscode.postMessage({ command: 'openDiff', filePath, hash, parentHash });
660
+ }
661
+ });
662
+
663
+ // --- Branch dropdown ---
664
+ let selectedBranch = 'all';
665
+ const branchTrigger = document.getElementById('branch-trigger');
666
+ const branchMenu = document.getElementById('branch-dropdown-menu');
667
+ const branchFilterInput = document.getElementById('branch-filter');
668
+ const branchListEl = document.getElementById('branch-list');
669
+
670
+ branchTrigger.addEventListener('click', (e) => {
671
+ e.stopPropagation();
672
+ const wasHidden = branchMenu.classList.contains('hidden');
673
+ branchMenu.classList.toggle('hidden');
674
+ if (wasHidden) {
675
+ branchFilterInput.value = '';
676
+ renderBranchOptions('');
677
+ branchFilterInput.focus();
678
+ }
679
+ });
680
+
681
+ document.addEventListener('click', (e) => {
682
+ if (!e.target.closest('#branch-selector')) branchMenu.classList.add('hidden');
683
+ });
684
+
685
+ branchFilterInput.addEventListener('input', () => {
686
+ renderBranchOptions(branchFilterInput.value.toLowerCase());
687
+ });
688
+ branchFilterInput.addEventListener('click', (e) => e.stopPropagation());
689
+
690
+ function renderBranchOptions(filter) {
691
+ const options = [{ name: 'all', label: 'All Branches', current: false }];
692
+ state.branches.forEach(b => {
693
+ if (b.remote && !state.settings.showRemoteBranches) return;
694
+ options.push({ name: b.name, label: (b.current ? '* ' : '') + b.name, current: b.current });
695
+ });
696
+ const filtered = filter ? options.filter(o => o.label.toLowerCase().includes(filter)) : options;
697
+ branchListEl.innerHTML = filtered.map(o =>
698
+ '<div class="branch-option' + (o.name === selectedBranch ? ' selected' : '') + '" data-branch="' + escHtml(o.name) + '">' + escHtml(o.label) + '</div>'
699
+ ).join('');
700
+ }
701
+
702
+ branchListEl.addEventListener('click', (e) => {
703
+ const opt = e.target.closest('.branch-option');
704
+ if (!opt) return;
705
+ const branch = opt.dataset.branch;
706
+ selectedBranch = branch;
707
+ branchTrigger.textContent = branch === 'all' ? 'All Branches' : branch;
708
+ branchMenu.classList.add('hidden');
709
+ state.commits = [];
710
+ document.getElementById('commit-list').innerHTML = '';
711
+ vscode.postMessage({ command: 'requestCommits', branch, maxCommits: state.maxCommits });
712
+ });
713
+
714
+ function renderBranchSelector() {
715
+ const branchNames = state.branches.map(b => b.name);
716
+ if (selectedBranch !== 'all' && !branchNames.includes(selectedBranch)) selectedBranch = 'all';
717
+ branchTrigger.textContent = selectedBranch === 'all' ? 'All Branches' : selectedBranch;
718
+ }
719
+
720
+ // --- Refresh ---
721
+ document.getElementById('btn-refresh').addEventListener('click', () => {
722
+ state.commits = [];
723
+ document.getElementById('commit-list').innerHTML = '';
724
+ vscode.postMessage({ command: 'requestRepoInfo' });
725
+ vscode.postMessage({ command: 'requestCommits', maxCommits: state.maxCommits });
726
+ });
727
+
728
+ // --- Fetch ---
729
+ const btnFetch = document.getElementById('btn-fetch');
730
+ let fetchInProgress = false;
731
+ let autoFetchTimer = null;
732
+
733
+ function doFetch() {
734
+ fetchInProgress = true;
735
+ btnFetch.classList.add('btn-fetching');
736
+ btnFetch.title = 'Fetching...';
737
+ vscode.postMessage({ command: 'gitAction', action: 'fetch', args: { prune: true } });
738
+ }
739
+
740
+ btnFetch.addEventListener('click', () => { if (!fetchInProgress) doFetch(); });
741
+
742
+ function startAutoFetch(intervalSec) {
743
+ stopAutoFetch();
744
+ if (!intervalSec || intervalSec <= 0) return;
745
+ const ms = Math.max(intervalSec, 10) * 1000;
746
+ autoFetchTimer = setInterval(() => { if (!fetchInProgress) doFetch(); }, ms);
747
+ }
748
+ function stopAutoFetch() {
749
+ if (autoFetchTimer) { clearInterval(autoFetchTimer); autoFetchTimer = null; }
750
+ }
751
+
752
+ // --- Worktree popover ---
753
+ const wtPopover = document.getElementById('worktree-popover');
754
+ const btnWorktree = document.getElementById('btn-worktree');
755
+
756
+ btnWorktree.addEventListener('click', (e) => {
757
+ e.stopPropagation();
758
+ const wasHidden = wtPopover.classList.contains('hidden');
759
+ wtPopover.classList.toggle('hidden');
760
+ if (wasHidden) vscode.postMessage({ command: 'requestWorktrees' });
761
+ });
762
+
763
+ document.addEventListener('click', (e) => {
764
+ if (!e.target.closest('.worktree-dropdown')) wtPopover.classList.add('hidden');
765
+ });
766
+
767
+ function renderWorktreeList() {
768
+ const listEl = document.getElementById('worktree-list');
769
+ const countEl = btnWorktree.querySelector('.wt-count');
770
+ const wts = state.worktrees;
771
+ if (countEl) {
772
+ countEl.textContent = wts.length;
773
+ countEl.style.display = wts.length > 1 ? '' : 'none';
774
+ }
775
+ if (!wts.length) {
776
+ listEl.innerHTML = '<div class="wt-empty">No worktrees found</div>';
777
+ return;
778
+ }
779
+ listEl.innerHTML = wts.map((wt, i) => {
780
+ const branchName = wt.branch || (wt.isDetached ? 'detached HEAD' : '(bare)');
781
+ const shortHash = wt.head ? wt.head.substring(0, 7) : '';
782
+ const isCurrent = wt.path === state.repo;
783
+ let badges = '';
784
+ if (isCurrent) badges += ' <span class="wt-badge wt-badge-current">current</span>';
785
+ if (wt.isMain && !isCurrent) badges += ' <span class="wt-badge">main</span>';
786
+ if (wt.locked) badges += ' <span class="wt-badge wt-badge-locked">locked</span>';
787
+ if (wt.prunable) badges += ' <span class="wt-badge">prunable</span>';
788
+ if (wt.isDetached) badges += ' <span class="wt-badge">detached</span>';
789
+ const actions = isCurrent ? ''
790
+ : '<button class="wt-open" data-idx="' + i + '" title="Open in PPM">' + ICONS.fileOpen + '</button>'
791
+ + (wt.isMain ? '' : '<button class="wt-remove" data-idx="' + i + '" title="Remove worktree">' + ICONS.trash + '</button>');
792
+ return '<div class="wt-item' + (isCurrent ? ' wt-item-active' : '') + '">'
793
+ + '<div class="wt-item-info">'
794
+ + '<div class="wt-item-branch">' + ICONS.gitBranch + ' ' + escHtml(branchName) + badges + '</div>'
795
+ + '<div class="wt-item-path" title="' + escHtml(wt.path) + '">' + escHtml(wt.path) + ' <span style="color:var(--subtle)">' + shortHash + '</span></div>'
796
+ + '</div>'
797
+ + (actions ? '<div class="wt-item-actions">' + actions + '</div>' : '')
798
+ + '</div>';
799
+ }).join('');
800
+
801
+ // Bind action buttons
802
+ listEl.querySelectorAll('.wt-open').forEach(btn => {
803
+ btn.addEventListener('click', (e) => {
804
+ e.stopPropagation();
805
+ const wt = state.worktrees[parseInt(btn.dataset.idx)];
806
+ if (wt) vscode.postMessage({ command: 'openWorktree', path: wt.path });
807
+ });
808
+ });
809
+ listEl.querySelectorAll('.wt-remove').forEach(btn => {
810
+ btn.addEventListener('click', (e) => {
811
+ e.stopPropagation();
812
+ const wt = state.worktrees[parseInt(btn.dataset.idx)];
813
+ if (!wt) return;
814
+ showDialog({
815
+ title: 'Remove Worktree',
816
+ message: 'Remove worktree at "' + wt.path + '"?',
817
+ destructive: true,
818
+ confirmLabel: 'Remove',
819
+ onConfirm: () => vscode.postMessage({ command: 'removeWorktree', path: wt.path }),
820
+ });
821
+ });
822
+ });
823
+ }
824
+
825
+ document.getElementById('wt-add').addEventListener('click', () => {
826
+ showCreateWorktreeDialog();
827
+ });
828
+
829
+ document.getElementById('wt-prune').addEventListener('click', () => {
830
+ showDialog({
831
+ title: 'Prune Worktrees',
832
+ message: 'Remove stale worktree entries (worktrees whose directories no longer exist)?',
833
+ confirmLabel: 'Prune',
834
+ onConfirm: () => vscode.postMessage({ command: 'pruneWorktrees' }),
835
+ });
836
+ });
837
+
838
+ function showCreateWorktreeDialog(startPoint) {
839
+ const overlay = document.createElement('div');
840
+ overlay.className = 'dialog-overlay';
841
+ const dialog = document.createElement('div');
842
+ dialog.className = 'dialog';
843
+ dialog.innerHTML = '<h3>Add Worktree</h3>'
844
+ + '<p style="font-size:12px;margin-bottom:8px">Path for the new worktree directory:</p>'
845
+ + '<input type="text" id="wt-dialog-path" placeholder="/path/to/worktree" style="width:100%;margin-bottom:8px" />'
846
+ + '<p style="font-size:12px;margin-bottom:4px">Branch:</p>'
847
+ + '<div style="display:flex;gap:8px;margin-bottom:6px">'
848
+ + '<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>'
849
+ + '<label style="font-size:11px;display:flex;align-items:center;gap:4px"><input type="radio" name="wt-branch-mode" value="new" /> New branch</label>'
850
+ + '</div>'
851
+ + '<input type="text" id="wt-dialog-branch" placeholder="Branch name" style="width:100%;margin-bottom:8px" />'
852
+ + (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" />');
853
+
854
+ const actions = document.createElement('div');
855
+ actions.className = 'dialog-actions';
856
+ const cancelBtn = document.createElement('button');
857
+ cancelBtn.textContent = 'Cancel';
858
+ cancelBtn.className = 'secondary';
859
+ cancelBtn.addEventListener('click', () => overlay.remove());
860
+ const confirmBtn = document.createElement('button');
861
+ confirmBtn.textContent = 'Create';
862
+ confirmBtn.className = 'btn-primary';
863
+ confirmBtn.addEventListener('click', () => {
864
+ const path = dialog.querySelector('#wt-dialog-path').value.trim();
865
+ if (!path) { showToast('Path is required', 'error'); return; }
866
+ const branch = dialog.querySelector('#wt-dialog-branch').value.trim();
867
+ const mode = dialog.querySelector('input[name="wt-branch-mode"]:checked').value;
868
+ const sp = dialog.querySelector('#wt-dialog-start');
869
+ const startPt = sp ? sp.value.trim() : '';
870
+ const msg = { command: 'addWorktree', path };
871
+ if (mode === 'new' && branch) { msg.newBranch = branch; }
872
+ else if (branch) { msg.branch = branch; }
873
+ if (startPt) msg.startPoint = startPt;
874
+ vscode.postMessage(msg);
875
+ overlay.remove();
876
+ });
877
+ actions.appendChild(cancelBtn);
878
+ actions.appendChild(confirmBtn);
879
+ dialog.appendChild(actions);
880
+ overlay.appendChild(dialog);
881
+ document.body.appendChild(overlay);
882
+ setTimeout(() => dialog.querySelector('#wt-dialog-path').focus(), 50);
883
+ overlay.addEventListener('keydown', (e) => {
884
+ if (e.key === 'Escape') overlay.remove();
885
+ if (e.key === 'Enter') confirmBtn.click();
886
+ });
887
+ }
888
+
889
+ // --- Merge/rebase banner ---
890
+ function renderMergeBanner() {
891
+ let banner = document.getElementById('merge-banner');
892
+ if (!state.mergeState) {
893
+ if (banner) banner.remove();
894
+ return;
895
+ }
896
+ const ms = state.mergeState;
897
+ const typeLabel = ms.type === 'cherry-pick' ? 'Cherry-pick' : ms.type.charAt(0).toUpperCase() + ms.type.slice(1);
898
+ const progressText = ms.progress ? ' (' + ms.progress + ')' : '';
899
+ const msgText = ms.message ? ' — ' + escHtml(ms.message) : '';
900
+
901
+ let buttonsHtml = '';
902
+ if (ms.type === 'rebase') {
903
+ buttonsHtml = '<button class="btn-sm btn-continue" data-merge-action="rebaseContinue">Continue</button>'
904
+ + '<button class="btn-sm" data-merge-action="rebaseSkip">Skip</button>'
905
+ + '<button class="btn-sm btn-abort" data-merge-action="rebaseAbort">Abort</button>';
906
+ } else if (ms.type === 'merge') {
907
+ buttonsHtml = '<button class="btn-sm btn-abort" data-merge-action="mergeAbort">Abort</button>';
908
+ } else if (ms.type === 'cherry-pick') {
909
+ buttonsHtml = '<button class="btn-sm btn-continue" data-merge-action="cherryPickContinue">Continue</button>'
910
+ + '<button class="btn-sm btn-abort" data-merge-action="cherryPickAbort">Abort</button>';
911
+ }
912
+
913
+ if (!banner) {
914
+ banner = document.createElement('div');
915
+ banner.id = 'merge-banner';
916
+ banner.className = 'merge-banner';
917
+ const toolbar = document.getElementById('toolbar');
918
+ toolbar.parentNode.insertBefore(banner, toolbar.nextSibling);
919
+ }
920
+ banner.innerHTML = '<span class="banner-icon">⚠</span>'
921
+ + '<span class="banner-text"><strong>' + typeLabel + ' in progress' + progressText + '</strong>' + msgText + '</span>'
922
+ + '<div class="banner-actions">' + buttonsHtml + '</div>';
923
+
924
+ banner.querySelectorAll('[data-merge-action]').forEach(btn => {
925
+ btn.addEventListener('click', () => {
926
+ const action = btn.dataset.mergeAction;
927
+ if (action.includes('Abort')) {
928
+ showDialog({
929
+ title: 'Abort ' + typeLabel,
930
+ message: 'Abort the current ' + ms.type + '? Any resolved conflicts will be lost.',
931
+ destructive: true,
932
+ confirmLabel: 'Abort',
933
+ onConfirm: () => gitAction(action, {}),
934
+ });
935
+ } else {
936
+ gitAction(action, {});
937
+ }
938
+ });
939
+ });
940
+ }
941
+
942
+ // --- Stash popover ---
943
+ const btnStash = document.getElementById('btn-stash');
944
+ const stashPopover = document.getElementById('stash-popover');
945
+
946
+ btnStash.addEventListener('click', (e) => {
947
+ e.stopPropagation();
948
+ const wasHidden = stashPopover.classList.contains('hidden');
949
+ stashPopover.classList.toggle('hidden');
950
+ if (wasHidden) vscode.postMessage({ command: 'requestStashes' });
951
+ });
952
+
953
+ document.addEventListener('click', (e) => {
954
+ if (!e.target.closest('.stash-dropdown')) stashPopover.classList.add('hidden');
955
+ });
956
+
957
+ function renderStashList() {
958
+ const listEl = document.getElementById('stash-list');
959
+ const countEl = btnStash.querySelector('.stash-count');
960
+ const stashes = state.stashes;
961
+ if (countEl) {
962
+ countEl.textContent = stashes.length;
963
+ countEl.style.display = stashes.length > 0 ? '' : 'none';
964
+ }
965
+ if (!stashes.length) {
966
+ listEl.innerHTML = '<div class="stash-empty">No stashes</div>';
967
+ return;
968
+ }
969
+ listEl.innerHTML = stashes.map((s, i) => {
970
+ const ref = 'stash@{' + s.index + '}';
971
+ return '<div class="stash-item">'
972
+ + '<div class="stash-item-info">'
973
+ + '<div class="stash-item-ref">' + escHtml(ref) + '</div>'
974
+ + '<div class="stash-item-msg" title="' + escHtml(s.message) + '">' + escHtml(s.message) + '</div>'
975
+ + '</div>'
976
+ + '<div class="stash-item-actions">'
977
+ + '<button class="stash-apply btn-sm" data-idx="' + i + '" title="Apply (keep stash)">Apply</button>'
978
+ + '<button class="stash-pop btn-sm" data-idx="' + i + '" title="Pop (apply & remove)">Pop</button>'
979
+ + '<button class="stash-drop btn-sm" data-idx="' + i + '" title="Drop (delete)">Drop</button>'
980
+ + '</div>'
981
+ + '</div>';
982
+ }).join('');
983
+
984
+ listEl.querySelectorAll('.stash-apply').forEach(btn => {
985
+ btn.addEventListener('click', (e) => {
986
+ e.stopPropagation();
987
+ const s = state.stashes[parseInt(btn.dataset.idx)];
988
+ if (s) gitAction('stashApply', { stashRef: 'stash@{' + s.index + '}' });
989
+ });
990
+ });
991
+ listEl.querySelectorAll('.stash-pop').forEach(btn => {
992
+ btn.addEventListener('click', (e) => {
993
+ e.stopPropagation();
994
+ const s = state.stashes[parseInt(btn.dataset.idx)];
995
+ if (s) gitAction('stashPop', { stashRef: 'stash@{' + s.index + '}' });
996
+ });
997
+ });
998
+ listEl.querySelectorAll('.stash-drop').forEach(btn => {
999
+ btn.addEventListener('click', (e) => {
1000
+ e.stopPropagation();
1001
+ const s = state.stashes[parseInt(btn.dataset.idx)];
1002
+ if (!s) return;
1003
+ showDialog({
1004
+ title: 'Drop Stash',
1005
+ message: 'Delete stash@{' + s.index + '}? This cannot be undone.',
1006
+ destructive: true,
1007
+ confirmLabel: 'Drop',
1008
+ onConfirm: () => gitAction('stashDrop', { stashRef: 'stash@{' + s.index + '}' }),
1009
+ });
1010
+ });
1011
+ });
1012
+ }
1013
+
1014
+ document.getElementById('stash-save').addEventListener('click', () => {
1015
+ showDialog({
1016
+ title: 'Stash Changes',
1017
+ input: { placeholder: 'Stash message (optional)' },
1018
+ confirmLabel: 'Stash',
1019
+ onConfirm: (msg) => gitAction('stashSave', msg ? { message: msg } : {}),
1020
+ });
1021
+ });
1022
+
1023
+ // --- Graph column resize ---
1024
+ {
1025
+ const resizeHandle = document.getElementById('graph-resize-handle');
1026
+ let resizing = false, startX = 0, startW = 0;
1027
+ resizeHandle.addEventListener('pointerdown', (e) => {
1028
+ e.preventDefault();
1029
+ resizing = true;
1030
+ startX = e.clientX;
1031
+ startW = document.querySelector('.col-graph').offsetWidth;
1032
+ resizeHandle.classList.add('dragging');
1033
+ resizeHandle.setPointerCapture(e.pointerId);
1034
+ });
1035
+ resizeHandle.addEventListener('pointermove', (e) => {
1036
+ if (!resizing) return;
1037
+ const newW = Math.max(40, Math.min(400, startW + e.clientX - startX));
1038
+ document.documentElement.style.setProperty('--graph-col-w', newW + 'px');
1039
+ });
1040
+ resizeHandle.addEventListener('pointerup', (e) => {
1041
+ if (!resizing) return;
1042
+ resizing = false;
1043
+ resizeHandle.classList.remove('dragging');
1044
+ const newW = Math.max(40, Math.min(400, startW + e.clientX - startX));
1045
+ state.graphColWidth = newW;
1046
+ document.documentElement.style.setProperty('--graph-col-w', newW + 'px');
1047
+ });
1048
+ resizeHandle.addEventListener('dblclick', () => {
1049
+ state.graphColWidth = null;
1050
+ graphRender(-1);
1051
+ });
1052
+ }
1053
+
1054
+ // --- Graph rendering (faithful port of vscode-git-graph graph.ts) ---
1055
+
1056
+ class GBranch {
1057
+ constructor(colour) {
1058
+ this._colour = colour;
1059
+ this._end = 0;
1060
+ this._lines = [];
1061
+ this._numUncommitted = 0;
1062
+ }
1063
+ addLine(p1, p2, isCommitted, lockedFirst) {
1064
+ this._lines.push({ p1, p2, lockedFirst });
1065
+ if (isCommitted) {
1066
+ if (p2.x === 0 && p2.y < this._numUncommitted) this._numUncommitted = p2.y;
1067
+ } else {
1068
+ this._numUncommitted++;
1069
+ }
1070
+ }
1071
+ getColour() { return this._colour; }
1072
+ getEnd() { return this._end; }
1073
+ setEnd(end) { this._end = end; }
1074
+
1075
+ draw(svg, config, expandAt) {
1076
+ const colour = config.colours[this._colour % config.colours.length];
1077
+ const d = config.grid.y * (config.style === 'angular' ? 0.38 : 0.8);
1078
+ const pxLines = [];
1079
+ let curPath = '';
1080
+
1081
+ for (let i = 0; i < this._lines.length; i++) {
1082
+ const ln = this._lines[i];
1083
+ let x1 = ln.p1.x * config.grid.x + config.grid.offsetX;
1084
+ let y1 = ln.p1.y * config.grid.y + config.grid.offsetY;
1085
+ let x2 = ln.p2.x * config.grid.x + config.grid.offsetX;
1086
+ let y2 = ln.p2.y * config.grid.y + config.grid.offsetY;
1087
+
1088
+ if (expandAt > -1) {
1089
+ if (ln.p1.y > expandAt) {
1090
+ y1 += config.grid.expandY; y2 += config.grid.expandY;
1091
+ } else if (ln.p2.y > expandAt) {
1092
+ if (x1 === x2) {
1093
+ y2 += config.grid.expandY;
1094
+ } else if (ln.lockedFirst) {
1095
+ pxLines.push({ p1: { x: x1, y: y1 }, p2: { x: x2, y: y2 }, isC: i >= this._numUncommitted, lf: ln.lockedFirst });
1096
+ 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 });
1097
+ continue;
1098
+ } else {
1099
+ 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 });
1100
+ y1 += config.grid.expandY; y2 += config.grid.expandY;
1101
+ }
1102
+ }
1103
+ }
1104
+ pxLines.push({ p1: { x: x1, y: y1 }, p2: { x: x2, y: y2 }, isC: i >= this._numUncommitted, lf: ln.lockedFirst });
1105
+ }
1106
+
1107
+ // Simplify consecutive vertical segments
1108
+ let si = 0;
1109
+ while (si < pxLines.length - 1) {
1110
+ const a = pxLines[si], b = pxLines[si + 1];
1111
+ 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) {
1112
+ a.p2.y = b.p2.y;
1113
+ pxLines.splice(si + 1, 1);
1114
+ } else { si++; }
1115
+ }
1116
+
1117
+ // Build SVG paths
1118
+ for (let i = 0; i < pxLines.length; i++) {
1119
+ const pl = pxLines[i];
1120
+ const x1 = pl.p1.x, y1 = pl.p1.y, x2 = pl.p2.x, y2 = pl.p2.y;
1121
+
1122
+ if (curPath !== '' && i > 0 && pl.isC !== pxLines[i - 1].isC) {
1123
+ GBranch._drawPath(svg, curPath, pxLines[i - 1].isC, colour);
1124
+ curPath = '';
1125
+ }
1126
+ if (curPath === '' || (i > 0 && (x1 !== pxLines[i - 1].p2.x || y1 !== pxLines[i - 1].p2.y))) {
1127
+ curPath += 'M' + x1.toFixed(0) + ',' + y1.toFixed(1);
1128
+ }
1129
+ if (x1 === x2) {
1130
+ curPath += 'L' + x2.toFixed(0) + ',' + y2.toFixed(1);
1131
+ } else if (config.style === 'angular') {
1132
+ 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);
1133
+ } else {
1134
+ curPath += 'C' + x1.toFixed(0) + ',' + (y1 + d).toFixed(1) + ' ' + x2.toFixed(0) + ',' + (y2 - d).toFixed(1) + ' ' + x2.toFixed(0) + ',' + y2.toFixed(1);
1135
+ }
1136
+ }
1137
+ if (curPath !== '') GBranch._drawPath(svg, curPath, pxLines[pxLines.length - 1].isC, colour);
1138
+ }
1139
+
1140
+ static _drawPath(svg, path, isCommitted, colour) {
1141
+ const line = document.createElementNS(SVG_NS, 'path');
1142
+ line.setAttribute('class', 'line');
1143
+ line.setAttribute('d', path);
1144
+ line.setAttribute('stroke', isCommitted ? colour : '#808080');
1145
+ if (!isCommitted) line.setAttribute('stroke-dasharray', '2');
1146
+ svg.appendChild(line);
1147
+ }
1148
+ }
1149
+
1150
+ class GVertex {
1151
+ constructor(id, isStash) {
1152
+ this.id = id;
1153
+ this.isStash = isStash;
1154
+ this._x = 0;
1155
+ this._children = [];
1156
+ this._parents = [];
1157
+ this._nextParent = 0;
1158
+ this._onBranch = null;
1159
+ this._isCommitted = true;
1160
+ this._isCurrent = false;
1161
+ this._nextX = 0;
1162
+ this._connections = [];
1163
+ }
1164
+ addChild(v) { this._children.push(v); }
1165
+ getChildren() { return this._children; }
1166
+ addParent(v) { this._parents.push(v); }
1167
+ getParents() { return this._parents; }
1168
+ hasParents() { return this._parents.length > 0; }
1169
+ getNextParent() { return this._nextParent < this._parents.length ? this._parents[this._nextParent] : null; }
1170
+ registerParentProcessed() { this._nextParent++; }
1171
+ isMerge() { return this._parents.length > 1; }
1172
+
1173
+ addToBranch(branch, x) { if (this._onBranch === null) { this._onBranch = branch; this._x = x; } }
1174
+ isNotOnBranch() { return this._onBranch === null; }
1175
+ isOnThisBranch(branch) { return this._onBranch === branch; }
1176
+ getBranch() { return this._onBranch; }
1177
+
1178
+ getPoint() { return { x: this._x, y: this.id }; }
1179
+ getNextPoint() { return { x: this._nextX, y: this.id }; }
1180
+
1181
+ getPointConnectingTo(vertex, onBranch) {
1182
+ for (let i = 0; i < this._connections.length; i++) {
1183
+ if (this._connections[i].connectsTo === vertex && this._connections[i].onBranch === onBranch) return { x: i, y: this.id };
1184
+ }
1185
+ return null;
1186
+ }
1187
+ registerUnavailablePoint(x, connectsTo, onBranch) {
1188
+ if (x === this._nextX) { this._nextX = x + 1; this._connections[x] = { connectsTo, onBranch }; }
1189
+ }
1190
+
1191
+ getColour() { return this._onBranch !== null ? this._onBranch.getColour() : 0; }
1192
+ getIsCommitted() { return this._isCommitted; }
1193
+ setNotCommitted() { this._isCommitted = false; }
1194
+ setCurrent() { this._isCurrent = true; }
1195
+
1196
+ draw(svg, config, expandOffset, overListener, outListener) {
1197
+ if (this._onBranch === null) return;
1198
+ const colour = this._isCommitted ? config.colours[this._onBranch.getColour() % config.colours.length] : '#808080';
1199
+ const cx = (this._x * config.grid.x + config.grid.offsetX).toString();
1200
+ const cy = (this.id * config.grid.y + config.grid.offsetY + (expandOffset ? config.grid.expandY : 0)).toString();
1201
+
1202
+ const circle = document.createElementNS(SVG_NS, 'circle');
1203
+ circle.dataset.id = this.id.toString();
1204
+ circle.setAttribute('cx', cx);
1205
+ circle.setAttribute('cy', cy);
1206
+ circle.setAttribute('r', '4');
1207
+ if (this._isCurrent) {
1208
+ circle.setAttribute('class', 'graphCurrent');
1209
+ circle.setAttribute('stroke', colour);
1210
+ } else {
1211
+ circle.setAttribute('fill', colour);
1212
+ }
1213
+ svg.appendChild(circle);
1214
+
1215
+ if (this.isStash && !this._isCurrent) {
1216
+ circle.setAttribute('r', '4.5');
1217
+ circle.setAttribute('class', 'graphStashOuter');
1218
+ const inner = document.createElementNS(SVG_NS, 'circle');
1219
+ inner.setAttribute('cx', cx);
1220
+ inner.setAttribute('cy', cy);
1221
+ inner.setAttribute('r', '2');
1222
+ inner.setAttribute('class', 'graphStashInner');
1223
+ svg.appendChild(inner);
1224
+ }
1225
+
1226
+ circle.addEventListener('mouseover', overListener);
1227
+ circle.addEventListener('mouseout', outListener);
1228
+ }
1229
+ }
1230
+
1231
+ // --- Graph layout state ---
1232
+ let gVertices = [], gBranches = [], gAvailColours = [], gCommitLookup = {};
1233
+
1234
+ function graphLoadCommits(commits) {
1235
+ gVertices = []; gBranches = []; gAvailColours = [];
1236
+ if (commits.length === 0) return;
1237
+
1238
+ const stashHashes = new Set(state.stashes.map(s => s.hash));
1239
+ const nullVertex = new GVertex(NULL_VERTEX_ID, false);
1240
+ const lookup = {};
1241
+ for (let i = 0; i < commits.length; i++) {
1242
+ lookup[commits[i].hash] = i;
1243
+ gVertices.push(new GVertex(i, stashHashes.has(commits[i].hash)));
1244
+ }
1245
+ gCommitLookup = lookup;
1246
+
1247
+ for (let i = 0; i < commits.length; i++) {
1248
+ for (let j = 0; j < commits[i].parents.length; j++) {
1249
+ const ph = commits[i].parents[j];
1250
+ if (typeof lookup[ph] === 'number') {
1251
+ gVertices[i].addParent(gVertices[lookup[ph]]);
1252
+ gVertices[lookup[ph]].addChild(gVertices[i]);
1253
+ } else {
1254
+ gVertices[i].addParent(nullVertex);
1255
+ }
1256
+ }
1257
+ }
1258
+
1259
+ if (state.head && typeof lookup[state.head] === 'number') {
1260
+ gVertices[lookup[state.head]].setCurrent();
1261
+ }
1262
+
1263
+ let i = 0;
1264
+ while (i < gVertices.length) {
1265
+ if (gVertices[i].getNextParent() !== null || gVertices[i].isNotOnBranch()) {
1266
+ graphDeterminePath(i);
1267
+ } else { i++; }
1268
+ }
1269
+ }
1270
+
1271
+ function graphDeterminePath(startAt) {
1272
+ let i = startAt;
1273
+ let vertex = gVertices[i], parentVertex = gVertices[i].getNextParent(), curVertex;
1274
+ let lastPoint = vertex.isNotOnBranch() ? vertex.getNextPoint() : vertex.getPoint(), curPoint;
1275
+
1276
+ if (parentVertex !== null && parentVertex.id !== NULL_VERTEX_ID && vertex.isMerge() && !vertex.isNotOnBranch() && !parentVertex.isNotOnBranch()) {
1277
+ let foundPtp = false, pBranch = parentVertex.getBranch();
1278
+ for (i = startAt + 1; i < gVertices.length; i++) {
1279
+ curVertex = gVertices[i];
1280
+ curPoint = curVertex.getPointConnectingTo(parentVertex, pBranch);
1281
+ if (curPoint !== null) { foundPtp = true; } else { curPoint = curVertex.getNextPoint(); }
1282
+ pBranch.addLine(lastPoint, curPoint, vertex.getIsCommitted(), !foundPtp && curVertex !== parentVertex ? lastPoint.x < curPoint.x : true);
1283
+ curVertex.registerUnavailablePoint(curPoint.x, parentVertex, pBranch);
1284
+ lastPoint = curPoint;
1285
+ if (foundPtp) { vertex.registerParentProcessed(); break; }
1286
+ }
1287
+ } else {
1288
+ const branch = new GBranch(graphGetAvailableColour(startAt));
1289
+ vertex.addToBranch(branch, lastPoint.x);
1290
+ vertex.registerUnavailablePoint(lastPoint.x, vertex, branch);
1291
+ for (i = startAt + 1; i < gVertices.length; i++) {
1292
+ curVertex = gVertices[i];
1293
+ curPoint = parentVertex === curVertex && !parentVertex.isNotOnBranch() ? curVertex.getPoint() : curVertex.getNextPoint();
1294
+ branch.addLine(lastPoint, curPoint, vertex.getIsCommitted(), lastPoint.x < curPoint.x);
1295
+ curVertex.registerUnavailablePoint(curPoint.x, parentVertex, branch);
1296
+ lastPoint = curPoint;
1297
+ if (parentVertex === curVertex) {
1298
+ vertex.registerParentProcessed();
1299
+ const onBranch = !parentVertex.isNotOnBranch();
1300
+ parentVertex.addToBranch(branch, curPoint.x);
1301
+ vertex = parentVertex;
1302
+ parentVertex = vertex.getNextParent();
1303
+ if (parentVertex === null || onBranch) break;
1304
+ }
1305
+ }
1306
+ if (i === gVertices.length && parentVertex !== null && parentVertex.id === NULL_VERTEX_ID) {
1307
+ vertex.registerParentProcessed();
1308
+ }
1309
+ branch.setEnd(i);
1310
+ gBranches.push(branch);
1311
+ gAvailColours[branch.getColour()] = i;
1312
+ }
1313
+ }
1314
+
1315
+ function graphGetAvailableColour(startAt) {
1316
+ for (let i = 0; i < gAvailColours.length; i++) {
1317
+ if (startAt > gAvailColours[i]) return i;
1318
+ }
1319
+ gAvailColours.push(0);
1320
+ return gAvailColours.length - 1;
1321
+ }
1322
+
1323
+ function graphRender(expandIdx) {
1324
+ const container = document.getElementById('graph-svg-container');
1325
+ container.innerHTML = '';
1326
+ if (gVertices.length === 0) { if (state.graphColWidth === null) document.documentElement.style.setProperty('--graph-col-w', '40px'); return; }
1327
+
1328
+ // Detect mobile: match CSS breakpoint where row height changes to 44px
1329
+ const isMobile = window.matchMedia('(max-width: 768px)').matches;
1330
+ const cfg = isMobile
1331
+ ? { ...graphConfig, grid: { ...graphConfig.grid, y: 44, offsetY: 22 } }
1332
+ : graphConfig;
1333
+
1334
+ const svg = document.createElementNS(SVG_NS, 'svg');
1335
+ const group = document.createElementNS(SVG_NS, 'g');
1336
+
1337
+ for (let i = 0; i < gBranches.length; i++) gBranches[i].draw(group, cfg, expandIdx);
1338
+
1339
+ const overL = (e) => graphVertexOver(e), outL = (e) => graphVertexOut(e);
1340
+ for (let i = 0; i < gVertices.length; i++) {
1341
+ gVertices[i].draw(group, cfg, expandIdx > -1 && i > expandIdx, overL, outL);
1342
+ }
1343
+
1344
+ svg.appendChild(group);
1345
+
1346
+ let maxX = 0;
1347
+ for (let i = 0; i < gVertices.length; i++) {
1348
+ const p = gVertices[i].getNextPoint();
1349
+ if (p.x > maxX) maxX = p.x;
1350
+ }
1351
+ const w = 2 * cfg.grid.offsetX + Math.max(maxX - 1, 0) * cfg.grid.x;
1352
+ const h = gVertices.length * cfg.grid.y + cfg.grid.offsetY - cfg.grid.y / 2 + (expandIdx > -1 ? cfg.grid.expandY : 0);
1353
+
1354
+ const gw = Math.max(w, 40);
1355
+ svg.setAttribute('width', gw.toString());
1356
+ svg.setAttribute('height', h.toString());
1357
+ container.appendChild(svg);
1358
+ if (state.graphColWidth === null) document.documentElement.style.setProperty('--graph-col-w', gw + 'px');
1359
+ }
1360
+
1361
+ function graphVertexOver(e) {
1362
+ if (!e.target || !e.target.dataset || !e.target.dataset.id) return;
1363
+ const id = parseInt(e.target.dataset.id);
1364
+ if (id >= 0 && id < state.commits.length) {
1365
+ const rows = document.querySelectorAll('.commit-row:not(.header-row)');
1366
+ if (rows[id]) rows[id].classList.add('graph-hover');
1367
+ e.target.setAttribute('r', e.target.classList.contains('graphStashOuter') ? '5.5' : '5');
1368
+ }
1369
+ }
1370
+ function graphVertexOut(e) {
1371
+ if (!e.target || !e.target.dataset || !e.target.dataset.id) return;
1372
+ const id = parseInt(e.target.dataset.id);
1373
+ if (id >= 0) {
1374
+ const rows = document.querySelectorAll('.commit-row:not(.header-row)');
1375
+ if (rows[id]) rows[id].classList.remove('graph-hover');
1376
+ e.target.setAttribute('r', e.target.classList.contains('graphStashOuter') ? '4.5' : '4');
1377
+ }
1378
+ }
1379
+
1380
+ // --- Commit list ---
1381
+ function getDisplayCommits() {
1382
+ const u = state.uncommitted;
1383
+ const totalFiles = u ? (u.staged.length + u.unstaged.length + (u.conflicted ? u.conflicted.length : 0)) : 0;
1384
+ if (!u || totalFiles === 0) return state.commits;
1385
+ const virtualCommit = {
1386
+ hash: 'uncommitted',
1387
+ parents: state.head ? [state.head] : [],
1388
+ author: '',
1389
+ authorEmail: '',
1390
+ authorDate: Math.floor(Date.now() / 1000),
1391
+ committer: '',
1392
+ committerEmail: '',
1393
+ commitDate: Math.floor(Date.now() / 1000),
1394
+ refs: [],
1395
+ message: 'Uncommitted Changes (' + totalFiles + ' files)',
1396
+ };
1397
+ return [virtualCommit, ...state.commits];
1398
+ }
1399
+
1400
+ function renderCommitList() {
1401
+ const container = document.getElementById('commit-list');
1402
+ container.innerHTML = '';
1403
+
1404
+ const displayCommits = getDisplayCommits();
1405
+ graphLoadCommits(displayCommits);
1406
+
1407
+ // Mark uncommitted vertex for dashed lines
1408
+ if (displayCommits.length > 0 && displayCommits[0].hash === 'uncommitted') {
1409
+ if (gVertices.length > 0) gVertices[0].setNotCommitted();
1410
+ }
1411
+
1412
+ displayCommits.forEach((commit, idx) => {
1413
+ const isVirtual = commit.hash === 'uncommitted';
1414
+ const row = document.createElement('div');
1415
+ row.className = 'commit-row' + (isVirtual ? ' virtual' : '');
1416
+ row.dataset.hash = commit.hash;
1417
+
1418
+ // Graph spacer column (SVG overlays this area)
1419
+ const graphCol = document.createElement('div');
1420
+ graphCol.className = 'col-graph';
1421
+
1422
+ // Message column with ref badges
1423
+ const msgCol = document.createElement('div');
1424
+ msgCol.className = 'col-message';
1425
+ let badges = '';
1426
+ if (commit.refs) {
1427
+ commit.refs.forEach(ref => {
1428
+ if (ref.type === 'tag' && !state.settings.showTags) return;
1429
+ if (ref.type === 'remote' && !state.settings.showRemoteBranches) return;
1430
+ badges += '<span class="ref-badge ref-' + ref.type + '">' + escHtml(ref.name) + '</span>';
1431
+ });
1432
+ }
1433
+ msgCol.innerHTML = badges + formatCommitMessage(commit.message);
1434
+
1435
+ // Attach context menu and double-click to ref badges
1436
+ msgCol.querySelectorAll('.ref-badge').forEach(badge => {
1437
+ const refName = badge.textContent;
1438
+ const refType = badge.className.includes('ref-head') ? 'head'
1439
+ : badge.className.includes('ref-remote') ? 'remote'
1440
+ : badge.className.includes('ref-tag') ? 'tag' : 'local';
1441
+ badge.style.cursor = 'pointer';
1442
+ badge.addEventListener('dblclick', (e) => {
1443
+ e.stopPropagation();
1444
+ gitAction('checkout', { target: refName });
1445
+ });
1446
+ badge.addEventListener('contextmenu', (e) => {
1447
+ e.preventDefault();
1448
+ e.stopPropagation();
1449
+ showBranchContextMenu(e.clientX, e.clientY, refName, refType, commit);
1450
+ });
1451
+ });
1452
+
1453
+ const authorCol = document.createElement('div');
1454
+ authorCol.className = 'col-author';
1455
+ authorCol.textContent = isVirtual ? '' : commit.author;
1456
+
1457
+ const dateCol = document.createElement('div');
1458
+ dateCol.className = 'col-date';
1459
+ dateCol.textContent = isVirtual ? 'now' : formatDate(commit.commitDate);
1460
+
1461
+ const hashCol = document.createElement('div');
1462
+ hashCol.className = 'col-hash';
1463
+ hashCol.textContent = isVirtual ? '...' : commit.hash.substring(0, 7);
1464
+
1465
+ row.appendChild(graphCol);
1466
+ row.appendChild(msgCol);
1467
+ row.appendChild(authorCol);
1468
+ row.appendChild(dateCol);
1469
+ row.appendChild(hashCol);
1470
+
1471
+ row.addEventListener('click', () => selectCommit(commit.hash));
1472
+ if (isVirtual) {
1473
+ row.addEventListener('contextmenu', (e) => {
1474
+ e.preventDefault();
1475
+ showUncommittedContextMenu(e.clientX, e.clientY);
1476
+ });
1477
+ setupLongPress(row, (x, y) => showUncommittedContextMenu(x, y));
1478
+ } else {
1479
+ row.addEventListener('contextmenu', (e) => {
1480
+ e.preventDefault();
1481
+ showCommitContextMenu(e.clientX, e.clientY, commit);
1482
+ });
1483
+ setupLongPress(row, (x, y) => showCommitContextMenu(x, y, commit));
1484
+ }
1485
+
1486
+ container.appendChild(row);
1487
+ });
1488
+
1489
+ graphRender(-1);
1490
+ }
1491
+
1492
+ function selectCommit(hash) {
1493
+ // Deselect previous
1494
+ document.querySelectorAll('.commit-row.selected').forEach(el => el.classList.remove('selected'));
1495
+
1496
+ if (state.selectedCommit === hash) {
1497
+ state.selectedCommit = null;
1498
+ state.expandedCommit = null;
1499
+ document.getElementById('detail-panel').classList.add('hidden');
1500
+ return;
1501
+ }
1502
+
1503
+ state.selectedCommit = hash;
1504
+ state.expandedCommit = hash;
1505
+ const row = document.querySelector('[data-hash="' + CSS.escape(hash) + '"]');
1506
+ if (row) row.classList.add('selected');
1507
+
1508
+ if (hash === 'uncommitted') {
1509
+ renderUncommittedDetail();
1510
+ return;
1511
+ }
1512
+ vscode.postMessage({ command: 'requestCommitDetails', hash });
1513
+ }
1514
+
1515
+ // --- File tree helpers ---
1516
+ function buildFileTree(files) {
1517
+ const root = { name: '', children: {}, files: [] };
1518
+ for (const f of files) {
1519
+ const parts = f.path.split('/');
1520
+ let node = root;
1521
+ for (let i = 0; i < parts.length - 1; i++) {
1522
+ if (!node.children[parts[i]]) node.children[parts[i]] = { name: parts[i], children: {}, files: [] };
1523
+ node = node.children[parts[i]];
1524
+ }
1525
+ node.files.push({ ...f, fileName: parts[parts.length - 1] });
1526
+ }
1527
+ return root;
1528
+ }
1529
+
1530
+ function countFiles(node) {
1531
+ let count = node.files.length;
1532
+ for (const child of Object.values(node.children)) count += countFiles(child);
1533
+ return count;
1534
+ }
1535
+
1536
+ function renderFileTree(node, depth, hash, parentHash, section) {
1537
+ let html = '';
1538
+ const dirs = Object.keys(node.children).sort();
1539
+ for (const dir of dirs) {
1540
+ const child = node.children[dir];
1541
+ html += '<div class="tree-dir" style="padding-left:' + (depth * 16) + 'px">';
1542
+ html += ICONS.folderOpen + ' <span class="tree-dir-name">' + escHtml(dir) + '/</span>';
1543
+ html += '<span class="tree-dir-count">(' + countFiles(child) + ')</span></div>';
1544
+ html += renderFileTree(child, depth + 1, hash, parentHash, section);
1545
+ }
1546
+ const sortedFiles = [...node.files].sort((a, b) => a.fileName.localeCompare(b.fileName));
1547
+ for (const f of sortedFiles) {
1548
+ html += '<div class="file-item file-clickable" style="padding-left:' + (depth * 16) + 'px" ';
1549
+ html += 'data-path="' + escHtml(f.path) + '" data-hash="' + escHtml(hash) + '" data-parent="' + escHtml(parentHash) + '">';
1550
+ html += '<span class="file-status file-status-' + escHtml(f.status) + '">' + escHtml(f.status) + '</span>';
1551
+ html += '<span class="file-name">' + escHtml(f.fileName) + '</span>';
1552
+ if (f.additions > 0 || f.deletions > 0) {
1553
+ html += '<span class="file-stat">';
1554
+ if (f.additions > 0) html += '<span class="add">+' + f.additions + '</span> ';
1555
+ if (f.deletions > 0) html += '<span class="del">-' + f.deletions + '</span>';
1556
+ html += '</span>';
1557
+ }
1558
+ if (section) html += renderFileActions(f, section);
1559
+ html += '</div>';
1560
+ }
1561
+ return html;
1562
+ }
1563
+
1564
+ function renderFileListHtml(files, hash, parentHash, section) {
1565
+ if (state.fileViewMode === 'tree') {
1566
+ return renderFileTree(buildFileTree(files), 0, hash, parentHash, section);
1567
+ }
1568
+ return files.map(f =>
1569
+ '<div class="file-item file-clickable" data-path="' + escHtml(f.path) + '" data-hash="' + escHtml(hash) + '" data-parent="' + escHtml(parentHash || '') + '">' +
1570
+ '<span class="file-status file-status-' + escHtml(f.status) + '">' + escHtml(f.status) + '</span>' +
1571
+ '<span class="file-name">' + escHtml(f.path) + '</span>' +
1572
+ '<span class="file-stat">' +
1573
+ (f.additions > 0 ? '<span class="add">+' + f.additions + '</span> ' : '') +
1574
+ (f.deletions > 0 ? '<span class="del">-' + f.deletions + '</span>' : '') +
1575
+ '</span>' +
1576
+ (section ? renderFileActions(f, section) : '') +
1577
+ '</div>'
1578
+ ).join('');
1579
+ }
1580
+
1581
+ function renderFileActions(file, section) {
1582
+ let html = '<span class="file-actions">';
1583
+ if (section === 'unstaged') {
1584
+ html += '<button class="file-action-btn" data-action="stage" data-file="' + escHtml(file.path) + '" title="Stage">' + ICONS.plus + '</button>';
1585
+ html += '<button class="file-action-btn" data-action="discard" data-file="' + escHtml(file.path) + '" title="Discard changes">' + ICONS.x + '</button>';
1586
+ } else if (section === 'staged') {
1587
+ html += '<button class="file-action-btn" data-action="unstage" data-file="' + escHtml(file.path) + '" title="Unstage">' + ICONS.minus + '</button>';
1588
+ }
1589
+ html += '<button class="file-action-btn" data-action="open" data-file="' + escHtml(file.path) + '" title="Open file">' + ICONS.fileOpen + '</button>';
1590
+ html += '</span>';
1591
+ return html;
1592
+ }
1593
+
1594
+ function fileViewToggleHtml() {
1595
+ return '<div class="file-view-toggle">' +
1596
+ '<button class="toggle-btn' + (state.fileViewMode === 'list' ? ' active' : '') + '" data-view="list" title="List view">' + ICONS.list + '</button>' +
1597
+ '<button class="toggle-btn' + (state.fileViewMode === 'tree' ? ' active' : '') + '" data-view="tree" title="Tree view">' + ICONS.tree + '</button>' +
1598
+ '</div>';
1599
+ }
1600
+
1601
+ function renderUncommittedDetail() {
1602
+ const panel = document.getElementById('detail-panel');
1603
+ panel.classList.remove('hidden');
1604
+ const u = state.uncommitted;
1605
+ if (!u) { panel.classList.add('hidden'); return; }
1606
+ let html = '<h3>Uncommitted Changes</h3>';
1607
+ const hasFiles = u.staged.length > 0 || u.unstaged.length > 0 || (u.conflicted && u.conflicted.length > 0);
1608
+ if (hasFiles) {
1609
+ html += fileViewToggleHtml();
1610
+ }
1611
+ // Conflict section (above staged/unstaged)
1612
+ if (u.conflicted && u.conflicted.length > 0) {
1613
+ html += '<div class="file-list"><div class="conflict-header">⚠ Conflicts (' + u.conflicted.length + ')</div>';
1614
+ html += u.conflicted.map(f => {
1615
+ const fileName = f.path.split('/').pop() || f.path;
1616
+ return '<div class="file-item">'
1617
+ + '<span class="file-status file-status-U">U</span>'
1618
+ + '<span class="file-clickable" data-file="' + escHtml(f.path) + '" data-hash="uncommitted" data-parent="' + escHtml(state.head) + '">' + escHtml(f.path) + '</span>'
1619
+ + '<div class="file-actions">'
1620
+ + '<button class="file-action-btn" data-action="open-conflict" data-file="' + escHtml(f.path) + '" title="Open conflict file">' + ICONS.fileOpen + '</button>'
1621
+ + '<button class="file-action-btn" data-action="stage" data-file="' + escHtml(f.path) + '" title="Mark resolved (stage)">' + ICONS.plus + '</button>'
1622
+ + '</div></div>';
1623
+ }).join('');
1624
+ html += '</div>';
1625
+ }
1626
+ if (u.staged.length > 0) {
1627
+ html += '<div class="file-list"><div class="section-actions"><strong>Staged (' + u.staged.length + '):</strong>';
1628
+ html += '<button class="btn-sm section-action-btn" data-action="unstage-all">Unstage All</button></div>';
1629
+ html += renderFileListHtml(u.staged, 'staged', state.head, 'staged');
1630
+ html += '</div>';
1631
+ }
1632
+ if (u.unstaged.length > 0) {
1633
+ html += '<div class="file-list"><div class="section-actions"><strong>Unstaged (' + u.unstaged.length + '):</strong>';
1634
+ html += '<button class="btn-sm section-action-btn" data-action="stage-all">Stage All</button></div>';
1635
+ html += renderFileListHtml(u.unstaged, 'uncommitted', state.head, 'unstaged');
1636
+ html += '</div>';
1637
+ }
1638
+ if (u.staged.length === 0 && u.unstaged.length === 0 && (!u.conflicted || u.conflicted.length === 0)) {
1639
+ html += '<p>No uncommitted changes.</p>';
1640
+ }
1641
+ html += '<div class="commit-section">';
1642
+ html += '<textarea id="commit-message" placeholder="Commit message..." rows="3"></textarea>';
1643
+ html += '<div class="commit-actions"><button id="btn-commit" class="btn-sm btn-commit" disabled>Commit</button></div>';
1644
+ html += '</div>';
1645
+ panel.innerHTML = html;
1646
+ wireCommitControls();
1647
+ }
1648
+
1649
+ function wireCommitControls() {
1650
+ const textarea = document.getElementById('commit-message');
1651
+ const commitBtn = document.getElementById('btn-commit');
1652
+ if (!textarea || !commitBtn) return;
1653
+ const updateBtn = () => {
1654
+ const hasMsg = textarea.value.trim().length > 0;
1655
+ const hasStaged = state.uncommitted && state.uncommitted.staged.length > 0;
1656
+ commitBtn.disabled = !(hasMsg && hasStaged);
1657
+ };
1658
+ textarea.addEventListener('input', updateBtn);
1659
+ updateBtn();
1660
+ commitBtn.addEventListener('click', () => {
1661
+ const message = textarea.value.trim();
1662
+ if (!message) return;
1663
+ vscode.postMessage({ command: 'gitAction', action: 'commit', args: { message } });
1664
+ textarea.value = '';
1665
+ commitBtn.disabled = true;
1666
+ });
1667
+ }
1668
+
1669
+ // --- Detail panel ---
1670
+ function renderDetailPanel(detail) {
1671
+ state._lastDetail = detail;
1672
+ const panel = document.getElementById('detail-panel');
1673
+ panel.classList.remove('hidden');
1674
+
1675
+ let html = '<h3>Commit Details</h3>';
1676
+ html += '<div class="detail-field"><span class="label">Hash:</span> ' + escHtml(detail.hash) + '</div>';
1677
+ html += '<div class="detail-field"><span class="label">Author:</span> ' + escHtml(detail.author) + ' &lt;' + escHtml(detail.authorEmail) + '&gt;</div>';
1678
+ html += '<div class="detail-field"><span class="label">Date:</span> ' + new Date(detail.authorDate * 1000).toLocaleString() + '</div>';
1679
+ if (detail.committer !== detail.author) {
1680
+ html += '<div class="detail-field"><span class="label">Committer:</span> ' + escHtml(detail.committer) + ' &lt;' + escHtml(detail.committerEmail) + '&gt;</div>';
1681
+ }
1682
+ if (detail.parents.length > 0) {
1683
+ html += '<div class="detail-field"><span class="label">Parents:</span> ' + detail.parents.map(p => escHtml(p.substring(0, 7))).join(', ') + '</div>';
1684
+ }
1685
+ html += '<div class="detail-message">' + escHtml(detail.message) + '</div>';
1686
+
1687
+ if (detail.fileChanges && detail.fileChanges.length > 0) {
1688
+ html += '<div class="file-list">' + fileViewToggleHtml() + '<strong>Files changed (' + detail.fileChanges.length + '):</strong>';
1689
+ html += renderFileListHtml(detail.fileChanges, detail.hash, detail.parents[0] || '');
1690
+ html += '</div>';
1691
+ }
1692
+
1693
+ panel.innerHTML = html;
1694
+ }
1695
+
1696
+ // --- Context menu ---
1697
+ function showCommitContextMenu(x, y, commit) {
1698
+ const menu = document.getElementById('context-menu');
1699
+ const items = [
1700
+ { label: 'Copy Commit Hash', action: () => copyText(commit.hash) },
1701
+ { label: 'Copy Short Hash', action: () => copyText(commit.hash.substring(0, 7)) },
1702
+ { separator: true },
1703
+ { label: 'Checkout...', action: () => gitAction('checkout', { target: commit.hash }) },
1704
+ { label: 'Create Branch Here...', action: () => promptAndAction('Branch name:', (name) => gitAction('createBranch', { name, startPoint: commit.hash })) },
1705
+ { label: 'Create Tag Here...', action: () => promptAndAction('Tag name:', (name) => gitAction('createTag', { name, hash: commit.hash })) },
1706
+ { label: 'Create Worktree Here...', action: () => showCreateWorktreeDialog(commit.hash) },
1707
+ { separator: true },
1708
+ { label: 'Rebase current branch onto this...', action: () => {
1709
+ showDialog({
1710
+ title: 'Rebase',
1711
+ message: 'Rebase current branch (' + escHtml(state.currentBranch) + ') onto commit ' + commit.hash.substring(0, 7) + '?',
1712
+ rawMessage: true,
1713
+ confirmLabel: 'Rebase',
1714
+ onConfirm: () => gitAction('rebase', { branch: commit.hash }),
1715
+ });
1716
+ }},
1717
+ ];
1718
+ // Add "Create PR" if PR creation is configured and commit has a branch ref
1719
+ if (state.settings.prCreation && state.settings.prCreation.urlTemplate) {
1720
+ const branchRef = (commit.refs || []).find(r => r.type === 'local' || r.type === 'head');
1721
+ if (branchRef) {
1722
+ items.push({ separator: true });
1723
+ items.push({ label: 'Create Pull Request (' + branchRef.name + ')', action: () => openPrUrl(branchRef.name) });
1724
+ }
1725
+ }
1726
+ items.push(
1727
+ { separator: true },
1728
+ { label: 'Cherry-pick', action: () => gitAction('cherryPick', { hash: commit.hash }) },
1729
+ { label: 'Revert', action: () => gitAction('revert', { hash: commit.hash }) },
1730
+ { separator: true },
1731
+ { label: 'Reset Current Branch to Here...', destructive: true, action: () => promptResetMode(commit.hash) },
1732
+ );
1733
+
1734
+ let html = '';
1735
+ items.forEach((item, idx) => {
1736
+ if (item.separator) {
1737
+ html += '<div class="ctx-separator"></div>';
1738
+ } else {
1739
+ html += '<div class="ctx-item' + (item.destructive ? ' destructive' : '') + '" data-idx="' + idx + '">' + escHtml(item.label) + '</div>';
1740
+ }
1741
+ });
1742
+ menu.innerHTML = html;
1743
+
1744
+ // Position (clamp to viewport)
1745
+ menu.style.left = Math.min(x, window.innerWidth - 200) + 'px';
1746
+ menu.style.top = Math.min(y, window.innerHeight - 300) + 'px';
1747
+ menu.classList.remove('hidden');
1748
+
1749
+ // Bind click handlers
1750
+ menu.querySelectorAll('.ctx-item').forEach(el => {
1751
+ const idx = parseInt(el.dataset.idx);
1752
+ const item = items[idx];
1753
+ if (item && item.action) el.addEventListener('click', () => { hideContextMenu(); item.action(); });
1754
+ });
1755
+
1756
+ // Close on click outside
1757
+ setTimeout(() => document.addEventListener('click', hideContextMenu, { once: true }), 0);
1758
+ }
1759
+
1760
+ function showUncommittedContextMenu(x, y) {
1761
+ const menu = document.getElementById('context-menu');
1762
+ const items = [
1763
+ { label: 'Stash Uncommitted Changes...', action: () => {
1764
+ showDialog({
1765
+ title: 'Stash Changes',
1766
+ input: { placeholder: 'Stash message (optional)' },
1767
+ confirmLabel: 'Stash',
1768
+ onConfirm: (message) => gitAction('stashSave', message ? { message } : {}),
1769
+ });
1770
+ }},
1771
+ { label: 'Reset Uncommitted Changes...', destructive: true, action: () => {
1772
+ showDialog({
1773
+ title: 'Reset Changes',
1774
+ message: 'Reset all uncommitted changes. Staged changes will be unstaged.',
1775
+ select: { options: ['mixed', 'hard'], defaultValue: 'mixed', label: 'Reset mode:' },
1776
+ destructive: true,
1777
+ confirmLabel: 'Reset',
1778
+ onConfirm: (mode) => {
1779
+ if (mode === 'hard') {
1780
+ showDialog({
1781
+ title: 'Confirm Hard Reset',
1782
+ message: 'WARNING: --hard will permanently discard ALL uncommitted changes!',
1783
+ destructive: true,
1784
+ confirmLabel: 'Reset Hard',
1785
+ onConfirm: () => gitAction('reset', { mode: 'hard', hash: 'HEAD' }),
1786
+ });
1787
+ } else {
1788
+ gitAction('reset', { mode, hash: 'HEAD' });
1789
+ }
1790
+ },
1791
+ });
1792
+ }},
1793
+ { label: 'Clean Untracked Files...', destructive: true, action: () => {
1794
+ showDialog({
1795
+ title: 'Clean Untracked Files',
1796
+ message: 'Permanently delete all untracked files and directories. This cannot be undone!',
1797
+ destructive: true,
1798
+ confirmLabel: 'Clean',
1799
+ onConfirm: () => gitAction('clean', {}),
1800
+ });
1801
+ }},
1802
+ { separator: true },
1803
+ { label: 'Open Source Control View', action: () => vscode.postMessage({ command: 'openSourceControl' }) },
1804
+ ];
1805
+
1806
+ let html = '';
1807
+ items.forEach((item, idx) => {
1808
+ if (item.separator) {
1809
+ html += '<div class="ctx-separator"></div>';
1810
+ } else {
1811
+ html += '<div class="ctx-item' + (item.destructive ? ' destructive' : '') + '" data-idx="' + idx + '">' + escHtml(item.label) + '</div>';
1812
+ }
1813
+ });
1814
+ menu.innerHTML = html;
1815
+ menu.style.left = Math.min(x, window.innerWidth - 200) + 'px';
1816
+ menu.style.top = Math.min(y, window.innerHeight - 300) + 'px';
1817
+ menu.classList.remove('hidden');
1818
+ menu.querySelectorAll('.ctx-item').forEach(el => {
1819
+ const idx = parseInt(el.dataset.idx);
1820
+ const item = items[idx];
1821
+ if (item && item.action) el.addEventListener('click', () => { hideContextMenu(); item.action(); });
1822
+ });
1823
+ setTimeout(() => document.addEventListener('click', hideContextMenu, { once: true }), 0);
1824
+ }
1825
+
1826
+ function showBranchContextMenu(x, y, branchName, refType, commit) {
1827
+ const menu = document.getElementById('context-menu');
1828
+ const items = [];
1829
+
1830
+ if (refType === 'head' || refType === 'local') {
1831
+ items.push(
1832
+ { label: 'Checkout "' + branchName + '"', action: () => gitAction('checkout', { target: branchName }) },
1833
+ { label: 'Merge into current branch', action: () => showDialog({
1834
+ title: 'Merge "' + branchName + '"',
1835
+ message: 'Merge "' + branchName + '" into the current branch?',
1836
+ confirmLabel: 'Merge',
1837
+ onConfirm: () => gitAction('merge', { branch: branchName }),
1838
+ })
1839
+ },
1840
+ { label: 'Rebase onto "' + branchName + '"', action: () => showDialog({
1841
+ title: 'Rebase onto "' + branchName + '"',
1842
+ message: 'Rebase current branch onto "' + branchName + '"?',
1843
+ confirmLabel: 'Rebase',
1844
+ onConfirm: () => gitAction('rebase', { branch: branchName }),
1845
+ })
1846
+ },
1847
+ { separator: true },
1848
+ { label: 'Rename branch...', action: () => showDialog({
1849
+ title: 'Rename Branch',
1850
+ input: { placeholder: 'New branch name', defaultValue: branchName },
1851
+ confirmLabel: 'Rename',
1852
+ onConfirm: (newName) => { if (newName && newName !== branchName) gitAction('renameBranch', { oldName: branchName, newName }); },
1853
+ })
1854
+ },
1855
+ );
1856
+ if (refType !== 'head') {
1857
+ items.push(
1858
+ { label: 'Delete branch...', destructive: true, action: () => showDialog({
1859
+ title: 'Delete Branch',
1860
+ message: 'Delete local branch "' + branchName + '"?',
1861
+ destructive: true,
1862
+ confirmLabel: 'Delete',
1863
+ onConfirm: () => gitAction('deleteBranch', { name: branchName, force: false }),
1864
+ })
1865
+ },
1866
+ );
1867
+ }
1868
+ if (state.settings.prCreation && state.settings.prCreation.urlTemplate) {
1869
+ items.push({ separator: true });
1870
+ items.push({ label: 'Create Pull Request', action: () => openPrUrl(branchName) });
1871
+ }
1872
+ } else if (refType === 'remote') {
1873
+ items.push(
1874
+ { label: 'Checkout as local branch', action: () => gitAction('checkout', { target: branchName }) },
1875
+ { separator: true },
1876
+ { label: 'Delete remote branch...', destructive: true, action: () => showDialog({
1877
+ title: 'Delete Remote Branch',
1878
+ message: 'Delete remote branch "' + branchName + '"? This cannot be undone.',
1879
+ destructive: true,
1880
+ confirmLabel: 'Delete',
1881
+ onConfirm: () => {
1882
+ const parts = branchName.split('/');
1883
+ const remote = parts[0];
1884
+ const branch = parts.slice(1).join('/');
1885
+ gitAction('push', { remote, branch, force: false, delete: true });
1886
+ },
1887
+ })
1888
+ },
1889
+ );
1890
+ } else if (refType === 'tag') {
1891
+ items.push(
1892
+ { label: 'Checkout tag "' + branchName + '"', action: () => gitAction('checkout', { target: branchName }) },
1893
+ { separator: true },
1894
+ { label: 'Delete tag...', destructive: true, action: () => showDialog({
1895
+ title: 'Delete Tag',
1896
+ message: 'Delete tag "' + branchName + '"?',
1897
+ destructive: true,
1898
+ confirmLabel: 'Delete',
1899
+ onConfirm: () => gitAction('deleteTag', { name: branchName }),
1900
+ })
1901
+ },
1902
+ );
1903
+ }
1904
+
1905
+ let html = '';
1906
+ items.forEach((item, idx) => {
1907
+ if (item.separator) {
1908
+ html += '<div class="ctx-separator"></div>';
1909
+ } else {
1910
+ html += '<div class="ctx-item' + (item.destructive ? ' destructive' : '') + '" data-idx="' + idx + '">' + escHtml(item.label) + '</div>';
1911
+ }
1912
+ });
1913
+ menu.innerHTML = html;
1914
+ menu.style.left = Math.min(x, window.innerWidth - 220) + 'px';
1915
+ menu.style.top = Math.min(y, window.innerHeight - 300) + 'px';
1916
+ menu.classList.remove('hidden');
1917
+ menu.querySelectorAll('.ctx-item').forEach(el => {
1918
+ const idx = parseInt(el.dataset.idx);
1919
+ const item = items[idx];
1920
+ if (item && item.action) el.addEventListener('click', () => { hideContextMenu(); item.action(); });
1921
+ });
1922
+ setTimeout(() => document.addEventListener('click', hideContextMenu, { once: true }), 0);
1923
+ }
1924
+
1925
+ function hideContextMenu() {
1926
+ document.getElementById('context-menu').classList.add('hidden');
1927
+ }
1928
+
1929
+ function gitAction(action, args) {
1930
+ vscode.postMessage({ command: 'gitAction', action, args });
1931
+ }
1932
+
1933
+ function promptAndAction(title, callback) {
1934
+ showDialog({ title, input: { placeholder: title }, onConfirm: (val) => { if (val) callback(val); } });
1935
+ }
1936
+
1937
+ function promptResetMode(hash) {
1938
+ showDialog({
1939
+ title: 'Reset Current Branch',
1940
+ select: { options: ['soft', 'mixed', 'hard'], defaultValue: 'mixed', label: 'Reset mode:' },
1941
+ onConfirm: (mode) => {
1942
+ if (mode === 'hard') {
1943
+ showDialog({
1944
+ title: 'Confirm Hard Reset',
1945
+ message: 'WARNING: --hard will discard ALL uncommitted changes. This cannot be undone!',
1946
+ destructive: true,
1947
+ confirmLabel: 'Reset Hard',
1948
+ onConfirm: () => gitAction('reset', { mode, hash }),
1949
+ });
1950
+ } else {
1951
+ gitAction('reset', { mode, hash });
1952
+ }
1953
+ },
1954
+ });
1955
+ }
1956
+
1957
+ function copyText(text) {
1958
+ navigator.clipboard.writeText(text).catch(() => {});
1959
+ }
1960
+
1961
+ // --- Dialog system ---
1962
+ function showDialog(opts) {
1963
+ const overlay = document.createElement('div');
1964
+ overlay.className = 'dialog-overlay';
1965
+ const dialog = document.createElement('div');
1966
+ dialog.className = 'dialog';
1967
+ dialog.innerHTML = '<h3>' + escHtml(opts.title || 'Dialog') + '</h3>';
1968
+ if (opts.message) {
1969
+ const msgHtml = opts.rawMessage ? opts.message : escHtml(opts.message);
1970
+ dialog.innerHTML += '<p' + (opts.destructive ? ' class="warning"' : '') + '>' + msgHtml + '</p>';
1971
+ }
1972
+
1973
+ let inputEl = null;
1974
+ if (opts.input) {
1975
+ inputEl = document.createElement('input');
1976
+ inputEl.type = 'text';
1977
+ inputEl.placeholder = opts.input.placeholder || '';
1978
+ if (opts.input.defaultValue) inputEl.value = opts.input.defaultValue;
1979
+ dialog.appendChild(inputEl);
1980
+ }
1981
+ if (opts.select) {
1982
+ if (opts.select.label) dialog.innerHTML += '<p>' + escHtml(opts.select.label) + '</p>';
1983
+ inputEl = document.createElement('select');
1984
+ opts.select.options.forEach(o => {
1985
+ const opt = document.createElement('option');
1986
+ opt.value = o; opt.textContent = o;
1987
+ if (o === opts.select.defaultValue) opt.selected = true;
1988
+ inputEl.appendChild(opt);
1989
+ });
1990
+ dialog.appendChild(inputEl);
1991
+ }
1992
+
1993
+ const actions = document.createElement('div');
1994
+ actions.className = 'dialog-actions';
1995
+ const cancelBtn = document.createElement('button');
1996
+ cancelBtn.textContent = opts.cancelLabel || 'Cancel';
1997
+ cancelBtn.className = 'secondary';
1998
+ cancelBtn.addEventListener('click', () => overlay.remove());
1999
+ const confirmBtn = document.createElement('button');
2000
+ confirmBtn.textContent = opts.confirmLabel || 'OK';
2001
+ confirmBtn.className = opts.destructive ? 'btn-danger' : 'btn-primary';
2002
+ confirmBtn.addEventListener('click', () => {
2003
+ overlay.remove();
2004
+ if (opts.onConfirm) opts.onConfirm(inputEl ? inputEl.value : undefined);
2005
+ });
2006
+ actions.appendChild(cancelBtn);
2007
+ actions.appendChild(confirmBtn);
2008
+ dialog.appendChild(actions);
2009
+ overlay.appendChild(dialog);
2010
+ document.body.appendChild(overlay);
2011
+
2012
+ // Focus input and handle Enter/Escape
2013
+ if (inputEl) setTimeout(() => inputEl.focus(), 50);
2014
+ overlay.addEventListener('keydown', (e) => {
2015
+ if (e.key === 'Escape') overlay.remove();
2016
+ if (e.key === 'Enter') confirmBtn.click();
2017
+ });
2018
+ }
2019
+
2020
+ // --- Mobile long-press ---
2021
+ function setupLongPress(el, callback) {
2022
+ let timer = null;
2023
+ let startX = 0, startY = 0;
2024
+ el.addEventListener('touchstart', (e) => {
2025
+ startX = e.touches[0].clientX;
2026
+ startY = e.touches[0].clientY;
2027
+ timer = setTimeout(() => { e.preventDefault(); callback(startX, startY); }, 500);
2028
+ }, { passive: false });
2029
+ el.addEventListener('touchmove', (e) => {
2030
+ if (timer && (Math.abs(e.touches[0].clientX - startX) > 10 || Math.abs(e.touches[0].clientY - startY) > 10)) {
2031
+ clearTimeout(timer); timer = null;
2032
+ }
2033
+ }, { passive: true });
2034
+ el.addEventListener('touchend', () => { if (timer) { clearTimeout(timer); timer = null; } });
2035
+ el.addEventListener('touchcancel', () => { if (timer) { clearTimeout(timer); timer = null; } });
2036
+ }
2037
+
2038
+ // --- Text formatter (URLs, issues, commit hashes) ---
2039
+ function formatCommitMessage(msg) {
2040
+ let safe = escHtml(msg);
2041
+ // Apply issue linking rules from settings
2042
+ const rules = state.settings.issueLinkingRules || [];
2043
+ for (const rule of rules) {
2044
+ if (!rule.pattern) continue;
2045
+ if (rule.pattern.length > 200) continue; // ReDoS guard
2046
+ try {
2047
+ const re = new RegExp(rule.pattern, 'g');
2048
+ if (rule.url) {
2049
+ safe = safe.replace(re, function(match) {
2050
+ let href = rule.url;
2051
+ for (let i = 1; i < arguments.length - 2; i++) {
2052
+ if (typeof arguments[i] === 'string') href = href.split('$' + i).join(arguments[i]);
2053
+ }
2054
+ return '<a class="commit-link" href="' + escHtml(href) + '" target="_blank" title="' + escHtml(href) + '">' + match + '</a>';
2055
+ });
2056
+ } else {
2057
+ safe = safe.replace(re, '<span class="commit-link" title="$&">$&</span>');
2058
+ }
2059
+ } catch (e) { /* invalid regex — skip */ }
2060
+ }
2061
+ // Short commit hashes
2062
+ safe = safe.replace(/\\b([0-9a-f]{7,40})\\b/g, '<span class="commit-link" title="$1">$1</span>');
2063
+ // URLs — skip if already inside an <a> tag
2064
+ safe = safe.replace(/(<a[^>]*>.*?<\\/a>)|(https?:\\/\\/[^\\s<]+)/g, (m, linked, url) => {
2065
+ if (linked) return linked;
2066
+ return '<a class="commit-link" href="' + url + '" target="_blank">' + url + '</a>';
2067
+ });
2068
+ return safe;
2069
+ }
2070
+
2071
+ // --- Find widget ---
2072
+ const findBar = document.getElementById('find-bar');
2073
+ const findInput = document.getElementById('find-input');
2074
+
2075
+ document.getElementById('btn-find').addEventListener('click', toggleFind);
2076
+
2077
+ function toggleFind() {
2078
+ findBar.classList.toggle('hidden');
2079
+ if (!findBar.classList.contains('hidden')) findInput.focus();
2080
+ else clearSearch();
2081
+ }
2082
+
2083
+ findInput.addEventListener('input', () => doSearch(findInput.value));
2084
+ document.getElementById('find-next').addEventListener('click', () => navigateSearch(1));
2085
+ document.getElementById('find-prev').addEventListener('click', () => navigateSearch(-1));
2086
+ document.getElementById('find-close').addEventListener('click', () => { findBar.classList.add('hidden'); clearSearch(); });
2087
+
2088
+ function doSearch(query) {
2089
+ clearSearchHighlights();
2090
+ state.searchMatches = [];
2091
+ state.searchIndex = -1;
2092
+ if (!query.trim()) { document.getElementById('find-count').textContent = ''; return; }
2093
+ const q = query.toLowerCase();
2094
+ const displayCommits = getDisplayCommits();
2095
+ document.querySelectorAll('.commit-row:not(.header-row)').forEach((row, idx) => {
2096
+ const commit = displayCommits[idx];
2097
+ if (!commit || commit.hash === 'uncommitted') return;
2098
+ const match = commit.message.toLowerCase().includes(q) ||
2099
+ commit.author.toLowerCase().includes(q) ||
2100
+ commit.hash.toLowerCase().startsWith(q);
2101
+ if (match) { state.searchMatches.push(idx); row.classList.add('search-match'); }
2102
+ });
2103
+ document.getElementById('find-count').textContent = state.searchMatches.length + ' match(es)';
2104
+ if (state.searchMatches.length > 0) navigateSearch(0);
2105
+ }
2106
+
2107
+ function navigateSearch(dir) {
2108
+ if (state.searchMatches.length === 0) return;
2109
+ if (dir === 0) state.searchIndex = 0;
2110
+ else state.searchIndex = (state.searchIndex + dir + state.searchMatches.length) % state.searchMatches.length;
2111
+ const idx = state.searchMatches[state.searchIndex];
2112
+ const rows = document.querySelectorAll('.commit-row:not(.header-row)');
2113
+ if (rows[idx]) rows[idx].scrollIntoView({ block: 'center' });
2114
+ document.getElementById('find-count').textContent = (state.searchIndex + 1) + ' of ' + state.searchMatches.length;
2115
+ }
2116
+
2117
+ function clearSearch() {
2118
+ clearSearchHighlights();
2119
+ state.searchMatches = [];
2120
+ state.searchIndex = -1;
2121
+ findInput.value = '';
2122
+ document.getElementById('find-count').textContent = '';
2123
+ }
2124
+
2125
+ function clearSearchHighlights() {
2126
+ document.querySelectorAll('.search-match').forEach(el => el.classList.remove('search-match'));
2127
+ }
2128
+
2129
+ // --- Keyboard shortcuts ---
2130
+ document.addEventListener('keydown', (e) => {
2131
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f') { e.preventDefault(); toggleFind(); }
2132
+ if (e.key === 'Escape') {
2133
+ hideContextMenu();
2134
+ if (!findBar.classList.contains('hidden')) { findBar.classList.add('hidden'); clearSearch(); }
2135
+ 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')); }
2136
+ }
2137
+ });
2138
+
2139
+ // --- Scroll to load more ---
2140
+ document.getElementById('graph-container').addEventListener('scroll', (e) => {
2141
+ const container = e.target;
2142
+ if (container.scrollTop + container.clientHeight >= container.scrollHeight - 50) {
2143
+ if (!state.loading && state.commits.length >= state.maxCommits) {
2144
+ state.loading = true;
2145
+ document.getElementById('loading').classList.remove('hidden');
2146
+ vscode.postMessage({ command: 'requestCommits', maxCommits: state.maxCommits, skip: state.commits.length });
2147
+ }
2148
+ }
2149
+ });
2150
+
2151
+ // --- Utilities ---
2152
+ function escHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;'); }
2153
+
2154
+ function formatDate(ts) {
2155
+ const fmt = state.settings.dateFormat;
2156
+ if (fmt === 'iso') return new Date(ts * 1000).toISOString().substring(0, 16).replace('T', ' ');
2157
+ if (fmt === 'absolute') return new Date(ts * 1000).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
2158
+ const now = Date.now() / 1000;
2159
+ const diff = now - ts;
2160
+ if (diff < 60) return 'just now';
2161
+ if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
2162
+ if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
2163
+ if (diff < 2592000) return Math.floor(diff / 86400) + 'd ago';
2164
+ if (diff < 31536000) return Math.floor(diff / 2592000) + 'mo ago';
2165
+ return Math.floor(diff / 31536000) + 'y ago';
2166
+ }
2167
+
2168
+ function updateStatus() {
2169
+ const parts = [];
2170
+ if (state.currentBranch) parts.push(state.currentBranch);
2171
+ parts.push(state.commits.length + ' commits');
2172
+ parts.push(state.branches.length + ' branches');
2173
+ parts.push(state.tags.length + ' tags');
2174
+ document.getElementById('status-text').textContent = parts.join(' | ');
2175
+ }
2176
+
2177
+ // --- Settings panel ---
2178
+ const settingsPanel = document.getElementById('settings-panel');
2179
+
2180
+ document.getElementById('btn-settings').addEventListener('click', () => {
2181
+ const isOpen = settingsPanel.classList.toggle('open');
2182
+ if (isOpen) {
2183
+ vscode.postMessage({ command: 'requestSettings' });
2184
+ vscode.postMessage({ command: 'requestUserDetails' });
2185
+ renderRemotesList();
2186
+ }
2187
+ });
2188
+ document.getElementById('settings-close').addEventListener('click', () => {
2189
+ settingsPanel.classList.remove('open');
2190
+ });
2191
+
2192
+ function applySettingsToUI() {
2193
+ const s = state.settings;
2194
+ document.getElementById('s-maxCommits').value = s.maxCommits;
2195
+ document.getElementById('s-showTags').checked = s.showTags;
2196
+ document.getElementById('s-showStashes').checked = s.showStashes;
2197
+ document.getElementById('s-showRemoteBranches').checked = s.showRemoteBranches;
2198
+ document.getElementById('s-graphStyle').value = s.graphStyle;
2199
+ document.getElementById('s-firstParentOnly').checked = s.firstParentOnly;
2200
+ document.getElementById('s-dateFormat').value = s.dateFormat;
2201
+ document.getElementById('s-commitOrdering').value = s.commitOrdering;
2202
+ document.getElementById('s-autoFetchInterval').value = s.autoFetchInterval || 0;
2203
+ graphConfig.style = s.graphStyle;
2204
+ startAutoFetch(s.autoFetchInterval);
2205
+ renderIssueRules();
2206
+ applyPrSettingsToUI();
2207
+ }
2208
+
2209
+ // General setting change handlers
2210
+ ['showTags', 'showStashes', 'showRemoteBranches', 'firstParentOnly'].forEach(key => {
2211
+ document.getElementById('s-' + key).addEventListener('change', (e) => {
2212
+ vscode.postMessage({ command: 'updateSetting', key, value: e.target.checked });
2213
+ state.settings[key] = e.target.checked;
2214
+ renderCommitList();
2215
+ });
2216
+ });
2217
+ ['graphStyle', 'dateFormat', 'commitOrdering'].forEach(key => {
2218
+ document.getElementById('s-' + key).addEventListener('change', (e) => {
2219
+ vscode.postMessage({ command: 'updateSetting', key, value: e.target.value });
2220
+ state.settings[key] = e.target.value;
2221
+ if (key === 'graphStyle') graphConfig.style = e.target.value;
2222
+ if (key === 'dateFormat') renderCommitList();
2223
+ });
2224
+ });
2225
+ document.getElementById('s-maxCommits').addEventListener('change', (e) => {
2226
+ const n = parseInt(e.target.value, 10);
2227
+ if (n > 0 && n <= 10000) {
2228
+ state.maxCommits = n;
2229
+ state.settings.maxCommits = n;
2230
+ vscode.postMessage({ command: 'updateSetting', key: 'maxCommits', value: n });
2231
+ }
2232
+ });
2233
+ document.getElementById('s-autoFetchInterval').addEventListener('change', (e) => {
2234
+ const val = parseInt(e.target.value, 10);
2235
+ state.settings.autoFetchInterval = val;
2236
+ vscode.postMessage({ command: 'updateSetting', key: 'autoFetchInterval', value: val });
2237
+ startAutoFetch(val);
2238
+ });
2239
+
2240
+ // User details
2241
+ document.getElementById('s-saveUser').addEventListener('click', () => {
2242
+ const name = document.getElementById('s-userName').value.trim();
2243
+ const email = document.getElementById('s-userEmail').value.trim();
2244
+ vscode.postMessage({ command: 'updateUserDetails', name, email });
2245
+ });
2246
+
2247
+ // Remotes
2248
+ function renderRemotesList() {
2249
+ const container = document.getElementById('s-remotes-list');
2250
+ if (state.remotes.length === 0) {
2251
+ container.innerHTML = '<p style="font-size:12px;color:var(--subtext)">No remotes configured.</p>';
2252
+ return;
2253
+ }
2254
+ container.innerHTML = state.remotes.map(r =>
2255
+ '<div class="remote-item">' +
2256
+ '<div class="remote-name">' + escHtml(r.name) + '</div>' +
2257
+ '<div class="remote-url">' + escHtml(r.fetchUrl) + '</div>' +
2258
+ '<div class="remote-actions">' +
2259
+ '<button class="btn-sm" data-edit-remote="' + escHtml(r.name) + '">Edit URL</button>' +
2260
+ '<button class="btn-sm" style="color:var(--red)" data-rm-remote="' + escHtml(r.name) + '">Remove</button>' +
2261
+ '</div>' +
2262
+ '</div>'
2263
+ ).join('');
2264
+ }
2265
+
2266
+ document.getElementById('s-remotes-list').addEventListener('click', (e) => {
2267
+ const editBtn = e.target.closest('[data-edit-remote]');
2268
+ if (editBtn) {
2269
+ const name = editBtn.dataset.editRemote;
2270
+ const remote = state.remotes.find(r => r.name === name);
2271
+ showDialog({
2272
+ title: 'Edit Remote URL: ' + name,
2273
+ input: { placeholder: 'New URL', defaultValue: remote ? remote.fetchUrl : '' },
2274
+ onConfirm: (url) => { if (url) vscode.postMessage({ command: 'editRemoteUrl', name, url }); },
2275
+ });
2276
+ return;
2277
+ }
2278
+ const rmBtn = e.target.closest('[data-rm-remote]');
2279
+ if (rmBtn) {
2280
+ const name = rmBtn.dataset.rmRemote;
2281
+ showDialog({
2282
+ title: 'Remove Remote',
2283
+ message: 'Remove remote "' + name + '"? This cannot be undone.',
2284
+ destructive: true,
2285
+ confirmLabel: 'Remove',
2286
+ onConfirm: () => vscode.postMessage({ command: 'removeRemote', name }),
2287
+ });
2288
+ }
2289
+ });
2290
+
2291
+ document.getElementById('s-addRemote').addEventListener('click', () => {
2292
+ const name = document.getElementById('s-newRemoteName').value.trim();
2293
+ const url = document.getElementById('s-newRemoteUrl').value.trim();
2294
+ if (name && url) {
2295
+ vscode.postMessage({ command: 'addRemote', name, url });
2296
+ document.getElementById('s-newRemoteName').value = '';
2297
+ document.getElementById('s-newRemoteUrl').value = '';
2298
+ }
2299
+ });
2300
+
2301
+ // --- Issue Linking ---
2302
+ function renderIssueRules() {
2303
+ const rules = state.settings.issueLinkingRules || [];
2304
+ const container = document.getElementById('issue-rules-list');
2305
+ container.innerHTML = rules.map((r, i) =>
2306
+ '<div class="issue-rule-row" data-idx="' + i + '">' +
2307
+ '<input type="text" class="rule-pattern" placeholder="Regex, e.g. #(\\d+)" value="' + escHtml(r.pattern) + '">' +
2308
+ '<input type="text" class="rule-url" placeholder="URL with $1, e.g. https://..." value="' + escHtml(r.url) + '">' +
2309
+ '<button class="rule-remove" title="Remove">&times;</button>' +
2310
+ '</div>'
2311
+ ).join('');
2312
+ }
2313
+
2314
+ let issueRuleDebounce = null;
2315
+ document.getElementById('issue-rules-list').addEventListener('input', (e) => {
2316
+ const row = e.target.closest('.issue-rule-row');
2317
+ if (!row) return;
2318
+ const idx = parseInt(row.dataset.idx);
2319
+ const rules = [...(state.settings.issueLinkingRules || [])];
2320
+ if (!rules[idx]) return;
2321
+ if (e.target.classList.contains('rule-pattern')) {
2322
+ rules[idx] = { ...rules[idx], pattern: e.target.value };
2323
+ try { new RegExp(e.target.value); e.target.classList.remove('rule-error'); }
2324
+ catch { e.target.classList.add('rule-error'); return; }
2325
+ }
2326
+ if (e.target.classList.contains('rule-url')) {
2327
+ rules[idx] = { ...rules[idx], url: e.target.value };
2328
+ }
2329
+ state.settings.issueLinkingRules = rules;
2330
+ clearTimeout(issueRuleDebounce);
2331
+ issueRuleDebounce = setTimeout(() => {
2332
+ vscode.postMessage({ command: 'updateSetting', key: 'issueLinkingRules', value: rules });
2333
+ }, 500);
2334
+ });
2335
+
2336
+ document.getElementById('issue-rules-list').addEventListener('click', (e) => {
2337
+ if (!e.target.closest('.rule-remove')) return;
2338
+ const row = e.target.closest('.issue-rule-row');
2339
+ const idx = parseInt(row.dataset.idx);
2340
+ const rules = [...(state.settings.issueLinkingRules || [])];
2341
+ rules.splice(idx, 1);
2342
+ state.settings.issueLinkingRules = rules;
2343
+ vscode.postMessage({ command: 'updateSetting', key: 'issueLinkingRules', value: rules });
2344
+ renderIssueRules();
2345
+ });
2346
+
2347
+ document.getElementById('add-issue-rule').addEventListener('click', () => {
2348
+ const rules = [...(state.settings.issueLinkingRules || []), { pattern: '', url: '' }];
2349
+ state.settings.issueLinkingRules = rules;
2350
+ vscode.postMessage({ command: 'updateSetting', key: 'issueLinkingRules', value: rules });
2351
+ renderIssueRules();
2352
+ });
2353
+
2354
+ // --- PR Creation ---
2355
+ const PR_TEMPLATES = {
2356
+ github: 'https://github.com/\${owner}/\${repo}/compare/\${targetBranch}...\${sourceBranch}?expand=1',
2357
+ gitlab: 'https://gitlab.com/\${owner}/\${repo}/-/merge_requests/new?source_branch=\${sourceBranch}&target_branch=\${targetBranch}',
2358
+ bitbucket: 'https://bitbucket.org/\${owner}/\${repo}/pull-requests/new?source=\${sourceBranch}&dest=\${targetBranch}',
2359
+ custom: '',
2360
+ };
2361
+
2362
+ document.getElementById('pr-provider').addEventListener('change', (e) => {
2363
+ const provider = e.target.value;
2364
+ const prConfig = document.getElementById('pr-config');
2365
+ if (!provider) {
2366
+ prConfig.classList.add('hidden');
2367
+ state.settings.prCreation = null;
2368
+ vscode.postMessage({ command: 'updateSetting', key: 'prCreation', value: null });
2369
+ return;
2370
+ }
2371
+ prConfig.classList.remove('hidden');
2372
+ document.getElementById('pr-url-template').value = PR_TEMPLATES[provider] || '';
2373
+ document.getElementById('pr-target').value = 'main';
2374
+ vscode.postMessage({ command: 'requestOwnerRepo' });
2375
+ });
2376
+
2377
+ document.getElementById('pr-save').addEventListener('click', () => {
2378
+ const provider = document.getElementById('pr-provider').value;
2379
+ if (!provider) return;
2380
+ const config = {
2381
+ provider,
2382
+ urlTemplate: document.getElementById('pr-url-template').value.trim(),
2383
+ owner: document.getElementById('pr-owner').value.trim(),
2384
+ repo: document.getElementById('pr-repo').value.trim(),
2385
+ defaultTargetBranch: document.getElementById('pr-target').value.trim() || 'main',
2386
+ };
2387
+ state.settings.prCreation = config;
2388
+ vscode.postMessage({ command: 'updateSetting', key: 'prCreation', value: config });
2389
+ });
2390
+
2391
+ function applyPrSettingsToUI() {
2392
+ const pr = state.settings.prCreation;
2393
+ if (!pr) {
2394
+ document.getElementById('pr-provider').value = '';
2395
+ document.getElementById('pr-config').classList.add('hidden');
2396
+ return;
2397
+ }
2398
+ document.getElementById('pr-provider').value = pr.provider;
2399
+ document.getElementById('pr-config').classList.remove('hidden');
2400
+ document.getElementById('pr-owner').value = pr.owner || '';
2401
+ document.getElementById('pr-repo').value = pr.repo || '';
2402
+ document.getElementById('pr-target').value = pr.defaultTargetBranch || 'main';
2403
+ document.getElementById('pr-url-template').value = pr.urlTemplate || '';
2404
+ }
2405
+
2406
+ function openPrUrl(sourceBranch) {
2407
+ const pr = state.settings.prCreation;
2408
+ if (!pr || !pr.urlTemplate) return;
2409
+ const url = pr.urlTemplate
2410
+ .replace(/\\$\\{owner\\}/g, encodeURIComponent(pr.owner))
2411
+ .replace(/\\$\\{repo\\}/g, encodeURIComponent(pr.repo))
2412
+ .replace(/\\$\\{sourceBranch\\}/g, encodeURIComponent(sourceBranch))
2413
+ .replace(/\\$\\{targetBranch\\}/g, encodeURIComponent(pr.defaultTargetBranch || 'main'));
2414
+ window.open(url, '_blank');
2415
+ }
2416
+ `;
2417
+ }