'use client'; import { useState, useRef, memo, useCallback } from 'react'; import { IconCheck, IconX, IconTerminal } from '@tabler/icons-react'; import { GridSpinner } from '@/components/grid-spinner'; import type { Message } from '@/lib/types/http'; import { Badge } from '@kandev/ui/badge'; import { ExpandableRow } from './expandable-row'; interface ScriptExecutionMetadata { script_type: 'setup' | 'cleanup' | 'agent_boot'; agent_name?: string; command: string; status: 'starting' | 'running' | 'exited' | 'failed'; exit_code?: number; is_resuming?: boolean; started_at?: string; completed_at?: string; process_id?: string; error?: string; } export const ScriptExecutionMessage = memo(function ScriptExecutionMessage({ comment }: { comment: Message }) { const metadata = comment.metadata as ScriptExecutionMetadata | undefined; const [manualExpandState, setManualExpandState] = useState(null); const prevStatusRef = useRef(metadata?.status); const status = metadata?.status; const scriptType = metadata?.script_type; // Reset manual state when status changes (allows auto-expand behavior to resume) if (prevStatusRef.current !== status) { prevStatusRef.current = status; if (manualExpandState !== null) { setManualExpandState(null); } } const isRunning = status === 'starting' || status === 'running'; const isSuccess = status === 'exited' && (metadata?.exit_code === 0 || metadata?.exit_code === undefined); // Auto-expand when running (matching tool-execute-message pattern) const autoExpanded = isRunning; const isExpanded = manualExpandState ?? autoExpanded; const handleToggle = useCallback(() => { setManualExpandState((prev) => !(prev ?? autoExpanded)); }, [autoExpanded]); // Fallback for missing metadata if (!metadata || !scriptType) { const hasExpandableContent = !!comment.content; return ( } header={
Script Execution (metadata unavailable)
} hasExpandableContent={hasExpandableContent} isExpanded={isExpanded} onToggle={handleToggle} > {comment.content && (
              {comment.content}
            
)}
); } const { command, exit_code, error } = metadata; const isAgentBoot = scriptType === 'agent_boot'; const isSetup = scriptType === 'setup'; const hasDuration = !!(metadata.started_at && metadata.completed_at); const hasExpandableContent = !!(command || comment.content || error || exit_code !== undefined || hasDuration); // Build header based on script_type let headerContent: React.ReactNode; if (isAgentBoot) { const agentName = metadata.agent_name || 'agent'; headerContent = (
{metadata.is_resuming ? (isSuccess ? 'Resumed' : isRunning ? 'Resuming' : 'Failed to resume') : (isSuccess ? 'Started' : isRunning ? 'Starting' : 'Failed to start') } agent {agentName} {isRunning && }
); } else { headerContent = (
{isSetup ? 'Setup' : 'Cleanup'} {command} {isRunning && } {isSuccess && !isRunning && ( )} {!isSuccess && !isRunning && ( )}
); } return ( } header={headerContent} hasExpandableContent={hasExpandableContent} isExpanded={isExpanded} onToggle={handleToggle} >
{/* Command (agent_boot only — setup/cleanup already show it in header) */} {isAgentBoot && command && (
Command
              {command}
            
)} {/* Output */} {comment.content && comment.content.trim() !== '' && (
Output
              {comment.content}
            
)} {/* Error message */} {error && (
Error
{error}
)} {/* Footer with exit code and duration */} {isAgentBoot && !isRunning && metadata.started_at && metadata.completed_at && (
Duration: {calculateDuration(metadata.started_at, metadata.completed_at)}
)} {!isAgentBoot && !isRunning && exit_code !== undefined && (
Exit code: {exit_code} {metadata.started_at && metadata.completed_at && ( Duration: {calculateDuration(metadata.started_at, metadata.completed_at)} )}
)}
); }); // Helper function to calculate duration function calculateDuration(startedAt: string, completedAt: string): string { try { const start = new Date(startedAt).getTime(); const end = new Date(completedAt).getTime(); const durationMs = end - start; if (durationMs < 1000) { return `${durationMs}ms`; } else if (durationMs < 60000) { return `${(durationMs / 1000).toFixed(1)}s`; } else { const minutes = Math.floor(durationMs / 60000); const seconds = Math.floor((durationMs % 60000) / 1000); return `${minutes}m ${seconds}s`; } } catch { return 'N/A'; } }