Skip to content

QuangThai/pi-ask

Repository files navigation

pi-ask

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.

pi-ask preview

Inspired by the AskUserQuestion tool from Claude Code / OpenCode / Codex CLI.

Install

# 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.

What it looks like

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: true are 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-selectSpace to toggle, Enter to confirm
  • Review tab — see all answers before submitting; navigate back to any tab to edit
  • Keyboard navigation↑↓ move, Enter confirm, Space toggle, ←→/Tab switch tabs, Esc dismiss

Tool call (transcript)

ask_user_question 2 questions (Storage, UI)
  ↓
Storage: Tool details
UI: Review tab, Custom answer

Usage for the LLM

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" }
      ]
    }
  ]
}

Conditional follow-up example

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 showWhen over separate ask_user_question calls. One dialog with a conditional chain is faster and less disruptive than asking multiple times.

Rules:

  • id must be unique per question; value must be unique per option
  • required defaults to true; set required: false to let the user explicitly skip a question
  • showWhen: { questionId, equals } shows a follow-up only after the parent is confirmed with that option value (one level deep; Other text never matches)
  • Use recommended: true on 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 bindings

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

Submission guarantees

  • An unanswered required question can be visited in Review but cannot be submitted; Enter is a no-op until every visible question is confirmed.
  • An optional question can be explicitly skipped with Enter; it is omitted from the submitted answers array.
  • 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 showWhen follow-up is hidden until its parent is confirmed with the matching option value; hidden questions are omitted from tabs, Review, and answers.
  • 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 selectedValues and customText together; the LLM transcript preserves both.
  • Terminal exit/abort, user dismissal, invalid input, and unavailable UI have distinct result statuses: aborted, dismissed, invalid, and unavailable.

Architecture

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:

  1. 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.
  2. Result by question ID, not text — answers map via stable questionId/value, avoiding duplicate-text collisions.
  3. State in tool result details — answers persist in the Pi session JSONL via built-in toolResult.details. Branch tracking is automatic: /tree or /fork uses the correct branch's answers. No appendEntry, no external state.
  4. Non-TUI = disabled — in ctx.mode !== "tui", returns status: "unavailable" and deactivates itself so the model won't retry.
  5. Keyboard-first WCAG — all actions work with ↑↓ Enter Space Esc ←→; no mouse dependency; color is never the sole indicator.
  6. No dead renderingrender() caches by width and invalidates on state/theme change.
  7. TUI-only custom componentctx.ui.custom() opens only in ctx.mode === "tui"; RPC, JSON, and print modes return an explicit unavailable result.
  8. Terminal and IME safety — rendered lines are clamped to the supplied display width; the questionnaire forwards focus to its inline Editor for IME-aware terminals.

Reliability

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 package gallery readiness

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.

Development

npm install
npm test                 # unit and integration tests
npm run typecheck        # tsc --noEmit
npm run lint             # biome check
npm run pack:dry         # verify package contents

Test interactively:

pi -e ./src/index.ts --model sonnet

Support and security

License

MIT © 2026 QuangThai

About

No description, website, or topics provided.

Resources

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages