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
@@ -152,82 +152,6 @@ export class BaseQueryService {
152
152
  this.propertyCatalogCache = new Map();
153
153
  }
154
154
 
155
- get workspaceStateProvider() {
156
- return this.snapshotStore.workspaceStateProvider;
157
- }
158
-
159
- set workspaceStateProvider(value) {
160
- this.snapshotStore.workspaceStateProvider = value;
161
- }
162
-
163
- get workspaceStateSynchronizer() {
164
- return this.snapshotStore.workspaceStateSynchronizer;
165
- }
166
-
167
- set workspaceStateSynchronizer(value) {
168
- this.snapshotStore.workspaceStateSynchronizer = value;
169
- }
170
-
171
- get indexSnapshot() {
172
- return this.snapshotStore.indexSnapshot;
173
- }
174
-
175
- set indexSnapshot(value) {
176
- this.snapshotStore.indexSnapshot = value;
177
- }
178
-
179
- get lastWorkspaceState() {
180
- return this.snapshotStore.lastWorkspaceState;
181
- }
182
-
183
- set lastWorkspaceState(value) {
184
- this.snapshotStore.lastWorkspaceState = value;
185
- }
186
-
187
- async getWorkspaceState() {
188
- return this.snapshotStore.getWorkspaceState();
189
- }
190
-
191
- createSnapshotRow(...args) {
192
- return this.snapshotStore.createSnapshotRow(...args);
193
- }
194
-
195
- rebuildBacklinks(...args) {
196
- return this.snapshotStore.rebuildBacklinks(...args);
197
- }
198
-
199
- async buildIndexSnapshot(...args) {
200
- return this.snapshotStore.buildIndexSnapshot(...args);
201
- }
202
-
203
- async synchronizeWorkspaceState() {
204
- return this.snapshotStore.synchronizeWorkspaceState();
205
- }
206
-
207
- removeSnapshotPath(...args) {
208
- return this.snapshotStore.removeSnapshotPath(...args);
209
- }
210
-
211
- upsertSnapshotPath(...args) {
212
- return this.snapshotStore.upsertSnapshotPath(...args);
213
- }
214
-
215
- collectImpactedSourcesForMembershipChanges(...args) {
216
- return this.snapshotStore.collectImpactedSourcesForMembershipChanges(...args);
217
- }
218
-
219
- async refreshSnapshotRows(...args) {
220
- return this.snapshotStore.refreshSnapshotRows(...args);
221
- }
222
-
223
- async ensureIndexSnapshot(...args) {
224
- return this.snapshotStore.ensureIndexSnapshot(...args);
225
- }
226
-
227
- async initializeFromWorkspaceState(...args) {
228
- return this.snapshotStore.initializeFromWorkspaceState(...args);
229
- }
230
-
231
155
  async applyWorkspaceChange(...args) {
232
156
  const result = await this.snapshotStore.applyWorkspaceChange(...args);
233
157
  const scannedAt = this.snapshotStore.indexSnapshot?.scannedAt;
@@ -279,7 +203,7 @@ export class BaseQueryService {
279
203
  }
280
204
 
281
205
  const definition = this.getCachedDefinition(source === null ? `file:${basePath}` : `source:${sourcePath}:${basePath}`, baseSource);
282
- const snapshot = await this.ensureIndexSnapshot({
206
+ const snapshot = await this.snapshotStore.ensureIndexSnapshot({
283
207
  basePath,
284
208
  sourcePath,
285
209
  });
@@ -1,7 +1,14 @@
1
- import { summarizeCommentExcerpt } from '../../domain/comment-threads.js';
2
-
3
1
  const COMMENT_OVERVIEW_PREVIEW_MAX_LENGTH = 140;
4
2
 
3
+ function summarizeCommentMarkdown(value, maxLength = COMMENT_OVERVIEW_PREVIEW_MAX_LENGTH) {
4
+ const normalized = String(value ?? '').replace(/\r\n?/gu, '\n').trim();
5
+ if (normalized.length <= maxLength) {
6
+ return normalized;
7
+ }
8
+
9
+ return `${normalized.slice(0, Math.max(maxLength - 1, 1)).trimEnd()}…`;
10
+ }
11
+
5
12
  function asFiniteNumber(value) {
6
13
  return Number.isFinite(value) ? value : null;
7
14
  }
@@ -58,7 +65,7 @@ function createThreadSummary(thread) {
58
65
  latestActivityAt,
59
66
  latestMessage: latestMessage
60
67
  ? {
61
- bodyPreview: summarizeCommentExcerpt(latestMessage.body, COMMENT_OVERVIEW_PREVIEW_MAX_LENGTH),
68
+ bodyPreview: summarizeCommentMarkdown(latestMessage.body),
62
69
  createdAt: asFiniteNumber(latestMessage.createdAt) ?? latestActivityAt,
63
70
  userColor: asString(latestMessage.userColor),
64
71
  userName: asString(latestMessage.userName) || 'Anonymous',
@@ -1,4 +1,5 @@
1
1
  const DOCX_EXPORTER_CREATOR = 'CollabMD';
2
+ let converterPromise = null;
2
3
 
3
4
  function normalizeDocxBuffer(result) {
4
5
  if (Buffer.isBuffer(result)) {
@@ -16,47 +17,37 @@ function normalizeDocxBuffer(result) {
16
17
  throw new Error('DOCX exporter returned an unsupported payload');
17
18
  }
18
19
 
19
- export class DocxExporter {
20
- constructor({
21
- loadConverter = () => import('@turbodocx/html-to-docx'),
22
- } = {}) {
23
- this.loadConverter = loadConverter;
24
- this.converterPromise = null;
25
- }
26
-
27
- async ensureConverter() {
28
- if (this.converterPromise) {
29
- return this.converterPromise;
30
- }
31
-
32
- this.converterPromise = this.loadConverter().then((module) => {
20
+ async function ensureConverter() {
21
+ if (!converterPromise) {
22
+ converterPromise = import('@turbodocx/html-to-docx').then((module) => {
33
23
  const converter = module?.default ?? module?.HTMLToDOCX ?? module;
34
24
  if (typeof converter !== 'function') {
35
25
  throw new Error('DOCX converter failed to load');
36
26
  }
37
27
  return converter;
38
28
  });
39
- return this.converterPromise;
40
29
  }
41
30
 
42
- async render({
43
- html,
44
- title = '',
45
- } = {}) {
46
- const converter = await this.ensureConverter();
47
- const result = await converter(String(html ?? ''), null, {
48
- creator: DOCX_EXPORTER_CREATOR,
49
- font: 'Arial',
50
- footer: false,
51
- pageNumber: false,
52
- table: {
53
- row: {
54
- cantSplit: true,
55
- },
31
+ return converterPromise;
32
+ }
33
+
34
+ export async function renderDocx({
35
+ html,
36
+ title = '',
37
+ } = {}) {
38
+ const converter = await ensureConverter();
39
+ const result = await converter(String(html ?? ''), null, {
40
+ creator: DOCX_EXPORTER_CREATOR,
41
+ font: 'Arial',
42
+ footer: false,
43
+ pageNumber: false,
44
+ table: {
45
+ row: {
46
+ cantSplit: true,
56
47
  },
57
- title: String(title ?? ''),
58
- });
48
+ },
49
+ title: String(title ?? ''),
50
+ });
59
51
 
60
- return normalizeDocxBuffer(result);
61
- }
52
+ return normalizeDocxBuffer(result);
62
53
  }
@@ -106,7 +106,7 @@ function createRipgrepArgs(query, {
106
106
  '--max-filesize',
107
107
  String(maxFileSize || DEFAULT_MAX_FILE_SIZE),
108
108
  '--max-count',
109
- String(maxSnippetsPerFile),
109
+ String(Math.max(Number(maxSnippetsPerFile) || DEFAULT_MAX_SNIPPETS_PER_FILE, 1) + 1),
110
110
  '--glob',
111
111
  '!.git/**',
112
112
  '--glob',
@@ -171,6 +171,7 @@ export function parseRipgrepJson(stdout, {
171
171
  kind: getVaultFileKind(filePath) ?? 'text',
172
172
  matchCount: 0,
173
173
  snippets: [],
174
+ truncated: false,
174
175
  };
175
176
  filesByPath.set(filePath, fileGroup);
176
177
  files.push(fileGroup);
@@ -187,6 +188,7 @@ export function parseRipgrepJson(stdout, {
187
188
  matchCount += 1;
188
189
 
189
190
  if (fileGroup.snippets.length >= maxSnippetsPerFile) {
191
+ fileGroup.truncated = true;
190
192
  truncated = true;
191
193
  continue;
192
194
  }
@@ -42,6 +42,20 @@ export function createWorkspaceMetadata(pathValue, type, info = {}) {
42
42
  };
43
43
  }
44
44
 
45
+ function enrichWorkspaceEntries(entries, metadata) {
46
+ const enrichedEntries = new Map(entries);
47
+ metadata.forEach((entryMetadata, pathValue) => {
48
+ const entry = enrichedEntries.get(pathValue);
49
+ const mtimeMs = Number(entryMetadata?.mtimeMs);
50
+ if (!entry || !Number.isFinite(mtimeMs) || entry.mtimeMs === mtimeMs) {
51
+ return;
52
+ }
53
+
54
+ enrichedEntries.set(pathValue, { ...entry, mtimeMs });
55
+ });
56
+ return enrichedEntries;
57
+ }
58
+
45
59
  export function collectWorkspaceStateStats(entries = new Map(), metadata = new Map()) {
46
60
  const filePaths = [];
47
61
  const markdownPaths = [];
@@ -75,8 +89,11 @@ export function collectWorkspaceStateStats(entries = new Map(), metadata = new M
75
89
  export function createWorkspaceStateSnapshot(entries = new Map(), metadata = new Map(), {
76
90
  scannedAt = Date.now(),
77
91
  } = {}) {
78
- const normalizedEntries = entries instanceof Map ? entries : new Map(entries ?? []);
79
92
  const normalizedMetadata = metadata instanceof Map ? metadata : new Map(metadata ?? []);
93
+ const normalizedEntries = enrichWorkspaceEntries(
94
+ entries instanceof Map ? entries : new Map(entries ?? []),
95
+ normalizedMetadata,
96
+ );
80
97
  const stats = collectWorkspaceStateStats(normalizedEntries, normalizedMetadata);
81
98
 
82
99
  return {
@@ -385,21 +402,24 @@ export function createWorkspaceTree(entries = new Map()) {
385
402
  return;
386
403
  }
387
404
 
388
- if (entry?.type === 'directory' || entry?.nodeType === 'directory') {
389
- nodesByPath.set(normalizedPath, {
405
+ const mtimeMs = Number(entry?.mtimeMs);
406
+ const node = entry?.type === 'directory' || entry?.nodeType === 'directory'
407
+ ? {
390
408
  children: [],
391
409
  name: entry?.name ?? basename(normalizedPath),
392
410
  path: normalizedPath,
393
411
  type: 'directory',
394
- });
395
- return;
396
- }
412
+ }
413
+ : {
414
+ name: entry?.name ?? basename(normalizedPath),
415
+ path: normalizedPath,
416
+ type: entry?.type ?? getVaultTreeNodeType(normalizedPath),
417
+ };
397
418
 
398
- nodesByPath.set(normalizedPath, {
399
- name: entry?.name ?? basename(normalizedPath),
400
- path: normalizedPath,
401
- type: entry?.type ?? getVaultTreeNodeType(normalizedPath),
402
- });
419
+ if (node.type !== 'directory' && Number.isFinite(mtimeMs)) {
420
+ node.mtimeMs = mtimeMs;
421
+ }
422
+ nodesByPath.set(normalizedPath, node);
403
423
  });
404
424
 
405
425
  nodesByPath.forEach((node, pathValue) => {
@@ -423,6 +443,7 @@ export function workspaceEntriesEqual(left = {}, right = {}) {
423
443
  && left.nodeType === right.nodeType
424
444
  && left.parentPath === right.parentPath
425
445
  && left.path === right.path
446
+ && left.mtimeMs === right.mtimeMs
426
447
  && left.type === right.type
427
448
  );
428
449
  }
@@ -1,5 +1,5 @@
1
1
  import { countPatchLines, parseNumstatOutput, parseUnifiedDiff } from './parsers.js';
2
- import { createDiffResponse, createEmptyStats } from './responses.js';
2
+ import { createEmptyStats } from './responses.js';
3
3
 
4
4
  function createSectionMap(sections = []) {
5
5
  return new Map(sections.map((section) => [section.key, section]));
@@ -162,12 +162,18 @@ export class GitDiffService {
162
162
  async getDiff({ allowLargePatch = false, metaOnly = false, path = null, scope = 'working-tree' } = {}) {
163
163
  const isGitRepo = await this.commandRunner.isGitRepo();
164
164
  if (!isGitRepo) {
165
- return createDiffResponse({
165
+ return {
166
166
  files: [],
167
167
  isGitRepo: false,
168
168
  metaOnly,
169
+ path: null,
169
170
  scope,
170
- });
171
+ summary: {
172
+ additions: 0,
173
+ deletions: 0,
174
+ filesChanged: 0,
175
+ },
176
+ };
171
177
  }
172
178
 
173
179
  const resolvedScope = scope === 'staged' || scope === 'all'
@@ -214,13 +220,14 @@ export class GitDiffService {
214
220
  scope,
215
221
  });
216
222
 
217
- return createDiffResponse({
223
+ return {
218
224
  files: scopedFiles,
225
+ isGitRepo: true,
219
226
  metaOnly: true,
220
227
  path,
221
228
  scope,
222
229
  summary: scopeSummary,
223
- });
230
+ };
224
231
  }
225
232
 
226
233
  const isSinglePathRequest = Boolean(path);
@@ -234,7 +241,7 @@ export class GitDiffService {
234
241
  };
235
242
 
236
243
  if (!allowLargePatch && fileLineCount > this.maxInitialPatchLines) {
237
- return createDiffResponse({
244
+ return {
238
245
  files: [{
239
246
  ...currentFile,
240
247
  canLoadFullPatch: true,
@@ -246,11 +253,12 @@ export class GitDiffService {
246
253
  },
247
254
  tooLarge: true,
248
255
  }],
256
+ isGitRepo: true,
249
257
  metaOnly: false,
250
258
  path,
251
259
  scope,
252
260
  summary,
253
- });
261
+ };
254
262
  }
255
263
 
256
264
  const syntheticFiles = await this.untrackedFileService.buildSyntheticDiffs([currentFile]);
@@ -263,7 +271,7 @@ export class GitDiffService {
263
271
  },
264
272
  };
265
273
 
266
- return createDiffResponse({
274
+ return {
267
275
  files: [{
268
276
  ...currentFile,
269
277
  ...detail,
@@ -271,11 +279,12 @@ export class GitDiffService {
271
279
  patchLineCount: countPatchLines(detail),
272
280
  tooLarge: false,
273
281
  }],
282
+ isGitRepo: true,
274
283
  metaOnly: false,
275
284
  path,
276
285
  scope,
277
286
  summary,
278
- });
287
+ };
279
288
  }
280
289
 
281
290
  let singleFileSummary = null;
@@ -293,7 +302,7 @@ export class GitDiffService {
293
302
  && singleFileSummary.filesChanged > 0
294
303
  && (singleFileSummary.additions + singleFileSummary.deletions) > this.maxInitialPatchLines
295
304
  ) {
296
- return createDiffResponse({
305
+ return {
297
306
  files: [{
298
307
  ...currentFile,
299
308
  canLoadFullPatch: true,
@@ -305,11 +314,12 @@ export class GitDiffService {
305
314
  },
306
315
  tooLarge: true,
307
316
  }],
317
+ isGitRepo: true,
308
318
  metaOnly: false,
309
319
  path,
310
320
  scope,
311
321
  summary: singleFileSummary,
312
- });
322
+ };
313
323
  }
314
324
  }
315
325
 
@@ -378,12 +388,13 @@ export class GitDiffService {
378
388
  filesChanged: 0,
379
389
  });
380
390
 
381
- return createDiffResponse({
391
+ return {
382
392
  files: mergedFiles,
393
+ isGitRepo: true,
383
394
  metaOnly: false,
384
395
  path,
385
396
  scope,
386
397
  summary: isSinglePathRequest ? singleFileSummary ?? summary : summary,
387
- });
398
+ };
388
399
  }
389
400
  }
@@ -1,6 +1,10 @@
1
- import { createGitRequestError } from './errors.js';
1
+ import { execFile as execFileCallback } from 'node:child_process';
2
+ import { access } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+
6
+ import { createRequestError as createGitRequestError } from '../http/http-errors.js';
2
7
  import { normalizeRelativeGitPath } from './path-utils.js';
3
- import { GitCommandRunner } from './command-runner.js';
4
8
  import { GitDiffService } from './diff-service.js';
5
9
  import { GitHistoryService } from './history-service.js';
6
10
  import { parseNameStatusOutput } from './parsers.js';
@@ -9,6 +13,8 @@ import { GitStatusService } from './status-service.js';
9
13
  import { GitUntrackedFileService } from './untracked-files.js';
10
14
  import { PullBackupStore } from '../persistence/pull-backup-store.js';
11
15
 
16
+ const execFile = promisify(execFileCallback);
17
+
12
18
  function buildAuthorEnv(author = null) {
13
19
  const name = String(author?.name ?? '').trim();
14
20
  const email = String(author?.email ?? '').trim();
@@ -35,12 +41,36 @@ export class GitService {
35
41
  statusCacheTtlMs = 2_000,
36
42
  vaultDir,
37
43
  }) {
38
- this.commandRunner = new GitCommandRunner({
39
- commandEnv,
40
- enabled,
41
- execFileImpl,
42
- vaultDir,
43
- });
44
+ const gitExecFile = execFileImpl ?? execFile;
45
+ this.commandRunner = {
46
+ async isGitRepo() {
47
+ if (!enabled) {
48
+ return false;
49
+ }
50
+
51
+ try {
52
+ await access(join(vaultDir, '.git'));
53
+ return true;
54
+ } catch {
55
+ return false;
56
+ }
57
+ },
58
+ async execGit(args, { env = null } = {}) {
59
+ const result = await gitExecFile('git', ['-c', 'core.quotepath=false', ...args], {
60
+ cwd: vaultDir,
61
+ encoding: 'utf8',
62
+ env: {
63
+ ...process.env,
64
+ ...(commandEnv ?? {}),
65
+ ...(env ?? {}),
66
+ },
67
+ maxBuffer: 5 * 1024 * 1024,
68
+ timeout: 10_000,
69
+ });
70
+
71
+ return String(result.stdout ?? '');
72
+ },
73
+ };
44
74
  this.untrackedFileService = new GitUntrackedFileService({ vaultDir });
45
75
  this.statusService = new GitStatusService({
46
76
  commandRunner: this.commandRunner,
@@ -177,7 +207,7 @@ export class GitService {
177
207
  throw createGitRequestError(
178
208
  409,
179
209
  'Cannot pull because local and remote commits have diverged; fast-forward only pull is not possible.',
180
- 'pull_diverged_ff_only',
210
+ { requestCode: 'pull_diverged_ff_only' },
181
211
  );
182
212
  }
183
213
 
@@ -431,7 +461,7 @@ export class GitService {
431
461
  return createGitRequestError(
432
462
  409,
433
463
  'Pull applied remote updates, but reapplying local changes caused conflicts. Review the conflicted files and the pull backup summary.',
434
- 'pull_conflicted_after_autostash',
464
+ { requestCode: 'pull_conflicted_after_autostash' },
435
465
  );
436
466
  }
437
467