@hienlh/ppm 0.9.86 → 0.9.87
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/260415-0932-git-graph-stash-rebase-conflicts/reports/code-reviewer-260415-1020-stash-rebase-conflicts.md +288 -0
- package/260415-0932-git-graph-stash-rebase-conflicts/reports/tester-260415-1020-build-check.md +117 -0
- package/260415-1150-ext-silent-failure-debugging/reports/code-reviewer-260415-1159-ext-error-reporting-review.md +205 -0
- package/260415-1150-ext-silent-failure-debugging/reports/docs-manager-260415-1206-ext-error-reporting.md +99 -0
- package/260415-1150-ext-silent-failure-debugging/reports/tester-260415-1159-extension-error-reporting.md +174 -0
- package/CHANGELOG.md +14 -0
- package/dist/web/assets/{chat-tab-BEEd-Km4.js → chat-tab-R4gKsnxD.js} +1 -1
- package/dist/web/assets/{code-editor-Ij4p30cr.js → code-editor-Br0vzTOy.js} +2 -2
- package/dist/web/assets/conflict-editor-BPgCjnNz.js +19 -0
- package/dist/web/assets/{csv-preview-CwQnOa3E.js → csv-preview-BZRICDP0.js} +1 -1
- package/dist/web/assets/{database-viewer-C1UHSgft.js → database-viewer-DaUoQ-oR.js} +1 -1
- package/dist/web/assets/{diff-viewer-CVx5naBA.js → diff-viewer-BzvK3gAE.js} +1 -1
- package/dist/web/assets/extension-webview-CGepEw-b.js +3 -0
- package/dist/web/assets/{index-OqgGFmh8.js → index-CKsEzQ4f.js} +4 -4
- package/dist/web/assets/index-Chf0otez.css +2 -0
- package/dist/web/assets/keybindings-store-D5zgHod8.js +1 -0
- package/dist/web/assets/{markdown-renderer-CRy8xw2B.js → markdown-renderer-DSYnGywb.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-Biua8ov5.js → port-forwarding-tab-vmqDKmk2.js} +1 -1
- package/dist/web/assets/{postgres-viewer-BcVjCAl4.js → postgres-viewer-0lIAosrr.js} +1 -1
- package/dist/web/assets/{settings-tab-C9X-N8hE.js → settings-tab-CMnv1fce.js} +1 -1
- package/dist/web/assets/{sql-query-editor-BFvRvJn0.js → sql-query-editor-Bc2hAwqT.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-CPfvwFl4.js → sqlite-viewer-B60MS2Dy.js} +1 -1
- package/dist/web/assets/{terminal-tab-mWwk_weB.js → terminal-tab-CCJoLstH.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-CPaeSMAA.js → use-monaco-theme-BJK48EmK.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +39 -6
- package/docs/project-changelog.md +86 -25
- package/docs/project-roadmap.md +3 -2
- package/docs/system-architecture.md +44 -1
- package/package.json +1 -1
- package/packages/ext-git-graph/src/extension.ts +126 -5
- package/packages/ext-git-graph/src/types.ts +13 -2
- package/packages/ext-git-graph/src/webview-html.ts +223 -5
- package/src/server/ws/extensions.ts +28 -2
- package/src/services/extension-host-worker.ts +6 -1
- package/src/services/extension.service.ts +17 -3
- package/src/types/extension-messages.ts +1 -1
- package/src/web/components/editor/conflict-editor.tsx +368 -0
- package/src/web/components/extensions/extension-webview.tsx +45 -3
- package/src/web/components/layout/editor-panel.tsx +1 -0
- package/src/web/components/layout/mobile-nav.tsx +1 -0
- package/src/web/components/layout/tab-bar.tsx +1 -0
- package/src/web/components/layout/tab-content.tsx +5 -0
- package/src/web/hooks/use-extension-ws.ts +8 -0
- package/src/web/stores/extension-store.ts +8 -0
- package/src/web/stores/panel-utils.ts +2 -0
- package/src/web/stores/tab-store.ts +2 -1
- package/dist/web/assets/extension-webview-CHVVpV34.js +0 -3
- package/dist/web/assets/index-vA7juDri.css +0 -2
- package/dist/web/assets/keybindings-store-BQxgPV5o.js +0 -1
- /package/dist/web/assets/{lib-CeBVkQ-7.js → lib-DSLzfeW0.js} +0 -0
|
@@ -27,6 +27,16 @@ ${getStyles()}
|
|
|
27
27
|
<button id="btn-fetch" title="Fetch from remotes"></button>
|
|
28
28
|
</div>
|
|
29
29
|
<div class="toolbar-right">
|
|
30
|
+
<div class="stash-dropdown">
|
|
31
|
+
<button id="btn-stash" title="Stashes"></button>
|
|
32
|
+
<div id="stash-popover" class="stash-popover hidden">
|
|
33
|
+
<div class="stash-popover-header"><span>Stashes</span></div>
|
|
34
|
+
<div id="stash-list" class="stash-list"></div>
|
|
35
|
+
<div class="stash-popover-footer">
|
|
36
|
+
<button id="stash-save" class="btn-sm">+ Stash Changes</button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
30
40
|
<div class="worktree-dropdown">
|
|
31
41
|
<button id="btn-worktree" title="Worktrees"></button>
|
|
32
42
|
<div id="worktree-popover" class="worktree-popover hidden">
|
|
@@ -192,6 +202,37 @@ button:active { background: var(--surface); }
|
|
|
192
202
|
.wt-empty { padding: 16px; text-align: center; font-size: 11px; color: var(--subtext); }
|
|
193
203
|
@media (max-width: 768px) { .branch-option { padding: 10px 12px; min-height: 44px; } }
|
|
194
204
|
|
|
205
|
+
/* Stash popover */
|
|
206
|
+
.stash-dropdown { position: relative; }
|
|
207
|
+
#btn-stash { display: flex; align-items: center; gap: 4px; font-size: 12px; padding: 4px 8px; }
|
|
208
|
+
#btn-stash .stash-count { background: var(--accent, #58a6ff); color: #fff; font-size: 10px; border-radius: 8px; padding: 0 5px; min-width: 16px; text-align: center; line-height: 16px; }
|
|
209
|
+
.stash-popover { position: absolute; top: 100%; right: 0; z-index: 60; background: var(--surface); border: 1px solid var(--border2); border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.2); min-width: 300px; max-width: 400px; margin-top: 4px; display: flex; flex-direction: column; }
|
|
210
|
+
.stash-popover-header { padding: 8px 12px; font-size: 12px; font-weight: 600; border-bottom: 1px solid var(--border); }
|
|
211
|
+
.stash-list { overflow-y: auto; max-height: 240px; }
|
|
212
|
+
.stash-item { padding: 8px 12px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; font-size: 12px; }
|
|
213
|
+
.stash-item:last-child { border-bottom: none; }
|
|
214
|
+
.stash-item-info { flex: 1; min-width: 0; }
|
|
215
|
+
.stash-item-ref { font-weight: 600; font-size: 11px; color: var(--subtext); }
|
|
216
|
+
.stash-item-msg { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
217
|
+
.stash-item-actions { display: flex; gap: 4px; flex-shrink: 0; }
|
|
218
|
+
.stash-item-actions button { min-width: 24px; min-height: 24px; padding: 2px 6px; font-size: 10px; }
|
|
219
|
+
.stash-empty { padding: 16px; text-align: center; font-size: 11px; color: var(--subtext); }
|
|
220
|
+
.stash-popover-footer { padding: 8px 12px; border-top: 1px solid var(--border); display: flex; gap: 6px; }
|
|
221
|
+
.stash-popover-footer .btn-sm { flex: 1; }
|
|
222
|
+
|
|
223
|
+
/* Merge/rebase banner */
|
|
224
|
+
.merge-banner { padding: 8px 12px; background: rgba(133, 77, 14, 0.12); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; font-size: 12px; flex-shrink: 0; }
|
|
225
|
+
.merge-banner .banner-icon { color: #eab308; }
|
|
226
|
+
.merge-banner .banner-text { flex: 1; }
|
|
227
|
+
.merge-banner .banner-actions { display: flex; gap: 4px; }
|
|
228
|
+
.merge-banner .banner-actions button { font-size: 11px; padding: 2px 8px; }
|
|
229
|
+
.merge-banner .btn-continue { background: var(--green); color: #fff; border-color: transparent; }
|
|
230
|
+
.merge-banner .btn-abort { background: var(--red); color: #fff; border-color: transparent; }
|
|
231
|
+
|
|
232
|
+
/* Conflict section */
|
|
233
|
+
.conflict-header { padding: 4px 0; font-size: 12px; font-weight: 600; color: var(--red); display: flex; align-items: center; gap: 4px; }
|
|
234
|
+
.file-status-U { color: var(--red); font-weight: bold; }
|
|
235
|
+
|
|
195
236
|
/* Find bar */
|
|
196
237
|
.find-bar { display: flex; align-items: center; gap: 6px; padding: 6px 12px; border-bottom: 1px solid var(--border); background: var(--surface); }
|
|
197
238
|
.find-bar input { flex: 1; background: var(--bg); color: var(--text); border: 1px solid var(--border2); border-radius: 4px; padding: 4px 8px; font-size: 12px; }
|
|
@@ -393,6 +434,7 @@ const state = {
|
|
|
393
434
|
graphColWidth: null,
|
|
394
435
|
fileViewMode: 'list',
|
|
395
436
|
worktrees: [],
|
|
437
|
+
mergeState: null,
|
|
396
438
|
_lastDetail: null,
|
|
397
439
|
};
|
|
398
440
|
|
|
@@ -412,6 +454,7 @@ const ICONS = {
|
|
|
412
454
|
fileOpen: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>',
|
|
413
455
|
gitBranch: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 01-9 9"/></svg>',
|
|
414
456
|
trash: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>',
|
|
457
|
+
archive: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>',
|
|
415
458
|
};
|
|
416
459
|
|
|
417
460
|
// --- Toast notifications ---
|
|
@@ -431,6 +474,7 @@ document.getElementById('btn-fetch').innerHTML = ICONS.download;
|
|
|
431
474
|
document.getElementById('btn-find').innerHTML = ICONS.search;
|
|
432
475
|
document.getElementById('btn-settings').innerHTML = ICONS.settings;
|
|
433
476
|
document.getElementById('btn-worktree').innerHTML = ICONS.gitBranch + ' <span class="wt-count" style="display:none">0</span>';
|
|
477
|
+
document.getElementById('btn-stash').innerHTML = ICONS.archive + ' <span class="stash-count" style="display:none">0</span>';
|
|
434
478
|
vscode.postMessage({ command: 'ready' });
|
|
435
479
|
|
|
436
480
|
// --- Message handler ---
|
|
@@ -479,9 +523,12 @@ window.addEventListener('message', (event) => {
|
|
|
479
523
|
break;
|
|
480
524
|
case 'loadUncommitted':
|
|
481
525
|
state.uncommitted = msg.data;
|
|
526
|
+
state.mergeState = msg.data?.mergeState || null;
|
|
482
527
|
renderCommitList();
|
|
528
|
+
renderMergeBanner();
|
|
483
529
|
if (state.selectedCommit === 'uncommitted') {
|
|
484
|
-
|
|
530
|
+
const u = msg.data;
|
|
531
|
+
if (!u || (u.staged.length === 0 && u.unstaged.length === 0 && (!u.conflicted || u.conflicted.length === 0))) {
|
|
485
532
|
state.selectedCommit = null;
|
|
486
533
|
state.expandedCommit = null;
|
|
487
534
|
document.getElementById('detail-panel').classList.add('hidden');
|
|
@@ -531,11 +578,19 @@ window.addEventListener('message', (event) => {
|
|
|
531
578
|
if (msg.result.ok && (msg.action === 'addWorktree' || msg.action === 'removeWorktree' || msg.action === 'pruneWorktrees')) {
|
|
532
579
|
vscode.postMessage({ command: 'requestWorktrees' });
|
|
533
580
|
}
|
|
581
|
+
// Refresh stash list after stash mutations
|
|
582
|
+
if (msg.result.ok && ['stashSave','stashPop','stashDrop','stashApply'].includes(msg.action)) {
|
|
583
|
+
vscode.postMessage({ command: 'requestStashes' });
|
|
584
|
+
}
|
|
534
585
|
break;
|
|
535
586
|
case 'loadWorktrees':
|
|
536
587
|
state.worktrees = msg.data || [];
|
|
537
588
|
renderWorktreeList();
|
|
538
589
|
break;
|
|
590
|
+
case 'loadStashes':
|
|
591
|
+
state.stashes = msg.data || [];
|
|
592
|
+
renderStashList();
|
|
593
|
+
break;
|
|
539
594
|
case 'error':
|
|
540
595
|
document.getElementById('status-text').textContent = 'Error: ' + msg.message;
|
|
541
596
|
break;
|
|
@@ -552,6 +607,8 @@ document.getElementById('detail-panel').addEventListener('click', (e) => {
|
|
|
552
607
|
const file = actionBtn.dataset.file;
|
|
553
608
|
if (action === 'open') {
|
|
554
609
|
vscode.postMessage({ command: 'openFile', filePath: file });
|
|
610
|
+
} else if (action === 'open-conflict') {
|
|
611
|
+
vscode.postMessage({ command: 'openConflictFile', filePath: file });
|
|
555
612
|
} else if (action === 'stage') {
|
|
556
613
|
vscode.postMessage({ command: 'gitAction', action: 'stage', args: { files: [file] } });
|
|
557
614
|
} else if (action === 'unstage') {
|
|
@@ -829,6 +886,140 @@ function showCreateWorktreeDialog(startPoint) {
|
|
|
829
886
|
});
|
|
830
887
|
}
|
|
831
888
|
|
|
889
|
+
// --- Merge/rebase banner ---
|
|
890
|
+
function renderMergeBanner() {
|
|
891
|
+
let banner = document.getElementById('merge-banner');
|
|
892
|
+
if (!state.mergeState) {
|
|
893
|
+
if (banner) banner.remove();
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const ms = state.mergeState;
|
|
897
|
+
const typeLabel = ms.type === 'cherry-pick' ? 'Cherry-pick' : ms.type.charAt(0).toUpperCase() + ms.type.slice(1);
|
|
898
|
+
const progressText = ms.progress ? ' (' + ms.progress + ')' : '';
|
|
899
|
+
const msgText = ms.message ? ' — ' + escHtml(ms.message) : '';
|
|
900
|
+
|
|
901
|
+
let buttonsHtml = '';
|
|
902
|
+
if (ms.type === 'rebase') {
|
|
903
|
+
buttonsHtml = '<button class="btn-sm btn-continue" data-merge-action="rebaseContinue">Continue</button>'
|
|
904
|
+
+ '<button class="btn-sm" data-merge-action="rebaseSkip">Skip</button>'
|
|
905
|
+
+ '<button class="btn-sm btn-abort" data-merge-action="rebaseAbort">Abort</button>';
|
|
906
|
+
} else if (ms.type === 'merge') {
|
|
907
|
+
buttonsHtml = '<button class="btn-sm btn-abort" data-merge-action="mergeAbort">Abort</button>';
|
|
908
|
+
} else if (ms.type === 'cherry-pick') {
|
|
909
|
+
buttonsHtml = '<button class="btn-sm btn-continue" data-merge-action="cherryPickContinue">Continue</button>'
|
|
910
|
+
+ '<button class="btn-sm btn-abort" data-merge-action="cherryPickAbort">Abort</button>';
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (!banner) {
|
|
914
|
+
banner = document.createElement('div');
|
|
915
|
+
banner.id = 'merge-banner';
|
|
916
|
+
banner.className = 'merge-banner';
|
|
917
|
+
const toolbar = document.getElementById('toolbar');
|
|
918
|
+
toolbar.parentNode.insertBefore(banner, toolbar.nextSibling);
|
|
919
|
+
}
|
|
920
|
+
banner.innerHTML = '<span class="banner-icon">⚠</span>'
|
|
921
|
+
+ '<span class="banner-text"><strong>' + typeLabel + ' in progress' + progressText + '</strong>' + msgText + '</span>'
|
|
922
|
+
+ '<div class="banner-actions">' + buttonsHtml + '</div>';
|
|
923
|
+
|
|
924
|
+
banner.querySelectorAll('[data-merge-action]').forEach(btn => {
|
|
925
|
+
btn.addEventListener('click', () => {
|
|
926
|
+
const action = btn.dataset.mergeAction;
|
|
927
|
+
if (action.includes('Abort')) {
|
|
928
|
+
showDialog({
|
|
929
|
+
title: 'Abort ' + typeLabel,
|
|
930
|
+
message: 'Abort the current ' + ms.type + '? Any resolved conflicts will be lost.',
|
|
931
|
+
destructive: true,
|
|
932
|
+
confirmLabel: 'Abort',
|
|
933
|
+
onConfirm: () => gitAction(action, {}),
|
|
934
|
+
});
|
|
935
|
+
} else {
|
|
936
|
+
gitAction(action, {});
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// --- Stash popover ---
|
|
943
|
+
const btnStash = document.getElementById('btn-stash');
|
|
944
|
+
const stashPopover = document.getElementById('stash-popover');
|
|
945
|
+
|
|
946
|
+
btnStash.addEventListener('click', (e) => {
|
|
947
|
+
e.stopPropagation();
|
|
948
|
+
const wasHidden = stashPopover.classList.contains('hidden');
|
|
949
|
+
stashPopover.classList.toggle('hidden');
|
|
950
|
+
if (wasHidden) vscode.postMessage({ command: 'requestStashes' });
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
document.addEventListener('click', (e) => {
|
|
954
|
+
if (!e.target.closest('.stash-dropdown')) stashPopover.classList.add('hidden');
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
function renderStashList() {
|
|
958
|
+
const listEl = document.getElementById('stash-list');
|
|
959
|
+
const countEl = btnStash.querySelector('.stash-count');
|
|
960
|
+
const stashes = state.stashes;
|
|
961
|
+
if (countEl) {
|
|
962
|
+
countEl.textContent = stashes.length;
|
|
963
|
+
countEl.style.display = stashes.length > 0 ? '' : 'none';
|
|
964
|
+
}
|
|
965
|
+
if (!stashes.length) {
|
|
966
|
+
listEl.innerHTML = '<div class="stash-empty">No stashes</div>';
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
listEl.innerHTML = stashes.map((s, i) => {
|
|
970
|
+
const ref = 'stash@{' + s.index + '}';
|
|
971
|
+
return '<div class="stash-item">'
|
|
972
|
+
+ '<div class="stash-item-info">'
|
|
973
|
+
+ '<div class="stash-item-ref">' + escHtml(ref) + '</div>'
|
|
974
|
+
+ '<div class="stash-item-msg" title="' + escHtml(s.message) + '">' + escHtml(s.message) + '</div>'
|
|
975
|
+
+ '</div>'
|
|
976
|
+
+ '<div class="stash-item-actions">'
|
|
977
|
+
+ '<button class="stash-apply btn-sm" data-idx="' + i + '" title="Apply (keep stash)">Apply</button>'
|
|
978
|
+
+ '<button class="stash-pop btn-sm" data-idx="' + i + '" title="Pop (apply & remove)">Pop</button>'
|
|
979
|
+
+ '<button class="stash-drop btn-sm" data-idx="' + i + '" title="Drop (delete)">Drop</button>'
|
|
980
|
+
+ '</div>'
|
|
981
|
+
+ '</div>';
|
|
982
|
+
}).join('');
|
|
983
|
+
|
|
984
|
+
listEl.querySelectorAll('.stash-apply').forEach(btn => {
|
|
985
|
+
btn.addEventListener('click', (e) => {
|
|
986
|
+
e.stopPropagation();
|
|
987
|
+
const s = state.stashes[parseInt(btn.dataset.idx)];
|
|
988
|
+
if (s) gitAction('stashApply', { stashRef: 'stash@{' + s.index + '}' });
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
listEl.querySelectorAll('.stash-pop').forEach(btn => {
|
|
992
|
+
btn.addEventListener('click', (e) => {
|
|
993
|
+
e.stopPropagation();
|
|
994
|
+
const s = state.stashes[parseInt(btn.dataset.idx)];
|
|
995
|
+
if (s) gitAction('stashPop', { stashRef: 'stash@{' + s.index + '}' });
|
|
996
|
+
});
|
|
997
|
+
});
|
|
998
|
+
listEl.querySelectorAll('.stash-drop').forEach(btn => {
|
|
999
|
+
btn.addEventListener('click', (e) => {
|
|
1000
|
+
e.stopPropagation();
|
|
1001
|
+
const s = state.stashes[parseInt(btn.dataset.idx)];
|
|
1002
|
+
if (!s) return;
|
|
1003
|
+
showDialog({
|
|
1004
|
+
title: 'Drop Stash',
|
|
1005
|
+
message: 'Delete stash@{' + s.index + '}? This cannot be undone.',
|
|
1006
|
+
destructive: true,
|
|
1007
|
+
confirmLabel: 'Drop',
|
|
1008
|
+
onConfirm: () => gitAction('stashDrop', { stashRef: 'stash@{' + s.index + '}' }),
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
document.getElementById('stash-save').addEventListener('click', () => {
|
|
1015
|
+
showDialog({
|
|
1016
|
+
title: 'Stash Changes',
|
|
1017
|
+
input: { placeholder: 'Stash message (optional)' },
|
|
1018
|
+
confirmLabel: 'Stash',
|
|
1019
|
+
onConfirm: (msg) => gitAction('stashSave', msg ? { message: msg } : {}),
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
|
|
832
1023
|
// --- Graph column resize ---
|
|
833
1024
|
{
|
|
834
1025
|
const resizeHandle = document.getElementById('graph-resize-handle');
|
|
@@ -1189,7 +1380,8 @@ function graphVertexOut(e) {
|
|
|
1189
1380
|
// --- Commit list ---
|
|
1190
1381
|
function getDisplayCommits() {
|
|
1191
1382
|
const u = state.uncommitted;
|
|
1192
|
-
|
|
1383
|
+
const totalFiles = u ? (u.staged.length + u.unstaged.length + (u.conflicted ? u.conflicted.length : 0)) : 0;
|
|
1384
|
+
if (!u || totalFiles === 0) return state.commits;
|
|
1193
1385
|
const virtualCommit = {
|
|
1194
1386
|
hash: 'uncommitted',
|
|
1195
1387
|
parents: state.head ? [state.head] : [],
|
|
@@ -1200,7 +1392,7 @@ function getDisplayCommits() {
|
|
|
1200
1392
|
committerEmail: '',
|
|
1201
1393
|
commitDate: Math.floor(Date.now() / 1000),
|
|
1202
1394
|
refs: [],
|
|
1203
|
-
message: 'Uncommitted Changes (' +
|
|
1395
|
+
message: 'Uncommitted Changes (' + totalFiles + ' files)',
|
|
1204
1396
|
};
|
|
1205
1397
|
return [virtualCommit, ...state.commits];
|
|
1206
1398
|
}
|
|
@@ -1412,9 +1604,25 @@ function renderUncommittedDetail() {
|
|
|
1412
1604
|
const u = state.uncommitted;
|
|
1413
1605
|
if (!u) { panel.classList.add('hidden'); return; }
|
|
1414
1606
|
let html = '<h3>Uncommitted Changes</h3>';
|
|
1415
|
-
|
|
1607
|
+
const hasFiles = u.staged.length > 0 || u.unstaged.length > 0 || (u.conflicted && u.conflicted.length > 0);
|
|
1608
|
+
if (hasFiles) {
|
|
1416
1609
|
html += fileViewToggleHtml();
|
|
1417
1610
|
}
|
|
1611
|
+
// Conflict section (above staged/unstaged)
|
|
1612
|
+
if (u.conflicted && u.conflicted.length > 0) {
|
|
1613
|
+
html += '<div class="file-list"><div class="conflict-header">⚠ Conflicts (' + u.conflicted.length + ')</div>';
|
|
1614
|
+
html += u.conflicted.map(f => {
|
|
1615
|
+
const fileName = f.path.split('/').pop() || f.path;
|
|
1616
|
+
return '<div class="file-item">'
|
|
1617
|
+
+ '<span class="file-status file-status-U">U</span>'
|
|
1618
|
+
+ '<span class="file-clickable" data-file="' + escHtml(f.path) + '" data-hash="uncommitted" data-parent="' + escHtml(state.head) + '">' + escHtml(f.path) + '</span>'
|
|
1619
|
+
+ '<div class="file-actions">'
|
|
1620
|
+
+ '<button class="file-action-btn" data-action="open-conflict" data-file="' + escHtml(f.path) + '" title="Open conflict file">' + ICONS.fileOpen + '</button>'
|
|
1621
|
+
+ '<button class="file-action-btn" data-action="stage" data-file="' + escHtml(f.path) + '" title="Mark resolved (stage)">' + ICONS.plus + '</button>'
|
|
1622
|
+
+ '</div></div>';
|
|
1623
|
+
}).join('');
|
|
1624
|
+
html += '</div>';
|
|
1625
|
+
}
|
|
1418
1626
|
if (u.staged.length > 0) {
|
|
1419
1627
|
html += '<div class="file-list"><div class="section-actions"><strong>Staged (' + u.staged.length + '):</strong>';
|
|
1420
1628
|
html += '<button class="btn-sm section-action-btn" data-action="unstage-all">Unstage All</button></div>';
|
|
@@ -1427,7 +1635,7 @@ function renderUncommittedDetail() {
|
|
|
1427
1635
|
html += renderFileListHtml(u.unstaged, 'uncommitted', state.head, 'unstaged');
|
|
1428
1636
|
html += '</div>';
|
|
1429
1637
|
}
|
|
1430
|
-
if (u.staged.length === 0 && u.unstaged.length === 0) {
|
|
1638
|
+
if (u.staged.length === 0 && u.unstaged.length === 0 && (!u.conflicted || u.conflicted.length === 0)) {
|
|
1431
1639
|
html += '<p>No uncommitted changes.</p>';
|
|
1432
1640
|
}
|
|
1433
1641
|
html += '<div class="commit-section">';
|
|
@@ -1496,6 +1704,16 @@ function showCommitContextMenu(x, y, commit) {
|
|
|
1496
1704
|
{ label: 'Create Branch Here...', action: () => promptAndAction('Branch name:', (name) => gitAction('createBranch', { name, startPoint: commit.hash })) },
|
|
1497
1705
|
{ label: 'Create Tag Here...', action: () => promptAndAction('Tag name:', (name) => gitAction('createTag', { name, hash: commit.hash })) },
|
|
1498
1706
|
{ label: 'Create Worktree Here...', action: () => showCreateWorktreeDialog(commit.hash) },
|
|
1707
|
+
{ separator: true },
|
|
1708
|
+
{ label: 'Rebase current branch onto this...', action: () => {
|
|
1709
|
+
showDialog({
|
|
1710
|
+
title: 'Rebase',
|
|
1711
|
+
message: 'Rebase current branch (' + escHtml(state.currentBranch) + ') onto commit ' + commit.hash.substring(0, 7) + '?',
|
|
1712
|
+
rawMessage: true,
|
|
1713
|
+
confirmLabel: 'Rebase',
|
|
1714
|
+
onConfirm: () => gitAction('rebase', { branch: commit.hash }),
|
|
1715
|
+
});
|
|
1716
|
+
}},
|
|
1499
1717
|
];
|
|
1500
1718
|
// Add "Create PR" if PR creation is configured and commit has a branch ref
|
|
1501
1719
|
if (state.settings.prCreation && state.settings.prCreation.urlTemplate) {
|
|
@@ -71,9 +71,14 @@ async function handleMessage(ws: ExtWsSocket, raw: string | Buffer): Promise<voi
|
|
|
71
71
|
|
|
72
72
|
switch (msg.type) {
|
|
73
73
|
case "ready": {
|
|
74
|
-
// Send current contributions on connect
|
|
74
|
+
// Send current contributions + any activation errors on connect
|
|
75
75
|
const contributions = contributionRegistry.getAll();
|
|
76
|
-
|
|
76
|
+
const { extensionService } = await import("../../services/extension.service.ts");
|
|
77
|
+
const activationErrors = Object.fromEntries(extensionService.getActivationErrors());
|
|
78
|
+
const readyMsg: ExtServerMsg = Object.keys(activationErrors).length > 0
|
|
79
|
+
? { type: "contributions:update", contributions, activationErrors }
|
|
80
|
+
: { type: "contributions:update", contributions };
|
|
81
|
+
ws.send(JSON.stringify(readyMsg));
|
|
77
82
|
break;
|
|
78
83
|
}
|
|
79
84
|
|
|
@@ -81,15 +86,36 @@ async function handleMessage(ws: ExtWsSocket, raw: string | Buffer): Promise<voi
|
|
|
81
86
|
try {
|
|
82
87
|
const { extensionService } = await import("../../services/extension.service.ts");
|
|
83
88
|
if (extensionService["rpc"]) {
|
|
89
|
+
console.log(`[ExtWS] command:execute "${msg.command}"`);
|
|
84
90
|
const result = await extensionService["rpc"].sendRequest<{ ok: boolean; error?: string }>(
|
|
85
91
|
"ext:command:execute", msg.command, ...(msg.args ?? []),
|
|
86
92
|
);
|
|
87
93
|
if (!result?.ok) {
|
|
88
94
|
console.error(`[ExtWS] command:execute failed: ${result?.error ?? "unknown"}`);
|
|
95
|
+
broadcastExtMsg({
|
|
96
|
+
type: "notification",
|
|
97
|
+
id: `cmd-error-${Date.now()}`,
|
|
98
|
+
level: "error",
|
|
99
|
+
message: `Extension command failed: ${result?.error ?? "unknown error"}`,
|
|
100
|
+
});
|
|
89
101
|
}
|
|
102
|
+
} else {
|
|
103
|
+
console.error(`[ExtWS] command:execute: extension host not ready`);
|
|
104
|
+
broadcastExtMsg({
|
|
105
|
+
type: "notification",
|
|
106
|
+
id: `cmd-error-${Date.now()}`,
|
|
107
|
+
level: "error",
|
|
108
|
+
message: `Extension host not ready. Try reloading the page.`,
|
|
109
|
+
});
|
|
90
110
|
}
|
|
91
111
|
} catch (e) {
|
|
92
112
|
console.error(`[ExtWS] command:execute error:`, e);
|
|
113
|
+
broadcastExtMsg({
|
|
114
|
+
type: "notification",
|
|
115
|
+
id: `cmd-error-${Date.now()}`,
|
|
116
|
+
level: "error",
|
|
117
|
+
message: `Extension command error: ${e instanceof Error ? e.message : String(e)}`,
|
|
118
|
+
});
|
|
93
119
|
}
|
|
94
120
|
break;
|
|
95
121
|
}
|
|
@@ -29,6 +29,7 @@ self.addEventListener("message", (event: MessageEvent<RpcMessage>) => {
|
|
|
29
29
|
|
|
30
30
|
rpc.onRequest("ext:activate", async (params) => {
|
|
31
31
|
const [extId, entryPath, extensionPath, storedState, baseUrl] = params as [string, string, string, Record<string, Record<string, string | null>>?, string?];
|
|
32
|
+
console.log(`[ExtHost] activating ${extId} from ${entryPath}`);
|
|
32
33
|
if (activeExtensions.has(extId)) return { ok: true, already: true };
|
|
33
34
|
|
|
34
35
|
// Expose server base URL so extensions can use fetch() with absolute URLs
|
|
@@ -68,6 +69,7 @@ rpc.onRequest("ext:activate", async (params) => {
|
|
|
68
69
|
window: api.window as WindowService,
|
|
69
70
|
commands: api.commands as CommandService,
|
|
70
71
|
});
|
|
72
|
+
console.log(`[ExtHost] activated ${extId} (${activeExtensions.size} total)`);
|
|
71
73
|
return { ok: true };
|
|
72
74
|
} catch (e) {
|
|
73
75
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -101,20 +103,23 @@ rpc.onRequest("ext:deactivate", async (params) => {
|
|
|
101
103
|
|
|
102
104
|
rpc.onRequest("ext:command:execute", async (params) => {
|
|
103
105
|
const [command, ...args] = params as [string, ...unknown[]];
|
|
106
|
+
console.log(`[ExtHost] command:execute "${command}" (${activeExtensions.size} extensions active)`);
|
|
104
107
|
for (const [extId, ext] of activeExtensions) {
|
|
105
108
|
if (ext.commands) {
|
|
106
109
|
const hasLocal = (ext.commands as any).localHandlers?.has(command);
|
|
107
110
|
if (!hasLocal) continue;
|
|
111
|
+
console.log(`[ExtHost] routing "${command}" → ${extId}`);
|
|
108
112
|
try {
|
|
109
113
|
const result = await (ext.commands as any).executeCommand(command, ...args);
|
|
110
114
|
return { ok: true, result };
|
|
111
115
|
} catch (e) {
|
|
112
116
|
const msg = e instanceof Error ? e.message : String(e);
|
|
113
|
-
console.error(`[ExtHost]
|
|
117
|
+
console.error(`[ExtHost] command "${command}" in ${extId} threw:`, msg);
|
|
114
118
|
return { ok: false, error: msg };
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
}
|
|
122
|
+
console.warn(`[ExtHost] command not found: "${command}"`);
|
|
118
123
|
return { ok: false, error: `Command not found: ${command}` };
|
|
119
124
|
});
|
|
120
125
|
|
|
@@ -13,6 +13,7 @@ class ExtensionService {
|
|
|
13
13
|
private worker: Worker | null = null;
|
|
14
14
|
private rpc: RpcChannel | null = null;
|
|
15
15
|
private activatedIds = new Set<string>();
|
|
16
|
+
private activationErrors = new Map<string, string>();
|
|
16
17
|
private workerReady = false;
|
|
17
18
|
private installing = new Set<string>();
|
|
18
19
|
private extensionPaths = new Map<string, string>();
|
|
@@ -56,6 +57,7 @@ class ExtensionService {
|
|
|
56
57
|
if (this.worker) { this.worker.terminate(); this.worker = null; }
|
|
57
58
|
this.workerReady = false;
|
|
58
59
|
this.activatedIds.clear();
|
|
60
|
+
this.activationErrors.clear();
|
|
59
61
|
this.extensionPaths.clear();
|
|
60
62
|
this.bundledIds.clear();
|
|
61
63
|
contributionRegistry.clear();
|
|
@@ -141,15 +143,20 @@ class ExtensionService {
|
|
|
141
143
|
const port = cfg.get("port") ?? 8080;
|
|
142
144
|
const baseUrl = `http://localhost:${port}`;
|
|
143
145
|
|
|
146
|
+
console.log(`[ExtService] activating ${id} (entry: ${entryPath})`);
|
|
144
147
|
const result = await rpc.sendRequest<{ ok: boolean; error?: string }>(
|
|
145
148
|
"ext:activate", id, entryPath, extDir, storedState, baseUrl,
|
|
146
149
|
);
|
|
147
|
-
if (!result.ok)
|
|
150
|
+
if (!result.ok) {
|
|
151
|
+
this.activationErrors.set(id, result.error ?? "Unknown activation error");
|
|
152
|
+
throw new Error(`Failed to activate ${id}: ${result.error}`);
|
|
153
|
+
}
|
|
148
154
|
|
|
155
|
+
this.activationErrors.delete(id);
|
|
149
156
|
this.activatedIds.add(id);
|
|
150
157
|
if (manifest.contributes) contributionRegistry.register(id, manifest.contributes);
|
|
151
158
|
this.broadcastContributions();
|
|
152
|
-
console.log(`[ExtService]
|
|
159
|
+
console.log(`[ExtService] activated ${id} successfully`);
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
async deactivate(id: string): Promise<void> {
|
|
@@ -236,7 +243,9 @@ class ExtensionService {
|
|
|
236
243
|
}
|
|
237
244
|
for (const row of getExtensions()) {
|
|
238
245
|
if (row.enabled !== 1) continue;
|
|
246
|
+
console.log(`[ExtService] startup: activating ${row.id}...`);
|
|
239
247
|
try { await this.activate(row.id); } catch (e) {
|
|
248
|
+
this.activationErrors.set(row.id, e instanceof Error ? e.message : String(e));
|
|
240
249
|
console.error(`[ExtService] Failed to activate ${row.id} on startup:`, e);
|
|
241
250
|
}
|
|
242
251
|
}
|
|
@@ -252,12 +261,17 @@ class ExtensionService {
|
|
|
252
261
|
isActivated(id: string): boolean { return this.activatedIds.has(id); }
|
|
253
262
|
isBundled(id: string): boolean { return this.bundledIds.has(id); }
|
|
254
263
|
getExtensionsDir(): string { return resolve(getPpmDir(), "extensions"); }
|
|
264
|
+
getActivationErrors(): Map<string, string> { return new Map(this.activationErrors); }
|
|
255
265
|
|
|
256
266
|
/** Push current contributions to all connected browser clients */
|
|
257
267
|
private broadcastContributions(): void {
|
|
258
268
|
try {
|
|
259
269
|
const { broadcastExtMsg } = require("../server/ws/extensions.ts");
|
|
260
|
-
|
|
270
|
+
const contributions = contributionRegistry.getAll();
|
|
271
|
+
broadcastExtMsg(this.activationErrors.size > 0
|
|
272
|
+
? { type: "contributions:update", contributions, activationErrors: Object.fromEntries(this.activationErrors) }
|
|
273
|
+
: { type: "contributions:update", contributions },
|
|
274
|
+
);
|
|
261
275
|
} catch {}
|
|
262
276
|
}
|
|
263
277
|
}
|
|
@@ -51,7 +51,7 @@ export type ExtServerMsg =
|
|
|
51
51
|
| { type: "webview:postMessage"; panelId: string; message: unknown }
|
|
52
52
|
| { type: "tab:open"; tabType: string; title: string; projectId: string | null; closable?: boolean; metadata?: Record<string, unknown> }
|
|
53
53
|
| { type: "project:switch"; projectName: string }
|
|
54
|
-
| { type: "contributions:update"; contributions: ExtensionContributes };
|
|
54
|
+
| { type: "contributions:update"; contributions: ExtensionContributes; activationErrors?: Record<string, string> };
|
|
55
55
|
|
|
56
56
|
// --- Client → Server messages ---
|
|
57
57
|
|