Skip to content

Commit 2203596

Browse files
Merge pull request #11 from JeanBaptisteRenard/feat/pr49-delete-worktree-dialog
feat(worktree): rich delete confirmation dialog with dirty-file status
2 parents 1ab01aa + 6d74fd1 commit 2203596

3 files changed

Lines changed: 155 additions & 1 deletion

File tree

preload.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ contextBridge.exposeInMainWorld('api', {
4242
addProject: (projectPath) => ipcRenderer.invoke('add-project', projectPath),
4343
removeProject: (projectPath) => ipcRenderer.invoke('remove-project', projectPath),
4444
deleteWorktree: (worktreePath) => ipcRenderer.invoke('delete-worktree', worktreePath),
45+
worktreeStatus: (worktreePath) => ipcRenderer.invoke('worktree-status', worktreePath),
4546
openExternal: (url) => ipcRenderer.invoke('open-external', url),
4647

4748
// Send (fire-and-forget)

public/sidebar.js

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,8 @@ function rebindSidebarEvents(projects) {
720720
wtDeleteBtn.onclick = async (e) => {
721721
e.stopPropagation();
722722
const name = wtProject.projectPath.split('/').pop();
723-
if (!confirm(`Delete worktree "${name}" from disk?\n\nThis runs "git worktree remove -f" and permanently removes the working tree. This cannot be undone.`)) return;
723+
const confirmed = await showDeleteWorktreeDialog(name, wtProject.projectPath);
724+
if (!confirmed) return;
724725
const result = await window.api.deleteWorktree(wtProject.projectPath);
725726
if (result && result.ok) {
726727
loadProjects();
@@ -1025,3 +1026,75 @@ function startRename(summaryEl, session) {
10251026
}
10261027
});
10271028
}
1029+
1030+
// --- Delete worktree confirmation dialog ---
1031+
// Returns a Promise<boolean> — true if the user confirmed deletion.
1032+
async function showDeleteWorktreeDialog(name, worktreePath) {
1033+
// Fetch worktree status (dirty files) while the dialog is shown
1034+
const statusPromise = window.api.worktreeStatus(worktreePath);
1035+
1036+
return new Promise((resolve) => {
1037+
const overlay = document.createElement('div');
1038+
overlay.className = 'new-session-overlay';
1039+
1040+
const dialog = document.createElement('div');
1041+
dialog.className = 'new-session-dialog delete-worktree-dialog';
1042+
1043+
dialog.innerHTML = `
1044+
<h3>Delete worktree "${escapeHtml(name)}"?</h3>
1045+
<div class="delete-worktree-warning">
1046+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;margin-top:1px"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
1047+
<span>Any uncommitted changes in this worktree will be permanently lost.</span>
1048+
</div>
1049+
<div class="delete-worktree-status" id="dwt-status">
1050+
<span class="dwt-loading">Checking worktree status…</span>
1051+
</div>
1052+
<div class="new-session-actions">
1053+
<button class="new-session-cancel-btn" id="dwt-cancel">Cancel</button>
1054+
<button class="delete-worktree-confirm-btn" id="dwt-confirm">Delete anyway</button>
1055+
</div>
1056+
`;
1057+
1058+
overlay.appendChild(dialog);
1059+
document.body.appendChild(overlay);
1060+
1061+
const statusEl = dialog.querySelector('#dwt-status');
1062+
1063+
// Populate status once the IPC resolves
1064+
statusPromise.then((status) => {
1065+
if (!overlay.isConnected) return; // dialog already closed
1066+
if (!status || !status.ok) {
1067+
const errMsg = (status && status.error) ? escapeHtml(status.error) : 'Unknown error';
1068+
statusEl.innerHTML = `<span class="dwt-error">Unable to read worktree status: ${errMsg}</span>`;
1069+
return;
1070+
}
1071+
if (status.total === 0) {
1072+
statusEl.innerHTML = `<span class="dwt-clean">Worktree is clean — no uncommitted changes.</span>`;
1073+
return;
1074+
}
1075+
const shown = status.dirty.slice(0, 10);
1076+
const overflow = status.total - shown.length;
1077+
const lines = shown.map(l => escapeHtml(l)).join('\n');
1078+
const extra = overflow > 0 ? `\n+ ${overflow} more…` : '';
1079+
statusEl.innerHTML = `<div class="dwt-dirty-label">${status.total} uncommitted file${status.total !== 1 ? 's' : ''}:</div><pre class="dwt-dirty-list">${lines}${extra}</pre>`;
1080+
}).catch((err) => {
1081+
if (!overlay.isConnected) return;
1082+
statusEl.innerHTML = `<span class="dwt-error">Unable to read worktree status: ${escapeHtml(String(err))}</span>`;
1083+
});
1084+
1085+
function close(confirmed) {
1086+
overlay.remove();
1087+
document.removeEventListener('keydown', onKey);
1088+
resolve(confirmed);
1089+
}
1090+
1091+
dialog.querySelector('#dwt-cancel').onclick = () => close(false);
1092+
dialog.querySelector('#dwt-confirm').onclick = () => close(true);
1093+
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(false); });
1094+
1095+
function onKey(e) {
1096+
if (e.key === 'Escape') close(false);
1097+
}
1098+
document.addEventListener('keydown', onKey);
1099+
});
1100+
}

public/style.css

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3617,6 +3617,86 @@ body { display: flex; flex-direction: column; }
36173617
border-color: rgba(62,207,90,0.5);
36183618
}
36193619

3620+
/* ========== DELETE WORKTREE DIALOG ========== */
3621+
.delete-worktree-dialog {
3622+
width: 460px;
3623+
}
3624+
3625+
.delete-worktree-warning {
3626+
display: flex;
3627+
align-items: flex-start;
3628+
gap: 8px;
3629+
background: rgba(224,80,112,0.08);
3630+
border: 1px solid rgba(224,80,112,0.25);
3631+
border-radius: 8px;
3632+
padding: 10px 14px;
3633+
font-size: 13px;
3634+
color: #e05070;
3635+
margin-bottom: 14px;
3636+
line-height: 1.4;
3637+
}
3638+
3639+
.delete-worktree-status {
3640+
background: rgba(255,255,255,0.03);
3641+
border: 1px solid rgba(255,255,255,0.07);
3642+
border-radius: 8px;
3643+
padding: 10px 14px;
3644+
font-size: 12px;
3645+
color: #9090a8;
3646+
min-height: 36px;
3647+
margin-bottom: 4px;
3648+
}
3649+
3650+
.dwt-loading {
3651+
font-style: italic;
3652+
color: #606078;
3653+
}
3654+
3655+
.dwt-clean {
3656+
color: #3ecf82;
3657+
}
3658+
3659+
.dwt-error {
3660+
color: #e07050;
3661+
word-break: break-word;
3662+
}
3663+
3664+
.dwt-dirty-label {
3665+
color: #e0a050;
3666+
margin-bottom: 6px;
3667+
font-weight: 600;
3668+
}
3669+
3670+
.dwt-dirty-list {
3671+
margin: 0;
3672+
font-family: 'SF Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', monospace;
3673+
font-size: 11px;
3674+
color: #c0c0d8;
3675+
white-space: pre-wrap;
3676+
word-break: break-all;
3677+
line-height: 1.5;
3678+
max-height: 160px;
3679+
overflow-y: auto;
3680+
}
3681+
3682+
.delete-worktree-confirm-btn {
3683+
background: rgba(224,80,112,0.12);
3684+
border: 1px solid rgba(224,80,112,0.35);
3685+
color: #e05070;
3686+
font-size: 13px;
3687+
padding: 8px 20px;
3688+
border-radius: 6px;
3689+
cursor: pointer;
3690+
font-family: inherit;
3691+
font-weight: 600;
3692+
transition: all 0.15s;
3693+
}
3694+
3695+
.delete-worktree-confirm-btn:hover {
3696+
background: rgba(224,80,112,0.22);
3697+
border-color: rgba(224,80,112,0.55);
3698+
}
3699+
36203700
/* ========== NEW SESSION POPOVER ========== */
36213701
.new-session-popover {
36223702
position: fixed;

0 commit comments

Comments
 (0)