feat(dialogs): compose select with text input - #43
Conversation
Let a select option declare text fields, marking it user-provided:
choosing it collects those values one at a time in declared order and
submits them with the selection, so a list of known choices can offer
"let me type it" without the caller stitching two dialogs together.
Collection reuses the entry component from the standalone input dialog
and stays inside the same render session, so terminal state is never
restored between stages. The option list stops accepting navigation and
selection the moment collection begins, and Escape or Ctrl-C at any
stage cancels the whole dialog and discards collected values.
select now resolves { value, values } rather than the bare value, which
is how a caller tells a plain option (empty record) from a user-provided
one. Empty field lists and repeated field names within one option are
rejected before rendering.
Completes change 0017.
…ction Two defects found reviewing the composition change: Ctrl-C was dropped when Ink delivered it in the same synchronous pass as the Enter that began field collection: the option list had already committed to the option, and the field entry had not mounted yet, so neither handler answered it and the dialog stayed open. The list now answers cancellation before declining everything else, so Escape and Ctrl-C cancel at every stage including that window. A field named __proto__ reached the inherited setter on the accumulator and its value vanished, so the result carried fewer names than the option declared. The accumulator is prototype-free, and the spread that produces the result defines rather than assigns, so any opaque name survives as an own property.
A submitted field's entry stays mounted for the rest of the synchronous pass Ink dispatches one stdin chunk in, so a further Enter in that chunk answered the next field before it was ever presented, and did it with the previous entry's value. Each entry now submits under the field index it was rendered for, and a submission for a field the dialog has already moved past is declined.
There was a problem hiding this comment.
🟡 Changes recommended
The dialogs spec changelog contains an internally inconsistent “Desired” entry for 2026-08-29 despite the spec now stating all requirements are implemented and listing implementation entries for the same date.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds “composed” dialog behavior to the bundled dialogs provider: select options can now be marked as user-provided via declared text fields, and choosing such an option collects those fields sequentially within the same Ink render session. This completes change 0017 by implementing composition, updating the select result shape, expanding tests, and syncing documentation/index status.
Changes:
- Extend
selectoptions with optionalfieldsand changeselectto resolve{ value, values } | undefined, collecting declared text fields when applicable. - Reuse a shared text-entry component for both standalone
inputand per-field collection withinselect, ensuring single-session behavior and consistent cancellation semantics. - Update tests and documentation (spec/manual/index/change status) to reflect the implemented composition and new result shape.
File summaries
| File | Description |
|---|---|
| test/dialogs-plugin.test.ts | Updates select consumption to the new { value, values } result shape and adds extensive coverage for user-provided options and single-session behavior. |
| plugins/dialogs/index.ts | Implements field declarations on options, shared Entry component reuse, sequential field collection, and updated select return type. |
| docs/specs/dialogs/index.md | Updates narrative to reflect implementation and adds a changelog entry for composed select behavior. |
| docs/manual/plugins.md | Documents the new select result shape and the user-provided option composition behavior. |
| docs/index.yml | Marks change 0017 as complete. |
| docs/index.md | Marks change 0017 as complete in the index table. |
| docs/changes/0017-add-dialog-text-input-and-composition.md | Marks the change as complete and checks off Task B items. |
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
What this does
A
selectoption may now declare textfields, which marks it user-provided. Choosing it collects those values instead of resolving immediately, so one dialog can offer a list of known choices alongside a "let me type it" entry — the interaction the user experiences as one decision is one dialog, rather than a select the caller has to stitch to a separate input with its own terminal state, cancellation, and teardown in between.inputdialog already ships, including that field's own optionalinitialValue. The next field is not presented until the previous one is submitted.undefined, and discards every value already collected. There is no return to the option list and no partial result.Breaking change, no external callers
selectnow resolves{ value, values } | undefinedrather than the bare value.valuesis empty for a plain option and carries exactly the declared field names for a user-provided one, which is how a caller tells the two apart; option values are opaque, so the provider cannot merge collected values into one.The dialogs capability is internal to bundled plugins and is not exported from
@fx/tx/plugin, so there is no external caller to migrate. Every in-repo caller and test consumer is updated here.Completes change 0017
This is the final pull request of change 0017. Task B's checkboxes are ticked and the status is flipped to
completein the change document,docs/index.yml, anddocs/index.md, with a matching row added to the Dialogs spec changelog.docs/manual/plugins.mddocuments composition and the new result shape.Multi-select stays a documented non-goal, as do dropdown/checkbox/numeric/masked/multi-line fields, a form presenting several fields at once, validation, and back-navigation.
Verification
bun run checkexits 0: Biome clean,tscclean, 466 tests pass, and 100% statement, function, and line coverage on every production source file.Review notes
A scoped Codex branch review converged over three passes — 2, 1, then 0 blocking findings. All three were accepted, fixed, and each is pinned by a test that was mutation-checked against the unfixed code:
__proto__reached the inherited setter on the accumulator and its value vanished from the result.All three belong to one class — a handler still live for the remainder of the pass in which Ink dispatches one stdin chunk — and every site of it was enumerated and either fixed or pinned as already safe.
Nothing below the bar is outstanding, and nothing was deferred.