1426 lines · 50.3 KB · TypeScript
RawDownload
import { ArchiveIcon, ArchiveX, LoaderIcon, PlusIcon, RefreshCwIcon } from "lucide-react";import { useQueryClient } from "@tanstack/react-query";import { Link } from "@tanstack/react-router";import { useCallback, useMemo, useRef, useState } from "react";import {  defaultInstanceIdForDriver,  type DesktopUpdateChannel,  PROVIDER_DISPLAY_NAMES,  ProviderDriverKind,  type ProviderInstanceConfig,  type ProviderInstanceId,  type ScopedThreadRef,} from "@t3tools/contracts";import { scopeThreadRef } from "@t3tools/client-runtime";import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";import { createModelSelection } 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 { 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 {  getCustomModelOptionsByInstance,  resolveAppModelSelectionState,} from "../../modelSelection";import {  deriveProviderInstanceEntries,  sortProviderInstanceEntries,} from "../../providerInstances";import { ensureLocalApi, readLocalApi } from "../../localApi";import { useShallow } from "zustand/react/shallow";import {  selectProjectsAcrossEnvironments,  selectThreadShellsAcrossEnvironments,  useStore,} from "../../store";import { formatRelativeTime, formatRelativeTimeLabel } from "../../timestampFormat";import { Button } from "../ui/button";import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "../ui/empty";import { DraftInput } from "../ui/draft-input";import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";import { Switch } from "../ui/switch";import { stackedThreadToast, toastManager } from "../ui/toast";import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";import { AddProviderInstanceDialog } from "./AddProviderInstanceDialog";import {  canOneClickUpdateProviderCandidate,  collectProviderUpdateCandidates,  hasOneClickUpdateProviderCandidate,  isProviderUpdateActive,  type ProviderUpdateCandidate,} from "../ProviderUpdateLaunchNotification.logic";import { ProviderInstanceCard } from "./ProviderInstanceCard";import { DRIVER_OPTIONS, getDriverOption } from "./providerDriverMeta";import {  buildProviderInstanceUpdatePatch,  formatDiagnosticsDescription,} from "./SettingsPanels.logic";import {  SettingResetButton,  SettingsPageContainer,  SettingsRow,  SettingsSection,  useRelativeTimeTick,} from "./settingsLayout";import { ProjectFavicon } from "../ProjectFavicon";import { 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; const DEFAULT_DRIVER_KIND = ProviderDriverKind.make("codex"); function withoutProviderInstanceKey<V>(  record: Readonly<Record<ProviderInstanceId, V>> | undefined,  key: ProviderInstanceId,): Record<ProviderInstanceId, V> {  const next = { ...record } as Record<ProviderInstanceId, V>;  delete next[key];  return next;} function withoutProviderInstanceFavorites(  favorites: ReadonlyArray<{ readonly provider: ProviderInstanceId; readonly model: string }>,  instanceId: ProviderInstanceId,) {  return favorites.filter((favorite) => favorite.provider !== instanceId);} const PROVIDER_SETTINGS = DRIVER_OPTIONS.map((definition) => ({  provider: definition.value,})); 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 [isChangingUpdateChannel, setIsChangingUpdateChannel] = useState(false);   const updateState = updateStateQuery.data ?? null;  const hasDesktopBridge = typeof window !== "undefined" && Boolean(window.desktopBridge);  const selectedUpdateChannel = updateState?.channel ?? "latest";   const handleUpdateChannelChange = useCallback(    (channel: DesktopUpdateChannel) => {      const bridge = window.desktopBridge;      if (        !bridge ||        typeof bridge.setUpdateChannel !== "function" ||        channel === selectedUpdateChannel      ) {        return;      }       setIsChangingUpdateChannel(true);      void bridge        .setUpdateChannel(channel)        .then((state) => {          setDesktopUpdateStateQueryData(queryClient, state);        })        .catch((error: unknown) => {          toastManager.add(            stackedThreadToast({              type: "error",              title: "Could not change update track",              description: error instanceof Error ? error.message : "Update track change failed.",            }),          );        })        .finally(() => {          setIsChangingUpdateChannel(false);        });    },    [queryClient, selectedUpdateChannel],  );   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(            stackedThreadToast({              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(            stackedThreadToast({              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(            stackedThreadToast({              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(          stackedThreadToast({            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>        }      />      <SettingsRow        title="Update track"        description="Stable follows full releases. Nightly follows the nightly desktop channel and can switch back to stable immediately."        control={          <Select            value={selectedUpdateChannel}            onValueChange={(value) => {              handleUpdateChannelChange(value as DesktopUpdateChannel);            }}          >            <SelectTrigger              className="w-full sm:w-40"              aria-label="Update track"              disabled={!hasDesktopBridge || isChangingUpdateChannel}            >              <SelectValue>                {selectedUpdateChannel === "nightly" ? "Nightly" : "Stable"}              </SelectValue>            </SelectTrigger>            <SelectPopup align="end" alignItemWithTrigger={false}>              <SelectItem hideIndicator value="latest">                Stable              </SelectItem>              <SelectItem hideIndicator value="nightly">                Nightly              </SelectItem>            </SelectPopup>          </Select>        }      />    </>  );} export function useSettingsRestore(onRestored?: () => void) {  const { theme, setTheme } = useTheme();  const settings = useSettings();  const { updateSettings } = useUpdateSettings();   const isGitWritingModelDirty = !Equal.equals(    settings.textGenerationModelSelection ?? null,    DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection ?? null,  );   const changedSettingLabels = useMemo(    () => [      ...(theme !== "system" ? ["Theme"] : []),      ...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat        ? ["Time format"]        : []),      ...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount        ? ["Visible threads"]        : []),      ...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap        ? ["Diff line wrapping"]        : []),      ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace        ? ["Diff whitespace changes"]        : []),      ...(settings.autoOpenPlanSidebar !== DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar        ? ["Auto-open task panel"]        : []),      ...(settings.enableAssistantStreaming !== DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming        ? ["Assistant output"]        : []),      ...(settings.defaultThreadEnvMode !== DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode        ? ["New thread mode"]        : []),      ...(settings.addProjectBaseDirectory !== DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory        ? ["Add project base directory"]        : []),      ...(settings.confirmThreadArchive !== DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive        ? ["Archive confirmation"]        : []),      ...(settings.confirmThreadDelete !== DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete        ? ["Delete confirmation"]        : []),      ...(isGitWritingModelDirty ? ["Git writing model"] : []),    ],    [      isGitWritingModelDirty,      settings.autoOpenPlanSidebar,      settings.confirmThreadArchive,      settings.confirmThreadDelete,      settings.addProjectBaseDirectory,      settings.defaultThreadEnvMode,      settings.diffIgnoreWhitespace,      settings.diffWordWrap,      settings.enableAssistantStreaming,      settings.sidebarThreadPreviewCount,      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");    updateSettings({      timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,      diffWordWrap: DEFAULT_UNIFIED_SETTINGS.diffWordWrap,      diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,      sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,      autoOpenPlanSidebar: DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar,      enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming,      defaultThreadEnvMode: DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode,      addProjectBaseDirectory: DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory,      confirmThreadArchive: DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive,      confirmThreadDelete: DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete,      textGenerationModelSelection: DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection,    });    onRestored?.();  }, [changedSettingLabels, onRestored, setTheme, updateSettings]);   return {    changedSettingLabels,    restoreDefaults,  };} export function GeneralSettingsPanel() {  const { theme, setTheme } = useTheme();  const settings = useSettings();  const { updateSettings } = useUpdateSettings();  const observability = useServerObservability();  const serverProviders = useServerProviders();  const diagnosticsDescription = formatDiagnosticsDescription({    localTracingEnabled: observability?.localTracingEnabled ?? false,    otlpTracesEnabled: observability?.otlpTracesEnabled ?? false,    otlpTracesUrl: observability?.otlpTracesUrl,    otlpMetricsEnabled: observability?.otlpMetricsEnabled ?? false,    otlpMetricsUrl: observability?.otlpMetricsUrl,  });   const textGenerationModelSelection = resolveAppModelSelectionState(settings, serverProviders);  const textGenInstanceId = textGenerationModelSelection.instanceId;  const textGenModel = textGenerationModelSelection.model;  const textGenModelOptions = textGenerationModelSelection.options;  const gitModelInstanceEntries = sortProviderInstanceEntries(    deriveProviderInstanceEntries(serverProviders),  );  const textGenInstanceEntry = gitModelInstanceEntries.find(    (entry) => entry.instanceId === textGenInstanceId,  );  const textGenProvider: ProviderDriverKind =    textGenInstanceEntry?.driverKind ?? DEFAULT_DRIVER_KIND;  const gitModelOptionsByInstance = getCustomModelOptionsByInstance(    settings,    serverProviders,    textGenInstanceId,    textGenModel,  );  const isGitWritingModelDirty = !Equal.equals(    settings.textGenerationModelSelection ?? null,    DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection ?? null,  );   return (    <SettingsPageContainer>      <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") {                  setTheme(value);                }              }}

Lines 1–500 of 1426.