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 { getAppModelOptions, MAX_CUSTOM_MODEL_LENGTH, useAppSettings } from "../appSettings"; import { resolveAndPersistPreferredEditor } from "../editorPreferences"; import { isElectron } from "../env"; import { useTheme } from "../hooks/useTheme"; import { serverConfigQueryOptions } from "../lib/serverReactQuery"; import { ensureNativeApi } from "../nativeApi"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue, } from "../components/ui/select"; import { Switch } from "../components/ui/switch"; import { APP_VERSION } from "../branding"; import { SidebarInset } from "~/components/ui/sidebar"; const THEME_OPTIONS = [ { value: "system", label: "System", description: "Match your OS appearance setting.", }, { value: "light", label: "Light", description: "Always use the light theme.", }, { value: "dark", label: "Dark", description: "Always use the dark theme.", }, ] as const; const MODEL_PROVIDER_SETTINGS: Array<{ provider: ProviderKind; title: string; description: string; placeholder: string; example: string; }> = [ { provider: "codex", title: "Codex", description: "Save additional Codex model slugs for the picker and `/model` command.", placeholder: "your-codex-model-slug", example: "gpt-6.7-codex-ultra-preview", }, { provider: "claudeAgent", title: "Claude", description: "Save additional Claude model slugs for the picker and `/model` command.", placeholder: "your-claude-model-slug", example: "claude-sonnet-5-0", }, ] as const; const TIMESTAMP_FORMAT_LABELS = { locale: "System default", "12-hour": "12-hour", "24-hour": "24-hour", } as const; function getCustomModelsForProvider( settings: ReturnType["settings"], provider: ProviderKind, ) { switch (provider) { case "claudeAgent": return settings.customClaudeModels; case "codex": default: return settings.customCodexModels; } } function getDefaultCustomModelsForProvider( defaults: ReturnType["defaults"], provider: ProviderKind, ) { switch (provider) { case "claudeAgent": return defaults.customClaudeModels; case "codex": default: return defaults.customCodexModels; } } function patchCustomModels(provider: ProviderKind, models: string[]) { switch (provider) { case "claudeAgent": return { customClaudeModels: models }; case "codex": default: return { customCodexModels: models }; } } function SettingsRouteView() { const { theme, setTheme, resolvedTheme } = useTheme(); const { settings, defaults, updateSettings } = useAppSettings(); const serverConfigQuery = useQuery(serverConfigQueryOptions()); const [isOpeningKeybindings, setIsOpeningKeybindings] = useState(false); const [openKeybindingsError, setOpenKeybindingsError] = useState(null); const [customModelInputByProvider, setCustomModelInputByProvider] = useState< Record >({ codex: "", claudeAgent: "", }); const [customModelErrorByProvider, setCustomModelErrorByProvider] = useState< Partial> >({}); const codexBinaryPath = settings.codexBinaryPath; const codexHomePath = settings.codexHomePath; const keybindingsConfigPath = serverConfigQuery.data?.keybindingsConfigPath ?? null; const availableEditors = serverConfigQuery.data?.availableEditors; const gitTextGenerationModelOptions = getAppModelOptions( "codex", settings.customCodexModels, settings.textGenerationModel, ); const selectedGitTextGenerationModelLabel = gitTextGenerationModelOptions.find( (option) => option.slug === (settings.textGenerationModel ?? DEFAULT_GIT_TEXT_GENERATION_MODEL), )?.name ?? settings.textGenerationModel; const openKeybindingsFile = useCallback(() => { if (!keybindingsConfigPath) return; setOpenKeybindingsError(null); setIsOpeningKeybindings(true); const api = ensureNativeApi(); const editor = resolveAndPersistPreferredEditor(availableEditors ?? []); if (!editor) { setOpenKeybindingsError("No available editors found."); setIsOpeningKeybindings(false); return; } void api.shell .openInEditor(keybindingsConfigPath, editor) .catch((error) => { setOpenKeybindingsError( error instanceof Error ? error.message : "Unable to open keybindings file.", ); }) .finally(() => { setIsOpeningKeybindings(false); }); }, [availableEditors, keybindingsConfigPath]); const addCustomModel = useCallback( (provider: ProviderKind) => { const customModelInput = customModelInputByProvider[provider]; const customModels = getCustomModelsForProvider(settings, provider); const normalized = normalizeModelSlug(customModelInput, provider); if (!normalized) { setCustomModelErrorByProvider((existing) => ({ ...existing, [provider]: "Enter a model slug.", })); return; } if (getModelOptions(provider).some((option) => option.slug === normalized)) { setCustomModelErrorByProvider((existing) => ({ ...existing, [provider]: "That model is already built in.", })); return; } if (normalized.length > MAX_CUSTOM_MODEL_LENGTH) { setCustomModelErrorByProvider((existing) => ({ ...existing, [provider]: `Model slugs must be ${MAX_CUSTOM_MODEL_LENGTH} characters or less.`, })); return; } if (customModels.includes(normalized)) { setCustomModelErrorByProvider((existing) => ({ ...existing, [provider]: "That custom model is already saved.", })); return; } updateSettings(patchCustomModels(provider, [...customModels, normalized])); setCustomModelInputByProvider((existing) => ({ ...existing, [provider]: "", })); setCustomModelErrorByProvider((existing) => ({ ...existing, [provider]: null, })); }, [customModelInputByProvider, settings, updateSettings], ); const removeCustomModel = useCallback( (provider: ProviderKind, slug: string) => { const customModels = getCustomModelsForProvider(settings, provider); updateSettings( patchCustomModels( provider, customModels.filter((model) => model !== slug), ), ); setCustomModelErrorByProvider((existing) => ({ ...existing, [provider]: null, })); }, [settings, updateSettings], ); return (
{isElectron && (
Settings
)}

Settings

Configure app-level preferences for this device.

Appearance

Choose how T3 Code looks across the app.

{THEME_OPTIONS.map((option) => { const selected = theme === option.value; return ( ); })}

Active theme: {resolvedTheme}

Timestamp format

System default follows your browser or OS time format. 12-hour{" "} and 24-hour force the hour cycle.

{settings.timestampFormat !== defaults.timestampFormat ? (
) : null}

Codex App Server

These overrides apply to new sessions and let you use a non-default Codex install.

Binary source

{codexBinaryPath || "PATH"}

Models

Save additional provider model slugs so they appear in the chat model picker and `/model` command suggestions.

{MODEL_PROVIDER_SETTINGS.map((providerSettings) => { const provider = providerSettings.provider; const customModels = getCustomModelsForProvider(settings, provider); const customModelInput = customModelInputByProvider[provider]; const customModelError = customModelErrorByProvider[provider] ?? null; return (

{providerSettings.title}

{providerSettings.description}

{customModelError ? (

{customModelError}

) : null}

Saved custom models: {customModels.length}

{customModels.length > 0 ? ( ) : null}
{customModels.length > 0 ? (
{customModels.map((slug) => (
{slug}
))}
) : (
No custom models saved yet.
)}
); })}

Git

Configure the model used for generating commit messages, PR titles, and branch names.

Text generation model

Model used for auto-generated git content.

{settings.textGenerationModel !== defaults.textGenerationModel ? (
) : null}

Threads

Choose the default workspace mode for newly created draft threads.

Default to New worktree

New threads start in New worktree mode instead of Local.

updateSettings({ defaultThreadEnvMode: checked ? "worktree" : "local", }) } aria-label="Default new threads to New worktree mode" />
{settings.defaultThreadEnvMode !== defaults.defaultThreadEnvMode ? (
) : null}

Responses

Control how assistant output is rendered during a turn.

Stream assistant messages

Show token-by-token output while a response is in progress.

updateSettings({ enableAssistantStreaming: Boolean(checked), }) } aria-label="Stream assistant messages" />
{settings.enableAssistantStreaming !== defaults.enableAssistantStreaming ? (
) : null}

Keybindings

Open the persisted keybindings.json file to edit advanced bindings directly.

Config file path

{keybindingsConfigPath ?? "Resolving keybindings path..."}

Opens in your preferred editor selection.

{openKeybindingsError ? (

{openKeybindingsError}

) : null}

Safety

Additional guardrails for destructive local actions.

Confirm thread deletion

Ask for confirmation before deleting a thread and its chat history.

updateSettings({ confirmThreadDelete: Boolean(checked), }) } aria-label="Confirm thread deletion" />
{settings.confirmThreadDelete !== defaults.confirmThreadDelete ? (
) : null}

About

Application version and environment information.

Version

Current version of the application.

{APP_VERSION}
); } export const Route = createFileRoute("/_chat/settings")({ component: SettingsRouteView, });