Skip to content

Approval and Question Cards ​

The cards Chat draws under an assistant message when a turn pauses: ToolApprovalCard for a tool call that needs a decision, and QuestionCard for questions the assistant asks. MessageActionCards draws every card of a message. Use them directly in a custom layout, or replace them with renderToolApproval / renderQuestion.

New in 0.4.0

Everything on this page is new. For the flow (what the person sees, deadlines, history, errors) read Approvals and Questions.

An approval card: the tool name, the time left, Details, a remember checkbox and Deny and Approve

Import ​

tsx
import {
  ToolApprovalCard,
  QuestionCard,
  MessageActionCards,
  type ToolApprovalCardProps,
  type QuestionCardProps,
  type ChatActionsConfig,
} from "@cognipeer/chat-ui";

ToolApprovalCard ​

tsx
<ToolApprovalCard
  action={{ id: "act_1", type: "tool_approval", toolName: "send_email", args: { to: "a@example.com" }, expiresAt: "2026-10-12T18:00:00Z" }}
  allowArgumentEdit
  onResolve={(resolution) => resolveAction("act_1", resolution)}
/>
PropTypeDefaultDescription
actionPendingAction of type: "tool_approval"requiredThe request: toolName, title, summary, args, argsSchema, expiresAt
stateActionStatependingWhere the card is in its life: pending, resolving, resolved, cancelled, expired, with the resolution when known
readOnlybooleanfalseThis app cannot answer: shows "Waiting for approval in another app" instead of the buttons
allowArgumentEditbooleanfalse"Details" becomes a JSON editor (only when the action has arguments)
expiryWarningSecondsnumber600The countdown turns amber below this
autoFocusbooleanfalseMove focus to the card when it mounts (the card itself, never a button that decides)
onResolve(resolution) => voidrequiredCalled with { type: "tool_approval", decision, comment?, approvedArgs?, rememberForSession? }
classNamestring-Container class

The card shows the title (title, else "Allow {tool}?"), the summary, the countdown, the collapsible arguments, a "Don't ask again for this tool in this chat" checkbox, "Add a reason" for a denial, and Deny / Approve. Approve is disabled while the edited arguments are not a valid JSON object or break argsSchema; the reasons are listed in a role="alert" list.

QuestionCard ​

tsx
<QuestionCard
  action={{
    id: "act_2",
    type: "user_question",
    questions: [{ question: "Which environment?", header: "Environment", options: [{ label: "staging" }, { label: "production", description: "Needs a window" }] }],
  }}
  onResolve={(resolution) => resolveAction("act_2", resolution)}
/>
PropTypeDefaultDescription
actionPendingAction of type: "user_question"requiredquestions (question, header, multiSelect, placeholder, required, options), allowFreeText, expiresAt
state, readOnly, expiryWarningSeconds, autoFocus, classNameAs above (autoFocus lands on the first control)
onResolve(resolution) => voidrequiredCalled with { type: "user_question", answers, cancelled? }

Behaviour: a lone single-choice question without previews answers as soon as an option is picked (Enter in its free-text field also sends); anything else collects answers and waits for Send. A question is required unless it says required: false (asterisk); Skip (which answers { answers: {}, cancelled: true }) is offered only when no question is required. An option with preview is a list row with a collapsed "Preview".

MessageActionCards and ChatActionsConfig ​

MessageActionCards draws a card for every action of a paused message (message.pendingActions except client_tool), reading how each ended from message.actionStates. ChatMessage and ChatMessageList use it; their actions prop is a ChatActionsConfig:

FieldDefaultEffect
onResolve(actionId, resolution)noneSends the answer. Without it the cards are read-only
canResolvetruefalse when the transport cannot answer: the cards are read-only
enableToolApprovals, enableQuestionstruefalse makes that kind read-only
allowArgumentEditfalseSee above
expiryWarningSeconds600See above
renderToolApproval, renderQuestionbuilt-in cardsReplace a card: they receive { action, state, readOnly, resolve }
focusActionIdsnoneCards for these action ids take focus when they mount (ChatMessageList sets it for cards that appeared while the list was on screen)
tsx
<ChatMessageList
  messages={chat.messages}
  actions={{
    onResolve: chat.resolveAction,
    canResolve: chat.canResolveActions,
    renderToolApproval: ({ action, resolve, readOnly }) => (
      <MyApprovalCard action={action} disabled={readOnly} onAllow={() => resolve({ type: "tool_approval", decision: "allow" })} />
    ),
  }}
/>

Accessibility ​

Cards are labelled groups. A new card is announced and takes focus (a question on its first control, an approval on the card itself), the outcome is a role="status" line, the editor's problems are a role="alert", and the countdown is plain text. See Accessibility.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub