diff --git a/apps/web/src/components/editor/panels/chat/message-bubble.tsx b/apps/web/src/components/editor/panels/chat/message-bubble.tsx index aed99f73..5bd90e75 100644 --- a/apps/web/src/components/editor/panels/chat/message-bubble.tsx +++ b/apps/web/src/components/editor/panels/chat/message-bubble.tsx @@ -4,26 +4,38 @@ import { cn } from "@/utils/ui"; import type { ChatMessage, ToolCall } from "@/agent/types"; import { isTranscriptData, formatTimestamp } from "./transcript-utils"; import type { TranscriptData } from "./transcript-utils"; -import { ToolCallCard } from "./tool-call-card"; -import { ToolResultCard } from "./tool-result-card"; +import { + ToolExecutionCard, + type ToolExecutionPair, +} from "./tool-execution-card"; interface MessageBubbleProps { message: ChatMessage; messages: ChatMessage[]; } -function findToolCallName( - messages: ChatMessage[], - toolCallId?: string, -): string | null { - if (!toolCallId) return null; +function buildToolCallPairs(messages: ChatMessage[]): ToolExecutionPair[] { + const pairs: ToolExecutionPair[] = []; + const resultIndex = new Map(); + for (const msg of messages) { - if (msg.role === "assistant" && msg.toolCalls) { - const match = msg.toolCalls.find((tc: ToolCall) => tc.id === toolCallId); - if (match) return match.name; + if (msg.role === "tool_result" && msg.toolCallId) { + resultIndex.set(msg.toolCallId, msg.content); } } - return null; + + for (const msg of messages) { + if (msg.role === "assistant" && msg.toolCalls) { + for (const tc of msg.toolCalls) { + pairs.push({ + toolCall: tc, + resultContent: resultIndex.get(tc.id), + }); + } + } + } + + return pairs; } function TranscriptCard({ data }: { data: TranscriptData }) { @@ -53,11 +65,25 @@ function TranscriptCard({ data }: { data: TranscriptData }) { ); } +function isToolResultAbsorbed( + message: ChatMessage, + allMessages: ChatMessage[], +): boolean { + if (message.role !== "tool_result" || !message.toolCallId) return false; + return allMessages.some( + (m) => + m.role === "assistant" && + m.toolCalls?.some((tc: ToolCall) => tc.id === message.toolCallId), + ); +} + export function MessageBubble({ message, messages }: MessageBubbleProps) { const isUser = message.role === "user"; if (message.role === "tool_result") { - const toolName = findToolCallName(messages, message.toolCallId); + if (isToolResultAbsorbed(message, messages)) { + return null; + } try { const parsed = JSON.parse(message.content); @@ -68,23 +94,9 @@ export function MessageBubble({ message, messages }: MessageBubbleProps) { ); } - } catch { - // not JSON - } + } catch {} - if (toolName) { - return ( -
- -
- ); - } - - return ( -
- -
- ); + return null; } const hasToolCalls = @@ -92,6 +104,18 @@ export function MessageBubble({ message, messages }: MessageBubbleProps) { message.toolCalls && message.toolCalls.length > 0; + const toolPairs = hasToolCalls + ? message.toolCalls!.map((tc: ToolCall) => { + const result = messages.find( + (m) => m.role === "tool_result" && m.toolCallId === tc.id, + ); + return { + toolCall: tc, + resultContent: result?.content, + }; + }) + : []; + return (
{message.content}

)} - {hasToolCalls && ( + {toolPairs.length > 0 && (
- {message.toolCalls?.map((tc: ToolCall) => ( - + {toolPairs.map((pair) => ( + ))}
)} diff --git a/apps/web/src/components/editor/panels/chat/tool-call-card.tsx b/apps/web/src/components/editor/panels/chat/tool-call-card.tsx deleted file mode 100644 index 6f634ca8..00000000 --- a/apps/web/src/components/editor/panels/chat/tool-call-card.tsx +++ /dev/null @@ -1,52 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { cn } from "@/utils/ui"; -import { formatToolCall } from "./tool-formatters"; -import type { ToolCall } from "@/agent/types"; - -interface ToolCallCardProps { - toolCall: ToolCall; -} - -export function ToolCallCard({ toolCall }: ToolCallCardProps) { - const [open, setOpen] = useState(false); - const summary = formatToolCall(toolCall.name, toolCall.args); - - return ( -
- - {open && ( -
-
-						{JSON.stringify(toolCall.args, null, 2)}
-					
-
- )} -
- ); -} diff --git a/apps/web/src/components/editor/panels/chat/tool-execution-card.tsx b/apps/web/src/components/editor/panels/chat/tool-execution-card.tsx new file mode 100644 index 00000000..1a9c50bb --- /dev/null +++ b/apps/web/src/components/editor/panels/chat/tool-execution-card.tsx @@ -0,0 +1,164 @@ +"use client"; + +import { useState } from "react"; +import { cn } from "@/utils/ui"; +import { formatToolCall, formatToolResult } from "./tool-formatters"; +import type { ToolCall } from "@/agent/types"; + +export interface ToolExecutionPair { + toolCall: ToolCall; + resultContent?: string; +} + +interface ToolExecutionCardProps { + pair: ToolExecutionPair; +} + +type Status = "pending" | "success" | "error"; + +function getStatus(resultContent?: string): Status { + if (resultContent === undefined) return "pending"; + try { + const parsed = JSON.parse(resultContent); + if (parsed && typeof parsed === "object" && "error" in parsed) return "error"; + } catch {} + return "success"; +} + +function StatusIcon({ status }: { status: Status }) { + if (status === "pending") { + return ( + + + + + ); + } + + if (status === "error") { + return ( + + + + ); + } + + return ( + + + + ); +} + +export function ToolExecutionCard({ pair }: ToolExecutionCardProps) { + const { toolCall, resultContent } = pair; + const [open, setOpen] = useState(false); + const status = getStatus(resultContent); + + const callSummary = formatToolCall(toolCall.name, toolCall.args); + const resultSummary = resultContent + ? formatToolResult(toolCall.name, resultContent) + : null; + + const hasResult = + status === "success" && resultContent !== undefined && resultContent !== "{}"; + + return ( +
+ + {open && ( +
+
+ + Params + +
+							{JSON.stringify(toolCall.args, null, 2)}
+						
+
+ {status === "error" && resultContent && ( +
+ + Error + +
+								{resultContent}
+							
+
+ )} + {hasResult && status === "success" && ( +
+ + Output + +
+								{(() => {
+									try {
+										return JSON.stringify(JSON.parse(resultContent!), null, 2);
+									} catch {
+										return resultContent;
+									}
+								})()}
+							
+
+ )} +
+ )} +
+ ); +} diff --git a/apps/web/src/components/editor/panels/chat/tool-result-card.tsx b/apps/web/src/components/editor/panels/chat/tool-result-card.tsx deleted file mode 100644 index 3f2e9063..00000000 --- a/apps/web/src/components/editor/panels/chat/tool-result-card.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { cn } from "@/utils/ui"; -import { formatToolResult } from "./tool-formatters"; - -interface ToolResultCardProps { - name: string; - content: string; -} - -export function ToolResultCard({ name, content }: ToolResultCardProps) { - const [open, setOpen] = useState(false); - const summary = formatToolResult(name, content); - - if (!summary) return null; - - return ( -
- - {open && ( -
-
-						{content}
-					
-
- )} -
- ); -}