From a25156769d8128856440fd07dc7b12a7e7d11e83 Mon Sep 17 00:00:00 2001 From: Benjamin Jesuiter Date: Mon, 16 Feb 2026 20:30:14 +0100 Subject: [PATCH] CLI: switch command selector to interactive autocomplete --- src/cli/program/command-selector.ts | 77 +++++++++-------------------- 1 file changed, 24 insertions(+), 53 deletions(-) diff --git a/src/cli/program/command-selector.ts b/src/cli/program/command-selector.ts index e86bf24825b..2754e547bf9 100644 --- a/src/cli/program/command-selector.ts +++ b/src/cli/program/command-selector.ts @@ -1,16 +1,14 @@ import type { Command } from "commander"; -import { isCancel, select as clackSelect, text as clackText } from "@clack/prompts"; +import { autocomplete as clackAutocomplete, isCancel } from "@clack/prompts"; import { stylePromptHint, stylePromptMessage } from "../../terminal/prompt-style.js"; -import { theme } from "../../terminal/theme.js"; import { fuzzyFilterLower, prepareSearchItems } from "../../tui/components/fuzzy-filter.js"; import { getCoreCliCommandNames, registerCoreCliByName } from "./command-registry.js"; import { getProgramContext } from "./program-context.js"; import { getSubCliEntries, registerSubCliByName } from "./register.subclis.js"; -const SEARCH_AGAIN_VALUE = "__search_again__"; const SHOW_HELP_VALUE = "__show_help__"; const PATH_SEPARATOR = "\u0000"; -const MAX_MATCHES = 24; +const MAX_RESULTS = 200; type CommandSelectorCandidate = { path: string[]; @@ -122,64 +120,37 @@ export async function runInteractiveCommandSelector(program: Command): Promise({ - message: - stylePromptMessage( - shown.length === matches.length - ? `Select a command (${matches.length} matches)` - : `Select a command (showing ${shown.length} of ${matches.length} matches)`, - ) ?? "Select a command", - options: [ - ...shown.map((candidate) => ({ + const selection = await clackAutocomplete({ + message: stylePromptMessage("Find and run a command") ?? "Find and run a command", + placeholder: "Type to fuzzy-search (e.g. msg snd)", + maxItems: 10, + // We pre-rank the list with our fuzzy scorer, then opt out of clack's own + // filter so item order stays stable and score-based. + filter: () => true, + options() { + const query = this.userInput.trim(); + const ranked = rankCommandSelectorCandidates(candidates, query).slice(0, MAX_RESULTS); + return [ + ...ranked.map((candidate) => ({ value: candidate.path.join(PATH_SEPARATOR), label: candidate.label, hint: stylePromptHint(candidate.description), })), - { - value: SEARCH_AGAIN_VALUE, - label: "Search again", - hint: stylePromptHint("Change your fuzzy query"), - }, { value: SHOW_HELP_VALUE, label: "Show help", hint: stylePromptHint("Skip selector and print CLI help"), }, - ], - }); + ]; + }, + }); - if (isCancel(selection) || selection === SHOW_HELP_VALUE) { - return null; - } - if (selection === SEARCH_AGAIN_VALUE) { - continue; - } - - return selection - .split(PATH_SEPARATOR) - .map((segment) => segment.trim()) - .filter(Boolean); + if (isCancel(selection) || selection === SHOW_HELP_VALUE) { + return null; } + + return selection + .split(PATH_SEPARATOR) + .map((segment) => segment.trim()) + .filter(Boolean); }