diff --git a/packages/tui/src/component/dialog-model.tsx b/packages/tui/src/component/dialog-model.tsx index 38db569aaa24..1146fcb5d0b2 100644 --- a/packages/tui/src/component/dialog-model.tsx +++ b/packages/tui/src/component/dialog-model.tsx @@ -118,10 +118,7 @@ export function DialogModel(props: { providerID?: string }) { if (needle) { return [ - ...sortModelOptions( - fuzzysort.go(needle, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj), - false, - ), + ...searchModelOptions(needle, providerOptions), ...fuzzysort.go(needle, popularProviders, { keys: ["title"] }).map((x) => x.obj), ] } @@ -183,6 +180,12 @@ export function DialogModel(props: { providerID?: string }) { ) } +// Search results keep fuzzysort's relevance order: re-sorting them by release date +// would rank a scattered-letter match above an exact one. +export function searchModelOptions(needle: string, options: T[]) { + return fuzzysort.go(needle, options, { keys: ["title", "category"] }).map((x) => x.obj) +} + export function sortModelOptions( options: T[], newestFirst: boolean, diff --git a/packages/tui/test/cli/cmd/tui/model-options.test.ts b/packages/tui/test/cli/cmd/tui/model-options.test.ts index 97bae7532fe9..fc7dd146e1a5 100644 --- a/packages/tui/test/cli/cmd/tui/model-options.test.ts +++ b/packages/tui/test/cli/cmd/tui/model-options.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { sortModelOptions } from "../../../../src/component/dialog-model" +import { searchModelOptions, sortModelOptions } from "../../../../src/component/dialog-model" describe("sortModelOptions", () => { test("orders provider-scoped model choices by newest release first", () => { @@ -30,3 +30,36 @@ describe("sortModelOptions", () => { expect(sorted.map((model) => model.title)).toEqual(["Free new", "Free old", "GLM 5.2", "GLM 5.1", "GLM 5"]) }) }) + +describe("searchModelOptions", () => { + const category = "OpenRouter" + // Titles from a real OpenRouter catalogue, newest first as the picker sorts them. + const options = [ + { title: "inclusionai/ling-3.0-flash", category, releaseDate: "2026-09-01" }, + { title: "inclusionai/ling-3.0-flash-vl", category, releaseDate: "2026-09-01" }, + { title: "openai/gpt-5.6-luna", category, releaseDate: "2026-08-01" }, + { title: "openai/gpt-5.6-luna-pro", category, releaseDate: "2026-08-01" }, + { title: "poolside/laguna-s-2.1", category, releaseDate: "2026-07-01" }, + { title: "~openai/gpt-luna-latest", category, releaseDate: "2026-06-01" }, + ] + + test("ranks names containing the query above scattered-letter matches", () => { + const titles = searchModelOptions("luna", options).map((option) => option.title) + const exact = titles.filter((title) => title.includes("luna")) + + // Every name that literally contains "luna" comes before any that only match + // its letters in order, like incLUsioNAi or LagUNA. + expect(titles.slice(0, exact.length)).toEqual(exact) + expect(exact).toHaveLength(3) + }) + + test("keeps scattered-letter matches, below the exact ones", () => { + const titles = searchModelOptions("luna", options).map((option) => option.title) + + // Search stays fuzzy: every option matches l-u-n-a in order, so none are dropped. + expect(titles).toHaveLength(options.length) + expect(titles.slice(3)).toEqual( + expect.arrayContaining(["inclusionai/ling-3.0-flash", "inclusionai/ling-3.0-flash-vl", "poolside/laguna-s-2.1"]), + ) + }) +})