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
@@ -24,6 +24,10 @@ import {
24
24
  tryParseSceneJson,
25
25
  } from '../domain/excalidraw-scene.js';
26
26
  import { resolveWsBaseUrl } from '../domain/runtime-paths.js';
27
+ import {
28
+ normalizeCommentQuote,
29
+ } from '../../domain/comment-threads.js';
30
+ import { CommentThreadStore } from './comment-thread-store.js';
27
31
  import { stopReconnectOnControlledClose } from './yjs-provider-reset-guard.js';
28
32
 
29
33
  const DEFAULT_EMPTY_SCENE_GUARD_MS = 250;
@@ -39,16 +43,21 @@ export const EXCALIDRAW_ROOM_CONNECTION_STATE = Object.freeze({
39
43
  RECONNECTING_READONLY: 'reconnecting-readonly',
40
44
  });
41
45
 
42
- function stableJson(value) {
43
- if (Array.isArray(value)) {
44
- return `[${value.map((entry) => stableJson(entry)).join(',')}]`;
46
+ function areExcalidrawFilesEqual(left, right) {
47
+ if (left === right) {
48
+ return true;
45
49
  }
46
50
 
47
- if (value && typeof value === 'object') {
48
- return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
51
+ if (!left || !right || typeof left !== 'object' || typeof right !== 'object') {
52
+ return false;
49
53
  }
50
54
 
51
- return JSON.stringify(value);
55
+ return left.id === right.id
56
+ && left.mimeType === right.mimeType
57
+ && left.dataURL === right.dataURL
58
+ && left.created === right.created
59
+ && left.lastRetrieved === right.lastRetrieved
60
+ && left.version === right.version;
52
61
  }
53
62
 
54
63
  function normalizeElementVersion(value) {
@@ -81,6 +90,35 @@ function shouldSyncElementToRoom(localElement, roomElement) {
81
90
  return localVersionNonce < roomVersionNonce;
82
91
  }
83
92
 
93
+ function createDiagramCommentAnchor(element) {
94
+ const elementId = typeof element?.id === 'string' ? element.id.trim() : '';
95
+ const x = Number(element?.x);
96
+ const y = Number(element?.y);
97
+ const width = Number(element?.width);
98
+ const height = Number(element?.height);
99
+ if (!elementId || ![x, y, width, height].every(Number.isFinite)) {
100
+ return null;
101
+ }
102
+
103
+ return {
104
+ anchorKind: 'diagram-element',
105
+ anchorPoint: {
106
+ x: x + (width / 2),
107
+ y: y + (height / 2),
108
+ },
109
+ anchorQuote: normalizeCommentQuote(element.text),
110
+ anchorSnapshot: {
111
+ height,
112
+ text: normalizeCommentQuote(element.text),
113
+ type: typeof element.type === 'string' ? element.type : 'element',
114
+ width,
115
+ x,
116
+ y,
117
+ },
118
+ elementId,
119
+ };
120
+ }
121
+
84
122
  export class ExcalidrawRoomClient {
85
123
  constructor({
86
124
  cancelAnimationFrameFn = (frameId) => cancelAnimationFrame(frameId),
@@ -89,6 +127,7 @@ export class ExcalidrawRoomClient {
89
127
  filePath = '',
90
128
  now = () => Date.now(),
91
129
  onCollaboratorsChange = () => {},
130
+ onCommentThreadsChange = () => {},
92
131
  onConnectionStateChange = () => {},
93
132
  onRemoteSceneJson = () => {},
94
133
  requestAnimationFrameFn = (callback) => requestAnimationFrame(callback),
@@ -106,6 +145,7 @@ export class ExcalidrawRoomClient {
106
145
  this.filePath = filePath;
107
146
  this.now = now;
108
147
  this.onCollaboratorsChange = onCollaboratorsChange;
148
+ this.onCommentThreadsChange = onCommentThreadsChange;
109
149
  this.onConnectionStateChange = onConnectionStateChange;
110
150
  this.onRemoteSceneJson = onRemoteSceneJson;
111
151
  this.requestAnimationFrameFn = requestAnimationFrameFn;
@@ -138,12 +178,22 @@ export class ExcalidrawRoomClient {
138
178
  this.lastViewportSignature = '';
139
179
  this.lastSelectedIdsSignature = '';
140
180
  this.pendingEmptySceneCandidate = null;
181
+ this.structuredObserversAttached = false;
141
182
  this.canWriteToRoom = false;
142
183
  this.waitingForAuthoritativeSync = false;
143
184
  this.connectionState = EXCALIDRAW_ROOM_CONNECTION_STATE.CLOSED;
144
185
  this.hasAuthoritativeSync = false;
145
186
  this.applyingSharedSnapshotDepth = 0;
146
187
  this.suppressStructuredSceneUpdateDepth = 0;
188
+ this.commentThreadStore = new CommentThreadStore({
189
+ canWrite: () => this.canWriteToRoom,
190
+ createAnchor: createDiagramCommentAnchor,
191
+ getDoc: () => this.ydoc,
192
+ getEditorState: () => null,
193
+ getLocalUser: () => this.getLocalUser(),
194
+ onCommentsChange: (threads) => this.onCommentThreadsChange(threads),
195
+ resolveThread: (thread) => thread,
196
+ });
147
197
 
148
198
  this.handleAwarenessChange = () => {
149
199
  this.onCollaboratorsChange(buildCollaboratorsMap(this.awareness));
@@ -177,6 +227,25 @@ export class ExcalidrawRoomClient {
177
227
  return this.localUser;
178
228
  }
179
229
 
230
+ getCommentThreads() {
231
+ return this.commentThreadStore.getCommentThreads();
232
+ }
233
+
234
+ createCommentThread({ body, element } = {}) {
235
+ return this.commentThreadStore.createCommentThread({
236
+ anchor: element,
237
+ body,
238
+ });
239
+ }
240
+
241
+ replyToCommentThread(threadId, body) {
242
+ return this.commentThreadStore.replyToCommentThread(threadId, body);
243
+ }
244
+
245
+ deleteCommentThread(threadId) {
246
+ return this.commentThreadStore.deleteCommentThread(threadId);
247
+ }
248
+
180
249
  getConnectionState() {
181
250
  return this.connectionState;
182
251
  }
@@ -268,7 +337,16 @@ export class ExcalidrawRoomClient {
268
337
  return this.localUser;
269
338
  }
270
339
 
271
- async connect({ initialUser = null } = {}) {
340
+ async connect(options = {}) {
341
+ try {
342
+ return await this.connectInternal(options);
343
+ } catch (error) {
344
+ this.disconnect();
345
+ throw error;
346
+ }
347
+ }
348
+
349
+ async connectInternal({ initialUser = null } = {}) {
272
350
  this.localUser = initialUser;
273
351
  this.setConnectionState(EXCALIDRAW_ROOM_CONNECTION_STATE.CONNECTING);
274
352
 
@@ -277,6 +355,7 @@ export class ExcalidrawRoomClient {
277
355
  this.lastSceneJson = JSON.stringify(scene);
278
356
  this.hasAuthoritativeSync = true;
279
357
  this.setConnectionState(EXCALIDRAW_ROOM_CONNECTION_STATE.AUTHORITATIVE);
358
+ this.onCommentThreadsChange([]);
280
359
  return scene;
281
360
  }
282
361
 
@@ -367,6 +446,11 @@ export class ExcalidrawRoomClient {
367
446
  this.roomElements.observeDeep(this.handleStructuredSceneUpdate);
368
447
  this.roomFiles.observeDeep(this.handleStructuredSceneUpdate);
369
448
  this.roomAppState.observe(this.handleStructuredSceneUpdate);
449
+ this.structuredObserversAttached = true;
450
+ this.commentThreadStore.bind({
451
+ commentThreads: this.ydoc.getArray('comments'),
452
+ ydoc: this.ydoc,
453
+ });
370
454
  if (usedApiFallback && this.canWriteToRoom) {
371
455
  this.commitSceneJson(this.lastSceneJson, {
372
456
  origin: 'excalidraw-api-fallback',
@@ -501,7 +585,7 @@ export class ExcalidrawRoomClient {
501
585
 
502
586
  mergeLocallyChangedAppState(appState, baseSceneJson) {
503
587
  const baseScene = parseSceneJson(baseSceneJson);
504
- const roomScene = parseSceneJson(this.getStructuredSceneJson() || this.lastSceneJson);
588
+ const roomScene = parseSceneJson(this.lastSceneJson || this.getStructuredSceneJson());
505
589
  const nextAppState = {
506
590
  ...roomScene.appState,
507
591
  };
@@ -523,7 +607,7 @@ export class ExcalidrawRoomClient {
523
607
  elements,
524
608
  files,
525
609
  } = {}) {
526
- const roomSceneJson = this.getStructuredSceneJson() || this.lastSceneJson || baseSceneJson;
610
+ const roomSceneJson = this.lastSceneJson || this.getStructuredSceneJson() || baseSceneJson;
527
611
  const roomScene = parseSceneJson(roomSceneJson);
528
612
  const nextAppState = this.mergeLocallyChangedAppState(appState, baseSceneJson);
529
613
  const roomElementsById = new Map(roomScene.elements.map((element) => [element.id, element]));
@@ -533,7 +617,7 @@ export class ExcalidrawRoomClient {
533
617
  const nextFiles = files && typeof files === 'object' ? files : {};
534
618
  const changedFiles = Object.fromEntries(
535
619
  Object.entries(nextFiles).filter(([fileId, file]) => (
536
- stableJson(file) !== stableJson(roomScene.files?.[fileId])
620
+ !areExcalidrawFilesEqual(file, roomScene.files?.[fileId])
537
621
  )),
538
622
  );
539
623
  const appStateChanged = (
@@ -869,10 +953,15 @@ export class ExcalidrawRoomClient {
869
953
  this.lastSelectedIdsSignature = '';
870
954
  this.pendingEmptySceneCandidate = null;
871
955
 
872
- this.roomMeta?.unobserve(this.handleStructuredSceneUpdate);
873
- this.roomElements?.unobserveDeep(this.handleStructuredSceneUpdate);
874
- this.roomFiles?.unobserveDeep(this.handleStructuredSceneUpdate);
875
- this.roomAppState?.unobserve(this.handleStructuredSceneUpdate);
956
+ if (this.structuredObserversAttached) {
957
+ this.roomMeta?.unobserve(this.handleStructuredSceneUpdate);
958
+ this.roomElements?.unobserveDeep(this.handleStructuredSceneUpdate);
959
+ this.roomFiles?.unobserveDeep(this.handleStructuredSceneUpdate);
960
+ this.roomAppState?.unobserve(this.handleStructuredSceneUpdate);
961
+ this.structuredObserversAttached = false;
962
+ }
963
+
964
+ this.commentThreadStore.unbind();
876
965
 
877
966
  if (this.awareness) {
878
967
  this.awareness.off('change', this.handleAwarenessChange);
@@ -74,6 +74,11 @@ export class GitApiClient {
74
74
  return parseApiResponse(response, 'Failed to load historical file preview');
75
75
  }
76
76
 
77
+ getFileAttachmentUrl({ hash, path } = {}) {
78
+ const params = createSearchParams({ hash, path });
79
+ return resolveApiUrl(`/git/file-attachment?${params.toString()}`);
80
+ }
81
+
77
82
  async stageFile({ path, requestId = null } = {}) {
78
83
  return this.#postJson('/git/stage', { path }, { requestId, fallbackError: 'Failed to stage file' });
79
84
  }
@@ -111,6 +111,7 @@ export class LobbyPresence {
111
111
 
112
112
  this.provider.on('status', ({ status }) => {
113
113
  this._connected = status === 'connected';
114
+ this._emitChange();
114
115
  });
115
116
 
116
117
  this.provider.on('sync', (isSynced) => {
@@ -42,6 +42,45 @@ export class VaultApiClient {
42
42
  return parseApiResponse(response, 'Failed to read file');
43
43
  }
44
44
 
45
+ async readBacklinks(filePath, { signal } = {}) {
46
+ const response = await fetch(
47
+ resolveApiUrl(`/backlinks?file=${encodeURIComponent(filePath)}`),
48
+ { signal },
49
+ );
50
+ const data = await parseApiResponse(response, 'Failed to load backlinks');
51
+ return Array.isArray(data.backlinks) ? data.backlinks : [];
52
+ }
53
+
54
+ async syncStructurizrWorkspace({ path, source }) {
55
+ const response = await fetch(resolveApiUrl('/structurizr/sync'), {
56
+ body: JSON.stringify({
57
+ path,
58
+ source,
59
+ }),
60
+ headers: {
61
+ 'Content-Type': 'application/json',
62
+ },
63
+ method: 'POST',
64
+ });
65
+ return parseApiResponse(response, 'Failed to sync Structurizr workspace');
66
+ }
67
+
68
+ async renderSvg(source) {
69
+ const response = await fetch(resolveApiUrl('/plantuml/render'), {
70
+ body: JSON.stringify({ source }),
71
+ headers: {
72
+ 'Content-Type': 'application/json',
73
+ },
74
+ method: 'POST',
75
+ });
76
+ const data = await parseApiResponse(response, 'Failed to render PlantUML');
77
+ if (!data.ok || typeof data.svg !== 'string') {
78
+ throw new Error(data.error || 'Failed to render PlantUML');
79
+ }
80
+
81
+ return data.svg;
82
+ }
83
+
45
84
  async searchText({ limit = 50, query = '', signal = null } = {}) {
46
85
  const searchParams = new URLSearchParams({
47
86
  limit: String(limit),
@@ -236,6 +275,18 @@ export class VaultApiClient {
236
275
  return parseApiResponse(response, 'Failed to upload image');
237
276
  }
238
277
 
278
+ async uploadFile({ file, path, requestId = null }) {
279
+ const response = await fetch(resolveApiUrl('/file/upload'), {
280
+ body: file,
281
+ headers: createRequestHeaders(requestId, {
282
+ 'Content-Type': file?.type || 'application/octet-stream',
283
+ 'X-CollabMD-File-Path': encodeHeaderMetadata(path),
284
+ }),
285
+ method: 'POST',
286
+ });
287
+ return parseApiResponse(response, 'Failed to upload file');
288
+ }
289
+
239
290
  async downloadFile(path) {
240
291
  const fallbackFileName = getVaultPathLeaf(path) || 'download';
241
292
  return triggerDownload(resolveApiUrl(`/download/file?path=${encodeURIComponent(path)}`), {
@@ -43,11 +43,18 @@ function createNode(entry) {
43
43
  };
44
44
  }
45
45
 
46
- return {
46
+ const node = {
47
47
  name: entry.name,
48
48
  path: entry.path,
49
49
  type: entry.type,
50
50
  };
51
+
52
+ const mtimeMs = Number(entry.mtimeMs);
53
+ if (Number.isFinite(mtimeMs)) {
54
+ node.mtimeMs = mtimeMs;
55
+ }
56
+
57
+ return node;
51
58
  }
52
59
 
53
60
  function sortNodes(nodes = []) {
@@ -184,6 +191,11 @@ class WorkspaceTreeModel {
184
191
  node.name = nextNode.name;
185
192
  node.path = nextNode.path;
186
193
  node.type = nextNode.type;
194
+ if (nextNode.mtimeMs === undefined) {
195
+ delete node.mtimeMs;
196
+ } else {
197
+ node.mtimeMs = nextNode.mtimeMs;
198
+ }
187
199
  if (node.type === 'directory' && !Array.isArray(node.children)) {
188
200
  node.children = [];
189
201
  }
@@ -1,20 +1,14 @@
1
1
  import { stripVaultFileExtension } from '../../domain/file-kind.js';
2
2
  import { getVaultPathLeaf, getVaultPathParent } from '../domain/vault-paths.js';
3
+ import {
4
+ createCommentOverviewThread,
5
+ formatAnchorLabel,
6
+ } from './comment-ui/comment-ui-shared.js';
3
7
 
4
8
  function asArray(value) {
5
9
  return Array.isArray(value) ? value : [];
6
10
  }
7
11
 
8
- function formatLineLabel(anchor = {}) {
9
- const startLine = Number(anchor.startLine || 0);
10
- const endLine = Number(anchor.endLine || startLine);
11
- if (!Number.isFinite(startLine) || startLine <= 0) {
12
- return 'No source anchor';
13
- }
14
-
15
- return startLine === endLine ? `Line ${startLine}` : `Lines ${startLine}-${endLine}`;
16
- }
17
-
18
12
  function createOverviewState({ copy, title, tone = '' }) {
19
13
  const state = document.createElement('div');
20
14
  state.className = 'comment-overview-empty ui-empty-state ui-empty-state--compact';
@@ -80,7 +74,7 @@ export class CommentOverviewController {
80
74
  try {
81
75
  const payload = await this.vaultApiClient.readCommentOverview();
82
76
  this.errorMessage = '';
83
- this.setOverview(payload?.overview ?? payload ?? {});
77
+ this.setOverview(payload?.overview ?? payload ?? {}, { render: false });
84
78
  } catch (error) {
85
79
  console.error('[comments] Failed to load comment overview:', error.message);
86
80
  this.errorMessage = 'Unable to load open comments.';
@@ -95,7 +89,7 @@ export class CommentOverviewController {
95
89
  return this.refreshPromise;
96
90
  }
97
91
 
98
- setOverview(overview = {}) {
92
+ setOverview(overview = {}, { render = true } = {}) {
99
93
  this.errorMessage = '';
100
94
  this.overview = {
101
95
  files: asArray(overview.files),
@@ -105,7 +99,9 @@ export class CommentOverviewController {
105
99
  this.onOverviewChange?.(this.overview, {
106
100
  threadCounts: this.getThreadCounts(),
107
101
  });
108
- this.render();
102
+ if (render) {
103
+ this.render();
104
+ }
109
105
  }
110
106
 
111
107
  render() {
@@ -187,9 +183,15 @@ export class CommentOverviewController {
187
183
  }
188
184
 
189
185
  createThreadButton(filePath, thread) {
190
- const button = document.createElement('button');
191
- button.type = 'button';
192
- button.className = 'comment-overview-thread';
186
+ const messageCount = Number(thread.messageCount || 0);
187
+ const button = createCommentOverviewThread({
188
+ authorName: thread.latestMessage?.userName || thread.createdByName || 'Anonymous',
189
+ lineLabel: formatAnchorLabel(thread.anchor),
190
+ messageCount,
191
+ previewBody: thread.latestMessage?.bodyPreview || '',
192
+ quote: thread.anchor?.quote || 'Source anchored comment',
193
+ timestamp: this.formatTimestamp(thread.latestActivityAt),
194
+ });
193
195
  button.addEventListener('click', () => {
194
196
  this.onThreadSelect?.({
195
197
  anchor: thread.anchor,
@@ -197,43 +199,6 @@ export class CommentOverviewController {
197
199
  threadId: thread.id,
198
200
  });
199
201
  });
200
-
201
- const header = document.createElement('div');
202
- header.className = 'comment-overview-thread-header';
203
-
204
- const line = document.createElement('span');
205
- line.className = 'comment-overview-thread-line';
206
- line.textContent = formatLineLabel(thread.anchor);
207
-
208
- const meta = document.createElement('span');
209
- meta.className = 'comment-overview-thread-meta';
210
- meta.textContent = this.formatTimestamp(thread.latestActivityAt);
211
- header.append(line, meta);
212
-
213
- const body = document.createElement('div');
214
- body.className = 'comment-overview-thread-body';
215
-
216
- const quote = document.createElement('span');
217
- quote.className = 'comment-overview-thread-quote';
218
- quote.textContent = thread.anchor?.quote || 'Source anchored comment';
219
-
220
- const preview = document.createElement('p');
221
- preview.className = 'comment-overview-thread-preview';
222
- preview.textContent = thread.latestMessage?.bodyPreview || '';
223
- body.append(preview, quote);
224
-
225
- const footer = document.createElement('div');
226
- footer.className = 'comment-overview-thread-footer';
227
- const author = document.createElement('span');
228
- author.className = 'comment-overview-thread-author';
229
- author.textContent = thread.latestMessage?.userName || thread.createdByName || 'Anonymous';
230
- const replies = document.createElement('span');
231
- replies.className = 'comment-overview-thread-message-count';
232
- const messageCount = Number(thread.messageCount || 0);
233
- replies.textContent = `${messageCount} message${messageCount === 1 ? '' : 's'}`;
234
- footer.append(author, replies);
235
-
236
- button.append(header, body, footer);
237
202
  return button;
238
203
  }
239
204
 
@@ -360,17 +360,6 @@ function createThreadElement(thread) {
360
360
  const actions = document.createElement('div');
361
361
  actions.className = 'ui-record-actions comment-thread-card-actions';
362
362
 
363
- const jump = document.createElement('button');
364
- jump.type = 'button';
365
- jump.className = buttonClassNames({
366
- variant: 'ghost',
367
- size: 'compact',
368
- pill: true,
369
- extra: ['ui-action-pill', 'comment-thread-card-action'],
370
- });
371
- jump.textContent = 'Jump';
372
- jump.addEventListener('click', () => this.onNavigateToLine?.(thread.anchor?.startLine ?? 1));
373
-
374
363
  const reply = document.createElement('button');
375
364
  reply.type = 'button';
376
365
  reply.className = buttonClassNames({
@@ -409,7 +398,7 @@ function createThreadElement(thread) {
409
398
  await this.onResolveThread?.(thread.id);
410
399
  });
411
400
 
412
- actions.append(jump, reply, resolve);
401
+ actions.append(reply, resolve);
413
402
 
414
403
  thread.messages.forEach((message, index) => {
415
404
  article.appendChild(this.createMessageElement(thread, message, {