Skip to content

Conversation

@ascorbic
Copy link

@ascorbic ascorbic commented Nov 16, 2025

A first pass at adding the async/search functionality to the autocomplete prompt.

I've added a new filteredOptions option, that's a function which returns filtered results, which is optionally async. I've then re-implemented the existing funcitonality using this lower-level function.

There is a little bit of trickery to avoid needing to specify whether the function is async and still skip debouncing if it's sync: it just checks if the result is a thenable, then sets that as a property meaning it skips debouncing for the next keypresses.

I've included some (mostly AI-generated) tests and examples, and all existing tests still pass.

@changeset-bot
Copy link

changeset-bot bot commented Nov 16, 2025

🦋 Changeset detected

Latest commit: 6c5294c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@clack/prompts Minor
@clack/core Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

extends PromptOptions<T['value'] | T['value'][], AutocompletePrompt<T>> {
options: T[] | ((this: AutocompletePrompt<T>) => T[]);
filter?: FilterFunction<T>;
filteredOptions?: never;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please bikeshed this name

}

private render() {
protected render() {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to be able to call this manually, as we're not just rendering on keypress

"<cursor.down count=3>",
"<erase.down>",
"[36m│[39m [2mSearch:[22m z█[2m (0 matches)[22m
"[36m│[39m [2mSearch:[22m z█
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We no longer display "(0 matches)" here, which I think is fine as there's "No matches found" imemdiately below. This is because we don't show it unless the user has entered some text. Previously the check for whether to display the number os results was if the total number of items is different from the filtered number, but that's not usable now.

multiple?: boolean;
}

type AutocompleteOptions<T extends OptionLike> =
Copy link
Collaborator

@43081j 43081j Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd like to avoid needing two option types ideally

in my attempt at this, i ended up with:

interface AutocompleteOptions<T extends OptionLike>
	extends PromptOptions<T['value'] | T['value'][], AutocompletePrompt<T>> {
	options: MaybePromise<T[]> | ((this: AutocompletePrompt<T>) => MaybePromise<T[]>);
	filter?: FilterFunction<T>;
	multiple?: boolean;
}

most of the changes in the constructor then go away, because the constructor just needs to set #options. the overridden prompt is what would initialise the first set of options.

in that, mine does this:

	protected async _initializeOptions(): Promise<void> {
		const optionsResult = this.options;
		const isAsync = isThenable(optionsResult);

		if (isAsync) {
			this.#isLoading = true;
			this.render();
		}

		const options = await optionsResult;

		this.#resolvedOptions = options;

		if (isAsync) {
			this.#isLoading = false;
			this.render();
		}

        // existing untouched logic here from the constructor in main

this same pattern happens in the user input handler, and the prompt renderer itself uses resolvedOptions (a getter that just returns #resolvedOptions) instead of options

let me know what you think. i'd like to get this PR shaped in a way that we both agree rather than me just going off and doing a different thing

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks reasonable. This presumably needs you to pass a () => true filter function, which is quite awkward DX. I wonder if there's a better way to do that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're right and i don't like that either 😅

i wonder if we need a filterStrategy: 'local' | 'remote' or some such thing

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the original reason why I had the filtered and unfiltered arrays as separate args

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants