import { useCallback, useState } from 'react'; import { Box, Stack, Typography, Chip, Button, TextField, IconButton, Link } from '@mui/material'; import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty'; import LaunchIcon from '@mui/icons-material/Launch'; import SendIcon from '@mui/icons-material/Send'; import { useAgentStore } from '@/store/agentStore'; import { useLayoutStore } from '@/store/layoutStore'; import { useSessionStore } from '@/store/sessionStore'; import { apiFetch } from '@/utils/api'; import { logger } from '@/utils/logger'; import type { TraceLog } from '@/types/agent'; interface ToolCallGroupProps { tools: TraceLog[]; } /** Check if a running tool has been stuck for too long (5 minutes). */ const TOOL_TIMEOUT_MS = 5 * 60 * 1000; function isTimedOut(log: TraceLog): boolean { if (log.completed || log.approvalStatus === 'pending') return false; const elapsed = Date.now() - new Date(log.timestamp).getTime(); return elapsed > TOOL_TIMEOUT_MS; } // ── Status icon based on tool state ───────────────────────────────── function StatusIcon({ log }: { log: TraceLog }) { // Awaiting approval if (log.approvalStatus === 'pending') { return ; } // Rejected if (log.approvalStatus === 'rejected') { return ; } // Timed out if (isTimedOut(log)) { return ; } // Running (not completed yet) if (!log.completed) { return ( ); } // Failed if (log.success === false) { return ; } // Completed successfully return ; } // ── Status chip label ─────────────────────────────────────────────── function statusLabel(log: TraceLog): string | null { if (log.approvalStatus === 'pending') return 'awaiting approval'; if (log.approvalStatus === 'rejected') return 'rejected'; if (isTimedOut(log)) return 'timed out'; if (!log.completed) return 'running'; return null; } function statusColor(log: TraceLog): string { if (log.approvalStatus === 'pending') return 'var(--accent-yellow)'; if (log.approvalStatus === 'rejected') return 'var(--accent-red)'; if (isTimedOut(log)) return 'var(--muted-text)'; return 'var(--accent-yellow)'; } // ── Inline approval UI ────────────────────────────────────────────── function InlineApproval({ log, onResolve, }: { log: TraceLog; onResolve: (toolCallId: string, approved: boolean, feedback?: string) => void; }) { const [feedback, setFeedback] = useState(''); return ( {/* Tool description */} {log.tool === 'hf_jobs' && log.args && ( Execute {log.tool} on{' '} {String(log.args.hardware_flavor || 'default')} {!!log.args.timeout && ( <> with timeout {String(log.args.timeout)} )} )} {/* Feedback + buttons */} setFeedback(e.target.value)} variant="outlined" sx={{ '& .MuiOutlinedInput-root': { bgcolor: 'rgba(0,0,0,0.15)', fontFamily: 'inherit', fontSize: '0.8rem', }, }} /> onResolve(log.toolCallId || '', false, feedback || 'Rejected by user')} disabled={!feedback} size="small" sx={{ color: 'var(--accent-red)', border: '1px solid rgba(255,255,255,0.05)', borderRadius: '6px', '&:hover': { bgcolor: 'rgba(224,90,79,0.1)', borderColor: 'var(--accent-red)' }, '&.Mui-disabled': { color: 'rgba(255,255,255,0.1)' }, }} > ); } // ── Main component ────────────────────────────────────────────────── export default function ToolCallGroup({ tools }: ToolCallGroupProps) { const { showToolOutput, setPanelTab, setActivePanelTab, clearPanelTabs } = useAgentStore(); const { setRightPanelOpen, setLeftSidebarOpen } = useLayoutStore(); const { activeSessionId } = useSessionStore(); const handleClick = useCallback( (log: TraceLog) => { // For hf_jobs with scripts, use tab system if (log.tool === 'hf_jobs' && log.args?.script) { clearPanelTabs(); setPanelTab({ id: 'script', title: 'Script', content: String(log.args.script), language: 'python', }); if (log.output) { setPanelTab({ id: 'output', title: 'Output', content: log.output, language: 'markdown', }); } if (log.jobLogs) { setPanelTab({ id: 'logs', title: 'Logs', content: log.jobLogs, language: 'text', }); } // Default to output if it exists (most useful), otherwise script setActivePanelTab(log.output ? 'output' : 'script'); setRightPanelOpen(true); setLeftSidebarOpen(false); return; } // Show output if completed, or args if still running if (log.completed && log.output) { showToolOutput(log); } else if (log.args) { const content = JSON.stringify(log.args, null, 2); showToolOutput({ ...log, output: content }); } else { return; } setRightPanelOpen(true); }, [showToolOutput, setRightPanelOpen, setLeftSidebarOpen, clearPanelTabs, setPanelTab, setActivePanelTab], ); const handleApprovalResolve = useCallback( async (toolCallId: string, approved: boolean, feedback?: string) => { if (!activeSessionId) return; try { const res = await apiFetch('/api/approve', { method: 'POST', body: JSON.stringify({ session_id: activeSessionId, approvals: [{ tool_call_id: toolCallId, approved, feedback: approved ? null : feedback || 'Rejected by user', }], }), }); if (res.ok) { // Optimistic update: immediately reflect approval status in the UI const { updateTraceLog, updateCurrentTurnTrace, setProcessing } = useAgentStore.getState(); updateTraceLog(toolCallId, '', { approvalStatus: approved ? 'approved' : 'rejected', completed: !approved, // Rejected tools are done; approved ones will run }); updateCurrentTurnTrace(activeSessionId); if (approved) setProcessing(true); } } catch (e) { logger.error('Approval failed:', e); } }, [activeSessionId], ); return ( }> {tools.map((log) => { const clickable = (log.completed && !!log.output) || !!log.args; const label = statusLabel(log); const isPendingApproval = log.approvalStatus === 'pending'; return ( {/* Main tool row */} !isPendingApproval && handleClick(log)} sx={{ px: 1.5, py: 1, cursor: isPendingApproval ? 'default' : clickable ? 'pointer' : 'default', transition: 'background-color 0.15s', '&:hover': clickable && !isPendingApproval ? { bgcolor: 'var(--hover-bg)' } : {}, }} > {log.tool} {label && ( )} {clickable && !isPendingApproval && ( )} {/* Job status + link row */} {(log.jobUrl || log.jobStatus) && ( {log.jobStatus && ( {log.jobStatus} )} {log.jobUrl && ( e.stopPropagation()} sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.5, color: 'var(--accent-yellow)', fontSize: '0.68rem', textDecoration: 'none', '&:hover': { textDecoration: 'underline' }, }} > View on HF )} )} {/* Inline approval UI (only when pending) */} {isPendingApproval && ( )} ); })} ); }