diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index bcbbe69287b..b5bc17e7f2d 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -185,11 +185,37 @@ export function DialogModel(props: { providerID?: string }) { ) : [] - // Search shows a single merged list (favorites inline) - if (needle) { - const filteredProviders = fuzzysort.go(needle, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj) - const filteredPopular = fuzzysort.go(needle, popularProviders, { keys: ["title"] }).map((x) => x.obj) - return [...filteredProviders, ...filteredPopular] + // Apply fuzzy filtering to each section separately, maintaining section order + // Sort with prefix matches first (alphabetically), then other matches (alphabetically) + // Preserve "Free first" ordering within each group + if (q) { + const needle = q.toLowerCase() + const sortWithPrefixFirst = (items: T[]): T[] => + items.sort((a, b) => { + const aTitle = a.title.toLowerCase() + const bTitle = b.title.toLowerCase() + const aIsPrefix = aTitle.startsWith(needle) + const bIsPrefix = bTitle.startsWith(needle) + if (aIsPrefix && !bIsPrefix) return -1 + if (!aIsPrefix && bIsPrefix) return 1 + // Preserve "Free first" within same prefix group + if (a.footer === "Free" && b.footer !== "Free") return -1 + if (a.footer !== "Free" && b.footer === "Free") return 1 + return a.title.localeCompare(b.title) + }) + const filteredFavorites = sortWithPrefixFirst( + fuzzysort.go(q, favoriteOptions, { keys: ["title"] }).map((x) => x.obj), + ) + const filteredRecents = sortWithPrefixFirst( + fuzzysort.go(q, recentOptions, { keys: ["title"] }).map((x) => x.obj).slice(0, 5), + ) + const filteredProviders = sortWithPrefixFirst( + fuzzysort.go(q, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj), + ) + const filteredPopular = sortWithPrefixFirst( + fuzzysort.go(q, popularProviders, { keys: ["title"] }).map((x) => x.obj), + ) + return [...filteredFavorites, ...filteredRecents, ...filteredProviders, ...filteredPopular] } return [...favoriteOptions, ...recentOptions, ...providerOptions, ...popularProviders] diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index f1cdaaa5292..e649ca26deb 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -78,7 +78,19 @@ export function DialogSelect(props: DialogSelectProps) { const result = pipe( props.options, filter((x) => x.disabled !== true), - (x) => (!needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj)), + (x) => { + if (!needle) return x + const fuzzyResults = fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj) + return fuzzyResults.sort((a, b) => { + const aTitle = a.title.toLowerCase() + const bTitle = b.title.toLowerCase() + const aIsPrefix = aTitle.startsWith(needle) + const bIsPrefix = bTitle.startsWith(needle) + if (aIsPrefix && !bIsPrefix) return -1 + if (!aIsPrefix && bIsPrefix) return 1 + return a.title.localeCompare(b.title) + }) + }, ) return result }) @@ -87,7 +99,6 @@ export function DialogSelect(props: DialogSelectProps) { const result = pipe( filtered(), groupBy((x) => x.category ?? ""), - // mapValues((x) => x.sort((a, b) => a.title.localeCompare(b.title))), entries(), ) return result