Tool UI
Default tool cards, custom renderers, and pending approvals.
Tool calls are message parts. Render them with Assistant.Tool unless you need a custom card.
import { isPaiToolPart } from "@pai/react";
if (isPaiToolPart(part)) {
return (
<Assistant.Tool
key={part.toolCallId}
message={message}
part={part}
/>
);
}The default renderer is enough for "the model searched docs." Write a card when the output is a chart, a citation list, or an approval.
Custom cards
Register on the binding so every Assistant.Tool picks them up:
export const Assistant = Pai.agent("main", {
toolRenderers: {
searchDocs: (tool) => <SearchDocsCard tool={tool} />,
},
});Or pass toolRenderers on ThreadProvider for one view.
The card receives ToolRenderProps:
| Field | Meaning |
|---|---|
part | Native tool part (state, input, output, errorText, data, cancelled) |
runStatus | Liveness of the run that produced the call. Distinguishes execute-in-flight from a stranded input-available part. |
action | Set only while the tool is waiting on a person or a client tool |
Read tool progress from ToolData: tool.part.data?.progress?.at(-1)?.value.
function SearchDocsCard({
tool,
}: {
tool: ToolRenderProps<AssistantPai["agents"]["main"], "searchDocs">;
}) {
if (tool.part.state === "input-streaming") {
return <p>Searching…</p>;
}
if (tool.part.state === "output-error") {
return <p>{tool.part.errorText}</p>;
}
if (tool.part.state !== "output-available") {
return <p>Search: {tool.part.input?.query}</p>;
}
return (
<ul>
{tool.part.output.hits.map((hit) => (
<li key={hit.url}>{hit.title}</li>
))}
</ul>
);
}Use import type { AssistantPai } from the server contract file (or codegen). Do not import the agent runtime into the browser.
Approvals
When the tool suspends, tool.action is the pending action. Submit from the card:
const action = tool.action;
if (action) {
return (
<div>
<p>{action.input.summary}</p>
<button
type="button"
onClick={() => void action.submit({ approved: true })}
>
Publish
</button>
</div>
);
}Assistant.useAction(action) adds isPending / isSubmitting if the card needs local submit state. Keep that hook in a child that only mounts when action exists.
For a tray outside the transcript, scan with chat.getPendingAction(message, part).
See Human approval for the server tool.
Next
- React tests —
toolProps("searchDocs").outputAvailable(...) - Client tools