1510 lines · 55.7 KB · TypeScript
RawDownload
import {  ArchiveIcon,  ArchiveX,  ChevronDownIcon,  InfoIcon,  LoaderIcon,  PlusIcon,  RefreshCwIcon,  XIcon,} from "lucide-react";import { useQueryClient } from "@tanstack/react-query";import { type ReactNode, useCallback, useMemo, useRef, useState } from "react";import {  PROVIDER_DISPLAY_NAMES,  type ScopedThreadRef,  type ProviderKind,  type ServerProvider,  type ServerProviderModel,} from "@t3tools/contracts";import { scopeThreadRef } from "@t3tools/client-runtime";import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";import { normalizeModelSlug } from "@t3tools/shared/model";import { Equal } from "effect";import { APP_VERSION } from "../../branding";import {  canCheckForUpdate,  getDesktopUpdateButtonTooltip,  getDesktopUpdateInstallConfirmationMessage,  isDesktopUpdateButtonDisabled,  resolveDesktopUpdateButtonAction,} from "../../components/desktopUpdate.logic";import { ProviderModelPicker } from "../chat/ProviderModelPicker";import { TraitsPicker } from "../chat/TraitsPicker";import { resolveAndPersistPreferredEditor } from "../../editorPreferences";import { isElectron } from "../../env";import { useTheme } from "../../hooks/useTheme";import { useSettings, useUpdateSettings } from "../../hooks/useSettings";import { useThreadActions } from "../../hooks/useThreadActions";import {  setDesktopUpdateStateQueryData,  useDesktopUpdateState,} from "../../lib/desktopUpdateReactQuery";import {  MAX_CUSTOM_MODEL_LENGTH,  getCustomModelOptionsByProvider,  resolveAppModelSelectionState,} from "../../modelSelection";import { ensureLocalApi, readLocalApi } from "../../localApi";import { useShallow } from "zustand/react/shallow";import {  selectProjectsAcrossEnvironments,  selectThreadShellsAcrossEnvironments,  useStore,} from "../../store";import { formatRelativeTime, formatRelativeTimeLabel } from "../../timestampFormat";import { cn } from "../../lib/utils";import { Button } from "../ui/button";import { Collapsible, CollapsibleContent } from "../ui/collapsible";import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty";import { Input } from "../ui/input";import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";import { Switch } from "../ui/switch";import { toastManager } from "../ui/toast";import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";import {  SettingResetButton,  SettingsPageContainer,  SettingsRow,  SettingsSection,  useRelativeTimeTick,} from "./settingsLayout";import { ProjectFavicon } from "../ProjectFavicon";import {  useServerAvailableEditors,  useServerKeybindingsConfigPath,  useServerObservability,  useServerProviders,} from "../../rpc/serverState"; const THEME_OPTIONS = [  {    value: "system",    label: "System",  },  {    value: "light",    label: "Light",  },  {    value: "dark",    label: "Dark",  },] 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 PROVIDER_SETTINGS: readonly InstallProviderSettings[] = [  {    provider: "codex",    title: "Codex",    binaryPlaceholder: "Codex binary path",    binaryDescription: "Path to the Codex binary",    homePathKey: "codexHomePath",    homePlaceholder: "CODEX_HOME",    homeDescription: "Optional custom Codex home and config directory.",  },  {    provider: "claudeAgent",    title: "Claude",    binaryPlaceholder: "Claude binary path",    binaryDescription: "Path to the Claude binary",  },] as const; const PROVIDER_STATUS_STYLES = {  disabled: {    dot: "bg-amber-400",  },  error: {    dot: "bg-destructive",  },  ready: {    dot: "bg-success",  },  warning: {    dot: "bg-warning",  },} as const; function getProviderSummary(provider: ServerProvider | undefined) {  if (!provider) {    return {      headline: "Checking provider status",      detail: "Waiting for the server to report installation and authentication details.",    };  }  if (!provider.enabled) {    return {      headline: "Disabled",      detail:        provider.message ?? "This provider is installed but disabled for new sessions in T3 Code.",    };  }  if (!provider.installed) {    return {      headline: "Not found",      detail: provider.message ?? "CLI not detected on PATH.",    };  }  if (provider.auth.status === "authenticated") {    const authLabel = provider.auth.label ?? provider.auth.type;    return {      headline: authLabel ? `Authenticated · ${authLabel}` : "Authenticated",      detail: provider.message ?? null,    };  }  if (provider.auth.status === "unauthenticated") {    return {      headline: "Not authenticated",      detail: provider.message ?? null,    };  }  if (provider.status === "warning") {    return {      headline: "Needs attention",      detail:        provider.message ?? "The provider is installed, but the server could not fully verify it.",    };  }  if (provider.status === "error") {    return {      headline: "Unavailable",      detail: provider.message ?? "The provider failed its startup checks.",    };  }  return {    headline: "Available",    detail: provider.message ?? "Installed and ready, but authentication could not be verified.",  };} function getProviderVersionLabel(version: string | null | undefined) {  if (!version) return null;  return version.startsWith("v") ? version : `v${version}`;} function ProviderLastChecked({ lastCheckedAt }: { lastCheckedAt: string | null }) {  useRelativeTimeTick();  const lastCheckedRelative = lastCheckedAt ? formatRelativeTime(lastCheckedAt) : null;   if (!lastCheckedRelative) {    return null;  }   return (    <span className="text-[11px] text-muted-foreground/60">      {lastCheckedRelative.suffix ? (        <>          Checked <span className="font-mono tabular-nums">{lastCheckedRelative.value}</span>{" "}          {lastCheckedRelative.suffix}        </>      ) : (        <>Checked {lastCheckedRelative.value}</>      )}    </span>  );} function AboutVersionTitle() {  return (    <span className="inline-flex items-center gap-2">      <span>Version</span>      <code className="text-[11px] font-medium text-muted-foreground">{APP_VERSION}</code>    </span>  );} function AboutVersionSection() {  const queryClient = useQueryClient();  const updateStateQuery = useDesktopUpdateState();   const updateState = updateStateQuery.data ?? null;   const handleButtonClick = useCallback(() => {    const bridge = window.desktopBridge;    if (!bridge) return;     const action = updateState ? resolveDesktopUpdateButtonAction(updateState) : "none";     if (action === "download") {      void bridge        .downloadUpdate()        .then((result) => {          setDesktopUpdateStateQueryData(queryClient, result.state);        })        .catch((error: unknown) => {          toastManager.add({            type: "error",            title: "Could not download update",            description: error instanceof Error ? error.message : "Download failed.",          });        });      return;    }     if (action === "install") {      const confirmed = window.confirm(        getDesktopUpdateInstallConfirmationMessage(          updateState ?? { availableVersion: null, downloadedVersion: null },        ),      );      if (!confirmed) return;      void bridge        .installUpdate()        .then((result) => {          setDesktopUpdateStateQueryData(queryClient, result.state);        })        .catch((error: unknown) => {          toastManager.add({            type: "error",            title: "Could not install update",            description: error instanceof Error ? error.message : "Install failed.",          });        });      return;    }     if (typeof bridge.checkForUpdate !== "function") return;    void bridge      .checkForUpdate()      .then((result) => {        setDesktopUpdateStateQueryData(queryClient, result.state);        if (!result.checked) {          toastManager.add({            type: "error",            title: "Could not check for updates",            description:              result.state.message ?? "Automatic updates are not available in this build.",          });        }      })      .catch((error: unknown) => {        toastManager.add({          type: "error",          title: "Could not check for updates",          description: error instanceof Error ? error.message : "Update check failed.",        });      });  }, [queryClient, updateState]);   const action = updateState ? resolveDesktopUpdateButtonAction(updateState) : "none";  const buttonTooltip = updateState ? getDesktopUpdateButtonTooltip(updateState) : null;  const buttonDisabled =    action === "none"      ? !canCheckForUpdate(updateState)      : isDesktopUpdateButtonDisabled(updateState);   const actionLabel: Record<string, string> = { download: "Download", install: "Install" };  const statusLabel: Record<string, string> = {    checking: "Checking…",    downloading: "Downloading…",    "up-to-date": "Up to Date",  };  const buttonLabel =    actionLabel[action] ?? statusLabel[updateState?.status ?? ""] ?? "Check for Updates";  const description =    action === "download" || action === "install"      ? "Update available."      : "Current version of the application.";   return (    <SettingsRow      title={<AboutVersionTitle />}      description={description}      control={        <Tooltip>          <TooltipTrigger            render={              <Button                size="xs"                variant={action === "install" ? "default" : "outline"}                disabled={buttonDisabled}                onClick={handleButtonClick}              >                {buttonLabel}              </Button>            }          />          {buttonTooltip ? <TooltipPopup>{buttonTooltip}</TooltipPopup> : null}        </Tooltip>      }    />  );} export function useSettingsRestore(onRestored?: () => void) {  const { theme, setTheme } = useTheme();  const settings = useSettings();  const { resetSettings } = useUpdateSettings();   const isGitWritingModelDirty = !Equal.equals(    settings.textGenerationModelSelection ?? null,    DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection ?? null,  );  const areProviderSettingsDirty = PROVIDER_SETTINGS.some((providerSettings) => {    const currentSettings = settings.providers[providerSettings.provider];    const defaultSettings = DEFAULT_UNIFIED_SETTINGS.providers[providerSettings.provider];    return !Equal.equals(currentSettings, defaultSettings);  });   const changedSettingLabels = useMemo(    () => [      ...(theme !== "system" ? ["Theme"] : []),      ...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat        ? ["Time format"]        : []),      ...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap        ? ["Diff line wrapping"]        : []),      ...(settings.enableAssistantStreaming !== DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming        ? ["Assistant output"]        : []),      ...(settings.defaultThreadEnvMode !== DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode        ? ["New thread mode"]        : []),      ...(settings.confirmThreadArchive !== DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive        ? ["Archive confirmation"]        : []),      ...(settings.confirmThreadDelete !== DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete        ? ["Delete confirmation"]        : []),      ...(isGitWritingModelDirty ? ["Git writing model"] : []),      ...(areProviderSettingsDirty ? ["Providers"] : []),    ],    [      areProviderSettingsDirty,      isGitWritingModelDirty,      settings.confirmThreadArchive,      settings.confirmThreadDelete,      settings.defaultThreadEnvMode,      settings.diffWordWrap,      settings.enableAssistantStreaming,      settings.timestampFormat,      theme,    ],  );   const restoreDefaults = useCallback(async () => {    if (changedSettingLabels.length === 0) return;    const api = readLocalApi();    const confirmed = await (api ?? ensureLocalApi()).dialogs.confirm(      ["Restore default settings?", `This will reset: ${changedSettingLabels.join(", ")}.`].join(        "\n",      ),    );    if (!confirmed) return;     setTheme("system");    resetSettings();    onRestored?.();  }, [changedSettingLabels, onRestored, resetSettings, setTheme]);   return {    changedSettingLabels,    restoreDefaults,  };} export function GeneralSettingsPanel() {  const { theme, setTheme } = useTheme();  const settings = useSettings();  const { updateSettings } = useUpdateSettings();  const [openingPathByTarget, setOpeningPathByTarget] = useState({    keybindings: false,    logsDirectory: false,  });  const [openPathErrorByTarget, setOpenPathErrorByTarget] = useState<    Partial<Record<"keybindings" | "logsDirectory", string | null>>  >({});  const [openProviderDetails, setOpenProviderDetails] = useState<Record<ProviderKind, boolean>>({    codex: Boolean(      settings.providers.codex.binaryPath !== DEFAULT_UNIFIED_SETTINGS.providers.codex.binaryPath ||      settings.providers.codex.homePath !== DEFAULT_UNIFIED_SETTINGS.providers.codex.homePath ||      settings.providers.codex.customModels.length > 0,    ),    claudeAgent: Boolean(      settings.providers.claudeAgent.binaryPath !==        DEFAULT_UNIFIED_SETTINGS.providers.claudeAgent.binaryPath ||      settings.providers.claudeAgent.customModels.length > 0,    ),  });  const [customModelInputByProvider, setCustomModelInputByProvider] = useState<    Record<ProviderKind, string>  >({    codex: "",    claudeAgent: "",  });  const [customModelErrorByProvider, setCustomModelErrorByProvider] = useState<    Partial<Record<ProviderKind, string | null>>  >({});  const [isRefreshingProviders, setIsRefreshingProviders] = useState(false);  const refreshingRef = useRef(false);  const modelListRefs = useRef<Partial<Record<ProviderKind, HTMLDivElement | null>>>({});  const refreshProviders = useCallback(() => {    if (refreshingRef.current) return;    refreshingRef.current = true;    setIsRefreshingProviders(true);    void ensureLocalApi()      .server.refreshProviders()      .catch((error: unknown) => {        console.warn("Failed to refresh providers", error);      })      .finally(() => {        refreshingRef.current = false;        setIsRefreshingProviders(false);      });  }, []);   const keybindingsConfigPath = useServerKeybindingsConfigPath();  const availableEditors = useServerAvailableEditors();  const observability = useServerObservability();  const serverProviders = useServerProviders();  const codexHomePath = settings.providers.codex.homePath;  const logsDirectoryPath = observability?.logsDirectoryPath ?? null;  const diagnosticsDescription = (() => {    const exports: string[] = [];    if (observability?.otlpTracesEnabled && observability.otlpTracesUrl) {      exports.push(`traces to ${observability.otlpTracesUrl}`);    }    if (observability?.otlpMetricsEnabled && observability.otlpMetricsUrl) {      exports.push(`metrics to ${observability.otlpMetricsUrl}`);    }    const mode = observability?.localTracingEnabled ? "Local trace file" : "Terminal logs only";    return exports.length > 0 ? `${mode}. OTLP exporting ${exports.join(" and ")}.` : `${mode}.`;  })();   const textGenerationModelSelection = resolveAppModelSelectionState(settings, serverProviders);  const textGenProvider = textGenerationModelSelection.provider;  const textGenModel = textGenerationModelSelection.model;  const textGenModelOptions = textGenerationModelSelection.options;  const gitModelOptionsByProvider = getCustomModelOptionsByProvider(    settings,    serverProviders,    textGenProvider,    textGenModel,  );

Lines 1–500 of 1510.