import { useState, useEffect } from 'react'; import { t } from '../i18n'; import type { AgentLineage } from '../types'; // ── Helpers ────────────────────────────────────────────────────────────────── const MEMORY_SCOPE_COLORS: Record = { full: 'bg-green-900/50 text-green-300', summary: 'bg-yellow-900/50 text-yellow-300', none: 'bg-gray-700 text-gray-400', }; function formatDate(ts: string): string { try { return new Date(ts).toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric', }); } catch { return ts; } } function countDescendants(node: AgentLineage): number { if (!node.children || node.children.length === 0) return 0; return node.children.reduce((sum, c) => sum + 1 + countDescendants(c), 0); } // ── Spawn Form (inline) ─────────────────────────────────────────────────────── interface SpawnFormProps { parentId: string; onSpawn: (child: AgentLineage) => void; onCancel: () => void; } function SpawnForm({ parentId, onSpawn, onCancel }: SpawnFormProps) { const [name, setName] = useState(''); const [memoryScope, setMemoryScope] = useState('summary'); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!name.trim()) { setError(t('lineages.nameRequired') || 'Name is required'); return; } setSaving(true); setError(''); try { const res = await fetch(`/api/executions/lineages/${parentId}/spawn`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name.trim(), memory_scope: memoryScope }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const child: AgentLineage = await res.json(); onSpawn(child); } catch (err) { setError(`${t('lineages.spawnFailed') || 'Spawn failed'}: ${(err as Error).message}`); } finally { setSaving(false); } }; return (
e.stopPropagation()} >

{t('lineages.spawnChild') || 'Spawn Child Agent'}

setName(e.target.value)} placeholder="Child agent name" autoFocus className="input-base text-xs py-1.5 col-span-1" />
{error &&

{error}

}
); } // ── Inherited Memory Viewer ─────────────────────────────────────────────────── interface InheritedMemoryProps { memory: string; } function InheritedMemoryViewer({ memory }: InheritedMemoryProps) { const [expanded, setExpanded] = useState(false); let parsed: Record = {}; try { parsed = JSON.parse(memory || '{}'); } catch {} const entries = Object.entries(parsed); if (entries.length === 0) return null; return (
{expanded && (
{entries.map(([key, value]) => (
{key} {value}
))}
)}
); } // ── Tree Node ───────────────────────────────────────────────────────────────── interface TreeNodeProps { node: AgentLineage; depth: number; onChildSpawned: (parentId: string, child: AgentLineage) => void; onNodeDeleted: (id: string) => void; } function TreeNode({ node, depth, onChildSpawned, onNodeDeleted }: TreeNodeProps) { const [showSpawnForm, setShowSpawnForm] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false); const handleDelete = async (e: React.MouseEvent) => { e.stopPropagation(); try { const res = await fetch(`/api/executions/lineages/${node.id}`, { method: 'DELETE' }); if (!res.ok) throw new Error(`HTTP ${res.status}`); onNodeDeleted(node.id); // only after success } catch { setConfirmDelete(false); } }; const handleSpawn = (child: AgentLineage) => { onChildSpawned(node.id, child); setShowSpawnForm(false); }; return (
{/* Vertical connector for non-root nodes */} {depth > 0 && (
)}
{/* Horizontal connector */} {depth > 0 && (
)}
{node.name} v{node.version} {node.memory_scope} {formatDate(node.created_at)}
{confirmDelete ? (
) : ( )}
{showSpawnForm && ( setShowSpawnForm(false)} /> )}
{/* Render children */} {node.children && node.children.length > 0 && (
{node.children.map(child => ( ))}
)}
); } // ── Create Root Form ────────────────────────────────────────────────────────── interface CreateRootFormProps { onCreate: (lineage: AgentLineage) => void; onCancel: () => void; } function CreateRootForm({ onCreate, onCancel }: CreateRootFormProps) { const [name, setName] = useState(''); const [memoryScope, setMemoryScope] = useState('full'); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!name.trim()) { setError(t('lineages.nameRequired') || 'Name is required'); return; } setSaving(true); setError(''); try { const res = await fetch('/api/executions/lineages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name.trim(), memory_scope: memoryScope }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const created: AgentLineage = await res.json(); onCreate(created); } catch (err) { setError(`${t('lineages.createFailed') || 'Create failed'}: ${(err as Error).message}`); } finally { setSaving(false); } }; return (

{t('lineages.createLineage') || 'New Root Lineage'}

setName(e.target.value)} placeholder="e.g. Research Agent v1" autoFocus className="input-base w-full" />
{error &&

{error}

}
); } // ── Helpers: tree mutation ──────────────────────────────────────────────────── function insertChild(tree: AgentLineage, parentId: string, child: AgentLineage): AgentLineage { if (tree.id === parentId) { return { ...tree, children: [...(tree.children ?? []), { ...child, children: [] }], }; } return { ...tree, children: (tree.children ?? []).map(c => insertChild(c, parentId, child)), }; } function removeNode(tree: AgentLineage, nodeId: string): AgentLineage | null { if (tree.id === nodeId) return null; return { ...tree, children: (tree.children ?? []) .map(c => removeNode(c, nodeId)) .filter(Boolean) as AgentLineage[], }; } // ── Main Page ───────────────────────────────────────────────────────────────── export default function LineagesPage() { const [rootLineages, setRootLineages] = useState([]); const [loading, setLoading] = useState(true); const [selectedId, setSelectedId] = useState(null); const [selectedTree, setSelectedTree] = useState(null); const [loadingTree, setLoadingTree] = useState(false); const [showCreate, setShowCreate] = useState(false); // Fetch root lineages list useEffect(() => { fetch('/api/executions/lineages') .then(r => r.json()) .then((data: AgentLineage[]) => { if (Array.isArray(data)) setRootLineages(data); }) .catch(console.error) .finally(() => setLoading(false)); }, []); // Fetch full tree when selection changes useEffect(() => { if (!selectedId) { setSelectedTree(null); return; } setLoadingTree(true); fetch(`/api/executions/lineages/${selectedId}/tree`) .then(r => r.json()) .then((data: AgentLineage) => setSelectedTree(data)) .catch(console.error) .finally(() => setLoadingTree(false)); }, [selectedId]); const handleCreate = (lineage: AgentLineage) => { setRootLineages(prev => [...prev, lineage]); setShowCreate(false); setSelectedId(lineage.id); }; const handleChildSpawned = (parentId: string, child: AgentLineage) => { if (!selectedTree) return; setSelectedTree(tree => tree ? insertChild(tree, parentId, child) : tree); }; const handleNodeDeleted = (id: string) => { // If the deleted node is the root, remove from list too if (id === selectedId) { setRootLineages(prev => prev.filter(l => l.id !== id)); setSelectedId(null); setSelectedTree(null); return; } if (selectedTree) { const updated = removeNode(selectedTree, id); setSelectedTree(updated); } // Refresh root list in case a root was deleted via nested action setRootLineages(prev => prev.filter(l => l.id !== id)); }; const handleRootDelete = async (id: string) => { try { const res = await fetch(`/api/executions/lineages/${id}`, { method: 'DELETE' }); if (!res.ok) return; setRootLineages(prev => prev.filter(l => l.id !== id)); if (selectedId === id) { setSelectedId(null); setSelectedTree(null); } } catch {} }; return (
{/* Header */}

{t('lineages.title') || 'Lineages'}

{t('lineages.subtitle') || 'Agent lineage trees with inherited memory'}

{!showCreate && ( )}
{/* Two-panel body */}
{/* Left panel: root list */}
{showCreate && ( setShowCreate(false)} /> )} {loading ? (
{t('lineages.loading') || 'Loading...'}
) : rootLineages.length === 0 && !showCreate ? (
🧬

{t('lineages.noLineages') || 'No lineages yet'}

) : ( rootLineages.map(lineage => { const spawnCount = countDescendants(lineage); return (
setSelectedId(lineage.id)} onKeyDown={e => e.key === 'Enter' && setSelectedId(lineage.id)} className={`w-full text-left card transition-colors cursor-pointer group ${ lineage.id === selectedId ? 'border-blue-600 bg-gray-700/60' : 'hover:border-gray-600' }`} >
{lineage.name} v{lineage.version}
{lineage.memory_scope} {spawnCount > 0 && ( {spawnCount} child{spawnCount !== 1 ? 'ren' : ''} )}
); }) )}
{/* Right panel: tree view */}
{selectedId ? ( loadingTree ? (
{t('lineages.loadingTree') || 'Loading tree...'}
) : selectedTree ? (

{t('lineages.treeView') || 'Lineage Tree'}

) : (

{t('lineages.loadError') || 'Failed to load lineage'}

) ) : (
🧬

{t('lineages.selectLineage') || 'Select a lineage to view its tree'}

)}
); }