Skip to content

feat(dialogs): compose select with text input - #43

Merged
fx merged 6 commits into
mainfrom
feat/0017-select-composition
Aug 29, 2026
Merged

feat(dialogs): compose select with text input#43
fx merged 6 commits into
mainfrom
feat/0017-select-composition

Conversation

@fx

@fx fx commented Aug 29, 2026

Copy link
Copy Markdown
Owner

What this does

A select option may now declare text fields, 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.

  • Fields are collected one at a time in declared order, each reusing the entry component the standalone input dialog already ships, including that field's own optional initialValue. The next field is not presented until the previous one is submitted.
  • The whole interaction is one render session: no unmount, no terminal restore, no raw-mode churn, and no settlement between the selection and field stages, so the existing cleanup-before-settlement contract applies once to the whole thing.
  • The option list stops accepting navigation and re-selection the moment collection begins.
  • Escape or Ctrl-C at any stage cancels the entire dialog, resolves undefined, and discards every value already collected. There is no return to the option list and no partial result.
  • An option whose field list is empty, or that repeats a field name within itself, is rejected before rendering — joining the existing empty-options and non-interactive rejections.

Breaking change, no external callers

select now resolves { value, values } | undefined rather than the bare value. values is 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 complete in the change document, docs/index.yml, and docs/index.md, with a matching row added to the Dialogs spec changelog. docs/manual/plugins.md documents 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 check exits 0: Biome clean, tsc clean, 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:

  • Ctrl-C was dropped when Ink delivered it in the same synchronous pass as the Enter that began collection, before the field entry had mounted.
  • A field named __proto__ reached the inherited setter on the accumulator and its value vanished from the result.
  • A submitted field's entry stayed live for the rest of that same pass and could answer the next field before it was presented.

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.

fx added 6 commits August 29, 2026 09:20
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.
Copilot AI lite review requested due to automatic review settings August 29, 2026 17:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 select options with optional fields and change select to resolve { value, values } | undefined, collecting declared text fields when applicable.
  • Reuse a shared text-entry component for both standalone input and per-field collection within select, 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.

Comment thread docs/specs/dialogs/index.md
@fx
fx merged commit 5cb7be3 into main Aug 29, 2026
2 checks passed
@fx
fx deleted the feat/0017-select-composition branch August 29, 2026 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants