Display Tool Calls
This recipe shows how to display tool calls in your chat UI with loading states and results. You'll handle tool_call and tool_result events, then render them as rich UI components.
Handling Tool Call Events
First, set up onEvent to process tool_call and tool_result events:
import { useStreamChat } from "@deltakit/react";
const { messages } = useStreamChat({
api: "/api/chat",
onEvent: (event, { appendText, appendPart, setMessages }) => {
switch (event.type) {
case "text_delta":
appendText(event.delta);
break;
case "tool_call":
appendPart({
type: "tool_call",
tool_name: event.tool_name,
argument: event.argument,
callId: event.call_id,
});
break;
case "tool_result":
setMessages((msgs) => {
const last = msgs[msgs.length - 1];
if (last?.role !== "assistant") return msgs;
const updatedParts = last.parts.map((part) => {
if (part.type === "tool_call" && part.callId === event.call_id) {
return { ...part, result: event.output };
}
return part;
});
return [...msgs.slice(0, -1), { ...last, parts: updatedParts }];
});
break;
}
},
});When you provide onEvent, it replaces the default handler. You must handle text_delta yourself.
Rendering Parts
Build a switch component that renders each content part type:
import type { ContentPart } from "@deltakit/react";
function Part({ part }: { part: ContentPart }) {
switch (part.type) {
case "text":
return <p>{part.text}</p>;
case "tool_call":
return <ToolCall part={part} />;
case "reasoning":
return <em>{part.text}</em>;
}
}Use it in your message list:
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}:</strong>
{msg.parts.map((part, i) => (
<Part key={i} part={part} />
))}
</div>
))}Tool Call Component
A ToolCallPart has this shape:
interface ToolCallPart {
type: "tool_call";
tool_name: string; // e.g. "get_weather"
argument: string; // JSON string of arguments
callId?: string; // Links to tool_result
result?: string; // Filled when the result arrives
}Render it with a loading/result state:
import type { ToolCallPart } from "@deltakit/react";
function ToolCall({ part }: { part: ToolCallPart }) {
// Parse the JSON argument for display
let args: Record<string, unknown> = {};
try {
args = JSON.parse(part.argument);
} catch {
// ignore parse errors
}
return (
<div style={{ background: "#f5f5f5", padding: 12, borderRadius: 8, margin: "8px 0" }}>
<strong>{part.tool_name}</strong>
<pre style={{ fontSize: 12 }}>{JSON.stringify(args, null, 2)}</pre>
{part.result ? (
<div>Result: {part.result}</div>
) : (
<div style={{ color: "#888" }}>Running...</div>
)}
</div>
);
}Multiple Tool Calls
A single response can contain multiple tool calls. Each arrives as a separate tool_call/tool_result event pair, linked by callId:
data: {"type":"tool_call","tool_name":"search","argument":"...","call_id":"call_1"}
data: {"type":"tool_result","call_id":"call_1","output":"..."}
data: {"type":"tool_call","tool_name":"calculate","argument":"...","call_id":"call_2"}
data: {"type":"tool_result","call_id":"call_2","output":"..."}
data: {"type":"text_delta","delta":"Based on the results..."}
data: [DONE]All parts end up in the same assistant message's parts array, rendered in order.
Server Setup
See Tool Calls (Protocol) for the server-side SSE format and a full FastAPI example.