collabmd 0.1.19 → 0.1.21
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.
- package/README.md +13 -11
- package/docker-compose.yml +1 -1
- package/package.json +1 -1
- package/public/assets/css/style.css +1 -1
- package/public/assets/js/chunks/chunk-GFDM7YCF.js +1 -0
- package/public/assets/js/chunks/editor-session-EAUBJCHH.js +22 -0
- package/public/assets/js/chunks/{preview-render-compiler-UZ4SZDQQ.js → preview-render-compiler-6Y7TKXFJ.js} +1 -1
- package/public/assets/js/chunks/{quick-switcher-controller-J7I3CUET.js → quick-switcher-controller-A6SKH5HI.js} +1 -1
- package/public/assets/js/{excalidraw-editor-ZDYKMZOL.js → excalidraw-editor-TCIH6GJL.js} +1 -1
- package/public/assets/js/excalidraw-editor.js +1 -1
- package/public/assets/js/main.js +102 -75
- package/public/assets/js/preview-render-worker.js +15 -15
- package/public/index.html +14 -4
- package/src/client/application/app-shell/git-feature.js +86 -19
- package/src/client/application/app-shell/ui-feature.js +4 -0
- package/src/client/application/app-shell-elements.js +1 -0
- package/src/client/bootstrap/collabmd-app-shell.js +16 -0
- package/src/client/domain/vault-utils.js +2 -2
- package/src/client/excalidraw-editor.js +2 -1
- package/src/client/infrastructure/editor-session.js +4 -0
- package/src/client/infrastructure/editor-view-adapter.js +245 -7
- package/src/client/infrastructure/workspace-sync-client.js +324 -0
- package/src/client/presentation/backlinks-panel.js +157 -101
- package/src/client/presentation/comment-ui-controller.js +233 -10
- package/src/client/presentation/file-explorer-controller.js +15 -3
- package/src/client/presentation/file-explorer-view.js +152 -11
- package/src/client/presentation/git-panel-controller.js +73 -0
- package/src/client/presentation/outline-controller.js +25 -0
- package/src/client/styles/style.css +184 -9
- package/src/domain/wiki-link-resolver.js +16 -5
- package/src/domain/workspace-change.js +68 -0
- package/src/domain/workspace-room.js +3 -0
- package/src/server/create-app-server.js +41 -10
- package/src/server/domain/backlink-index.js +94 -1
- package/src/server/domain/collaboration/collaboration-room.js +191 -22
- package/src/server/domain/collaboration/room-registry.js +64 -10
- package/src/server/infrastructure/git/errors.js +4 -1
- package/src/server/infrastructure/git/git-service.js +215 -1
- package/src/server/infrastructure/git/responses.js +12 -19
- package/src/server/infrastructure/http/create-git-api-command-handler.js +58 -43
- package/src/server/infrastructure/http/create-git-api-handler.js +2 -0
- package/src/server/infrastructure/http/create-git-api-query-handler.js +16 -1
- package/src/server/infrastructure/http/create-request-handler.js +7 -0
- package/src/server/infrastructure/http/create-vault-api-command-handler.js +58 -14
- package/src/server/infrastructure/http/create-vault-api-handler.js +3 -0
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +3 -1
- package/src/server/infrastructure/http/http-response.js +65 -28
- package/src/server/infrastructure/persistence/pull-backup-store.js +283 -0
- package/src/server/infrastructure/persistence/vault-file-store.js +179 -52
- package/src/server/infrastructure/workspace/file-system-sync-service.js +488 -0
- package/src/server/infrastructure/workspace/workspace-mutation-coordinator.js +551 -0
- package/public/assets/js/chunks/chunk-R3DDMJHH.js +0 -1
- package/public/assets/js/chunks/editor-session-AH6Z3MXW.js +0 -22
package/public/index.html
CHANGED
|
@@ -364,16 +364,26 @@
|
|
|
364
364
|
<div class="preview-body">
|
|
365
365
|
<div id="previewContainer" class="preview-container">
|
|
366
366
|
<div id="previewContent" class="preview-content"></div>
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
367
|
+
<div id="backlinksInlinePanel" class="backlinks-panel backlinks-panel-inline hidden">
|
|
368
|
+
<div class="backlinks-header" role="button" tabindex="0" aria-expanded="false">
|
|
369
|
+
<svg class="backlinks-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
|
370
|
+
<span class="backlinks-toggle">0 Linked Mentions</span>
|
|
371
|
+
<span class="backlinks-count"></span>
|
|
372
|
+
</div>
|
|
373
|
+
<div class="backlinks-body" aria-hidden="true" inert>
|
|
374
|
+
<div class="backlinks-list"></div>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
<div id="backlinksPanel" class="backlinks-panel-layer hidden">
|
|
379
|
+
<div class="backlinks-panel backlinks-panel-dock" data-backlinks-variant="dock">
|
|
370
380
|
<div class="backlinks-header" role="button" tabindex="0" aria-expanded="false">
|
|
371
381
|
<svg class="backlinks-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
|
372
382
|
<span class="backlinks-toggle">0 Linked Mentions</span>
|
|
373
383
|
<span class="backlinks-count"></span>
|
|
374
384
|
</div>
|
|
375
385
|
<div class="backlinks-body" aria-hidden="true" inert>
|
|
376
|
-
<div class="backlinks-list"
|
|
386
|
+
<div class="backlinks-list"></div>
|
|
377
387
|
</div>
|
|
378
388
|
</div>
|
|
379
389
|
</div>
|
|
@@ -1,17 +1,30 @@
|
|
|
1
|
+
import { createWorkspaceChange } from '../../../domain/workspace-change.js';
|
|
1
2
|
import { resolveApiUrl } from '../../domain/runtime-paths.js';
|
|
2
3
|
|
|
3
4
|
function normalizeWorkspaceChange(workspaceChange = {}) {
|
|
4
|
-
return
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
return createWorkspaceChange(workspaceChange);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createWorkspaceRequestId() {
|
|
9
|
+
if (globalThis.crypto?.randomUUID) {
|
|
10
|
+
return globalThis.crypto.randomUUID();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return `workspace-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export const gitFeature = {
|
|
17
|
+
formatPullBackupToast(pullBackup = null) {
|
|
18
|
+
const fileCount = Number(pullBackup?.fileCount || 0);
|
|
19
|
+
if (fileCount === 1) {
|
|
20
|
+
return 'Pulled latest changes. 1 overlapping local file was backed up.';
|
|
21
|
+
}
|
|
22
|
+
if (fileCount > 1) {
|
|
23
|
+
return `Pulled latest changes. ${fileCount} overlapping local files were backed up.`;
|
|
24
|
+
}
|
|
25
|
+
return 'Pulled latest changes. Overlapping local changes were recorded in a pull backup.';
|
|
26
|
+
},
|
|
27
|
+
|
|
15
28
|
setGitOperationStatus(message = '') {
|
|
16
29
|
const badge = this.elements.gitOperationStatus;
|
|
17
30
|
if (!badge) {
|
|
@@ -83,16 +96,24 @@ export const gitFeature = {
|
|
|
83
96
|
},
|
|
84
97
|
|
|
85
98
|
async postGitAction(endpoint, payload) {
|
|
99
|
+
const requestId = createWorkspaceRequestId();
|
|
100
|
+
this.pendingWorkspaceRequestIds?.add(requestId);
|
|
86
101
|
const response = await fetch(endpoint, {
|
|
87
102
|
body: JSON.stringify(payload),
|
|
88
103
|
headers: {
|
|
89
104
|
'Content-Type': 'application/json',
|
|
105
|
+
'X-CollabMD-Request-Id': requestId,
|
|
90
106
|
},
|
|
91
107
|
method: 'POST',
|
|
92
108
|
});
|
|
93
109
|
const data = await response.json();
|
|
94
110
|
if (!response.ok) {
|
|
95
|
-
|
|
111
|
+
this.pendingWorkspaceRequestIds?.delete(requestId);
|
|
112
|
+
const error = new Error(data.error || 'Git action failed');
|
|
113
|
+
if (typeof data?.code === 'string') {
|
|
114
|
+
error.code = data.code;
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
96
117
|
}
|
|
97
118
|
return data;
|
|
98
119
|
},
|
|
@@ -130,11 +151,6 @@ export const gitFeature = {
|
|
|
130
151
|
const workspaceChange = normalizeWorkspaceChange(result.workspaceChange);
|
|
131
152
|
await this.refreshWorkspaceAfterGitAction({ filePath, preferredScope });
|
|
132
153
|
this.handleWorkspaceChangeForCurrentFile(workspaceChange, { action, local: true, showToast: showLocalFileToast });
|
|
133
|
-
this.lobby.sendWorkspaceEvent({
|
|
134
|
-
action,
|
|
135
|
-
sourceRef: result.sourceRef ?? null,
|
|
136
|
-
workspaceChange,
|
|
137
|
-
});
|
|
138
154
|
return workspaceChange;
|
|
139
155
|
},
|
|
140
156
|
|
|
@@ -150,21 +166,27 @@ export const gitFeature = {
|
|
|
150
166
|
return false;
|
|
151
167
|
}
|
|
152
168
|
|
|
153
|
-
this.navigation.navigateToFile(null);
|
|
154
169
|
if (!showToast) {
|
|
170
|
+
if (renameEntry) {
|
|
171
|
+
this.navigation.navigateToFile(renameEntry.newPath);
|
|
172
|
+
} else {
|
|
173
|
+
this.navigation.navigateToFile(null);
|
|
174
|
+
}
|
|
155
175
|
return true;
|
|
156
176
|
}
|
|
157
177
|
|
|
158
178
|
const displayName = this.getDisplayName(currentFilePath);
|
|
159
179
|
if (renameEntry) {
|
|
180
|
+
this.navigation.navigateToFile(renameEntry.newPath);
|
|
160
181
|
this.toastController.show(
|
|
161
182
|
local
|
|
162
|
-
? `${displayName}
|
|
163
|
-
: `${displayName} moved
|
|
183
|
+
? `${displayName} moved to ${this.getDisplayName(renameEntry.newPath)}`
|
|
184
|
+
: `${displayName} moved on disk`,
|
|
164
185
|
);
|
|
165
186
|
return true;
|
|
166
187
|
}
|
|
167
188
|
|
|
189
|
+
this.navigation.navigateToFile(null);
|
|
168
190
|
this.toastController.show(
|
|
169
191
|
local
|
|
170
192
|
? `${displayName} was removed by ${action}`
|
|
@@ -178,13 +200,45 @@ export const gitFeature = {
|
|
|
178
200
|
return;
|
|
179
201
|
}
|
|
180
202
|
|
|
203
|
+
if (event.requestId && this.pendingWorkspaceRequestIds?.has(event.requestId)) {
|
|
204
|
+
this.pendingWorkspaceRequestIds.delete(event.requestId);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
181
208
|
const workspaceChange = normalizeWorkspaceChange(event.workspaceChange);
|
|
182
|
-
|
|
209
|
+
if (event.origin === 'git') {
|
|
210
|
+
await this.refreshGitAfterAction();
|
|
211
|
+
}
|
|
183
212
|
this.handleWorkspaceChangeForCurrentFile(workspaceChange, {
|
|
184
|
-
action: event.action || '
|
|
213
|
+
action: event.action || event.origin || 'workspace',
|
|
185
214
|
local: false,
|
|
186
215
|
showToast: true,
|
|
187
216
|
});
|
|
217
|
+
|
|
218
|
+
if (
|
|
219
|
+
event.origin === 'filesystem'
|
|
220
|
+
&& this.currentFilePath
|
|
221
|
+
&& workspaceChange.changedPaths.includes(this.currentFilePath)
|
|
222
|
+
) {
|
|
223
|
+
const canUseInlineCue = workspaceChange.changedPaths.length === 1;
|
|
224
|
+
const highlightRange = canUseInlineCue && Array.isArray(event.highlightRanges)
|
|
225
|
+
? event.highlightRanges.find((entry) => entry.path === this.currentFilePath)
|
|
226
|
+
: null;
|
|
227
|
+
const didFlash = highlightRange
|
|
228
|
+
? this.session?.flashExternalUpdate?.(highlightRange)
|
|
229
|
+
: false;
|
|
230
|
+
if (!didFlash) {
|
|
231
|
+
this.toastController.show(`${this.getDisplayName(this.currentFilePath)} updated from disk`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (
|
|
236
|
+
this.currentFilePath
|
|
237
|
+
&& Array.isArray(event.reloadRequiredPaths)
|
|
238
|
+
&& event.reloadRequiredPaths.includes(this.currentFilePath)
|
|
239
|
+
) {
|
|
240
|
+
this.toastController.show(`${this.getDisplayName(this.currentFilePath)} needs a manual reload`);
|
|
241
|
+
}
|
|
188
242
|
},
|
|
189
243
|
|
|
190
244
|
async stageGitFile(filePath, { scope = 'working-tree' } = {}) {
|
|
@@ -242,7 +296,20 @@ export const gitFeature = {
|
|
|
242
296
|
result,
|
|
243
297
|
showLocalFileToast: true,
|
|
244
298
|
});
|
|
299
|
+
if (result.pullBackup) {
|
|
300
|
+
this.toastController.show(this.formatPullBackupToast(result.pullBackup), 5000);
|
|
301
|
+
}
|
|
245
302
|
} catch (error) {
|
|
303
|
+
if (error?.code === 'pull_diverged_ff_only') {
|
|
304
|
+
this.toastController.show('Cannot pull because local and remote commits have diverged. Fast-forward only pull is not possible.', 5000);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (error?.code === 'pull_conflicted_after_autostash') {
|
|
309
|
+
this.toastController.show('Pull updated the branch, but reapplying local changes caused conflicts. Review the conflicted files and the pull backup summary.', 6000);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
246
313
|
this.toastController.show(error.message || 'Failed to pull branch');
|
|
247
314
|
}
|
|
248
315
|
});
|
|
@@ -534,6 +534,9 @@ export const uiFeature = {
|
|
|
534
534
|
if (!this.lobby.provider) {
|
|
535
535
|
this.lobby.connect();
|
|
536
536
|
}
|
|
537
|
+
if (!this.workspaceSync.provider) {
|
|
538
|
+
this.workspaceSync.connect();
|
|
539
|
+
}
|
|
537
540
|
|
|
538
541
|
if (wasInactive) {
|
|
539
542
|
if (this.fileExplorerReady) {
|
|
@@ -554,6 +557,7 @@ export const uiFeature = {
|
|
|
554
557
|
this.elements.displayNameDialog.close();
|
|
555
558
|
}
|
|
556
559
|
this.lobby.disconnect();
|
|
560
|
+
this.workspaceSync.disconnect();
|
|
557
561
|
this.globalUsers = [];
|
|
558
562
|
this.chatMessages = [];
|
|
559
563
|
this.chatMessageIds.clear();
|
|
@@ -61,6 +61,7 @@ export function bindAppShellElements(doc = document) {
|
|
|
61
61
|
emptyStateNewFileBtn: doc.getElementById('emptyStateNewFileBtn'),
|
|
62
62
|
emptyStateSearchBtn: doc.getElementById('emptyStateSearchBtn'),
|
|
63
63
|
backlinksPanel: doc.getElementById('backlinksPanel'),
|
|
64
|
+
backlinksInlinePanel: doc.getElementById('backlinksInlinePanel'),
|
|
64
65
|
commentSelectionButton: doc.getElementById('commentSelectionBtn'),
|
|
65
66
|
commentsDrawer: doc.getElementById('commentsDrawer'),
|
|
66
67
|
commentsDrawerEmpty: doc.getElementById('commentsDrawerEmpty'),
|
|
@@ -18,6 +18,7 @@ import { BrowserPreferencesPort } from '../infrastructure/browser-preferences-po
|
|
|
18
18
|
import { getRuntimeConfig } from '../infrastructure/runtime-config.js';
|
|
19
19
|
import { TabActivityLock } from '../infrastructure/tab-activity-lock.js';
|
|
20
20
|
import { vaultApiClient } from '../infrastructure/vault-api-client.js';
|
|
21
|
+
import { WorkspaceSyncClient } from '../infrastructure/workspace-sync-client.js';
|
|
21
22
|
import { BacklinksPanel } from '../presentation/backlinks-panel.js';
|
|
22
23
|
import { CommentUiController } from '../presentation/comment-ui-controller.js';
|
|
23
24
|
import { ExcalidrawEmbedController } from '../presentation/excalidraw-embed-controller.js';
|
|
@@ -111,11 +112,22 @@ export class CollabMdAppShell {
|
|
|
111
112
|
this.quickSwitcherModulePromise = null;
|
|
112
113
|
this.fileExplorerReadyPromise = Promise.resolve();
|
|
113
114
|
this.mobileBreakpointQuery = window.matchMedia('(max-width: 768px)');
|
|
115
|
+
this.pendingWorkspaceRequestIds = new Set();
|
|
114
116
|
|
|
115
117
|
this.lobby = new LobbyPresence({
|
|
116
118
|
preferredUserName: this.getStoredUserName(),
|
|
117
119
|
onChange: (users) => this.updateGlobalUsers(users),
|
|
118
120
|
onChatChange: (messages, meta) => this.updateChatMessages(messages, meta),
|
|
121
|
+
});
|
|
122
|
+
this.workspaceSync = new WorkspaceSyncClient({
|
|
123
|
+
onTreeChange: (tree, metadata = {}) => {
|
|
124
|
+
const wasReady = this.fileExplorerReady;
|
|
125
|
+
this.fileExplorer.setTree(tree, metadata);
|
|
126
|
+
this.fileExplorerReady = true;
|
|
127
|
+
if (!wasReady && this.isTabActive) {
|
|
128
|
+
void this.handleHashChange();
|
|
129
|
+
}
|
|
130
|
+
},
|
|
119
131
|
onWorkspaceEvent: (event) => {
|
|
120
132
|
void this.handleIncomingWorkspaceEvent(event);
|
|
121
133
|
},
|
|
@@ -131,6 +143,7 @@ export class CollabMdAppShell {
|
|
|
131
143
|
this.gitPanel = new GitPanelController({
|
|
132
144
|
enabled: this.runtimeConfig.gitEnabled !== false,
|
|
133
145
|
onCommitStaged: () => this.openGitCommitDialog(),
|
|
146
|
+
onOpenPullBackup: (filePath) => filePath && this.navigation.navigateToFile(filePath),
|
|
134
147
|
onPullBranch: () => this.pullGitBranch(),
|
|
135
148
|
onPushBranch: () => this.pushGitBranch(),
|
|
136
149
|
onRepoChange: (isGitRepo, status) => this.handleGitRepoChange(isGitRepo, status),
|
|
@@ -149,6 +162,7 @@ export class CollabMdAppShell {
|
|
|
149
162
|
this.scrollSyncController.suspendSync(250);
|
|
150
163
|
this.session?.scrollToLine(sourceLine, 0);
|
|
151
164
|
},
|
|
165
|
+
onWillOpen: () => this.commentUi?.closeDrawer(),
|
|
152
166
|
});
|
|
153
167
|
this.videoEmbed = new VideoEmbedController({
|
|
154
168
|
previewElement: this.elements.previewContent,
|
|
@@ -196,6 +210,7 @@ export class CollabMdAppShell {
|
|
|
196
210
|
scrollEditorToLine: (lineNumber, viewportRatio) => this.session?.scrollToLine(lineNumber, viewportRatio),
|
|
197
211
|
});
|
|
198
212
|
this.backlinksPanel = new BacklinksPanel({
|
|
213
|
+
inlinePanelElement: this.elements.backlinksInlinePanel,
|
|
199
214
|
onFileSelect: (filePath) => this.handleFileSelection(filePath, { closeSidebarOnMobile: true }),
|
|
200
215
|
panelElement: this.elements.backlinksPanel,
|
|
201
216
|
});
|
|
@@ -214,6 +229,7 @@ export class CollabMdAppShell {
|
|
|
214
229
|
commentsDrawerList: this.elements.commentsDrawerList,
|
|
215
230
|
commentsToggleButton: this.elements.commentsToggleButton,
|
|
216
231
|
editorContainer: this.elements.editorContainer,
|
|
232
|
+
onWillOpenDrawer: () => this.outlineController.close(),
|
|
217
233
|
previewContainer: this.elements.previewContainer,
|
|
218
234
|
previewElement: this.elements.previewContent,
|
|
219
235
|
onCreateThread: ({ anchor, body }) => this.session?.createCommentThread({ anchor, body }),
|
|
@@ -25,8 +25,8 @@ export function escapeHtml(text) {
|
|
|
25
25
|
*
|
|
26
26
|
* Matching rules (in order):
|
|
27
27
|
* 1. Exact path match (with .md appended if missing)
|
|
28
|
-
* 2.
|
|
29
|
-
* 3.
|
|
28
|
+
* 2. Exact path without .md extension matches the target
|
|
29
|
+
* 3. Filename/path suffix match at any directory depth
|
|
30
30
|
*
|
|
31
31
|
* @param {string} target — the raw wiki-link target text
|
|
32
32
|
* @param {string[]} files — list of vault file paths
|
|
@@ -149,6 +149,7 @@ if (isTestMode) {
|
|
|
149
149
|
&& collabReady
|
|
150
150
|
&& roomClient.canWriteToRoom === true
|
|
151
151
|
&& roomClient.waitingForAuthoritativeSync === false
|
|
152
|
+
&& roomClient.isApplyingSharedSnapshot() === false
|
|
152
153
|
),
|
|
153
154
|
isReady: () => collabReady && Boolean(excalidrawAPI) && Boolean(getNativeHistoryButton('undo')) && Boolean(getNativeHistoryButton('redo')),
|
|
154
155
|
redoShared: () => triggerNativeHistory('redo'),
|
|
@@ -506,7 +507,7 @@ window.addEventListener('message', (event) => {
|
|
|
506
507
|
});
|
|
507
508
|
|
|
508
509
|
function scheduleSyncToRoom(elements, appState, files) {
|
|
509
|
-
if (!collabReady || suppressOnChange
|
|
510
|
+
if (!collabReady || suppressOnChange) {
|
|
510
511
|
return;
|
|
511
512
|
}
|
|
512
513
|
|
|
@@ -229,6 +229,10 @@ export class EditorSession {
|
|
|
229
229
|
return this.viewAdapter.insertText(text);
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
flashExternalUpdate(range) {
|
|
233
|
+
return this.viewAdapter.flashRemoteRange(range);
|
|
234
|
+
}
|
|
235
|
+
|
|
232
236
|
waitForInitialSync(timeoutMs = 1500) {
|
|
233
237
|
return this.collaborationClient.waitForInitialSync(timeoutMs);
|
|
234
238
|
}
|