diff --git a/src/application/components/chat/ChatMessage.tsx b/src/application/components/chat/ChatMessage.tsx index 679c237..2c233fb 100644 --- a/src/application/components/chat/ChatMessage.tsx +++ b/src/application/components/chat/ChatMessage.tsx @@ -20,6 +20,7 @@ interface ChatMessageProps { agentName: string; threadId?: string; events?: TraceEvent[]; + isLast?: boolean; } interface SubagentTimeline { @@ -89,7 +90,13 @@ function formatTimestamp(ts: string): string { }).format(date); } -function ChatMessageImpl({ message, agentName, threadId, events }: Readonly) { +function ChatMessageImpl({ + message, + agentName, + threadId, + events, + isLast, +}: Readonly) { const isHuman = message.role === MessageRole.HUMAN; const isAi = message.role === MessageRole.AI; const isAwaitingHitl = message.status === MessageStatus.AWAITING_HITL; @@ -103,6 +110,10 @@ function ChatMessageImpl({ message, agentName, threadId, events }: Readonly (events ? parentToolEvents(events) : { toolCalls: [], toolResults: [] }), [events], ); + const hitlDecisionEvents = useMemo( + () => (events ? events.filter((e) => e.type === TraceEventType.HITL_DECISION) : []), + [events], + ); if (isHuman) { return ( @@ -160,6 +171,27 @@ function ChatMessageImpl({ message, agentName, threadId, events }: Readonly + {hitlDecisionEvents.map((ev) => { + const action = ev.name ?? ""; + let label = "✎ Edited"; + let className = "border-accent text-accent"; + if (action === "approve") { + label = "✓ Approved"; + className = "border-success text-success"; + } else if (action === "reject") { + label = ev.content ? `✗ Rejected: ${ev.content}` : "✗ Rejected"; + className = "border-danger text-danger"; + } + return ( + + {label} + + ); + })} + {parentTools.toolCalls.map((call) => ( - {isAwaitingHitl && message.tool_calls?.length && threadId && ( + {isAwaitingHitl && !!message.tool_calls?.length && !!threadId && isLast && ( )} @@ -213,7 +245,8 @@ const ChatMessage = memo(ChatMessageImpl, (prev, next) => { prev.agentName === next.agentName && prev.threadId === next.threadId && prev.message === next.message && - prev.events === next.events + prev.events === next.events && + prev.isLast === next.isLast ); }); diff --git a/src/application/components/chat/HITLReviewPanel.tsx b/src/application/components/chat/HITLReviewPanel.tsx index da12d92..26db721 100644 --- a/src/application/components/chat/HITLReviewPanel.tsx +++ b/src/application/components/chat/HITLReviewPanel.tsx @@ -4,133 +4,202 @@ import { Button } from "@/application/components/ui/button"; import { Input } from "@/application/components/ui/input"; import { useSendMessage } from "@/application/hooks/chat/useSendMessage"; import type { ToolCall } from "@/domain/entities/chat/message"; +import type { HitlDecisionInput } from "@/domain/entities/chat/chatRequest"; interface HITLReviewPanelProps { toolCalls: ToolCall[]; threadId: string; } -type ReviewState = "idle" | "reviewing" | "rejecting"; +type DecisionAction = "approve" | "reject"; -export default function HITLReviewPanel({ toolCalls, threadId }: Readonly) { - const [reviewState, setReviewState] = useState("idle"); - const [rejectReason, setRejectReason] = useState(""); - const sendMessage = useSendMessage(threadId); - - const toolName = toolCalls[0]?.name ?? "Unknown tool"; - const toolCallId = toolCalls[0]?.id ?? ""; - - function handleApprove() { - sendMessage.mutate({ - tool_call_id: toolCallId, - action: "approve", - }); - } - - function handleReject() { - if (reviewState !== "rejecting") { - setReviewState("rejecting"); - return; - } - - sendMessage.mutate( - { - tool_call_id: toolCallId, - action: "reject", - reason: rejectReason || "Rejected by user", - }, - { - onSuccess: () => { - setReviewState("idle"); - setRejectReason(""); - }, - }, - ); - } - - if (reviewState === "idle") { - return ( -
-
-
-
- -
-
- ); - } +interface ToolDecisionRowProps { + toolCall: ToolCall; + isRejecting: boolean; + rejectReason: string; + isPending: boolean; + onApprove: (id: string) => void; + onReject: (id: string) => void; + onReasonChange: (id: string, reason: string) => void; + onConfirmReject: () => void; +} +function ToolDecisionRow({ + toolCall, + isRejecting, + rejectReason, + isPending, + onApprove, + onReject, + onReasonChange, + onConfirmReject, +}: Readonly) { return ( -
-
-