collabmd 0.1.20 → 0.1.23
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 +98 -9
- package/bin/collabmd.js +3 -2
- package/docker-compose.yml +13 -1
- package/package.json +2 -1
- package/public/assets/css/style.css +1 -1
- package/public/assets/js/chunks/{chunk-HEWVH67U.js → chunk-CYK7KO7W.js} +8 -8
- package/public/assets/js/chunks/chunk-GFDM7YCF.js +1 -0
- package/public/assets/js/chunks/editor-session-S5AT7BMV.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-5YWMGLKF.js} +145 -62
- package/public/assets/js/excalidraw-editor.js +1 -1
- package/public/assets/js/main.js +503 -182
- package/public/assets/js/preview-render-worker.js +15 -15
- package/public/index.html +104 -84
- package/src/client/application/app-shell/git-feature.js +304 -27
- package/src/client/application/app-shell/ui-feature.js +69 -7
- package/src/client/application/app-shell/workspace-feature.js +46 -0
- package/src/client/application/app-shell-elements.js +2 -0
- package/src/client/application/mermaid-preview-hydrator.js +1 -1
- package/src/client/application/plantuml-preview-hydrator.js +1 -1
- package/src/client/application/workspace-coordinator.js +147 -18
- package/src/client/application/workspace-preview-controller.js +34 -0
- package/src/client/application/workspace-route-controller.js +104 -0
- package/src/client/bootstrap/collabmd-app-shell.js +107 -0
- package/src/client/domain/runtime-paths.js +5 -0
- package/src/client/domain/vault-utils.js +2 -2
- package/src/client/excalidraw-editor.js +52 -3
- package/src/client/infrastructure/auth-client.js +219 -52
- package/src/client/infrastructure/browser-navigation-port.js +20 -0
- package/src/client/infrastructure/editor-collaboration-client.js +4 -0
- package/src/client/infrastructure/editor-session.js +51 -7
- package/src/client/infrastructure/editor-view-adapter.js +297 -53
- package/src/client/infrastructure/runtime-config.js +66 -0
- package/src/client/infrastructure/workspace-sync-client.js +324 -0
- package/src/client/presentation/comment-ui-controller.js +274 -31
- package/src/client/presentation/comments-panel.js +2 -2
- package/src/client/presentation/excalidraw-embed-controller.js +60 -2
- package/src/client/presentation/file-explorer-controller.js +15 -3
- package/src/client/presentation/file-explorer-view.js +152 -11
- package/src/client/presentation/file-history-view-controller.js +806 -0
- package/src/client/presentation/git-diff-view-controller.js +718 -121
- package/src/client/presentation/git-panel-controller.js +458 -66
- package/src/client/presentation/layout-controller.js +1 -1
- package/src/client/styles/components/actions.css +89 -0
- package/src/client/styles/components/feedback.css +43 -0
- package/src/client/styles/components/navigation.css +68 -0
- package/src/client/styles/components/scrollbars.css +104 -0
- package/src/client/styles/components/states.css +289 -0
- package/src/client/styles/components/surfaces.css +155 -0
- package/src/client/styles/features/backlinks.css +213 -0
- package/src/client/styles/features/collaboration-chat.css +228 -0
- package/src/client/styles/features/collaboration-dialogs.css +154 -0
- package/src/client/styles/features/collaboration-presence.css +103 -0
- package/src/client/styles/features/collaboration-ui.css +3 -0
- package/src/client/styles/features/comment-card.css +236 -0
- package/src/client/styles/features/comment-markdown.css +127 -0
- package/src/client/styles/features/comment-overlays.css +60 -0
- package/src/client/styles/features/comments-drawer.css +120 -0
- package/src/client/styles/features/comments.css +4 -0
- package/src/client/styles/features/diagram-preview.css +227 -0
- package/src/client/styles/features/diff-commit.css +268 -0
- package/src/client/styles/features/diff-lines.css +160 -0
- package/src/client/styles/features/diff-toolbar.css +98 -0
- package/src/client/styles/features/diff.css +3 -0
- package/src/client/styles/features/embedded-preview.css +218 -0
- package/src/client/styles/features/git-branch.css +155 -0
- package/src/client/styles/features/git-files.css +105 -0
- package/src/client/styles/features/git-history.css +339 -0
- package/src/client/styles/features/git.css +3 -0
- package/src/client/styles/features/media-lightbox.css +157 -0
- package/src/client/styles/features/outline.css +115 -0
- package/src/client/styles/features/preview-content.css +3 -0
- package/src/client/styles/features/preview-markdown.css +201 -0
- package/src/client/styles/features/preview-sidebars.css +2 -0
- package/src/client/styles/features/quick-switcher.css +167 -0
- package/src/client/styles/foundation/themes.css +347 -0
- package/src/client/styles/foundation/tokens.css +66 -0
- package/src/client/styles/foundation/utilities.css +36 -0
- package/src/client/styles/layout/editor-page.css +203 -0
- package/src/client/styles/layout/overlays.css +132 -0
- package/src/client/styles/layout/responsive.css +251 -0
- package/src/client/styles/layout/shell.css +314 -0
- package/src/client/styles/layout/sidebar.css +176 -0
- package/src/client/styles/layout/view-modes.css +92 -0
- package/src/client/styles/overrides/codemirror.css +53 -0
- package/src/client/styles/overrides/highlightjs.css +7 -0
- package/src/client/styles/overrides/vendor.css +2 -0
- package/src/client/styles/primitives/controls.css +276 -0
- package/src/client/styles/style.css +25 -4901
- 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/auth/create-auth-service.js +373 -15
- package/src/server/auth/session-cookie.js +30 -4
- package/src/server/config/env.js +121 -5
- package/src/server/create-app-server.js +49 -10
- package/src/server/domain/backlink-index.js +94 -1
- package/src/server/domain/collaboration/collaboration-room.js +281 -69
- package/src/server/domain/collaboration/room-registry.js +64 -10
- package/src/server/index.js +3 -1
- package/src/server/infrastructure/git/command-runner.js +2 -1
- package/src/server/infrastructure/git/git-service.js +48 -2
- package/src/server/infrastructure/git/history-service.js +683 -0
- package/src/server/infrastructure/git/parsers.js +30 -0
- package/src/server/infrastructure/git/responses.js +85 -19
- package/src/server/infrastructure/http/create-auth-api-handler.js +30 -2
- package/src/server/infrastructure/http/create-git-api-command-handler.js +55 -43
- package/src/server/infrastructure/http/create-git-api-handler.js +4 -0
- package/src/server/infrastructure/http/create-git-api-query-handler.js +61 -0
- package/src/server/infrastructure/http/create-request-handler.js +22 -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/vault-file-store.js +163 -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-TAV2VXTA.js +0 -22
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { FileOpenLifecycle } from './file-open-lifecycle.js';
|
|
2
2
|
import { WorkspaceChromeController } from './workspace-chrome-controller.js';
|
|
3
3
|
|
|
4
|
+
const BOOTSTRAP_RENDER_DELAY_MS = 150;
|
|
5
|
+
|
|
6
|
+
function delay(ms) {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
setTimeout(resolve, ms);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
export class WorkspaceCoordinator {
|
|
5
13
|
constructor({
|
|
6
14
|
attachEditorScroller,
|
|
@@ -18,6 +26,7 @@ export class WorkspaceCoordinator {
|
|
|
18
26
|
isMermaidFile,
|
|
19
27
|
isPlantUmlFile,
|
|
20
28
|
isTabActive,
|
|
29
|
+
loadBootstrapContent = null,
|
|
21
30
|
loadEditorSessionClass,
|
|
22
31
|
loadBacklinks,
|
|
23
32
|
onBeforeFileOpen,
|
|
@@ -30,6 +39,7 @@ export class WorkspaceCoordinator {
|
|
|
30
39
|
onImagePaste,
|
|
31
40
|
onSelectionChange,
|
|
32
41
|
onSessionAssigned = null,
|
|
42
|
+
onFileOpenMetric = null,
|
|
33
43
|
onRenderExcalidrawPreview,
|
|
34
44
|
onRenderImagePreview,
|
|
35
45
|
onSyncWrapToggle,
|
|
@@ -58,6 +68,7 @@ export class WorkspaceCoordinator {
|
|
|
58
68
|
this.isMermaidFile = isMermaidFile ?? (() => false);
|
|
59
69
|
this.isPlantUmlFile = isPlantUmlFile ?? (() => false);
|
|
60
70
|
this.isTabActive = isTabActive;
|
|
71
|
+
this.loadBootstrapContent = loadBootstrapContent;
|
|
61
72
|
this.loadEditorSessionClassPort = loadEditorSessionClass;
|
|
62
73
|
this.loadBacklinks = loadBacklinks;
|
|
63
74
|
this.onBeforeFileOpen = onBeforeFileOpen;
|
|
@@ -70,6 +81,7 @@ export class WorkspaceCoordinator {
|
|
|
70
81
|
this.onImagePaste = onImagePaste;
|
|
71
82
|
this.onSelectionChange = onSelectionChange;
|
|
72
83
|
this.onSessionAssigned = onSessionAssigned;
|
|
84
|
+
this.onFileOpenMetric = onFileOpenMetric;
|
|
73
85
|
this.onRenderExcalidrawPreview = onRenderExcalidrawPreview;
|
|
74
86
|
this.onSyncWrapToggle = onSyncWrapToggle;
|
|
75
87
|
this.onUpdateActiveFile = onUpdateActiveFile;
|
|
@@ -122,6 +134,14 @@ export class WorkspaceCoordinator {
|
|
|
122
134
|
});
|
|
123
135
|
}
|
|
124
136
|
|
|
137
|
+
reportFileOpenMetric(name, loadToken, data = {}) {
|
|
138
|
+
this.onFileOpenMetric?.(name, {
|
|
139
|
+
filePath: this.stateStore.get('currentFilePath'),
|
|
140
|
+
loadToken,
|
|
141
|
+
...data,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
125
145
|
cleanupSession() {
|
|
126
146
|
this.session?.destroy();
|
|
127
147
|
this.session = null;
|
|
@@ -146,11 +166,13 @@ export class WorkspaceCoordinator {
|
|
|
146
166
|
}
|
|
147
167
|
|
|
148
168
|
const loadToken = this.stateStore.nextSessionLoadToken();
|
|
169
|
+
const openStartedAt = performance.now();
|
|
149
170
|
|
|
150
171
|
this.cleanupSession();
|
|
151
172
|
const chromeState = this.chromeController.prepareForFileOpen(filePath, {
|
|
152
173
|
resetConnectionState: !isExcalidraw && !isImage,
|
|
153
174
|
});
|
|
175
|
+
this.reportFileOpenMetric('open_started', loadToken, { filePath });
|
|
154
176
|
|
|
155
177
|
if (isExcalidraw || isImage) {
|
|
156
178
|
this.onSessionAssigned?.(null);
|
|
@@ -198,37 +220,144 @@ export class WorkspaceCoordinator {
|
|
|
198
220
|
this.onSessionAssigned?.(session);
|
|
199
221
|
|
|
200
222
|
try {
|
|
201
|
-
|
|
223
|
+
let fileOpenReady = false;
|
|
224
|
+
let fileOpenFinalized = false;
|
|
225
|
+
let liveSyncComplete = false;
|
|
202
226
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
227
|
+
const readySession = async (reason) => {
|
|
228
|
+
if (fileOpenReady || loadToken !== this.stateStore.get('sessionLoadToken')) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fileOpenReady = true;
|
|
233
|
+
this.lifecycle.attachSessionScroller(session);
|
|
234
|
+
session.applyTheme(this.getTheme());
|
|
235
|
+
this.chromeController.markFileOpenReady(session);
|
|
236
|
+
this.reportFileOpenMetric('editor_ready', loadToken, { reason });
|
|
237
|
+
session.requestMeasure();
|
|
238
|
+
await this.waitForNextPaint();
|
|
239
|
+
|
|
240
|
+
if (fileOpenFinalized || loadToken !== this.stateStore.get('sessionLoadToken')) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
207
243
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
244
|
+
fileOpenFinalized = true;
|
|
245
|
+
this.chromeController.finalizeFileOpen({
|
|
246
|
+
filePath,
|
|
247
|
+
isExcalidraw,
|
|
248
|
+
session,
|
|
249
|
+
supportsBacklinks: chromeState.supportsBacklinks,
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const bootstrapPromise = this.loadBootstrapContent
|
|
254
|
+
? (async () => {
|
|
255
|
+
this.reportFileOpenMetric('bootstrap_fetch_started', loadToken);
|
|
256
|
+
try {
|
|
257
|
+
const content = await this.loadBootstrapContent(filePath);
|
|
258
|
+
this.reportFileOpenMetric('bootstrap_fetch_completed', loadToken, {
|
|
259
|
+
found: content !== null,
|
|
260
|
+
});
|
|
261
|
+
return content;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
this.reportFileOpenMetric('bootstrap_fetch_completed', loadToken, {
|
|
264
|
+
error: error.message,
|
|
265
|
+
found: false,
|
|
266
|
+
});
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
})()
|
|
270
|
+
: Promise.resolve(null);
|
|
271
|
+
|
|
272
|
+
const initializePromise = session.initialize(filePath);
|
|
273
|
+
const liveSyncPromise = (async () => {
|
|
274
|
+
await initializePromise;
|
|
275
|
+
|
|
276
|
+
if (loadToken !== this.stateStore.get('sessionLoadToken')) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await session.waitForInitialSync(null);
|
|
281
|
+
if (loadToken !== this.stateStore.get('sessionLoadToken')) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
liveSyncComplete = true;
|
|
286
|
+
session.activateCollaborativeView?.();
|
|
287
|
+
this.lifecycle.attachSessionScroller(session);
|
|
288
|
+
session.applyTheme(this.getTheme());
|
|
289
|
+
this.reportFileOpenMetric('initial_sync_complete', loadToken);
|
|
290
|
+
session.ensureInitialContent?.();
|
|
291
|
+
if (!fileOpenReady) {
|
|
292
|
+
await readySession('live-sync');
|
|
293
|
+
} else {
|
|
294
|
+
session.requestMeasure();
|
|
295
|
+
}
|
|
296
|
+
return true;
|
|
297
|
+
})();
|
|
298
|
+
|
|
299
|
+
const bootstrapVisibilityPromise = (async () => {
|
|
300
|
+
const bootstrapContent = await bootstrapPromise;
|
|
301
|
+
if (
|
|
302
|
+
bootstrapContent === null
|
|
303
|
+
|| liveSyncComplete
|
|
304
|
+
|| fileOpenReady
|
|
305
|
+
|| loadToken !== this.stateStore.get('sessionLoadToken')
|
|
306
|
+
) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const elapsedMs = performance.now() - openStartedAt;
|
|
311
|
+
const remainingDelayMs = Math.max(0, BOOTSTRAP_RENDER_DELAY_MS - elapsedMs);
|
|
312
|
+
if (remainingDelayMs > 0) {
|
|
313
|
+
const winner = await Promise.race([
|
|
314
|
+
liveSyncPromise.then((didSync) => (didSync ? 'live-sync' : 'stale')),
|
|
315
|
+
delay(remainingDelayMs).then(() => 'timeout'),
|
|
316
|
+
]);
|
|
317
|
+
if (winner === 'live-sync') {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (
|
|
323
|
+
liveSyncComplete
|
|
324
|
+
|| fileOpenReady
|
|
325
|
+
|| loadToken !== this.stateStore.get('sessionLoadToken')
|
|
326
|
+
) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const didApplyBootstrap = session.showBootstrapContent({
|
|
331
|
+
content: bootstrapContent,
|
|
332
|
+
filePath,
|
|
333
|
+
});
|
|
334
|
+
if (!didApplyBootstrap && !session.hasBootstrapContent?.()) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
this.reportFileOpenMetric('bootstrap_shown', loadToken);
|
|
339
|
+
await readySession('bootstrap');
|
|
340
|
+
return true;
|
|
341
|
+
})();
|
|
342
|
+
|
|
343
|
+
await initializePromise;
|
|
212
344
|
|
|
213
345
|
if (loadToken !== this.stateStore.get('sessionLoadToken')) {
|
|
214
346
|
session.destroy();
|
|
215
347
|
return;
|
|
216
348
|
}
|
|
217
349
|
|
|
218
|
-
|
|
219
|
-
session.requestMeasure();
|
|
220
|
-
await this.waitForNextPaint();
|
|
350
|
+
await Promise.all([liveSyncPromise, bootstrapVisibilityPromise]);
|
|
221
351
|
|
|
222
352
|
if (loadToken !== this.stateStore.get('sessionLoadToken')) {
|
|
223
353
|
session.destroy();
|
|
224
354
|
return;
|
|
225
355
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
});
|
|
356
|
+
|
|
357
|
+
if (!fileOpenReady) {
|
|
358
|
+
session.ensureInitialContent?.();
|
|
359
|
+
await readySession('post-initialize');
|
|
360
|
+
}
|
|
232
361
|
} catch (error) {
|
|
233
362
|
console.error('[app] Failed to initialize editor:', error);
|
|
234
363
|
session.destroy();
|
|
@@ -164,6 +164,40 @@ export class WorkspacePreviewController {
|
|
|
164
164
|
this.schedulePreviewLayoutSyncCallback({ delayMs: 0 });
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
renderTextFilePreview({ content = '' } = {}) {
|
|
168
|
+
const previewElement = this.elements.previewContent;
|
|
169
|
+
if (!previewElement) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.videoEmbed?.detachForCommit();
|
|
174
|
+
this.excalidrawEmbed.detachForCommit();
|
|
175
|
+
this.resetPreviewMode();
|
|
176
|
+
const renderHost = this.previewRenderer.ensureRenderHost();
|
|
177
|
+
this.previewRenderer.normalizePreviewChildren(renderHost);
|
|
178
|
+
|
|
179
|
+
const shell = document.createElement('div');
|
|
180
|
+
shell.className = 'preview-shell';
|
|
181
|
+
const pre = document.createElement('pre');
|
|
182
|
+
const code = document.createElement('code');
|
|
183
|
+
code.textContent = String(content ?? '');
|
|
184
|
+
pre.appendChild(code);
|
|
185
|
+
shell.appendChild(pre);
|
|
186
|
+
|
|
187
|
+
if (renderHost) {
|
|
188
|
+
renderHost.replaceChildren(shell);
|
|
189
|
+
renderHost.style.minHeight = '';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
previewElement.dataset.renderPhase = 'ready';
|
|
193
|
+
this.outlineController.close();
|
|
194
|
+
this.backlinksPanel.clear();
|
|
195
|
+
this.scrollSyncController.setLargeDocumentMode(false);
|
|
196
|
+
this.scrollSyncController.invalidatePreviewBlocks();
|
|
197
|
+
this.videoEmbed?.reconcileEmbeds(previewElement);
|
|
198
|
+
this.schedulePreviewLayoutSyncCallback({ delayMs: 0 });
|
|
199
|
+
}
|
|
200
|
+
|
|
167
201
|
createResizeHandler(restoreSidebarState) {
|
|
168
202
|
let resizeTimer = null;
|
|
169
203
|
return () => {
|
|
@@ -2,9 +2,11 @@ export class WorkspaceRouteController {
|
|
|
2
2
|
constructor({
|
|
3
3
|
backlinksPanel,
|
|
4
4
|
clearInitialFileBootstrap,
|
|
5
|
+
clearStaticPreviewDocument = null,
|
|
5
6
|
closeSidebarOnMobile,
|
|
6
7
|
elements,
|
|
7
8
|
excalidrawEmbed,
|
|
9
|
+
fileHistoryView = null,
|
|
8
10
|
fileExplorer,
|
|
9
11
|
getIsTabActive,
|
|
10
12
|
getSessionLoadToken,
|
|
@@ -22,7 +24,11 @@ export class WorkspaceRouteController {
|
|
|
22
24
|
setSessionLoadToken,
|
|
23
25
|
setSidebarTab,
|
|
24
26
|
setCurrentFilePath,
|
|
27
|
+
showGitCommit,
|
|
25
28
|
showGitDiff,
|
|
29
|
+
showGitFileHistory,
|
|
30
|
+
showGitFilePreview,
|
|
31
|
+
showGitHistory,
|
|
26
32
|
syncMainChrome,
|
|
27
33
|
videoEmbed,
|
|
28
34
|
workspaceCoordinator,
|
|
@@ -30,9 +36,11 @@ export class WorkspaceRouteController {
|
|
|
30
36
|
}) {
|
|
31
37
|
this.backlinksPanel = backlinksPanel;
|
|
32
38
|
this.clearInitialFileBootstrap = clearInitialFileBootstrap;
|
|
39
|
+
this.clearStaticPreviewDocument = clearStaticPreviewDocument;
|
|
33
40
|
this.closeSidebarOnMobile = closeSidebarOnMobile;
|
|
34
41
|
this.elements = elements;
|
|
35
42
|
this.excalidrawEmbed = excalidrawEmbed;
|
|
43
|
+
this.fileHistoryView = fileHistoryView;
|
|
36
44
|
this.fileExplorer = fileExplorer;
|
|
37
45
|
this.getIsTabActive = getIsTabActive;
|
|
38
46
|
this.getSessionLoadToken = getSessionLoadToken;
|
|
@@ -50,7 +58,11 @@ export class WorkspaceRouteController {
|
|
|
50
58
|
this.setSessionLoadToken = setSessionLoadToken;
|
|
51
59
|
this.setSidebarTab = setSidebarTab;
|
|
52
60
|
this.setCurrentFilePath = setCurrentFilePath;
|
|
61
|
+
this.showGitCommit = showGitCommit;
|
|
53
62
|
this.showGitDiff = showGitDiff;
|
|
63
|
+
this.showGitFileHistory = showGitFileHistory;
|
|
64
|
+
this.showGitFilePreview = showGitFilePreview;
|
|
65
|
+
this.showGitHistory = showGitHistory;
|
|
54
66
|
this.syncMainChrome = syncMainChrome;
|
|
55
67
|
this.videoEmbed = videoEmbed;
|
|
56
68
|
this.workspaceCoordinator = workspaceCoordinator;
|
|
@@ -76,13 +88,39 @@ export class WorkspaceRouteController {
|
|
|
76
88
|
return;
|
|
77
89
|
}
|
|
78
90
|
|
|
91
|
+
if (route.type === 'git-file-history') {
|
|
92
|
+
this.setSidebarTab('files');
|
|
93
|
+
await this.showGitFileHistory(route);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (route.type === 'git-file-preview') {
|
|
98
|
+
this.setSidebarTab('files');
|
|
99
|
+
await this.showGitFilePreview(route);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (route.type === 'git-history') {
|
|
104
|
+
this.setSidebarTab('git');
|
|
105
|
+
await this.showGitHistory();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (route.type === 'git-commit') {
|
|
110
|
+
this.setSidebarTab('git');
|
|
111
|
+
await this.showGitCommit(route);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
79
115
|
this.setSidebarTab('files');
|
|
80
116
|
await this.openFile(route.filePath);
|
|
81
117
|
}
|
|
82
118
|
|
|
83
119
|
showEmptyState() {
|
|
84
120
|
this.gitDiffView.hide();
|
|
121
|
+
this.fileHistoryView?.hide?.();
|
|
85
122
|
this.workspaceCoordinator.cleanupSession();
|
|
123
|
+
this.clearStaticPreviewDocument?.();
|
|
86
124
|
this.setSession(null);
|
|
87
125
|
this.setSessionLoadToken(this.getSessionLoadToken() + 1);
|
|
88
126
|
this.clearInitialFileBootstrap();
|
|
@@ -113,8 +151,10 @@ export class WorkspaceRouteController {
|
|
|
113
151
|
}
|
|
114
152
|
|
|
115
153
|
showDiffState() {
|
|
154
|
+
this.fileHistoryView?.hide?.();
|
|
116
155
|
this.setSessionLoadToken(this.getSessionLoadToken() + 1);
|
|
117
156
|
this.clearInitialFileBootstrap();
|
|
157
|
+
this.clearStaticPreviewDocument?.();
|
|
118
158
|
this.workspaceCoordinator.cleanupSession();
|
|
119
159
|
this.setSession(null);
|
|
120
160
|
this.resetPreviewMode();
|
|
@@ -141,10 +181,74 @@ export class WorkspaceRouteController {
|
|
|
141
181
|
this.backlinksPanel.clear();
|
|
142
182
|
}
|
|
143
183
|
|
|
184
|
+
showFileHistoryState(filePath) {
|
|
185
|
+
this.gitDiffView.hide();
|
|
186
|
+
this.setSessionLoadToken(this.getSessionLoadToken() + 1);
|
|
187
|
+
this.clearInitialFileBootstrap();
|
|
188
|
+
this.clearStaticPreviewDocument?.();
|
|
189
|
+
this.workspaceCoordinator.cleanupSession();
|
|
190
|
+
this.setSession(null);
|
|
191
|
+
this.resetPreviewMode();
|
|
192
|
+
this.layoutController.reset();
|
|
193
|
+
this.setCurrentFilePath(filePath);
|
|
194
|
+
this.lobby.setCurrentFile(null);
|
|
195
|
+
this.fileExplorer.setActiveFile(filePath);
|
|
196
|
+
|
|
197
|
+
this.elements.emptyState?.classList.add('hidden');
|
|
198
|
+
this.elements.editorPage?.classList.add('hidden');
|
|
199
|
+
this.elements.diffPage?.classList.remove('hidden');
|
|
200
|
+
if (this.elements.previewContent) {
|
|
201
|
+
this.elements.previewContent.innerHTML = '';
|
|
202
|
+
this.elements.previewContent.dataset.renderPhase = 'ready';
|
|
203
|
+
}
|
|
204
|
+
this.videoEmbed?.reconcileEmbeds(this.elements.previewContent);
|
|
205
|
+
this.resetPreviewSurface();
|
|
206
|
+
|
|
207
|
+
this.elements.outlineToggle?.classList.add('hidden');
|
|
208
|
+
this.elements.markdownToolbar?.classList.add('hidden');
|
|
209
|
+
|
|
210
|
+
this.renderAvatars();
|
|
211
|
+
this.renderPresence();
|
|
212
|
+
this.backlinksPanel.clear();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
showPreviewOnlyState(filePath) {
|
|
216
|
+
this.gitDiffView.hide();
|
|
217
|
+
this.fileHistoryView?.hide?.();
|
|
218
|
+
this.setSessionLoadToken(this.getSessionLoadToken() + 1);
|
|
219
|
+
this.clearInitialFileBootstrap();
|
|
220
|
+
this.clearStaticPreviewDocument?.();
|
|
221
|
+
this.workspaceCoordinator.cleanupSession();
|
|
222
|
+
this.setSession(null);
|
|
223
|
+
this.resetPreviewMode();
|
|
224
|
+
this.layoutController.reset();
|
|
225
|
+
this.setCurrentFilePath(filePath);
|
|
226
|
+
this.lobby.setCurrentFile(null);
|
|
227
|
+
this.fileExplorer.setActiveFile(filePath);
|
|
228
|
+
|
|
229
|
+
this.elements.emptyState?.classList.add('hidden');
|
|
230
|
+
this.elements.editorPage?.classList.remove('hidden');
|
|
231
|
+
this.elements.diffPage?.classList.add('hidden');
|
|
232
|
+
if (this.elements.previewContent) {
|
|
233
|
+
this.elements.previewContent.innerHTML = '';
|
|
234
|
+
this.elements.previewContent.dataset.renderPhase = 'ready';
|
|
235
|
+
}
|
|
236
|
+
this.videoEmbed?.reconcileEmbeds(this.elements.previewContent);
|
|
237
|
+
this.resetPreviewSurface();
|
|
238
|
+
|
|
239
|
+
this.elements.markdownToolbar?.classList.add('hidden');
|
|
240
|
+
|
|
241
|
+
this.renderAvatars();
|
|
242
|
+
this.renderPresence();
|
|
243
|
+
this.backlinksPanel.clear();
|
|
244
|
+
}
|
|
245
|
+
|
|
144
246
|
async openFile(filePath) {
|
|
145
247
|
this.imageLightbox?.close?.();
|
|
146
248
|
this.gitPanel.setSelection();
|
|
147
249
|
this.gitDiffView.hide();
|
|
250
|
+
this.fileHistoryView?.hide?.();
|
|
251
|
+
this.clearStaticPreviewDocument?.();
|
|
148
252
|
this.syncMainChrome({ mode: 'editor' });
|
|
149
253
|
await this.workspaceCoordinator.openFile(filePath);
|
|
150
254
|
this.setSession(this.workspaceCoordinator.getSession());
|
|
@@ -18,10 +18,12 @@ 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';
|
|
24
25
|
import { FileExplorerController } from '../presentation/file-explorer-controller.js';
|
|
26
|
+
import { FileHistoryViewController } from '../presentation/file-history-view-controller.js';
|
|
25
27
|
import { GitDiffViewController } from '../presentation/git-diff-view-controller.js';
|
|
26
28
|
import { GitPanelController } from '../presentation/git-panel-controller.js';
|
|
27
29
|
import { LayoutController } from '../presentation/layout-controller.js';
|
|
@@ -102,6 +104,7 @@ export class CollabMdAppShell {
|
|
|
102
104
|
this._previewHydrationPaused = false;
|
|
103
105
|
this._previewLayoutResizeObserver = null;
|
|
104
106
|
this._previewLayoutSyncTimer = null;
|
|
107
|
+
this._staticPreviewDocument = null;
|
|
105
108
|
this.pendingGitResetPath = null;
|
|
106
109
|
this.chatTimeFormatter = new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' });
|
|
107
110
|
this.chatNotificationsEnabled = this.preferences.getChatNotificationsEnabled();
|
|
@@ -111,11 +114,23 @@ export class CollabMdAppShell {
|
|
|
111
114
|
this.quickSwitcherModulePromise = null;
|
|
112
115
|
this.fileExplorerReadyPromise = Promise.resolve();
|
|
113
116
|
this.mobileBreakpointQuery = window.matchMedia('(max-width: 768px)');
|
|
117
|
+
this.pendingWorkspaceRequestIds = new Set();
|
|
118
|
+
this._fileOpenPerf = null;
|
|
114
119
|
|
|
115
120
|
this.lobby = new LobbyPresence({
|
|
116
121
|
preferredUserName: this.getStoredUserName(),
|
|
117
122
|
onChange: (users) => this.updateGlobalUsers(users),
|
|
118
123
|
onChatChange: (messages, meta) => this.updateChatMessages(messages, meta),
|
|
124
|
+
});
|
|
125
|
+
this.workspaceSync = new WorkspaceSyncClient({
|
|
126
|
+
onTreeChange: (tree, metadata = {}) => {
|
|
127
|
+
const wasReady = this.fileExplorerReady;
|
|
128
|
+
this.fileExplorer.setTree(tree, metadata);
|
|
129
|
+
this.fileExplorerReady = true;
|
|
130
|
+
if (!wasReady && this.isTabActive) {
|
|
131
|
+
void this.handleHashChange();
|
|
132
|
+
}
|
|
133
|
+
},
|
|
119
134
|
onWorkspaceEvent: (event) => {
|
|
120
135
|
void this.handleIncomingWorkspaceEvent(event);
|
|
121
136
|
},
|
|
@@ -136,6 +151,7 @@ export class CollabMdAppShell {
|
|
|
136
151
|
onPushBranch: () => this.pushGitBranch(),
|
|
137
152
|
onRepoChange: (isGitRepo, status) => this.handleGitRepoChange(isGitRepo, status),
|
|
138
153
|
onResetFile: (filePath, { scope }) => this.openGitResetDialog(filePath, { scope }),
|
|
154
|
+
onSelectCommit: (hash, { path }) => this.handleGitCommitSelection(hash, { closeSidebarOnMobile: true, path }),
|
|
139
155
|
onSelectDiff: (filePath, { scope }) => this.handleGitDiffSelection(filePath, { closeSidebarOnMobile: true, scope }),
|
|
140
156
|
onStageFile: (filePath, { scope }) => this.stageGitFile(filePath, { scope }),
|
|
141
157
|
onUnstageFile: (filePath, { scope }) => this.unstageGitFile(filePath, { scope }),
|
|
@@ -163,6 +179,10 @@ export class CollabMdAppShell {
|
|
|
163
179
|
getFileList: () => this.fileExplorer.flatDocumentFiles,
|
|
164
180
|
getSourceFilePath: () => this.currentFilePath,
|
|
165
181
|
onAfterRenderCommit: (_previewElement, stats) => {
|
|
182
|
+
this.recordFileOpenMetric('preview_committed', {
|
|
183
|
+
chars: stats?.chars ?? 0,
|
|
184
|
+
renderVersion: stats?.renderVersion ?? 0,
|
|
185
|
+
});
|
|
166
186
|
this.videoEmbed.reconcileEmbeds(this.elements.previewContent);
|
|
167
187
|
this.videoEmbed.syncLayout();
|
|
168
188
|
this.excalidrawEmbed.reconcileEmbeds(this.elements.previewContent, { isLargeDocument: stats.isLargeDocument });
|
|
@@ -254,12 +274,35 @@ export class CollabMdAppShell {
|
|
|
254
274
|
vaultApiClient: this.vaultApiClient,
|
|
255
275
|
});
|
|
256
276
|
this.gitDiffView = new GitDiffViewController({
|
|
277
|
+
onBackToHistory: ({ historyFilePath } = {}) => {
|
|
278
|
+
if (historyFilePath) {
|
|
279
|
+
this.navigation.navigateToGitFileHistory({ filePath: historyFilePath });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.navigation.navigateToGitHistory();
|
|
283
|
+
},
|
|
257
284
|
onCommitStaged: () => this.openGitCommitDialog(),
|
|
258
285
|
onOpenFile: (filePath) => filePath && this.navigation.navigateToFile(filePath),
|
|
259
286
|
onStageFile: (filePath, { scope }) => this.stageGitFile(filePath, { scope }),
|
|
260
287
|
onUnstageFile: (filePath, { scope }) => this.unstageGitFile(filePath, { scope }),
|
|
261
288
|
toastController: this.toastController,
|
|
262
289
|
});
|
|
290
|
+
this.fileHistoryView = new FileHistoryViewController({
|
|
291
|
+
diffRenderer: this.gitDiffView,
|
|
292
|
+
onOpenCommitDiff: (hash, { historyFilePath, path }) => this.handleGitCommitSelection(hash, {
|
|
293
|
+
closeSidebarOnMobile: false,
|
|
294
|
+
historyFilePath,
|
|
295
|
+
path,
|
|
296
|
+
}),
|
|
297
|
+
onOpenFile: (filePath) => filePath && this.navigation.navigateToFile(filePath),
|
|
298
|
+
onOpenPreview: ({ hash, path, currentFilePath }) => this.handleGitFilePreviewSelection({
|
|
299
|
+
hash,
|
|
300
|
+
path,
|
|
301
|
+
currentFilePath,
|
|
302
|
+
}),
|
|
303
|
+
onOpenWorkspaceDiff: (filePath) => this.handleGitDiffSelection(filePath, { closeSidebarOnMobile: false, scope: 'all' }),
|
|
304
|
+
toastController: this.toastController,
|
|
305
|
+
});
|
|
263
306
|
this.tabActivityLock = new TabActivityLock({
|
|
264
307
|
onActivated: ({ takeover }) => this.handleTabActivated({ takeover }),
|
|
265
308
|
onBlocked: () => this.handleTabBlocked({ reason: 'active-elsewhere' }),
|
|
@@ -301,6 +344,10 @@ export class CollabMdAppShell {
|
|
|
301
344
|
isMermaidFile: (filePath) => this.isMermaidFile(filePath),
|
|
302
345
|
isPlantUmlFile: (filePath) => this.isPlantUmlFile(filePath),
|
|
303
346
|
isTabActive: () => this.isTabActive,
|
|
347
|
+
loadBootstrapContent: async (filePath) => {
|
|
348
|
+
const response = await this.vaultApiClient.readFile(filePath);
|
|
349
|
+
return typeof response?.content === 'string' ? response.content : null;
|
|
350
|
+
},
|
|
304
351
|
loadEditorSessionClass: () => this.loadEditorSessionClass(),
|
|
305
352
|
loadBacklinks: (filePath) => this.backlinksPanel.load(filePath),
|
|
306
353
|
onBeforeFileOpen: () => {
|
|
@@ -333,6 +380,7 @@ export class CollabMdAppShell {
|
|
|
333
380
|
},
|
|
334
381
|
onSelectionChange: (anchor) => this.handleCommentSelectionChange(anchor),
|
|
335
382
|
onImagePaste: (file) => this.handleEditorImageInsert(file),
|
|
383
|
+
onFileOpenMetric: (name, payload) => this.recordFileOpenMetric(name, payload),
|
|
336
384
|
onSessionAssigned: (session) => {
|
|
337
385
|
this.session = session;
|
|
338
386
|
this.commentUi.attachSession(session);
|
|
@@ -348,6 +396,7 @@ export class CollabMdAppShell {
|
|
|
348
396
|
onUpdateVisibleChrome: (filePath, { displayName }) => {
|
|
349
397
|
this.syncFileChrome(filePath);
|
|
350
398
|
this.syncCommentChrome(filePath);
|
|
399
|
+
this.syncFileHistoryButton({ filePath, mode: 'editor' });
|
|
351
400
|
if (this.elements.activeFileName) {
|
|
352
401
|
this.elements.activeFileName.textContent = displayName;
|
|
353
402
|
}
|
|
@@ -361,9 +410,11 @@ export class CollabMdAppShell {
|
|
|
361
410
|
this.workspaceRouteController = new WorkspaceRouteController({
|
|
362
411
|
backlinksPanel: this.backlinksPanel,
|
|
363
412
|
clearInitialFileBootstrap: () => this.clearInitialFileBootstrap(),
|
|
413
|
+
clearStaticPreviewDocument: () => this.clearStaticPreviewDocument(),
|
|
364
414
|
closeSidebarOnMobile: () => this.closeSidebarOnMobile(),
|
|
365
415
|
elements: this.elements,
|
|
366
416
|
excalidrawEmbed: this.excalidrawEmbed,
|
|
417
|
+
fileHistoryView: this.fileHistoryView,
|
|
367
418
|
fileExplorer: this.fileExplorer,
|
|
368
419
|
getIsTabActive: () => this.isTabActive,
|
|
369
420
|
getSessionLoadToken: () => this.sessionLoadToken,
|
|
@@ -394,7 +445,11 @@ export class CollabMdAppShell {
|
|
|
394
445
|
this.sessionLoadToken = value;
|
|
395
446
|
},
|
|
396
447
|
setSidebarTab: (value) => this.setSidebarTab(value),
|
|
448
|
+
showGitCommit: (route) => this.showGitCommit(route),
|
|
397
449
|
showGitDiff: (route) => this.showGitDiff(route),
|
|
450
|
+
showGitFileHistory: (route) => this.showGitFileHistory(route),
|
|
451
|
+
showGitFilePreview: (route) => this.showGitFilePreview(route),
|
|
452
|
+
showGitHistory: () => this.showGitHistory(),
|
|
398
453
|
syncMainChrome: (payload) => this.syncMainChrome(payload),
|
|
399
454
|
videoEmbed: this.videoEmbed,
|
|
400
455
|
workspaceCoordinator: this.workspaceCoordinator,
|
|
@@ -440,6 +495,58 @@ export class CollabMdAppShell {
|
|
|
440
495
|
get followedCursorSignature() { return this.stateStore.get('followedCursorSignature'); }
|
|
441
496
|
set followedCursorSignature(value) { this.stateStore.set('followedCursorSignature', value); }
|
|
442
497
|
|
|
498
|
+
publishFileOpenPerf() {
|
|
499
|
+
if (typeof window === 'undefined') {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
window.__COLLABMD_PERF__ ??= {};
|
|
504
|
+
window.__COLLABMD_PERF__.fileOpen = this._fileOpenPerf
|
|
505
|
+
? {
|
|
506
|
+
...this._fileOpenPerf,
|
|
507
|
+
details: { ...this._fileOpenPerf.details },
|
|
508
|
+
marks: { ...this._fileOpenPerf.marks },
|
|
509
|
+
}
|
|
510
|
+
: null;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
recordFileOpenMetric(name, payload = {}) {
|
|
514
|
+
if (name === 'open_started') {
|
|
515
|
+
this._fileOpenPerf = {
|
|
516
|
+
details: {
|
|
517
|
+
filePath: payload.filePath || this.currentFilePath || '',
|
|
518
|
+
loadToken: payload.loadToken ?? 0,
|
|
519
|
+
},
|
|
520
|
+
marks: {
|
|
521
|
+
open_started: performance.now(),
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
this.publishFileOpenPerf();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (!this._fileOpenPerf) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const metricLoadToken = payload.loadToken ?? this._fileOpenPerf.details.loadToken;
|
|
533
|
+
if (metricLoadToken !== this._fileOpenPerf.details.loadToken) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
this._fileOpenPerf.marks[name] = performance.now();
|
|
538
|
+
if (payload.filePath) {
|
|
539
|
+
this._fileOpenPerf.details.filePath = payload.filePath;
|
|
540
|
+
}
|
|
541
|
+
Object.entries(payload).forEach(([key, value]) => {
|
|
542
|
+
if (key === 'filePath' || key === 'loadToken') {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
this._fileOpenPerf.details[key] = value;
|
|
546
|
+
});
|
|
547
|
+
this.publishFileOpenPerf();
|
|
548
|
+
}
|
|
549
|
+
|
|
443
550
|
loadEditorSessionClass() {
|
|
444
551
|
if (!this._editorSessionModulePromise) {
|
|
445
552
|
this._editorSessionModulePromise = import('../infrastructure/editor-session.js')
|
|
@@ -63,6 +63,8 @@ export function getClientRuntimeConfig() {
|
|
|
63
63
|
auth: {
|
|
64
64
|
enabled: false,
|
|
65
65
|
implemented: true,
|
|
66
|
+
loginEndpoint: '/api/auth/oidc/login',
|
|
67
|
+
provider: '',
|
|
66
68
|
requiresLogin: false,
|
|
67
69
|
sessionEndpoint: '/api/auth/session',
|
|
68
70
|
statusEndpoint: '/api/auth/status',
|
|
@@ -79,6 +81,8 @@ export function getClientRuntimeConfig() {
|
|
|
79
81
|
const authConfig = {
|
|
80
82
|
enabled: false,
|
|
81
83
|
implemented: true,
|
|
84
|
+
loginEndpoint: '/api/auth/oidc/login',
|
|
85
|
+
provider: '',
|
|
82
86
|
requiresLogin: false,
|
|
83
87
|
sessionEndpoint: '/api/auth/session',
|
|
84
88
|
statusEndpoint: '/api/auth/status',
|
|
@@ -90,6 +94,7 @@ export function getClientRuntimeConfig() {
|
|
|
90
94
|
...rawConfig,
|
|
91
95
|
auth: {
|
|
92
96
|
...authConfig,
|
|
97
|
+
loginEndpoint: applyBasePath(basePath, authConfig.loginEndpoint),
|
|
93
98
|
sessionEndpoint: applyBasePath(basePath, authConfig.sessionEndpoint),
|
|
94
99
|
statusEndpoint: applyBasePath(basePath, authConfig.statusEndpoint),
|
|
95
100
|
},
|
|
@@ -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
|