From 64a6b8c4510f0f716606c4ea65979e3ace5eda38 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 5 Jul 2026 16:20:57 +0000 Subject: [PATCH 1/3] Optimize model picker search rerenders Co-authored-by: Julius Marminge --- .../components/chat/ModelPickerContent.tsx | 139 +++++++++++++----- .../components/chat/modelPickerSearch.test.ts | 13 +- .../src/components/chat/modelPickerSearch.ts | 4 + 3 files changed, 115 insertions(+), 41 deletions(-) diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index e0d6b9afc5e..e6bea2b1c0c 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -5,12 +5,27 @@ import { } from "@t3tools/contracts"; import { resolveSelectableModel } from "@t3tools/shared/model"; import { LegendList, type LegendListRef } from "@legendapp/list/react"; -import { memo, useMemo, useState, useCallback, useEffect, useLayoutEffect, useRef } from "react"; +import { + memo, + useMemo, + useState, + useCallback, + useEffect, + useLayoutEffect, + useRef, + type ChangeEvent, + type KeyboardEvent, + type RefObject, +} from "react"; import { SearchIcon } from "lucide-react"; import { ModelListRow } from "./ModelListRow"; import { ModelPickerSidebar } from "./ModelPickerSidebar"; import { isModelPickerNewModel } from "./modelPickerModelHighlights"; -import { buildModelPickerSearchText, scoreModelPickerSearch } from "./modelPickerSearch"; +import { + buildModelPickerSearchText, + normalizeModelPickerSearchQuery, + scoreModelPickerSearch, +} from "./modelPickerSearch"; import { Combobox, ComboboxEmpty, ComboboxInput, ComboboxListVirtualized } from "../ui/combobox"; import { ModelEsque } from "./providerIconUtils"; import { @@ -43,6 +58,72 @@ type ModelPickerItem = { const EMPTY_MODEL_JUMP_LABELS = new Map(); +const ModelPickerSearchInput = memo(function ModelPickerSearchInput(props: { + inputRef: RefObject; + onNormalizedSearchQueryChange: (query: string) => void; + onRequestClose?: () => void; + onSelectHighlightedModel: () => boolean; +}) { + const { inputRef, onNormalizedSearchQueryChange, onRequestClose, onSelectHighlightedModel } = + props; + const [rawSearchQuery, setRawSearchQuery] = useState(""); + const publishedSearchQueryRef = useRef(normalizeModelPickerSearchQuery(rawSearchQuery)); + + const handleSearchChange = useCallback( + (event: ChangeEvent) => { + const nextRawSearchQuery = event.target.value; + setRawSearchQuery(nextRawSearchQuery); + + const nextSearchQuery = normalizeModelPickerSearchQuery(nextRawSearchQuery); + if (nextSearchQuery === publishedSearchQueryRef.current) { + return; + } + publishedSearchQueryRef.current = nextSearchQuery; + onNormalizedSearchQueryChange(nextSearchQuery); + }, + [onNormalizedSearchQueryChange], + ); + + const handleSearchKeyDown = useCallback( + (event: KeyboardEvent) => { + if (event.key === "Escape") { + event.preventDefault(); + event.stopPropagation(); + onRequestClose?.(); + return; + } + if (event.key === "Enter" && onSelectHighlightedModel()) { + (event as typeof event & { preventBaseUIHandler?: () => void }).preventBaseUIHandler?.(); + event.preventDefault(); + event.stopPropagation(); + return; + } + event.stopPropagation(); + }, + [onRequestClose, onSelectHighlightedModel], + ); + + return ( + + } + value={rawSearchQuery} + onChange={handleSearchChange} + onKeyDown={handleSearchKeyDown} + onMouseDown={(event) => event.stopPropagation()} + onTouchStart={(event) => event.stopPropagation()} + size="sm" + unstyled + /> + ); +}); + // Split a `${instanceId}:${slug}` combobox key back into its pieces. Slugs // can contain colons (e.g. some vendor model ids), so we only split on the // first colon — anything after that is the slug. @@ -221,7 +302,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { }, [modelOptionsByInstance, entryByInstanceId, readyInstanceSet]); const isLocked = props.lockedProvider !== null; - const isSearching = searchQuery.trim().length > 0; + const isSearching = searchQuery.length > 0; const lockedDisabledInstanceIds = useMemo(() => { if (!isLocked) { return undefined; @@ -261,7 +342,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { let result = flatModels; // Apply tokenized fuzzy search across the combined provider/model search fields. - if (searchQuery.trim()) { + if (searchQuery) { const rankedMatches = result .map((model) => ({ model, @@ -385,6 +466,15 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { }, [entryByInstanceId, getModelDisabledReason, modelOptionsByInstance, onInstanceModelChange], ); + const selectHighlightedModel = useCallback((): boolean => { + const highlightedModelKey = highlightedModelKeyRef.current; + if (!highlightedModelKey) { + return false; + } + const { instanceId, slug } = splitInstanceModelKey(highlightedModelKey); + handleModelSelect(slug, instanceId); + return true; + }, [handleModelSelect]); const toggleFavorite = useCallback( (instanceId: ProviderInstanceId, model: string) => { @@ -577,42 +667,11 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { {/* Search bar */}
- - } - value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Escape") { - e.preventDefault(); - e.stopPropagation(); - props.onRequestClose?.(); - return; - } - if (e.key === "Enter" && highlightedModelKeyRef.current) { - ( - e as typeof e & { preventBaseUIHandler?: () => void } - ).preventBaseUIHandler?.(); - e.preventDefault(); - e.stopPropagation(); - const { instanceId, slug } = splitInstanceModelKey( - highlightedModelKeyRef.current, - ); - handleModelSelect(slug, instanceId); - return; - } - e.stopPropagation(); - }} - onMouseDown={(e) => e.stopPropagation()} - onTouchStart={(e) => e.stopPropagation()} - size="sm" - unstyled +
diff --git a/apps/web/src/components/chat/modelPickerSearch.test.ts b/apps/web/src/components/chat/modelPickerSearch.test.ts index 57a7142c6b9..ea07a9404e3 100644 --- a/apps/web/src/components/chat/modelPickerSearch.test.ts +++ b/apps/web/src/components/chat/modelPickerSearch.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from "vite-plus/test"; -import { buildModelPickerSearchText, scoreModelPickerSearch } from "./modelPickerSearch"; +import { + buildModelPickerSearchText, + normalizeModelPickerSearchQuery, + scoreModelPickerSearch, +} from "./modelPickerSearch"; describe("buildModelPickerSearchText", () => { it("builds provider-agnostic search text from generic fields", () => { @@ -15,6 +19,13 @@ describe("buildModelPickerSearchText", () => { }); }); +describe("normalizeModelPickerSearchQuery", () => { + it("collapses search text to the effective query used by ranking", () => { + expect(normalizeModelPickerSearchQuery(" GPT 5.5 ")).toBe("gpt 5.5"); + expect(normalizeModelPickerSearchQuery(" ")).toBe(""); + }); +}); + describe("scoreModelPickerSearch", () => { it("matches typo-tolerant multi-token queries", () => { expect( diff --git a/apps/web/src/components/chat/modelPickerSearch.ts b/apps/web/src/components/chat/modelPickerSearch.ts index ff265e4a65a..7a9d7f890bd 100644 --- a/apps/web/src/components/chat/modelPickerSearch.ts +++ b/apps/web/src/components/chat/modelPickerSearch.ts @@ -52,6 +52,10 @@ export function buildModelPickerSearchText(model: ModelPickerSearchableModel): s ); } +export function normalizeModelPickerSearchQuery(query: string): string { + return normalizeSearchQuery(query); +} + export function scoreModelPickerSearch( model: ModelPickerSearchableModel, query: string, From 0b3a318d193055043f550451be84fe50e86b0418 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 5 Jul 2026 16:21:51 +0000 Subject: [PATCH 2/3] Normalize model picker search tokens Co-authored-by: Julius Marminge --- apps/web/src/components/chat/modelPickerSearch.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/chat/modelPickerSearch.ts b/apps/web/src/components/chat/modelPickerSearch.ts index 7a9d7f890bd..2d39f8ff216 100644 --- a/apps/web/src/components/chat/modelPickerSearch.ts +++ b/apps/web/src/components/chat/modelPickerSearch.ts @@ -53,7 +53,10 @@ export function buildModelPickerSearchText(model: ModelPickerSearchableModel): s } export function normalizeModelPickerSearchQuery(query: string): string { - return normalizeSearchQuery(query); + return normalizeSearchQuery(query) + .split(/\s+/u) + .filter((token) => token.length > 0) + .join(" "); } export function scoreModelPickerSearch( From 8481e20015dff7299831740383f2b081f1df1957 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 5 Jul 2026 16:22:59 +0000 Subject: [PATCH 3/3] Fix model picker optional close prop Co-authored-by: Julius Marminge --- apps/web/src/components/chat/ModelPickerContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index e6bea2b1c0c..5818fee212c 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -670,8 +670,8 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {