Interactive Q&A bridge for the Pi coding agent that converts unstructured questions from LLMs into structured TUI forms.
When an LLM sends a message containing multiple questions, traditional chat requires manual text replies that are error-prone and tedious. pi-answr extracts those questions and renders an interactive terminal form, ensuring the LLM receives exactly the data format it needs.
- Question Extraction — Uses remote LLMs to parse unstructured text into structured question schemas
- Interactive TUI Forms — Terminal UI with keyboard navigation for radio, checkbox, and text inputs
- Draft Autosave — Responses are persisted automatically to prevent data loss during long forms
- Questionnaire Restoration — Reopen the last questionnaire with
/answer:again - Tool Integration — LLMs can trigger forms directly via the
ask_user_questiontool - Template System — Configurable answer formatting templates
pi install git:https://github.com/k0valik/pi-answrRestart Pi if already running.
Extracts questions from the last assistant message and displays an interactive form.
- Finds the most recent assistant message in the session branch
- Sends the message to an extraction model (configured in settings)
- Parses the JSON response into a question schema
- Renders the TUI form
- On submit, formats answers and sends back to the chat
Implemented in index.ts:
answerHandler()— Main command handler (lines ~295-380)selectExtractionModel()fromextraction.ts— Model selection logic
Reopens the last questionnaire without re-extracting. Prompts to restore previous answers if drafts exist.
- Attempts to reconstruct cache from session entries
- Prompts user to restore saved answers
- Re-renders the form with prior responses filled in
Implemented in index.ts:
answerAgainHandler()— Lines ~420-510
LLMs can invoke this tool directly to request structured user input. Registered only if toolEnabled: true in settings.
| Field | Type | Description |
|---|---|---|
title |
string? | Form title |
description |
string? | Brief context |
questions |
array | Array of question objects |
| Field | Type | Description |
|---|---|---|
id |
string | Unique identifier |
type |
"radio" / "checkbox" / "text" | Input type |
prompt |
string | Question text |
label |
string? | Short label for progress |
options |
array? | For radio/checkbox |
allowOther |
boolean? | Add "Other..." option |
required |
boolean? | Is answer required |
placeholder |
string? | Placeholder for text input |
default |
string / string[]? | Pre-selected values |
{
"value": "postgres",
"label": "PostgreSQL",
"description": "Best for relational data"
}Answers are returned as formatted text:
Q1: Database: postgres
Q2: Languages: typescript, rust
Q3: Project Name: my-cool-app
Or if cancelled: User cancelled the form
Implemented in index.ts:
- Tool registration —
pi.registerTool()block (lines ~175-290) execute()function — Form rendering and result formatting- Input schema defined via
AskUserQuestionParams(lines ~140-155)
- See example.config.json
Add an answer block to your settings.json:
{
"answer": {
"toolEnabled": true,
"extractionModels": [
{ "provider": "openai-codex", "id": "gpt-5.4-mini" },
{ "provider": "github-copilot", "id": "gpt-5.4-mini" },
{ "provider": "anthropic", "id": "claude-haiku-4-5" }
],
"extractionTimeoutMs": 30000,
"debugNotifications": false,
"answerTemplates": [
{ "label": "Q&A", "template": "Q{{index}}: {{question}}\nA: {{answer}}" },
{ "label": "Concise", "template": "{{question}}: {{answer}}" }
],
"drafts": {
"enabled": true,
"autosaveMs": 1000,
"promptOnRestore": true
}
}
}| Option | Type | Default | Description |
|---|---|---|---|
toolEnabled |
boolean | true |
Enable the ask_user_question tool |
extractionModels |
array | (see below) | Models used for extraction, sensible defaults below, doesn't break main conversation prompt cache |
extractionTimeoutMs |
number | 30000 |
Extraction timeout in ms |
debugNotifications |
boolean | false |
Show tool error notifications |
answerTemplates |
array | [] | Custom answer formatting templates |
drafts.enabled |
boolean | true |
Enable draft autosave |
drafts.autosaveMs |
number | 1000 |
Autosave delay in ms |
drafts.promptOnRestore |
boolean | true |
Prompt before restoring drafts |
Default extraction models (DEFAULT_MODEL_PREFERENCES in extraction.ts):
[
{ provider: "openai-codex", id: "gpt-5.4-mini" },
{ provider: "github-copilot", "id": "gpt-5.4-mini" },
{ provider: "anthropic", id: "claude-haiku-4-5" }
]Settings are read from (in order of precedence):
- Project:
./.pi/agent/settings.json - Global:
~/.pi/agent/settings.json
Implemented in index.ts:
loadAnswerSettings()— Settings loading with caching (lines ~95-130)getAnswerSettingsPaths()— Path resolution
| File | Purpose |
|---|---|
index.ts |
Extension entry, commands, tool registration |
extraction.ts |
LLM extraction logic, model selection |
schema.ts |
Question schemas, normalization, parsing |
templates.ts |
Answer formatting templates |
drafts.ts |
Auto-save draft persistence |
tui.ts |
Terminal UI component |
- User runs
/answer answerHandler()finds the last assistant messageselectExtractionModel()picks an available model from preferences- Message sent to model with
DEFAULT_SYSTEM_PROMPT(extraction.ts) - Model returns JSON with question schema
parseExtractionResult()parses JSON (schema.ts)- Questions normalized via
normalizeQuestions()(schema.ts) - TUI form rendered
- Answers formatted and sent as chat message
schema.ts provides the core data model:
UnifiedQuestion— Raw question from extraction or tool paramsNormalizedQuestion— Fully typed question for TUInormalizeQuestions()— Converts raw input to normalized formparseExtractionResult()— Parses LLM JSON response
Implemented in drafts.ts:
createDraftStore()— Creates a draft persistence storegetLatestDraftForEntry()— Retrieves saved draftgetInitialResponses()— Restores responses from draftderiveAnswersFromResponses()— Converts TUI responses to answer strings
Drafts are stored as session entries with type answer:draft.
When a user completes all questions and presses Tab once more, they reach the Summary tab (the final position in the tab order).
The Summary page displays:
- All answers reviewed — Shows every question with its answer (or "(no answer)" if unanswered)
- Two selection options:
- "Confirm All" — Submit the form
- "Revisit Questions" — Go back to a specific unanswered question
Before submission, the form checks for unanswered questions with getUnansweredQuestions() (tui.ts). If questions are marked required: true and left blank:
- First press of
Entershows a warning banner:⚠ X question(s) not answered - Second press of
Enterproceeds with submission anyway - Pressing
Up/Downto select "Revisit Questions" navigates directly to the first unanswered question
The progress indicator shows:
[●]— Current question[✓]— Answered and confirmed (pressed Enter)[○]— Answered (selected but not confirmed)[ ]— Unanswered
Navigation in Summary:
↑/↓— Toggle between Confirm All and Revisit QuestionsEnter— Confirm selectionEsc— Go back to last questionEsc Esc— Direct jump to first unanswered questionTab— Cycle back to first question
Implemented in tui.ts:
showingConfirmationstate variableconfirmPageSelection— "confirm" or "revisit"confirmWarningShown— controls warning displaygetUnansweredQuestions()(lines ~170-177) — scans for required-but-empty questionssubmit()(lines ~310-330) — final submission handler
| Key | Action |
|---|---|
Tab |
Next question (wraps to first) |
Shift+Tab |
Previous question (wraps to last) |
Up/Down |
Cycle through options |
Enter |
Confirm / Submit (two-step) |
Ctrl+T |
Cycle answer templates |
Escape |
Go back |
Space |
Select checkbox |
Ctrl+C or Escape twice |
Cancel form |
Answer templates use variable substitution:
| Variable | Description |
|---|---|
{{question}} |
Question text |
{{context}} |
Optional context/header |
{{answer}} |
User's answer |
{{index}} |
Question number (1-based) |
{{total}} |
Total questions |
Example template:
{{index}}. {{question}}
Answer: {{answer}}
Renders as:
1. Which color do you prefer?
Answer: blue
Implemented in templates.ts:
normalizeTemplates()— Parses template configurationapplyTemplate()— Variable substitution
| Condition | LLM Receives |
|---|---|
| UI not available | "Error: UI not available" |
| No questions provided | "Error: No questions provided" |
| User cancels | "User cancelled the form" |
- Pi coding agent (latest version)
- At least one model configured in
~/.pi/agent/models.json