Skip to content

refactor: Build prompts from intent constructors instead of a layout flag - #459

Draft
tiurin wants to merge 2 commits into
mainfrom
prompt-intent-constructors
Draft

refactor: Build prompts from intent constructors instead of a layout flag#459
tiurin wants to merge 2 commits into
mainfrom
prompt-intent-constructors

Conversation

@tiurin

@tiurin tiurin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

WIP - the description is mostly correct however

Motivation

#457 and #458 each fixed one prompt that had shipped with the wrong layout, and the review comment on #457 wrote down the rule they were both applying:

  • Inline Y/n for binary confirmations: "Remove snapshot? [y/N]"
  • Vertical choices for distinct actions: "Keep waiting", "Stop and exit", etc.

That rule lived nowhere the code could see it. UserInputRequestEvent asked its author for a Vertical bool, which is a rendering decision. It is easy to leave the field out. Labels had drifted the same way into three styles at once: [ENTER] Log in again, Update now [U], and a bare AWS advertising no key at all.

Solution

Give user input a vocabulary of intents and derive the layout from it. User promts can be built using one of the new constructors in internal/output/prompt.go. The developer uses the appropriate prompt type which automatically applies the design rule :

Constructor What it is Rendering
output.Confirm(prompt, DefaultYes|DefaultNo, ch) y/n on an action the user already requested inline [y/N], capitalized answer is what ENTER picks
output.ActionChoice(prompt, options, ch) a choice between distinct outcomes one selectable row per option
output.Acknowledge(prompt, label, ch) a single keypress, no choice inline

UserInputRequestEvent can't be used directly, but can be easily extended with a new prompt type if needed. See technical details:

Technical details

To enforce the usage of constructors, UserInputRequestEvent's fields are unexported - this is done by renaming variables from uppercase to lowercase and read-only accessors are added. This way a struct literal built anywhere outside internal/output does not compile:

internal/reset/reset.go:43:36: unknown field Prompt in struct literal of type
output.UserInputRequestEvent, but does have unexported prompt

Unexporting only Vertical would not have worked — Go lets a keyed literal from another package omit unexported fields, so the bypass would still compile and would still default to the inline layout that caused the bug. Removing every exported field is what closes it. UserInputRequestEvent{} remains legal but inert: no prompt, nil channel, nothing settable.

Also:

  • output.OptionLabel derives the [KEY] shortcut from each option's Key, so labels are plain prose (Keep waiting) and can't drift into three styles again.
  • All eight existing prompts are ported; handlers compare against output.KeyYes/KeyNo/KeyAny instead of string literals.
  • Plain/spinner rendering of a vertical prompt keeps each option's shortcut (FormatPromptEvent) — a one-line fallback of plain-prose labels would otherwise leave the user with nothing to press.
  • The synthetic option shapes internal/ui's tests used to build by hand (an explicit enter option outranking an uppercase default, all-lowercase labels, non-letter labels) are unreachable in production now, so those rules are pinned in TestResolveOption, which feeds resolveOption arbitrary slices directly.

Applying the rule uniformly shifts a few prompts that were on the wrong side of it:

Prompt Before After
License re-login [ENTER to log in again/ESC to exit] appended to a wrapping question [R] Re-authenticate / [ESC] Exit as rows; the sentence keeps only the reason
reset, volume clear [Yes/NO] [y/N]
Update notification Update now [U] [U] Update now
Emulator picker bare AWS / Azure rows [A] AWS / [Z] Azure
Docs

Two user-visible changes worth a writer's eye, both cosmetic-adjacent but not invisible:

  • The license re-login prompt's accept key moved from ENTER to R ([R] Re-authenticate). ENTER still works — it confirms the highlighted row — but any doc or screenshot that says "press ENTER to log in again" now describes a second-best path. The decline key (ESC) is unchanged.
  • Destructive confirmations (lstk reset, lstk volume clear) now render [y/N] instead of [Yes/NO]. Same keys, same default (no); only the hint changed.

Everything else is internal: no command, flag, env var, or config surface changes.

Review

WIP

Validation

  • go test ./internal/... ./cmd/... -count=1
  • golangci-lint run ./... — 0 issues
  • make build
  • Seal proved by adding a raw literal to internal/reset/reset.go and confirming go build ./... rejects it

Follow-up to #457 and #458.

Co-Authored-By: Claude <noreply@anthropic.com>
@tiurin tiurin added semver: patch docs: needed Pull request requires documentation updates labels Aug 13, 2026
@tiurin tiurin changed the title Build prompts from intent constructors instead of a layout flag refactor: Build prompts from intent constructors instead of a layout flag Aug 13, 2026
@tiurin tiurin added docs: skip Pull request does not require documentation changes and removed docs: needed Pull request requires documentation updates labels Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs: skip Pull request does not require documentation changes semver: patch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant