diff --git a/apps/web/src/appSettings.ts b/apps/web/src/appSettings.ts index e4f4d8b1ca..49182aeb26 100644 --- a/apps/web/src/appSettings.ts +++ b/apps/web/src/appSettings.ts @@ -1,6 +1,6 @@ import { useCallback } from "react"; import { Option, Schema } from "effect"; -import { TrimmedNonEmptyString, type ProviderKind } from "@t3tools/contracts"; +import { ProviderKind, TrimmedNonEmptyString } from "@t3tools/contracts"; import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; import { useLocalStorage } from "./hooks/useLocalStorage"; import { EnvMode } from "./components/BranchToolbar.logic"; @@ -41,6 +41,8 @@ export const AppSettingsSchema = Schema.Struct({ customCodexModels: Schema.Array(Schema.String).pipe(withDefaults(() => [])), customClaudeModels: Schema.Array(Schema.String).pipe(withDefaults(() => [])), textGenerationModel: Schema.optional(TrimmedNonEmptyString), + defaultProvider: Schema.optional(ProviderKind), + defaultModel: Schema.optional(TrimmedNonEmptyString), }); export type AppSettings = typeof AppSettingsSchema.Type; export interface AppModelOption { diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index eaead424fb..26f06c93a0 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -462,11 +462,11 @@ export default function ChatView({ threadId }: ChatViewProps) { ? buildLocalDraftThread( threadId, draftThread, - fallbackDraftProject?.model ?? DEFAULT_MODEL_BY_PROVIDER.codex, + fallbackDraftProject?.model ?? settings.defaultModel ?? DEFAULT_MODEL_BY_PROVIDER.codex, localDraftError, ) : undefined, - [draftThread, fallbackDraftProject?.model, localDraftError, threadId], + [draftThread, fallbackDraftProject?.model, settings.defaultModel, localDraftError, threadId], ); const activeThread = serverThread ?? localDraftThread; const runtimeMode = @@ -591,10 +591,14 @@ export default function ChatView({ threadId }: ChatViewProps) { const lockedProvider: ProviderKind | null = hasThreadStarted ? (sessionProvider ?? selectedProviderByThreadId ?? null) : null; - const selectedProvider: ProviderKind = lockedProvider ?? selectedProviderByThreadId ?? "codex"; + const selectedProvider: ProviderKind = + lockedProvider ?? selectedProviderByThreadId ?? settings.defaultProvider ?? "codex"; const baseThreadModel = resolveModelSlugForProvider( selectedProvider, - activeThread?.model ?? activeProject?.model ?? getDefaultModel(selectedProvider), + activeThread?.model ?? + activeProject?.model ?? + settings.defaultModel ?? + getDefaultModel(selectedProvider), ); const customModelsByProvider = useMemo( () => ({ diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index bcef110c1b..afa1d3d619 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -433,7 +433,7 @@ export default function Sidebar() { projectId, title, workspaceRoot: cwd, - defaultModel: DEFAULT_MODEL_BY_PROVIDER.codex, + defaultModel: appSettings.defaultModel ?? DEFAULT_MODEL_BY_PROVIDER.codex, createdAt, }); await handleNewThread(projectId, { @@ -462,6 +462,7 @@ export default function Sidebar() { isAddingProject, projects, shouldBrowseForProjectImmediately, + appSettings.defaultModel, appSettings.defaultThreadEnvMode, ], ); diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index f3dee29096..6b91266dd6 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -2,8 +2,9 @@ import { createFileRoute } from "@tanstack/react-router"; import { useQuery } from "@tanstack/react-query"; import { useCallback, useState } from "react"; import { type ProviderKind, DEFAULT_GIT_TEXT_GENERATION_MODEL } from "@t3tools/contracts"; -import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; +import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; import { getAppModelOptions, MAX_CUSTOM_MODEL_LENGTH, useAppSettings } from "../appSettings"; +import { PROVIDER_OPTIONS } from "../session-logic"; import { resolveAndPersistPreferredEditor } from "../editorPreferences"; import { isElectron } from "../env"; import { useTheme } from "../hooks/useTheme"; @@ -126,6 +127,19 @@ function SettingsRouteView() { const keybindingsConfigPath = serverConfigQuery.data?.keybindingsConfigPath ?? null; const availableEditors = serverConfigQuery.data?.availableEditors; + const effectiveDefaultProvider: ProviderKind = settings.defaultProvider ?? "codex"; + const defaultModelOptionsForProvider = getAppModelOptions( + effectiveDefaultProvider, + settings.customCodexModels, + settings.defaultModel, + ); + const effectiveDefaultModel = settings.defaultModel ?? getDefaultModel(effectiveDefaultProvider); + const selectedDefaultModelLabel = + defaultModelOptionsForProvider.find((option) => option.slug === effectiveDefaultModel)?.name ?? + effectiveDefaultModel; + + const availableProviderOptions = PROVIDER_OPTIONS.filter((option) => option.available); + const gitTextGenerationModelOptions = getAppModelOptions( "codex", settings.customCodexModels, @@ -394,6 +408,108 @@ function SettingsRouteView() { +
+
+

Defaults

+

+ Set the default provider and model for new threads. These are used when no + thread-specific or project-level override is set. +

+
+ +
+ {availableProviderOptions.length > 1 ? ( +
+
+

Default provider

+

+ Provider used for new threads when none is specified. +

+
+ +
+ ) : null} + +
+
+

Default model

+

+ Model used for new threads when no project or thread override is set. +

+
+ +
+ + {(settings.defaultProvider !== defaults.defaultProvider || + settings.defaultModel !== defaults.defaultModel) ? ( +
+ +
+ ) : null} +
+
+

Models