From 639cc1e078b598d48e6e1c88563d2d0396041c2b Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Thu, 16 Apr 2026 13:52:53 +1000 Subject: [PATCH 1/2] fix(staged): reload timeline after cache invalidation and prevent stale worktreePath overwrites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes for commits not appearing after project-from-PR creation: 1. Dispatch 'timeline-invalidated' CustomEvent from invalidateBranchTimeline and invalidateProjectBranchTimelines, and listen for it in BranchCard to trigger a re-fetch — previously the loadedTimelineKey guard prevented re-loading after cache invalidation. 2. Merge branches in the project-setup-progress handler to preserve worktreePath when a stale async response would overwrite it with null. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/staged/src/lib/commands.ts | 4 ++++ .../src/lib/features/branches/BranchCard.svelte | 12 ++++++++++++ .../src/lib/features/projects/ProjectHome.svelte | 14 ++++++++++++-- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/staged/src/lib/commands.ts b/apps/staged/src/lib/commands.ts index 82137cd9..306c98b0 100644 --- a/apps/staged/src/lib/commands.ts +++ b/apps/staged/src/lib/commands.ts @@ -320,6 +320,9 @@ const inFlightTimelines = new Map>(); export function invalidateBranchTimeline(branchId: string): void { timelineCache.delete(branchId); + window.dispatchEvent( + new CustomEvent('timeline-invalidated', { detail: { branchIds: [branchId] } }) + ); } interface GetBranchTimelineOptions { @@ -373,6 +376,7 @@ export function invalidateProjectBranchTimelines(branchIds: string[]): void { for (const id of branchIds) { timelineCache.delete(id); } + window.dispatchEvent(new CustomEvent('timeline-invalidated', { detail: { branchIds } })); } // ============================================================================= diff --git a/apps/staged/src/lib/features/branches/BranchCard.svelte b/apps/staged/src/lib/features/branches/BranchCard.svelte index 7d138b3c..9e6186f2 100644 --- a/apps/staged/src/lib/features/branches/BranchCard.svelte +++ b/apps/staged/src/lib/features/branches/BranchCard.svelte @@ -513,6 +513,18 @@ void loadTimeline(); }); + // Re-fetch timeline when the cache is invalidated (e.g. after project-setup-progress) + $effect(() => { + const handler = (e: Event) => { + const { branchIds } = (e as CustomEvent<{ branchIds: string[] }>).detail; + if (branchIds.includes(branch.id) && (branch.worktreePath || isRemote)) { + void loadTimeline(); + } + }; + window.addEventListener('timeline-invalidated', handler); + return () => window.removeEventListener('timeline-invalidated', handler); + }); + let revalidationVersion = 0; async function loadTimeline() { diff --git a/apps/staged/src/lib/features/projects/ProjectHome.svelte b/apps/staged/src/lib/features/projects/ProjectHome.svelte index 464a452b..9cec80b6 100644 --- a/apps/staged/src/lib/features/projects/ProjectHome.svelte +++ b/apps/staged/src/lib/features/projects/ProjectHome.svelte @@ -118,8 +118,18 @@ ]); setProjects(projectsList); projects = projectsList; - branchesByProject = new Map(branchesByProject).set(projectId, branches); - commands.invalidateProjectBranchTimelines(branches.map((b) => b.id)); + // Merge branches carefully: don't let a stale async response overwrite + // worktreePath with null when we already have it set. + const existingBranches = branchesByProject.get(projectId) || []; + const mergedBranches = branches.map((newBranch) => { + const existing = existingBranches.find((b) => b.id === newBranch.id); + if (existing?.worktreePath && !newBranch.worktreePath) { + return { ...newBranch, worktreePath: existing.worktreePath }; + } + return newBranch; + }); + branchesByProject = new Map(branchesByProject).set(projectId, mergedBranches); + commands.invalidateProjectBranchTimelines(mergedBranches.map((b) => b.id)); workspaceLifecycle.enqueueInitialSetup(projectId, branches); replaceProjectRepos(projectId, repos); void repoBadgeStore.ensureForRepos( From 0cff4a38aad82c949ab5087119b029d0fe6955e1 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Thu, 16 Apr 2026 13:55:23 +1000 Subject: [PATCH 2/2] fix(staged): propagate git errors from timeline instead of swallowing them Previously, get_commits_since_base errors were silently caught and replaced with an empty vec, which the frontend then cached as a valid empty timeline. This caused commits to appear missing when transient git errors (e.g., ref-lock races during concurrent worktree setup) occurred during the initial timeline load. By propagating the error, the frontend's cache-on-success logic ensures failed loads aren't cached and will be retried. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/staged/src-tauri/src/timeline.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/apps/staged/src-tauri/src/timeline.rs b/apps/staged/src-tauri/src/timeline.rs index a0326c78..dbfa3523 100644 --- a/apps/staged/src-tauri/src/timeline.rs +++ b/apps/staged/src-tauri/src/timeline.rs @@ -83,14 +83,10 @@ fn build_branch_timeline(store: &Arc, branch_id: &str) -> Result commits, - Err(e) => { - log::warn!("Failed to get commits since base for branch {branch_id}: {e:?}"); - vec![] - } - }; + let git_commits = git::get_commits_since_base(worktree_path, &branch.base_branch) + .map_err(|e| { + format!("Failed to get commits since base for branch {branch_id}: {e:?}") + })?; // For each git commit, look up our metadata (session linkage) for gc in git_commits {