Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 16 additions & 21 deletions packages/web/src/components/BottomSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useEffect, useRef } from "react";
import { getAttentionLevel, isPRRateLimited, isPRUnenriched, type DashboardSession } from "@/lib/types";
import { getAttentionLevel, isPRUnenriched, type DashboardSession } from "@/lib/types";
import { getSessionTitle } from "@/lib/format";
import { projectSessionPath } from "@/lib/routes";

Expand All @@ -24,12 +24,10 @@ function formatTagLabel(value: string): string {
}

function isTag(
value:
| {
label: string;
tone: "accent" | "neutral" | "mono";
}
| null,
value: {
label: string;
tone: "accent" | "neutral" | "mono";
} | null,
): value is { label: string; tone: "accent" | "neutral" | "mono" } {
return value !== null;
}
Expand Down Expand Up @@ -76,7 +74,7 @@ export function BottomSheet({
if (!sheetRef.current) return;

const focusable = sheetRef.current.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
if (focusable.length === 0) return;

Expand Down Expand Up @@ -122,13 +120,13 @@ export function BottomSheet({

const title = getSessionTitle(session);
const attention = getAttentionLevel(session);
const summary =
session.summary && !session.summaryIsFallback ? session.summary : null;
const summary = session.summary && !session.summaryIsFallback ? session.summary : null;
const hasLiveTerminateAction =
attention !== "done" && attention !== "merge" && session.status !== "terminated";
const pr = session.pr;
const showLivePrData = Boolean(pr && !isPRRateLimited(pr) && !isPRUnenriched(pr));
const showTerminalStatePills = attention === "done" || session.status === "terminated" || session.activity === "exited";
const showLivePrData = Boolean(pr && !isPRUnenriched(pr));
const showTerminalStatePills =
attention === "done" || session.status === "terminated" || session.activity === "exited";
const tags = [
{ label: formatTagLabel(attention), tone: "accent" as const },
{ label: formatTagLabel(session.status), tone: "neutral" as const },
Expand All @@ -141,11 +139,7 @@ export function BottomSheet({
return (
<>
{/* Backdrop */}
<div
className="bottom-sheet-backdrop"
onClick={onCancel}
aria-hidden="true"
/>
<div className="bottom-sheet-backdrop" onClick={onCancel} aria-hidden="true" />

{/* Sheet */}
<div
Expand Down Expand Up @@ -191,7 +185,9 @@ export function BottomSheet({
<div className="bottom-sheet__preview-content">
<div className="bottom-sheet__preview-header">
<span className="bottom-sheet__preview-id">{session.id}</span>
<span className="bottom-sheet__preview-time">{getRelativeTime(session.lastActivityAt)}</span>
<span className="bottom-sheet__preview-time">
{getRelativeTime(session.lastActivityAt)}
</span>
</div>
<h2 id="bottom-sheet-title" className="bottom-sheet__title">
{title}
Expand All @@ -207,8 +203,7 @@ export function BottomSheet({
{pr ? <span className="bottom-sheet__preview-pr">#{pr.number}</span> : null}
{showLivePrData && pr ? (
<span className="bottom-sheet__preview-diff">
<span className="bottom-sheet__preview-diff-add">+{pr.additions}</span>
{" "}
<span className="bottom-sheet__preview-diff-add">+{pr.additions}</span>{" "}
<span className="bottom-sheet__preview-diff-del">-{pr.deletions}</span>
</span>
) : null}
Expand All @@ -228,7 +223,7 @@ export function BottomSheet({
? "approved"
: pr.reviewDecision === "changes_requested"
? "changes requested"
: "needs review"}
: "needs review"}
</span>
{showTerminalStatePills ? (
<span className="bottom-sheet__tag bottom-sheet__tag--accent">
Expand Down
52 changes: 6 additions & 46 deletions packages/web/src/components/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
type DashboardOrchestratorLink,
type DashboardAttentionZoneMode,
getAttentionLevel,
isPRRateLimited,
} from "@/lib/types";
import { AttentionZone } from "./AttentionZone";
import { DynamicFavicon, countNeedingAttention } from "./DynamicFavicon";
Expand Down Expand Up @@ -173,9 +172,11 @@ function DashboardInner({
return sessions.filter((s) => s.projectId === projectId);
}, [sessions, projectId]);
const connectionStatus: "connected" | "reconnecting" | "disconnected" =
mux?.status === "disconnected" ? "disconnected"
: mux?.status === "connected" ? "connected"
: "reconnecting";
mux?.status === "disconnected"
? "disconnected"
: mux?.status === "connected"
? "connected"
: "reconnecting";
const recoveredFromLoadError = Boolean(dashboardLoadError) && liveSessionsResolved;
const ssrLoadError = recoveredFromLoadError ? undefined : dashboardLoadError;
// Live WS error takes precedence; fall back to SSR load error when live data hasn't resolved it.
Expand All @@ -185,7 +186,6 @@ function DashboardInner({
const routerRef = useRef(router);
routerRef.current = router;
const activeSessionId = searchParams.get("session") ?? undefined;
const [rateLimitDismissed, setRateLimitDismissed] = useState(false);
const [activeOrchestrators, setActiveOrchestrators] =
useState<DashboardOrchestratorLink[]>(orchestratorLinks);
const [spawningProjectIds, setSpawningProjectIds] = useState<string[]>([]);
Expand Down Expand Up @@ -457,20 +457,14 @@ function DashboardInner({
<span className="font-semibold text-[var(--color-status-error)]">
Orchestrator failed to load
</span>
<span className="break-words text-[var(--color-text-secondary)]">
{visibleLoadError}
</span>
<span className="break-words text-[var(--color-text-secondary)]">{visibleLoadError}</span>
<span className="text-[var(--color-text-secondary)]">
Confirm <span className="font-mono text-[10px]">agent-orchestrator.yaml</span> exists and is
valid, then run <span className="font-mono text-[10px]">ao doctor</span> for diagnostics.
</span>
</div>
) : null;

const anyRateLimited = useMemo(
() => sessions.some((session) => session.pr && isPRRateLimited(session.pr)),
[sessions],
);
const normalizedProjectName = projectName?.trim().toLowerCase();
const headerProjectLabel =
normalizedProjectName === "agent orchestrator"
Expand Down Expand Up @@ -624,40 +618,6 @@ function DashboardInner({

<div className="dashboard-main__body">
{loadErrorBanner}
{anyRateLimited && !rateLimitDismissed && (
<div className="dashboard-alert mb-4 flex items-center gap-2.5 border border-[color-mix(in_srgb,var(--color-status-attention)_25%,transparent)] bg-[var(--color-tint-yellow)] 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"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 8v4M12 16h.01" />
</svg>
<span className="flex-1">
GitHub API rate limited — PR data (CI status, review state, sizes) may be
stale. Will retry automatically on next refresh.
</span>
<button
onClick={() => setRateLimitDismissed(true)}
className="ml-1 shrink-0 opacity-60 hover:opacity-100"
aria-label="Dismiss"
>
<svg
className="h-3.5 w-3.5"
fill="none"
stroke="currentColor"
strokeWidth="2"
viewBox="0 0 24 24"
>
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
</div>
)}

{allProjectsView && (
<ProjectOverviewGrid
overviews={projectOverviews}
Expand Down
78 changes: 41 additions & 37 deletions packages/web/src/components/PRStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { type DashboardPR, isPRRateLimited, isPRUnenriched } from "@/lib/types";
import { type DashboardPR, isPRUnenriched } from "@/lib/types";
import { CIBadge } from "./CIBadge";

export function getSizeLabel(additions: number, deletions: number): string {
Expand All @@ -14,7 +14,6 @@ interface PRStatusProps {

export function PRStatus({ pr }: PRStatusProps) {
const sizeLabel = getSizeLabel(pr.additions, pr.deletions);
const rateLimited = isPRRateLimited(pr);
const unenriched = isPRUnenriched(pr);

return (
Expand All @@ -30,14 +29,14 @@ export function PRStatus({ pr }: PRStatusProps) {
#{pr.number}
</a>

{/* Size — shimmer when unenriched, hide when rate limited */}
{!rateLimited && (unenriched ? (
{/* Size — shimmer when unenriched */}
{unenriched ? (
<span className="inline-block h-[14px] w-16 animate-pulse rounded-full bg-[var(--color-bg-subtle)]" />
) : (
<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" && (
Expand All @@ -54,14 +53,16 @@ export function PRStatus({ pr }: PRStatusProps) {
)}

{/* CI status — shimmer when unenriched */}
{pr.state === "open" && !pr.isDraft && !rateLimited && (unenriched ? (
<span className="inline-block h-[14px] w-14 animate-pulse rounded-full bg-[var(--color-bg-subtle)]" />
) : (
<CIBadge status={pr.ciStatus} checks={pr.ciChecks} />
))}
{pr.state === "open" &&
!pr.isDraft &&
(unenriched ? (
<span className="inline-block h-[14px] w-14 animate-pulse rounded-full bg-[var(--color-bg-subtle)]" />
) : (
<CIBadge status={pr.ciStatus} checks={pr.ciChecks} />
))}

{/* Review decision (only for open PRs with real data) */}
{pr.state === "open" && pr.reviewDecision === "approved" && !rateLimited && !unenriched && (
{pr.state === "open" && pr.reviewDecision === "approved" && !unenriched && (
<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>
Expand All @@ -77,23 +78,22 @@ interface PRTableRowProps {

export function PRTableRow({ pr, muted = false }: PRTableRowProps) {
const sizeLabel = getSizeLabel(pr.additions, pr.deletions);
const rateLimited = isPRRateLimited(pr);
const unenriched = isPRUnenriched(pr);
const hideData = rateLimited || unenriched;
const hideData = unenriched;

const reviewLabel = hideData
? "—"
: pr.state === "merged"
? "merged"
: pr.state === "closed"
? "closed"
: pr.isDraft
? "draft"
: pr.reviewDecision === "approved"
? "approved"
: pr.reviewDecision === "changes_requested"
? "changes requested"
: "needs review";
: pr.isDraft
? "draft"
: pr.reviewDecision === "approved"
? "approved"
: pr.reviewDecision === "changes_requested"
? "changes requested"
: "needs review";

const reviewClass = hideData
? "text-[var(--color-text-tertiary)]"
Expand All @@ -105,10 +105,14 @@ export function PRTableRow({ pr, muted = false }: PRTableRowProps) {
? "text-[var(--color-accent-red)]"
: "text-[var(--color-accent-yellow)]";

const shimmer = <span className="inline-block h-3 w-12 animate-pulse rounded bg-[var(--color-bg-subtle)]" />;
const shimmer = (
<span className="inline-block h-3 w-12 animate-pulse rounded bg-[var(--color-bg-subtle)]" />
);

return (
<tr className={`border-b border-[var(--color-border-subtle)] transition-colors hover:bg-[var(--color-bg-subtle)]${muted ? " opacity-60 hover:opacity-100" : ""}`}>
<tr
className={`border-b border-[var(--color-border-subtle)] transition-colors hover:bg-[var(--color-bg-subtle)]${muted ? " opacity-60 hover:opacity-100" : ""}`}
>
<td className="px-3 py-2.5 text-sm">
<a href={pr.url} target="_blank" rel="noopener noreferrer" className="hover:underline">
#{pr.number}
Expand All @@ -120,8 +124,8 @@ export function PRTableRow({ pr, muted = false }: PRTableRowProps) {
</a>
</td>
<td className="px-3 py-2.5 text-sm">
{unenriched ? shimmer : rateLimited ? (
<span className="text-[var(--color-text-tertiary)]">—</span>
{unenriched ? (
shimmer
) : (
<>
<span className="text-[var(--color-accent-green)]">+{pr.additions}</span>{" "}
Expand All @@ -131,11 +135,7 @@ export function PRTableRow({ pr, muted = false }: PRTableRowProps) {
)}
</td>
<td className="px-3 py-2.5">
{unenriched ? shimmer : rateLimited ? (
<span className="text-[var(--color-text-tertiary)]">—</span>
) : (
<CIBadge status={pr.ciStatus} checks={pr.ciChecks} compact />
)}
{unenriched ? shimmer : <CIBadge status={pr.ciStatus} checks={pr.ciChecks} compact />}
</td>
<td className={`px-3 py-2.5 text-xs font-semibold ${reviewClass}`}>
{unenriched ? shimmer : reviewLabel}
Expand Down Expand Up @@ -168,9 +168,8 @@ function getReviewColor(pr: DashboardPR): string {
}

export function PRCard({ pr, muted = false }: PRTableRowProps) {
const rateLimited = isPRRateLimited(pr);
const unenriched = isPRUnenriched(pr);
const hideData = rateLimited || unenriched;
const hideData = unenriched;

const ciLabel = hideData
? "—"
Expand All @@ -194,7 +193,9 @@ export function PRCard({ pr, muted = false }: PRTableRowProps) {
? "changes"
: "needs review";

const shimmer = <span className="inline-block h-3 w-10 animate-pulse rounded bg-[var(--color-bg-subtle)]" />;
const shimmer = (
<span className="inline-block h-3 w-10 animate-pulse rounded bg-[var(--color-bg-subtle)]" />
);
const diffLabel = hideData ? null : `+${pr.additions} -${pr.deletions}`;
const lineTone =
pr.state === "merged"
Expand All @@ -219,7 +220,9 @@ export function PRCard({ pr, muted = false }: PRTableRowProps) {
<span className="mobile-pr-card__title">{pr.title}</span>
</div>
<div className={`mobile-pr-card__meta ${lineTone}`}>
{unenriched ? shimmer : (
{unenriched ? (
shimmer
) : (
<span className="mobile-pr-card__metric-value">
<span
className="mobile-pr-card__ci-dot"
Expand All @@ -228,12 +231,13 @@ export function PRCard({ pr, muted = false }: PRTableRowProps) {
<span style={{ color: hideData ? undefined : getCiTextColor(pr) }}>{ciLabel}</span>
</span>
)}
<span className="mobile-pr-card__review" style={{ color: hideData ? undefined : getReviewColor(pr) }}>
<span
className="mobile-pr-card__review"
style={{ color: hideData ? undefined : getReviewColor(pr) }}
>
{unenriched ? shimmer : reviewLabel}
</span>
<span className="mobile-pr-card__diff">
{hideData ? shimmer : diffLabel}
</span>
<span className="mobile-pr-card__diff">{hideData ? shimmer : diffLabel}</span>
</div>
</a>
);
Expand Down
Loading
Loading