Tools and tasks
A tool call rendered as what it is: a state machine with parameters going in and a result or
an error coming back. Confirmation is the gate in front of the ones that should not run
unasked.
Import
import {
Tool,
ToolContent,
ToolHeader,
ToolInput,
ToolOutput,
} from '@elirobinson/ai-elements/components/tool';
import {
Confirmation,
ConfirmationAction,
ConfirmationActions,
ConfirmationRequest,
ConfirmationTitle,
} from '@elirobinson/ai-elements/components/confirmation';
import { Task, TaskContent, TaskItem, TaskTrigger } from '@elirobinson/ai-elements/components/task';The states
ToolHeader takes the state off the AI SDK's tool part and renders a badge for it. Seven
states, each with its own label and icon: input-streaming (Pending), input-available
(Running), output-available (Completed), output-error (Error), approval-requested
(Awaiting Approval), approval-responded (Responded) and output-denied (Denied).
ToolInput renders whatever input you hand it regardless of state, so a call that was
denied still shows what was declined as long as the panel contains one. ToolOutput renders
nothing at all when it has neither an output nor an errorText.
Everything is escaped
Tool arguments and results are never treated as markup:
ToolInputruns the input throughJSON.stringifyand hands the string toCodeBlock.ToolOutputdoes the same for an object, treats a string as code, and puts anything else in a plain<div>as React children.errorTextgoes into a<div>as a text child.CodeBlockbuilds React token elements from Shiki's tokenizer and renders them into<pre><code>. There is nodangerouslySetInnerHTMLon this path.
Model-authored content reaching this component is rendered as text, not as HTML.
Confirmation
Confirmation renders null unless it has an approval and the state has moved past
input-streaming / input-available — so the gate cannot appear before there is something
to approve. ConfirmationRequest, ConfirmationAccepted and ConfirmationRejected each
show only in the states they are named for, so the same tree covers the ask and both answers.
ConfirmationActionis a real<button type="button">with no dense classification, sobutton-floorapplies: 44×44, not the 32px its ownh-8asks for.- Nothing here moves focus. No component in this family sets
autoFocusor callsfocus(), so nothing is focused when the gate appears and the buttons enter the tab order in the order you render them. Which action a keyboard user reaches first is your DOM order, and it is worth deciding on purpose. ConfirmationActiondoes not mergeclassName— it spreads props after its own, so aclassNameyou pass replaces its sizing rather than adding to it.Confirmationrenders the sharedAlert, which setsrole="alert"— an assertive live region. So the gate announces itself on insertion whateveraria-liveyou put on the thread around it. It is the only component in this family that lands inside a live region, and it is the one where that is probably right.
Task
Searched the codebase
Task is a collapsible that starts open. TaskTrigger renders its default content with
asChild, and that default content is a <div> — it takes the click handler and the
expanded state, but a <div> is not a tab stop, so the default trigger cannot be reached or
operated from the keyboard. Pass a <button> as its child when the disclosure has to work
for everyone.
What this system changed
| Patch | What changed, and why |
|---|---|
button-floor | primary — ui/button.tsx — every <Button> in the tree. Measured default 36px, sm 32px, icon 36x36, icon-sm 32x32, icon-lg 40x40. The floor every vendored button gets unless it is classified dense elsewhere in this list. shadcn sizes at 32-40px, which clears AA and misses this system’s AAA default; the `not-data-[touch-target=dense]` guard is what keeps that a default rather than a blanket, and it cannot be undone by a consumer’s className the way a merged utility could. |
Read from vendoredElementTargets in @elirobinson/ai-patterns/contracts, which the browser audit writes.
No entry names tool.tsx, confirmation.tsx or task.tsx: every control in this family is
a shared primitive, and the floor it gets is the one above.