From a9144eccf885ec436161a2b69a8adec4bee2ece2 Mon Sep 17 00:00:00 2001 From: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:39:51 +0530 Subject: [PATCH] feat(app): improvements to model search (#34954) --- .../dialog-select-model-search.test.ts | 19 +++++++++++++++++++ .../components/dialog-select-model-search.ts | 18 ++++++++++++++++++ .../src/components/dialog-select-model.tsx | 18 +++++------------- 3 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 packages/app/src/components/dialog-select-model-search.test.ts create mode 100644 packages/app/src/components/dialog-select-model-search.ts diff --git a/packages/app/src/components/dialog-select-model-search.test.ts b/packages/app/src/components/dialog-select-model-search.test.ts new file mode 100644 index 0000000000..3ee8009140 --- /dev/null +++ b/packages/app/src/components/dialog-select-model-search.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, test } from "bun:test" +import { matchesModelSearch } from "./dialog-select-model-search" + +describe("matchesModelSearch", () => { + test("matches model names across separators", () => { + expect(matchesModelSearch("gpt 5", ["GPT-5.5"])).toBe(true) + expect(matchesModelSearch("gpt-5", ["GPT-5.5"])).toBe(true) + expect(matchesModelSearch("gpt5", ["GPT-5.5"])).toBe(true) + }) + + test("matches any searchable model field", () => { + expect(matchesModelSearch("open ai", ["GPT-5.5", "gpt-5.5", "OpenAI"])).toBe(true) + expect(matchesModelSearch("gpt 5", ["GPT-5.5", "gpt-5.5", "OpenAI"])).toBe(true) + }) + + test("does not match unrelated searches", () => { + expect(matchesModelSearch("claude", ["GPT-5.5", "gpt-5.5", "OpenAI"])).toBe(false) + }) +}) diff --git a/packages/app/src/components/dialog-select-model-search.ts b/packages/app/src/components/dialog-select-model-search.ts new file mode 100644 index 0000000000..901f3fc114 --- /dev/null +++ b/packages/app/src/components/dialog-select-model-search.ts @@ -0,0 +1,18 @@ +export const normalizeModelSearch = (value: string) => + value + .toLowerCase() + .replace(/[^\p{Letter}\p{Number}]+/gu, " ") + .trim() + .replace(/\s+/g, " ") + +export const compactModelSearch = (value: string) => normalizeModelSearch(value).replaceAll(" ", "") + +export const matchesModelSearch = (query: string, values: string[]) => { + const search = normalizeModelSearch(query) + if (!search) return true + + const compactSearch = compactModelSearch(query) + return values.some( + (value) => normalizeModelSearch(value).includes(search) || compactModelSearch(value).includes(compactSearch), + ) +} diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index 9725133fd6..778c504596 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -17,6 +17,7 @@ import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" import { ModelTooltip } from "./model-tooltip" import { useLanguage } from "@/context/language" import { decode64 } from "@/utils/base64" +import { matchesModelSearch } from "./dialog-select-model-search" const isFree = (provider: string, cost: { input: number } | undefined) => provider === "opencode" && (!cost || cost.input === 0) @@ -242,13 +243,10 @@ export function ModelSelectorPopoverV2(props: { .filter((item) => (props.provider ? item.provider.id === props.provider : true)), ) const models = createMemo(() => { - const search = store.search.trim().toLowerCase() + const search = store.search.trim() const filtered = search ? allModels().filter( - (item) => - item.name.toLowerCase().includes(search) || - item.id.toLowerCase().includes(search) || - item.provider.name.toLowerCase().includes(search), + (item) => matchesModelSearch(search, [item.name, item.id, item.provider.name]), ) : allModels() @@ -333,16 +331,10 @@ export function ModelSelectorPopoverV2(props: { queueMicrotask(() => activeItem()?.scrollIntoView({ block: "nearest" })) } const setSearch = (value: string) => { - const search = value.trim().toLowerCase() + const search = value.trim() const first = [...allModels()] .sort((a, b) => a.name.localeCompare(b.name)) - .find( - (item) => - !search || - item.name.toLowerCase().includes(search) || - item.id.toLowerCase().includes(search) || - item.provider.name.toLowerCase().includes(search), - ) + .find((item) => matchesModelSearch(search, [item.name, item.id, item.provider.name])) setStore({ search: value, active: first ? modelKey(first) : manageKey }) }