PixelDosa UI

Agent Ask

A structured question from an agent that needs input before it can continue — the UI surface for MCP elicitation, with the asker named and refusal made first-class.

finance-db is asking

Which quarter should I reconcile?

Two datasets match “Q3 revenue” and they disagree by $4,200.

Dataset
Include refunds?

Installation

bash
npx shadcn@latest add @pixeldosa/agent-ask

Requires `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.

finance-db is asking

Which quarter should I reconcile?

Two datasets match “Q3 revenue” and they disagree by $4,200.

Dataset

Missing parameter

calendar is asking

What date range should I summarise?

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.

mail-server is asking

Should I include archived threads?

Include archived

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

PropTypeDefaultDescription
questionstring—Required. Targeted and answerable.
sourcestring—Required. Who is asking.
fieldsAskField[]—Required. text, number, confirm, or select.
explanationstring—Context that makes the question answerable.
onRespond(values) => void—Required.
onDecline() => void—Refuse this question, continue without it.
onCancel() => void—Abandon the operation.
submitLabelstring"Send"
headingLevel2 | 3 | 4 | 5 | 63Heading 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-pressed so 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.