···11+---
22+"@clack/prompts": patch
33+"@clack/core": patch
44+---
55+66+Adds `placeholder` option to `autocomplete`. When the placeholder is set and the input is empty, pressing `tab` will set the value to `placeholder`.
77+
+27
packages/core/src/prompts/autocomplete.ts
···5151 options: T[] | ((this: AutocompletePrompt<T>) => T[]);
5252 filter?: FilterFunction<T>;
5353 multiple?: boolean;
5454+ /**
5555+ * When set (non-empty), pressing Tab with no input fills the field with this value
5656+ * and runs the normal filter/selection logic so the user can confirm with Enter.
5757+ * Tab only fills the input when the placeholder matches at least one option under
5858+ * the prompt's filter (so the value remains selectable).
5959+ */
6060+ placeholder?: string;
5461}
55625663export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
···6673 #lastUserInput = '';
6774 #filterFn: FilterFunction<T>;
6875 #options: T[] | (() => T[]);
7676+ #placeholder: string | undefined;
69777078 get cursor(): number {
7179 return this.#cursor;
···94102 super(opts);
9510396104 this.#options = opts.options;
105105+ this.#placeholder = opts.placeholder;
97106 const options = this.options;
98107 this.filteredOptions = [...options];
99108 this.multiple = opts.multiple === true;
···142151 const isUpKey = key.name === 'up';
143152 const isDownKey = key.name === 'down';
144153 const isReturnKey = key.name === 'return';
154154+155155+ // Tab with empty input and placeholder: fill input with placeholder to trigger autocomplete
156156+ // Only when the placeholder matches at least one (non-disabled) option so the value remains selectable
157157+ const isEmptyOrOnlyTab = this.userInput === '' || this.userInput === '\t';
158158+ const placeholder = this.#placeholder;
159159+ const options = this.options;
160160+ const placeholderMatchesOption =
161161+ placeholder !== undefined &&
162162+ placeholder !== '' &&
163163+ options.some((opt) => !opt.disabled && this.#filterFn(placeholder, opt));
164164+ if (key.name === 'tab' && isEmptyOrOnlyTab && placeholderMatchesOption) {
165165+ if (this.userInput === '\t') {
166166+ this._clearUserInput();
167167+ }
168168+ this._setUserInput(placeholder, true);
169169+ this.isNavigating = false;
170170+ return;
171171+ }
145172146173 // Start navigation mode with up/down arrows
147174 if (isUpKey || isDownKey) {
+34
packages/core/test/prompts/autocomplete.test.ts
···197197 expect(instance.selectedValues).to.deep.equal([]);
198198 expect(result).to.deep.equal([]);
199199 });
200200+201201+ test('Tab with empty input and placeholder fills input and submit returns matching option', async () => {
202202+ const instance = new AutocompletePrompt({
203203+ input,
204204+ output,
205205+ render: () => 'foo',
206206+ options: testOptions,
207207+ placeholder: 'apple',
208208+ });
209209+210210+ const promise = instance.prompt();
211211+ input.emit('keypress', '\t', { name: 'tab' });
212212+ input.emit('keypress', '', { name: 'return' });
213213+ const result = await promise;
214214+215215+ expect(instance.userInput).to.equal('apple');
216216+ expect(result).to.equal('apple');
217217+ });
218218+219219+ test('Tab with non-matching placeholder does not fill input', async () => {
220220+ const instance = new AutocompletePrompt({
221221+ input,
222222+ output,
223223+ render: () => 'foo',
224224+ options: testOptions,
225225+ placeholder: 'Type to search...',
226226+ });
227227+228228+ instance.prompt();
229229+ input.emit('keypress', '\t', { name: 'tab' });
230230+231231+ // Placeholder does not match any option, so input must not be filled with placeholder
232232+ expect(instance.userInput).not.to.equal('Type to search...');
233233+ });
200234});