@zhushanwen/pi-ask-user

Inline adaptive ask_user tool for Pi — single/multi-question structured input with split-pane preview and inline editor.

Packages

Package details

extension

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 an index.ts fallback that masks a missing pi manifest 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:

  1. The request has ≥2 reasonable approaches.
  2. You have already gathered context (read/grep) and the answer is still genuinely ambiguous.
  3. 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_user calls 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 allowComment was 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 isError when 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 an Other option.

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 (executectx.ui.custom without options): the question belongs in the conversation flow, not a modal that obscures context.
  • Why Other is 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 of options guarantees 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 criteria
  • e2e-test-cases.md — end-to-end scenarios
  • clarification.md / plan.md — design rationale

Cross-references between these and the implementation are in ARCHITECTURE.md.

License

MIT