import { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { StatusBadge } from '@/components/StatusBadge'; import { AgentOutputViewer } from '@/components/AgentOutputViewer'; import { trpc } from '@/lib/trpc'; import { formatRelativeTime } from '@/lib/utils'; import { toast } from 'sonner'; interface ErrandDetailPanelProps { errandId: string; onClose: () => void; } export function ErrandDetailPanel({ errandId, onClose }: ErrandDetailPanelProps) { const [message, setMessage] = useState(''); const errandQuery = trpc.errand.get.useQuery({ id: errandId }); const errand = errandQuery.data; const diffQuery = trpc.errand.diff.useQuery( { id: errandId }, { enabled: errand?.status !== 'active' }, ); const utils = trpc.useUtils(); const completeMutation = trpc.errand.complete.useMutation({ onSuccess: () => { utils.errand.list.invalidate(); errandQuery.refetch(); }, }); const mergeMutation = trpc.errand.merge.useMutation({ onSuccess: () => { utils.errand.list.invalidate(); toast.success(`Merged into ${errand?.baseBranch ?? 'base'}`); onClose(); }, onError: () => { errandQuery.refetch(); }, }); const deleteMutation = trpc.errand.delete.useMutation({ onSuccess: () => { utils.errand.list.invalidate(); onClose(); }, }); const abandonMutation = trpc.errand.abandon.useMutation({ onSuccess: () => { utils.errand.list.invalidate(); errandQuery.refetch(); }, }); const sendMutation = trpc.errand.sendMessage.useMutation({ onSuccess: () => { utils.errand.list.invalidate(); setMessage(''); }, }); // Escape key closes useEffect(() => { function onKeyDown(e: KeyboardEvent) { if (e.key === 'Escape') onClose(); } document.addEventListener('keydown', onKeyDown); return () => document.removeEventListener('keydown', onKeyDown); }, [onClose]); const chatDisabled = errand?.status !== 'active' || sendMutation.isPending; return ( <> {/* Backdrop */} {/* Panel */} {/* Loading state */} {errandQuery.isLoading && ( <>
Loading…

Loading errand…

)} {/* Error state */} {errandQuery.error && ( <>
Error

Failed to load errand.

)} {/* Loaded state */} {errand && ( <> {/* Header */}

{errand.description}

{errand.branch}

{errand.agentAlias && ( {errand.agentAlias} )}
{/* View: Active */} {errand.status === 'active' && ( <>
{errand.agentId && ( )}
{/* Chat input */}
{ e.preventDefault(); if (!message.trim()) return; sendMutation.mutate({ id: errandId, message }); }} >
setMessage(e.target.value)} placeholder="Send a message to the agent…" disabled={chatDisabled} title={ chatDisabled && errand.status !== 'active' ? 'Agent is not running' : undefined } />
{/* Footer */}
)} {/* View: Pending Review / Conflict */} {(errand.status === 'pending_review' || errand.status === 'conflict') && ( <>
{/* Conflict notice */} {errand.status === 'conflict' && (errand.conflictFiles?.length ?? 0) > 0 && (
Merge conflict in {errand.conflictFiles!.length} file(s):{' '} {errand.conflictFiles!.join(', ')} — resolve manually in the worktree then re-merge.
)} {/* Diff block */}
{diffQuery.isLoading ? (

Loading diff…

) : diffQuery.data?.diff ? (
                          {diffQuery.data.diff}
                        
) : (

No changes — branch has no commits.

)}
{/* Footer */}
)} {/* View: Merged / Abandoned */} {(errand.status === 'merged' || errand.status === 'abandoned') && ( <>
{/* Info line */}
{errand.status === 'merged' ? `Merged into ${errand.baseBranch} · ${formatRelativeTime(errand.updatedAt.toISOString())}` : `Abandoned · ${formatRelativeTime(errand.updatedAt.toISOString())}`}
{/* Read-only diff */}
{diffQuery.data?.diff ? (
                          {diffQuery.data.diff}
                        
) : (

No changes — branch has no commits.

)}
{/* Footer */}
)} )}
); }