[READ-ONLY] Mirror of https://github.com/bombshell-dev/clack. Effortlessly build beautiful command-line apps bomb.sh/docs/clack/basics/getting-started/
cli command-line command-line-app node prompt prompts
5

Configure Feed

Select the types of activity you want to include in your feed.

feat(prompts,core): make autocomplete placeholder tabbable (#485)

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

authored by

paul valladares
Nate Moore
and committed by
GitHub
(Mar 16, 2026, 4:41 PM -0500) bdf89a5f 52fce8a6

+116
+7
.changeset/dirty-actors-find.md
··· 1 + --- 2 + "@clack/prompts": patch 3 + "@clack/core": patch 4 + --- 5 + 6 + Adds `placeholder` option to `autocomplete`. When the placeholder is set and the input is empty, pressing `tab` will set the value to `placeholder`. 7 +
+27
packages/core/src/prompts/autocomplete.ts
··· 51 51 options: T[] | ((this: AutocompletePrompt<T>) => T[]); 52 52 filter?: FilterFunction<T>; 53 53 multiple?: boolean; 54 + /** 55 + * When set (non-empty), pressing Tab with no input fills the field with this value 56 + * and runs the normal filter/selection logic so the user can confirm with Enter. 57 + * Tab only fills the input when the placeholder matches at least one option under 58 + * the prompt's filter (so the value remains selectable). 59 + */ 60 + placeholder?: string; 54 61 } 55 62 56 63 export default class AutocompletePrompt<T extends OptionLike> extends Prompt< ··· 66 73 #lastUserInput = ''; 67 74 #filterFn: FilterFunction<T>; 68 75 #options: T[] | (() => T[]); 76 + #placeholder: string | undefined; 69 77 70 78 get cursor(): number { 71 79 return this.#cursor; ··· 94 102 super(opts); 95 103 96 104 this.#options = opts.options; 105 + this.#placeholder = opts.placeholder; 97 106 const options = this.options; 98 107 this.filteredOptions = [...options]; 99 108 this.multiple = opts.multiple === true; ··· 142 151 const isUpKey = key.name === 'up'; 143 152 const isDownKey = key.name === 'down'; 144 153 const isReturnKey = key.name === 'return'; 154 + 155 + // Tab with empty input and placeholder: fill input with placeholder to trigger autocomplete 156 + // Only when the placeholder matches at least one (non-disabled) option so the value remains selectable 157 + const isEmptyOrOnlyTab = this.userInput === '' || this.userInput === '\t'; 158 + const placeholder = this.#placeholder; 159 + const options = this.options; 160 + const placeholderMatchesOption = 161 + placeholder !== undefined && 162 + placeholder !== '' && 163 + options.some((opt) => !opt.disabled && this.#filterFn(placeholder, opt)); 164 + if (key.name === 'tab' && isEmptyOrOnlyTab && placeholderMatchesOption) { 165 + if (this.userInput === '\t') { 166 + this._clearUserInput(); 167 + } 168 + this._setUserInput(placeholder, true); 169 + this.isNavigating = false; 170 + return; 171 + } 145 172 146 173 // Start navigation mode with up/down arrows 147 174 if (isUpKey || isDownKey) {
+34
packages/core/test/prompts/autocomplete.test.ts
··· 197 197 expect(instance.selectedValues).to.deep.equal([]); 198 198 expect(result).to.deep.equal([]); 199 199 }); 200 + 201 + test('Tab with empty input and placeholder fills input and submit returns matching option', async () => { 202 + const instance = new AutocompletePrompt({ 203 + input, 204 + output, 205 + render: () => 'foo', 206 + options: testOptions, 207 + placeholder: 'apple', 208 + }); 209 + 210 + const promise = instance.prompt(); 211 + input.emit('keypress', '\t', { name: 'tab' }); 212 + input.emit('keypress', '', { name: 'return' }); 213 + const result = await promise; 214 + 215 + expect(instance.userInput).to.equal('apple'); 216 + expect(result).to.equal('apple'); 217 + }); 218 + 219 + test('Tab with non-matching placeholder does not fill input', async () => { 220 + const instance = new AutocompletePrompt({ 221 + input, 222 + output, 223 + render: () => 'foo', 224 + options: testOptions, 225 + placeholder: 'Type to search...', 226 + }); 227 + 228 + instance.prompt(); 229 + input.emit('keypress', '\t', { name: 'tab' }); 230 + 231 + // Placeholder does not match any option, so input must not be filled with placeholder 232 + expect(instance.userInput).not.to.equal('Type to search...'); 233 + }); 200 234 });
+2
packages/prompts/src/autocomplete.ts
··· 85 85 options: opts.options, 86 86 initialValue: opts.initialValue ? [opts.initialValue] : undefined, 87 87 initialUserInput: opts.initialUserInput, 88 + placeholder: opts.placeholder, 88 89 filter: 89 90 opts.filter ?? 90 91 ((search: string, opt: Option<Value>) => { ··· 267 268 const prompt = new AutocompletePrompt<Option<Value>>({ 268 269 options: opts.options, 269 270 multiple: true, 271 + placeholder: opts.placeholder, 270 272 filter: 271 273 opts.filter ?? 272 274 ((search, opt) => {
+46
packages/prompts/test/autocomplete.test.ts
··· 137 137 expect(value).toBe('apple'); 138 138 }); 139 139 140 + test('Tab with placeholder fills input and Enter submits matching option', async () => { 141 + const result = autocomplete({ 142 + message: 'Select a fruit', 143 + placeholder: 'apple', 144 + options: testOptions, 145 + input, 146 + output, 147 + }); 148 + 149 + input.emit('keypress', '\t', { name: 'tab' }); 150 + input.emit('keypress', '', { name: 'return' }); 151 + const value = await result; 152 + expect(value).toBe('apple'); 153 + }); 154 + 155 + test('Tab with non-matching placeholder does not fill input', async () => { 156 + const result = autocomplete({ 157 + message: 'Select a fruit', 158 + placeholder: 'Type to search...', 159 + options: testOptions, 160 + input, 161 + output, 162 + }); 163 + 164 + input.emit('keypress', '\t', { name: 'tab' }); 165 + input.emit('keypress', '', { name: 'return' }); 166 + const value = await result; 167 + // Tab did not fill input with placeholder (no option matches), so Enter submits first option 168 + expect(value).toBe('apple'); 169 + }); 170 + 140 171 test('supports initialValue', async () => { 141 172 const result = autocomplete({ 142 173 message: 'Select a fruit', ··· 409 440 const value = await result; 410 441 expect(value).toEqual([]); 411 442 expect(output.buffer).toMatchSnapshot(); 443 + }); 444 + 445 + test('Tab with placeholder fills input; Enter submits current selection', async () => { 446 + const result = autocompleteMultiselect({ 447 + message: 'Select fruits', 448 + placeholder: 'apple', 449 + options: testOptions, 450 + input, 451 + output, 452 + }); 453 + 454 + input.emit('keypress', '\t', { name: 'tab' }); 455 + input.emit('keypress', '', { name: 'return' }); 456 + const value = await result; 457 + expect(value).toEqual([]); 412 458 }); 413 459 }); 414 460