Pacerelle Docs

Widgets

Integration samples for every supported widget in the JavaScript and Python SDKs.

Widgets are structured prompts inside a conversation. Use them when an agent needs approval, a choice, a form, progress feedback, a date/time value, or a file from the user.

Every widget uses the same pattern:

  1. Send the widget from the agent handler.
  2. Wait for the user to answer in the app.
  3. Read the answer from the next message's widget response.
  4. Continue the local task.

Handle widget responses

JavaScript

client.onMessage(async (message, agent) => {
  if (!message.widgetResponse) return;

  const { ref, value, cancelled } = message.widgetResponse;
  if (cancelled) {
    await agent.sendMessage({
      conversationId: message.conversationId,
      to: message.from,
      text: `Cancelled: ${ref}`,
    });
    return;
  }

  console.log(ref, value);
});

Python

async def handle(message, agent):
    if not message.widget_response:
        return

    response = message.widget_response
    if response.cancelled:
        await agent.send_message(
            conversation_id=message.conversation_id,
            to=message.from_id,
            text=f"Cancelled: {response.ref}",
        )
        return

    print(response.ref, response.value)

Confirmation

Use a confirmation widget before destructive or expensive work.

JavaScript

await agent.sendConfirmWidget({
  conversationId: message.conversationId,
  to: message.from,
  id: "confirm-delete-cache",
  title: "Delete local cache?",
  body: "The agent will remove generated cache files from this machine.",
  danger: true,
  labels: { yes: "Delete", no: "Keep files" },
});

Python

await agent.send_confirm_widget(
    conversation_id=message.conversation_id,
    to=message.from_id,
    widget_id="confirm-delete-cache",
    title="Delete local cache?",
    body="The agent will remove generated cache files from this machine.",
    danger=True,
    labels={"yes": "Delete", "no": "Keep files"},
)

Response value

true

Handle the response

if (message.widgetResponse?.ref === "confirm-delete-cache") {
  if (message.widgetResponse.value === true) await deleteCache();
}

Choice

Use choice when the user must select one or more options.

JavaScript

await agent.sendChoiceWidget({
  conversationId: message.conversationId,
  to: message.from,
  id: "choose-export-format",
  title: "Choose export format",
  body: "The agent will generate the selected file type.",
  options: [
    { id: "pdf", label: "PDF" },
    { id: "docx", label: "Word document" },
    { id: "md", label: "Markdown" },
  ],
});

Python

await agent.send_choice_widget(
    conversation_id=message.conversation_id,
    to=message.from_id,
    widget_id="choose-export-format",
    title="Choose export format",
    body="The agent will generate the selected file type.",
    options=[
        {"id": "pdf", "label": "PDF"},
        {"id": "docx", "label": "Word document"},
        {"id": "md", "label": "Markdown"},
    ],
)

Response value

"pdf"

Multiple choices

For multiple choices, set multi: true in JavaScript or pass multi=True in Python:

await agent.sendChoiceWidget({
  conversationId: message.conversationId,
  to: message.from,
  id: "choose-checks",
  title: "Choose checks to run",
  multi: true,
  options: [
    { id: "lint", label: "Lint" },
    { id: "test", label: "Tests" },
    { id: "build", label: "Build" },
  ],
});
await agent.send_choice_widget(
    conversation_id=message.conversation_id,
    to=message.from_id,
    widget_id="choose-checks",
    title="Choose checks to run",
    multi=True,
    options=[
        {"id": "lint", "label": "Lint"},
        {"id": "test", "label": "Tests"},
        {"id": "build", "label": "Build"},
    ],
)

Multiple-choice response value:

["lint", "test"]

Permission

Use permission when the user must grant a scope for an action.

JavaScript

await agent.sendPermissionWidget({
  conversationId: message.conversationId,
  to: message.from,
  id: "allow-folder-read",
  title: "Allow folder read?",
  body: "The agent wants to inspect files in ./docs before answering.",
  scopes: ["once", "session"],
});

Python

await agent.send_permission_widget(
    conversation_id=message.conversation_id,
    to=message.from_id,
    widget_id="allow-folder-read",
    title="Allow folder read?",
    body="The agent wants to inspect files in ./docs before answering.",
    scopes=["once", "session"],
)

Response value

{ "granted": true, "scope": "once" }

Handle the response

if (message.widgetResponse?.ref === "allow-folder-read") {
  const permission = message.widgetResponse.value as { granted?: boolean; scope?: string };
  if (!permission.granted) return;

  await readDocsFolder({ scope: permission.scope });
}

Form

Use a form when the agent needs typed fields instead of a free-text answer.

JavaScript

await agent.sendFormWidget({
  conversationId: message.conversationId,
  to: message.from,
  id: "release-form",
  title: "Release details",
  body: "Fill this before the agent starts the release task.",
  submitLabel: "Start release",
  fields: [
    { name: "version", label: "Version", type: "text", required: true },
    { name: "priority", label: "Priority", type: "number", min: 1, max: 5 },
    { name: "notes", label: "Notes", type: "textarea" },
    { name: "announce", label: "Announce in chat", type: "checkbox" },
  ],
});

Python

await agent.send_form_widget(
    conversation_id=message.conversation_id,
    to=message.from_id,
    widget_id="release-form",
    title="Release details",
    body="Fill this before the agent starts the release task.",
    submitLabel="Start release",
    fields=[
        {"name": "version", "label": "Version", "type": "text", "required": True},
        {"name": "priority", "label": "Priority", "type": "number", "min": 1, "max": 5},
        {"name": "notes", "label": "Notes", "type": "textarea"},
        {"name": "announce", "label": "Announce in chat", "type": "checkbox"},
    ],
)

Response value

{
  "version": "1.2.0",
  "priority": 3,
  "notes": "Ship after QA signs off.",
  "announce": true
}

Progress

Use progress for work that takes more than a few seconds.

JavaScript

const progressId = await agent.sendProgressWidget({
  conversationId: message.conversationId,
  to: message.from,
  id: "build-progress",
  title: "Building project",
  body: "The agent is running install, tests, and build.",
  value: 10,
  max: 100,
  cancellable: true,
});

await agent.sendWidgetUpdate({
  conversationId: message.conversationId,
  to: message.from,
  ref: progressId,
  spec: { value: 75, body: "Running tests" },
});

Python

progress_id = await agent.send_progress_widget(
    conversation_id=message.conversation_id,
    to=message.from_id,
    widget_id="build-progress",
    title="Building project",
    body="The agent is running install, tests, and build.",
    value=10,
    max=100,
    cancellable=True,
)

await agent.send_widget_update(
    conversation_id=message.conversation_id,
    to=message.from_id,
    ref=progress_id,
    spec={"value": 75, "body": "Running tests"},
)

Cancellation response

{ "cancelled": true }

Date and Time

Use date/time when the user must choose a calendar or schedule value.

JavaScript

await agent.sendDateTimeWidget({
  conversationId: message.conversationId,
  to: message.from,
  id: "schedule-review",
  title: "Schedule review",
  body: "Pick a review date.",
  mode: "date",
  min: "2026-05-23",
});

Python

await agent.send_datetime_widget(
    conversation_id=message.conversation_id,
    to=message.from_id,
    widget_id="schedule-review",
    title="Schedule review",
    body="Pick a review date.",
    mode="date",
    min="2026-05-23",
)

Modes

Use mode: "time" for a time picker and mode: "datetime" for a date-time picker.

Response value:

{ "mode": "date", "value": "2026-05-24" }

File Picker

Use file picker when the user must provide local files to the agent.

JavaScript

await agent.sendFilePickerWidget({
  conversationId: message.conversationId,
  to: message.from,
  id: "upload-brief",
  title: "Upload project brief",
  body: "Attach the PDF or Markdown brief the agent should read.",
  multiple: true,
  accept: ["application/pdf", ".md"],
  maxFiles: 3,
});

Python

await agent.send_file_picker_widget(
    conversation_id=message.conversation_id,
    to=message.from_id,
    widget_id="upload-brief",
    title="Upload project brief",
    body="Attach the PDF or Markdown brief the agent should read.",
    multiple=True,
    accept=["application/pdf", ".md"],
    max_files=3,
)

Response value

{
  "files": [
    {
      "id": "attachment-id",
      "name": "brief.pdf",
      "mime": "application/pdf",
      "size": 48291,
      "blob_id": "blob-id"
    }
  ]
}

Handle uploaded files

The SDK also exposes uploaded attachments on the message, so your handler can process the files after the user submits the widget.

JavaScript:

if (message.widgetResponse?.ref === "upload-brief") {
  for (const file of message.attachments) {
    console.log(file.name, file.mime, file.size);
  }
}

Python:

if message.widget_response and message.widget_response.ref == "upload-brief":
    for file in message.attachments or []:
        print(file.name, file.mime, file.size)

Design rules for agents

  • Use confirmation widgets for destructive or irreversible actions.
  • Use permission widgets before reading local folders, writing files, or calling external services.
  • Keep form fields short and explicit.
  • Report progress for work that takes more than a few seconds.
  • Treat file uploads as sensitive input and validate type and size locally.
  • Do not ask for secrets in a widget unless the user explicitly expects that flow.
  • Give every widget a stable id so responses are easy to correlate.