1044 lines · 42.1 KB · TypeScript
RawDownload
import { createFileRoute } from "@tanstack/react-router";import { useQuery } from "@tanstack/react-query";import { ChevronDownIcon, PlusIcon, RotateCcwIcon, Undo2Icon, XIcon } from "lucide-react";import { type ReactNode, useCallback, useState } from "react";import { type ProviderKind } from "@t3tools/contracts";import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";import { useAppSettings } from "../appSettings";import {  getCustomModelOptionsByProvider,  MAX_CUSTOM_MODEL_LENGTH,  MODEL_PROVIDER_SETTINGS,  resolveAppModelSelectionState,} from "../modelSelection";import { APP_VERSION } from "../branding";import { Button } from "../components/ui/button";import { Collapsible, CollapsibleContent } from "../components/ui/collapsible";import { Input } from "../components/ui/input";import {  Select,  SelectItem,  SelectPopup,  SelectTrigger,  SelectValue,} from "../components/ui/select";import { SidebarTrigger } from "../components/ui/sidebar";import { Switch } from "../components/ui/switch";import { ProviderModelPicker } from "../components/chat/ProviderModelPicker";import { TraitsPicker } from "../components/chat/TraitsPicker";import { SidebarInset } from "../components/ui/sidebar";import { Tooltip, TooltipPopup, TooltipTrigger } from "../components/ui/tooltip";import { resolveAndPersistPreferredEditor } from "../editorPreferences";import { isElectron } from "../env";import { useTheme } from "../hooks/useTheme";import { serverConfigQueryOptions } from "../lib/serverReactQuery";import { cn } from "../lib/utils";import { ensureNativeApi, readNativeApi } from "../nativeApi"; 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 TIMESTAMP_FORMAT_LABELS = {  locale: "System default",  "12-hour": "12-hour",  "24-hour": "24-hour",} as const; type InstallProviderSettings = {  provider: ProviderKind;  title: string;  binaryPlaceholder: string;  binaryDescription: ReactNode;  homePathKey?: "codexHomePath";  homePlaceholder?: string;  homeDescription?: ReactNode;}; const INSTALL_PROVIDER_SETTINGS: readonly InstallProviderSettings[] = [  {    provider: "codex",    title: "Codex",    binaryPlaceholder: "Codex binary path",    binaryDescription: (      <>        Leave blank to use <code>codex</code> from your PATH.      </>    ),    homePathKey: "codexHomePath",    homePlaceholder: "CODEX_HOME",    homeDescription: "Optional custom Codex home and config directory.",  },  {    provider: "claudeAgent",    title: "Claude",    binaryPlaceholder: "Claude binary path",    binaryDescription: (      <>        Leave blank to use <code>claude</code> from your PATH.      </>    ),  },]; function SettingsSection({ title, children }: { title: string; children: ReactNode }) {  return (    <section className="space-y-3">      <h2 className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">        {title}      </h2>      <div className="relative overflow-hidden rounded-2xl border bg-card not-dark:bg-clip-padding text-card-foreground shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]">        {children}      </div>    </section>  );} function SettingsRow({  title,  description,  status,  resetAction,  control,  children,  onClick,}: {  title: string;  description: string;  status?: ReactNode;  resetAction?: ReactNode;  control?: ReactNode;  children?: ReactNode;  onClick?: () => void;}) {  return (    <div      className="border-t border-border px-4 py-4 first:border-t-0 sm:px-5"      data-slot="settings-row"    >      <div        className={cn(          "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",          onClick && "cursor-pointer",        )}        onClick={onClick}      >        <div className="min-w-0 flex-1 space-y-1">          <div className="flex min-h-5 items-center gap-1.5">            <h3 className="text-sm font-medium text-foreground">{title}</h3>            <span className="inline-flex h-5 w-5 shrink-0 items-center justify-center">              {resetAction}            </span>          </div>          <p className="text-xs text-muted-foreground">{description}</p>          {status ? <div className="pt-1 text-[11px] text-muted-foreground">{status}</div> : null}        </div>        {control ? (          <div className="flex w-full shrink-0 items-center gap-2 sm:w-auto sm:justify-end">            {control}          </div>        ) : null}      </div>      {children}    </div>  );} function SettingResetButton({ label, onClick }: { label: string; onClick: () => void }) {  return (    <Tooltip>      <TooltipTrigger        render={          <Button            size="icon-xs"            variant="ghost"            aria-label={`Reset ${label} to default`}            className="size-5 rounded-sm p-0 text-muted-foreground hover:text-foreground"            onClick={(event) => {              event.stopPropagation();              onClick();            }}          >            <Undo2Icon className="size-3" />          </Button>        }      />      <TooltipPopup side="top">Reset to default</TooltipPopup>    </Tooltip>  );} function SettingsRouteView() {  const { theme, setTheme } = useTheme();  const { settings, defaults, updateSettings, resetSettings } = useAppSettings();  const serverConfigQuery = useQuery(serverConfigQueryOptions());  const [isOpeningKeybindings, setIsOpeningKeybindings] = useState(false);  const [openKeybindingsError, setOpenKeybindingsError] = useState<string | null>(null);  const [openInstallProviders, setOpenInstallProviders] = useState<Record<ProviderKind, boolean>>({    codex: Boolean(settings.providers.codex.binaryPath || settings.providers.codex.homePath),    claudeAgent: Boolean(settings.providers.claudeAgent.binaryPath),  });  const [selectedCustomModelProvider, setSelectedCustomModelProvider] =    useState<ProviderKind>("codex");  const [customModelInputByProvider, setCustomModelInputByProvider] = useState<    Record<ProviderKind, string>  >({    codex: "",    claudeAgent: "",  });  const [customModelErrorByProvider, setCustomModelErrorByProvider] = useState<    Partial<Record<ProviderKind, string | null>>  >({});  const [showAllCustomModels, setShowAllCustomModels] = useState(false);   const codexHomePath = settings.providers.codex.homePath;  const keybindingsConfigPath = serverConfigQuery.data?.keybindingsConfigPath ?? null;  const availableEditors = serverConfigQuery.data?.availableEditors;   const textGenerationModelSelection = resolveAppModelSelectionState(settings);  const textGenProvider = textGenerationModelSelection.provider;  const textGenModel = textGenerationModelSelection.model;  const textGenModelOptions = textGenerationModelSelection.options;  const gitModelOptionsByProvider = getCustomModelOptionsByProvider(    settings,    textGenProvider,    textGenModel,  );  const selectedCustomModelProviderSettings = MODEL_PROVIDER_SETTINGS.find(    (providerSettings) => providerSettings.provider === selectedCustomModelProvider,  )!;  const selectedCustomModelInput = customModelInputByProvider[selectedCustomModelProvider];  const selectedCustomModelError = customModelErrorByProvider[selectedCustomModelProvider] ?? null;  const totalCustomModels =    settings.providers.codex.customModels.length +    settings.providers.claudeAgent.customModels.length;  const savedCustomModelRows = MODEL_PROVIDER_SETTINGS.flatMap((providerSettings) =>    settings.providers[providerSettings.provider].customModels.map((slug) => ({      key: `${providerSettings.provider}:${slug}`,      provider: providerSettings.provider,      providerTitle: providerSettings.title,      slug,    })),  );  const visibleCustomModelRows = showAllCustomModels    ? savedCustomModelRows    : savedCustomModelRows.slice(0, 5);  const isInstallSettingsDirty =    settings.providers.claudeAgent.binaryPath !== defaults.providers.claudeAgent.binaryPath ||    settings.providers.codex.binaryPath !== defaults.providers.codex.binaryPath ||    settings.providers.codex.homePath !== defaults.providers.codex.homePath;  const changedSettingLabels = [    ...(theme !== "system" ? ["Theme"] : []),    ...(settings.timestampFormat !== defaults.timestampFormat ? ["Time format"] : []),    ...(settings.diffWordWrap !== defaults.diffWordWrap ? ["Diff line wrapping"] : []),    ...(settings.enableAssistantStreaming !== defaults.enableAssistantStreaming      ? ["Assistant output"]      : []),    ...(settings.defaultThreadEnvMode !== defaults.defaultThreadEnvMode ? ["New thread mode"] : []),    ...(settings.confirmThreadDelete !== defaults.confirmThreadDelete      ? ["Delete confirmation"]      : []),    ...(JSON.stringify(settings.textGenerationModelSelection ?? null) !==    JSON.stringify(defaults.textGenerationModelSelection ?? null)      ? ["Git writing model"]      : []),    ...(settings.providers.codex.customModels.length > 0 ||    settings.providers.claudeAgent.customModels.length > 0      ? ["Custom models"]      : []),    ...(isInstallSettingsDirty ? ["Provider installs"] : []),  ];   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 = settings.providers[provider].customModels;      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({        providers: {          ...settings.providers,          [provider]: {            ...settings.providers[provider],            customModels: [...customModels, normalized],          },        },      });      setCustomModelInputByProvider((existing) => ({        ...existing,        [provider]: "",      }));      setCustomModelErrorByProvider((existing) => ({        ...existing,        [provider]: null,      }));    },    [customModelInputByProvider, settings, updateSettings],  );   const removeCustomModel = useCallback(    (provider: ProviderKind, slug: string) => {      const customModels = settings.providers[provider].customModels;      updateSettings({        providers: {          ...settings.providers,          [provider]: {            ...settings.providers[provider],            customModels: customModels.filter((model) => model !== slug),          },        },      });      setCustomModelErrorByProvider((existing) => ({        ...existing,        [provider]: null,      }));    },    [settings, updateSettings],  );   async function restoreDefaults() {    if (changedSettingLabels.length === 0) return;     const api = readNativeApi();    const confirmed = await (api ?? ensureNativeApi()).dialogs.confirm(      ["Restore default settings?", `This will reset: ${changedSettingLabels.join(", ")}.`].join(        "\n",      ),    );    if (!confirmed) return;     setTheme("system");    resetSettings();    setOpenInstallProviders({      codex: false,      claudeAgent: false,    });    setSelectedCustomModelProvider("codex");    setCustomModelInputByProvider({      codex: "",      claudeAgent: "",    });    setCustomModelErrorByProvider({});  }   return (    <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate">      <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background text-foreground">        {!isElectron && (          <header className="border-b border-border px-3 py-2 sm:px-5">            <div className="flex items-center gap-2">              <SidebarTrigger className="size-7 shrink-0 md:hidden" />              <span className="text-sm font-medium text-foreground">Settings</span>              <div className="ms-auto flex items-center gap-2">                <Button                  size="xs"                  variant="outline"                  disabled={changedSettingLabels.length === 0}                  onClick={() => void restoreDefaults()}                >                  <RotateCcwIcon className="size-3.5" />                  Restore defaults                </Button>              </div>            </div>          </header>        )}         {isElectron && (          <div className="drag-region flex h-[52px] shrink-0 items-center border-b border-border px-5">            <span className="text-xs font-medium tracking-wide text-muted-foreground/70">              Settings            </span>            <div className="ms-auto flex items-center gap-2">              <Button                size="xs"                variant="outline"                disabled={changedSettingLabels.length === 0}                onClick={() => void restoreDefaults()}              >                <RotateCcwIcon className="size-3.5" />                Restore defaults              </Button>            </div>          </div>        )}         <div className="flex-1 overflow-y-auto p-6">          <div className="mx-auto flex w-full max-w-2xl flex-col gap-6">            <SettingsSection title="General">              <SettingsRow                title="Theme"                description="Choose how T3 Code looks across the app."                resetAction={                  theme !== "system" ? (                    <SettingResetButton label="theme" onClick={() => setTheme("system")} />                  ) : null                }                control={                  <Select                    value={theme}                    onValueChange={(value) => {                      if (value !== "system" && value !== "light" && value !== "dark") return;                      setTheme(value);                    }}                  >                    <SelectTrigger className="w-full sm:w-40" aria-label="Theme preference">                      <SelectValue>                        {THEME_OPTIONS.find((option) => option.value === theme)?.label ?? "System"}                      </SelectValue>                    </SelectTrigger>                    <SelectPopup align="end" alignItemWithTrigger={false}>                      {THEME_OPTIONS.map((option) => (                        <SelectItem hideIndicator key={option.value} value={option.value}>                          {option.label}                        </SelectItem>                      ))}                    </SelectPopup>                  </Select>                }              />               <SettingsRow                title="Time format"                description="System default follows your browser or OS clock preference."                resetAction={                  settings.timestampFormat !== defaults.timestampFormat ? (                    <SettingResetButton                      label="time format"                      onClick={() =>                        updateSettings({                          timestampFormat: defaults.timestampFormat,                        })                      }                    />                  ) : null                }                control={                  <Select                    value={settings.timestampFormat}                    onValueChange={(value) => {                      if (value !== "locale" && value !== "12-hour" && value !== "24-hour") {                        return;                      }                      updateSettings({                        timestampFormat: value,                      });                    }}                  >                    <SelectTrigger className="w-full sm:w-40" aria-label="Timestamp format">                      <SelectValue>{TIMESTAMP_FORMAT_LABELS[settings.timestampFormat]}</SelectValue>                    </SelectTrigger>                    <SelectPopup align="end" alignItemWithTrigger={false}>                      <SelectItem hideIndicator value="locale">                        {TIMESTAMP_FORMAT_LABELS.locale}                      </SelectItem>                      <SelectItem hideIndicator value="12-hour">

Lines 1–500 of 1044.