@hank-warren/pi-ask-user-question
Structured questionnaire tool for Pi with numbered options, digit hotkeys and Tab-to-comment, composed from the shared permission-selector component.
Package details
Install @hank-warren/pi-ask-user-question from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@hank-warren/pi-ask-user-question- Package
@hank-warren/pi-ask-user-question- Version
0.8.0- Published
- Nov 6, 2026
- Downloads
- 561/mo · 273/wk
- Author
- hank-warren
- License
- MIT
- Types
- extension
- Size
- 85.4 KB
- Dependencies
- 1 dependency · 3 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@hank-warren/pi-ask-user-question
A structured questionnaire the model can put to you when it would otherwise guess. Instead of a free-form "which do you prefer?" in chat, you get a dialog with numbered options, digit hotkeys, a typed-answer escape, and Tab-to-comment.
v0.5 ships 1-4 questions per call as cycleable tabs, single- or multi-select, with an optional preview pane. See the spec §14.
Install
pi install npm:@hank-warren/pi-ask-user-question
It must not be installed alongside @juicesharp/rpiv-ask-user-question — both
register a tool named ask_user_question.
Keys
| Key | Action |
|---|---|
1–9 |
Select that option immediately (toggle it, on a multi-select question) |
Space |
Toggle the highlighted option (multi-select only) |
↑ / ↓ |
Move the highlight |
Enter |
Confirm the highlighted option, or submit the checked ones |
n |
Write a note: Enter answers with the highlighted option and the note (multi-select: keeps the note for the submit) |
Tab / → |
Next question |
Shift+Tab / ← |
Previous question |
Esc |
Decline the questionnaire (or leave note/typed-answer mode) |
With several questions, each is a tab you can cycle through in any order; answering one jumps to the next unanswered question, and the call returns once every question has an answer. Cycling back and re-answering replaces that question's answer rather than recording a second one.
Multi-select
A question with multiSelect: true renders as checkboxes, for the cases where
several answers hold at once — "which of these packages should change", "which
checks to run before merging":
→ [x] 1. pi-stats
[ ] 2. pi-statusline
[x] 3. pi-plan-mode
[ ] 4. Type something.
space/1-9 toggle · ↑↓ move · enter confirm (2) · n add note · esc cancel
- Space and the digit hotkeys both toggle; a digit no longer commits, so one keystroke cannot end the question early.
- Enter submits the checked options in list order and is inert until at least one is checked — the count in the hint is the tell.
- The answer comes back as the chosen labels joined with
,. - Multi-select questions may carry 2-6 options; single-select stays at 2-4.
- Checking
Type something.alongside other options opens the free-text field with those ticks preserved, and the typed value is appended to them rather than replacing them, sopi-stats, pi-plan-mode, and also the docs siteis a single answer.
Mutually exclusive choices stay single-select; that is still the default.
Previews
An option may carry a preview field — markdown shown in a pane below the
options while that option is highlighted. Use it for concrete artifacts worth
comparing (ASCII mockups, code snippets, configuration variations), not for
simple preference questions. The pane is stacked rather than side-by-side, and
long previews are clipped with a … N more lines marker so the options always
stay visible.
Why n and not Tab for notes? Tab cycles questions here, matching
@juicesharp/rpiv-ask-user-question. pi-auto-permissions approval prompts
keep Tab for notes — they are single-question and have no tabs to cycle.
Every question gets an appended Type something. row for a free-text
answer. The model is not allowed to author that row itself — reserved labels
are rejected at runtime.
What the model gets back
User has answered your questions: "<question>"="<answer>". … You can now continue with the user's answers in mind., with a few additions:
- A note follows its answer as
user notes: …. - Typed text is marked
(typed by the user), or(includes text the user typed)on a multi-select answer, because a typed answer is often a question or a change of direction rather than a choice. - On
Esc, answers already given follow the decline:User declined to answer questions. Before declining, the user answered: …. - A chosen option's preview is not echoed back (the model wrote it); it stays in
details.answers[].preview.
A header over 16 characters is clipped to 15 plus … rather than rejected.
Typing answers and notes
The custom answer and notes are typed in pi's own input-box editor, so they
behave like writing a prompt: Shift+Enter for a new line, pastes of any size
with their line breaks kept, word motion and undo, and @ or Tab path
completion from the session's working directory — the way to point the model
at a screenshot or file (@/tmp/screenshot.png). Esc closes completion
first, then the field, back to the options.
Where the dialog renders
The questionnaire renders in the editor area, exactly where ctx.ui.select
puts pi's own selectors — not as an overlay floating over the transcript.
An overlay is composited over the bottom rows of the viewport, so the chat lines underneath it are unreachable: you are already scrolled to the bottom and there is nothing left to scroll. Rendering in the document flow pushes the transcript up instead of covering it, so every line stays readable in the terminal's own scrollback while you answer.
No monkey patching
Numbered options and Tab-to-comment come from OptionSelector, imported from
@hank-warren/pi-permission-selector and rendered
through ctx.ui.custom(). pi exposes no setSelectorComponent hook, so the
only alternative would be patching pi's internal ExtensionSelectorComponent —
which this package deliberately avoids. The trade-off: consistent behavior
across the dialogs we own, and nothing to break when pi changes its internals.
Subagents cannot use this tool
Whenever ctx.hasUI is false — which is every headless subagent child — the
tool is removed from the active tool set before the agent starts. A background
run can therefore never block waiting on a human, and a child can never route a
question up to its supervisor. This is intentional, not a limitation; see
reconcile.ts.
Events
Other extensions can observe the questionnaire without touching this one:
pi.events.on("hank:ask-user:blocked", ({ active }) => {
// active === true while a human is being asked
});
pi.events.on("hank:ask-user:prompt", ({ id, questions }) => {
// questions[].question / .header / .multiSelect / .options[].label
});
Answering from outside the terminal
An extension in the same pi process (a relay to a phone, say) can answer the
open questionnaire by the id its prompt event carried. The terminal dialog
stays live, the first answer wins, and the tool returns exactly what the same
answer typed in the terminal would. One entry per question, in order:
pi.events.emit("hank:ask-user:answer", {
id,
answers: [
{ choice: "Postgres", notes: "see @docs/db.md" }, // single-select
{ choices: ["pi-stats", "pi-stash"], text: "pi-agents" }, // multi-select, plus typed text
],
});
pi.events.emit("hank:ask-user:answer", { id, cancelled: true }); // decline, like Esc
An answer that does not fit — wrong count, a label that is not an option, both
a choice and text on a single-select question — is dropped whole. When the
dialog closes, hank:ask-user:blocked reports { active: false, id, remote }.
Channel names are immutable and payloads are append-only — see
events.ts for the full stability policy.
Changelog
See CHANGELOG.md for release history.
License
MIT