Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions main.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { app, BrowserWindow, dialog, ipcMain, Menu, screen, shell } = require('electron');
const { Worker } = require('worker_threads');

Check warning on line 2 in main.js

View workflow job for this annotation

GitHub Actions / test (20)

'Worker' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 2 in main.js

View workflow job for this annotation

GitHub Actions / test (22)

'Worker' is assigned a value but never used. Allowed unused vars must match /^_/u
const { execFile } = require('child_process');
const path = require('path');
const fs = require('fs');
Expand All @@ -16,7 +16,7 @@
}

// getFolderIndexMtimeMs moved to session-cache.js
const { startMcpServer, shutdownMcpServer, shutdownAll: shutdownAllMcp, resolvePendingDiff, rekeyMcpServer, cleanStaleLockFiles } = require('./mcp-bridge');

Check warning on line 19 in main.js

View workflow job for this annotation

GitHub Actions / test (20)

'cleanStaleLockFiles' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 19 in main.js

View workflow job for this annotation

GitHub Actions / test (22)

'cleanStaleLockFiles' is assigned a value but never used. Allowed unused vars must match /^_/u
const { fetchAndTransformUsage } = require('./claude-auth');
log.transports.file.level = app.isPackaged ? 'info' : 'debug';
log.transports.console.level = app.isPackaged ? 'info' : 'debug';
Expand All @@ -36,7 +36,7 @@
);

// Shell profiles → shell-profiles.js
const { discoverShellProfiles, getShellProfiles, resolveShell, isWindows, isWslShell, windowsToWslPath, shellArgs } = require('./shell-profiles');

Check warning on line 39 in main.js

View workflow job for this annotation

GitHub Actions / test (20)

'isWindows' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 39 in main.js

View workflow job for this annotation

GitHub Actions / test (20)

'discoverShellProfiles' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 39 in main.js

View workflow job for this annotation

GitHub Actions / test (22)

'isWindows' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 39 in main.js

View workflow job for this annotation

GitHub Actions / test (22)

'discoverShellProfiles' is assigned a value but never used. Allowed unused vars must match /^_/u
const { startScheduler } = require('./schedule-runner');
const { encodeProjectPath } = require('./encode-project-path');

Expand Down Expand Up @@ -73,7 +73,7 @@
getMeta, getAllMeta, toggleStar, setName, setArchived,
isCachePopulated, getAllCached, getCachedByFolder, getCachedByParent, getCachedFolder, getCachedSession, upsertCachedSessions,
deleteCachedSession, deleteCachedFolder,
getFolderMeta, getAllFolderMeta, setFolderMeta,

Check warning on line 76 in main.js

View workflow job for this annotation

GitHub Actions / test (20)

'getFolderMeta' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 76 in main.js

View workflow job for this annotation

GitHub Actions / test (22)

'getFolderMeta' is assigned a value but never used. Allowed unused vars must match /^_/u
upsertSearchEntries, updateSearchTitle, deleteSearchSession, deleteSearchFolder, deleteSearchType,
searchByType, isSearchIndexPopulated, searchFtsRecreated,
getSetting, setSetting, deleteSetting,
Expand Down Expand Up @@ -289,8 +289,8 @@
setFolderMeta, getAllFolderMeta, getAllMeta, getAllCached, getSetting, getMeta, setName,
},
});
const { readSessionFile, readFolderFromFilesystem, refreshFolder, populateCacheFromFilesystem,

Check warning on line 292 in main.js

View workflow job for this annotation

GitHub Actions / test (20)

'populateCacheFromFilesystem' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 292 in main.js

View workflow job for this annotation

GitHub Actions / test (20)

'readFolderFromFilesystem' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 292 in main.js

View workflow job for this annotation

GitHub Actions / test (20)

'readSessionFile' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 292 in main.js

View workflow job for this annotation

GitHub Actions / test (22)

'populateCacheFromFilesystem' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 292 in main.js

View workflow job for this annotation

GitHub Actions / test (22)

'readFolderFromFilesystem' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 292 in main.js

View workflow job for this annotation

GitHub Actions / test (22)

'readSessionFile' is assigned a value but never used. Allowed unused vars must match /^_/u
buildProjectsFromCache, notifyRendererProjectsChanged, sendStatus, populateCacheViaWorker } = sessionCache;

Check warning on line 293 in main.js

View workflow job for this annotation

GitHub Actions / test (20)

'sendStatus' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 293 in main.js

View workflow job for this annotation

GitHub Actions / test (22)

'sendStatus' is assigned a value but never used. Allowed unused vars must match /^_/u


// --- IPC: browse-folder ---
Expand Down Expand Up @@ -444,6 +444,26 @@
});
});

// --- 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);
Expand Down Expand Up @@ -1127,7 +1147,7 @@
// WSL profiles only work for plain terminals — Claude CLI sessions need the
// Windows shell because session data lives on the Windows filesystem.
const requestedProfile = resolveShell(effectiveProfileId);
const useWslProfile = isWslShell(requestedProfile.path) && isPlainTerminal;

Check warning on line 1150 in main.js

View workflow job for this annotation

GitHub Actions / test (20)

'useWslProfile' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 1150 in main.js

View workflow job for this annotation

GitHub Actions / test (22)

'useWslProfile' is assigned a value but never used. Allowed unused vars must match /^_/u
const shellProfile = (isWslShell(requestedProfile.path) && !isPlainTerminal)
? resolveShell('auto')
: requestedProfile;
Expand Down
1 change: 1 addition & 0 deletions preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
75 changes: 74 additions & 1 deletion public/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -998,3 +999,75 @@ function startRename(summaryEl, session) {
}
});
}

// --- Delete worktree confirmation dialog ---
// Returns a Promise<boolean> — 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 = `
<h3>Delete worktree "${escapeHtml(name)}"?</h3>
<div class="delete-worktree-warning">
<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>
<span>Any uncommitted changes in this worktree will be permanently lost.</span>
</div>
<div class="delete-worktree-status" id="dwt-status">
<span class="dwt-loading">Checking worktree status…</span>
</div>
<div class="new-session-actions">
<button class="new-session-cancel-btn" id="dwt-cancel">Cancel</button>
<button class="delete-worktree-confirm-btn" id="dwt-confirm">Delete anyway</button>
</div>
`;

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 = `<span class="dwt-error">Unable to read worktree status: ${errMsg}</span>`;
return;
}
if (status.total === 0) {
statusEl.innerHTML = `<span class="dwt-clean">Worktree is clean — no uncommitted changes.</span>`;
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 = `<div class="dwt-dirty-label">${status.total} uncommitted file${status.total !== 1 ? 's' : ''}:</div><pre class="dwt-dirty-list">${lines}${extra}</pre>`;
}).catch((err) => {
if (!overlay.isConnected) return;
statusEl.innerHTML = `<span class="dwt-error">Unable to read worktree status: ${escapeHtml(String(err))}</span>`;
});

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);
});
}
80 changes: 80 additions & 0 deletions public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading