From 0203c4e58f9ec205f727e7ce89eb595babe63f9d Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 19 Jan 2026 10:53:37 +0000 Subject: [PATCH 1/2] feat(prompts): add custom filter option to autocomplete --- packages/prompts/src/autocomplete.ts | 13 ++- .../__snapshots__/autocomplete.test.ts.snap | 98 +++++++++++++++++++ packages/prompts/test/autocomplete.test.ts | 82 ++++++++++++++++ 3 files changed, 189 insertions(+), 4 deletions(-) diff --git a/packages/prompts/src/autocomplete.ts b/packages/prompts/src/autocomplete.ts index 89f5b704..44abb67f 100644 --- a/packages/prompts/src/autocomplete.ts +++ b/packages/prompts/src/autocomplete.ts @@ -62,6 +62,11 @@ interface AutocompleteSharedOptions extends CommonOptions { * Validates the value */ validate?: (value: Value | Value[] | undefined) => string | Error | undefined; + /** + * Custom filter function to match options against search input. + * If not provided, a default filter that matches label, hint, and value is used. + */ + filter?: (search: string, option: Option) => boolean; } export interface AutocompleteOptions extends AutocompleteSharedOptions { @@ -80,9 +85,9 @@ export const autocomplete = (opts: AutocompleteOptions) => { options: opts.options, initialValue: opts.initialValue ? [opts.initialValue] : undefined, initialUserInput: opts.initialUserInput, - filter: (search: string, opt: Option) => { + filter: opts.filter ?? ((search: string, opt: Option) => { return getFilteredOption(search, opt); - }, + }), signal: opts.signal, input: opts.input, output: opts.output, @@ -238,9 +243,9 @@ export const autocompleteMultiselect = (opts: AutocompleteMultiSelectOpti const prompt = new AutocompletePrompt>({ options: opts.options, multiple: true, - filter: (search, opt) => { + filter: opts.filter ?? ((search, opt) => { return getFilteredOption(search, opt); - }, + }), validate: () => { if (opts.required && prompt.selectedValues.length === 0) { return 'Please select at least one item'; diff --git a/packages/prompts/test/__snapshots__/autocomplete.test.ts.snap b/packages/prompts/test/__snapshots__/autocomplete.test.ts.snap index 4f299d9f..280658c2 100644 --- a/packages/prompts/test/__snapshots__/autocomplete.test.ts.snap +++ b/packages/prompts/test/__snapshots__/autocomplete.test.ts.snap @@ -335,6 +335,67 @@ exports[`autocomplete > supports initialValue 1`] = ` ] `; +exports[`autocomplete with custom filter > falls back to default filter when not provided 1`] = ` +[ + "", + "│ +◆ Select a fruit +│ +│ Search: _ +│ ● Apple +│ ○ Banana +│ ○ Cherry +│ ↑/↓ to select • Enter: confirm • Type: to search +└", + "", + "", + "", + "│ Search: a█ (2 matches) +│ ● Apple +│ ○ Banana +│ ↑/↓ to select • Enter: confirm • Type: to search +└", + "", + "", + "", + "◇ Select a fruit +│ Apple", + " +", + "", +] +`; + +exports[`autocomplete with custom filter > uses custom filter function when provided 1`] = ` +[ + "", + "│ +◆ Select a fruit +│ +│ Search: _ +│ ● Apple +│ ○ Banana +│ ○ Cherry +│ ↑/↓ to select • Enter: confirm • Type: to search +└", + "", + "", + "", + "│ Search: a█ (1 match) +│ ● Apple +│ ↑/↓ to select • Enter: confirm • Type: to search +└", + "", + "", + "", + "◇ Select a fruit +│ Apple", + " +", + "", +] +`; + exports[`autocompleteMultiselect > can be aborted by a signal 1`] = ` [ "", @@ -461,3 +522,40 @@ exports[`autocompleteMultiselect > renders error when empty selection & required "", ] `; + +exports[`autocompleteMultiselect > supports custom filter function 1`] = ` +[ + "", + "│ +◆ Select fruits +│ +│ Search: _ +│ ◻ Apple +│ ◻ Banana +│ ◻ Cherry +│ ◻ Grape +│ ◻ Orange +│ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search +└", + "", + "", + "", + "│ Search: a█ (1 match) +│ ◻ Apple +│ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search +└", + "", + "", + "", + "│ ◼ Apple", + "", + "", + "", + "", + "◇ Select fruits +│ 1 items selected", + " +", + "", +] +`; diff --git a/packages/prompts/test/autocomplete.test.ts b/packages/prompts/test/autocomplete.test.ts index dcd27891..645128c4 100644 --- a/packages/prompts/test/autocomplete.test.ts +++ b/packages/prompts/test/autocomplete.test.ts @@ -297,4 +297,86 @@ describe('autocompleteMultiselect', () => { expect(value).toEqual(['banana', 'cherry']); expect(output.buffer).toMatchSnapshot(); }); + + test('supports custom filter function', async () => { + const result = autocompleteMultiselect({ + message: 'Select fruits', + options: testOptions, + input, + output, + // Custom filter that only matches exact prefix + filter: (search, option) => { + const label = option.label ?? String(option.value ?? ''); + return label.toLowerCase().startsWith(search.toLowerCase()); + }, + }); + + // Type 'a' - should match 'Apple' only (not 'Banana' which contains 'a') + input.emit('keypress', 'a', { name: 'a' }); + input.emit('keypress', '', { name: 'tab' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + expect(value).toEqual(['apple']); + expect(output.buffer).toMatchSnapshot(); + }); +}); + +describe('autocomplete with custom filter', () => { + let input: MockReadable; + let output: MockWritable; + const testOptions = [ + { value: 'apple', label: 'Apple' }, + { value: 'banana', label: 'Banana' }, + { value: 'cherry', label: 'Cherry' }, + ]; + + beforeEach(() => { + input = new MockReadable(); + output = new MockWritable(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + test('uses custom filter function when provided', async () => { + const result = autocomplete({ + message: 'Select a fruit', + options: testOptions, + input, + output, + // Custom filter that only matches exact prefix + filter: (search, option) => { + const label = option.label ?? String(option.value ?? ''); + return label.toLowerCase().startsWith(search.toLowerCase()); + }, + }); + + // Type 'a' - should match 'Apple' only (not 'Banana' which contains 'a') + input.emit('keypress', 'a', { name: 'a' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + expect(value).toBe('apple'); + expect(output.buffer).toMatchSnapshot(); + }); + + test('falls back to default filter when not provided', async () => { + const result = autocomplete({ + message: 'Select a fruit', + options: testOptions, + input, + output, + }); + + // Type 'a' - default filter should match both 'Apple' and 'Banana' + input.emit('keypress', 'a', { name: 'a' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + // First match should be selected + expect(value).toBe('apple'); + expect(output.buffer).toMatchSnapshot(); + }); }); From 73af0a8d567b66fba5d2e8c089ef42f046b8e973 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 19 Jan 2026 16:31:58 +0000 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20changeset=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/afraid-rabbits-grin.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/afraid-rabbits-grin.md diff --git a/.changeset/afraid-rabbits-grin.md b/.changeset/afraid-rabbits-grin.md new file mode 100644 index 00000000..bb6b7eb1 --- /dev/null +++ b/.changeset/afraid-rabbits-grin.md @@ -0,0 +1,5 @@ +--- +"@clack/prompts": minor +--- + +This adds a custom filter function to autocompleteMultiselect. It could be used, for example, to support fuzzy searching logic.