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,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 '
|
|
49
|
+
return 'ui-status-badge--success';
|
|
49
50
|
case 'deleted':
|
|
50
|
-
return '
|
|
51
|
+
return 'ui-status-badge--danger';
|
|
51
52
|
case 'renamed':
|
|
52
|
-
return '
|
|
53
|
+
return 'ui-status-badge--accent';
|
|
53
54
|
case 'untracked':
|
|
54
|
-
return '
|
|
55
|
+
return 'ui-status-badge--muted';
|
|
55
56
|
default:
|
|
56
|
-
return '
|
|
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
|
|
78
|
-
<span
|
|
78
|
+
<span class="git-sync-metric git-sync-metric--ahead">↑${Number(branch.ahead || 0)}</span>
|
|
79
|
+
<span class="git-sync-metric git-sync-metric--behind">↓${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
|
-
|
|
224
|
-
this.refreshTimer = null;
|
|
275
|
+
this.stopStatusPolling();
|
|
225
276
|
return;
|
|
226
277
|
}
|
|
227
278
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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="
|
|
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.
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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
|
-
${
|
|
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
|
}
|