authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-05 22:29:31-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
log9de85ea16e5228ae68ceb3b52b03ab66f8f1b17c
treef216e130564597ecc9115e8b3c3089a7cb7bac61
parent10dde452f90fc3d4d1aea0add67d13cdfb2567ab

fix: preserve custom model picker selections

Co-authored-by: codex <codex@users.noreply.github.com>

3 files changed, 64 insertions(+), 24 deletions(-)

apps/web/src/appSettings.test.ts+18-1
......@@ -1,6 +1,11 @@
11import { describe, expect, it } from "vitest";
22
3import { getAppModelOptions, getSlashModelOptions, normalizeCustomModelSlugs } from "./appSettings";
3import {
4 getAppModelOptions,
5 getSlashModelOptions,
6 normalizeCustomModelSlugs,
7 resolveAppModelSelection,
8} from "./appSettings";
49
510describe("normalizeCustomModelSlugs", () => {
611 it("normalizes aliases, removes built-ins, and deduplicates values", () => {
......@@ -42,6 +47,18 @@ describe("getAppModelOptions", () => {
4247 });
4348});
4449
50describe("resolveAppModelSelection", () => {
51 it("preserves saved custom model slugs instead of falling back to the default", () => {
52 expect(resolveAppModelSelection("codex", ["galapagos-alpha"], "galapagos-alpha")).toBe(
53 "galapagos-alpha",
54 );
55 });
56
57 it("falls back to the provider default when no model is selected", () => {
58 expect(resolveAppModelSelection("codex", [], "")).toBe("gpt-5.4");
59 });
60});
61
4562describe("getSlashModelOptions", () => {
4663 it("includes saved custom model slugs for /model command suggestions", () => {
4764 const options = getSlashModelOptions(
apps/web/src/appSettings.ts+33-1
......@@ -1,7 +1,7 @@
11import { useCallback, useSyncExternalStore } from "react";
22import { Option, Schema } from "effect";
33import { type ProviderKind } from "@t3tools/contracts";
4import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";
4import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";
55
66const APP_SETTINGS_STORAGE_KEY = "t3code:app-settings:v1";
77const MAX_CUSTOM_MODEL_COUNT = 32;
......@@ -111,6 +111,38 @@ export function getAppModelOptions(
111111 return options;
112112}
113113
114export function resolveAppModelSelection(
115 provider: ProviderKind,
116 customModels: readonly string[],
117 selectedModel: string | null | undefined,
118): string {
119 const options = getAppModelOptions(provider, customModels, selectedModel);
120 const trimmedSelectedModel = selectedModel?.trim();
121 if (trimmedSelectedModel) {
122 const direct = options.find((option) => option.slug === trimmedSelectedModel);
123 if (direct) {
124 return direct.slug;
125 }
126
127 const byName = options.find(
128 (option) => option.name.toLowerCase() === trimmedSelectedModel.toLowerCase(),
129 );
130 if (byName) {
131 return byName.slug;
132 }
133 }
134
135 const normalizedSelectedModel = normalizeModelSlug(selectedModel, provider);
136 if (!normalizedSelectedModel) {
137 return getDefaultModel(provider);
138 }
139
140 return (
141 options.find((option) => option.slug === normalizedSelectedModel)?.slug ??
142 getDefaultModel(provider)
143 );
144}
145
114146export function getSlashModelOptions(
115147 provider: ProviderKind,
116148 customModels: readonly string[],
apps/web/src/components/ChatView.tsx+13-22
......@@ -196,7 +196,7 @@ import { Toggle } from "./ui/toggle";
196196import { SidebarTrigger } from "./ui/sidebar";
197197import { newCommandId, newMessageId, newThreadId } from "~/lib/utils";
198198import { readNativeApi } from "~/nativeApi";
199import { getAppModelOptions, useAppSettings } from "../appSettings";
199import { getAppModelOptions, resolveAppModelSelection, useAppSettings } from "../appSettings";
200200import {
201201 type ComposerImageAttachment,
202202 type DraftThreadEnvMode,
......@@ -762,30 +762,18 @@ export default function ChatView({ threadId }: ChatViewProps) {
762762 selectedProvider,
763763 activeThread?.model ?? activeProject?.model ?? getDefaultModel(selectedProvider),
764764 );
765 const customModelsForSelectedProvider = settings.customCodexModels;
765766 const selectedModel = useMemo(() => {
766767 const draftModel = composerDraft.model;
767768 if (!draftModel) {
768769 return baseThreadModel;
769770 }
770
771 const providerOptions = getCustomModelOptionsByProvider(settings)[selectedProvider];
772 const directMatch = providerOptions.find((option) => option.slug === draftModel);
773 if (directMatch) {
774 return directMatch.slug as ModelSlug;
775 }
776
777 const normalizedDraftModel = normalizeModelSlug(draftModel, selectedProvider);
778 if (normalizedDraftModel) {
779 const normalizedMatch = providerOptions.find(
780 (option) => option.slug === normalizedDraftModel,
781 );
782 if (normalizedMatch) {
783 return normalizedMatch.slug as ModelSlug;
784 }
785 }
786
787 return resolveModelSlugForProvider(selectedProvider, draftModel);
788 }, [baseThreadModel, composerDraft.model, selectedProvider, settings]);
771 return resolveAppModelSelection(
772 selectedProvider,
773 customModelsForSelectedProvider,
774 draftModel,
775 ) as ModelSlug;
776 }, [baseThreadModel, composerDraft.model, customModelsForSelectedProvider, selectedProvider]);
789777 const reasoningOptions = getReasoningEffortOptions(selectedProvider);
790778 const supportsReasoningEffort = reasoningOptions.length > 0;
791779 const selectedEffort = composerDraft.effort ?? getDefaultReasoningEffort(selectedProvider);
......@@ -2979,9 +2967,11 @@ export default function ChatView({ threadId }: ChatViewProps) {
29792967 scheduleComposerFocus();
29802968 return;
29812969 }
2982 const resolvedModel = resolveModelSlugForProvider(provider, model);
29832970 setComposerDraftProvider(activeThread.id, provider);
2984 setComposerDraftModel(activeThread.id, resolvedModel);
2971 setComposerDraftModel(
2972 activeThread.id,
2973 resolveAppModelSelection(provider, settings.customCodexModels, model),
2974 );
29852975 scheduleComposerFocus();
29862976 },
29872977 [
......@@ -2990,6 +2980,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
29902980 scheduleComposerFocus,
29912981 setComposerDraftModel,
29922982 setComposerDraftProvider,
2983 settings.customCodexModels,
29932984 ],
29942985 );
29952986 const onEffortSelect = useCallback(