From 254ed7b8d3c892abee16f3925c4095591f37d8f4 Mon Sep 17 00:00:00 2001 From: TonybynMp4 <97451137+TonybynMp4@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:26:24 +0200 Subject: [PATCH 1/3] fix(web): hoist favorited legacy models to the top of the model picker Co-Authored-By: Claude Opus 4.8 --- .../components/chat/ModelPickerContent.tsx | 18 ++++++++++--- apps/web/src/modelOrdering.test.ts | 27 +++++++++++++++++++ apps/web/src/modelOrdering.ts | 26 ++++++++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index 5ee3743a33ee..bf360e79c636 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -45,7 +45,11 @@ import { isProviderInstancePickerVisible, type ProviderInstanceEntry, } from "../../providerInstances"; -import { providerModelKey, sortProviderModelItems } from "../../modelOrdering"; +import { + partitionLegacyModels, + providerModelKey, + sortProviderModelItems, +} from "../../modelOrdering"; type ModelPickerItem = { slug: string; @@ -522,8 +526,14 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { if (isSearching || selectedInstanceId === "favorites") { return null; } - const currentModels = filteredModels.filter((model) => !model.isLegacy); - const legacyModels = filteredModels.filter((model) => model.isLegacy); + // Favorited models are hoisted to the top, and that hoist has to win over + // the legacy split: a favorited legacy model stays in the main list rather + // than being buried in the collapsed legacy group. Unfavoriting it drops it + // back into the legacy section on the next render. + const { current: currentModels, legacy: legacyModels } = partitionLegacyModels( + filteredModels, + (model) => favoritesSet.has(providerModelKey(model.instanceId, model.slug)), + ); if (legacyModels.length === 0) { return null; } @@ -533,7 +543,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { legacyModels, isExpanded: expandedLegacyInstances.has(selectedInstanceId), }; - }, [expandedLegacyInstances, filteredModels, isSearching, selectedInstanceId]); + }, [expandedLegacyInstances, favoritesSet, filteredModels, isSearching, selectedInstanceId]); const visibleModels = useMemo(() => { if (!legacySection) { diff --git a/apps/web/src/modelOrdering.test.ts b/apps/web/src/modelOrdering.test.ts index 852d3b0e13dc..7b7b3af8eb56 100644 --- a/apps/web/src/modelOrdering.test.ts +++ b/apps/web/src/modelOrdering.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vite-plus/test"; import { ProviderInstanceId } from "@t3tools/contracts"; import { + partitionLegacyModels, providerModelKey, sortModelsForProviderInstance, sortProviderModelItems, @@ -49,4 +50,30 @@ describe("model ordering", () => { }).map((item) => item.slug), ).toEqual(["gpt-5.4-mini", "gpt-5.5", "crest-alpha", "claude-opus-4-6"]); }); + + describe("partitionLegacyModels", () => { + const models = [ + { slug: "opus-5" }, + { slug: "sonnet-5" }, + { slug: "opus-4-8", isLegacy: true }, + { slug: "haiku-3", isLegacy: true }, + ]; + + it("keeps a favorited legacy model in the main list instead of the legacy group", () => { + const { current, legacy } = partitionLegacyModels( + models, + (model) => model.slug === "opus-4-8", + ); + + expect(current.map((model) => model.slug)).toEqual(["opus-5", "sonnet-5", "opus-4-8"]); + expect(legacy.map((model) => model.slug)).toEqual(["haiku-3"]); + }); + + it("returns legacy models to the legacy group once unfavorited", () => { + const { current, legacy } = partitionLegacyModels(models, () => false); + + expect(current.map((model) => model.slug)).toEqual(["opus-5", "sonnet-5"]); + expect(legacy.map((model) => model.slug)).toEqual(["opus-4-8", "haiku-3"]); + }); + }); }); diff --git a/apps/web/src/modelOrdering.ts b/apps/web/src/modelOrdering.ts index c8b5d9cec643..69b5399ce152 100644 --- a/apps/web/src/modelOrdering.ts +++ b/apps/web/src/modelOrdering.ts @@ -55,6 +55,32 @@ export function sortModelsForProviderInstance( return Arr.sort(models, Order.combineAll(orders)); } +export interface LegacyPartitionItem { + readonly isLegacy?: boolean | undefined; +} + +/** + * Split an already-sorted model list into the main list and the collapsed + * legacy group. Favorited models stay in the main list even when they are + * legacy, so the favorite hoist wins over the legacy split; unfavoriting a + * legacy model returns it to the legacy group. `isFavorite` decides membership. + */ +export function partitionLegacyModels( + models: ReadonlyArray, + isFavorite: (model: T) => boolean, +): { readonly current: T[]; readonly legacy: T[] } { + const current: T[] = []; + const legacy: T[] = []; + for (const model of models) { + if (model.isLegacy === true && !isFavorite(model)) { + legacy.push(model); + } else { + current.push(model); + } + } + return { current, legacy }; +} + export function sortProviderModelItems( items: ReadonlyArray, options?: { From 66e157a0b211dbbb3083dc535d13585352df39a9 Mon Sep 17 00:00:00 2001 From: TonybynMp4 <97451137+TonybynMp4@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:33:22 +0200 Subject: [PATCH 2/3] fix(web): don't auto-expand legacy group for a favorited active model Co-Authored-By: Claude Opus 4.8 --- .../components/chat/ModelPickerContent.tsx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index bf360e79c636..7b563bb43aea 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -242,16 +242,22 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { return favorites.length > 0 ? "favorites" : props.activeInstanceId; }, ); - const [expandedLegacyInstances, setExpandedLegacyInstances] = useState( - () => - new Set( + const [expandedLegacyInstances, setExpandedLegacyInstances] = useState(() => { + // Auto-expand the legacy group only when the active model actually lives + // there. A favorited legacy model is hoisted into the main list, so + // expanding the group for it would open an unrelated section. + const activeIsFavorite = favorites.some( + (fav) => fav.provider === props.activeInstanceId && fav.model === activeModelSlug, + ); + return new Set( + !activeIsFavorite && modelOptionsByInstance .get(props.activeInstanceId) ?.some((model) => model.slug === activeModelSlug && model.isLegacy) - ? [props.activeInstanceId] - : [], - ), - ); + ? [props.activeInstanceId] + : [], + ); + }); const serverKeybindings = useAtomValue(primaryServerKeybindingsAtom); const keybindings = providedKeybindings ?? serverKeybindings; const updateSettings = useUpdateClientSettings(); From 723ccff038a4c735ac3b799be813dc4f2de057c4 Mon Sep 17 00:00:00 2001 From: TonybynMp4 <97451137+TonybynMp4@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:58:02 +0200 Subject: [PATCH 3/3] fix(web): optimize lookup --- .../components/chat/ModelPickerContent.tsx | 21 +++++++------- apps/web/src/modelOrdering.test.ts | 29 +++++++++++++++++++ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index 7b563bb43aea..3cdb4d26df44 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -242,12 +242,20 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { return favorites.length > 0 ? "favorites" : props.activeInstanceId; }, ); + // Create a Set for efficient lookup. Favorites are keyed by + // `${instanceId}:${slug}`; the storage schema widened from ProviderDriverKind + // to ProviderInstanceId so pre-migration favorites keyed by driver slugs + // (e.g. `"codex:gpt-5"`) still resolve — the default instance id equals + // the driver slug. + const favoritesSet = useMemo(() => { + return new Set(favorites.map((fav) => providerModelKey(fav.provider, fav.model))); + }, [favorites]); const [expandedLegacyInstances, setExpandedLegacyInstances] = useState(() => { // Auto-expand the legacy group only when the active model actually lives // there. A favorited legacy model is hoisted into the main list, so // expanding the group for it would open an unrelated section. - const activeIsFavorite = favorites.some( - (fav) => fav.provider === props.activeInstanceId && fav.model === activeModelSlug, + const activeIsFavorite = favoritesSet.has( + providerModelKey(props.activeInstanceId, activeModelSlug), ); return new Set( !activeIsFavorite && @@ -290,15 +298,6 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { }; }, [focusSearchInput]); - // Create a Set for efficient lookup. Favorites are keyed by - // `${instanceId}:${slug}`; the storage schema widened from ProviderDriverKind - // to ProviderInstanceId so pre-migration favorites keyed by driver slugs - // (e.g. `"codex:gpt-5"`) still resolve — the default instance id equals - // the driver slug. - const favoritesSet = useMemo(() => { - return new Set(favorites.map((fav) => providerModelKey(fav.provider, fav.model))); - }, [favorites]); - /** * Lookup table keyed by `instanceId`. Used for display name + driver * kind enrichment and for `ready`/enabled filtering before flattening diff --git a/apps/web/src/modelOrdering.test.ts b/apps/web/src/modelOrdering.test.ts index 7b7b3af8eb56..4d15b3b9fb64 100644 --- a/apps/web/src/modelOrdering.test.ts +++ b/apps/web/src/modelOrdering.test.ts @@ -75,5 +75,34 @@ describe("model ordering", () => { expect(current.map((model) => model.slug)).toEqual(["opus-5", "sonnet-5"]); expect(legacy.map((model) => model.slug)).toEqual(["opus-4-8", "haiku-3"]); }); + + it("returns empty partitions for an empty list", () => { + const { current, legacy } = partitionLegacyModels([], () => false); + + expect(current).toEqual([]); + expect(legacy).toEqual([]); + }); + + it("treats an explicit isLegacy: false as a current model", () => { + const { current, legacy } = partitionLegacyModels( + [{ slug: "opus-5", isLegacy: false }], + () => false, + ); + + expect(current.map((model) => model.slug)).toEqual(["opus-5"]); + expect(legacy).toEqual([]); + }); + + it("leaves the legacy group empty when every legacy model is favorited", () => { + const { current, legacy } = partitionLegacyModels(models, (model) => model.isLegacy === true); + + expect(current.map((model) => model.slug)).toEqual([ + "opus-5", + "sonnet-5", + "opus-4-8", + "haiku-3", + ]); + expect(legacy).toEqual([]); + }); }); });