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

```bash
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

```typescript
{
  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** (`execute` → `ctx.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
