|
| 1 | +import { Bot, GitBranch, Send, Play, Square, Clock } from 'lucide-react'; |
| 2 | +import { Badge } from '@/components/ui/badge'; |
| 3 | +import type { CollabAgentTool, CollabAgentToolCallStatus, CollabAgentState, CollabAgentStatus } from '@/bindings/v2'; |
| 4 | + |
| 5 | +// Shape of the collabAgentToolCall item from item/completed. |
| 6 | +export interface CollabAgentToolCallItemData { |
| 7 | + type: 'collabAgentToolCall'; |
| 8 | + id: string; |
| 9 | + tool: CollabAgentTool; |
| 10 | + status: CollabAgentToolCallStatus; |
| 11 | + senderThreadId: string; |
| 12 | + receiverThreadIds: string[]; |
| 13 | + prompt: string | null; |
| 14 | + model: string | null; |
| 15 | + reasoningEffort: string | null; |
| 16 | + agentsStates: Record<string, CollabAgentState>; |
| 17 | +} |
| 18 | + |
| 19 | +// Human-readable label for each collab tool operation. |
| 20 | +function toolLabel(tool: CollabAgentTool, status: CollabAgentToolCallStatus): string { |
| 21 | + const done = status === 'completed'; |
| 22 | + switch (tool) { |
| 23 | + case 'spawnAgent': return done ? 'Spawned sub-agent' : 'Spawning sub-agent'; |
| 24 | + case 'sendInput': return done ? 'Sent to sub-agent' : 'Sending to sub-agent'; |
| 25 | + case 'resumeAgent': return done ? 'Resumed sub-agent' : 'Resuming sub-agent'; |
| 26 | + case 'wait': return done ? 'Waited for sub-agent' : 'Waiting for sub-agent'; |
| 27 | + case 'closeAgent': return done ? 'Closed sub-agent' : 'Closing sub-agent'; |
| 28 | + default: return 'Sub-agent operation'; |
| 29 | + } |
| 30 | +} |
| 31 | + |
| 32 | +function toolIcon(tool: CollabAgentTool) { |
| 33 | + switch (tool) { |
| 34 | + case 'spawnAgent': return <GitBranch className="h-3.5 w-3.5" />; |
| 35 | + case 'sendInput': return <Send className="h-3.5 w-3.5" />; |
| 36 | + case 'resumeAgent': return <Play className="h-3.5 w-3.5" />; |
| 37 | + case 'wait': return <Clock className="h-3.5 w-3.5" />; |
| 38 | + case 'closeAgent': return <Square className="h-3.5 w-3.5" />; |
| 39 | + default: return <Bot className="h-3.5 w-3.5" />; |
| 40 | + } |
| 41 | +} |
| 42 | + |
| 43 | +// Map agent-level status to a badge variant + label. |
| 44 | +function agentStatusDisplay(status: CollabAgentStatus): { |
| 45 | + label: string; |
| 46 | + className: string; |
| 47 | +} { |
| 48 | + switch (status) { |
| 49 | + case 'running': return { label: 'running', className: 'bg-blue-500/15 text-blue-700 dark:text-blue-400' }; |
| 50 | + case 'completed': return { label: 'done', className: 'bg-green-500/15 text-green-700 dark:text-green-400' }; |
| 51 | + case 'errored': return { label: 'errored', className: 'bg-red-500/15 text-red-700 dark:text-red-400' }; |
| 52 | + case 'interrupted': return { label: 'stopped', className: 'bg-yellow-500/15 text-yellow-700 dark:text-yellow-400' }; |
| 53 | + case 'shutdown': return { label: 'shutdown', className: 'bg-muted text-muted-foreground' }; |
| 54 | + case 'pendingInit': return { label: 'starting', className: 'bg-muted text-muted-foreground' }; |
| 55 | + case 'notFound': return { label: 'not found', className: 'bg-red-500/15 text-red-700 dark:text-red-400' }; |
| 56 | + default: return { label: status, className: 'bg-muted text-muted-foreground' }; |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +// Shorten a full thread UUID to the last 8 chars for display. |
| 61 | +function shortId(threadId: string): string { |
| 62 | + return threadId.length > 8 ? `…${threadId.slice(-8)}` : threadId; |
| 63 | +} |
| 64 | + |
| 65 | +interface CollabAgentToolCallItemProps { |
| 66 | + item: CollabAgentToolCallItemData; |
| 67 | +} |
| 68 | + |
| 69 | +export function CollabAgentToolCallItem({ item }: CollabAgentToolCallItemProps) { |
| 70 | + const { tool, status, receiverThreadIds, prompt, agentsStates } = item; |
| 71 | + |
| 72 | + const callStatusClass = |
| 73 | + status === 'completed' |
| 74 | + ? 'border-green-500/30 bg-green-500/5' |
| 75 | + : status === 'failed' |
| 76 | + ? 'border-red-500/30 bg-red-500/5' |
| 77 | + : 'border-blue-500/30 bg-blue-500/5'; |
| 78 | + |
| 79 | + const agentEntries = Object.entries(agentsStates ?? {}); |
| 80 | + |
| 81 | + return ( |
| 82 | + <div className={`rounded-md border px-3 py-2.5 text-xs space-y-2 ${callStatusClass}`}> |
| 83 | + {/* Header row */} |
| 84 | + <div className="flex items-center gap-2"> |
| 85 | + <span className="flex items-center gap-1 text-muted-foreground"> |
| 86 | + {toolIcon(tool)} |
| 87 | + <span className="font-medium text-foreground">{toolLabel(tool, status)}</span> |
| 88 | + </span> |
| 89 | + |
| 90 | + {/* Receiver thread IDs */} |
| 91 | + {receiverThreadIds.length > 0 && ( |
| 92 | + <span className="text-muted-foreground"> |
| 93 | + {receiverThreadIds.map((id) => ( |
| 94 | + <code key={id} className="font-mono text-[10px] bg-muted rounded px-1 py-0.5 mr-1"> |
| 95 | + {shortId(id)} |
| 96 | + </code> |
| 97 | + ))} |
| 98 | + </span> |
| 99 | + )} |
| 100 | + |
| 101 | + {/* Overall call status badge */} |
| 102 | + <Badge |
| 103 | + variant="outline" |
| 104 | + className={`ml-auto text-[10px] px-1.5 py-0 ${ |
| 105 | + status === 'completed' |
| 106 | + ? 'border-green-500/40 text-green-700 dark:text-green-400' |
| 107 | + : status === 'failed' |
| 108 | + ? 'border-red-500/40 text-red-700 dark:text-red-400' |
| 109 | + : 'border-blue-500/40 text-blue-700 dark:text-blue-400' |
| 110 | + }`} |
| 111 | + > |
| 112 | + {status} |
| 113 | + </Badge> |
| 114 | + </div> |
| 115 | + |
| 116 | + {/* Prompt snippet — first 120 chars */} |
| 117 | + {prompt && ( |
| 118 | + <p className="text-muted-foreground italic line-clamp-2 leading-relaxed"> |
| 119 | + {prompt.length > 120 ? `${prompt.slice(0, 120)}…` : prompt} |
| 120 | + </p> |
| 121 | + )} |
| 122 | + |
| 123 | + {/* Per-agent state rows */} |
| 124 | + {agentEntries.length > 0 && ( |
| 125 | + <div className="space-y-1 pt-0.5"> |
| 126 | + {agentEntries.map(([threadId, agentState]) => { |
| 127 | + const { label, className } = agentStatusDisplay(agentState.status); |
| 128 | + return ( |
| 129 | + <div key={threadId} className="flex items-center gap-2"> |
| 130 | + <Bot className="h-3 w-3 text-muted-foreground flex-shrink-0" /> |
| 131 | + <code className="font-mono text-[10px] text-muted-foreground"> |
| 132 | + {shortId(threadId)} |
| 133 | + </code> |
| 134 | + <span className={`rounded px-1.5 py-0.5 text-[10px] font-medium ${className}`}> |
| 135 | + {label} |
| 136 | + </span> |
| 137 | + {agentState.message && ( |
| 138 | + <span className="text-muted-foreground truncate">{agentState.message}</span> |
| 139 | + )} |
| 140 | + </div> |
| 141 | + ); |
| 142 | + })} |
| 143 | + </div> |
| 144 | + )} |
| 145 | + </div> |
| 146 | + ); |
| 147 | +} |
0 commit comments