diff --git a/main.js b/main.js
index a24a1b2..c61578f 100644
--- a/main.js
+++ b/main.js
@@ -341,6 +341,47 @@ ipcMain.handle('remove-project', (_event, projectPath) => {
}
});
+// --- IPC: remap-project ---
+ipcMain.handle('remap-project', (_event, oldPath, newPath) => {
+ try {
+ const stat = fs.statSync(newPath);
+ if (!stat.isDirectory()) return { error: 'Path is not a directory' };
+
+ // Find the folder key for the old project path
+ const folder = oldPath.replace(/[/_]/g, '-').replace(/^-/, '-');
+ const folderPath = path.join(PROJECTS_DIR, folder);
+ if (!fs.existsSync(folderPath)) return { error: 'No session data found for this project' };
+
+ // Rewrite cwd in all session JSONL files so CLI --resume also works
+ const jsonlFiles = fs.readdirSync(folderPath).filter(f => f.endsWith('.jsonl'));
+ for (const file of jsonlFiles) {
+ const filePath = path.join(folderPath, file);
+ const content = fs.readFileSync(filePath, 'utf8');
+ const updated = content.split('\n').map(line => {
+ if (!line) return line;
+ try {
+ const parsed = JSON.parse(line);
+ if (parsed.cwd === oldPath) {
+ parsed.cwd = newPath;
+ return JSON.stringify(parsed);
+ }
+ } catch {}
+ return line;
+ }).join('\n');
+ const tmp = filePath + '.tmp';
+ fs.writeFileSync(tmp, updated);
+ fs.renameSync(tmp, filePath);
+ }
+
+ // Refresh the folder cache so the new path takes effect
+ refreshFolder(folder);
+ notifyRendererProjectsChanged();
+ return { ok: true };
+ } catch (err) {
+ return { error: err.message };
+ }
+});
+
// --- IPC: get-projects ---
ipcMain.handle('open-external', (_event, url) => {
log.info('[open-external IPC]', url);
diff --git a/preload.js b/preload.js
index 91d8b5e..a7b0e4a 100644
--- a/preload.js
+++ b/preload.js
@@ -35,6 +35,7 @@ contextBridge.exposeInMainWorld('api', {
browseFolder: () => ipcRenderer.invoke('browse-folder'),
addProject: (projectPath) => ipcRenderer.invoke('add-project', projectPath),
removeProject: (projectPath) => ipcRenderer.invoke('remove-project', projectPath),
+ remapProject: (oldPath, newPath) => ipcRenderer.invoke('remap-project', oldPath, newPath),
openExternal: (url) => ipcRenderer.invoke('open-external', url),
// Send (fire-and-forget)
diff --git a/public/app.js b/public/app.js
index 9875965..ab8f1de 100644
--- a/public/app.js
+++ b/public/app.js
@@ -731,6 +731,7 @@ async function launchNewSession(project, sessionOptions) {
if (!result.ok) {
entry.terminal.write(`\r\nError: ${result.error}\r\n`);
entry.closed = true;
+ showSession(sessionId);
return;
}
if (typeof setSessionMcpActive === 'function') setSessionMcpActive(sessionId, !!result.mcpActive);
@@ -797,6 +798,7 @@ async function openSession(session, customOptions) {
if (!result.ok) {
entry.terminal.write(`\r\nError: ${result.error}\r\n`);
entry.closed = true;
+ showSession(sessionId);
return;
}
if (typeof setSessionMcpActive === 'function') setSessionMcpActive(sessionId, !!result.mcpActive);
diff --git a/public/sidebar.js b/public/sidebar.js
index a26916f..34cd22c 100644
--- a/public/sidebar.js
+++ b/public/sidebar.js
@@ -278,14 +278,15 @@ function renderProjects(projects, resort) {
// Build DOM
const group = document.createElement('div');
- group.className = 'project-group';
+ group.className = 'project-group' + (project.missing ? ' missing' : '');
group.id = fId;
const header = document.createElement('div');
header.className = 'project-header';
header.id = 'ph-' + fId;
const shortName = project.projectPath.split('/').filter(Boolean).slice(-2).join('/');
- header.innerHTML = `▼ ${shortName}`;
+ const missingIcon = project.missing ? ' ' : '';
+ header.innerHTML = `▼ ${missingIcon}${escapeHtml(shortName)}`;
const scheduleBtn = document.createElement('button');
scheduleBtn.className = 'project-schedule-btn';
@@ -305,6 +306,14 @@ function renderProjects(projects, resort) {
archiveGroupBtn.innerHTML = ICONS.archive(18);
header.appendChild(archiveGroupBtn);
+ if (project.missing) {
+ const remapBtn = document.createElement('button');
+ remapBtn.className = 'project-remap-btn';
+ remapBtn.title = 'Change project path';
+ remapBtn.innerHTML = '';
+ header.appendChild(remapBtn);
+ }
+
const newBtn = document.createElement('button');
newBtn.className = 'project-new-btn';
newBtn.innerHTML = '';
@@ -313,8 +322,10 @@ function renderProjects(projects, resort) {
const sessionsList = buildSessionsList(fId, visible, older);
- // Auto-collapse if most recent session is older than threshold, or project matched with no sessions
- if (project._projectMatchedOnly) {
+ // Auto-collapse if project path is missing, most recent session is older than threshold, or project matched with no sessions
+ if (project.missing) {
+ header.classList.add('collapsed');
+ } else if (project._projectMatchedOnly) {
header.classList.add('collapsed');
} else if (searchMatchIds === null && !showStarredOnly && !showRunningOnly) {
const mostRecent = filtered[0]?.modified;
@@ -454,6 +465,22 @@ function rebindSidebarEvents(projects) {
if (settingsBtn) {
settingsBtn.onclick = (e) => { e.stopPropagation(); openSettingsViewer('project', project.projectPath); };
}
+ const remapBtn = header.querySelector('.project-remap-btn');
+ if (remapBtn) {
+ remapBtn.onclick = async (e) => {
+ e.stopPropagation();
+ const newPath = await window.api.browseFolder();
+ if (!newPath) return;
+ const shortName = project.projectPath.split('/').filter(Boolean).slice(-2).join('/');
+ if (!confirm(`Remap ${shortName} to:\n${newPath}?`)) return;
+ const result = await window.api.remapProject(project.projectPath, newPath);
+ if (result.error) {
+ alert('Failed to remap: ' + result.error);
+ } else {
+ loadProjects();
+ }
+ };
+ }
const archiveGroupBtn = header.querySelector('.project-archive-btn');
if (archiveGroupBtn) {
archiveGroupBtn.onclick = async (e) => {
@@ -474,7 +501,7 @@ function rebindSidebarEvents(projects) {
};
}
header.onclick = (e) => {
- if (e.target.closest('.project-new-btn') || e.target.closest('.project-archive-btn') || e.target.closest('.project-settings-btn') || e.target.closest('.project-schedule-btn')) return;
+ if (e.target.closest('.project-new-btn') || e.target.closest('.project-archive-btn') || e.target.closest('.project-settings-btn') || e.target.closest('.project-schedule-btn') || e.target.closest('.project-remap-btn')) return;
header.classList.toggle('collapsed');
};
}
@@ -558,6 +585,14 @@ function rebindSidebarEvents(projects) {
const session = sessionMap.get(sessionId);
if (!session) return;
+ // Sessions under missing projects can't be opened — the path no longer exists
+ if (item.closest('.project-group.missing')) {
+ item.classList.add('disabled');
+ item.title = 'Project path no longer exists — use "Change path" to fix';
+ item.onclick = () => {};
+ return;
+ }
+
item.onclick = () => openSession(session);
const pin = item.querySelector('.session-pin');
diff --git a/public/style.css b/public/style.css
index f6070c0..011b282 100644
--- a/public/style.css
+++ b/public/style.css
@@ -470,6 +470,40 @@ body { display: flex; flex-direction: column; }
display: none;
}
+/* Missing project path */
+.project-group.missing {
+ opacity: 0.55;
+}
+.project-group.missing:hover {
+ opacity: 0.8;
+}
+.project-missing-icon {
+ color: #e8a838;
+ vertical-align: middle;
+ margin-right: 2px;
+ flex-shrink: 0;
+}
+.project-remap-btn {
+ background: none;
+ border: none;
+ color: #e8a838;
+ cursor: pointer;
+ padding: 2px 4px;
+ border-radius: 4px;
+ display: none;
+ align-items: center;
+}
+.project-group.missing .project-remap-btn {
+ display: inline-flex;
+}
+.project-remap-btn:hover {
+ background: rgba(232, 168, 56, 0.15);
+}
+.session-item.disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
/* Worktree nested groups */
.worktree-group {
margin-left: 12px;
diff --git a/session-cache.js b/session-cache.js
index 4a306de..dfdcedf 100644
--- a/session-cache.js
+++ b/session-cache.js
@@ -175,7 +175,7 @@ function buildProjectsFromCache(showArchived) {
for (const row of cachedRows) {
if (hiddenProjects.has(row.projectPath)) continue;
if (!projectMap.has(row.folder)) {
- projectMap.set(row.folder, { folder: row.folder, projectPath: row.projectPath, sessions: [] });
+ projectMap.set(row.folder, { folder: row.folder, projectPath: row.projectPath, sessions: [], missing: !fs.existsSync(row.projectPath) });
}
const meta = metaMap.get(row.sessionId);
const s = {
@@ -203,7 +203,7 @@ function buildProjectsFromCache(showArchived) {
if (!projectMap.has(d.name)) {
const projectPath = deriveProjectPath(path.join(PROJECTS_DIR, d.name), d.name);
if (projectPath && !hiddenProjects.has(projectPath)) {
- projectMap.set(d.name, { folder: d.name, projectPath, sessions: [] });
+ projectMap.set(d.name, { folder: d.name, projectPath, sessions: [], missing: !fs.existsSync(projectPath) });
}
}
}
@@ -236,6 +236,9 @@ function buildProjectsFromCache(showArchived) {
}
projects.sort((a, b) => {
+ // Missing projects go to the bottom
+ if (a.missing && !b.missing) return 1;
+ if (!a.missing && b.missing) return -1;
// Empty projects go to the bottom
if (a.sessions.length === 0 && b.sessions.length > 0) return 1;
if (b.sessions.length === 0 && a.sessions.length > 0) return -1;