React UI
Tool and human input
Combine tool-call rendering with approval and question controls in @anvia/react-ui.
Tool calls usually live inside assistant messages. Human-input controls usually live near the composer or in a side panel.
This recipe is UI composition only. For the event and decision route contract, see Human review end to end.
import { HumanInput, Message } from "@anvia/react-ui";
export function ToolMessageParts() {
return (
<Message.Parts>
{(part) =>
part.type === "tool" ? (
<Message.Part>
<Message.Tool className="tool-card" renderWhen="always">
<header className="tool-card-header">
<Message.ToolName />
<Message.ToolStatus />
</header>
<Message.ToolInput />
<Message.ToolOutput />
<Message.ToolError />
</Message.Tool>
</Message.Part>
) : (
<Message.Part />
)
}
</Message.Parts>
);
}
export function ReviewPanel() {
return (
<HumanInput.Panel className="review-panel">
<HumanInput.Status />
<HumanInput.Approvals className="approvals">
{(approval) => (
<HumanInput.Approval className="approval">
<strong>{approval.toolName}</strong>
{approval.args !== undefined ? <pre>{approval.args}</pre> : null}
<HumanInput.ApprovalReason placeholder="Reason for the audit log..." />
<div className="approval-actions">
<HumanInput.Reject>Reject</HumanInput.Reject>
<HumanInput.Approve>Approve</HumanInput.Approve>
</div>
</HumanInput.Approval>
)}
</HumanInput.Approvals>
<HumanInput.Questions className="questions">
<HumanInput.Question className="question">
<HumanInput.QuestionPrompt />
<HumanInput.QuestionSubmit>Submit answer</HumanInput.QuestionSubmit>
</HumanInput.Question>
</HumanInput.Questions>
</HumanInput.Panel>
);
}.tool-card,
.review-panel,
.approval,
.question {
border: 1px solid #3f3f46;
border-radius: 10px;
background: #18181b;
padding: 12px;
}
.tool-card,
.review-panel,
.approval,
.question {
display: grid;
gap: 10px;
}
.tool-card-header,
.approval-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}import { HumanInput, Message } from "@anvia/react-ui";
export function ToolMessageParts() {
return (
<Message.Parts>
{(part) =>
part.type === "tool" ? (
<Message.Part>
<Message.Tool className="grid gap-2.5 rounded-lg border border-zinc-700 bg-zinc-900 p-3" renderWhen="always">
<header className="flex items-center justify-between gap-2.5">
<Message.ToolName />
<Message.ToolStatus />
</header>
<Message.ToolInput />
<Message.ToolOutput />
<Message.ToolError />
</Message.Tool>
</Message.Part>
) : (
<Message.Part />
)
}
</Message.Parts>
);
}
export function ReviewPanel() {
return (
<HumanInput.Panel className="grid gap-2.5 rounded-lg border border-zinc-700 bg-zinc-900 p-3">
<HumanInput.Status />
<HumanInput.Approvals className="grid gap-2.5" />
<HumanInput.Questions className="grid gap-2.5" />
</HumanInput.Panel>
);
}Pending-only activity
Render a compact activity strip by filtering message parts to pending tools.
<Message.Parts filter={(part) => part.type === "tool" && part.state !== "output-available"}>
<Message.Part>
<Message.Tool renderWhen="pending" className="tool-activity" />
</Message.Part>
</Message.Parts>
Persistent review lane
Use keepMounted when empty approval or question wrappers should still occupy a stable layout area.
<aside className="review-lane">
<HumanInput.Status />
<HumanInput.Approvals keepMounted />
<HumanInput.Questions keepMounted />
</aside>