feat(web): graceful GitHub API rate limit handling in UI
When the GitHub plugin hits rate limits, the dashboard now: - Shows a single amber banner: "GitHub API rate limited — PR data (CI status, review state, sizes) may be stale. Will retry on next refresh." - Hides CI badge, review decision, and size pill on PR cards (they'd show wrong values: +0 -0 XS, CI failing) - Shows a subtle "⚠ PR data rate limited" note on affected cards instead of misleading alert badges - Skips CI/review/conflict-based attention zone classification for rate-limited PRs (prevents sessions moving to Review due to phantom "CI failing" from the fallback value) - Doesn't cache partial rate-limited data so next refresh retries live API calls as soon as the rate limit window resets What still works when rate limited: - Session ID, title, branch, PR number/link - Session activity status (working/spawning/etc.) - Merge button if mergeability was already cached - Restore/terminate/send actions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8249679dac
commit
1ad71940c9
|
|
@ -7,6 +7,7 @@ import {
|
|||
type DashboardPR,
|
||||
type AttentionLevel,
|
||||
getAttentionLevel,
|
||||
isPRRateLimited,
|
||||
} from "@/lib/types";
|
||||
import { CI_STATUS } from "@composio/ao-core/types";
|
||||
import { AttentionZone } from "./AttentionZone";
|
||||
|
|
@ -85,6 +86,11 @@ export function Dashboard({ sessions, stats, orchestratorId, projectName }: Dash
|
|||
|
||||
const hasKanbanSessions = KANBAN_LEVELS.some((l) => grouped[l].length > 0);
|
||||
|
||||
const anyRateLimited = useMemo(
|
||||
() => sessions.some((s) => s.pr && isPRRateLimited(s.pr)),
|
||||
[sessions],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="px-8 py-7">
|
||||
<DynamicFavicon sessions={sessions} projectName={projectName} />
|
||||
|
|
@ -109,6 +115,19 @@ export function Dashboard({ sessions, stats, orchestratorId, projectName }: Dash
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Rate limit notice */}
|
||||
{anyRateLimited && (
|
||||
<div className="mb-6 flex items-center gap-2.5 rounded border border-[rgba(245,158,11,0.25)] bg-[rgba(245,158,11,0.05)] px-3.5 py-2.5 text-[11px] text-[var(--color-status-attention)]">
|
||||
<svg className="h-3.5 w-3.5 shrink-0" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
||||
</svg>
|
||||
<span>
|
||||
GitHub API rate limited — PR data (CI status, review state, sizes) may be stale.
|
||||
{" "}Will retry automatically on next refresh.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Kanban columns for active zones */}
|
||||
{hasKanbanSessions && (
|
||||
<div className="mb-8 flex gap-4 overflow-x-auto pb-2">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import type { DashboardPR } from "@/lib/types";
|
||||
import { isPRRateLimited } from "@/lib/types";
|
||||
import { CIBadge } from "./CIBadge";
|
||||
|
||||
function getSizeLabel(additions: number, deletions: number): string {
|
||||
|
|
@ -14,6 +15,7 @@ interface PRStatusProps {
|
|||
|
||||
export function PRStatus({ pr }: PRStatusProps) {
|
||||
const sizeLabel = getSizeLabel(pr.additions, pr.deletions);
|
||||
const rateLimited = isPRRateLimited(pr);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
|
|
@ -28,10 +30,12 @@ export function PRStatus({ pr }: PRStatusProps) {
|
|||
#{pr.number}
|
||||
</a>
|
||||
|
||||
{/* Size */}
|
||||
<span className="inline-flex items-center rounded-full bg-[rgba(125,133,144,0.08)] px-2 py-0.5 text-[10px] font-semibold text-[var(--color-text-muted)]">
|
||||
+{pr.additions} -{pr.deletions} {sizeLabel}
|
||||
</span>
|
||||
{/* Size — hide when rate limited (would show +0 -0 XS) */}
|
||||
{!rateLimited && (
|
||||
<span className="inline-flex items-center rounded-full bg-[rgba(125,133,144,0.08)] px-2 py-0.5 text-[10px] font-semibold text-[var(--color-text-muted)]">
|
||||
+{pr.additions} -{pr.deletions} {sizeLabel}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Merged badge */}
|
||||
{pr.state === "merged" && (
|
||||
|
|
@ -47,11 +51,13 @@ export function PRStatus({ pr }: PRStatusProps) {
|
|||
</span>
|
||||
)}
|
||||
|
||||
{/* CI status (only for open PRs) */}
|
||||
{pr.state === "open" && !pr.isDraft && <CIBadge status={pr.ciStatus} checks={pr.ciChecks} />}
|
||||
{/* CI status — only when we have real data */}
|
||||
{pr.state === "open" && !pr.isDraft && !rateLimited && (
|
||||
<CIBadge status={pr.ciStatus} checks={pr.ciChecks} />
|
||||
)}
|
||||
|
||||
{/* Review decision (only for open PRs) */}
|
||||
{pr.state === "open" && pr.reviewDecision === "approved" && (
|
||||
{/* Review decision (only for open PRs with real data) */}
|
||||
{pr.state === "open" && pr.reviewDecision === "approved" && !rateLimited && (
|
||||
<span className="inline-flex items-center rounded-full bg-[rgba(63,185,80,0.1)] px-2 py-0.5 text-[10px] font-semibold text-[var(--color-accent-green)]">
|
||||
approved
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
type DashboardSession,
|
||||
type AttentionLevel,
|
||||
getAttentionLevel,
|
||||
isPRRateLimited,
|
||||
TERMINAL_STATUSES,
|
||||
TERMINAL_ACTIVITIES,
|
||||
} from "@/lib/types";
|
||||
|
|
@ -72,6 +73,7 @@ export function SessionCard({ session, onSend, onKill, onMerge, onRestore }: Ses
|
|||
timerRef.current = setTimeout(() => setSendingAction(null), 2000);
|
||||
};
|
||||
|
||||
const rateLimited = pr ? isPRRateLimited(pr) : false;
|
||||
const alerts = getAlerts(session);
|
||||
const isReadyToMerge = pr?.mergeability.mergeable && pr.state === "open";
|
||||
const isTerminal =
|
||||
|
|
@ -144,8 +146,20 @@ export function SessionCard({ session, onSend, onKill, onMerge, onRestore }: Ses
|
|||
{pr && <PRStatus pr={pr} />}
|
||||
</div>
|
||||
|
||||
{/* Rate limited indicator */}
|
||||
{rateLimited && pr?.state === "open" && (
|
||||
<div className="px-4 pb-3">
|
||||
<span className="inline-flex items-center gap-1 text-[10px] text-[var(--color-text-muted)]">
|
||||
<svg className="h-3 w-3 text-[var(--color-status-attention)]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
||||
</svg>
|
||||
PR data rate limited
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Merge button or alert tags */}
|
||||
{(alerts.length > 0 || isReadyToMerge) && (
|
||||
{!rateLimited && (alerts.length > 0 || isReadyToMerge) && (
|
||||
<div className="px-4 pb-3.5 pt-0.5">
|
||||
{isReadyToMerge && pr ? (
|
||||
<button
|
||||
|
|
@ -304,6 +318,7 @@ interface Alert {
|
|||
function getAlerts(session: DashboardSession): Alert[] {
|
||||
const pr = session.pr;
|
||||
if (!pr || pr.state !== "open") return [];
|
||||
if (isPRRateLimited(pr)) return [];
|
||||
|
||||
const alerts: Alert[] = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -149,6 +149,15 @@ export interface SSEActivityEvent {
|
|||
timestamp: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when this PR's enrichment data couldn't be fetched due to
|
||||
* API rate limiting. When true, CI status / review decision / mergeability
|
||||
* may be stale defaults — don't make decisions based on them.
|
||||
*/
|
||||
export function isPRRateLimited(pr: DashboardPR): boolean {
|
||||
return pr.mergeability.blockers.includes("API rate limited or unavailable");
|
||||
}
|
||||
|
||||
/** Determines which attention zone a session belongs to */
|
||||
export function getAttentionLevel(session: DashboardSession): AttentionLevel {
|
||||
// ── Done: terminal states ─────────────────────────────────────────
|
||||
|
|
@ -200,7 +209,7 @@ export function getAttentionLevel(session: DashboardSession): AttentionLevel {
|
|||
if (session.status === "ci_failed" || session.status === "changes_requested") {
|
||||
return "review";
|
||||
}
|
||||
if (session.pr) {
|
||||
if (session.pr && !isPRRateLimited(session.pr)) {
|
||||
const pr = session.pr;
|
||||
if (pr.ciStatus === CI_STATUS.FAILING) return "review";
|
||||
if (pr.reviewDecision === "changes_requested") return "review";
|
||||
|
|
@ -211,7 +220,7 @@ export function getAttentionLevel(session: DashboardSession): AttentionLevel {
|
|||
if (session.status === "review_pending") {
|
||||
return "pending";
|
||||
}
|
||||
if (session.pr) {
|
||||
if (session.pr && !isPRRateLimited(session.pr)) {
|
||||
const pr = session.pr;
|
||||
if (!pr.isDraft && pr.unresolvedThreads > 0) return "pending";
|
||||
if (!pr.isDraft && (pr.reviewDecision === "pending" || pr.reviewDecision === "none")) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue