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.
Files changed (99) hide show
  1. package/README.md +94 -7
  2. package/bin/collabmd.js +3 -2
  3. package/docker-compose.yml +13 -1
  4. package/package.json +2 -1
  5. package/public/assets/css/style.css +1 -1
  6. package/public/assets/js/chunks/{chunk-HEWVH67U.js → chunk-CYK7KO7W.js} +8 -8
  7. package/public/assets/js/chunks/editor-session-S5AT7BMV.js +22 -0
  8. package/public/assets/js/{excalidraw-editor-TCIH6GJL.js → excalidraw-editor-5YWMGLKF.js} +145 -62
  9. package/public/assets/js/excalidraw-editor.js +1 -1
  10. package/public/assets/js/main.js +503 -182
  11. package/public/index.html +104 -84
  12. package/src/client/application/app-shell/git-feature.js +247 -9
  13. package/src/client/application/app-shell/ui-feature.js +65 -7
  14. package/src/client/application/app-shell/workspace-feature.js +46 -0
  15. package/src/client/application/app-shell-elements.js +2 -0
  16. package/src/client/application/mermaid-preview-hydrator.js +1 -1
  17. package/src/client/application/plantuml-preview-hydrator.js +1 -1
  18. package/src/client/application/workspace-coordinator.js +147 -18
  19. package/src/client/application/workspace-preview-controller.js +34 -0
  20. package/src/client/application/workspace-route-controller.js +104 -0
  21. package/src/client/bootstrap/collabmd-app-shell.js +95 -0
  22. package/src/client/domain/runtime-paths.js +5 -0
  23. package/src/client/excalidraw-editor.js +50 -2
  24. package/src/client/infrastructure/auth-client.js +219 -52
  25. package/src/client/infrastructure/browser-navigation-port.js +20 -0
  26. package/src/client/infrastructure/editor-collaboration-client.js +4 -0
  27. package/src/client/infrastructure/editor-session.js +47 -7
  28. package/src/client/infrastructure/editor-view-adapter.js +122 -58
  29. package/src/client/infrastructure/runtime-config.js +66 -0
  30. package/src/client/presentation/comment-ui-controller.js +82 -26
  31. package/src/client/presentation/comments-panel.js +2 -2
  32. package/src/client/presentation/excalidraw-embed-controller.js +60 -2
  33. package/src/client/presentation/file-history-view-controller.js +806 -0
  34. package/src/client/presentation/git-diff-view-controller.js +718 -121
  35. package/src/client/presentation/git-panel-controller.js +458 -66
  36. package/src/client/presentation/layout-controller.js +1 -1
  37. package/src/client/styles/components/actions.css +89 -0
  38. package/src/client/styles/components/feedback.css +43 -0
  39. package/src/client/styles/components/navigation.css +68 -0
  40. package/src/client/styles/components/scrollbars.css +104 -0
  41. package/src/client/styles/components/states.css +289 -0
  42. package/src/client/styles/components/surfaces.css +155 -0
  43. package/src/client/styles/features/backlinks.css +213 -0
  44. package/src/client/styles/features/collaboration-chat.css +228 -0
  45. package/src/client/styles/features/collaboration-dialogs.css +154 -0
  46. package/src/client/styles/features/collaboration-presence.css +103 -0
  47. package/src/client/styles/features/collaboration-ui.css +3 -0
  48. package/src/client/styles/features/comment-card.css +236 -0
  49. package/src/client/styles/features/comment-markdown.css +127 -0
  50. package/src/client/styles/features/comment-overlays.css +60 -0
  51. package/src/client/styles/features/comments-drawer.css +120 -0
  52. package/src/client/styles/features/comments.css +4 -0
  53. package/src/client/styles/features/diagram-preview.css +227 -0
  54. package/src/client/styles/features/diff-commit.css +268 -0
  55. package/src/client/styles/features/diff-lines.css +160 -0
  56. package/src/client/styles/features/diff-toolbar.css +98 -0
  57. package/src/client/styles/features/diff.css +3 -0
  58. package/src/client/styles/features/embedded-preview.css +218 -0
  59. package/src/client/styles/features/git-branch.css +155 -0
  60. package/src/client/styles/features/git-files.css +105 -0
  61. package/src/client/styles/features/git-history.css +339 -0
  62. package/src/client/styles/features/git.css +3 -0
  63. package/src/client/styles/features/media-lightbox.css +157 -0
  64. package/src/client/styles/features/outline.css +115 -0
  65. package/src/client/styles/features/preview-content.css +3 -0
  66. package/src/client/styles/features/preview-markdown.css +201 -0
  67. package/src/client/styles/features/preview-sidebars.css +2 -0
  68. package/src/client/styles/features/quick-switcher.css +167 -0
  69. package/src/client/styles/foundation/themes.css +347 -0
  70. package/src/client/styles/foundation/tokens.css +66 -0
  71. package/src/client/styles/foundation/utilities.css +36 -0
  72. package/src/client/styles/layout/editor-page.css +203 -0
  73. package/src/client/styles/layout/overlays.css +132 -0
  74. package/src/client/styles/layout/responsive.css +251 -0
  75. package/src/client/styles/layout/shell.css +314 -0
  76. package/src/client/styles/layout/sidebar.css +176 -0
  77. package/src/client/styles/layout/view-modes.css +92 -0
  78. package/src/client/styles/overrides/codemirror.css +53 -0
  79. package/src/client/styles/overrides/highlightjs.css +7 -0
  80. package/src/client/styles/overrides/vendor.css +2 -0
  81. package/src/client/styles/primitives/controls.css +276 -0
  82. package/src/client/styles/style.css +25 -4945
  83. package/src/server/auth/create-auth-service.js +373 -15
  84. package/src/server/auth/session-cookie.js +30 -4
  85. package/src/server/config/env.js +121 -5
  86. package/src/server/create-app-server.js +8 -0
  87. package/src/server/domain/collaboration/collaboration-room.js +90 -47
  88. package/src/server/index.js +3 -1
  89. package/src/server/infrastructure/git/command-runner.js +2 -1
  90. package/src/server/infrastructure/git/git-service.js +48 -2
  91. package/src/server/infrastructure/git/history-service.js +683 -0
  92. package/src/server/infrastructure/git/parsers.js +30 -0
  93. package/src/server/infrastructure/git/responses.js +73 -0
  94. package/src/server/infrastructure/http/create-auth-api-handler.js +30 -2
  95. package/src/server/infrastructure/http/create-git-api-command-handler.js +2 -0
  96. package/src/server/infrastructure/http/create-git-api-handler.js +2 -0
  97. package/src/server/infrastructure/http/create-git-api-query-handler.js +61 -0
  98. package/src/server/infrastructure/http/create-request-handler.js +15 -0
  99. package/public/assets/js/chunks/editor-session-EAUBJCHH.js +0 -22
@@ -53,6 +53,46 @@ export const gitFeature = {
53
53
  this.navigation.navigateToGitDiff({ filePath, scope });
54
54
  },
55
55
 
56
+ handleGitCommitSelection(hash, { closeSidebarOnMobile = false, historyFilePath = null, path = null } = {}) {
57
+ if (closeSidebarOnMobile) {
58
+ this.closeSidebarOnMobile();
59
+ }
60
+
61
+ this.navigation.navigateToGitCommit({ hash, historyFilePath, path });
62
+ },
63
+
64
+ handleGitHistorySelection({ closeSidebarOnMobile = false } = {}) {
65
+ if (closeSidebarOnMobile) {
66
+ this.closeSidebarOnMobile();
67
+ }
68
+
69
+ this.navigation.navigateToGitHistory();
70
+ },
71
+
72
+ handleGitFileHistorySelection(filePath = this.currentFilePath, { closeSidebarOnMobile = false } = {}) {
73
+ if (closeSidebarOnMobile) {
74
+ this.closeSidebarOnMobile();
75
+ }
76
+
77
+ if (!filePath) {
78
+ return;
79
+ }
80
+
81
+ this.navigation.navigateToGitFileHistory({ filePath });
82
+ },
83
+
84
+ handleGitFilePreviewSelection({ hash, path, currentFilePath = null, closeSidebarOnMobile = false } = {}) {
85
+ if (closeSidebarOnMobile) {
86
+ this.closeSidebarOnMobile();
87
+ }
88
+
89
+ if (!hash || !path) {
90
+ return;
91
+ }
92
+
93
+ this.navigation.navigateToGitFilePreview({ hash, path, currentFilePath });
94
+ },
95
+
56
96
  handleGitRepoChange(isGitRepo, status = null) {
57
97
  this.gitRepoAvailable = Boolean(isGitRepo);
58
98
  this.elements.sidebarTabs?.classList.toggle('hidden', !this.gitRepoAvailable);
@@ -64,35 +104,209 @@ export const gitFeature = {
64
104
  this.gitDiffView.setRepoStatus(this.gitRepoAvailable ? status : null);
65
105
 
66
106
  if (!this.gitRepoAvailable && this.activeSidebarTab === 'git') {
107
+ this.syncFileHistoryButton({ mode: this.navigation.getHashRoute().type === 'git-file-preview' ? 'history-preview' : 'editor' });
67
108
  this.setSidebarTab('files');
68
109
  return;
69
110
  }
70
111
 
71
- if (this.gitRepoAvailable && this.navigation.getHashRoute().type === 'git-diff' && this.activeSidebarTab !== 'git') {
112
+ const routeType = this.navigation.getHashRoute().type;
113
+ if (
114
+ this.gitRepoAvailable
115
+ && (routeType === 'git-diff' || routeType === 'git-commit' || routeType === 'git-history')
116
+ && this.activeSidebarTab !== 'git'
117
+ ) {
72
118
  this.setSidebarTab('git');
73
119
  }
120
+
121
+ this.syncFileHistoryButton({
122
+ filePath: this.currentFilePath,
123
+ mode: routeType === 'git-file-preview'
124
+ ? 'history-preview'
125
+ : routeType === 'file'
126
+ ? 'editor'
127
+ : 'empty',
128
+ });
129
+ },
130
+
131
+ syncFileHistoryButton({ filePath = this.currentFilePath, mode = 'editor' } = {}) {
132
+ const button = this.elements.fileHistoryButton;
133
+ const label = this.elements.fileHistoryButtonLabel;
134
+ if (!button || !label) {
135
+ return;
136
+ }
137
+
138
+ const supported = this.gitRepoAvailable && this.supportsFileHistory?.(filePath);
139
+ if (!supported) {
140
+ button.classList.add('hidden');
141
+ return;
142
+ }
143
+
144
+ button.classList.remove('hidden');
145
+ if (mode === 'history-preview') {
146
+ label.textContent = 'Back to History';
147
+ button.setAttribute('aria-label', 'Back to file history');
148
+ button.title = 'Back to file history';
149
+ return;
150
+ }
151
+
152
+ if (mode !== 'editor') {
153
+ button.classList.add('hidden');
154
+ return;
155
+ }
156
+
157
+ label.textContent = 'History';
158
+ button.setAttribute('aria-label', 'Open file history');
159
+ button.title = 'View file history';
74
160
  },
75
161
 
76
- syncMainChrome({ mode, title = null } = {}) {
77
- const isDiffMode = mode === 'diff';
78
- this.elements.toolbarCenter?.classList.toggle('hidden', isDiffMode);
79
- this.elements.mobileViewToggle?.classList.toggle('hidden', isDiffMode);
80
- this.elements.userCount?.classList.toggle('hidden', isDiffMode);
81
- this.elements.toolbarDiffBadge?.classList.toggle('hidden', !isDiffMode);
162
+ syncMainChrome({ badgeLabel = '', mode, title = null } = {}) {
163
+ const isSpecialMode = mode === 'diff' || mode === 'history' || mode === 'history-preview';
164
+ this.elements.toolbarCenter?.classList.toggle('hidden', isSpecialMode);
165
+ this.elements.mobileViewToggle?.classList.toggle('hidden', isSpecialMode);
166
+ this.elements.userCount?.classList.toggle('hidden', isSpecialMode);
167
+ this.elements.toolbarDiffBadge?.classList.toggle('hidden', !badgeLabel);
168
+ if (this.elements.toolbarDiffBadge) {
169
+ this.elements.toolbarDiffBadge.textContent = badgeLabel;
170
+ }
82
171
 
83
172
  if (title && this.elements.activeFileName) {
84
173
  this.elements.activeFileName.textContent = title;
85
174
  }
175
+
176
+ this.syncFileHistoryButton({ filePath: this.currentFilePath, mode });
86
177
  },
87
178
 
88
179
  async showGitDiff({ filePath = null, scope = 'all' } = {}) {
89
- this.gitPanel.setSelection(filePath ? { path: filePath, scope } : {});
180
+ this.clearStaticPreviewDocument?.();
181
+ this.gitPanel.setSelection(filePath ? { path: filePath, scope, source: 'workspace' } : {});
182
+ this.gitPanel.setMode('changes');
90
183
  this.showDiffState();
91
184
  this.syncMainChrome({
185
+ badgeLabel: 'Diff',
92
186
  mode: 'diff',
93
187
  title: this.gitDiffView.getToolbarTitle({ filePath, scope }),
94
188
  });
95
- await this.gitDiffView.open({ filePath, scope });
189
+ this.syncFileHistoryButton({ filePath, mode: 'diff' });
190
+ await this.gitDiffView.openWorkspaceDiff({ filePath, scope });
191
+ },
192
+
193
+ async showGitCommit({ hash, path = null, historyFilePath = null } = {}) {
194
+ this.clearStaticPreviewDocument?.();
195
+ this.gitPanel.setMode('history');
196
+ this.gitPanel.setSelection(hash ? { commitHash: hash, path, source: 'commit' } : { source: 'commit' });
197
+ this.showDiffState();
198
+ this.syncMainChrome({
199
+ badgeLabel: 'Diff',
200
+ mode: 'diff',
201
+ title: this.gitDiffView.getToolbarTitle({ commitHash: hash, path, source: 'commit' }),
202
+ });
203
+ this.syncFileHistoryButton({ filePath: path, mode: 'diff' });
204
+ await this.gitDiffView.openCommitDiff({ hash, historyFilePath, path });
205
+ },
206
+
207
+ async showGitHistory() {
208
+ this.clearStaticPreviewDocument?.();
209
+ this.gitPanel.setMode('history');
210
+ this.gitPanel.setSelection({ source: 'commit' });
211
+ this.workspaceRouteController?.showEmptyState?.();
212
+ this.syncMainChrome({
213
+ mode: 'empty',
214
+ title: 'Git History',
215
+ });
216
+ this.syncFileHistoryButton({ mode: 'empty' });
217
+ },
218
+
219
+ async showGitFileHistory({ filePath = null } = {}) {
220
+ if (!filePath) {
221
+ this.workspaceRouteController?.showEmptyState?.();
222
+ return;
223
+ }
224
+
225
+ this.clearStaticPreviewDocument?.();
226
+ this.commentUi?.attachSession?.(null);
227
+ this.commentUi?.setCurrentFile?.(filePath, {
228
+ fileKind: this.getCommentFileKind(filePath),
229
+ supported: false,
230
+ });
231
+ this.handleCommentThreadsChange?.([]);
232
+ this.handleCommentSelectionChange?.(null);
233
+ this.workspaceRouteController?.showFileHistoryState?.(filePath);
234
+ this.syncMainChrome({
235
+ badgeLabel: 'History',
236
+ mode: 'history',
237
+ title: this.getDisplayName(filePath),
238
+ });
239
+ this.syncFileHistoryButton({ filePath, mode: 'history' });
240
+ await this.fileHistoryView.openFileHistory({ filePath });
241
+ },
242
+
243
+ async showGitFilePreview({ hash, filePath = null, currentFilePath = null } = {}) {
244
+ const resolvedCurrentFilePath = String(currentFilePath ?? '').trim() || filePath;
245
+ if (!hash || !filePath || !resolvedCurrentFilePath) {
246
+ this.workspaceRouteController?.showEmptyState?.();
247
+ return;
248
+ }
249
+
250
+ this.commentUi?.attachSession?.(null);
251
+ this.commentUi?.setCurrentFile?.(resolvedCurrentFilePath, {
252
+ fileKind: this.getCommentFileKind(resolvedCurrentFilePath),
253
+ supported: false,
254
+ });
255
+ this.handleCommentThreadsChange?.([]);
256
+ this.handleCommentSelectionChange?.(null);
257
+ this.workspaceRouteController?.showPreviewOnlyState?.(resolvedCurrentFilePath);
258
+ this.layoutController.setView('preview', { persist: false });
259
+ this.syncMainChrome({
260
+ badgeLabel: 'History Preview',
261
+ mode: 'history-preview',
262
+ title: this.getDisplayName(resolvedCurrentFilePath),
263
+ });
264
+ this.syncFileHistoryButton({ filePath: resolvedCurrentFilePath, mode: 'history-preview' });
265
+
266
+ try {
267
+ const query = new URLSearchParams();
268
+ query.set('hash', hash);
269
+ query.set('path', filePath);
270
+ const response = await fetch(resolveApiUrl(`/git/file-snapshot?${query.toString()}`));
271
+ const data = await response.json();
272
+ if (!response.ok) {
273
+ throw new Error(data.error || 'Failed to load historical file preview');
274
+ }
275
+
276
+ this.setStaticPreviewDocument?.({
277
+ ...data,
278
+ currentFilePath: resolvedCurrentFilePath,
279
+ });
280
+ this.resetPreviewMode();
281
+ this.elements.markdownToolbar?.classList.add('hidden');
282
+ this.elements.outlineToggle?.classList.toggle('hidden', data.fileKind !== 'markdown');
283
+
284
+ if (data.fileKind === 'markdown' || data.fileKind === 'mermaid' || data.fileKind === 'plantuml') {
285
+ if (data.fileKind === 'mermaid') {
286
+ this.elements.previewContent?.classList.add('is-mermaid-file-preview');
287
+ } else if (data.fileKind === 'plantuml') {
288
+ this.elements.previewContent?.classList.add('is-plantuml-file-preview');
289
+ }
290
+ this.previewRenderer.beginDocumentLoad();
291
+ this.previewRenderer.queueRender();
292
+ return;
293
+ }
294
+
295
+ this.elements.outlineToggle?.classList.add('hidden');
296
+ this.renderTextFilePreview({
297
+ content: data.content,
298
+ filePath: resolvedCurrentFilePath,
299
+ });
300
+ } catch (error) {
301
+ console.error('[git-preview] Failed to load file snapshot:', error);
302
+ this.clearStaticPreviewDocument?.();
303
+ this.toastController.show('Failed to load historical file preview');
304
+ this.renderTextFilePreview({
305
+ content: 'Failed to load historical file preview.',
306
+ filePath: resolvedCurrentFilePath,
307
+ });
308
+ this.elements.outlineToggle?.classList.add('hidden');
309
+ }
96
310
  },
97
311
 
98
312
  async postGitAction(endpoint, payload) {
@@ -127,6 +341,30 @@ export const gitFeature = {
127
341
  await this.gitPanel.refresh({ force: true });
128
342
 
129
343
  const route = this.navigation.getHashRoute();
344
+ if (route.type === 'git-history') {
345
+ await this.showGitHistory();
346
+ return;
347
+ }
348
+
349
+ if (route.type === 'git-file-history') {
350
+ await this.showGitFileHistory({ filePath: route.filePath });
351
+ return;
352
+ }
353
+
354
+ if (route.type === 'git-file-preview') {
355
+ await this.showGitFilePreview({
356
+ hash: route.hash,
357
+ filePath: route.filePath,
358
+ currentFilePath: route.currentFilePath,
359
+ });
360
+ return;
361
+ }
362
+
363
+ if (route.type === 'git-commit') {
364
+ await this.showGitCommit({ hash: route.hash, path: route.path, historyFilePath: route.historyFilePath });
365
+ return;
366
+ }
367
+
130
368
  if (route.type !== 'git-diff') {
131
369
  return;
132
370
  }
@@ -15,10 +15,13 @@ export const uiFeature = {
15
15
  this.fileExplorer.initialize();
16
16
  this.gitPanel.initialize();
17
17
  this.gitDiffView.initialize();
18
+ this.fileHistoryView.initialize();
18
19
  this.initializePreviewLayoutObserver();
20
+ this.syncIdentityManagementUi();
19
21
  this.syncCurrentUserName();
20
22
  this.syncWrapToggle();
21
23
  this.syncChatNotificationButton();
24
+ this.syncFileHistoryButton({ mode: 'empty' });
22
25
  this.renderChat();
23
26
  void this.gitPanel.refresh({ force: true });
24
27
  this.elements.chatInput?.setAttribute('maxlength', String(this.lobbyChatMessageMaxLength));
@@ -39,6 +42,11 @@ export const uiFeature = {
39
42
  });
40
43
  },
41
44
 
45
+ isIdentityManagedByAuth() {
46
+ return this.runtimeConfig?.auth?.strategy === 'oidc'
47
+ && this.runtimeConfig?.auth?.provider === 'google';
48
+ },
49
+
42
50
  bindEvents() {
43
51
  this.elements.emptyStateNewFileBtn?.addEventListener('click', () => {
44
52
  this.fileExplorer.actionController.handleNewFile();
@@ -65,6 +73,16 @@ export const uiFeature = {
65
73
  void this.copyCurrentLink();
66
74
  });
67
75
 
76
+ this.elements.fileHistoryButton?.addEventListener('click', () => {
77
+ const route = this.navigation.getHashRoute();
78
+ if (route.type === 'git-file-preview') {
79
+ this.handleGitFileHistorySelection(route.currentFilePath ?? route.filePath, { closeSidebarOnMobile: true });
80
+ return;
81
+ }
82
+
83
+ this.handleGitFileHistorySelection(this.currentFilePath, { closeSidebarOnMobile: true });
84
+ });
85
+
68
86
  this.elements.editNameButton?.addEventListener('click', () => {
69
87
  this.openDisplayNameDialog();
70
88
  });
@@ -389,6 +407,11 @@ export const uiFeature = {
389
407
 
390
408
  handleConnectionChange(state) {
391
409
  this.connectionState = state;
410
+ if (state?.firstConnection) {
411
+ this.recordFileOpenMetric?.('ws_connected', {
412
+ status: state.status,
413
+ });
414
+ }
392
415
  this.renderPresence();
393
416
 
394
417
  if (state.unreachable && !this.connectionHelpShown) {
@@ -398,6 +421,10 @@ export const uiFeature = {
398
421
  },
399
422
 
400
423
  openDisplayNameDialog({ mode = 'edit' } = {}) {
424
+ if (this.isIdentityManagedByAuth()) {
425
+ return;
426
+ }
427
+
401
428
  if (!this.isTabActive) {
402
429
  return;
403
430
  }
@@ -439,7 +466,12 @@ export const uiFeature = {
439
466
  },
440
467
 
441
468
  promptForDisplayNameIfNeeded() {
442
- if (!this.isTabActive || this._hasPromptedForDisplayName || this.getStoredUserName()) {
469
+ if (
470
+ !this.isTabActive
471
+ || this._hasPromptedForDisplayName
472
+ || this.getStoredUserName()
473
+ || this.isIdentityManagedByAuth()
474
+ ) {
443
475
  return;
444
476
  }
445
477
 
@@ -450,6 +482,10 @@ export const uiFeature = {
450
482
  },
451
483
 
452
484
  handleDisplayNameSubmit() {
485
+ if (this.isIdentityManagedByAuth()) {
486
+ return;
487
+ }
488
+
453
489
  const input = this.elements.displayNameInput;
454
490
  const dialog = this.elements.displayNameDialog;
455
491
  if (!input || !dialog) return;
@@ -501,10 +537,21 @@ export const uiFeature = {
501
537
  const name = this.getCurrentUserName();
502
538
  el.textContent = name || 'Set name';
503
539
  el.classList.toggle('has-name', Boolean(name));
504
- this.elements.editNameButton?.setAttribute(
505
- 'aria-label',
506
- `${name || 'Set name'}. Change display name`,
507
- );
540
+ if (!this.isIdentityManagedByAuth()) {
541
+ this.elements.editNameButton?.setAttribute(
542
+ 'aria-label',
543
+ `${name || 'Set name'}. Change display name`,
544
+ );
545
+ }
546
+ },
547
+
548
+ syncIdentityManagementUi() {
549
+ const isManaged = this.isIdentityManagedByAuth();
550
+ this.elements.editNameButton?.classList.toggle('hidden', isManaged);
551
+ this.elements.editNameButton?.toggleAttribute('disabled', isManaged);
552
+ if (isManaged && this.elements.displayNameDialog?.open) {
553
+ this.elements.displayNameDialog.close();
554
+ }
508
555
  },
509
556
 
510
557
  toggleLineWrapping() {
@@ -550,8 +597,14 @@ export const uiFeature = {
550
597
  }
551
598
  },
552
599
 
553
- handleTabBlocked({ reason } = {}) {
600
+ async handleTabBlocked({ reason } = {}) {
554
601
  const wasActive = this.isTabActive;
602
+ const blockedFilePath = this.currentFilePath;
603
+ const shouldPrepareExcalidrawDisconnect = Boolean(
604
+ blockedFilePath
605
+ && this.isExcalidrawFile?.(blockedFilePath),
606
+ );
607
+
555
608
  this.isTabActive = false;
556
609
  if (this.elements.displayNameDialog?.open) {
557
610
  this.elements.displayNameDialog.close();
@@ -566,9 +619,14 @@ export const uiFeature = {
566
619
  this.followedUserClientId = null;
567
620
  this.followedCursorSignature = '';
568
621
  this.connectionState = { status: 'disconnected', unreachable: false };
622
+ this.showTabLockOverlay({ reason });
623
+
624
+ if (shouldPrepareExcalidrawDisconnect) {
625
+ await this.excalidrawEmbed.prepareFileDisconnect(blockedFilePath);
626
+ }
627
+
569
628
  this.showEmptyState();
570
629
  this.renderChat();
571
- this.showTabLockOverlay({ reason });
572
630
 
573
631
  if (wasActive && reason === 'taken-over') {
574
632
  this.toastController.show('Another tab took over this session');
@@ -1,4 +1,5 @@
1
1
  import {
2
+ getVaultFileKind,
2
3
  isExcalidrawFilePath,
3
4
  isImageAttachmentFilePath,
4
5
  isMermaidFilePath,
@@ -28,9 +29,50 @@ export const workspaceFeature = {
28
29
  },
29
30
 
30
31
  getPreviewSource() {
32
+ const previewDocument = this.getStaticPreviewDocument?.();
33
+ const previewFilePath = previewDocument?.currentFilePath ?? previewDocument?.filePath ?? null;
34
+ if (previewDocument && previewFilePath && previewFilePath === this.currentFilePath) {
35
+ if (this.isMermaidFile(this.currentFilePath)) {
36
+ return this.createDiagramPreviewDocument('mermaid', previewDocument.content);
37
+ }
38
+
39
+ if (this.isPlantUmlFile(this.currentFilePath)) {
40
+ return this.createDiagramPreviewDocument('plantuml', previewDocument.content);
41
+ }
42
+
43
+ return String(previewDocument.content ?? '');
44
+ }
45
+
31
46
  return this.workspacePreviewController.getPreviewSource(this.currentFilePath);
32
47
  },
33
48
 
49
+ getStaticPreviewDocument() {
50
+ return this._staticPreviewDocument ?? null;
51
+ },
52
+
53
+ setStaticPreviewDocument(document) {
54
+ const normalizedFilePath = document?.filePath ?? document?.path ?? null;
55
+ const normalizedCurrentFilePath = document?.currentFilePath ?? normalizedFilePath;
56
+ this._staticPreviewDocument = document
57
+ ? {
58
+ content: String(document.content ?? ''),
59
+ currentFilePath: normalizedCurrentFilePath,
60
+ fileKind: document.fileKind ?? getVaultFileKind(normalizedFilePath),
61
+ filePath: normalizedFilePath,
62
+ hash: document.hash ?? null,
63
+ }
64
+ : null;
65
+ },
66
+
67
+ clearStaticPreviewDocument() {
68
+ this._staticPreviewDocument = null;
69
+ },
70
+
71
+ supportsFileHistory(filePath) {
72
+ const kind = getVaultFileKind(filePath);
73
+ return kind !== null && kind !== 'image';
74
+ },
75
+
34
76
  getDisplayName(filePath) {
35
77
  return stripVaultFileExtension(String(filePath ?? '')
36
78
  .split('/')
@@ -53,6 +95,10 @@ export const workspaceFeature = {
53
95
  this.workspacePreviewController.renderImageFilePreview(filePath);
54
96
  },
55
97
 
98
+ renderTextFilePreview(payload) {
99
+ this.workspacePreviewController.renderTextFilePreview(payload);
100
+ },
101
+
56
102
  createResizeHandler() {
57
103
  return this.workspacePreviewController.createResizeHandler(() => this.restoreSidebarState());
58
104
  },
@@ -38,6 +38,8 @@ export function bindAppShellElements(doc = document) {
38
38
  editorPage: doc.getElementById('editor-page'),
39
39
  diffPage: doc.getElementById('diff-page'),
40
40
  fileSearch: doc.getElementById('fileSearch'),
41
+ fileHistoryButton: doc.getElementById('fileHistoryBtn'),
42
+ fileHistoryButtonLabel: doc.getElementById('fileHistoryBtnLabel'),
41
43
  filesSidebarTab: doc.getElementById('filesSidebarTab'),
42
44
  gitSearch: doc.getElementById('gitSearch'),
43
45
  gitSearchInput: doc.getElementById('gitSearchInput'),
@@ -561,7 +561,7 @@ export class MermaidPreviewHydrator extends DiagramPreviewHydrator {
561
561
  createZoomButton(label, ariaLabel) {
562
562
  const button = document.createElement('button');
563
563
  button.type = 'button';
564
- button.className = 'mermaid-zoom-btn diagram-preview-action-btn';
564
+ button.className = 'mermaid-zoom-btn ui-preview-action';
565
565
  button.setAttribute('aria-label', ariaLabel);
566
566
  button.title = ariaLabel;
567
567
  button.textContent = label;
@@ -554,7 +554,7 @@ export class PlantUmlPreviewHydrator extends DiagramPreviewHydrator {
554
554
  createToolButton(label, ariaLabel) {
555
555
  const button = document.createElement('button');
556
556
  button.type = 'button';
557
- button.className = 'plantuml-tool-btn diagram-preview-action-btn';
557
+ button.className = 'plantuml-tool-btn ui-preview-action';
558
558
  button.setAttribute('aria-label', ariaLabel);
559
559
  button.title = ariaLabel;
560
560
  button.textContent = label;