Skip to content
This repository was archived by the owner on Feb 6, 2026. It is now read-only.
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
51 changes: 30 additions & 21 deletions src/lib/BranchHome.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -443,12 +433,31 @@
</div>
</div>
{:else}
{@const branchId = branch.id}
{@const projectId = branch.projectId}
{@const worktreePath = branch.worktreePath}
{@const baseBranch = branch.baseBranch}
{@const branchName = branch.branchName}
<BranchCard
{branch}
{refreshKey}
onViewDiff={() => 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}
Expand Down
88 changes: 74 additions & 14 deletions src/lib/stores/tabState.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
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;
}
}

Expand Down