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
@@ -1,6 +1,7 @@
1
1
  import { escapeHtml } from '../domain/vault-utils.js';
2
2
  import { resolveApiUrl } from '../domain/runtime-paths.js';
3
3
 
4
+ const HISTORY_PAGE_SIZE = 30;
4
5
  const REFRESH_INTERVAL_MS = 10_000;
5
6
 
6
7
  function getPathDir(pathValue) {
@@ -45,15 +46,15 @@ function actionIconSvg(action) {
45
46
  function badgeClass(status) {
46
47
  switch (status) {
47
48
  case 'added':
48
- return 'added';
49
+ return 'ui-status-badge--success';
49
50
  case 'deleted':
50
- return 'deleted';
51
+ return 'ui-status-badge--danger';
51
52
  case 'renamed':
52
- return 'renamed';
53
+ return 'ui-status-badge--accent';
53
54
  case 'untracked':
54
- return 'untracked';
55
+ return 'ui-status-badge--muted';
55
56
  default:
56
- return 'modified';
57
+ return 'ui-status-badge--warning';
57
58
  }
58
59
  }
59
60
 
@@ -74,8 +75,8 @@ function renderBranchMetrics(summary = {}, branch = {}) {
74
75
  if (branch.upstream || Number(branch.ahead || 0) > 0 || Number(branch.behind || 0) > 0) {
75
76
  return `
76
77
  <span class="git-sync-info" aria-label="Remote sync status">
77
- <span style="color: var(--color-success);">&#8593;${Number(branch.ahead || 0)}</span>
78
- <span style="color: var(--color-text-faint);">&#8595;${Number(branch.behind || 0)}</span>
78
+ <span class="git-sync-metric git-sync-metric--ahead">&#8593;${Number(branch.ahead || 0)}</span>
79
+ <span class="git-sync-metric git-sync-metric--behind">&#8595;${Number(branch.behind || 0)}</span>
79
80
  </span>
80
81
  `;
81
82
  }
@@ -83,6 +84,11 @@ function renderBranchMetrics(summary = {}, branch = {}) {
83
84
  return '<span class="git-sync-info git-sync-info-muted">Clean</span>';
84
85
  }
85
86
 
87
+ function renderHistoryRowTitle(commit = {}) {
88
+ const subject = String(commit.subject || '').trim() || commit.shortHash || 'Commit';
89
+ return escapeHtml(subject);
90
+ }
91
+
86
92
  export class GitPanelController {
87
93
  constructor({
88
94
  enabled = true,
@@ -92,6 +98,7 @@ export class GitPanelController {
92
98
  onPushBranch = () => {},
93
99
  onRepoChange = () => {},
94
100
  onResetFile = () => {},
101
+ onSelectCommit = () => {},
95
102
  onSelectDiff = () => {},
96
103
  onStageFile = () => {},
97
104
  onUnstageFile = () => {},
@@ -106,6 +113,7 @@ export class GitPanelController {
106
113
  this.onPushBranch = onPushBranch;
107
114
  this.onRepoChange = onRepoChange;
108
115
  this.onResetFile = onResetFile;
116
+ this.onSelectCommit = onSelectCommit;
109
117
  this.onSelectDiff = onSelectDiff;
110
118
  this.onStageFile = onStageFile;
111
119
  this.onUnstageFile = onUnstageFile;
@@ -118,16 +126,39 @@ export class GitPanelController {
118
126
  this.active = false;
119
127
  this.refreshTimer = null;
120
128
  this.searchQuery = '';
129
+ this.panelMode = 'changes';
121
130
  this.collapsedSections = new Set();
122
131
  this.selection = {
132
+ commitHash: null,
123
133
  path: null,
124
134
  scope: null,
135
+ source: 'workspace',
125
136
  };
126
137
  this.pendingActionKey = null;
138
+ this.history = {
139
+ commits: [],
140
+ error: '',
141
+ hasMore: false,
142
+ loaded: false,
143
+ loading: false,
144
+ loadingMore: false,
145
+ offset: 0,
146
+ };
127
147
  }
128
148
 
129
149
  initialize() {
130
150
  this.panel?.addEventListener('click', (event) => {
151
+ const modeButton = event.target instanceof Element
152
+ ? event.target.closest('[data-git-panel-mode]')
153
+ : null;
154
+ if (modeButton) {
155
+ const nextMode = modeButton.getAttribute('data-git-panel-mode');
156
+ if (nextMode === 'changes' || nextMode === 'history') {
157
+ void this.setMode(nextMode);
158
+ }
159
+ return;
160
+ }
161
+
131
162
  const toggleButton = event.target instanceof Element
132
163
  ? event.target.closest('[data-git-section-toggle]')
133
164
  : null;
@@ -137,6 +168,26 @@ export class GitPanelController {
137
168
  return;
138
169
  }
139
170
 
171
+ const historyButton = event.target instanceof Element
172
+ ? event.target.closest('[data-git-commit-hash]')
173
+ : null;
174
+ if (historyButton && !event.target.closest('[data-git-history-load-more]')) {
175
+ const hash = historyButton.getAttribute('data-git-commit-hash');
176
+ if (!hash) {
177
+ return;
178
+ }
179
+ this.onSelectCommit(hash, { path: this.selection.commitHash === hash ? this.selection.path : null });
180
+ return;
181
+ }
182
+
183
+ const loadMoreButton = event.target instanceof Element
184
+ ? event.target.closest('[data-git-history-load-more]')
185
+ : null;
186
+ if (loadMoreButton) {
187
+ void this.loadMoreHistory();
188
+ return;
189
+ }
190
+
140
191
  const fileButton = event.target instanceof Element
141
192
  ? event.target.closest('[data-git-path]')
142
193
  : null;
@@ -210,31 +261,71 @@ export class GitPanelController {
210
261
  this.searchQuery = String(event.target?.value ?? '').trim().toLowerCase();
211
262
  this.render();
212
263
  });
264
+ this.syncSearchUi();
213
265
  }
214
266
 
215
267
  setActive(active) {
216
268
  const nextActive = Boolean(active);
217
- if (this.active === nextActive && (!nextActive || this.refreshTimer)) {
269
+ if (this.active === nextActive && (!nextActive || this.refreshTimer || this.panelMode === 'history')) {
218
270
  return;
219
271
  }
220
272
 
221
273
  this.active = nextActive;
222
274
  if (!this.active) {
223
- clearInterval(this.refreshTimer);
224
- this.refreshTimer = null;
275
+ this.stopStatusPolling();
225
276
  return;
226
277
  }
227
278
 
228
- void this.refresh();
229
- if (!this.refreshTimer) {
230
- this.refreshTimer = setInterval(() => {
231
- void this.refresh();
232
- }, REFRESH_INTERVAL_MS);
279
+ this.syncSearchUi();
280
+ void this.refresh({ force: false, includeHistory: this.panelMode === 'history' });
281
+ if (this.panelMode === 'changes') {
282
+ this.ensureStatusPolling();
283
+ } else {
284
+ this.stopStatusPolling();
233
285
  }
234
286
  }
235
287
 
236
- setSelection({ path = null, scope = null } = {}) {
237
- this.selection = { path, scope };
288
+ async setMode(mode, { forceHistoryRefresh = false } = {}) {
289
+ const normalizedMode = mode === 'history' ? 'history' : 'changes';
290
+ const modeChanged = this.panelMode !== normalizedMode;
291
+ this.panelMode = normalizedMode;
292
+ this.syncSearchUi();
293
+ this.render();
294
+
295
+ if (!this.active) {
296
+ return;
297
+ }
298
+
299
+ if (this.panelMode === 'changes') {
300
+ this.ensureStatusPolling();
301
+ if (modeChanged) {
302
+ await this.refresh({ force: false, includeHistory: false });
303
+ }
304
+ return;
305
+ }
306
+
307
+ this.stopStatusPolling();
308
+ if (forceHistoryRefresh || modeChanged || !this.history.loaded) {
309
+ await this.refreshHistory({ force: forceHistoryRefresh || modeChanged });
310
+ }
311
+ }
312
+
313
+ setSelection({
314
+ commitHash = null,
315
+ path = null,
316
+ scope = null,
317
+ source = 'workspace',
318
+ } = {}) {
319
+ this.selection = {
320
+ commitHash,
321
+ path,
322
+ scope,
323
+ source,
324
+ };
325
+ if (source === 'commit' || commitHash) {
326
+ this.panelMode = 'history';
327
+ this.syncSearchUi();
328
+ }
238
329
  this.render();
239
330
  }
240
331
 
@@ -251,7 +342,43 @@ export class GitPanelController {
251
342
  this.render();
252
343
  }
253
344
 
254
- async refresh({ force = false } = {}) {
345
+ ensureStatusPolling() {
346
+ if (this.refreshTimer || !this.active || this.panelMode !== 'changes') {
347
+ return;
348
+ }
349
+ this.refreshTimer = setInterval(() => {
350
+ void this.refresh({ force: false, includeHistory: false });
351
+ }, REFRESH_INTERVAL_MS);
352
+ }
353
+
354
+ stopStatusPolling() {
355
+ clearInterval(this.refreshTimer);
356
+ this.refreshTimer = null;
357
+ }
358
+
359
+ syncSearchUi() {
360
+ if (!this.searchInput) {
361
+ return;
362
+ }
363
+ this.searchInput.setAttribute(
364
+ 'placeholder',
365
+ this.panelMode === 'history' ? 'Filter loaded commits...' : 'Search changes...',
366
+ );
367
+ this.searchInput.setAttribute(
368
+ 'aria-label',
369
+ this.panelMode === 'history' ? 'Filter loaded commits' : 'Search changed files',
370
+ );
371
+ }
372
+
373
+ async refresh({ force = false, includeHistory = this.panelMode === 'history' } = {}) {
374
+ const status = await this.refreshStatus({ force });
375
+ if (includeHistory && status.isGitRepo) {
376
+ await this.refreshHistory({ force });
377
+ }
378
+ return status;
379
+ }
380
+
381
+ async refreshStatus({ force = false } = {}) {
255
382
  if (!this.enabled) {
256
383
  this.status = {
257
384
  isGitRepo: false,
@@ -259,6 +386,16 @@ export class GitPanelController {
259
386
  summary: { changedFiles: 0 },
260
387
  };
261
388
  this.pullBackups = [];
389
+ this.history = {
390
+ ...this.history,
391
+ commits: [],
392
+ error: '',
393
+ hasMore: false,
394
+ loaded: false,
395
+ loading: false,
396
+ loadingMore: false,
397
+ offset: 0,
398
+ };
262
399
  this.onRepoChange(false, this.status);
263
400
  this.render();
264
401
  return this.status;
@@ -284,6 +421,17 @@ export class GitPanelController {
284
421
  } catch (backupError) {
285
422
  console.error('[git-panel] Failed to load pull backups:', backupError);
286
423
  }
424
+ } else {
425
+ this.history = {
426
+ ...this.history,
427
+ commits: [],
428
+ error: '',
429
+ hasMore: false,
430
+ loaded: false,
431
+ loading: false,
432
+ loadingMore: false,
433
+ offset: 0,
434
+ };
287
435
  }
288
436
  this.onRepoChange(Boolean(data.isGitRepo), data);
289
437
  this.render();
@@ -297,12 +445,144 @@ export class GitPanelController {
297
445
  summary: { changedFiles: 0 },
298
446
  };
299
447
  this.pullBackups = [];
448
+ this.history = {
449
+ ...this.history,
450
+ commits: [],
451
+ error: '',
452
+ hasMore: false,
453
+ loaded: false,
454
+ loading: false,
455
+ loadingMore: false,
456
+ offset: 0,
457
+ };
300
458
  this.onRepoChange(false, this.status);
301
459
  this.render();
302
460
  return this.status;
303
461
  }
304
462
  }
305
463
 
464
+ async refreshHistory({ force = false } = {}) {
465
+ if (!this.status?.isGitRepo) {
466
+ this.history = {
467
+ ...this.history,
468
+ commits: [],
469
+ error: '',
470
+ hasMore: false,
471
+ loaded: false,
472
+ loading: false,
473
+ loadingMore: false,
474
+ offset: 0,
475
+ };
476
+ this.render();
477
+ return this.history;
478
+ }
479
+
480
+ if (this.history.loading && !force) {
481
+ return this.history;
482
+ }
483
+
484
+ this.history = {
485
+ ...this.history,
486
+ error: '',
487
+ loading: true,
488
+ loadingMore: false,
489
+ };
490
+ if (force) {
491
+ this.history = {
492
+ ...this.history,
493
+ commits: [],
494
+ hasMore: false,
495
+ loaded: false,
496
+ offset: 0,
497
+ };
498
+ }
499
+ this.render();
500
+
501
+ try {
502
+ const query = new URLSearchParams();
503
+ query.set('limit', String(HISTORY_PAGE_SIZE));
504
+ query.set('offset', '0');
505
+ const response = await fetch(resolveApiUrl(`/git/history?${query.toString()}`));
506
+ const data = await response.json();
507
+ if (!response.ok) {
508
+ throw new Error(data.error || 'Failed to load git history');
509
+ }
510
+
511
+ this.history = {
512
+ commits: Array.isArray(data.commits) ? data.commits : [],
513
+ error: '',
514
+ hasMore: Boolean(data.hasMore),
515
+ loaded: true,
516
+ loading: false,
517
+ loadingMore: false,
518
+ offset: Array.isArray(data.commits) ? data.commits.length : 0,
519
+ };
520
+ this.render();
521
+ return this.history;
522
+ } catch (error) {
523
+ console.error('[git-panel] Failed to load git history:', error);
524
+ this.toastController?.show('Failed to load git history');
525
+ this.history = {
526
+ ...this.history,
527
+ error: 'Failed to load git history',
528
+ loading: false,
529
+ loadingMore: false,
530
+ };
531
+ this.render();
532
+ return this.history;
533
+ }
534
+ }
535
+
536
+ async loadMoreHistory() {
537
+ if (
538
+ !this.status?.isGitRepo
539
+ || this.history.loading
540
+ || this.history.loadingMore
541
+ || !this.history.hasMore
542
+ ) {
543
+ return;
544
+ }
545
+
546
+ this.history = {
547
+ ...this.history,
548
+ error: '',
549
+ loadingMore: true,
550
+ };
551
+ this.render();
552
+
553
+ try {
554
+ const query = new URLSearchParams();
555
+ query.set('limit', String(HISTORY_PAGE_SIZE));
556
+ query.set('offset', String(this.history.offset));
557
+ const response = await fetch(resolveApiUrl(`/git/history?${query.toString()}`));
558
+ const data = await response.json();
559
+ if (!response.ok) {
560
+ throw new Error(data.error || 'Failed to load git history');
561
+ }
562
+
563
+ const nextCommits = Array.isArray(data.commits) ? data.commits : [];
564
+ this.history = {
565
+ commits: [...this.history.commits, ...nextCommits],
566
+ error: '',
567
+ hasMore: Boolean(data.hasMore),
568
+ loaded: true,
569
+ loading: false,
570
+ loadingMore: false,
571
+ offset: this.history.offset + nextCommits.length,
572
+ };
573
+ this.render();
574
+ } catch (error) {
575
+ console.error('[git-panel] Failed to load more git history:', error);
576
+ this.toastController?.show('Failed to load git history');
577
+ this.history = {
578
+ ...this.history,
579
+ error: 'Failed to load git history',
580
+ loadingMore: false,
581
+ };
582
+ this.render();
583
+ }
584
+ }
585
+
306
586
  filterFiles(files = []) {
307
587
  if (!this.searchQuery) {
308
588
  return files;
@@ -314,6 +594,18 @@ export class GitPanelController {
314
594
  ));
315
595
  }
316
596
 
597
+ filterCommits(commits = []) {
598
+ if (!this.searchQuery) {
599
+ return commits;
600
+ }
601
+
602
+ return commits.filter((commit) => (
603
+ String(commit.subject || '').toLowerCase().includes(this.searchQuery)
604
+ || String(commit.shortHash || '').toLowerCase().includes(this.searchQuery)
605
+ || String(commit.authorName || '').toLowerCase().includes(this.searchQuery)
606
+ ));
607
+ }
608
+
317
609
  async handleFileAction(action, filePath, scope) {
318
610
  const actionKey = `${action}:${scope}:${filePath}`;
319
611
  if (this.pendingActionKey === actionKey) {
@@ -388,7 +680,7 @@ export class GitPanelController {
388
680
  <button class="git-section-header" type="button" data-git-section-toggle="${escapeHtml(section.key)}">
389
681
  ${chevronSvg(isCollapsed)}
390
682
  ${escapeHtml(section.label)}
391
- <span class="git-section-count">${files.length}</span>
683
+ <span class="ui-pill-badge ui-pill-badge--count ui-pill-badge--muted git-section-count">${files.length}</span>
392
684
  </button>
393
685
  <div class="git-file-list${isCollapsed ? ' hidden' : ''}">
394
686
  ${files.map((file) => this.renderFile(file)).join('')}
@@ -407,7 +699,7 @@ export class GitPanelController {
407
699
  <button class="git-section-header" type="button" data-git-section-toggle="pull-backups">
408
700
  ${chevronSvg(this.collapsedSections.has('pull-backups'))}
409
701
  Pull Backups
410
- <span class="git-section-count">${this.pullBackups.length}</span>
702
+ <span class="ui-pill-badge ui-pill-badge--count ui-pill-badge--muted git-section-count">${this.pullBackups.length}</span>
411
703
  </button>
412
704
  <div class="git-file-list${this.collapsedSections.has('pull-backups') ? ' hidden' : ''}">
413
705
  ${this.pullBackups.map((backup) => this.renderPullBackup(backup)).join('')}
@@ -421,25 +713,27 @@ export class GitPanelController {
421
713
  const fileCount = Number(backup?.fileCount || 0);
422
714
 
423
715
  return `
424
- <div class="git-file-row">
716
+ <div class="ui-item-row git-file-row">
425
717
  <button
426
- class="git-file-item"
718
+ class="ui-item-main git-file-item"
427
719
  type="button"
428
720
  data-git-pull-backup-path="${escapeHtml(backup.summaryPath || '')}"
429
721
  >
430
722
  ${fileIconSvg()}
431
- <span class="git-file-copy">
432
- <span class="git-file-name">Pull backup ${escapeHtml(backup.id || '')}</span>
433
- <span class="git-file-path">${escapeHtml(`${createdAt} · ${backup.branch || 'HEAD'} · ${fileCount} file${fileCount === 1 ? '' : 's'}`)}</span>
723
+ <span class="ui-item-copy git-file-copy">
724
+ <span class="ui-item-title git-file-name">Pull backup ${escapeHtml(backup.id || '')}</span>
725
+ <span class="ui-item-subtitle git-file-path">${escapeHtml(`${createdAt} · ${backup.branch || 'HEAD'} · ${fileCount} file${fileCount === 1 ? '' : 's'}`)}</span>
434
726
  </span>
435
- <span class="git-status-badge modified">BK</span>
727
+ <span class="ui-status-badge ui-status-badge--warning">BK</span>
436
728
  </button>
437
729
  </div>
438
730
  `;
439
731
  }
440
732
 
441
733
  renderFile(file) {
442
- const isActive = this.selection.path === file.path && this.selection.scope === file.scope;
734
+ const isActive = this.selection.source === 'workspace'
735
+ && this.selection.path === file.path
736
+ && this.selection.scope === file.scope;
443
737
  const dirPath = getPathDir(file.path);
444
738
  const displayName = getPathLeaf(file.path);
445
739
  const statusClass = badgeClass(file.status);
@@ -452,23 +746,23 @@ export class GitPanelController {
452
746
  const isResetPending = this.pendingActionKey === resetActionKey;
453
747
 
454
748
  return `
455
- <div class="git-file-row${isActive ? ' active' : ''}">
749
+ <div class="ui-item-row git-file-row${isActive ? ' active' : ''}">
456
750
  <button
457
- class="git-file-item${isActive ? ' active' : ''}"
751
+ class="ui-item-main git-file-item${isActive ? ' active' : ''}"
458
752
  type="button"
459
753
  data-git-path="${escapeHtml(file.path)}"
460
754
  data-git-scope="${escapeHtml(file.scope)}"
461
755
  >
462
756
  ${fileIconSvg()}
463
- <span class="git-file-copy">
464
- <span class="git-file-name">${escapeHtml(displayName)}</span>
465
- ${dirPath ? `<span class="git-file-path">${escapeHtml(dirPath)}</span>` : ''}
757
+ <span class="ui-item-copy git-file-copy">
758
+ <span class="ui-item-title git-file-name">${escapeHtml(displayName)}</span>
759
+ ${dirPath ? `<span class="ui-item-subtitle git-file-path">${escapeHtml(dirPath)}</span>` : ''}
466
760
  </span>
467
- <span class="git-status-badge ${statusClass}">${escapeHtml(file.code)}</span>
761
+ <span class="ui-status-badge ${statusClass}">${escapeHtml(file.code)}</span>
468
762
  </button>
469
- <div class="git-file-actions">
763
+ <div class="ui-item-actions git-file-actions">
470
764
  <button
471
- class="git-file-action-btn"
765
+ class="ui-icon-button ui-action-icon ui-action-icon--surface"
472
766
  type="button"
473
767
  data-git-file-action="reset"
474
768
  data-git-path="${escapeHtml(file.path)}"
@@ -480,7 +774,7 @@ export class GitPanelController {
480
774
  ${isResetPending ? '...' : actionIconSvg('reset')}
481
775
  </button>
482
776
  <button
483
- class="git-file-action-btn"
777
+ class="ui-icon-button ui-action-icon ui-action-icon--surface"
484
778
  type="button"
485
779
  data-git-file-action="${stageAction.value}"
486
780
  data-git-path="${escapeHtml(file.path)}"
@@ -496,6 +790,130 @@ export class GitPanelController {
496
790
  `;
497
791
  }
498
792
 
793
+ renderPanelModes() {
794
+ return `
795
+ <div class="ui-segmented-control ui-segmented-control--pill git-panel-mode-switch" role="tablist" aria-label="Git panel modes">
796
+ <button
797
+ class="ui-segmented-btn git-panel-mode-btn${this.panelMode === 'changes' ? ' active' : ''}"
798
+ type="button"
799
+ data-git-panel-mode="changes"
800
+ aria-selected="${this.panelMode === 'changes'}"
801
+ >
802
+ Changes
803
+ </button>
804
+ <button
805
+ class="ui-segmented-btn git-panel-mode-btn${this.panelMode === 'history' ? ' active' : ''}"
806
+ type="button"
807
+ data-git-panel-mode="history"
808
+ aria-selected="${this.panelMode === 'history'}"
809
+ >
810
+ History
811
+ </button>
812
+ </div>
813
+ `;
814
+ }
815
+
816
+ renderHistoryRow(commit) {
817
+ const isActive = this.selection.source === 'commit' && this.selection.commitHash === commit.hash;
818
+ const fileCount = Number(commit.filesChanged || 0);
819
+
820
+ return `
821
+ <button
822
+ class="ui-record-surface git-history-row${isActive ? ' active' : ''}"
823
+ type="button"
824
+ data-git-commit-hash="${escapeHtml(commit.hash || '')}"
825
+ title="${escapeHtml(commit.authoredAt || '')}"
826
+ >
827
+ <span class="ui-record-header git-history-row-top">
828
+ <span class="ui-record-title git-history-subject">${renderHistoryRowTitle(commit)}</span>
829
+ <span class="ui-pill-badge ui-pill-badge--code git-history-hash">${escapeHtml(commit.shortHash || '')}</span>
830
+ </span>
831
+ <span class="ui-record-meta git-history-row-meta">
832
+ <span>${escapeHtml(commit.authorName || 'Unknown')}</span>
833
+ <span>${escapeHtml(commit.relativeDateLabel || '')}</span>
834
+ <span>${fileCount} file${fileCount === 1 ? '' : 's'}</span>
835
+ ${commit.isMergeCommit ? '<span>Merge</span>' : ''}
836
+ </span>
837
+ <span class="ui-record-meta git-history-row-stats">
838
+ <span class="git-change-add">+${Number(commit.additions || 0)}</span>
839
+ <span class="git-change-del">-${Number(commit.deletions || 0)}</span>
840
+ </span>
841
+ </button>
842
+ `;
843
+ }
844
+
845
+ renderHistoryPanel() {
846
+ const commits = this.filterCommits(this.history.commits);
847
+
848
+ if (this.history.loading && !this.history.loaded) {
849
+ return '<div class="git-panel-empty">Loading git history...</div>';
850
+ }
851
+
852
+ if (this.history.error && !this.history.loaded) {
853
+ return `<div class="git-panel-empty">${escapeHtml(this.history.error)}</div>`;
854
+ }
855
+
856
+ if (this.history.loaded && commits.length === 0 && this.history.commits.length === 0) {
857
+ return '<div class="git-panel-empty">No commits yet on this branch.</div>';
858
+ }
859
+
860
+ if (this.history.loaded && commits.length === 0) {
861
+ return '<div class="git-panel-empty">No loaded commits match your filter.</div>';
862
+ }
863
+
864
+ return `
865
+ <div class="git-history-list">
866
+ ${commits.map((commit) => this.renderHistoryRow(commit)).join('')}
867
+ </div>
868
+ ${this.history.hasMore ? `
869
+ <div class="git-history-footer">
870
+ <button
871
+ class="ui-button ui-button--compact ui-action-button ui-action-button--surface btn btn-secondary git-history-load-more"
872
+ type="button"
873
+ data-git-history-load-more
874
+ ${this.history.loadingMore ? 'disabled' : ''}
875
+ >
876
+ ${this.history.loadingMore ? 'Loading...' : 'Load More'}
877
+ </button>
878
+ </div>
879
+ ` : ''}
880
+ `;
881
+ }
882
+
883
+ renderChangesPanel() {
884
+ const pullBackupsMarkup = this.renderPullBackupsSection();
885
+ const sectionMarkup = (this.status.sections ?? [])
886
+ .map((section) => this.renderSection(section))
887
+ .filter(Boolean)
888
+ .join('');
889
+ const hasChanges = Boolean(this.status.summary?.changedFiles);
890
+ const hasStagedChanges = Number(this.status.summary?.staged || 0) > 0;
891
+ const isCommitPending = this.pendingActionKey === 'commit-staged';
892
+
893
+ return `
894
+ ${pullBackupsMarkup}
895
+ ${sectionMarkup || '<div class="git-panel-empty">No local changes</div>'}
896
+ ${hasChanges ? `
897
+ <div class="git-panel-footer">
898
+ <div class="git-panel-footer-actions">
899
+ <button class="ui-button ui-button--compact ui-action-button ui-action-button--surface btn btn-secondary" type="button" data-git-view-all>
900
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M2 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
901
+ View Full Diff
902
+ </button>
903
+ <button
904
+ class="ui-button ui-action-button ui-action-button--wide btn btn-primary git-footer-commit-btn"
905
+ type="button"
906
+ data-git-commit-staged
907
+ ${!hasStagedChanges || isCommitPending ? 'disabled' : ''}
908
+ >
909
+ ${isCommitPending ? 'Working...' : 'Commit Staged'}
910
+ </button>
911
+ </div>
912
+ </div>
913
+ ` : ''}
914
+ `;
915
+ }
916
+
499
917
  renderEmpty(message) {
500
918
  if (!this.panel) {
501
919
  return;
@@ -520,14 +938,6 @@ export class GitPanelController {
520
938
  }
521
939
 
522
940
  const branch = this.status.branch ?? {};
523
- const pullBackupsMarkup = this.renderPullBackupsSection();
524
- const sectionMarkup = (this.status.sections ?? [])
525
- .map((section) => this.renderSection(section))
526
- .filter(Boolean)
527
- .join('');
528
- const hasChanges = Boolean(this.status.summary?.changedFiles);
529
- const hasStagedChanges = Number(this.status.summary?.staged || 0) > 0;
530
- const isCommitPending = this.pendingActionKey === 'commit-staged';
531
941
  const hasUpstream = Boolean(branch.upstream);
532
942
  const isPullPending = this.pendingActionKey === 'sync:pull';
533
943
  const isPushPending = this.pendingActionKey === 'sync:push';
@@ -543,7 +953,7 @@ export class GitPanelController {
543
953
  </div>
544
954
  <div class="git-branch-actions" role="group" aria-label="Remote sync actions">
545
955
  <button
546
- class="git-branch-action-btn"
956
+ class="ui-button ui-button--compact ui-pill-button ui-action-pill ui-action-pill--surface btn btn-secondary"
547
957
  type="button"
548
958
  data-git-sync-action="pull"
549
959
  title="${hasUpstream ? 'Pull remote changes (fast-forward only)' : 'No upstream branch configured'}"
@@ -553,7 +963,7 @@ export class GitPanelController {
553
963
  ${isPullPending ? '...' : `${actionIconSvg('pull')}<span>Pull</span>`}
554
964
  </button>
555
965
  <button
556
- class="git-branch-action-btn"
966
+ class="ui-button ui-button--compact ui-pill-button ui-action-pill ui-action-pill--surface btn btn-secondary"
557
967
  type="button"
558
968
  data-git-sync-action="push"
559
969
  title="${hasUpstream ? 'Push local commits' : 'No upstream branch configured'}"
@@ -563,27 +973,9 @@ export class GitPanelController {
563
973
  ${isPushPending ? '...' : `${actionIconSvg('push')}<span>Push</span>`}
564
974
  </button>
565
975
  </div>
976
+ ${this.renderPanelModes()}
566
977
  </div>
567
- ${pullBackupsMarkup}
568
- ${sectionMarkup || '<div class="git-panel-empty">No local changes</div>'}
569
- ${hasChanges ? `
570
- <div class="git-panel-footer">
571
- <div class="git-panel-footer-actions">
572
- <button class="git-view-all-btn" type="button" data-git-view-all>
573
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M2 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
574
- View Full Diff
575
- </button>
576
- <button
577
- class="git-footer-commit-btn"
578
- type="button"
579
- data-git-commit-staged
580
- ${!hasStagedChanges || isCommitPending ? 'disabled' : ''}
581
- >
582
- ${isCommitPending ? 'Working...' : 'Commit Staged'}
583
- </button>
584
- </div>
585
- </div>
586
- ` : ''}
978
+ ${this.panelMode === 'history' ? this.renderHistoryPanel() : this.renderChangesPanel()}
587
979
  `;
588
980
  }
589
981
  }