diff --git a/packages/ui/src/features/sidebar/components/items/TaskIcon.tsx b/packages/ui/src/features/sidebar/components/items/TaskIcon.tsx index fbe1adcfb5..5d586864ab 100644 --- a/packages/ui/src/features/sidebar/components/items/TaskIcon.tsx +++ b/packages/ui/src/features/sidebar/components/items/TaskIcon.tsx @@ -184,61 +184,90 @@ function CloudStatusIcon({ ); } +type PrStateMeta = { Icon: typeof GitMerge; color: string; label: string }; +const PR_STATE_META: Record, PrStateMeta> = { + merged: { Icon: GitMerge, color: "var(--purple-11)", label: "PR merged" }, + open: { Icon: GitPullRequest, color: "var(--green-11)", label: "PR open" }, + draft: { Icon: GitPullRequest, color: "var(--gray-9)", label: "Draft PR" }, + closed: { Icon: GitPullRequest, color: "var(--red-11)", label: "PR closed" }, +}; +const DIFF_META: PrStateMeta = { + Icon: GitBranch, + color: "var(--amber-11)", + label: "Has changes", +}; + function PrStatusIcon({ prState, hasDiff, size, + provenanceBadge, + threadUrl, }: { prState?: SidebarPrState; hasDiff?: boolean; size: number; + /** When set (cloud tasks), a small provenance glyph (cloud or origin + * product) is stacked on the icon's bottom-right corner so "where this ran" + * stays visible alongside the PR state. */ + provenanceBadge?: OriginProductMeta; + /** Originating thread URL; keeps the badge state clickable like + * `CloudStatusIcon` does for origin-branded tasks. */ + threadUrl?: string; }) { - if (prState === "merged") { - return ( - - - - - - ); - } - if (prState === "open") { - return ( - - - - - - ); - } - if (prState === "draft") { - return ( - - - - - - ); - } - if (prState === "closed") { - return ( - - - - - - ); - } - if (hasDiff) { + const meta = prState ? PR_STATE_META[prState] : hasDiff ? DIFF_META : null; + if (!meta) return null; + + if (!provenanceBadge) { return ( - + - + ); } - return null; + + // Stack the provenance glyph over the PR icon's bottom-right corner. A + // radial-gradient mask punches a hole in the PR glyph under the badge so + // both shapes stay legible on any row background (hover, selected, command + // palette highlight) without hardcoding a cutout ring color. + const badgeSize = Math.round(size * 0.66); + const overflow = 2; + const holeCenter = size + overflow - badgeSize / 2; + const holeRadius = badgeSize / 2 + 1; + const mask = `radial-gradient(circle at ${holeCenter}px ${holeCenter}px, transparent ${holeRadius}px, black ${holeRadius + 0.5}px)`; + const icon = ( + + + + + ); + return ( + + {renderIconSpan({ + icon, + link: threadUrl, + ariaLabel: threadUrl + ? `Open ${provenanceBadge.label} thread` + : undefined, + })} + + ); } export interface TaskIconProps { @@ -280,6 +309,8 @@ export function TaskIcon({ }: TaskIconProps) { const isCloudTask = workspaceMode === "cloud"; const isTerminalCloud = isCloudTask && isTerminalStatus(taskRunStatus); + const cloudRunFailed = + taskRunStatus === "failed" || taskRunStatus === "cancelled"; const originProductMeta = getOriginProductMeta(originProduct); if (needsPermission) { @@ -308,7 +339,12 @@ export function TaskIcon({ ); } - if (isTerminalCloud) { + // A failed/cancelled cloud run keeps the red cloud icon — the failure is + // the actionable signal there. A cloud run that finished cleanly and has a + // PR falls through to the PR-state icon (with a provenance badge), because + // once the run is done the branch's lifecycle is the state worth scanning + // for — the same one local runs show. + if (isTerminalCloud && (cloudRunFailed || !prState)) { return ( ; + return ( + + ); } if (isPinned) { return ;