feat: auto-fetch PR data on worktree change and add refresh to empty state

Add useEffect to fetch PR data when the active worktree/branch changes,
and add a refresh button with loading spinner to the "no PR found" empty
state. Extract check helpers to separate file to stay within line limit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jinjing
2026-03-27 10:35:28 -07:00
co-authored by Claude Opus 4.6
parent dd4eb42f5d
commit 8ac5c66fab
2 changed files with 71 additions and 45 deletions
@@ -4,7 +4,6 @@ import {
CircleX,
LoaderCircle,
CircleDashed,
CircleMinus,
ExternalLink,
RefreshCw,
Check,
@@ -14,63 +13,22 @@ import {
import { useAppStore } from '@/store'
import { cn } from '@/lib/utils'
import PRActions from './PRActions'
import { PullRequestIcon, CHECK_ICON, CHECK_COLOR, prStateColor } from './checks-helpers'
import type { PRInfo, PRCheckDetail } from '../../../../shared/types'
function PullRequestIcon({ className }: { className?: string }): React.JSX.Element {
return (
<svg viewBox="0 0 16 16" aria-hidden fill="currentColor" className={className}>
<path
fillRule="evenodd"
d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.25 2.25 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1.5 1.5 0 011.5 1.5v5.628a2.25 2.25 0 101.5 0V5.5A3 3 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"
/>
</svg>
)
}
const CHECK_ICON: Record<string, React.ComponentType<{ className?: string }>> = {
success: CircleCheck,
failure: CircleX,
pending: LoaderCircle,
neutral: CircleDashed,
skipped: CircleMinus,
cancelled: CircleX,
timed_out: CircleX
}
const CHECK_COLOR: Record<string, string> = {
success: 'text-emerald-500',
failure: 'text-rose-500',
pending: 'text-amber-500',
neutral: 'text-muted-foreground',
skipped: 'text-muted-foreground/60',
cancelled: 'text-muted-foreground/60',
timed_out: 'text-rose-500'
}
function prStateColor(state: PRInfo['state']): string {
switch (state) {
case 'merged':
return 'bg-purple-500/15 text-purple-500 border-purple-500/20'
case 'open':
return 'bg-emerald-500/15 text-emerald-500 border-emerald-500/20'
case 'closed':
return 'bg-muted text-muted-foreground border-border'
case 'draft':
return 'bg-muted text-muted-foreground/70 border-border'
}
}
export default function ChecksPanel(): React.JSX.Element {
const activeWorktreeId = useAppStore((s) => s.activeWorktreeId)
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
const repos = useAppStore((s) => s.repos)
const prCache = useAppStore((s) => s.prCache)
const fetchPRForBranch = useAppStore((s) => s.fetchPRForBranch)
const refreshGitHubForWorktree = useAppStore((s) => s.refreshGitHubForWorktree)
const fetchPRChecks = useAppStore((s) => s.fetchPRChecks)
const [checks, setChecks] = useState<PRCheckDetail[]>([])
const [checksLoading, setChecksLoading] = useState(false)
const [emptyRefreshing, setEmptyRefreshing] = useState(false)
const [editingTitle, setEditingTitle] = useState(false)
const [titleDraft, setTitleDraft] = useState('')
const [titleSaving, setTitleSaving] = useState(false)
@@ -99,6 +57,13 @@ export default function ChecksPanel(): React.JSX.Element {
const pr: PRInfo | null = prCacheKey ? (prCache[prCacheKey]?.data ?? null) : null
const prNumber = pr?.number ?? null
// Fetch PR data when the active worktree/branch changes
useEffect(() => {
if (repo && branch) {
void fetchPRForBranch(repo.path, branch)
}
}, [repo, branch, fetchPRForBranch])
// Fetch checks via cached store method
const fetchChecks = useCallback(async () => {
if (!repo || !prNumber) {
@@ -247,6 +212,20 @@ export default function ChecksPanel(): React.JSX.Element {
<div className="text-[10px] text-muted-foreground/60">
Push your branch and open a PR to see checks here
</div>
<button
className="p-1.5 rounded-md hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
title="Refresh"
disabled={emptyRefreshing}
onClick={() => {
if (activeWorktreeId) {
setEmptyRefreshing(true)
refreshGitHubForWorktree(activeWorktreeId)
setTimeout(() => setEmptyRefreshing(false), 2000)
}
}}
>
<RefreshCw className={cn('size-4', emptyRefreshing && 'animate-spin')} />
</button>
</div>
)
}
@@ -0,0 +1,47 @@
import React from 'react'
import { CircleCheck, CircleX, LoaderCircle, CircleDashed, CircleMinus } from 'lucide-react'
import type { PRInfo } from '../../../../shared/types'
export function PullRequestIcon({ className }: { className?: string }): React.JSX.Element {
return (
<svg viewBox="0 0 16 16" aria-hidden fill="currentColor" className={className}>
<path
fillRule="evenodd"
d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.25 2.25 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1.5 1.5 0 011.5 1.5v5.628a2.25 2.25 0 101.5 0V5.5A3 3 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"
/>
</svg>
)
}
export const CHECK_ICON: Record<string, React.ComponentType<{ className?: string }>> = {
success: CircleCheck,
failure: CircleX,
pending: LoaderCircle,
neutral: CircleDashed,
skipped: CircleMinus,
cancelled: CircleX,
timed_out: CircleX
}
export const CHECK_COLOR: Record<string, string> = {
success: 'text-emerald-500',
failure: 'text-rose-500',
pending: 'text-amber-500',
neutral: 'text-muted-foreground',
skipped: 'text-muted-foreground/60',
cancelled: 'text-muted-foreground/60',
timed_out: 'text-rose-500'
}
export function prStateColor(state: PRInfo['state']): string {
switch (state) {
case 'merged':
return 'bg-purple-500/15 text-purple-500 border-purple-500/20'
case 'open':
return 'bg-emerald-500/15 text-emerald-500 border-emerald-500/20'
case 'closed':
return 'bg-muted text-muted-foreground border-border'
case 'draft':
return 'bg-muted text-muted-foreground/70 border-border'
}
}