mirror of
https://github.com/block/buzz.git
synced 2026-11-02 23:41:08 +00:00
Keep the PR monitor live: gh token fallback, honest errors, poll gating
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<String> {
|
||||
["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<Option<String>> = 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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user