AgentExecutionsDebuggerTab.tsx12.8 KBView on GitHub import { Check, Copy, ChevronRight, GitBranch, Search } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import React, { useState, useMemo } from 'react';
import { Link } from 'react-router';
import { cn } from '@/styles/stylingUtils';
import { JsonTreeView } from './JsonTreeView';
interface AgentExecutionsTabProps {
executions: Record<string, any>;
onCopy: (text: string, id: string) => void;
copiedId: string | null;
}
export const AgentExecutionsDebuggerTab: React.FC<AgentExecutionsTabProps> = ({
executions,
onCopy,
copiedId,
}) => {
const [expandedExecutions, setExpandedExecutions] = useState<Set<string>>(new Set());
const [searchQuery, setSearchQuery] = useState('');
const executionEntries = useMemo(() => Object.entries(executions), [executions]);
const filteredExecutions = useMemo(() => {
if (!searchQuery) return executionEntries;
const query = searchQuery.toLowerCase();
return executionEntries.filter(([runId, execution]) => {
if (runId.toLowerCase().includes(query)) return true;
if (execution?.data?.conversationId?.toLowerCase().includes(query)) return true;
if (execution?.data?.threadId?.toLowerCase().includes(query)) return true;
if (execution?.data?.aopName?.toLowerCase().includes(query)) return true;
if (execution?.data?.status?.toLowerCase().includes(query)) return true;
try {
const jsonString = JSON.stringify(execution).toLowerCase();
if (jsonString.includes(query)) return true;
} catch {}
return false;
});
}, [executionEntries, searchQuery]);
const toggleExecution = (runId: string) => {
setExpandedExecutions((prev) => {
const next = new Set(prev);
if (next.has(runId)) {
next.delete(runId);
} else {
next.add(runId);
}
return next;
});
};
return (
<div className="flex h-full flex-col">
{/* Search Bar */}
<div className="border-b border-gray-200 p-2 dark:border-gray-700">
<div className="relative">
<Search className="absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="Search executions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full rounded border border-gray-200 py-1 pl-7 pr-2 text-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800"
/>
</div>
</div>
{/* Content Area */}
<div className="flex-1 overflow-y-auto">
{filteredExecutions.length === 0 ? (
<div className="flex h-full items-center justify-center">
<div className="text-center text-gray-500">
<p className="text-xs">
{searchQuery ? 'No matching executions found' : 'No agent executions loaded'}
</p>
</div>
</div>
) : (
<div className="space-y-1 p-2">
{filteredExecutions.map(([runId, executionWithMeta]) => {
const execution = executionWithMeta?.data;
const isExpanded = expandedExecutions.has(runId);
return (
<div
key=[redacted]
className="overflow-hidden rounded border border-gray-200 dark:border-gray-700"
>
{/* Header */}
<button
onClick={() => toggleExecution(runId)}
className="flex w-full items-center justify-between bg-gray-50 px-2 py-1 text-left transition-colors hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700"
>
<div className="flex items-center gap-2">
<motion.div
initial={false}
animate={{ rotate: isExpanded ? 90 : 0 }}
transition={{ duration: 0.2 }}
>
<ChevronRight className="h-3 w-3 text-gray-500" />
</motion.div>
<span className="font-mono text-xs text-gray-700 dark:text-gray-300">
{execution?.aopName || 'Unknown AOP'}
</span>
<span
className={cn(
'rounded px-1.5 py-0.5 text-xs font-medium',
execution?.status === 'completed' || execution?.status === 'final'
? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'
: execution?.status === 'failed'
? 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300'
: execution?.status === 'pending'
? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300'
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300',
)}
>
{execution?.status || 'unknown'}
</span>
</div>
<button
onClick={(e) => {
e.stopPropagation();
onCopy(runId, runId);
}}
className="rounded p-1 transition-colors hover:bg-gray-200 dark:hover:bg-gray-600"
title="Copy RunId"
>
{copiedId === runId ? (
<Check className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3 text-gray-500" />
)}
</button>
</button>
{/* Expanded Content */}
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
>
<div className="border-t border-gray-200 bg-white p-2 dark:border-gray-700 dark:bg-gray-900">
{/* Quick Info */}
<div className="mb-2 space-y-1 text-xs">
<div className="flex items-start gap-2">
<span className="font-medium text-gray-500 dark:text-gray-400">
Run ID:
</span>
<button
onClick={() => onCopy(runId, `${runId}-runid`)}
className="group flex items-center gap-1 rounded px-1 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
>
<span className="font-mono text-gray-700 dark:text-gray-300">
{runId}
</span>
{copiedId === `${runId}-runid` ? (
<Check className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3 text-gray-400 opacity-0 group-hover:opacity-100" />
)}
</button>
</div>
<div className="flex items-start gap-2">
<span className="font-medium text-gray-500 dark:text-gray-400">
Trace:
</span>
<Link
to={`/settings/agentExecutions/${runId}`}
className="flex cursor-pointer items-center gap-1 rounded px-1 text-blue-600 transition-colors hover:bg-gray-100 hover:underline dark:text-blue-400 dark:hover:bg-gray-800"
>
<GitBranch className="h-3 w-3" />
Open waterfall
</Link>
</div>
{execution?.conversationId && (
<div className="flex items-start gap-2">
<span className="font-medium text-gray-500 dark:text-gray-400">
Conversation:
</span>
<button
onClick={() =>
onCopy(execution.conversationId, `${runId}-convoid`)
}
className="group flex items-center gap-1 rounded px-1 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
>
<span className="font-mono text-purple-600 dark:text-purple-400">
{execution.conversationId.slice(0, 20)}...
</span>
{copiedId === `${runId}-convoid` ? (
<Check className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3 text-gray-400 opacity-0 group-hover:opacity-100" />
)}
</button>
</div>
)}
{execution?.threadId && (
<div className="flex items-start gap-2">
<span className="font-medium text-gray-500 dark:text-gray-400">
Thread:
</span>
<button
onClick={() => onCopy(execution.threadId, `${runId}-threadid`)}
className="group flex items-center gap-1 rounded px-1 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
>
<span className="font-mono text-blue-600 dark:text-blue-400">
{execution.threadId.slice(0, 20)}...
</span>
{copiedId === `${runId}-threadid` ? (
<Check className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3 text-gray-400 opacity-0 group-hover:opacity-100" />
)}
</button>
</div>
)}
{execution?.summary && (
<div className="flex flex-col gap-1">
<span className="font-medium text-gray-500 dark:text-gray-400">
Summary:
</span>
<span className="text-gray-700 dark:text-gray-300">
{execution.summary}
</span>
</div>
)}
<div className="flex items-start gap-2">
<span className="font-medium text-gray-500 dark:text-gray-400">
Created:
</span>
<span className="text-gray-700 dark:text-gray-300">
{execution?.createdAt
? new Date(execution.createdAt).toLocaleString()
: 'N/A'}
</span>
</div>
</div>
{/* Full JSON */}
<details className="mt-2">
<summary className="cursor-pointer text-xs font-medium text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
View Full Data
</summary>
<div className="mt-1">
<JsonTreeView data={executionWithMeta} onCopy={onCopy} copiedId={copiedId} defaultExpandDepth={2} className="max-h-60" />
</div>
</details>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
)}
</div>
</div>
);
};