Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion apps/web/src/components/Sidebar.tsx
Comment thread
binbandit marked this conversation as resolved.
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
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