Interactive keyboard-first questionnaire for Pi Coding Agent. Presents the user 1–4 structured questions with options, custom answers, and a review tab before final submission.
Inspired by the AskUserQuestion tool from Claude Code / OpenCode / Codex CLI.
# Install from npm (recommended)
pi install npm:@nguyenquangthai/pi-ask
# Or pin a Git commit/tag
pi install git:github.com/QuangThai/pi-ask@v0.1.0
# Local development
pi install ./Prerequisites: Pi >=0.80.7 and Node.js >=20.6.0.
The tool (ask_user_question) makes the LLM pause and show a keyboard-driven dialog:
- Questions tab — each question has a header, optional context, and 2–4 options
- Recommended — options marked
recommended: trueare moved to the top and show a(Recommended)hint; they are never pre-selected - Other — pick "Other — add your own answer" to enter free text via the inline editor
- Multi-select —
Spaceto toggle,Enterto confirm - Review tab — see all answers before submitting; navigate back to any tab to edit
- Keyboard navigation —
↑↓move,Enterconfirm,Spacetoggle,←→/Tabswitch tabs,Escdismiss
ask_user_question 2 questions (Storage, UI)
↓
Storage: Tool details
UI: Review tab, Custom answer
When facing ambiguity, the model calls ask_user_question. Example:
{
"questions": [
{
"id": "persistence",
"header": "Persist",
"question": "How should session state be persisted?",
"context": "Answers must survive pi /tree and /fork operations.",
"multiSelect": false,
"required": false,
"options": [
{ "value": "details", "label": "Tool result details", "recommended": true },
{ "value": "file", "label": "File" },
{ "value": "env", "label": "Environment variable" }
]
}
]
}Use showWhen to ask a follow-up only when it is relevant:
{
"questions": [
{
"id": "stack",
"header": "Stack",
"question": "What are you building?",
"context": "Selecting Backend reveals a database question.",
"multiSelect": false,
"options": [
{ "value": "frontend", "label": "Frontend UI" },
{ "value": "backend", "label": "Backend API", "recommended": true }
]
},
{
"id": "db",
"header": "DB",
"question": "Which database?",
"multiSelect": false,
"showWhen": { "questionId": "stack", "equals": "backend" },
"options": [
{ "value": "postgres", "label": "PostgreSQL" },
{ "value": "sqlite", "label": "SQLite" }
]
}
]
}| User picks… | Behavior |
|---|---|
| Frontend UI | DB tab hidden — submit only shows { stack: frontend } |
| Backend API | DB tab appears — user picks a database; submit shows { stack: backend, db: postgres } |
| Backend → picks Postgres → reopens and switches to Frontend | DB answer cleared and removed from the result; hidden required children never block submit |
When to use: Prefer
showWhenover separateask_user_questioncalls. One dialog with a conditional chain is faster and less disruptive than asking multiple times.
Rules:
idmust be unique per question;valuemust be unique per optionrequireddefaults totrue; setrequired: falseto let the user explicitly skip a questionshowWhen: { questionId, equals }shows a follow-up only after the parent is confirmed with that optionvalue(one level deep; Other text never matches)- Use
recommended: trueon the best option (moved to the top with a hint; user must select it explicitly) - Do not include a custom "Other" option — it is automatic
header≤ 12 characters- Free-text Other answers are capped at 4,000 characters; terminal control characters are removed
| Key | Context | Action |
|---|---|---|
↑ ↓ |
Options list | Move cursor |
Enter |
Required single-select option | Select and confirm |
Enter |
Optional question with no answer | Skip and confirm |
Space |
Option row | Select single option / toggle multi-select option |
Enter |
Selected options | Confirm question |
Enter / Space |
"Other — add your own answer" | Open inline editor |
Enter |
Inline editor (with text) | Save and close |
Esc |
Inline editor | Cancel |
← → / Tab |
Multi-question tabs | Switch tabs |
Enter |
Review tab | Submit all |
Esc |
Anywhere | Cancel / dismiss |
- An unanswered required question can be visited in Review but cannot be submitted;
Enteris a no-op until every visible question is confirmed. - An optional question can be explicitly skipped with
Enter; it is omitted from the submittedanswersarray. - A required multi-select question with no checked option and no Other text cannot be confirmed.
- Saving blank Other text clears it. If that leaves no answer, the question becomes unconfirmed and blocks Submit.
- Editing a selected answer or Other text unconfirms that question until the user confirms it again.
- A
showWhenfollow-up is hidden until its parent is confirmed with the matching optionvalue; hidden questions are omitted from tabs, Review, andanswers. - Editing or unconfirming a parent clears and hides dependent children; a hidden required child does not block submit.
- Multi-select answers are serialized in the original option order, regardless of the order in which options were toggled.
- A submitted answer may carry
selectedValuesandcustomTexttogether; the LLM transcript preserves both. - Terminal exit/abort, user dismissal, invalid input, and unavailable UI have distinct result statuses:
aborted,dismissed,invalid, andunavailable.
src/
├── index.ts # Tool registration, non‑TUI fallback, renderCall/renderResult
├── schema.ts # TypeBox schemas + validation
├── state.ts # Reducer: navigation, selection, confirm, toResult
├── component.ts # QuestionnaireComponent (pi-tui, no pi-coding-agent import)
tests/
├── state.test.ts # reducer and result-contract tests
├── component.test.ts # keyboard and rendering tests
└── tool.test.ts # runtime validation and lifecycle tests
Key design decisions:
- Built-in "Other" row — Pi's LLM should not add its own "Other" option; the component adds "Other — add your own answer" automatically. For multi-select questions, the custom text supplements selected options.
- Result by question ID, not text — answers map via stable
questionId/value, avoiding duplicate-text collisions. - State in tool result
details— answers persist in the Pi session JSONL via built-intoolResult.details. Branch tracking is automatic:/treeor/forkuses the correct branch's answers. NoappendEntry, no external state. - Non-TUI = disabled — in
ctx.mode !== "tui", returnsstatus: "unavailable"and deactivates itself so the model won't retry. - Keyboard-first WCAG — all actions work with
↑↓ Enter Space Esc ←→; no mouse dependency; color is never the sole indicator. - No dead rendering —
render()caches by width and invalidates on state/theme change. - TUI-only custom component —
ctx.ui.custom()opens only inctx.mode === "tui"; RPC, JSON, and print modes return an explicitunavailableresult. - Terminal and IME safety — rendered lines are clamped to the supplied display width; the questionnaire forwards focus to its inline
Editorfor IME-aware terminals.
npm run check covers reducer invariants, keyboard flows, review navigation, Other editing, narrow terminal widths, runtime validation, non-TUI fallback, and aborts before and after opening the dialog. The package tarball includes only runtime source and release metadata.
GitHub Actions runs this check, a production dependency audit, package dry-run, and a clean tarball-install smoke test on Node 20 and 22.
Implementation choices are verified against:
- Pi extension API and lifecycle: https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md
- Pi custom-component, focus, keyboard, and width contract: https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/tui.md
- Reference questionnaire test coverage: https://github.com/ghoseb/pi-askuserquestion
- Alternative
askcontract (free text, review disposition, dismissal): https://github.com/IgorWarzocha/howaboua-pi-stuff/tree/main/packages/pi-ask
pi.dev/packages indexes npm packages tagged with the pi-package keyword; it does not accept a separate package upload. This package is prepared with the required keyword and its preview asset is hosted at a stable GitHub URL through pi.image in package.json. It will appear in the gallery only after a future npm publish.
npm install
npm test # unit and integration tests
npm run typecheck # tsc --noEmit
npm run lint # biome check
npm run pack:dry # verify package contentsTest interactively:
pi -e ./src/index.ts --model sonnet- Report bugs or request features through GitHub Issues.
- See SECURITY.md for responsible vulnerability reporting.
MIT © 2026 QuangThai
