import React, { useCallback, useMemo } from 'react' import type { ChatMessageMeta, HumanEscalationOption } from '../types/chat' import { MarkdownBody } from './MarkdownBody' interface EscalationPanelProps { meta: ChatMessageMeta onReply: (text: string) => void responded: boolean } function firstLine(text: string): string { return text.split('\n').map((line) => line.trim()).find(Boolean) ?? text } function checkpointStatusLabel(status: string): string { switch (status) { case 'timeout': case 'timed_out': case 'expired': return 'Expired' case 'stale': case 'invalid': return 'Inactive' case 'cancelled': case 'canceled': return 'Cancelled' case 'resolved': return 'Resolved' default: return 'Responded' } } export const EscalationPanel = React.memo(function EscalationPanel({ meta, onReply, responded, }: EscalationPanelProps) { const isResponded = responded const checkpointStatus = String(meta.checkpoint_status ?? '').trim().toLowerCase() const resolvedLabel = checkpointStatusLabel(checkpointStatus) const prompt = String(meta.prompt ?? meta.summary ?? '') const lines = useMemo( () => prompt.split('\n').map((line) => line.trim()).filter(Boolean), [prompt], ) const title = firstLine(prompt).replace(/^\[[^\]]+\]\s*/, '') || 'Action Required' const details = lines.slice(1).join('\n').trim() const summary = String(meta.summary ?? '').trim() const options = (meta.options ?? []).filter((opt): opt is HumanEscalationOption => !!opt?.id) const activeSubagents = useMemo( () => (meta.active_subagents ?? []).filter((item) => !!item && typeof item === 'object'), [meta.active_subagents], ) const permissionRequests = useMemo( () => (meta.permission_requests ?? []).filter((item) => !!item && typeof item === 'object'), [meta.permission_requests], ) const worktreePath = String(meta.worktree_path ?? '').trim() const hasRuntimeState = activeSubagents.length > 0 || permissionRequests.length > 0 || !!worktreePath const handleReply = useCallback((option: HumanEscalationOption) => { if (isResponded) return onReply(option.label || option.id) }, [isResponded, onReply]) return (
{title}
{String(meta.escalation_type ?? 'decision_needed').replace(/_/g, ' ')} {isResponded && {resolvedLabel}}
{summary && summary !== title && (
Summary
)} {details && (
Request
)} {hasRuntimeState && (
Runtime State
{worktreePath &&
Worktree: {worktreePath}
} {activeSubagents.length > 0 &&
Active subagents: {activeSubagents.length}
} {permissionRequests.length > 0 &&
Pending permission records: {permissionRequests.length}
}
)} {!isResponded && options.length > 0 && (
{options.map((option) => ( ))}
)} {!isResponded && meta.default_action && (
Default on timeout: {meta.default_action}
)}
) })