Skip to content

Commit 69c5f37

Browse files
committed
feat: add CollabAgentToolCallItem to visualize multi-agent operations in the event feed
1 parent b27acfe commit 69c5f37

3 files changed

Lines changed: 153 additions & 2 deletions

File tree

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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+
}

src/components/codex/items/EventItem.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { AgentMessageItem } from './AgentMessageItem';
99
import { CommandActionItem } from './CommandActionItem';
1010
import { IndividualFileChanges } from './IndividualFileChanges';
1111
import { SummaryFileChanges } from './SummaryFileChanges';
12+
import { CollabAgentToolCallItem, type CollabAgentToolCallItemData } from './CollabAgentToolCallItem';
1213
import {
1314
aggregateFileChanges,
1415
aggregateTurnChangesFromContext,
@@ -62,7 +63,6 @@ const getRollbackTurnsForTurn = (
6263
}
6364
}
6465

65-
// Edit should remove the selected turn itself plus all later turns.
6666
return laterTurnIds.size + 1;
6767
};
6868

@@ -131,6 +131,9 @@ export const renderEvent = (event: ServerNotification, context?: RenderEventCont
131131
case 'enteredReviewMode':
132132
case 'exitedReviewMode':
133133
return null;
134+
case 'collabAgentToolCall':
135+
// Render the multi-agent sub-agent operation card.
136+
return <CollabAgentToolCallItem item={item as unknown as CollabAgentToolCallItemData} />;
134137
default:
135138
return (
136139
<CollapsedJsonItem label={item.type} value={item} />
@@ -173,7 +176,7 @@ export const renderEvent = (event: ServerNotification, context?: RenderEventCont
173176
</div>
174177
);
175178
case 'item/fileChange/outputDelta':
176-
return null
179+
return null;
177180
case 'item/commandExecution/terminalInteraction':
178181
return (
179182
<div className="rounded-md border border-slate-300/80 bg-slate-100/40 px-2 py-1 text-xs text-slate-700">

src/components/codex/items/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './CollabAgentToolCallItem';
12
export * from './EventItem';
23
export * from './ApprovalItem';
34
export * from './RequestUserInputItem';

0 commit comments

Comments
 (0)