Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/web/src/appSettings.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 {
Expand Down
12 changes: 8 additions & 4 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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(
() => ({
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -462,6 +462,7 @@ export default function Sidebar() {
isAddingProject,
projects,
shouldBrowseForProjectImmediately,
appSettings.defaultModel,
appSettings.defaultThreadEnvMode,
],
);
Expand Down
118 changes: 117 additions & 1 deletion apps/web/src/routes/_chat.settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -394,6 +408,108 @@ function SettingsRouteView() {
</div>
</section>

<section className="rounded-2xl border border-border bg-card p-5">
<div className="mb-4">
<h2 className="text-sm font-medium text-foreground">Defaults</h2>
<p className="mt-1 text-xs text-muted-foreground">
Set the default provider and model for new threads. These are used when no
thread-specific or project-level override is set.
</p>
</div>

<div className="space-y-4">
{availableProviderOptions.length > 1 ? (
<div className="flex flex-col gap-4 rounded-lg border border-border bg-background px-3 py-3 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-foreground">Default provider</p>
<p className="text-xs text-muted-foreground">
Provider used for new threads when none is specified.
</p>
</div>
<Select
value={effectiveDefaultProvider}
onValueChange={(value) => {
const provider = value as ProviderKind;
updateSettings({
defaultProvider: provider,
// Reset default model when provider changes so it falls back to
// the new provider's built-in default.
defaultModel: undefined,
});
}}
>
<SelectTrigger
className="w-full shrink-0 sm:w-48"
aria-label="Default provider"
>
<SelectValue>
{availableProviderOptions.find(
(option) => option.value === effectiveDefaultProvider,
)?.label ?? effectiveDefaultProvider}
</SelectValue>
</SelectTrigger>
<SelectPopup align="end">
{availableProviderOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectPopup>
</Select>
</div>
) : null}

<div className="flex flex-col gap-4 rounded-lg border border-border bg-background px-3 py-3 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-foreground">Default model</p>
<p className="text-xs text-muted-foreground">
Model used for new threads when no project or thread override is set.
</p>
</div>
<Select
value={effectiveDefaultModel}
onValueChange={(value) => {
if (value) {
updateSettings({ defaultModel: value });
}
}}
>
<SelectTrigger
className="w-full shrink-0 sm:w-48"
aria-label="Default model"
>
<SelectValue>{selectedDefaultModelLabel}</SelectValue>
</SelectTrigger>
<SelectPopup align="end">
{defaultModelOptionsForProvider.map((option) => (
<SelectItem key={option.slug} value={option.slug}>
{option.name}
</SelectItem>
))}
</SelectPopup>
</Select>
</div>

{(settings.defaultProvider !== defaults.defaultProvider ||
settings.defaultModel !== defaults.defaultModel) ? (
<div className="flex justify-end">
<Button
size="xs"
variant="outline"
onClick={() =>
updateSettings({
defaultProvider: defaults.defaultProvider,
defaultModel: defaults.defaultModel,
})
}
>
Restore defaults
</Button>
</div>
) : null}
</div>
</section>

<section className="rounded-2xl border border-border bg-card p-5">
<div className="mb-4">
<h2 className="text-sm font-medium text-foreground">Models</h2>
Expand Down
Loading