Files
autocoder/ui/src/components/FeatureCard.tsx
Caitlyn Byrne 656df0fd9a feat: add "blocked for human input" feature across full stack
Agents can now request structured human input when they encounter
genuine blockers (API keys, design choices, external configs). The
request is displayed in the UI with a dynamic form, and the human's
response is stored and made available when the agent resumes.

Changes span 21 files + 1 new component:
- Database: 3 new columns (needs_human_input, human_input_request,
  human_input_response) with migration
- MCP: new feature_request_human_input tool + guards on existing tools
- API: new resolve-human-input endpoint, 4th feature bucket
- Orchestrator: skip needs_human_input features in scheduling
- Progress: 4-tuple return from count_passing_tests
- WebSocket: needs_human_input count in progress messages
- UI: conditional 4th Kanban column, HumanInputForm component,
  amber status indicators, dependency graph support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 14:11:35 -05:00

130 lines
4.5 KiB
TypeScript

import { CheckCircle2, Circle, Loader2, MessageCircle, UserCircle } from 'lucide-react'
import type { Feature, ActiveAgent } from '../lib/types'
import { DependencyBadge } from './DependencyBadge'
import { AgentAvatar } from './AgentAvatar'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
interface FeatureCardProps {
feature: Feature
onClick: () => void
isInProgress?: boolean
allFeatures?: Feature[]
activeAgent?: ActiveAgent
}
// Generate consistent color for category
function getCategoryColor(category: string): string {
const colors = [
'bg-pink-500',
'bg-cyan-500',
'bg-green-500',
'bg-yellow-500',
'bg-orange-500',
'bg-purple-500',
'bg-blue-500',
]
let hash = 0
for (let i = 0; i < category.length; i++) {
hash = category.charCodeAt(i) + ((hash << 5) - hash)
}
return colors[Math.abs(hash) % colors.length]
}
export function FeatureCard({ feature, onClick, isInProgress, allFeatures = [], activeAgent }: FeatureCardProps) {
const categoryColor = getCategoryColor(feature.category)
const isBlocked = feature.blocked || (feature.blocking_dependencies && feature.blocking_dependencies.length > 0)
const hasActiveAgent = !!activeAgent
return (
<Card
onClick={onClick}
className={`
cursor-pointer transition-all hover:border-primary py-3
${isInProgress ? 'animate-pulse' : ''}
${feature.passes ? 'border-primary/50' : ''}
${feature.needs_human_input ? 'border-amber-500/50' : ''}
${isBlocked && !feature.passes && !feature.needs_human_input ? 'border-destructive/50 opacity-80' : ''}
${hasActiveAgent ? 'ring-2 ring-primary ring-offset-2' : ''}
`}
>
<CardContent className="p-4 space-y-3">
{/* Header */}
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2">
<Badge className={`${categoryColor} text-white`}>
{feature.category}
</Badge>
<DependencyBadge feature={feature} allFeatures={allFeatures} compact />
</div>
<span className="font-mono text-sm text-muted-foreground">
#{feature.priority}
</span>
</div>
{/* Name */}
<h3 className="font-semibold line-clamp-2">
{feature.name}
</h3>
{/* Description */}
<p className="text-sm text-muted-foreground line-clamp-2">
{feature.description}
</p>
{/* Agent working on this feature */}
{activeAgent && (
<div className="flex items-center gap-2 py-2 px-2 rounded-md bg-primary/10 border border-primary/30">
<AgentAvatar name={activeAgent.agentName} state={activeAgent.state} size="sm" />
<div className="flex-1 min-w-0">
<div className="text-xs font-semibold text-primary">
{activeAgent.agentName} is working on this!
</div>
{activeAgent.thought && (
<div className="flex items-center gap-1 mt-0.5">
<MessageCircle size={10} className="text-muted-foreground shrink-0" />
<p className="text-[10px] text-muted-foreground truncate italic">
{activeAgent.thought}
</p>
</div>
)}
</div>
</div>
)}
{/* Status */}
<div className="flex items-center gap-2 text-sm">
{isInProgress ? (
<>
<Loader2 size={16} className="animate-spin text-primary" />
<span className="text-primary font-medium">Processing...</span>
</>
) : feature.passes ? (
<>
<CheckCircle2 size={16} className="text-primary" />
<span className="text-primary font-medium">Complete</span>
</>
) : feature.needs_human_input ? (
<>
<UserCircle size={16} className="text-amber-500" />
<span className="text-amber-500 font-medium">Needs Your Input</span>
</>
) : isBlocked ? (
<>
<Circle size={16} className="text-destructive" />
<span className="text-destructive">Blocked</span>
</>
) : (
<>
<Circle size={16} className="text-muted-foreground" />
<span className="text-muted-foreground">Pending</span>
</>
)}
</div>
</CardContent>
</Card>
)
}