diff --git a/src/lib/BranchHome.svelte b/src/lib/BranchHome.svelte index 2c4c964..40fe353 100644 --- a/src/lib/BranchHome.svelte +++ b/src/lib/BranchHome.svelte @@ -20,6 +20,7 @@ import NewProjectModal from './NewProjectModal.svelte'; import ConfirmDialog from './ConfirmDialog.svelte'; import { DiffSpec } from './types'; + import { windowState, closeTab } from './stores/tabState.svelte'; interface Props { onViewDiff?: (projectId: string, repoPath: string, spec: DiffSpec, label: string) => void; @@ -255,6 +256,7 @@ if (!branchToDelete) return; const id = branchToDelete.id; + const worktreePath = branchToDelete.worktreePath; // Close dialog and show spinner immediately branchToDelete = null; deletingBranchIds = new Set(deletingBranchIds).add(id); @@ -266,6 +268,12 @@ const newDeleting = new Set(deletingBranchIds); newDeleting.delete(id); deletingBranchIds = newDeleting; + + // Close any tabs that were viewing this branch's worktree + const tabsToClose = windowState.tabs.filter((tab) => tab.repoPath === worktreePath); + for (const tab of tabsToClose) { + closeTab(tab.id); + } } catch (e) { // Failure: remove spinner, show error card const newDeleting = new Set(deletingBranchIds); @@ -281,24 +289,6 @@ deleteErrors = newErrors; } - function handleViewDiff(branch: Branch) { - onViewDiff?.( - branch.projectId, - branch.worktreePath, - DiffSpec.mergeBaseDiff(branch.baseBranch, branch.branchName), - `${branch.baseBranch}..${branch.branchName}` - ); - } - - function handleViewCommitDiff(branch: Branch, commitSha: string) { - onViewDiff?.( - branch.projectId, - branch.worktreePath, - DiffSpec.fromRevs(`${commitSha}~1`, commitSha), - commitSha.slice(0, 7) - ); - } - function handleNewProjectCreated(project: GitProject) { projects = [...projects, project]; showNewProjectModal = false; @@ -443,12 +433,31 @@ {:else} + {@const branchId = branch.id} + {@const projectId = branch.projectId} + {@const worktreePath = branch.worktreePath} + {@const baseBranch = branch.baseBranch} + {@const branchName = branch.branchName} handleViewDiff(branch)} - onViewCommitDiff={(sha) => handleViewCommitDiff(branch, sha)} - onDelete={() => handleDeleteBranch(branch.id)} + onViewDiff={() => { + onViewDiff?.( + projectId, + worktreePath, + DiffSpec.mergeBaseDiff(baseBranch, branchName), + `${baseBranch}..${branchName}` + ); + }} + onViewCommitDiff={(sha) => { + onViewDiff?.( + projectId, + worktreePath, + DiffSpec.fromRevs(`${sha}~1`, sha), + sha.slice(0, 7) + ); + }} + onDelete={() => handleDeleteBranch(branchId)} /> {/if} {/each} diff --git a/src/lib/stores/tabState.svelte.ts b/src/lib/stores/tabState.svelte.ts index da38084..c008b56 100644 --- a/src/lib/stores/tabState.svelte.ts +++ b/src/lib/stores/tabState.svelte.ts @@ -235,22 +235,82 @@ export async function loadTabsFromStorage( const data = await loadWindowTabsFromStore(windowState.windowLabel); if (data) { + // Filter out tabs with non-existent worktree paths to prevent errors + // when branches have been deleted outside the app + const validTabs = await Promise.all( + data.tabs.map(async (t) => { + // Check if the repo path exists (use Tauri's fs API or simple check) + try { + // For now, we'll use a simple heuristic: filter out obvious worktree paths + // that don't exist. Non-worktree paths (like main repo) should be kept. + const isWorktreePath = t.repoPath.includes('/.staged/worktrees/'); + if (isWorktreePath) { + // For worktree paths, verify they exist before restoring the tab + const exists = await pathExists(t.repoPath); + if (!exists) { + console.warn( + `[TabState] Skipping tab "${t.repoName}" with non-existent worktree path: ${t.repoPath}` + ); + return null; + } + } + return t; + } catch (e) { + console.warn(`[TabState] Error checking tab path "${t.repoPath}":`, e); + return t; // Keep tab if we can't check (fail open) + } + }) + ); + // Create tabs with isolated state instances // Plain objects are created - the parent windowState.tabs array is already reactive - windowState.tabs = data.tabs.map((t) => ({ - id: t.id || t.projectId, // Fallback for old format - projectId: t.projectId || t.id, // Fallback for old format - repoPath: t.repoPath, - repoName: t.repoName, - subpath: t.subpath || null, - diffState: createDiffState(), - commentsState: createCommentsState(), - diffSelection: createDiffSelection(), - agentState: createAgentState(), - referenceFilesState: createReferenceFilesState(), - needsRefresh: false, - })); - windowState.activeTabIndex = data.activeTabIndex; + windowState.tabs = validTabs + .filter((t) => t !== null) + .map((t) => ({ + id: t.id || t.projectId, // Fallback for old format + projectId: t.projectId || t.id, // Fallback for old format + repoPath: t.repoPath, + repoName: t.repoName, + subpath: t.subpath || null, + diffState: createDiffState(), + commentsState: createCommentsState(), + diffSelection: createDiffSelection(), + agentState: createAgentState(), + referenceFilesState: createReferenceFilesState(), + needsRefresh: false, + })); + + // Adjust active tab index if the active tab was filtered out + if (windowState.tabs.length > 0 && data.activeTabIndex >= windowState.tabs.length) { + windowState.activeTabIndex = 0; + } else if (windowState.tabs.length > 0) { + windowState.activeTabIndex = Math.min(data.activeTabIndex, windowState.tabs.length - 1); + } else { + windowState.activeTabIndex = 0; + } + + // Save the cleaned-up tabs back to storage to prevent stale tabs from persisting + if (validTabs.filter((t) => t !== null).length !== data.tabs.length) { + console.log( + `[TabState] Removed ${data.tabs.length - windowState.tabs.length} stale tab(s) from storage` + ); + saveTabsToStorage(); + } + } +} + +/** + * Check if a path exists by attempting to read it. + * Uses the existing listDirectory backend command. + */ +async function pathExists(path: string): Promise { + try { + // Try to list the directory - if it exists, this will succeed + const { invoke } = await import('@tauri-apps/api/core'); + await invoke('list_directory', { path }); + return true; + } catch { + return false; } }