collabmd 0.1.44 → 0.1.46

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 (291) hide show
  1. package/README.md +39 -19
  2. package/bin/collabmd.js +27 -1
  3. package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CYM5nxUi.js +1 -0
  4. package/dist/client/assets/browser-utils--H_DkyEF.js +1 -0
  5. package/dist/client/assets/chunk-ERSWUTIN-hHdvnhaY.js +41 -0
  6. package/dist/client/assets/{chunk-CZQZEIFA-DT3N9fo7.js → chunk-YGJVY4SA-CjSe7Bx7.js} +2 -2
  7. package/dist/client/assets/comment-thread-store-CdcwrObc.js +1 -0
  8. package/dist/client/assets/comment-threads-9J6Em-_y.js +4 -0
  9. package/dist/client/assets/comment-ui-shared-BC4pt4w8.js +7 -0
  10. package/dist/client/assets/dist-B5HTH2BO.js +1 -0
  11. package/dist/client/assets/{dist-DhLvC8DS.js → dist-B5Jf1SLZ.js} +2 -2
  12. package/dist/client/assets/{dist-CnL7Y2sM.js → dist-BDt3rz7l.js} +1 -1
  13. package/dist/client/assets/dist-BEwK4csK.js +1 -0
  14. package/dist/client/assets/dist-BKW4PALF.js +1 -0
  15. package/dist/client/assets/dist-BWcHHOx-.js +1 -0
  16. package/dist/client/assets/{dist-CYZmbokb.js → dist-BalScOwZ.js} +9 -9
  17. package/dist/client/assets/dist-Bf8f4HmM.js +6 -0
  18. package/dist/client/assets/dist-BjHJDtyU.js +1 -0
  19. package/dist/client/assets/dist-Bm6pf9Bl.js +1 -0
  20. package/dist/client/assets/dist-BoVBdSjk.js +1 -0
  21. package/dist/client/assets/dist-CC5IrQMG.js +1 -0
  22. package/dist/client/assets/{dist-C11hHqv_.js → dist-CCP_AKPG.js} +12 -12
  23. package/dist/client/assets/dist-CZgOmxK6.js +1 -0
  24. package/dist/client/assets/{dist-D2AQ-76g.js → dist-Cqhv5LAk.js} +1 -1
  25. package/dist/client/assets/{dist-MvcV4190.js → dist-D-Z6FLem.js} +1 -1
  26. package/dist/client/assets/dist-DLMiIsNC.js +11 -0
  27. package/dist/client/assets/{dist-3bgCp7iY.js → dist-DZKu9Cs8.js} +1 -1
  28. package/dist/client/assets/dist-DaYdhcVs.js +1 -0
  29. package/dist/client/assets/dist-FWJsgGYI.js +1 -0
  30. package/dist/client/assets/dist-Kh2COra_.js +1 -0
  31. package/dist/client/assets/{dist-JYEDRrPT.js → dist-PDZ3bSqm.js} +1 -1
  32. package/dist/client/assets/{dist-Drp8uu5J.js → dist-TjMh8n1u.js} +1 -1
  33. package/dist/client/assets/{dist-DGb27Dl6.js → dist-dtAdgL39.js} +1 -1
  34. package/dist/client/assets/dist-jv4WdPnK.js +9 -0
  35. package/dist/client/assets/{dist-DK5Gs2wa.js → dist-rGBmqxC7.js} +1 -1
  36. package/dist/client/assets/{dist-DCzuX1AF.js → dist-shrl6K_0.js} +1 -1
  37. package/dist/client/assets/dist-tOaegDYS2.js +1 -0
  38. package/dist/client/assets/dist-ule2n6ak.js +1 -0
  39. package/dist/client/assets/dist-xElaxbia.js +1 -0
  40. package/dist/client/assets/drawio-editor-FzY1cVw8.js +9 -0
  41. package/dist/client/assets/drawioEditor-BjiKJqlc.js +2 -0
  42. package/dist/client/assets/editor-session-Cm9ekeNo.js +54 -0
  43. package/dist/client/assets/embedded-editor-base-CwTkk911.css +1 -0
  44. package/dist/client/assets/{en-GADW4C6J-DQ6zyCEM.js → en-7NFCYGKX-BrgYZAEe.js} +1 -1
  45. package/dist/client/assets/{preview-render-profile-BZzGSg6K.js → es-DXGV3RHd.js} +13 -13
  46. package/dist/client/assets/excalidraw-editor-B46zsTFI.js +9 -0
  47. package/dist/client/assets/excalidraw-editor-Bt7gMSoh.css +1 -0
  48. package/dist/client/assets/excalidraw-scene-BOben2gr.js +1 -0
  49. package/dist/client/assets/excalidrawEditor-CT2pHq7e.js +2 -0
  50. package/dist/client/assets/export-page-A7NHawJY.js +39 -0
  51. package/dist/client/assets/exportDocument-B2L8fe34.css +1 -0
  52. package/dist/client/assets/exportDocument-BaqcOAAW.js +2 -0
  53. package/dist/client/assets/file-icon-svg-CQAjlWjK.js +1 -0
  54. package/dist/client/assets/file-kind-BbqgGzpc.js +1 -0
  55. package/dist/client/assets/image-QXRYHUNP-SWEV8H0J.js +1 -0
  56. package/dist/client/assets/index-ClbjMBVt.js +2 -0
  57. package/dist/client/assets/index-Pum8ET-5.css +1 -0
  58. package/dist/client/assets/main-B3uPyqic.js +893 -0
  59. package/dist/client/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
  60. package/dist/client/assets/pdf-BL9Pr2D5.js +54 -0
  61. package/dist/client/assets/pdf.worker-CLesOks4.mjs +64857 -0
  62. package/dist/client/assets/pdf_viewer-FVkkThnz.js +551 -0
  63. package/dist/client/assets/preview-diagram-utils-txBCoW8d.js +1 -0
  64. package/dist/client/assets/{preview-render-compiler-BSaQO_p3.js → preview-render-compiler-BD-XhkS2.js} +2 -2
  65. package/dist/client/assets/preview-render-compiler-BKedaMxx.js +1 -0
  66. package/dist/client/assets/preview-render-profile-CE-5rVTC.js +1 -0
  67. package/dist/client/assets/{preview-render-worker-B42LWvrD.js → preview-render-worker-CiPqc9n6.js} +20 -20
  68. package/dist/client/assets/prod-Dy20X7eT.js +1 -0
  69. package/dist/client/assets/prod-hnwh7wRM.js +177 -0
  70. package/dist/client/assets/quick-switcher-controller-fqOYR7qR.js +12 -0
  71. package/dist/client/assets/runtime-config-BLrEKVO1.js +1 -0
  72. package/dist/client/assets/vault-api-client-_LpV1aIn.js +6 -0
  73. package/dist/client/assets/{yjs-provider-reset-guard-Cox4nxwu.js → yjs-provider-reset-guard-DE0vINDP.js} +1 -1
  74. package/dist/client/drawio-editor.html +3 -2
  75. package/dist/client/excalidraw-editor.html +3 -2
  76. package/dist/client/export-document.html +4 -3
  77. package/dist/client/index.html +109 -27
  78. package/docker-compose.yml +42 -0
  79. package/package.json +26 -15
  80. package/scripts/local-structurizr-compose.mjs +101 -0
  81. package/src/client/app/drawio-editor-entry.js +0 -3
  82. package/src/client/app/drawio-editor.html +1 -0
  83. package/src/client/app/excalidraw-editor-entry.js +0 -3
  84. package/src/client/app/excalidraw-editor.html +1 -0
  85. package/src/client/app/export-document-entry.js +0 -3
  86. package/src/client/app/export-document.html +1 -0
  87. package/src/client/app/index.html +106 -24
  88. package/src/client/app/main-entry.js +0 -5
  89. package/src/client/application/app-shell/chat-feature.js +180 -124
  90. package/src/client/application/app-shell/comments-feature.js +20 -11
  91. package/src/client/application/app-shell/git-feature.js +16 -9
  92. package/src/client/application/app-shell/presence-feature.js +18 -11
  93. package/src/client/application/app-shell/ui-feature-shell.js +78 -17
  94. package/src/client/application/app-shell/ui-feature-sidebar.js +74 -1
  95. package/src/client/application/app-shell/ui-feature-tab-activity.js +2 -2
  96. package/src/client/application/app-shell/ui-feature-toolbar.js +2 -52
  97. package/src/client/application/app-shell/workspace-feature.js +27 -84
  98. package/src/client/application/app-shell-elements.js +9 -1
  99. package/src/client/application/diagram-chrome.js +161 -166
  100. package/src/client/application/diagram-preview-export.js +7 -34
  101. package/src/client/application/diagram-preview-hydrator.js +1 -19
  102. package/src/client/application/markdown-frontmatter.js +0 -2
  103. package/src/client/application/mermaid-preview-hydrator.js +1 -14
  104. package/src/client/application/pdf-preview-controller.js +992 -0
  105. package/src/client/application/plantuml-preview-hydrator.js +0 -10
  106. package/src/client/application/preview-diagram-utils.js +31 -2
  107. package/src/client/application/preview-render-compiler.js +2 -1
  108. package/src/client/application/preview-renderer.js +2 -2
  109. package/src/client/application/quick-switcher-loader.js +3 -1
  110. package/src/client/application/structurizr-preview-controller.js +395 -0
  111. package/src/client/application/workspace-coordinator.js +25 -3
  112. package/src/client/application/workspace-preview-controller.js +91 -5
  113. package/src/client/bootstrap/collabmd-app-shell.js +155 -43
  114. package/src/client/browser-utils.js +44 -0
  115. package/src/client/domain/excalidraw-collaboration.js +0 -2
  116. package/src/client/domain/room.js +38 -0
  117. package/src/client/domain/runtime-paths.js +26 -5
  118. package/src/client/domain/structurizr-language.js +99 -0
  119. package/src/client/domain/vault-paths.js +48 -0
  120. package/src/client/excalidraw-editor.js +833 -51
  121. package/src/client/export/export-pipeline.js +6 -34
  122. package/src/client/infrastructure/auth-client.js +4 -10
  123. package/src/client/infrastructure/browser-notification-port.js +33 -20
  124. package/src/client/infrastructure/browser-preferences-port.js +46 -9
  125. package/src/client/infrastructure/comment-thread-store.js +34 -13
  126. package/src/client/infrastructure/editor-session.js +10 -0
  127. package/src/client/infrastructure/editor-view-adapter.js +133 -5
  128. package/src/client/infrastructure/excalidraw-room-client.js +103 -14
  129. package/src/client/infrastructure/git-api-client.js +5 -0
  130. package/src/client/infrastructure/lobby-presence.js +1 -0
  131. package/src/client/infrastructure/vault-api-client.js +51 -0
  132. package/src/client/infrastructure/workspace-sync-client.js +13 -1
  133. package/src/client/presentation/comment-overview-controller.js +18 -53
  134. package/src/client/presentation/comment-ui/comment-ui-card.js +1 -12
  135. package/src/client/presentation/comment-ui/comment-ui-layout.js +242 -219
  136. package/src/client/presentation/comment-ui/comment-ui-render.js +32 -45
  137. package/src/client/presentation/comment-ui/comment-ui-shared.js +68 -13
  138. package/src/client/presentation/comment-ui-controller.js +5 -1
  139. package/src/client/presentation/drawio-embed-controller.js +9 -102
  140. package/src/client/presentation/drawio-viewer.js +118 -0
  141. package/src/client/presentation/excalidraw-embed-controller.js +74 -8
  142. package/src/client/presentation/file-action-controller.js +134 -0
  143. package/src/client/presentation/file-explorer-controller.js +33 -3
  144. package/src/client/presentation/file-explorer-view.js +59 -34
  145. package/src/client/presentation/file-icon-svg.js +40 -0
  146. package/src/client/presentation/file-tree-state.js +12 -4
  147. package/src/client/presentation/git-diff-view-controller.js +731 -3
  148. package/src/client/presentation/outline-controller.js +86 -0
  149. package/src/client/presentation/quick-switcher-controller.js +257 -74
  150. package/src/client/presentation/quick-switcher-text-search.js +3 -3
  151. package/src/client/presentation/toast-controller.js +3 -0
  152. package/src/client/styles/components/feedback.css +6 -0
  153. package/src/client/styles/components/navigation.css +11 -1
  154. package/src/client/styles/features/collaboration-chat.css +49 -53
  155. package/src/client/styles/features/collaboration-presence.css +9 -7
  156. package/src/client/styles/features/comment-card.css +25 -11
  157. package/src/client/styles/features/comment-overlays.css +0 -4
  158. package/src/client/styles/features/comment-overview.css +44 -21
  159. package/src/client/styles/features/comments-drawer.css +2 -100
  160. package/src/client/styles/features/diagram-preview.css +81 -18
  161. package/src/client/styles/features/diff-special.css +309 -0
  162. package/src/client/styles/features/embedded-preview.css +8 -33
  163. package/src/client/styles/features/quick-switcher.css +8 -0
  164. package/src/client/styles/foundation/themes.css +6 -0
  165. package/src/client/styles/foundation/tokens.css +1 -0
  166. package/src/client/styles/layout/editor-page.css +40 -0
  167. package/src/client/styles/layout/responsive.css +31 -1
  168. package/src/client/styles/layout/shell.css +383 -7
  169. package/src/client/styles/layout/sidebar.css +47 -15
  170. package/src/client/styles/layout/view-modes.css +1 -0
  171. package/src/client/styles/overrides/codemirror.css +217 -15
  172. package/src/client/styles/style.css +1 -0
  173. package/src/client/styles/surfaces/excalidraw-editor.css +294 -0
  174. package/src/domain/comment-threads.js +63 -5
  175. package/src/domain/excalidraw-room-codec.js +6 -4
  176. package/src/domain/file-kind.js +35 -3
  177. package/src/server/application/workspace-reconciliation.js +1 -1
  178. package/src/server/config/env.js +79 -0
  179. package/src/server/create-app-server.js +14 -3
  180. package/src/server/domain/bases/base-definition.js +1 -1
  181. package/src/server/domain/bases/base-expression-runtime.js +1 -4
  182. package/src/server/domain/bases/base-index-snapshot-store.js +1 -4
  183. package/src/server/domain/bases/base-query-service.js +1 -77
  184. package/src/server/domain/bases/base-transform.js +1 -4
  185. package/src/server/domain/collaboration/collaboration-room.js +55 -21
  186. package/src/server/domain/comment-overview.js +25 -8
  187. package/src/server/domain/docx-exporter.js +24 -33
  188. package/src/server/domain/ripgrep-search-service.js +7 -1
  189. package/src/server/domain/workspace-state.js +32 -11
  190. package/src/server/infrastructure/git/diff-service.js +29 -13
  191. package/src/server/infrastructure/git/git-service.js +61 -10
  192. package/src/server/infrastructure/git/history-service.js +96 -29
  193. package/src/server/infrastructure/git/image-mime.js +14 -0
  194. package/src/server/infrastructure/git/path-utils.js +1 -1
  195. package/src/server/infrastructure/git/responses.js +0 -95
  196. package/src/server/infrastructure/git/untracked-files.js +28 -1
  197. package/src/server/infrastructure/github/github-app-client.js +22 -15
  198. package/src/server/infrastructure/http/create-git-api-query-handler.js +31 -1
  199. package/src/server/infrastructure/http/create-request-handler.js +18 -31
  200. package/src/server/infrastructure/http/create-static-handler.js +5 -2
  201. package/src/server/infrastructure/http/create-structurizr-api-handler.js +145 -0
  202. package/src/server/infrastructure/http/create-vault-api-command-handler.js +70 -8
  203. package/src/server/infrastructure/http/create-vault-api-query-handler.js +51 -3
  204. package/src/server/infrastructure/http/http-errors.js +7 -1
  205. package/src/server/infrastructure/http/http-request-helpers.js +22 -0
  206. package/src/server/infrastructure/persistence/path-utils.js +1 -1
  207. package/src/server/infrastructure/persistence/vault-file-store.js +3 -0
  208. package/src/server/infrastructure/structurizr/structurizr-embed.css +43 -0
  209. package/src/server/infrastructure/structurizr/structurizr-workspace-service.js +374 -0
  210. package/src/server/infrastructure/websocket/attach-collaboration-gateway.js +4 -22
  211. package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CbSISC1w.js +0 -1
  212. package/dist/client/assets/bases-preview-controller-2pLHk0uk.js +0 -143
  213. package/dist/client/assets/browser-utils-CKtI_rZl.js +0 -1
  214. package/dist/client/assets/button-BCEiw4Rw.js +0 -1
  215. package/dist/client/assets/chunk-AXR3VHVW-0GTk_evH.js +0 -41
  216. package/dist/client/assets/comment-threads-D-iKphIp.js +0 -4
  217. package/dist/client/assets/diagram-action-icons-aa_KOv9h.js +0 -36
  218. package/dist/client/assets/dist-BH87JoHx.js +0 -11
  219. package/dist/client/assets/dist-BIkPRZO7.js +0 -6
  220. package/dist/client/assets/dist-BqCsFpx4.js +0 -1
  221. package/dist/client/assets/dist-C2JHCt8U.js +0 -1
  222. package/dist/client/assets/dist-CZjTtmTu.js +0 -1
  223. package/dist/client/assets/dist-Cig4Ov_X.js +0 -1
  224. package/dist/client/assets/dist-CqQEC8gR.js +0 -1
  225. package/dist/client/assets/dist-D4Rzqod1.js +0 -1
  226. package/dist/client/assets/dist-D5sn-mwk.js +0 -9
  227. package/dist/client/assets/dist-D7G-n87v.js +0 -1
  228. package/dist/client/assets/dist-DRqOrNgN.js +0 -1
  229. package/dist/client/assets/dist-DetkMS_4.js +0 -1
  230. package/dist/client/assets/dist-Dp6FVkQ1.js +0 -1
  231. package/dist/client/assets/dist-DtbjNB28.js +0 -1
  232. package/dist/client/assets/dist-DyrYcvpo2.js +0 -1
  233. package/dist/client/assets/dist-GrygD2zE.js +0 -1
  234. package/dist/client/assets/dist-HmEpWUow.js +0 -1
  235. package/dist/client/assets/dist-RTa81Km7.js +0 -1
  236. package/dist/client/assets/drawio-editor-DDvFoarP.js +0 -9
  237. package/dist/client/assets/drawio-embed-controller-BD1GTRwn.js +0 -1
  238. package/dist/client/assets/drawioEditor-Bu5grlH5.js +0 -2
  239. package/dist/client/assets/editor-session-DGlpiAsK.js +0 -16
  240. package/dist/client/assets/embedded-editor-base-C9roKYpW.css +0 -1
  241. package/dist/client/assets/excalidraw-editor-BHu6k8d4.js +0 -9
  242. package/dist/client/assets/excalidraw-editor-Cr_184EF.css +0 -1
  243. package/dist/client/assets/excalidraw-embed-controller-y2fenN_g.js +0 -1
  244. package/dist/client/assets/excalidraw-embed-reconciler-DGiagExg.js +0 -1
  245. package/dist/client/assets/excalidraw-scene-BvsQ6mLP.js +0 -176
  246. package/dist/client/assets/excalidrawEditor-4vFE--Uw.js +0 -2
  247. package/dist/client/assets/export-page-Cby8JzPO.js +0 -39
  248. package/dist/client/assets/exportDocument-C9v-bc7I.css +0 -1
  249. package/dist/client/assets/exportDocument-CRHCfsdo.js +0 -2
  250. package/dist/client/assets/file-history-view-controller-BDjEV9xJ.js +0 -112
  251. package/dist/client/assets/file-kind-BKPOIRPE.js +0 -1
  252. package/dist/client/assets/git-diff-view-controller-DgdM1aqb.js +0 -150
  253. package/dist/client/assets/git-panel-controller-BDTR2HVp.js +0 -207
  254. package/dist/client/assets/image-XGD7JQR6-XDUWHRpO.js +0 -1
  255. package/dist/client/assets/index-7xOyXDhP.css +0 -1
  256. package/dist/client/assets/index-Bz1KuQ6y.js +0 -2
  257. package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  258. package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  259. package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  260. package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  261. package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  262. package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  263. package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  264. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  265. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  266. package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  267. package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  268. package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  269. package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  270. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  271. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  272. package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  273. package/dist/client/assets/main-Ci07m9oP.js +0 -152
  274. package/dist/client/assets/preview-diagram-utils-n_bbJA9V.js +0 -1
  275. package/dist/client/assets/preview-render-compiler-csGM5ZFz.js +0 -1
  276. package/dist/client/assets/quick-switcher-controller-CoiYvllm.js +0 -12
  277. package/dist/client/assets/room-CDrmIdDt.js +0 -1
  278. package/dist/client/assets/runtime-config-CDgeuPWN.js +0 -1
  279. package/dist/client/assets/runtime-config-loader-BHGniHP5.js +0 -1
  280. package/dist/client/assets/runtime-paths-DvpihCEq.js +0 -1
  281. package/dist/client/assets/segmented-control-DJrHSz8M.js +0 -1
  282. package/dist/client/assets/vault-api-client-sLB2bo56.js +0 -1
  283. package/dist/client/assets/vault-paths-CVkuh0j-.js +0 -5
  284. package/src/client/bootstrap/lazy-controller-feature.js +0 -433
  285. package/src/client/infrastructure/backlinks-api-client.js +0 -16
  286. package/src/client/infrastructure/plantuml-api-client.js +0 -19
  287. package/src/client/infrastructure/runtime-config-loader.js +0 -56
  288. package/src/server/domain/collaboration/room-persistence-controller.js +0 -80
  289. package/src/server/infrastructure/git/command-runner.js +0 -49
  290. package/src/server/infrastructure/git/errors.js +0 -8
  291. package/src/server/infrastructure/http/create-esm-proxy-handler.js +0 -130
@@ -1,6 +1,283 @@
1
1
  import { escapeHtml } from '../domain/vault-utils.js';
2
2
  import { getVaultPathLeaf, getVaultPathParent } from '../domain/vault-paths.js';
3
+ import { resolveApiUrl } from '../domain/runtime-paths.js';
4
+ import { renderDrawioViewer } from './drawio-viewer.js';
3
5
  import { buttonClassNames } from './components/ui/button.js';
6
+ import { getVaultFileKind } from '../../domain/file-kind.js';
7
+
8
+ const IMAGE_MIME_LABELS = Object.freeze({
9
+ '.gif': 'GIF',
10
+ '.jpeg': 'JPEG',
11
+ '.jpg': 'JPEG',
12
+ '.png': 'PNG',
13
+ '.svg': 'SVG',
14
+ '.webp': 'WebP',
15
+ });
16
+
17
+ function getDiffFileKind(file = null) {
18
+ return file?.fileKind || getVaultFileKind(file?.path) || null;
19
+ }
20
+
21
+ function getImageFormat(filePath = '') {
22
+ const normalizedPath = String(filePath ?? '').toLowerCase();
23
+ const extension = Object.keys(IMAGE_MIME_LABELS).find((candidate) => normalizedPath.endsWith(candidate));
24
+ return extension ? IMAGE_MIME_LABELS[extension] : 'Image';
25
+ }
26
+
27
+ function parseExcalidrawScene(rawSource = '') {
28
+ try {
29
+ const parsed = JSON.parse(String(rawSource ?? ''));
30
+ if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.elements)) {
31
+ return null;
32
+ }
33
+ return parsed;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ function pickExcalidrawSceneSource(lines = []) {
40
+ if (lines.length === 0) {
41
+ return null;
42
+ }
43
+
44
+ const joinedSource = lines.join('\n');
45
+ if (parseExcalidrawScene(joinedSource)) {
46
+ return joinedSource;
47
+ }
48
+
49
+ return lines.find((line) => parseExcalidrawScene(line)) ?? null;
50
+ }
51
+
52
+ function getExcalidrawSceneSources(detail = {}) {
53
+ const beforeLines = [];
54
+ const afterLines = [];
55
+
56
+ for (const hunk of detail.hunks ?? []) {
57
+ for (const line of hunk.lines ?? []) {
58
+ if (line.type === 'context') {
59
+ beforeLines.push(line.content);
60
+ afterLines.push(line.content);
61
+ } else if (line.type === 'deletion') {
62
+ beforeLines.push(line.content);
63
+ } else if (line.type === 'addition') {
64
+ afterLines.push(line.content);
65
+ }
66
+ }
67
+ }
68
+
69
+ const beforeSource = pickExcalidrawSceneSource(beforeLines);
70
+ const afterSource = pickExcalidrawSceneSource(afterLines);
71
+ const isAdded = detail.status === 'added' || detail.status === 'untracked';
72
+ const isDeleted = detail.status === 'deleted';
73
+
74
+ return {
75
+ after: isDeleted ? null : parseExcalidrawScene(afterSource),
76
+ before: isAdded ? null : parseExcalidrawScene(beforeSource),
77
+ afterSource: isDeleted ? '' : afterSource || '',
78
+ beforeSource: isAdded ? '' : beforeSource || '',
79
+ };
80
+ }
81
+
82
+ function getVisibleSceneElements(scene = null) {
83
+ return Array.isArray(scene?.elements)
84
+ ? scene.elements.filter((element) => element && !element.isDeleted)
85
+ : [];
86
+ }
87
+
88
+ function sceneElementSignature(element) {
89
+ if (!element || typeof element !== 'object') {
90
+ return '';
91
+ }
92
+
93
+ const copy = { ...element };
94
+ delete copy.version;
95
+ delete copy.versionNonce;
96
+ return JSON.stringify(copy);
97
+ }
98
+
99
+ function summarizeExcalidrawChange(beforeScene, afterScene) {
100
+ if (!beforeScene && !afterScene) {
101
+ return null;
102
+ }
103
+
104
+ const beforeById = new Map(getVisibleSceneElements(beforeScene).map((element) => [element.id, element]));
105
+ const afterById = new Map(getVisibleSceneElements(afterScene).map((element) => [element.id, element]));
106
+ let added = 0;
107
+ let removed = 0;
108
+ let updated = 0;
109
+
110
+ for (const [id, element] of afterById) {
111
+ if (!beforeById.has(id)) {
112
+ added += 1;
113
+ } else if (sceneElementSignature(beforeById.get(id)) !== sceneElementSignature(element)) {
114
+ updated += 1;
115
+ }
116
+ }
117
+
118
+ for (const id of beforeById.keys()) {
119
+ if (!afterById.has(id)) {
120
+ removed += 1;
121
+ }
122
+ }
123
+
124
+ return {
125
+ added,
126
+ afterCount: afterById.size,
127
+ beforeCount: beforeById.size,
128
+ removed,
129
+ updated,
130
+ };
131
+ }
132
+
133
+ function formatByteLength(value) {
134
+ const bytes = Number(value);
135
+ if (!Number.isFinite(bytes) || bytes < 0) {
136
+ return '';
137
+ }
138
+ if (bytes < 1024) {
139
+ return `${bytes} B`;
140
+ }
141
+ if (bytes < 1024 * 1024) {
142
+ return `${(bytes / 1024).toFixed(bytes < 10 * 1024 ? 1 : 0)} KB`;
143
+ }
144
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
145
+ }
146
+
147
+ function hasDrawioSourceRoot(source) {
148
+ return /<(?:mxfile|mxGraphModel|diagram)\b/iu.test(String(source ?? ''));
149
+ }
150
+
151
+ function formatDrawioSource(source) {
152
+ return String(source ?? '')
153
+ .replace(/>\s*</gu, '>\n<')
154
+ .trim();
155
+ }
156
+
157
+ function pickDrawioSource(lines = []) {
158
+ if (lines.length === 0) {
159
+ return '';
160
+ }
161
+
162
+ const joinedSource = lines.join('\n');
163
+ if (hasDrawioSourceRoot(joinedSource)) {
164
+ return joinedSource;
165
+ }
166
+
167
+ return lines.find((line) => hasDrawioSourceRoot(line)) ?? '';
168
+ }
169
+
170
+ function getDrawioSourcePair(detail = {}) {
171
+ const beforeLines = [];
172
+ const afterLines = [];
173
+
174
+ for (const hunk of detail.hunks ?? []) {
175
+ for (const line of hunk.lines ?? []) {
176
+ if (line.type === 'context') {
177
+ beforeLines.push(line.content);
178
+ afterLines.push(line.content);
179
+ } else if (line.type === 'deletion') {
180
+ beforeLines.push(line.content);
181
+ } else if (line.type === 'addition') {
182
+ afterLines.push(line.content);
183
+ }
184
+ }
185
+ }
186
+
187
+ const beforeSource = typeof detail.drawioBeforeSource === 'string'
188
+ ? detail.drawioBeforeSource
189
+ : pickDrawioSource(beforeLines);
190
+ const afterSource = typeof detail.drawioAfterSource === 'string'
191
+ ? detail.drawioAfterSource
192
+ : pickDrawioSource(afterLines);
193
+ const isAdded = detail.status === 'added' || detail.status === 'untracked';
194
+ const isDeleted = detail.status === 'deleted';
195
+
196
+ return {
197
+ after: isDeleted ? '' : afterSource,
198
+ before: isAdded ? '' : beforeSource,
199
+ };
200
+ }
201
+
202
+ function decodeXmlAttribute(value) {
203
+ return String(value ?? '')
204
+ .replace(/&quot;/gu, '"')
205
+ .replace(/&apos;/gu, "'")
206
+ .replace(/&amp;/gu, '&')
207
+ .replace(/&lt;/gu, '<')
208
+ .replace(/&gt;/gu, '>');
209
+ }
210
+
211
+ function getDrawioCellMap(source) {
212
+ const cells = new Map();
213
+ const cellPattern = /<mxCell\b([^>]*?)(?:\/|>([\s\S]*?)<\/mxCell\s*)>/giu;
214
+ let match;
215
+
216
+ while ((match = cellPattern.exec(String(source ?? ''))) !== null) {
217
+ const idMatch = match[1].match(/\bid\s*=\s*(['"])(.*?)\1/iu);
218
+ const id = decodeXmlAttribute(idMatch?.[2] || '');
219
+ if (!id || id === '0' || id === '1') {
220
+ continue;
221
+ }
222
+
223
+ const signature = `${match[1]}${match[2] || ''}`.replace(/\s+/gu, ' ').trim();
224
+ cells.set(id, signature);
225
+ }
226
+
227
+ return cells;
228
+ }
229
+
230
+ function isEmptyDrawioModel(source) {
231
+ return /<mxGraphModel\b/iu.test(String(source ?? '')) && getDrawioCellMap(source).size === 0;
232
+ }
233
+
234
+ function summarizeDrawioChange(beforeSource, afterSource) {
235
+ if (!beforeSource && !afterSource) {
236
+ return null;
237
+ }
238
+
239
+ const beforeCells = getDrawioCellMap(beforeSource);
240
+ const afterCells = getDrawioCellMap(afterSource);
241
+ const cellBased = beforeCells.size > 0 || afterCells.size > 0;
242
+ let added = 0;
243
+ let removed = 0;
244
+ let updated = 0;
245
+
246
+ if (cellBased) {
247
+ for (const [id, signature] of afterCells) {
248
+ if (!beforeCells.has(id)) {
249
+ added += 1;
250
+ } else if (beforeCells.get(id) !== signature) {
251
+ updated += 1;
252
+ }
253
+ }
254
+
255
+ for (const id of beforeCells.keys()) {
256
+ if (!afterCells.has(id)) {
257
+ removed += 1;
258
+ }
259
+ }
260
+
261
+ if (beforeSource !== afterSource && added === 0 && removed === 0 && updated === 0) {
262
+ updated = 1;
263
+ }
264
+ } else {
265
+ const hasBefore = Boolean(beforeSource);
266
+ const hasAfter = Boolean(afterSource);
267
+ added = !hasBefore && hasAfter ? 1 : 0;
268
+ removed = hasBefore && !hasAfter ? 1 : 0;
269
+ updated = hasBefore && hasAfter && beforeSource !== afterSource ? 1 : 0;
270
+ }
271
+
272
+ return {
273
+ added,
274
+ afterCount: afterCells.size,
275
+ beforeCount: beforeCells.size,
276
+ cellBased,
277
+ removed,
278
+ updated,
279
+ };
280
+ }
4
281
 
5
282
  function createSectionId(pathValue) {
6
283
  return `diff-section-${encodeURIComponent(String(pathValue ?? '')).replace(/%/g, '_')}`;
@@ -136,6 +413,7 @@ function renderSplitRow(leftLine, rightLine) {
136
413
 
137
414
  export class GitDiffViewController {
138
415
  constructor({
416
+ getTheme = () => 'dark',
139
417
  gitApiClient = null,
140
418
  onBackToHistory = null,
141
419
  onCommitStaged = null,
@@ -143,7 +421,9 @@ export class GitDiffViewController {
143
421
  onStageFile = null,
144
422
  onUnstageFile = null,
145
423
  toastController = null,
424
+ vaultApiClient = null,
146
425
  } = {}) {
426
+ this.getTheme = getTheme;
147
427
  this.gitApiClient = gitApiClient;
148
428
  this.onBackToHistory = onBackToHistory;
149
429
  this.onCommitStaged = onCommitStaged;
@@ -151,6 +431,7 @@ export class GitDiffViewController {
151
431
  this.onStageFile = onStageFile;
152
432
  this.onUnstageFile = onUnstageFile;
153
433
  this.toastController = toastController;
434
+ this.vaultApiClient = vaultApiClient;
154
435
  this.page = document.getElementById('diff-page');
155
436
  this.content = document.getElementById('diffContent');
156
437
  this.scrollContainer = document.getElementById('diffScroll');
@@ -180,8 +461,13 @@ export class GitDiffViewController {
180
461
  this.loadingFiles = new Set();
181
462
  this.collapsedFiles = new Set();
182
463
  this.fileLoadPromises = new Map();
464
+ this.excalidrawPreviewPayloads = new Map();
465
+ this.excalidrawPreviewCounter = 0;
466
+ this.drawioPreviewPayloads = new Map();
467
+ this.drawioPreviewCounter = 0;
183
468
  this.requestScope = 'all';
184
469
  this.commitHash = null;
470
+ this.commitBaseRef = null;
185
471
  this.commitMeta = null;
186
472
  this.historyFilePath = null;
187
473
  this.pendingAction = null;
@@ -330,8 +616,11 @@ export class GitDiffViewController {
330
616
  this.loadingFiles.clear();
331
617
  this.collapsedFiles.clear();
332
618
  this.fileLoadPromises.clear();
619
+ this.excalidrawPreviewPayloads.clear();
620
+ this.drawioPreviewPayloads.clear();
333
621
  this.requestScope = 'all';
334
622
  this.commitHash = null;
623
+ this.commitBaseRef = null;
335
624
  this.commitMeta = null;
336
625
  this.historyFilePath = null;
337
626
  this.pendingAction = null;
@@ -343,6 +632,7 @@ export class GitDiffViewController {
343
632
  this.source = 'workspace';
344
633
  this.layoutMode = 'focused';
345
634
  this.commitHash = null;
635
+ this.commitBaseRef = null;
346
636
  this.commitMeta = null;
347
637
  this.historyFilePath = null;
348
638
  this.activeFilePath = filePath;
@@ -351,6 +641,8 @@ export class GitDiffViewController {
351
641
  this.loadingFiles.clear();
352
642
  this.collapsedFiles.clear();
353
643
  this.fileLoadPromises.clear();
644
+ this.excalidrawPreviewPayloads.clear();
645
+ this.drawioPreviewPayloads.clear();
354
646
  this.requestScope = scope;
355
647
  this.renderLoading('Loading diff summary...');
356
648
 
@@ -392,6 +684,7 @@ export class GitDiffViewController {
392
684
  this.source = 'commit';
393
685
  this.layoutMode = 'stacked';
394
686
  this.commitHash = String(hash ?? '').trim() || null;
687
+ this.commitBaseRef = null;
395
688
  this.commitMeta = null;
396
689
  this.historyFilePath = String(historyFilePath ?? '').trim() || null;
397
690
  this.activeFilePath = path || null;
@@ -400,6 +693,8 @@ export class GitDiffViewController {
400
693
  this.loadingFiles.clear();
401
694
  this.collapsedFiles.clear();
402
695
  this.fileLoadPromises.clear();
696
+ this.excalidrawPreviewPayloads.clear();
697
+ this.drawioPreviewPayloads.clear();
403
698
  this.requestScope = 'all';
404
699
  this.renderLoading('Loading commit summary...');
405
700
 
@@ -410,6 +705,7 @@ export class GitDiffViewController {
410
705
  });
411
706
  this.data = data;
412
707
  this.commitMeta = data.commit ?? null;
708
+ this.commitBaseRef = data.baseRef || null;
413
709
  const initialIndex = path
414
710
  ? Math.max(0, data.files.findIndex((file) => file.path === path))
415
711
  : 0;
@@ -547,12 +843,408 @@ export class GitDiffViewController {
547
843
  `;
548
844
  }
549
845
 
846
+ getGitImageUrl(hash, filePath) {
847
+ if (!hash || !filePath) {
848
+ return '';
849
+ }
850
+
851
+ if (typeof this.gitApiClient?.getFileAttachmentUrl === 'function') {
852
+ return this.gitApiClient.getFileAttachmentUrl({ hash, path: filePath });
853
+ }
854
+
855
+ return resolveApiUrl(`/git/file-attachment?hash=${encodeURIComponent(hash)}&path=${encodeURIComponent(filePath)}`);
856
+ }
857
+
858
+ getCurrentImageUrl(filePath) {
859
+ if (!filePath) {
860
+ return '';
861
+ }
862
+
863
+ return resolveApiUrl(`/attachment?path=${encodeURIComponent(filePath)}`);
864
+ }
865
+
866
+ getImageDiffSources(file) {
867
+ const isAdded = file.status === 'added' || file.status === 'untracked';
868
+ const isDeleted = file.status === 'deleted';
869
+ const oldPath = file.oldPath || file.path;
870
+ const newPath = file.path;
871
+
872
+ if (this.source === 'commit') {
873
+ return {
874
+ after: isDeleted ? '' : this.getGitImageUrl(this.commitHash, newPath),
875
+ before: isAdded ? '' : this.getGitImageUrl(this.commitBaseRef, oldPath),
876
+ };
877
+ }
878
+
879
+ return {
880
+ after: isDeleted ? '' : this.getCurrentImageUrl(newPath),
881
+ before: isAdded ? '' : this.getGitImageUrl('HEAD', oldPath),
882
+ };
883
+ }
884
+
885
+ renderImagePane({ file, label, source, emptyLabel }) {
886
+ const imageMarkup = source
887
+ ? `<img class="diff-media-image" src="${escapeHtml(source)}" alt="${escapeHtml(`${label} ${file.path}`)}" loading="lazy" decoding="async">`
888
+ : `<div class="diff-media-empty"><span class="diff-media-empty-icon" aria-hidden="true">—</span><span>${escapeHtml(emptyLabel)}</span></div>`;
889
+
890
+ return `
891
+ <div class="diff-media-pane">
892
+ <div class="diff-media-pane-header">
893
+ <span class="diff-media-pane-label">${escapeHtml(label)}</span>
894
+ <span class="diff-media-pane-meta">${escapeHtml(getImageFormat(file.path))}</span>
895
+ </div>
896
+ <div class="diff-media-canvas">${imageMarkup}</div>
897
+ </div>
898
+ `;
899
+ }
900
+
901
+ renderImageDiff(file) {
902
+ const sources = this.getImageDiffSources(file);
903
+ const sizeLabel = formatByteLength(file.byteLength);
904
+ return `
905
+ <section class="diff-special diff-media-diff" aria-label="Image diff">
906
+ <div class="diff-special-header">
907
+ <div>
908
+ <span class="diff-special-eyebrow">Binary preview</span>
909
+ <h2 class="diff-special-title">Image comparison</h2>
910
+ </div>
911
+ <span class="diff-special-note">${escapeHtml(getImageFormat(file.path))}${sizeLabel ? ` · ${escapeHtml(sizeLabel)}` : ''}</span>
912
+ </div>
913
+ <div class="diff-media-grid">
914
+ ${this.renderImagePane({
915
+ emptyLabel: 'No previous image',
916
+ file,
917
+ label: 'Before',
918
+ source: sources.before,
919
+ })}
920
+ ${this.renderImagePane({
921
+ emptyLabel: 'No image in this revision',
922
+ file,
923
+ label: 'After',
924
+ source: sources.after,
925
+ })}
926
+ </div>
927
+ <p class="diff-special-footnote">Image bytes are shown as a visual comparison instead of a text patch.</p>
928
+ </section>
929
+ `;
930
+ }
931
+
932
+ registerExcalidrawPreview(scene) {
933
+ if (!scene) {
934
+ return '';
935
+ }
936
+
937
+ this.excalidrawPreviewCounter += 1;
938
+ const id = `diff-excalidraw-preview-${this.excalidrawPreviewCounter.toString(36)}`;
939
+ this.excalidrawPreviewPayloads.set(id, scene);
940
+ return id;
941
+ }
942
+
943
+ renderExcalidrawPane({ label, scene, source, emptyLabel }) {
944
+ const previewId = this.registerExcalidrawPreview(scene);
945
+ const previewMarkup = scene
946
+ ? `
947
+ <div class="diff-excalidraw-preview" data-diff-excalidraw-preview="${escapeHtml(previewId)}">
948
+ <span class="diff-excalidraw-preview-loading">Rendering diagram…</span>
949
+ </div>
950
+ `
951
+ : `<div class="diff-excalidraw-preview diff-excalidraw-preview--empty"><span>${escapeHtml(emptyLabel)}</span></div>`;
952
+ const sourceMarkup = source
953
+ ? `
954
+ <details class="diff-excalidraw-source">
955
+ <summary>Structured scene</summary>
956
+ <pre>${escapeHtml(source)}</pre>
957
+ </details>
958
+ `
959
+ : '';
960
+
961
+ return `
962
+ <div class="diff-excalidraw-pane">
963
+ <div class="diff-excalidraw-pane-header">
964
+ <span class="diff-media-pane-label">${escapeHtml(label)}</span>
965
+ <span class="diff-media-pane-meta">${scene ? `${getVisibleSceneElements(scene).length} elements` : '—'}</span>
966
+ </div>
967
+ ${previewMarkup}
968
+ ${sourceMarkup}
969
+ </div>
970
+ `;
971
+ }
972
+
973
+ renderExcalidrawDiff(file) {
974
+ const scenes = getExcalidrawSceneSources(file);
975
+ const changeSummary = summarizeExcalidrawChange(scenes.before, scenes.after);
976
+ const stats = changeSummary ?? {
977
+ added: 0,
978
+ afterCount: 0,
979
+ beforeCount: 0,
980
+ removed: 0,
981
+ updated: 0,
982
+ };
983
+ const hasSceneData = Boolean(scenes.before || scenes.after);
984
+ const loadButton = file.tooLarge && file.canLoadFullPatch
985
+ ? `<button class="${buttonClassNames({ variant: 'secondary', extra: 'diff-load-full-btn' })}" type="button" data-load-full-diff data-diff-file-path="${escapeHtml(file.path)}">Load full diff</button>`
986
+ : '';
987
+
988
+ return `
989
+ <section class="diff-special diff-excalidraw-diff" aria-label="Excalidraw diff">
990
+ <div class="diff-special-header">
991
+ <div>
992
+ <span class="diff-special-eyebrow">Diagram-aware preview</span>
993
+ <h2 class="diff-special-title">Excalidraw scene changes</h2>
994
+ </div>
995
+ <span class="diff-special-note">${hasSceneData ? `${stats.afterCount} visible elements` : 'Scene preview unavailable'}</span>
996
+ </div>
997
+ <div class="diff-scene-summary" aria-label="Scene change summary">
998
+ <span class="diff-scene-stat diff-scene-stat--add"><strong>+${stats.added}</strong><span>added</span></span>
999
+ <span class="diff-scene-stat diff-scene-stat--update"><strong>~${stats.updated}</strong><span>updated</span></span>
1000
+ <span class="diff-scene-stat diff-scene-stat--remove"><strong>-${stats.removed}</strong><span>removed</span></span>
1001
+ </div>
1002
+ <div class="diff-excalidraw-grid">
1003
+ ${this.renderExcalidrawPane({
1004
+ emptyLabel: 'No previous scene',
1005
+ label: 'Before',
1006
+ scene: scenes.before,
1007
+ source: scenes.beforeSource ? JSON.stringify(scenes.before || parseExcalidrawScene(scenes.beforeSource), null, 2) : '',
1008
+ })}
1009
+ ${this.renderExcalidrawPane({
1010
+ emptyLabel: file.tooLarge ? 'Load the full diff to preview this scene' : 'No scene in this revision',
1011
+ label: 'After',
1012
+ scene: scenes.after,
1013
+ source: scenes.afterSource ? JSON.stringify(scenes.after || parseExcalidrawScene(scenes.afterSource), null, 2) : '',
1014
+ })}
1015
+ </div>
1016
+ ${loadButton}
1017
+ <p class="diff-special-footnote">The preview compares drawable elements by id; expand “Structured scene” when you need the underlying JSON.</p>
1018
+ </section>
1019
+ `;
1020
+ }
1021
+
1022
+ async readDrawioSource({ hash = null, path = null, current = false } = {}) {
1023
+ if (!path) {
1024
+ return '';
1025
+ }
1026
+
1027
+ try {
1028
+ if (current) {
1029
+ const response = await this.vaultApiClient?.readFile?.(path);
1030
+ return typeof response?.content === 'string' ? response.content : '';
1031
+ }
1032
+
1033
+ if (!hash || typeof this.gitApiClient?.readFileSnapshot !== 'function') {
1034
+ return '';
1035
+ }
1036
+
1037
+ const response = await this.gitApiClient.readFileSnapshot({ hash, path });
1038
+ return typeof response?.content === 'string' ? response.content : '';
1039
+ } catch {
1040
+ return '';
1041
+ }
1042
+ }
1043
+
1044
+ async enrichDrawioDetail(detail) {
1045
+ const sources = getDrawioSourcePair(detail);
1046
+ let beforeSource = sources.before;
1047
+ let afterSource = sources.after;
1048
+ const isAdded = detail.status === 'added' || detail.status === 'untracked';
1049
+ const isDeleted = detail.status === 'deleted';
1050
+ const beforePath = detail.oldPath || detail.path;
1051
+ const afterPath = detail.path;
1052
+
1053
+ if (!isAdded && !hasDrawioSourceRoot(beforeSource)) {
1054
+ beforeSource = await this.readDrawioSource({
1055
+ current: false,
1056
+ hash: this.source === 'commit' ? this.commitBaseRef : 'HEAD',
1057
+ path: beforePath,
1058
+ });
1059
+ }
1060
+
1061
+ if (!isDeleted && !hasDrawioSourceRoot(afterSource)) {
1062
+ afterSource = await this.readDrawioSource({
1063
+ current: this.source !== 'commit',
1064
+ hash: this.source === 'commit' ? this.commitHash : null,
1065
+ path: afterPath,
1066
+ });
1067
+ }
1068
+
1069
+ return {
1070
+ ...detail,
1071
+ drawioAfterSource: afterSource,
1072
+ drawioBeforeSource: beforeSource,
1073
+ };
1074
+ }
1075
+
1076
+ registerDrawioPreview(source) {
1077
+ if (!source) {
1078
+ return '';
1079
+ }
1080
+
1081
+ this.drawioPreviewCounter += 1;
1082
+ const id = `diff-drawio-preview-${this.drawioPreviewCounter.toString(36)}`;
1083
+ this.drawioPreviewPayloads.set(id, source);
1084
+ return id;
1085
+ }
1086
+
1087
+ renderDrawioPane({ emptyLabel, label, source }) {
1088
+ const isEmptyModel = isEmptyDrawioModel(source);
1089
+ const previewId = isEmptyModel ? '' : this.registerDrawioPreview(source);
1090
+ const previewMarkup = source && !isEmptyModel
1091
+ ? `
1092
+ <div class="diff-drawio-preview" data-diff-drawio-preview="${escapeHtml(previewId)}">
1093
+ <span class="diff-drawio-preview-loading">Rendering draw.io preview…</span>
1094
+ </div>
1095
+ `
1096
+ : `<div class="diff-drawio-preview diff-drawio-preview--empty"><span>${escapeHtml(isEmptyModel ? 'No drawable cells' : emptyLabel)}</span></div>`;
1097
+ const sourceMarkup = source
1098
+ ? `
1099
+ <details class="diff-drawio-source">
1100
+ <summary>XML source</summary>
1101
+ <pre>${escapeHtml(formatDrawioSource(source))}</pre>
1102
+ </details>
1103
+ `
1104
+ : '';
1105
+
1106
+ return `
1107
+ <div class="diff-drawio-pane">
1108
+ <div class="diff-drawio-pane-header">
1109
+ <span class="diff-media-pane-label">${escapeHtml(label)}</span>
1110
+ <span class="diff-media-pane-meta">draw.io</span>
1111
+ </div>
1112
+ ${previewMarkup}
1113
+ ${sourceMarkup}
1114
+ </div>
1115
+ `;
1116
+ }
1117
+
1118
+ renderDrawioDiff(file) {
1119
+ const sources = getDrawioSourcePair(file);
1120
+ const changeSummary = summarizeDrawioChange(sources.before, sources.after);
1121
+ const stats = changeSummary ?? {
1122
+ added: 0,
1123
+ afterCount: 0,
1124
+ beforeCount: 0,
1125
+ cellBased: false,
1126
+ removed: 0,
1127
+ updated: 0,
1128
+ };
1129
+ const hasSource = Boolean(sources.before || sources.after);
1130
+ const countLabel = !hasSource
1131
+ ? 'Preview unavailable'
1132
+ : stats.cellBased
1133
+ ? `${stats.afterCount} drawable cells`
1134
+ : 'Source-level comparison';
1135
+ const loadButton = file.tooLarge && file.canLoadFullPatch
1136
+ ? `<button class="${buttonClassNames({ variant: 'secondary', extra: 'diff-load-full-btn' })}" type="button" data-load-full-diff data-diff-file-path="${escapeHtml(file.path)}">Load full diff</button>`
1137
+ : '';
1138
+
1139
+ return `
1140
+ <section class="diff-special diff-drawio-diff" aria-label="draw.io diff">
1141
+ <div class="diff-special-header">
1142
+ <div>
1143
+ <span class="diff-special-eyebrow">Diagram-aware preview</span>
1144
+ <h2 class="diff-special-title">draw.io diagram changes</h2>
1145
+ </div>
1146
+ <span class="diff-special-note">${escapeHtml(countLabel)}</span>
1147
+ </div>
1148
+ <div class="diff-scene-summary" aria-label="draw.io change summary">
1149
+ <span class="diff-scene-stat diff-scene-stat--add"><strong>+${stats.added}</strong><span>added</span></span>
1150
+ <span class="diff-scene-stat diff-scene-stat--update"><strong>~${stats.updated}</strong><span>updated</span></span>
1151
+ <span class="diff-scene-stat diff-scene-stat--remove"><strong>-${stats.removed}</strong><span>removed</span></span>
1152
+ </div>
1153
+ <div class="diff-drawio-grid">
1154
+ ${this.renderDrawioPane({
1155
+ emptyLabel: 'No previous diagram',
1156
+ label: 'Before',
1157
+ source: sources.before,
1158
+ })}
1159
+ ${this.renderDrawioPane({
1160
+ emptyLabel: file.tooLarge ? 'Load the full diff to preview this diagram' : 'No diagram in this revision',
1161
+ label: 'After',
1162
+ source: sources.after,
1163
+ })}
1164
+ </div>
1165
+ ${loadButton}
1166
+ <p class="diff-special-footnote">Both revisions use the draw.io viewer; expand “XML source” when you need the underlying document.</p>
1167
+ </section>
1168
+ `;
1169
+ }
1170
+
1171
+ async hydrateExcalidrawPreviews() {
1172
+ if (this.excalidrawPreviewPayloads.size === 0 || !this.content) {
1173
+ return;
1174
+ }
1175
+
1176
+ let exportToSvg;
1177
+ try {
1178
+ ({ exportToSvg } = await import('@excalidraw/excalidraw'));
1179
+ } catch (error) {
1180
+ console.warn('[git-diff] Excalidraw preview renderer unavailable:', error);
1181
+ return;
1182
+ }
1183
+
1184
+ for (const [previewId, scene] of this.excalidrawPreviewPayloads) {
1185
+ const preview = this.content.querySelector?.(`[data-diff-excalidraw-preview="${previewId}"]`);
1186
+ if (!preview) {
1187
+ continue;
1188
+ }
1189
+
1190
+ try {
1191
+ const svg = await exportToSvg({
1192
+ appState: {
1193
+ exportBackground: true,
1194
+ exportWithDarkMode: false,
1195
+ viewBackgroundColor: scene.appState?.viewBackgroundColor || '#ffffff',
1196
+ },
1197
+ elements: getVisibleSceneElements(scene),
1198
+ exportPadding: 24,
1199
+ files: scene.files || null,
1200
+ });
1201
+ preview.replaceChildren(svg);
1202
+ preview.classList.add('is-ready');
1203
+ } catch (error) {
1204
+ console.warn('[git-diff] Failed to render Excalidraw scene preview:', error);
1205
+ preview.textContent = 'Visual preview unavailable';
1206
+ preview.classList.add('is-error');
1207
+ }
1208
+ }
1209
+ }
1210
+
1211
+ async hydrateDrawioPreviews() {
1212
+ if (this.drawioPreviewPayloads.size === 0 || !this.content) {
1213
+ return;
1214
+ }
1215
+
1216
+ for (const [previewId, source] of this.drawioPreviewPayloads) {
1217
+ const preview = this.content.querySelector?.(`[data-diff-drawio-preview="${previewId}"]`);
1218
+ if (!preview) {
1219
+ continue;
1220
+ }
1221
+
1222
+ try {
1223
+ await renderDrawioViewer(preview, {
1224
+ ariaLabel: 'draw.io diagram diff preview',
1225
+ className: 'mxgraph drawio-viewer-frame diff-drawio-viewer-frame',
1226
+ source,
1227
+ theme: this.getTheme?.() === 'light' ? 'light' : 'dark',
1228
+ });
1229
+ preview.classList.add('is-ready');
1230
+ } catch (error) {
1231
+ console.warn('[git-diff] Failed to render draw.io diagram preview:', error);
1232
+ preview.textContent = 'Visual preview unavailable';
1233
+ preview.classList.add('is-error');
1234
+ }
1235
+ }
1236
+ }
1237
+
550
1238
  renderFileHeader(file) {
1239
+ const fileKind = getDiffFileKind(file);
1240
+ const headerStats = fileKind === 'image'
1241
+ ? `<span class="diff-file-header-kind">${escapeHtml(getImageFormat(file.path))} image</span>`
1242
+ : `<span class="diff-file-header-stats"><span class="ui-stat-token ui-stat-token--add diff-stats-add">+${file.stats?.additions ?? 0}</span><span class="ui-stat-token ui-stat-token--del diff-stats-del">-${file.stats?.deletions ?? 0}</span></span>`;
551
1243
  return `
552
1244
  <div class="diff-file-header">
553
1245
  <span class="diff-file-path">${escapeHtml(file.path)}</span>
554
1246
  <span class="ui-status-badge ${badgeClass(file.status)}">${escapeHtml(file.status)}</span>
555
- <span class="diff-file-header-stats"><span class="ui-stat-token ui-stat-token--add diff-stats-add">+${file.stats?.additions ?? 0}</span><span class="ui-stat-token ui-stat-token--del diff-stats-del">-${file.stats?.deletions ?? 0}</span></span>
1247
+ ${headerStats}
556
1248
  </div>
557
1249
  `;
558
1250
  }
@@ -602,6 +1294,19 @@ export class GitDiffViewController {
602
1294
  return '<div class="diff-empty-state">Select a file to load its diff.</div>';
603
1295
  }
604
1296
 
1297
+ const fileKind = getDiffFileKind(detail);
1298
+ if (fileKind === 'image') {
1299
+ return `${includeHeader ? this.renderFileHeader(detail) : ''}${this.renderImageDiff(detail)}`;
1300
+ }
1301
+
1302
+ if (fileKind === 'excalidraw') {
1303
+ return `${includeHeader ? this.renderFileHeader(detail) : ''}${this.renderExcalidrawDiff(detail)}`;
1304
+ }
1305
+
1306
+ if (fileKind === 'drawio') {
1307
+ return `${includeHeader ? this.renderFileHeader(detail) : ''}${this.renderDrawioDiff(detail)}`;
1308
+ }
1309
+
605
1310
  if (detail.tooLarge) {
606
1311
  return `
607
1312
  ${includeHeader ? this.renderFileHeader(detail) : ''}
@@ -1095,6 +1800,10 @@ export class GitDiffViewController {
1095
1800
  };
1096
1801
  }
1097
1802
 
1803
+ if (getDiffFileKind(detail) === 'drawio') {
1804
+ detail = await this.enrichDrawioDetail(detail);
1805
+ }
1806
+
1098
1807
  if (cacheKey) {
1099
1808
  this.fileCache.set(cacheKey, detail);
1100
1809
  }
@@ -1134,8 +1843,22 @@ export class GitDiffViewController {
1134
1843
  }
1135
1844
 
1136
1845
  if (this.stats) {
1137
- const additions = this.data?.summary?.additions ?? 0;
1138
- const deletions = this.data?.summary?.deletions ?? 0;
1846
+ const imageLineStats = (this.data?.files ?? [])
1847
+ .filter((file) => getDiffFileKind(file) === 'image')
1848
+ .reduce((summary, file) => ({
1849
+ additions: summary.additions + Number(file.stats?.additions || 0),
1850
+ deletions: summary.deletions + Number(file.stats?.deletions || 0),
1851
+ }), { additions: 0, deletions: 0 });
1852
+ const files = this.data?.files ?? [];
1853
+ const allFilesAreImages = files.length > 0 && files.every((file) => getDiffFileKind(file) === 'image');
1854
+ const rawAdditions = Number(this.data?.summary?.additions ?? 0);
1855
+ const rawDeletions = Number(this.data?.summary?.deletions ?? 0);
1856
+ const additions = allFilesAreImages
1857
+ ? 0
1858
+ : Math.max(0, rawAdditions - imageLineStats.additions);
1859
+ const deletions = allFilesAreImages
1860
+ ? 0
1861
+ : Math.max(0, rawDeletions - imageLineStats.deletions);
1139
1862
  this.stats.innerHTML = `
1140
1863
  <span class="ui-stat-token ui-stat-token--add diff-stats-add">+${additions}</span>
1141
1864
  <span class="ui-stat-token ui-stat-token--del diff-stats-del">-${deletions}</span>
@@ -1207,12 +1930,17 @@ export class GitDiffViewController {
1207
1930
  return;
1208
1931
  }
1209
1932
 
1933
+ this.excalidrawPreviewPayloads.clear();
1934
+ this.drawioPreviewPayloads.clear();
1935
+
1210
1936
  if (this.source === 'commit') {
1211
1937
  this.content.innerHTML = `${this.renderCommitHeader()}${this.renderCommitBody()}`;
1212
1938
  } else {
1213
1939
  this.content.innerHTML = this.renderFocusedFileBody();
1214
1940
  }
1215
1941
  this.syncToolbar();
1942
+ void this.hydrateExcalidrawPreviews();
1943
+ void this.hydrateDrawioPreviews();
1216
1944
  }
1217
1945
 
1218
1946
  getToolbarTitle({ commitHash = null, filePath = null, path = null, scope = 'all', source = 'workspace' } = {}) {