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.

Parameters

{  "path": "packages/tokens/src/tokens.css"}

Result

44px

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).

Parameters

{  "query": "touch target"}
Input streaming

Parameters

{  "query": "touch target"}

Result

3 matches in contracts.json.

Output available

Parameters

{  "query": "touch target"}

Error

The index is not built.
Output error

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:

  • ToolInput runs the input through JSON.stringify and hands the string to CodeBlock.
  • ToolOutput does the same for an object, treats a string as code, and puts anything else in a plain <div> as React children.
  • errorText goes into a <div> as a text child.
  • CodeBlock builds React token elements from Shiki's tokenizer and renders them into <pre><code>. There is no dangerouslySetInnerHTML on 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.

  • ConfirmationAction is a real <button type="button"> with no dense classification, so button-floor applies: 44×44, not the 32px its own h-8 asks for.
  • Nothing here moves focus. No component in this family sets autoFocus or calls focus(), 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.
  • ConfirmationAction does not merge className — it spreads props after its own, so a className you pass replaces its sizing rather than adding to it.
  • Confirmation renders the shared Alert, which sets role="alert" — an assertive live region. So the gate announces itself on insertion whatever aria-live you 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

Read packages/tokens/src/tokens.css

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

PatchWhat changed, and why
button-floorprimary — 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.