| author | |
| committer | |
| log | 9de85ea16e5228ae68ceb3b52b03ab66f8f1b17c |
| tree | f216e130564597ecc9115e8b3c3089a7cb7bac61 |
| parent | 10dde452f90fc3d4d1aea0add67d13cdfb2567ab |
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 @@ | ... | @@ -1,6 +1,11 @@ |
| 1 | import { describe, expect, it } from "vitest"; | 1 | import { describe, expect, it } from "vitest"; |
| 2 | 2 | ||
| 3 | import { getAppModelOptions, getSlashModelOptions, normalizeCustomModelSlugs } from "./appSettings"; | 3 | import { |
| 4 | getAppModelOptions, | ||
| 5 | getSlashModelOptions, | ||
| 6 | normalizeCustomModelSlugs, | ||
| 7 | resolveAppModelSelection, | ||
| 8 | } from "./appSettings"; | ||
| 4 | 9 | ||
| 5 | describe("normalizeCustomModelSlugs", () => { | 10 | describe("normalizeCustomModelSlugs", () => { |
| 6 | it("normalizes aliases, removes built-ins, and deduplicates values", () => { | 11 | it("normalizes aliases, removes built-ins, and deduplicates values", () => { |
| ... | @@ -42,6 +47,18 @@ describe("getAppModelOptions", () => { | ... | @@ -42,6 +47,18 @@ describe("getAppModelOptions", () => { |
| 42 | }); | 47 | }); |
| 43 | }); | 48 | }); |
| 44 | 49 | ||
| 50 | describe("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 | |||
| 45 | describe("getSlashModelOptions", () => { | 62 | describe("getSlashModelOptions", () => { |
| 46 | it("includes saved custom model slugs for /model command suggestions", () => { | 63 | it("includes saved custom model slugs for /model command suggestions", () => { |
| 47 | const options = getSlashModelOptions( | 64 | const options = getSlashModelOptions( |
apps/web/src/appSettings.ts+33-1| ... | @@ -1,7 +1,7 @@ | ... | @@ -1,7 +1,7 @@ |
| 1 | import { useCallback, useSyncExternalStore } from "react"; | 1 | import { useCallback, useSyncExternalStore } from "react"; |
| 2 | import { Option, Schema } from "effect"; | 2 | import { Option, Schema } from "effect"; |
| 3 | import { type ProviderKind } from "@t3tools/contracts"; | 3 | import { type ProviderKind } from "@t3tools/contracts"; |
| 4 | import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; | 4 | import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; |
| 5 | 5 | ||
| 6 | const APP_SETTINGS_STORAGE_KEY = "t3code:app-settings:v1"; | 6 | const APP_SETTINGS_STORAGE_KEY = "t3code:app-settings:v1"; |
| 7 | const MAX_CUSTOM_MODEL_COUNT = 32; | 7 | const MAX_CUSTOM_MODEL_COUNT = 32; |
| ... | @@ -111,6 +111,38 @@ export function getAppModelOptions( | ... | @@ -111,6 +111,38 @@ export function getAppModelOptions( |
| 111 | return options; | 111 | return options; |
| 112 | } | 112 | } |
| 113 | 113 | ||
| 114 | export 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 | |||
| 114 | export function getSlashModelOptions( | 146 | export function getSlashModelOptions( |
| 115 | provider: ProviderKind, | 147 | provider: ProviderKind, |
| 116 | customModels: readonly string[], | 148 | customModels: readonly string[], |
apps/web/src/components/ChatView.tsx+13-22| ... | @@ -196,7 +196,7 @@ import { Toggle } from "./ui/toggle"; | ... | @@ -196,7 +196,7 @@ import { Toggle } from "./ui/toggle"; |
| 196 | import { SidebarTrigger } from "./ui/sidebar"; | 196 | import { SidebarTrigger } from "./ui/sidebar"; |
| 197 | import { newCommandId, newMessageId, newThreadId } from "~/lib/utils"; | 197 | import { newCommandId, newMessageId, newThreadId } from "~/lib/utils"; |
| 198 | import { readNativeApi } from "~/nativeApi"; | 198 | import { readNativeApi } from "~/nativeApi"; |
| 199 | import { getAppModelOptions, useAppSettings } from "../appSettings"; | 199 | import { getAppModelOptions, resolveAppModelSelection, useAppSettings } from "../appSettings"; |
| 200 | import { | 200 | import { |
| 201 | type ComposerImageAttachment, | 201 | type ComposerImageAttachment, |
| 202 | type DraftThreadEnvMode, | 202 | type DraftThreadEnvMode, |
| ... | @@ -762,30 +762,18 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -762,30 +762,18 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 762 | selectedProvider, | 762 | selectedProvider, |
| 763 | activeThread?.model ?? activeProject?.model ?? getDefaultModel(selectedProvider), | 763 | activeThread?.model ?? activeProject?.model ?? getDefaultModel(selectedProvider), |
| 764 | ); | 764 | ); |
| 765 | const customModelsForSelectedProvider = settings.customCodexModels; | ||
| 765 | const selectedModel = useMemo(() => { | 766 | const selectedModel = useMemo(() => { |
| 766 | const draftModel = composerDraft.model; | 767 | const draftModel = composerDraft.model; |
| 767 | if (!draftModel) { | 768 | if (!draftModel) { |
| 768 | return baseThreadModel; | 769 | return baseThreadModel; |
| 769 | } | 770 | } |
| 770 | 771 | return resolveAppModelSelection( | |
| 771 | const providerOptions = getCustomModelOptionsByProvider(settings)[selectedProvider]; | 772 | selectedProvider, |
| 772 | const directMatch = providerOptions.find((option) => option.slug === draftModel); | 773 | customModelsForSelectedProvider, |
| 773 | if (directMatch) { | 774 | draftModel, |
| 774 | return directMatch.slug as ModelSlug; | 775 | ) as ModelSlug; |
| 775 | } | 776 | }, [baseThreadModel, composerDraft.model, customModelsForSelectedProvider, selectedProvider]); |
| 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]); | ||
| 789 | const reasoningOptions = getReasoningEffortOptions(selectedProvider); | 777 | const reasoningOptions = getReasoningEffortOptions(selectedProvider); |
| 790 | const supportsReasoningEffort = reasoningOptions.length > 0; | 778 | const supportsReasoningEffort = reasoningOptions.length > 0; |
| 791 | const selectedEffort = composerDraft.effort ?? getDefaultReasoningEffort(selectedProvider); | 779 | const selectedEffort = composerDraft.effort ?? getDefaultReasoningEffort(selectedProvider); |
| ... | @@ -2979,9 +2967,11 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -2979,9 +2967,11 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2979 | scheduleComposerFocus(); | 2967 | scheduleComposerFocus(); |
| 2980 | return; | 2968 | return; |
| 2981 | } | 2969 | } |
| 2982 | const resolvedModel = resolveModelSlugForProvider(provider, model); | ||
| 2983 | setComposerDraftProvider(activeThread.id, provider); | 2970 | setComposerDraftProvider(activeThread.id, provider); |
| 2984 | setComposerDraftModel(activeThread.id, resolvedModel); | 2971 | setComposerDraftModel( |
| 2972 | activeThread.id, | ||
| 2973 | resolveAppModelSelection(provider, settings.customCodexModels, model), | ||
| 2974 | ); | ||
| 2985 | scheduleComposerFocus(); | 2975 | scheduleComposerFocus(); |
| 2986 | }, | 2976 | }, |
| 2987 | [ | 2977 | [ |
| ... | @@ -2990,6 +2980,7 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -2990,6 +2980,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2990 | scheduleComposerFocus, | 2980 | scheduleComposerFocus, |
| 2991 | setComposerDraftModel, | 2981 | setComposerDraftModel, |
| 2992 | setComposerDraftProvider, | 2982 | setComposerDraftProvider, |
| 2983 | settings.customCodexModels, | ||
| 2993 | ], | 2984 | ], |
| 2994 | ); | 2985 | ); |
| 2995 | const onEffortSelect = useCallback( | 2986 | const onEffortSelect = useCallback( |