collabmd 0.1.44 → 0.1.45

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 (170) hide show
  1. package/dist/client/assets/{chunk-AXR3VHVW-0GTk_evH.js → chunk-ERSWUTIN-ME9RVNel.js} +3 -3
  2. package/dist/client/assets/{chunk-CZQZEIFA-DT3N9fo7.js → chunk-YGJVY4SA-CjSe7Bx7.js} +2 -2
  3. package/dist/client/assets/{comment-threads-D-iKphIp.js → comment-threads-DDegmaFO.js} +1 -1
  4. package/dist/client/assets/drawio-editor-ClE8jpoG.js +9 -0
  5. package/dist/client/assets/drawioEditor-CXaTM9Ds.js +2 -0
  6. package/dist/client/assets/editor-session-GTwZS6Df.js +16 -0
  7. package/dist/client/assets/embedded-editor-base-CiNEc8vy.css +1 -0
  8. package/dist/client/assets/{en-GADW4C6J-DQ6zyCEM.js → en-7NFCYGKX-BrgYZAEe.js} +1 -1
  9. package/dist/client/assets/{excalidraw-editor-BHu6k8d4.js → excalidraw-editor-BZVM81BO.js} +2 -2
  10. package/dist/client/assets/excalidraw-editor-CyGwOhLz.css +1 -0
  11. package/dist/client/assets/excalidraw-scene-C8n9CHsK.js +176 -0
  12. package/dist/client/assets/excalidrawEditor-DxJanCze.js +2 -0
  13. package/dist/client/assets/export-page-CUfws2T-.js +39 -0
  14. package/dist/client/assets/exportDocument-BCuxqXDY.js +2 -0
  15. package/dist/client/assets/exportDocument-DV-OX74E.css +1 -0
  16. package/dist/client/assets/image-QXRYHUNP-lfTHE3bb.js +1 -0
  17. package/dist/client/assets/index-C3YnUgMD.css +1 -0
  18. package/dist/client/assets/index-D2wXraJ1.js +2 -0
  19. package/dist/client/assets/main-DtiGz7uI.js +795 -0
  20. package/dist/client/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
  21. package/dist/client/assets/preview-diagram-utils-txBCoW8d.js +1 -0
  22. package/dist/client/assets/quick-switcher-controller-shW_6vPY.js +12 -0
  23. package/dist/client/assets/room-CQLoqtMo.js +1 -0
  24. package/dist/client/assets/runtime-config-DSGmGE9V.js +1 -0
  25. package/dist/client/assets/vault-api-client-C6bcGwy-.js +5 -0
  26. package/dist/client/assets/{yjs-provider-reset-guard-Cox4nxwu.js → yjs-provider-reset-guard-DE0vINDP.js} +1 -1
  27. package/dist/client/drawio-editor.html +3 -2
  28. package/dist/client/excalidraw-editor.html +3 -2
  29. package/dist/client/export-document.html +4 -3
  30. package/dist/client/index.html +97 -27
  31. package/package.json +10 -12
  32. package/src/client/app/drawio-editor-entry.js +0 -3
  33. package/src/client/app/drawio-editor.html +1 -0
  34. package/src/client/app/excalidraw-editor-entry.js +0 -3
  35. package/src/client/app/excalidraw-editor.html +1 -0
  36. package/src/client/app/export-document-entry.js +0 -3
  37. package/src/client/app/export-document.html +1 -0
  38. package/src/client/app/index.html +94 -24
  39. package/src/client/app/main-entry.js +0 -5
  40. package/src/client/application/app-shell/chat-feature.js +180 -124
  41. package/src/client/application/app-shell/comments-feature.js +1 -1
  42. package/src/client/application/app-shell/git-feature.js +4 -9
  43. package/src/client/application/app-shell/presence-feature.js +16 -10
  44. package/src/client/application/app-shell/ui-feature-shell.js +33 -15
  45. package/src/client/application/app-shell/ui-feature-sidebar.js +74 -1
  46. package/src/client/application/app-shell/ui-feature-tab-activity.js +2 -2
  47. package/src/client/application/app-shell/workspace-feature.js +22 -83
  48. package/src/client/application/app-shell-elements.js +7 -1
  49. package/src/client/application/diagram-chrome.js +161 -166
  50. package/src/client/application/diagram-preview-export.js +7 -34
  51. package/src/client/application/diagram-preview-hydrator.js +1 -19
  52. package/src/client/application/markdown-frontmatter.js +0 -2
  53. package/src/client/application/mermaid-preview-hydrator.js +1 -14
  54. package/src/client/application/plantuml-preview-hydrator.js +0 -10
  55. package/src/client/application/preview-diagram-utils.js +31 -2
  56. package/src/client/application/preview-render-compiler.js +2 -1
  57. package/src/client/application/preview-renderer.js +2 -2
  58. package/src/client/application/quick-switcher-loader.js +3 -1
  59. package/src/client/application/workspace-preview-controller.js +0 -3
  60. package/src/client/bootstrap/collabmd-app-shell.js +125 -39
  61. package/src/client/domain/excalidraw-collaboration.js +0 -2
  62. package/src/client/domain/room.js +38 -0
  63. package/src/client/export/export-pipeline.js +6 -34
  64. package/src/client/infrastructure/auth-client.js +4 -10
  65. package/src/client/infrastructure/browser-notification-port.js +33 -20
  66. package/src/client/infrastructure/browser-preferences-port.js +36 -9
  67. package/src/client/infrastructure/editor-view-adapter.js +87 -3
  68. package/src/client/infrastructure/lobby-presence.js +1 -0
  69. package/src/client/infrastructure/vault-api-client.js +25 -0
  70. package/src/client/infrastructure/workspace-sync-client.js +13 -1
  71. package/src/client/presentation/comment-overview-controller.js +18 -53
  72. package/src/client/presentation/comment-ui/comment-ui-card.js +1 -12
  73. package/src/client/presentation/comment-ui/comment-ui-layout.js +242 -219
  74. package/src/client/presentation/comment-ui/comment-ui-render.js +32 -45
  75. package/src/client/presentation/comment-ui/comment-ui-shared.js +65 -14
  76. package/src/client/presentation/comment-ui-controller.js +5 -1
  77. package/src/client/presentation/file-action-controller.js +32 -0
  78. package/src/client/presentation/file-explorer-controller.js +33 -3
  79. package/src/client/presentation/file-explorer-view.js +49 -5
  80. package/src/client/presentation/file-tree-state.js +10 -4
  81. package/src/client/presentation/quick-switcher-controller.js +243 -72
  82. package/src/client/presentation/quick-switcher-text-search.js +2 -2
  83. package/src/client/presentation/toast-controller.js +3 -0
  84. package/src/client/styles/components/feedback.css +6 -0
  85. package/src/client/styles/features/collaboration-chat.css +49 -53
  86. package/src/client/styles/features/collaboration-presence.css +9 -7
  87. package/src/client/styles/features/comment-card.css +25 -11
  88. package/src/client/styles/features/comment-overlays.css +0 -4
  89. package/src/client/styles/features/comment-overview.css +44 -21
  90. package/src/client/styles/features/comments-drawer.css +2 -100
  91. package/src/client/styles/features/diagram-preview.css +5 -18
  92. package/src/client/styles/features/embedded-preview.css +8 -33
  93. package/src/client/styles/features/quick-switcher.css +8 -0
  94. package/src/client/styles/foundation/tokens.css +1 -0
  95. package/src/client/styles/layout/editor-page.css +40 -0
  96. package/src/client/styles/layout/responsive.css +31 -1
  97. package/src/client/styles/layout/shell.css +25 -6
  98. package/src/client/styles/layout/sidebar.css +47 -15
  99. package/src/client/styles/overrides/codemirror.css +217 -15
  100. package/src/server/application/workspace-reconciliation.js +1 -1
  101. package/src/server/create-app-server.js +2 -3
  102. package/src/server/domain/bases/base-query-service.js +1 -77
  103. package/src/server/domain/comment-overview.js +10 -3
  104. package/src/server/domain/docx-exporter.js +24 -33
  105. package/src/server/domain/ripgrep-search-service.js +3 -1
  106. package/src/server/domain/workspace-state.js +32 -11
  107. package/src/server/infrastructure/git/diff-service.js +24 -13
  108. package/src/server/infrastructure/git/git-service.js +40 -10
  109. package/src/server/infrastructure/git/history-service.js +42 -29
  110. package/src/server/infrastructure/git/path-utils.js +1 -1
  111. package/src/server/infrastructure/git/responses.js +0 -95
  112. package/src/server/infrastructure/github/github-app-client.js +22 -15
  113. package/src/server/infrastructure/http/create-request-handler.js +2 -8
  114. package/src/server/infrastructure/http/create-vault-api-command-handler.js +5 -5
  115. package/src/server/infrastructure/http/http-errors.js +7 -1
  116. package/dist/client/assets/bases-preview-controller-2pLHk0uk.js +0 -143
  117. package/dist/client/assets/button-BCEiw4Rw.js +0 -1
  118. package/dist/client/assets/diagram-action-icons-aa_KOv9h.js +0 -36
  119. package/dist/client/assets/drawio-editor-DDvFoarP.js +0 -9
  120. package/dist/client/assets/drawio-embed-controller-BD1GTRwn.js +0 -1
  121. package/dist/client/assets/drawioEditor-Bu5grlH5.js +0 -2
  122. package/dist/client/assets/editor-session-DGlpiAsK.js +0 -16
  123. package/dist/client/assets/embedded-editor-base-C9roKYpW.css +0 -1
  124. package/dist/client/assets/excalidraw-editor-Cr_184EF.css +0 -1
  125. package/dist/client/assets/excalidraw-embed-controller-y2fenN_g.js +0 -1
  126. package/dist/client/assets/excalidraw-embed-reconciler-DGiagExg.js +0 -1
  127. package/dist/client/assets/excalidraw-scene-BvsQ6mLP.js +0 -176
  128. package/dist/client/assets/excalidrawEditor-4vFE--Uw.js +0 -2
  129. package/dist/client/assets/export-page-Cby8JzPO.js +0 -39
  130. package/dist/client/assets/exportDocument-C9v-bc7I.css +0 -1
  131. package/dist/client/assets/exportDocument-CRHCfsdo.js +0 -2
  132. package/dist/client/assets/file-history-view-controller-BDjEV9xJ.js +0 -112
  133. package/dist/client/assets/git-diff-view-controller-DgdM1aqb.js +0 -150
  134. package/dist/client/assets/git-panel-controller-BDTR2HVp.js +0 -207
  135. package/dist/client/assets/image-XGD7JQR6-XDUWHRpO.js +0 -1
  136. package/dist/client/assets/index-7xOyXDhP.css +0 -1
  137. package/dist/client/assets/index-Bz1KuQ6y.js +0 -2
  138. package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  139. package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  140. package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  141. package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  142. package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  143. package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  144. package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  145. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  146. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  147. package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  148. package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  149. package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  150. package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  151. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  152. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  153. package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  154. package/dist/client/assets/main-Ci07m9oP.js +0 -152
  155. package/dist/client/assets/preview-diagram-utils-n_bbJA9V.js +0 -1
  156. package/dist/client/assets/quick-switcher-controller-CoiYvllm.js +0 -12
  157. package/dist/client/assets/room-CDrmIdDt.js +0 -1
  158. package/dist/client/assets/runtime-config-CDgeuPWN.js +0 -1
  159. package/dist/client/assets/runtime-config-loader-BHGniHP5.js +0 -1
  160. package/dist/client/assets/runtime-paths-DvpihCEq.js +0 -1
  161. package/dist/client/assets/segmented-control-DJrHSz8M.js +0 -1
  162. package/dist/client/assets/vault-api-client-sLB2bo56.js +0 -1
  163. package/dist/client/assets/vault-paths-CVkuh0j-.js +0 -5
  164. package/src/client/bootstrap/lazy-controller-feature.js +0 -433
  165. package/src/client/infrastructure/backlinks-api-client.js +0 -16
  166. package/src/client/infrastructure/plantuml-api-client.js +0 -19
  167. package/src/client/infrastructure/runtime-config-loader.js +0 -56
  168. package/src/server/infrastructure/git/command-runner.js +0 -49
  169. package/src/server/infrastructure/git/errors.js +0 -8
  170. package/src/server/infrastructure/http/create-esm-proxy-handler.js +0 -130
@@ -1,36 +1,49 @@
1
1
  export class BrowserNotificationPort {
2
- constructor({
3
- NotificationImpl = globalThis.Notification,
4
- focusWindow = () => globalThis.window?.focus?.(),
5
- } = {}) {
6
- this.NotificationImpl = NotificationImpl;
7
- this.focusWindow = focusWindow;
2
+ getApi() {
3
+ return typeof globalThis.Notification === 'function'
4
+ ? globalThis.Notification
5
+ : null;
8
6
  }
9
7
 
10
8
  getPermission() {
11
- return typeof this.NotificationImpl === 'function'
12
- ? this.NotificationImpl.permission
13
- : 'unsupported';
9
+ return this.getApi()?.permission ?? 'unsupported';
14
10
  }
15
11
 
16
12
  async requestPermission() {
17
- return typeof this.NotificationImpl === 'function'
18
- ? this.NotificationImpl.requestPermission()
19
- : 'unsupported';
13
+ const api = this.getApi();
14
+ if (!api || typeof api.requestPermission !== 'function') {
15
+ return 'unsupported';
16
+ }
17
+
18
+ if (api.permission !== 'default') {
19
+ return api.permission;
20
+ }
21
+
22
+ try {
23
+ return await api.requestPermission();
24
+ } catch {
25
+ return api.permission ?? 'default';
26
+ }
20
27
  }
21
28
 
22
- createNotification({ body, onClick, tag, title }) {
23
- if (typeof this.NotificationImpl !== 'function') {
29
+ show({ body, onClick, tag, title } = {}) {
30
+ const api = this.getApi();
31
+ if (!api || api.permission !== 'granted' || !body) {
24
32
  return null;
25
33
  }
26
34
 
27
- const notification = new this.NotificationImpl(title, { body, tag });
28
- if (typeof onClick === 'function') {
29
- notification.addEventListener('click', () => {
30
- this.focusWindow();
31
- onClick();
35
+ try {
36
+ const notification = new api(title, {
37
+ body,
38
+ renotify: true,
39
+ tag,
32
40
  });
41
+ if (onClick) {
42
+ notification.onclick = onClick;
43
+ }
44
+ return notification;
45
+ } catch {
46
+ return null;
33
47
  }
34
- return notification;
35
48
  }
36
49
  }
@@ -15,16 +15,20 @@ function writeStorage(storage, key, value) {
15
15
  }
16
16
  }
17
17
 
18
+ const MAX_RECENT_FILES = 20;
19
+
18
20
  export class BrowserPreferencesPort {
19
21
  constructor({
20
- chatNotificationsKey,
22
+ fileTreeShowExtensionsKey,
21
23
  lineWrappingKey,
24
+ recentFilesKey = 'collabmd-recent-files',
22
25
  sidebarVisibleKey,
23
26
  userNameKey,
24
27
  storage = globalThis.localStorage,
25
28
  }) {
26
- this.chatNotificationsKey = chatNotificationsKey;
29
+ this.fileTreeShowExtensionsKey = fileTreeShowExtensionsKey;
27
30
  this.lineWrappingKey = lineWrappingKey;
31
+ this.recentFilesKey = recentFilesKey;
28
32
  this.sidebarVisibleKey = sidebarVisibleKey;
29
33
  this.storage = storage;
30
34
  this.userNameKey = userNameKey;
@@ -38,6 +42,14 @@ export class BrowserPreferencesPort {
38
42
  writeStorage(this.storage, this.userNameKey, name);
39
43
  }
40
44
 
45
+ getFileTreeShowExtensions() {
46
+ return readStorage(this.storage, this.fileTreeShowExtensionsKey, null) === 'true';
47
+ }
48
+
49
+ setFileTreeShowExtensions(showFileExtensions) {
50
+ writeStorage(this.storage, this.fileTreeShowExtensionsKey, showFileExtensions ? 'true' : 'false');
51
+ }
52
+
41
53
  getLineWrappingEnabled() {
42
54
  return readStorage(this.storage, this.lineWrappingKey, null) !== 'false';
43
55
  }
@@ -46,6 +58,28 @@ export class BrowserPreferencesPort {
46
58
  writeStorage(this.storage, this.lineWrappingKey, String(enabled));
47
59
  }
48
60
 
61
+ getRecentFiles() {
62
+ const stored = readStorage(this.storage, this.recentFilesKey, '[]');
63
+ try {
64
+ const recentFiles = JSON.parse(stored);
65
+ return Array.isArray(recentFiles)
66
+ ? [...new Set(recentFiles.filter((filePath) => typeof filePath === 'string' && filePath))]
67
+ : [];
68
+ } catch {
69
+ return [];
70
+ }
71
+ }
72
+
73
+ recordRecentFile(filePath) {
74
+ if (!filePath) return;
75
+
76
+ const recentFiles = [
77
+ filePath,
78
+ ...this.getRecentFiles().filter((recentFilePath) => recentFilePath !== filePath),
79
+ ];
80
+ writeStorage(this.storage, this.recentFilesKey, JSON.stringify(recentFiles.slice(0, MAX_RECENT_FILES)));
81
+ }
82
+
49
83
  getSidebarVisible() {
50
84
  return readStorage(this.storage, this.sidebarVisibleKey, null);
51
85
  }
@@ -54,11 +88,4 @@ export class BrowserPreferencesPort {
54
88
  writeStorage(this.storage, this.sidebarVisibleKey, showSidebar ? 'true' : 'false');
55
89
  }
56
90
 
57
- getChatNotificationsEnabled() {
58
- return readStorage(this.storage, this.chatNotificationsKey, null) === 'true';
59
- }
60
-
61
- setChatNotificationsEnabled(enabled) {
62
- writeStorage(this.storage, this.chatNotificationsKey, String(enabled));
63
- }
64
91
  }
@@ -20,7 +20,14 @@ import {
20
20
  syntaxHighlighting,
21
21
  } from '@codemirror/language';
22
22
  import { languages } from '@codemirror/language-data';
23
- import { highlightSelectionMatches, openSearchPanel, searchKeymap } from '@codemirror/search';
23
+ import {
24
+ findNext,
25
+ getSearchQuery,
26
+ highlightSelectionMatches,
27
+ openSearchPanel,
28
+ searchKeymap,
29
+ searchPanelOpen,
30
+ } from '@codemirror/search';
24
31
  import { Compartment, EditorSelection, EditorState, Prec, StateEffect, StateField } from '@codemirror/state';
25
32
  import { oneDark } from '@codemirror/theme-one-dark';
26
33
  import {
@@ -54,10 +61,82 @@ const REMOTE_UPDATE_FLASH_DURATION_MS = 1350;
54
61
  const REMOTE_UPDATE_CARET_MAX_LENGTH = 160;
55
62
  const RECENT_LOCAL_INPUT_WINDOW_MS = 900;
56
63
  const TASK_LIST_MARKER_PATTERN = /^(\s*(?:>\s*)*(?:[-+*]|\d+[.)])\s+\[)( |x|X)(\])(\s.*)?$/;
64
+
65
+ function openSearchPanelPreservingViewport(view) {
66
+ const scrollTop = view.scrollDOM.scrollTop;
67
+ const selection = view.state.selection;
68
+ const applied = openSearchPanel(view);
69
+
70
+ if (applied) {
71
+ requestAnimationFrame(() => {
72
+ if (view.dom.isConnected && view.state.selection.eq(selection)) {
73
+ view.scrollDOM.scrollTop = scrollTop;
74
+ }
75
+ });
76
+ }
77
+
78
+ return applied;
79
+ }
80
+
81
+ function getSearchQueryKey(query) {
82
+ return JSON.stringify([query.search, query.caseSensitive, query.regexp, query.wholeWord]);
83
+ }
84
+
85
+ function createSearchNavigationListener() {
86
+ let lastQueryKey = '';
87
+ let panelWasOpen = false;
88
+ let pendingFrame = 0;
89
+
90
+ return EditorView.updateListener.of((update) => {
91
+ const panelOpen = searchPanelOpen(update.state);
92
+ const query = getSearchQuery(update.state);
93
+ const queryKey = getSearchQueryKey(query);
94
+ const shouldReveal = panelOpen
95
+ && query.valid
96
+ && (!panelWasOpen || queryKey !== lastQueryKey);
97
+
98
+ panelWasOpen = panelOpen;
99
+ lastQueryKey = queryKey;
100
+
101
+ if (!shouldReveal) {
102
+ return;
103
+ }
104
+
105
+ if (pendingFrame) {
106
+ cancelAnimationFrame(pendingFrame);
107
+ }
108
+
109
+ pendingFrame = requestAnimationFrame(() => {
110
+ pendingFrame = 0;
111
+ if (!update.view.dom.isConnected) {
112
+ return;
113
+ }
114
+
115
+ const currentQuery = getSearchQuery(update.view.state);
116
+ if (
117
+ !searchPanelOpen(update.view.state)
118
+ || !currentQuery.valid
119
+ || getSearchQueryKey(currentQuery) !== queryKey
120
+ ) {
121
+ return;
122
+ }
123
+
124
+ const searchInput = update.view.dom.querySelector('.cm-search input[name="search"]');
125
+ const inputSelection = searchInput === update.view.root.activeElement && searchInput
126
+ ? { end: searchInput.selectionEnd, start: searchInput.selectionStart }
127
+ : null;
128
+ findNext(update.view);
129
+ if (inputSelection && searchInput?.setSelectionRange) {
130
+ searchInput.setSelectionRange(inputSelection.start, inputSelection.end);
131
+ }
132
+ });
133
+ });
134
+ }
135
+
57
136
  const EDITOR_COMMANDS = Object.freeze({
58
137
  indentLess,
59
138
  indentMore,
60
- openSearch: openSearchPanel,
139
+ openSearch: openSearchPanelPreservingViewport,
61
140
  redo,
62
141
  undo,
63
142
  });
@@ -398,11 +477,16 @@ export class EditorViewAdapter {
398
477
  crosshairCursor(),
399
478
  highlightActiveLine(),
400
479
  highlightSelectionMatches(),
480
+ createSearchNavigationListener(),
401
481
  keymap.of([
402
482
  ...closeBracketsKeymap,
403
483
  ...defaultKeymap,
404
484
  ...historyKeymap,
405
- ...searchKeymap,
485
+ ...searchKeymap.map((binding) => (
486
+ binding.run === openSearchPanel
487
+ ? { ...binding, run: openSearchPanelPreservingViewport }
488
+ : binding
489
+ )),
406
490
  ...foldKeymap,
407
491
  indentWithTab,
408
492
  ]),
@@ -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,31 @@ 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 renderSvg(source) {
55
+ const response = await fetch(resolveApiUrl('/plantuml/render'), {
56
+ body: JSON.stringify({ source }),
57
+ headers: {
58
+ 'Content-Type': 'application/json',
59
+ },
60
+ method: 'POST',
61
+ });
62
+ const data = await parseApiResponse(response, 'Failed to render PlantUML');
63
+ if (!data.ok || typeof data.svg !== 'string') {
64
+ throw new Error(data.error || 'Failed to render PlantUML');
65
+ }
66
+
67
+ return data.svg;
68
+ }
69
+
45
70
  async searchText({ limit = 50, query = '', signal = null } = {}) {
46
71
  const searchParams = new URLSearchParams({
47
72
  limit: String(limit),
@@ -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, {