@fadhelhaidar/pi-ask-user 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/hosts.md CHANGED
@@ -7,7 +7,7 @@ render at all.
7
7
 
8
8
  | Environment | What the model sees | What you see |
9
9
  | --- | --- | --- |
10
- | Interactive terminal | `ask_user_question` in its tool list | The full tabbed TUI overlay |
10
+ | Interactive terminal | `ask_user_question` in its tool list | The full tabbed TUI dialog, inline in Pi's editor area (or as an overlay — see below) |
11
11
  | RPC / ACP host (VS Code pendant, Zed, Paseo) | `ask_user_question` in its tool list | A sequence of the host's own native select and input dialogs |
12
12
  | Non-interactive run (no UI) | Nothing — the tool is removed | Nothing |
13
13
 
@@ -47,15 +47,58 @@ TUI produces. Trade-offs inherent to the native primitives:
47
47
  Submit tab — are terminal-only; the host's native `select` and `input` primitives carry
48
48
  no note field.
49
49
  - Multi-select is a free-text input: type the option numbers, comma-separated
50
- (`1,3`). Any token that is not a valid option index is treated as a typed custom answer,
51
- which is how the `Type something.` escape survives. An empty input commits an empty
52
- selection, matching `Next` with nothing toggled.
50
+ (`1,3`), optionally followed by `; your own answer` to submit checks and custom text
51
+ together. A reply that is no option number is taken as the typed custom answer; an empty
52
+ input commits an empty selection, matching `Next` with nothing toggled. A blank custom
53
+ answer stays blank (this fork keeps that behavior; upstream returns to the picker).
53
54
  - Dismissing any dialog cancels the whole questionnaire, mirroring `Esc` in the TUI.
54
55
 
55
56
  If the host can render neither custom UI nor dialogs, the call returns
56
57
  `error: "no_custom_ui"` with text telling the model the user never saw the questions and
57
58
  that it should ask them as plain chat text instead — explicitly not a decline.
58
59
 
60
+ ## Where the dialog renders
61
+
62
+ In a terminal the questionnaire mounts inline in Pi's editor slot by default, capped at
63
+ 70% of the terminal height (at least 12 rows), so `PageUp`/`PageDown` and the mouse wheel
64
+ keep scrolling the transcript while a question is open. It falls back to an overlay in two
65
+ cases, both of which mean the inline slot cannot receive keys:
66
+
67
+ - Another questionnaire already holds the slot.
68
+ - An overlay that captures input (for example `/btw`) is on screen. A question is stacked
69
+ on top of it as an overlay instead, and collapses to its one-line row rather than
70
+ hiding, because Pi pops overlays from the top of its stack.
71
+
72
+ `ctx.ui.custom` options that already allocate the dialog's rows (marked with the host
73
+ symbol `rpiv-ask-user-question:host-allocated-rows`) are exempt from the 70% cap, so a
74
+ host that sizes the slot itself is never shrunk twice.
75
+
76
+ Because Pi closes an overlay by popping the top of its stack, an answer that arrives from
77
+ outside (`rpiv:ask-user:answer`) removes a stacked questionnaire through its own overlay
78
+ handle rather than the host's close callback. Answering the upper one is unaffected; the
79
+ lower one leaves the stack without disturbing it.
80
+
81
+ ## Answering from outside the terminal
82
+
83
+ A program driving Pi — a pane supervisor, a phone or web bridge, a test harness — can
84
+ answer an open questionnaire without synthesizing keystrokes:
85
+
86
+ | Event | Direction | Purpose |
87
+ | --- | --- | --- |
88
+ | `rpiv:ask-user:prompt` | out | Questions, plus the `toolCallId` naming this questionnaire |
89
+ | `rpiv:ask-user:blocked` | out | `{ active: true }` while input is awaited, `{ active: false }` when it ends, both carrying `toolCallId` |
90
+ | `rpiv:ask-user:answer` | **in** | Answer or cancel the questionnaire named by `toolCallId` |
91
+ | `rpiv:ask-user:answer-result` | out | One verdict per attempt: `{ ok }`, or `{ ok: false, code, reason }` with `code` one of `not_found`, `invalid_payload`, `invalid_answer`, `unsupported_host` |
92
+
93
+ `rpiv:ask-user:answer` takes `answers` — one `{ questionIndex, optionIndexes?, text?, notes? }`
94
+ entry per question, where single-select takes one index or `text` and multi-select takes
95
+ either or both — plus an optional `globalNote`, or `{ cancel: true }` to decline as `Esc`
96
+ does. Validation is strict and all-or-nothing: a missing, duplicated, out-of-range or
97
+ blank answer applies nothing and leaves the dialog open, since a partly applied answer
98
+ would reach the model indistinguishable from one the user gave. The first answer wins,
99
+ whether it came from the keyboard or from this event, and the events are terminal-only:
100
+ RPC hosts answer through their own dialogs and return `unsupported_host`.
101
+
59
102
  ## Conditional surfaces
60
103
 
61
104
  Some parts of the dialog exist only under the right conditions:
@@ -68,6 +111,7 @@ Some parts of the dialog exist only under the right conditions:
68
111
  | Side-by-side preview | An option carries a `preview`, and terminal and pane are both ≥ 100 columns |
69
112
  | Preview pane at all | Single-select questions only |
70
113
  | Collapse shortcut | `collapseKey` is not `"off"` |
114
+ | Overlay instead of inline | Another questionnaire holds the editor slot, or an overlay that captures keys is on screen |
71
115
  | Full overlay hide on collapse | The host also exposes raw terminal input (the only path that can reopen a hidden overlay); without it, collapsing shrinks the dialog to a visible one-line row instead |
72
116
  | Localized chrome | `@juicesharp/rpiv-i18n` is installed |
73
117
 
package/docs/keyboard.md CHANGED
@@ -13,7 +13,8 @@ adapts to the size of your terminal.
13
13
  | `Esc` | Cancel the whole questionnaire. | Everywhere except the notes editor, where it closes notes |
14
14
  | `Tab` / `Shift+Tab` | Next / previous tab, wrapping. `→` / `←` do the same. | Multi-question dialogs only |
15
15
  | `Space` | Insert a normal space while typing; never toggle a checkbox. | Text inputs only |
16
- | `n` | Open the notes editor for the focused question — or, on the Submit tab, the global note for the whole questionnaire. | Every question tab; the Submit tab in multi-question dialogs |
16
+ | `n` | Open the notes editor for the focused question — or, on the Submit tab, the global note for the whole questionnaire. Configurable via `notesKey`. | Every question tab; the Submit tab in multi-question dialogs |
17
+ | `1`–`9` | Act on the row with that number: answer a single-select question, toggle a multi-select row, or move to `Type something.` so you can type. A digit you bound yourself, or set as `notesKey`, keeps that meaning. | Option list |
17
18
  | `Ctrl+G` | Open Pi's configured external editor with the current custom-answer draft. | `Type something.` input |
18
19
  | `Ctrl+U` | Clear the current custom-answer draft. | `Type something.` input |
19
20
  | `Ctrl+]` | Collapse or expand the dialog. Configurable via `collapseKey`. | Everywhere, including while collapsed |
@@ -61,11 +62,17 @@ against the canonical English strings.
61
62
  ## Notes
62
63
 
63
64
  `n` opens a notes editor on any question tab, whether the question is single- or
64
- multi-select and whether or not its options carry previews. Notes are stored in a
65
+ multi-select and whether or not its options carry previews. On non-Latin layouts set
66
+ `notesKey` (see [Configuration](configuration.md)) to the character your keyboard types,
67
+ or to a key spec such as `"ctrl+n"`. Notes are stored in a
65
68
  side-band keyed by tab index, not inside the answer, so writing a note does not mark a
66
69
  question as answered — the Submit tab still lists it as outstanding. The note merges into
67
70
  the answer when you confirm it, and reaches the model as `user notes: <text>`.
68
71
 
72
+ A saved note stays visible after its editor closes, as one dimmed
73
+ `Note for this answer: …` row that `n` reopens; the preview hint switches to
74
+ `Note saved: press n to edit`, and the Submit-tab review uses the same label.
75
+
69
76
  On the Submit tab, `n` opens the global note editor instead — one note covering the whole
70
77
  questionnaire. It lives outside every answer, so it survives tab switches and never marks
71
78
  a question as answered; it reaches the model as `global note: <text>`, and submitting with
@@ -76,15 +83,19 @@ keystrokes edit the buffer, so `n` types an `n`. Pasted line breaks are preserve
76
83
 
77
84
  ## Collapse mode
78
85
 
79
- `Ctrl+]` gets the dialog out of the way: the overlay is marked hidden in Pi's overlay
80
- stack and shrinks to a single dim hint row, so the transcript it was covering becomes
81
- readable and chat scrolling resumes. Press the same key to bring the questionnaire back
82
- with your answers intact. The first time you collapse, Pi notifies you with the key to
83
- press — that message names your configured key.
86
+ `Ctrl+]` gets the dialog out of the way: it shrinks to a single dim hint row, so the
87
+ transcript becomes readable and chat scrolling resumes. Press the same key to bring the
88
+ questionnaire back with your answers intact. The first time you collapse, Pi notifies you
89
+ with the key to press — that message names your configured key.
84
90
 
85
- Because Pi routes no input to a hidden overlay, the collapse key is additionally captured
86
- at the raw terminal level. It only acts when the questionnaire is hidden or focused, so a
87
- different overlay on top of it (for example `/btw`) keeps its keystrokes.
91
+ How it collapses depends on where the dialog opened. Inline (Pi's editor area) it simply
92
+ keeps the editor slot and the one-line row. An overlay is marked hidden in Pi's overlay
93
+ stack instead, and because Pi routes no input to a hidden overlay the collapse key is
94
+ additionally captured at the raw terminal level; it only acts when the questionnaire is
95
+ hidden or focused, so a different overlay on top of it (for example `/btw`) keeps its
96
+ keystrokes. A question that opened on top of `/btw` keeps its one-line row rather than
97
+ hiding, since Pi closes overlays from the top of the stack and would otherwise pop `/btw`
98
+ instead of the hidden question.
88
99
 
89
100
  While collapsed, every keystroke other than cancel is ignored, so you cannot mutate
90
101
  answers you cannot see.
@@ -95,15 +106,24 @@ among others — set a different `collapseKey`, or `"off"` to disable the shortc
95
106
 
96
107
  ## Layout
97
108
 
109
+ The dialog opens inline in Pi's editor area below the transcript, where Pi's own prompts
110
+ appear, and takes at most 70% of the terminal height (never fewer than 12 rows); it
111
+ scrolls within that space, so the transcript above it stays scrollable while a question is
112
+ open. A second questionnaire opened while an inline one is showing, or a question arriving
113
+ while another overlay such as `/btw` is open, opens as an overlay on top instead, so the
114
+ question on screen is the one receiving keys.
115
+
98
116
  Options render in a vertical list. When any option in a single-select question carries a
99
117
  `preview`, the dialog splits into a side-by-side layout with the option list on the left
100
118
  and a bordered monospace preview box on the right — but only when both the terminal and
101
119
  the dialog pane are at least 100 columns wide. Below that, the preview stacks underneath
102
120
  the options instead.
103
121
 
104
- When the dialog is taller than the terminal, the body scrolls between a sticky heading and
105
- a sticky footer, and an overflow indicator shows which direction is clipped: `↑` for
106
- content above, `↓` for content below, `↕` for both.
122
+ When the dialog is taller than the space it has, the header and question stay pinned while
123
+ the body scrolls between them and a sticky footer, and an overflow indicator shows which
124
+ direction is clipped: `↑` for content above, `↓` for content below, `↕` for both. The
125
+ markers never replace the focused row, padding is trimmed before content, and only the
126
+ padding — never the body — is dropped to make the dialog fit.
107
127
 
108
128
  The footer hint line adapts to context — it drops the notes hint and appends the
109
129
  `Shift+Enter` newline hint whenever a text editor has the keyboard, with `Ctrl+U` still at
@@ -57,8 +57,13 @@ the footer hint segments (including the collapsed-state line and the Submit tab'
57
57
  submit-readiness prompt, incomplete-answers warning, and committed-note `Note` label, the
58
58
  preview pane's empty and notes-affordance lines, the notes header and the global-note
59
59
  header on the Submit tab, the
60
- external-editor failure notification, and the two RPC dialog prompts. Twenty-six keys in total,
61
- all under the namespace `@juicesharp/rpiv-ask-user-question`.
60
+ external-editor failure notification, the saved-note row label and the preview's "press
61
+ <key> to edit" line, and the two RPC dialog prompts. Thirty-one keys in total, all under
62
+ the namespace `@juicesharp/rpiv-ask-user-question`.
63
+
64
+ The transcript receipts the model's answer is rendered into — the `✓`, `✗`, `not
65
+ answered` and `no answer` strings — are translated too (`transcript.*`), because they are
66
+ chrome in the transcript rather than prompt input.
62
67
 
63
68
  Everything the *model* reads stays English by design: the tool description, the parameter
64
69
  schema descriptions, error messages, and the reserved-label list. Those are prompt inputs,
@@ -100,27 +100,59 @@ question is blank.
100
100
 
101
101
  Cancelling, and any result with neither answer segments nor a global note, both collapse
102
102
  to the single string `User declined to answer questions` so the model sees one canonical
103
- signal. Partial submission is allowed: unanswered questions simply contribute no segment.
103
+ signal. Partial submission is allowed: unanswered questions contribute no segment, and
104
+ their question text is listed in an `unanswered: "<question>", …` segment after the
105
+ answers, with the same indexes in `details.unanswered`. A global note counts as an answer
106
+ on its own, so a note with every question blank still yields the answered envelope.
104
107
  A cancelled result always reads as the decline in text; its note, if any, survives only
105
108
  in `details.globalNote`.
106
109
 
107
110
  ## Event contract
108
111
 
109
- The package publishes one event on Pi's event bus, emitted after validation passes and
110
- before the dialog is shown. Import it from the `/events` subpath:
112
+ The package publishes three events on Pi's event bus — one before the dialog is shown, two
113
+ bracketing the wait — and accepts a fourth that answers the questionnaire from outside the
114
+ terminal. Import them from the `/events` subpath:
111
115
 
112
116
  ```ts
113
- import { ASK_USER_PROMPT_EVENT, type AskUserPromptEventPayload } from "@juicesharp/rpiv-ask-user-question/events";
117
+ import { ASK_USER_PROMPT_EVENT, type AskUserPromptEventPayload } from "@fadhelhaidar/pi-ask-user/events";
114
118
 
115
119
  pi.events.on(ASK_USER_PROMPT_EVENT, (payload: AskUserPromptEventPayload) => {
116
- // payload.questions[].{ question, header, multiSelect, options[] }
120
+ // payload.toolCallId, payload.questions[].{ question, header, multiSelect, options[] }
117
121
  // payload.questions[].options[].{ label, description, hasPreview }
118
122
  });
119
123
  ```
120
124
 
121
125
  The channel name is `rpiv:ask-user:prompt`. Preview *content* is deliberately not shipped
122
126
  in the payload — only `hasPreview: boolean` — so listeners forwarding the event across a
123
- process or network boundary stay cheap.
127
+ process or network boundary stay cheap. The prompt event fires after validation passes and
128
+ before the dialog is shown; `rpiv:ask-user:blocked` then brackets the wait with
129
+ `{ active: true, toolCallId }` and `{ active: false, toolCallId }`, including when the
130
+ dialog never mounts because an answer arrived first.
131
+
132
+ ## Answering from outside the terminal
133
+
134
+ A supervisor, bridge or test harness emits `rpiv:ask-user:answer` on the same bus:
135
+
136
+ ```ts
137
+ pi.events.emit(ASK_USER_ANSWER_EVENT, {
138
+ toolCallId,
139
+ answers: [{ questionIndex: 0, optionIndexes: [1], notes: "keep the bus factor up" }],
140
+ globalNote: "ship on Tuesday",
141
+ });
142
+ ```
143
+
144
+ `answers` carries one entry per question — `optionIndexes` for chosen options (authored
145
+ order is preserved, not the order given here), `text` for a typed custom answer, `notes`
146
+ for that question's note; multi-select accepts both `optionIndexes` and `text` together,
147
+ single-select takes one index or `text`. Send `{ cancel: true }` instead to decline as `Esc`
148
+ does. Every attempt gets exactly one `rpiv:ask-user:answer-result` verdict:
149
+ `{ ok: true }`, or `{ ok: false, code, reason }` with `code` one of `not_found`,
150
+ `invalid_payload`, `invalid_answer` or `unsupported_host`. Validation is strict and
151
+ all-or-nothing — a blank, duplicated or out-of-range answer applies nothing and leaves the
152
+ dialog open — and the first answer wins, whether it came from the keyboard or this event.
153
+ Questionnaires are addressed by the `toolCallId` on the prompt event, so parallel calls
154
+ are independent. Events answer terminal dialogs only; RPC hosts answer through their own
155
+ dialogs and reject with `unsupported_host`.
124
156
 
125
157
  Stability policy for the `rpiv:*` namespace: channel names are immutable, payload changes
126
158
  are append-only and always optional, payloads stay JSON-safe, and any breaking change ships
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fadhelhaidar/pi-ask-user",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "Standalone Pi questionnaire with combined multi-select and custom answers, Enter-only toggling, and preserved drafts.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,5 +0,0 @@
1
- // events.ts
2
- var ASK_USER_PROMPT_EVENT2 = "rpiv:ask-user:prompt";
3
- var ASK_USER_BLOCKED_EVENT2 = "rpiv:ask-user:blocked";
4
-
5
- export { ASK_USER_PROMPT_EVENT2, ASK_USER_BLOCKED_EVENT2 };