From 527330a093a4d656db0c8406a89fa7445c34c1ad Mon Sep 17 00:00:00 2001 From: klopez4212 Date: Sun, 5 Jul 2026 07:10:32 +0100 Subject: [PATCH] Keep the PR monitor live: gh token fallback, honest errors, poll gating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The monitor went stale-empty overnight because every failure path compounded: the app usually launches without GITHUB_TOKEN in its env, so all GitHub calls ran anonymous (60 req/h) while the panel polls ~180 req/h — rate-limited within minutes — and non-success responses returned Ok(None), which the UI rendered as a confident "No checks / 0 open comments" instead of an error. Three fixes: ambient_github_token falls back to `gh auth token` (resolved once per process) so the desktop uses the CLI credential that's actually on the machine; GitHub commands now return Err on non-404 failures so React Query keeps the last good snapshot and retries instead of overwriting it with nothing; and polling pauses entirely while the drawer is closed — unless automation is armed, which keeps watching in the background. Co-Authored-By: Claude Fable 5 --- .../src-tauri/src/commands/link_preview.rs | 61 ++++++++++++++++--- .../src/features/chats/ui/ChatWorkPanel.tsx | 17 ++++-- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/desktop/src-tauri/src/commands/link_preview.rs b/desktop/src-tauri/src/commands/link_preview.rs index e2d78f3099..ce255c2f5e 100644 --- a/desktop/src-tauri/src/commands/link_preview.rs +++ b/desktop/src-tauri/src/commands/link_preview.rs @@ -103,9 +103,14 @@ pub async fn fetch_github_pull_request( .send() .await .map_err(|error| format!("github request failed: {error}"))?; - if !response.status().is_success() { + if response.status() == reqwest::StatusCode::NOT_FOUND { return Ok(None); } + if !response.status().is_success() { + // Rate limits and transient failures must NOT read as "no data" — + // the UI keeps its last good snapshot on error. + return Err(format!("github request failed: {}", response.status())); + } let body: serde_json::Value = response .json() @@ -167,9 +172,14 @@ pub async fn fetch_github_check_summary( .send() .await .map_err(|error| format!("github request failed: {error}"))?; - if !response.status().is_success() { + if response.status() == reqwest::StatusCode::NOT_FOUND { return Ok(None); } + if !response.status().is_success() { + // Rate limits and transient failures must NOT read as "no data" — + // the UI keeps its last good snapshot on error. + return Err(format!("github request failed: {}", response.status())); + } let body: serde_json::Value = response .json() @@ -249,9 +259,12 @@ pub async fn fetch_github_pr_comment_state( .send() .await .map_err(|error| format!("github request failed: {error}"))?; - if !pr_response.status().is_success() { + if pr_response.status() == reqwest::StatusCode::NOT_FOUND { return Ok(None); } + if !pr_response.status().is_success() { + return Err(format!("github request failed: {}", pr_response.status())); + } let pr: serde_json::Value = pr_response .json() .await @@ -264,9 +277,15 @@ pub async fn fetch_github_pr_comment_state( .send() .await .map_err(|error| format!("github request failed: {error}"))?; - if !comments_response.status().is_success() { + if comments_response.status() == reqwest::StatusCode::NOT_FOUND { return Ok(None); } + if !comments_response.status().is_success() { + return Err(format!( + "github request failed: {}", + comments_response.status() + )); + } let comments: serde_json::Value = comments_response .json() .await @@ -417,12 +436,34 @@ fn parse_github_remote(remote: &str) -> Option<(String, String)> { } fn ambient_github_token() -> Option { - ["GITHUB_TOKEN", "GH_TOKEN"].iter().find_map(|name| { - std::env::var(name) - .ok() - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) - }) + static TOKEN: std::sync::OnceLock> = std::sync::OnceLock::new(); + TOKEN + .get_or_init(|| { + let env_token = ["GITHUB_TOKEN", "GH_TOKEN"].iter().find_map(|name| { + std::env::var(name) + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + }); + if env_token.is_some() { + return env_token; + } + // Desktop apps rarely inherit a token env (launched from Finder or + // a clean shell), but the gh CLI credential is usually present — + // without it the anonymous 60 req/h limit starves the PR monitor + // within minutes. Resolved once per process. + let output = std::process::Command::new("gh") + .args(["auth", "token"]) + .stdin(std::process::Stdio::null()) + .output() + .ok()?; + if !output.status.success() { + return None; + } + let token = String::from_utf8_lossy(&output.stdout).trim().to_string(); + (!token.is_empty()).then_some(token) + }) + .clone() } fn is_valid_github_name(value: &str) -> bool { diff --git a/desktop/src/features/chats/ui/ChatWorkPanel.tsx b/desktop/src/features/chats/ui/ChatWorkPanel.tsx index 2c1d688a3e..23f2d774d3 100644 --- a/desktop/src/features/chats/ui/ChatWorkPanel.tsx +++ b/desktop/src/features/chats/ui/ChatWorkPanel.tsx @@ -55,23 +55,32 @@ export function ChatWorkPanel({ projectPath?: string | null; }) { // No PR link in the chat yet? Discover it from the branch via the - // project's git remote (agents don't always paste the URL). + // project's git remote (agents don't always paste the URL). All GitHub + // polling pauses while the drawer is closed — a hidden panel burning the + // API rate limit is how the monitor went stale-empty. + const automation = useChatWorkAutomation(chatId); + // Armed automation keeps watching with the drawer closed; otherwise a + // hidden panel stops polling entirely. + const monitorActive = + open || automation.autoFixCi || automation.addressComments; const discoveredPrQuery = useGithubPrForBranchQuery( - prHref ? null : projectPath, + monitorActive && !prHref ? projectPath : null, branch, ); const effectiveHref = prHref ?? discoveredPrQuery.data ?? null; const preview = effectiveHref ? parseSupportedLinkPreview(effectiveHref) : null; - const ref = effectiveHref ? parseGithubPullRequestRef(effectiveHref) : null; + const parsedRef = effectiveHref + ? parseGithubPullRequestRef(effectiveHref) + : null; + const ref = monitorActive ? parsedRef : null; const prQuery = useGithubPullRequestQuery(ref); const pr = prQuery.data ?? null; const checksQuery = useGithubCheckSummaryQuery(ref, pr?.headSha); const checks = checksQuery.data ?? null; const commentStateQuery = useGithubCommentStateQuery(ref); const openThreads = commentStateQuery.data?.openThreads ?? 0; - const automation = useChatWorkAutomation(chatId); // Live activity wins over the PR's head ref: the agent may have moved to a // new worktree since opening the PR, and activity updates immediately. const currentBranch = branch?.trim() || pr?.headRef?.trim() || null;