collabmd 0.1.21 → 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 +94 -7
- 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/editor-session-S5AT7BMV.js +22 -0
- package/public/assets/js/{excalidraw-editor-TCIH6GJL.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/index.html +104 -84
- package/src/client/application/app-shell/git-feature.js +247 -9
- package/src/client/application/app-shell/ui-feature.js +65 -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 +95 -0
- package/src/client/domain/runtime-paths.js +5 -0
- package/src/client/excalidraw-editor.js +50 -2
- 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 +47 -7
- package/src/client/infrastructure/editor-view-adapter.js +122 -58
- package/src/client/infrastructure/runtime-config.js +66 -0
- package/src/client/presentation/comment-ui-controller.js +82 -26
- package/src/client/presentation/comments-panel.js +2 -2
- package/src/client/presentation/excalidraw-embed-controller.js +60 -2
- 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 -4945
- 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 +8 -0
- package/src/server/domain/collaboration/collaboration-room.js +90 -47
- 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 +73 -0
- package/src/server/infrastructure/http/create-auth-api-handler.js +30 -2
- package/src/server/infrastructure/http/create-git-api-command-handler.js +2 -0
- package/src/server/infrastructure/http/create-git-api-handler.js +2 -0
- package/src/server/infrastructure/http/create-git-api-query-handler.js +61 -0
- package/src/server/infrastructure/http/create-request-handler.js +15 -0
- package/public/assets/js/chunks/editor-session-EAUBJCHH.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());
|
|
@@ -23,6 +23,7 @@ import { BacklinksPanel } from '../presentation/backlinks-panel.js';
|
|
|
23
23
|
import { CommentUiController } from '../presentation/comment-ui-controller.js';
|
|
24
24
|
import { ExcalidrawEmbedController } from '../presentation/excalidraw-embed-controller.js';
|
|
25
25
|
import { FileExplorerController } from '../presentation/file-explorer-controller.js';
|
|
26
|
+
import { FileHistoryViewController } from '../presentation/file-history-view-controller.js';
|
|
26
27
|
import { GitDiffViewController } from '../presentation/git-diff-view-controller.js';
|
|
27
28
|
import { GitPanelController } from '../presentation/git-panel-controller.js';
|
|
28
29
|
import { LayoutController } from '../presentation/layout-controller.js';
|
|
@@ -103,6 +104,7 @@ export class CollabMdAppShell {
|
|
|
103
104
|
this._previewHydrationPaused = false;
|
|
104
105
|
this._previewLayoutResizeObserver = null;
|
|
105
106
|
this._previewLayoutSyncTimer = null;
|
|
107
|
+
this._staticPreviewDocument = null;
|
|
106
108
|
this.pendingGitResetPath = null;
|
|
107
109
|
this.chatTimeFormatter = new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' });
|
|
108
110
|
this.chatNotificationsEnabled = this.preferences.getChatNotificationsEnabled();
|
|
@@ -113,6 +115,7 @@ export class CollabMdAppShell {
|
|
|
113
115
|
this.fileExplorerReadyPromise = Promise.resolve();
|
|
114
116
|
this.mobileBreakpointQuery = window.matchMedia('(max-width: 768px)');
|
|
115
117
|
this.pendingWorkspaceRequestIds = new Set();
|
|
118
|
+
this._fileOpenPerf = null;
|
|
116
119
|
|
|
117
120
|
this.lobby = new LobbyPresence({
|
|
118
121
|
preferredUserName: this.getStoredUserName(),
|
|
@@ -148,6 +151,7 @@ export class CollabMdAppShell {
|
|
|
148
151
|
onPushBranch: () => this.pushGitBranch(),
|
|
149
152
|
onRepoChange: (isGitRepo, status) => this.handleGitRepoChange(isGitRepo, status),
|
|
150
153
|
onResetFile: (filePath, { scope }) => this.openGitResetDialog(filePath, { scope }),
|
|
154
|
+
onSelectCommit: (hash, { path }) => this.handleGitCommitSelection(hash, { closeSidebarOnMobile: true, path }),
|
|
151
155
|
onSelectDiff: (filePath, { scope }) => this.handleGitDiffSelection(filePath, { closeSidebarOnMobile: true, scope }),
|
|
152
156
|
onStageFile: (filePath, { scope }) => this.stageGitFile(filePath, { scope }),
|
|
153
157
|
onUnstageFile: (filePath, { scope }) => this.unstageGitFile(filePath, { scope }),
|
|
@@ -175,6 +179,10 @@ export class CollabMdAppShell {
|
|
|
175
179
|
getFileList: () => this.fileExplorer.flatDocumentFiles,
|
|
176
180
|
getSourceFilePath: () => this.currentFilePath,
|
|
177
181
|
onAfterRenderCommit: (_previewElement, stats) => {
|
|
182
|
+
this.recordFileOpenMetric('preview_committed', {
|
|
183
|
+
chars: stats?.chars ?? 0,
|
|
184
|
+
renderVersion: stats?.renderVersion ?? 0,
|
|
185
|
+
});
|
|
178
186
|
this.videoEmbed.reconcileEmbeds(this.elements.previewContent);
|
|
179
187
|
this.videoEmbed.syncLayout();
|
|
180
188
|
this.excalidrawEmbed.reconcileEmbeds(this.elements.previewContent, { isLargeDocument: stats.isLargeDocument });
|
|
@@ -266,12 +274,35 @@ export class CollabMdAppShell {
|
|
|
266
274
|
vaultApiClient: this.vaultApiClient,
|
|
267
275
|
});
|
|
268
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
|
+
},
|
|
269
284
|
onCommitStaged: () => this.openGitCommitDialog(),
|
|
270
285
|
onOpenFile: (filePath) => filePath && this.navigation.navigateToFile(filePath),
|
|
271
286
|
onStageFile: (filePath, { scope }) => this.stageGitFile(filePath, { scope }),
|
|
272
287
|
onUnstageFile: (filePath, { scope }) => this.unstageGitFile(filePath, { scope }),
|
|
273
288
|
toastController: this.toastController,
|
|
274
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
|
+
});
|
|
275
306
|
this.tabActivityLock = new TabActivityLock({
|
|
276
307
|
onActivated: ({ takeover }) => this.handleTabActivated({ takeover }),
|
|
277
308
|
onBlocked: () => this.handleTabBlocked({ reason: 'active-elsewhere' }),
|
|
@@ -313,6 +344,10 @@ export class CollabMdAppShell {
|
|
|
313
344
|
isMermaidFile: (filePath) => this.isMermaidFile(filePath),
|
|
314
345
|
isPlantUmlFile: (filePath) => this.isPlantUmlFile(filePath),
|
|
315
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
|
+
},
|
|
316
351
|
loadEditorSessionClass: () => this.loadEditorSessionClass(),
|
|
317
352
|
loadBacklinks: (filePath) => this.backlinksPanel.load(filePath),
|
|
318
353
|
onBeforeFileOpen: () => {
|
|
@@ -345,6 +380,7 @@ export class CollabMdAppShell {
|
|
|
345
380
|
},
|
|
346
381
|
onSelectionChange: (anchor) => this.handleCommentSelectionChange(anchor),
|
|
347
382
|
onImagePaste: (file) => this.handleEditorImageInsert(file),
|
|
383
|
+
onFileOpenMetric: (name, payload) => this.recordFileOpenMetric(name, payload),
|
|
348
384
|
onSessionAssigned: (session) => {
|
|
349
385
|
this.session = session;
|
|
350
386
|
this.commentUi.attachSession(session);
|
|
@@ -360,6 +396,7 @@ export class CollabMdAppShell {
|
|
|
360
396
|
onUpdateVisibleChrome: (filePath, { displayName }) => {
|
|
361
397
|
this.syncFileChrome(filePath);
|
|
362
398
|
this.syncCommentChrome(filePath);
|
|
399
|
+
this.syncFileHistoryButton({ filePath, mode: 'editor' });
|
|
363
400
|
if (this.elements.activeFileName) {
|
|
364
401
|
this.elements.activeFileName.textContent = displayName;
|
|
365
402
|
}
|
|
@@ -373,9 +410,11 @@ export class CollabMdAppShell {
|
|
|
373
410
|
this.workspaceRouteController = new WorkspaceRouteController({
|
|
374
411
|
backlinksPanel: this.backlinksPanel,
|
|
375
412
|
clearInitialFileBootstrap: () => this.clearInitialFileBootstrap(),
|
|
413
|
+
clearStaticPreviewDocument: () => this.clearStaticPreviewDocument(),
|
|
376
414
|
closeSidebarOnMobile: () => this.closeSidebarOnMobile(),
|
|
377
415
|
elements: this.elements,
|
|
378
416
|
excalidrawEmbed: this.excalidrawEmbed,
|
|
417
|
+
fileHistoryView: this.fileHistoryView,
|
|
379
418
|
fileExplorer: this.fileExplorer,
|
|
380
419
|
getIsTabActive: () => this.isTabActive,
|
|
381
420
|
getSessionLoadToken: () => this.sessionLoadToken,
|
|
@@ -406,7 +445,11 @@ export class CollabMdAppShell {
|
|
|
406
445
|
this.sessionLoadToken = value;
|
|
407
446
|
},
|
|
408
447
|
setSidebarTab: (value) => this.setSidebarTab(value),
|
|
448
|
+
showGitCommit: (route) => this.showGitCommit(route),
|
|
409
449
|
showGitDiff: (route) => this.showGitDiff(route),
|
|
450
|
+
showGitFileHistory: (route) => this.showGitFileHistory(route),
|
|
451
|
+
showGitFilePreview: (route) => this.showGitFilePreview(route),
|
|
452
|
+
showGitHistory: () => this.showGitHistory(),
|
|
410
453
|
syncMainChrome: (payload) => this.syncMainChrome(payload),
|
|
411
454
|
videoEmbed: this.videoEmbed,
|
|
412
455
|
workspaceCoordinator: this.workspaceCoordinator,
|
|
@@ -452,6 +495,58 @@ export class CollabMdAppShell {
|
|
|
452
495
|
get followedCursorSignature() { return this.stateStore.get('followedCursorSignature'); }
|
|
453
496
|
set followedCursorSignature(value) { this.stateStore.set('followedCursorSignature', value); }
|
|
454
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
|
+
|
|
455
550
|
loadEditorSessionClass() {
|
|
456
551
|
if (!this._editorSessionModulePromise) {
|
|
457
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
|
},
|