From e6bb6ba79bb392bbb8e1f4c77e0b13725bd413b8 Mon Sep 17 00:00:00 2001 From: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:17:07 -0600 Subject: [PATCH] feat(prompts): add showInstructions opt-out --- .changeset/fresh-facts-search.md | 5 +++ packages/prompts/src/group-multi-select.ts | 13 +++++- packages/prompts/src/multi-select.ts | 12 +++++- packages/prompts/src/select.ts | 14 ++++++- .../group-multi-select.test.ts.snap | 42 +++++++++++++++++++ .../__snapshots__/multi-select.test.ts.snap | 40 ++++++++++++++++++ .../test/__snapshots__/select.test.ts.snap | 40 ++++++++++++++++++ .../prompts/test/group-multi-select.test.ts | 18 ++++++++ packages/prompts/test/multi-select.test.ts | 16 +++++++ packages/prompts/test/select.test.ts | 15 +++++++ 10 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 .changeset/fresh-facts-search.md diff --git a/.changeset/fresh-facts-search.md b/.changeset/fresh-facts-search.md new file mode 100644 index 00000000..8df5788d --- /dev/null +++ b/.changeset/fresh-facts-search.md @@ -0,0 +1,5 @@ +--- +"@clack/prompts": minor +--- + +Add `showInstructions` option to `select`, `multiselect`, and `groupMultiselect`. Keyboard hints remain shown by default; pass `showInstructions: false` to hide them. diff --git a/packages/prompts/src/group-multi-select.ts b/packages/prompts/src/group-multi-select.ts index 054228f6..819d81e5 100644 --- a/packages/prompts/src/group-multi-select.ts +++ b/packages/prompts/src/group-multi-select.ts @@ -60,6 +60,12 @@ export interface GroupMultiSelectOptions extends CommonOptions { * @default 0 */ groupSpacing?: number; + + /** + * Show keyboard instructions below the option list. + * @default true + */ + showInstructions?: boolean; } /** @@ -191,6 +197,7 @@ export const groupMultiselect = (opts: GroupMultiSelectOptions) => ); }; const required = opts.required ?? true; + const showInstructions = opts.showInstructions ?? true; return new GroupMultiSelectPrompt({ options: opts.options, @@ -288,7 +295,11 @@ export const groupMultiselect = (opts: GroupMultiSelectOptions) => default: { const guidePrefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : ''; const titleLineCount = title.split('\n').length; - const footerLines = formatInstructionFooter(MULTISELECT_INSTRUCTIONS, hasGuide); + const footerLines = showInstructions + ? formatInstructionFooter(MULTISELECT_INSTRUCTIONS, hasGuide) + : hasGuide + ? [styleText('cyan', S_BAR_END)] + : []; const footerText = footerLines.join('\n'); const footerLineCount = footerLines.length + 1; const optionsText = limitOptions({ diff --git a/packages/prompts/src/multi-select.ts b/packages/prompts/src/multi-select.ts index a04df770..e5db7069 100644 --- a/packages/prompts/src/multi-select.ts +++ b/packages/prompts/src/multi-select.ts @@ -27,6 +27,11 @@ export interface MultiSelectOptions extends CommonOptions { maxItems?: number; required?: boolean; cursorAt?: Value; + /** + * Show keyboard instructions below the option list. + * @default true + */ + showInstructions?: boolean; } const computeLabel = (label: string, format: (text: string) => string) => { return label @@ -77,6 +82,7 @@ export const multiselect = (opts: MultiSelectOptions) => { return `${styleText('dim', S_CHECKBOX_INACTIVE)} ${computeLabel(label, (text) => styleText('dim', text))}`; }; const required = opts.required ?? true; + const showInstructions = opts.showInstructions ?? true; return new MultiSelectPrompt({ options: opts.options, @@ -179,7 +185,11 @@ export const multiselect = (opts: MultiSelectOptions) => { default: { const prefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : ''; const titleLineCount = title.split('\n').length; - const footerLines = formatInstructionFooter(MULTISELECT_INSTRUCTIONS, hasGuide); + const footerLines = showInstructions + ? formatInstructionFooter(MULTISELECT_INSTRUCTIONS, hasGuide) + : hasGuide + ? [styleText('cyan', S_BAR_END)] + : []; const footerText = footerLines.join('\n'); const footerLineCount = footerLines.length + 1; return `${title}${prefix}${limitOptions({ diff --git a/packages/prompts/src/select.ts b/packages/prompts/src/select.ts index 5d012b13..d6b5dae1 100644 --- a/packages/prompts/src/select.ts +++ b/packages/prompts/src/select.ts @@ -4,6 +4,7 @@ import { type CommonOptions, formatInstructionFooter, S_BAR, + S_BAR_END, S_RADIO_ACTIVE, S_RADIO_INACTIVE, symbol, @@ -75,6 +76,11 @@ export interface SelectOptions extends CommonOptions { options: Option[]; initialValue?: Value; maxItems?: number; + /** + * Show keyboard instructions below the option list. + * @default true + */ + showInstructions?: boolean; } const computeLabel = (label: string, format: (text: string) => string) => { @@ -111,6 +117,8 @@ export const select = (opts: SelectOptions) => { } }; + const showInstructions = opts.showInstructions ?? true; + return new SelectPrompt({ options: opts.options, signal: opts.signal, @@ -151,7 +159,11 @@ export const select = (opts: SelectOptions) => { default: { const prefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : ''; const titleLineCount = title.split('\n').length; - const footerLines = formatInstructionFooter(SELECT_INSTRUCTIONS, hasGuide); + const footerLines = showInstructions + ? formatInstructionFooter(SELECT_INSTRUCTIONS, hasGuide) + : hasGuide + ? [styleText('cyan', S_BAR_END)] + : []; const footerText = footerLines.join('\n'); const footerLineCount = footerLines.length + 1; return `${title}${prefix}${limitOptions({ diff --git a/packages/prompts/test/__snapshots__/group-multi-select.test.ts.snap b/packages/prompts/test/__snapshots__/group-multi-select.test.ts.snap index 2319ef31..edec4aab 100644 --- a/packages/prompts/test/__snapshots__/group-multi-select.test.ts.snap +++ b/packages/prompts/test/__snapshots__/group-multi-select.test.ts.snap @@ -664,6 +664,27 @@ exports[`groupMultiselect (isCI = false) > selectableGroups = false > selecting ] `; +exports[`groupMultiselect (isCI = false) > showInstructions: false hides instruction footer 1`] = ` +[ + "", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "◇ foo +│", + " +", + "", +] +`; + exports[`groupMultiselect (isCI = false) > sliding window loops downwards 1`] = ` [ "", @@ -1631,6 +1652,27 @@ exports[`groupMultiselect (isCI = true) > selectableGroups = false > selecting a ] `; +exports[`groupMultiselect (isCI = true) > showInstructions: false hides instruction footer 1`] = ` +[ + "", + "│ +◆ foo +│ ◻ group1 +│ │ ◻ group1value0 +│ └ ◻ group1value1 +└ +", + "", + "", + "", + "◇ foo +│", + " +", + "", +] +`; + exports[`groupMultiselect (isCI = true) > sliding window loops downwards 1`] = ` [ "", diff --git a/packages/prompts/test/__snapshots__/multi-select.test.ts.snap b/packages/prompts/test/__snapshots__/multi-select.test.ts.snap index 40aeb081..ae07ee85 100644 --- a/packages/prompts/test/__snapshots__/multi-select.test.ts.snap +++ b/packages/prompts/test/__snapshots__/multi-select.test.ts.snap @@ -575,6 +575,26 @@ exports[`multiselect (isCI = false) > renders validation errors 1`] = ` ] `; +exports[`multiselect (isCI = false) > showInstructions: false hides instruction footer 1`] = ` +[ + "", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +└ +", + "", + "", + "", + "◇ foo +│ none", + " +", + "", +] +`; + exports[`multiselect (isCI = false) > shows hints for all selected options 1`] = ` [ "", @@ -1514,6 +1534,26 @@ exports[`multiselect (isCI = true) > renders validation errors 1`] = ` ] `; +exports[`multiselect (isCI = true) > showInstructions: false hides instruction footer 1`] = ` +[ + "", + "│ +◆ foo +│ ◻ opt0 +│ ◻ opt1 +└ +", + "", + "", + "", + "◇ foo +│ none", + " +", + "", +] +`; + exports[`multiselect (isCI = true) > shows hints for all selected options 1`] = ` [ "", diff --git a/packages/prompts/test/__snapshots__/select.test.ts.snap b/packages/prompts/test/__snapshots__/select.test.ts.snap index 4bc53b46..5a1b34b1 100644 --- a/packages/prompts/test/__snapshots__/select.test.ts.snap +++ b/packages/prompts/test/__snapshots__/select.test.ts.snap @@ -461,6 +461,26 @@ exports[`select (isCI = false) > renders options and message 1`] = ` ] `; +exports[`select (isCI = false) > showInstructions: false hides instruction footer 1`] = ` +[ + "", + "│ +◆ foo +│ ● opt0 +│ ○ opt1 +└ +", + "", + "", + "", + "◇ foo +│ opt0", + " +", + "", +] +`; + exports[`select (isCI = false) > up arrow selects previous option 1`] = ` [ "", @@ -1061,6 +1081,26 @@ exports[`select (isCI = true) > renders options and message 1`] = ` ] `; +exports[`select (isCI = true) > showInstructions: false hides instruction footer 1`] = ` +[ + "", + "│ +◆ foo +│ ● opt0 +│ ○ opt1 +└ +", + "", + "", + "", + "◇ foo +│ opt0", + " +", + "", +] +`; + exports[`select (isCI = true) > up arrow selects previous option 1`] = ` [ "", diff --git a/packages/prompts/test/group-multi-select.test.ts b/packages/prompts/test/group-multi-select.test.ts index 33769ebf..a7d115fd 100644 --- a/packages/prompts/test/group-multi-select.test.ts +++ b/packages/prompts/test/group-multi-select.test.ts @@ -481,4 +481,22 @@ describe.each(['true', 'false'])('groupMultiselect (isCI = %s)', (isCI) => { expect(value).toEqual(['group1value0']); expect(output.buffer).toMatchSnapshot(); }); + + test('showInstructions: false hides instruction footer', async () => { + const result = prompts.groupMultiselect({ + message: 'foo', + input, + output, + showInstructions: false, + required: false, + options: { + group1: [{ value: 'group1value0' }, { value: 'group1value1' }], + }, + }); + + input.emit('keypress', '', { name: 'return' }); + + await result; + expect(output.buffer).toMatchSnapshot(); + }); }); diff --git a/packages/prompts/test/multi-select.test.ts b/packages/prompts/test/multi-select.test.ts index 4f05598c..e11125e1 100644 --- a/packages/prompts/test/multi-select.test.ts +++ b/packages/prompts/test/multi-select.test.ts @@ -476,4 +476,20 @@ describe.each(['true', 'false'])('multiselect (isCI = %s)', (isCI) => { expect(value).toEqual(['opt6']); expect(output.buffer).toMatchSnapshot(); }); + + test('showInstructions: false hides instruction footer', async () => { + const result = prompts.multiselect({ + message: 'foo', + options: [{ value: 'opt0' }, { value: 'opt1' }], + showInstructions: false, + required: false, + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + await result; + expect(output.buffer).toMatchSnapshot(); + }); }); diff --git a/packages/prompts/test/select.test.ts b/packages/prompts/test/select.test.ts index f0c92ce9..fc3a1751 100644 --- a/packages/prompts/test/select.test.ts +++ b/packages/prompts/test/select.test.ts @@ -377,6 +377,21 @@ describe.each(['true', 'false'])('select (isCI = %s)', (isCI) => { expect(output.buffer).toMatchSnapshot(); }); + test('showInstructions: false hides instruction footer', async () => { + const result = prompts.select({ + message: 'foo', + options: [{ value: 'opt0' }, { value: 'opt1' }], + showInstructions: false, + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + await result; + expect(output.buffer).toMatchSnapshot(); + }); + test('correctly limits options with explicit multiline message', async () => { output.rows = 12;