@zhushanwen/pi-ask-user
Inline adaptive ask_user tool for Pi — single/multi-question structured input with split-pane preview and inline editor.
Package details
Install @zhushanwen/pi-ask-user from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@zhushanwen/pi-ask-user- Package
@zhushanwen/pi-ask-user- Version
4.0.0- Published
- Aug 3, 2026
- Downloads
- 1,907/mo · 1,111/wk
- Author
- zhushanwen321
- License
- MIT
- Types
- extension
- Size
- 261.5 KB
- Dependencies
- 1 dependency · 4 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
@zhushanwen/pi-ask-user
Inline adaptive ask_user tool for the Pi coding agent. Resolves ambiguity the agent cannot resolve itself — a single question (no tab bar) or 1-4 questions (tabbed view + submit), with split-pane option preview on wide terminals, an inline free-text editor, and optional comments.
The tool's primary caller is the LLM. This README covers both how an agent should use it (top sections) and how a maintainer reads the code (File structure → Design notes).
Install
pi install npm:@zhushanwen/pi-ask-user
Dev-only symlink: during local development you may symlink this package into
~/.pi/agent/extensions/for debugging, but never use the symlinked copy for daily work. Local directory discovery has anindex.tsfallback that masks a missingpimanifest field — npm-installed copies then silently fail to load. See the repo root CLAUDE.md "扩展安装红线".
When to use
Call ask_user only when all three hold:
- The request has ≥2 reasonable approaches.
- You have already gathered context (read/grep) and the answer is still genuinely ambiguous.
- Picking wrong means redoing real work.
If you can form a defensible recommendation from the codebase, proceed and state your choice — do not ask. Models over-ask because asking feels safer than deciding; resist this.
When NOT to use
- Trivia answerable by reading code/docs — plain text suffices.
- Simple confirmations ("I'll delete X") — plain text suffices.
- Outsourcing judgment you should make — if context makes the answer clear, decide.
- Free-form requirements / long-form feedback — this tool returns short selections only.
- High-frequency grilling — do not chain
ask_usercalls as a default fallback when stuck. If you have no context to pass, you are not ready to ask — read code first. - Reversible decisions — if a wrong guess is cheap to roll back, just decide.
If you recommend an option, prefix its label with (Recommended) and list it first.
Parameters
{
questions: Array<{
question: string; // one self-contained decision; ≤1000 chars; no control chars (incl. \n)
header?: string; // tab label ≤12 chars; REQUIRED (non-empty) when questions.length > 1
context?: string; // 1-3 sentences of what you learned; shown above the question
options: Array<{ // 2-4 mutually exclusive options; do NOT add an 'Other' — it is automatic
label: string; // ≤ ~40 chars (longer overflows the split-pane UI); also the answer value
description?: string; // short rationale shown under the label and in the preview pane
}>;
multiSelect?: boolean; // default false; true only when several options can validly apply
allowComment?: boolean; // default false; lets the user append a free-text note after selecting
}>
} // questions: 1-4 entries
Constraints at a glance
| Field | Constraint | Enforced by |
|---|---|---|
questions |
1-4 entries | schema (minItems/maxItems) |
options |
2-4 entries | schema |
question |
≤1000 chars, no control chars (incl. \n), unique within the call |
schema description + validate.ts |
header |
≤12 chars; required when questions.length > 1 |
validate.ts (length + non-empty) |
options[].label |
non-empty, unique within the question | validate.ts |
Validation errors are returned as isError: true with a message that names the violation and tells you how to fix it — correct the parameters and retry.
Result format
On success the tool returns the answers joined as "question" = "answer" lines. Answer composition rules:
- Single-select: the chosen
label. - Multi-select: selected labels joined with
,(e.g.A, B). - Free-text (Other): whatever the user typed.
- Comment: if
allowCommentwas set, the user's note is appended after—(e.g.Postgres — needs TLS).
A question with no answer reports as (no answer).
Behavior on failure / cancellation
| Situation | Return | What the agent should do |
|---|---|---|
| Parameter validation fails | isError: true + fix hint |
Correct params and retry |
| No interactive UI (headless) | isError: true, tool disabled for the session |
Proceed with a defensible decision stated in text, or wait for the user — do not retry |
| Agent aborted (goal cancelled / context compacted) | cancelled: true |
The text identifies it as an agent abort, not a user cancel. Do not assume an answer; do not retry ask_user — propagate the abort, or wait for new instructions if the decision is still required. |
| User cancels (Esc → confirm, or Cancel button) | cancelled: true |
Wait for new instructions, or re-ask with refined options if the decision is still required |
| Unexpected error | isError: true + { error } |
Retry once with corrected parameters, or proceed with a defensible decision |
The headless branch physically removes the tool from the session (setActiveTools) — this is deliberate, so a function-calling loop cannot keep retrying ask_user in a non-interactive context.
Features
- Adaptive layout: single question → no tab bar; 1-4 questions → tabbed view + Submit tab.
- Split-pane preview (≥84 cols): option list left, selected option detail right. The right pane is plain-text option detail (label + description), not a Markdown renderer.
- Inline free-text editor: select "Other" → Enter → type a custom answer. Multi-line aware, soft-wrapped.
- Optional comments:
allowComment: true→ after selecting, the user may append a short note. - Multi-select:
multiSelect: true→ toggle checkboxes with Space, Enter to confirm. - Esc confirm-to-cancel: Esc on the first question opens a confirm overlay (a second Esc cancels; any other key stays).
- Headless-safe: disables the tool and returns
isErrorwhen no UI is available.
File structure
extensions/ask-user/
├── index.ts # re-export entry (Pi loads via package.json pi.extensions)
├── package.json
├── README.md # this file — usage contract for LLM callers + overview
├── ARCHITECTURE.md # internals: dependency graph, state machine, defensive flow
├── vitest.config.ts
└── src/
├── index.ts # Tool factory: registerTool + execute (6-step defensive flow) + renderCall/renderResult
├── types.ts # Input schema, Result schema, shared state types (QuestionState/ThemeLike) — dependency leaf
├── validate.ts # pure parameter validation; error messages aimed at LLM fixability
├── component.ts # AskUserComponent: state machine, input routing, race guards
├── question-view.ts # pure render: option list, split-pane, inline editor
└── submit-view.ts # pure render: Submit tab, answer summary, buildResult
types.ts is intentionally the shared dependency leaf — it holds QuestionState/ThemeLike (not component.ts) so the two pure-render views depend only on the leaf, breaking a would-be component → view → component cycle. See ARCHITECTURE.md for the full graph.
Steer mechanism
The tool registers three steering channels to discourage over-asking:
description— the long tool description shown in the agent's tool catalog (the three preconditions + negative cases).promptSnippet— one-line summary injected into the system prompt.promptGuidelines— six focused rules reinforcing: gather context first, one decision per question, no trivia, don't outsource judgment, don't add anOtheroption.
All three are consistent and point the same direction. If you tune behavior, edit all three together in src/index.ts to avoid drift.
Design notes
- Why inline, not overlay (
execute→ctx.ui.customwithoutoptions): the question belongs in the conversation flow, not a modal that obscures context. - Why
Otheris auto-appended, not in the schema: free-text input is the user's escape hatch and must not be something the LLM can omit or mislabel. Keeping it out ofoptionsguarantees it is always present and always last. - Why
←/→does not switch tabs: left/right is reserved for the Submit tab's Submit/Cancel focus toggle, so it does not yank focus away while navigating an option list. - Why validation messages are verbose: every message names the violation and gives a fix path, because the reader is an LLM that will retry.
Spec reference
The original design spec, acceptance criteria (FR-x / AC-x), and E2E test cases live under .xyz-harness/2026-06-15-ask-user/:
spec.md— requirements + functional/acceptance criteriae2e-test-cases.md— end-to-end scenariosclarification.md/plan.md— design rationale
Cross-references between these and the implementation are in ARCHITECTURE.md.
License
MIT