Skip to content
Merged
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
104 changes: 104 additions & 0 deletions components/pr-check-status.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use client'

import { useEffect, useState } from 'react'
import { Check, Loader2, X } from 'lucide-react'

interface CheckRun {
id: number
name: string
status: string
conclusion: string | null
html_url: string
started_at: string | null
completed_at: string | null
}

interface PRCheckStatusProps {
taskId: string
prStatus: 'open' | 'closed' | 'merged'
isActive?: boolean
className?: string
}

export function PRCheckStatus({ taskId, prStatus, isActive = false, className = '' }: PRCheckStatusProps) {
const [checkRuns, setCheckRuns] = useState<CheckRun[]>([])
const [isLoading, setIsLoading] = useState(true)

useEffect(() => {
const fetchCheckRuns = async () => {
try {
const response = await fetch(`/api/tasks/${taskId}/check-runs`)
if (response.ok) {
const data = await response.json()
if (data.success && data.checkRuns) {
setCheckRuns(data.checkRuns)
}
}
} catch (error) {
console.error('Error fetching check runs:', error)
} finally {
setIsLoading(false)
}
}

fetchCheckRuns()
// Refresh every 30 seconds for in-progress checks
const interval = setInterval(fetchCheckRuns, 30000)
return () => clearInterval(interval)
}, [taskId])

// Only show indicator for open PRs
if (prStatus !== 'open') {
return null
}

// Don't render anything if loading or no check runs
if (isLoading || checkRuns.length === 0) {
return null
}

// Determine overall status
const hasInProgress = checkRuns.some((run) => run.status === 'in_progress' || run.status === 'queued')
const hasFailed = checkRuns.some((run) => run.conclusion === 'failure' || run.conclusion === 'cancelled')
const hasNeutral = checkRuns.some((run) => run.conclusion === 'neutral')
const allPassed = checkRuns.every((run) => run.status === 'completed' && run.conclusion === 'success')

// Determine background color based on active state
const bgColor = isActive ? 'bg-accent' : 'bg-card'

// Render the appropriate indicator
// Note: Check failed first to ensure failures are always visible, even if other checks are in progress
if (hasFailed) {
return (
<div className={`absolute -bottom-0.5 -right-0.5 ${bgColor} rounded-full p-0.5 ${className}`}>
<div className="w-1 h-1 rounded-full bg-red-500" />
</div>
)
}

if (hasInProgress) {
return (
<div className={`absolute -bottom-0.5 -right-0.5 ${bgColor} rounded-full p-0.5 ${className}`}>
<div className="w-1 h-1 rounded-full bg-yellow-500 animate-pulse" />
</div>
)
}

if (hasNeutral) {
return (
<div className={`absolute -bottom-0.5 -right-0.5 ${bgColor} rounded-full p-0.5 ${className}`}>
<div className="w-1 h-1 rounded-full bg-blue-500" />
</div>
)
}

if (allPassed) {
return (
<div className={`absolute -bottom-0.5 -right-0.5 ${bgColor} rounded-full p-0.5 ${className}`}>
<Check className="w-1.5 h-1.5 text-green-500" strokeWidth={3} />
</div>
)
}

return null
}
8 changes: 7 additions & 1 deletion components/task-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { useTasks } from '@/components/app-layout'
import { useAtomValue } from 'jotai'
import { sessionAtom } from '@/lib/atoms/session'
import { PRStatusIcon } from '@/components/pr-status-icon'
import { PRCheckStatus } from '@/components/pr-check-status'

// Model mappings for human-friendly names
const AGENT_MODELS = {
Expand Down Expand Up @@ -397,7 +398,12 @@ export function TaskSidebar({ tasks, onTaskSelect, width = 288 }: TaskSidebarPro
</div>
{task.repoUrl && (
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-0.5">
{task.prStatus && <PRStatusIcon status={task.prStatus} />}
{task.prStatus && (
<div className="relative">
<PRStatusIcon status={task.prStatus} />
<PRCheckStatus taskId={task.id} prStatus={task.prStatus} isActive={isActive} />
</div>
)}
<span className="truncate">
{(() => {
try {
Expand Down
8 changes: 7 additions & 1 deletion components/tasks-list-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type { Session } from '@/lib/session/types'
import { VERCEL_DEPLOY_URL } from '@/lib/constants'
import { Claude, Codex, Copilot, Cursor, Gemini, OpenCode } from '@/components/logos'
import { PRStatusIcon } from '@/components/pr-status-icon'
import { PRCheckStatus } from '@/components/pr-check-status'

interface TasksListClientProps {
user: Session['user'] | null
Expand Down Expand Up @@ -420,7 +421,12 @@ export function TasksListClient({ user, authProvider, initialStars = 1056 }: Tas
</div>
{task.repoUrl && (
<div className="flex items-center gap-2 text-xs text-muted-foreground mb-1">
{task.prStatus && <PRStatusIcon status={task.prStatus} />}
{task.prStatus && (
<div className="relative">
<PRStatusIcon status={task.prStatus} />
<PRCheckStatus taskId={task.id} prStatus={task.prStatus} />
</div>
)}
<span className="truncate">
{(() => {
try {
Expand Down
Loading