Installation
npx shadcn@latest add @pixeldosa/agent-askRequires `cn` at `@/lib/utils`. MCP's elicitation spec requires that a client show which server is asking and offer both decline and cancel — `source` is therefore a required prop, and you should wire `onDecline` and `onCancel` rather than omitting them, even though the component tolerates their absence for non-MCP uses.
Usage
"use client";
import { AgentAsk } from "@/components/ui/agent-ask";
<AgentAsk
source="finance-db"
question="Which quarter should I reconcile?"
explanation="Two datasets match “Q3 revenue” and they disagree by $4,200."
fields={[
{
name: "dataset",
type: "select",
label: "Dataset",
required: true,
options: [
{ value: "final", label: "Q3 Revenue Final.xlsx", hint: "Edited 3 days ago" },
{ value: "draft", label: "Q3 Revenue.xlsx", hint: "Edited 6 weeks ago" },
],
},
]}
onRespond={(values) => resume(values)}
onDecline={() => resume(null)}
onCancel={() => abort()}
/>
Features
- The asker is named, and it's required. MCP's elicitation spec obliges a client to show which server is requesting input. It's also the only basis a user has for judging whether the question is safe to answer — "some software wants your data" isn't answerable.
- Refusal is two answers, not one. Don't answer continues without the information; Cancel abandons the operation. Collapsing them would silently turn a narrow refusal into an abort.
- Options over prose. A select renders as labelled cards with hints; a confirm renders explicit Yes/No. A question with buttons is answerable in a second — the same question in a text box is homework.
- It holds perfectly still. This is the user's turn, so nothing moves. Pair it with
AgentPresence state="asking". - Submit is gated until every required field is genuinely answered.
Variants
Disambiguation
The case elicitation is mostly used for — two plausible matches, no safe guess. The hints are what make it answerable without opening either file.
Missing parameter
Confirm
Explicit Yes/No rather than a checkbox — an unchecked box is ambiguous between "no" and "not answered", which matters when submit is gated on being answered.
Examples
Wiring to MCP elicitation — the server pauses, you collect, you resume with the echoed state:
const [request, setRequest] = React.useState<InputRequiredResult | null>(null);
{request ? (
<AgentAsk
source={request.serverName}
question={request.prompt}
fields={toFields(request.inputRequests)}
onRespond={(values) => resumeCall({ inputResponses: values, requestState: request.requestState })}
onDecline={() => resumeCall({ inputResponses: null, requestState: request.requestState })}
onCancel={() => abortCall()}
/>
) : null}
Pairing with the presence indicator, so the ambient state and the question agree:
<AgentPresence state="asking" form="line" size="sm" />
<AgentAsk source={server} question={prompt} fields={fields} onRespond={resume} />
Props
| Prop | Type | Default | Description |
|---|---|---|---|
question | string | — | Required. Targeted and answerable. |
source | string | — | Required. Who is asking. |
fields | AskField[] | — | Required. text, number, confirm, or select. |
explanation | string | — | Context that makes the question answerable. |
onRespond | (values) => void | — | Required. |
onDecline | () => void | — | Refuse this question, continue without it. |
onCancel | () => void | — | Abandon the operation. |
submitLabel | string | "Send" | |
headingLevel | 2 | 3 | 4 | 5 | 6 | 3 | Heading level, so the component fits your page's outline. |
Accessibility
- A real
<form>labelled by the question, with real labels on every control and a<fieldset>/<legend>around each option group. - Yes/No use
aria-pressedso the chosen answer is exposed, and the tri-state (yes / no / unanswered) stays distinguishable. - Every control's hit area reaches ~44px while staying visually compact.
- Text inputs are 16px on small viewports, so iOS Safari doesn't zoom the page on focus.
- Selection is shown by border and background, never colour alone.
When not to use
Approving a consequential action the agent has already decided on is AI Approval Gate
— that's "may I do this?", where this is "which one did you mean?". The agent's ambient
activity is Agent Presence. And this is not a chat input: no thread, no history.
why it's built this way
Engineering Notes
Three decisions separate this from a chat prompt asking the same question. First, provenance is a required prop rather than an optional nicety: MCP obliges a client to show which server is requesting input, and it is also the only basis a user has for judging whether the question is safe to answer at all. Second, refusal is modelled as two distinct outcomes instead of one dismissal — declining answers 'not this information' while cancelling answers 'not this operation', and collapsing them would silently convert a narrow refusal into an abort. Third, the field types deliberately bias toward concrete options: `select` renders as labelled radio cards carrying optional hints (the disambiguation case, which is what elicitation is mostly used for, is far better served by 'Q3 Revenue Final.xlsx — edited 3 days ago' than by a text box), and `confirm` renders explicit Yes/No buttons rather than a checkbox, since a checkbox's unchecked state is ambiguous between 'no' and 'not answered'. That ambiguity matters here because submit is gated on required fields actually being answered. The component deliberately does not compose AI Action Toolbar for its actions despite the system's compose-don't-reinvent rule: the primary control is a real form submit button, and wrapping form submission inside a `role="toolbar"` would misreport the semantics to assistive technology for the sake of visual consistency. Text inputs are 16px on small viewports because anything smaller makes iOS Safari zoom the page on focus.
how it moves, and when it doesn't
Motion Notes
Almost none, and that is the design. This is a user-turn surface under the turn-taking grammar (DESIGN.md §3a), which means it holds perfectly still — the absence of motion is precisely what signals that nothing proceeds until the user acts, and animating it would put it back in the same visual register as the machine's own busy states. The only motion present is the shared colour transition on hover, press and focus at `--pd-duration-instant`, matching every other control in the system, and it is dropped entirely under `prefers-reduced-motion` while the selected-state border and background still communicate which option is chosen. Entrance animation belongs to whatever surface reveals this component, not to the component itself — the same rule Confidence Meter and AI Action Toolbar hold.