Skip to content

Commit 0ade297

Browse files
authored
Add PR status overlay on git status icon (#180)
* Add PR status overlay to bottom-right of sidebar task icon * Reduce cursor dot size in agent user interface * Fix circle border to match task bg for active/inactive state * Shrink status dot and hide on merged prs; show only for open prs * Prioritize failed over in progress in PRCheckStatus * Make the dot blue when the check is natural --------- Co-authored-by: Chris Tate <[email protected]>
1 parent c5500dd commit 0ade297

File tree

3 files changed

+118
-2
lines changed

3 files changed

+118
-2
lines changed

components/pr-check-status.tsx

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
'use client'
2+
3+
import { useEffect, useState } from 'react'
4+
import { Check, Loader2, X } from 'lucide-react'
5+
6+
interface CheckRun {
7+
id: number
8+
name: string
9+
status: string
10+
conclusion: string | null
11+
html_url: string
12+
started_at: string | null
13+
completed_at: string | null
14+
}
15+
16+
interface PRCheckStatusProps {
17+
taskId: string
18+
prStatus: 'open' | 'closed' | 'merged'
19+
isActive?: boolean
20+
className?: string
21+
}
22+
23+
export function PRCheckStatus({ taskId, prStatus, isActive = false, className = '' }: PRCheckStatusProps) {
24+
const [checkRuns, setCheckRuns] = useState<CheckRun[]>([])
25+
const [isLoading, setIsLoading] = useState(true)
26+
27+
useEffect(() => {
28+
const fetchCheckRuns = async () => {
29+
try {
30+
const response = await fetch(`/api/tasks/${taskId}/check-runs`)
31+
if (response.ok) {
32+
const data = await response.json()
33+
if (data.success && data.checkRuns) {
34+
setCheckRuns(data.checkRuns)
35+
}
36+
}
37+
} catch (error) {
38+
console.error('Error fetching check runs:', error)
39+
} finally {
40+
setIsLoading(false)
41+
}
42+
}
43+
44+
fetchCheckRuns()
45+
// Refresh every 30 seconds for in-progress checks
46+
const interval = setInterval(fetchCheckRuns, 30000)
47+
return () => clearInterval(interval)
48+
}, [taskId])
49+
50+
// Only show indicator for open PRs
51+
if (prStatus !== 'open') {
52+
return null
53+
}
54+
55+
// Don't render anything if loading or no check runs
56+
if (isLoading || checkRuns.length === 0) {
57+
return null
58+
}
59+
60+
// Determine overall status
61+
const hasInProgress = checkRuns.some((run) => run.status === 'in_progress' || run.status === 'queued')
62+
const hasFailed = checkRuns.some((run) => run.conclusion === 'failure' || run.conclusion === 'cancelled')
63+
const hasNeutral = checkRuns.some((run) => run.conclusion === 'neutral')
64+
const allPassed = checkRuns.every((run) => run.status === 'completed' && run.conclusion === 'success')
65+
66+
// Determine background color based on active state
67+
const bgColor = isActive ? 'bg-accent' : 'bg-card'
68+
69+
// Render the appropriate indicator
70+
// Note: Check failed first to ensure failures are always visible, even if other checks are in progress
71+
if (hasFailed) {
72+
return (
73+
<div className={`absolute -bottom-0.5 -right-0.5 ${bgColor} rounded-full p-0.5 ${className}`}>
74+
<div className="w-1 h-1 rounded-full bg-red-500" />
75+
</div>
76+
)
77+
}
78+
79+
if (hasInProgress) {
80+
return (
81+
<div className={`absolute -bottom-0.5 -right-0.5 ${bgColor} rounded-full p-0.5 ${className}`}>
82+
<div className="w-1 h-1 rounded-full bg-yellow-500 animate-pulse" />
83+
</div>
84+
)
85+
}
86+
87+
if (hasNeutral) {
88+
return (
89+
<div className={`absolute -bottom-0.5 -right-0.5 ${bgColor} rounded-full p-0.5 ${className}`}>
90+
<div className="w-1 h-1 rounded-full bg-blue-500" />
91+
</div>
92+
)
93+
}
94+
95+
if (allPassed) {
96+
return (
97+
<div className={`absolute -bottom-0.5 -right-0.5 ${bgColor} rounded-full p-0.5 ${className}`}>
98+
<Check className="w-1.5 h-1.5 text-green-500" strokeWidth={3} />
99+
</div>
100+
)
101+
}
102+
103+
return null
104+
}

components/task-sidebar.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { useTasks } from '@/components/app-layout'
2525
import { useAtomValue } from 'jotai'
2626
import { sessionAtom } from '@/lib/atoms/session'
2727
import { PRStatusIcon } from '@/components/pr-status-icon'
28+
import { PRCheckStatus } from '@/components/pr-check-status'
2829

2930
// Model mappings for human-friendly names
3031
const AGENT_MODELS = {
@@ -397,7 +398,12 @@ export function TaskSidebar({ tasks, onTaskSelect, width = 288 }: TaskSidebarPro
397398
</div>
398399
{task.repoUrl && (
399400
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-0.5">
400-
{task.prStatus && <PRStatusIcon status={task.prStatus} />}
401+
{task.prStatus && (
402+
<div className="relative">
403+
<PRStatusIcon status={task.prStatus} />
404+
<PRCheckStatus taskId={task.id} prStatus={task.prStatus} isActive={isActive} />
405+
</div>
406+
)}
401407
<span className="truncate">
402408
{(() => {
403409
try {

components/tasks-list-client.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type { Session } from '@/lib/session/types'
2828
import { VERCEL_DEPLOY_URL } from '@/lib/constants'
2929
import { Claude, Codex, Copilot, Cursor, Gemini, OpenCode } from '@/components/logos'
3030
import { PRStatusIcon } from '@/components/pr-status-icon'
31+
import { PRCheckStatus } from '@/components/pr-check-status'
3132

3233
interface TasksListClientProps {
3334
user: Session['user'] | null
@@ -420,7 +421,12 @@ export function TasksListClient({ user, authProvider, initialStars = 1056 }: Tas
420421
</div>
421422
{task.repoUrl && (
422423
<div className="flex items-center gap-2 text-xs text-muted-foreground mb-1">
423-
{task.prStatus && <PRStatusIcon status={task.prStatus} />}
424+
{task.prStatus && (
425+
<div className="relative">
426+
<PRStatusIcon status={task.prStatus} />
427+
<PRCheckStatus taskId={task.id} prStatus={task.prStatus} />
428+
</div>
429+
)}
424430
<span className="truncate">
425431
{(() => {
426432
try {

0 commit comments

Comments
 (0)