From 3d13fd5643b844aaefcdfc4d3fcf9b5c1e6fe539 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Thu, 2 Apr 2026 11:11:11 +1100 Subject: [PATCH 1/5] chore: add PR polling diagnostics --- apps/staged/src-tauri/src/prs.rs | 78 +++++++++++++++++++ .../branches/BranchCardPrButton.svelte | 32 +++++++- 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/apps/staged/src-tauri/src/prs.rs b/apps/staged/src-tauri/src/prs.rs index cab40ffac..0a478b67d 100644 --- a/apps/staged/src-tauri/src/prs.rs +++ b/apps/staged/src-tauri/src/prs.rs @@ -242,6 +242,14 @@ pub async fn refresh_pr_status( .ok_or_else(|| format!("Project not found: {}", branch.project_id))?; let (github_repo, _) = resolve_branch_repo_and_subpath(&store, &project, &branch)?; + log::info!( + "refresh_pr_status: starting branch_id={} project_id={} pr_number={} repo={}", + branch_id, + branch.project_id, + pr_number, + github_repo + ); + // Run the blocking gh CLI call on a background thread so we don't // block the Tauri IPC thread and starve other commands. let pr_status = { @@ -255,6 +263,13 @@ pub async fn refresh_pr_status( let pr_status = match pr_status { Ok(status) => status, Err(e) => { + log::info!( + "refresh_pr_status: fetch failed branch_id={} pr_number={} repo={} error={}", + branch_id, + pr_number, + github_repo, + e + ); log::error!( "refresh_pr_status failed for branch_id={}, pr_number={}: {}", branch_id, @@ -280,6 +295,18 @@ pub async fn refresh_pr_status( ) .map_err(|e| e.to_string())?; + log::info!( + "refresh_pr_status: success branch_id={} pr_number={} state={} checks={} review_decision={} mergeable={} draft={} head_sha={}", + branch_id, + pr_number, + pr_status.state, + pr_status.checks_summary.state, + pr_status.review_decision.as_deref().unwrap_or("null"), + mergeable, + pr_status.is_draft, + pr_status.head_sha.as_deref().unwrap_or("null") + ); + app_handle .emit( "pr-status-changed", @@ -318,6 +345,12 @@ pub async fn refresh_all_pr_statuses( .filter(|b| b.pr_number.is_some()) .collect(); + log::info!( + "refresh_all_pr_statuses: starting project_id={} branch_count_with_prs={}", + project_id, + branches_with_prs.len() + ); + let mut refreshed_count = 0u32; for branch in branches_with_prs { @@ -325,6 +358,12 @@ pub async fn refresh_all_pr_statuses( let github_repo = match resolve_branch_repo_and_subpath(&store, &project, &branch) { Ok((repo, _)) => repo, Err(e) => { + log::info!( + "refresh_all_pr_statuses: repo resolution failed branch_id={} pr_number={} error={}", + branch.id, + pr_number, + e + ); log::warn!( "Failed to resolve repo for branch {} (PR #{}): {}", branch.id, @@ -335,6 +374,14 @@ pub async fn refresh_all_pr_statuses( } }; + log::info!( + "refresh_all_pr_statuses: polling branch_id={} project_id={} pr_number={} repo={}", + branch.id, + branch.project_id, + pr_number, + github_repo + ); + let pr_result = { let github_repo = github_repo.clone(); tauri::async_runtime::spawn_blocking(move || { @@ -358,12 +405,30 @@ pub async fn refresh_all_pr_statuses( None, pr_status.head_sha.clone(), ) { + log::info!( + "refresh_all_pr_statuses: store update failed branch_id={} pr_number={} error={}", + branch.id, + pr_number, + e + ); log::warn!("Failed to update PR status for branch {}: {}", branch.id, e); continue; } refreshed_count += 1; + log::info!( + "refresh_all_pr_statuses: success branch_id={} pr_number={} state={} checks={} review_decision={} mergeable={} draft={} head_sha={}", + branch.id, + pr_number, + pr_status.state, + pr_status.checks_summary.state, + pr_status.review_decision.as_deref().unwrap_or("null"), + mergeable, + pr_status.is_draft, + pr_status.head_sha.as_deref().unwrap_or("null") + ); + if let Err(e) = app_handle.emit( "pr-status-changed", PrStatusEvent { @@ -380,6 +445,13 @@ pub async fn refresh_all_pr_statuses( } } Err(e) => { + log::info!( + "refresh_all_pr_statuses: fetch failed branch_id={} pr_number={} repo={} error={}", + branch.id, + pr_number, + github_repo, + e + ); log::warn!( "Failed to fetch PR status for branch {} (PR #{}): {}", branch.id, @@ -394,6 +466,12 @@ pub async fn refresh_all_pr_statuses( .emit("pr-statuses-refreshed", &project_id) .map_err(|e| format!("Failed to emit event: {}", e))?; + log::info!( + "refresh_all_pr_statuses: finished project_id={} refreshed_count={}", + project_id, + refreshed_count + ); + Ok(refreshed_count) } diff --git a/apps/staged/src/lib/features/branches/BranchCardPrButton.svelte b/apps/staged/src/lib/features/branches/BranchCardPrButton.svelte index 7cf4c0a0a..0fdcdf787 100644 --- a/apps/staged/src/lib/features/branches/BranchCardPrButton.svelte +++ b/apps/staged/src/lib/features/branches/BranchCardPrButton.svelte @@ -224,6 +224,9 @@ if (prStatusState === 'MERGED' || prStatusState === 'CLOSED') { if (prStatusPollTimer) { + console.info( + `[BranchCardPrButton] Stopping PR status poll timer for branch=${branch.id} because PR state is ${prStatusState}` + ); clearInterval(prStatusPollTimer); prStatusPollTimer = null; } @@ -239,15 +242,24 @@ if (shouldPoll) { if (prStatusPollTimer) { + console.info( + `[BranchCardPrButton] Resetting PR status poll timer for branch=${branch.id} interval=${pollInterval}ms checks=${prStatusChecks ?? 'null'} focused=${isWindowFocused}` + ); clearInterval(prStatusPollTimer); } prStatusPollTimer = setInterval(async () => { if (prStatusRefreshing) { + console.info( + `[BranchCardPrButton] Skipping PR status poll for branch=${branch.id} because a refresh is already in flight` + ); return; } try { prStatusRefreshing = true; + console.info( + `[BranchCardPrButton] Starting PR status poll for branch=${branch.id} pr=${branch.prNumber} interval=${pollInterval}ms checks=${prStatusChecks ?? 'null'} focused=${isWindowFocused}` + ); let timeoutId: ReturnType; const timeout = new Promise((_, reject) => { timeoutId = setTimeout( @@ -257,17 +269,26 @@ }); try { await Promise.race([commands.refreshPrStatus(branch.id), timeout]); + console.info( + `[BranchCardPrButton] PR status poll succeeded for branch=${branch.id} pr=${branch.prNumber}` + ); } finally { clearTimeout(timeoutId!); } } catch (e) { - console.error(`[BranchCardPrButton] Poll refresh failed for branch=${branch.id}:`, e); + console.info(`[BranchCardPrButton] PR status poll failed for branch=${branch.id}:`, e); } finally { prStatusRefreshing = false; } }, pollInterval); + console.info( + `[BranchCardPrButton] Started PR status poll timer for branch=${branch.id} pr=${branch.prNumber} interval=${pollInterval}ms checks=${prStatusChecks ?? 'null'} focused=${isWindowFocused}` + ); } else { if (prStatusPollTimer) { + console.info( + `[BranchCardPrButton] Stopping PR status poll timer for branch=${branch.id} pr=${branch.prNumber} because focused=${isWindowFocused}` + ); clearInterval(prStatusPollTimer); prStatusPollTimer = null; } @@ -275,6 +296,9 @@ return () => { if (prStatusPollTimer) { + console.info( + `[BranchCardPrButton] Cleaning up PR status poll timer for branch=${branch.id} pr=${branch.prNumber}` + ); clearInterval(prStatusPollTimer); prStatusPollTimer = null; } @@ -288,6 +312,9 @@ handleFocus = () => { isWindowFocused = true; if (branch.prNumber && !prStatusRefreshing) { + console.info( + `[BranchCardPrButton] Window focused; refreshing PR status immediately for branch=${branch.id} pr=${branch.prNumber}` + ); commands .refreshPrStatus(branch.id) .catch((e) => console.error('Failed to refresh PR status on focus:', e)); @@ -300,6 +327,9 @@ window.addEventListener('blur', handleBlur); if (branch.prNumber) { + console.info( + `[BranchCardPrButton] Fetching initial PR status for branch=${branch.id} pr=${branch.prNumber}` + ); commands .refreshPrStatus(branch.id) .catch((e) => console.error('Failed to fetch initial PR status:', e)); From 841863f6bf5aeae41c3182d5bf4440988b9a8044 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Thu, 2 Apr 2026 11:51:58 +1100 Subject: [PATCH 2/5] fix: refresh PR status after PR creation completes --- apps/staged/src/lib/listeners/sessionStatusListener.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/staged/src/lib/listeners/sessionStatusListener.ts b/apps/staged/src/lib/listeners/sessionStatusListener.ts index 2c0a7cb50..254696b7f 100644 --- a/apps/staged/src/lib/listeners/sessionStatusListener.ts +++ b/apps/staged/src/lib/listeners/sessionStatusListener.ts @@ -110,8 +110,9 @@ async function handlePrCompletion(sessionId: string, branchId: string, status: s if (prNumber) { try { await commands.updateBranchPr(branchId, prNumber); + await commands.refreshPrStatus(branchId); } catch (storageError) { - console.error('Failed to persist PR number to storage:', storageError); + console.error('Failed to persist or refresh PR state after creation:', storageError); } } prStateStore.setPrCreated(branchId, foundUrl); From e652e95dc520364296bcabfd88e4ac077f99980e Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Thu, 2 Apr 2026 11:54:26 +1100 Subject: [PATCH 3/5] fix: start PR polling for PR-backed branch creation --- .../branches/BranchCardPrButton.svelte | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/apps/staged/src/lib/features/branches/BranchCardPrButton.svelte b/apps/staged/src/lib/features/branches/BranchCardPrButton.svelte index 0fdcdf787..03bdf13f9 100644 --- a/apps/staged/src/lib/features/branches/BranchCardPrButton.svelte +++ b/apps/staged/src/lib/features/branches/BranchCardPrButton.svelte @@ -82,6 +82,7 @@ // PR status polling state let prStatusPollTimer: ReturnType | null = null; let prStatusRefreshing = $state(false); + let lastImmediateRefreshPrNumber = $state(null); // PR status fields (local state, updated via events) let prStatusState = $state(null); @@ -305,6 +306,29 @@ }; }); + // Kick off an immediate refresh whenever this branch gains a PR number. + // This covers branches hydrated after mount, such as project/repo creation from an existing PR. + $effect(() => { + const prNumber = branch.prNumber; + + if (!prNumber) { + lastImmediateRefreshPrNumber = null; + return; + } + + if (!isWindowFocused || prStatusRefreshing || lastImmediateRefreshPrNumber === prNumber) { + return; + } + + lastImmediateRefreshPrNumber = prNumber; + console.info( + `[BranchCardPrButton] Fetching initial PR status for branch=${branch.id} pr=${prNumber}` + ); + commands + .refreshPrStatus(branch.id) + .catch((e) => console.error('Failed to fetch initial PR status:', e)); + }); + onMount(() => { window.addEventListener('keydown', handleOptionDown); window.addEventListener('keyup', handleOptionUp); @@ -325,15 +349,6 @@ }; window.addEventListener('focus', handleFocus); window.addEventListener('blur', handleBlur); - - if (branch.prNumber) { - console.info( - `[BranchCardPrButton] Fetching initial PR status for branch=${branch.id} pr=${branch.prNumber}` - ); - commands - .refreshPrStatus(branch.id) - .catch((e) => console.error('Failed to fetch initial PR status:', e)); - } }); onDestroy(() => { From e801a3bfb0b5724526c7e4290233b328da37d08b Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Thu, 2 Apr 2026 12:01:12 +1100 Subject: [PATCH 4/5] chore: remove PR polling diagnostics --- apps/staged/src-tauri/src/prs.rs | 78 ------------------- .../branches/BranchCardPrButton.svelte | 32 +------- 2 files changed, 1 insertion(+), 109 deletions(-) diff --git a/apps/staged/src-tauri/src/prs.rs b/apps/staged/src-tauri/src/prs.rs index 0a478b67d..cab40ffac 100644 --- a/apps/staged/src-tauri/src/prs.rs +++ b/apps/staged/src-tauri/src/prs.rs @@ -242,14 +242,6 @@ pub async fn refresh_pr_status( .ok_or_else(|| format!("Project not found: {}", branch.project_id))?; let (github_repo, _) = resolve_branch_repo_and_subpath(&store, &project, &branch)?; - log::info!( - "refresh_pr_status: starting branch_id={} project_id={} pr_number={} repo={}", - branch_id, - branch.project_id, - pr_number, - github_repo - ); - // Run the blocking gh CLI call on a background thread so we don't // block the Tauri IPC thread and starve other commands. let pr_status = { @@ -263,13 +255,6 @@ pub async fn refresh_pr_status( let pr_status = match pr_status { Ok(status) => status, Err(e) => { - log::info!( - "refresh_pr_status: fetch failed branch_id={} pr_number={} repo={} error={}", - branch_id, - pr_number, - github_repo, - e - ); log::error!( "refresh_pr_status failed for branch_id={}, pr_number={}: {}", branch_id, @@ -295,18 +280,6 @@ pub async fn refresh_pr_status( ) .map_err(|e| e.to_string())?; - log::info!( - "refresh_pr_status: success branch_id={} pr_number={} state={} checks={} review_decision={} mergeable={} draft={} head_sha={}", - branch_id, - pr_number, - pr_status.state, - pr_status.checks_summary.state, - pr_status.review_decision.as_deref().unwrap_or("null"), - mergeable, - pr_status.is_draft, - pr_status.head_sha.as_deref().unwrap_or("null") - ); - app_handle .emit( "pr-status-changed", @@ -345,12 +318,6 @@ pub async fn refresh_all_pr_statuses( .filter(|b| b.pr_number.is_some()) .collect(); - log::info!( - "refresh_all_pr_statuses: starting project_id={} branch_count_with_prs={}", - project_id, - branches_with_prs.len() - ); - let mut refreshed_count = 0u32; for branch in branches_with_prs { @@ -358,12 +325,6 @@ pub async fn refresh_all_pr_statuses( let github_repo = match resolve_branch_repo_and_subpath(&store, &project, &branch) { Ok((repo, _)) => repo, Err(e) => { - log::info!( - "refresh_all_pr_statuses: repo resolution failed branch_id={} pr_number={} error={}", - branch.id, - pr_number, - e - ); log::warn!( "Failed to resolve repo for branch {} (PR #{}): {}", branch.id, @@ -374,14 +335,6 @@ pub async fn refresh_all_pr_statuses( } }; - log::info!( - "refresh_all_pr_statuses: polling branch_id={} project_id={} pr_number={} repo={}", - branch.id, - branch.project_id, - pr_number, - github_repo - ); - let pr_result = { let github_repo = github_repo.clone(); tauri::async_runtime::spawn_blocking(move || { @@ -405,30 +358,12 @@ pub async fn refresh_all_pr_statuses( None, pr_status.head_sha.clone(), ) { - log::info!( - "refresh_all_pr_statuses: store update failed branch_id={} pr_number={} error={}", - branch.id, - pr_number, - e - ); log::warn!("Failed to update PR status for branch {}: {}", branch.id, e); continue; } refreshed_count += 1; - log::info!( - "refresh_all_pr_statuses: success branch_id={} pr_number={} state={} checks={} review_decision={} mergeable={} draft={} head_sha={}", - branch.id, - pr_number, - pr_status.state, - pr_status.checks_summary.state, - pr_status.review_decision.as_deref().unwrap_or("null"), - mergeable, - pr_status.is_draft, - pr_status.head_sha.as_deref().unwrap_or("null") - ); - if let Err(e) = app_handle.emit( "pr-status-changed", PrStatusEvent { @@ -445,13 +380,6 @@ pub async fn refresh_all_pr_statuses( } } Err(e) => { - log::info!( - "refresh_all_pr_statuses: fetch failed branch_id={} pr_number={} repo={} error={}", - branch.id, - pr_number, - github_repo, - e - ); log::warn!( "Failed to fetch PR status for branch {} (PR #{}): {}", branch.id, @@ -466,12 +394,6 @@ pub async fn refresh_all_pr_statuses( .emit("pr-statuses-refreshed", &project_id) .map_err(|e| format!("Failed to emit event: {}", e))?; - log::info!( - "refresh_all_pr_statuses: finished project_id={} refreshed_count={}", - project_id, - refreshed_count - ); - Ok(refreshed_count) } diff --git a/apps/staged/src/lib/features/branches/BranchCardPrButton.svelte b/apps/staged/src/lib/features/branches/BranchCardPrButton.svelte index 03bdf13f9..4a1972b90 100644 --- a/apps/staged/src/lib/features/branches/BranchCardPrButton.svelte +++ b/apps/staged/src/lib/features/branches/BranchCardPrButton.svelte @@ -225,9 +225,6 @@ if (prStatusState === 'MERGED' || prStatusState === 'CLOSED') { if (prStatusPollTimer) { - console.info( - `[BranchCardPrButton] Stopping PR status poll timer for branch=${branch.id} because PR state is ${prStatusState}` - ); clearInterval(prStatusPollTimer); prStatusPollTimer = null; } @@ -243,24 +240,15 @@ if (shouldPoll) { if (prStatusPollTimer) { - console.info( - `[BranchCardPrButton] Resetting PR status poll timer for branch=${branch.id} interval=${pollInterval}ms checks=${prStatusChecks ?? 'null'} focused=${isWindowFocused}` - ); clearInterval(prStatusPollTimer); } prStatusPollTimer = setInterval(async () => { if (prStatusRefreshing) { - console.info( - `[BranchCardPrButton] Skipping PR status poll for branch=${branch.id} because a refresh is already in flight` - ); return; } try { prStatusRefreshing = true; - console.info( - `[BranchCardPrButton] Starting PR status poll for branch=${branch.id} pr=${branch.prNumber} interval=${pollInterval}ms checks=${prStatusChecks ?? 'null'} focused=${isWindowFocused}` - ); let timeoutId: ReturnType; const timeout = new Promise((_, reject) => { timeoutId = setTimeout( @@ -270,26 +258,17 @@ }); try { await Promise.race([commands.refreshPrStatus(branch.id), timeout]); - console.info( - `[BranchCardPrButton] PR status poll succeeded for branch=${branch.id} pr=${branch.prNumber}` - ); } finally { clearTimeout(timeoutId!); } } catch (e) { - console.info(`[BranchCardPrButton] PR status poll failed for branch=${branch.id}:`, e); + console.error(`[BranchCardPrButton] Poll refresh failed for branch=${branch.id}:`, e); } finally { prStatusRefreshing = false; } }, pollInterval); - console.info( - `[BranchCardPrButton] Started PR status poll timer for branch=${branch.id} pr=${branch.prNumber} interval=${pollInterval}ms checks=${prStatusChecks ?? 'null'} focused=${isWindowFocused}` - ); } else { if (prStatusPollTimer) { - console.info( - `[BranchCardPrButton] Stopping PR status poll timer for branch=${branch.id} pr=${branch.prNumber} because focused=${isWindowFocused}` - ); clearInterval(prStatusPollTimer); prStatusPollTimer = null; } @@ -297,9 +276,6 @@ return () => { if (prStatusPollTimer) { - console.info( - `[BranchCardPrButton] Cleaning up PR status poll timer for branch=${branch.id} pr=${branch.prNumber}` - ); clearInterval(prStatusPollTimer); prStatusPollTimer = null; } @@ -321,9 +297,6 @@ } lastImmediateRefreshPrNumber = prNumber; - console.info( - `[BranchCardPrButton] Fetching initial PR status for branch=${branch.id} pr=${prNumber}` - ); commands .refreshPrStatus(branch.id) .catch((e) => console.error('Failed to fetch initial PR status:', e)); @@ -336,9 +309,6 @@ handleFocus = () => { isWindowFocused = true; if (branch.prNumber && !prStatusRefreshing) { - console.info( - `[BranchCardPrButton] Window focused; refreshing PR status immediately for branch=${branch.id} pr=${branch.prNumber}` - ); commands .refreshPrStatus(branch.id) .catch((e) => console.error('Failed to refresh PR status on focus:', e)); From 48ad63d5ef88b357ba40a9e0359bdb82fe0f5a7f Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Thu, 2 Apr 2026 12:08:25 +1100 Subject: [PATCH 5/5] fix: separate PR persistence and refresh failures --- .../staged/src/lib/listeners/sessionStatusListener.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/staged/src/lib/listeners/sessionStatusListener.ts b/apps/staged/src/lib/listeners/sessionStatusListener.ts index 254696b7f..9a13c0f8d 100644 --- a/apps/staged/src/lib/listeners/sessionStatusListener.ts +++ b/apps/staged/src/lib/listeners/sessionStatusListener.ts @@ -110,9 +110,16 @@ async function handlePrCompletion(sessionId: string, branchId: string, status: s if (prNumber) { try { await commands.updateBranchPr(branchId, prNumber); - await commands.refreshPrStatus(branchId); } catch (storageError) { - console.error('Failed to persist or refresh PR state after creation:', storageError); + console.error('Failed to persist PR state after creation:', storageError); + prStateStore.setPrError(branchId, 'Failed to save PR details after creation.'); + return; + } + + try { + await commands.refreshPrStatus(branchId); + } catch (refreshError) { + console.error('Failed to refresh PR state after creation:', refreshError); } } prStateStore.setPrCreated(branchId, foundUrl);