From ee71c3d33f4060e997d051f59a63433d4f0bbb02 Mon Sep 17 00:00:00 2001 From: brennanb2025 Date: Thu, 25 Jun 2026 11:11:38 -0700 Subject: [PATCH] Use branch PR cache for sidebar status Co-authored-by: Orca --- .../sidebar/WorktreeCard.pr-display.test.tsx | 54 ++++++++++++++++++- .../src/components/sidebar/WorktreeCard.tsx | 25 ++++++++- .../sidebar/WorktreeCardStatusSlot.tsx | 18 +++---- 3 files changed, 85 insertions(+), 12 deletions(-) diff --git a/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx index dbf90b0faa2..f3bc4768bc2 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx @@ -2,7 +2,13 @@ import { renderToStaticMarkup } from 'react-dom/server' import type { ReactNode } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { HostedReviewInfo } from '../../../../shared/hosted-review' -import type { GlobalSettings, Repo, Worktree, WorktreeCardProperty } from '../../../../shared/types' +import type { + GlobalSettings, + PRInfo, + Repo, + Worktree, + WorktreeCardProperty +} from '../../../../shared/types' import { COMPACT_WORKTREE_CARD_PROPERTIES } from '../../../../shared/worktree-card-properties' import type { WorkspacePortScanResult } from '../../../../shared/workspace-ports' @@ -14,6 +20,7 @@ const updateWorktreeMeta = vi.fn() let worktreeCardProperties: WorktreeCardProperty[] = ['status'] let hostedReviewCache: Record = {} +let prCache: Record = {} let workspacePortScan: WorkspacePortScanResult | null = null let settings: Partial | null = null @@ -29,6 +36,7 @@ vi.mock('@/store', () => ({ issueCache: {}, linearIssueCache: {}, openModal, + prCache, projectGroups: [], remoteBranchConflictByWorktreeId: {}, settings, @@ -121,6 +129,19 @@ function makeHostedReview(overrides: Partial = {}): HostedRevi } } +function makePRInfo(overrides: Partial = {}): PRInfo { + return { + number: 456, + title: 'Fix stale GH PR', + state: 'open', + url: 'https://github.com/acme/orca/pull/456', + checksStatus: 'success', + updatedAt: '2026-05-17T00:00:00.000Z', + mergeable: 'MERGEABLE', + ...overrides + } +} + function renderWorktreeCardMarkup(element: ReactNode): string { return renderToStaticMarkup(<>{element}) } @@ -140,6 +161,7 @@ describe('WorktreeCard linked PR display', () => { vi.clearAllMocks() worktreeCardProperties = ['status'] hostedReviewCache = {} + prCache = {} workspacePortScan = null settings = null }) @@ -616,4 +638,34 @@ describe('WorktreeCard linked PR display', () => { expect(markup).not.toContain('Linked PR #456') expect(markup).not.toContain('CI checks') }) + + it('uses branch PR cache for the status slot before hosted-review metadata warms', async () => { + settings = { experimentalNewWorktreeCardStyle: true } + worktreeCardProperties = ['status'] + prCache = { + 'repo-1::feature/local-branch': { + data: makePRInfo({ + number: 6340, + title: 'Remove split terminal from onboarding checklist', + state: 'merged', + checksStatus: 'success' + }), + fetchedAt: Date.now() + } + } + const { default: WorktreeCard } = await import('./WorktreeCard') + + const markup = renderWorktreeCardMarkup( + + ) + + expect(markup).toContain('PR: Merged') + expect(markup).toContain('text-purple-600/70') + expect(markup).not.toContain('Branch') + expect(markup).not.toContain('lucide-git-branch') + }) }) diff --git a/src/renderer/src/components/sidebar/WorktreeCard.tsx b/src/renderer/src/components/sidebar/WorktreeCard.tsx index 07a0b887fb2..e8f54e0b268 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useCallback, useState } from 'react' import { useAppStore } from '@/store' import { getHostedReviewCacheKey } from '@/store/slices/hosted-review' import { issueCacheKey as getIssueCacheKey } from '@/store/slices/github' +import { getGitHubPRCacheKey } from '@/store/slices/github-cache-key' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip' @@ -29,6 +30,7 @@ import { cn } from '@/lib/utils' import { activateWorktreeFromSidebar } from '@/lib/sidebar-worktree-activation' import { isFolderRepo } from '../../../../shared/repo-kind' import type { HostedReviewInfo } from '../../../../shared/hosted-review' +import { hostedReviewInfoFromGitHubPRInfo } from '../../../../shared/hosted-review-github' import type { GitHubWorkItem, Worktree, @@ -398,6 +400,17 @@ const WorktreeCard = React.memo(function WorktreeCard({ repo.executionHostId ) : '' + const prCacheKey = + repo && branch + ? getGitHubPRCacheKey( + repo.path, + repo.id, + branch, + settings, + repo.connectionId, + repo.executionHostId + ) + : '' const issueCacheKey = repo && worktree.linkedIssue ? getIssueCacheKey( @@ -417,6 +430,7 @@ const WorktreeCard = React.memo(function WorktreeCard({ const hostedReviewEntry = useAppStore((s) => hostedReviewCacheKey ? s.hostedReviewCache[hostedReviewCacheKey] : undefined ) + const prCacheEntry = useAppStore((s) => (prCacheKey ? s.prCache[prCacheKey] : undefined)) const issueEntry = useAppStore((s) => (issueCacheKey ? s.issueCache[issueCacheKey] : undefined)) const linearIssueEntry = useAppStore((s) => linearIssueCacheKey ? s.linearIssueCache[linearIssueCacheKey] : undefined @@ -427,19 +441,26 @@ const WorktreeCard = React.memo(function WorktreeCard({ const hostedReview: HostedReviewInfo | null | undefined = hostedReviewEntry !== undefined ? hostedReviewEntry.data : undefined + // Why: ChecksPanel can discover a branch PR before hosted-review metadata + // warms. The sidebar status slot should not fall back to "branch" while + // the branch PR cache already knows the review state. + const cachedBranchReview = + hostedReview === undefined && prCacheEntry?.data + ? hostedReviewInfoFromGitHubPRInfo(prCacheEntry.data) + : hostedReview const linkedGitLabMR = worktree.linkedGitLabMR ?? null const linkedBitbucketPR = worktree.linkedBitbucketPR ?? null const linkedAzureDevOpsPR = worktree.linkedAzureDevOpsPR ?? null const linkedGiteaPR = worktree.linkedGiteaPR ?? null const prDisplay = getWorktreeCardPrDisplay( - hostedReview, + cachedBranchReview, worktree.linkedPR, linkedGitLabMR, linkedBitbucketPR, linkedAzureDevOpsPR, linkedGiteaPR, { - reviewHintKey: hostedReviewEntry?.linkedReviewHintKey + reviewHintKey: hostedReviewEntry?.linkedReviewHintKey ?? (prCacheEntry?.data ? '' : undefined) } ) const issue: IssueInfo | null | undefined = worktree.linkedIssue diff --git a/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.tsx b/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.tsx index eeff2b9c5f8..7dfec9d0899 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardStatusSlot.tsx @@ -65,15 +65,6 @@ function overlayNewCardUnreadStatus( function getReviewStatusTooltip(review: WorktreeCardPrDisplay): string { const label = getReviewLabel(review) - if (review.status === 'failure') { - return `${label} checks: Failed` - } - if (review.status === 'pending') { - return `${label} checks: Pending` - } - if (review.status === 'success') { - return `${label} checks: Passing` - } if (review.state === 'merged') { return `${label}: Merged` } @@ -83,6 +74,15 @@ function getReviewStatusTooltip(review: WorktreeCardPrDisplay): string { if (review.state === 'draft') { return `${label}: Draft` } + if (review.status === 'failure') { + return `${label} checks: Failed` + } + if (review.status === 'pending') { + return `${label} checks: Pending` + } + if (review.status === 'success') { + return `${label} checks: Passing` + } return `${label}: Open` }