55 * On-canvas detail popup for agent nodes.
66 *
77 * Appears next to the selected node, showing agent details,
8- * step config (pipeline), execution state, and quick actions.
8+ * step config (pipeline), execution state, I/O inspector, and quick actions.
99 * Uses glassmorphism styling for a premium floating card look.
1010 */
1111
12- import { useEffect , useRef } from 'react'
12+ import { useEffect , useRef , useState } from 'react'
1313import { useReactFlow } from '@xyflow/react'
1414import type { Node } from '@xyflow/react'
1515import {
@@ -22,8 +22,12 @@ import {
2222 Check ,
2323 Loader2 ,
2424 X ,
25+ ArrowDownToLine ,
26+ ArrowUpFromLine ,
27+ ChevronDown ,
28+ ChevronRight ,
2529} from 'lucide-react'
26- import type { Agent , Team , PipelineConfig , PipelineStep } from '@/types'
30+ import type { Agent , Team , PipelineConfig , PipelineStep , TeamMessage } from '@/types'
2731import type { AgentNodeData } from './nodes/AgentNode'
2832import type { ExecutionNodeState } from './useExecutionState'
2933
@@ -32,6 +36,7 @@ interface NodeDetailPopupProps {
3236 team : Team
3337 agents : Agent [ ]
3438 executionStates : Map < string , ExecutionNodeState > | null
39+ runMessages ?: TeamMessage [ ]
3540 onDelete ?: ( nodeId : string ) => void
3641 onClose : ( ) => void
3742}
@@ -43,9 +48,11 @@ const EXEC_STATUS_CONFIG: Record<ExecutionNodeState, { label: string; className:
4348 failed : { label : 'Failed' , className : 'text-red-400' , Icon : X } ,
4449}
4550
46- export function NodeDetailPopup ( { node, team, agents, executionStates, onDelete, onClose } : NodeDetailPopupProps ) {
51+ export function NodeDetailPopup ( { node, team, agents, executionStates, runMessages , onDelete, onClose } : NodeDetailPopupProps ) {
4752 const popupRef = useRef < HTMLDivElement > ( null )
4853 const { getNodesBounds, flowToScreenPosition } = useReactFlow ( )
54+ const [ showInput , setShowInput ] = useState ( false )
55+ const [ showOutput , setShowOutput ] = useState ( false )
4956
5057 const nodeData = node . data as unknown as AgentNodeData
5158 const isAgentNode = node . type === 'agentNode'
@@ -68,6 +75,10 @@ export function NodeDetailPopup({ node, team, agents, executionStates, onDelete,
6875 const isBrain = nodeData . role === 'brain' || nodeData . role === 'orchestrator'
6976 const canDelete = isAgentNode && ! protectedIds . has ( node . id ) && ! ( isBrain && team . mode === 'orchestrator' )
7077
78+ // I/O: filter messages for this agent
79+ const { inputText, outputText } = getNodeIO ( agentId , runMessages )
80+ const hasIO = inputText || outputText
81+
7182 // Calculate screen position of popup
7283 const bounds = getNodesBounds ( [ node ] )
7384 const screenPos = flowToScreenPosition ( { x : bounds . x + bounds . width + 12 , y : bounds . y } )
@@ -211,6 +222,62 @@ export function NodeDetailPopup({ node, team, agents, executionStates, onDelete,
211222 </ >
212223 ) }
213224
225+ { /* I/O Inspector */ }
226+ { hasIO && (
227+ < >
228+ < hr className = "border-white/5 mb-2" />
229+ < div className = "space-y-1.5 mb-3" >
230+ < p className = "text-[10px] font-medium uppercase tracking-wider text-gray-600" > I/O Inspector</ p >
231+
232+ { /* Input */ }
233+ { inputText && (
234+ < div >
235+ < button
236+ type = "button"
237+ onClick = { ( ) => setShowInput ( ! showInput ) }
238+ className = "flex items-center gap-1 text-[10px] text-sky-400 hover:text-sky-300 transition-colors w-full"
239+ >
240+ < ArrowDownToLine className = "h-3 w-3" />
241+ < span className = "font-medium" > Input</ span >
242+ { showInput
243+ ? < ChevronDown className = "h-2.5 w-2.5 ml-auto" />
244+ : < ChevronRight className = "h-2.5 w-2.5 ml-auto" />
245+ }
246+ </ button >
247+ { showInput && (
248+ < pre className = "mt-1 rounded-md bg-black/30 border border-white/5 p-2 text-[10px] text-gray-400 leading-relaxed max-h-32 overflow-y-auto whitespace-pre-wrap break-words" >
249+ { inputText }
250+ </ pre >
251+ ) }
252+ </ div >
253+ ) }
254+
255+ { /* Output */ }
256+ { outputText && (
257+ < div >
258+ < button
259+ type = "button"
260+ onClick = { ( ) => setShowOutput ( ! showOutput ) }
261+ className = "flex items-center gap-1 text-[10px] text-emerald-400 hover:text-emerald-300 transition-colors w-full"
262+ >
263+ < ArrowUpFromLine className = "h-3 w-3" />
264+ < span className = "font-medium" > Output</ span >
265+ { showOutput
266+ ? < ChevronDown className = "h-2.5 w-2.5 ml-auto" />
267+ : < ChevronRight className = "h-2.5 w-2.5 ml-auto" />
268+ }
269+ </ button >
270+ { showOutput && (
271+ < pre className = "mt-1 rounded-md bg-black/30 border border-white/5 p-2 text-[10px] text-gray-300 leading-relaxed max-h-32 overflow-y-auto whitespace-pre-wrap break-words" >
272+ { outputText }
273+ </ pre >
274+ ) }
275+ </ div >
276+ ) }
277+ </ div >
278+ </ >
279+ ) }
280+
214281 { /* Brain protection notice */ }
215282 { isBrain && team . mode === 'orchestrator' && (
216283 < div className = "flex items-start gap-1.5 rounded-md bg-amber-500/5 border border-amber-500/15 px-2.5 py-2 mb-3" >
@@ -247,3 +314,35 @@ export function NodeDetailPopup({ node, team, agents, executionStates, onDelete,
247314 </ div >
248315 )
249316}
317+
318+ /**
319+ * Extract input/output text for a specific agent from team messages.
320+ * Input = messages received by this agent (receiver_agent_id).
321+ * Output = messages sent by this agent (sender_agent_id).
322+ */
323+ function getNodeIO (
324+ agentId : string | undefined ,
325+ messages ?: TeamMessage [ ] ,
326+ ) : { inputText : string ; outputText : string } {
327+ if ( ! agentId || ! messages || messages . length === 0 ) {
328+ return { inputText : '' , outputText : '' }
329+ }
330+
331+ const inputMsgs = messages . filter (
332+ ( m ) => m . receiver_agent_id === agentId && m . content ,
333+ )
334+ const outputMsgs = messages . filter (
335+ ( m ) => m . sender_agent_id === agentId && m . content && m . message_type !== 'delegation' ,
336+ )
337+
338+ // Use the last input/output message (most recent)
339+ const inputText = inputMsgs . length > 0
340+ ? inputMsgs [ inputMsgs . length - 1 ] . content
341+ : ''
342+ const outputText = outputMsgs . length > 0
343+ ? outputMsgs [ outputMsgs . length - 1 ] . content
344+ : ''
345+
346+ return { inputText, outputText }
347+ }
348+
0 commit comments