diff --git a/main.js b/main.js index 19800549..fdf58491 100644 --- a/main.js +++ b/main.js @@ -444,6 +444,26 @@ ipcMain.handle('delete-worktree', (_event, worktreePath) => { }); }); +// --- IPC: worktree-status --- +ipcMain.handle('worktree-status', (_event, worktreePath) => { + return new Promise((resolve) => { + const normalizedPath = worktreePath.replace(/\/$/, ''); + const match = normalizedPath.match(WORKTREE_PATH_RE); + if (!match) { + return resolve({ ok: false, error: 'Path does not match a recognized worktree layout' }); + } + const parentRepo = match[1]; + + execFile('git', ['-C', parentRepo, '-C', normalizedPath, 'status', '--porcelain'], (err, stdout, stderr) => { + if (err) { + return resolve({ ok: false, error: (stderr || err.message || String(err)).trim() }); + } + const dirty = stdout.split('\n').map(l => l.trimEnd()).filter(Boolean); + resolve({ ok: true, dirty, total: dirty.length }); + }); + }); +}); + // --- IPC: get-projects --- ipcMain.handle('open-external', (_event, url) => { log.info('[open-external IPC]', url); diff --git a/preload.js b/preload.js index db71964c..6961f56d 100644 --- a/preload.js +++ b/preload.js @@ -42,6 +42,7 @@ contextBridge.exposeInMainWorld('api', { addProject: (projectPath) => ipcRenderer.invoke('add-project', projectPath), removeProject: (projectPath) => ipcRenderer.invoke('remove-project', projectPath), deleteWorktree: (worktreePath) => ipcRenderer.invoke('delete-worktree', worktreePath), + worktreeStatus: (worktreePath) => ipcRenderer.invoke('worktree-status', worktreePath), openExternal: (url) => ipcRenderer.invoke('open-external', url), // Send (fire-and-forget) diff --git a/public/sidebar.js b/public/sidebar.js index 7ca2bc42..7827f80d 100644 --- a/public/sidebar.js +++ b/public/sidebar.js @@ -699,7 +699,8 @@ function rebindSidebarEvents(projects) { wtDeleteBtn.onclick = async (e) => { e.stopPropagation(); const name = wtProject.projectPath.split('/').pop(); - 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; + const confirmed = await showDeleteWorktreeDialog(name, wtProject.projectPath); + if (!confirmed) return; const result = await window.api.deleteWorktree(wtProject.projectPath); if (result && result.ok) { loadProjects(); @@ -998,3 +999,75 @@ function startRename(summaryEl, session) { } }); } + +// --- Delete worktree confirmation dialog --- +// Returns a Promise — true if the user confirmed deletion. +async function showDeleteWorktreeDialog(name, worktreePath) { + // Fetch worktree status (dirty files) while the dialog is shown + const statusPromise = window.api.worktreeStatus(worktreePath); + + return new Promise((resolve) => { + const overlay = document.createElement('div'); + overlay.className = 'new-session-overlay'; + + const dialog = document.createElement('div'); + dialog.className = 'new-session-dialog delete-worktree-dialog'; + + dialog.innerHTML = ` +

Delete worktree "${escapeHtml(name)}"?

+
+ + Any uncommitted changes in this worktree will be permanently lost. +
+
+ Checking worktree status… +
+
+ + +
+ `; + + overlay.appendChild(dialog); + document.body.appendChild(overlay); + + const statusEl = dialog.querySelector('#dwt-status'); + + // Populate status once the IPC resolves + statusPromise.then((status) => { + if (!overlay.isConnected) return; // dialog already closed + if (!status || !status.ok) { + const errMsg = (status && status.error) ? escapeHtml(status.error) : 'Unknown error'; + statusEl.innerHTML = `Unable to read worktree status: ${errMsg}`; + return; + } + if (status.total === 0) { + statusEl.innerHTML = `Worktree is clean — no uncommitted changes.`; + return; + } + const shown = status.dirty.slice(0, 10); + const overflow = status.total - shown.length; + const lines = shown.map(l => escapeHtml(l)).join('\n'); + const extra = overflow > 0 ? `\n+ ${overflow} more…` : ''; + statusEl.innerHTML = `
${status.total} uncommitted file${status.total !== 1 ? 's' : ''}:
${lines}${extra}
`; + }).catch((err) => { + if (!overlay.isConnected) return; + statusEl.innerHTML = `Unable to read worktree status: ${escapeHtml(String(err))}`; + }); + + function close(confirmed) { + overlay.remove(); + document.removeEventListener('keydown', onKey); + resolve(confirmed); + } + + dialog.querySelector('#dwt-cancel').onclick = () => close(false); + dialog.querySelector('#dwt-confirm').onclick = () => close(true); + overlay.addEventListener('click', (e) => { if (e.target === overlay) close(false); }); + + function onKey(e) { + if (e.key === 'Escape') close(false); + } + document.addEventListener('keydown', onKey); + }); +} diff --git a/public/style.css b/public/style.css index 31c85dad..9b88cc9b 100644 --- a/public/style.css +++ b/public/style.css @@ -3583,6 +3583,86 @@ body { display: flex; flex-direction: column; } border-color: rgba(62,207,90,0.5); } +/* ========== DELETE WORKTREE DIALOG ========== */ +.delete-worktree-dialog { + width: 460px; +} + +.delete-worktree-warning { + display: flex; + align-items: flex-start; + gap: 8px; + background: rgba(224,80,112,0.08); + border: 1px solid rgba(224,80,112,0.25); + border-radius: 8px; + padding: 10px 14px; + font-size: 13px; + color: #e05070; + margin-bottom: 14px; + line-height: 1.4; +} + +.delete-worktree-status { + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.07); + border-radius: 8px; + padding: 10px 14px; + font-size: 12px; + color: #9090a8; + min-height: 36px; + margin-bottom: 4px; +} + +.dwt-loading { + font-style: italic; + color: #606078; +} + +.dwt-clean { + color: #3ecf82; +} + +.dwt-error { + color: #e07050; + word-break: break-word; +} + +.dwt-dirty-label { + color: #e0a050; + margin-bottom: 6px; + font-weight: 600; +} + +.dwt-dirty-list { + margin: 0; + font-family: 'SF Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', monospace; + font-size: 11px; + color: #c0c0d8; + white-space: pre-wrap; + word-break: break-all; + line-height: 1.5; + max-height: 160px; + overflow-y: auto; +} + +.delete-worktree-confirm-btn { + background: rgba(224,80,112,0.12); + border: 1px solid rgba(224,80,112,0.35); + color: #e05070; + font-size: 13px; + padding: 8px 20px; + border-radius: 6px; + cursor: pointer; + font-family: inherit; + font-weight: 600; + transition: all 0.15s; +} + +.delete-worktree-confirm-btn:hover { + background: rgba(224,80,112,0.22); + border-color: rgba(224,80,112,0.55); +} + /* ========== NEW SESSION POPOVER ========== */ .new-session-popover { position: fixed;